diff --git a/partner_multi_relation/README.rst b/partner_multi_relation/README.rst index f73d5aaa6..fe20b1e24 100644 --- a/partner_multi_relation/README.rst +++ b/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 * Bruno Joliveau * Adriana Ierfino +* Numigi (tm) and all its contributors (https://bit.ly/numigiens) Maintainer ---------- diff --git a/partner_multi_relation/__manifest__.py b/partner_multi_relation/__manifest__.py index 33fa9d4db..011685f32 100644 --- a/partner_multi_relation/__manifest__.py +++ b/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, diff --git a/partner_multi_relation/i18n/fr.po b/partner_multi_relation/i18n/fr.po index 1eaa041c7..d5c4c6a45 100644 --- a/partner_multi_relation/i18n/fr.po +++ b/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" diff --git a/partner_multi_relation/models/res_partner.py b/partner_multi_relation/models/res_partner.py index e37ce4157..d9ab05e75 100644 --- a/partner_multi_relation/models/res_partner.py +++ b/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): diff --git a/partner_multi_relation/models/res_partner_relation.py b/partner_multi_relation/models/res_partner_relation.py index 01972214a..0907ab202 100644 --- a/partner_multi_relation/models/res_partner_relation.py +++ b/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') + ) diff --git a/partner_multi_relation/models/res_partner_relation_all.py b/partner_multi_relation/models/res_partner_relation_all.py index 63e051b57..c8189a312 100644 --- a/partner_multi_relation/models/res_partner_relation_all.py +++ b/partner_multi_relation/models/res_partner_relation_all.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014-2018 Therp BV # 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. diff --git a/partner_multi_relation/models/res_partner_relation_type.py b/partner_multi_relation/models/res_partner_relation_type.py index 1a6aa2fa2..dbcfd2570 100644 --- a/partner_multi_relation/models/res_partner_relation_type.py +++ b/partner_multi_relation/models/res_partner_relation_type.py @@ -1,9 +1,9 @@ # Copyright 2013-2018 Therp BV . # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). """Define the type of relations that can exist between partners.""" -from 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 diff --git a/partner_multi_relation/models/res_partner_relation_type_selection.py b/partner_multi_relation/models/res_partner_relation_type_selection.py index 7a00b044d..599988ac2 100644 --- a/partner_multi_relation/models/res_partner_relation_type_selection.py +++ b/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): diff --git a/partner_multi_relation/tests/test_partner_relation.py b/partner_multi_relation/tests/test_partner_relation.py index dfb64b449..569314e00 100644 --- a/partner_multi_relation/tests/test_partner_relation.py +++ b/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. diff --git a/partner_multi_relation/tests/test_partner_relation_all.py b/partner_multi_relation/tests/test_partner_relation_all.py index e00f814d6..415329096 100644 --- a/partner_multi_relation/tests/test_partner_relation_all.py +++ b/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 diff --git a/partner_multi_relation/tests/test_partner_relation_common.py b/partner_multi_relation/tests/test_partner_relation_common.py index 10d830082..441314c80 100644 --- a/partner_multi_relation/tests/test_partner_relation_common.py +++ b/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): diff --git a/partner_multi_relation/tests/test_partner_search.py b/partner_multi_relation/tests/test_partner_search.py index aa1f0e11e..d5f4ec707 100644 --- a/partner_multi_relation/tests/test_partner_search.py +++ b/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 diff --git a/partner_multi_relation/views/res_partner_relation_type.xml b/partner_multi_relation/views/res_partner_relation_type.xml index fb3cbd605..b82e704b5 100644 --- a/partner_multi_relation/views/res_partner_relation_type.xml +++ b/partner_multi_relation/views/res_partner_relation_type.xml @@ -34,7 +34,7 @@ name="right" attrs="{'invisible': [('is_symmetric', '=', True)]}" > - +