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 -*- |
# -*- coding: utf-8 -*- |
||||
# © 2015 Antiun Ingeniería S.L. - Sergio Teruel |
# © 2015 Antiun Ingeniería S.L. - Sergio Teruel |
||||
# © 2015 Antiun Ingeniería S.L. - Carlos Dauden |
# © 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 |
# 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"?> |
<?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" |
<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" |
<menuitem id="menu_base_custom_info_template" |
||||
action="custom_info_template_action" |
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" |
<menuitem id="menu_base_custom_info_value" |
||||
action="custom_info_value_action" |
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