Browse Source

[IMP]base_custom_info: New types and access rules system.

* Now you can define properties types, and access rules are inherited from the model/record
  linked to the custom info record.
* Simplified version of computed value.
* Implement for res.partner.
* Add tests and fix bugs discovered in the meantime.
* Allow to disable partner custom info tab, and custom info menu.
* All of it can be set within general settings.
* Now, by default, this module does not display custom info for partners unless in demo mode.
  Better fit for a base module.
* You can disable the top menu entry too if it disturbs you, or enable it for everybody.
* Give a special form when editing in partner custom info tab.
* Sortable properties.
* Sort values at onchange time.
* Improve performance in onchange.
* Split in several model files.

# Conflicts:
#	base_custom_info/__manifest__.py
#	base_custom_info/i18n/es.po
pull/1115/head
Jairo Llopis 8 years ago
committed by Fanha Giang
parent
commit
4378c7d4be
  1. 158
      base_custom_info/README.rst
  2. 2
      base_custom_info/__init__.py
  3. 19
      base_custom_info/__manifest__.py
  4. 4
      base_custom_info/demo/custom.info.option.csv
  5. 6
      base_custom_info/demo/custom.info.property.csv
  6. 2
      base_custom_info/demo/custom.info.template.csv
  7. 12
      base_custom_info/demo/res_groups.xml
  8. 10
      base_custom_info/migrations/9.0.2.0.0/pre-migrate.py
  9. 11
      base_custom_info/models/__init__.py
  10. 134
      base_custom_info/models/custom_info.py
  11. 34
      base_custom_info/models/custom_info_category.py
  12. 38
      base_custom_info/models/custom_info_option.py
  13. 113
      base_custom_info/models/custom_info_property.py
  14. 69
      base_custom_info/models/custom_info_template.py
  15. 283
      base_custom_info/models/custom_info_value.py
  16. 22
      base_custom_info/models/res_partner.py
  17. 13
      base_custom_info/security/ir.model.access.csv
  18. 31
      base_custom_info/security/res_groups.xml
  19. 5
      base_custom_info/tests/__init__.py
  20. 119
      base_custom_info/tests/test_partner.py
  21. 131
      base_custom_info/tests/test_value_conversion.py
  22. 53
      base_custom_info/views/custom_info_category_view.xml
  23. 52
      base_custom_info/views/custom_info_option_view.xml
  24. 63
      base_custom_info/views/custom_info_property_view.xml
  25. 38
      base_custom_info/views/custom_info_template_view.xml
  26. 80
      base_custom_info/views/custom_info_value_view.xml
  27. 44
      base_custom_info/views/menu.xml
  28. 28
      base_custom_info/views/res_partner_view.xml
  29. 6
      base_custom_info/wizard/__init__.py
  30. 20
      base_custom_info/wizard/base_config_settings.py
  31. 33
      base_custom_info/wizard/base_config_settings_view.xml

158
base_custom_info/README.rst

@ -6,8 +6,126 @@
Base Custom Info Base Custom Info
================ ================
This module allows to create custom fields in models without altering model's
structure.
This module allows you to attach custom information to records without the need
to alter the database structure too much.
Definitions
===========
This module defines several concepts that you have to understand.
Templates
---------
A *template* is a collection of *properties* that a record should have.
*Templates* always apply to a given model, and then you can choose among the
current templates for the model you are using when you edit a record of that
model.
I.e., This addon includes a demo template called "Smart partners", that applies
to the model ``res.partner``, so if you edit any partner, you can choose that
template and get its properties autofilled.
Properties
----------
A *property* is the "name" of the field. *Templates* can have any amount of
*properties*, and when you apply a *template* to a record, it automatically
gets all of its *properties* filled, empty (unless they have a *Default
value*), ready to assign *values*.
You can set a property to as *required* to force it have a value, although you
should keep in mind that for yes/no properties, this would mean that only *yes*
can be selected, and for numeric properties, zero would be forbidden.
Also you can set *Minimum* and *Maximum* limits for every *property*, but those
limits are only used when the data type is text (to constrain its length) or
number. To skip this constraint, just set a maximum smaller than the minimum.
*Properties* always belong to a template, and as such, to a model.
*Properties* define the data type (text, number, yes/no...), and when the type
is "Selection", then you can define what *options* are available.
I.e., the "Smart partners" *template* has the following *properties*:
- Name of his/her teacher
- Amount of people that hates him/her for being so smart
- Average note on all subjects
- Does he/she believe he/she is the smartest person on earth?
- What weaknesses does he/she have?
When you set that template to any partner, you will then be able to fill these
*properties* with *values*.
Categories
----------
*Properties* can also belong to a *category*, which allows you to sort them in
a logical way, and makes further development easier.
For example, the ``website_sale_custom_info`` addon uses these to display a
technical datasheet per product in your online shop, sorted and separated by
category.
You are not required to give a *category* to every *property*.
Options
-------
When a *property*'s type is "Selection", then you define the *options*
available, so the *value* must be one of these *options*.
I.e., the "What weaknesses does he/she have?" *property* has some options:
- Loves junk food
- Needs videogames
- Huge glasses
The *value* will always be one of these.
Value
-----
When you assign a *template* to a partner, and then you get the *properties* it
should have, you still have to set a *value* for each property.
*Values* can be of different types (whole numbers, constrained selection,
booleans...), depending on how the *property* was defined. However, there is
always the ``value`` field, that is a text string, and converts automatically
to/from the correct type.
Why would I need this?
~~~~~~~~~~~~~~~~~~~~~~
Imagine you have some partners that are foreign, and that for those partners
you need some extra information that is not needed for others, and you do not
want to fill the partners model with a lot of fields that will be empty most of
the time.
In this case, you could define a *template* called "Foreign partners", which
will be applied to ``res.partner`` objects, and defines some *properties* that
these are expected to have.
Then you could assign that *template* to a partner, and automatically you will
get a subtable of all the properties it should have, with tools to fill their
*values* correctly.
Does this work with any model?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Yes and no.
Yes, because this is a base module that provides the tools to make this work
with any model.
No, because, although the tools are provided, they are only applied to the
``res.partner`` model. This is by design, because different models can have
different needs, and we don't want to depend on every possible model.
So, if you want to apply this to other models, you will have to develop a
little additional addon that depends on this one. If you are a developer, refer
to the *Development* section below.
Installation Installation
============ ============
@ -18,6 +136,17 @@ concrete models.
This module is a technical dependency and is to be installed in parallel to This module is a technical dependency and is to be installed in parallel to
other modules. other modules.
Configuration
=============
To enable the main *Custom Info* menu:
#. Enable *Settings > General Settings > Manage custom information*.
To enable partner's custom info tab:
#. Enable *Settings > General Settings > Edit custom information in partners*.
Usage Usage
===== =====
@ -26,7 +155,7 @@ expected for a given record.
To define a template, you need to: To define a template, you need to:
* Go to *Settings > Custom Info > Templates*.
* Go to *Custom Info > Templates*.
* Create one. * Create one.
* Add some *Properties* to it. * Add some *Properties* to it.
@ -35,11 +164,19 @@ properties.
To manage the properties, you need to: To manage the properties, you need to:
* Go to *Settings > Custom Info > Properties*.
* Go to *Custom Info > Properties*.
To manage the property categories, you need to:
* Go to *Custom Info > Categories*.
Some properties can have a number of options to choose, to manage them:
* Go to *Custom Info > Options*.
To manage their values, you need to: To manage their values, you need to:
* Go to *Settings > Custom Info > Values*.
* Go to *Custom Info > Values*.
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas .. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot :alt: Try me on Runbot
@ -51,10 +188,19 @@ Development
To create a module that supports custom information, just depend on this module To create a module that supports custom information, just depend on this module
and inherit from the ``custom.info`` model. and inherit from the ``custom.info`` model.
See an example in the ``product_custom_info`` addon.
Known issues / Roadmap Known issues / Roadmap
====================== ======================
* All data types of custom information values are text strings.
* Custom properties cannot be shared among templates.
* You get an error if you press *Save & New* when setting property values in
partner form.
* `There seems to be a bug in the web client that makes subviews appear empty
after an onchange
<https://github.com/OCA/server-tools/pull/492#issuecomment-237594285>`_.
This module includes a workaround for that, but the bug should be fixed and
the workaround removed.
Bug Tracker Bug Tracker
=========== ===========

2
base_custom_info/__init__.py

@ -3,4 +3,4 @@
# © 2015 Antiun Ingeniería S.L. - Carlos Dauden # © 2015 Antiun Ingeniería S.L. - Carlos Dauden
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html # License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html
from . import models
from . import models, wizard

19
base_custom_info/__manifest__.py

@ -8,16 +8,27 @@
'name': "Base Custom Info", 'name': "Base Custom Info",
'summary': "Add custom field in models", 'summary': "Add custom field in models",
'category': 'Tools', 'category': 'Tools',
'version': '9.0.1.0.0',
'version': '9.0.2.0.0',
'depends': [ 'depends': [
'base',
'base_setup',
], ],
'data': [ 'data': [
'security/ir.model.access.csv',
'security/res_groups.xml',
'views/custom_info_category_view.xml',
'views/custom_info_option_view.xml',
'views/custom_info_template_view.xml', 'views/custom_info_template_view.xml',
'views/custom_info_property_view.xml', 'views/custom_info_property_view.xml',
'views/custom_info_value_view.xml', 'views/custom_info_value_view.xml',
'views/menu.xml', 'views/menu.xml',
'security/ir.model.access.csv',
'views/res_partner_view.xml',
'wizard/base_config_settings_view.xml',
],
'demo': [
'demo/custom.info.template.csv',
'demo/custom.info.property.csv',
'demo/custom.info.option.csv',
'demo/res_groups.xml',
], ],
"images": [ "images": [
"images/menu.png", "images/menu.png",
@ -29,7 +40,7 @@
'Incaser Informatica S.L., ' 'Incaser Informatica S.L., '
'Tecnativa, ' 'Tecnativa, '
'Odoo Community Association (OCA)', 'Odoo Community Association (OCA)',
'website': 'http://www.antiun.com',
'website': 'https://www.tecnativa.com',
'license': 'AGPL-3', 'license': 'AGPL-3',
'installable': True, 'installable': True,
} }

4
base_custom_info/demo/custom.info.option.csv

@ -0,0 +1,4 @@
id,name,property_ids:id
opt_food,Loves junk food,prop_weaknesses
opt_videogames,Needs videogames,prop_weaknesses
opt_glasses,Huge glasses,prop_weaknesses

6
base_custom_info/demo/custom.info.property.csv

@ -0,0 +1,6 @@
id,name,template_id:id,field_type,default_value,required
prop_teacher,Name of his/her teacher,tpl_smart,str,,
prop_haters,Amount of people that hates him/her for being so smart,tpl_smart,int,,
prop_avg_note,Average note on all subjects,tpl_smart,float,,True
prop_smartypants,Does he/she believe he/she is the smartest person on earth?,tpl_smart,bool,,
prop_weaknesses,What weaknesses does he/she have?,tpl_smart,id,Huge glasses,

2
base_custom_info/demo/custom.info.template.csv

@ -0,0 +1,2 @@
id,name,model
tpl_smart,Smart partners,res.partner

12
base_custom_info/demo/res_groups.xml

@ -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>

10
base_custom_info/migrations/9.0.2.0.0/pre-migrate.py

@ -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")

11
base_custom_info/models/__init__.py

@ -1,6 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# © 2015 Antiun Ingeniería S.L. - Sergio Teruel # © 2015 Antiun Ingeniería S.L. - Sergio Teruel
# © 2015 Antiun Ingeniería S.L. - Carlos Dauden # © 2015 Antiun Ingeniería S.L. - Carlos Dauden
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html # License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html
from . import custom_info
from . import (
custom_info_template,
custom_info_property,
custom_info_category,
custom_info_option,
custom_info_value,
custom_info,
res_partner,
)

134
base_custom_info/models/custom_info.py

@ -1,121 +1,61 @@
# -*- 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
# © 2015 Antiun Ingeniería S.L. - Jairo Llopis
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html # License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html
from 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 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',
domain=lambda self: [("model", "=", self._name)],
string='Custom Information Template') string='Custom Information Template')
custom_info_ids = fields.One2many( custom_info_ids = fields.One2many(
comodel_name='custom.info.value', comodel_name='custom.info.value',
inverse_name='res_id', inverse_name='res_id',
domain=lambda self: [("model", "=", self._name)], domain=lambda self: [("model", "=", self._name)],
context={"embed": True},
auto_join=True, auto_join=True,
string='Custom Properties') string='Custom Properties')
@api.multi @api.multi
@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,
"res_id": self.id,
})
new = [(5, False, False)]
for prop in self.custom_info_template_id.property_ids:
new += [(0, False, {
"property_id": prop.id,
"res_id": self.id,
"model": self._name,
})]
self.custom_info_ids = new
self.custom_info_ids._onchange_property_set_default_value()
self.custom_info_ids._inverse_value()
self.custom_info_ids._compute_value()
# HACK https://github.com/OCA/server-tools/pull/492#issuecomment-237594285
@api.multi
def onchange(self, values, field_name, field_onchange):
"""Add custom info children values that will be probably changed."""
subfields = ("category_id", "field_type", "required", "property_id",
"res_id", "model", "value", "value_str", "value_int",
"value_float", "value_bool", "value_id")
for subfield in subfields:
field_onchange.setdefault("custom_info_ids." + subfield, "")
return super(CustomInfo, self).onchange(
values, field_name, field_onchange)
@api.multi @api.multi
def unlink(self): def unlink(self):
@ -124,3 +64,13 @@ class CustomInfo(models.AbstractModel):
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),
])

34
base_custom_info/models/custom_info_category.py

@ -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)

38
base_custom_info/models/custom_info_option.py

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html
from openerp import api, fields, models
class CustomInfoOption(models.Model):
_description = "Available options for a custom property"
_name = "custom.info.option"
_order = "name"
name = fields.Char(index=True, translate=True, required=True)
property_ids = fields.Many2many(
comodel_name="custom.info.property",
string="Properties",
help="Properties where this option is enabled.",
)
value_ids = fields.One2many(
comodel_name="custom.info.value",
inverse_name="value_id",
string="Values",
help="Values that have set this option.",
)
@api.multi
def check_access_rule(self, operation):
"""You access an option if you access at least one property."""
last = None
for prop in self.mapped("property_ids"):
try:
prop.check_access_rule(operation)
return
except Exception as last:
pass
if last:
raise last
return super(CustomInfoOption, self).check_access_rule(operation)

113
base_custom_info/models/custom_info_property.py

@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html
from openerp import _, api, fields, models
from openerp.exceptions import UserError, ValidationError
class CustomInfoProperty(models.Model):
"""Name of the custom information property."""
_description = "Custom information property"
_name = "custom.info.property"
_order = "template_id, category_sequence, category_id, sequence, id"
_sql_constraints = [
("name_template",
"UNIQUE (name, template_id)",
"Another property with that name exists for that template."),
]
name = fields.Char(required=True, translate=True)
sequence = fields.Integer(index=True)
category_id = fields.Many2one(
comodel_name="custom.info.category",
string="Category",
)
category_sequence = fields.Integer(
related="category_id.sequence",
store=True,
readonly=True,
)
template_id = fields.Many2one(
comodel_name='custom.info.template',
string='Template',
required=True)
model = fields.Char(
related="template_id.model",
readonly=True,
auto_join=True,
)
info_value_ids = fields.One2many(
comodel_name="custom.info.value",
inverse_name="property_id",
string="Property Values")
default_value = fields.Char(
translate=True,
help="Will be applied by default to all custom values of this "
"property. This is a char field, so you have to enter some value "
"that can be converted to the field type you choose.",
)
required = fields.Boolean()
minimum = fields.Float(
help="For numeric fields, it means the minimum possible value; "
"for text fields, it means the minimum possible length. "
"If it is bigger than the maximum, then this check is skipped",
)
maximum = fields.Float(
default=-1,
help="For numeric fields, it means the maximum possible value; "
"for text fields, it means the maximum possible length. "
"If it is smaller than the minimum, then this check is skipped",
)
field_type = fields.Selection(
selection=[
("str", "Text"),
("int", "Whole number"),
("float", "Decimal number"),
("bool", "Yes/No"),
("id", "Selection"),
],
default="str",
required=True,
help="Type of information that can be stored in the property.",
)
option_ids = fields.Many2many(
comodel_name="custom.info.option",
string="Options",
help="When the field type is 'selection', choose the available "
"options here.",
)
@api.multi
def check_access_rule(self, operation):
"""You access a property if you access its template."""
self.mapped("template_id").check_access_rule(operation)
return super(CustomInfoProperty, self).check_access_rule(operation)
@api.one
@api.constrains("default_value", "field_type")
def _check_default_value(self):
"""Ensure the default value is valid."""
if self.default_value:
try:
self.env["custom.info.value"]._transform_value(
self.default_value, self.field_type, self)
except ValueError:
selection = dict(
self._fields["field_type"].get_description(self.env)
["selection"])
raise ValidationError(
_("Default value %s cannot be converted to type %s.") %
(self.default_value, selection[self.field_type]))
@api.multi
@api.onchange("required", "field_type")
def _onchange_required_warn(self):
"""Warn if the required flag implies a possible weird behavior."""
if self.required:
if self.field_type == "bool":
raise UserError(
_("If you require a Yes/No field, you can only set Yes."))
if self.field_type in {"int", "float"}:
raise UserError(
_("If you require a numeric field, you cannot set it to "
"zero."))

69
base_custom_info/models/custom_info_template.py

@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html
from openerp import _, api, fields, models
from openerp.exceptions import ValidationError
class CustomInfoTemplate(models.Model):
"""Defines custom properties expected for a given database object."""
_description = "Custom information template"
_name = "custom.info.template"
_order = "model_id, name"
_sql_constraints = [
("name_model",
"UNIQUE (name, model)",
"Another template with that name exists for that model."),
]
name = fields.Char(required=True, translate=True)
model = fields.Char(
index=True,
readonly=True,
required=True)
model_id = fields.Many2one(
'ir.model',
'Model',
compute="_compute_model_id",
store=True,
ondelete="cascade",
)
property_ids = fields.One2many(
'custom.info.property',
'template_id',
'Properties',
oldname="info_ids",
context={"embed": True},
)
@api.multi
@api.depends("model")
def _compute_model_id(self):
"""Get a related model from its name, for better UI."""
for s in self:
s.model_id = self.env["ir.model"].search([("model", "=", s.model)])
@api.multi
@api.constrains("model")
def _check_model(self):
"""Ensure model exists."""
for s in self:
if s.model not in self.env:
raise ValidationError(_("Model does not exist."))
# Avoid error when updating base module and a submodule extends a
# model that falls out of this one's dependency graph
with self.env.norecompute():
oldmodels = set(s.mapped("property_ids.info_value_ids.model"))
if oldmodels and {s.model} != oldmodels:
raise ValidationError(
_("You cannot change the model because it is in use."))
@api.multi
def check_access_rule(self, operation):
"""You access a template if you access its model."""
for s in self:
model = self.env[s.model]
model.check_access_rights(operation)
model.check_access_rule(operation)
return super(CustomInfoTemplate, self).check_access_rule(operation)

283
base_custom_info/models/custom_info_value.py

@ -0,0 +1,283 @@
# -*- coding: utf-8 -*-
# © 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html
from openerp import _, api, fields, models, SUPERUSER_ID
from openerp.exceptions import ValidationError
from openerp.tools.safe_eval import safe_eval
class CustomInfoValue(models.Model):
_description = "Custom information value"
_name = "custom.info.value"
_rec_name = 'value'
_order = ("model, res_id, category_sequence, category_id, "
"property_sequence, property_id")
_sql_constraints = [
("property_owner",
"UNIQUE (property_id, model, res_id)",
"Another property with that name exists for that resource."),
]
model = fields.Char(
related="property_id.model",
index=True,
readonly=True,
auto_join=True,
store=True,
)
owner_id = fields.Reference(
selection="_selection_owner_id",
string="Owner",
compute="_compute_owner_id",
inverse="_inverse_owner_id",
help="Record that owns this custom value.",
)
res_id = fields.Integer(
"Resource ID",
required=True,
index=True,
store=True,
ondelete="cascade",
)
property_id = fields.Many2one(
comodel_name='custom.info.property',
required=True,
string='Property')
property_sequence = fields.Integer(
related="property_id.sequence",
store=True,
index=True,
readonly=True,
)
category_sequence = fields.Integer(
related="property_id.category_id.sequence",
store=True,
readonly=True,
)
category_id = fields.Many2one(
related="property_id.category_id",
store=True,
readonly=True,
)
name = fields.Char(related='property_id.name', readonly=True)
field_type = fields.Selection(related="property_id.field_type")
field_name = fields.Char(
compute="_compute_field_name",
help="Technical name of the field where the value is stored.",
)
required = fields.Boolean(related="property_id.required")
value = fields.Char(
compute="_compute_value",
inverse="_inverse_value",
search="_search_value",
help="Value, always converted to/from the typed field.",
)
value_str = fields.Char(
string="Text value",
translate=True,
index=True,
)
value_int = fields.Integer(
string="Whole number value",
index=True,
)
value_float = fields.Float(
string="Decimal number value",
index=True,
)
value_bool = fields.Boolean(
string="Yes/No value",
index=True,
)
value_id = fields.Many2one(
comodel_name="custom.info.option",
string="Selection value",
ondelete="cascade",
domain="[('property_ids', 'in', [property_id])]",
)
@api.multi
def check_access_rule(self, operation):
"""You access a value if you access its property and owner record."""
if self.env.uid == SUPERUSER_ID:
return
for s in self:
s.property_id.check_access_rule(operation)
s.owner_id.check_access_rights(operation)
s.owner_id.check_access_rule(operation)
return super(CustomInfoValue, self).check_access_rule(operation)
@api.model
def create(self, vals):
"""Skip constrains in 1st lap."""
# HACK https://github.com/odoo/odoo/pull/13439
if "value" in vals:
self.env.context.skip_required = True
return super(CustomInfoValue, self).create(vals)
@api.model
def _selection_owner_id(self):
"""You can choose among models linked to a template."""
models = self.env["ir.model.fields"].search([
("ttype", "=", "many2one"),
("relation", "=", "custom.info.template"),
("model_id.transient", "=", False),
"!", ("model", "=like", "custom.info.%"),
]).mapped("model_id")
models = models.search([("id", "in", models.ids)], order="name")
return [(m.model, m.name) for m in models
if m.model in self.env and self.env[m.model]._auto]
@api.multi
@api.depends("property_id.field_type")
def _compute_field_name(self):
"""Get the technical name where the real typed value is stored."""
for s in self:
s.field_name = "value_{!s}".format(s.property_id.field_type)
@api.multi
@api.depends("res_id", "model")
def _compute_owner_id(self):
"""Get the id from the linked record."""
for s in self:
s.owner_id = "{},{}".format(s.model, s.res_id)
@api.multi
def _inverse_owner_id(self):
"""Store the owner according to the model and ID."""
for s in self:
s.model = s.owner_id._name
s.res_id = s.owner_id.id
@api.multi
@api.depends("property_id.field_type", "field_name", "value_str",
"value_int", "value_float", "value_bool", "value_id")
def _compute_value(self):
"""Get the value as a string, from the original field."""
for s in self:
if s.field_type == "id":
s.value = ", ".join(s.value_id.mapped("display_name"))
elif s.field_type == "bool":
s.value = _("Yes") if s.value_bool else _("No")
else:
s.value = getattr(s, s.field_name, False)
@api.multi
def _inverse_value(self):
"""Write the value correctly converted in the typed field."""
for s in self:
s[s.field_name] = self._transform_value(
s.value, s.field_type, s.property_id)
@api.one
@api.constrains("required", "field_name", "value_str", "value_int",
"value_float", "value_bool", "value_id")
def _check_required(self):
"""Ensure required fields are filled"""
# HACK https://github.com/odoo/odoo/pull/13439
try:
del self.env.context.skip_required
except AttributeError:
if self.required and not self[self.field_name]:
raise ValidationError(
_("Property %s is required.") %
self.property_id.display_name)
@api.one
@api.constrains("property_id", "field_type", "field_name",
"value_str", "value_int", "value_float")
def _check_min_max_limits(self):
"""Ensure value falls inside the property's stablished limits."""
minimum, maximum = self.property_id.minimum, self.property_id.maximum
if minimum <= maximum:
value = self[self.field_name]
if not value:
# This is a job for :meth:`.~_check_required`
return
if self.field_type == "str":
number = len(self.value_str)
message = _(
"Length for %(prop)s is %(val)s, but it should be "
"between %(min)d and %(max)d.")
elif self.field_type in {"int", "float"}:
number = value
if self.field_type == "int":
message = _(
"Value for %(prop)s is %(val)s, but it should be "
"between %(min)d and %(max)d.")
else:
message = _(
"Value for %(prop)s is %(val)s, but it should be "
"between %(min)f and %(max)f.")
else:
return
if not minimum <= number <= maximum:
raise ValidationError(message % {
"prop": self.property_id.display_name,
"val": number,
"min": minimum,
"max": maximum,
})
@api.multi
@api.onchange("property_id")
def _onchange_property_set_default_value(self):
"""Load default value for this property."""
for record in self:
if not record.value and record.property_id.default_value:
record.value = record.property_id.default_value
@api.model
def _transform_value(self, value, format_, properties=None):
"""Transforms a text value to the expected format.
:param str/bool value:
Custom value in raw string.
:param str format_:
Target conversion format for the value. Must be available among
``custom.info.property`` options.
:param recordset properties:
Useful when :param:`format_` is ``id``, as it helps to ensure the
option is available in these properties. If :param:`format_` is
``id`` and :param:`properties` is ``None``, no transformation will
be made for :param:`value`.
"""
if not value:
value = False
elif format_ == "id" and properties:
value = self.env["custom.info.option"].search(
[("property_ids", "in", properties.ids),
("name", "=ilike", value)])
value.ensure_one()
elif format_ == "bool":
value = value.strip().lower() not in {
"0", "false", "", "no", "off", _("No").lower()}
elif format_ not in {"str", "id"}:
value = safe_eval("{!s}({!r})".format(format_, value))
return value
@api.model
def _search_value(self, operator, value):
"""Search from the stored field directly."""
options = (
o[0] for o in
self.property_id._fields["field_type"]
.get_description(self.env)["selection"])
domain = []
for fmt in options:
try:
_value = (self._transform_value(value, fmt)
if not isinstance(value, list) else
[self._transform_value(v, fmt) for v in value])
except ValueError:
# If you are searching something that cannot be casted, then
# your property is probably from another type
continue
domain += [
"&",
("field_type", "=", fmt),
("value_" + fmt, operator, _value),
]
return ["|"] * (len(domain) / 3 - 1) + domain

22
base_custom_info/models/res_partner.py

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html
from openerp import fields, models
class ResPartner(models.Model):
"""Implement custom information for partners.
Besides adding some visible feature to the module, this is useful for
testing and example purposes.
"""
_name = "res.partner"
_inherit = [_name, "custom.info"]
custom_info_template_id = fields.Many2one(
context={"default_model": _name},
)
custom_info_ids = fields.One2many(
context={"default_model": _name},
)

13
base_custom_info/security/ir.model.access.csv

@ -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

31
base_custom_info/security/res_groups.xml

@ -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>

5
base_custom_info/tests/__init__.py

@ -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

119
base_custom_info/tests/test_partner.py

@ -0,0 +1,119 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from openerp.exceptions import AccessError, ValidationError
from openerp.tests.common import TransactionCase
class PartnerCase(TransactionCase):
def setUp(self, *args, **kwargs):
super(PartnerCase, self).setUp(*args, **kwargs)
self.agrolait = self.env.ref("base.res_partner_2")
self.tpl = self.env.ref("base_custom_info.tpl_smart")
self.demouser = self.env.ref("base.user_demo")
def set_custom_info_for_agrolait(self):
"""Used when you need to use some created custom info."""
self.agrolait.custom_info_template_id = self.tpl
self.env["custom.info.value"].create({
"res_id": self.agrolait.id,
"property_id": self.env.ref("base_custom_info.prop_haters").id,
"value_int": 5,
})
def test_access_granted(self):
"""Access to the model implies access to custom info."""
# Demo user has contact creation permissions by default
agrolait = self.agrolait.sudo(self.demouser)
agrolait.custom_info_template_id = self.tpl
agrolait.env["custom.info.value"].create({
"res_id": agrolait.id,
"property_id":
agrolait.env.ref("base_custom_info.prop_weaknesses").id,
"value_id": agrolait.env.ref("base_custom_info.opt_food").id,
})
agrolait.custom_info_template_id.property_ids[0].name = "Changed!"
agrolait.env.ref("base_custom_info.opt_food").name = "Changed!"
def test_access_denied(self):
"""Forbidden access to the model forbids it to custom info."""
# Remove permissions to demo user
self.demouser.groups_id = self.env.ref("base.group_portal")
agrolait = self.agrolait.sudo(self.demouser)
with self.assertRaises(AccessError):
agrolait.custom_info_template_id = self.tpl
with self.assertRaises(AccessError):
agrolait.env["custom.info.value"].create({
"res_id": agrolait.id,
"property_id":
agrolait.env.ref("base_custom_info.prop_weaknesses").id,
"value_id": agrolait.env.ref("base_custom_info.opt_food").id,
})
with self.assertRaises(AccessError):
agrolait.custom_info_template_id.property_ids[0].name = "Changed!"
with self.assertRaises(AccessError):
agrolait.env.ref("base_custom_info.opt_food").name = "Changed!"
def test_apply_unapply_template(self):
"""(Un)apply a template to a owner and it gets filled."""
# Applying a template autofills the values
self.agrolait.custom_info_template_id = self.tpl
with self.env.do_in_onchange():
self.agrolait._onchange_custom_info_template_id()
self.assertEqual(
len(self.agrolait.custom_info_ids),
len(self.tpl.property_ids))
self.assertEqual(
self.agrolait.custom_info_ids.mapped("property_id"),
self.tpl.property_ids)
# Unapplying a template empties the values
self.agrolait.custom_info_template_id = False
self.agrolait._onchange_custom_info_template_id()
self.assertFalse(self.agrolait.custom_info_template_id)
def test_template_model_and_model_id_match(self):
"""Template's model and model_id fields match."""
self.assertEqual(self.tpl.model, self.tpl.model_id.model)
self.tpl.model = "res.users"
self.assertEqual(self.tpl.model, self.tpl.model_id.model)
def test_template_model_must_exist(self):
"""Cannot create templates for unexisting models."""
with self.assertRaises(ValidationError):
self.tpl.model = "yabadabaduu"
def test_change_used_model_fails(self):
"""If a template's model is already used, you cannot change it."""
self.set_custom_info_for_agrolait()
with self.assertRaises(ValidationError):
self.tpl.model = "res.users"
def test_owners_selection(self):
"""Owners selection includes only the required matches."""
choices = dict(self.env["custom.info.value"]._selection_owner_id())
self.assertIn("res.partner", choices)
self.assertNotIn("ir.model", choices)
self.assertNotIn("custom.info.property", choices)
self.assertNotIn("custom.info", choices)
def test_owner_id(self):
"""Check the computed owner id for a value."""
self.set_custom_info_for_agrolait()
self.assertEqual(self.agrolait.custom_info_ids.owner_id, self.agrolait)
def test_get_custom_info_value(self):
"""Check the custom info getter helper works fine."""
self.set_custom_info_for_agrolait()
result = self.agrolait.get_custom_info_value(
self.env.ref("base_custom_info.prop_haters"))
self.assertEqual(result.field_type, "int")
self.assertEqual(result.field_name, "value_int")
self.assertEqual(result[result.field_name], 5)
self.assertEqual(result.value_int, 5)
self.assertEqual(result.value, "5")

131
base_custom_info/tests/test_value_conversion.py

@ -0,0 +1,131 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import logging
from openerp.tests.common import TransactionCase
_logger = logging.getLogger(__name__)
class ValueConversionCase(TransactionCase):
def setUp(self):
super(ValueConversionCase, self).setUp()
self.agrolait = self.env.ref("base.res_partner_2")
self.tpl = self.env.ref("base_custom_info.tpl_smart")
self.prop_str = self.env.ref("base_custom_info.prop_teacher")
self.prop_int = self.env.ref("base_custom_info.prop_haters")
self.prop_float = self.env.ref("base_custom_info.prop_avg_note")
self.prop_bool = self.env.ref("base_custom_info.prop_smartypants")
self.prop_id = self.env.ref("base_custom_info.prop_weaknesses")
def create_value(self, prop, value, field="value"):
"""Create a custom info value."""
_logger.info(
"Creating. prop: %s; value: %s; field: %s", prop, value, field)
self.agrolait.custom_info_template_id = self.tpl
if field == "value":
value = str(value)
self.value = self.env["custom.info.value"].create({
"res_id": self.agrolait.id,
"property_id": prop.id,
field: value,
})
def creation_found(self, value):
"""Ensure you can search what you just created."""
prop = self.value.property_id
_logger.info(
"Searching. prop: %s; value: %s", prop, value)
self.assertEqual(
self.value.search([
("property_id", "=", prop.id),
("value", "=", value)]),
self.value)
self.assertEqual(
self.value.search([
("property_id", "=", prop.id),
("value", "in", [value])]),
self.value)
self.assertIs(
self.value.search([
("property_id", "=", prop.id),
("value", "not in", [value])]).id,
False)
def test_to_str(self):
"""Conversion to text."""
self.create_value(self.prop_str, "Mr. Einstein")
self.creation_found("Mr. Einstein")
self.assertEqual(self.value.value, self.value.value_str)
def test_from_str(self):
"""Conversion from text."""
self.create_value(self.prop_str, "Mr. Einstein", "value_str")
self.creation_found("Mr. Einstein")
self.assertEqual(self.value.value, self.value.value_str)
def test_to_int(self):
"""Conversion to whole number."""
self.create_value(self.prop_int, 5)
self.creation_found("5")
self.assertEqual(int(self.value.value), self.value.value_int)
def test_from_int(self):
"""Conversion from whole number."""
self.create_value(self.prop_int, 5, "value_int")
self.creation_found("5")
self.assertEqual(int(self.value.value), self.value.value_int)
def test_to_float(self):
"""Conversion to decimal number."""
self.create_value(self.prop_float, 10.5)
self.creation_found("10.5")
self.assertEqual(float(self.value.value), self.value.value_float)
def test_from_float(self):
"""Conversion from decimal number."""
self.create_value(self.prop_float, 10.5, "value_float")
self.creation_found("10.5")
self.assertEqual(float(self.value.value), self.value.value_float)
def test_to_bool_true(self):
"""Conversion to yes."""
self.create_value(self.prop_bool, "True")
self.creation_found("True")
self.assertEqual(self.value.with_context(lang="en_US").value, "Yes")
self.assertIs(self.value.value_bool, True)
def test_from_bool_true(self):
"""Conversion from yes."""
self.create_value(self.prop_bool, "True", "value_bool")
self.creation_found("True")
self.assertEqual(self.value.with_context(lang="en_US").value, "Yes")
self.assertIs(self.value.value_bool, True)
def test_to_bool_false(self):
"""Conversion to no."""
self.create_value(self.prop_bool, "False")
self.assertEqual(self.value.with_context(lang="en_US").value, "No")
self.assertIs(self.value.value_bool, False)
def test_from_bool_false(self):
"""Conversion from no."""
self.create_value(self.prop_bool, False, "value_bool")
self.assertEqual(self.value.with_context(lang="en_US").value, "No")
self.assertIs(self.value.value_bool, False)
def test_to_id(self):
"""Conversion to selection."""
self.create_value(self.prop_id, "Needs videogames")
self.creation_found("Needs videogames")
self.assertEqual(self.value.value, self.value.value_id.name)
def test_from_id(self):
"""Conversion from selection."""
self.create_value(
self.prop_id,
self.env.ref("base_custom_info.opt_videogames").id,
"value_id")
self.creation_found("Needs videogames")
self.assertEqual(self.value.value, self.value.value_id.name)

53
base_custom_info/views/custom_info_category_view.xml

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<odoo>
<record id="custom_info_category_tree" model="ir.ui.view">
<field name="name">Custom Info Category Tree</field>
<field name="model">custom.info.category</field>
<field name="arch" type="xml">
<tree string="Custom Info Categories">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="property_ids"/>
</tree>
</field>
</record>
<record id="custom_info_category_form" model="ir.ui.view">
<field name="name">Custom Info Category Form</field>
<field name="model">custom.info.category</field>
<field name="arch" type="xml">
<form string="Custom Info Template Properties">
<sheet>
<group>
<field name="name"/>
<field name="sequence"/>
<field name="property_ids"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="custom_info_category_search" model="ir.ui.view">
<field name="name">Custom Info Category Search</field>
<field name="model">custom.info.category</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="property_ids"/>
</search>
</field>
</record>
<record id="custom_info_category_action" model="ir.actions.act_window">
<field name="name">Categories</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">custom.info.category</field>
<field name="view_mode">tree,form</field>
<field name="view_type">form</field>
</record>
</odoo>

52
base_custom_info/views/custom_info_option_view.xml

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<odoo>
<record id="custom_info_option_tree" model="ir.ui.view">
<field name="name">Custom Info Option Tree</field>
<field name="model">custom.info.option</field>
<field name="arch" type="xml">
<tree string="Custom Info Options">
<field name="name"/>
<field name="property_ids"/>
</tree>
</field>
</record>
<record id="custom_info_option_form" model="ir.ui.view">
<field name="name">Custom Info Option Form</field>
<field name="model">custom.info.option</field>
<field name="arch" type="xml">
<form string="Custom Info Template Properties">
<sheet>
<group>
<field name="name"/>
<field name="property_ids"/>
<field name="value_ids"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="custom_info_option_search" model="ir.ui.view">
<field name="name">Custom Info Option Search</field>
<field name="model">custom.info.option</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="property_ids"/>
</search>
</field>
</record>
<record id="custom_info_option_action" model="ir.actions.act_window">
<field name="name">Options</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">custom.info.option</field>
<field name="view_mode">tree,form</field>
<field name="view_type">form</field>
</record>
</odoo>

63
base_custom_info/views/custom_info_property_view.xml

@ -1,36 +1,77 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<openerp>
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<odoo>
<record id="base_custom_info_template_line_tree" model="ir.ui.view">
<field name="name">base.custom.info.property.tree</field>
<record id="custom_info_property_tree" model="ir.ui.view">
<field name="name">Custom Info Property Tree</field>
<field name="model">custom.info.property</field> <field name="model">custom.info.property</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Custom Info Templates">
<tree string="Custom Info Properties">
<field name="sequence" widget="handle"/>
<field name="name"/> <field name="name"/>
<field name="template_id"/>
<field name="template_id" invisible="context.get('embed')"/>
<field name="field_type"/>
<field name="category_id"/>
<field name="required"/>
<field name="default_value"/>
</tree> </tree>
</field> </field>
</record> </record>
<record id="base_custom_info_template_line_form" model="ir.ui.view">
<field name="name">base.custom.info.property.form</field>
<record id="custom_info_property_form" model="ir.ui.view">
<field name="name">Custom Info Property Form</field>
<field name="model">custom.info.property</field> <field name="model">custom.info.property</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Custom Info Template Properties"> <form string="Custom Info Template Properties">
<sheet> <sheet>
<group> <group>
<field name="name"/> <field name="name"/>
<field name="template_id"/>
<field name="template_id"
invisible="context.get('embed')"/>
<field name="field_type"/>
<field name="category_id"/>
<field name="required"/>
<field name="default_value"/>
<field name="minimum"/>
<field name="maximum"/>
</group> </group>
<group> <group>
<field name="info_value_ids"/>
<field name="info_value_ids"
invisible="context.get('embed')"/>
<field
name="option_ids"
attrs="{'invisible': [('field_type', '!=', 'id')]}"/>
</group> </group>
</sheet> </sheet>
</form> </form>
</field> </field>
</record> </record>
<record id="custom_info_template_line_action" model="ir.actions.act_window">
<record id="custom_info_property_search" model="ir.ui.view">
<field name="name">Custom Info Property Search</field>
<field name="model">custom.info.property</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="template_id"/>
<field name="field_type"/>
<field name="category_id"/>
<field name="required"/>
<field name="default_value"/>
<group expand="0" string="Group By">
<filter
string="Template"
context="{'group_by': 'template_id'}"/>
<filter
string="Category"
context="{'group_by': 'category_id'}"/>
</group>
</search>
</field>
</record>
<record id="custom_info_property_action" model="ir.actions.act_window">
<field name="name">Properties</field> <field name="name">Properties</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
<field name="res_model">custom.info.property</field> <field name="res_model">custom.info.property</field>
@ -38,4 +79,4 @@
<field name="view_type">form</field> <field name="view_type">form</field>
</record> </record>
</openerp>
</odoo>

38
base_custom_info/views/custom_info_template_view.xml

@ -1,21 +1,23 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<openerp>
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<odoo>
<record id="base_custom_info_template_tree" model="ir.ui.view"> <record id="base_custom_info_template_tree" model="ir.ui.view">
<field name="name">base.custom.info.template.tree</field>
<field name="name">Custom Info Template Tree</field>
<field name="model">custom.info.template</field> <field name="model">custom.info.template</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Custom Info Templates"> <tree string="Custom Info Templates">
<field name="name"/> <field name="name"/>
<field name="model"/> <field name="model"/>
<field name="model_id"/> <field name="model_id"/>
<field name="info_ids"/>
<field name="property_ids"/>
</tree> </tree>
</field> </field>
</record> </record>
<record id="base_custom_info_template_form" model="ir.ui.view"> <record id="base_custom_info_template_form" model="ir.ui.view">
<field name="name">base.custom.info.template.form</field>
<field name="name">Custom Info Template Form</field>
<field name="model">custom.info.template</field> <field name="model">custom.info.template</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Custom Info Template"> <form string="Custom Info Template">
@ -26,17 +28,30 @@
<field name="model_id"/> <field name="model_id"/>
</group> </group>
<group> <group>
<field name="info_ids">
<tree string="Info Lines" editable="bottom">
<field name="name"/>
</tree>
</field>
<field name="property_ids"/>
</group> </group>
</sheet> </sheet>
</form> </form>
</field> </field>
</record> </record>
<record id="base_custom_info_template_search" model="ir.ui.view">
<field name="name">Custom Info Template Search</field>
<field name="model">custom.info.template</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="model_id"/>
<field name="property_ids"/>
<group expand="0" string="Group By">
<filter
string="Model"
context="{'group_by': 'model_id'}"/>
</group>
</search>
</field>
</record>
<record id="custom_info_template_action" model="ir.actions.act_window"> <record id="custom_info_template_action" model="ir.actions.act_window">
<field name="name">Templates</field> <field name="name">Templates</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
@ -49,10 +64,9 @@
<p class="oe_view_nocontent_create"> <p class="oe_view_nocontent_create">
Click to define a new custom info template. Click to define a new custom info template.
</p><p> </p><p>
You must define a custom info template for each
product properties group.
You must define a custom info template for each properties group.
</p> </p>
</field> </field>
</record> </record>
</openerp>
</odoo>

80
base_custom_info/views/custom_info_value_view.xml

@ -1,19 +1,87 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<openerp>
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<odoo>
<record id="base_custom_info_value_tree" model="ir.ui.view"> <record id="base_custom_info_value_tree" model="ir.ui.view">
<field name="name">base.custom.info.value.tree</field>
<field name="name">Custom Info Value Tree</field>
<field name="model">custom.info.value</field> <field name="model">custom.info.value</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Custom Property Values">
<tree string="Custom Property Values" create="0" delete="0">
<field name="owner_id" invisible="context.get('embed')"/>
<field name="category_id"/>
<field name="property_id"/> <field name="property_id"/>
<field name="value"/> <field name="value"/>
<field name="model_id"/>
<field name="res_id"/>
</tree> </tree>
</field> </field>
</record> </record>
<record id="base_custom_info_value_form" model="ir.ui.view">
<field name="name">Custom Info Value Form</field>
<field name="model">custom.info.value</field>
<field name="arch" type="xml">
<form>
<sheet>
<group name="metadata">
<field name="owner_id" invisible="context.get('embed')"/>
<field name="category_id"/>
<field name="property_id" readonly="context.get('embed')"/>
<field name="field_type" readonly="True"/>
<field name="required" readonly="True"/>
</group>
<group name="value">
<field
name="value_str"
attrs="{'invisible': [('field_type', '!=', 'str')], 'required': [('required', '=', True), ('field_type', '=', 'str')]}"/>
<field
name="value_int"
attrs="{'invisible': [('field_type', '!=', 'int')], 'required': [('required', '=', True), ('field_type', '=', 'int')]}"/>
<field
name="value_float"
attrs="{'invisible': [('field_type', '!=', 'float')], 'required': [('required', '=', True), ('field_type', '=', 'float')]}"/>
<field
name="value_bool"
attrs="{'invisible': [('field_type', '!=', 'bool')], 'required': [('required', '=', True), ('field_type', '=', 'bool')]}"/>
<field
name="value_id"
widget="selection"
attrs="{'invisible': [('field_type', '!=', 'id')], 'required': [('required', '=', True), ('field_type', '=', 'id')]}"/>
</group>
<div invisible="context.get('embed')"
class="alert alert-warning">
<strong>Warning!</strong>
You might see no changes in parent form until you save it.
</div>
</sheet>
</form>
</field>
</record>
<record id="base_custom_info_value_search" model="ir.ui.view">
<field name="name">Custom Info Value Search</field>
<field name="model">custom.info.value</field>
<field name="arch" type="xml">
<search>
<field name="model"/>
<field name="res_id"/>
<field name="category_id"/>
<field name="property_id"/>
<field name="value"/>
<group expand="0" string="Group By">
<filter
string="Owner"
context="{'group_by': ['model' , 'res_id']}"/>
<filter
string="Category"
context="{'group_by': 'category_id'}"/>
<filter
string="Property"
context="{'group_by': 'property_id'}"/>
</group>
</search>
</field>
</record>
<record id="custom_info_value_action" model="ir.actions.act_window"> <record id="custom_info_value_action" model="ir.actions.act_window">
<field name="name">Values</field> <field name="name">Values</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
@ -22,4 +90,4 @@
<field name="view_type">form</field> <field name="view_type">form</field>
</record> </record>
</openerp>
</odoo>

44
base_custom_info/views/menu.xml

@ -1,23 +1,41 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<openerp>
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<odoo>
<!--Parent Custom Info in Settings-->
<!--Base menus -->
<menuitem id="menu_base_custom_info" name="Custom Info" <menuitem id="menu_base_custom_info" name="Custom Info"
parent="base.menu_administration" sequence="45"/>
groups="base_custom_info.group_basic"/>
<menuitem
id="menu_basic"
name="Basic"
parent="menu_base_custom_info"/>
<menuitem
id="menu_advanced"
name="Advanced"
groups="base_custom_info.group_basic"
parent="menu_base_custom_info"/>
<!--base.custom.info.template-->
<menuitem id="menu_base_custom_info_template" <menuitem id="menu_base_custom_info_template"
action="custom_info_template_action" action="custom_info_template_action"
parent="menu_base_custom_info" sequence="5"/>
<!--base.custom.info.template.line-->
<menuitem id="menu_base_custom_info_template_line"
action="custom_info_template_line_action"
parent="menu_base_custom_info" sequence="10"/>
parent="menu_basic" sequence="10"/>
<!--base.custom.info.value-->
<menuitem id="menu_base_custom_info_value" <menuitem id="menu_base_custom_info_value"
action="custom_info_value_action" action="custom_info_value_action"
parent="menu_base_custom_info" sequence="15"/>
parent="menu_basic" sequence="20"/>
<menuitem id="menu_category"
action="custom_info_category_action"
parent="menu_advanced" sequence="10"/>
<menuitem id="menu_property"
action="custom_info_property_action"
parent="menu_advanced" sequence="20"/>
<menuitem id="menu_option"
action="custom_info_option_action"
parent="menu_advanced" sequence="30"/>
</openerp>
</odoo>

28
base_custom_info/views/res_partner_view.xml

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<odoo>
<record id="view_partner_form" model="ir.ui.view">
<field name="name">Custom info in partners form</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook[1]">
<page name="custom_info"
string="Custom Information"
groups="base_custom_info.group_partner">
<group>
<field
name="custom_info_template_id"
options='{"no_quick_create": True}'/>
<field
name="custom_info_ids"
attrs="{'invisible': [('custom_info_template_id', '=', False)]}"/>
</group>
</page>
</xpath>
</field>
</record>
</odoo>

6
base_custom_info/wizard/__init__.py

@ -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

20
base_custom_info/wizard/base_config_settings.py

@ -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",
)

33
base_custom_info/wizard/base_config_settings_view.xml

@ -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>
Loading…
Cancel
Save