Browse Source

[MIG] partner_multi_relation: Continue migration to 11.0

* Bug Fix
* Enable additional columns on res_partner_relation_all.

  Enable to add additional columns from res_partner_relation in the sql view of res_partner_relation_all.

* Only require inverse_name when relation type is not symmetric
* Remove deprecated @api.one and replace openerp with odoo
* Add website to manifest
* Prevent disabling allow_self if reflexive relations exist
* Enable deleting/ending invalid relations after deactivating reflexivity.
* Handle case of invalid future reflexive relations
pull/654/head
David Dufresne 7 years ago
committed by Pedro M. Baeza
parent
commit
97ce337912
  1. 7
      partner_multi_relation/README.rst
  2. 5
      partner_multi_relation/__manifest__.py
  3. 6
      partner_multi_relation/i18n/fr.po
  4. 4
      partner_multi_relation/models/res_partner.py
  5. 110
      partner_multi_relation/models/res_partner_relation.py
  6. 26
      partner_multi_relation/models/res_partner_relation_all.py
  7. 101
      partner_multi_relation/models/res_partner_relation_type.py
  8. 4
      partner_multi_relation/models/res_partner_relation_type_selection.py
  9. 123
      partner_multi_relation/tests/test_partner_relation.py
  10. 2
      partner_multi_relation/tests/test_partner_relation_all.py
  11. 2
      partner_multi_relation/tests/test_partner_relation_common.py
  12. 4
      partner_multi_relation/tests/test_partner_search.py
  13. 2
      partner_multi_relation/views/res_partner_relation_type.xml

7
partner_multi_relation/README.rst

@ -22,7 +22,7 @@ Usage
=====
Before being able to use relations, you'll have define some first. Do that in
Sales / Configuration / Address Book / Partner relations. Here, you need to
Contacts / Relations / Partner relations. Here, you need to
name both sides of the relation: To have an assistant-relation, you would name
one side 'is assistant of' and the other side 'has assistant'. This relation
only makes sense between people, so you would choose 'Person' for both partner
@ -31,11 +31,11 @@ while the relation 'has worked for' should have persons on the left side and
companies on the right side. If you leave this field empty, the relation is
applicable to all types of partners.
If you use categories to further specify the type of partners, you could for
If you use categories (tags) to further specify the type of partners, you could for
example enforce that the 'is member of' relation can only have companies with
label 'Organization' on the left side.
Now open a partner and choose relations as appropriate in the 'Relations' tab.
Now open a partner, click on the 'Relations' smart button and add relations as appropriate.
Searching partners with relations
---------------------------------
@ -87,6 +87,7 @@ Contributors
* Sandy Carter <sandy.carter@savoirfairelinux.com>
* Bruno Joliveau <bruno.joliveau@savoirfairelinux.com>
* Adriana Ierfino <adriana.ierfino@savoirfairelinux.com>
* Numigi (tm) and all its contributors (https://bit.ly/numigiens)
Maintainer
----------

5
partner_multi_relation/__manifest__.py

@ -2,8 +2,9 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Partner Relations",
"version": "1.0.0",
"version": "11.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",
@ -15,11 +16,11 @@
"data/demo.xml",
],
"data": [
'security/ir.model.access.csv',
"views/res_partner_relation_all.xml",
'views/res_partner.xml',
'views/res_partner_relation_type.xml',
'views/menu.xml',
'security/ir.model.access.csv',
],
"auto_install": False,
"installable": True,

6
partner_multi_relation/i18n/fr.po

@ -374,6 +374,12 @@ msgstr "Les enregistrements avec la date de fin dans le passé sont inactifs"
msgid "Reflexive"
msgstr "Réflexive"
#. module: partner_multi_relation
#: code:addons/partner_multi_relation/models/res_partner_relation_type.py:179
#, python-format
msgid "Reflexivity could not be disabled for the relation type {relation_type}. There are existing reflexive relations defined for the following partners: {partners}"
msgstr "La reflexivité n'a pas pu être désactivée pour le type de relation {relation_type}. Il existe des relations réflexives pour les partenaires suivants: {partners}"
#. module: partner_multi_relation
#: model:ir.model.fields,field_description:partner_multi_relation.field_res_partner_relation_all_relation_id
msgid "Relation"

4
partner_multi_relation/models/res_partner.py

@ -3,8 +3,8 @@
"""Support connections between partners."""
import numbers
from openerp import _, api, exceptions, fields, models
from openerp.osv.expression import is_leaf, OR, FALSE_LEAF
from odoo import _, api, exceptions, fields, models
from odoo.osv.expression import is_leaf, OR, FALSE_LEAF
class ResPartner(models.Model):

110
partner_multi_relation/models/res_partner_relation.py

@ -2,8 +2,8 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
# pylint: disable=api-one-deprecated
"""Store relations (connections) between partners."""
from openerp import _, api, fields, models
from openerp.exceptions import ValidationError
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class ResPartnerRelation(models.Model):
@ -49,20 +49,19 @@ class ResPartnerRelation(models.Model):
vals['left_partner_id'] = context.get('active_id')
return super(ResPartnerRelation, self).create(vals)
@api.one
@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
"""
if (self.date_start and self.date_end and
self.date_start > self.date_end):
raise ValidationError(
_('The starting date cannot be after the ending date.')
)
for record in self:
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.')
)
@api.one
@api.constrains('left_partner_id', 'type_id')
def _check_partner_left(self):
"""Check left partner for required company or person
@ -71,7 +70,6 @@ class ResPartnerRelation(models.Model):
"""
self._check_partner("left")
@api.one
@api.constrains('right_partner_id', 'type_id')
def _check_partner_right(self):
"""Check right partner for required company or person
@ -80,43 +78,43 @@ class ResPartnerRelation(models.Model):
"""
self._check_partner("right")
@api.one
@api.multi
def _check_partner(self, side):
"""Check partner for required company or person, and for category
:param str side: left or right
:raises ValidationError: When constraint is violated
"""
assert side in ['left', 'right']
ptype = getattr(self.type_id, "contact_type_%s" % side)
partner = getattr(self, '%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
)
category = getattr(self.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)
)
@api.one
for record in self:
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)):
raise ValidationError(
_('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)
)
@api.constrains('left_partner_id', 'right_partner_id')
def _check_not_with_self(self):
"""Not allowed to link partner to same partner
:raises ValidationError: When constraint is violated
"""
if self.left_partner_id == self.right_partner_id:
if not (self.type_id and self.type_id.allow_self):
raise ValidationError(
_('Partners cannot have a relation with themselves.')
)
for record in self:
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.')
)
@api.one
@api.constrains(
'left_partner_id',
'type_id',
@ -132,25 +130,27 @@ class ResPartnerRelation(models.Model):
"""
# pylint: disable=no-member
# pylint: disable=no-value-for-parameter
domain = [
('type_id', '=', self.type_id.id),
('id', '!=', self.id),
('left_partner_id', '=', self.left_partner_id.id),
('right_partner_id', '=', self.right_partner_id.id),
]
if self.date_start:
domain += [
'|',
('date_end', '=', False),
('date_end', '>=', self.date_start),
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),
]
if self.date_end:
domain += [
'|',
('date_start', '=', False),
('date_start', '<=', self.date_end),
]
if self.search(domain):
raise ValidationError(
_('There is already a similar relation with overlapping dates')
)
if record.date_start:
domain += [
'|',
('date_end', '=', False),
('date_end', '>=', record.date_start),
]
if record.date_end:
domain += [
'|',
('date_start', '=', False),
('date_start', '<=', record.date_end),
]
if record.search(domain):
raise ValidationError(
_('There is already a similar relation with '
'overlapping dates')
)

26
partner_multi_relation/models/res_partner_relation_all.py

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Copyright 2014-2018 Therp BV <http://therp.nl>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
# pylint: disable=method-required-super
@ -7,9 +6,9 @@ import logging
from psycopg2.extensions import AsIs
from openerp import _, api, fields, models
from openerp.exceptions import ValidationError
from openerp.tools import drop_view_if_exists
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import drop_view_if_exists
_logger = logging.getLogger(__name__)
@ -27,6 +26,7 @@ SELECT
rel.date_start,
rel.date_end,
%(is_inverse)s as is_inverse
%(extra_additional_columns)s
FROM res_partner_relation rel"""
# Register inverse relations
@ -41,6 +41,7 @@ SELECT
rel.date_start,
rel.date_end,
%(is_inverse)s as is_inverse
%(extra_additional_columns)s
FROM res_partner_relation rel"""
@ -112,7 +113,10 @@ class ResPartnerRelationAll(models.AbstractModel):
key_offset=_last_key_offset,
select_sql=select_sql % {
'key_offset': _last_key_offset,
'is_inverse': is_inverse})
'is_inverse': is_inverse,
'extra_additional_columns':
self._get_additional_relation_columns(),
})
def get_register(self):
register = collections.OrderedDict()
@ -133,7 +137,7 @@ class ResPartnerRelationAll(models.AbstractModel):
register = self.get_register()
union_select = ' UNION '.join(
[register[key]['select_sql']
for key in register.iterkeys() if key != '_lastkey'])
for key in register if key != '_lastkey'])
return """\
CREATE OR REPLACE VIEW %%(table)s AS
WITH base_selection AS (%(union_select)s)
@ -155,6 +159,16 @@ CREATE OR REPLACE VIEW %%(table)s AS
"""Utility function to define padding in one place."""
return 100
def _get_additional_relation_columns(self):
"""Get additionnal columns from res_partner_relation.
This allows to add fields to the model res.partner.relation
and display these fields in the res.partner.relation.all list view.
:return: ', rel.column_a, rel.column_b_id'
"""
return ''
def _get_additional_view_fields(self):
"""Allow inherit models to add fields to view.

101
partner_multi_relation/models/res_partner_relation_type.py

@ -1,9 +1,9 @@
# Copyright 2013-2018 Therp BV <https://therp.nl>.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
"""Define the type of relations that can exist between partners."""
from openerp import _, api, fields, models
from openerp.exceptions import ValidationError
from openerp.osv.expression import AND, OR
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.osv.expression import AND, OR
HANDLE_INVALID_ONCHANGE = [
@ -83,6 +83,27 @@ class ResPartnerRelationType(models.Model):
('p', _('Person')),
]
@api.model
def _end_active_relations(self, relations):
"""End the relations that are active.
If a relation is current, that is, if it has a start date
in the past and end date in the future (or no end date),
the end date will be set to the current date.
If a relation has a end date in the past, then it is inactive and
will not be modified.
:param relations: a recordset of relations (not necessarily all active)
"""
today = fields.Date.today()
for relation in relations:
if relation.date_start and relation.date_start >= today:
relation.unlink()
elif not relation.date_end or relation.date_end > today:
relation.write({'date_end': today})
@api.multi
def check_existing(self, vals):
"""Check wether records exist that do not fit new criteria."""
@ -147,16 +168,64 @@ class ResPartnerRelationType(models.Model):
elif handling == 'delete':
invalid_relations.unlink()
else:
# Delete future records, end other ones, ignore relations
# already ended:
cutoff_date = fields.Date.today()
for relation in invalid_relations:
if (relation.date_start and
relation.date_start >= cutoff_date):
relation.unlink()
elif (not relation.date_end or
relation.date_end > cutoff_date):
relation.write({'date_end': cutoff_date})
self._end_active_relations(invalid_relations)
def _get_reflexive_relations(self):
"""Get all reflexive relations for this relation type.
:return: a recordset of res.partner.relation.
"""
self.env.cr.execute(
"""
SELECT id FROM res_partner_relation
WHERE left_partner_id = right_partner_id
AND type_id = %(relation_type_id)s
""", {
'relation_type_id': self.id,
}
)
reflexive_relation_ids = [r[0] for r in self.env.cr.fetchall()]
return self.env['res.partner.relation'].browse(reflexive_relation_ids)
def _check_no_existing_reflexive_relations(self):
"""Check that no reflexive relation exists for these relation types."""
for relation_type in self:
relations = relation_type._get_reflexive_relations()
if relations:
raise ValidationError(
_("Reflexivity could not be disabled for the relation "
"type {relation_type}. There are existing reflexive "
"relations defined for the following partners: "
"{partners}").format(
relation_type=relation_type.display_name,
partners=relations.mapped(
'left_partner_id.display_name')))
def _delete_existing_reflexive_relations(self):
"""Delete existing reflexive relations for these relation types."""
for relation_type in self:
relations = relation_type._get_reflexive_relations()
relations.unlink()
def _end_active_reflexive_relations(self):
"""End active reflexive relations for these relation types."""
for relation_type in self:
reflexive_relations = relation_type._get_reflexive_relations()
self._end_active_relations(reflexive_relations)
def _handle_deactivation_of_allow_self(self):
"""Handle the deactivation of reflexivity on these relations types."""
restrict_relation_types = self.filtered(
lambda t: t.handle_invalid_onchange == 'restrict')
restrict_relation_types._check_no_existing_reflexive_relations()
delete_relation_types = self.filtered(
lambda t: t.handle_invalid_onchange == 'delete')
delete_relation_types._delete_existing_reflexive_relations()
end_relation_types = self.filtered(
lambda t: t.handle_invalid_onchange == 'end')
end_relation_types._end_active_reflexive_relations()
@api.multi
def _update_right_vals(self, vals):
@ -186,11 +255,17 @@ class ResPartnerRelationType(models.Model):
def write(self, vals):
"""Handle existing relations if conditions change."""
self.check_existing(vals)
for rec in self:
rec_vals = vals.copy()
if rec_vals.get('is_symmetric', rec.is_symmetric):
self._update_right_vals(rec_vals)
super(ResPartnerRelationType, rec).write(rec_vals)
allow_self_disabled = 'allow_self' in vals and not vals['allow_self']
if allow_self_disabled:
self._handle_deactivation_of_allow_self()
return True
@api.multi

4
partner_multi_relation/models/res_partner_relation_type_selection.py

@ -14,8 +14,8 @@ the field labels translatable.
"""
from psycopg2.extensions import AsIs
from openerp import api, fields, models
from openerp.tools import drop_view_if_exists
from odoo import api, fields, models
from odoo.tools import drop_view_if_exists
class ResPartnerRelationTypeSelection(models.Model):

123
partner_multi_relation/tests/test_partner_relation.py

@ -1,10 +1,10 @@
# Copyright 2016-2017 Therp BV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from datetime import date
from datetime import datetime, date, timedelta
from dateutil.relativedelta import relativedelta
from openerp import fields
from openerp.exceptions import ValidationError
from odoo import fields
from odoo.exceptions import ValidationError
from .test_partner_relation_common import TestPartnerRelationCommon
@ -56,6 +56,123 @@ class TestPartnerRelation(TestPartnerRelationCommon):
'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.
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})
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})
self.assertTrue(reflexive_relation)
with self.assertRaises(ValidationError):
type_allow.allow_self = False
def test_self_disallowed_with_delete_invalid_relations(self):
"""Test handle_invalid_onchange delete with allow_self disabled.
When deactivating allow_self, if handle_invalid_onchange is set
to delete, then existing reflexive relations are deleted.
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.allow_self = False
self.assertFalse(reflexive_relation.exists())
self.assertTrue(normal_relation.exists())
def test_self_disallowed_with_end_invalid_relations(self):
"""Test handle_invalid_onchange delete with allow_self disabled.
When deactivating allow_self, if handle_invalid_onchange is set
to end, then active reflexive relations are ended.
Non reflexive relations are not modified.
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.allow_self = False
self.assertEqual(reflexive_relation.date_end, fields.Date.today())
self.assertEqual(past_reflexive_relation.date_end, '2000-01-01')
self.assertFalse(normal_relation.date_end)
def test_self_disallowed_with_future_reflexive_relation(self):
"""Test future reflexive relations are deleted.
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.allow_self = False
self.assertFalse(future_reflexive_relation.exists())
def test_self_default(self):
"""Test default not to allow relation with same partner.

2
partner_multi_relation/tests/test_partner_relation_all.py

@ -1,6 +1,6 @@
# Copyright 2016-2017 Therp BV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp.exceptions import ValidationError
from odoo.exceptions import ValidationError
from .test_partner_relation_common import TestPartnerRelationCommon

2
partner_multi_relation/tests/test_partner_relation_common.py

@ -1,6 +1,6 @@
# Copyright 2016 Therp BV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp.tests import common
from odoo.tests import common
class TestPartnerRelationCommon(common.TransactionCase):

4
partner_multi_relation/tests/test_partner_search.py

@ -1,8 +1,8 @@
# Copyright 2015 Camptocamp SA
# Copyright 2016 Therp BV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import fields
from openerp.exceptions import ValidationError
from odoo import fields
from odoo.exceptions import ValidationError
from .test_partner_relation_common import TestPartnerRelationCommon

2
partner_multi_relation/views/res_partner_relation_type.xml

@ -34,7 +34,7 @@
name="right"
attrs="{'invisible': [('is_symmetric', '=', True)]}"
>
<field name="name_inverse" />
<field name="name_inverse" attrs="{'required': [('is_symmetric', '=', False)]}"/>
<field name="contact_type_right" />
<field name="partner_category_right" />
</group>

Loading…
Cancel
Save