forked from OCA/partner-contact
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[10.0][IMP] partner_identification: Add field computation and inverses (
OCA#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
- Loading branch information
1 parent
e251ed2
commit 2d0a67e
Showing
7 changed files
with
290 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,12 +8,163 @@ | |
# Antonio Espinosa <[email protected]> | ||
# 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), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |