Browse Source

Better partner_firstname. Merged rebase.

Fix wrong naming.

Remove subclassing of exception, since there is only one.

Rename exception according to PEP8.

Reduce tests' redundancy.

Add test for whitespace trimming.

Use unicode for code & tests.

Fix wrong comments.

Split _name_inverse adding _name_clean_inverse.

Conflicts:
	partner_firstname/models.py

Remove license header in XML files.

Preserve old view names.

Credits to last translator.

Increase coverage.

Add args to exception to display the correct message in the UI.

Execute _firstname_install when installing, and log it.

Rename methods to follow guidelines.

Better docstrings.

Workaround https://github.com/odoo/odoo/issues/6324.

Fix users not being able to create companies.

This happened because the invert method was not being called when using
the UI, and because lastname & firstname fields were required while
hidden.

Add new tests and fix the resulting bugs.

New tests, some fail.

Add docs to test modules.

Fix recursive onchange misbehavior & tests.

Fix UI problem when lastname was u"".

It should be `False` to avoid `required` errors.

Use new() to create onchange records. Reduce redundancy in tests.

Tests work with `mail` module installed now.

Sometimes, the only way is to just skip them.
pull/663/head
Jairo Llopis 10 years ago
committed by Jairo Llopis
parent
commit
694c0a6ff7
  1. 56
      partner_firstname/README.rst
  2. 11
      partner_firstname/__init__.py
  3. 33
      partner_firstname/__openerp__.py
  4. 19
      partner_firstname/data/res_partner.yml
  5. 27
      partner_firstname/exceptions.py
  6. 52
      partner_firstname/i18n/es.po
  7. 33
      partner_firstname/i18n/partner_firstname.pot
  8. 136
      partner_firstname/models.py
  9. 131
      partner_firstname/partner.py
  10. 60
      partner_firstname/partner_view.xml
  11. 49
      partner_firstname/res_user.py
  12. 35
      partner_firstname/res_user_view.xml
  13. 2
      partner_firstname/tests/__init__.py
  14. 96
      partner_firstname/tests/base.py
  15. 68
      partner_firstname/tests/test_empty.py
  16. 90
      partner_firstname/tests/test_name.py
  17. 102
      partner_firstname/tests/test_onchange.py
  18. 153
      partner_firstname/tests/test_partner_firstname.py
  19. 98
      partner_firstname/views/res_partner.xml
  20. 32
      partner_firstname/views/res_user.xml

56
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 <nicolas.bessi@camptocamp.com>
* Jonathan Nemry <jonathan.nemry@acsone.eu>
* Olivier Laurent <olivier.laurent@acsone.eu>
* Hans Henrik Gabelgaard <hhg@gabelgaard.org>
* Jairo Llopis <j.llopis@grupoesoc.es>
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.

11
partner_firstname/__init__.py

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi. Copyright Camptocamp SA # Author: Nicolas Bessi. Copyright Camptocamp SA
# Copyright (C)
# 2014: Agile Business Group (<http://www.agilebg.com>)
# 2015: Grupo ESOC <www.grupoesoc.es>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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 # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import partner
from . import res_user
from . import models

33
partner_firstname/__openerp__.py

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi. Copyright Camptocamp SA # Author: Nicolas Bessi. Copyright Camptocamp SA
# Copyright (C)
# 2014: Agile Business Group (<http://www.agilebg.com>)
# 2015: Grupo ESOC <www.grupoesoc.es>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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 # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{ {
'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 <jonathan.nemry@acsone.eu>
Olivier Laurent <olivier.laurent@acsone.eu>
""",
'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)", 'author': "Camptocamp,Odoo Community Association (OCA)",
'maintainer': 'Camptocamp, Acsone', 'maintainer': 'Camptocamp, Acsone',
'category': 'Extra Tools', 'category': 'Extra Tools',
'website': 'http://www.camptocamp.com, http://www.acsone.eu', 'website': 'http://www.camptocamp.com, http://www.acsone.eu',
'depends': ['base'], 'depends': ['base'],
'data': [ 'data': [
'partner_view.xml',
'res_user_view.xml',
'views/res_partner.xml',
'views/res_user.xml',
'data/res_partner.yml',
], ],
'demo': [], 'demo': [],
'test': [], 'test': [],

19
partner_firstname/data/res_partner.yml

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Copyright (C)
# 2015: Grupo ESOC <www.grupoesoc.es>
#
# 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 <http://www.gnu.org/licenses/>.
- !function {model: res.partner, name: _install_partner_firstname}

27
partner_firstname/exceptions.py

@ -0,0 +1,27 @@
# -*- encoding: utf-8 -*-
# Odoo, Open Source Management Solution
# Copyright (C) 2014-2015 Grupo ESOC <www.grupoesoc.es>
#
# 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 <http://www.gnu.org/licenses/>.
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)

52
partner_firstname/i18n/es.po

@ -6,48 +6,54 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: OpenERP Server 7.0\n" "Project-Id-Version: OpenERP Server 7.0\n"
"Report-Msgid-Bugs-To: \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 <j.llopis@grupoesoc.es>\n"
"Language-Team: \n" "Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: \n" "Plural-Forms: \n"
"X-Generator: Poedit 1.7.5\n"
#. module: partner_firstname #. 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 #, 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 #. module: partner_firstname
#: field:res.partner,firstname:0 #: field:res.partner,firstname:0
msgid "Firstname"
msgid "First name"
msgstr "Nombre" msgstr "Nombre"
#. module: partner_firstname
#: view:res.partner:partner_firstname.partner_form
msgid "Is a Company?"
msgstr "¿Es una empresa?"
#. module: partner_firstname #. module: partner_firstname
#: field:res.partner,lastname:0 #: field:res.partner,lastname:0
msgid "Lastname"
msgstr "Apellidos"
msgid "Last name"
msgstr "Apellido"
#. module: partner_firstname #. 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 #. module: partner_firstname
#: model:ir.model,name:partner_firstname.model_res_partner #: model:ir.model,name:partner_firstname.model_res_partner
msgid "Partner" msgid "Partner"
msgstr "Empresa" msgstr "Empresa"
#~ msgid "True"
#~ msgstr "Verdadero"
#~ msgid "%s (copy)"
#~ msgstr "%s (copia)"
#~ msgid "Users"
#~ msgstr "Usuarios"

33
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: # This file contains the translation of the following modules:
# * partner_firstname # * partner_firstname
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: OpenERP Server 7.0\n"
"Project-Id-Version: Odoo Server 8.0-20150327\n"
"Report-Msgid-Bugs-To: \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" "Last-Translator: <>\n"
"Language-Team: \n" "Language-Team: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -16,38 +16,33 @@ msgstr ""
"Plural-Forms: \n" "Plural-Forms: \n"
#. module: partner_firstname #. 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 #, python-format
msgid "%s (copy)"
msgid "Error(s) with partner %d's name."
msgstr "" msgstr ""
#. module: partner_firstname #. module: partner_firstname
#: model:ir.model,name:partner_firstname.model_res_users
msgid "Users"
#: field:res.partner,firstname:0
msgid "First name"
msgstr "" msgstr ""
#. module: partner_firstname #. module: partner_firstname
#: field:res.partner,firstname:0
msgid "Firstname"
#: view:res.partner:partner_firstname.partner_form
msgid "Is a Company?"
msgstr "" msgstr ""
#. module: partner_firstname #. module: partner_firstname
#: field:res.partner,lastname:0 #: field:res.partner,lastname:0
msgid "Lastname"
msgid "Last name"
msgstr "" msgstr ""
#. module: partner_firstname #. 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 "" msgstr ""
#. module: partner_firstname #. module: partner_firstname
#: model:ir.model,name:partner_firstname.model_res_partner #: model:ir.model,name:partner_firstname.model_res_partner
msgid "Partner" msgid "Partner"
msgstr "" msgstr ""

136
partner_firstname/models.py

@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
# Author: Nicolas Bessi. Copyright Camptocamp SA
# Copyright (C)
# 2014: Agile Business Group (<http://www.agilebg.com>)
# 2015: Grupo ESOC <www.grupoesoc.es>
#
# 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 <http://www.gnu.org/licenses/>.
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))

131
partner_firstname/partner.py

@ -1,131 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Nicolas Bessi. Copyright Camptocamp SA
# Copyright (C) 2014 Agile Business Group (<http://www.agilebg.com>)
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
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)}

60
partner_firstname/partner_view.xml

@ -1,60 +0,0 @@
<openerp>
<data>
<record id="view_partner_simple_form_firstname" model="ir.ui.view">
<field name="name">res.partner.simplified.form.firstname</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_simple_form"/>
<field name="arch" type="xml">
<field name="name" position="attributes">
<attribute name="attrs">{'readonly': [('is_company', '=', False)], 'required': [('is_company', '=', True)]}</attribute>
</field>
<field name="category_id" position="before">
<group attrs="{'invisible': [('is_company', '=', True)]}">
<field name="lastname" attrs="{'required': [('is_company', '=', False)]}"/>
<field name="firstname" />
</group>
</field>
</field>
</record>
<record id="view_partner_form_firstname" model="ir.ui.view">
<field name="name">res.partner.form.firstname</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<field name="name" position="attributes">
<attribute name="attrs">{'readonly': [('is_company', '=', False)], 'required': [('is_company', '=', True)]}</attribute>
</field>
<field name="category_id" position="before">
<group attrs="{'invisible': [('is_company', '=', True)]}">
<field name="lastname" attrs="{'required': [('is_company', '=', False)]}"/>
<field name="firstname"/>
</group>
</field>
<!-- Add firstname and last name in inner contact form of child_ids -->
<xpath expr="//field[@name='child_ids']/form//field[@name='category_id']" position="before">
<group attrs="{'invisible': [('is_company', '=', True)]}">
<field name="lastname" attrs="{'required': [('is_company', '=', False)]}"/>
<field name="firstname"/>
</group>
</xpath>
<xpath expr="//field[@name='child_ids']/form//field[@name='category_id']" position="attributes">
<attribute name="style"/>
</xpath>
<xpath expr="//field[@name='child_ids']/form//label[@for='name']" position="before">
<div class="oe_edit_only">
<field name="is_company"
on_change="onchange_type(is_company)"/>
<label for="is_company"
string="Is a Company?"/>
</div>
</xpath>
<xpath expr="//field[@name='child_ids']/form//field[@name='name']" position="attributes">
<attribute name="attrs">{'readonly': [('is_company', '=', False)], 'required': [('is_company', '=', True)]}</attribute>
</xpath>
</field>
</record>
</data>
</openerp>

49
partner_firstname/res_user.py

@ -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 <http://www.gnu.org/licenses/>.
#
##############################################################################
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)

35
partner_firstname/res_user_view.xml

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- Replace name with first name and last name -->
<record id="view_users_form" model="ir.ui.view">
<field name="name">res.users.form.firstname</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
<label for="name" position="attributes">
<attribute name="invisible">1</attribute>
</label>
<label for="name" position="after">
<label for="firstname" class="oe_edit_only"/>
</label>
<field name="name" position="attributes">
<attribute name="invisible">1</attribute>
</field>
<field name="name" position="after">
<field name="firstname"/>
</field>
<label for="login" position="before">
<label for="lastname" class="oe_edit_only"/>
<h1><field name="lastname"/></h1>
</label>
</field>
</record>
</data>
</openerp>

2
partner_firstname/tests/__init__.py

@ -28,4 +28,4 @@
# #
############################################################################## ##############################################################################
from . import test_partner_firstname
from . import test_empty, test_name, test_onchange

96
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

68
partner_firstname/tests/test_empty.py

@ -0,0 +1,68 @@
# -*- encoding: utf-8 -*-
# Odoo, Open Source Management Solution
# Copyright (C) 2014-2015 Grupo ESOC <www.grupoesoc.es>
#
# 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 <http://www.gnu.org/licenses/>.
"""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()

90
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()

102
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)))

153
partner_firstname/tests/test_partner_firstname.py

@ -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')

98
partner_firstname/views/res_partner.xml

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_partner_simple_form_firstname" model="ir.ui.view">
<field name="name">Add firstname and lastname</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_simple_form"/>
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='name']" position="attributes">
<attribute name="attrs">{
'readonly': [('is_company', '=', False)],
'required': [('is_company', '=', True)]
}</attribute>
</xpath>
<xpath expr="//field[@name='category_id']" position="before">
<group attrs="{'invisible': [('is_company', '=', True)]}">
<field name="lastname" attrs=
"{'required': [('firstname', '=', False),
('is_company', '=', False)]}"/>
<field name="firstname" attrs=
"{'required': [('lastname', '=', False),
('is_company', '=', False)]}"/>
</group>
</xpath>
</data>
</field>
</record>
<record id="view_partner_form_firstname" model="ir.ui.view">
<field name="name">Add firstname and surnames</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='name']" position="attributes">
<attribute name="attrs">{
'readonly': [('is_company', '=', False)],
'required': [('is_company', '=', True)]
}</attribute>
</xpath>
<xpath expr="//field[@name='category_id']" position="before">
<group attrs="{'invisible': [('is_company', '=', True)]}">
<field name="lastname" attrs=
"{'required': [('firstname', '=', False),
('is_company', '=', False)]}"/>
<field name="firstname" attrs=
"{'required': [('lastname', '=', False),
('is_company', '=', False)]}"/>
</group>
</xpath>
<!-- Modify inner contact form of child_ids -->
<xpath expr="//field[@name='child_ids']/form
//field[@name='category_id']"
position="before">
<group attrs="{'invisible': [('is_company', '=', True)]}">
<field name="lastname" attrs=
"{'required': [('firstname', '=', False),
('is_company', '=', False)]}"/>
<field name="firstname" attrs=
"{'required': [('lastname', '=', False),
('is_company', '=', False)]}"/>
</group>
</xpath>
<xpath expr="//field[@name='child_ids']/form
//field[@name='category_id']"
position="attributes">
<attribute name="style"/>
</xpath>
<xpath expr="//field[@name='child_ids']/form//label[@for='name']"
position="before">
<div class="oe_edit_only">
<field name="is_company"
on_change="onchange_type(is_company)"/>
<label for="is_company"
string="Is a Company?"/>
</div>
</xpath>
<xpath expr="//field[@name='child_ids']/form//field[@name='name']"
position="attributes">
<attribute name="attrs">{
'readonly': [('is_company', '=', False)],
'required': [('is_company', '=', True)]
}</attribute>
</xpath>
</data>
</field>
</record>
</data>
</openerp>

32
partner_firstname/views/res_user.xml

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- Required before modifying `base.vew_users_form`.
https://github.com/odoo/odoo/issues/6324#issuecomment-93534579 -->
<function model="res.groups" name="update_user_groups_view" />
<record id="view_users_form" model="ir.ui.view">
<field name="name">Add firstname and surnames</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='name']" position="attributes">
<attribute name="readonly">True</attribute>
</xpath>
<xpath expr="//field[@name='email']" position="after">
<group>
<field name="lastname"
attrs="{'required': [('firstname', '=', False)]}"/>
<field name="firstname"
attrs="{'required': [('lastname', '=', False)]}"/>
</group>
</xpath>
</data>
</field>
</record>
</data>
</openerp>
Loading…
Cancel
Save