Browse Source
Merge pull request #1115 from fanha99/10.0-mig-base_custom_info
Merge pull request #1115 from fanha99/10.0-mig-base_custom_info
[10.0] [MIG] base custom infopull/1091/head
Pedro M. Baeza
6 years ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 2351 additions and 321 deletions
-
192base_custom_info/README.rst
-
8base_custom_info/__init__.py
-
38base_custom_info/__manifest__.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
-
573base_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
-
17base_custom_info/models/__init__.py
-
169base_custom_info/models/custom_info.py
-
34base_custom_info/models/custom_info_category.py
-
45base_custom_info/models/custom_info_option.py
-
112base_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 -*- |
|||
# © 2015 Antiun Ingeniería S.L. - Sergio Teruel |
|||
# © 2015 Antiun Ingeniería S.L. - Carlos Dauden |
|||
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html |
|||
# Copyright 2015 Antiun Ingeniería S.L. - Sergio Teruel |
|||
# Copyright 2015 Antiun Ingeniería S.L. - Carlos Dauden |
|||
# 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 -*- |
|||
# © 2015 Antiun Ingeniería S.L. - Sergio Teruel |
|||
# © 2015 Antiun Ingeniería S.L. - Carlos Dauden |
|||
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html |
|||
# Copyright 2015 Antiun Ingeniería S.L. - Sergio Teruel |
|||
# Copyright 2015 Antiun Ingeniería S.L. - Carlos Dauden |
|||
# Copyright 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 -*- |
|||
# © 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 odoo 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" |
|||
_name = "custom.info" |
|||
|
|||
custom_info_template_id = fields.Many2one( |
|||
comodel_name='custom.info.template', |
|||
string='Custom Information Template') |
|||
domain=lambda self: [("model", "=", self._name)], |
|||
string='Custom Information Template', |
|||
) |
|||
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)], |
|||
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 |
|||
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') |
|||
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, |
|||
"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 |
|||
def unlink(self): |
|||
"""Remove linked custom info this way, as can't be handled |
|||
automatically. |
|||
""" |
|||
info_values = self.mapped('custom_info_ids') |
|||
res = super(CustomInfo, self).unlink() |
|||
if res: |
|||
info_values.unlink() |
|||
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 -*- |
|||
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html |
|||
|
|||
from odoo 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 odoo 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,112 @@ |
|||
# -*- 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 odoo import _, api, fields, models |
|||
from odoo.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.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 odoo import _, api, fields, models |
|||
from odoo.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 odoo import _, api, fields, models, SUPERUSER_ID |
|||
from odoo.exceptions import ValidationError |
|||
from odoo.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 odoo 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 odoo.exceptions import AccessError, ValidationError |
|||
from odoo.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 odoo.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"?> |
|||
<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" |
|||
action="custom_info_template_action" |
|||
parent="menu_base_custom_info" sequence="5"/> |
|||
|
|||
<!--base.custom.info.template.line--> |
|||
<menuitem id="menu_base_custom_info_template_line" |
|||
action="custom_info_template_line_action" |
|||
parent="menu_base_custom_info" sequence="10"/> |
|||
parent="menu_basic" sequence="10"/> |
|||
|
|||
<!--base.custom.info.value--> |
|||
<menuitem id="menu_base_custom_info_value" |
|||
action="custom_info_value_action" |
|||
parent="menu_base_custom_info" sequence="15"/> |
|||
parent="menu_basic" sequence="20"/> |
|||
|
|||
<menuitem id="menu_category" |
|||
action="custom_info_category_action" |
|||
parent="menu_advanced" sequence="10"/> |
|||
|
|||
<menuitem id="menu_property" |
|||
action="custom_info_property_action" |
|||
parent="menu_advanced" sequence="20"/> |
|||
|
|||
<menuitem id="menu_option" |
|||
action="custom_info_option_action" |
|||
parent="menu_advanced" sequence="30"/> |
|||
|
|||
</openerp> |
|||
</odoo> |
@ -0,0 +1,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 -*- |
|||
# Copyright 2015 Antiun Ingeniería S.L. - Sergio Teruel |
|||
# Copyright 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 odoo 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