Browse Source

[IMP] base_custom_info: Recursive templates.

- Select an option with an additional template and get it filled on the owner.
- Drop support for onchange, too many bugs to workaround.
- Improve demo data.
- Improve tests.
- Almost cool material icon, this is an app now!
- Fun pics.
pull/492/head
Jairo Llopis 8 years ago
committed by Pedro M. Baeza
parent
commit
d0dfdc76c2
  1. 39
      base_custom_info/README.rst
  2. 3
      base_custom_info/__openerp__.py
  3. 3
      base_custom_info/demo/custom.info.category.csv
  4. 14
      base_custom_info/demo/custom.info.option.csv
  5. 14
      base_custom_info/demo/custom.info.property.csv
  6. 1
      base_custom_info/demo/custom.info.template.csv
  7. 6
      base_custom_info/demo/custom_info_property_defaults.yml
  8. 93
      base_custom_info/models/custom_info.py
  9. 6
      base_custom_info/models/custom_info_option.py
  10. 25
      base_custom_info/models/custom_info_value.py
  11. BIN
      base_custom_info/static/description/customizations-everywhere.jpg
  12. BIN
      base_custom_info/static/description/icon.png
  13. 68
      base_custom_info/static/description/icon.svg
  14. BIN
      base_custom_info/static/description/templateception.jpg
  15. 104
      base_custom_info/tests/test_partner.py
  16. 37
      base_custom_info/tests/test_value_conversion.py
  17. 2
      base_custom_info/views/custom_info_option_view.xml
  18. 6
      base_custom_info/views/custom_info_property_view.xml
  19. 16
      base_custom_info/views/res_partner_view.xml

39
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. 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 Value
----- -----
@ -196,11 +219,17 @@ Known issues / Roadmap
* Custom properties cannot be shared among templates. * Custom properties cannot be shared among templates.
* You get an error if you press *Save & New* when setting property values in * You get an error if you press *Save & New* when setting property values in
partner form. partner form.
* `There seems to be a bug in the web client that makes subviews appear empty
after an onchange
<https://github.com/OCA/server-tools/pull/492#issuecomment-237594285>`_.
This module includes a workaround for that, but the bug should be fixed and
the workaround removed.
* 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
<https://github.com/odoo/odoo/issues/2693#issuecomment-56825399>`_, 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
<https://github.com/odoo/odoo/issues/7472>`_. 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 Bug Tracker
=========== ===========

3
base_custom_info/__openerp__.py

@ -25,9 +25,11 @@
'wizard/base_config_settings_view.xml', 'wizard/base_config_settings_view.xml',
], ],
'demo': [ 'demo': [
'demo/custom.info.category.csv',
'demo/custom.info.template.csv', 'demo/custom.info.template.csv',
'demo/custom.info.property.csv', 'demo/custom.info.property.csv',
'demo/custom.info.option.csv', 'demo/custom.info.option.csv',
'demo/custom_info_property_defaults.yml',
'demo/res_groups.xml', 'demo/res_groups.xml',
], ],
"images": [ "images": [
@ -42,5 +44,6 @@
'Odoo Community Association (OCA)', 'Odoo Community Association (OCA)',
'website': 'https://www.tecnativa.com', 'website': 'https://www.tecnativa.com',
'license': 'LGPL-3', 'license': 'LGPL-3',
'application': True,
'installable': True, 'installable': True,
} }

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

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

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

14
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

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

@ -1,2 +1,3 @@
id,name,model id,name,model
tpl_smart,Smart partners,res.partner tpl_smart,Smart partners,res.partner
tpl_gamer,Gamers,res.partner

6
base_custom_info/demo/custom_info_property_defaults.yml

@ -0,0 +1,6 @@
# Copyright 2016 Jairo Llopis <jairo.llopis@tecnativa.com>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
- Setting default values after loading custom.info.option.csv
- !record {model: custom.info.property, id: prop_weaknesses}:
default_value: Huge glasses

93
base_custom_info/models/custom_info.py

@ -29,33 +29,27 @@ class CustomInfo(models.AbstractModel):
context={"embed": True}, context={"embed": True},
auto_join=True, auto_join=True,
string='Custom Properties') 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 @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 @api.multi
def unlink(self): def unlink(self):
@ -65,6 +59,16 @@ class CustomInfo(models.AbstractModel):
info_values.unlink() info_values.unlink()
return res 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.multi
@api.returns("custom.info.value") @api.returns("custom.info.value")
def get_custom_info_value(self, properties): def get_custom_info_value(self, properties):
@ -74,3 +78,44 @@ class CustomInfo(models.AbstractModel):
("res_id", "in", self.ids), ("res_id", "in", self.ids),
("property_id", "in", properties.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()

6
base_custom_info/models/custom_info_option.py

@ -22,6 +22,12 @@ class CustomInfoOption(models.Model):
string="Values", string="Values",
help="Values that have set this option.", 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 @api.multi
def check_access_rule(self, operation): def check_access_rule(self, operation):

25
base_custom_info/models/custom_info_value.py

@ -109,11 +109,26 @@ class CustomInfoValue(models.Model):
@api.model @api.model
def create(self, vals): 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 # HACK https://github.com/odoo/odoo/pull/13439
if "value" in vals: if "value" in vals:
self.env.context.skip_required = True 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 @api.model
def _selection_owner_id(self): def _selection_owner_id(self):
@ -178,7 +193,8 @@ class CustomInfoValue(models.Model):
try: try:
del self.env.context.skip_required del self.env.context.skip_required
except AttributeError: 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( raise ValidationError(
_("Property %s is required.") % _("Property %s is required.") %
self.property_id.display_name) self.property_id.display_name)
@ -188,6 +204,9 @@ class CustomInfoValue(models.Model):
"value_str", "value_int", "value_float") "value_str", "value_int", "value_float")
def _check_min_max_limits(self): def _check_min_max_limits(self):
"""Ensure value falls inside the property's stablished limits.""" """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 minimum, maximum = self.property_id.minimum, self.property_id.maximum
if minimum <= maximum: if minimum <= maximum:
value = self[self.field_name] value = self[self.field_name]

BIN
base_custom_info/static/description/customizations-everywhere.jpg

After

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

BIN
base_custom_info/static/description/icon.png

After

Width: 200  |  Height: 200  |  Size: 5.2 KiB

68
base_custom_info/static/description/icon.svg

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="200"
height="200"
viewBox="0 0 200 200"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="icon.svg"
inkscape:export-filename="icon.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#9c0c65"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="704"
id="namedview6"
showgrid="false"
inkscape:snap-page="true"
inkscape:zoom="1.2291667"
inkscape:cx="88.427025"
inkscape:cy="64.892414"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<g
id="g4169"
transform="translate(-2.3856908,-0.417955)">
<path
inkscape:connector-curvature="0"
id="path4"
d="m 63.145923,117.98015 9.436965,0 0,-9.43697 -9.436965,0 m 4.718483,61.34028 c -20.808512,0 -37.747871,-16.93935 -37.747871,-37.74786 0,-20.80852 16.939359,-37.747875 37.747871,-37.747875 20.808514,0 37.747864,16.939355 37.747864,37.747875 0,20.80851 -16.93935,37.74786 -37.747864,37.74786 m 0,-84.932702 A 47.18484,47.18484 0 0 0 20.679568,132.1356 47.18484,47.18484 0 0 0 67.864406,179.32044 47.18484,47.18484 0 0 0 115.04924,132.1356 47.18484,47.18484 0 0 0 67.864406,84.950758 m -4.718483,70.777262 9.436965,0 0,-28.31091 -9.436965,0 0,28.31091 z" />
<path
style="stroke:#9c0c65;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4-3"
d="m 161.99013,23.51547 c -2.44155,0 -4.8831,0.830128 -6.88517,2.8322 -14.0145,14.014501 -41.94585,41.945843 -41.94585,41.945843 l 7.32466,7.324652 -17.09086,17.090856 -9.766208,0 -9.766196,19.532409 9.766196,9.7662 19.532408,-9.7662 0,-9.76621 17.09086,-17.090851 7.32465,7.324652 c 0,0 27.93134,-27.931342 41.94585,-41.945843 3.02752,-4.443623 3.80881,-9.961528 0,-13.770347 L 168.8753,26.34767 c -2.00206,-2.002072 -4.44362,-2.8322 -6.88517,-2.8322 m 0,10.596331 9.7662,9.766203 -34.18171,34.181712 -9.7662,-9.766203 34.18171,-34.181712 z"
inkscape:connector-curvature="0" />
</g>
</svg>

BIN
base_custom_info/static/description/templateception.jpg

After

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

104
base_custom_info/tests/test_partner.py

@ -16,25 +16,21 @@ class PartnerCase(TransactionCase):
def set_custom_info_for_agrolait(self): def set_custom_info_for_agrolait(self):
"""Used when you need to use some created custom info.""" """Used when you need to use some created custom info."""
self.agrolait.custom_info_template_id = self.tpl 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): def test_access_granted(self):
"""Access to the model implies access to custom info.""" """Access to the model implies access to custom info."""
# Demo user has contact creation permissions by default # Demo user has contact creation permissions by default
agrolait = self.agrolait.sudo(self.demouser) agrolait = self.agrolait.sudo(self.demouser)
agrolait.custom_info_template_id = self.tpl 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): def test_access_denied(self):
"""Forbidden access to the model forbids it to custom info.""" """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.""" """(Un)apply a template to a owner and it gets filled."""
# Applying a template autofills the values # Applying a template autofills the values
self.agrolait.custom_info_template_id = self.tpl self.agrolait.custom_info_template_id = self.tpl
with self.env.do_in_onchange():
self.agrolait._onchange_custom_info_template_id()
self.assertEqual( self.assertEqual(
len(self.agrolait.custom_info_ids), len(self.agrolait.custom_info_ids),
len(self.tpl.property_ids)) len(self.tpl.property_ids))
@ -74,8 +68,8 @@ class PartnerCase(TransactionCase):
# Unapplying a template empties the values # Unapplying a template empties the values
self.agrolait.custom_info_template_id = False 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_template_id)
self.assertFalse(self.agrolait.custom_info_ids)
def test_template_model_and_model_id_match(self): def test_template_model_and_model_id_match(self):
"""Template's model and model_id fields match.""" """Template's model and model_id fields match."""
@ -105,7 +99,8 @@ class PartnerCase(TransactionCase):
def test_owner_id(self): def test_owner_id(self):
"""Check the computed owner id for a value.""" """Check the computed owner id for a value."""
self.set_custom_info_for_agrolait() 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): def test_get_custom_info_value(self):
"""Check the custom info getter helper works fine.""" """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[result.field_name], 5)
self.assertEqual(result.value_int, 5) self.assertEqual(result.value_int, 5)
self.assertEqual(result.value, "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)

37
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_bool = self.env.ref("base_custom_info.prop_smartypants")
self.prop_id = self.env.ref("base_custom_info.prop_weaknesses") 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.""" """Create a custom info value."""
_logger.info( _logger.info(
"Creating. prop: %s; value: %s; field: %s", prop, value, field) "Creating. prop: %s; value: %s; field: %s", prop, value, field)
self.agrolait.custom_info_template_id = self.tpl self.agrolait.custom_info_template_id = self.tpl
if field == "value": if field == "value":
value = str(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): def creation_found(self, value):
"""Ensure you can search what you just created.""" """Ensure you can search what you just created."""
@ -55,75 +52,75 @@ class ValueConversionCase(TransactionCase):
def test_to_str(self): def test_to_str(self):
"""Conversion to text.""" """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.creation_found("Mr. Einstein")
self.assertEqual(self.value.value, self.value.value_str) self.assertEqual(self.value.value, self.value.value_str)
def test_from_str(self): def test_from_str(self):
"""Conversion from text.""" """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.creation_found("Mr. Einstein")
self.assertEqual(self.value.value, self.value.value_str) self.assertEqual(self.value.value, self.value.value_str)
def test_to_int(self): def test_to_int(self):
"""Conversion to whole number.""" """Conversion to whole number."""
self.create_value(self.prop_int, 5)
self.fill_value(self.prop_int, 5)
self.creation_found("5") self.creation_found("5")
self.assertEqual(int(self.value.value), self.value.value_int) self.assertEqual(int(self.value.value), self.value.value_int)
def test_from_int(self): def test_from_int(self):
"""Conversion from whole number.""" """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.creation_found("5")
self.assertEqual(int(self.value.value), self.value.value_int) self.assertEqual(int(self.value.value), self.value.value_int)
def test_to_float(self): def test_to_float(self):
"""Conversion to decimal number.""" """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) self.assertEqual(float(self.value.value), self.value.value_float)
def test_from_float(self): def test_from_float(self):
"""Conversion from decimal number.""" """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) self.assertEqual(float(self.value.value), self.value.value_float)
def test_to_bool_true(self): def test_to_bool_true(self):
"""Conversion to yes.""" """Conversion to yes."""
self.create_value(self.prop_bool, "True")
self.fill_value(self.prop_bool, "True")
self.creation_found("True") self.creation_found("True")
self.assertEqual(self.value.with_context(lang="en_US").value, "Yes") self.assertEqual(self.value.with_context(lang="en_US").value, "Yes")
self.assertIs(self.value.value_bool, True) self.assertIs(self.value.value_bool, True)
def test_from_bool_true(self): def test_from_bool_true(self):
"""Conversion from yes.""" """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.creation_found("True")
self.assertEqual(self.value.with_context(lang="en_US").value, "Yes") self.assertEqual(self.value.with_context(lang="en_US").value, "Yes")
self.assertIs(self.value.value_bool, True) self.assertIs(self.value.value_bool, True)
def test_to_bool_false(self): def test_to_bool_false(self):
"""Conversion to no.""" """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.assertEqual(self.value.with_context(lang="en_US").value, "No")
self.assertIs(self.value.value_bool, False) self.assertIs(self.value.value_bool, False)
def test_from_bool_false(self): def test_from_bool_false(self):
"""Conversion from no.""" """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.assertEqual(self.value.with_context(lang="en_US").value, "No")
self.assertIs(self.value.value_bool, False) self.assertIs(self.value.value_bool, False)
def test_to_id(self): def test_to_id(self):
"""Conversion to selection.""" """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.creation_found("Needs videogames")
self.assertEqual(self.value.value, self.value.value_id.name) self.assertEqual(self.value.value, self.value.value_id.name)
def test_from_id(self): def test_from_id(self):
"""Conversion from selection.""" """Conversion from selection."""
self.create_value(
self.fill_value(
self.prop_id, self.prop_id,
self.env.ref("base_custom_info.opt_videogames").id, self.env.ref("base_custom_info.opt_videogames").id,
"value_id") "value_id")

2
base_custom_info/views/custom_info_option_view.xml

@ -9,6 +9,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Custom Info Options"> <tree string="Custom Info Options">
<field name="name"/> <field name="name"/>
<field name="template_id"/>
<field name="property_ids"/> <field name="property_ids"/>
</tree> </tree>
</field> </field>
@ -22,6 +23,7 @@
<sheet> <sheet>
<group> <group>
<field name="name"/> <field name="name"/>
<field name="template_id"/>
<field name="property_ids"/> <field name="property_ids"/>
<field name="value_ids"/> <field name="value_ids"/>
</group> </group>

6
base_custom_info/views/custom_info_property_view.xml

@ -33,8 +33,10 @@
<field name="category_id"/> <field name="category_id"/>
<field name="required"/> <field name="required"/>
<field name="default_value"/> <field name="default_value"/>
<field name="minimum"/>
<field name="maximum"/>
<field name="minimum"
attrs="{'invisible': [('field_type', 'not in', ['str', 'int', 'float'])]}"/>
<field name="maximum"
attrs="{'invisible': [('field_type', 'not in', ['str', 'int', 'float'])]}"/>
</group> </group>
<group> <group>
<field name="info_value_ids" <field name="info_value_ids"

16
base_custom_info/views/res_partner_view.xml

@ -13,12 +13,22 @@
string="Custom Information" string="Custom Information"
groups="base_custom_info.group_partner"> groups="base_custom_info.group_partner">
<group> <group>
<group>
<field name="dirty_templates" invisible="True"/>
<field <field
name="custom_info_template_id" name="custom_info_template_id"
options='{"no_quick_create": True}'/> options='{"no_quick_create": True}'/>
<field
name="custom_info_ids"
attrs="{'invisible': [('custom_info_template_id', '=', False)]}"/>
</group>
<group>
<button
string="Reload custom information templates"
type="object"
name="action_custom_info_templates_fill"
attrs="{'invisible': [('dirty_templates', '=', False)]}"/>
</group>
<group colspan="2">
<field name="custom_info_ids"/>
</group>
</group> </group>
</page> </page>
</xpath> </xpath>

Loading…
Cancel
Save