Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[10.0][IMP] partner_identification: Add field computation and inverses #419

Merged
merged 2 commits into from
May 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion partner_identification/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
Expand All @@ -25,6 +25,7 @@
'Tecnativa,'
'Camptocamp,'
'ACSONE SA/NV,'
'LasLabs,'
'Odoo Community Association (OCA)',
'website': 'https://odoo-community.org/',
'license': 'AGPL-3',
Expand Down
157 changes: 154 additions & 3 deletions partner_identification/models/res_partner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
]
7 changes: 3 additions & 4 deletions partner_identification/models/res_partner_id_category.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion partner_identification/models/res_partner_id_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions partner_identification/tests/__init__.py
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
4 changes: 2 additions & 2 deletions partner_identification/tests/test_partner_identification.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# © 2016 ACSONE SA/NV (<http://acsone.eu>)
# 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):
Expand Down
126 changes: 126 additions & 0 deletions partner_identification/tests/test_res_partner.py
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)