diff --git a/base_custom_info/README.rst b/base_custom_info/README.rst index 60eecd504..6d6c57177 100644 --- a/base_custom_info/README.rst +++ b/base_custom_info/README.rst @@ -6,8 +6,126 @@ Base Custom Info ================ -This module allows to create custom fields in models without altering model's -structure. +This module allows you to attach custom information to records without the need +to alter the database structure too much. + +Definitions +=========== + +This module defines several concepts that you have to understand. + +Templates +--------- + +A *template* is a collection of *properties* that a record should have. +*Templates* always apply to a given model, and then you can choose among the +current templates for the model you are using when you edit a record of that +model. + +I.e., This addon includes a demo template called "Smart partners", that applies +to the model ``res.partner``, so if you edit any partner, you can choose that +template and get its properties autofilled. + +Properties +---------- + +A *property* is the "name" of the field. *Templates* can have any amount of +*properties*, and when you apply a *template* to a record, it automatically +gets all of its *properties* filled, empty (unless they have a *Default +value*), ready to assign *values*. + +You can set a property to as *required* to force it have a value, although you +should keep in mind that for yes/no properties, this would mean that only *yes* +can be selected, and for numeric properties, zero would be forbidden. + +Also you can set *Minimum* and *Maximum* limits for every *property*, but those +limits are only used when the data type is text (to constrain its length) or +number. To skip this constraint, just set a maximum smaller than the minimum. + +*Properties* always belong to a template, and as such, to a model. + +*Properties* define the data type (text, number, yes/no...), and when the type +is "Selection", then you can define what *options* are available. + +I.e., the "Smart partners" *template* has the following *properties*: + +- Name of his/her teacher +- Amount of people that hates him/her for being so smart +- Average note on all subjects +- Does he/she believe he/she is the smartest person on earth? +- What weaknesses does he/she have? + +When you set that template to any partner, you will then be able to fill these +*properties* with *values*. + +Categories +---------- + +*Properties* can also belong to a *category*, which allows you to sort them in +a logical way, and makes further development easier. + +For example, the ``website_sale_custom_info`` addon uses these to display a +technical datasheet per product in your online shop, sorted and separated by +category. + +You are not required to give a *category* to every *property*. + +Options +------- + +When a *property*'s type is "Selection", then you define the *options* +available, so the *value* must be one of these *options*. + +I.e., the "What weaknesses does he/she have?" *property* has some options: + +- Loves junk food +- Needs videogames +- Huge glasses + +The *value* will always be one of these. + +Value +----- + +When you assign a *template* to a partner, and then you get the *properties* it +should have, you still have to set a *value* for each property. + +*Values* can be of different types (whole numbers, constrained selection, +booleans...), depending on how the *property* was defined. However, there is +always the ``value`` field, that is a text string, and converts automatically +to/from the correct type. + +Why would I need this? +~~~~~~~~~~~~~~~~~~~~~~ + +Imagine you have some partners that are foreign, and that for those partners +you need some extra information that is not needed for others, and you do not +want to fill the partners model with a lot of fields that will be empty most of +the time. + +In this case, you could define a *template* called "Foreign partners", which +will be applied to ``res.partner`` objects, and defines some *properties* that +these are expected to have. + +Then you could assign that *template* to a partner, and automatically you will +get a subtable of all the properties it should have, with tools to fill their +*values* correctly. + +Does this work with any model? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Yes and no. + +Yes, because this is a base module that provides the tools to make this work +with any model. + +No, because, although the tools are provided, they are only applied to the +``res.partner`` model. This is by design, because different models can have +different needs, and we don't want to depend on every possible model. + +So, if you want to apply this to other models, you will have to develop a +little additional addon that depends on this one. If you are a developer, refer +to the *Development* section below. Installation ============ @@ -18,6 +136,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 +155,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,11 +164,19 @@ 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 @@ -51,10 +188,19 @@ 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. +* You get an error if you press *Save & New* when setting property values in + partner form. +* `There seems to be a bug in the web client that makes subviews appear empty + after an onchange + `_. + This module includes a workaround for that, but the bug should be fixed and + the workaround removed. Bug Tracker =========== diff --git a/base_custom_info/__init__.py b/base_custom_info/__init__.py index cc6e9affa..a518dce55 100644 --- a/base_custom_info/__init__.py +++ b/base_custom_info/__init__.py @@ -3,4 +3,4 @@ # © 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 diff --git a/base_custom_info/__openerp__.py b/base_custom_info/__openerp__.py index f1daa9ea1..81bcec5f1 100644 --- a/base_custom_info/__openerp__.py +++ b/base_custom_info/__openerp__.py @@ -8,16 +8,27 @@ 'name': "Base Custom Info", 'summary': "Add custom field in models", 'category': 'Tools', - 'version': '9.0.1.0.0', + 'version': '9.0.2.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.template.csv', + 'demo/custom.info.property.csv', + 'demo/custom.info.option.csv', + 'demo/res_groups.xml', ], "images": [ "images/menu.png", @@ -29,7 +40,7 @@ 'Incaser Informatica S.L., ' 'Tecnativa, ' 'Odoo Community Association (OCA)', - 'website': 'http://www.antiun.com', + 'website': 'https://www.tecnativa.com', 'license': 'LGPL-3', 'installable': True, } diff --git a/base_custom_info/demo/custom.info.option.csv b/base_custom_info/demo/custom.info.option.csv new file mode 100644 index 000000000..aa072dc80 --- /dev/null +++ b/base_custom_info/demo/custom.info.option.csv @@ -0,0 +1,4 @@ +id,name,property_ids:id +opt_food,Loves junk food,prop_weaknesses +opt_videogames,Needs videogames,prop_weaknesses +opt_glasses,Huge glasses,prop_weaknesses diff --git a/base_custom_info/demo/custom.info.property.csv b/base_custom_info/demo/custom.info.property.csv new file mode 100644 index 000000000..64c3a3066 --- /dev/null +++ b/base_custom_info/demo/custom.info.property.csv @@ -0,0 +1,6 @@ +id,name,template_id:id,field_type,default_value,required +prop_teacher,Name of his/her teacher,tpl_smart,str,, +prop_haters,Amount of people that hates him/her for being so smart,tpl_smart,int,, +prop_avg_note,Average note on all subjects,tpl_smart,float,,True +prop_smartypants,Does he/she believe he/she is the smartest person on earth?,tpl_smart,bool,, +prop_weaknesses,What weaknesses does he/she have?,tpl_smart,id,Huge glasses, diff --git a/base_custom_info/demo/custom.info.template.csv b/base_custom_info/demo/custom.info.template.csv new file mode 100644 index 000000000..1ec9e2bfd --- /dev/null +++ b/base_custom_info/demo/custom.info.template.csv @@ -0,0 +1,2 @@ +id,name,model +tpl_smart,Smart partners,res.partner diff --git a/base_custom_info/demo/res_groups.xml b/base_custom_info/demo/res_groups.xml new file mode 100644 index 000000000..be6dcbc2f --- /dev/null +++ b/base_custom_info/demo/res_groups.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/base_custom_info/i18n/es.po b/base_custom_info/i18n/es.po index 9632caf95..b5eb90896 100644 --- a/base_custom_info/i18n/es.po +++ b/base_custom_info/i18n/es.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * base_custom_info -# +# # Translators: # OCA Transbot , 2016 # Pedro M. Baeza , 2016 @@ -9,20 +9,53 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 9.0c\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-12-17 02:07+0000\n" -"PO-Revision-Date: 2016-12-17 02:07+0000\n" -"Last-Translator: Pedro M. Baeza , 2016\n" +"POT-Creation-Date: 2016-09-09 11:16+0200\n" +"PO-Revision-Date: 2016-09-09 11:17+0200\n" +"Last-Translator: Jairo Llopis \n" "Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\n" +"Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: es\n" +"Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 1.8.8\n" + +#. module: base_custom_info +#: model:ir.ui.view,arch_db:base_custom_info.base_custom_info_value_inner_form +msgid "" +"Warning!\n" +" You might see no changes in parent form until you save it." +msgstr "" +"¡Atención!\n" +" Puede que no vea los cambios en el formulario principal " +"hasta que lo 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ñadir una pestaña en el formulario de empresas para editar su inf. " +"personalizada." + +#. module: base_custom_info +#: model:ir.ui.menu,name:base_custom_info.menu_advanced +msgid "Advanced" +msgstr "Avanzado" #. 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 "Un modelo que obtiene su \"ir.model\" calculado" +#: 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 @@ -39,12 +72,53 @@ 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: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.base_custom_info_value_search +#: model:ir.ui.view,arch_db:base_custom_info.custom_info_property_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 "Pulse para definir una nueva plantilla de información personalizada." +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 @@ -52,6 +126,8 @@ 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 @@ -61,31 +137,56 @@ msgstr "Creado el" #. 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.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.base_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 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" @@ -97,21 +198,43 @@ 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:99 +#, 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 @@ -119,8 +242,74 @@ 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: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.property,default_value:base_custom_info.prop_weaknesses +msgid "Gafas gigantes" +msgstr "Gafas gigantes" + +#. module: base_custom_info +#: model:ir.ui.view,arch_db:base_custom_info.base_custom_info_template_search +#: model:ir.ui.view,arch_db:base_custom_info.base_custom_info_value_search +#: model:ir.ui.view,arch_db:base_custom_info.custom_info_property_search +msgid "Group By" +msgstr "Agrupar por..." + +#. module: base_custom_info +#: model:custom.info.option,name:base_custom_info.opt_glasses +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 @@ -128,27 +317,37 @@ msgid "ID" msgstr "ID" #. 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:109 +#, 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:112 +#, 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" +"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 msgid "Last Modified on" -msgstr "Última modificación en" +msgstr "Última modificación el" #. 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 @@ -156,6 +355,8 @@ msgid "Last Updated by" msgstr "Última actualización por" #. 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 @@ -163,16 +364,52 @@ msgid "Last Updated on" msgstr "Última actualización el" #. 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 +#: code:addons/base_custom_info/models/custom_info_value.py:199 +#, 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_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_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.base_custom_info_template_search msgid "Model" msgstr "Modelo" #. module: base_custom_info +#: code:addons/base_custom_info/models/custom_info_template.py:52 +#, python-format +msgid "Model does not exist." +msgstr "El modelo no existe." + +#. 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 @@ -180,29 +417,124 @@ 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:161 +#: code:addons/base_custom_info/models/custom_info_value.py:256 +#, 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 +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.base_custom_info_value_search +msgid "Owner" +msgstr "Dueño" + +#. module: base_custom_info +#: model:ir.model,name:base_custom_info.model_res_partner +msgid "Partner" +msgstr "Empresa" + +#. 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 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.base_custom_info_value_search msgid "Property" msgstr "Propiedad" +#. module: base_custom_info +#: code:addons/base_custom_info/models/custom_info_value.py:183 +#, python-format +msgid "Property %s is required." +msgstr "La propiedad %s es obligatoria" + #. module: base_custom_info #: model:ir.model.fields,field_description:base_custom_info.field_custom_info_property_info_value_ids msgid "Property Values" msgstr "Valor de la propiedad" +#. 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" +#. 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.template,name:base_custom_info.tpl_smart +msgid "Smart partners" +msgstr "Gente lista" + +#. 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" @@ -212,22 +544,139 @@ 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:205 +#, 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:209 +#, 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.actions.act_window,help:base_custom_info.custom_info_template_action +#: 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 "" -"You must define a custom info template for each\n" -" product properties group." +"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 "" -"Debe definir una plantilla de información personalizada para cada grupo de " +"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:161 +#, 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:59 +#, 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 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" diff --git a/base_custom_info/migrations/9.0.2.0.0/pre-migrate.py b/base_custom_info/migrations/9.0.2.0.0/pre-migrate.py new file mode 100644 index 000000000..574e261dd --- /dev/null +++ b/base_custom_info/migrations/9.0.2.0.0/pre-migrate.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Jairo Llopis +# 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") diff --git a/base_custom_info/models/__init__.py b/base_custom_info/models/__init__.py index 4ce49af98..e0593f285 100644 --- a/base_custom_info/models/__init__.py +++ b/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 +# © 2016 Jairo Llopis # 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, +) diff --git a/base_custom_info/models/custom_info.py b/base_custom_info/models/custom_info.py index 297a25ade..7ef469e3a 100644 --- a/base_custom_info/models/custom_info.py +++ b/base_custom_info/models/custom_info.py @@ -1,121 +1,61 @@ # -*- 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 +# © 2016 Jairo Llopis # License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html from openerp import api, fields, models -class CustomInfoModelLink(models.AbstractModel): - _description = "A model that gets its ``ir.model`` computed" - _name = "custom.info.model_link" - - model = fields.Char( - index=True, - readonly=True, - required=True) - model_id = fields.Many2one( - 'ir.model', - 'Model', - compute="_compute_model_id", - store=True) - - @api.multi - @api.depends("model") - def _compute_model_id(self): - """Get a related model from its name, for better UI.""" - for s in self: - s.model_id = self.env["ir.model"].search([("model", "=", s.model)]) - - -class CustomInfoTemplate(models.Model): - """Defines custom properties expected for a given database object.""" - _description = "Custom information template" - _name = "custom.info.template" - _inherit = "custom.info.model_link" - _sql_constraints = [ - ("name_model", - "UNIQUE (name, model)", - "Another template with that name exists for that model."), - ] - - name = fields.Char(required=True, translate=True) - info_ids = fields.One2many( - 'custom.info.property', - 'template_id', - 'Properties') - - -class CustomInfoProperty(models.Model): - """Name of the custom information property.""" - _description = "Custom information property" - _name = "custom.info.property" - _sql_constraints = [ - ("name_template", - "UNIQUE (name, template_id)", - "Another property with that name exists for that template."), - ] - - name = fields.Char(required=True, translate=True) - template_id = fields.Many2one( - comodel_name='custom.info.template', - string='Template', - required=True) - info_value_ids = fields.One2many( - comodel_name="custom.info.value", - inverse_name="property_id", - string="Property Values") - - -class CustomInfoValue(models.Model): - _description = "Custom information value" - _name = "custom.info.value" - _inherit = "custom.info.model_link" - _rec_name = 'value' - _sql_constraints = [ - ("property_model_res", - "UNIQUE (property_id, model, res_id)", - "Another property with that name exists for that resource."), - ] - - res_id = fields.Integer("Resource ID", index=True, required=True) - property_id = fields.Many2one( - comodel_name='custom.info.property', - required=True, - string='Property') - name = fields.Char(related='property_id.name', readonly=True) - value = fields.Char(translate=True, index=True) +class CustomInfo(models.AbstractModel): + """Models that inherit this one will get custom information for free! + They will probably want to declare a default model in the context of the + :attr:`custom_info_template_id` field. -class CustomInfo(models.AbstractModel): + See example in :mod:`res_partner`. + """ _description = "Inheritable abstract model to add custom info in any model" _name = "custom.info" custom_info_template_id = fields.Many2one( comodel_name='custom.info.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', domain=lambda self: [("model", "=", self._name)], + context={"embed": True}, auto_join=True, string='Custom Properties') @api.multi @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, - }) + new = [(5, False, False)] + for prop in self.custom_info_template_id.property_ids: + new += [(0, False, { + "property_id": prop.id, + "res_id": self.id, + "model": self._name, + })] + self.custom_info_ids = new + self.custom_info_ids._onchange_property_set_default_value() + self.custom_info_ids._inverse_value() + self.custom_info_ids._compute_value() + + # HACK https://github.com/OCA/server-tools/pull/492#issuecomment-237594285 + @api.multi + def onchange(self, values, field_name, field_onchange): + """Add custom info children values that will be probably changed.""" + subfields = ("category_id", "field_type", "required", "property_id", + "res_id", "model", "value", "value_str", "value_int", + "value_float", "value_bool", "value_id") + for subfield in subfields: + field_onchange.setdefault("custom_info_ids." + subfield, "") + return super(CustomInfo, self).onchange( + values, field_name, field_onchange) @api.multi def unlink(self): @@ -124,3 +64,13 @@ class CustomInfo(models.AbstractModel): 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), + ]) diff --git a/base_custom_info/models/custom_info_category.py b/base_custom_info/models/custom_info_category.py new file mode 100644 index 000000000..f48551471 --- /dev/null +++ b/base_custom_info/models/custom_info_category.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# © 2016 Jairo Llopis +# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html + +from openerp import api, fields, models + + +class CustomInfoCategory(models.Model): + _description = "Categorize custom info properties" + _name = "custom.info.category" + _order = "sequence, name" + + name = fields.Char(index=True, translate=True, required=True) + sequence = fields.Integer(index=True) + property_ids = fields.One2many( + comodel_name="custom.info.property", + inverse_name="category_id", + string="Properties", + help="Properties in this category.", + ) + + @api.multi + def check_access_rule(self, operation): + """You access a category if you access at least one property.""" + last = None + for prop in self.mapped("property_ids"): + try: + prop.check_access_rule(operation) + return + except Exception as last: + pass + if last: + raise last + return super(CustomInfoCategory, self).check_access_rule(operation) diff --git a/base_custom_info/models/custom_info_option.py b/base_custom_info/models/custom_info_option.py new file mode 100644 index 000000000..08d812c41 --- /dev/null +++ b/base_custom_info/models/custom_info_option.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# © 2016 Jairo Llopis +# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html + +from openerp import api, fields, models + + +class CustomInfoOption(models.Model): + _description = "Available options for a custom property" + _name = "custom.info.option" + _order = "name" + + name = fields.Char(index=True, translate=True, required=True) + property_ids = fields.Many2many( + comodel_name="custom.info.property", + string="Properties", + help="Properties where this option is enabled.", + ) + value_ids = fields.One2many( + comodel_name="custom.info.value", + inverse_name="value_id", + string="Values", + help="Values that have set this option.", + ) + + @api.multi + def check_access_rule(self, operation): + """You access an option if you access at least one property.""" + last = None + for prop in self.mapped("property_ids"): + try: + prop.check_access_rule(operation) + return + except Exception as last: + pass + if last: + raise last + return super(CustomInfoOption, self).check_access_rule(operation) diff --git a/base_custom_info/models/custom_info_property.py b/base_custom_info/models/custom_info_property.py new file mode 100644 index 000000000..67917cff3 --- /dev/null +++ b/base_custom_info/models/custom_info_property.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# © 2016 Jairo Llopis +# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html +from openerp import _, api, fields, models +from openerp.exceptions import UserError, ValidationError + + +class CustomInfoProperty(models.Model): + """Name of the custom information property.""" + _description = "Custom information property" + _name = "custom.info.property" + _order = "template_id, category_sequence, category_id, sequence, id" + _sql_constraints = [ + ("name_template", + "UNIQUE (name, template_id)", + "Another property with that name exists for that template."), + ] + + name = fields.Char(required=True, translate=True) + sequence = fields.Integer(index=True) + category_id = fields.Many2one( + comodel_name="custom.info.category", + string="Category", + ) + category_sequence = fields.Integer( + related="category_id.sequence", + store=True, + readonly=True, + ) + template_id = fields.Many2one( + comodel_name='custom.info.template', + string='Template', + required=True) + model = fields.Char( + related="template_id.model", + readonly=True, + auto_join=True, + ) + info_value_ids = fields.One2many( + comodel_name="custom.info.value", + inverse_name="property_id", + string="Property Values") + default_value = fields.Char( + translate=True, + help="Will be applied by default to all custom values of this " + "property. This is a char field, so you have to enter some value " + "that can be converted to the field type you choose.", + ) + required = fields.Boolean() + minimum = fields.Float( + help="For numeric fields, it means the minimum possible value; " + "for text fields, it means the minimum possible length. " + "If it is bigger than the maximum, then this check is skipped", + ) + maximum = fields.Float( + default=-1, + help="For numeric fields, it means the maximum possible value; " + "for text fields, it means the maximum possible length. " + "If it is smaller than the minimum, then this check is skipped", + ) + field_type = fields.Selection( + selection=[ + ("str", "Text"), + ("int", "Whole number"), + ("float", "Decimal number"), + ("bool", "Yes/No"), + ("id", "Selection"), + ], + default="str", + required=True, + help="Type of information that can be stored in the property.", + ) + option_ids = fields.Many2many( + comodel_name="custom.info.option", + string="Options", + help="When the field type is 'selection', choose the available " + "options here.", + ) + + @api.multi + def check_access_rule(self, operation): + """You access a property if you access its template.""" + self.mapped("template_id").check_access_rule(operation) + return super(CustomInfoProperty, self).check_access_rule(operation) + + @api.one + @api.constrains("default_value", "field_type") + def _check_default_value(self): + """Ensure the default value is valid.""" + if self.default_value: + try: + self.env["custom.info.value"]._transform_value( + self.default_value, self.field_type, self) + except ValueError: + selection = dict( + self._fields["field_type"].get_description(self.env) + ["selection"]) + raise ValidationError( + _("Default value %s cannot be converted to type %s.") % + (self.default_value, selection[self.field_type])) + + @api.multi + @api.onchange("required", "field_type") + def _onchange_required_warn(self): + """Warn if the required flag implies a possible weird behavior.""" + if self.required: + if self.field_type == "bool": + raise UserError( + _("If you require a Yes/No field, you can only set Yes.")) + if self.field_type in {"int", "float"}: + raise UserError( + _("If you require a numeric field, you cannot set it to " + "zero.")) diff --git a/base_custom_info/models/custom_info_template.py b/base_custom_info/models/custom_info_template.py new file mode 100644 index 000000000..16e814e0e --- /dev/null +++ b/base_custom_info/models/custom_info_template.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# © 2016 Jairo Llopis +# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html + +from openerp import _, api, fields, models +from openerp.exceptions import ValidationError + + +class CustomInfoTemplate(models.Model): + """Defines custom properties expected for a given database object.""" + _description = "Custom information template" + _name = "custom.info.template" + _order = "model_id, name" + _sql_constraints = [ + ("name_model", + "UNIQUE (name, model)", + "Another template with that name exists for that model."), + ] + + name = fields.Char(required=True, translate=True) + model = fields.Char( + index=True, + readonly=True, + required=True) + model_id = fields.Many2one( + 'ir.model', + 'Model', + compute="_compute_model_id", + store=True, + ondelete="cascade", + ) + property_ids = fields.One2many( + 'custom.info.property', + 'template_id', + 'Properties', + oldname="info_ids", + context={"embed": True}, + ) + + @api.multi + @api.depends("model") + def _compute_model_id(self): + """Get a related model from its name, for better UI.""" + for s in self: + s.model_id = self.env["ir.model"].search([("model", "=", s.model)]) + + @api.multi + @api.constrains("model") + def _check_model(self): + """Ensure model exists.""" + for s in self: + if s.model not in self.env: + raise ValidationError(_("Model does not exist.")) + # Avoid error when updating base module and a submodule extends a + # model that falls out of this one's dependency graph + with self.env.norecompute(): + oldmodels = set(s.mapped("property_ids.info_value_ids.model")) + if oldmodels and {s.model} != oldmodels: + raise ValidationError( + _("You cannot change the model because it is in use.")) + + @api.multi + def check_access_rule(self, operation): + """You access a template if you access its model.""" + for s in self: + model = self.env[s.model] + model.check_access_rights(operation) + model.check_access_rule(operation) + return super(CustomInfoTemplate, self).check_access_rule(operation) diff --git a/base_custom_info/models/custom_info_value.py b/base_custom_info/models/custom_info_value.py new file mode 100644 index 000000000..321b25f22 --- /dev/null +++ b/base_custom_info/models/custom_info_value.py @@ -0,0 +1,283 @@ +# -*- coding: utf-8 -*- +# © 2016 Jairo Llopis +# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html +from openerp import _, api, fields, models, SUPERUSER_ID +from openerp.exceptions import ValidationError +from openerp.tools.safe_eval import safe_eval + + +class CustomInfoValue(models.Model): + _description = "Custom information value" + _name = "custom.info.value" + _rec_name = 'value' + _order = ("model, res_id, category_sequence, category_id, " + "property_sequence, property_id") + _sql_constraints = [ + ("property_owner", + "UNIQUE (property_id, model, res_id)", + "Another property with that name exists for that resource."), + ] + + model = fields.Char( + related="property_id.model", + index=True, + readonly=True, + auto_join=True, + store=True, + ) + owner_id = fields.Reference( + selection="_selection_owner_id", + string="Owner", + compute="_compute_owner_id", + inverse="_inverse_owner_id", + help="Record that owns this custom value.", + ) + res_id = fields.Integer( + "Resource ID", + required=True, + index=True, + store=True, + ondelete="cascade", + ) + property_id = fields.Many2one( + comodel_name='custom.info.property', + required=True, + string='Property') + property_sequence = fields.Integer( + related="property_id.sequence", + store=True, + index=True, + readonly=True, + ) + category_sequence = fields.Integer( + related="property_id.category_id.sequence", + store=True, + readonly=True, + ) + category_id = fields.Many2one( + related="property_id.category_id", + store=True, + readonly=True, + ) + name = fields.Char(related='property_id.name', readonly=True) + field_type = fields.Selection(related="property_id.field_type") + field_name = fields.Char( + compute="_compute_field_name", + help="Technical name of the field where the value is stored.", + ) + required = fields.Boolean(related="property_id.required") + value = fields.Char( + compute="_compute_value", + inverse="_inverse_value", + search="_search_value", + help="Value, always converted to/from the typed field.", + ) + value_str = fields.Char( + string="Text value", + translate=True, + index=True, + ) + value_int = fields.Integer( + string="Whole number value", + index=True, + ) + value_float = fields.Float( + string="Decimal number value", + index=True, + ) + value_bool = fields.Boolean( + string="Yes/No value", + index=True, + ) + value_id = fields.Many2one( + comodel_name="custom.info.option", + string="Selection value", + ondelete="cascade", + domain="[('property_ids', 'in', [property_id])]", + ) + + @api.multi + def check_access_rule(self, operation): + """You access a value if you access its property and owner record.""" + if self.env.uid == SUPERUSER_ID: + return + for s in self: + s.property_id.check_access_rule(operation) + s.owner_id.check_access_rights(operation) + s.owner_id.check_access_rule(operation) + return super(CustomInfoValue, self).check_access_rule(operation) + + @api.model + def create(self, vals): + """Skip constrains in 1st lap.""" + # HACK https://github.com/odoo/odoo/pull/13439 + if "value" in vals: + self.env.context.skip_required = True + return super(CustomInfoValue, self).create(vals) + + @api.model + def _selection_owner_id(self): + """You can choose among models linked to a template.""" + models = self.env["ir.model.fields"].search([ + ("ttype", "=", "many2one"), + ("relation", "=", "custom.info.template"), + ("model_id.transient", "=", False), + "!", ("model", "=like", "custom.info.%"), + ]).mapped("model_id") + models = models.search([("id", "in", models.ids)], order="name") + return [(m.model, m.name) for m in models + if m.model in self.env and self.env[m.model]._auto] + + @api.multi + @api.depends("property_id.field_type") + def _compute_field_name(self): + """Get the technical name where the real typed value is stored.""" + for s in self: + s.field_name = "value_{!s}".format(s.property_id.field_type) + + @api.multi + @api.depends("res_id", "model") + def _compute_owner_id(self): + """Get the id from the linked record.""" + for s in self: + s.owner_id = "{},{}".format(s.model, s.res_id) + + @api.multi + def _inverse_owner_id(self): + """Store the owner according to the model and ID.""" + for s in self: + s.model = s.owner_id._name + s.res_id = s.owner_id.id + + @api.multi + @api.depends("property_id.field_type", "field_name", "value_str", + "value_int", "value_float", "value_bool", "value_id") + def _compute_value(self): + """Get the value as a string, from the original field.""" + for s in self: + if s.field_type == "id": + s.value = ", ".join(s.value_id.mapped("display_name")) + elif s.field_type == "bool": + s.value = _("Yes") if s.value_bool else _("No") + else: + s.value = getattr(s, s.field_name, False) + + @api.multi + def _inverse_value(self): + """Write the value correctly converted in the typed field.""" + for s in self: + s[s.field_name] = self._transform_value( + s.value, s.field_type, s.property_id) + + @api.one + @api.constrains("required", "field_name", "value_str", "value_int", + "value_float", "value_bool", "value_id") + def _check_required(self): + """Ensure required fields are filled""" + # HACK https://github.com/odoo/odoo/pull/13439 + try: + del self.env.context.skip_required + except AttributeError: + if self.required and not self[self.field_name]: + raise ValidationError( + _("Property %s is required.") % + self.property_id.display_name) + + @api.one + @api.constrains("property_id", "field_type", "field_name", + "value_str", "value_int", "value_float") + def _check_min_max_limits(self): + """Ensure value falls inside the property's stablished limits.""" + minimum, maximum = self.property_id.minimum, self.property_id.maximum + if minimum <= maximum: + value = self[self.field_name] + if not value: + # This is a job for :meth:`.~_check_required` + return + if self.field_type == "str": + number = len(self.value_str) + message = _( + "Length for %(prop)s is %(val)s, but it should be " + "between %(min)d and %(max)d.") + elif self.field_type in {"int", "float"}: + number = value + if self.field_type == "int": + message = _( + "Value for %(prop)s is %(val)s, but it should be " + "between %(min)d and %(max)d.") + else: + message = _( + "Value for %(prop)s is %(val)s, but it should be " + "between %(min)f and %(max)f.") + else: + return + if not minimum <= number <= maximum: + raise ValidationError(message % { + "prop": self.property_id.display_name, + "val": number, + "min": minimum, + "max": maximum, + }) + + @api.multi + @api.onchange("property_id") + def _onchange_property_set_default_value(self): + """Load default value for this property.""" + for record in self: + if not record.value and record.property_id.default_value: + record.value = record.property_id.default_value + + @api.model + def _transform_value(self, value, format_, properties=None): + """Transforms a text value to the expected format. + + :param str/bool value: + Custom value in raw string. + + :param str format_: + Target conversion format for the value. Must be available among + ``custom.info.property`` options. + + :param recordset properties: + Useful when :param:`format_` is ``id``, as it helps to ensure the + option is available in these properties. If :param:`format_` is + ``id`` and :param:`properties` is ``None``, no transformation will + be made for :param:`value`. + """ + if not value: + value = False + elif format_ == "id" and properties: + value = self.env["custom.info.option"].search( + [("property_ids", "in", properties.ids), + ("name", "=ilike", value)]) + value.ensure_one() + elif format_ == "bool": + value = value.strip().lower() not in { + "0", "false", "", "no", "off", _("No").lower()} + elif format_ not in {"str", "id"}: + value = safe_eval("{!s}({!r})".format(format_, value)) + return value + + @api.model + def _search_value(self, operator, value): + """Search from the stored field directly.""" + options = ( + o[0] for o in + self.property_id._fields["field_type"] + .get_description(self.env)["selection"]) + domain = [] + for fmt in options: + try: + _value = (self._transform_value(value, fmt) + if not isinstance(value, list) else + [self._transform_value(v, fmt) for v in value]) + except ValueError: + # If you are searching something that cannot be casted, then + # your property is probably from another type + continue + domain += [ + "&", + ("field_type", "=", fmt), + ("value_" + fmt, operator, _value), + ] + return ["|"] * (len(domain) / 3 - 1) + domain diff --git a/base_custom_info/models/res_partner.py b/base_custom_info/models/res_partner.py new file mode 100644 index 000000000..cc431bfbf --- /dev/null +++ b/base_custom_info/models/res_partner.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Jairo Llopis +# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html + +from openerp import fields, models + + +class ResPartner(models.Model): + """Implement custom information for partners. + + Besides adding some visible feature to the module, this is useful for + testing and example purposes. + """ + _name = "res.partner" + _inherit = [_name, "custom.info"] + + custom_info_template_id = fields.Many2one( + context={"default_model": _name}, + ) + custom_info_ids = fields.One2many( + context={"default_model": _name}, + ) diff --git a/base_custom_info/security/ir.model.access.csv b/base_custom_info/security/ir.model.access.csv index d285e7021..ee65f8e15 100644 --- a/base_custom_info/security/ir.model.access.csv +++ b/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 diff --git a/base_custom_info/security/res_groups.xml b/base_custom_info/security/res_groups.xml new file mode 100644 index 000000000..684c141c8 --- /dev/null +++ b/base_custom_info/security/res_groups.xml @@ -0,0 +1,31 @@ + + + + + + + Custom Information + + + + Display in partner form + + Will be able to edit custom information from partner's form. + + + + Basic management + + The user will be able to manage basic custom information. + + + + Advanced management + + The user will be able to manage advanced custom information. + + + + + diff --git a/base_custom_info/tests/__init__.py b/base_custom_info/tests/__init__.py new file mode 100644 index 000000000..18f016dd7 --- /dev/null +++ b/base_custom_info/tests/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Jairo Llopis +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import test_partner, test_value_conversion diff --git a/base_custom_info/tests/test_partner.py b/base_custom_info/tests/test_partner.py new file mode 100644 index 000000000..ee39778ce --- /dev/null +++ b/base_custom_info/tests/test_partner.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Jairo Llopis +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from openerp.exceptions import AccessError, ValidationError +from openerp.tests.common import TransactionCase + + +class PartnerCase(TransactionCase): + def setUp(self, *args, **kwargs): + super(PartnerCase, self).setUp(*args, **kwargs) + self.agrolait = self.env.ref("base.res_partner_2") + self.tpl = self.env.ref("base_custom_info.tpl_smart") + self.demouser = self.env.ref("base.user_demo") + + def set_custom_info_for_agrolait(self): + """Used when you need to use some created custom info.""" + self.agrolait.custom_info_template_id = self.tpl + self.env["custom.info.value"].create({ + "res_id": self.agrolait.id, + "property_id": self.env.ref("base_custom_info.prop_haters").id, + "value_int": 5, + }) + + def test_access_granted(self): + """Access to the model implies access to custom info.""" + # Demo user has contact creation permissions by default + agrolait = self.agrolait.sudo(self.demouser) + agrolait.custom_info_template_id = self.tpl + agrolait.env["custom.info.value"].create({ + "res_id": agrolait.id, + "property_id": + agrolait.env.ref("base_custom_info.prop_weaknesses").id, + "value_id": agrolait.env.ref("base_custom_info.opt_food").id, + }) + agrolait.custom_info_template_id.property_ids[0].name = "Changed!" + agrolait.env.ref("base_custom_info.opt_food").name = "Changed!" + + def test_access_denied(self): + """Forbidden access to the model forbids it to custom info.""" + # Remove permissions to demo user + self.demouser.groups_id = self.env.ref("base.group_portal") + + agrolait = self.agrolait.sudo(self.demouser) + with self.assertRaises(AccessError): + agrolait.custom_info_template_id = self.tpl + + with self.assertRaises(AccessError): + agrolait.env["custom.info.value"].create({ + "res_id": agrolait.id, + "property_id": + agrolait.env.ref("base_custom_info.prop_weaknesses").id, + "value_id": agrolait.env.ref("base_custom_info.opt_food").id, + }) + + with self.assertRaises(AccessError): + agrolait.custom_info_template_id.property_ids[0].name = "Changed!" + + with self.assertRaises(AccessError): + agrolait.env.ref("base_custom_info.opt_food").name = "Changed!" + + def test_apply_unapply_template(self): + """(Un)apply a template to a owner and it gets filled.""" + # Applying a template autofills the values + self.agrolait.custom_info_template_id = self.tpl + with self.env.do_in_onchange(): + self.agrolait._onchange_custom_info_template_id() + self.assertEqual( + len(self.agrolait.custom_info_ids), + len(self.tpl.property_ids)) + self.assertEqual( + self.agrolait.custom_info_ids.mapped("property_id"), + self.tpl.property_ids) + + # Unapplying a template empties the values + self.agrolait.custom_info_template_id = False + self.agrolait._onchange_custom_info_template_id() + self.assertFalse(self.agrolait.custom_info_template_id) + + def test_template_model_and_model_id_match(self): + """Template's model and model_id fields match.""" + self.assertEqual(self.tpl.model, self.tpl.model_id.model) + self.tpl.model = "res.users" + self.assertEqual(self.tpl.model, self.tpl.model_id.model) + + def test_template_model_must_exist(self): + """Cannot create templates for unexisting models.""" + with self.assertRaises(ValidationError): + self.tpl.model = "yabadabaduu" + + def test_change_used_model_fails(self): + """If a template's model is already used, you cannot change it.""" + self.set_custom_info_for_agrolait() + with self.assertRaises(ValidationError): + self.tpl.model = "res.users" + + def test_owners_selection(self): + """Owners selection includes only the required matches.""" + choices = dict(self.env["custom.info.value"]._selection_owner_id()) + self.assertIn("res.partner", choices) + self.assertNotIn("ir.model", choices) + self.assertNotIn("custom.info.property", choices) + self.assertNotIn("custom.info", choices) + + def test_owner_id(self): + """Check the computed owner id for a value.""" + self.set_custom_info_for_agrolait() + self.assertEqual(self.agrolait.custom_info_ids.owner_id, self.agrolait) + + def test_get_custom_info_value(self): + """Check the custom info getter helper works fine.""" + self.set_custom_info_for_agrolait() + result = self.agrolait.get_custom_info_value( + self.env.ref("base_custom_info.prop_haters")) + self.assertEqual(result.field_type, "int") + self.assertEqual(result.field_name, "value_int") + self.assertEqual(result[result.field_name], 5) + self.assertEqual(result.value_int, 5) + self.assertEqual(result.value, "5") diff --git a/base_custom_info/tests/test_value_conversion.py b/base_custom_info/tests/test_value_conversion.py new file mode 100644 index 000000000..1f18c2dcf --- /dev/null +++ b/base_custom_info/tests/test_value_conversion.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Jairo Llopis +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import logging + +from openerp.tests.common import TransactionCase + +_logger = logging.getLogger(__name__) + + +class ValueConversionCase(TransactionCase): + def setUp(self): + super(ValueConversionCase, self).setUp() + self.agrolait = self.env.ref("base.res_partner_2") + self.tpl = self.env.ref("base_custom_info.tpl_smart") + self.prop_str = self.env.ref("base_custom_info.prop_teacher") + self.prop_int = self.env.ref("base_custom_info.prop_haters") + self.prop_float = self.env.ref("base_custom_info.prop_avg_note") + self.prop_bool = self.env.ref("base_custom_info.prop_smartypants") + self.prop_id = self.env.ref("base_custom_info.prop_weaknesses") + + def create_value(self, prop, value, field="value"): + """Create a custom info value.""" + _logger.info( + "Creating. prop: %s; value: %s; field: %s", prop, value, field) + self.agrolait.custom_info_template_id = self.tpl + if field == "value": + value = str(value) + self.value = self.env["custom.info.value"].create({ + "res_id": self.agrolait.id, + "property_id": prop.id, + field: value, + }) + + def creation_found(self, value): + """Ensure you can search what you just created.""" + prop = self.value.property_id + _logger.info( + "Searching. prop: %s; value: %s", prop, value) + self.assertEqual( + self.value.search([ + ("property_id", "=", prop.id), + ("value", "=", value)]), + self.value) + self.assertEqual( + self.value.search([ + ("property_id", "=", prop.id), + ("value", "in", [value])]), + self.value) + self.assertIs( + self.value.search([ + ("property_id", "=", prop.id), + ("value", "not in", [value])]).id, + False) + + def test_to_str(self): + """Conversion to text.""" + self.create_value(self.prop_str, "Mr. Einstein") + self.creation_found("Mr. Einstein") + self.assertEqual(self.value.value, self.value.value_str) + + def test_from_str(self): + """Conversion from text.""" + self.create_value(self.prop_str, "Mr. Einstein", "value_str") + self.creation_found("Mr. Einstein") + self.assertEqual(self.value.value, self.value.value_str) + + def test_to_int(self): + """Conversion to whole number.""" + self.create_value(self.prop_int, 5) + self.creation_found("5") + self.assertEqual(int(self.value.value), self.value.value_int) + + def test_from_int(self): + """Conversion from whole number.""" + self.create_value(self.prop_int, 5, "value_int") + self.creation_found("5") + self.assertEqual(int(self.value.value), self.value.value_int) + + def test_to_float(self): + """Conversion to decimal number.""" + self.create_value(self.prop_float, 10.5) + self.creation_found("10.5") + self.assertEqual(float(self.value.value), self.value.value_float) + + def test_from_float(self): + """Conversion from decimal number.""" + self.create_value(self.prop_float, 10.5, "value_float") + self.creation_found("10.5") + self.assertEqual(float(self.value.value), self.value.value_float) + + def test_to_bool_true(self): + """Conversion to yes.""" + self.create_value(self.prop_bool, "True") + self.creation_found("True") + self.assertEqual(self.value.with_context(lang="en_US").value, "Yes") + self.assertIs(self.value.value_bool, True) + + def test_from_bool_true(self): + """Conversion from yes.""" + self.create_value(self.prop_bool, "True", "value_bool") + self.creation_found("True") + self.assertEqual(self.value.with_context(lang="en_US").value, "Yes") + self.assertIs(self.value.value_bool, True) + + def test_to_bool_false(self): + """Conversion to no.""" + self.create_value(self.prop_bool, "False") + self.assertEqual(self.value.with_context(lang="en_US").value, "No") + self.assertIs(self.value.value_bool, False) + + def test_from_bool_false(self): + """Conversion from no.""" + self.create_value(self.prop_bool, False, "value_bool") + self.assertEqual(self.value.with_context(lang="en_US").value, "No") + self.assertIs(self.value.value_bool, False) + + def test_to_id(self): + """Conversion to selection.""" + self.create_value(self.prop_id, "Needs videogames") + self.creation_found("Needs videogames") + self.assertEqual(self.value.value, self.value.value_id.name) + + def test_from_id(self): + """Conversion from selection.""" + self.create_value( + self.prop_id, + self.env.ref("base_custom_info.opt_videogames").id, + "value_id") + self.creation_found("Needs videogames") + self.assertEqual(self.value.value, self.value.value_id.name) diff --git a/base_custom_info/views/custom_info_category_view.xml b/base_custom_info/views/custom_info_category_view.xml new file mode 100644 index 000000000..d9b5a7355 --- /dev/null +++ b/base_custom_info/views/custom_info_category_view.xml @@ -0,0 +1,53 @@ + + + + + + Custom Info Category Tree + custom.info.category + + + + + + + + + + + Custom Info Category Form + custom.info.category + +
+ + + + + + + +
+
+
+ + + Custom Info Category Search + custom.info.category + + + + + + + + + + Categories + ir.actions.act_window + custom.info.category + tree,form + form + + +
diff --git a/base_custom_info/views/custom_info_option_view.xml b/base_custom_info/views/custom_info_option_view.xml new file mode 100644 index 000000000..dfad88e3e --- /dev/null +++ b/base_custom_info/views/custom_info_option_view.xml @@ -0,0 +1,52 @@ + + + + + + Custom Info Option Tree + custom.info.option + + + + + + + + + + Custom Info Option Form + custom.info.option + +
+ + + + + + + +
+
+
+ + + Custom Info Option Search + custom.info.option + + + + + + + + + + Options + ir.actions.act_window + custom.info.option + tree,form + form + + +
diff --git a/base_custom_info/views/custom_info_property_view.xml b/base_custom_info/views/custom_info_property_view.xml index 72bda5437..21d447ec6 100644 --- a/base_custom_info/views/custom_info_property_view.xml +++ b/base_custom_info/views/custom_info_property_view.xml @@ -1,36 +1,77 @@ - + + - - base.custom.info.property.tree + + Custom Info Property Tree custom.info.property - + + - + + + + + - - base.custom.info.property.form + + Custom Info Property Form custom.info.property
- + + + + + + + - + +
- + + Custom Info Property Search + custom.info.property + + + + + + + + + + + + + + + + + Properties ir.actions.act_window custom.info.property @@ -38,4 +79,4 @@ form -
+ diff --git a/base_custom_info/views/custom_info_template_view.xml b/base_custom_info/views/custom_info_template_view.xml index 3e9ff7cbd..d8500fe96 100644 --- a/base_custom_info/views/custom_info_template_view.xml +++ b/base_custom_info/views/custom_info_template_view.xml @@ -1,21 +1,23 @@ - + + - base.custom.info.template.tree + Custom Info Template Tree custom.info.template - + - base.custom.info.template.form + Custom Info Template Form custom.info.template
@@ -26,17 +28,30 @@ - - - - - +
+ + Custom Info Template Search + custom.info.template + + + + + + + + + + + + Templates ir.actions.act_window @@ -49,10 +64,9 @@

Click to define a new custom info template.

- You must define a custom info template for each - product properties group. + You must define a custom info template for each properties group.

-
+ diff --git a/base_custom_info/views/custom_info_value_view.xml b/base_custom_info/views/custom_info_value_view.xml index 53ed922dd..519721095 100644 --- a/base_custom_info/views/custom_info_value_view.xml +++ b/base_custom_info/views/custom_info_value_view.xml @@ -1,19 +1,87 @@ - + + - base.custom.info.value.tree + Custom Info Value Tree custom.info.value - + + + - - + + Custom Info Value Form + custom.info.value + +
+ + + + + + + + + + + + + + + +
+ Warning! + You might see no changes in parent form until you save it. +
+
+
+
+
+ + + Custom Info Value Search + custom.info.value + + + + + + + + + + + + + + + + Values ir.actions.act_window @@ -22,4 +90,4 @@ form -
+ diff --git a/base_custom_info/views/menu.xml b/base_custom_info/views/menu.xml index 8f4b2220c..f69e82584 100644 --- a/base_custom_info/views/menu.xml +++ b/base_custom_info/views/menu.xml @@ -1,23 +1,41 @@ - + + - + + groups="base_custom_info.group_basic"/> + + + + - - - - + parent="menu_basic" sequence="10"/> - + parent="menu_basic" sequence="20"/> + + + + + + - + diff --git a/base_custom_info/views/res_partner_view.xml b/base_custom_info/views/res_partner_view.xml new file mode 100644 index 000000000..5f64e41f0 --- /dev/null +++ b/base_custom_info/views/res_partner_view.xml @@ -0,0 +1,28 @@ + + + + + + Custom info in partners form + res.partner + + + + + + + + + + + + + + diff --git a/base_custom_info/wizard/__init__.py b/base_custom_info/wizard/__init__.py new file mode 100644 index 000000000..d3a343ecb --- /dev/null +++ b/base_custom_info/wizard/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# © 2015 Antiun Ingeniería S.L. - Sergio Teruel +# © 2015 Antiun Ingeniería S.L. - Carlos Dauden +# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html + +from . import base_config_settings diff --git a/base_custom_info/wizard/base_config_settings.py b/base_custom_info/wizard/base_config_settings.py new file mode 100644 index 000000000..6d78f1209 --- /dev/null +++ b/base_custom_info/wizard/base_config_settings.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Jairo Llopis +# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html + +from openerp import fields, models + + +class BaseConfigSettings(models.TransientModel): + _inherit = "base.config.settings" + + group_custom_info_manager = fields.Boolean( + string="Manage custom information", + implied_group="base_custom_info.group_basic", + help="Allow all employees to manage custom information", + ) + group_custom_info_partner = fields.Boolean( + string="Edit custom information in partners", + implied_group="base_custom_info.group_partner", + help="Add a tab in the partners form to edit custom information", + ) diff --git a/base_custom_info/wizard/base_config_settings_view.xml b/base_custom_info/wizard/base_config_settings_view.xml new file mode 100644 index 000000000..095b60943 --- /dev/null +++ b/base_custom_info/wizard/base_config_settings_view.xml @@ -0,0 +1,33 @@ + + + + + + Allow to enable partners custom info + base.config.settings + + + + + + + + + + + +