From 7894810ec668603aea1bc56125ddd6acc8f699c6 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Tue, 2 Jun 2020 15:27:19 +0200 Subject: [PATCH] [12.0][IMP] base_custom_info: Create a specific view --- base_custom_info/README.rst | 3 + base_custom_info/__manifest__.py | 4 +- .../demo/custom.info.property.csv | 15 +- .../migrations/12.0.2.0.0/pre-migration.py | 32 ++++ base_custom_info/models/__init__.py | 2 + base_custom_info/models/custom_info.py | 15 +- .../models/custom_info_property.py | 36 ++++- base_custom_info/models/custom_info_value.py | 37 ++--- .../models/ir_actions_act_window_view.py | 12 ++ base_custom_info/models/ir_ui_view.py | 10 ++ base_custom_info/readme/CONTRIBUTORS.rst | 3 + .../static/description/index.html | 4 + .../static/src/js/custom_info_renderer.js | 148 ++++++++++++++++++ .../static/src/js/custom_info_view.js | 24 +++ .../static/src/js/relational_fields.js | 58 +++++++ .../static/src/scss/custom_info.scss | 9 ++ .../static/src/xml/custom_info_item.xml | 21 +++ base_custom_info/tests/__init__.py | 3 +- base_custom_info/tests/test_partner.py | 12 +- base_custom_info/tests/test_required.py | 48 ++++++ .../tests/test_value_conversion.py | 16 +- .../views/custom_info_property_view.xml | 2 + .../views/custom_info_value_view.xml | 25 +++ base_custom_info/views/res_partner_view.xml | 2 +- .../views/webclient_templates.xml | 19 +++ 25 files changed, 512 insertions(+), 48 deletions(-) create mode 100644 base_custom_info/migrations/12.0.2.0.0/pre-migration.py create mode 100644 base_custom_info/models/ir_actions_act_window_view.py create mode 100644 base_custom_info/models/ir_ui_view.py create mode 100644 base_custom_info/static/src/js/custom_info_renderer.js create mode 100644 base_custom_info/static/src/js/custom_info_view.js create mode 100644 base_custom_info/static/src/js/relational_fields.js create mode 100644 base_custom_info/static/src/scss/custom_info.scss create mode 100644 base_custom_info/static/src/xml/custom_info_item.xml create mode 100644 base_custom_info/tests/test_required.py create mode 100644 base_custom_info/views/webclient_templates.xml diff --git a/base_custom_info/README.rst b/base_custom_info/README.rst index 90bbbe13d..f3c3400ac 100644 --- a/base_custom_info/README.rst +++ b/base_custom_info/README.rst @@ -260,6 +260,9 @@ Contributors * Jairo Llopis * Pedro M. Baeza * Alexandre Díaz +* Creu Blanca: + + * Enric Tobella Maintainers ~~~~~~~~~~~ diff --git a/base_custom_info/__manifest__.py b/base_custom_info/__manifest__.py index f874a9fc7..f2011249c 100644 --- a/base_custom_info/__manifest__.py +++ b/base_custom_info/__manifest__.py @@ -7,11 +7,12 @@ 'name': "Base Custom Info", 'summary': "Add custom field in models", 'category': 'Tools', - 'version': '12.0.1.0.2', + 'version': '12.0.2.0.0', 'depends': [ 'base_setup', ], 'data': [ + 'views/webclient_templates.xml', 'security/ir.model.access.csv', 'security/res_groups_security.xml', 'views/custom_info_category_view.xml', @@ -23,6 +24,7 @@ 'views/res_partner_view.xml', 'wizard/res_config_settings_view.xml', ], + 'qweb': ['static/src/xml/custom_info_item.xml'], 'demo': [ 'demo/custom.info.category.csv', 'demo/custom.info.template.csv', diff --git a/base_custom_info/demo/custom.info.property.csv b/base_custom_info/demo/custom.info.property.csv index 724562f7a..568bde66d 100644 --- a/base_custom_info/demo/custom.info.property.csv +++ b/base_custom_info/demo/custom.info.property.csv @@ -1,8 +1,9 @@ -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 +id,name,template_id:id,widget,required,minimum,maximum,category_id:id,sequence +prop_teacher,Name of his/her teacher,tpl_smart,char,,1,30,,100 +prop_haters,Amount of people that hates him/her for being so smart,tpl_smart,integer,,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 +prop_smartypants,Does he/she believe he/she is the smartest person on earth?,tpl_smart,boolean,,0,-1,,400 +prop_weaknesses,What weaknesses does he/she have?,tpl_smart,many2one,,0,-1,,500 +prop_fav_genre,Favourite videogames genre,tpl_gamer,many2one,,0,-1,cat_gaming,600 +prop_fav_game,Favourite videogame,tpl_gamer,char,,0,-1,cat_gaming,700 +prop_buy_fav_game,When Favourite videogame was bought?,tpl_gamer,date,,0,-1,cat_gaming,700 diff --git a/base_custom_info/migrations/12.0.2.0.0/pre-migration.py b/base_custom_info/migrations/12.0.2.0.0/pre-migration.py new file mode 100644 index 000000000..69c568153 --- /dev/null +++ b/base_custom_info/migrations/12.0.2.0.0/pre-migration.py @@ -0,0 +1,32 @@ +# Copyright 2020 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + cr = env.cr + if not openupgrade.column_exists( + cr, + 'custom_info_property', + 'widget' + ): + openupgrade.add_fields( + env, + [('widget', 'custom.info.property', 'custom_info_property', 'char', + False, 'base_custom_info')] + ) + transform_values = [ + ('str', 'char'), + ('int', 'integer'), + ('bool', 'boolean'), + ('float', 'float'), + ('id', 'many2one') + ] + for field_type, widget in transform_values: + cr.execute( + "UPDATE custom_info_property SET widget = %s WHERE " + "field_type = %s", + (widget, field_type) + ) diff --git a/base_custom_info/models/__init__.py b/base_custom_info/models/__init__.py index 21969ab25..426294b54 100644 --- a/base_custom_info/models/__init__.py +++ b/base_custom_info/models/__init__.py @@ -11,4 +11,6 @@ from . import ( custom_info_value, custom_info, res_partner, + ir_actions_act_window_view, + ir_ui_view ) diff --git a/base_custom_info/models/custom_info.py b/base_custom_info/models/custom_info.py index 7bf669b07..df5059265 100644 --- a/base_custom_info/models/custom_info.py +++ b/base_custom_info/models/custom_info.py @@ -54,13 +54,20 @@ class CustomInfo(models.AbstractModel): 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({ + vals = { "property_id": prop.id, "res_id": self.id, - "value": prop.default_value, - }) + } + if prop.default_value: + if prop.field_type != 'id': + vals["value_%s" % prop.field_type] = prop.default_value + else: + vals["value_id"] = self.env['custom.info.option'].search([ + ('property_ids', '=', prop.id), + ('name', '=', prop.default_value) + ], limit=1).id + newvalue = self.custom_info_ids.new(vals) newvalue._onchange_property_set_default_value() - newvalue._inverse_value() newvalue._compute_value() values += newvalue self.custom_info_ids = values diff --git a/base_custom_info/models/custom_info_property.py b/base_custom_info/models/custom_info_property.py index af7b06515..8aad188d8 100644 --- a/base_custom_info/models/custom_info_property.py +++ b/base_custom_info/models/custom_info_property.py @@ -64,9 +64,24 @@ class CustomInfoProperty(models.Model): ("int", "Whole number"), ("float", "Decimal number"), ("bool", "Yes/No"), + ("date", "Date"), ("id", "Selection"), ], - default="str", + compute="_compute_field_type", + store=True, + ) + widget = fields.Selection( + selection=[ + ("boolean", "Boolean"), + ("float", "Decimal"), + ("integer", "Integer"), + ("date", "Date"), + ("char", "Single line text"), + ("text", "Multi line Text"), + ("html", "Complex text"), + ("many2one", "Choice"), + ], + default="char", required=True, help="Type of information that can be stored in the property.", ) @@ -77,6 +92,25 @@ class CustomInfoProperty(models.Model): "options here.", ) + @api.model + def _get_field_type_map(self): + return { + "boolean": "bool", + "float": "float", + "integer": "int", + "date": "date", + "char": "str", + "text": "str", + "html": "str", + "many2one": "id", + } + + @api.depends('widget') + def _compute_field_type(self): + field_type_map = self._get_field_type_map() + for record in self: + record.field_type = field_type_map.get(record.widget, 'str') + @api.multi def check_access_rule(self, operation): """You access a property if you access its template.""" diff --git a/base_custom_info/models/custom_info_value.py b/base_custom_info/models/custom_info_value.py index 47956bb2f..92b90b061 100644 --- a/base_custom_info/models/custom_info_value.py +++ b/base_custom_info/models/custom_info_value.py @@ -34,7 +34,6 @@ class CustomInfoValue(models.Model): ) 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, @@ -50,13 +49,16 @@ class CustomInfoValue(models.Model): field_type = fields.Selection( related="property_id.field_type", readonly=True, ) + widget = fields.Selection( + related="property_id.widget", 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", + compute="_compute_value", search="_search_value", help="Value, always converted to/from the typed field.", ) @@ -64,6 +66,7 @@ class CustomInfoValue(models.Model): 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_date = fields.Date(string="Date value", index=True) value_id = fields.Many2one( comodel_name="custom.info.option", string="Selection value", ondelete="cascade", domain="[('property_ids', '=', property_id)]", @@ -125,18 +128,6 @@ class CustomInfoValue(models.Model): 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): @@ -172,7 +163,17 @@ class CustomInfoValue(models.Model): "max": maximum, }) - @api.multi + @api.constrains('value_str', 'value_int', 'value_float', 'value_bool', + 'value_date', 'value_id', 'property_id') + def _check_required(self): + for record in self: + if not record.required: + continue + if not record.value: + raise ValidationError(_( + 'Some required elements have not been fulfilled' + )) + @api.onchange("property_id") def _onchange_property_set_default_value(self): """Load default value for this property.""" @@ -182,12 +183,6 @@ class CustomInfoValue(models.Model): if not record.field_type and record.property_id.field_type: record.field_type = record.property_id.field_type - @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. diff --git a/base_custom_info/models/ir_actions_act_window_view.py b/base_custom_info/models/ir_actions_act_window_view.py new file mode 100644 index 000000000..efb6c48ea --- /dev/null +++ b/base_custom_info/models/ir_actions_act_window_view.py @@ -0,0 +1,12 @@ +# Copyright 2019 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class IrActionsActWindowView(models.Model): + _inherit = "ir.actions.act_window.view" + + view_mode = fields.Selection( + selection_add=[("custom_info", "Custom Info")] + ) diff --git a/base_custom_info/models/ir_ui_view.py b/base_custom_info/models/ir_ui_view.py new file mode 100644 index 000000000..fc5da3e26 --- /dev/null +++ b/base_custom_info/models/ir_ui_view.py @@ -0,0 +1,10 @@ +# Copyright 2019 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class IrUiView(models.Model): + _inherit = "ir.ui.view" + + type = fields.Selection(selection_add=[("custom_info", "Custom Info")]) diff --git a/base_custom_info/readme/CONTRIBUTORS.rst b/base_custom_info/readme/CONTRIBUTORS.rst index 68554c340..f63a7257b 100644 --- a/base_custom_info/readme/CONTRIBUTORS.rst +++ b/base_custom_info/readme/CONTRIBUTORS.rst @@ -6,3 +6,6 @@ * Jairo Llopis * Pedro M. Baeza * Alexandre Díaz +* Creu Blanca: + + * Enric Tobella diff --git a/base_custom_info/static/description/index.html b/base_custom_info/static/description/index.html index c440fbacf..fdddb875c 100644 --- a/base_custom_info/static/description/index.html +++ b/base_custom_info/static/description/index.html @@ -581,6 +581,10 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
  • Alexandre Díaz <alexandre.diaz@tecnativa.com>
  • +
  • Creu Blanca: +
  • diff --git a/base_custom_info/static/src/js/custom_info_renderer.js b/base_custom_info/static/src/js/custom_info_renderer.js new file mode 100644 index 000000000..9b706b458 --- /dev/null +++ b/base_custom_info/static/src/js/custom_info_renderer.js @@ -0,0 +1,148 @@ +odoo.define('base_custom_info.CustomInfoRenderer', function (require) { + "use strict"; + + var BasicRenderer = require('web.BasicRenderer'); + var field_registry = require('web.field_registry'); + var core = require('web.core'); + var qweb = core.qweb; + + var CustomInfoRenderer = BasicRenderer.extend({ + init: function (parent, state, params) { + params = _.defaults({}, params, { + viewType: "custom_info", + }); + this._super(parent, state, params); + if ( + parent !== undefined && + parent.mode === 'edit' && + params.mode === undefined + ) { + this.mode = 'edit'; + } + this.recordWidgets = []; + }, + _getWidgetOptions: function (data) { + var mode = this.mode; + if (data.data.readonly) { + mode = "readonly"; + } + var options = { + attrs: { + options: {}, + modifiers: {}, + }, + mode: mode, + viewType: this.viewType, + }; + if (data.data.required) { + options.attrs.modifiers.required = true; + } + if (data.data.widget === 'many2one') { + options.attrs.options.no_create_edit = true + options.attrs.options.no_open = true + } + return options; + }, + _renderView: function () { + var self = this; + var $table = $(qweb.render('base_custom_info.table')); + var $body = $table.find('tbody'); + this.$el.empty(); + this.recordWidgets = []; + $table.appendTo(this.$el); + var fieldInfo = {}; + _.each(this.state.data, function (data) { + var element = $(qweb.render( + 'base_custom_info.item', + { + widget: self, + data: data, + }, + )); + var Widget = field_registry.get(data.data.widget); + if (Widget !== undefined) { + self._renderCustomInfoWidget(Widget, element, data) + } + element.appendTo($body); + }); + return this._super(); + }, + _renderCustomInfoWidget: function(Widget, element, data) { + var options = this._getWidgetOptions(data); + var widget = new Widget( + this, "value_" + data.data.field_type, data, options); + this.recordWidgets.push(widget); + this._registerModifiers(widget, data, element, _.pick(options, 'mode')); + var node = element.find(".result_data"); + widget.appendTo(node); + }, + _onNavigationMove: function (ev) { + var currentIndex; + if (ev.data.direction === "next") { + currentIndex = this.recordWidgets.indexOf(ev.data.target || ev.target); + if ( (currentIndex + 1) >= (this.recordWidgets || []).length) { + return; + } + ev.stopPropagation(); + this._activateNextCustomInfoWidget(currentIndex); + } else if (ev.data.direction === "previous") { + currentIndex = this.recordWidgets.indexOf(ev.data.target); + if ( currentIndex <= 0) { + return; + } + ev.stopPropagation(); + this._activatePreviousCustomInfoWidget(currentIndex); + } + }, + _activateNextCustomInfoWidget: function (currentIndex) { + currentIndex = (currentIndex + 1) % (this.recordWidgets || []).length; + var activatedIndex = this._activateCustomInfoWidget(currentIndex, {inc: 1}); + if (activatedIndex === -1 ) { // no widget have been activated, we should go to the edit/save buttons + this.trigger_up('focus_control_button'); + this.lastActivatedFieldIndex = -1; + } + else { + this.lastActivatedFieldIndex = activatedIndex; + } + return this.lastActivatedFieldIndex; + }, + _activatePreviousCustomInfoWidget: function (currentIndex) { + currentIndex = currentIndex ? (currentIndex - 1) : ((this.recordWidgets || []).length - 1); + return this._activateCustomInfoWidget(currentIndex, {inc:-1}); + }, + _activateCustomInfoWidget: function (currentIndex, options) { + options = options || {}; + _.defaults(options, {inc: 1, wrap: false}); + currentIndex = Math.max(0,currentIndex); // do not allow negative currentIndex + + for (var i = 0 ; i < this.recordWidgets.length ; i++) { + var activated = this.recordWidgets[currentIndex].activate( + { + event: options.event, + noAutomaticCreate: options.noAutomaticCreate || false + }); + if (activated) { + return currentIndex; + } + + currentIndex += options.inc; + if (currentIndex >= this.recordWidgets.length) { + if (options.wrap) { + currentIndex -= this.recordWidgets.length; + } else { + return -1; + } + } else if (currentIndex < 0) { + if (options.wrap) { + currentIndex += this.recordWidgets.length; + } else { + return -1; + } + } + } + return -1; + }, + }); + + return CustomInfoRenderer; +}); diff --git a/base_custom_info/static/src/js/custom_info_view.js b/base_custom_info/static/src/js/custom_info_view.js new file mode 100644 index 000000000..731ec4f97 --- /dev/null +++ b/base_custom_info/static/src/js/custom_info_view.js @@ -0,0 +1,24 @@ +odoo.define('base_custom_info.CustomInfoView', function (require) { + "use strict"; + + var BasicView = require('web.BasicView'); + var CustomInfoRenderer = require('base_custom_info.CustomInfoRenderer'); + var view_registry = require('web.view_registry'); + var core = require('web.core'); + + var _lt = core._lt; + + var CustomInfoView = BasicView.extend({ + display_name: _lt("Custom Info"), + viewType: 'custom_info', + config: _.extend({}, BasicView.prototype.config, { + Renderer: CustomInfoRenderer, + }), + multi_record: true, + searchable: false, + }); + + view_registry.add('custom_info', CustomInfoView); + + return CustomInfoView; +}); diff --git a/base_custom_info/static/src/js/relational_fields.js b/base_custom_info/static/src/js/relational_fields.js new file mode 100644 index 000000000..fce832284 --- /dev/null +++ b/base_custom_info/static/src/js/relational_fields.js @@ -0,0 +1,58 @@ +odoo.define('base_custom_info.relational_fields', function (require) { + "use strict"; + + var CustomInfoRenderer = require('base_custom_info.CustomInfoRenderer'); + var relational_fields = require('web.relational_fields'); + var fieldUtils = require('web.field_utils'); + + relational_fields.FieldOne2Many.include({ + _getRenderer: function () { + if (this.view.arch.tag === 'custom_info') { + return CustomInfoRenderer; + } + return this._super.apply(this, arguments); + }, + _updateCustomInfoItem : function (data) { + var result = { + value_float: data.value_float, + value_str: data.value_str, + value_int: data.value_int, + value_bool: data.value_bool, + value_date: data.value_date, + }; + if (data.value_id.res_id !== undefined) + result['value_id'] = {id: data.value_id.res_id}; + return result; + }, + _saveCustomInfo: function () { + var self = this; + _.each(this.renderer.recordWidgets, function (widget) { + self._setValue({ + operation: 'UPDATE', + id: widget.dataPointID, + data: self._updateCustomInfoItem(widget.recordData), + }); + }); + }, + commitChanges: function () { + if (this.renderer && + this.renderer.viewType === "custom_info" + ) { + var self = this; + this.renderer.commitChanges().then(function () { + return self._saveCustomInfo(); + }); + } + return this._super.apply(this, arguments); + }, + activate: function (options) { + var result = this._super.apply(this, arguments); + if (result && this.renderer.viewType === 'custom_info') { + if (this.renderer.recordWidgets.length > 0) { + this.renderer.recordWidgets[0].$input.focus(); + } + } + return result; + }, + }); +}); diff --git a/base_custom_info/static/src/scss/custom_info.scss b/base_custom_info/static/src/scss/custom_info.scss new file mode 100644 index 000000000..d73875988 --- /dev/null +++ b/base_custom_info/static/src/scss/custom_info.scss @@ -0,0 +1,9 @@ +.o_form_view { + .o_field_widget, .btn { + .custom_info_value { + .o_field_widget { + margin-bottom: $o-form-spacing-unit; + } + } + } +} diff --git a/base_custom_info/static/src/xml/custom_info_item.xml b/base_custom_info/static/src/xml/custom_info_item.xml new file mode 100644 index 000000000..a50b1cc89 --- /dev/null +++ b/base_custom_info/static/src/xml/custom_info_item.xml @@ -0,0 +1,21 @@ + + diff --git a/base_custom_info/tests/__init__.py b/base_custom_info/tests/__init__.py index 18f016dd7..20d797fb3 100644 --- a/base_custom_info/tests/__init__.py +++ b/base_custom_info/tests/__init__.py @@ -1,5 +1,4 @@ -# -*- 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 +from . import test_partner, test_required, test_value_conversion diff --git a/base_custom_info/tests/test_partner.py b/base_custom_info/tests/test_partner.py index 2635ad947..9f42cbe4e 100644 --- a/base_custom_info/tests/test_partner.py +++ b/base_custom_info/tests/test_partner.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 Jairo Llopis # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). @@ -135,7 +134,8 @@ class PartnerCase(TransactionCase): 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" + val_weaknesses.value_id = self.env.ref( + "base_custom_info.opt_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()) @@ -153,8 +153,8 @@ class PartnerCase(TransactionCase): 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") + val.value_str = (u"Don Walter Antonio José de la Cruz Hëisenberg " + u"de Borbón Westley Jordy López Manuélez") def test_low_average_note(self): """Come on, you are supposed to be smart!""" @@ -162,7 +162,7 @@ class PartnerCase(TransactionCase): val = self.agrolait.get_custom_info_value( self.env.ref("base_custom_info.prop_avg_note")) with self.assertRaises(ValidationError): - val.value = "-1" + val.value_float = -1 def test_high_average_note(self): """Too smart!""" @@ -170,4 +170,4 @@ class PartnerCase(TransactionCase): val = self.agrolait.get_custom_info_value( self.env.ref("base_custom_info.prop_avg_note")) with self.assertRaises(ValidationError): - val.value = "11" + val.value_float = 11 diff --git a/base_custom_info/tests/test_required.py b/base_custom_info/tests/test_required.py new file mode 100644 index 000000000..dfc5bc48f --- /dev/null +++ b/base_custom_info/tests/test_required.py @@ -0,0 +1,48 @@ +# Copyright 2020 Creu Blanca +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo.tests.common import TransactionCase, Form +from odoo.exceptions import ValidationError + + +class PartnerCase(TransactionCase): + def setUp(self, *args, **kwargs): + super(PartnerCase, self).setUp(*args, **kwargs) + self.agrolait = self.env.ref("base.res_partner_2") + self.template = self.env['custom.info.template'].create({ + 'name': 'TEST Template', + 'model_id': self.env.ref('base.model_res_partner').id, + 'property_ids': [(0, 0, { + 'name': 'Property', + 'widget': 'char', + 'required': True, + })] + }) + + def test_required_form_failure(self): + with Form(self.agrolait) as f: + self.assertFalse(f.custom_info_template_id) + self.assertFalse(f.custom_info_ids) + f.custom_info_template_id = self.template + self.assertTrue(f.custom_info_ids) + with self.assertRaises(AssertionError): + f.save() + f.custom_info_template_id = self.env['custom.info.template'] + self.assertFalse(f.custom_info_ids) + + def test_required_failure(self): + self.assertFalse(self.agrolait.custom_info_template_id) + self.assertFalse(self.agrolait.custom_info_ids) + self.agrolait.custom_info_template_id = self.template + with self.assertRaises(ValidationError): + self.agrolait._onchange_custom_info_template_id() + + def test_required(self): + with Form(self.agrolait) as f: + self.assertFalse(f.custom_info_template_id) + self.assertFalse(f.custom_info_ids) + f.custom_info_template_id = self.template + self.assertEqual(1, len(f.custom_info_ids)) + with f.custom_info_ids.edit(0) as info: + info.value_str = 'HELLO' + self.assertTrue(self.agrolait.custom_info_ids.value) diff --git a/base_custom_info/tests/test_value_conversion.py b/base_custom_info/tests/test_value_conversion.py index 29be332df..616c705da 100644 --- a/base_custom_info/tests/test_value_conversion.py +++ b/base_custom_info/tests/test_value_conversion.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 Jairo Llopis # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). import logging @@ -26,7 +25,13 @@ class ValueConversionCase(TransactionCase): self.agrolait.custom_info_template_id = self.tpl self.agrolait._onchange_custom_info_template_id() if field == "value": - value = str(value) + field = "value_%s" % prop.field_type + if prop.field_type == "id" and isinstance(value, str): + value = self.env["custom.info.option"].search([ + ('property_ids', '=', prop.id), + ('name', '=', value) + ], limit=1).id + self.assertTrue(value) self.value = self.agrolait.get_custom_info_value(prop) self.value[field] = value @@ -35,6 +40,7 @@ class ValueConversionCase(TransactionCase): prop = self.value.property_id _logger.info( "Searching. prop: %s; value: %s", prop, value) + self.assertEqual( self.value.search([ ("property_id", "=", prop.id), @@ -89,21 +95,21 @@ class ValueConversionCase(TransactionCase): def test_to_bool_true(self): """Conversion to yes.""" - self.fill_value(self.prop_bool, "True") + 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.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.fill_value(self.prop_bool, False) self.assertEqual(self.value.with_context(lang="en_US").value, "No") self.assertIs(self.value.value_bool, False) diff --git a/base_custom_info/views/custom_info_property_view.xml b/base_custom_info/views/custom_info_property_view.xml index 8a787d6d3..f7a8bbe18 100644 --- a/base_custom_info/views/custom_info_property_view.xml +++ b/base_custom_info/views/custom_info_property_view.xml @@ -38,6 +38,7 @@ + @@ -78,6 +79,7 @@ + diff --git a/base_custom_info/views/custom_info_value_view.xml b/base_custom_info/views/custom_info_value_view.xml index 733d2572c..cc110c5a7 100644 --- a/base_custom_info/views/custom_info_value_view.xml +++ b/base_custom_info/views/custom_info_value_view.xml @@ -25,6 +25,28 @@ + + custom.info.value + + + + + + + + + + + + + + + + + + + + custom.info.value @@ -63,6 +85,9 @@ + diff --git a/base_custom_info/views/webclient_templates.xml b/base_custom_info/views/webclient_templates.xml new file mode 100644 index 000000000..af932532b --- /dev/null +++ b/base_custom_info/views/webclient_templates.xml @@ -0,0 +1,19 @@ + + + +