Browse Source

Merge pull request #1115 from fanha99/10.0-mig-base_custom_info

[10.0] [MIG] base custom info
pull/1091/head
Pedro M. Baeza 6 years ago
committed by GitHub
parent
commit
29721d09d0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 192
      base_custom_info/README.rst
  2. 8
      base_custom_info/__init__.py
  3. 38
      base_custom_info/__manifest__.py
  4. 3
      base_custom_info/demo/custom.info.category.csv
  5. 10
      base_custom_info/demo/custom.info.option.csv
  6. 8
      base_custom_info/demo/custom.info.property.csv
  7. 3
      base_custom_info/demo/custom.info.template.csv
  8. 6
      base_custom_info/demo/custom_info_property_defaults.yml
  9. 12
      base_custom_info/demo/res_groups.xml
  10. 577
      base_custom_info/i18n/es.po
  11. BIN
      base_custom_info/images/customizations-everywhere.jpg
  12. BIN
      base_custom_info/images/templateception.jpg
  13. 10
      base_custom_info/migrations/9.0.2.0.0/pre-migrate.py
  14. 17
      base_custom_info/models/__init__.py
  15. 173
      base_custom_info/models/custom_info.py
  16. 34
      base_custom_info/models/custom_info_category.py
  17. 45
      base_custom_info/models/custom_info_option.py
  18. 112
      base_custom_info/models/custom_info_property.py
  19. 76
      base_custom_info/models/custom_info_template.py
  20. 241
      base_custom_info/models/custom_info_value.py
  21. 19
      base_custom_info/models/res_partner.py
  22. 13
      base_custom_info/security/ir.model.access.csv
  23. 31
      base_custom_info/security/res_groups.xml
  24. BIN
      base_custom_info/static/description/icon.png
  25. 82
      base_custom_info/static/description/icon.svg
  26. 5
      base_custom_info/tests/__init__.py
  27. 171
      base_custom_info/tests/test_partner.py
  28. 129
      base_custom_info/tests/test_value_conversion.py
  29. 50
      base_custom_info/views/custom_info_category_view.xml
  30. 79
      base_custom_info/views/custom_info_option_view.xml
  31. 134
      base_custom_info/views/custom_info_property_view.xml
  32. 118
      base_custom_info/views/custom_info_template_view.xml
  33. 129
      base_custom_info/views/custom_info_value_view.xml
  34. 56
      base_custom_info/views/menu.xml
  35. 32
      base_custom_info/views/res_partner_view.xml
  36. 6
      base_custom_info/wizard/__init__.py
  37. 20
      base_custom_info/wizard/base_config_settings.py
  38. 33
      base_custom_info/wizard/base_config_settings_view.xml

192
base_custom_info/README.rst

@ -1,13 +1,154 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
================
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.
Recursive templates using options
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Oh dear customization lovers! Options can be used to customize the custom
information template!
.. figure:: /base_custom_info/static/description/customizations-everywhere.jpg
:alt: Customizations Everywhere
If you assign an *additional template* to an option, and while using the owner
form you choose that option, you can then press *reload custom information
templates* to make the owner update itself to include all the properties in all
the involved templates. If you do not press the button, anyway the reloading
will be performed when saving the owner record.
.. figure:: /base_custom_info/static/description/templateception.jpg
:alt: Templateception
I.e., if you select the option "Needs videogames" for the property "What
weaknesses does he/she have?" of a smart partner and press *reload custom
information templates*, you will get 2 new properties to fill: "Favourite
videogames genre" and "Favourite videogame".
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
============
@ -18,6 +159,17 @@ concrete models.
This module is a technical dependency and is to be installed in parallel to
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
=====
@ -26,7 +178,7 @@ expected for a given record.
To define a template, you need to:
* Go to *Settings > Custom Info > Templates*.
* Go to *Custom Info > Templates*.
* Create one.
* Add some *Properties* to it.
@ -35,15 +187,23 @@ properties.
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:
* Go to *Settings > Custom Info > Values*.
* Go to *Custom Info > Values*.
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/135/9.0
:target: https://runbot.odoo-community.org/runbot/135/10.0
Development
===========
@ -51,10 +211,13 @@ Development
To create a module that supports custom information, just depend on this module
and inherit from the ``custom.info`` model.
See an example in the ``product_custom_info`` addon.
Known issues / Roadmap
======================
* All data types of custom information values are text strings.
* Custom properties cannot be shared among templates.
* Required attributes are for now only set in the UI, not in the ORM itself.
Bug Tracker
===========
@ -70,10 +233,11 @@ Credits
Contributors
------------
* Rafael Blasco <rafabn@antiun.com>
* Carlos Dauden <carlos@incaser.es>
* Sergio Teruel <sergio@incaser.es>
* Jairo Llopis <yajo.sk8@gmail.com>
* Rafael Blasco <rafael.blasco@tecnativa.com>
* Carlos Dauden <carlos.dauden@tecnativa.com>
* Sergio Teruel <sergio.teruel@tecnativa.com>
* Jairo Llopis <jairo.llopis@tecnativa.com>
* Pedro M. Baeza <pedro.baeza@tecnativa.com>
Maintainer
----------

8
base_custom_info/__init__.py

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

38
base_custom_info/__manifest__.py

@ -1,23 +1,36 @@
# -*- coding: utf-8 -*-
# © 2015 Antiun Ingeniería S.L. - Sergio Teruel
# © 2015 Antiun Ingeniería S.L. - Carlos Dauden
# © 2015-2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# 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 2015-2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html
{
'name': "Base Custom Info",
'summary': "Add custom field in models",
'category': 'Tools',
'version': '9.0.1.0.0',
'version': '10.0.1.0.0',
'depends': [
'base',
'base_setup',
],
'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_property_view.xml',
'views/custom_info_value_view.xml',
'views/menu.xml',
'security/ir.model.access.csv',
'views/res_partner_view.xml',
'wizard/base_config_settings_view.xml',
],
'demo': [
'demo/custom.info.category.csv',
'demo/custom.info.template.csv',
'demo/custom.info.property.csv',
'demo/custom.info.option.csv',
'demo/custom_info_property_defaults.yml',
'demo/res_groups.xml',
],
"images": [
"images/menu.png",
@ -25,11 +38,10 @@
"images/templates.png",
"images/values.png",
],
'author': 'Antiun Ingeniería S.L., '
'Incaser Informatica S.L., '
'Tecnativa, '
'author': 'Tecnativa, '
'Odoo Community Association (OCA)',
'website': 'http://www.antiun.com',
'license': 'AGPL-3',
'installable': False,
'website': 'https://github.com/OCA/server-tools',
'license': 'LGPL-3',
'application': True,
'installable': True,
}

3
base_custom_info/demo/custom.info.category.csv

@ -0,0 +1,3 @@
id,name,sequence
cat_statics,Statistics,50
cat_gaming,Gaming,100

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

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

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

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

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

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

6
base_custom_info/demo/custom_info_property_defaults.yml

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

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>

577
base_custom_info/i18n/es.po

@ -1,27 +1,61 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * base_custom_info
#
# Translators:
# OCA Transbot <transbot@odoo-community.org>, 2016
# * base_custom_info
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 9.0c\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-09-10 02:52+0000\n"
"PO-Revision-Date: 2016-09-10 02:52+0000\n"
"Last-Translator: OCA Transbot <transbot@odoo-community.org>, 2016\n"
"Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\n"
"POT-Creation-Date: 2017-02-13 00:39+0000\n"
"PO-Revision-Date: 2017-02-13 00:39+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: es\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Plural-Forms: \n"
#. module: base_custom_info
#: model:ir.model,name:base_custom_info.model_custom_info_model_link
msgid "A model that gets its ``ir.model`` computed"
msgstr ""
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_value_form
msgid "<strong>Warning!</strong>\n"
" You might see no changes in parent form until you save it."
msgstr "<strong>¡Aviso!</strong>\n"
" Puede no ver cambios en el formulario padre hasta que guarde."
#. module: base_custom_info
#: model:ir.model.fields,help:base_custom_info.field_base_config_settings_group_custom_info_partner
msgid "Add a tab in the partners form to edit custom information"
msgstr "Añade una pestaña en el formulario de empresas para editar su inf. personalizada."
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_option_template_id
msgid "Additional template"
msgstr "Plantilla adicional"
#. module: base_custom_info
#: model:ir.model.fields,help:base_custom_info.field_custom_info_option_template_id
msgid "Additional template to be applied to the owner if this option is chosen."
msgstr "Plantilla adicional a ser aplicada al propietario si esta opción se escoge."
#. module: base_custom_info
#: model:ir.ui.menu,name:base_custom_info.menu_advanced
msgid "Advanced"
msgstr "Avanzado"
#. module: base_custom_info
#: model:res.groups,name:base_custom_info.group_advanced
msgid "Advanced management"
msgstr "Gestión avanzada"
#. module: base_custom_info
#: model:ir.model.fields,help:base_custom_info.field_base_config_settings_group_custom_info_manager
msgid "Allow all employees to manage custom information"
msgstr "Permitir a todos los empleados gestionar inf. personalizada."
#. module: base_custom_info
#: model:custom.info.property,name:base_custom_info.prop_haters
msgid "Amount of people that hates him/her for being so smart"
msgstr "Cantidad de gente que lo odia por ser tan listo"
#. module: base_custom_info
#: sql_constraint:custom.info.value:0
@ -38,12 +72,58 @@ msgstr "Ya existe otra propiedad con ese nombre en esa plantilla."
msgid "Another template with that name exists for that model."
msgstr "Ya existe otra plantilla con ese nombre para ese modelo."
#. module: base_custom_info
#: model:ir.model,name:base_custom_info.model_custom_info_option
msgid "Available options for a custom property"
msgstr "Opciones disponibles para una propiedad personalizada"
#. module: base_custom_info
#: model:custom.info.property,name:base_custom_info.prop_avg_note
msgid "Average note on all subjects"
msgstr "Nota media en todas las materias"
#. module: base_custom_info
#: model:ir.ui.menu,name:base_custom_info.menu_basic
msgid "Basic"
msgstr "Básico"
#. module: base_custom_info
#: model:res.groups,name:base_custom_info.group_basic
msgid "Basic management"
msgstr "Gestión básica"
#. module: base_custom_info
#: model:custom.info.option,name:base_custom_info.opt_cars
msgid "Cars"
msgstr "Coches"
#. module: base_custom_info
#: model:ir.actions.act_window,name:base_custom_info.custom_info_category_action
#: model:ir.ui.menu,name:base_custom_info.menu_category
msgid "Categories"
msgstr "Categorías"
#. module: base_custom_info
#: model:ir.model,name:base_custom_info.model_custom_info_category
msgid "Categorize custom info properties"
msgstr "Categorizar las propiedades de inf. personalizada"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_category_id
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_category_id
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_property_search
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_value_search
msgid "Category"
msgstr "Categoría"
#. module: base_custom_info
#: model:ir.actions.act_window,help:base_custom_info.custom_info_template_action
msgid "Click to define a new custom info template."
msgstr ""
msgstr "Pulse para definir una nueva plantilla de inf. personalizada."
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_category_create_uid
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_option_create_uid
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_create_uid
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_template_create_uid
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_create_uid
@ -51,66 +131,115 @@ msgid "Created by"
msgstr "Creado por"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_category_create_date
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_option_create_date
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_create_date
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_template_create_date
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_create_date
msgid "Created on"
msgstr "Creado el"
msgstr "Creado en"
#. module: base_custom_info
#: model:ir.ui.menu,name:base_custom_info.menu_base_custom_info
msgid "Custom Info"
msgstr "Información personalizada"
msgstr "Inf. personalizada"
#. module: base_custom_info
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_category_tree
msgid "Custom Info Categories"
msgstr "Categorías de inf. personalizada"
#. module: base_custom_info
#: model:ir.ui.view,arch_db:base_custom_info.base_custom_info_template_form
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_option_tree
msgid "Custom Info Options"
msgstr "Opciones de inf. personalizada"
#. module: base_custom_info
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_property_tree
msgid "Custom Info Properties"
msgstr "Propiedades de inf. personalizada"
#. module: base_custom_info
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_template_form
msgid "Custom Info Template"
msgstr "Plantilla de información personalizada"
msgstr "Plantilla de inf. personalizada"
#. module: base_custom_info
#: model:ir.ui.view,arch_db:base_custom_info.base_custom_info_template_line_form
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_category_form
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_option_form
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_property_form
msgid "Custom Info Template Properties"
msgstr "Propiedades de la plantilla de información personalizada"
msgstr "Propiedades de la plantilla de inf. personalizada"
#. module: base_custom_info
#: model:ir.ui.view,arch_db:base_custom_info.base_custom_info_template_line_tree
#: model:ir.ui.view,arch_db:base_custom_info.base_custom_info_template_tree
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_template_tree
msgid "Custom Info Templates"
msgstr "Plantillas de información personalizada"
msgstr "Plantillas de inf. personalizada"
#. module: base_custom_info
#: model:ir.module.category,name:base_custom_info.category
#: model:ir.ui.view,arch_db:base_custom_info.view_general_configuration
#: model:ir.ui.view,arch_db:base_custom_info.view_partner_form
msgid "Custom Information"
msgstr "Inf. personalizada"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_custom_info_template_id
#: model:ir.model.fields,field_description:base_custom_info.field_res_partner_custom_info_template_id
msgid "Custom Information Template"
msgstr "Plantilla de información personalizada"
msgstr "Plantilla de inf. personalizada"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_custom_info_ids
#: model:ir.model.fields,field_description:base_custom_info.field_res_partner_custom_info_ids
msgid "Custom Properties"
msgstr "Propiedades personalizadas"
#. module: base_custom_info
#: model:ir.ui.view,arch_db:base_custom_info.base_custom_info_value_tree
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_value_tree
msgid "Custom Property Values"
msgstr "Valores de las propiedades personalizadas"
#. module: base_custom_info
#: model:ir.model,name:base_custom_info.model_custom_info_property
msgid "Custom information property"
msgstr "Propiedad de información personalizada"
msgstr "Propiedad de inf. personalizada"
#. module: base_custom_info
#: model:ir.model,name:base_custom_info.model_custom_info_template
msgid "Custom information template"
msgstr "Plantilla de información personalizada"
msgstr "Plantilla de inf. personalizada"
#. module: base_custom_info
#: model:ir.model,name:base_custom_info.model_custom_info_value
msgid "Custom information value"
msgstr "Valor de información personalizada"
msgstr "Valor de inf. personalizada"
#. module: base_custom_info
#: selection:custom.info.property,field_type:0
msgid "Decimal number"
msgstr "Número decimal"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_value_float
msgid "Decimal number value"
msgstr "Valor del número decimal"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_default_value
msgid "Default value"
msgstr "Valor por defecto"
#. module: base_custom_info
#: code:addons/base_custom_info/models/custom_info_property.py:101
#, python-format
msgid "Default value %s cannot be converted to type %s."
msgstr "El valor por defecto %s no se puede convertir al tipo %s."
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_category_display_name
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_display_name
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_model_link_display_name
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_option_display_name
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_display_name
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_template_display_name
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_display_name
@ -118,29 +247,110 @@ msgid "Display Name"
msgstr "Nombre mostrado"
#. module: base_custom_info
#: model:res.groups,name:base_custom_info.group_partner
msgid "Display in partner form"
msgstr "Mostrar en el formulario de empresas"
#. module: base_custom_info
#: model:custom.info.property,name:base_custom_info.prop_smartypants
msgid "Does he/she believe he/she is the smartest person on earth?"
msgstr "¿Se cree la persona más lista de la Tierra?"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_base_config_settings_group_custom_info_partner
msgid "Edit custom information in partners"
msgstr "Editar inf. personalizada en empresas"
#. module: base_custom_info
#: model:custom.info.property,name:base_custom_info.prop_fav_game
msgid "Favourite videogame"
msgstr "Videojuego favorito"
#. module: base_custom_info
#: model:custom.info.property,name:base_custom_info.prop_fav_genre
msgid "Favourite videogames genre"
msgstr "Género de videojuegos favorito"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_field_name
msgid "Field name"
msgstr "Nombre del campo"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_field_type
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_field_type
msgid "Field type"
msgstr "Tipo del campo"
#. module: base_custom_info
#: model:ir.model.fields,help:base_custom_info.field_custom_info_property_maximum
msgid "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"
msgstr "Para campos numéricos, significa el valor máximo permitido; para campos de texto, significa la longitud máxima permitida. Si es menor que el mínimo, entonces esta comprobación se omite."
#. module: base_custom_info
#: model:ir.model.fields,help:base_custom_info.field_custom_info_property_minimum
msgid "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"
msgstr "Para campos numéricos, significa el valor mínimo permitido; para campos de texto, significa la longitud mínima permitida. Si es mayor que el máximo, entonces esta comprobación se omite."
#. module: base_custom_info
#: model:custom.info.template,name:base_custom_info.tpl_gamer
msgid "Gamers"
msgstr "Jugadores"
#. module: base_custom_info
#: model:custom.info.category,name:base_custom_info.cat_gaming
msgid "Gaming"
msgstr "Juego"
#. module: base_custom_info
#: model:custom.info.option,name:base_custom_info.opt_graphical_adventure
msgid "Graphical adventure"
msgstr "Aventura gráfica"
#. module: base_custom_info
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_property_search
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_template_search
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_value_search
msgid "Group By"
msgstr "Agrupar por"
#. module: base_custom_info
#: model:custom.info.option,name:base_custom_info.opt_glasses
#: model:custom.info.property,default_value:base_custom_info.prop_weaknesses
msgid "Huge glasses"
msgstr "Gafas gigantes"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_category_id
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_id
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_model_link_id
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_option_id
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_id
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_template_id
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_id
msgid "ID"
msgstr "ID"
msgstr "ID (identificación)"
#. module: base_custom_info
#: model:ir.ui.view,arch_db:base_custom_info.base_custom_info_template_form
msgid "Info Lines"
msgstr "Líneas de información"
#: code:addons/base_custom_info/models/custom_info_property.py:111
#, python-format
msgid "If you require a Yes/No field, you can only set Yes."
msgstr "Si requiere un campo Sí/No, sólo podrá escoger Sí."
#. module: base_custom_info
#: code:addons/base_custom_info/models/custom_info_property.py:114
#, python-format
msgid "If you require a numeric field, you cannot set it to zero."
msgstr "Si requiere un campo numérico, no podrá ponerlo a cero."
#. module: base_custom_info
#: model:ir.model,name:base_custom_info.model_custom_info
msgid "Inheritable abstract model to add custom info in any model"
msgstr ""
"Modelo abstracto que se puede heredar para añadir información personalizada "
"a cualquier modelo"
msgstr "Modelo abstracto que se puede heredar para añadir inf. personalizada a cualquier modelo"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info___last_update
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_model_link___last_update
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_category___last_update
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_option___last_update
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property___last_update
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_template___last_update
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value___last_update
@ -148,30 +358,65 @@ msgid "Last Modified on"
msgstr "Última modificación en"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_category_write_uid
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_option_write_uid
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_write_uid
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_template_write_uid
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_write_uid
msgid "Last Updated by"
msgstr "Última actualización por"
msgstr "Última actualización de"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_category_write_date
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_option_write_date
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_write_date
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_template_write_date
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_write_date
msgid "Last Updated on"
msgstr "Última actualización el"
msgstr "Última actualización en"
#. module: base_custom_info
#: code:addons/base_custom_info/models/custom_info_value.py:152
#, python-format
msgid "Length for %(prop)s is %(val)s, but it should be between %(min)d and %(max)d."
msgstr "La longitud de %(prop)s es %(val)s, pero debería estar entre %(min)d y %(max)d."
#. module: base_custom_info
#: model:custom.info.option,name:base_custom_info.opt_food
msgid "Loves junk food"
msgstr "Le encanta la comida basura"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_base_config_settings_group_custom_info_manager
msgid "Manage custom information"
msgstr "Gestionar inf. personalizada"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_maximum
msgid "Maximum"
msgstr "Máximo"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_minimum
msgid "Minimum"
msgstr "Mínimo"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_model_link_model
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_model_link_model_id
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_template_model
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_template_model_id
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_model
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_model_id
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_template_search
msgid "Model"
msgstr "Modelo"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_model
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_template_model
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_model
msgid "Model technical name"
msgstr "Nombre técnico del modelo"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_category_name
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_option_name
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_name
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_template_name
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_name
@ -179,14 +424,69 @@ msgid "Name"
msgstr "Nombre"
#. module: base_custom_info
#: model:ir.actions.act_window,name:base_custom_info.custom_info_template_line_action
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_template_info_ids
#: model:ir.ui.menu,name:base_custom_info.menu_base_custom_info_template_line
#: model:custom.info.property,name:base_custom_info.prop_teacher
msgid "Name of his/her teacher"
msgstr "Nombre de su profesor"
#. module: base_custom_info
#: model:custom.info.option,name:base_custom_info.opt_videogames
msgid "Needs videogames"
msgstr "Necesita videojuegos"
#. module: base_custom_info
#: code:addons/base_custom_info/models/custom_info_value.py:123
#: code:addons/base_custom_info/models/custom_info_value.py:215
#, python-format
msgid "No"
msgstr "No"
#. module: base_custom_info
#: model:ir.actions.act_window,name:base_custom_info.custom_info_option_action
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_option_ids
#: model:ir.ui.menu,name:base_custom_info.menu_option
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_property_form
msgid "Options"
msgstr "Opciones"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_owner_id
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_value_search
msgid "Owner"
msgstr "Propietario"
#. module: base_custom_info
#: model:ir.model,name:base_custom_info.model_res_partner
msgid "Partner"
msgstr "Empresa"
#. module: base_custom_info
#: model:custom.info.option,name:base_custom_info.opt_platforms
msgid "Platforms"
msgstr "Plataformas"
#. module: base_custom_info
#: model:ir.actions.act_window,name:base_custom_info.custom_info_property_action
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_category_property_ids
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_option_property_ids
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_template_property_ids
#: model:ir.ui.menu,name:base_custom_info.menu_property
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_template_form
msgid "Properties"
msgstr "Propiedades"
#. module: base_custom_info
#: model:ir.model.fields,help:base_custom_info.field_custom_info_category_property_ids
msgid "Properties in this category."
msgstr "Propiedades en esta categoría."
#. module: base_custom_info
#: model:ir.model.fields,help:base_custom_info.field_custom_info_option_property_ids
msgid "Properties where this option is enabled."
msgstr "Propiedades en las que esta opción está disponible."
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_property_id
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_value_search
msgid "Property"
msgstr "Propiedad"
@ -195,13 +495,79 @@ msgstr "Propiedad"
msgid "Property Values"
msgstr "Valor de la propiedad"
#. module: base_custom_info
#: model:custom.info.option,name:base_custom_info.opt_rpg
msgid "RPG"
msgstr "RPG"
#. module: base_custom_info
#: model:ir.model.fields,help:base_custom_info.field_custom_info_value_owner_id
msgid "Record that owns this custom value."
msgstr "Registro que posee este valor personalizado."
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_required
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_required
msgid "Required"
msgstr "Requerido"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_res_id
msgid "Resource ID"
msgstr "ID del recurso"
msgstr "ID del Recurso"
#. module: base_custom_info
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_property_form
msgid "Select one of the existing options or create a new one clicking on 'Add an item'"
msgstr "Seleccione una de la opciones existentes o cree una nueva pulsando en 'Añadir un elemento'"
#. module: base_custom_info
#: selection:custom.info.property,field_type:0
msgid "Selection"
msgstr "Selección"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_value_id
msgid "Selection value"
msgstr "Valor de selección"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_category_sequence
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_category_sequence
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_sequence
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_category_sequence
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_property_sequence
msgid "Sequence"
msgstr "Secuencia"
#. module: base_custom_info
#: model:custom.info.option,name:base_custom_info.opt_shooter
msgid "Shooter"
msgstr "Shooter"
#. module: base_custom_info
#: model:custom.info.template,name:base_custom_info.tpl_smart
msgid "Smart partners"
msgstr "Gente lista"
#. module: base_custom_info
#: model:custom.info.category,name:base_custom_info.cat_statics
msgid "Statics"
msgstr "Statics"
#. module: base_custom_info
#: model:custom.info.option,name:base_custom_info.opt_strategy
msgid "Strategy"
msgstr "Estrategia"
#. module: base_custom_info
#: model:ir.model.fields,help:base_custom_info.field_custom_info_value_field_name
msgid "Technical name of the field where the value is stored."
msgstr "Nombre técnico del campo donde se guarda este valor"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_template_id
#: model:ir.ui.view,arch_db:base_custom_info.custom_info_property_search
msgid "Template"
msgstr "Plantilla"
@ -211,20 +577,125 @@ msgstr "Plantilla"
msgid "Templates"
msgstr "Plantillas"
#. module: base_custom_info
#: selection:custom.info.property,field_type:0
msgid "Text"
msgstr "Texto"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_value_str
msgid "Text value"
msgstr "Valor de texto"
#. module: base_custom_info
#: model:res.groups,comment:base_custom_info.group_advanced
msgid "The user will be able to manage advanced custom information."
msgstr "El usuario tendrá acceso a una gestión avanzada de la inf. personalizada."
#. module: base_custom_info
#: model:res.groups,comment:base_custom_info.group_basic
msgid "The user will be able to manage basic custom information."
msgstr "El usuario tendrá acceso a una gestión básica de la inf. personalizada."
#. module: base_custom_info
#: model:ir.model.fields,help:base_custom_info.field_custom_info_property_field_type
#: model:ir.model.fields,help:base_custom_info.field_custom_info_value_field_type
msgid "Type of information that can be stored in the property."
msgstr "Tipo de información que se puede almacenar en esta propiedad."
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_value
msgid "Value"
msgstr "Valor"
#. module: base_custom_info
#: code:addons/base_custom_info/models/custom_info_value.py:158
#, python-format
msgid "Value for %(prop)s is %(val)s, but it should be between %(min)d and %(max)d."
msgstr "El valor de %(prop)s es %(val)s, pero debería estar entre %(min)d y %(max)d."
#. module: base_custom_info
#: code:addons/base_custom_info/models/custom_info_value.py:162
#, python-format
msgid "Value for %(prop)s is %(val)s, but it should be between %(min)f and %(max)f."
msgstr "El valor de %(prop)s es %(val)s, pero debería estar entre %(min)f y %(max)f."
#. module: base_custom_info
#: model:ir.model.fields,help:base_custom_info.field_custom_info_value_value
msgid "Value, always converted to/from the typed field."
msgstr "Valor, siempre convertido del/al campo tipado."
#. module: base_custom_info
#: model:ir.actions.act_window,name:base_custom_info.custom_info_value_action
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_option_value_ids
#: model:ir.ui.menu,name:base_custom_info.menu_base_custom_info_value
msgid "Values"
msgstr "Valores"
#. module: base_custom_info
#: model:ir.model.fields,help:base_custom_info.field_custom_info_option_value_ids
msgid "Values that have set this option."
msgstr "Valores que han escogido esta opción."
#. module: base_custom_info
#: model:custom.info.property,name:base_custom_info.prop_weaknesses
msgid "What weaknesses does he/she have?"
msgstr "¿Qué debilidades tiene?"
#. module: base_custom_info
#: model:ir.model.fields,help:base_custom_info.field_custom_info_property_option_ids
msgid "When the field type is 'selection', choose the available options here."
msgstr "Cuando el tipo de campo es 'selección', escoja las opciones disponibles aquí."
#. module: base_custom_info
#: selection:custom.info.property,field_type:0
msgid "Whole number"
msgstr "Número entero"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_value_int
msgid "Whole number value"
msgstr "Valor del número entero"
#. module: base_custom_info
#: model:res.groups,comment:base_custom_info.group_partner
msgid "Will be able to edit custom information from partner's form."
msgstr "Podrá editar inf. personalizada en el formulario de empresa."
#. module: base_custom_info
#: model:ir.model.fields,help:base_custom_info.field_custom_info_property_default_value
msgid "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."
msgstr "Se aplicará por defecto a todos los valores personalizados de esta propiedad. Este campo es de texto, así que tiene que introducir un valor que se pueda convertir al tipo de campo que ha escogido."
#. module: base_custom_info
#: code:addons/base_custom_info/models/custom_info_value.py:123
#, python-format
msgid "Yes"
msgstr "Sí"
#. module: base_custom_info
#: selection:custom.info.property,field_type:0
msgid "Yes/No"
msgstr "Sí/No"
#. module: base_custom_info
#: model:ir.model.fields,field_description:base_custom_info.field_custom_info_value_value_bool
msgid "Yes/No value"
msgstr "Valor sí/no"
#. module: base_custom_info
#: code:addons/base_custom_info/models/custom_info_template.py:66
#, python-format
msgid "You cannot change the model because it is in use."
msgstr "No puede cambiar el modelo porque ya se está usando."
#. module: base_custom_info
#: model:ir.actions.act_window,help:base_custom_info.custom_info_template_action
msgid ""
"You must define a custom info template for each\n"
" product properties group."
msgstr ""
msgid "You must define a custom info template for each properties group."
msgstr "Debe definir una plantilla de inf. personalizada por cada grupo de propiedades."
#. module: base_custom_info
#: model:ir.model,name:base_custom_info.model_base_config_settings
msgid "base.config.settings"
msgstr "base.config.settings"

BIN
base_custom_info/images/customizations-everywhere.jpg

After

Width: 500  |  Height: 380  |  Size: 120 KiB

BIN
base_custom_info/images/templateception.jpg

After

Width: 318  |  Height: 240  |  Size: 57 KiB

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

17
base_custom_info/models/__init__.py

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

173
base_custom_info/models/custom_info.py

@ -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,
"res_id": self.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"))

34
base_custom_info/models/custom_info_category.py

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

45
base_custom_info/models/custom_info_option.py

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

112
base_custom_info/models/custom_info_property.py

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

76
base_custom_info/models/custom_info_template.py

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

241
base_custom_info/models/custom_info_value.py

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

19
base_custom_info/models/res_partner.py

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

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>

BIN
base_custom_info/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 4.0 KiB

82
base_custom_info/static/description/icon.svg

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

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

171
base_custom_info/tests/test_partner.py

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

129
base_custom_info/tests/test_value_conversion.py

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

50
base_custom_info/views/custom_info_category_view.xml

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

79
base_custom_info/views/custom_info_option_view.xml

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

134
base_custom_info/views/custom_info_property_view.xml

@ -1,41 +1,105 @@
<?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>
<record id="base_custom_info_template_line_tree" model="ir.ui.view">
<field name="name">base.custom.info.property.tree</field>
<field name="model">custom.info.property</field>
<field name="arch" type="xml">
<tree string="Custom Info Templates">
<field name="name"/>
<field name="template_id"/>
</tree>
</field>
</record>
<record id="custom_info_property_tree" model="ir.ui.view">
<field name="model">custom.info.property</field>
<field name="priority" eval="999"/>
<field name="arch" type="xml">
<tree string="Custom Info Properties">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="field_type"/>
<field name="category_id"/>
<field name="required"/>
<field name="default_value"/>
</tree>
</field>
</record>
<record id="base_custom_info_template_line_form" model="ir.ui.view">
<field name="name">base.custom.info.property.form</field>
<field name="model">custom.info.property</field>
<field name="arch" type="xml">
<form string="Custom Info Template Properties">
<sheet>
<group>
<field name="name"/>
<field name="template_id"/>
</group>
<group>
<field name="info_value_ids"/>
<record id="custom_info_property_tree_full" model="ir.ui.view">
<field name="model">custom.info.property</field>
<field name="inherit_id" ref="custom_info_property_tree"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="template_id"/>
</field>
</field>
</record>
<record id="custom_info_property_form" model="ir.ui.view">
<field name="model">custom.info.property</field>
<field name="priority" eval="999"/>
<field name="arch" type="xml">
<form string="Custom Info Template Properties">
<sheet>
<group>
<field name="name"/>
<field name="field_type"/>
<field name="category_id"/>
<field name="required"/>
<field name="default_value"/>
<field name="minimum"
attrs="{'invisible': [('field_type', 'not in', ['str', 'int', 'float'])]}"
/>
<field name="maximum"
attrs="{'invisible': [('field_type', 'not in', ['str', 'int', 'float'])]}"
/>
</group>
<group name="Options" col="1" attrs="{'invisible': [('field_type', '!=', 'id')]}">
<label string="Select one of the existing options or create a new one clicking on 'Add an item'"/>
<field name="option_ids"
nolabel="1"
context="{'form_view_ref': 'base_custom_info.custom_info_option_form', 'tree_view_ref': 'base_custom_info.custom_info_option_tree'}"
/>
</group>
</sheet>
</form>
</field>
</record>
<record id="custom_info_property_form_full" model="ir.ui.view">
<field name="model">custom.info.property</field>
<field name="inherit_id" ref="custom_info_property_form"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="template_id"/>
</field>
</field>
</record>
<record id="custom_info_property_search" model="ir.ui.view">
<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>
</sheet>
</form>
</field>
</record>
</search>
</field>
</record>
<record id="custom_info_template_line_action" model="ir.actions.act_window">
<field name="name">Properties</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">custom.info.property</field>
<field name="view_mode">tree,form</field>
<field name="view_type">form</field>
</record>
<record id="custom_info_property_action" model="ir.actions.act_window">
<field name="name">Properties</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">custom.info.property</field>
<field name="view_mode">tree,form</field>
<field name="view_type">form</field>
</record>
</openerp>
</odoo>

118
base_custom_info/views/custom_info_template_view.xml

@ -1,58 +1,72 @@
<?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>
<record id="base_custom_info_template_tree" model="ir.ui.view">
<field name="name">base.custom.info.template.tree</field>
<field name="model">custom.info.template</field>
<field name="arch" type="xml">
<tree string="Custom Info Templates">
<field name="name"/>
<field name="model"/>
<field name="model_id"/>
<field name="info_ids"/>
</tree>
</field>
</record>
<record id="custom_info_template_tree" model="ir.ui.view">
<field name="model">custom.info.template</field>
<field name="arch" type="xml">
<tree string="Custom Info Templates">
<field name="name"/>
<field name="model"/>
<field name="model_id"/>
<field name="property_ids"/>
</tree>
</field>
</record>
<record id="base_custom_info_template_form" model="ir.ui.view">
<field name="name">base.custom.info.template.form</field>
<field name="model">custom.info.template</field>
<field name="arch" type="xml">
<form string="Custom Info Template">
<sheet>
<group>
<field name="name"/>
<field name="model"/>
<field name="model_id"/>
</group>
<group>
<field name="info_ids">
<tree string="Info Lines" editable="bottom">
<field name="name"/>
</tree>
</field>
<record id="custom_info_template_form" model="ir.ui.view">
<field name="model">custom.info.template</field>
<field name="arch" type="xml">
<form string="Custom Info Template">
<sheet>
<group>
<field name="name"/>
<field name="model"/>
<field name="model_id"/>
</group>
<group name="Properties">
<field name="property_ids"
nolabel="1"
context="{'form_view_ref': 'base_custom_info.custom_info_property_form', 'tree_view_ref': 'base_custom_info.custom_info_property_tree'}"
/>
</group>
</sheet>
</form>
</field>
</record>
<record id="custom_info_template_search" model="ir.ui.view">
<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>
</sheet>
</form>
</field>
</record>
</search>
</field>
</record>
<record id="custom_info_template_action" model="ir.actions.act_window">
<field name="name">Templates</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">custom.info.template</field>
<field name="view_mode">tree,form</field>
<field name="view_type">form</field>
<field name="view_id" eval="False"/> <!-- Force empty -->
<field name="domain" eval="False"/> <!-- Force empty -->
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to define a new custom info template.
</p><p>
You must define a custom info template for each
product properties group.
</p>
</field>
</record>
<record id="custom_info_template_action" model="ir.actions.act_window">
<field name="name">Templates</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">custom.info.template</field>
<field name="view_mode">tree,form</field>
<field name="view_type">form</field>
<field name="view_id" eval="False"/> <!-- Force empty -->
<field name="domain" eval="False"/> <!-- Force empty -->
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to define a new custom info template.
</p>
<p>
You must define a custom info template for each properties group.
</p>
</field>
</record>
</openerp>
</odoo>

129
base_custom_info/views/custom_info_value_view.xml

@ -1,25 +1,112 @@
<?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>
<record id="base_custom_info_value_tree" model="ir.ui.view">
<field name="name">base.custom.info.value.tree</field>
<field name="model">custom.info.value</field>
<field name="arch" type="xml">
<tree string="Custom Property Values">
<field name="property_id"/>
<field name="value"/>
<field name="model_id"/>
<field name="res_id"/>
</tree>
</field>
</record>
<record id="custom_info_value_tree" model="ir.ui.view">
<field name="model">custom.info.value</field>
<field name="arch" type="xml">
<tree string="Custom Property Values" create="0" delete="0">
<field name="owner_id" invisible="context.get('embed')"/>
<field name="property_id"/>
<field name="category_id"/>
<field name="required" invisible="1"/>
<field name="value"
attrs="{'required': [('required', '=', True)]}"
/>
</tree>
</field>
</record>
<record id="custom_info_value_action" model="ir.actions.act_window">
<field name="name">Values</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">custom.info.value</field>
<field name="view_mode">tree,form</field>
<field name="view_type">form</field>
</record>
<record id="custom_info_value_tree_editable" model="ir.ui.view">
<field name="model">custom.info.value</field>
<field name="priority" eval="999"/>
<field name="mode">primary</field>
<field name="inherit_id" ref="custom_info_value_tree"/>
<field name="arch" type="xml">
<tree position="attributes">
<attribute name="editable">bottom</attribute>
</tree>
<field name="value" position="after">
<field name="field_type" invisible="1"/>
<field name="value_id"
widget="selection"
attrs="{'invisible': [('field_type', '!=', 'id')], 'required': [('required', '=', True), ('field_type', '=', 'id')]}"
/>
</field>
</field>
</record>
</openerp>
<record id="custom_info_value_form" model="ir.ui.view">
<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="property_id" readonly="context.get('embed')"/>
<field name="category_id"/>
<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 class="alert alert-warning" invisible="not context.get('embed')">
<strong>Warning!</strong>
You might see no changes in parent form until you save it.
</div>
</sheet>
</form>
</field>
</record>
<record id="custom_info_value_search" model="ir.ui.view">
<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">
<field name="name">Values</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">custom.info.value</field>
<field name="view_mode">tree,form</field>
<field name="view_type">form</field>
</record>
</odoo>

56
base_custom_info/views/menu.xml

@ -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"
/>
<!--base.custom.info.template-->
<menuitem id="menu_base_custom_info_template"
action="custom_info_template_action"
parent="menu_base_custom_info" sequence="5"/>
<menuitem
id="menu_basic"
name="Basic"
parent="menu_base_custom_info"/>
<!--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"/>
<menuitem
id="menu_advanced"
name="Advanced"
groups="base_custom_info.group_basic"
parent="menu_base_custom_info"/>
<!--base.custom.info.value-->
<menuitem id="menu_base_custom_info_value"
action="custom_info_value_action"
parent="menu_base_custom_info" sequence="15"/>
<menuitem id="menu_base_custom_info_template"
action="custom_info_template_action"
parent="menu_basic" sequence="10"/>
</openerp>
<menuitem id="menu_base_custom_info_value"
action="custom_info_value_action"
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"/>
</odoo>

32
base_custom_info/views/res_partner_view.xml

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

6
base_custom_info/wizard/__init__.py

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

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

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