Browse Source
Merge pull request #492 from Tecnativa/9.0-base_custom_info-field_types
Merge pull request #492 from Tecnativa/9.0-base_custom_info-field_types
[9.0][IMP][base_custom_info] New types and access rules system.pull/847/head
Pedro M. Baeza
8 years ago
committed by
GitHub
38 changed files with 2342 additions and 314 deletions
-
190base_custom_info/README.rst
-
4base_custom_info/__init__.py
-
30base_custom_info/__openerp__.py
-
3base_custom_info/demo/custom.info.category.csv
-
10base_custom_info/demo/custom.info.option.csv
-
8base_custom_info/demo/custom.info.property.csv
-
3base_custom_info/demo/custom.info.template.csv
-
6base_custom_info/demo/custom_info_property_defaults.yml
-
12base_custom_info/demo/res_groups.xml
-
576base_custom_info/i18n/es.po
-
BINbase_custom_info/images/customizations-everywhere.jpg
-
BINbase_custom_info/images/templateception.jpg
-
10base_custom_info/migrations/9.0.2.0.0/pre-migrate.py
-
13base_custom_info/models/__init__.py
-
167base_custom_info/models/custom_info.py
-
34base_custom_info/models/custom_info_category.py
-
45base_custom_info/models/custom_info_option.py
-
113base_custom_info/models/custom_info_property.py
-
76base_custom_info/models/custom_info_template.py
-
241base_custom_info/models/custom_info_value.py
-
19base_custom_info/models/res_partner.py
-
13base_custom_info/security/ir.model.access.csv
-
31base_custom_info/security/res_groups.xml
-
BINbase_custom_info/static/description/icon.png
-
82base_custom_info/static/description/icon.svg
-
5base_custom_info/tests/__init__.py
-
171base_custom_info/tests/test_partner.py
-
129base_custom_info/tests/test_value_conversion.py
-
50base_custom_info/views/custom_info_category_view.xml
-
79base_custom_info/views/custom_info_option_view.xml
-
88base_custom_info/views/custom_info_property_view.xml
-
46base_custom_info/views/custom_info_template_view.xml
-
103base_custom_info/views/custom_info_value_view.xml
-
50base_custom_info/views/menu.xml
-
32base_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
@ -1,6 +1,6 @@ |
|||||
# -*- 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 |
||||
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html |
|
||||
|
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html |
||||
|
|
||||
from . import models |
|
||||
|
from . import models, wizard |
@ -0,0 +1,3 @@ |
|||||
|
id,name,sequence |
||||
|
cat_statics,Statistics,50 |
||||
|
cat_gaming,Gaming,100 |
@ -0,0 +1,10 @@ |
|||||
|
id,name,property_ids:id,template_id:id |
||||
|
opt_food,Loves junk food,prop_weaknesses, |
||||
|
opt_videogames,Needs videogames,prop_weaknesses,tpl_gamer |
||||
|
opt_glasses,Huge glasses,prop_weaknesses, |
||||
|
opt_shooter,Shooter,prop_fav_genre, |
||||
|
opt_platforms,Platforms,prop_fav_genre, |
||||
|
opt_cars,Cars,prop_fav_genre, |
||||
|
opt_rpg,RPG,prop_fav_genre, |
||||
|
opt_strategy,Strategy,prop_fav_genre, |
||||
|
opt_graphical_adventure,Graphical adventure,prop_fav_genre, |
@ -0,0 +1,8 @@ |
|||||
|
id,name,template_id:id,field_type,required,minimum,maximum,category_id:id,sequence |
||||
|
prop_teacher,Name of his/her teacher,tpl_smart,str,,1,30,,100 |
||||
|
prop_haters,Amount of people that hates him/her for being so smart,tpl_smart,int,,0,99999,cat_statics,200 |
||||
|
prop_avg_note,Average note on all subjects,tpl_smart,float,True,0,10,cat_statics,300 |
||||
|
prop_smartypants,Does he/she believe he/she is the smartest person on earth?,tpl_smart,bool,,0,-1,,400 |
||||
|
prop_weaknesses,What weaknesses does he/she have?,tpl_smart,id,,0,-1,,500 |
||||
|
prop_fav_genre,Favourite videogames genre,tpl_gamer,id,,0,-1,cat_gaming,600 |
||||
|
prop_fav_game,Favourite videogame,tpl_gamer,str,,0,-1,cat_gaming,700 |
@ -0,0 +1,3 @@ |
|||||
|
id,name,model,model_id:id |
||||
|
tpl_smart,Smart partners,res.partner,base.model_res_partner |
||||
|
tpl_gamer,Gamers,res.partner,base.model_res_partner |
@ -0,0 +1,6 @@ |
|||||
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
- Setting default values after loading custom.info.option.csv |
||||
|
|
||||
|
- !record {model: custom.info.property, id: prop_weaknesses}: |
||||
|
default_value: Huge glasses |
@ -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> |
After Width: 500 | Height: 380 | Size: 120 KiB |
After Width: 318 | Height: 240 | Size: 57 KiB |
@ -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 |
||||
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html |
|
||||
|
# © 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, |
||||
|
) |
@ -1,126 +1,97 @@ |
|||||
# -*- coding: utf-8 -*- |
# -*- coding: utf-8 -*- |
||||
# © 2015 Antiun Ingeniería S.L. - Sergio Teruel |
|
||||
# © 2015 Antiun Ingeniería S.L. - Carlos Dauden |
|
||||
# © 2015 Antiun Ingeniería S.L. - Jairo Llopis |
|
||||
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html |
|
||||
|
# Copyright 2015 Sergio Teruel <sergio.teruel@tecnativa.com> |
||||
|
# Copyright 2015 Carlos Dauden <carlos.dauden@tecnativa.com> |
||||
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
||||
|
# Copyright 2017 Pedro M. Baeza <pedro.baeza@tecnativa.com> |
||||
|
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html |
||||
|
|
||||
from openerp import api, fields, models |
from openerp import api, fields, models |
||||
|
|
||||
|
|
||||
class CustomInfoModelLink(models.AbstractModel): |
|
||||
_description = "A model that gets its ``ir.model`` computed" |
|
||||
_name = "custom.info.model_link" |
|
||||
|
|
||||
model = fields.Char( |
|
||||
index=True, |
|
||||
readonly=True, |
|
||||
required=True) |
|
||||
model_id = fields.Many2one( |
|
||||
'ir.model', |
|
||||
'Model', |
|
||||
compute="_compute_model_id", |
|
||||
store=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)]) |
|
||||
|
|
||||
|
|
||||
class CustomInfoTemplate(models.Model): |
|
||||
"""Defines custom properties expected for a given database object.""" |
|
||||
_description = "Custom information template" |
|
||||
_name = "custom.info.template" |
|
||||
_inherit = "custom.info.model_link" |
|
||||
_sql_constraints = [ |
|
||||
("name_model", |
|
||||
"UNIQUE (name, model)", |
|
||||
"Another template with that name exists for that model."), |
|
||||
] |
|
||||
|
|
||||
name = fields.Char(required=True, translate=True) |
|
||||
info_ids = fields.One2many( |
|
||||
'custom.info.property', |
|
||||
'template_id', |
|
||||
'Properties') |
|
||||
|
|
||||
|
|
||||
class CustomInfoProperty(models.Model): |
|
||||
"""Name of the custom information property.""" |
|
||||
_description = "Custom information property" |
|
||||
_name = "custom.info.property" |
|
||||
_sql_constraints = [ |
|
||||
("name_template", |
|
||||
"UNIQUE (name, template_id)", |
|
||||
"Another property with that name exists for that template."), |
|
||||
] |
|
||||
|
|
||||
name = fields.Char(required=True, translate=True) |
|
||||
template_id = fields.Many2one( |
|
||||
comodel_name='custom.info.template', |
|
||||
string='Template', |
|
||||
required=True) |
|
||||
info_value_ids = fields.One2many( |
|
||||
comodel_name="custom.info.value", |
|
||||
inverse_name="property_id", |
|
||||
string="Property Values") |
|
||||
|
|
||||
|
|
||||
class CustomInfoValue(models.Model): |
|
||||
_description = "Custom information value" |
|
||||
_name = "custom.info.value" |
|
||||
_inherit = "custom.info.model_link" |
|
||||
_rec_name = 'value' |
|
||||
_sql_constraints = [ |
|
||||
("property_model_res", |
|
||||
"UNIQUE (property_id, model, res_id)", |
|
||||
"Another property with that name exists for that resource."), |
|
||||
] |
|
||||
|
|
||||
res_id = fields.Integer("Resource ID", index=True, required=True) |
|
||||
property_id = fields.Many2one( |
|
||||
comodel_name='custom.info.property', |
|
||||
required=True, |
|
||||
string='Property') |
|
||||
name = fields.Char(related='property_id.name', readonly=True) |
|
||||
value = fields.Char(translate=True, index=True) |
|
||||
|
class CustomInfo(models.AbstractModel): |
||||
|
"""Models that inherit from this one will get custom information for free! |
||||
|
|
||||
|
They will probably want to declare a default model in the context of the |
||||
|
:attr:`custom_info_template_id` field. |
||||
|
|
||||
class CustomInfo(models.AbstractModel): |
|
||||
|
See example in :mod:`res_partner`. |
||||
|
""" |
||||
_description = "Inheritable abstract model to add custom info in any model" |
_description = "Inheritable abstract model to add custom info in any model" |
||||
_name = "custom.info" |
_name = "custom.info" |
||||
|
|
||||
custom_info_template_id = fields.Many2one( |
custom_info_template_id = fields.Many2one( |
||||
comodel_name='custom.info.template', |
comodel_name='custom.info.template', |
||||
string='Custom Information Template') |
|
||||
|
domain=lambda self: [("model", "=", self._name)], |
||||
|
string='Custom Information Template', |
||||
|
) |
||||
custom_info_ids = fields.One2many( |
custom_info_ids = fields.One2many( |
||||
comodel_name='custom.info.value', |
|
||||
inverse_name='res_id', |
|
||||
|
comodel_name='custom.info.value', inverse_name='res_id', |
||||
domain=lambda self: [("model", "=", self._name)], |
domain=lambda self: [("model", "=", self._name)], |
||||
auto_join=True, |
|
||||
string='Custom Properties') |
|
||||
|
auto_join=True, string='Custom Properties', |
||||
|
) |
||||
|
|
||||
|
# HACK: Until https://github.com/odoo/odoo/pull/10557 is merged |
||||
|
# https://github.com/OCA/server-tools/pull/492#issuecomment-237594285 |
||||
@api.multi |
@api.multi |
||||
|
def onchange(self, values, field_name, field_onchange): # pragma: no cover |
||||
|
x2many_field = 'custom_info_ids' |
||||
|
if x2many_field in field_onchange: |
||||
|
subfields = getattr(self, x2many_field)._fields.keys() |
||||
|
for subfield in subfields: |
||||
|
field_onchange.setdefault( |
||||
|
u"{}.{}".format(x2many_field, subfield), u"", |
||||
|
) |
||||
|
return super(CustomInfo, self).onchange( |
||||
|
values, field_name, field_onchange, |
||||
|
) |
||||
|
|
||||
@api.onchange('custom_info_template_id') |
@api.onchange('custom_info_template_id') |
||||
def _onchange_custom_info_template_id(self): |
def _onchange_custom_info_template_id(self): |
||||
if not self.custom_info_template_id: |
|
||||
self.custom_info_ids = False |
|
||||
else: |
|
||||
info_list = self.custom_info_ids.mapped('property_id') |
|
||||
for info_name in self.custom_info_template_id.info_ids: |
|
||||
if info_name not in info_list: |
|
||||
self.custom_info_ids |= self.custom_info_ids.new({ |
|
||||
'model': self._name, |
|
||||
'property_id': info_name.id, |
|
||||
|
tmpls = self.all_custom_info_templates() |
||||
|
props_good = tmpls.mapped("property_ids") |
||||
|
props_enabled = self.mapped("custom_info_ids.property_id") |
||||
|
to_add = props_good - props_enabled |
||||
|
to_remove = props_enabled - props_good |
||||
|
values = self.custom_info_ids |
||||
|
values = values.filtered(lambda r: r.property_id not in to_remove) |
||||
|
for prop in to_add.sorted(): |
||||
|
newvalue = self.custom_info_ids.new({ |
||||
|
"property_id": prop.id, |
||||
"res_id": self.id, |
"res_id": self.id, |
||||
|
"value": prop.default_value, |
||||
}) |
}) |
||||
|
# HACK https://github.com/odoo/odoo/issues/13076 |
||||
|
newvalue._inverse_value() |
||||
|
newvalue._compute_value() |
||||
|
values += newvalue |
||||
|
self.custom_info_ids = values |
||||
|
# Default values implied new templates? Then this is recursive |
||||
|
if self.all_custom_info_templates() != tmpls: |
||||
|
self._onchange_custom_info_template_id() |
||||
|
|
||||
@api.multi |
@api.multi |
||||
def unlink(self): |
def unlink(self): |
||||
|
"""Remove linked custom info this way, as can't be handled |
||||
|
automatically. |
||||
|
""" |
||||
info_values = self.mapped('custom_info_ids') |
info_values = self.mapped('custom_info_ids') |
||||
res = super(CustomInfo, self).unlink() |
res = super(CustomInfo, self).unlink() |
||||
if res: |
if res: |
||||
info_values.unlink() |
info_values.unlink() |
||||
return res |
return res |
||||
|
|
||||
|
@api.multi |
||||
|
@api.returns("custom.info.value") |
||||
|
def get_custom_info_value(self, properties): |
||||
|
"""Get ``custom.info.value`` records for the given property.""" |
||||
|
return self.env["custom.info.value"].search([ |
||||
|
("model", "=", self._name), |
||||
|
("res_id", "in", self.ids), |
||||
|
("property_id", "in", properties.ids), |
||||
|
]) |
||||
|
|
||||
|
@api.multi |
||||
|
def all_custom_info_templates(self): |
||||
|
"""Get all custom info templates involved in these owners.""" |
||||
|
return (self.mapped("custom_info_template_id") | |
||||
|
self.mapped("custom_info_ids.value_id.template_id")) |
@ -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,45 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
||||
|
# Copyright 2017 Pedro M. Baeza <pedro.baeza@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.", |
||||
|
) |
||||
|
template_id = fields.Many2one( |
||||
|
comodel_name="custom.info.template", |
||||
|
string="Additional template", |
||||
|
help="Additional template to be applied to the owner if this option " |
||||
|
"is chosen.", |
||||
|
) |
||||
|
|
||||
|
@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 -*- |
||||
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
||||
|
# Copyright 2017 Pedro M. Baeza <pedro.baeza@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, ondelete="cascade", |
||||
|
) |
||||
|
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,76 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
||||
|
# Copyright 2017 Pedro M. Baeza <pedro.baeza@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_id)", |
||||
|
"Another template with that name exists for that model."), |
||||
|
] |
||||
|
|
||||
|
name = fields.Char(required=True, translate=True) |
||||
|
model = fields.Char( |
||||
|
string="Model technical name", inverse="_inverse_model", |
||||
|
compute="_compute_model", search="_search_model" |
||||
|
) |
||||
|
model_id = fields.Many2one( |
||||
|
comodel_name='ir.model', string='Model', ondelete="restrict", |
||||
|
required=True, auto_join=True, |
||||
|
) |
||||
|
property_ids = fields.One2many( |
||||
|
comodel_name='custom.info.property', inverse_name='template_id', |
||||
|
string='Properties', oldname="info_ids", |
||||
|
) |
||||
|
|
||||
|
@api.multi |
||||
|
@api.depends("model_id") |
||||
|
def _compute_model(self): |
||||
|
for r in self: |
||||
|
r.model = r.model_id.model |
||||
|
|
||||
|
@api.multi |
||||
|
def _inverse_model(self): |
||||
|
for r in self: |
||||
|
r.model_id = self.env["ir.model"].search([("model", "=", r.model)]) |
||||
|
|
||||
|
@api.model |
||||
|
def _search_model(self, operator, value): |
||||
|
models = self.env['ir.model'].search([('model', operator, value)]) |
||||
|
return [('model_id', 'in', models.ids)] |
||||
|
|
||||
|
@api.onchange('model') |
||||
|
def _onchange_model(self): |
||||
|
self._inverse_model() |
||||
|
|
||||
|
@api.multi |
||||
|
@api.constrains("model_id") |
||||
|
def _check_model(self): |
||||
|
"""Avoid error when updating base module and a submodule extends a |
||||
|
model that falls out of this one's dependency graph. |
||||
|
""" |
||||
|
for record in self: |
||||
|
with self.env.norecompute(): |
||||
|
oldmodels = record.mapped("property_ids.info_value_ids.model") |
||||
|
if oldmodels and record.model not in 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 record in self: |
||||
|
model = self.env[record.model_id.model or record.model] |
||||
|
model.check_access_rights(operation) |
||||
|
model.check_access_rule(operation) |
||||
|
return super(CustomInfoTemplate, self).check_access_rule(operation) |
@ -0,0 +1,241 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
||||
|
# Copyright 2017 Pedro M. Baeza <pedro.baeza@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( |
||||
|
string="Resource ID", required=True, index=True, store=True, |
||||
|
ondelete="cascade", |
||||
|
) |
||||
|
property_id = fields.Many2one( |
||||
|
comodel_name='custom.info.property', required=True, string='Property', |
||||
|
readonly=True, |
||||
|
) |
||||
|
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", readonly=True, |
||||
|
) |
||||
|
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", readonly=True) |
||||
|
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 owner record.""" |
||||
|
if self.env.uid != SUPERUSER_ID: |
||||
|
for record in self.filtered('owner_id'): |
||||
|
record.owner_id.check_access_rights(operation) |
||||
|
record.owner_id.check_access_rule(operation) |
||||
|
return super(CustomInfoValue, self).check_access_rule(operation) |
||||
|
|
||||
|
@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 record in self: |
||||
|
record.owner_id = "{},{}".format(record.model, record.res_id) |
||||
|
|
||||
|
@api.multi |
||||
|
def _inverse_owner_id(self): |
||||
|
"""Store the owner according to the model and ID.""" |
||||
|
for record in self.filtered('owner_id'): |
||||
|
record.model = record.owner_id._name |
||||
|
record.res_id = record.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 = s.value_id.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 record in self: |
||||
|
if (record.field_type == "id" and |
||||
|
record.value == record.value_id.display_name): |
||||
|
# Avoid another search that can return a different value |
||||
|
continue |
||||
|
record[record.field_name] = self._transform_value( |
||||
|
record.value, record.field_type, record.property_id, |
||||
|
) |
||||
|
|
||||
|
@api.one |
||||
|
@api.constrains("property_id", "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.onchange('value') |
||||
|
def _onchange_value(self): |
||||
|
"""Inverse function is not launched after writing, so we need to |
||||
|
trigger it right now.""" |
||||
|
self._inverse_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", u"%{}%".format(value)), |
||||
|
], limit=1) |
||||
|
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,19 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
||||
|
# Copyright 2017 Pedro M. Baeza <pedro.baeza@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> |
After Width: 128 | Height: 128 | Size: 4.0 KiB |
@ -0,0 +1,82 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?> |
||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) --> |
||||
|
|
||||
|
<svg |
||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/" |
||||
|
xmlns:cc="http://creativecommons.org/ns#" |
||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" |
||||
|
xmlns:svg="http://www.w3.org/2000/svg" |
||||
|
xmlns="http://www.w3.org/2000/svg" |
||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" |
||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" |
||||
|
id="svg2" |
||||
|
version="1.1" |
||||
|
inkscape:version="0.91 r13725" |
||||
|
width="60" |
||||
|
height="60" |
||||
|
viewBox="0 0 60 60" |
||||
|
sodipodi:docname="icon.svg" |
||||
|
inkscape:export-filename="icon.png" |
||||
|
inkscape:export-xdpi="192" |
||||
|
inkscape:export-ydpi="192"> |
||||
|
<metadata |
||||
|
id="metadata8"> |
||||
|
<rdf:RDF> |
||||
|
<cc:Work |
||||
|
rdf:about=""> |
||||
|
<dc:format>image/svg+xml</dc:format> |
||||
|
<dc:type |
||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> |
||||
|
<dc:title /> |
||||
|
</cc:Work> |
||||
|
</rdf:RDF> |
||||
|
</metadata> |
||||
|
<defs |
||||
|
id="defs6" /> |
||||
|
<sodipodi:namedview |
||||
|
pagecolor="#ffffff" |
||||
|
bordercolor="#666666" |
||||
|
borderopacity="1" |
||||
|
objecttolerance="10" |
||||
|
gridtolerance="10" |
||||
|
guidetolerance="10" |
||||
|
inkscape:pageopacity="0" |
||||
|
inkscape:pageshadow="2" |
||||
|
inkscape:window-width="1855" |
||||
|
inkscape:window-height="1176" |
||||
|
id="namedview4" |
||||
|
showgrid="false" |
||||
|
inkscape:zoom="11.125147" |
||||
|
inkscape:cx="30.182701" |
||||
|
inkscape:cy="33.453159" |
||||
|
inkscape:window-x="65" |
||||
|
inkscape:window-y="24" |
||||
|
inkscape:window-maximized="1" |
||||
|
inkscape:current-layer="svg2" /> |
||||
|
<rect |
||||
|
style="opacity:1;fill:#9c0c65;fill-opacity:1" |
||||
|
id="rect4147" |
||||
|
width="60" |
||||
|
height="60" |
||||
|
x="0" |
||||
|
y="0" /> |
||||
|
<path |
||||
|
style="opacity:1;fill:#000000;fill-opacity:0.39215687" |
||||
|
d="M 46.318359 7.140625 L 26.125 27.333984 C 22.675251 26.053376 17.543435 26.175054 10.058594 28.617188 L 0 38.675781 L 0 60 L 19.734375 60 L 30.587891 49.146484 C 31.777187 45.669439 32.902469 40.559366 32.345703 36.074219 L 53.798828 14.621094 L 46.318359 7.140625 z " |
||||
|
id="rect4171" /> |
||||
|
<g |
||||
|
id="g4169" |
||||
|
transform="matrix(0.3061173,0,0,0.3061173,-1.0360053,-1.0457906)" |
||||
|
style="fill:#ffffff;stroke:none"> |
||||
|
<path |
||||
|
inkscape:connector-curvature="0" |
||||
|
id="path4" |
||||
|
d="m 63.145923,117.98015 9.436965,0 0,-9.43697 -9.436965,0 m 4.718483,61.34028 c -20.808512,0 -37.747871,-16.93935 -37.747871,-37.74786 0,-20.80852 16.939359,-37.747875 37.747871,-37.747875 20.808514,0 37.747864,16.939355 37.747864,37.747875 0,20.80851 -16.93935,37.74786 -37.747864,37.74786 m 0,-84.932702 A 47.18484,47.18484 0 0 0 20.679568,132.1356 47.18484,47.18484 0 0 0 67.864406,179.32044 47.18484,47.18484 0 0 0 115.04924,132.1356 47.18484,47.18484 0 0 0 67.864406,84.950758 m -4.718483,70.777262 9.436965,0 0,-28.31091 -9.436965,0 0,28.31091 z" |
||||
|
style="fill:#ffffff;stroke:none" /> |
||||
|
<path |
||||
|
style="fill:#ffffff;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" |
||||
|
id="path4-3" |
||||
|
d="m 161.99013,23.51547 c -2.44155,0 -4.8831,0.830128 -6.88517,2.8322 -14.0145,14.014501 -41.94585,41.945843 -41.94585,41.945843 l 7.32466,7.324652 -17.09086,17.090856 -9.766208,0 -9.766196,19.532409 9.766196,9.7662 19.532408,-9.7662 0,-9.76621 17.09086,-17.090851 7.32465,7.324652 c 0,0 27.93134,-27.931342 41.94585,-41.945843 3.02752,-4.443623 3.80881,-9.961528 0,-13.770347 L 168.8753,26.34767 c -2.00206,-2.002072 -4.44362,-2.8322 -6.88517,-2.8322 m 0,10.596331 9.7662,9.766203 -34.18171,34.181712 -9.7662,-9.766203 34.18171,-34.181712 z" |
||||
|
inkscape:connector-curvature="0" /> |
||||
|
</g> |
||||
|
</svg> |
@ -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,171 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). |
||||
|
|
||||
|
from psycopg2 import IntegrityError |
||||
|
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.agrolait._onchange_custom_info_template_id() |
||||
|
self.agrolait.get_custom_info_value( |
||||
|
self.env.ref("base_custom_info.prop_haters")).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._onchange_custom_info_template_id() |
||||
|
prop_weaknesses = agrolait.env.ref("base_custom_info.prop_weaknesses") |
||||
|
val_weaknesses = agrolait.get_custom_info_value(prop_weaknesses) |
||||
|
opt_food = agrolait.env.ref("base_custom_info.opt_food") |
||||
|
val_weaknesses.value_id = opt_food |
||||
|
agrolait.custom_info_template_id.name = "Changed template name" |
||||
|
opt_food.name = "Changed option name" |
||||
|
prop_weaknesses.name = "Changed property name" |
||||
|
|
||||
|
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 |
||||
|
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) |
||||
|
self.assertFalse(self.agrolait.custom_info_ids) |
||||
|
|
||||
|
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(IntegrityError): |
||||
|
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.mapped("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") |
||||
|
|
||||
|
def test_default_values(self): |
||||
|
"""Default values get applied.""" |
||||
|
self.agrolait.custom_info_template_id = self.tpl |
||||
|
self.agrolait._onchange_custom_info_template_id() |
||||
|
val_weaknesses = self.agrolait.get_custom_info_value( |
||||
|
self.env.ref("base_custom_info.prop_weaknesses")) |
||||
|
opt_glasses = self.env.ref("base_custom_info.opt_glasses") |
||||
|
self.assertEqual(val_weaknesses.value_id, opt_glasses) |
||||
|
self.assertEqual(val_weaknesses.value, opt_glasses.name) |
||||
|
|
||||
|
def test_recursive_templates(self): |
||||
|
"""Recursive templates get loaded when required.""" |
||||
|
self.set_custom_info_for_agrolait() |
||||
|
prop_weaknesses = self.env.ref("base_custom_info.prop_weaknesses") |
||||
|
val_weaknesses = self.agrolait.get_custom_info_value(prop_weaknesses) |
||||
|
val_weaknesses.value = "Needs videogames" |
||||
|
tpl_gamer = self.env.ref("base_custom_info.tpl_gamer") |
||||
|
self.agrolait.invalidate_cache() |
||||
|
self.assertIn(tpl_gamer, self.agrolait.all_custom_info_templates()) |
||||
|
self.agrolait._onchange_custom_info_template_id() |
||||
|
self.assertTrue( |
||||
|
tpl_gamer.property_ids < |
||||
|
self.agrolait.mapped("custom_info_ids.property_id")) |
||||
|
cat_gaming = self.env.ref("base_custom_info.cat_gaming") |
||||
|
self.assertIn( |
||||
|
cat_gaming, self.agrolait.mapped("custom_info_ids.category_id")) |
||||
|
|
||||
|
def test_long_teacher_name(self): |
||||
|
"""Wow, your teacher cannot have such a long name!""" |
||||
|
self.set_custom_info_for_agrolait() |
||||
|
val = self.agrolait.get_custom_info_value( |
||||
|
self.env.ref("base_custom_info.prop_teacher")) |
||||
|
with self.assertRaises(ValidationError): |
||||
|
val.value = (u"Don Walter Antonio José de la Cruz Hëisenberg de " |
||||
|
u"Borbón Westley Jordy López Manuélez") |
||||
|
|
||||
|
def test_low_average_note(self): |
||||
|
"""Come on, you are supposed to be smart!""" |
||||
|
self.set_custom_info_for_agrolait() |
||||
|
val = self.agrolait.get_custom_info_value( |
||||
|
self.env.ref("base_custom_info.prop_avg_note")) |
||||
|
with self.assertRaises(ValidationError): |
||||
|
val.value = "-1" |
||||
|
|
||||
|
def test_high_average_note(self): |
||||
|
"""Too smart!""" |
||||
|
self.set_custom_info_for_agrolait() |
||||
|
val = self.agrolait.get_custom_info_value( |
||||
|
self.env.ref("base_custom_info.prop_avg_note")) |
||||
|
with self.assertRaises(ValidationError): |
||||
|
val.value = "11" |
@ -0,0 +1,129 @@ |
|||||
|
# -*- 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 fill_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 |
||||
|
self.agrolait._onchange_custom_info_template_id() |
||||
|
if field == "value": |
||||
|
value = str(value) |
||||
|
self.value = self.agrolait.get_custom_info_value(prop) |
||||
|
self.value[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.fill_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.fill_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.fill_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.fill_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.fill_value(self.prop_float, 9.5) |
||||
|
self.creation_found("9.5") |
||||
|
self.assertEqual(float(self.value.value), self.value.value_float) |
||||
|
|
||||
|
def test_from_float(self): |
||||
|
"""Conversion from decimal number.""" |
||||
|
self.fill_value(self.prop_float, 9.5, "value_float") |
||||
|
self.creation_found("9.5") |
||||
|
self.assertEqual(float(self.value.value), self.value.value_float) |
||||
|
|
||||
|
def test_to_bool_true(self): |
||||
|
"""Conversion to yes.""" |
||||
|
self.fill_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.fill_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.fill_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.fill_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.fill_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.fill_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,50 @@ |
|||||
|
<?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="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="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="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,79 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
||||
|
Copyright 2017 Pedro M. Baeza <pedro.baeza@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="model">custom.info.option</field> |
||||
|
<field name="priority" eval="999"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<tree string="Custom Info Options" editable="bottom"> |
||||
|
<field name="name"/> |
||||
|
<!-- Hidden for now from backend UI --> |
||||
|
<field name="template_id" invisible="1"/> |
||||
|
</tree> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record id="custom_info_option_tree_full" model="ir.ui.view"> |
||||
|
<field name="model">custom.info.option</field> |
||||
|
<field name="inherit_id" ref="custom_info_option_tree"/> |
||||
|
<field name="mode">primary</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<tree position="attributes"> |
||||
|
<attribute name="editable"/> |
||||
|
</tree> |
||||
|
<field name="name" position="before"> |
||||
|
<field name="property_ids" widget="many2many_tags"/> |
||||
|
</field> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record id="custom_info_option_form" model="ir.ui.view"> |
||||
|
<field name="model">custom.info.option</field> |
||||
|
<field name="priority" eval="999"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form string="Custom Info Template Properties"> |
||||
|
<sheet> |
||||
|
<group> |
||||
|
<field name="name"/> |
||||
|
<!-- Hidden for now from backend UI --> |
||||
|
<field name="template_id" invisible="1"/> |
||||
|
</group> |
||||
|
</sheet> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record id="custom_info_option_form_full" model="ir.ui.view"> |
||||
|
<field name="model">custom.info.option</field> |
||||
|
<field name="inherit_id" ref="custom_info_option_form"/> |
||||
|
<field name="mode">primary</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<field name="template_id" position="after"> |
||||
|
<field name="property_ids"/> |
||||
|
<field name="value_ids"/> |
||||
|
</field> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record id="custom_info_option_search" model="ir.ui.view"> |
||||
|
<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,45 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
<?xml version="1.0" encoding="utf-8"?> |
||||
<openerp> |
|
||||
|
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
||||
|
Copyright 2017 Pedro M. Baeza <pedro.baeza@tecnativa.com> |
||||
|
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). --> |
||||
|
<odoo> |
||||
|
|
||||
<!--Parent Custom Info in Settings--> |
|
||||
<menuitem id="menu_base_custom_info" name="Custom Info" |
|
||||
parent="base.menu_administration" sequence="45"/> |
|
||||
|
<!--Base menus --> |
||||
|
<menuitem id="menu_base_custom_info" |
||||
|
name="Custom Info" |
||||
|
groups="base_custom_info.group_basic" |
||||
|
web_icon="base_custom_info,static/description/icon.png" |
||||
|
/> |
||||
|
|
||||
|
<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,32 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
||||
|
Copyright 2017 Pedro M. Baeza <pedro.baeza@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="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> |
||||
|
<group> |
||||
|
<field name="custom_info_template_id" |
||||
|
options='{"no_quick_create": True}' |
||||
|
/> |
||||
|
</group> |
||||
|
<field name="custom_info_ids" |
||||
|
colspan="4" |
||||
|
nolabel="1" |
||||
|
context="{'embed': True, 'tree_view_ref': 'base_custom_info.custom_info_value_tree_editable'}" |
||||
|
/> |
||||
|
</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