# Copyright 2013-2018 Therp BV . # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). """Define the type of relations that can exist between partners.""" from odoo import _, api, fields, models from odoo.exceptions import ValidationError from odoo.osv.expression import AND, OR HANDLE_INVALID_ONCHANGE = [ ("restrict", _("Do not allow change that will result in invalid relations")), ("ignore", _("Allow existing relations that do not fit changed conditions")), ("end", _("End relations per today, if they do not fit changed conditions")), ("delete", _("Delete relations that do not fit changed conditions")), ] class ResPartnerRelationType(models.Model): """Model that defines relation types that might exist between partners""" _name = "res.partner.relation.type" _description = "Partner Relation Type" _order = "name" name = fields.Char(string="Name", required=True, translate=True) name_inverse = fields.Char(string="Inverse name", required=True, translate=True) contact_type_left = fields.Selection( selection="get_partner_types", string="Left partner type" ) contact_type_right = fields.Selection( selection="get_partner_types", string="Right partner type" ) partner_category_left = fields.Many2one( comodel_name="res.partner.category", string="Left partner category" ) partner_category_right = fields.Many2one( comodel_name="res.partner.category", string="Right partner category" ) allow_self = fields.Boolean( string="Reflexive", help="This relation can be set up with the same partner left and " "right", default=False, ) is_symmetric = fields.Boolean( string="Symmetric", help="This relation is the same from right to left as from left to" " right", default=False, ) handle_invalid_onchange = fields.Selection( selection=HANDLE_INVALID_ONCHANGE, string="Invalid relation handling", required=True, default="restrict", help="When adding relations criteria like partner type and category" " are checked.\n" "However when you change the criteria, there might be relations" " that do not fit the new criteria.\n" "Specify how this situation should be handled.", ) @api.model def get_partner_types(self): """A partner can be an organisation or an individual.""" # pylint: disable=no-self-use return [("c", _("Organisation")), ("p", _("Person"))] @api.model def _end_active_relations(self, relations): """End the relations that are active. If a relation is current, that is, if it has a start date in the past and end date in the future (or no end date), the end date will be set to the current date. If a relation has a end date in the past, then it is inactive and will not be modified. :param relations: a recordset of relations (not necessarily all active) """ today = fields.Date.today() for relation in relations: if relation.date_start and relation.date_start >= today: relation.unlink() elif not relation.date_end or relation.date_end > today: relation.write({"date_end": today}) def check_existing(self, vals): """Check wether records exist that do not fit new criteria.""" relation_model = self.env["res.partner.relation"] def get_type_condition(vals, side): """Add if needed check for contact type.""" fieldname1 = "contact_type_%s" % side fieldname2 = "%s_partner_id.is_company" % side contact_type = fieldname1 in vals and vals[fieldname1] or False if contact_type == "c": # Records that are not companies are invalid: return [(fieldname2, "=", False)] if contact_type == "p": # Records that are companies are invalid: return [(fieldname2, "=", True)] return [] def get_category_condition(vals, side): """Add if needed check for partner category.""" fieldname1 = "partner_category_%s" % side fieldname2 = "%s_partner_id.category_id" % side category_id = fieldname1 in vals and vals[fieldname1] or False if category_id: # Records that do not have the specified category are invalid: return [(fieldname2, "not in", [category_id])] return [] for this in self: handling = ( "handle_invalid_onchange" in vals and vals["handle_invalid_onchange"] or this.handle_invalid_onchange ) if handling == "ignore": continue invalid_conditions = [] for side in ["left", "right"]: invalid_conditions = OR( [invalid_conditions, get_type_condition(vals, side)] ) invalid_conditions = OR( [invalid_conditions, get_category_condition(vals, side)] ) if not invalid_conditions: return # only look at relations for this type invalid_domain = AND([[("type_id", "=", this.id)], invalid_conditions]) invalid_relations = relation_model.with_context(active_test=False).search( invalid_domain ) if invalid_relations: if handling == "restrict": raise ValidationError( _( "There are already relations not satisfying the" " conditions for partner type or category." ) ) elif handling == "delete": invalid_relations.unlink() else: self._end_active_relations(invalid_relations) def _get_reflexive_relations(self): """Get all reflexive relations for this relation type. :return: a recordset of res.partner.relation. """ self.env.cr.execute( """ SELECT id FROM res_partner_relation WHERE left_partner_id = right_partner_id AND type_id = %(relation_type_id)s """, {"relation_type_id": self.id}, ) reflexive_relation_ids = [r[0] for r in self.env.cr.fetchall()] return self.env["res.partner.relation"].browse(reflexive_relation_ids) def _check_no_existing_reflexive_relations(self): """Check that no reflexive relation exists for these relation types.""" for relation_type in self: relations = relation_type._get_reflexive_relations() if relations: raise ValidationError( _( "Reflexivity could not be disabled for the relation " "type {relation_type}. There are existing reflexive " "relations defined for the following partners: " "{partners}" ).format( relation_type=relation_type.display_name, partners=relations.mapped("left_partner_id.display_name"), ) ) def _delete_existing_reflexive_relations(self): """Delete existing reflexive relations for these relation types.""" for relation_type in self: relations = relation_type._get_reflexive_relations() relations.unlink() def _end_active_reflexive_relations(self): """End active reflexive relations for these relation types.""" for relation_type in self: reflexive_relations = relation_type._get_reflexive_relations() self._end_active_relations(reflexive_relations) def _handle_deactivation_of_allow_self(self): """Handle the deactivation of reflexivity on these relations types.""" restrict_relation_types = self.filtered( lambda t: t.handle_invalid_onchange == "restrict" ) restrict_relation_types._check_no_existing_reflexive_relations() delete_relation_types = self.filtered( lambda t: t.handle_invalid_onchange == "delete" ) delete_relation_types._delete_existing_reflexive_relations() end_relation_types = self.filtered(lambda t: t.handle_invalid_onchange == "end") end_relation_types._end_active_reflexive_relations() def _update_right_vals(self, vals): """Make sure that on symmetric relations, right vals follow left vals. @attention: All fields ending in `_right` will have their values replaced by the values of the fields whose names end in `_left`. """ vals["name_inverse"] = vals.get("name", self.name) # For all left keys in model, take value for right either from # left key in vals, or if not present, from right key in self: left_keys = [key for key in self._fields if key.endswith("_left")] for left_key in left_keys: right_key = left_key.replace("_left", "_right") vals[right_key] = vals.get(left_key, self[left_key]) if hasattr(vals[right_key], "id"): vals[right_key] = vals[right_key].id @api.model def create(self, vals): if vals.get("is_symmetric"): self._update_right_vals(vals) return super(ResPartnerRelationType, self).create(vals) def write(self, vals): """Handle existing relations if conditions change.""" self.check_existing(vals) for rec in self: rec_vals = vals.copy() if rec_vals.get("is_symmetric", rec.is_symmetric): self._update_right_vals(rec_vals) super(ResPartnerRelationType, rec).write(rec_vals) allow_self_disabled = "allow_self" in vals and not vals["allow_self"] if allow_self_disabled: self._handle_deactivation_of_allow_self() return True def unlink(self): """Allow delete of relation type, even when connections exist. Relations can be deleted if relation type allows it. """ relation_model = self.env["res.partner.relation"] for rec in self: if rec.handle_invalid_onchange == "delete": # Automatically delete relations, so existing relations # do not prevent unlink of relation type: relations = relation_model.search([("type_id", "=", rec.id)]) relations.unlink() return super(ResPartnerRelationType, self).unlink()