From 082f4f96ce0ee0a9c4b545e5256f437057c607d6 Mon Sep 17 00:00:00 2001 From: Juan Jose Scarafia Date: Wed, 2 Nov 2016 21:20:52 -0300 Subject: [PATCH] FIX pylint (cherry picked from commit eadcae217103fd8f2f8d3db87c6fb659e048812b) Conflicts: partner_identification/__openerp__.py Set version to 8.0.1.0.0. Remove dependency on sales_team, as the relevant change is not in 8.0. Change emails to the new ones (cherry picked from commit 3455ae614e28d7807fc19d7be54512cbe59d44ec) Update new name (cherry picked from commit 5b592d7562fddac0cf48c71e6607cf17c009e993) [FIX] try me on runbot link (cherry picked from commit bd587b6058a17814cee5496a0ed3c126600f6fd6) Conflicts: partner_identification/README.rst Changed runbot to 8.0 [IMP] partner_identification: Add context override (#373) Allow for context override of validations using ``id_no_validate`` (cherry picked from commit 76c2e7b784916cdca0753a46a7b2be75edc1d70d) [10.0][IMP] partner_identification: Add field computation and inverses (#419) * [IMP] partner_identification: Add field computation and inverses * Add methods to allow for computation and inverse of an ID field of a specific category type * [IMP] partner_identification: Add search option (cherry picked from commit 19c5fb6de2a710dd50248fd843465f454de887bf) [FIX] partner_identification: Infinite loop in search (#436) (cherry picked from commit fa9b390dc62f66ef33acd7aacdfb3b79912ebc28) [FIX] partner-contact CI interactions (cherry picked from commit bc93e7bbc3e0f059b228970f0a05e57f0efba310) [ADD][8.0] Backport of the 9.0 module. (cherry picked from commit a42540381d448c3a62fabf69043ed23bf1aeca3e) [8.0][MIG] partner_identification backport --- README.rst | 17 ++- __openerp__.py | 7 +- models/res_partner.py | 161 ++++++++++++++++++++++++++- models/res_partner_id_category.py | 2 + tests/__init__.py | 2 + tests/test_partner_identification.py | 22 +++- tests/test_res_partner.py | 131 ++++++++++++++++++++++ 7 files changed, 329 insertions(+), 13 deletions(-) create mode 100644 tests/test_res_partner.py diff --git a/README.rst b/README.rst index 5da05a1168e..4717336bbb5 100644 --- a/README.rst +++ b/README.rst @@ -15,7 +15,7 @@ and vary from country to country. * Fiscal ID's * Membership numbers * Driver license -* ... +* etc Installation @@ -35,8 +35,15 @@ Name: Code: Code, abbreviation or acronym of this ID type. For example, 'driver_license' Python validation code: - Optional python code called to validate ID numbers of this ID type. + Optional python code called to validate ID numbers of this ID type. This functionality can be + overridden by setting ``id_no_validate`` to ``True`` in the context, such as: + .. code-block:: python + + partner.with_context(id_no_validate=True).write({ + 'name': 'Bad Value', + 'category_id': self.env.ref('id_category_only_numerics').id, + }) Usage ===== @@ -63,7 +70,7 @@ Notes: .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/ + :target: https://runbot.odoo-community.org/runbot/134/8.0 Known issues / Roadmap @@ -97,7 +104,9 @@ Contributors * Ferdinand Gassauer * Gerhard Könighofer * Laurent Mignon -* Yajo +* Jairo Llopis +* Dave Lasley +* Kevin Graveman Maintainer ---------- diff --git a/__openerp__.py b/__openerp__.py index 6c1c7ac431c..911027ccd86 100644 --- a/__openerp__.py +++ b/__openerp__.py @@ -11,7 +11,7 @@ { 'name': 'Partner Identification Numbers', 'category': 'Customer Relationship Management', - 'version': '8.0.1.0.0', + 'version': '8.0.1.1.1', 'data': [ 'views/res_partner_id_category_view.xml', 'views/res_partner_id_number_view.xml', @@ -19,10 +19,11 @@ 'security/ir.model.access.csv', ], 'author': 'ChriCar Beteiligungs- und Beratungs- GmbH, ' - 'Antiun Ingeniería S.L.', + 'Tecnativa,' 'Camptocamp,' 'ACSONE SA/NV,' - 'Odoo Community Association (OCA)' + 'LasLabs,' + 'Odoo Community Association (OCA)', 'website': 'https://odoo-community.org/', 'license': 'AGPL-3', 'installable': True, diff --git a/models/res_partner.py b/models/res_partner.py index 63b8fcd4268..6d5170d9a7e 100644 --- a/models/res_partner.py +++ b/models/res_partner.py @@ -8,12 +8,165 @@ # Antonio Espinosa # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp import models, fields +from openerp import api, models, fields, _ +from openerp.exceptions import ValidationError class ResPartner(models.Model): - _inherit = 'res.partner' + _inherit = 'res.partner' # pylint: disable=R7980 id_numbers = fields.One2many( - comodel_name='res.partner.id_number', inverse_name='partner_id', - string="Identification Numbers") + comodel_name='res.partner.id_number', + inverse_name='partner_id', + string="Identification Numbers", + ) + + @api.multi + @api.depends('id_numbers') + def _compute_identification(self, field_name, category_code): + """ Compute a field that indicates a certain ID type. + + Use this on a field that represents a certain ID type. It will compute + the desired field as that ID(s). + + This ID can be worked with as if it were a Char field, but it will + be relating back to a ``res.partner.id_number`` instead. + + Example: + + .. code-block:: python + + social_security = fields.Char( + compute=lambda s: s._compute_identification( + 'social_security', 'SSN', + ), + inverse=lambda s: s._inverse_identification( + 'social_security', 'SSN', + ), + search=lambda s, *a: s._search_identification( + 'SSN', *a + ), + ) + + Args: + field_name (str): Name of field to set. + category_code (str): Category code of the Identification type. + """ + for record in self: + id_numbers = record.id_numbers.filtered( + lambda r: r.category_id.code == category_code + ) + if not id_numbers: + continue + value = id_numbers[0].name + record[field_name] = value + + @api.multi + def _inverse_identification(self, field_name, category_code): + """ Inverse for an identification field. + + This method will create a new record, or modify the existing one + in order to allow for the associated field to work like a Char. + + If a category does not exist of the correct code, it will be created + using `category_code` as both the `name` and `code` values. + + If the value of the target field is unset, the associated ID will + be deactivated in order to preserve history. + + Example: + + .. code-block:: python + + social_security = fields.Char( + compute=lambda s: s._compute_identification( + 'social_security', 'SSN', + ), + inverse=lambda s: s._inverse_identification( + 'social_security', 'SSN', + ), + search=lambda s, *a: s._search_identification( + 'SSN', *a + ), + ) + + Args: + field_name (str): Name of field to set. + category_code (str): Category code of the Identification type. + """ + for record in self: + id_number = record.id_numbers.filtered( + lambda r: r.category_id.code == category_code + ) + record_len = len(id_number) + # Record for category is not existent. + if record_len == 0: + name = record[field_name] + if not name: + # No value to set + continue + category = self.env['res.partner.id_category'].search([ + ('code', '=', category_code), + ]) + if not category: + category = self.env['res.partner.id_category'].create({ + 'code': category_code, + 'name': category_code, + }) + self.env['res.partner.id_number'].create({ + 'partner_id': record.id, + 'category_id': category.id, + 'name': name, + }) + # There was an identification record singleton found. + elif record_len == 1: + value = record[field_name] + if value: + id_number.name = value + else: + id_number.active = False + # Guard against writing wrong records. + else: + raise ValidationError(_( + 'This %s has multiple IDs of this type (%s), so a write ' + 'via the %s field is not possible. In order to fix this, ' + 'please use the IDs tab.', + ) % ( + record._name, category_code, field_name, + )) + + @api.model + def _search_identification(self, category_code, operator, value): + """ Search method for an identification field. + + Example: + + .. code-block:: python + + social_security = fields.Char( + compute=lambda s: s._compute_identification( + 'social_security', 'SSN', + ), + inverse=lambda s: s._inverse_identification( + 'social_security', 'SSN', + ), + search=lambda s, *a: s._search_identification( + 'SSN', *a + ), + ) + + Args: + category_code (str): Category code of the Identification type. + operator (str): Operator of domain. + value (str): Value to search for. + + Returns: + list: Domain to search with. + """ + id_numbers = self.env['res.partner.id_number'].search([ + ('name', operator, value), + ('category_id.code', '=', category_code), + ]) + return [ + ('id_numbers.id', 'in', id_numbers.ids), + ] diff --git a/models/res_partner_id_category.py b/models/res_partner_id_category.py index 01f38d0500c..fbefdff6b67 100644 --- a/models/res_partner_id_category.py +++ b/models/res_partner_id_category.py @@ -55,6 +55,8 @@ def validate_id_number(self, id_number): python validation code fails """ self.ensure_one() + if self.env.context.get('id_no_validate'): + return eval_context = self._validation_eval_context(id_number) try: safe_eval(self.validation_code, diff --git a/tests/__init__.py b/tests/__init__.py index f848b5bcfb5..dd49771a402 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1,5 @@ # -*- coding: utf-8 -*- # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + from . import test_partner_identification +from . import test_res_partner diff --git a/tests/test_partner_identification.py b/tests/test_partner_identification.py index 9d46f7642b6..b237311f40f 100644 --- a/tests/test_partner_identification.py +++ b/tests/test_partner_identification.py @@ -2,7 +2,7 @@ # © 2016 ACSONE SA/NV () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from psycopg2._psycopg import IntegrityError -import openerp.tests.common as common +from openerp.tests import common from openerp.exceptions import ValidationError @@ -75,7 +75,7 @@ def test_partner_id_number_validation(self): 'category_id': partner_id_category2.id }) - def test_bad_calidation_code(self): + def test_bad_validation_code(self): partner_id_category = self.env['res.partner.id_category'].create({ 'code': 'id_code', 'name': 'id_name', @@ -90,3 +90,21 @@ def test_bad_calidation_code(self): 'name': '1234', 'category_id': partner_id_category.id })]}) + + def test_bad_validation_code_override(self): + """ It should allow a bad validation code if context overrides. """ + partner_id_category = self.env['res.partner.id_category'].create({ + 'code': 'id_code', + 'name': 'id_name', + 'validation_code': """ +if id_number.name != '1234' # missing : + failed = True +""" + }) + partner_1 = self.env.ref('base.res_partner_1').with_context( + id_no_validate=True, + ) + partner_1.write({'id_numbers': [(0, 0, { + 'name': '1234', + 'category_id': partner_id_category.id + })]}) diff --git a/tests/test_res_partner.py b/tests/test_res_partner.py new file mode 100644 index 00000000000..7afa6242224 --- /dev/null +++ b/tests/test_res_partner.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp import fields, models +from openerp.tests import common +from openerp.exceptions import ValidationError + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + social_security = fields.Char( + compute=lambda s: s._compute_identification( + 'social_security', 'SSN', + ), + inverse=lambda s: s._inverse_identification( + 'social_security', 'SSN', + ), + search=lambda s, *a: s._search_identification( + 'SSN', *a + ), + ) + + +class TestResPartner(common.SavepointCase): + + @classmethod + def _init_test_model(cls, model_cls): + """ Build a model from model_cls in order to test abstract models. + Note that this does not actually create a table in the database, so + there may be some unidentified edge cases. + Args: + model_cls (openerp.models.BaseModel): Class of model to initialize + Returns: + model_cls: Instance + """ + registry = cls.env.registry + cr = cls.env.cr + inst = model_cls._build_model(registry, cr) + model = cls.env[model_cls._inherit].with_context(todo=[]) + model._prepare_setup() + model._setup_base(partial=False) + model._setup_fields() + model._setup_complete() + model._auto_init() + # model.init() + model._auto_end() + return inst + + @classmethod + def setUpClass(cls): + super(TestResPartner, cls).setUpClass() + cls.env.registry.enter_test_mode() + cls._init_test_model(ResPartner) + + @classmethod + def tearDownClass(cls): + cls.env.registry.leave_test_mode() + super(TestResPartner, cls).tearDownClass() + + def setUp(self): + super(TestResPartner, self).setUp() + bad_cat = self.env['res.partner.id_category'].create({ + 'code': 'another_code', + 'name': 'another_name', + }) + self.env['res.partner.id_number'].create({ + 'name': 'Bad ID', + 'category_id': bad_cat.id, + 'partner_id': self.env.user.partner_id.id, + }) + self.partner_id_category = self.env['res.partner.id_category'].create({ + 'code': 'id_code', + 'name': 'id_name', + }) + self.partner = self.env.user.partner_id + self.partner_id = self.env['res.partner.id_number'].create({ + 'name': 'Good ID', + 'category_id': self.partner_id_category.id, + 'partner_id': self.partner.id, + }) + + def test_compute_identification(self): + """ It should set the proper field to the proper ID name. """ + self.partner._compute_identification('name', 'id_code') + self.assertEqual(self.partner.name, self.partner_id.name) + + def test_inverse_identification_saves(self): + """ It should set the ID name to the proper field value. """ + self.partner._inverse_identification('name', 'id_code') + self.assertEqual(self.partner_id.name, self.partner.name) + + def test_inverse_identification_creates_new_category(self): + """ It should create a new category of the type if non-existent. """ + self.partner._inverse_identification('name', 'new_code_type') + category = self.env['res.partner.id_category'].search([ + ('code', '=', 'new_code_type'), + ]) + self.assertTrue(category) + + def test_inverse_identification_creates_new_id(self): + """ It should create a new ID of the type if non-existent. """ + category = self.env['res.partner.id_category'].create({ + 'code': 'new_code_type', + 'name': 'new_code_type', + }) + self.partner._inverse_identification('name', 'new_code_type') + identification = self.env['res.partner.id_number'].search([ + ('category_id', '=', category.id), + ('partner_id', '=', self.partner.id), + ]) + self.assertEqual(identification.name, self.partner.name) + + def test_inverse_identification_multi_exception(self): + """ It should not allow a write when multiple IDs of same type. """ + self.env['res.partner.id_number'].create({ + 'name': 'Another ID', + 'category_id': self.partner_id_category.id, + 'partner_id': self.partner.id, + }) + with self.assertRaises(ValidationError): + self.partner._inverse_identification('name', 'id_code') + + def test_search_identification(self): + """ It should return the right record when searched by ID. """ + self.partner.social_security = 'Test' + partner = self.env['res.partner'].search([ + ('social_security', '=', 'Test'), + ]) + self.assertEqual(partner, self.partner)