Browse Source
[IMP]base_custom_info: New types and access rules system.
[IMP]base_custom_info: New types and access rules system.
* Now you can define properties types, and access rules are inherited from the model/record linked to the custom info record. * Simplified version of computed value. * Implement for res.partner. * Add tests and fix bugs discovered in the meantime. * Allow to disable partner custom info tab, and custom info menu. * All of it can be set within general settings. * Now, by default, this module does not display custom info for partners unless in demo mode. Better fit for a base module. * You can disable the top menu entry too if it disturbs you, or enable it for everybody. * Give a special form when editing in partner custom info tab. * Sortable properties. * Sort values at onchange time. * Improve performance in onchange. * Split in several model files. # Conflicts: # base_custom_info/__manifest__.py # base_custom_info/i18n/es.popull/1115/head
Jairo Llopis
8 years ago
committed by
Fanha Giang
31 changed files with 1480 additions and 153 deletions
-
158base_custom_info/README.rst
-
2base_custom_info/__init__.py
-
19base_custom_info/__manifest__.py
-
4base_custom_info/demo/custom.info.option.csv
-
6base_custom_info/demo/custom.info.property.csv
-
2base_custom_info/demo/custom.info.template.csv
-
12base_custom_info/demo/res_groups.xml
-
10base_custom_info/migrations/9.0.2.0.0/pre-migrate.py
-
11base_custom_info/models/__init__.py
-
134base_custom_info/models/custom_info.py
-
34base_custom_info/models/custom_info_category.py
-
38base_custom_info/models/custom_info_option.py
-
113base_custom_info/models/custom_info_property.py
-
69base_custom_info/models/custom_info_template.py
-
283base_custom_info/models/custom_info_value.py
-
22base_custom_info/models/res_partner.py
-
13base_custom_info/security/ir.model.access.csv
-
31base_custom_info/security/res_groups.xml
-
5base_custom_info/tests/__init__.py
-
119base_custom_info/tests/test_partner.py
-
131base_custom_info/tests/test_value_conversion.py
-
53base_custom_info/views/custom_info_category_view.xml
-
52base_custom_info/views/custom_info_option_view.xml
-
63base_custom_info/views/custom_info_property_view.xml
-
38base_custom_info/views/custom_info_template_view.xml
-
80base_custom_info/views/custom_info_value_view.xml
-
44base_custom_info/views/menu.xml
-
28base_custom_info/views/res_partner_view.xml
-
6base_custom_info/wizard/__init__.py
-
20base_custom_info/wizard/base_config_settings.py
-
33base_custom_info/wizard/base_config_settings_view.xml
@ -0,0 +1,4 @@ |
|||
id,name,property_ids:id |
|||
opt_food,Loves junk food,prop_weaknesses |
|||
opt_videogames,Needs videogames,prop_weaknesses |
|||
opt_glasses,Huge glasses,prop_weaknesses |
@ -0,0 +1,6 @@ |
|||
id,name,template_id:id,field_type,default_value,required |
|||
prop_teacher,Name of his/her teacher,tpl_smart,str,, |
|||
prop_haters,Amount of people that hates him/her for being so smart,tpl_smart,int,, |
|||
prop_avg_note,Average note on all subjects,tpl_smart,float,,True |
|||
prop_smartypants,Does he/she believe he/she is the smartest person on earth?,tpl_smart,bool,, |
|||
prop_weaknesses,What weaknesses does he/she have?,tpl_smart,id,Huge glasses, |
@ -0,0 +1,2 @@ |
|||
id,name,model |
|||
tpl_smart,Smart partners,res.partner |
@ -0,0 +1,12 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). --> |
|||
|
|||
<odoo> |
|||
|
|||
<!-- Enable custom info for partners in demo instances by default --> |
|||
<record id="base.group_user" model="res.groups"> |
|||
<field name="implied_ids" eval="[(4, ref('group_partner'))]"/> |
|||
</record> |
|||
|
|||
</odoo> |
@ -0,0 +1,10 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
|||
# pragma: no-cover |
|||
|
|||
|
|||
def migrate(cr, version): |
|||
"""Update database from previous versions, before updating module.""" |
|||
cr.execute( |
|||
"ALTER TABLE custom_info_value RENAME COLUMN value TO value_str") |
@ -1,6 +1,15 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2015 Antiun Ingeniería S.L. - Sergio Teruel |
|||
# © 2015 Antiun Ingeniería S.L. - Carlos Dauden |
|||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html |
|||
|
|||
from . import custom_info |
|||
from . import ( |
|||
custom_info_template, |
|||
custom_info_property, |
|||
custom_info_category, |
|||
custom_info_option, |
|||
custom_info_value, |
|||
custom_info, |
|||
res_partner, |
|||
) |
@ -0,0 +1,34 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html |
|||
|
|||
from openerp import api, fields, models |
|||
|
|||
|
|||
class CustomInfoCategory(models.Model): |
|||
_description = "Categorize custom info properties" |
|||
_name = "custom.info.category" |
|||
_order = "sequence, name" |
|||
|
|||
name = fields.Char(index=True, translate=True, required=True) |
|||
sequence = fields.Integer(index=True) |
|||
property_ids = fields.One2many( |
|||
comodel_name="custom.info.property", |
|||
inverse_name="category_id", |
|||
string="Properties", |
|||
help="Properties in this category.", |
|||
) |
|||
|
|||
@api.multi |
|||
def check_access_rule(self, operation): |
|||
"""You access a category if you access at least one property.""" |
|||
last = None |
|||
for prop in self.mapped("property_ids"): |
|||
try: |
|||
prop.check_access_rule(operation) |
|||
return |
|||
except Exception as last: |
|||
pass |
|||
if last: |
|||
raise last |
|||
return super(CustomInfoCategory, self).check_access_rule(operation) |
@ -0,0 +1,38 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html |
|||
|
|||
from openerp import api, fields, models |
|||
|
|||
|
|||
class CustomInfoOption(models.Model): |
|||
_description = "Available options for a custom property" |
|||
_name = "custom.info.option" |
|||
_order = "name" |
|||
|
|||
name = fields.Char(index=True, translate=True, required=True) |
|||
property_ids = fields.Many2many( |
|||
comodel_name="custom.info.property", |
|||
string="Properties", |
|||
help="Properties where this option is enabled.", |
|||
) |
|||
value_ids = fields.One2many( |
|||
comodel_name="custom.info.value", |
|||
inverse_name="value_id", |
|||
string="Values", |
|||
help="Values that have set this option.", |
|||
) |
|||
|
|||
@api.multi |
|||
def check_access_rule(self, operation): |
|||
"""You access an option if you access at least one property.""" |
|||
last = None |
|||
for prop in self.mapped("property_ids"): |
|||
try: |
|||
prop.check_access_rule(operation) |
|||
return |
|||
except Exception as last: |
|||
pass |
|||
if last: |
|||
raise last |
|||
return super(CustomInfoOption, self).check_access_rule(operation) |
@ -0,0 +1,113 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html |
|||
from openerp import _, api, fields, models |
|||
from openerp.exceptions import UserError, ValidationError |
|||
|
|||
|
|||
class CustomInfoProperty(models.Model): |
|||
"""Name of the custom information property.""" |
|||
_description = "Custom information property" |
|||
_name = "custom.info.property" |
|||
_order = "template_id, category_sequence, category_id, sequence, id" |
|||
_sql_constraints = [ |
|||
("name_template", |
|||
"UNIQUE (name, template_id)", |
|||
"Another property with that name exists for that template."), |
|||
] |
|||
|
|||
name = fields.Char(required=True, translate=True) |
|||
sequence = fields.Integer(index=True) |
|||
category_id = fields.Many2one( |
|||
comodel_name="custom.info.category", |
|||
string="Category", |
|||
) |
|||
category_sequence = fields.Integer( |
|||
related="category_id.sequence", |
|||
store=True, |
|||
readonly=True, |
|||
) |
|||
template_id = fields.Many2one( |
|||
comodel_name='custom.info.template', |
|||
string='Template', |
|||
required=True) |
|||
model = fields.Char( |
|||
related="template_id.model", |
|||
readonly=True, |
|||
auto_join=True, |
|||
) |
|||
info_value_ids = fields.One2many( |
|||
comodel_name="custom.info.value", |
|||
inverse_name="property_id", |
|||
string="Property Values") |
|||
default_value = fields.Char( |
|||
translate=True, |
|||
help="Will be applied by default to all custom values of this " |
|||
"property. This is a char field, so you have to enter some value " |
|||
"that can be converted to the field type you choose.", |
|||
) |
|||
required = fields.Boolean() |
|||
minimum = fields.Float( |
|||
help="For numeric fields, it means the minimum possible value; " |
|||
"for text fields, it means the minimum possible length. " |
|||
"If it is bigger than the maximum, then this check is skipped", |
|||
) |
|||
maximum = fields.Float( |
|||
default=-1, |
|||
help="For numeric fields, it means the maximum possible value; " |
|||
"for text fields, it means the maximum possible length. " |
|||
"If it is smaller than the minimum, then this check is skipped", |
|||
) |
|||
field_type = fields.Selection( |
|||
selection=[ |
|||
("str", "Text"), |
|||
("int", "Whole number"), |
|||
("float", "Decimal number"), |
|||
("bool", "Yes/No"), |
|||
("id", "Selection"), |
|||
], |
|||
default="str", |
|||
required=True, |
|||
help="Type of information that can be stored in the property.", |
|||
) |
|||
option_ids = fields.Many2many( |
|||
comodel_name="custom.info.option", |
|||
string="Options", |
|||
help="When the field type is 'selection', choose the available " |
|||
"options here.", |
|||
) |
|||
|
|||
@api.multi |
|||
def check_access_rule(self, operation): |
|||
"""You access a property if you access its template.""" |
|||
self.mapped("template_id").check_access_rule(operation) |
|||
return super(CustomInfoProperty, self).check_access_rule(operation) |
|||
|
|||
@api.one |
|||
@api.constrains("default_value", "field_type") |
|||
def _check_default_value(self): |
|||
"""Ensure the default value is valid.""" |
|||
if self.default_value: |
|||
try: |
|||
self.env["custom.info.value"]._transform_value( |
|||
self.default_value, self.field_type, self) |
|||
except ValueError: |
|||
selection = dict( |
|||
self._fields["field_type"].get_description(self.env) |
|||
["selection"]) |
|||
raise ValidationError( |
|||
_("Default value %s cannot be converted to type %s.") % |
|||
(self.default_value, selection[self.field_type])) |
|||
|
|||
@api.multi |
|||
@api.onchange("required", "field_type") |
|||
def _onchange_required_warn(self): |
|||
"""Warn if the required flag implies a possible weird behavior.""" |
|||
if self.required: |
|||
if self.field_type == "bool": |
|||
raise UserError( |
|||
_("If you require a Yes/No field, you can only set Yes.")) |
|||
if self.field_type in {"int", "float"}: |
|||
raise UserError( |
|||
_("If you require a numeric field, you cannot set it to " |
|||
"zero.")) |
@ -0,0 +1,69 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html |
|||
|
|||
from openerp import _, api, fields, models |
|||
from openerp.exceptions import ValidationError |
|||
|
|||
|
|||
class CustomInfoTemplate(models.Model): |
|||
"""Defines custom properties expected for a given database object.""" |
|||
_description = "Custom information template" |
|||
_name = "custom.info.template" |
|||
_order = "model_id, name" |
|||
_sql_constraints = [ |
|||
("name_model", |
|||
"UNIQUE (name, model)", |
|||
"Another template with that name exists for that model."), |
|||
] |
|||
|
|||
name = fields.Char(required=True, translate=True) |
|||
model = fields.Char( |
|||
index=True, |
|||
readonly=True, |
|||
required=True) |
|||
model_id = fields.Many2one( |
|||
'ir.model', |
|||
'Model', |
|||
compute="_compute_model_id", |
|||
store=True, |
|||
ondelete="cascade", |
|||
) |
|||
property_ids = fields.One2many( |
|||
'custom.info.property', |
|||
'template_id', |
|||
'Properties', |
|||
oldname="info_ids", |
|||
context={"embed": True}, |
|||
) |
|||
|
|||
@api.multi |
|||
@api.depends("model") |
|||
def _compute_model_id(self): |
|||
"""Get a related model from its name, for better UI.""" |
|||
for s in self: |
|||
s.model_id = self.env["ir.model"].search([("model", "=", s.model)]) |
|||
|
|||
@api.multi |
|||
@api.constrains("model") |
|||
def _check_model(self): |
|||
"""Ensure model exists.""" |
|||
for s in self: |
|||
if s.model not in self.env: |
|||
raise ValidationError(_("Model does not exist.")) |
|||
# Avoid error when updating base module and a submodule extends a |
|||
# model that falls out of this one's dependency graph |
|||
with self.env.norecompute(): |
|||
oldmodels = set(s.mapped("property_ids.info_value_ids.model")) |
|||
if oldmodels and {s.model} != oldmodels: |
|||
raise ValidationError( |
|||
_("You cannot change the model because it is in use.")) |
|||
|
|||
@api.multi |
|||
def check_access_rule(self, operation): |
|||
"""You access a template if you access its model.""" |
|||
for s in self: |
|||
model = self.env[s.model] |
|||
model.check_access_rights(operation) |
|||
model.check_access_rule(operation) |
|||
return super(CustomInfoTemplate, self).check_access_rule(operation) |
@ -0,0 +1,283 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html |
|||
from openerp import _, api, fields, models, SUPERUSER_ID |
|||
from openerp.exceptions import ValidationError |
|||
from openerp.tools.safe_eval import safe_eval |
|||
|
|||
|
|||
class CustomInfoValue(models.Model): |
|||
_description = "Custom information value" |
|||
_name = "custom.info.value" |
|||
_rec_name = 'value' |
|||
_order = ("model, res_id, category_sequence, category_id, " |
|||
"property_sequence, property_id") |
|||
_sql_constraints = [ |
|||
("property_owner", |
|||
"UNIQUE (property_id, model, res_id)", |
|||
"Another property with that name exists for that resource."), |
|||
] |
|||
|
|||
model = fields.Char( |
|||
related="property_id.model", |
|||
index=True, |
|||
readonly=True, |
|||
auto_join=True, |
|||
store=True, |
|||
) |
|||
owner_id = fields.Reference( |
|||
selection="_selection_owner_id", |
|||
string="Owner", |
|||
compute="_compute_owner_id", |
|||
inverse="_inverse_owner_id", |
|||
help="Record that owns this custom value.", |
|||
) |
|||
res_id = fields.Integer( |
|||
"Resource ID", |
|||
required=True, |
|||
index=True, |
|||
store=True, |
|||
ondelete="cascade", |
|||
) |
|||
property_id = fields.Many2one( |
|||
comodel_name='custom.info.property', |
|||
required=True, |
|||
string='Property') |
|||
property_sequence = fields.Integer( |
|||
related="property_id.sequence", |
|||
store=True, |
|||
index=True, |
|||
readonly=True, |
|||
) |
|||
category_sequence = fields.Integer( |
|||
related="property_id.category_id.sequence", |
|||
store=True, |
|||
readonly=True, |
|||
) |
|||
category_id = fields.Many2one( |
|||
related="property_id.category_id", |
|||
store=True, |
|||
readonly=True, |
|||
) |
|||
name = fields.Char(related='property_id.name', readonly=True) |
|||
field_type = fields.Selection(related="property_id.field_type") |
|||
field_name = fields.Char( |
|||
compute="_compute_field_name", |
|||
help="Technical name of the field where the value is stored.", |
|||
) |
|||
required = fields.Boolean(related="property_id.required") |
|||
value = fields.Char( |
|||
compute="_compute_value", |
|||
inverse="_inverse_value", |
|||
search="_search_value", |
|||
help="Value, always converted to/from the typed field.", |
|||
) |
|||
value_str = fields.Char( |
|||
string="Text value", |
|||
translate=True, |
|||
index=True, |
|||
) |
|||
value_int = fields.Integer( |
|||
string="Whole number value", |
|||
index=True, |
|||
) |
|||
value_float = fields.Float( |
|||
string="Decimal number value", |
|||
index=True, |
|||
) |
|||
value_bool = fields.Boolean( |
|||
string="Yes/No value", |
|||
index=True, |
|||
) |
|||
value_id = fields.Many2one( |
|||
comodel_name="custom.info.option", |
|||
string="Selection value", |
|||
ondelete="cascade", |
|||
domain="[('property_ids', 'in', [property_id])]", |
|||
) |
|||
|
|||
@api.multi |
|||
def check_access_rule(self, operation): |
|||
"""You access a value if you access its property and owner record.""" |
|||
if self.env.uid == SUPERUSER_ID: |
|||
return |
|||
for s in self: |
|||
s.property_id.check_access_rule(operation) |
|||
s.owner_id.check_access_rights(operation) |
|||
s.owner_id.check_access_rule(operation) |
|||
return super(CustomInfoValue, self).check_access_rule(operation) |
|||
|
|||
@api.model |
|||
def create(self, vals): |
|||
"""Skip constrains in 1st lap.""" |
|||
# HACK https://github.com/odoo/odoo/pull/13439 |
|||
if "value" in vals: |
|||
self.env.context.skip_required = True |
|||
return super(CustomInfoValue, self).create(vals) |
|||
|
|||
@api.model |
|||
def _selection_owner_id(self): |
|||
"""You can choose among models linked to a template.""" |
|||
models = self.env["ir.model.fields"].search([ |
|||
("ttype", "=", "many2one"), |
|||
("relation", "=", "custom.info.template"), |
|||
("model_id.transient", "=", False), |
|||
"!", ("model", "=like", "custom.info.%"), |
|||
]).mapped("model_id") |
|||
models = models.search([("id", "in", models.ids)], order="name") |
|||
return [(m.model, m.name) for m in models |
|||
if m.model in self.env and self.env[m.model]._auto] |
|||
|
|||
@api.multi |
|||
@api.depends("property_id.field_type") |
|||
def _compute_field_name(self): |
|||
"""Get the technical name where the real typed value is stored.""" |
|||
for s in self: |
|||
s.field_name = "value_{!s}".format(s.property_id.field_type) |
|||
|
|||
@api.multi |
|||
@api.depends("res_id", "model") |
|||
def _compute_owner_id(self): |
|||
"""Get the id from the linked record.""" |
|||
for s in self: |
|||
s.owner_id = "{},{}".format(s.model, s.res_id) |
|||
|
|||
@api.multi |
|||
def _inverse_owner_id(self): |
|||
"""Store the owner according to the model and ID.""" |
|||
for s in self: |
|||
s.model = s.owner_id._name |
|||
s.res_id = s.owner_id.id |
|||
|
|||
@api.multi |
|||
@api.depends("property_id.field_type", "field_name", "value_str", |
|||
"value_int", "value_float", "value_bool", "value_id") |
|||
def _compute_value(self): |
|||
"""Get the value as a string, from the original field.""" |
|||
for s in self: |
|||
if s.field_type == "id": |
|||
s.value = ", ".join(s.value_id.mapped("display_name")) |
|||
elif s.field_type == "bool": |
|||
s.value = _("Yes") if s.value_bool else _("No") |
|||
else: |
|||
s.value = getattr(s, s.field_name, False) |
|||
|
|||
@api.multi |
|||
def _inverse_value(self): |
|||
"""Write the value correctly converted in the typed field.""" |
|||
for s in self: |
|||
s[s.field_name] = self._transform_value( |
|||
s.value, s.field_type, s.property_id) |
|||
|
|||
@api.one |
|||
@api.constrains("required", "field_name", "value_str", "value_int", |
|||
"value_float", "value_bool", "value_id") |
|||
def _check_required(self): |
|||
"""Ensure required fields are filled""" |
|||
# HACK https://github.com/odoo/odoo/pull/13439 |
|||
try: |
|||
del self.env.context.skip_required |
|||
except AttributeError: |
|||
if self.required and not self[self.field_name]: |
|||
raise ValidationError( |
|||
_("Property %s is required.") % |
|||
self.property_id.display_name) |
|||
|
|||
@api.one |
|||
@api.constrains("property_id", "field_type", "field_name", |
|||
"value_str", "value_int", "value_float") |
|||
def _check_min_max_limits(self): |
|||
"""Ensure value falls inside the property's stablished limits.""" |
|||
minimum, maximum = self.property_id.minimum, self.property_id.maximum |
|||
if minimum <= maximum: |
|||
value = self[self.field_name] |
|||
if not value: |
|||
# This is a job for :meth:`.~_check_required` |
|||
return |
|||
if self.field_type == "str": |
|||
number = len(self.value_str) |
|||
message = _( |
|||
"Length for %(prop)s is %(val)s, but it should be " |
|||
"between %(min)d and %(max)d.") |
|||
elif self.field_type in {"int", "float"}: |
|||
number = value |
|||
if self.field_type == "int": |
|||
message = _( |
|||
"Value for %(prop)s is %(val)s, but it should be " |
|||
"between %(min)d and %(max)d.") |
|||
else: |
|||
message = _( |
|||
"Value for %(prop)s is %(val)s, but it should be " |
|||
"between %(min)f and %(max)f.") |
|||
else: |
|||
return |
|||
if not minimum <= number <= maximum: |
|||
raise ValidationError(message % { |
|||
"prop": self.property_id.display_name, |
|||
"val": number, |
|||
"min": minimum, |
|||
"max": maximum, |
|||
}) |
|||
|
|||
@api.multi |
|||
@api.onchange("property_id") |
|||
def _onchange_property_set_default_value(self): |
|||
"""Load default value for this property.""" |
|||
for record in self: |
|||
if not record.value and record.property_id.default_value: |
|||
record.value = record.property_id.default_value |
|||
|
|||
@api.model |
|||
def _transform_value(self, value, format_, properties=None): |
|||
"""Transforms a text value to the expected format. |
|||
|
|||
:param str/bool value: |
|||
Custom value in raw string. |
|||
|
|||
:param str format_: |
|||
Target conversion format for the value. Must be available among |
|||
``custom.info.property`` options. |
|||
|
|||
:param recordset properties: |
|||
Useful when :param:`format_` is ``id``, as it helps to ensure the |
|||
option is available in these properties. If :param:`format_` is |
|||
``id`` and :param:`properties` is ``None``, no transformation will |
|||
be made for :param:`value`. |
|||
""" |
|||
if not value: |
|||
value = False |
|||
elif format_ == "id" and properties: |
|||
value = self.env["custom.info.option"].search( |
|||
[("property_ids", "in", properties.ids), |
|||
("name", "=ilike", value)]) |
|||
value.ensure_one() |
|||
elif format_ == "bool": |
|||
value = value.strip().lower() not in { |
|||
"0", "false", "", "no", "off", _("No").lower()} |
|||
elif format_ not in {"str", "id"}: |
|||
value = safe_eval("{!s}({!r})".format(format_, value)) |
|||
return value |
|||
|
|||
@api.model |
|||
def _search_value(self, operator, value): |
|||
"""Search from the stored field directly.""" |
|||
options = ( |
|||
o[0] for o in |
|||
self.property_id._fields["field_type"] |
|||
.get_description(self.env)["selection"]) |
|||
domain = [] |
|||
for fmt in options: |
|||
try: |
|||
_value = (self._transform_value(value, fmt) |
|||
if not isinstance(value, list) else |
|||
[self._transform_value(v, fmt) for v in value]) |
|||
except ValueError: |
|||
# If you are searching something that cannot be casted, then |
|||
# your property is probably from another type |
|||
continue |
|||
domain += [ |
|||
"&", |
|||
("field_type", "=", fmt), |
|||
("value_" + fmt, operator, _value), |
|||
] |
|||
return ["|"] * (len(domain) / 3 - 1) + domain |
@ -0,0 +1,22 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html |
|||
|
|||
from openerp import fields, models |
|||
|
|||
|
|||
class ResPartner(models.Model): |
|||
"""Implement custom information for partners. |
|||
|
|||
Besides adding some visible feature to the module, this is useful for |
|||
testing and example purposes. |
|||
""" |
|||
_name = "res.partner" |
|||
_inherit = [_name, "custom.info"] |
|||
|
|||
custom_info_template_id = fields.Many2one( |
|||
context={"default_model": _name}, |
|||
) |
|||
custom_info_ids = fields.One2many( |
|||
context={"default_model": _name}, |
|||
) |
@ -1,7 +1,6 @@ |
|||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" |
|||
"access_custom_info_template_user","custom.info.template.user","model_custom_info_template","base.group_user",1,0,0,0 |
|||
"access_custom_info_property_user","custom.info.template.line.user","model_custom_info_property","base.group_user",1,0,0,0 |
|||
"access_custom_info_value_user","custom.info.value.user","model_custom_info_value","base.group_user",1,0,0,0 |
|||
"access_custom_info_template_sale_manager","custom.info.template.salemanager","model_custom_info_template","base.group_system",1,1,1,1 |
|||
"access_custom_info_property_sale_manager","custom.info.template.line.salemanager","model_custom_info_property","base.group_system",1,1,1,1 |
|||
"access_custom_info_value_sale_manager","custom.info.value.salemanager","model_custom_info_value","base.group_system",1,1,1,1 |
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink |
|||
access_template,Access custom info templates,model_custom_info_template,,1,1,1,1 |
|||
access_property,Access custom info properties,model_custom_info_property,,1,1,1,1 |
|||
access_value,Access custom info values,model_custom_info_value,,1,1,1,1 |
|||
access_option,Access custom info options,model_custom_info_option,,1,1,1,1 |
|||
access_category,Access custom info categories,model_custom_info_category,,1,1,1,1 |
@ -0,0 +1,31 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). --> |
|||
|
|||
<odoo> |
|||
|
|||
<record id="category" model="ir.module.category"> |
|||
<field name="name">Custom Information</field> |
|||
</record> |
|||
|
|||
<record id="group_partner" model="res.groups"> |
|||
<field name="name">Display in partner form</field> |
|||
<field name="category_id" ref="category"/> |
|||
<field name="comment">Will be able to edit custom information from partner's form.</field> |
|||
</record> |
|||
|
|||
<record id="group_basic" model="res.groups"> |
|||
<field name="name">Basic management</field> |
|||
<field name="category_id" ref="category"/> |
|||
<field name="comment">The user will be able to manage basic custom information.</field> |
|||
</record> |
|||
|
|||
<record id="group_advanced" model="res.groups"> |
|||
<field name="name">Advanced management</field> |
|||
<field name="category_id" ref="category"/> |
|||
<field name="comment">The user will be able to manage advanced custom information.</field> |
|||
<field name="implied_ids" eval="[(4, ref('group_basic'))]"/> |
|||
<field name="users" eval="[(4, ref('base.user_root'))]"/> |
|||
</record> |
|||
|
|||
</odoo> |
@ -0,0 +1,5 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
|||
|
|||
from . import test_partner, test_value_conversion |
@ -0,0 +1,119 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
|||
|
|||
from openerp.exceptions import AccessError, ValidationError |
|||
from openerp.tests.common import TransactionCase |
|||
|
|||
|
|||
class PartnerCase(TransactionCase): |
|||
def setUp(self, *args, **kwargs): |
|||
super(PartnerCase, self).setUp(*args, **kwargs) |
|||
self.agrolait = self.env.ref("base.res_partner_2") |
|||
self.tpl = self.env.ref("base_custom_info.tpl_smart") |
|||
self.demouser = self.env.ref("base.user_demo") |
|||
|
|||
def set_custom_info_for_agrolait(self): |
|||
"""Used when you need to use some created custom info.""" |
|||
self.agrolait.custom_info_template_id = self.tpl |
|||
self.env["custom.info.value"].create({ |
|||
"res_id": self.agrolait.id, |
|||
"property_id": self.env.ref("base_custom_info.prop_haters").id, |
|||
"value_int": 5, |
|||
}) |
|||
|
|||
def test_access_granted(self): |
|||
"""Access to the model implies access to custom info.""" |
|||
# Demo user has contact creation permissions by default |
|||
agrolait = self.agrolait.sudo(self.demouser) |
|||
agrolait.custom_info_template_id = self.tpl |
|||
agrolait.env["custom.info.value"].create({ |
|||
"res_id": agrolait.id, |
|||
"property_id": |
|||
agrolait.env.ref("base_custom_info.prop_weaknesses").id, |
|||
"value_id": agrolait.env.ref("base_custom_info.opt_food").id, |
|||
}) |
|||
agrolait.custom_info_template_id.property_ids[0].name = "Changed!" |
|||
agrolait.env.ref("base_custom_info.opt_food").name = "Changed!" |
|||
|
|||
def test_access_denied(self): |
|||
"""Forbidden access to the model forbids it to custom info.""" |
|||
# Remove permissions to demo user |
|||
self.demouser.groups_id = self.env.ref("base.group_portal") |
|||
|
|||
agrolait = self.agrolait.sudo(self.demouser) |
|||
with self.assertRaises(AccessError): |
|||
agrolait.custom_info_template_id = self.tpl |
|||
|
|||
with self.assertRaises(AccessError): |
|||
agrolait.env["custom.info.value"].create({ |
|||
"res_id": agrolait.id, |
|||
"property_id": |
|||
agrolait.env.ref("base_custom_info.prop_weaknesses").id, |
|||
"value_id": agrolait.env.ref("base_custom_info.opt_food").id, |
|||
}) |
|||
|
|||
with self.assertRaises(AccessError): |
|||
agrolait.custom_info_template_id.property_ids[0].name = "Changed!" |
|||
|
|||
with self.assertRaises(AccessError): |
|||
agrolait.env.ref("base_custom_info.opt_food").name = "Changed!" |
|||
|
|||
def test_apply_unapply_template(self): |
|||
"""(Un)apply a template to a owner and it gets filled.""" |
|||
# Applying a template autofills the values |
|||
self.agrolait.custom_info_template_id = self.tpl |
|||
with self.env.do_in_onchange(): |
|||
self.agrolait._onchange_custom_info_template_id() |
|||
self.assertEqual( |
|||
len(self.agrolait.custom_info_ids), |
|||
len(self.tpl.property_ids)) |
|||
self.assertEqual( |
|||
self.agrolait.custom_info_ids.mapped("property_id"), |
|||
self.tpl.property_ids) |
|||
|
|||
# Unapplying a template empties the values |
|||
self.agrolait.custom_info_template_id = False |
|||
self.agrolait._onchange_custom_info_template_id() |
|||
self.assertFalse(self.agrolait.custom_info_template_id) |
|||
|
|||
def test_template_model_and_model_id_match(self): |
|||
"""Template's model and model_id fields match.""" |
|||
self.assertEqual(self.tpl.model, self.tpl.model_id.model) |
|||
self.tpl.model = "res.users" |
|||
self.assertEqual(self.tpl.model, self.tpl.model_id.model) |
|||
|
|||
def test_template_model_must_exist(self): |
|||
"""Cannot create templates for unexisting models.""" |
|||
with self.assertRaises(ValidationError): |
|||
self.tpl.model = "yabadabaduu" |
|||
|
|||
def test_change_used_model_fails(self): |
|||
"""If a template's model is already used, you cannot change it.""" |
|||
self.set_custom_info_for_agrolait() |
|||
with self.assertRaises(ValidationError): |
|||
self.tpl.model = "res.users" |
|||
|
|||
def test_owners_selection(self): |
|||
"""Owners selection includes only the required matches.""" |
|||
choices = dict(self.env["custom.info.value"]._selection_owner_id()) |
|||
self.assertIn("res.partner", choices) |
|||
self.assertNotIn("ir.model", choices) |
|||
self.assertNotIn("custom.info.property", choices) |
|||
self.assertNotIn("custom.info", choices) |
|||
|
|||
def test_owner_id(self): |
|||
"""Check the computed owner id for a value.""" |
|||
self.set_custom_info_for_agrolait() |
|||
self.assertEqual(self.agrolait.custom_info_ids.owner_id, self.agrolait) |
|||
|
|||
def test_get_custom_info_value(self): |
|||
"""Check the custom info getter helper works fine.""" |
|||
self.set_custom_info_for_agrolait() |
|||
result = self.agrolait.get_custom_info_value( |
|||
self.env.ref("base_custom_info.prop_haters")) |
|||
self.assertEqual(result.field_type, "int") |
|||
self.assertEqual(result.field_name, "value_int") |
|||
self.assertEqual(result[result.field_name], 5) |
|||
self.assertEqual(result.value_int, 5) |
|||
self.assertEqual(result.value, "5") |
@ -0,0 +1,131 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
|||
import logging |
|||
|
|||
from openerp.tests.common import TransactionCase |
|||
|
|||
_logger = logging.getLogger(__name__) |
|||
|
|||
|
|||
class ValueConversionCase(TransactionCase): |
|||
def setUp(self): |
|||
super(ValueConversionCase, self).setUp() |
|||
self.agrolait = self.env.ref("base.res_partner_2") |
|||
self.tpl = self.env.ref("base_custom_info.tpl_smart") |
|||
self.prop_str = self.env.ref("base_custom_info.prop_teacher") |
|||
self.prop_int = self.env.ref("base_custom_info.prop_haters") |
|||
self.prop_float = self.env.ref("base_custom_info.prop_avg_note") |
|||
self.prop_bool = self.env.ref("base_custom_info.prop_smartypants") |
|||
self.prop_id = self.env.ref("base_custom_info.prop_weaknesses") |
|||
|
|||
def create_value(self, prop, value, field="value"): |
|||
"""Create a custom info value.""" |
|||
_logger.info( |
|||
"Creating. prop: %s; value: %s; field: %s", prop, value, field) |
|||
self.agrolait.custom_info_template_id = self.tpl |
|||
if field == "value": |
|||
value = str(value) |
|||
self.value = self.env["custom.info.value"].create({ |
|||
"res_id": self.agrolait.id, |
|||
"property_id": prop.id, |
|||
field: value, |
|||
}) |
|||
|
|||
def creation_found(self, value): |
|||
"""Ensure you can search what you just created.""" |
|||
prop = self.value.property_id |
|||
_logger.info( |
|||
"Searching. prop: %s; value: %s", prop, value) |
|||
self.assertEqual( |
|||
self.value.search([ |
|||
("property_id", "=", prop.id), |
|||
("value", "=", value)]), |
|||
self.value) |
|||
self.assertEqual( |
|||
self.value.search([ |
|||
("property_id", "=", prop.id), |
|||
("value", "in", [value])]), |
|||
self.value) |
|||
self.assertIs( |
|||
self.value.search([ |
|||
("property_id", "=", prop.id), |
|||
("value", "not in", [value])]).id, |
|||
False) |
|||
|
|||
def test_to_str(self): |
|||
"""Conversion to text.""" |
|||
self.create_value(self.prop_str, "Mr. Einstein") |
|||
self.creation_found("Mr. Einstein") |
|||
self.assertEqual(self.value.value, self.value.value_str) |
|||
|
|||
def test_from_str(self): |
|||
"""Conversion from text.""" |
|||
self.create_value(self.prop_str, "Mr. Einstein", "value_str") |
|||
self.creation_found("Mr. Einstein") |
|||
self.assertEqual(self.value.value, self.value.value_str) |
|||
|
|||
def test_to_int(self): |
|||
"""Conversion to whole number.""" |
|||
self.create_value(self.prop_int, 5) |
|||
self.creation_found("5") |
|||
self.assertEqual(int(self.value.value), self.value.value_int) |
|||
|
|||
def test_from_int(self): |
|||
"""Conversion from whole number.""" |
|||
self.create_value(self.prop_int, 5, "value_int") |
|||
self.creation_found("5") |
|||
self.assertEqual(int(self.value.value), self.value.value_int) |
|||
|
|||
def test_to_float(self): |
|||
"""Conversion to decimal number.""" |
|||
self.create_value(self.prop_float, 10.5) |
|||
self.creation_found("10.5") |
|||
self.assertEqual(float(self.value.value), self.value.value_float) |
|||
|
|||
def test_from_float(self): |
|||
"""Conversion from decimal number.""" |
|||
self.create_value(self.prop_float, 10.5, "value_float") |
|||
self.creation_found("10.5") |
|||
self.assertEqual(float(self.value.value), self.value.value_float) |
|||
|
|||
def test_to_bool_true(self): |
|||
"""Conversion to yes.""" |
|||
self.create_value(self.prop_bool, "True") |
|||
self.creation_found("True") |
|||
self.assertEqual(self.value.with_context(lang="en_US").value, "Yes") |
|||
self.assertIs(self.value.value_bool, True) |
|||
|
|||
def test_from_bool_true(self): |
|||
"""Conversion from yes.""" |
|||
self.create_value(self.prop_bool, "True", "value_bool") |
|||
self.creation_found("True") |
|||
self.assertEqual(self.value.with_context(lang="en_US").value, "Yes") |
|||
self.assertIs(self.value.value_bool, True) |
|||
|
|||
def test_to_bool_false(self): |
|||
"""Conversion to no.""" |
|||
self.create_value(self.prop_bool, "False") |
|||
self.assertEqual(self.value.with_context(lang="en_US").value, "No") |
|||
self.assertIs(self.value.value_bool, False) |
|||
|
|||
def test_from_bool_false(self): |
|||
"""Conversion from no.""" |
|||
self.create_value(self.prop_bool, False, "value_bool") |
|||
self.assertEqual(self.value.with_context(lang="en_US").value, "No") |
|||
self.assertIs(self.value.value_bool, False) |
|||
|
|||
def test_to_id(self): |
|||
"""Conversion to selection.""" |
|||
self.create_value(self.prop_id, "Needs videogames") |
|||
self.creation_found("Needs videogames") |
|||
self.assertEqual(self.value.value, self.value.value_id.name) |
|||
|
|||
def test_from_id(self): |
|||
"""Conversion from selection.""" |
|||
self.create_value( |
|||
self.prop_id, |
|||
self.env.ref("base_custom_info.opt_videogames").id, |
|||
"value_id") |
|||
self.creation_found("Needs videogames") |
|||
self.assertEqual(self.value.value, self.value.value_id.name) |
@ -0,0 +1,53 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). --> |
|||
<odoo> |
|||
|
|||
<record id="custom_info_category_tree" model="ir.ui.view"> |
|||
<field name="name">Custom Info Category Tree</field> |
|||
<field name="model">custom.info.category</field> |
|||
<field name="arch" type="xml"> |
|||
<tree string="Custom Info Categories"> |
|||
<field name="sequence" widget="handle"/> |
|||
<field name="name"/> |
|||
<field name="property_ids"/> |
|||
</tree> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="custom_info_category_form" model="ir.ui.view"> |
|||
<field name="name">Custom Info Category Form</field> |
|||
<field name="model">custom.info.category</field> |
|||
<field name="arch" type="xml"> |
|||
<form string="Custom Info Template Properties"> |
|||
<sheet> |
|||
<group> |
|||
<field name="name"/> |
|||
<field name="sequence"/> |
|||
<field name="property_ids"/> |
|||
</group> |
|||
</sheet> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="custom_info_category_search" model="ir.ui.view"> |
|||
<field name="name">Custom Info Category Search</field> |
|||
<field name="model">custom.info.category</field> |
|||
<field name="arch" type="xml"> |
|||
<search> |
|||
<field name="name"/> |
|||
<field name="property_ids"/> |
|||
</search> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="custom_info_category_action" model="ir.actions.act_window"> |
|||
<field name="name">Categories</field> |
|||
<field name="type">ir.actions.act_window</field> |
|||
<field name="res_model">custom.info.category</field> |
|||
<field name="view_mode">tree,form</field> |
|||
<field name="view_type">form</field> |
|||
</record> |
|||
|
|||
</odoo> |
@ -0,0 +1,52 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). --> |
|||
<odoo> |
|||
|
|||
<record id="custom_info_option_tree" model="ir.ui.view"> |
|||
<field name="name">Custom Info Option Tree</field> |
|||
<field name="model">custom.info.option</field> |
|||
<field name="arch" type="xml"> |
|||
<tree string="Custom Info Options"> |
|||
<field name="name"/> |
|||
<field name="property_ids"/> |
|||
</tree> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="custom_info_option_form" model="ir.ui.view"> |
|||
<field name="name">Custom Info Option Form</field> |
|||
<field name="model">custom.info.option</field> |
|||
<field name="arch" type="xml"> |
|||
<form string="Custom Info Template Properties"> |
|||
<sheet> |
|||
<group> |
|||
<field name="name"/> |
|||
<field name="property_ids"/> |
|||
<field name="value_ids"/> |
|||
</group> |
|||
</sheet> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="custom_info_option_search" model="ir.ui.view"> |
|||
<field name="name">Custom Info Option Search</field> |
|||
<field name="model">custom.info.option</field> |
|||
<field name="arch" type="xml"> |
|||
<search> |
|||
<field name="name"/> |
|||
<field name="property_ids"/> |
|||
</search> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="custom_info_option_action" model="ir.actions.act_window"> |
|||
<field name="name">Options</field> |
|||
<field name="type">ir.actions.act_window</field> |
|||
<field name="res_model">custom.info.option</field> |
|||
<field name="view_mode">tree,form</field> |
|||
<field name="view_type">form</field> |
|||
</record> |
|||
|
|||
</odoo> |
@ -1,23 +1,41 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<openerp> |
|||
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). --> |
|||
<odoo> |
|||
|
|||
<!--Parent Custom Info in Settings--> |
|||
<!--Base menus --> |
|||
<menuitem id="menu_base_custom_info" name="Custom Info" |
|||
parent="base.menu_administration" sequence="45"/> |
|||
groups="base_custom_info.group_basic"/> |
|||
|
|||
<menuitem |
|||
id="menu_basic" |
|||
name="Basic" |
|||
parent="menu_base_custom_info"/> |
|||
|
|||
<menuitem |
|||
id="menu_advanced" |
|||
name="Advanced" |
|||
groups="base_custom_info.group_basic" |
|||
parent="menu_base_custom_info"/> |
|||
|
|||
<!--base.custom.info.template--> |
|||
<menuitem id="menu_base_custom_info_template" |
|||
action="custom_info_template_action" |
|||
parent="menu_base_custom_info" sequence="5"/> |
|||
|
|||
<!--base.custom.info.template.line--> |
|||
<menuitem id="menu_base_custom_info_template_line" |
|||
action="custom_info_template_line_action" |
|||
parent="menu_base_custom_info" sequence="10"/> |
|||
parent="menu_basic" sequence="10"/> |
|||
|
|||
<!--base.custom.info.value--> |
|||
<menuitem id="menu_base_custom_info_value" |
|||
action="custom_info_value_action" |
|||
parent="menu_base_custom_info" sequence="15"/> |
|||
parent="menu_basic" sequence="20"/> |
|||
|
|||
<menuitem id="menu_category" |
|||
action="custom_info_category_action" |
|||
parent="menu_advanced" sequence="10"/> |
|||
|
|||
<menuitem id="menu_property" |
|||
action="custom_info_property_action" |
|||
parent="menu_advanced" sequence="20"/> |
|||
|
|||
<menuitem id="menu_option" |
|||
action="custom_info_option_action" |
|||
parent="menu_advanced" sequence="30"/> |
|||
|
|||
</openerp> |
|||
</odoo> |
@ -0,0 +1,28 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). --> |
|||
<odoo> |
|||
|
|||
<record id="view_partner_form" model="ir.ui.view"> |
|||
<field name="name">Custom info in partners form</field> |
|||
<field name="model">res.partner</field> |
|||
<field name="inherit_id" ref="base.view_partner_form"/> |
|||
<field name="arch" type="xml"> |
|||
<xpath expr="//notebook[1]"> |
|||
<page name="custom_info" |
|||
string="Custom Information" |
|||
groups="base_custom_info.group_partner"> |
|||
<group> |
|||
<field |
|||
name="custom_info_template_id" |
|||
options='{"no_quick_create": True}'/> |
|||
<field |
|||
name="custom_info_ids" |
|||
attrs="{'invisible': [('custom_info_template_id', '=', False)]}"/> |
|||
</group> |
|||
</page> |
|||
</xpath> |
|||
</field> |
|||
</record> |
|||
|
|||
</odoo> |
@ -0,0 +1,6 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# © 2015 Antiun Ingeniería S.L. - Sergio Teruel |
|||
# © 2015 Antiun Ingeniería S.L. - Carlos Dauden |
|||
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html |
|||
|
|||
from . import base_config_settings |
@ -0,0 +1,20 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html |
|||
|
|||
from openerp import fields, models |
|||
|
|||
|
|||
class BaseConfigSettings(models.TransientModel): |
|||
_inherit = "base.config.settings" |
|||
|
|||
group_custom_info_manager = fields.Boolean( |
|||
string="Manage custom information", |
|||
implied_group="base_custom_info.group_basic", |
|||
help="Allow all employees to manage custom information", |
|||
) |
|||
group_custom_info_partner = fields.Boolean( |
|||
string="Edit custom information in partners", |
|||
implied_group="base_custom_info.group_partner", |
|||
help="Add a tab in the partners form to edit custom information", |
|||
) |
@ -0,0 +1,33 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). --> |
|||
<odoo> |
|||
|
|||
<record id="view_general_configuration" model="ir.ui.view"> |
|||
<field name="name">Allow to enable partners custom info</field> |
|||
<field name="model">base.config.settings</field> |
|||
<field name="inherit_id" ref="base_setup.view_general_configuration"/> |
|||
<field name="arch" type="xml"> |
|||
<xpath expr="//group[@name='authentication']" position="after"> |
|||
<group name="custom_info"> |
|||
<group> |
|||
<label for="id" string="Custom Information"/> |
|||
<div> |
|||
<div> |
|||
<field name="group_custom_info_partner" |
|||
class="oe_inline"/> |
|||
<label for="group_custom_info_partner"/> |
|||
</div> |
|||
<div> |
|||
<field name="group_custom_info_manager" |
|||
class="oe_inline"/> |
|||
<label for="group_custom_info_manager"/> |
|||
</div> |
|||
</div> |
|||
</group> |
|||
</group> |
|||
</xpath> |
|||
</field> |
|||
</record> |
|||
|
|||
</odoo> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue