diff --git a/base_custom_info/README.rst b/base_custom_info/README.rst index 6d6c57177..8271d9f6e 100644 --- a/base_custom_info/README.rst +++ b/base_custom_info/README.rst @@ -84,6 +84,29 @@ I.e., the "What weaknesses does he/she have?" *property* has some options: The *value* will always be one of these. +Recursive templates using options +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Oh dear customization lovers! Options can be used to customize the custom +information template! + +.. figure:: /base_custom_info/static/description/customizations-everywhere.jpg + :alt: Customizations Everywhere + +If you assign an *additional template* to an option, and while using the owner +form you choose that option, you can then press *reload custom information +templates* to make the owner update itself to include all the properties in all +the involved templates. If you do not press the button, anyway the reloading +will be performed when saving the owner record. + +.. figure:: /base_custom_info/static/description/templateception.jpg + :alt: Templateception + +I.e., if you select the option "Needs videogames" for the property "What +weaknesses does he/she have?" of a smart partner and press *reload custom +information templates*, you will get 2 new properties to fill: "Favourite +videogames genre" and "Favourite videogame". + Value ----- @@ -196,11 +219,17 @@ Known issues / Roadmap * 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. +* You have to press *reload custom information templates*, when the optimal + thing would be the reloading taking place whenever needed: when you change + the template, or when you choose an option that has an additional template. + However, `currently it is impossible for a x2many field to update itself + `_, and it is + needed to skip some checks when you are saving a record after filling the + templates, which has to be done by `changing the context, something also not + possible currently at onchange time + `_. So there are some technical + limitations that do not let us reach the ideal UX for this addon. So, in + short, press the button when you see it and be happy. Bug Tracker =========== diff --git a/base_custom_info/__openerp__.py b/base_custom_info/__openerp__.py index 81bcec5f1..261fb15a6 100644 --- a/base_custom_info/__openerp__.py +++ b/base_custom_info/__openerp__.py @@ -25,9 +25,11 @@ 'wizard/base_config_settings_view.xml', ], 'demo': [ + 'demo/custom.info.category.csv', 'demo/custom.info.template.csv', 'demo/custom.info.property.csv', 'demo/custom.info.option.csv', + 'demo/custom_info_property_defaults.yml', 'demo/res_groups.xml', ], "images": [ @@ -42,5 +44,6 @@ 'Odoo Community Association (OCA)', 'website': 'https://www.tecnativa.com', 'license': 'LGPL-3', + 'application': True, 'installable': True, } diff --git a/base_custom_info/demo/custom.info.category.csv b/base_custom_info/demo/custom.info.category.csv new file mode 100644 index 000000000..35104e4ef --- /dev/null +++ b/base_custom_info/demo/custom.info.category.csv @@ -0,0 +1,3 @@ +id,name,sequence +cat_statics,Statics,50 +cat_gaming,Gaming,100 diff --git a/base_custom_info/demo/custom.info.option.csv b/base_custom_info/demo/custom.info.option.csv index aa072dc80..46f2c65bb 100644 --- a/base_custom_info/demo/custom.info.option.csv +++ b/base_custom_info/demo/custom.info.option.csv @@ -1,4 +1,10 @@ -id,name,property_ids:id -opt_food,Loves junk food,prop_weaknesses -opt_videogames,Needs videogames,prop_weaknesses -opt_glasses,Huge glasses,prop_weaknesses +id,name,property_ids:id,template_id:id +opt_food,Loves junk food,prop_weaknesses, +opt_videogames,Needs videogames,prop_weaknesses,tpl_gamer +opt_glasses,Huge glasses,prop_weaknesses, +opt_shooter,Shooter,prop_fav_genre, +opt_platforms,Platforms,prop_fav_genre, +opt_cars,Cars,prop_fav_genre, +opt_rpg,RPG,prop_fav_genre, +opt_strategy,Strategy,prop_fav_genre, +opt_graphical_adventure,Graphical adventure,prop_fav_genre, diff --git a/base_custom_info/demo/custom.info.property.csv b/base_custom_info/demo/custom.info.property.csv index 64c3a3066..724562f7a 100644 --- a/base_custom_info/demo/custom.info.property.csv +++ b/base_custom_info/demo/custom.info.property.csv @@ -1,6 +1,8 @@ -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, +id,name,template_id:id,field_type,required,minimum,maximum,category_id:id,sequence +prop_teacher,Name of his/her teacher,tpl_smart,str,,1,30,,100 +prop_haters,Amount of people that hates him/her for being so smart,tpl_smart,int,,0,99999,cat_statics,200 +prop_avg_note,Average note on all subjects,tpl_smart,float,True,0,10,cat_statics,300 +prop_smartypants,Does he/she believe he/she is the smartest person on earth?,tpl_smart,bool,,0,-1,,400 +prop_weaknesses,What weaknesses does he/she have?,tpl_smart,id,,0,-1,,500 +prop_fav_genre,Favourite videogames genre,tpl_gamer,id,,0,-1,cat_gaming,600 +prop_fav_game,Favourite videogame,tpl_gamer,str,,0,-1,cat_gaming,700 diff --git a/base_custom_info/demo/custom.info.template.csv b/base_custom_info/demo/custom.info.template.csv index 1ec9e2bfd..d6d96e0cc 100644 --- a/base_custom_info/demo/custom.info.template.csv +++ b/base_custom_info/demo/custom.info.template.csv @@ -1,2 +1,3 @@ id,name,model tpl_smart,Smart partners,res.partner +tpl_gamer,Gamers,res.partner diff --git a/base_custom_info/demo/custom_info_property_defaults.yml b/base_custom_info/demo/custom_info_property_defaults.yml new file mode 100644 index 000000000..592efa6dd --- /dev/null +++ b/base_custom_info/demo/custom_info_property_defaults.yml @@ -0,0 +1,6 @@ +# Copyright 2016 Jairo Llopis +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +- Setting default values after loading custom.info.option.csv + +- !record {model: custom.info.property, id: prop_weaknesses}: + default_value: Huge glasses diff --git a/base_custom_info/models/custom_info.py b/base_custom_info/models/custom_info.py index 7ef469e3a..45c70e963 100644 --- a/base_custom_info/models/custom_info.py +++ b/base_custom_info/models/custom_info.py @@ -29,33 +29,27 @@ class CustomInfo(models.AbstractModel): context={"embed": True}, auto_join=True, string='Custom Properties') + dirty_templates = fields.Boolean( + compute="_compute_dirty_templates", + ) - @api.multi - @api.onchange('custom_info_template_id') - def _onchange_custom_info_template_id(self): - 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/odoo/odoo/pull/11042 + @api.model + def create(self, vals): + res = super(CustomInfo, self).create(vals) + if not self.env.context.get("filling_templates"): + res.filtered( + "dirty_templates").action_custom_info_templates_fill() + return res - # HACK https://github.com/OCA/server-tools/pull/492#issuecomment-237594285 + # HACK https://github.com/odoo/odoo/pull/11042 @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) + def write(self, vals): + res = super(CustomInfo, self).write(vals) + if not self.env.context.get("filling_templates"): + self.filtered( + "dirty_templates").action_custom_info_templates_fill() + return res @api.multi def unlink(self): @@ -65,6 +59,16 @@ class CustomInfo(models.AbstractModel): info_values.unlink() return res + @api.one + @api.depends("custom_info_template_id", + "custom_info_ids.value_id.template_id") + def _compute_dirty_templates(self): + """Know if you need to reload the templates.""" + expected_properties = self.all_custom_info_templates().mapped( + "property_ids") + actual_properties = self.mapped("custom_info_ids.property_id") + self.dirty_templates = expected_properties != actual_properties + @api.multi @api.returns("custom.info.value") def get_custom_info_value(self, properties): @@ -74,3 +78,44 @@ class CustomInfo(models.AbstractModel): ("res_id", "in", self.ids), ("property_id", "in", properties.ids), ]) + + @api.multi + def all_custom_info_templates(self): + """Get all custom info templates involved in these owners.""" + return (self.mapped("custom_info_template_id") | + self.mapped("custom_info_ids.value_id.template_id")) + + @api.multi + def action_custom_info_templates_fill(self): + """Fill values with enabled custom info templates.""" + recursive_owners = self + for owner in self.with_context(filling_templates=True): + values = owner.custom_info_ids + tpls = owner.all_custom_info_templates() + props_good = tpls.mapped("property_ids") + props_enabled = owner.mapped("custom_info_ids.property_id") + to_add = props_good - props_enabled + to_rm = props_enabled - props_good + # Remove remaining properties + # HACK https://github.com/odoo/odoo/pull/13480 + values.filtered(lambda r: r.property_id in to_rm).unlink() + values = values.exists() + # Add new properties + for prop in to_add: + newvalue = values.new({ + "property_id": prop.id, + "res_id": owner.id, + }) + newvalue._onchange_property_set_default_value() + # HACK https://github.com/odoo/odoo/issues/13076 + newvalue._inverse_value() + newvalue._compute_value() + values |= newvalue + owner.custom_info_ids = values + # Default values implied new templates? Then this is recursive + if owner.all_custom_info_templates() == tpls: + recursive_owners -= owner + # Changes happened under a different environment; update own + self.invalidate_cache() + if recursive_owners: + return recursive_owners.action_custom_info_templates_fill() diff --git a/base_custom_info/models/custom_info_option.py b/base_custom_info/models/custom_info_option.py index 08d812c41..6e20d94bd 100644 --- a/base_custom_info/models/custom_info_option.py +++ b/base_custom_info/models/custom_info_option.py @@ -22,6 +22,12 @@ class CustomInfoOption(models.Model): string="Values", help="Values that have set this option.", ) + template_id = fields.Many2one( + comodel_name="custom.info.template", + string="Additional template", + help="Additional template to be applied to the owner if this option " + "is chosen.", + ) @api.multi def check_access_rule(self, operation): diff --git a/base_custom_info/models/custom_info_value.py b/base_custom_info/models/custom_info_value.py index 321b25f22..a7cd143a8 100644 --- a/base_custom_info/models/custom_info_value.py +++ b/base_custom_info/models/custom_info_value.py @@ -109,11 +109,26 @@ class CustomInfoValue(models.Model): @api.model def create(self, vals): - """Skip constrains in 1st lap.""" + """Skip constrains in 1st lap. Update owner templates.""" # HACK https://github.com/odoo/odoo/pull/13439 if "value" in vals: self.env.context.skip_required = True - return super(CustomInfoValue, self).create(vals) + result = super(CustomInfoValue, self).create(vals) + # HACK https://github.com/odoo/odoo/pull/11042 + if not self.env.context.get("filling_templates"): + result.owner_id.exists().filtered("dirty_templates") \ + .action_custom_info_templates_fill() + return result + + # HACK https://github.com/odoo/odoo/pull/11042 + @api.multi + def write(self, vals): + """Update owner templates.""" + result = super(CustomInfoValue, self).write(vals) + if not self.env.context.get("filling_templates"): + self.mapped("owner_id").exists().filtered("dirty_templates") \ + .action_custom_info_templates_fill() + return result @api.model def _selection_owner_id(self): @@ -178,7 +193,8 @@ class CustomInfoValue(models.Model): try: del self.env.context.skip_required except AttributeError: - if self.required and not self[self.field_name]: + if (not self.env.context.get("filling_templates") and + self.required and not self[self.field_name]): raise ValidationError( _("Property %s is required.") % self.property_id.display_name) @@ -188,6 +204,9 @@ class CustomInfoValue(models.Model): "value_str", "value_int", "value_float") def _check_min_max_limits(self): """Ensure value falls inside the property's stablished limits.""" + # Skip constraint while filling the partner template + if self.env.context.get("filling_templates"): + return minimum, maximum = self.property_id.minimum, self.property_id.maximum if minimum <= maximum: value = self[self.field_name] diff --git a/base_custom_info/static/description/customizations-everywhere.jpg b/base_custom_info/static/description/customizations-everywhere.jpg new file mode 100644 index 000000000..12dc88850 Binary files /dev/null and b/base_custom_info/static/description/customizations-everywhere.jpg differ diff --git a/base_custom_info/static/description/icon.png b/base_custom_info/static/description/icon.png new file mode 100644 index 000000000..896696e43 Binary files /dev/null and b/base_custom_info/static/description/icon.png differ diff --git a/base_custom_info/static/description/icon.svg b/base_custom_info/static/description/icon.svg new file mode 100644 index 000000000..54c80cb8c --- /dev/null +++ b/base_custom_info/static/description/icon.svg @@ -0,0 +1,68 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/base_custom_info/static/description/templateception.jpg b/base_custom_info/static/description/templateception.jpg new file mode 100644 index 000000000..268c29da0 Binary files /dev/null and b/base_custom_info/static/description/templateception.jpg differ diff --git a/base_custom_info/tests/test_partner.py b/base_custom_info/tests/test_partner.py index ee39778ce..9105586e7 100644 --- a/base_custom_info/tests/test_partner.py +++ b/base_custom_info/tests/test_partner.py @@ -16,25 +16,21 @@ class PartnerCase(TransactionCase): 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, - }) + self.agrolait.get_custom_info_value( + self.env.ref("base_custom_info.prop_haters")).value_int = 5 def test_access_granted(self): """Access to the model implies access to custom info.""" # Demo user has contact creation permissions by default agrolait = self.agrolait.sudo(self.demouser) agrolait.custom_info_template_id = self.tpl - agrolait.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!" + prop_weaknesses = agrolait.env.ref("base_custom_info.prop_weaknesses") + val_weaknesses = agrolait.get_custom_info_value(prop_weaknesses) + opt_food = agrolait.env.ref("base_custom_info.opt_food") + val_weaknesses.value_id = opt_food + agrolait.custom_info_template_id.name = "Changed template name" + opt_food.name = "Changed option name" + prop_weaknesses.name = "Changed property name" def test_access_denied(self): """Forbidden access to the model forbids it to custom info.""" @@ -63,8 +59,6 @@ class PartnerCase(TransactionCase): """(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)) @@ -74,8 +68,8 @@ class PartnerCase(TransactionCase): # Unapplying a template empties the values self.agrolait.custom_info_template_id = False - self.agrolait._onchange_custom_info_template_id() self.assertFalse(self.agrolait.custom_info_template_id) + self.assertFalse(self.agrolait.custom_info_ids) def test_template_model_and_model_id_match(self): """Template's model and model_id fields match.""" @@ -105,7 +99,8 @@ class PartnerCase(TransactionCase): 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) + self.assertEqual( + self.agrolait.mapped("custom_info_ids.owner_id"), self.agrolait) def test_get_custom_info_value(self): """Check the custom info getter helper works fine.""" @@ -117,3 +112,78 @@ class PartnerCase(TransactionCase): self.assertEqual(result[result.field_name], 5) self.assertEqual(result.value_int, 5) self.assertEqual(result.value, "5") + + def test_default_values(self): + """Default values get applied.""" + self.agrolait.custom_info_template_id = self.tpl + val_weaknesses = self.agrolait.get_custom_info_value( + self.env.ref("base_custom_info.prop_weaknesses")) + opt_glasses = self.env.ref("base_custom_info.opt_glasses") + self.assertEqual(val_weaknesses.value_id, opt_glasses) + self.assertEqual(val_weaknesses.value, opt_glasses.name) + + def test_recursive_templates(self): + """Recursive templates get loaded when required.""" + self.set_custom_info_for_agrolait() + prop_weaknesses = self.env.ref("base_custom_info.prop_weaknesses") + val_weaknesses = self.agrolait.get_custom_info_value(prop_weaknesses) + val_weaknesses.value = "Needs videogames" + tpl_gamer = self.env.ref("base_custom_info.tpl_gamer") + self.agrolait.invalidate_cache() + self.assertIn(tpl_gamer, self.agrolait.all_custom_info_templates()) + self.assertTrue( + tpl_gamer.property_ids < + self.agrolait.mapped("custom_info_ids.property_id")) + cat_gaming = self.env.ref("base_custom_info.cat_gaming") + self.assertIn( + cat_gaming, self.agrolait.mapped("custom_info_ids.category_id")) + + def test_long_teacher_name(self): + """Wow, your teacher cannot have such a long name!""" + self.set_custom_info_for_agrolait() + val = self.agrolait.get_custom_info_value( + self.env.ref("base_custom_info.prop_teacher")) + with self.assertRaises(ValidationError): + val.value = (u"Don Walter Antonio José de la Cruz Hëisenberg de " + u"Borbón Westley Jordy López Manuélez") + + def test_low_average_note(self): + """Come on, you are supposed to be smart!""" + self.set_custom_info_for_agrolait() + val = self.agrolait.get_custom_info_value( + self.env.ref("base_custom_info.prop_avg_note")) + with self.assertRaises(ValidationError): + val.value = "-1" + + def test_high_average_note(self): + """Too smart!""" + self.set_custom_info_for_agrolait() + val = self.agrolait.get_custom_info_value( + self.env.ref("base_custom_info.prop_avg_note")) + with self.assertRaises(ValidationError): + val.value = "11" + + def test_dirty_templates_setting_tpl(self): + """If you set a template, it gets dirty.""" + with self.env.do_in_onchange(): + self.assertFalse(self.agrolait.dirty_templates) + self.agrolait.custom_info_template_id = self.tpl + self.assertTrue(self.agrolait.dirty_templates) + + def test_dirty_templates_removing_tpl(self): + """If you remove a template, it gets dirty.""" + self.agrolait.custom_info_template_id = self.tpl + with self.env.do_in_onchange(): + self.assertFalse(self.agrolait.dirty_templates) + self.agrolait.custom_info_template_id = False + self.assertTrue(self.agrolait.dirty_templates) + + def test_dirty_templates_choosing_option(self): + """If you choose an option with an extra template, it gets dirty.""" + self.agrolait.custom_info_template_id = self.tpl + with self.env.do_in_onchange(): + self.assertFalse(self.agrolait.dirty_templates) + val = self.agrolait.get_custom_info_value( + self.env.ref("base_custom_info.prop_weaknesses")) + val.value_id = self.env.ref("base_custom_info.opt_videogames") + self.assertTrue(self.agrolait.dirty_templates) diff --git a/base_custom_info/tests/test_value_conversion.py b/base_custom_info/tests/test_value_conversion.py index 1f18c2dcf..8f3de8a86 100644 --- a/base_custom_info/tests/test_value_conversion.py +++ b/base_custom_info/tests/test_value_conversion.py @@ -19,18 +19,15 @@ class ValueConversionCase(TransactionCase): 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"): + def fill_value(self, prop, value, field="value"): """Create a custom info value.""" _logger.info( "Creating. prop: %s; value: %s; field: %s", prop, value, field) self.agrolait.custom_info_template_id = self.tpl 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, - }) + self.value = self.agrolait.get_custom_info_value(prop) + self.value[field] = value def creation_found(self, value): """Ensure you can search what you just created.""" @@ -55,75 +52,75 @@ class ValueConversionCase(TransactionCase): def test_to_str(self): """Conversion to text.""" - self.create_value(self.prop_str, "Mr. Einstein") + self.fill_value(self.prop_str, "Mr. Einstein") self.creation_found("Mr. Einstein") self.assertEqual(self.value.value, self.value.value_str) def test_from_str(self): """Conversion from text.""" - self.create_value(self.prop_str, "Mr. Einstein", "value_str") + self.fill_value(self.prop_str, "Mr. Einstein", "value_str") self.creation_found("Mr. Einstein") self.assertEqual(self.value.value, self.value.value_str) def test_to_int(self): """Conversion to whole number.""" - self.create_value(self.prop_int, 5) + self.fill_value(self.prop_int, 5) self.creation_found("5") self.assertEqual(int(self.value.value), self.value.value_int) def test_from_int(self): """Conversion from whole number.""" - self.create_value(self.prop_int, 5, "value_int") + self.fill_value(self.prop_int, 5, "value_int") self.creation_found("5") self.assertEqual(int(self.value.value), self.value.value_int) def test_to_float(self): """Conversion to decimal number.""" - self.create_value(self.prop_float, 10.5) - self.creation_found("10.5") + self.fill_value(self.prop_float, 9.5) + self.creation_found("9.5") self.assertEqual(float(self.value.value), self.value.value_float) def test_from_float(self): """Conversion from decimal number.""" - self.create_value(self.prop_float, 10.5, "value_float") - self.creation_found("10.5") + self.fill_value(self.prop_float, 9.5, "value_float") + self.creation_found("9.5") self.assertEqual(float(self.value.value), self.value.value_float) def test_to_bool_true(self): """Conversion to yes.""" - self.create_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.create_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.create_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) def test_from_bool_false(self): """Conversion from no.""" - self.create_value(self.prop_bool, False, "value_bool") + self.fill_value(self.prop_bool, False, "value_bool") self.assertEqual(self.value.with_context(lang="en_US").value, "No") self.assertIs(self.value.value_bool, False) def test_to_id(self): """Conversion to selection.""" - self.create_value(self.prop_id, "Needs videogames") + self.fill_value(self.prop_id, "Needs videogames") self.creation_found("Needs videogames") self.assertEqual(self.value.value, self.value.value_id.name) def test_from_id(self): """Conversion from selection.""" - self.create_value( + self.fill_value( self.prop_id, self.env.ref("base_custom_info.opt_videogames").id, "value_id") diff --git a/base_custom_info/views/custom_info_option_view.xml b/base_custom_info/views/custom_info_option_view.xml index dfad88e3e..3ac65804c 100644 --- a/base_custom_info/views/custom_info_option_view.xml +++ b/base_custom_info/views/custom_info_option_view.xml @@ -9,6 +9,7 @@ + @@ -22,6 +23,7 @@ + diff --git a/base_custom_info/views/custom_info_property_view.xml b/base_custom_info/views/custom_info_property_view.xml index 21d447ec6..71a14f6a4 100644 --- a/base_custom_info/views/custom_info_property_view.xml +++ b/base_custom_info/views/custom_info_property_view.xml @@ -33,8 +33,10 @@ - - + + - - + + + + + +