Browse Source
Better partner_firstname. Merged rebase.
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
20 changed files with 783 additions and 500 deletions
-
56partner_firstname/README.rst
-
11partner_firstname/__init__.py
-
33partner_firstname/__openerp__.py
-
19partner_firstname/data/res_partner.yml
-
27partner_firstname/exceptions.py
-
52partner_firstname/i18n/es.po
-
33partner_firstname/i18n/partner_firstname.pot
-
136partner_firstname/models.py
-
131partner_firstname/partner.py
-
60partner_firstname/partner_view.xml
-
49partner_firstname/res_user.py
-
35partner_firstname/res_user_view.xml
-
2partner_firstname/tests/__init__.py
-
96partner_firstname/tests/base.py
-
68partner_firstname/tests/test_empty.py
-
90partner_firstname/tests/test_name.py
-
102partner_firstname/tests/test_onchange.py
-
153partner_firstname/tests/test_partner_firstname.py
-
98partner_firstname/views/res_partner.xml
-
32partner_firstname/views/res_user.xml
@ -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. |
@ -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} |
@ -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) |
@ -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)) |
@ -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)} |
@ -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> |
@ -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) |
@ -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> |
@ -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 |
@ -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() |
@ -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() |
@ -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))) |
@ -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') |
@ -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> |
@ -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> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue