diff --git a/partner_firstname/README.rst b/partner_firstname/README.rst new file mode 100644 index 000000000..19a14dc6b --- /dev/null +++ b/partner_firstname/README.rst @@ -0,0 +1,56 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :alt: License: AGPL-3 + +Partner first name and last name +================================ + +This module was written to extend the functionality of contacts to support +having separate last name and first name. + +Usage +===== + +The field *name* becomes a stored function field concatenating the *last name* +and the *first name*. This avoids breaking compatibility with other modules. + +Users should fulfill manually the separate fields for *last name* and *first +name*, but in case you edit just the *name* field in some unexpected module, +there is an inverse function that tries to split that automatically. It assumes +that you write the *name* in format *"Lastname Firstname"*, but it could lead to +wrong splitting (because it's just blindly trying to guess what you meant), so +you better specify it manually. + +For the same reason, after installing, previous names for contacts will stay in +the *name* field, and the first time you edit any of them you will be asked to +supply the *last name* and *first name* (just once per contact). + +For further information, please visit: + +* https://www.odoo.com/forum/help-1 + +Credits +======= + +Contributors +------------ + +* Nicolas Bessi +* Jonathan Nemry +* Olivier Laurent +* Hans Henrik Gabelgaard +* Jairo Llopis + +Maintainer +---------- + +.. image:: http://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: http://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. diff --git a/partner_firstname/__init__.py b/partner_firstname/__init__.py index bb6fb792a..4cdd79b3f 100644 --- a/partner_firstname/__init__.py +++ b/partner_firstname/__init__.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- -############################################################################## -# + # Author: Nicolas Bessi. Copyright Camptocamp SA +# Copyright (C) +# 2014: Agile Business Group () +# 2015: Grupo ESOC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -15,8 +17,5 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -# -############################################################################## -from . import partner -from . import res_user +from . import models diff --git a/partner_firstname/__openerp__.py b/partner_firstname/__openerp__.py index c407ab7d3..dd2c7d77c 100644 --- a/partner_firstname/__openerp__.py +++ b/partner_firstname/__openerp__.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- -############################################################################## -# + # Author: Nicolas Bessi. Copyright Camptocamp SA +# Copyright (C) +# 2014: Agile Business Group () +# 2015: Grupo ESOC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -15,35 +17,20 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -# -############################################################################## { - 'name': 'Partner first name, last name', - 'description': """ -This module splits first name and last name for non company partners -==================================================================== - -The field 'name' becomes a stored function field concatenating lastname and -firstname -Note: in version 7.0, installing this module invalidates a yaml test in the -'edi' module - -Contributors -============ -Jonathan Nemry -Olivier Laurent - -""", - 'version': '1.2', + 'name': 'Partner first name and last name', + 'summary': "Split first name and last name for non company partners", + 'version': '2.0', 'author': "Camptocamp,Odoo Community Association (OCA)", 'maintainer': 'Camptocamp, Acsone', 'category': 'Extra Tools', 'website': 'http://www.camptocamp.com, http://www.acsone.eu', 'depends': ['base'], 'data': [ - 'partner_view.xml', - 'res_user_view.xml', + 'views/res_partner.xml', + 'views/res_user.xml', + 'data/res_partner.yml', ], 'demo': [], 'test': [], diff --git a/partner_firstname/data/res_partner.yml b/partner_firstname/data/res_partner.yml new file mode 100644 index 000000000..a88e01841 --- /dev/null +++ b/partner_firstname/data/res_partner.yml @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) +# 2015: Grupo ESOC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +- !function {model: res.partner, name: _install_partner_firstname} diff --git a/partner_firstname/exceptions.py b/partner_firstname/exceptions.py new file mode 100644 index 000000000..99d5b630e --- /dev/null +++ b/partner_firstname/exceptions.py @@ -0,0 +1,27 @@ +# -*- encoding: utf-8 -*- + +# Odoo, Open Source Management Solution +# Copyright (C) 2014-2015 Grupo ESOC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from openerp import _, exceptions + + +class EmptyNamesError(exceptions.ValidationError): + def __init__(self, record, value=_("No name is set.")): + self.record = record + self._value = value + self.name = _("Error(s) with partner %d's name.") % record.id + self.args = (self.name, value) diff --git a/partner_firstname/i18n/es.po b/partner_firstname/i18n/es.po index 2a1b52af7..43411dd67 100644 --- a/partner_firstname/i18n/es.po +++ b/partner_firstname/i18n/es.po @@ -6,48 +6,54 @@ msgid "" msgstr "" "Project-Id-Version: OpenERP Server 7.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-12-23 14:09+0000\n" -"PO-Revision-Date: 2014-12-23 14:09+0000\n" -"Last-Translator: <>\n" +"POT-Creation-Date: 2015-03-30 07:53+0000\n" +"PO-Revision-Date: 2015-03-30 10:01+0100\n" +"Last-Translator: Jairo Llopis \n" "Language-Team: \n" +"Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" +"Content-Transfer-Encoding: 8bit\n" "Plural-Forms: \n" +"X-Generator: Poedit 1.7.5\n" #. module: partner_firstname -#: code:addons/partner_firstname/partner.py:81 -#: code:addons/partner_firstname/res_user.py:35 -#: code:addons/partner_firstname/tests/test_partner_firstname.py:56 -#: code:addons/partner_firstname/tests/test_partner_firstname.py:57 -#: code:addons/partner_firstname/tests/test_partner_firstname.py:71 -#: code:addons/partner_firstname/tests/test_partner_firstname.py:72 +#: code:addons/partner_firstname/exceptions.py:26 #, python-format -msgid "%s (copy)" -msgstr "%s (copia)" - -#. module: partner_firstname -#: model:ir.model,name:partner_firstname.model_res_users -msgid "Users" -msgstr "Usuarios" +msgid "Error(s) with partner %d's name." +msgstr "Errores con el nombre de la empresa %d." #. module: partner_firstname #: field:res.partner,firstname:0 -msgid "Firstname" +msgid "First name" msgstr "Nombre" +#. module: partner_firstname +#: view:res.partner:partner_firstname.partner_form +msgid "Is a Company?" +msgstr "¿Es una empresa?" + #. module: partner_firstname #: field:res.partner,lastname:0 -msgid "Lastname" -msgstr "Apellidos" +msgid "Last name" +msgstr "Apellido" #. module: partner_firstname -#: view:res.partner:0 -msgid "Is a Company?" -msgstr "¿Es una empresa?" +#: code:addons/partner_firstname/exceptions.py:40 +#, python-format +msgid "No name is set." +msgstr "No se ha establecido ningún nombre." #. module: partner_firstname #: model:ir.model,name:partner_firstname.model_res_partner msgid "Partner" msgstr "Empresa" +#~ msgid "True" +#~ msgstr "Verdadero" + +#~ msgid "%s (copy)" +#~ msgstr "%s (copia)" + +#~ msgid "Users" +#~ msgstr "Usuarios" diff --git a/partner_firstname/i18n/partner_firstname.pot b/partner_firstname/i18n/partner_firstname.pot index ab75c787a..96d5a4925 100644 --- a/partner_firstname/i18n/partner_firstname.pot +++ b/partner_firstname/i18n/partner_firstname.pot @@ -1,13 +1,13 @@ -# Translation of OpenERP Server. +# Translation of Odoo Server. # This file contains the translation of the following modules: # * partner_firstname # msgid "" msgstr "" -"Project-Id-Version: OpenERP Server 7.0\n" +"Project-Id-Version: Odoo Server 8.0-20150327\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-01-22 14:09+0000\n" -"PO-Revision-Date: 2014-01-22 14:09+0000\n" +"POT-Creation-Date: 2015-03-30 07:53+0000\n" +"PO-Revision-Date: 2015-03-30 07:53+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -16,38 +16,33 @@ msgstr "" "Plural-Forms: \n" #. module: partner_firstname -#: code:addons/partner_firstname/partner.py:81 -#: code:addons/partner_firstname/res_user.py:35 -#: code:addons/partner_firstname/tests/test_partner_firstname.py:56 -#: code:addons/partner_firstname/tests/test_partner_firstname.py:57 -#: code:addons/partner_firstname/tests/test_partner_firstname.py:71 -#: code:addons/partner_firstname/tests/test_partner_firstname.py:72 +#: code:addons/partner_firstname/exceptions.py:26 #, python-format -msgid "%s (copy)" +msgid "Error(s) with partner %d's name." msgstr "" #. module: partner_firstname -#: model:ir.model,name:partner_firstname.model_res_users -msgid "Users" +#: field:res.partner,firstname:0 +msgid "First name" msgstr "" #. module: partner_firstname -#: field:res.partner,firstname:0 -msgid "Firstname" +#: view:res.partner:partner_firstname.partner_form +msgid "Is a Company?" msgstr "" #. module: partner_firstname #: field:res.partner,lastname:0 -msgid "Lastname" +msgid "Last name" msgstr "" #. module: partner_firstname -#: view:res.partner:0 -msgid "Is a Company?" +#: code:addons/partner_firstname/exceptions.py:40 +#, python-format +msgid "No name is set." msgstr "" #. module: partner_firstname #: model:ir.model,name:partner_firstname.model_res_partner msgid "Partner" msgstr "" - diff --git a/partner_firstname/models.py b/partner_firstname/models.py new file mode 100644 index 000000000..e33196af7 --- /dev/null +++ b/partner_firstname/models.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +# Author: Nicolas Bessi. Copyright Camptocamp SA +# Copyright (C) +# 2014: Agile Business Group () +# 2015: Grupo ESOC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import logging +from openerp import api, fields, models +from . import exceptions + + +_logger = logging.getLogger(__name__) + + +class ResPartner(models.Model): + """Adds last name and first name; name becomes a stored function field.""" + _inherit = 'res.partner' + + firstname = fields.Char("First name") + lastname = fields.Char("Last name") + name = fields.Char( + compute="_compute_name", + inverse="_inverse_name_after_cleaning_whitespace", + required=False, + store=True) + + @api.one + @api.depends("firstname", "lastname") + def _compute_name(self): + """Write the 'name' field according to splitted data.""" + self.name = u" ".join((p for p in (self.lastname, + self.firstname) if p)) + + @api.one + def _inverse_name_after_cleaning_whitespace(self): + """Clean whitespace in :attr:`~.name` and split it. + + Removes leading, trailing and duplicated whitespace. + + The splitting logic is stored separately in :meth:`~._inverse_name`, so + submodules can extend that method and get whitespace cleaning for free. + """ + # Remove unneeded whitespace + clean = u" ".join(self.name.split(None)) if self.name else self.name + + # Clean name avoiding infinite recursion + if self.name != clean: + self.name = clean + + # Save name in the real fields + else: + self._inverse_name() + + @api.one + def _inverse_name(self): + """Try to revert the effect of :meth:`._compute_name`. + + - If the partner is a company, save it in the lastname. + - Otherwise, make a guess. + + This method can be easily overriden by other submodules. + + When this method is called, :attr:`~.name` already has unified and + trimmed whitespace. + """ + # Company name goes to the lastname + if self.is_company or not self.name: + parts = [self.name or False, False] + + # Guess name splitting + else: + parts = self.name.split(" ", 1) + while len(parts) < 2: + parts.append(False) + + self.lastname, self.firstname = parts + + @api.one + @api.constrains("firstname", "lastname") + def _check_name(self): + """Ensure at least one name is set.""" + if not (self.firstname or self.lastname): + raise exceptions.EmptyNamesError(self) + + @api.one + @api.onchange("firstname", "lastname") + def _onchange_subnames(self): + """Avoid recursion when the user changes one of these fields. + + This forces to skip the :attr:`~.name` inversion when the user is + setting it in a not-inverted way. + """ + # Modify self's context without creating a new Environment. + # See https://github.com/odoo/odoo/issues/7472#issuecomment-119503916. + self.env.context = self.with_context(skip_onchange=True).env.context + + @api.one + @api.onchange("name") + def _onchange_name(self): + """Ensure :attr:`~.name` is inverted in the UI.""" + if self.env.context.get("skip_onchange"): + # Do not skip next onchange + self.env.context = ( + self.with_context(skip_onchange=False).env.context) + else: + self._inverse_name_after_cleaning_whitespace() + + @api.model + def _install_partner_firstname(self): + """Save names correctly in the database. + + Before installing the module, field ``name`` contains all full names. + When installing it, this method parses those names and saves them + correctly into the database. This can be called later too if needed. + """ + # Find records with empty firstname and lastname + records = self.search([("firstname", "=", False), + ("lastname", "=", False)]) + + # Force calculations there + records._inverse_name() + _logger.info("%d partners updated installing module.", len(records)) diff --git a/partner_firstname/partner.py b/partner_firstname/partner.py deleted file mode 100644 index 52dccd201..000000000 --- a/partner_firstname/partner.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Author: Nicolas Bessi. Copyright Camptocamp SA -# Copyright (C) 2014 Agile Business Group () -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -from openerp.osv import orm, fields -from openerp.tools.translate import _ -import logging - -_logger = logging.getLogger(__name__) - - -class ResPartner(orm.Model): - """Adds lastname and firstname, name become a stored function field""" - - _inherit = 'res.partner' - - def _set_default_value_on_column(self, cr, column_name, context=None): - res = super(ResPartner, self)._set_default_value_on_column( - cr, column_name, context=context) - if column_name == 'lastname': - cr.execute('UPDATE res_partner SET lastname = name WHERE name ' - 'IS NOT NULL AND lastname IS NULL') - cr.execute('ALTER TABLE res_partner ALTER COLUMN lastname ' - 'SET NOT NULL') - _logger.info("NOT NULL constraint for " - "res_partner.lastname correctly set") - return res - - def _prepare_name_custom(self, cursor, uid, partner, context=None): - """ - This function is designed to be inherited in a custom module - """ - names = (partner.lastname, partner.firstname) - fullname = " ".join([s for s in names if s]) - return fullname - - def _compute_name_custom(self, cursor, uid, ids, fname, arg, context=None): - res = {} - for partner in self.browse(cursor, uid, ids, context=context): - res[partner.id] = self._prepare_name_custom( - cursor, uid, partner, context=context) - return res - - def _write_name( - self, cursor, uid, partner_id, field_name, field_value, arg, - context=None - ): - """ - Try to reverse the effect of _compute_name_custom: - * if the partner is not a company and the firstname does not change in - the new name then firstname remains untouched and lastname is updated - accordingly - * otherwise lastname=new name and firstname=False - In addition an heuristic avoids to keep a firstname without a non-blank - lastname - """ - field_value = ( - field_value and not field_value.isspace() and field_value or False) - vals = {'lastname': field_value, 'firstname': False} - if field_value: - flds = self.read( - cursor, uid, [partner_id], ['firstname', 'is_company'], - context=context)[0] - if not flds['is_company']: - to_check = ' %s' % flds['firstname'] - if field_value.endswith(to_check): - ln = field_value[:-len(to_check)].strip() - if ln: - vals['lastname'] = ln - del(vals['firstname']) - else: - # If the lastname is deleted from the new name - # then the firstname becomes the lastname - vals['lastname'] = flds['firstname'] - - return self.write(cursor, uid, partner_id, vals, context=context) - - def copy_data(self, cr, uid, id, default=None, context=None): - """ - Avoid to replicate the firstname into the name when duplicating a - partner - """ - default = default or {} - if not default.get('lastname'): - default = default.copy() - default['lastname'] = ( - _('%s (copy)') % self.read( - cr, uid, [id], ['lastname'], context=context - )[0]['lastname'] - ) - if default.get('name'): - del(default['name']) - return super(ResPartner, self).copy_data( - cr, uid, id, default, context=context) - - def create(self, cursor, uid, vals, context=None): - """ - To support data backward compatibility we have to keep this overwrite - even if we use fnct_inv: otherwise we can't create entry because - lastname is mandatory and module will not install if there is demo data - """ - corr_vals = vals.copy() - if corr_vals.get('name'): - corr_vals['lastname'] = corr_vals['name'] - del(corr_vals['name']) - return super(ResPartner, self).create( - cursor, uid, corr_vals, context=context) - - _columns = {'name': fields.function(_compute_name_custom, string="Name", - type="char", store=True, - select=True, readonly=True, - fnct_inv=_write_name), - - 'firstname': fields.char("Firstname"), - 'lastname': fields.char("Lastname", required=True)} diff --git a/partner_firstname/partner_view.xml b/partner_firstname/partner_view.xml deleted file mode 100644 index 87f41b805..000000000 --- a/partner_firstname/partner_view.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - res.partner.simplified.form.firstname - res.partner - - - - {'readonly': [('is_company', '=', False)], 'required': [('is_company', '=', True)]} - - - - - - - - - - - - res.partner.form.firstname - res.partner - - - - {'readonly': [('is_company', '=', False)], 'required': [('is_company', '=', True)]} - - - - - - - - - - - - - - - - - - -
- -
-
- - {'readonly': [('is_company', '=', False)], 'required': [('is_company', '=', True)]} - - -
-
- -
-
diff --git a/partner_firstname/res_user.py b/partner_firstname/res_user.py deleted file mode 100644 index 83ec8be7c..000000000 --- a/partner_firstname/res_user.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -"""Extend res.users to be compatible with split name in res.partner.""" -############################################################################## -# -# Author: Nicolas Bessi. Copyright Camptocamp SA -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -from openerp import api, models -from openerp.tools.translate import _ - - -class ResUsers(models.Model): - """Extend res.users to be compatible with split name in res.partner.""" - _inherit = 'res.users' - - @api.onchange('firstname', 'lastname') - def change_name(self): - names = [name for name in [self.firstname, self.lastname] if name] - self.name = ' '.join(names) - - def copy_data(self, cr, uid, _id, default=None, context=None): - """ - Avoid to replicate the firstname into the name when duplicating a user - """ - default = default or {} - if not default.get('lastname'): - default = default.copy() - default['lastname'] = ( - _('%s (copy)') % self.read( - cr, uid, [_id], ['lastname'], context=context - )[0]['lastname'] - ) - if default.get('name'): - del(default['name']) - return super(ResUsers, self).copy_data( - cr, uid, _id, default, context=context) diff --git a/partner_firstname/res_user_view.xml b/partner_firstname/res_user_view.xml deleted file mode 100644 index f6516468d..000000000 --- a/partner_firstname/res_user_view.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - res.users.form.firstname - res.users - - - - - - - - diff --git a/partner_firstname/tests/__init__.py b/partner_firstname/tests/__init__.py index 71518a9dc..7dfc1f9cc 100644 --- a/partner_firstname/tests/__init__.py +++ b/partner_firstname/tests/__init__.py @@ -28,4 +28,4 @@ # ############################################################################## -from . import test_partner_firstname +from . import test_empty, test_name, test_onchange diff --git a/partner_firstname/tests/base.py b/partner_firstname/tests/base.py new file mode 100644 index 000000000..85e204435 --- /dev/null +++ b/partner_firstname/tests/base.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +# Authors: Nemry Jonathan +# Copyright (c) 2014 Acsone SA/NV (http://www.acsone.eu) +# All Rights Reserved +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsibility of assessing all potential +# consequences resulting from its eventual inadequacies and bugs. +# End users who are looking for a ready-to-use solution with commercial +# guarantees and support are strongly advised to contact a Free Software +# Service Company. +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +from openerp.tests.common import TransactionCase +from .. import exceptions as ex + + +class MailInstalled(): + def mail_installed(self): + """Check if ``mail`` module is installed.``""" + return (self.env["ir.module.module"] + .search([("name", "=", "mail")]) + .state == "installed") + + +class BaseCase(TransactionCase, MailInstalled): + def setUp(self): + super(BaseCase, self).setUp() + self.check_fields = True + self.expect(u"Núñez", u"Fernán") + self.create_original() + + def create_original(self): + self.original = self.env["res.partner"].create({ + "lastname": self.lastname, + "firstname": self.firstname}) + + def expect(self, lastname, firstname, name=None): + """Define what is expected in each field when ending.""" + self.lastname = lastname + self.firstname = firstname + self.name = name or u"%s %s" % (lastname, firstname) + + def tearDown(self): + if self.check_fields: + if not hasattr(self, "changed"): + self.changed = self.original + + for field in ("name", "lastname", "firstname"): + self.assertEqual( + getattr(self.changed, field), + getattr(self, field), + "Test failed with wrong %s" % field) + + super(BaseCase, self).tearDown() + + def test_copy(self): + """Copy the partner and compare the result.""" + self.expect(self.lastname, u"%s (copy)" % self.firstname) + self.changed = self.original.with_context(lang="en_US").copy() + + def test_one_name(self): + """Test what happens when only one name is given.""" + name = u"Mönty" + self.expect(name, False, name) + self.original.name = name + + def test_no_names(self): + """Test that you cannot set a partner/user without names.""" + self.check_fields = False + with self.assertRaises(ex.EmptyNamesError): + self.original.firstname = self.original.lastname = False + + +class OnChangeCase(TransactionCase): + is_company = False + + def new_partner(self): + """Create an empty partner. Ensure it is (or not) a company.""" + new = self.env["res.partner"].new() + new.is_company = self.is_company + return new diff --git a/partner_firstname/tests/test_empty.py b/partner_firstname/tests/test_empty.py new file mode 100644 index 000000000..280ed9e92 --- /dev/null +++ b/partner_firstname/tests/test_empty.py @@ -0,0 +1,68 @@ +# -*- encoding: utf-8 -*- + +# Odoo, Open Source Management Solution +# Copyright (C) 2014-2015 Grupo ESOC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Test situations where names are empty. + +To have more accurate results, remove the ``mail`` module before testing. +""" + +from openerp.tests.common import TransactionCase +from .base import MailInstalled +from .. import exceptions as ex + + +class CompanyCase(TransactionCase): + """Test ``res.partner`` when it is a company.""" + model = "res.partner" + context = {"default_is_company": True} + + def tearDown(self): + try: + data = {"name": self.name} + with self.assertRaises(ex.EmptyNamesError): + self.env[self.model].with_context(**self.context).create(data) + finally: + super(CompanyCase, self).tearDown() + + def test_name_empty_string(self): + """Test what happens when the name is an empty string.""" + self.name = "" + + def test_name_false(self): + """Test what happens when the name is ``False``.""" + self.name = False + + +class PersonCase(CompanyCase): + """Test ``res.partner`` when it is a person.""" + context = {"default_is_company": False} + + +class UserCase(CompanyCase, MailInstalled): + """Test ``res.users``.""" + model = "res.users" + context = {"default_login": "user@example.com"} + + def tearDown(self): + # Cannot create users if ``mail`` is installed + if self.mail_installed(): + # Skip tests + super(CompanyCase, self).tearDown() + else: + # Run tests + super(UserCase, self).tearDown() diff --git a/partner_firstname/tests/test_name.py b/partner_firstname/tests/test_name.py new file mode 100644 index 000000000..f471ad16e --- /dev/null +++ b/partner_firstname/tests/test_name.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- + +# Authors: Nemry Jonathan +# Copyright (c) 2014 Acsone SA/NV (http://www.acsone.eu) +# All Rights Reserved +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsibility of assessing all potential +# consequences resulting from its eventual inadequacies and bugs. +# End users who are looking for a ready-to-use solution with commercial +# guarantees and support are strongly advised to contact a Free Software +# Service Company. +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Test naming logic. + +To have more accurate results, remove the ``mail`` module before testing. +""" + +from .base import BaseCase + + +class PartnerContactCase(BaseCase): + def test_update_lastname(self): + """Change lastname.""" + self.expect(u"newlästname", self.firstname) + self.original.name = self.name + + def test_update_firstname(self): + """Change firstname.""" + self.expect(self.lastname, u"newfïrstname") + self.original.name = self.name + + def test_whitespace_cleanup(self): + """Check that whitespace in name gets cleared.""" + self.expect(u"newlästname", u"newfïrstname") + self.original.name = " newlästname newfïrstname " + + # Need this to refresh the ``name`` field + self.original.invalidate_cache() + + +class PartnerCompanyCase(BaseCase): + def create_original(self): + super(PartnerCompanyCase, self).create_original() + self.original.is_company = True + + def test_copy(self): + """Copy the partner and compare the result.""" + super(PartnerCompanyCase, self).test_copy() + self.expect(self.name, False, self.name) + + def test_company_inverse(self): + """Test the inverse method in a company record.""" + name = u"Thïs is a Companŷ" + self.expect(name, False, name) + self.original.name = name + + +class UserCase(PartnerContactCase): + def create_original(self): + name = u"%s %s" % (self.lastname, self.firstname) + + # Cannot create users if ``mail`` is installed + if self.mail_installed(): + self.original = self.env.ref("base.user_demo") + self.original.name = name + else: + self.original = self.env["res.users"].create({ + "name": name, + "login": "firstnametest@example.com"}) + + def test_copy(self): + """Copy the partner and compare the result.""" + # Skip if ``mail`` is installed + if not self.mail_installed(): + super(UserCase, self).test_copy() diff --git a/partner_firstname/tests/test_onchange.py b/partner_firstname/tests/test_onchange.py new file mode 100644 index 000000000..6d2ec278f --- /dev/null +++ b/partner_firstname/tests/test_onchange.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +"""These tests try to mimic the behavior of the UI form. + +The form operates in onchange mode, with its limitations. +""" + +from .base import OnChangeCase + + +class PartnerCompanyCase(OnChangeCase): + is_company = True + + def test_create_from_form(self): + """A user creates a company from the form.""" + name = u"Sôme company" + with self.env.do_in_onchange(): + # User presses ``new`` + partner = self.new_partner() + + # User sets a name, which triggers onchanges + partner.name = name + partner._onchange_name() + + self.assertEqual(partner.name, name) + self.assertEqual(partner.firstname, False) + self.assertEqual(partner.lastname, name) + + def test_empty_name_and_subnames(self): + """If the user empties ``name``, subnames must be ``False``. + + Otherwise, the ``required`` attr will not work as expected. + """ + with self.env.do_in_onchange(): + # User presses ``new`` + partner = self.new_partner() + + # User sets a name, which triggers onchanges + partner.name = u"Foó" + partner._onchange_name() + + # User unsets name, which triggers onchanges + partner.name = u"" + partner._onchange_name() + + self.assertEqual(partner.firstname, False) + self.assertEqual(partner.lastname, False) + + +class PartnerContactCase(OnChangeCase): + def test_create_from_form_only_firstname(self): + """A user creates a contact with only the firstname from the form.""" + firstname = u"Fïrst" + with self.env.do_in_onchange(): + # User presses ``new`` + partner = self.new_partner() + + # Changes firstname, which triggers onchanges + partner.firstname = firstname + partner._onchange_subnames() + partner._onchange_name() + + self.assertEqual(partner.lastname, False) + self.assertEqual(partner.firstname, firstname) + self.assertEqual(partner.name, firstname) + + def test_create_from_form_only_lastname(self): + """A user creates a contact with only the lastname from the form.""" + lastname = u"Läst" + with self.env.do_in_onchange(): + # User presses ``new`` + partner = self.new_partner() + + # Changes lastname, which triggers onchanges + partner.lastname = lastname + partner._onchange_subnames() + partner._onchange_name() + + self.assertEqual(partner.firstname, False) + self.assertEqual(partner.lastname, lastname) + self.assertEqual(partner.name, lastname) + + def test_create_from_form_all(self): + """A user creates a contact with all names from the form.""" + firstname = u"Fïrst" + lastname = u"Läst" + with self.env.do_in_onchange(): + # User presses ``new`` + partner = self.new_partner() + + # Changes firstname, which triggers onchanges + partner.firstname = firstname + partner._onchange_subnames() + partner._onchange_name() + + # Changes lastname, which triggers onchanges + partner.lastname = lastname + partner._onchange_subnames() + partner._onchange_name() + + self.assertEqual(partner.lastname, lastname) + self.assertEqual(partner.firstname, firstname) + self.assertEqual(partner.name, u" ".join((lastname, firstname))) diff --git a/partner_firstname/tests/test_partner_firstname.py b/partner_firstname/tests/test_partner_firstname.py deleted file mode 100644 index a4bac1ee0..000000000 --- a/partner_firstname/tests/test_partner_firstname.py +++ /dev/null @@ -1,153 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# Authors: Nemry Jonathan -# Copyright (c) 2014 Acsone SA/NV (http://www.acsone.eu) -# All Rights Reserved -# -# WARNING: This program as such is intended to be used by professional -# programmers who take the whole responsibility of assessing all potential -# consequences resulting from its eventual inadequacies and bugs. -# End users who are looking for a ready-to-use solution with commercial -# guarantees and support are strongly advised to contact a Free Software -# Service Company. -# -# This program is Free Software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# -# -import openerp.tests.common as common - -from openerp.tools.translate import _ - - -class test_partner_firstname(common.TransactionCase): - - def setUp(self): - super(test_partner_firstname, self).setUp() - - self.registry('ir.model').clear_caches() - self.registry('ir.model.data').clear_caches() - - self.user_model = self.registry("res.users") - self.partner_model = self.registry("res.partner") - self.fields_partner = { - 'lastname': 'lastname', 'firstname': 'firstname'} - self.fields_user = { - 'name': 'lastname', 'login': 'v5Ue4Tql0Pm67KX05g25A'} - - self.context = self.user_model.context_get(self.cr, self.uid) - - def test_copy_partner(self): - cr, uid, context = self.cr, self.uid, self.context - res_id = self.partner_model.create( - cr, uid, self.fields_partner, context=context) - res_id = self.partner_model.copy( - cr, uid, res_id, default={}, context=context) - vals = self.partner_model.read(cr, uid, [res_id], [ - 'name', 'lastname', 'firstname'], context=context)[0] - - self.assertEqual( - vals['name'], - _('%s (copy)') % 'lastname' + " firstname", - 'Copy of the partner failed with wrong name' - ) - self.assertEqual( - vals['lastname'], - _('%s (copy)') % 'lastname', - 'Copy of the partner failed with wrong lastname' - ) - self.assertEqual(vals['firstname'], 'firstname', - 'Copy of the partner failed with wrong firstname') - - def test_copy_user(self): - cr, uid, context = self.cr, self.uid, self.context - # create a user - res_id = self.user_model.create( - cr, uid, self.fields_user, context=context) - # get the related partner id and add it a firstname - flds = self.user_model.read( - cr, uid, [res_id], ['partner_id'], context=context)[0] - self.partner_model.write(cr, uid, flds['partner_id'][ - 0], {'firstname': 'firstname'}, context=context) - # copy the user and compare result - res_id = self.user_model.copy( - cr, uid, res_id, default={}, context=context) - vals = self.user_model.read( - cr, uid, [res_id], ['name', 'lastname', 'firstname'], - context=context)[0] - - self.assertEqual( - vals['name'], - _('%s (copy)') % 'lastname' + ' firstname', - 'Copy of the user failed with wrong name' - ) - self.assertEqual( - vals['lastname'], _('%s (copy)') % - 'lastname', 'Copy of the user failed with wrong lastname') - self.assertEqual(vals['firstname'], 'firstname', - 'Copy of the user failed with wrong firstname') - - def test_update_user_lastname(self): - cr, uid, context = self.cr, self.uid, self.context - # create a user - res_id = self.user_model.create( - cr, uid, self.fields_user, context=context) - # get the related partner id and add it a firstname - flds = self.user_model.read( - cr, uid, [res_id], ['partner_id'], context=context)[0] - self.partner_model.write( - cr, uid, flds['partner_id'][0], {'firstname': 'firstname'}, - context=context) - self.user_model.write( - cr, uid, res_id, {'name': 'change firstname'}, context=context) - vals = self.user_model.read( - cr, uid, [res_id], ['name', 'lastname', 'firstname'], - context=context)[0] - - self.assertEqual(vals['name'], 'change firstname', - 'Update of the user lastname failed with wrong name') - self.assertEqual( - vals['lastname'], 'change', - 'Update of the user lastname failed with wrong lastname') - self.assertEqual( - vals['firstname'], 'firstname', - 'Update of the user lastname failed with wrong firstname') - - def test_update_user_firstname(self): - cr, uid, context = self.cr, self.uid, self.context - # create a user - res_id = self.user_model.create( - cr, uid, self.fields_user, context=context) - # get the related partner id and add it a firstname - flds = self.user_model.read( - cr, uid, [res_id], ['partner_id'], context=context)[0] - self.partner_model.write( - cr, uid, flds['partner_id'][0], {'firstname': 'firstname'}, - context=context) - self.user_model.write( - cr, uid, res_id, {'name': 'lastname other'}, context=context) - vals = self.user_model.read( - cr, uid, [res_id], ['name', 'lastname', 'firstname'], - context=context)[0] - - self.assertEqual( - vals['name'], 'lastname other', - 'Update of the user firstname failed with wrong name') - self.assertEqual( - vals['lastname'], 'lastname other', - 'Update of the user firstname failed with wrong lastname') - self.assertFalse( - vals['firstname'], - 'Update of the user firstname failed with wrong firstname') diff --git a/partner_firstname/views/res_partner.xml b/partner_firstname/views/res_partner.xml new file mode 100644 index 000000000..0838628eb --- /dev/null +++ b/partner_firstname/views/res_partner.xml @@ -0,0 +1,98 @@ + + + + + + Add firstname and lastname + res.partner + + + + + { + 'readonly': [('is_company', '=', False)], + 'required': [('is_company', '=', True)] + } + + + + + + { + 'readonly': [('is_company', '=', False)], + 'required': [('is_company', '=', True)] + } + + + + + + + + + + + +
+ +
+
+ + + { + 'readonly': [('is_company', '=', False)], + 'required': [('is_company', '=', True)] + } + +
+
+
+ +
+
diff --git a/partner_firstname/views/res_user.xml b/partner_firstname/views/res_user.xml new file mode 100644 index 000000000..ddfe9c7ef --- /dev/null +++ b/partner_firstname/views/res_user.xml @@ -0,0 +1,32 @@ + + + + + + + + + Add firstname and surnames + res.users + + + + + True + + + + + + + + + + + + + +