Merge lp:~akretion-team/openerp-product-attributes/polymorphic-relations into lp:~product-core-editors/openerp-product-attributes/7.0

Proposed by Raphaël Valyi - http://www.akretion.com
Status: Merged
Approved by: Guewen Baconnier @ Camptocamp
Approved revision: 211
Merged at revision: 205
Proposed branch: lp:~akretion-team/openerp-product-attributes/polymorphic-relations
Merge into: lp:~product-core-editors/openerp-product-attributes/7.0
Diff against target: 348 lines (+153/-41)
3 files modified
product_custom_attributes/product.py (+13/-11)
product_custom_attributes/product_attribute.py (+111/-21)
product_custom_attributes/product_attribute_view.xml (+29/-9)
To merge this branch: bzr merge lp:~akretion-team/openerp-product-attributes/polymorphic-relations
Reviewer Review Type Date Requested Status
Martin Pascualon (community) Needs Fixing
Benoit Guillot - http://www.akretion.com (community) tested Approve
Guewen Baconnier @ Camptocamp code review, no test Approve
Review via email: mp+150725@code.launchpad.net

Description of the change

Hello,

This is the round 2 of my proposed generalization of product_custom_attributes for OpenERP v7 and based on my experience building 2 product configurators for OpenERP.

So it does what the commits says:

[IMP][product_custom_attributes] generalization of the m2o and m2m custom options: you can still define a selection of attribute.option (names) BUT now you can also use the relation_model_id and the 'Change Options' button to define a selection of existing references to ANY kind of OpenERP record you want! You can also simply define a domain of valid records of a given model if you prefer. This will make this module a powerful reusable component for OpenERP configurators such as a product configurator.

[IMP][product_custom_attributes] when an attribute is relational and has a specific relation_model_id defined, then it's now created with the according ttype.relation. Further, attribute edition now has its domain properly tuned if it's such a relational field. Notice that the old behavior is totally preserved.

Notice that if that is accepted, I plan a very simple round 3 that will just consist in extracting a part of that module into a lower level custom_attributes mixin module that could be injected in any OpenERP object, not just products.

I also plan a little demo to show the power of this change. I know that it's not a trivial merge. However, I insist that the former behavior is largely preserved (if not totally) while accepting such change could make this module used in many more situations by much more people. Or said differently: it's makes it a little more complex but then more people will be maintaining it and IMHO this is totally worth the few additional lines of this merge.

Be the LGTM Gods with me!

To post a comment you must log in.
208. By Akretion Bot <email address hidden>

[REF][product_custom_attributes] trivial cleanup

Revision history for this message
Raphaël Valyi - http://www.akretion.com (rvalyi) wrote :

Note: you may probably wonder why that new attribute_option_wizard wizard?

Well it's here to trick the OpenObject widget framework to make it easy to pick a few references to a custom OpenERP model (the relation_model_id of the attribute) while this model can change.

We could have left the user edit the value_ref field in the option_ids tree. But polymorphic m2o widget are a big PITA to edit in OpenERP (try to search for a specific model inside the selection to understand) and it would have been tricky to warranty the consistency: all option_ids lines should belong to the same model, the one selected in the relation_model_id field. All in all, from my experience, such a wizard is the less ugly way to offer that kind of user interface.

Hope this clarified the reason why.

209. By Raphaël Valyi - http://www.akretion.com

[FIX][product_custom_attributes] makes attribute_set something really optional in groups. That is groups of attributes can be used without sets; for instance a module can attach groups of attributes to product or partner categories and the we derivate the editable attributes from them.

210. By Raphaël Valyi - http://www.akretion.com

[REF][product_custom_attributes] now the json serialization data bag can belong to any object. This prepares the generalic mixin extraction. In a given specialization module, one could limit the available models.

Revision history for this message
Guewen Baconnier @ Camptocamp (gbaconnier-c2c) wrote :

s/osv.osv_memory/orm.TransientModel/

Same remark than the other merge proposal, you should call `setup_modifiers` when you add a node in the view.

Otherwise, it seems fine to me.

review: Needs Fixing (code review, no test)
211. By Raphaël Valyi - http://www.akretion.com

[IMP] call setup_modifiers + syntax to please C2C so they finally support the contact_id branch ;-)

Revision history for this message
Guewen Baconnier @ Camptocamp (gbaconnier-c2c) wrote :

LGTM

Thanks for the change

review: Approve (code review, no test)
212. By Benoit Guillot - http://www.akretion.com

[FIX] fix wrong imports and fix fields_view_get override

Revision history for this message
Benoit Guillot - http://www.akretion.com (benoit-guillot-z) wrote :

I have fixed some issues :

 - one introduced by the last commit wih the change of osv.osv_memory to orm.TransientModel
 - the other one to modify the view only when it's the form view

Revision history for this message
Benoit Guillot - http://www.akretion.com (benoit-guillot-z) wrote :

With this fixes, I approve the merge !

Revision history for this message
Benoit Guillot - http://www.akretion.com (benoit-guillot-z) :
review: Approve (tested)
Revision history for this message
Martin Pascualon (martinfpas) wrote :

I am new in openerp, i am use Ubuntu 12 and when i try to install i get this error

from unidecode import unidecode # Debian package python-unidecode
ImportError: No module named unidecode

Any ideas?
Thanks in advance,
Martin

review: Needs Fixing
Revision history for this message
debaetsr (rubendebaets) wrote :

Dear friend!

Here is some information that inspired me a lot, read it please, it may be helpful <http://action.getoffmylawnnow.com/e4jkqv>

Best wishes, ruben

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'product_custom_attributes/product.py'
2--- product_custom_attributes/product.py 2013-02-15 04:36:45 +0000
3+++ product_custom_attributes/product.py 2013-06-13 16:57:23 +0000
4@@ -22,16 +22,10 @@
5 from openerp.osv.orm import Model
6 from openerp.osv import fields
7 from openerp.osv.osv import except_osv
8+from openerp.osv.orm import setup_modifiers
9+from tools.translate import _
10 from lxml import etree
11-from tools.translate import _
12-
13-class product_template(Model):
14-
15- _inherit = "product.template"
16-
17- _columns = {
18- 'attribute_custom_tmpl': fields.serialized('Custom Template Attributes'),
19- }
20+
21
22 class product_product(Model):
23
24@@ -51,7 +45,6 @@
25
26 _columns = {
27 'attribute_set_id': fields.many2one('attribute.set', 'Attribute Set'),
28- 'attribute_custom_variant': fields.serialized('Custom Variant Attributes'),
29 'attribute_group_ids': fields.function(_attr_grp_ids, type='one2many',
30 relation='attribute.group', string='Groups')
31 }
32@@ -95,12 +88,21 @@
33 kwargs = {'name': "%s" % attribute.name}
34 if attribute.ttype == 'many2many':
35 parent = etree.SubElement(page, 'group', colspan="2", col="4")
36+ #FIXME the following isn't displayed in v7:
37 sep = etree.SubElement(parent, 'separator',
38 string="%s" % attribute.field_description, colspan="4")
39 kwargs['nolabel'] = "1"
40 if attribute.ttype in ['many2one', 'many2many']:
41- kwargs['domain'] = "[('attribute_id', '=', %s)]" % attribute.attribute_id.id
42+ if attribute.relation_model_id:
43+ if attribute.domain:
44+ kwargs['domain'] = attribute.domain
45+ else:
46+ ids = [op.value_ref.id for op in attribute.option_ids]
47+ kwargs['domain'] = "[('id', 'in', %s)]" % ids
48+ else:
49+ kwargs['domain'] = "[('attribute_id', '=', %s)]" % attribute.attribute_id.id
50 field = etree.SubElement(parent, 'field', **kwargs)
51+ setup_modifiers(field, self.fields_get(cr, uid, attribute.name, context))
52 return parent
53
54 def _build_attributes_notebook(self, cr, uid, attribute_group_ids, context=None):
55
56=== modified file 'product_custom_attributes/product_attribute.py'
57--- product_custom_attributes/product_attribute.py 2013-02-06 14:37:21 +0000
58+++ product_custom_attributes/product_attribute.py 2013-06-13 16:57:23 +0000
59@@ -19,28 +19,106 @@
60 # #
61 ###############################################################################
62
63-from openerp.osv.orm import Model
64+from openerp.osv import orm
65 from openerp.osv import fields
66 from openerp.osv.osv import except_osv
67+from lxml import etree
68 from openerp.tools.translate import _
69 from unidecode import unidecode # Debian package python-unidecode
70
71-class attribute_option(Model):
72+class attribute_option(orm.Model):
73 _name = "attribute.option"
74 _description = "Attribute Option"
75 _order="sequence"
76
77 _columns = {
78 'name': fields.char('Name', size=128, translate=True, required=True),
79+ 'value_ref': fields.reference('Reference', selection=[], size=128),
80 'attribute_id': fields.many2one('product.attribute', 'Product Attribute', required=True),
81 'sequence': fields.integer('Sequence'),
82 }
83
84-
85-class product_attribute(Model):
86+ def name_change(self, cr, uid, ids, name, relation_model_id, context=None):
87+ if relation_model_id:
88+ warning = {'title': _('Error!'), 'message': _("Use the 'Change Options' button instead to select appropriate model references'")}
89+ return {"value": {"name": False}, "warning": warning}
90+ else:
91+ return True
92+
93+class attribute_option_wizard(orm.TransientModel):
94+ _name = "attribute.option.wizard"
95+ _rec_name = 'attribute_id'
96+
97+ _columns = {
98+ 'attribute_id': fields.many2one('product.attribute', 'Product Attribute', required=True),
99+ }
100+
101+ _defaults = {
102+ 'attribute_id': lambda self, cr, uid, context: context.get('attribute_id', False)
103+ }
104+
105+ def validate(self, cr, uid, ids, context=None):
106+ return True
107+
108+ def create(self, cr, uid, vals, context=None):
109+ attr_obj = self.pool.get("product.attribute")
110+ attr = attr_obj.browse(cr, uid, vals['attribute_id'])
111+ op_ids = [op.id for op in attr.option_ids]
112+ opt_obj = self.pool.get("attribute.option")
113+ opt_obj.unlink(cr, uid, op_ids)
114+ for op_id in (vals.get("option_ids") and vals['option_ids'][0][2] or []):
115+ model = attr.relation_model_id.model
116+ name = self.pool.get(model).name_get(cr, uid, [op_id], context)[0][1]
117+ opt_obj.create(cr, uid, {
118+ 'attribute_id': vals['attribute_id'],
119+ 'name': name,
120+ 'value_ref': "%s,%s" % (attr.relation_model_id.model, op_id)
121+ })
122+ res = super(attribute_option_wizard, self).create(cr, uid, vals, context)
123+ return res
124+
125+ def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
126+ res = super(attribute_option_wizard, self).fields_view_get(cr, uid, view_id, view_type, context, toolbar, submenu)
127+ if view_type == 'form' and context and context.get("attribute_id"):
128+ attr_obj = self.pool.get("product.attribute")
129+ model_id = attr_obj.read(cr, uid, [context.get("attribute_id")], ['relation_model_id'])[0]['relation_model_id'][0]
130+ relation = self.pool.get("ir.model").read(cr, uid, [model_id], ["model"])[0]["model"]
131+ res['fields'].update({'option_ids': {
132+ 'domain': [],
133+ 'string': "Options",
134+ 'type': 'many2many',
135+ 'relation': relation,
136+ 'required': True,
137+ }
138+ })
139+ eview = etree.fromstring(res['arch'])
140+ options = etree.Element('field', name='option_ids', colspan='6')
141+ placeholder = eview.xpath("//separator[@string='options_placeholder']")[0]
142+ placeholder.getparent().replace(placeholder, options)
143+ res['arch'] = etree.tostring(eview, pretty_print=True)
144+ return res
145+
146+
147+class product_attribute(orm.Model):
148 _name = "product.attribute"
149 _description = "Product Attribute"
150 _inherits = {'ir.model.fields': 'field_id'}
151+
152+ def relation_model_id_change(self, cr, uid, ids, relation_model_id, option_ids, context=None):
153+ "removed selected options as they would be inconsistent"
154+ return {'value': {'option_ids': [(2, i[1]) for i in option_ids]}}
155+
156+ def button_add_options(self, cr, uid, ids, context=None):
157+ return {
158+ 'context': "{'attribute_id': %s}" % (ids[0]),
159+ 'name': _('Options Wizard'),
160+ 'view_type': 'form',
161+ 'view_mode': 'form',
162+ 'res_model': 'attribute.option.wizard',
163+ 'type': 'ir.actions.act_window',
164+ 'target': 'new',
165+ }
166+
167 _columns = {
168 'field_id': fields.many2one('ir.model.fields', 'Ir Model Fields', required=True, ondelete="cascade"),
169 'attribute_type': fields.selection([('char','Char'),
170@@ -57,28 +135,39 @@
171 'serialized': fields.boolean('Field serialized',
172 help="If serialized, the field will be stocked in the serialized field: "
173 "attribute_custom_tmpl or attribute_custom_variant depending on the field based_on"),
174- 'based_on': fields.selection([('product_template','Product Template'),
175- ('product_product','Product Variant')],
176- 'Based on', required=True),
177 'option_ids': fields.one2many('attribute.option', 'attribute_id', 'Attribute Options'),
178 'create_date': fields.datetime('Created date', readonly=True),
179+ 'relation_model_id': fields.many2one('ir.model', 'Model'),
180+ 'domain': fields.char('Domain', size=256),
181 }
182
183 def create(self, cr, uid, vals, context=None):
184- if vals.get('based_on') == 'product_template':
185- vals['model_id'] = self.pool.get('ir.model').search(cr, uid, [('model', '=', 'product.template')], context=context)[0]
186- serial_name = 'attribute_custom_tmpl'
187+ if vals.get('relation_model_id'):
188+ relation = self.pool.get('ir.model').read(cr, uid,
189+ [vals.get('relation_model_id')], ['model'])[0]['model']
190 else:
191- vals['model_id'] = self.pool.get('ir.model').search(cr, uid, [('model', '=', 'product.product')], context=context)[0]
192- serial_name = 'attribute_custom_variant'
193+ relation = 'attribute.option'
194 if vals.get('serialized'):
195- vals['serialization_field_id'] = self.pool.get('ir.model.fields').search(cr, uid, [('name', '=', serial_name)], context=context)[0]
196+ field_obj = self.pool.get('ir.model.fields')
197+ serialized_ids = field_obj.search(cr, uid,
198+ [('ttype', '=', 'serialized'), ('model_id', '=', vals['model_id']),
199+ ('name', '=', 'x_custom_json_attrs')], context=context)
200+ if serialized_ids:
201+ vals['serialization_field_id'] = serialized_ids[0]
202+ else:
203+ f_vals = {
204+ 'name': 'x_custom_json_attrs',
205+ 'field_description': 'Serialized JSON Attributes',
206+ 'ttype': 'serialized',
207+ 'model_id': vals['model_id'],
208+ }
209+ vals['serialization_field_id'] = field_obj.create(cr, uid, f_vals, {'manual': True})
210 if vals['attribute_type'] == 'select':
211 vals['ttype'] = 'many2one'
212- vals['relation'] = 'attribute.option'
213+ vals['relation'] = relation
214 elif vals['attribute_type'] == 'multiselect':
215 vals['ttype'] = 'many2many'
216- vals['relation'] = 'attribute.option'
217+ vals['relation'] = relation
218 if not vals.get('serialized'):
219 raise except_osv(_('Create Error'), _("The field serialized should be ticked for a multiselect field !"))
220 else:
221@@ -97,7 +186,8 @@
222 name = 'x_%s' % name
223 return {'value' : {'name' : unidecode(name)}}
224
225-class attribute_location(Model):
226+
227+class attribute_location(orm.Model):
228 _name = "attribute.location"
229 _description = "Attribute Location"
230 _order="sequence"
231@@ -118,26 +208,26 @@
232 }
233
234
235-
236-class attribute_group(Model):
237+class attribute_group(orm.Model):
238 _name= "attribute.group"
239 _description = "Attribute Group"
240 _order="sequence"
241
242 _columns = {
243 'name': fields.char('Name', size=128, required=True),
244- 'attribute_set_id': fields.many2one('attribute.set', 'Attribute Set', required=True),
245+ 'attribute_set_id': fields.many2one('attribute.set', 'Attribute Set'),
246 'attribute_ids': fields.one2many('attribute.location', 'attribute_group_id', 'Attributes'),
247 'sequence': fields.integer('Sequence'),
248 }
249
250 def create(self, cr, uid, vals, context=None):
251 for attribute in vals['attribute_ids']:
252- if attribute[2] and not attribute[2].get('attribute_set_id'):
253+ if vals.get('attribute_set_id') and attribute[2] and not attribute[2].get('attribute_set_id'):
254 attribute[2]['attribute_set_id'] = vals['attribute_set_id']
255 return super(attribute_group, self).create(cr, uid, vals, context)
256
257-class attribute_set(Model):
258+
259+class attribute_set(orm.Model):
260 _name = "attribute.set"
261 _description = "Attribute Set"
262 _columns = {
263
264=== modified file 'product_custom_attributes/product_attribute_view.xml'
265--- product_custom_attributes/product_attribute_view.xml 2013-02-11 22:58:10 +0000
266+++ product_custom_attributes/product_attribute_view.xml 2013-06-13 16:57:23 +0000
267@@ -83,28 +83,48 @@
268 <field name="field_description" on_change="onchange_field_description(field_description, context)"/>
269 <field name="name" attrs="{'readonly':[('create_date', '!=', False)]}" on_change="onchange_name(name, context)"/>
270 <field name="attribute_type" />
271- <field name="based_on" />
272+ <field name="model_id" />
273 <field name="serialized" />
274 <field name="size" attrs="{'invisible':[('attribute_type', '!=', 'char')]}"/>
275 <field name="translate" attrs="{'invisible':[('attribute_type', 'not in', ('char', 'text'))]}"/>
276 <newline />
277- <field name="option_ids" colspan="4" attrs="{'invisible':[('attribute_type', 'not in', ['select', 'multiselect'])]}" widget="one2many_list" nolabel="1">
278- <tree string="Attribute Options" editable="top" >
279- <field name="sequence" />
280- <field name="name" />
281+ <group colspan="4" attrs="{'invisible':[('attribute_type', 'not in', ['select', 'multiselect'])]}">
282+ <field name="relation_model_id" on_change="relation_model_id_change(relation_model_id, option_ids, context)"/>
283+ <field name="domain" attrs="{'invisible':[('relation_model_id', '=', False)]}"/>
284+ <group colspan="4" attrs="{'invisible':[('domain', '!=', False)]}">
285+ <button name="button_add_options" attrs="{'invisible':[('relation_model_id', '=', False)]}" type="object" string="Change Options"/>
286+ <field name="option_ids" colspan="4" nolabel="1">
287+ <tree string="Attribute Options" editable="top">
288+ <field name="sequence" invisible="1"/>
289+ <field name="name" on_change="name_change(name, parent.relation_model_id, context)"/>
290 </tree>
291 </field>
292+ </group>
293+ </group>
294 <field name="create_date" invisible="1"/>
295 </form>
296 </field>
297 </record>
298
299+ <record id="attribute_option_wizard_form_view" model="ir.ui.view">
300+ <field name="name">attribute.option.wizard</field>
301+ <field name="model">attribute.option.wizard</field>
302+ <field name="arch" type="xml">
303+ <form string="Options Wizard" col="6">
304+ <field name="attribute_id" invisible="1" colspan="2"/>
305+ <separator string="options_placeholder"/>
306+ <button special="cancel" string="Cancel" icon="gtk-cancel"/>
307+ <button name="validate" string="Validate" type="object" icon="gtk-convert"/>
308+ </form>
309+ </field>
310+ </record>
311+
312 <record id="attribute_option_form_view" model="ir.ui.view">
313 <field name="name">attribute.option.form</field>
314 <field name="model">attribute.option</field>
315 <field name="arch" type="xml">
316 <form string="Attribute Option" col="6">
317- <field name="name" colspan="2"/>
318+ <field name="value_ref" colspan="2"/>
319 <field name="sequence" colspan="2"/>
320 <field name="attribute_id" colspan="2"/>
321 </form>
322@@ -163,7 +183,7 @@
323 <field eval="1" name="priority"/>
324 <field name="arch" type="xml">
325 <tree string="Attribute Option">
326- <field name="name" />
327+ <field name="value_ref" />
328 </tree>
329 </field>
330 </record>
331@@ -175,7 +195,7 @@
332 <field name="arch" type="xml">
333 <tree string="Attribute Option">
334 <field name="sequence" />
335- <field name="name" />
336+ <field name="value_ref" />
337 <field name="attribute_id" />
338 </tree>
339 </field>
340@@ -229,7 +249,7 @@
341 <field name="model">attribute.option</field>
342 <field name="arch" type="xml">
343 <search string="Search Attribute Options">
344- <field name="name" />
345+ <field name="value_ref" />
346 <field name="attribute_id"/>
347 </search>
348 </field>

Subscribers

People subscribed via source and target branches