diff --git a/partner_second_lastname/README.rst b/partner_second_lastname/README.rst index a7e59b726..11c1229f7 100644 --- a/partner_second_lastname/README.rst +++ b/partner_second_lastname/README.rst @@ -1,6 +1,8 @@ .. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 +======================= Partner second lastname ======================= @@ -12,6 +14,26 @@ In some countries, it's important to have a second last name for contacts. Contact partners will need to fulfill at least one of the name fields (*First name*, *First last name* or *Second last name*). +Configuration +============= + +You can configure some common name patterns for the inverse function +in Settings > Configuration > General settings: + +* Lastname SecondLastname Firstname: For example 'Anderson Lavarge Robert' +* Lastname SecondLastname, Firstname: For example 'Anderson Lavarge, Robert' +* Firstname Lastname SecondLastname: For example 'Robert Anderson Lavarge' + +After applying the changes, you can recalculate all partners name clicking +"Recalculate names" button. Note: This process could take so much time depending +how many partners there are in database. + +You can use *_get_inverse_name* method to get firstname, lastname and +second lastname from a simple string and also *_get_computed_name* to get a +name form the firstname, lastname and second lastname. +These methods can be overridden to change the format specified above. + + Usage ===== @@ -24,11 +46,7 @@ To use this module, you need to: If you directly enter the full name instead of entering the other fields separately (maybe from other form), this module will try to guess the best match for your input and split it between firstname, lastname and second -lastname. - -If the name you enter is in the form *Firstname Lastname1 Lastname2*, it will -be split as such. If you use a comma, it will understand it as *Lastname1 -Lastname2, Firstname*. +lastname using an inverse function. If you can, always enter it manually please. Automatic guessing could fail for you easily in some corner cases. @@ -37,6 +55,15 @@ you easily in some corner cases. :alt: Try me on Runbot :target: https://runbot.odoo-community.org/runbot/134/8.0 + +Known issues / Roadmap +====================== + +Patterns for the inverse function are configurable only at system level. Maybe +this configuration could depend on partner language, country or company, +as discussed at `this OCA issue `_ + + Bug Tracker =========== @@ -55,6 +82,8 @@ Contributors * `Grupo ESOC `_: * `Jairo Llopis `_. +* `Antiun Ingeniería S.L. `_: + * `Antonio Espinosa `_. Maintainer ---------- diff --git a/partner_second_lastname/__openerp__.py b/partner_second_lastname/__openerp__.py index 128ce3e33..4acde00b1 100644 --- a/partner_second_lastname/__openerp__.py +++ b/partner_second_lastname/__openerp__.py @@ -4,12 +4,14 @@ { "name": "Partner second last name", - "version": "8.0.4.0.0", - "author": "Grupo ESOC, Odoo Community Association (OCA)", + "summary": "Have split first and second lastnames", + "version": "8.0.4.1.0", "license": "AGPL-3", + "website": "https://grupoesoc.es", + "author": "Grupo ESOC Ingeniería de Servicios, " + "Odoo Community Association (OCA)", "maintainer": "Odoo Community Association (OCA)", "category": "Extra Tools", - "website": "http://www.grupoesoc.es", "depends": [ "partner_firstname" ], @@ -18,4 +20,5 @@ "views/res_user.xml", ], "installable": True, + 'images': [], } diff --git a/partner_second_lastname/models.py b/partner_second_lastname/models.py deleted file mode 100644 index d2b48741c..000000000 --- a/partner_second_lastname/models.py +++ /dev/null @@ -1,97 +0,0 @@ -# -*- coding: utf-8 -*- -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -# © 2015 Grupo ESOC Ingeniería de Servicios, S.L.U. - -from openerp import api, fields, models -from openerp.addons.partner_firstname import exceptions - - -class ResPartner(models.Model): - """Adds a second last name.""" - - _inherit = "res.partner" - - lastname2 = fields.Char("Second last name") - - @api.model - def _get_computed_name(self, lastname, firstname, lastname2=None): - """Compute the name combined with the second lastname too. - - We have 2 lastnames, so lastnames and firstname will be separated by a - comma. - """ - names = list() - - if lastname: - names.append(lastname) - if lastname2: - names.append(lastname2) - if names and firstname: - names[-1] = names[-1] + "," - if firstname: - names.append(firstname) - - return u" ".join(names) - - @api.one - @api.depends("firstname", "lastname", "lastname2") - def _compute_name(self): - """Write :attr:`~.name` according to splitted data.""" - self.name = self._get_computed_name(self.lastname, - self.firstname, - self.lastname2) - - @api.one - def _inverse_name(self): - """Try to revert the effect of :meth:`._compute_name`.""" - parts = self._get_inverse_name(self.name, self.is_company) - - # Avoid to hit :meth:`~._check_name` with all 3 fields being ``False`` - before, after = dict(), dict() - for key, value in parts.iteritems(): - (before if value else after)[key] = value - self.update(before) - self.update(after) - - @api.model - def _get_inverse_name(self, name, is_company=False): - """Compute the inverted name. - - - If the partner is a company, save it in the lastname. - - Otherwise, make a guess. - """ - # Company name goes to the lastname - if is_company or not name: - parts = [False, name or False, False] - - # The comma separates the firstname - elif "," in name: - lastnames, firstname = name.split(",", 1) - parts = [firstname.strip()] + lastnames.split(" ", 1) - - # Without comma, the user wrote the firstname first - else: - parts = name.split(" ", 2) - - while len(parts) < 3: - parts.append(False) - - return {"firstname": parts[0], - "lastname": parts[1], - "lastname2": parts[2]} - - @api.one - @api.constrains("firstname", "lastname", "lastname2") - def _check_name(self): - """Ensure at least one name is set.""" - try: - super(ResPartner, self)._check_name() - except exceptions.EmptyNamesError as error: - if not self.lastname2: - raise error - - @api.one - @api.onchange("firstname", "lastname", "lastname2") - def _onchange_subnames(self): - """Trigger onchange with :attr:`~.lastname2` too.""" - super(ResPartner, self)._onchange_subnames() diff --git a/partner_second_lastname/models/__init__.py b/partner_second_lastname/models/__init__.py new file mode 100644 index 000000000..aa04cb891 --- /dev/null +++ b/partner_second_lastname/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import base_config_settings +from . import res_partner diff --git a/partner_second_lastname/models/base_config_settings.py b/partner_second_lastname/models/base_config_settings.py new file mode 100644 index 000000000..f11d8ca57 --- /dev/null +++ b/partner_second_lastname/models/base_config_settings.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp import models, api + + +class BaseConfigSettings(models.TransientModel): + _inherit = 'base.config.settings' + + def _partner_names_order_selection(self): + options = super( + BaseConfigSettings, self)._partner_names_order_selection() + new_labels = { + 'last_first': 'Lastname SecondLastname Firstname', + 'last_first_comma': 'Lastname SecondLastname, Firstname', + 'first_last': 'Firstname Lastname SecondLastname', + } + return [(k, new_labels[k]) if k in new_labels else (k, v) + for k, v in options] + + @api.multi + def _partners_for_recalculating(self): + return self.env['res.partner'].search([ + ('is_company', '=', False), + '|', '&', ('firstname', '!=', False), ('lastname', '!=', False), + '|', '&', ('firstname', '!=', False), ('lastname2', '!=', False), + '&', ('lastname', '!=', False), ('lastname2', '!=', False), + ]) diff --git a/partner_second_lastname/models/res_partner.py b/partner_second_lastname/models/res_partner.py new file mode 100644 index 000000000..8d4640dfa --- /dev/null +++ b/partner_second_lastname/models/res_partner.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +# © 2015 Grupo ESOC Ingeniería de Servicios, S.L.U. +# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa + +from openerp import api, fields, models +from openerp.addons.partner_firstname.models import exceptions + + +class ResPartner(models.Model): + """Adds a second last name.""" + + _inherit = "res.partner" + + lastname2 = fields.Char("Second last name", oldname="lastname_second") + + @api.model + def _get_computed_name(self, lastname, firstname, lastname2=None): + """Compute the name combined with the second lastname too. + + We have 2 lastnames, so lastnames and firstname will be separated by a + comma. + """ + order = self._get_names_order() + names = list() + if order == 'first_last': + if firstname: + names.append(firstname) + if lastname: + names.append(lastname) + if lastname2: + names.append(lastname2) + else: + if lastname: + names.append(lastname) + if lastname2: + names.append(lastname2) + if names and firstname and order == 'last_first_comma': + names[-1] = names[-1] + "," + if firstname: + names.append(firstname) + return u" ".join(names) + + @api.one + @api.depends("firstname", "lastname", "lastname2") + def _compute_name(self): + """Write :attr:`~.name` according to splitted data.""" + self.name = self._get_computed_name( + self.lastname, self.firstname, self.lastname2) + + @api.one + def _inverse_name(self): + """Try to revert the effect of :meth:`._compute_name`.""" + parts = self._get_inverse_name(self.name, self.is_company) + + # Avoid to hit :meth:`~._check_name` with all 3 fields being ``False`` + before, after = dict(), dict() + for key, value in parts.iteritems(): + (before if value else after)[key] = value + if any([before[k] != self[k] for k in before.keys()]): + self.update(before) + if any([after[k] != self[k] for k in after.keys()]): + self.update(after) + + @api.model + def _get_inverse_name(self, name, is_company=False): + """Compute the inverted name. + + - If the partner is a company, save it in the lastname. + - Otherwise, make a guess. + """ + # Company name goes to the lastname + result = { + 'firstname': False, + 'lastname': name or False, + 'lastname2': False, + } + if not is_company and name: + order = self._get_names_order() + result = super(ResPartner, self)._get_inverse_name( + name, is_company) + parts = [] + if order == 'last_first': + if result['firstname']: + parts = result['firstname'].split(" ", 1) + while len(parts) < 2: + parts.append(False) + result['lastname2'] = parts[0] + result['firstname'] = parts[1] + else: + if result['lastname']: + parts = result['lastname'].split(" ", 1) + while len(parts) < 2: + parts.append(False) + result['lastname'] = parts[0] + result['lastname2'] = parts[1] + return result + + @api.one + @api.constrains("firstname", "lastname", "lastname2") + def _check_name(self): + """Ensure at least one name is set.""" + try: + super(ResPartner, self)._check_name() + except exceptions.EmptyNamesError as error: + if not self.lastname2: + raise error + + @api.one + @api.onchange("firstname", "lastname", "lastname2") + def _onchange_subnames(self): + """Trigger onchange with :attr:`~.lastname2` too.""" + super(ResPartner, self)._onchange_subnames() diff --git a/partner_second_lastname/tests/test_name.py b/partner_second_lastname/tests/test_name.py index ba97e467d..92c963668 100644 --- a/partner_second_lastname/tests/test_name.py +++ b/partner_second_lastname/tests/test_name.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # © 2015 Grupo ESOC Ingeniería de Servicios, S.L.U. +# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa from openerp.tests.common import TransactionCase from openerp.addons.partner_firstname.tests.base import MailInstalled @@ -8,6 +9,11 @@ from openerp.addons.partner_firstname.tests.base import MailInstalled class CompanyCase(TransactionCase): """Test ``res.partner`` when it is a company.""" + def setUp(self): + super(CompanyCase, self).setUp() + self.env['ir.config_parameter'].set_param( + 'partner_names_order', 'first_last') + def tearDown(self): try: new = self.env["res.partner"].create({ @@ -75,6 +81,8 @@ class PersonCase(TransactionCase): def setUp(self): super(PersonCase, self).setUp() + self.env['ir.config_parameter'].set_param( + 'partner_names_order', 'last_first_comma') self.firstname = u"Fírstname" self.lastname = u"Làstname1" @@ -113,6 +121,9 @@ class PersonCase(TransactionCase): def test_firstname_first(self): """Create a person setting his first name first.""" + self.env['ir.config_parameter'].set_param( + 'partner_names_order', 'first_last') + self.template = "%(first)s %(last1)s %(last2)s" self.params = { "is_company": False, "name": "%s %s %s" % (self.firstname, @@ -121,7 +132,7 @@ class PersonCase(TransactionCase): } def test_firstname_last(self): - """Create a persong setting his first name last.""" + """Create a person setting his first name last.""" self.params = { "is_company": False, "name": "%s %s, %s" % (self.lastname, @@ -130,25 +141,29 @@ class PersonCase(TransactionCase): } def test_firstname_only(self): - """Create a persong setting his first name only.""" - self.lastname = self.lastname2 = False - self.template = "%(first)s" + """Create a person setting his first name only.""" + self.env['ir.config_parameter'].set_param( + 'partner_names_order', 'first_last') + self.firstname = self.lastname2 = False + self.template = "%(last1)s" self.params = { "is_company": False, - "name": self.firstname, + "name": self.lastname, } def test_firstname_lastname_only(self): - """Create a persong setting his first name and last name 1 only.""" + """Create a person setting his first name and last name 1 only.""" + self.env['ir.config_parameter'].set_param( + 'partner_names_order', 'first_last') self.lastname2 = False - self.template = "%(last1)s, %(first)s" + self.template = "%(first)s %(last1)s" self.params = { "is_company": False, "name": "%s %s" % (self.firstname, self.lastname), } def test_lastname_firstname_only(self): - """Create a persong setting his last name 1 and first name only.""" + """Create a person setting his last name 1 and first name only.""" self.lastname2 = False self.template = "%(last1)s, %(first)s" self.params = { diff --git a/partner_second_lastname/tests/test_onchange.py b/partner_second_lastname/tests/test_onchange.py index fdf71a424..4d6ae367c 100644 --- a/partner_second_lastname/tests/test_onchange.py +++ b/partner_second_lastname/tests/test_onchange.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # © 2015 Grupo ESOC Ingeniería de Servicios, S.L.U. +# © 2015 Antiun Ingenieria S.L. - Antonio Espinosa + """These tests try to mimic the behavior of the UI form. The form operates in onchange mode, with its limitations. @@ -12,6 +14,11 @@ from openerp.tests.common import TransactionCase class OnChangeCase(TransactionCase): is_company = False + def setUp(self): + super(OnChangeCase, self).setUp() + self.env['ir.config_parameter'].set_param( + 'partner_names_order', 'last_first_comma') + def new_partner(self): """Create an empty partner. Ensure it is (or not) a company.""" new = self.env["res.partner"].new()