# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from odoo import api, fields, models from odoo.osv import expression class ResPartner(models.Model): _inherit = 'res.partner' contact_type = fields.Selection( [('standalone', 'Standalone Contact'), ('attached', 'Attached to existing Contact'), ], compute='_compute_contact_type', store=True, index=True, default='standalone') contact_id = fields.Many2one( 'res.partner', string='Main Contact', domain=[('is_company', '=', False), ('contact_type', '=', 'standalone'), ], ) other_contact_ids = fields.One2many( 'res.partner', 'contact_id', string='Others Positions', ) @api.multi @api.depends('contact_id') def _compute_contact_type(self): for rec in self: rec.contact_type = 'attached' if rec.contact_id else 'standalone' def _basecontact_check_context(self, mode): """ Remove 'search_show_all_positions' for non-search mode. Keeping it in context can result in unexpected behaviour (ex: reading one2many might return wrong result - i.e with "attached contact" removed even if it's directly linked to a company). Actually, is easier to override a dictionary value to indicate it should be ignored... """ if (mode != 'search' and 'search_show_all_positions' in self.env.context): result = self.with_context( search_show_all_positions={'is_set': False}) else: result = self return result @api.model def search(self, args, offset=0, limit=None, order=None, count=False): """ Display only standalone contact matching ``args`` or having attached contact matching ``args`` """ ctx = self.env.context if (ctx.get('search_show_all_positions', {}).get('is_set') and not ctx['search_show_all_positions']['set_value']): args = expression.normalize_domain(args) attached_contact_args = expression.AND( (args, [('contact_type', '=', 'attached')]) ) attached_contacts = super(ResPartner, self).search( attached_contact_args) args = expression.OR((expression.AND(( [('contact_type', '=', 'standalone')], args)), [('other_contact_ids', 'in', attached_contacts.ids)], )) return super(ResPartner, self).search(args, offset=offset, limit=limit, order=order, count=count) @api.model def create(self, vals): """ When creating, use a modified self to alter the context (see comment in _basecontact_check_context). Also, we need to ensure that the name on an attached contact is the same as the name on the contact it is attached to.""" modified_self = self._basecontact_check_context('create') if not vals.get('name') and vals.get('contact_id'): vals['name'] = modified_self.browse(vals['contact_id']).name return super(ResPartner, modified_self).create(vals) @api.multi def read(self, fields=None, load='_classic_read'): modified_self = self._basecontact_check_context('read') return super(ResPartner, modified_self).read(fields=fields, load=load) @api.multi def write(self, vals): modified_self = self._basecontact_check_context('write') return super(ResPartner, modified_self).write(vals) @api.multi def unlink(self): modified_self = self._basecontact_check_context('unlink') return super(ResPartner, modified_self).unlink() @api.multi def _compute_commercial_partner(self): """ Returns the partner that is considered the commercial entity of this partner. The commercial entity holds the master data for all commercial fields (see :py:meth:`~_commercial_fields`) """ result = super(ResPartner, self)._compute_commercial_partner() for partner in self: if partner.contact_type == 'attached' and not partner.parent_id: partner.commercial_partner_id = partner.contact_id return result def _contact_fields(self): """ Returns the list of contact fields that are synced from the parent when a partner is attached to him. """ return ['name', 'title'] def _contact_sync_from_parent(self): """ Handle sync of contact fields when a new parent contact entity is set, as if they were related fields """ self.ensure_one() if self.contact_id: contact_fields = self._contact_fields() sync_vals = self.contact_id._update_fields_values(contact_fields) self.write(sync_vals) def update_contact(self, vals): if self.env.context.get('__update_contact_lock'): return contact_fields = self._contact_fields() contact_vals = dict( (field, vals[field]) for field in contact_fields if field in vals ) if contact_vals: self.with_context(__update_contact_lock=True).write(contact_vals) @api.multi def _fields_sync(self, update_values): """Sync commercial fields and address fields from company and to children, contact fields from contact and to attached contact after create/update, just as if those were all modeled as fields.related to the parent """ self.ensure_one() super(ResPartner, self)._fields_sync(update_values) contact_fields = self._contact_fields() # 1. From UPSTREAM: sync from parent contact if update_values.get('contact_id'): self._contact_sync_from_parent() # 2. To DOWNSTREAM: sync contact fields to parent or related elif any(field in contact_fields for field in update_values): update_ids = self.other_contact_ids.filtered( lambda p: not p.is_company) if self.contact_id: update_ids |= self.contact_id update_ids.update_contact(update_values) @api.onchange('contact_id') def _onchange_contact_id(self): if self.contact_id: self.name = self.contact_id.name @api.onchange('contact_type') def _onchange_contact_type(self): if self.contact_type == 'standalone': self.contact_id = False