Browse Source

[MIG] partner_multi_relation. Black, isort, pretty...

14.0
Ronald Portier 4 years ago
committed by Raf Ven
parent
commit
2dc738c623
  1. 19
      partner_multi_relation/__manifest__.py
  2. 215
      partner_multi_relation/data/demo.xml
  3. 181
      partner_multi_relation/models/res_partner.py
  4. 94
      partner_multi_relation/models/res_partner_relation.py
  5. 329
      partner_multi_relation/models/res_partner_relation_all.py
  6. 190
      partner_multi_relation/models/res_partner_relation_type.py
  7. 79
      partner_multi_relation/models/res_partner_relation_type_selection.py
  8. 2
      partner_multi_relation/readme/USAGE.rst
  9. 524
      partner_multi_relation/tests/test_partner_relation.py
  10. 327
      partner_multi_relation/tests/test_partner_relation_all.py
  11. 138
      partner_multi_relation/tests/test_partner_relation_common.py
  12. 74
      partner_multi_relation/tests/test_partner_search.py
  13. 55
      partner_multi_relation/views/menu.xml
  14. 78
      partner_multi_relation/views/res_partner.xml
  15. 174
      partner_multi_relation/views/res_partner_relation_all.xml
  16. 95
      partner_multi_relation/views/res_partner_relation_type.xml

19
partner_multi_relation/__manifest__.py

@ -2,25 +2,20 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Partner Relations",
"version": "12.0.1.2.1",
"version": "13.0.1.0.0",
"author": "Therp BV,Camptocamp,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/partner-contact",
"complexity": "normal",
"category": "Customer Relationship Management",
"license": "AGPL-3",
"depends": [
'contacts',
'sales_team',
],
"demo": [
"data/demo.xml",
],
"depends": ["contacts", "sales_team"],
"demo": ["data/demo.xml"],
"data": [
'security/ir.model.access.csv',
"security/ir.model.access.csv",
"views/res_partner_relation_all.xml",
'views/res_partner.xml',
'views/res_partner_relation_type.xml',
'views/menu.xml',
"views/res_partner.xml",
"views/res_partner_relation_type.xml",
"views/menu.xml",
],
"auto_install": False,
"installable": True,

215
partner_multi_relation/data/demo.xml

@ -1,116 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Added partner categories and partners to this file, because it
<!-- Added partner categories and partners to this file, because it
turned out to be a bad idea to rely on demo data in base module,
that can change from release to release. Only dependency on
countries remain. They are less likely to change/disappear.
-->
<!-- Partner relation types -->
<record id="rel_type_assistant" model="res.partner.relation.type">
<field name="name">Is assistant of</field>
<field name="name_inverse">Has assistant</field>
<field name="contact_type_left">p</field>
<field name="contact_type_right">p</field>
</record>
<record id="rel_type_competitor" model="res.partner.relation.type">
<field name="name">Is competitor of</field>
<field name="name_inverse">Is competitor of</field>
<field name="contact_type_left">c</field>
<field name="contact_type_right">c</field>
<field name="is_symmetric" eval="True" />
</record>
<record id="rel_type_has_worked_for" model="res.partner.relation.type">
<field name="name">Has worked for</field>
<field name="name_inverse">Has former employee</field>
<field name="contact_type_left">p</field>
<field name="contact_type_right">c</field>
</record>
<!-- Categories -->
<record id="res_partner_category_pmr_0" model="res.partner.category">
<field name="name">Washing Companies</field>
</record>
<record id="res_partner_category_pmr_4" model="res.partner.category">
<field name="name">Washing Gold</field>
<field name="parent_id" ref="res_partner_category_pmr_0"/>
</record>
<record id="res_partner_category_pmr_5" model="res.partner.category">
<field name="name">Washing Silver</field>
<field name="parent_id" ref="res_partner_category_pmr_0"/>
</record>
<record id="res_partner_category_pmr_11" model="res.partner.category">
<field name="name">Washing Services</field>
<field name="parent_id" ref="res_partner_category_pmr_0"/>
</record>
<!-- Partners -->
<record id="res_partner_pmr_great" model="res.partner">
<field name="name">Great Washing Powder Company</field>
<field
name="category_id"
eval="[(6, 0, [ref('res_partner_category_pmr_11'), ref('res_partner_category_pmr_4')])]"
/>
<field name="supplier">1</field>
<field name="customer">1</field>
<field name="is_company">1</field>
<field name="city">Le Bourget du Lac</field>
<field name="zip">73377</field>
<field name="phone">+33 4 49 23 44 54</field>
<field name="country_id" ref="base.fr"/>
<field name="street">93, Press Avenue</field>
<field name="email">great@yourcompany.example.com</field>
<field name="website">http://www.great.com</field>
</record>
<record id="res_partner_pmr_best" model="res.partner">
<field name="name">Best Washing Powder Company</field>
<field
name="category_id"
eval="[(6, 0, [ref('res_partner_category_pmr_4'), ref('res_partner_category_pmr_11')])]"
/>
<field name="supplier">1</field>
<field name="is_company">1</field>
<field name="city">Champs sur Marne</field>
<field name="zip">77420</field>
<field name="country_id" ref="base.fr"/>
<field name="email">best@yourcompany.example.com</field>
<field name="phone">+33 1 64 61 04 01</field>
<field name="street">12 rue Albert Einstein</field>
<field name="website">http://www.best.com/</field>
</record>
<record id="res_partner_pmr_super" model="res.partner">
<field name="name">Super Washing Powder Company</field>
<field
name="category_id"
eval="[(6,0,[ref('res_partner_category_pmr_5')])]"
/>
<field name="supplier">1</field>
<field eval="1" name="customer"/>
<field name="is_company">1</field>
<field name="street">3rd Floor, Room 3-C,</field>
<field name="street2">Carretera Panamericana, Km 1, Urb. Delgado Chalbaud</field>
<field name="city">Caracas</field>
<field name="zip">1090</field>
<field name="email">super@yourcompany.example.com</field>
<field name="phone">+58 212 681 0538</field>
<field name="country_id" ref="base.ve"/>
<field name="website">super.com</field>
</record>
<!-- Relations -->
<record id="rel_1" model="res.partner.relation">
<field name="left_partner_id" ref="res_partner_pmr_great" />
<field name="right_partner_id" ref="res_partner_pmr_super" />
<field name="type_id" ref="rel_type_competitor" />
</record>
<record id="rel_2" model="res.partner.relation">
<field name="left_partner_id" ref="res_partner_pmr_best" />
<field name="right_partner_id" ref="res_partner_pmr_super" />
<field name="type_id" ref="rel_type_competitor" />
</record>
<record id="rel_3" model="res.partner.relation">
<field name="left_partner_id" ref="res_partner_pmr_great" />
<field name="right_partner_id" ref="res_partner_pmr_best" />
<field name="type_id" ref="rel_type_competitor" />
</record>
<!-- Partner relation types -->
<record id="rel_type_assistant" model="res.partner.relation.type">
<field name="name">Is assistant of</field>
<field name="name_inverse">Has assistant</field>
<field name="contact_type_left">p</field>
<field name="contact_type_right">p</field>
</record>
<record id="rel_type_competitor" model="res.partner.relation.type">
<field name="name">Is competitor of</field>
<field name="name_inverse">Is competitor of</field>
<field name="contact_type_left">c</field>
<field name="contact_type_right">c</field>
<field name="is_symmetric" eval="True" />
</record>
<record id="rel_type_has_worked_for" model="res.partner.relation.type">
<field name="name">Has worked for</field>
<field name="name_inverse">Has former employee</field>
<field name="contact_type_left">p</field>
<field name="contact_type_right">c</field>
</record>
<!-- Categories -->
<record id="res_partner_category_pmr_0" model="res.partner.category">
<field name="name">Washing Companies</field>
</record>
<record id="res_partner_category_pmr_4" model="res.partner.category">
<field name="name">Washing Gold</field>
<field name="parent_id" ref="res_partner_category_pmr_0" />
</record>
<record id="res_partner_category_pmr_5" model="res.partner.category">
<field name="name">Washing Silver</field>
<field name="parent_id" ref="res_partner_category_pmr_0" />
</record>
<record id="res_partner_category_pmr_11" model="res.partner.category">
<field name="name">Washing Services</field>
<field name="parent_id" ref="res_partner_category_pmr_0" />
</record>
<!-- Partners -->
<record id="res_partner_pmr_great" model="res.partner">
<field name="name">Great Washing Powder Company</field>
<field
name="category_id"
eval="[(6, 0, [ref('res_partner_category_pmr_11'), ref('res_partner_category_pmr_4')])]"
/>
<field name="supplier">1</field>
<field name="customer">1</field>
<field name="is_company">1</field>
<field name="city">Le Bourget du Lac</field>
<field name="zip">73377</field>
<field name="phone">+33 4 49 23 44 54</field>
<field name="country_id" ref="base.fr" />
<field name="street">93, Press Avenue</field>
<field name="email">great@yourcompany.example.com</field>
<field name="website">http://www.great.com</field>
</record>
<record id="res_partner_pmr_best" model="res.partner">
<field name="name">Best Washing Powder Company</field>
<field
name="category_id"
eval="[(6, 0, [ref('res_partner_category_pmr_4'), ref('res_partner_category_pmr_11')])]"
/>
<field name="supplier">1</field>
<field name="is_company">1</field>
<field name="city">Champs sur Marne</field>
<field name="zip">77420</field>
<field name="country_id" ref="base.fr" />
<field name="email">best@yourcompany.example.com</field>
<field name="phone">+33 1 64 61 04 01</field>
<field name="street">12 rue Albert Einstein</field>
<field name="website">http://www.best.com/</field>
</record>
<record id="res_partner_pmr_super" model="res.partner">
<field name="name">Super Washing Powder Company</field>
<field name="category_id" eval="[(6,0,[ref('res_partner_category_pmr_5')])]" />
<field name="supplier">1</field>
<field eval="1" name="customer" />
<field name="is_company">1</field>
<field name="street">3rd Floor, Room 3-C,</field>
<field
name="street2"
>Carretera Panamericana, Km 1, Urb. Delgado Chalbaud</field>
<field name="city">Caracas</field>
<field name="zip">1090</field>
<field name="email">super@yourcompany.example.com</field>
<field name="phone">+58 212 681 0538</field>
<field name="country_id" ref="base.ve" />
<field name="website">super.com</field>
</record>
<!-- Relations -->
<record id="rel_1" model="res.partner.relation">
<field name="left_partner_id" ref="res_partner_pmr_great" />
<field name="right_partner_id" ref="res_partner_pmr_super" />
<field name="type_id" ref="rel_type_competitor" />
</record>
<record id="rel_2" model="res.partner.relation">
<field name="left_partner_id" ref="res_partner_pmr_best" />
<field name="right_partner_id" ref="res_partner_pmr_super" />
<field name="type_id" ref="rel_type_competitor" />
</record>
<record id="rel_3" model="res.partner.relation">
<field name="left_partner_id" ref="res_partner_pmr_great" />
<field name="right_partner_id" ref="res_partner_pmr_best" />
<field name="type_id" ref="rel_type_competitor" />
</record>
</odoo>

181
partner_multi_relation/models/res_partner.py

@ -4,51 +4,51 @@
import numbers
from odoo import _, api, exceptions, fields, models
from odoo.osv.expression import is_leaf, OR, FALSE_LEAF
from odoo.osv.expression import FALSE_LEAF, OR, is_leaf
class ResPartner(models.Model):
"""Extend partner with relations and allow to search for relations
in various ways.
"""
# pylint: disable=invalid-name
# pylint: disable=no-member
_inherit = 'res.partner'
_inherit = "res.partner"
relation_count = fields.Integer(
string='Relation Count',
compute="_compute_relation_count"
string="Relation Count", compute="_compute_relation_count"
)
relation_all_ids = fields.One2many(
comodel_name='res.partner.relation.all',
inverse_name='this_partner_id',
string='All relations with current partner',
comodel_name="res.partner.relation.all",
inverse_name="this_partner_id",
string="All relations with current partner",
auto_join=True,
selectable=False,
copy=False,
)
search_relation_type_id = fields.Many2one(
comodel_name='res.partner.relation.type.selection',
comodel_name="res.partner.relation.type.selection",
compute=lambda self: None,
search='_search_relation_type_id',
string='Has relation of type',
search="_search_relation_type_id",
string="Has relation of type",
)
search_relation_partner_id = fields.Many2one(
comodel_name='res.partner',
comodel_name="res.partner",
compute=lambda self: None,
search='_search_related_partner_id',
string='Has relation with',
search="_search_related_partner_id",
string="Has relation with",
)
search_relation_date = fields.Date(
compute=lambda self: None,
search='_search_relation_date',
string='Relation valid',
search="_search_relation_date",
string="Relation valid",
)
search_relation_partner_category_id = fields.Many2one(
comodel_name='res.partner.category',
comodel_name="res.partner.category",
compute=lambda self: None,
search='_search_related_partner_category_id',
string='Has relation with a partner in category',
search="_search_related_partner_category_id",
string="Has relation with a partner in category",
)
@api.depends("relation_all_ids")
@ -58,80 +58,78 @@ class ResPartner(models.Model):
Don't count inactive relations.
"""
for rec in self:
rec.relation_count = len(rec.relation_all_ids.filtered('active'))
rec.relation_count = len(rec.relation_all_ids.filtered("active"))
@api.model
def _search_relation_type_id(self, operator, value):
"""Search partners based on their type of relations."""
result = []
SUPPORTED_OPERATORS = (
'=',
'!=',
'like',
'not like',
'ilike',
'not ilike',
'in',
'not in',
"=",
"!=",
"like",
"not like",
"ilike",
"not ilike",
"in",
"not in",
)
if operator not in SUPPORTED_OPERATORS:
raise exceptions.ValidationError(
_('Unsupported search operator "%s"') % operator)
type_selection_model = self.env['res.partner.relation.type.selection']
_('Unsupported search operator "%s"') % operator
)
type_selection_model = self.env["res.partner.relation.type.selection"]
relation_type_selection = []
if operator == '=' and isinstance(value, numbers.Integral):
if operator == "=" and isinstance(value, numbers.Integral):
relation_type_selection += type_selection_model.browse(value)
elif operator == '!=' and isinstance(value, numbers.Integral):
relation_type_selection = type_selection_model.search([
('id', operator, value),
])
elif operator == "!=" and isinstance(value, numbers.Integral):
relation_type_selection = type_selection_model.search(
[("id", operator, value)]
)
else:
relation_type_selection = type_selection_model.search([
'|',
('type_id.name', operator, value),
('type_id.name_inverse', operator, value),
])
relation_type_selection = type_selection_model.search(
[
"|",
("type_id.name", operator, value),
("type_id.name_inverse", operator, value),
]
)
if not relation_type_selection:
result = [FALSE_LEAF]
for relation_type in relation_type_selection:
result = OR([
result,
result = OR(
[
('relation_all_ids.type_selection_id.id', '=',
relation_type.id),
],
])
result,
[("relation_all_ids.type_selection_id.id", "=", relation_type.id)],
]
)
return result
@api.model
def _search_related_partner_id(self, operator, value):
"""Find partner based on relation with other partner."""
# pylint: disable=no-self-use
return [
('relation_all_ids.other_partner_id', operator, value),
]
return [("relation_all_ids.other_partner_id", operator, value)]
@api.model
def _search_relation_date(self, operator, value):
"""Look only for relations valid at date of search."""
# pylint: disable=no-self-use
return [
'&',
'|',
('relation_all_ids.date_start', '=', False),
('relation_all_ids.date_start', '<=', value),
'|',
('relation_all_ids.date_end', '=', False),
('relation_all_ids.date_end', '>=', value),
"&",
"|",
("relation_all_ids.date_start", "=", False),
("relation_all_ids.date_start", "<=", value),
"|",
("relation_all_ids.date_end", "=", False),
("relation_all_ids.date_end", ">=", value),
]
@api.model
def _search_related_partner_category_id(self, operator, value):
"""Search for partner related to a partner with search category."""
# pylint: disable=no-self-use
return [
('relation_all_ids.other_partner_id.category_id', operator, value),
]
return [("relation_all_ids.other_partner_id.category_id", operator, value)]
@api.model
def search(self, args, offset=0, limit=None, order=None, count=False):
@ -142,26 +140,34 @@ class ResPartner(models.Model):
# pylint: disable=no-value-for-parameter
date_args = []
for arg in args:
if (is_leaf(arg) and isinstance(arg[0], str) and
arg[0].startswith('search_relation')):
if arg[0] == 'search_relation_date':
if (
is_leaf(arg)
and isinstance(arg[0], str)
and arg[0].startswith("search_relation")
):
if arg[0] == "search_relation_date":
date_args = []
break
if not date_args:
date_args = [
('search_relation_date', '=', fields.Date.today()),
]
date_args = [("search_relation_date", "=", fields.Date.today())]
# because of auto_join, we have to do the active test by hand
active_args = []
if self.env.context.get('active_test', True):
if self.env.context.get("active_test", True):
for arg in args:
if (is_leaf(arg) and isinstance(arg[0], str) and
arg[0].startswith('search_relation')):
active_args = [('relation_all_ids.active', '=', True)]
if (
is_leaf(arg)
and isinstance(arg[0], str)
and arg[0].startswith("search_relation")
):
active_args = [("relation_all_ids.active", "=", True)]
break
return super(ResPartner, self).search(
args + date_args + active_args, offset=offset, limit=limit,
order=order, count=count)
args + date_args + active_args,
offset=offset,
limit=limit,
order=order,
count=count,
)
@api.multi
def get_partner_type(self):
@ -170,28 +176,35 @@ class ResPartner(models.Model):
:rtype: str
"""
self.ensure_one()
return 'c' if self.is_company else 'p'
return "c" if self.is_company else "p"
@api.multi
def action_view_relations(self):
for contact in self:
relation_model = self.env['res.partner.relation.all']
relation_ids = relation_model.\
search(['|',
('this_partner_id', '=', contact.id),
('other_partner_id', '=', contact.id)])
relation_model = self.env["res.partner.relation.all"]
relation_ids = relation_model.search(
[
"|",
("this_partner_id", "=", contact.id),
("other_partner_id", "=", contact.id),
]
)
action = self.env.ref(
'partner_multi_relation.action_res_partner_relation_all'
"partner_multi_relation.action_res_partner_relation_all"
).read()[0]
action['domain'] = [('id', 'in', relation_ids.ids)]
context = action.get('context', '{}').strip()[1:-1]
elements = context.split(',') if context else []
to_add = ["""'search_default_this_partner_id': {0},
action["domain"] = [("id", "in", relation_ids.ids)]
context = action.get("context", "{}").strip()[1:-1]
elements = context.split(",") if context else []
to_add = [
"""'search_default_this_partner_id': {0},
'default_this_partner_id': {0},
'active_model': 'res.partner',
'active_id': {0},
'active_ids': [{0}],
'active_test': False""".format(contact.id)]
context = '{' + ', '.join(elements + to_add) + '}'
action['context'] = context
'active_test': False""".format(
contact.id
)
]
context = "{" + ", ".join(elements + to_add) + "}"
action["context"] = context
return action

94
partner_multi_relation/models/res_partner_relation.py

@ -15,54 +15,58 @@ class ResPartnerRelation(models.Model):
two times, once for the normal relation, once for the inverse relation,
will be used to maintain the data.
"""
_name = 'res.partner.relation'
_description = 'Partner relation'
_name = "res.partner.relation"
_description = "Partner relation"
left_partner_id = fields.Many2one(
comodel_name='res.partner',
string='Source Partner',
comodel_name="res.partner",
string="Source Partner",
required=True,
auto_join=True,
ondelete='cascade',
ondelete="cascade",
)
right_partner_id = fields.Many2one(
comodel_name='res.partner',
string='Destination Partner',
comodel_name="res.partner",
string="Destination Partner",
required=True,
auto_join=True,
ondelete='cascade',
ondelete="cascade",
)
type_id = fields.Many2one(
comodel_name='res.partner.relation.type',
string='Type',
comodel_name="res.partner.relation.type",
string="Type",
required=True,
auto_join=True,
)
date_start = fields.Date('Starting date')
date_end = fields.Date('Ending date')
date_start = fields.Date("Starting date")
date_end = fields.Date("Ending date")
@api.model
def create(self, vals):
"""Override create to correct values, before being stored."""
context = self.env.context
if 'left_partner_id' not in vals and context.get('active_id'):
vals['left_partner_id'] = context.get('active_id')
if "left_partner_id" not in vals and context.get("active_id"):
vals["left_partner_id"] = context.get("active_id")
return super(ResPartnerRelation, self).create(vals)
@api.constrains('date_start', 'date_end')
@api.constrains("date_start", "date_end")
def _check_dates(self):
"""End date should not be before start date, if not filled
:raises ValidationError: When constraint is violated
"""
for record in self:
if (record.date_start and record.date_end and
record.date_start > record.date_end):
if (
record.date_start
and record.date_end
and record.date_start > record.date_end
):
raise ValidationError(
_('The starting date cannot be after the ending date.')
_("The starting date cannot be after the ending date.")
)
@api.constrains('left_partner_id', 'type_id')
@api.constrains("left_partner_id", "type_id")
def _check_partner_left(self):
"""Check left partner for required company or person
@ -70,7 +74,7 @@ class ResPartnerRelation(models.Model):
"""
self._check_partner("left")
@api.constrains('right_partner_id', 'type_id')
@api.constrains("right_partner_id", "type_id")
def _check_partner_right(self):
"""Check right partner for required company or person
@ -86,23 +90,24 @@ class ResPartnerRelation(models.Model):
:raises ValidationError: When constraint is violated
"""
for record in self:
assert side in ['left', 'right']
assert side in ["left", "right"]
ptype = getattr(record.type_id, "contact_type_%s" % side)
partner = getattr(record, '%s_partner_id' % side)
if ((ptype == 'c' and not partner.is_company) or
(ptype == 'p' and partner.is_company)):
partner = getattr(record, "%s_partner_id" % side)
if (ptype == "c" and not partner.is_company) or (
ptype == "p" and partner.is_company
):
raise ValidationError(
_('The %s partner is not applicable for this '
'relation type.') % side
_("The %s partner is not applicable for this " "relation type.")
% side
)
category = getattr(record.type_id, "partner_category_%s" % side)
if category and category.id not in partner.category_id.ids:
raise ValidationError(
_('The %s partner does not have category %s.') %
(side, category.name)
_("The %s partner does not have category %s.")
% (side, category.name)
)
@api.constrains('left_partner_id', 'right_partner_id')
@api.constrains("left_partner_id", "right_partner_id")
def _check_not_with_self(self):
"""Not allowed to link partner to same partner
@ -112,15 +117,11 @@ class ResPartnerRelation(models.Model):
if record.left_partner_id == record.right_partner_id:
if not (record.type_id and record.type_id.allow_self):
raise ValidationError(
_('Partners cannot have a relation with themselves.')
_("Partners cannot have a relation with themselves.")
)
@api.constrains(
'left_partner_id',
'type_id',
'right_partner_id',
'date_start',
'date_end',
"left_partner_id", "type_id", "right_partner_id", "date_start", "date_end"
)
def _check_relation_uniqueness(self):
"""Forbid multiple active relations of the same type between the same
@ -132,25 +133,24 @@ class ResPartnerRelation(models.Model):
# pylint: disable=no-value-for-parameter
for record in self:
domain = [
('type_id', '=', record.type_id.id),
('id', '!=', record.id),
('left_partner_id', '=', record.left_partner_id.id),
('right_partner_id', '=', record.right_partner_id.id),
("type_id", "=", record.type_id.id),
("id", "!=", record.id),
("left_partner_id", "=", record.left_partner_id.id),
("right_partner_id", "=", record.right_partner_id.id),
]
if record.date_start:
domain += [
'|',
('date_end', '=', False),
('date_end', '>=', record.date_start),
"|",
("date_end", "=", False),
("date_end", ">=", record.date_start),
]
if record.date_end:
domain += [
'|',
('date_start', '=', False),
('date_start', '<=', record.date_end),
"|",
("date_start", "=", False),
("date_start", "<=", record.date_end),
]
if record.search(domain):
raise ValidationError(
_('There is already a similar relation with '
'overlapping dates')
_("There is already a similar relation with " "overlapping dates")
)

329
partner_multi_relation/models/res_partner_relation_all.py

@ -10,7 +10,6 @@ from odoo import _, api, fields, models
from odoo.exceptions import MissingError, ValidationError
from odoo.tools import drop_view_if_exists
_logger = logging.getLogger(__name__)
@ -47,97 +46,100 @@ FROM res_partner_relation rel"""
class ResPartnerRelationAll(models.AbstractModel):
"""Abstract model to show each relation from two sides."""
_auto = False
_log_access = False
_name = 'res.partner.relation.all'
_description = 'All (non-inverse + inverse) relations between partners'
_order = \
'this_partner_id, type_selection_id, date_end desc, date_start desc'
_name = "res.partner.relation.all"
_description = "All (non-inverse + inverse) relations between partners"
_order = "this_partner_id, type_selection_id, date_end desc, date_start desc"
res_model = fields.Char(
string='Resource Model',
string="Resource Model",
readonly=True,
required=True,
help="The database object this relation is based on.")
help="The database object this relation is based on.",
)
res_id = fields.Integer(
string='Resource ID',
string="Resource ID",
readonly=True,
required=True,
help="The id of the object in the model this relation is based on.")
help="The id of the object in the model this relation is based on.",
)
this_partner_id = fields.Many2one(
comodel_name='res.partner',
string='One Partner',
required=True)
comodel_name="res.partner", string="One Partner", required=True
)
other_partner_id = fields.Many2one(
comodel_name='res.partner',
string='Other Partner',
required=True)
comodel_name="res.partner", string="Other Partner", required=True
)
type_id = fields.Many2one(
comodel_name='res.partner.relation.type',
string='Underlying Relation Type',
comodel_name="res.partner.relation.type",
string="Underlying Relation Type",
readonly=True,
required=True)
date_start = fields.Date('Starting date')
date_end = fields.Date('Ending date')
required=True,
)
date_start = fields.Date("Starting date")
date_end = fields.Date("Ending date")
is_inverse = fields.Boolean(
string="Is reverse type?",
readonly=True,
help="Inverse relations are from right to left partner.")
help="Inverse relations are from right to left partner.",
)
type_selection_id = fields.Many2one(
comodel_name='res.partner.relation.type.selection',
string='Relation Type',
required=True)
comodel_name="res.partner.relation.type.selection",
string="Relation Type",
required=True,
)
active = fields.Boolean(
string='Active',
string="Active",
readonly=True,
help="Records with date_end in the past are inactive")
help="Records with date_end in the past are inactive",
)
any_partner_id = fields.Many2many(
comodel_name='res.partner',
string='Partner',
comodel_name="res.partner",
string="Partner",
compute=lambda self: None,
search='_search_any_partner_id')
search="_search_any_partner_id",
)
def register_specification(
self, register, base_name, is_inverse, select_sql):
_last_key_offset = register['_lastkey']
key_name = base_name + (is_inverse and '_inverse' or '')
def register_specification(self, register, base_name, is_inverse, select_sql):
_last_key_offset = register["_lastkey"]
key_name = base_name + (is_inverse and "_inverse" or "")
assert key_name not in register
assert '%%(padding)s' in select_sql
assert '%(key_offset)s' in select_sql
assert '%(is_inverse)s' in select_sql
assert "%%(padding)s" in select_sql
assert "%(key_offset)s" in select_sql
assert "%(is_inverse)s" in select_sql
_last_key_offset += 1
register['_lastkey'] = _last_key_offset
register["_lastkey"] = _last_key_offset
register[key_name] = dict(
base_name=base_name,
is_inverse=is_inverse,
key_offset=_last_key_offset,
select_sql=select_sql % {
'key_offset': _last_key_offset,
'is_inverse': is_inverse,
'extra_additional_columns':
self._get_additional_relation_columns(),
})
select_sql=select_sql
% {
"key_offset": _last_key_offset,
"is_inverse": is_inverse,
"extra_additional_columns": self._get_additional_relation_columns(),
},
)
def get_register(self):
register = collections.OrderedDict()
register['_lastkey'] = -1
self.register_specification(
register, 'relation', False, RELATIONS_SQL)
self.register_specification(
register, 'relation', True, RELATIONS_SQL_INVERSE)
register["_lastkey"] = -1
self.register_specification(register, "relation", False, RELATIONS_SQL)
self.register_specification(register, "relation", True, RELATIONS_SQL_INVERSE)
return register
def get_select_specification(self, base_name, is_inverse):
register = self.get_register()
key_name = base_name + (is_inverse and '_inverse' or '')
key_name = base_name + (is_inverse and "_inverse" or "")
return register[key_name]
def _get_statement(self):
"""Allow other modules to add to statement."""
register = self.get_register()
union_select = ' UNION '.join(
[register[key]['select_sql']
for key in register if key != '_lastkey'])
union_select = " UNION ".join(
[register[key]["select_sql"] for key in register if key != "_lastkey"]
)
return """\
CREATE OR REPLACE VIEW %%(table)s AS
WITH base_selection AS (%(union_select)s)
@ -153,7 +155,9 @@ CREATE OR REPLACE VIEW %%(table)s AS
FROM base_selection bas
JOIN res_partner_relation_type typ ON (bas.type_id = typ.id)
%%(additional_tables)s
""" % {'union_select': union_select}
""" % {
"union_select": union_select
}
def _get_padding(self):
"""Utility function to define padding in one place."""
@ -167,7 +171,7 @@ CREATE OR REPLACE VIEW %%(table)s AS
:return: ', rel.column_a, rel.column_b_id'
"""
return ''
return ""
def _get_additional_view_fields(self):
"""Allow inherit models to add fields to view.
@ -176,7 +180,7 @@ CREATE OR REPLACE VIEW %%(table)s AS
prepended by a comma, like so:
return ', typ.allow_self, typ.left_partner_category'
"""
return ''
return ""
def _get_additional_tables(self):
"""Allow inherit models to add tables (JOIN's) to view.
@ -184,7 +188,7 @@ CREATE OR REPLACE VIEW %%(table)s AS
Example:
return 'JOIN type_extention ext ON (bas.type_id = ext.id)'
"""
return ''
return ""
@api.model_cr_context
def _auto_init(self):
@ -192,12 +196,13 @@ CREATE OR REPLACE VIEW %%(table)s AS
drop_view_if_exists(cr, self._table)
cr.execute(
self._get_statement(),
{'table': AsIs(self._table),
'padding': self._get_padding(),
'additional_view_fields':
AsIs(self._get_additional_view_fields()),
'additional_tables':
AsIs(self._get_additional_tables())})
{
"table": AsIs(self._table),
"padding": self._get_padding(),
"additional_view_fields": AsIs(self._get_additional_view_fields()),
"additional_tables": AsIs(self._get_additional_tables()),
},
)
return super(ResPartnerRelationAll, self)._auto_init()
@api.model
@ -205,20 +210,24 @@ CREATE OR REPLACE VIEW %%(table)s AS
"""Search relation with partner, no matter on which side."""
# pylint: disable=no-self-use
return [
'|',
('this_partner_id', operator, value),
('other_partner_id', operator, value)]
"|",
("this_partner_id", operator, value),
("other_partner_id", operator, value),
]
@api.multi
def name_get(self):
return {
this.id: '%s %s %s' % (
this.id: "%s %s %s"
% (
this.this_partner_id.name,
this.type_selection_id.display_name,
this.other_partner_id.name,
) for this in self}
)
for this in self
}
@api.onchange('type_selection_id')
@api.onchange("type_selection_id")
def onchange_type_selection_id(self):
"""Add domain on partners according to category and contact_type."""
@ -228,72 +237,74 @@ CREATE OR REPLACE VIEW %%(table)s AS
"""
warning = {}
if partner:
test_domain = [('id', '=', partner.id)] + partner_domain
test_domain = [("id", "=", partner.id)] + partner_domain
else:
test_domain = partner_domain
partner_model = self.env['res.partner']
partner_model = self.env["res.partner"]
partners_found = partner_model.search(test_domain, limit=1)
if not partners_found:
warning['title'] = _('Error!')
warning["title"] = _("Error!")
if partner:
warning['message'] = (
_('%s partner incompatible with relation type.') %
side.title())
warning["message"] = (
_("%s partner incompatible with relation type.") % side.title()
)
else:
warning['message'] = (
_('No %s partner available for relation type.') %
side)
warning["message"] = (
_("No %s partner available for relation type.") % side
)
return warning
this_partner_domain = []
other_partner_domain = []
if self.type_selection_id.contact_type_this:
this_partner_domain.append((
'is_company', '=',
self.type_selection_id.contact_type_this == 'c'))
this_partner_domain.append(
("is_company", "=", self.type_selection_id.contact_type_this == "c")
)
if self.type_selection_id.partner_category_this:
this_partner_domain.append((
'category_id', 'in',
self.type_selection_id.partner_category_this.ids))
this_partner_domain.append(
("category_id", "in", self.type_selection_id.partner_category_this.ids)
)
if self.type_selection_id.contact_type_other:
other_partner_domain.append((
'is_company', '=',
self.type_selection_id.contact_type_other == 'c'))
other_partner_domain.append(
("is_company", "=", self.type_selection_id.contact_type_other == "c")
)
if self.type_selection_id.partner_category_other:
other_partner_domain.append((
'category_id', 'in',
self.type_selection_id.partner_category_other.ids))
result = {'domain': {
'this_partner_id': this_partner_domain,
'other_partner_id': other_partner_domain}}
other_partner_domain.append(
("category_id", "in", self.type_selection_id.partner_category_other.ids)
)
result = {
"domain": {
"this_partner_id": this_partner_domain,
"other_partner_id": other_partner_domain,
}
}
# Check wether domain results in no choice or wrong choice of partners:
warning = {}
partner_model = self.env['res.partner']
partner_model = self.env["res.partner"]
if this_partner_domain:
this_partner = False
if bool(self.this_partner_id.id):
this_partner = self.this_partner_id
else:
this_partner_id = \
'default_this_partner_id' in self.env.context and \
self.env.context['default_this_partner_id'] or \
'active_id' in self.env.context and \
self.env.context['active_id'] or \
False
this_partner_id = (
"default_this_partner_id" in self.env.context
and self.env.context["default_this_partner_id"]
or "active_id" in self.env.context
and self.env.context["active_id"]
or False
)
if this_partner_id:
this_partner = partner_model.browse(this_partner_id)
warning = check_partner_domain(
this_partner, this_partner_domain, _('this'))
warning = check_partner_domain(this_partner, this_partner_domain, _("this"))
if not warning and other_partner_domain:
warning = check_partner_domain(
self.other_partner_id, other_partner_domain, _('other'))
self.other_partner_id, other_partner_domain, _("other")
)
if warning:
result['warning'] = warning
result["warning"] = warning
return result
@api.onchange(
'this_partner_id',
'other_partner_id')
@api.onchange("this_partner_id", "other_partner_id")
def onchange_partner_id(self):
"""Set domain on type_selection_id based on partner(s) selected."""
@ -306,69 +317,68 @@ CREATE OR REPLACE VIEW %%(table)s AS
warning = {}
if not (type_selection_domain and self.type_selection_id):
return warning
test_domain = (
[('id', '=', self.type_selection_id.id)] +
type_selection_domain)
type_model = self.env['res.partner.relation.type.selection']
test_domain = [
("id", "=", self.type_selection_id.id)
] + type_selection_domain
type_model = self.env["res.partner.relation.type.selection"]
types_found = type_model.search(test_domain, limit=1)
if not types_found:
warning['title'] = _('Error!')
warning['message'] = _(
'Relation type incompatible with selected partner(s).')
warning["title"] = _("Error!")
warning["message"] = _(
"Relation type incompatible with selected partner(s)."
)
return warning
type_selection_domain = []
if self.this_partner_id:
type_selection_domain += [
'|',
('contact_type_this', '=', False),
('contact_type_this', '=',
self.this_partner_id.get_partner_type()),
'|',
('partner_category_this', '=', False),
('partner_category_this', 'in',
self.this_partner_id.category_id.ids)]
"|",
("contact_type_this", "=", False),
("contact_type_this", "=", self.this_partner_id.get_partner_type()),
"|",
("partner_category_this", "=", False),
("partner_category_this", "in", self.this_partner_id.category_id.ids),
]
if self.other_partner_id:
type_selection_domain += [
'|',
('contact_type_other', '=', False),
('contact_type_other', '=',
self.other_partner_id.get_partner_type()),
'|',
('partner_category_other', '=', False),
('partner_category_other', 'in',
self.other_partner_id.category_id.ids)]
result = {'domain': {
'type_selection_id': type_selection_domain}}
"|",
("contact_type_other", "=", False),
("contact_type_other", "=", self.other_partner_id.get_partner_type()),
"|",
("partner_category_other", "=", False),
("partner_category_other", "in", self.other_partner_id.category_id.ids),
]
result = {"domain": {"type_selection_id": type_selection_domain}}
# Check wether domain results in no choice or wrong choice for
# type_selection_id:
warning = check_type_selection_domain(type_selection_domain)
if warning:
result['warning'] = warning
result["warning"] = warning
return result
@api.model
def _correct_vals(self, vals, type_selection):
"""Fill left and right partner from this and other partner."""
vals = vals.copy()
if 'type_selection_id' in vals:
vals['type_id'] = type_selection.type_id.id
if "type_selection_id" in vals:
vals["type_id"] = type_selection.type_id.id
if type_selection.is_inverse:
if 'this_partner_id' in vals:
vals['right_partner_id'] = vals['this_partner_id']
if 'other_partner_id' in vals:
vals['left_partner_id'] = vals['other_partner_id']
if "this_partner_id" in vals:
vals["right_partner_id"] = vals["this_partner_id"]
if "other_partner_id" in vals:
vals["left_partner_id"] = vals["other_partner_id"]
else:
if 'this_partner_id' in vals:
vals['left_partner_id'] = vals['this_partner_id']
if 'other_partner_id' in vals:
vals['right_partner_id'] = vals['other_partner_id']
if "this_partner_id" in vals:
vals["left_partner_id"] = vals["this_partner_id"]
if "other_partner_id" in vals:
vals["right_partner_id"] = vals["other_partner_id"]
# Delete values not in underlying table:
for key in (
'this_partner_id',
'type_selection_id',
'other_partner_id',
'is_inverse'):
"this_partner_id",
"type_selection_id",
"other_partner_id",
"is_inverse",
):
if key in vals:
del vals[key]
return vals
@ -386,7 +396,7 @@ CREATE OR REPLACE VIEW %%(table)s AS
self.ensure_one()
# write for models other then res.partner.relation SHOULD
# be handled in inherited models:
relation_model = self.env['res.partner.relation']
relation_model = self.env["res.partner.relation"]
assert self.res_model == relation_model._name
base_resource.write(vals)
@ -394,14 +404,17 @@ CREATE OR REPLACE VIEW %%(table)s AS
def _get_type_selection_from_vals(self, vals):
"""Get type_selection_id straight from vals or compute from type_id.
"""
type_selection_id = vals.get('type_selection_id', False)
type_selection_id = vals.get("type_selection_id", False)
if not type_selection_id:
type_id = vals.get('type_id', False)
type_id = vals.get("type_id", False)
if type_id:
is_inverse = vals.get('is_inverse')
is_inverse = vals.get("is_inverse")
type_selection_id = type_id * 2 + (is_inverse and 1 or 0)
return type_selection_id and self.type_selection_id.browse(
type_selection_id) or False
return (
type_selection_id
and self.type_selection_id.browse(type_selection_id)
or False
)
@api.multi
def write(self, vals):
@ -421,19 +434,20 @@ CREATE OR REPLACE VIEW %%(table)s AS
@api.model
def _compute_base_name(self, type_selection):
"""This will be overridden for each inherit model."""
return 'relation'
return "relation"
@api.model
def _compute_id(self, base_resource, type_selection):
"""Compute id. Allow for enhancements in inherit model."""
base_name = self._compute_base_name(type_selection)
key_offset = self.get_select_specification(
base_name, type_selection.is_inverse)['key_offset']
base_name, type_selection.is_inverse
)["key_offset"]
return base_resource.id * self._get_padding() + key_offset
@api.model
def create_resource(self, vals, type_selection):
relation_model = self.env['res.partner.relation']
relation_model = self.env["res.partner.relation"]
return relation_model.create(vals)
@api.model
@ -444,8 +458,7 @@ CREATE OR REPLACE VIEW %%(table)s AS
"""
type_selection = self._get_type_selection_from_vals(vals)
if not type_selection: # Should not happen
raise ValidationError(
_('No relation type specified in vals: %s.') % vals)
raise ValidationError(_("No relation type specified in vals: %s.") % vals)
vals = self._correct_vals(vals, type_selection)
base_resource = self.create_resource(vals, type_selection)
res_id = self._compute_id(base_resource, type_selection)
@ -457,7 +470,7 @@ CREATE OR REPLACE VIEW %%(table)s AS
self.ensure_one()
# unlink for models other then res.partner.relation SHOULD
# be handled in inherited models:
relation_model = self.env['res.partner.relation']
relation_model = self.env["res.partner.relation"]
assert self.res_model == relation_model._name
base_resource.unlink()

190
partner_multi_relation/models/res_partner_relation_type.py

@ -5,83 +5,62 @@ 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')),
("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,
)
_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',
selection="get_partner_types", string="Left partner type"
)
contact_type_right = fields.Selection(
selection='get_partner_types',
string='Right partner type',
selection="get_partner_types", string="Right partner type"
)
partner_category_left = fields.Many2one(
comodel_name='res.partner.category',
string='Left partner category',
comodel_name="res.partner.category", string="Left partner category"
)
partner_category_right = fields.Many2one(
comodel_name='res.partner.category',
string='Right partner category',
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',
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",
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',
string="Invalid relation handling",
required=True,
default='restrict',
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.",
" 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')),
]
return [("c", _("Organisation")), ("p", _("Person"))]
@api.model
def _end_active_relations(self, relations):
@ -102,70 +81,68 @@ class ResPartnerRelationType(models.Model):
relation.unlink()
elif not relation.date_end or relation.date_end > today:
relation.write({'date_end': today})
relation.write({"date_end": today})
@api.multi
def check_existing(self, vals):
"""Check wether records exist that do not fit new criteria."""
relation_model = self.env['res.partner.relation']
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
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':
if contact_type == "c":
# Records that are not companies are invalid:
return [(fieldname2, '=', False)]
if contact_type == 'p':
return [(fieldname2, "=", False)]
if contact_type == "p":
# Records that are companies are invalid:
return [(fieldname2, '=', True)]
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
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 [(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
"handle_invalid_onchange" in vals
and vals["handle_invalid_onchange"]
or this.handle_invalid_onchange
)
if handling == 'ignore':
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),
])
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)
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':
if handling == "restrict":
raise ValidationError(
_('There are already relations not satisfying the'
' conditions for partner type or category.')
_(
"There are already relations not satisfying the"
" conditions for partner type or category."
)
)
elif handling == 'delete':
elif handling == "delete":
invalid_relations.unlink()
else:
self._end_active_relations(invalid_relations)
@ -180,12 +157,11 @@ class ResPartnerRelationType(models.Model):
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,
}
""",
{"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)
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."""
@ -193,13 +169,16 @@ class ResPartnerRelationType(models.Model):
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(
_(
"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')))
partners=relations.mapped("left_partner_id.display_name"),
)
)
def _delete_existing_reflexive_relations(self):
"""Delete existing reflexive relations for these relation types."""
@ -216,15 +195,16 @@ class ResPartnerRelationType(models.Model):
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')
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')
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 = self.filtered(lambda t: t.handle_invalid_onchange == "end")
end_relation_types._end_active_reflexive_relations()
@api.multi
@ -235,19 +215,19 @@ class ResPartnerRelationType(models.Model):
replaced by the values of the fields whose names end
in `_left`.
"""
vals['name_inverse'] = vals.get('name', self.name)
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')]
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')
right_key = left_key.replace("_left", "_right")
vals[right_key] = vals.get(left_key, self[left_key])
if hasattr(vals[right_key], 'id'):
if hasattr(vals[right_key], "id"):
vals[right_key] = vals[right_key].id
@api.model
def create(self, vals):
if vals.get('is_symmetric'):
if vals.get("is_symmetric"):
self._update_right_vals(vals)
return super(ResPartnerRelationType, self).create(vals)
@ -258,11 +238,11 @@ class ResPartnerRelationType(models.Model):
for rec in self:
rec_vals = vals.copy()
if rec_vals.get('is_symmetric', rec.is_symmetric):
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']
allow_self_disabled = "allow_self" in vals and not vals["allow_self"]
if allow_self_disabled:
self._handle_deactivation_of_allow_self()
@ -274,13 +254,11 @@ class ResPartnerRelationType(models.Model):
Relations can be deleted if relation type allows it.
"""
relation_model = self.env['res.partner.relation']
relation_model = self.env["res.partner.relation"]
for rec in self:
if rec.handle_invalid_onchange == 'delete':
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 = relation_model.search([("type_id", "=", rec.id)])
relations.unlink()
return super(ResPartnerRelationType, self).unlink()

79
partner_multi_relation/models/res_partner_relation_type_selection.py

@ -20,51 +20,41 @@ from odoo.tools import drop_view_if_exists
class ResPartnerRelationTypeSelection(models.Model):
"""Virtual relation types"""
_name = 'res.partner.relation.type.selection'
_description = 'All relation types'
_name = "res.partner.relation.type.selection"
_description = "All relation types"
_auto = False # Do not try to create table in _auto_init(..)
_foreign_keys = []
_log_access = False
_order = 'name asc'
_order = "name asc"
@api.model
def get_partner_types(self):
"""Partner types are defined by model res.partner.relation.type."""
# pylint: disable=no-self-use
rprt_model = self.env['res.partner.relation.type']
rprt_model = self.env["res.partner.relation.type"]
return rprt_model.get_partner_types()
type_id = fields.Many2one(
comodel_name='res.partner.relation.type',
string='Type',
)
name = fields.Char('Name')
type_id = fields.Many2one(comodel_name="res.partner.relation.type", string="Type")
name = fields.Char("Name")
contact_type_this = fields.Selection(
selection='get_partner_types',
string='Current record\'s partner type',
selection="get_partner_types", string="Current record's partner type"
)
is_inverse = fields.Boolean(
string="Is reverse type?",
help="Inverse relations are from right to left partner.",
)
contact_type_other = fields.Selection(
selection='get_partner_types',
string='Other record\'s partner type',
selection="get_partner_types", string="Other record's partner type"
)
partner_category_this = fields.Many2one(
comodel_name='res.partner.category',
string='Current record\'s category',
comodel_name="res.partner.category", string="Current record's category"
)
partner_category_other = fields.Many2one(
comodel_name='res.partner.category',
string='Other record\'s category',
)
allow_self = fields.Boolean(
string='Reflexive',
)
is_symmetric = fields.Boolean(
string='Symmetric',
comodel_name="res.partner.category", string="Other record's category"
)
allow_self = fields.Boolean(string="Reflexive")
is_symmetric = fields.Boolean(string="Symmetric")
def _get_additional_view_fields(self):
"""Allow inherit models to add fields to view.
@ -73,7 +63,7 @@ class ResPartnerRelationTypeSelection(models.Model):
prepended by a comma, like so:
return ', typ.allow_self, typ.left_partner_category'
"""
return ''
return ""
def _get_additional_tables(self):
"""Allow inherit models to add tables (JOIN's) to view.
@ -81,7 +71,7 @@ class ResPartnerRelationTypeSelection(models.Model):
Example:
return 'JOIN type_extention ext ON (bas.type_id = ext.id)'
"""
return ''
return ""
@api.model_cr_context
def _auto_init(self):
@ -122,29 +112,38 @@ CREATE OR REPLACE VIEW %(table)s AS
JOIN res_partner_relation_type typ ON (bas.type_id = typ.id)
%(additional_tables)s
""",
{'table': AsIs(self._table),
'underlying_table': AsIs('res_partner_relation_type'),
'additional_view_fields':
AsIs(self._get_additional_view_fields()),
'additional_tables':
AsIs(self._get_additional_tables())})
{
"table": AsIs(self._table),
"underlying_table": AsIs("res_partner_relation_type"),
"additional_view_fields": AsIs(self._get_additional_view_fields()),
"additional_tables": AsIs(self._get_additional_tables()),
},
)
return super(ResPartnerRelationTypeSelection, self)._auto_init()
@api.multi
def name_get(self):
"""Get name or name_inverse from underlying model."""
return [
(this.id,
this.is_inverse and this.type_id.name_inverse or
this.type_id.display_name)
for this in self]
(
this.id,
this.is_inverse
and this.type_id.name_inverse
or this.type_id.display_name,
)
for this in self
]
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
def name_search(self, name="", args=None, operator="ilike", limit=100):
"""Search for name or inverse name in underlying model."""
# pylint: disable=no-value-for-parameter
return self.search(
['|',
('type_id.name', operator, name),
('type_id.name_inverse', operator, name)] + (args or []),
limit=limit).name_get()
[
"|",
("type_id.name", operator, name),
("type_id.name_inverse", operator, name),
]
+ (args or []),
limit=limit,
).name_get()

2
partner_multi_relation/readme/USAGE.rst

@ -52,7 +52,7 @@ Symmetric
A symetric relation has the same value for the left and right sides.
For example, in a competitor relation, both companies are competitors of each other.
For example, in a competitor relation, both companies are competitors of each other.
.. image:: https://raw.githubusercontent.com/OCA/partner-contact/12.0/partner_multi_relation/static/description/relation_type_symmetric.png

524
partner_multi_relation/tests/test_partner_relation.py

@ -1,6 +1,7 @@
# Copyright 2016-2017 Therp BV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from datetime import datetime, date, timedelta
from datetime import date, datetime, timedelta
from dateutil.relativedelta import relativedelta
from odoo import fields
@ -16,25 +17,33 @@ class TestPartnerRelation(TestPartnerRelationCommon):
def test_selection_name_search(self):
"""Test wether we can find type selection on reverse name."""
selection_types = self.selection_model.name_search(
name=self.selection_person2company.name)
name=self.selection_person2company.name
)
self.assertTrue(selection_types)
self.assertTrue(
(self.selection_person2company.id,
self.selection_person2company.name) in selection_types)
(self.selection_person2company.id, self.selection_person2company.name)
in selection_types
)
def test_self_allowed(self):
"""Test creation of relation to same partner when type allows."""
type_allow = self.type_model.create({
'name': 'allow',
'name_inverse': 'allow_inverse',
'contact_type_left': 'p',
'contact_type_right': 'p',
'allow_self': True})
type_allow = self.type_model.create(
{
"name": "allow",
"name_inverse": "allow_inverse",
"contact_type_left": "p",
"contact_type_right": "p",
"allow_self": True,
}
)
self.assertTrue(type_allow)
reflexive_relation = self.relation_model.create({
'type_id': type_allow.id,
'left_partner_id': self.partner_01_person.id,
'right_partner_id': self.partner_01_person.id})
reflexive_relation = self.relation_model.create(
{
"type_id": type_allow.id,
"left_partner_id": self.partner_01_person.id,
"right_partner_id": self.partner_01_person.id,
}
)
self.assertTrue(reflexive_relation)
def test_self_disallowed(self):
@ -43,18 +52,24 @@ class TestPartnerRelation(TestPartnerRelationCommon):
Attempt to create a relation of a partner to the same partner should
raise an error when the type of relation explicitly disallows this.
"""
type_disallow = self.type_model.create({
'name': 'disallow',
'name_inverse': 'disallow_inverse',
'contact_type_left': 'p',
'contact_type_right': 'p',
'allow_self': False})
type_disallow = self.type_model.create(
{
"name": "disallow",
"name_inverse": "disallow_inverse",
"contact_type_left": "p",
"contact_type_right": "p",
"allow_self": False,
}
)
self.assertTrue(type_disallow)
with self.assertRaises(ValidationError):
self.relation_model.create({
'type_id': type_disallow.id,
'left_partner_id': self.partner_01_person.id,
'right_partner_id': self.partner_01_person.id})
self.relation_model.create(
{
"type_id": type_disallow.id,
"left_partner_id": self.partner_01_person.id,
"right_partner_id": self.partner_01_person.id,
}
)
def test_self_disallowed_after_self_relation_created(self):
"""Test that allow_self can not be true if a reflexive relation already exists.
@ -62,17 +77,23 @@ class TestPartnerRelation(TestPartnerRelationCommon):
If at least one reflexive relation exists for the given type,
reflexivity can not be disallowed.
"""
type_allow = self.type_model.create({
'name': 'allow',
'name_inverse': 'allow_inverse',
'contact_type_left': 'p',
'contact_type_right': 'p',
'allow_self': True})
type_allow = self.type_model.create(
{
"name": "allow",
"name_inverse": "allow_inverse",
"contact_type_left": "p",
"contact_type_right": "p",
"allow_self": True,
}
)
self.assertTrue(type_allow)
reflexive_relation = self.relation_model.create({
'type_id': type_allow.id,
'left_partner_id': self.partner_01_person.id,
'right_partner_id': self.partner_01_person.id})
reflexive_relation = self.relation_model.create(
{
"type_id": type_allow.id,
"left_partner_id": self.partner_01_person.id,
"right_partner_id": self.partner_01_person.id,
}
)
self.assertTrue(reflexive_relation)
with self.assertRaises(ValidationError):
type_allow.allow_self = False
@ -85,24 +106,30 @@ class TestPartnerRelation(TestPartnerRelationCommon):
Non reflexive relations are not modified.
"""
type_allow = self.type_model.create({
'name': 'allow',
'name_inverse': 'allow_inverse',
'contact_type_left': 'p',
'contact_type_right': 'p',
'allow_self': True,
'handle_invalid_onchange': 'delete',
})
reflexive_relation = self.relation_model.create({
'type_id': type_allow.id,
'left_partner_id': self.partner_01_person.id,
'right_partner_id': self.partner_01_person.id,
})
normal_relation = self.relation_model.create({
'type_id': type_allow.id,
'left_partner_id': self.partner_01_person.id,
'right_partner_id': self.partner_04_volunteer.id,
})
type_allow = self.type_model.create(
{
"name": "allow",
"name_inverse": "allow_inverse",
"contact_type_left": "p",
"contact_type_right": "p",
"allow_self": True,
"handle_invalid_onchange": "delete",
}
)
reflexive_relation = self.relation_model.create(
{
"type_id": type_allow.id,
"left_partner_id": self.partner_01_person.id,
"right_partner_id": self.partner_01_person.id,
}
)
normal_relation = self.relation_model.create(
{
"type_id": type_allow.id,
"left_partner_id": self.partner_01_person.id,
"right_partner_id": self.partner_04_volunteer.id,
}
)
type_allow.allow_self = False
self.assertFalse(reflexive_relation.exists())
@ -119,31 +146,39 @@ class TestPartnerRelation(TestPartnerRelationCommon):
Reflexive relations with an end date prior to the current date
are not modified.
"""
type_allow = self.type_model.create({
'name': 'allow',
'name_inverse': 'allow_inverse',
'contact_type_left': 'p',
'contact_type_right': 'p',
'allow_self': True,
'handle_invalid_onchange': 'end',
})
reflexive_relation = self.relation_model.create({
'type_id': type_allow.id,
'left_partner_id': self.partner_01_person.id,
'right_partner_id': self.partner_01_person.id,
'date_start': '2000-01-02',
})
past_reflexive_relation = self.relation_model.create({
'type_id': type_allow.id,
'left_partner_id': self.partner_01_person.id,
'right_partner_id': self.partner_01_person.id,
'date_end': '2000-01-01',
})
normal_relation = self.relation_model.create({
'type_id': type_allow.id,
'left_partner_id': self.partner_01_person.id,
'right_partner_id': self.partner_04_volunteer.id,
})
type_allow = self.type_model.create(
{
"name": "allow",
"name_inverse": "allow_inverse",
"contact_type_left": "p",
"contact_type_right": "p",
"allow_self": True,
"handle_invalid_onchange": "end",
}
)
reflexive_relation = self.relation_model.create(
{
"type_id": type_allow.id,
"left_partner_id": self.partner_01_person.id,
"right_partner_id": self.partner_01_person.id,
"date_start": "2000-01-02",
}
)
past_reflexive_relation = self.relation_model.create(
{
"type_id": type_allow.id,
"left_partner_id": self.partner_01_person.id,
"right_partner_id": self.partner_01_person.id,
"date_end": "2000-01-01",
}
)
normal_relation = self.relation_model.create(
{
"type_id": type_allow.id,
"left_partner_id": self.partner_01_person.id,
"right_partner_id": self.partner_04_volunteer.id,
}
)
type_allow.allow_self = False
self.assertEqual(reflexive_relation.date_end, fields.Date.today())
@ -156,20 +191,24 @@ class TestPartnerRelation(TestPartnerRelationCommon):
If handle_invalid_onchange is set to end, then deactivating
reflexivity will delete invalid relations in the future.
"""
type_allow = self.type_model.create({
'name': 'allow',
'name_inverse': 'allow_inverse',
'contact_type_left': 'p',
'contact_type_right': 'p',
'allow_self': True,
'handle_invalid_onchange': 'end',
})
future_reflexive_relation = self.relation_model.create({
'type_id': type_allow.id,
'left_partner_id': self.partner_01_person.id,
'right_partner_id': self.partner_01_person.id,
'date_start': datetime.now() + timedelta(1),
})
type_allow = self.type_model.create(
{
"name": "allow",
"name_inverse": "allow_inverse",
"contact_type_left": "p",
"contact_type_right": "p",
"allow_self": True,
"handle_invalid_onchange": "end",
}
)
future_reflexive_relation = self.relation_model.create(
{
"type_id": type_allow.id,
"left_partner_id": self.partner_01_person.id,
"right_partner_id": self.partner_01_person.id,
"date_start": datetime.now() + timedelta(1),
}
)
type_allow.allow_self = False
self.assertFalse(future_reflexive_relation.exists())
@ -180,17 +219,23 @@ class TestPartnerRelation(TestPartnerRelationCommon):
raise an error when the type of relation does not explicitly allow
this.
"""
type_default = self.type_model.create({
'name': 'default',
'name_inverse': 'default_inverse',
'contact_type_left': 'p',
'contact_type_right': 'p'})
type_default = self.type_model.create(
{
"name": "default",
"name_inverse": "default_inverse",
"contact_type_left": "p",
"contact_type_right": "p",
}
)
self.assertTrue(type_default)
with self.assertRaises(ValidationError):
self.relation_model.create({
'type_id': type_default.id,
'left_partner_id': self.partner_01_person.id,
'right_partner_id': self.partner_01_person.id})
self.relation_model.create(
{
"type_id": type_default.id,
"left_partner_id": self.partner_01_person.id,
"right_partner_id": self.partner_01_person.id,
}
)
def test_self_mixed(self):
"""Test creation of relation with wrong types.
@ -199,54 +244,65 @@ class TestPartnerRelation(TestPartnerRelationCommon):
type should raise an error.
"""
with self.assertRaises(ValidationError):
self.relation_model.create({
'type_id': self.type_company2person.id,
'left_partner_id': self.partner_01_person.id,
'right_partner_id': self.partner_02_company.id})
self.relation_model.create(
{
"type_id": self.type_company2person.id,
"left_partner_id": self.partner_01_person.id,
"right_partner_id": self.partner_02_company.id,
}
)
def test_symmetric(self):
"""Test creating symmetric relation."""
# Start out with non symmetric relation:
type_symmetric = self.type_model.create({
'name': 'not yet symmetric',
'name_inverse': 'the other side of not symmetric',
'is_symmetric': False,
'contact_type_left': False,
'contact_type_right': 'p'})
type_symmetric = self.type_model.create(
{
"name": "not yet symmetric",
"name_inverse": "the other side of not symmetric",
"is_symmetric": False,
"contact_type_left": False,
"contact_type_right": "p",
}
)
# not yet symmetric relation should result in two records in
# selection:
selection_symmetric = self.selection_model.search([
('type_id', '=', type_symmetric.id)])
selection_symmetric = self.selection_model.search(
[("type_id", "=", type_symmetric.id)]
)
self.assertEqual(len(selection_symmetric), 2)
# Now change to symmetric and test name and inverse name:
with self.env.do_in_draft():
type_symmetric.write({
'name': 'sym',
'is_symmetric': True})
type_symmetric.write({"name": "sym", "is_symmetric": True})
self.assertEqual(type_symmetric.is_symmetric, True)
self.assertEqual(type_symmetric.name_inverse, type_symmetric.name)
self.assertEqual(
type_symmetric.name_inverse,
type_symmetric.name)
self.assertEqual(
type_symmetric.contact_type_right,
type_symmetric.contact_type_left)
type_symmetric.contact_type_right, type_symmetric.contact_type_left
)
# now update the database:
type_symmetric.write({
'name': type_symmetric.name,
'is_symmetric': type_symmetric.is_symmetric,
'name_inverse': type_symmetric.name_inverse,
'contact_type_right': type_symmetric.contact_type_right})
type_symmetric.write(
{
"name": type_symmetric.name,
"is_symmetric": type_symmetric.is_symmetric,
"name_inverse": type_symmetric.name_inverse,
"contact_type_right": type_symmetric.contact_type_right,
}
)
# symmetric relation should result in only one record in
# selection:
selection_symmetric = self.selection_model.search([
('type_id', '=', type_symmetric.id)])
selection_symmetric = self.selection_model.search(
[("type_id", "=", type_symmetric.id)]
)
self.assertEqual(len(selection_symmetric), 1)
relation = self.relation_all_model.create({
'type_selection_id': selection_symmetric.id,
'this_partner_id': self.partner_02_company.id,
'other_partner_id': self.partner_01_person.id})
partners = self.partner_model.search([
('search_relation_type_id', '=', relation.type_selection_id.id)])
relation = self.relation_all_model.create(
{
"type_selection_id": selection_symmetric.id,
"this_partner_id": self.partner_02_company.id,
"other_partner_id": self.partner_01_person.id,
}
)
partners = self.partner_model.search(
[("search_relation_type_id", "=", relation.type_selection_id.id)]
)
self.assertTrue(self.partner_01_person in partners)
self.assertTrue(self.partner_02_company in partners)
@ -254,124 +310,148 @@ class TestPartnerRelation(TestPartnerRelationCommon):
"""Test check on category in relations."""
# Check on left side:
with self.assertRaises(ValidationError):
self.relation_model.create({
'type_id': self.type_ngo2volunteer.id,
'left_partner_id': self.partner_02_company.id,
'right_partner_id': self.partner_04_volunteer.id})
self.relation_model.create(
{
"type_id": self.type_ngo2volunteer.id,
"left_partner_id": self.partner_02_company.id,
"right_partner_id": self.partner_04_volunteer.id,
}
)
# Check on right side:
with self.assertRaises(ValidationError):
self.relation_model.create({
'type_id': self.type_ngo2volunteer.id,
'left_partner_id': self.partner_03_ngo.id,
'right_partner_id': self.partner_01_person.id})
self.relation_model.create(
{
"type_id": self.type_ngo2volunteer.id,
"left_partner_id": self.partner_03_ngo.id,
"right_partner_id": self.partner_01_person.id,
}
)
def test_relation_type_change(self):
"""Test change in relation type conditions."""
# First create a relation type having no particular conditions.
(type_school2student,
school2student,
school2student_inverse) = \
self._create_relation_type_selection({
'name': 'school has student',
'name_inverse': 'studies at school'})
(
type_school2student,
school2student,
school2student_inverse,
) = self._create_relation_type_selection(
{"name": "school has student", "name_inverse": "studies at school"}
)
# Second create relations based on those conditions.
partner_school = self.partner_model.create({
'name': 'Test School',
'is_company': True,
'ref': 'TS'})
partner_bart = self.partner_model.create({
'name': 'Bart Simpson',
'is_company': False,
'ref': 'BS'})
partner_lisa = self.partner_model.create({
'name': 'Lisa Simpson',
'is_company': False,
'ref': 'LS'})
relation_school2bart = self.relation_all_model.create({
'this_partner_id': partner_school.id,
'type_selection_id': school2student.id,
'other_partner_id': partner_bart.id})
partner_school = self.partner_model.create(
{"name": "Test School", "is_company": True, "ref": "TS"}
)
partner_bart = self.partner_model.create(
{"name": "Bart Simpson", "is_company": False, "ref": "BS"}
)
partner_lisa = self.partner_model.create(
{"name": "Lisa Simpson", "is_company": False, "ref": "LS"}
)
relation_school2bart = self.relation_all_model.create(
{
"this_partner_id": partner_school.id,
"type_selection_id": school2student.id,
"other_partner_id": partner_bart.id,
}
)
self.assertTrue(relation_school2bart)
relation_school2lisa = self.relation_all_model.create({
'this_partner_id': partner_school.id,
'type_selection_id': school2student.id,
'other_partner_id': partner_lisa.id})
relation_school2lisa = self.relation_all_model.create(
{
"this_partner_id": partner_school.id,
"type_selection_id": school2student.id,
"other_partner_id": partner_lisa.id,
}
)
self.assertTrue(relation_school2lisa)
relation_bart2lisa = self.relation_all_model.create({
'this_partner_id': partner_bart.id,
'type_selection_id': school2student.id,
'other_partner_id': partner_lisa.id})
relation_bart2lisa = self.relation_all_model.create(
{
"this_partner_id": partner_bart.id,
"type_selection_id": school2student.id,
"other_partner_id": partner_lisa.id,
}
)
self.assertTrue(relation_bart2lisa)
# Third creata a category and make it a condition for the
# relation type.
# - Test restriction
# - Test ignore
category_student = self.category_model.create({'name': 'Student'})
category_student = self.category_model.create({"name": "Student"})
with self.assertRaises(ValidationError):
type_school2student.write({
'partner_category_right': category_student.id})
type_school2student.write({"partner_category_right": category_student.id})
self.assertFalse(type_school2student.partner_category_right.id)
type_school2student.write({
'handle_invalid_onchange': 'ignore',
'partner_category_right': category_student.id})
type_school2student.write(
{
"handle_invalid_onchange": "ignore",
"partner_category_right": category_student.id,
}
)
self.assertEqual(
type_school2student.partner_category_right.id,
category_student.id)
type_school2student.partner_category_right.id, category_student.id
)
# Fourth make company type a condition for left partner
# - Test ending
# - Test deletion
partner_bart.write({
'category_id': [(4, category_student.id)]})
partner_lisa.write({
'category_id': [(4, category_student.id)]})
partner_bart.write({"category_id": [(4, category_student.id)]})
partner_lisa.write({"category_id": [(4, category_student.id)]})
# Future student to be deleted by end action:
partner_homer = self.partner_model.create({
'name': 'Homer Simpson',
'is_company': False,
'ref': 'HS',
'category_id': [(4, category_student.id)]})
relation_lisa2homer = self.relation_all_model.create({
'this_partner_id': partner_lisa.id,
'type_selection_id': school2student.id,
'other_partner_id': partner_homer.id,
'date_start': date.today() + relativedelta(months=+6)})
partner_homer = self.partner_model.create(
{
"name": "Homer Simpson",
"is_company": False,
"ref": "HS",
"category_id": [(4, category_student.id)],
}
)
relation_lisa2homer = self.relation_all_model.create(
{
"this_partner_id": partner_lisa.id,
"type_selection_id": school2student.id,
"other_partner_id": partner_homer.id,
"date_start": date.today() + relativedelta(months=+6),
}
)
self.assertTrue(relation_lisa2homer)
type_school2student.write({
'handle_invalid_onchange': 'end',
'contact_type_left': 'c'})
self.assertEqual(
relation_bart2lisa.date_end,
fields.Date.today())
type_school2student.write(
{"handle_invalid_onchange": "end", "contact_type_left": "c"}
)
self.assertEqual(relation_bart2lisa.date_end, fields.Date.today())
self.assertFalse(relation_lisa2homer.exists())
type_school2student.write({
'handle_invalid_onchange': 'delete',
'contact_type_left': 'c',
'contact_type_right': 'p'})
type_school2student.write(
{
"handle_invalid_onchange": "delete",
"contact_type_left": "c",
"contact_type_right": "p",
}
)
self.assertFalse(relation_bart2lisa.exists())
def test_relation_type_unlink(self):
"""Test delete of relation type, including deleting relations."""
# First create a relation type having restrict particular conditions.
type_model = self.env['res.partner.relation.type']
relation_model = self.env['res.partner.relation']
partner_model = self.env['res.partner']
type_school2student = type_model.create({
'name': 'school has student',
'name_inverse': 'studies at school',
'handle_invalid_onchange': 'delete'})
type_model = self.env["res.partner.relation.type"]
relation_model = self.env["res.partner.relation"]
partner_model = self.env["res.partner"]
type_school2student = type_model.create(
{
"name": "school has student",
"name_inverse": "studies at school",
"handle_invalid_onchange": "delete",
}
)
# Second create relation based on those conditions.
partner_school = partner_model.create({
'name': 'Test School',
'is_company': True,
'ref': 'TS'})
partner_bart = partner_model.create({
'name': 'Bart Simpson',
'is_company': False,
'ref': 'BS'})
relation_school2bart = relation_model.create({
'left_partner_id': partner_school.id,
'type_id': type_school2student.id,
'right_partner_id': partner_bart.id})
partner_school = partner_model.create(
{"name": "Test School", "is_company": True, "ref": "TS"}
)
partner_bart = partner_model.create(
{"name": "Bart Simpson", "is_company": False, "ref": "BS"}
)
relation_school2bart = relation_model.create(
{
"left_partner_id": partner_school.id,
"type_id": type_school2student.id,
"right_partner_id": partner_bart.id,
}
)
# Delete type. Relations with type should also cease to exist:
type_school2student.unlink()
self.assertFalse(relation_school2bart.exists())

327
partner_multi_relation/tests/test_partner_relation_all.py

@ -2,74 +2,87 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from datetime import date
from odoo.exceptions import ValidationError
from .test_partner_relation_common import TestPartnerRelationCommon
class TestPartnerRelation(TestPartnerRelationCommon):
def setUp(self):
super(TestPartnerRelation, self).setUp()
# Create a new relation type which will not have valid relations:
category_nobody = self.category_model.create({
'name': 'Nobody'})
(self.type_nobody,
self.selection_nobody,
self.selection_nobody_inverse) = (
self._create_relation_type_selection({
'name': 'has relation with nobody',
'name_inverse': 'nobody has relation with',
'contact_type_left': 'c',
'contact_type_right': 'p',
'partner_category_left': category_nobody.id,
'partner_category_right': category_nobody.id}))
category_nobody = self.category_model.create({"name": "Nobody"})
(
self.type_nobody,
self.selection_nobody,
self.selection_nobody_inverse,
) = self._create_relation_type_selection(
{
"name": "has relation with nobody",
"name_inverse": "nobody has relation with",
"contact_type_left": "c",
"contact_type_right": "p",
"partner_category_left": category_nobody.id,
"partner_category_right": category_nobody.id,
}
)
def _get_empty_relation(self):
"""Get empty relation record for onchange tests."""
# Need English, because we will compare text
return self.relation_all_model.with_context(lang='en_US').new({})
return self.relation_all_model.with_context(lang="en_US").new({})
def test_get_partner_types(self):
"""Partner types should contain at least 'c' and 'p'."""
partner_types = self.selection_model.get_partner_types()
type_codes = [ptype[0] for ptype in partner_types]
self.assertTrue('c' in type_codes)
self.assertTrue('p' in type_codes)
self.assertTrue("c" in type_codes)
self.assertTrue("p" in type_codes)
def test_create_with_active_id(self):
"""Test creation with this_partner_id from active_id."""
# Check wether we can create connection from company to person,
# taking the particular company from the active records:
relation = self.relation_all_model.with_context(
active_id=self.partner_02_company.id,
active_ids=self.partner_02_company.ids).create({
'other_partner_id': self.partner_01_person.id,
'type_selection_id': self.selection_company2person.id})
active_id=self.partner_02_company.id, active_ids=self.partner_02_company.ids
).create(
{
"other_partner_id": self.partner_01_person.id,
"type_selection_id": self.selection_company2person.id,
}
)
self.assertTrue(relation)
self.assertEqual(relation.this_partner_id, self.partner_02_company)
# Partner should have one relation now:
self.assertEqual(self.partner_01_person.relation_count, 1)
# Test create without type_selection_id:
with self.assertRaises(ValidationError):
self.relation_all_model.create({
'this_partner_id': self.partner_02_company.id,
'other_partner_id': self.partner_01_person.id})
self.relation_all_model.create(
{
"this_partner_id": self.partner_02_company.id,
"other_partner_id": self.partner_01_person.id,
}
)
def test_display_name(self):
"""Test display name"""
relation = self._create_company2person_relation()
self.assertEqual(
relation.display_name, '%s %s %s' % (
relation.display_name,
"%s %s %s"
% (
relation.this_partner_id.name,
relation.type_selection_id.name,
relation.other_partner_id.name))
relation.other_partner_id.name,
),
)
def test__regular_write(self):
"""Test write with valid data."""
relation = self._create_company2person_relation()
relation.write({'date_start': '2014-09-01'})
relation.write({"date_start": "2014-09-01"})
relation.invalidate_cache(ids=relation.ids)
self.assertEqual(relation.date_start, date(2014, 9, 1))
@ -77,90 +90,120 @@ class TestPartnerRelation(TestPartnerRelationCommon):
"""Test write with date_end before date_start."""
relation = self._create_company2person_relation()
with self.assertRaises(ValidationError):
relation.write({
'date_start': '2016-09-01',
'date_end': '2016-08-01'})
relation.write({"date_start": "2016-09-01", "date_end": "2016-08-01"})
def test_validate_overlapping_01(self):
"""Test create overlapping with no start / end dates."""
relation = self._create_company2person_relation()
with self.assertRaises(ValidationError):
# New relation with no start / end should give error
self.relation_all_model.create({
'this_partner_id': relation.this_partner_id.id,
'type_selection_id': relation.type_selection_id.id,
'other_partner_id': relation.other_partner_id.id})
self.relation_all_model.create(
{
"this_partner_id": relation.this_partner_id.id,
"type_selection_id": relation.type_selection_id.id,
"other_partner_id": relation.other_partner_id.id,
}
)
def test_validate_overlapping_02(self):
"""Test create overlapping with start / end dates."""
relation = self.relation_all_model.create({
'this_partner_id': self.partner_02_company.id,
'type_selection_id': self.selection_company2person.id,
'other_partner_id': self.partner_01_person.id,
'date_start': '2015-09-01',
'date_end': '2016-08-31'})
relation = self.relation_all_model.create(
{
"this_partner_id": self.partner_02_company.id,
"type_selection_id": self.selection_company2person.id,
"other_partner_id": self.partner_01_person.id,
"date_start": "2015-09-01",
"date_end": "2016-08-31",
}
)
# New relation with overlapping start / end should give error
with self.assertRaises(ValidationError):
self.relation_all_model.create({
'this_partner_id': relation.this_partner_id.id,
'type_selection_id': relation.type_selection_id.id,
'other_partner_id': relation.other_partner_id.id,
'date_start': '2016-08-01',
'date_end': '2017-07-30'})
self.relation_all_model.create(
{
"this_partner_id": relation.this_partner_id.id,
"type_selection_id": relation.type_selection_id.id,
"other_partner_id": relation.other_partner_id.id,
"date_start": "2016-08-01",
"date_end": "2017-07-30",
}
)
def test_validate_overlapping_03(self):
"""Test create not overlapping."""
relation = self.relation_all_model.create({
'this_partner_id': self.partner_02_company.id,
'type_selection_id': self.selection_company2person.id,
'other_partner_id': self.partner_01_person.id,
'date_start': '2015-09-01',
'date_end': '2016-08-31'})
relation_another_record = self.relation_all_model.create({
'this_partner_id': relation.this_partner_id.id,
'type_selection_id': relation.type_selection_id.id,
'other_partner_id': relation.other_partner_id.id,
'date_start': '2016-09-01',
'date_end': '2017-08-31'})
relation = self.relation_all_model.create(
{
"this_partner_id": self.partner_02_company.id,
"type_selection_id": self.selection_company2person.id,
"other_partner_id": self.partner_01_person.id,
"date_start": "2015-09-01",
"date_end": "2016-08-31",
}
)
relation_another_record = self.relation_all_model.create(
{
"this_partner_id": relation.this_partner_id.id,
"type_selection_id": relation.type_selection_id.id,
"other_partner_id": relation.other_partner_id.id,
"date_start": "2016-09-01",
"date_end": "2017-08-31",
}
)
self.assertTrue(relation_another_record)
def test_inverse_record(self):
"""Test creation of inverse record."""
relation = self._create_company2person_relation()
inverse_relation = self.relation_all_model.search([
('this_partner_id', '=', relation.other_partner_id.id),
('other_partner_id', '=', relation.this_partner_id.id)])
inverse_relation = self.relation_all_model.search(
[
("this_partner_id", "=", relation.other_partner_id.id),
("other_partner_id", "=", relation.this_partner_id.id),
]
)
self.assertEqual(len(inverse_relation), 1)
self.assertEqual(
inverse_relation.type_selection_id.name,
self.selection_person2company.name)
inverse_relation.type_selection_id.name, self.selection_person2company.name
)
def test_inverse_creation(self):
"""Test creation of record through inverse selection."""
relation = self.relation_all_model.create({
'this_partner_id': self.partner_01_person.id,
'type_selection_id': self.selection_person2company.id,
'other_partner_id': self.partner_02_company.id})
relation = self.relation_all_model.create(
{
"this_partner_id": self.partner_01_person.id,
"type_selection_id": self.selection_person2company.id,
"other_partner_id": self.partner_02_company.id,
}
)
# Check wether display name is what we should expect:
self.assertEqual(
relation.display_name, '%s %s %s' % (
relation.display_name,
"%s %s %s"
% (
self.partner_01_person.name,
self.selection_person2company.name,
self.partner_02_company.name))
self.partner_02_company.name,
),
)
def test_inverse_creation_type_id(self):
"""Test creation of record through inverse selection with type_id."""
relation = self.relation_all_model.create({
'this_partner_id': self.partner_01_person.id,
'type_id': self.selection_person2company.type_id.id,
'is_inverse': True,
'other_partner_id': self.partner_02_company.id})
relation = self.relation_all_model.create(
{
"this_partner_id": self.partner_01_person.id,
"type_id": self.selection_person2company.type_id.id,
"is_inverse": True,
"other_partner_id": self.partner_02_company.id,
}
)
# Check wether display name is what we should expect:
self.assertEqual(
relation.display_name, '%s %s %s' % (
relation.display_name,
"%s %s %s"
% (
self.partner_01_person.name,
self.selection_person2company.name,
self.partner_02_company.name))
self.partner_02_company.name,
),
)
def test_unlink(self):
"""Unlinking derived relation should unlink base relation."""
@ -178,104 +221,114 @@ class TestPartnerRelation(TestPartnerRelationCommon):
# 1. Test call with empty relation
relation_empty = self._get_empty_relation()
result = relation_empty.onchange_type_selection_id()
self.assertTrue('domain' in result)
self.assertFalse('warning' in result)
self.assertTrue('this_partner_id' in result['domain'])
self.assertFalse(result['domain']['this_partner_id'])
self.assertTrue('other_partner_id' in result['domain'])
self.assertFalse(result['domain']['other_partner_id'])
self.assertTrue("domain" in result)
self.assertFalse("warning" in result)
self.assertTrue("this_partner_id" in result["domain"])
self.assertFalse(result["domain"]["this_partner_id"])
self.assertTrue("other_partner_id" in result["domain"])
self.assertFalse(result["domain"]["other_partner_id"])
# 2. Test call with company 2 person relation
relation = self._create_company2person_relation()
domain = relation.onchange_type_selection_id()['domain']
self.assertTrue(
('is_company', '=', False) in domain['other_partner_id'])
domain = relation.onchange_type_selection_id()["domain"]
self.assertTrue(("is_company", "=", False) in domain["other_partner_id"])
# 3. Test with relation needing categories,
# take active partner from active_id:
relation_ngo_volunteer = self.relation_all_model.with_context(
active_id=self.partner_03_ngo.id).create({
'type_selection_id': self.selection_ngo2volunteer.id,
'other_partner_id': self.partner_04_volunteer.id})
domain = relation_ngo_volunteer.onchange_type_selection_id()['domain']
active_id=self.partner_03_ngo.id
).create(
{
"type_selection_id": self.selection_ngo2volunteer.id,
"other_partner_id": self.partner_04_volunteer.id,
}
)
domain = relation_ngo_volunteer.onchange_type_selection_id()["domain"]
self.assertTrue(
('category_id', 'in', [self.category_01_ngo.id]) in
domain['this_partner_id'])
("category_id", "in", [self.category_01_ngo.id])
in domain["this_partner_id"]
)
self.assertTrue(
('category_id', 'in', [self.category_02_volunteer.id]) in
domain['other_partner_id'])
("category_id", "in", [self.category_02_volunteer.id])
in domain["other_partner_id"]
)
# 4. Test with invalid or impossible combinations
relation_nobody = self._get_empty_relation()
with self.env.do_in_draft():
relation_nobody.type_selection_id = self.selection_nobody
warning = relation_nobody.onchange_type_selection_id()['warning']
self.assertTrue('message' in warning)
self.assertTrue('No this partner available' in warning['message'])
warning = relation_nobody.onchange_type_selection_id()["warning"]
self.assertTrue("message" in warning)
self.assertTrue("No this partner available" in warning["message"])
with self.env.do_in_draft():
relation_nobody.this_partner_id = self.partner_02_company
warning = relation_nobody.onchange_type_selection_id()['warning']
self.assertTrue('message' in warning)
self.assertTrue('incompatible' in warning['message'])
warning = relation_nobody.onchange_type_selection_id()["warning"]
self.assertTrue("message" in warning)
self.assertTrue("incompatible" in warning["message"])
# Allow left partner and check message for other partner:
self.type_nobody.write({'partner_category_left': False})
self.type_nobody.write({"partner_category_left": False})
self.selection_nobody.invalidate_cache(ids=self.selection_nobody.ids)
warning = relation_nobody.onchange_type_selection_id()['warning']
self.assertTrue('message' in warning)
self.assertTrue('No other partner available' in warning['message'])
warning = relation_nobody.onchange_type_selection_id()["warning"]
self.assertTrue("message" in warning)
self.assertTrue("No other partner available" in warning["message"])
def test_on_change_partner_id(self):
"""Test on_change_partner_id."""
# 1. Test call with empty relation
relation_empty = self._get_empty_relation()
result = relation_empty.onchange_partner_id()
self.assertTrue('domain' in result)
self.assertFalse('warning' in result)
self.assertTrue('type_selection_id' in result['domain'])
self.assertFalse(result['domain']['type_selection_id'])
self.assertTrue("domain" in result)
self.assertFalse("warning" in result)
self.assertTrue("type_selection_id" in result["domain"])
self.assertFalse(result["domain"]["type_selection_id"])
# 2. Test call with company 2 person relation
relation = self._create_company2person_relation()
domain = relation.onchange_partner_id()['domain']
self.assertTrue(
('contact_type_this', '=', 'c') in domain['type_selection_id'])
domain = relation.onchange_partner_id()["domain"]
self.assertTrue(("contact_type_this", "=", "c") in domain["type_selection_id"])
# 3. Test with invalid or impossible combinations
relation_nobody = self._get_empty_relation()
with self.env.do_in_draft():
relation_nobody.this_partner_id = self.partner_02_company
relation_nobody.type_selection_id = self.selection_nobody
warning = relation_nobody.onchange_partner_id()['warning']
self.assertTrue('message' in warning)
self.assertTrue('incompatible' in warning['message'])
warning = relation_nobody.onchange_partner_id()["warning"]
self.assertTrue("message" in warning)
self.assertTrue("incompatible" in warning["message"])
def test_write(self):
"""Test write. Special attention for changing type."""
relation_company2person = self._create_company2person_relation()
company_partner = relation_company2person.this_partner_id
# First get another worker:
partner_extra_person = self.partner_model.create({
'name': 'A new worker',
'is_company': False,
'ref': 'NW01'})
relation_company2person.write({
'other_partner_id': partner_extra_person.id})
partner_extra_person = self.partner_model.create(
{"name": "A new worker", "is_company": False, "ref": "NW01"}
)
relation_company2person.write({"other_partner_id": partner_extra_person.id})
self.assertEqual(
relation_company2person.other_partner_id.name,
partner_extra_person.name)
relation_company2person.other_partner_id.name, partner_extra_person.name
)
# We will also change to a type going from person to company:
(type_worker2company,
selection_worker2company,
selection_company2worker) = self._create_relation_type_selection({
'name': 'works for',
'name_inverse': 'has worker',
'contact_type_left': 'p',
'contact_type_right': 'c'})
relation_company2person.write({
'this_partner_id': partner_extra_person.id,
'type_selection_id': selection_worker2company.id,
'other_partner_id': company_partner.id})
(
type_worker2company,
selection_worker2company,
selection_company2worker,
) = self._create_relation_type_selection(
{
"name": "works for",
"name_inverse": "has worker",
"contact_type_left": "p",
"contact_type_right": "c",
}
)
relation_company2person.write(
{
"this_partner_id": partner_extra_person.id,
"type_selection_id": selection_worker2company.id,
"other_partner_id": company_partner.id,
}
)
self.assertEqual(
relation_company2person.this_partner_id.id,
partner_extra_person.id)
relation_company2person.this_partner_id.id, partner_extra_person.id
)
self.assertEqual(
relation_company2person.type_selection_id.id,
selection_worker2company.id)
relation_company2person.type_selection_id.id, selection_worker2company.id
)
self.assertEqual(
relation_company2person.other_partner_id.id,
company_partner.id)
relation_company2person.other_partner_id.id, company_partner.id
)

138
partner_multi_relation/tests/test_partner_relation_common.py

@ -4,73 +4,80 @@ from odoo.tests import common
class TestPartnerRelationCommon(common.TransactionCase):
def setUp(self):
super(TestPartnerRelationCommon, self).setUp()
self.partner_model = self.env['res.partner']
self.category_model = self.env['res.partner.category']
self.type_model = self.env['res.partner.relation.type']
self.selection_model = self.env['res.partner.relation.type.selection']
self.relation_model = self.env['res.partner.relation']
self.relation_all_model = self.env['res.partner.relation.all']
self.partner_01_person = self.partner_model.create({
'name': 'Test User 1',
'is_company': False,
'ref': 'PR01'})
self.partner_02_company = self.partner_model.create({
'name': 'Test Company',
'is_company': True,
'ref': 'PR02'})
self.partner_model = self.env["res.partner"]
self.category_model = self.env["res.partner.category"]
self.type_model = self.env["res.partner.relation.type"]
self.selection_model = self.env["res.partner.relation.type.selection"]
self.relation_model = self.env["res.partner.relation"]
self.relation_all_model = self.env["res.partner.relation.all"]
self.partner_01_person = self.partner_model.create(
{"name": "Test User 1", "is_company": False, "ref": "PR01"}
)
self.partner_02_company = self.partner_model.create(
{"name": "Test Company", "is_company": True, "ref": "PR02"}
)
# Create partners with specific categories:
self.category_01_ngo = self.category_model.create({'name': 'NGO'})
self.partner_03_ngo = self.partner_model.create({
'name': 'Test NGO',
'is_company': True,
'ref': 'PR03',
'category_id': [(4, self.category_01_ngo.id)]})
self.category_02_volunteer = self.category_model.create({
'name': 'Volunteer'})
self.partner_04_volunteer = self.partner_model.create({
'name': 'Test Volunteer',
'is_company': False,
'ref': 'PR04',
'category_id': [(4, self.category_02_volunteer.id)]})
self.category_01_ngo = self.category_model.create({"name": "NGO"})
self.partner_03_ngo = self.partner_model.create(
{
"name": "Test NGO",
"is_company": True,
"ref": "PR03",
"category_id": [(4, self.category_01_ngo.id)],
}
)
self.category_02_volunteer = self.category_model.create({"name": "Volunteer"})
self.partner_04_volunteer = self.partner_model.create(
{
"name": "Test Volunteer",
"is_company": False,
"ref": "PR04",
"category_id": [(4, self.category_02_volunteer.id)],
}
)
# Create a new relation type withouth categories:
(self.type_company2person,
self.selection_company2person,
self.selection_person2company) = \
self._create_relation_type_selection({
'name': 'mixed',
'name_inverse': 'mixed_inverse',
'contact_type_left': 'c',
'contact_type_right': 'p'})
(
self.type_company2person,
self.selection_company2person,
self.selection_person2company,
) = self._create_relation_type_selection(
{
"name": "mixed",
"name_inverse": "mixed_inverse",
"contact_type_left": "c",
"contact_type_right": "p",
}
)
# Create a new relation type with categories:
(self.type_ngo2volunteer,
self.selection_ngo2volunteer,
self.selection_volunteer2ngo) = \
self._create_relation_type_selection({
'name': 'NGO has volunteer',
'name_inverse': 'volunteer works for NGO',
'contact_type_left': 'c',
'contact_type_right': 'p',
'partner_category_left': self.category_01_ngo.id,
'partner_category_right': self.category_02_volunteer.id})
(
self.type_ngo2volunteer,
self.selection_ngo2volunteer,
self.selection_volunteer2ngo,
) = self._create_relation_type_selection(
{
"name": "NGO has volunteer",
"name_inverse": "volunteer works for NGO",
"contact_type_left": "c",
"contact_type_right": "p",
"partner_category_left": self.category_01_ngo.id,
"partner_category_right": self.category_02_volunteer.id,
}
)
def _create_relation_type_selection(self, vals):
"""Create relation type and return this with selection types."""
assert 'name' in vals, (
"Name missing in vals to create relation type. Vals: %s."
% vals)
assert 'name' in vals, (
"Name_inverse missing in vals to create relation type. Vals: %s."
% vals)
assert "name" in vals, (
"Name missing in vals to create relation type. Vals: %s." % vals
)
assert "name" in vals, (
"Name_inverse missing in vals to create relation type. Vals: %s." % vals
)
new_type = self.type_model.create(vals)
self.assertTrue(
new_type,
msg="No relation type created with vals %s." % vals)
selection_types = self.selection_model.search([
('type_id', '=', new_type.id)])
self.assertTrue(new_type, msg="No relation type created with vals %s." % vals)
selection_types = self.selection_model.search([("type_id", "=", new_type.id)])
for st in selection_types:
if st.is_inverse:
inverse_type_selection = st
@ -79,16 +86,21 @@ class TestPartnerRelationCommon(common.TransactionCase):
self.assertTrue(
inverse_type_selection,
msg="Failed to find inverse type selection based on"
" relation type created with vals %s." % vals)
" relation type created with vals %s." % vals,
)
self.assertTrue(
type_selection,
msg="Failed to find type selection based on"
" relation type created with vals %s." % vals)
" relation type created with vals %s." % vals,
)
return (new_type, type_selection, inverse_type_selection)
def _create_company2person_relation(self):
"""Utility function to get a relation from company 2 partner."""
return self.relation_all_model.create({
'type_selection_id': self.selection_company2person.id,
'this_partner_id': self.partner_02_company.id,
'other_partner_id': self.partner_01_person.id})
return self.relation_all_model.create(
{
"type_selection_id": self.selection_company2person.id,
"this_partner_id": self.partner_02_company.id,
"other_partner_id": self.partner_01_person.id,
}
)

74
partner_multi_relation/tests/test_partner_search.py

@ -8,68 +8,74 @@ from .test_partner_relation_common import TestPartnerRelationCommon
class TestPartnerSearch(TestPartnerRelationCommon):
def test_search_relation_type(self):
"""Test searching on relation type."""
relation = self._create_company2person_relation()
partners = self.partner_model.search([
('search_relation_type_id', '=', relation.type_selection_id.id)
])
partners = self.partner_model.search(
[("search_relation_type_id", "=", relation.type_selection_id.id)]
)
self.assertTrue(self.partner_02_company in partners)
partners = self.partner_model.search([
('search_relation_type_id', '!=', relation.type_selection_id.id)
])
partners = self.partner_model.search(
[("search_relation_type_id", "!=", relation.type_selection_id.id)]
)
self.assertTrue(self.partner_01_person in partners)
partners = self.partner_model.search([
('search_relation_type_id', '=', self.type_company2person.name)
])
partners = self.partner_model.search(
[("search_relation_type_id", "=", self.type_company2person.name)]
)
self.assertTrue(self.partner_01_person in partners)
self.assertTrue(self.partner_02_company in partners)
partners = self.partner_model.search([
('search_relation_type_id', '=', 'unknown relation')
])
partners = self.partner_model.search(
[("search_relation_type_id", "=", "unknown relation")]
)
self.assertFalse(partners)
# Check error with invalid search operator:
with self.assertRaises(ValidationError):
partners = self.partner_model.search([
('search_relation_type_id', 'child_of', 'some parent')
])
partners = self.partner_model.search(
[("search_relation_type_id", "child_of", "some parent")]
)
def test_search_relation_partner(self):
"""Test searching on related partner."""
self._create_company2person_relation()
partners = self.partner_model.search([
('search_relation_partner_id', '=', self.partner_02_company.id),
])
partners = self.partner_model.search(
[("search_relation_partner_id", "=", self.partner_02_company.id)]
)
self.assertTrue(self.partner_01_person in partners)
def test_search_relation_date(self):
"""Test searching on relations valid on a certain date."""
self._create_company2person_relation()
partners = self.partner_model.search([
('search_relation_date', '=', fields.Date.today()),
])
partners = self.partner_model.search(
[("search_relation_date", "=", fields.Date.today())]
)
self.assertTrue(self.partner_01_person in partners)
self.assertTrue(self.partner_02_company in partners)
def test_search_any_partner(self):
"""Test searching for partner left or right."""
self._create_company2person_relation()
both_relations = self.relation_all_model.search([
('any_partner_id', '=', self.partner_02_company.id),
])
both_relations = self.relation_all_model.search(
[("any_partner_id", "=", self.partner_02_company.id)]
)
self.assertEqual(len(both_relations), 2)
def test_search_partner_category(self):
"""Test searching for partners related to partners having category."""
relation_ngo_volunteer = self.relation_all_model.create({
'this_partner_id': self.partner_03_ngo.id,
'type_selection_id': self.selection_ngo2volunteer.id,
'other_partner_id': self.partner_04_volunteer.id,
})
relation_ngo_volunteer = self.relation_all_model.create(
{
"this_partner_id": self.partner_03_ngo.id,
"type_selection_id": self.selection_ngo2volunteer.id,
"other_partner_id": self.partner_04_volunteer.id,
}
)
self.assertTrue(relation_ngo_volunteer)
partners = self.partner_model.search([
('search_relation_partner_category_id', '=',
self.category_02_volunteer.id)
])
partners = self.partner_model.search(
[
(
"search_relation_partner_category_id",
"=",
self.category_02_volunteer.id,
)
]
)
self.assertTrue(self.partner_03_ngo in partners)

55
partner_multi_relation/views/menu.xml

@ -1,32 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<menuitem
id="menu_res_partner_relation"
name="Relations"
sequence="3"
parent="contacts.menu_contacts"
/>
<menuitem
id="menu_res_partner_relation_all"
sequence="3"
parent="menu_res_partner_relation"
action="action_res_partner_relation_all"
/>
<act_window
id="action_res_partner_relation_type"
res_model="res.partner.relation.type"
view_mode="tree,form"
name="Partner Relations Types"
/>
<menuitem
id="menu_res_partner_relation_type"
name="Relation Types"
parent="menu_res_partner_relation"
action="action_res_partner_relation_type"
/>
<menuitem
id="menu_res_partner_relation"
name="Relations"
sequence="3"
parent="contacts.menu_contacts"
/>
<menuitem
id="menu_res_partner_relation_all"
sequence="3"
parent="menu_res_partner_relation"
action="action_res_partner_relation_all"
/>
<act_window
id="action_res_partner_relation_type"
res_model="res.partner.relation.type"
view_mode="tree,form"
name="Partner Relations Types"
/>
<menuitem
id="menu_res_partner_relation_type"
name="Relation Types"
parent="menu_res_partner_relation"
action="action_res_partner_relation_type"
/>
</odoo>

78
partner_multi_relation/views/res_partner.xml

@ -1,43 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_res_partner_filter" model="ir.ui.view">
<field name="name">partner_multi_relation.view_partner_filter</field>
<field name="inherit_id" ref="base.view_res_partner_filter" />
<field name="model">res.partner</field>
<field type="xml" name="arch">
<field name="parent_id" position="after">
<field name="search_relation_partner_id" />
<field name="search_relation_type_id" />
<field name="search_relation_date" />
<field name="search_relation_partner_category_id" />
</field>
<record id="view_res_partner_filter" model="ir.ui.view">
<field name="name">partner_multi_relation.view_partner_filter</field>
<field name="inherit_id" ref="base.view_res_partner_filter" />
<field name="model">res.partner</field>
<field type="xml" name="arch">
<field name="parent_id" position="after">
<field name="search_relation_partner_id" />
<field name="search_relation_type_id" />
<field name="search_relation_date" />
<field name="search_relation_partner_category_id" />
</field>
</record>
<record id="view_partner_form" model="ir.ui.view">
<field name="name">partner_multi_relation.view_partner_form</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="model">res.partner</field>
<field type="xml" name="arch">
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_view_relations"
type="object"
class="oe_stat_button"
icon="fa-users">
<field name="relation_count"
widget="statinfo"
string="Relations"/>
</button>
</xpath>
</field>
</record>
<act_window id="action_show_partner_multi_relation"
name="Show partner's relations"
src_model="res.partner"
res_model="res.partner.relation.all"
domain="[('this_partner_id', 'in', active_ids)]"
key2="client_action_multi" />
</field>
</record>
<record id="view_partner_form" model="ir.ui.view">
<field name="name">partner_multi_relation.view_partner_form</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="model">res.partner</field>
<field type="xml" name="arch">
<xpath expr="//div[@name='button_box']" position="inside">
<button
name="action_view_relations"
type="object"
class="oe_stat_button"
icon="fa-users"
>
<field name="relation_count" widget="statinfo" string="Relations" />
</button>
</xpath>
</field>
</record>
<act_window
id="action_show_partner_multi_relation"
name="Show partner's relations"
src_model="res.partner"
res_model="res.partner.relation.all"
domain="[('this_partner_id', 'in', active_ids)]"
key2="client_action_multi"
/>
</odoo>

174
partner_multi_relation/views/res_partner_relation_all.xml

@ -1,101 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="tree_res_partner_relation_all" model="ir.ui.view">
<field name="model">res.partner.relation.all</field>
<field name="arch" type="xml">
<tree
string="Partner Relations"
colors="gray:not active; blue:date_start &gt; current_date"
editable="top"
>
<field
name="this_partner_id"
required="True"
options="{'no_create': True}"
/>
<field
name="type_selection_id"
required="True"
options="{'no_create': True}"
/>
<field
name="other_partner_id"
required="True"
options="{'no_create': True}"
/>
<field name="date_start" />
<field name="date_end" />
<field name="active" invisible="1" />
</tree>
</field>
</record>
<record id="search_res_partner_relation_all" model="ir.ui.view">
<field name="model">res.partner.relation.all</field>
<field name="arch" type="xml">
<search string="Search Relations">
<field name="any_partner_id" widget="many2one"/>
<field name="this_partner_id"/>
<field name="other_partner_id"/>
<field name="type_selection_id"/>
<record id="tree_res_partner_relation_all" model="ir.ui.view">
<field name="model">res.partner.relation.all</field>
<field name="arch" type="xml">
<tree
string="Partner Relations"
colors="gray:not active; blue:date_start &gt; current_date"
editable="top"
>
<field
name="this_partner_id"
required="True"
options="{'no_create': True}"
/>
<field
name="type_selection_id"
required="True"
options="{'no_create': True}"
/>
<field
name="other_partner_id"
required="True"
options="{'no_create': True}"
/>
<field name="date_start" />
<field name="date_end" />
<field name="active" invisible="1" />
</tree>
</field>
</record>
<record id="search_res_partner_relation_all" model="ir.ui.view">
<field name="model">res.partner.relation.all</field>
<field name="arch" type="xml">
<search string="Search Relations">
<field name="any_partner_id" widget="many2one" />
<field name="this_partner_id" />
<field name="other_partner_id" />
<field name="type_selection_id" />
<filter
name="left_to_right"
string="Left to right"
domain="[('is_inverse', '=', False)]"
/>
<filter
name="right_to_left"
string="Right to left"
domain="[('is_inverse', '=', True)]"
/>
<filter
name="include_part_records"
string="Include past records"
context="{'active_test': False}"
/>
<group expand="0" string="Group By">
<filter
name="left_to_right"
string="Left to right"
domain="[('is_inverse', '=', False)]"
/>
name="group_by_this_partner_id"
string="One Partner"
context="{'group_by': 'this_partner_id'}"
/>
<filter
name="right_to_left"
string="Right to left"
domain="[('is_inverse', '=', True)]"
/>
name="group_by_type_selection_id"
string="Relationship Type"
context="{'group_by': 'type_selection_id'}"
/>
<filter
name="include_part_records"
string="Include past records"
context="{'active_test': False}"
/>
<group expand="0" string="Group By">
<filter
name="group_by_this_partner_id"
string="One Partner"
context="{'group_by': 'this_partner_id'}"
/>
<filter
name="group_by_type_selection_id"
string="Relationship Type"
context="{'group_by': 'type_selection_id'}"
/>
<filter
name="group_by_other_partner_id"
string="Other Partner"
context="{'group_by': 'other_partner_id'}"
/>
</group>
</search>
</field>
</record>
<record
id="action_res_partner_relation_all"
model="ir.actions.act_window"
>
<field name="name">Relations</field>
<field name="res_model">res.partner.relation.all</field>
<field name="view_type">form</field>
<field name="view_mode">tree</field>
<field name="view_id" ref="tree_res_partner_relation_all"/>
<field
name="search_view_id"
ref="search_res_partner_relation_all"
/>
<field name="context">{'active_test': 0}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
name="group_by_other_partner_id"
string="Other Partner"
context="{'group_by': 'other_partner_id'}"
/>
</group>
</search>
</field>
</record>
<record id="action_res_partner_relation_all" model="ir.actions.act_window">
<field name="name">Relations</field>
<field name="res_model">res.partner.relation.all</field>
<field name="view_type">form</field>
<field name="view_mode">tree</field>
<field name="view_id" ref="tree_res_partner_relation_all" />
<field name="search_view_id" ref="search_res_partner_relation_all" />
<field name="context">{'active_test': 0}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Record and track your partners' relations. Relations may
be linked to other partners with a type either directly
or inversely.
</p>
</field>
</record>
</field>
</record>
</odoo>

95
partner_multi_relation/views/res_partner_relation_type.xml

@ -1,56 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="tree_res_partner_relation_type" model="ir.ui.view">
<field name="model">res.partner.relation.type</field>
<field type="xml" name="arch">
<tree>
<field name="name" />
<field name="name_inverse" />
<field name="contact_type_left" />
<field name="contact_type_right" />
<field name="allow_self" />
<field name="is_symmetric" />
</tree>
</field>
</record>
<record id="form_res_partner_relation_type" model="ir.ui.view">
<field name="model">res.partner.relation.type</field>
<field type="xml" name="arch">
<form>
<sheet>
<group>
<group
string="Left side of relation"
name="left"
>
<field name="name" />
<field name="contact_type_left" />
<field name="partner_category_left" />
</group>
<group
string="Right side of relation"
name="right"
attrs="{'invisible': [('is_symmetric', '=', True)]}"
>
<field name="name_inverse" attrs="{'required': [('is_symmetric', '=', False)]}"/>
<field name="contact_type_right" />
<field name="partner_category_right" />
</group>
<record id="tree_res_partner_relation_type" model="ir.ui.view">
<field name="model">res.partner.relation.type</field>
<field type="xml" name="arch">
<tree>
<field name="name" />
<field name="name_inverse" />
<field name="contact_type_left" />
<field name="contact_type_right" />
<field name="allow_self" />
<field name="is_symmetric" />
</tree>
</field>
</record>
<record id="form_res_partner_relation_type" model="ir.ui.view">
<field name="model">res.partner.relation.type</field>
<field type="xml" name="arch">
<form>
<sheet>
<group>
<group string="Left side of relation" name="left">
<field name="name" />
<field name="contact_type_left" />
<field name="partner_category_left" />
</group>
<group
name="properties"
string="Properties"
colspan= "6" col="4"
>
<field name="allow_self" />
<field name="is_symmetric" />
<field name="handle_invalid_onchange" />
string="Right side of relation"
name="right"
attrs="{'invisible': [('is_symmetric', '=', True)]}"
>
<field
name="name_inverse"
attrs="{'required': [('is_symmetric', '=', False)]}"
/>
<field name="contact_type_right" />
<field name="partner_category_right" />
</group>
</sheet>
</form>
</field>
</record>
</group>
<group name="properties" string="Properties" colspan="6" col="4">
<field name="allow_self" />
<field name="is_symmetric" />
<field name="handle_invalid_onchange" />
</group>
</sheet>
</form>
</field>
</record>
</odoo>
Loading…
Cancel
Save