Merge lp:~openerp-dev/openobject-server/trunk-replace-inherit_option_id-xmo into lp:openobject-server

Proposed by Xavier (Open ERP)
Status: Needs review
Proposed branch: lp:~openerp-dev/openobject-server/trunk-replace-inherit_option_id-xmo
Merge into: lp:openobject-server
Diff against target: 753 lines (+491/-42)
5 files modified
openerp/addons/base/ir/ir_ui_view.py (+95/-13)
openerp/addons/base/tests/test_views.py (+370/-10)
openerp/import_xml.rng (+16/-2)
openerp/osv/orm.py (+3/-13)
openerp/tools/convert.py (+7/-4)
To merge this branch: bzr merge lp:~openerp-dev/openobject-server/trunk-replace-inherit_option_id-xmo
Reviewer Review Type Date Requested Status
Olivier Dony (Odoo) Pending
Review via email: mp+218404@code.launchpad.net

Description of the change

Builds on https://code.launchpad.net/~openerp-dev/openobject-server/trunk-extended-view-inheritance-xmo/+merge/218256

Replaces inherit_option_id by a 3-state selection:
* always applied
* optional, currently enabled
* optional, currently disabled

Addons part (most of the work) at https://code.launchpad.net/~openerp-dev/openobject-addons/trunk-replace-inherit_option_id-xmo/+merge/218403

To post a comment you must log in.
5209. By Xavier (Open ERP)

[MERGE] from extended view inheritance, add help for application field

Unmerged revisions

5209. By Xavier (Open ERP)

[MERGE] from extended view inheritance, add help for application field

5208. By Xavier (Open ERP)

[IMP] inherit_option_id -> application

5207. By Xavier (Open ERP)

[IMP] prevent changing a view from application: always to application: disabled

not sure that's actually useful, and can still go always -> enabled -> disabled...

5206. By Xavier (Open ERP)

[ADD] application field & check during inheriting views read

5205. By Xavier (Open ERP)

[MERGE] from trunk

5204. By Xavier (Open ERP)

[ADD] support for primary mode in <template>

should probably validate that there's an inherit_id (?)

5203. By Xavier (Open ERP)

[ADD] use of explicit primary mode in read_combined

5202. By Xavier (Open ERP)

[FIX] default_view should be based on mode=primary, not on inherit_id=False

5201. By Xavier (Open ERP)

[ADD] mode attribute to views

Not used yet, only defined its relation to inherit_id:

not inherit_id + primary -> ok
not inherit_id + extension -> error
inherit_id + primary -> ok
inherit_id + extension -> ok

5200. By Xavier (Open ERP)

[IMP] simplify handling of callable _constraint message

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openerp/addons/base/ir/ir_ui_view.py'
2--- openerp/addons/base/ir/ir_ui_view.py 2014-05-01 15:26:04 +0000
3+++ openerp/addons/base/ir/ir_ui_view.py 2014-05-06 14:57:36 +0000
4@@ -117,8 +117,36 @@
5 'groups_id': fields.many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id',
6 string='Groups', help="If this field is empty, the view applies to all users. Otherwise, the view applies to the users of those groups only."),
7 'model_ids': fields.one2many('ir.model.data', 'res_id', domain=[('model','=','ir.ui.view')], auto_join=True),
8+
9+ 'mode': fields.selection(
10+ [('primary', "Base view"), ('extension', "Extension View")],
11+ string="View inheritance mode", required=True,
12+ help="""Only applies if this view inherits from an other one (inherit_id is not False/Null).
13+
14+* if extension (default), if this view is requested the closest primary view
15+ is looked up (via inherit_id), then all views inheriting from it with this
16+ view's model are applied
17+* if primary, the closest primary view is fully resolved (even if it uses a
18+ different model than this one), then this view's inheritance specs
19+ (<xpath/>) are applied, and the result is used as if it were this view's
20+ actual arch.
21+"""),
22+
23+ 'application': fields.selection([
24+ ('always', "Always applied"),
25+ ('enabled', "Optional, enabled"),
26+ ('disabled', "Optional, disabled"),
27+ ],
28+ required=True, string="Application status",
29+ help="""If this view is inherited,
30+* if always, the view always extends its parent
31+* if enabled, the view currently extends its parent but can be disabled
32+* if disabled, the view currently does not extend its parent but can be enabled
33+ """),
34 }
35 _defaults = {
36+ 'mode': 'primary',
37+ 'application': 'always',
38 'priority': 16,
39 }
40 _order = "priority,name"
41@@ -167,8 +195,14 @@
42 return False
43 return True
44
45+ _sql_constraints = [
46+ ('inheritance_mode',
47+ "CHECK (mode != 'extension' OR inherit_id IS NOT NULL)",
48+ "Invalid inheritance mode: if the mode is 'extension', the view must"
49+ " extend an other view"),
50+ ]
51 _constraints = [
52- (_check_xml, 'Invalid view definition', ['arch'])
53+ (_check_xml, 'Invalid view definition', ['arch']),
54 ]
55
56 def _auto_init(self, cr, context=None):
57@@ -177,6 +211,12 @@
58 if not cr.fetchone():
59 cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
60
61+ def _compute_defaults(self, cr, uid, values, context=None):
62+ if 'inherit_id' in values:
63+ values.setdefault(
64+ 'mode', 'extension' if values['inherit_id'] else 'primary')
65+ return values
66+
67 def create(self, cr, uid, values, context=None):
68 if 'type' not in values:
69 if values.get('inherit_id'):
70@@ -185,10 +225,13 @@
71 values['type'] = etree.fromstring(values['arch']).tag
72
73 if not values.get('name'):
74- values['name'] = "%s %s" % (values['model'], values['type'])
75+ values['name'] = "%s %s" % (values.get('model'), values['type'])
76
77 self.read_template.clear_cache(self)
78- return super(view, self).create(cr, uid, values, context)
79+ return super(view, self).create(
80+ cr, uid,
81+ self._compute_defaults(cr, uid, values, context=context),
82+ context=context)
83
84 def write(self, cr, uid, ids, vals, context=None):
85 if not isinstance(ids, (list, tuple)):
86@@ -202,17 +245,43 @@
87 if custom_view_ids:
88 self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
89
90+ if vals.get('application') == 'disabled':
91+ from_always = self.search(
92+ cr, uid, [('id', 'in', ids), ('application', '=', 'always')], context=context)
93+ if from_always:
94+ raise ValueError(
95+ "Can't disable views %s marked as always applied" % (
96+ ', '.join(map(str, from_always))))
97+
98 self.read_template.clear_cache(self)
99- ret = super(view, self).write(cr, uid, ids, vals, context)
100+ ret = super(view, self).write(
101+ cr, uid, ids,
102+ self._compute_defaults(cr, uid, vals, context=context),
103+ context)
104
105 # if arch is modified views become noupdatable
106- if 'arch' in vals and not context.get('install_mode', False):
107+ if 'arch' in vals and not context.get('install_mode'):
108 # TODO: should be doable in a read and a write
109 for view_ in self.browse(cr, uid, ids, context=context):
110 if view_.model_data_id:
111 self.pool.get('ir.model.data').write(cr, openerp.SUPERUSER_ID, view_.model_data_id.id, {'noupdate': True})
112 return ret
113
114+ def toggle(self, cr, uid, ids, context=None):
115+ """ Switches between enabled and disabled application statuses
116+ """
117+ for view in self.browse(cr, uid, ids, context=context):
118+ if view.application == 'enabled':
119+ view.write({'application': 'disabled'})
120+ elif view.application == 'disabled':
121+ view.write({'application': 'enabled'})
122+ else:
123+ raise ValueError(_("Can't toggle view %d with application %r") % (
124+ view.id,
125+ view.application,
126+ ))
127+
128+
129 def copy(self, cr, uid, id, default=None, context=None):
130 if not default:
131 default = {}
132@@ -224,7 +293,7 @@
133 # default view selection
134 def default_view(self, cr, uid, model, view_type, context=None):
135 """ Fetches the default view for the provided (model, view_type) pair:
136- view with no parent (inherit_id=Fase) with the lowest priority.
137+ primary view with the lowest priority.
138
139 :param str model:
140 :param int view_type:
141@@ -234,7 +303,7 @@
142 domain = [
143 ['model', '=', model],
144 ['type', '=', view_type],
145- ['inherit_id', '=', False],
146+ ['mode', '=', 'primary'],
147 ]
148 ids = self.search(cr, uid, domain, limit=1, order='priority', context=context)
149 if not ids:
150@@ -260,15 +329,19 @@
151
152 user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id)
153
154- check_view_ids = context and context.get('check_view_ids') or (0,)
155- conditions = [['inherit_id', '=', view_id], ['model', '=', model]]
156+ conditions = [
157+ ['inherit_id', '=', view_id],
158+ ['model', '=', model],
159+ ['mode', '=', 'extension'],
160+ ['application', 'in', ['always', 'enabled']],
161+ ]
162 if self.pool._init:
163 # Module init currently in progress, only consider views from
164 # modules whose code is already loaded
165 conditions.extend([
166 '|',
167 ['model_ids.module', 'in', tuple(self.pool._init_modules)],
168- ['id', 'in', check_view_ids],
169+ ['id', 'in', context and context.get('check_view_ids') or (0,)],
170 ])
171 view_ids = self.search(cr, uid, conditions, context=context)
172
173@@ -424,7 +497,7 @@
174 if context is None: context = {}
175 if root_id is None:
176 root_id = source_id
177- sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model, context=context)
178+ sql_inherit = self.pool['ir.ui.view'].get_inheriting_views_arch(cr, uid, source_id, model, context=context)
179 for (specs, view_id) in sql_inherit:
180 specs_tree = etree.fromstring(specs.encode('utf-8'))
181 if context.get('inherit_branding'):
182@@ -447,7 +520,7 @@
183
184 # if view_id is not a root view, climb back to the top.
185 base = v = self.browse(cr, uid, view_id, context=context)
186- while v.inherit_id:
187+ while v.mode != 'primary':
188 v = v.inherit_id
189 root_id = v.id
190
191@@ -457,7 +530,16 @@
192
193 # read the view arch
194 [view] = self.read(cr, uid, [root_id], fields=fields, context=context)
195- arch_tree = etree.fromstring(view['arch'].encode('utf-8'))
196+ view_arch = etree.fromstring(view['arch'].encode('utf-8'))
197+ if not v.inherit_id:
198+ arch_tree = view_arch
199+ else:
200+ parent_view = self.read_combined(
201+ cr, uid, v.inherit_id.id, fields=fields, context=context)
202+ arch_tree = etree.fromstring(parent_view['arch'])
203+ self.apply_inheritance_specs(
204+ cr, uid, arch_tree, view_arch, parent_view['id'], context=context)
205+
206
207 if context.get('inherit_branding'):
208 arch_tree.attrib.update({
209
210=== modified file 'openerp/addons/base/tests/test_views.py'
211--- openerp/addons/base/tests/test_views.py 2014-04-08 11:49:36 +0000
212+++ openerp/addons/base/tests/test_views.py 2014-05-06 14:57:36 +0000
213@@ -1,12 +1,16 @@
214 # -*- encoding: utf-8 -*-
215 from functools import partial
216+import itertools
217
218 import unittest2
219
220 from lxml import etree as ET
221 from lxml.builder import E
222
223+from psycopg2 import IntegrityError
224+
225 from openerp.tests import common
226+import openerp.tools
227
228 Field = E.field
229
230@@ -14,9 +18,22 @@
231 def setUp(self):
232 super(ViewCase, self).setUp()
233 self.addTypeEqualityFunc(ET._Element, self.assertTreesEqual)
234+ self.Views = self.registry('ir.ui.view')
235+
236+ def browse(self, id, context=None):
237+ return self.Views.browse(self.cr, self.uid, id, context=context)
238+ def create(self, value, context=None):
239+ return self.Views.create(self.cr, self.uid, value, context=context)
240+
241+ def read_combined(self, id):
242+ return self.Views.read_combined(
243+ self.cr, self.uid,
244+ id, ['arch'],
245+ context={'check_view_ids': self.Views.search(self.cr, self.uid, [])}
246+ )
247
248 def assertTreesEqual(self, n1, n2, msg=None):
249- self.assertEqual(n1.tag, n2.tag)
250+ self.assertEqual(n1.tag, n2.tag, msg)
251 self.assertEqual((n1.text or '').strip(), (n2.text or '').strip(), msg)
252 self.assertEqual((n1.tail or '').strip(), (n2.tail or '').strip(), msg)
253
254@@ -24,8 +41,8 @@
255 # equality (!?!?!?!)
256 self.assertEqual(dict(n1.attrib), dict(n2.attrib), msg)
257
258- for c1, c2 in zip(n1, n2):
259- self.assertTreesEqual(c1, c2, msg)
260+ for c1, c2 in itertools.izip_longest(n1, n2):
261+ self.assertEqual(c1, c2, msg)
262
263
264 class TestNodeLocator(common.TransactionCase):
265@@ -374,13 +391,6 @@
266 """ Applies a sequence of modificator archs to a base view
267 """
268
269-class TestViewCombined(ViewCase):
270- """
271- Test fallback operations of View.read_combined:
272- * defaults mapping
273- * ?
274- """
275-
276 class TestNoModel(ViewCase):
277 def test_create_view_nomodel(self):
278 View = self.registry('ir.ui.view')
279@@ -628,6 +638,8 @@
280 def _insert_view(self, **kw):
281 """Insert view into database via a query to passtrough validation"""
282 kw.pop('id', None)
283+ kw.setdefault('mode', 'extension' if kw.get('inherit_id') else 'primary')
284+ kw.setdefault('application', 'always')
285
286 keys = sorted(kw.keys())
287 fields = ','.join('"%s"' % (k.replace('"', r'\"'),) for k in keys)
288@@ -804,3 +816,351 @@
289 E.button(name="action_next", type="object", string="New button")),
290 string="Replacement title", version="7.0"
291 ))
292+
293+class ViewModeField(ViewCase):
294+ """
295+ This should probably, eventually, be folded back into other test case
296+ classes, integrating the test (or not) of the mode field to regular cases
297+ """
298+
299+ def testModeImplicitValue(self):
300+ """ mode is auto-generated from inherit_id:
301+ * inherit_id -> mode=extension
302+ * not inherit_id -> mode=primary
303+ """
304+ view = self.browse(self.create({
305+ 'inherit_id': None,
306+ 'arch': '<qweb/>'
307+ }))
308+ self.assertEqual(view.mode, 'primary')
309+
310+ view2 = self.browse(self.create({
311+ 'inherit_id': view.id,
312+ 'arch': '<qweb/>'
313+ }))
314+ self.assertEqual(view2.mode, 'extension')
315+
316+ @openerp.tools.mute_logger('openerp.sql_db')
317+ def testModeExplicit(self):
318+ view = self.browse(self.create({
319+ 'inherit_id': None,
320+ 'arch': '<qweb/>'
321+ }))
322+ view2 = self.browse(self.create({
323+ 'inherit_id': view.id,
324+ 'mode': 'primary',
325+ 'arch': '<qweb/>'
326+ }))
327+ self.assertEqual(view.mode, 'primary')
328+
329+ with self.assertRaises(IntegrityError):
330+ self.create({
331+ 'inherit_id': None,
332+ 'mode': 'extension',
333+ 'arch': '<qweb/>'
334+ })
335+
336+ @openerp.tools.mute_logger('openerp.sql_db')
337+ def testPurePrimaryToExtension(self):
338+ """
339+ A primary view with inherit_id=None can't be converted to extension
340+ """
341+ view_pure_primary = self.browse(self.create({
342+ 'inherit_id': None,
343+ 'arch': '<qweb/>'
344+ }))
345+ with self.assertRaises(IntegrityError):
346+ view_pure_primary.write({'mode': 'extension'})
347+
348+ def testInheritPrimaryToExtension(self):
349+ """
350+ A primary view with an inherit_id can be converted to extension
351+ """
352+ base = self.create({'inherit_id': None, 'arch': '<qweb/>'})
353+ view = self.browse(self.create({
354+ 'inherit_id': base,
355+ 'mode': 'primary',
356+ 'arch': '<qweb/>'
357+ }))
358+
359+ view.write({'mode': 'extension'})
360+
361+ def testDefaultExtensionToPrimary(self):
362+ """
363+ An extension view can be converted to primary
364+ """
365+ base = self.create({'inherit_id': None, 'arch': '<qweb/>'})
366+ view = self.browse(self.create({
367+ 'inherit_id': base,
368+ 'arch': '<qweb/>'
369+ }))
370+
371+ view.write({'mode': 'primary'})
372+
373+class TestDefaultView(ViewCase):
374+ def testDefaultViewBase(self):
375+ self.create({
376+ 'inherit_id': False,
377+ 'priority': 10,
378+ 'mode': 'primary',
379+ 'arch': '<qweb/>',
380+ })
381+ v2 = self.create({
382+ 'inherit_id': False,
383+ 'priority': 1,
384+ 'mode': 'primary',
385+ 'arch': '<qweb/>',
386+ })
387+
388+ default = self.Views.default_view(self.cr, self.uid, False, 'qweb')
389+ self.assertEqual(
390+ default, v2,
391+ "default_view should get the view with the lowest priority for "
392+ "a (model, view_type) pair"
393+ )
394+
395+ def testDefaultViewPrimary(self):
396+ v1 = self.create({
397+ 'inherit_id': False,
398+ 'priority': 10,
399+ 'mode': 'primary',
400+ 'arch': '<qweb/>',
401+ })
402+ self.create({
403+ 'inherit_id': False,
404+ 'priority': 5,
405+ 'mode': 'primary',
406+ 'arch': '<qweb/>',
407+ })
408+ v3 = self.create({
409+ 'inherit_id': v1,
410+ 'priority': 1,
411+ 'mode': 'primary',
412+ 'arch': '<qweb/>',
413+ })
414+
415+ default = self.Views.default_view(self.cr, self.uid, False, 'qweb')
416+ self.assertEqual(
417+ default, v3,
418+ "default_view should get the view with the lowest priority for "
419+ "a (model, view_type) pair in all the primary tables"
420+ )
421+
422+class TestViewCombined(ViewCase):
423+ """
424+ * When asked for a view, instead of looking for the closest parent with
425+ inherit_id=False look for mode=primary
426+ * If root.inherit_id, resolve the arch for root.inherit_id (?using which
427+ model?), then apply root's inheritance specs to it
428+ * Apply inheriting views on top
429+ """
430+
431+ def setUp(self):
432+ super(TestViewCombined, self).setUp()
433+
434+ self.a1 = self.create({
435+ 'model': 'a',
436+ 'arch': '<qweb><a1/></qweb>'
437+ })
438+ self.a2 = self.create({
439+ 'model': 'a',
440+ 'inherit_id': self.a1,
441+ 'priority': 5,
442+ 'arch': '<xpath expr="//a1" position="after"><a2/></xpath>'
443+ })
444+ self.a3 = self.create({
445+ 'model': 'a',
446+ 'inherit_id': self.a1,
447+ 'arch': '<xpath expr="//a1" position="after"><a3/></xpath>'
448+ })
449+ # mode=primary should be an inheritance boundary in both direction,
450+ # even within a model it should not extend the parent
451+ self.a4 = self.create({
452+ 'model': 'a',
453+ 'inherit_id': self.a1,
454+ 'mode': 'primary',
455+ 'arch': '<xpath expr="//a1" position="after"><a4/></xpath>',
456+ })
457+
458+ self.b1 = self.create({
459+ 'model': 'b',
460+ 'inherit_id': self.a3,
461+ 'mode': 'primary',
462+ 'arch': '<xpath expr="//a1" position="after"><b1/></xpath>'
463+ })
464+ self.b2 = self.create({
465+ 'model': 'b',
466+ 'inherit_id': self.b1,
467+ 'arch': '<xpath expr="//a1" position="after"><b2/></xpath>'
468+ })
469+
470+ self.c1 = self.create({
471+ 'model': 'c',
472+ 'inherit_id': self.a1,
473+ 'mode': 'primary',
474+ 'arch': '<xpath expr="//a1" position="after"><c1/></xpath>'
475+ })
476+ self.c2 = self.create({
477+ 'model': 'c',
478+ 'inherit_id': self.c1,
479+ 'priority': 5,
480+ 'arch': '<xpath expr="//a1" position="after"><c2/></xpath>'
481+ })
482+ self.c3 = self.create({
483+ 'model': 'c',
484+ 'inherit_id': self.c2,
485+ 'priority': 10,
486+ 'arch': '<xpath expr="//a1" position="after"><c3/></xpath>'
487+ })
488+
489+ self.d1 = self.create({
490+ 'model': 'd',
491+ 'inherit_id': self.b1,
492+ 'mode': 'primary',
493+ 'arch': '<xpath expr="//a1" position="after"><d1/></xpath>'
494+ })
495+
496+ def test_basic_read(self):
497+ arch = self.read_combined(self.a1)['arch']
498+ self.assertEqual(
499+ ET.fromstring(arch),
500+ E.qweb(
501+ E.a1(),
502+ E.a3(),
503+ E.a2(),
504+ ), arch)
505+
506+ def test_read_from_child(self):
507+ arch = self.read_combined(self.a3)['arch']
508+ self.assertEqual(
509+ ET.fromstring(arch),
510+ E.qweb(
511+ E.a1(),
512+ E.a3(),
513+ E.a2(),
514+ ), arch)
515+
516+ def test_read_from_child_primary(self):
517+ arch = self.read_combined(self.a4)['arch']
518+ self.assertEqual(
519+ ET.fromstring(arch),
520+ E.qweb(
521+ E.a1(),
522+ E.a4(),
523+ E.a3(),
524+ E.a2(),
525+ ), arch)
526+
527+ def test_cross_model_simple(self):
528+ arch = self.read_combined(self.c2)['arch']
529+ self.assertEqual(
530+ ET.fromstring(arch),
531+ E.qweb(
532+ E.a1(),
533+ E.c3(),
534+ E.c2(),
535+ E.c1(),
536+ E.a3(),
537+ E.a2(),
538+ ), arch)
539+
540+ def test_cross_model_double(self):
541+ arch = self.read_combined(self.d1)['arch']
542+ self.assertEqual(
543+ ET.fromstring(arch),
544+ E.qweb(
545+ E.a1(),
546+ E.d1(),
547+ E.b2(),
548+ E.b1(),
549+ E.a3(),
550+ E.a2(),
551+ ), arch)
552+
553+class TestOptionalViews(ViewCase):
554+ """
555+ Tests ability to enable/disable inherited views, formerly known as
556+ inherit_option_id
557+ """
558+
559+ def setUp(self):
560+ super(TestOptionalViews, self).setUp()
561+ self.v0 = self.create({
562+ 'model': 'a',
563+ 'arch': '<qweb><base/></qweb>',
564+ })
565+ self.v1 = self.create({
566+ 'model': 'a',
567+ 'inherit_id': self.v0,
568+ 'application': 'always',
569+ 'priority': 10,
570+ 'arch': '<xpath expr="//base" position="after"><v1/></xpath>',
571+ })
572+ self.v2 = self.create({
573+ 'model': 'a',
574+ 'inherit_id': self.v0,
575+ 'application': 'enabled',
576+ 'priority': 9,
577+ 'arch': '<xpath expr="//base" position="after"><v2/></xpath>',
578+ })
579+ self.v3 = self.create({
580+ 'model': 'a',
581+ 'inherit_id': self.v0,
582+ 'application': 'disabled',
583+ 'priority': 8,
584+ 'arch': '<xpath expr="//base" position="after"><v3/></xpath>'
585+ })
586+
587+ def test_applied(self):
588+ """ mandatory and enabled views should be applied
589+ """
590+ arch = self.read_combined(self.v0)['arch']
591+ self.assertEqual(
592+ ET.fromstring(arch),
593+ E.qweb(
594+ E.base(),
595+ E.v1(),
596+ E.v2(),
597+ )
598+ )
599+
600+ def test_applied_state_toggle(self):
601+ """ Change application states of v2 and v3, check that the results
602+ are as expected
603+ """
604+ self.browse(self.v2).write({'application': 'disabled'})
605+ arch = self.read_combined(self.v0)['arch']
606+ self.assertEqual(
607+ ET.fromstring(arch),
608+ E.qweb(
609+ E.base(),
610+ E.v1(),
611+ )
612+ )
613+
614+ self.browse(self.v3).write({'application': 'enabled'})
615+ arch = self.read_combined(self.v0)['arch']
616+ self.assertEqual(
617+ ET.fromstring(arch),
618+ E.qweb(
619+ E.base(),
620+ E.v1(),
621+ E.v3(),
622+ )
623+ )
624+
625+ self.browse(self.v2).write({'application': 'enabled'})
626+ arch = self.read_combined(self.v0)['arch']
627+ self.assertEqual(
628+ ET.fromstring(arch),
629+ E.qweb(
630+ E.base(),
631+ E.v1(),
632+ E.v2(),
633+ E.v3(),
634+ )
635+ )
636+
637+ def test_mandatory_no_disabled(self):
638+ with self.assertRaises(Exception):
639+ self.browse(self.v1).write({'application': 'disabled'})
640
641=== modified file 'openerp/import_xml.rng'
642--- openerp/import_xml.rng 2014-01-16 09:17:16 +0000
643+++ openerp/import_xml.rng 2014-05-06 14:57:36 +0000
644@@ -217,9 +217,23 @@
645 <rng:optional><rng:attribute name="priority"/></rng:optional>
646 <rng:choice>
647 <rng:group>
648- <rng:optional><rng:attribute name="inherit_id"/></rng:optional>
649- <rng:optional><rng:attribute name="inherit_option_id"/></rng:optional>
650+ <rng:optional>
651+ <rng:attribute name="inherit_id"/>
652+ <rng:optional>
653+ <rng:attribute name="primary">
654+ <rng:value>True</rng:value>
655+ </rng:attribute>
656+ </rng:optional>
657+ </rng:optional>
658 <rng:optional><rng:attribute name="groups"/></rng:optional>
659+ <rng:optional>
660+ <rng:attribute name="optional">
661+ <rng:choice>
662+ <rng:value>enabled</rng:value>
663+ <rng:value>disabled</rng:value>
664+ </rng:choice>
665+ </rng:attribute>
666+ </rng:optional>
667 </rng:group>
668 <rng:optional>
669 <rng:attribute name="page"><rng:value>True</rng:value></rng:attribute>
670
671=== modified file 'openerp/osv/orm.py'
672--- openerp/osv/orm.py 2014-04-16 14:34:31 +0000
673+++ openerp/osv/orm.py 2014-05-06 14:57:36 +0000
674@@ -729,7 +729,6 @@
675 _all_columns = {}
676
677 _table = None
678- _invalids = set()
679 _log_create = False
680 _sql_constraints = []
681 _protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists']
682@@ -1543,9 +1542,6 @@
683
684 yield dbid, xid, converted, dict(extras, record=stream.index)
685
686- def get_invalid_fields(self, cr, uid):
687- return list(self._invalids)
688-
689 def _validate(self, cr, uid, ids, context=None):
690 context = context or {}
691 lng = context.get('lang')
692@@ -1566,12 +1562,9 @@
693 # Check presence of __call__ directly instead of using
694 # callable() because it will be deprecated as of Python 3.0
695 if hasattr(msg, '__call__'):
696- tmp_msg = msg(self, cr, uid, ids, context=context)
697- if isinstance(tmp_msg, tuple):
698- tmp_msg, params = tmp_msg
699- translated_msg = tmp_msg % params
700- else:
701- translated_msg = tmp_msg
702+ translated_msg = msg(self, cr, uid, ids, context=context)
703+ if isinstance(translated_msg, tuple):
704+ translated_msg = translated_msg[0] % translated_msg[1]
705 else:
706 translated_msg = trans._get_source(cr, uid, self._name, 'constraint', lng, msg)
707 if extra_error:
708@@ -1579,11 +1572,8 @@
709 error_msgs.append(
710 _("The field(s) `%s` failed against a constraint: %s") % (', '.join(fields), translated_msg)
711 )
712- self._invalids.update(fields)
713 if error_msgs:
714 raise except_orm('ValidateError', '\n'.join(error_msgs))
715- else:
716- self._invalids.clear()
717
718 def default_get(self, cr, uid, fields_list, context=None):
719 """
720
721=== modified file 'openerp/tools/convert.py'
722--- openerp/tools/convert.py 2014-04-24 13:14:05 +0000
723+++ openerp/tools/convert.py 2014-05-06 14:57:36 +0000
724@@ -861,7 +861,7 @@
725 if '.' not in full_tpl_id:
726 full_tpl_id = '%s.%s' % (self.module, tpl_id)
727 # set the full template name for qweb <module>.<id>
728- if not (el.get('inherit_id') or el.get('inherit_option_id')):
729+ if not el.get('inherit_id'):
730 el.set('t-name', full_tpl_id)
731 el.tag = 't'
732 else:
733@@ -884,15 +884,18 @@
734 record.append(Field("qweb", name='type'))
735 record.append(Field(el.get('priority', "16"), name='priority'))
736 record.append(Field(el, name="arch", type="xml"))
737- for field_name in ('inherit_id','inherit_option_id'):
738- value = el.attrib.pop(field_name, None)
739- if value: record.append(Field(name=field_name, ref=value))
740+ if 'inherit_id' in el.attrib:
741+ record.append(Field(name='inherit_id', ref=el.get('inherit_id')))
742 groups = el.attrib.pop('groups', None)
743 if groups:
744 grp_lst = map(lambda x: "ref('%s')" % x, groups.split(','))
745 record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]"))
746 if el.attrib.pop('page', None) == 'True':
747 record.append(Field(name="page", eval="True"))
748+ if el.get('primary') == 'True':
749+ record.append(Field('primary', name='mode'))
750+ if el.get('optional'):
751+ record.append(Field(el.get('optional'), name='application'))
752
753 return self._tag_record(cr, record, data_node)
754