Browse Source

Merge pull request #612 from pankk/8.0-partner-identification-backport

[8.0][MIG] partner_identification backport
pull/420/merge
Pedro M. Baeza 7 years ago
committed by GitHub
parent
commit
79cfc588a8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      partner_identification/README.rst
  2. 7
      partner_identification/__openerp__.py
  3. 161
      partner_identification/models/res_partner.py
  4. 2
      partner_identification/models/res_partner_id_category.py
  5. 2
      partner_identification/tests/__init__.py
  6. 22
      partner_identification/tests/test_partner_identification.py
  7. 131
      partner_identification/tests/test_res_partner.py

17
partner_identification/README.rst

@ -15,7 +15,7 @@ and vary from country to country.
* Fiscal ID's * Fiscal ID's
* Membership numbers * Membership numbers
* Driver license * Driver license
* ...
* etc
Installation Installation
@ -35,8 +35,15 @@ Name:
Code: Code:
Code, abbreviation or acronym of this ID type. For example, 'driver_license' Code, abbreviation or acronym of this ID type. For example, 'driver_license'
Python validation code: 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 Usage
===== =====
@ -63,7 +70,7 @@ Notes:
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot :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 Known issues / Roadmap
@ -97,7 +104,9 @@ Contributors
* Ferdinand Gassauer <office@chrcar.at> * Ferdinand Gassauer <office@chrcar.at>
* Gerhard Könighofer <gerhard.koenighofer@swing-system.com> * Gerhard Könighofer <gerhard.koenighofer@swing-system.com>
* Laurent Mignon <laurent.mignon@acsone.eu> * Laurent Mignon <laurent.mignon@acsone.eu>
* Yajo <Yajo@users.noreply.github.com>
* Jairo Llopis <jairo.llopis@tecnativa.com>
* Dave Lasley <dave@laslabs.com>
* Kevin Graveman <k.graveman@onestein.nl>
Maintainer Maintainer
---------- ----------

7
partner_identification/__openerp__.py

@ -11,7 +11,7 @@
{ {
'name': 'Partner Identification Numbers', 'name': 'Partner Identification Numbers',
'category': 'Customer Relationship Management', 'category': 'Customer Relationship Management',
'version': '8.0.1.0.0',
'version': '8.0.1.1.1',
'data': [ 'data': [
'views/res_partner_id_category_view.xml', 'views/res_partner_id_category_view.xml',
'views/res_partner_id_number_view.xml', 'views/res_partner_id_number_view.xml',
@ -19,10 +19,11 @@
'security/ir.model.access.csv', 'security/ir.model.access.csv',
], ],
'author': 'ChriCar Beteiligungs- und Beratungs- GmbH, ' 'author': 'ChriCar Beteiligungs- und Beratungs- GmbH, '
'Antiun Ingeniería S.L.',
'Tecnativa,'
'Camptocamp,' 'Camptocamp,'
'ACSONE SA/NV,' 'ACSONE SA/NV,'
'Odoo Community Association (OCA)'
'LasLabs,'
'Odoo Community Association (OCA)',
'website': 'https://odoo-community.org/', 'website': 'https://odoo-community.org/',
'license': 'AGPL-3', 'license': 'AGPL-3',
'installable': True, 'installable': True,

161
partner_identification/models/res_partner.py

@ -8,12 +8,165 @@
# Antonio Espinosa <antonioea@antiun.com> # Antonio Espinosa <antonioea@antiun.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # 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): class ResPartner(models.Model):
_inherit = 'res.partner'
_inherit = 'res.partner' # pylint: disable=R7980
id_numbers = fields.One2many( 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),
]

2
partner_identification/models/res_partner_id_category.py

@ -55,6 +55,8 @@ class ResPartnerIdCategory(models.Model):
python validation code fails python validation code fails
""" """
self.ensure_one() self.ensure_one()
if self.env.context.get('id_no_validate'):
return
eval_context = self._validation_eval_context(id_number) eval_context = self._validation_eval_context(id_number)
try: try:
safe_eval(self.validation_code, safe_eval(self.validation_code,

2
partner_identification/tests/__init__.py

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import test_partner_identification from . import test_partner_identification
from . import test_res_partner

22
partner_identification/tests/test_partner_identification.py

@ -2,7 +2,7 @@
# © 2016 ACSONE SA/NV (<http://acsone.eu>) # © 2016 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from psycopg2._psycopg import IntegrityError from psycopg2._psycopg import IntegrityError
import openerp.tests.common as common
from openerp.tests import common
from openerp.exceptions import ValidationError from openerp.exceptions import ValidationError
@ -75,7 +75,7 @@ if id_number.name != '1235':
'category_id': partner_id_category2.id '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({ partner_id_category = self.env['res.partner.id_category'].create({
'code': 'id_code', 'code': 'id_code',
'name': 'id_name', 'name': 'id_name',
@ -90,3 +90,21 @@ if id_number.name != '1234' # missing :
'name': '1234', 'name': '1234',
'category_id': partner_id_category.id '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
})]})

131
partner_identification/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)
Loading…
Cancel
Save