From da92d94ecd5bfcf30a8bbf22592879605807fb0b Mon Sep 17 00:00:00 2001 From: Dave Lasley Date: Tue, 30 May 2017 02:16:20 -0700 Subject: [PATCH] [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 --- partner_identification/__openerp__.py | 3 +- partner_identification/models/res_partner.py | 157 +++++++++++++++++- .../models/res_partner_id_category.py | 7 +- .../models/res_partner_id_number.py | 2 +- partner_identification/tests/__init__.py | 2 + .../tests/test_partner_identification.py | 4 +- .../tests/test_res_partner.py | 126 ++++++++++++++ 7 files changed, 290 insertions(+), 11 deletions(-) create mode 100644 partner_identification/tests/test_res_partner.py diff --git a/partner_identification/__openerp__.py b/partner_identification/__openerp__.py index dcabbd38a..9567bc487 100644 --- a/partner_identification/__openerp__.py +++ b/partner_identification/__openerp__.py @@ -11,7 +11,7 @@ { 'name': 'Partner Identification Numbers', 'category': 'Customer Relationship Management', - 'version': '10.0.1.0.1', + 'version': '10.0.1.1.0', 'depends': [ 'sales_team', ], @@ -25,6 +25,7 @@ 'Tecnativa,' 'Camptocamp,' 'ACSONE SA/NV,' + 'LasLabs,' 'Odoo Community Association (OCA)', 'website': 'https://odoo-community.org/', 'license': 'AGPL-3', diff --git a/partner_identification/models/res_partner.py b/partner_identification/models/res_partner.py index 63b8fcd42..d4dcf2086 100644 --- a/partner_identification/models/res_partner.py +++ b/partner_identification/models/res_partner.py @@ -8,12 +8,163 @@ # Antonio Espinosa # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp import models, fields +from odoo import api, models, fields, _ +from odoo.exceptions import ValidationError class ResPartner(models.Model): _inherit = 'res.partner' 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( + 'social_security', '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( + 'social_security', '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, field_name, 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( + 'social_security', 'SSN', *a + ), + ) + + Args: + field_name (str): Name of field to set. + category_code (str): Category code of the Identification type. + + Returns: + list: Domain to search with. + """ + + return [ + (field_name, operator, value), + ('category_id.code', '=', category_code), + ] diff --git a/partner_identification/models/res_partner_id_category.py b/partner_identification/models/res_partner_id_category.py index 7b000f895..09a8e660d 100644 --- a/partner_identification/models/res_partner_id_category.py +++ b/partner_identification/models/res_partner_id_category.py @@ -10,10 +10,9 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp import api, models, fields -from openerp.exceptions import ValidationError, UserError -from openerp.tools.safe_eval import safe_eval -from openerp.tools.translate import _ +from odoo import api, models, fields, _ +from odoo.exceptions import ValidationError, UserError +from odoo.tools.safe_eval import safe_eval class ResPartnerIdCategory(models.Model): diff --git a/partner_identification/models/res_partner_id_number.py b/partner_identification/models/res_partner_id_number.py index a8859cec7..8138116ec 100644 --- a/partner_identification/models/res_partner_id_number.py +++ b/partner_identification/models/res_partner_id_number.py @@ -9,7 +9,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp import api, models, fields +from odoo import api, models, fields class ResPartnerIdNumber(models.Model): diff --git a/partner_identification/tests/__init__.py b/partner_identification/tests/__init__.py index f848b5bcf..dd49771a4 100644 --- a/partner_identification/tests/__init__.py +++ b/partner_identification/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/partner_identification/tests/test_partner_identification.py b/partner_identification/tests/test_partner_identification.py index 5451b5f25..eb3945dec 100644 --- a/partner_identification/tests/test_partner_identification.py +++ b/partner_identification/tests/test_partner_identification.py @@ -2,8 +2,8 @@ # © 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.exceptions import ValidationError +from odoo.tests import common +from odoo.exceptions import ValidationError class TestPartnerIdentificationBase(common.TransactionCase): diff --git a/partner_identification/tests/test_res_partner.py b/partner_identification/tests/test_res_partner.py new file mode 100644 index 000000000..15e455f57 --- /dev/null +++ b/partner_identification/tests/test_res_partner.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models +from odoo.tests import common +from odoo.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( + 'social_security', '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(partial=False) + 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) + + 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)