[ADD] base: server-side formview
Useful to better test business flows & replace yaml test files: instantiating records & calling onchanges by hand is very error-prone (it's easy to miss onchanges, or to manually set fields which can't be set through the view we're interested in, ...) odoo.tests.common.Form implements the basic *creation* flow (fields_get, default_get, onchange) up to saving the record, including proper handling of the required and readonly modifiers & their domains. * edition of existing records, including filtering out unmodified fields when saving * possibly buttons * ensuring the API is convenient
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': "Test testing utilities",
|
||||
'category': "Tests",
|
||||
'description': """Non-trivial testing utilities can require models & all
|
||||
|
||||
This here module is useful to validate that they're doing what they're
|
||||
supposed to do
|
||||
""",
|
||||
'depends': ['base'],
|
||||
'data': [
|
||||
'ir.model.access.csv',
|
||||
'menu.xml',
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_test_testing_utilities_a,access_test_testing_utilities_a,model_test_testing_utilities_a,,1,0,0,0
|
||||
access_test_testing_utilities_readonly,access_test_testing_utilities_readonly,model_test_testing_utilities_readonly,,1,0,0,0
|
||||
access_test_testing_utilities_c,access_test_testing_utilities_c,model_test_testing_utilities_c,,1,0,0,0
|
||||
access_test_testing_utilities_d,access_test_testing_utilities_d,model_test_testing_utilities_d,,1,0,0,0
|
||||
access_test_testing_utilities_e,access_test_testing_utilities_e,model_test_testing_utilities_e,,1,0,0,0
|
||||
access_test_testing_utilities_f,access_test_testing_utilities_f,model_test_testing_utilities_f,,1,0,0,0
|
||||
access_test_testing_utilities_g,access_test_testing_utilities_g,model_test_testing_utilities_g,,1,0,0,0
|
||||
access_test_testing_utilities_m2o,access_test_testing_utilities_m2o,model_test_testing_utilities_m2o,,1,0,0,0
|
||||
access_test_testing_utilities_default,access_test_testing_utilities_default,model_test_testing_utilities_default,,1,0,0,0
|
||||
access_test_testing_utilities_parent,access_test_testing_utilities_parent,model_test_testing_utilities_parent,,1,0,0,0
|
||||
access_test_testing_utilities_sub,access_test_testing_utilities_sub,model_test_testing_utilities_sub,,1,0,0,0
|
||||
access_test_testing_utilities_sub2,access_test_testing_utilities_sub2,model_test_testing_utilities_sub2,,1,0,0,0
|
||||
access_test_testing_utilities_sub3,access_test_testing_utilities_sub3,model_test_testing_utilities_sub3,,1,0,0,0
|
||||
|
@@ -0,0 +1,94 @@
|
||||
<odoo>
|
||||
<record id="action_test_o2m_onchange" model="ir.actions.act_window">
|
||||
<field name="name">Test o2m onchange thing</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">test_testing_utilities.parent</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_o2m" parent="base.menu_tests"
|
||||
name="O2M onchange test thing"
|
||||
action="action_test_o2m_onchange"/>
|
||||
|
||||
<record id="o2m_parent" model="ir.ui.view">
|
||||
<field name="name">Parent External Form</field>
|
||||
<field name="priority">1</field>
|
||||
<field name="model">test_testing_utilities.parent</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="value"/>
|
||||
<field name="v"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="subs"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="o2m_parent_readonly" model="ir.ui.view">
|
||||
<field name="name">Parent readonly o2m field</field>
|
||||
<field name="model">test_testing_utilities.parent</field>
|
||||
<field name="inherit_id" ref="o2m_parent"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='subs']" position="attributes">
|
||||
<attribute name="readonly">1</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="o2m_tree" model="ir.ui.view">
|
||||
<field name="name">Sub regular</field>
|
||||
<field name="priority">1</field>
|
||||
<field name="model">test_testing_utilities.sub</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="value"/>
|
||||
<field name="v"/>
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="o2m_parent_ed" model="ir.ui.view">
|
||||
<field name="name">Parent External Tree</field>
|
||||
<field name="model">test_testing_utilities.parent</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<field name="value"/>
|
||||
<field name="v"/>
|
||||
<field name="subs" context="{'tree_view_ref': 'test_testing_utilities.editable_external'}"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="editable_external" model="ir.ui.view">
|
||||
<field name="name">Sub regular</field>
|
||||
<field name="model">test_testing_utilities.sub</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom">
|
||||
<field name="value"/>
|
||||
<field name="v"/>
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="o2m_parent_inline" model="ir.ui.view">
|
||||
<field name="name">Parent Inline Tree</field>
|
||||
<field name="model">test_testing_utilities.parent</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<field name="value"/>
|
||||
<field name="v"/>
|
||||
<field name="subs">
|
||||
<tree editable="bottom">
|
||||
<field name="value"/>
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</field>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,171 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import division
|
||||
from odoo import api, fields, models
|
||||
|
||||
class A(models.Model):
|
||||
_name = 'test_testing_utilities.a'
|
||||
|
||||
f1 = fields.Integer(required=True)
|
||||
f2 = fields.Integer(default=42)
|
||||
f3 = fields.Integer()
|
||||
f4 = fields.Integer(compute='_compute_f4')
|
||||
|
||||
@api.onchange('f2')
|
||||
def _on_change_f2(self):
|
||||
self.f3 = int(self.f2 / 2)
|
||||
|
||||
@api.depends('f1', 'f2')
|
||||
def _compute_f4(self):
|
||||
for r in self:
|
||||
r.f4 = r.f2 / (r.f1 or 1)
|
||||
|
||||
class B(models.Model):
|
||||
_name = 'test_testing_utilities.readonly'
|
||||
|
||||
f1 = fields.Integer(default=1, readonly=True)
|
||||
f2 = fields.Integer(compute='_compute_f2')
|
||||
|
||||
@api.depends('f1')
|
||||
def _compute_f2(self):
|
||||
for r in self:
|
||||
r.f2 = 2 * r.f1
|
||||
|
||||
class C(models.Model):
|
||||
_name = 'test_testing_utilities.c'
|
||||
|
||||
name = fields.Char("name", required=True)
|
||||
f2 = fields.Many2one('test_testing_utilities.m2o')
|
||||
|
||||
@api.onchange('f2')
|
||||
def _on_change_f2(self):
|
||||
self.name = self.f2.name
|
||||
|
||||
class M2O(models.Model):
|
||||
_name = 'test_testing_utilities.m2o'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
|
||||
class M2Onchange(models.Model):
|
||||
_name = 'test_testing_utilities.d'
|
||||
|
||||
# used to check that defaults & onchange to m2o work
|
||||
f = fields.Many2one(
|
||||
'test_testing_utilities.m2o',
|
||||
required=True,
|
||||
default=lambda self: self.env['test_testing_utilities.m2o'].search(
|
||||
[], limit=1
|
||||
)
|
||||
)
|
||||
f2 = fields.Char()
|
||||
|
||||
@api.onchange('f2')
|
||||
def _on_change_f2(self):
|
||||
self.f = self.env['test_testing_utilities.m2o'].search([
|
||||
('name', 'ilike', self.f2),
|
||||
], limit=1)
|
||||
|
||||
class M2MChange(models.Model):
|
||||
_name = 'test_testing_utilities.e'
|
||||
|
||||
m2m = fields.Many2many('test_testing_utilities.sub2')
|
||||
count = fields.Integer(compute='_m2m_count')
|
||||
|
||||
@api.depends('m2m')
|
||||
def _m2m_count(self):
|
||||
for r in self:
|
||||
r.count = len(r.m2m)
|
||||
|
||||
class M2MSub(models.Model):
|
||||
_name = 'test_testing_utilities.sub2'
|
||||
|
||||
name = fields.Char()
|
||||
|
||||
class M2MChange2(models.Model):
|
||||
_name = 'test_testing_utilities.f'
|
||||
|
||||
def _get_some(self):
|
||||
r = self.env['test_testing_utilities.sub2'].search([], limit=2)
|
||||
return r
|
||||
|
||||
m2m = fields.Many2many(
|
||||
'test_testing_utilities.sub2',
|
||||
default=_get_some,
|
||||
)
|
||||
m2o = fields.Many2one('test_testing_utilities.sub2')
|
||||
|
||||
@api.onchange('m2o')
|
||||
def _on_change_m2o(self):
|
||||
self.m2m = self.m2m | self.m2o
|
||||
|
||||
class M2MReadonly(models.Model):
|
||||
_name = 'test_testing_utilities.g'
|
||||
|
||||
m2m = fields.Many2many('test_testing_utilities.sub3', readonly=True)
|
||||
|
||||
class M2MSub3(models.Model):
|
||||
_name = 'test_testing_utilities.sub3'
|
||||
|
||||
name = fields.Char()
|
||||
|
||||
class O2MChange(models.Model):
|
||||
_name = 'test_testing_utilities.parent'
|
||||
|
||||
value = fields.Integer(default=1)
|
||||
v = fields.Integer()
|
||||
subs = fields.One2many('test_testing_utilities.sub', 'parent_id')
|
||||
|
||||
@api.onchange('value', 'subs')
|
||||
def _onchange_values(self):
|
||||
self.v = self.value + sum(s.value for s in self.subs)
|
||||
|
||||
class O2MSub(models.Model):
|
||||
_name = 'test_testing_utilities.sub'
|
||||
|
||||
name = fields.Char(compute='_compute_name')
|
||||
value = fields.Integer(default=2)
|
||||
v = fields.Integer()
|
||||
parent_id = fields.Many2one('test_testing_utilities.parent')
|
||||
has_parent = fields.Boolean()
|
||||
|
||||
@api.onchange('value')
|
||||
def _onchange_value(self):
|
||||
self.v = self.value
|
||||
|
||||
@api.depends('v')
|
||||
def _compute_name(self):
|
||||
for r in self:
|
||||
r.name = str(r.v)
|
||||
|
||||
@api.onchange('has_parent')
|
||||
def _onchange_has_parent(self):
|
||||
self.has_parent = bool(self.parent_id)
|
||||
if self.has_parent:
|
||||
self.value = self.parent_id.value
|
||||
|
||||
class O2MDefault(models.Model):
|
||||
_name = 'test_testing_utilities.default'
|
||||
|
||||
def _default_subs(self):
|
||||
return [
|
||||
(0, 0, {'v': 5})
|
||||
]
|
||||
value = fields.Integer(default=1)
|
||||
v = fields.Integer()
|
||||
subs = fields.One2many('test_testing_utilities.sub3', 'parent_id', default=_default_subs)
|
||||
|
||||
class O2MSub3(models.Model):
|
||||
_name = 'test_testing_utilities.sub3'
|
||||
|
||||
name = fields.Char(compute='_compute_name')
|
||||
value = fields.Integer(default=2)
|
||||
v = fields.Integer(default=6)
|
||||
parent_id = fields.Many2one('test_testing_utilities.default')
|
||||
|
||||
@api.onchange('value')
|
||||
def _onchange_value(self):
|
||||
self.v = self.value
|
||||
|
||||
@api.depends('v')
|
||||
def _compute_name(self):
|
||||
for r in self:
|
||||
r.name = str(r.v)
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import test_form_impl
|
||||
@@ -0,0 +1,461 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test for the pseudo-form implementation (odoo.tests.common.Form), which should
|
||||
basically be a server-side implementation of form views (though probably not
|
||||
complete) intended for properly validating business "view" flows (onchanges,
|
||||
readonly, required, ...) and make it easier to generate sensible & coherent
|
||||
business objects.
|
||||
"""
|
||||
from operator import itemgetter
|
||||
|
||||
from odoo.tests.common import TransactionCase, Form
|
||||
|
||||
|
||||
class TestBasic(TransactionCase):
|
||||
def test_defaults(self):
|
||||
"""
|
||||
Checks that we can load a default form view and perform trivial
|
||||
default_get & onchanges & computations
|
||||
"""
|
||||
f = Form(self.env['test_testing_utilities.a'])
|
||||
self.assertEqual(f.id, False, "check that our record is not in db (yet)")
|
||||
|
||||
self.assertEqual(f.f2, 42)
|
||||
self.assertEqual(f.f3, 21)
|
||||
self.assertEqual(f.f4, 42)
|
||||
|
||||
f.f1 = 4
|
||||
self.assertEqual(f.f2, 42)
|
||||
self.assertEqual(f.f3, 21)
|
||||
self.assertEqual(f.f4, 10)
|
||||
|
||||
f.f2 = 8
|
||||
self.assertEqual(f.f3, 4)
|
||||
self.assertEqual(f.f4, 2)
|
||||
|
||||
r = f.save()
|
||||
self.assertEqual(
|
||||
(r.f1, r.f2, r.f3, r.f4),
|
||||
(4, 8, 4, 2),
|
||||
)
|
||||
|
||||
def test_required(self):
|
||||
f = Form(self.env['test_testing_utilities.a'])
|
||||
# f1 no default & no value => should fail
|
||||
with self.assertRaisesRegexp(AssertionError, 'f1 is a required field'):
|
||||
f.save()
|
||||
# set f1 and unset f2 => should work
|
||||
f.f1 = 1
|
||||
f.f2 = False
|
||||
r = f.save()
|
||||
self.assertEqual(
|
||||
(r.f1, r.f2, r.f3, r.f4),
|
||||
(1, 0, 0, 0)
|
||||
)
|
||||
|
||||
def test_readonly(self):
|
||||
"""
|
||||
Checks that fields with readonly modifiers (marked as readonly or
|
||||
computed w/o set) raise an error when set.
|
||||
"""
|
||||
f = Form(self.env['test_testing_utilities.readonly'])
|
||||
|
||||
with self.assertRaises(AssertionError):
|
||||
f.f1 = 5
|
||||
with self.assertRaises(AssertionError):
|
||||
f.f2 = 42
|
||||
|
||||
class TestM2O(TransactionCase):
|
||||
def test_default_and_onchange(self):
|
||||
""" Checks defaults & onchanges impacting m2o fields
|
||||
"""
|
||||
Sub = self.env['test_testing_utilities.m2o']
|
||||
a = Sub.create({'name': "A"})
|
||||
b = Sub.create({'name': "B"})
|
||||
|
||||
f = Form(self.env['test_testing_utilities.d'])
|
||||
|
||||
self.assertEqual(
|
||||
f.f, a,
|
||||
"The default value for the m2o should be the first Sub record"
|
||||
)
|
||||
f.f2 = "B"
|
||||
self.assertEqual(
|
||||
f.f, b,
|
||||
"The new m2o value should match the second field by name"
|
||||
)
|
||||
|
||||
f.save()
|
||||
|
||||
def test_set(self):
|
||||
"""
|
||||
Checks that we get/set recordsets for m2o & that set correctly
|
||||
triggers onchange
|
||||
"""
|
||||
r1 = self.env['test_testing_utilities.m2o'].create({'name': "A"})
|
||||
r2 = self.env['test_testing_utilities.m2o'].create({'name': "B"})
|
||||
|
||||
f = Form(self.env['test_testing_utilities.c'])
|
||||
|
||||
# check that basic manipulations work
|
||||
f.f2 = r1
|
||||
self.assertEqual(f.f2, r1)
|
||||
self.assertEqual(f.name, 'A')
|
||||
f.f2 = r2
|
||||
self.assertEqual(f.name, 'B')
|
||||
|
||||
# can't set an int to an m2o field
|
||||
with self.assertRaises(AssertionError):
|
||||
f.f2 = r1.id
|
||||
self.assertEqual(f.f2, r2)
|
||||
self.assertEqual(f.name, 'B')
|
||||
|
||||
# can't set a record of the wrong model
|
||||
temp = self.env['test_testing_utilities.readonly'].create({})
|
||||
with self.assertRaises(AssertionError):
|
||||
f.f2 = temp
|
||||
self.assertEqual(f.f2, r2)
|
||||
self.assertEqual(f.name, 'B')
|
||||
|
||||
r = f.save()
|
||||
self.assertEqual(r.f2, r2)
|
||||
|
||||
class TestM2M(TransactionCase):
|
||||
def test_add(self):
|
||||
Sub = self.env['test_testing_utilities.sub2']
|
||||
f = Form(self.env['test_testing_utilities.e'])
|
||||
|
||||
r1 = Sub.create({'name': "Item"})
|
||||
r2 = Sub.create({'name': "Item2"})
|
||||
|
||||
f.m2m.add(r1)
|
||||
f.m2m.add(r2)
|
||||
|
||||
r = f.save()
|
||||
|
||||
self.assertEqual(
|
||||
r.m2m,
|
||||
r1 | r2
|
||||
)
|
||||
|
||||
def test_remove_by_index(self):
|
||||
Sub = self.env['test_testing_utilities.sub2']
|
||||
f = Form(self.env['test_testing_utilities.e'])
|
||||
|
||||
r1 = Sub.create({'name': "Item"})
|
||||
r2 = Sub.create({'name': "Item2"})
|
||||
|
||||
f.m2m.add(r1)
|
||||
f.m2m.add(r2)
|
||||
f.m2m.remove(index=0)
|
||||
|
||||
r = f.save()
|
||||
|
||||
self.assertEqual(
|
||||
r.m2m,
|
||||
r2
|
||||
)
|
||||
|
||||
def test_remove_by_id(self):
|
||||
Sub = self.env['test_testing_utilities.sub2']
|
||||
f = Form(self.env['test_testing_utilities.e'])
|
||||
|
||||
r1 = Sub.create({'name': "Item"})
|
||||
r2 = Sub.create({'name': "Item2"})
|
||||
|
||||
f.m2m.add(r1)
|
||||
f.m2m.add(r2)
|
||||
f.m2m.remove(id=r1.id)
|
||||
|
||||
r = f.save()
|
||||
|
||||
self.assertEqual(
|
||||
r.m2m,
|
||||
r2
|
||||
)
|
||||
|
||||
def test_on_m2m_change(self):
|
||||
Sub = self.env['test_testing_utilities.sub2']
|
||||
f = Form(self.env['test_testing_utilities.e'])
|
||||
|
||||
self.assertEqual(f.count, 0)
|
||||
f.m2m.add(Sub.create({'name': 'a'}))
|
||||
self.assertEqual(f.count, 1)
|
||||
f.m2m.add(Sub.create({'name': 'a'}))
|
||||
f.m2m.add(Sub.create({'name': 'a'}))
|
||||
f.m2m.add(Sub.create({'name': 'a'}))
|
||||
self.assertEqual(f.count, 4)
|
||||
f.m2m.remove(index=0)
|
||||
f.m2m.remove(index=0)
|
||||
f.m2m.remove(index=0)
|
||||
self.assertEqual(f.count, 1)
|
||||
|
||||
def test_m2m_changed(self):
|
||||
Sub = self.env['test_testing_utilities.sub2']
|
||||
a = Sub.create({'name': 'a'})
|
||||
b = Sub.create({'name': 'b'})
|
||||
c = Sub.create({'name': 'c'})
|
||||
d = Sub.create({'name': 'd'})
|
||||
|
||||
f = Form(self.env['test_testing_utilities.f'])
|
||||
# check default_get
|
||||
self.assertEqual(f.m2m[:], a | b)
|
||||
|
||||
f.m2o = c
|
||||
self.assertEqual(f.m2m[:], a | b | c)
|
||||
|
||||
f.m2o = d
|
||||
self.assertEqual(f.m2m[:], a | b | c | d)
|
||||
|
||||
def test_m2m_readonly(self):
|
||||
Sub = self.env['test_testing_utilities.sub3']
|
||||
a = Sub.create({'name': 'a'})
|
||||
b = Sub.create({'name': 'b'})
|
||||
r = self.env['test_testing_utilities.g'].create({
|
||||
'm2m': [(6, 0, a.ids)]
|
||||
})
|
||||
|
||||
f = Form(r)
|
||||
|
||||
with self.assertRaises(AssertionError):
|
||||
f.m2m.add(b)
|
||||
with self.assertRaises(AssertionError):
|
||||
f.m2m.remove(id=a.id)
|
||||
|
||||
f.save()
|
||||
self.assertEqual(r.m2m, a)
|
||||
|
||||
|
||||
get = itemgetter('name', 'value', 'v')
|
||||
class TestO2M(TransactionCase):
|
||||
def test_basic_alterations(self):
|
||||
""" Tests that the o2m proxy allows adding, removing and editing o2m
|
||||
records
|
||||
"""
|
||||
f = Form(self.env['test_testing_utilities.parent'], view='test_testing_utilities.o2m_parent')
|
||||
|
||||
f.subs.new().save()
|
||||
f.subs.new().save()
|
||||
f.subs.new().save()
|
||||
f.subs.remove(index=0)
|
||||
|
||||
r = f.save()
|
||||
|
||||
self.assertEqual(
|
||||
[get(s) for s in r.subs],
|
||||
[("2", 2, 2), ("2", 2, 2)]
|
||||
)
|
||||
|
||||
with Form(r, view='test_testing_utilities.o2m_parent') as f:
|
||||
with f.subs.new() as sub:
|
||||
sub.value = 5
|
||||
f.subs.new().save()
|
||||
|
||||
with f.subs.edit(index=2) as sub:
|
||||
self.assertEqual(sub.v, 5)
|
||||
|
||||
f.subs.remove(index=0)
|
||||
|
||||
self.assertEqual(
|
||||
[get(s) for s in r.subs],
|
||||
[("2", 2, 2), ("5", 5, 5), ("2", 2, 2)]
|
||||
)
|
||||
|
||||
with Form(r, view='test_testing_utilities.o2m_parent') as f, \
|
||||
f.subs.edit(index=0) as sub,\
|
||||
self.assertRaises(AssertionError):
|
||||
sub.name = "whop whop"
|
||||
|
||||
def test_o2m_editable_list(self):
|
||||
""" Tests the o2m proxy when the list view is editable rather than
|
||||
delegating to a separate form view
|
||||
"""
|
||||
f = Form(self.env['test_testing_utilities.parent'], view='test_testing_utilities.o2m_parent_ed')
|
||||
|
||||
with f.subs.new() as s:
|
||||
s.value = 1
|
||||
with f.subs.new() as s:
|
||||
s.value = 3
|
||||
with f.subs.new() as s:
|
||||
s.value = 7
|
||||
|
||||
r = f.save()
|
||||
|
||||
self.assertEqual(r.v, 12)
|
||||
self.assertEqual(
|
||||
[get(s) for s in r.subs],
|
||||
[('1', 1, 1), ('3', 3, 3), ('7', 7, 7)]
|
||||
)
|
||||
|
||||
def test_o2m_inline(self):
|
||||
""" Tests the o2m proxy when the list and form views are provided
|
||||
inline rather than fetched separately
|
||||
"""
|
||||
f = Form(self.env['test_testing_utilities.parent'], view='test_testing_utilities.o2m_parent_inline')
|
||||
|
||||
with f.subs.new() as s:
|
||||
s.value = 42
|
||||
|
||||
r = f.save()
|
||||
|
||||
self.assertEqual(
|
||||
[get(s) for s in r.subs],
|
||||
[("0", 42, 0)],
|
||||
"should not have set v (and thus not name)"
|
||||
)
|
||||
|
||||
def test_o2m_default(self):
|
||||
""" Tests that default_get can return defaults for the o2m
|
||||
"""
|
||||
f = Form(self.env['test_testing_utilities.default'])
|
||||
|
||||
with f.subs.edit(index=0) as s:
|
||||
self.assertEqual(s.v, 5)
|
||||
self.assertEqual(s.value, False)
|
||||
|
||||
r = f.save()
|
||||
|
||||
self.assertEqual(
|
||||
[get(s) for s in r.subs],
|
||||
[("5", 2, 5)]
|
||||
)
|
||||
|
||||
def test_o2m_inner_default(self):
|
||||
""" Tests that creating an o2m record will get defaults for it
|
||||
"""
|
||||
f = Form(self.env['test_testing_utilities.default'])
|
||||
|
||||
with f.subs.new() as s:
|
||||
self.assertEqual(s.value, 2)
|
||||
self.assertEqual(s.v, 2, "should have onchanged value to v")
|
||||
|
||||
def test_o2m_onchange_parent(self):
|
||||
""" Tests that changing o2m content triggers onchange in the parent
|
||||
"""
|
||||
f = Form(self.env['test_testing_utilities.parent'])
|
||||
|
||||
self.assertEqual(f.value, 1, "value should have its default")
|
||||
self.assertEqual(f.v, 1, "v should be equal to value")
|
||||
|
||||
f.subs.new().save()
|
||||
|
||||
self.assertEqual(f.v, 3, "should be sum of value & children v")
|
||||
|
||||
def test_o2m_onchange_inner(self):
|
||||
""" Tests that editing a field of an o2m record triggers onchange
|
||||
in the o2m record and its parent
|
||||
"""
|
||||
f = Form(self.env['test_testing_utilities.parent'])
|
||||
|
||||
# only apply the onchange on edition end (?)
|
||||
with f.subs.new() as sub:
|
||||
sub.value = 6
|
||||
self.assertEqual(sub.v, 6)
|
||||
self.assertEqual(f.v, 1)
|
||||
self.assertEqual(f.v, 7)
|
||||
|
||||
def test_o2m_parent_content(self):
|
||||
""" Tests that when editing a field of an o2m the data sent contains
|
||||
the parent data
|
||||
"""
|
||||
f = Form(self.env['test_testing_utilities.parent'])
|
||||
|
||||
# only apply the onchange on edition end (?)
|
||||
with f.subs.new() as sub:
|
||||
sub.has_parent = True
|
||||
self.assertEqual(sub.has_parent, True)
|
||||
self.assertEqual(sub.value, 1)
|
||||
self.assertEqual(sub.v, 1)
|
||||
|
||||
def test_m2o_readonly(self):
|
||||
r = self.env['test_testing_utilities.parent'].create({
|
||||
'subs': [(0, 0, {})]
|
||||
})
|
||||
f = Form(r, view='test_testing_utilities.o2m_parent_readonly')
|
||||
|
||||
with self.assertRaises(AssertionError):
|
||||
f.subs.new()
|
||||
with self.assertRaises(AssertionError):
|
||||
f.subs.edit(index=0)
|
||||
with self.assertRaises(AssertionError):
|
||||
f.subs.remove(index=0)
|
||||
|
||||
class TestEdition(TransactionCase):
|
||||
""" These use the context manager form as we don't need the record
|
||||
post-save (we already have it) and it's easier to see what bits act on
|
||||
the form (inside `with`) versus outside. That let me catch a few
|
||||
mistakes.
|
||||
"""
|
||||
def test_trivial(self):
|
||||
r = self.env['test_testing_utilities.a'].create({
|
||||
'f1': 5,
|
||||
})
|
||||
|
||||
with Form(r) as f:
|
||||
self.assertEqual(f.id, r.id)
|
||||
self.assertEqual(f.f1, 5)
|
||||
self.assertEqual(f.f4, 8)
|
||||
|
||||
f.f2 = 5
|
||||
self.assertEqual(f.f3, 2)
|
||||
self.assertEqual(f.f4, 1)
|
||||
|
||||
self.assertEqual(r.f2, 5)
|
||||
self.assertEqual(r.f3, 2)
|
||||
|
||||
def test_m2o(self):
|
||||
Sub = self.env['test_testing_utilities.m2o']
|
||||
a = Sub.create({'name': 'a'})
|
||||
b = Sub.create({'name': 'b'})
|
||||
c = Sub.create({'name': 'c'})
|
||||
|
||||
r = self.env['test_testing_utilities.d'].create({
|
||||
'f': c.id,
|
||||
'f2': "OK",
|
||||
})
|
||||
|
||||
with Form(r) as f:
|
||||
# no default/onchange should have run so loading an incoherent
|
||||
# record should still be incoherent
|
||||
self.assertEqual(f.f, c)
|
||||
self.assertEqual(f.f2, 'OK')
|
||||
|
||||
f.f2 = "b"
|
||||
self.assertEqual(f.f, b)
|
||||
f.f2 = "Whoops"
|
||||
self.assertEqual(f.f, Sub)
|
||||
f.f2 = "a"
|
||||
self.assertEqual(f.f, a)
|
||||
|
||||
self.assertEqual(r.f2, "a")
|
||||
self.assertEqual(r.f, a)
|
||||
|
||||
def test_m2m_empty(self):
|
||||
sub = self.env['test_testing_utilities.sub2'].create({'name': 'a'})
|
||||
|
||||
r = self.env['test_testing_utilities.f'].create({
|
||||
'm2m': []
|
||||
})
|
||||
|
||||
with Form(r) as f:
|
||||
f.m2o = sub
|
||||
|
||||
self.assertEqual(r.m2o, sub)
|
||||
self.assertEqual(r.m2m, sub)
|
||||
|
||||
def test_m2m_nonempty(self):
|
||||
Sub = self.env['test_testing_utilities.sub2']
|
||||
a = Sub.create({'name': 'a'})
|
||||
b = Sub.create({'name': 'b'})
|
||||
c = Sub.create({'name': 'c'})
|
||||
|
||||
r = self.env['test_testing_utilities.f'].create({
|
||||
'm2m': [(6, 0, (a | b | c).ids)]
|
||||
})
|
||||
|
||||
with Form(r) as f:
|
||||
self.assertEqual(f.m2m[:], (a | b | c))
|
||||
f.m2m.remove(index=0)
|
||||
self.assertEqual(f.m2m[:], (b | c))
|
||||
|
||||
self.assertEqual(r.m2m, (b | c))
|
||||
+529
-3
@@ -7,23 +7,26 @@ helpers and classes to write tests.
|
||||
import errno
|
||||
import glob
|
||||
import importlib
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
import re
|
||||
import select
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import itertools
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timedelta
|
||||
from lxml import etree, html
|
||||
from pprint import pformat
|
||||
|
||||
import requests
|
||||
from lxml import etree, html
|
||||
|
||||
from odoo.tools import pycompat
|
||||
from odoo.models import BaseModel
|
||||
from odoo.tools import pycompat, safe_eval
|
||||
|
||||
try:
|
||||
from itertools import zip_longest as izip_longest
|
||||
@@ -477,3 +480,526 @@ def can_import(module):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
# TODO: sub-views (o2m, m2m) -> sub-form?
|
||||
# TODO: domains
|
||||
ref_re = re.compile("""
|
||||
# first match 'form_view_ref' key, backrefs are used to handle single or
|
||||
# double quoting of the value
|
||||
(['"])(?P<view_type>\w+)_view_ref\1
|
||||
# colon separator (with optional spaces around)
|
||||
\s*:\s*
|
||||
# open quote for value
|
||||
(['"])
|
||||
(?P<view_id>
|
||||
# we'll just match stuff which is normally part of an xid:
|
||||
# word and "." characters
|
||||
[.\w]+
|
||||
)
|
||||
# close with same quote as opening
|
||||
\2
|
||||
""", re.VERBOSE)
|
||||
class Form(object):
|
||||
def __init__(self, recordp, view=None):
|
||||
"""
|
||||
:param recordp: empty or singleton recordset. An empty recordset will
|
||||
put the view in "creation" mode and trigger calls to
|
||||
default_get and on-load onchanges, a singleton will
|
||||
put it in "edit" mode and only load the view'd data.
|
||||
:type recordp: odoo.models.BaseModel
|
||||
:param view: the id, xmlid or actual view object to use for
|
||||
onchanges and view constraints. If none is provided,
|
||||
simply loads the default view for the model.
|
||||
:type view: int | str | odoo.model.BaseModel
|
||||
"""
|
||||
# necessary as we're overriding setattr
|
||||
assert isinstance(recordp, BaseModel)
|
||||
env = recordp.env
|
||||
object.__setattr__(self, '_env', env)
|
||||
|
||||
# store model bit only
|
||||
object.__setattr__(self, '_model', recordp.browse(()))
|
||||
if isinstance(view, BaseModel):
|
||||
view_id = view.id
|
||||
elif isinstance(view, pycompat.string_types):
|
||||
view_id = env.ref(view).id
|
||||
else:
|
||||
view_id = view or False
|
||||
fvg = recordp.fields_view_get(view_id, 'form')
|
||||
arch = etree.fromstring(fvg['arch'])
|
||||
|
||||
object.__setattr__(self, '_view', fvg)
|
||||
# TODO: make this less crappy?
|
||||
# look up edition view for the O2M
|
||||
for f, descr in fvg['fields'].items():
|
||||
if descr['type'] != 'one2many':
|
||||
continue
|
||||
|
||||
node = next(n for n in arch.iter('field') if n.get('name') == f)
|
||||
default_view = next(
|
||||
(m for m in node.get('mode', 'tree').split(',') if m != 'form'),
|
||||
'tree'
|
||||
)
|
||||
|
||||
refs = {
|
||||
m.group('view_type'): m.group('view_id')
|
||||
for m in ref_re.finditer(node.get('context', ''))
|
||||
}
|
||||
# always fetch for simplicity, ensure we always have a tree and
|
||||
# a form view
|
||||
submodel = env[descr['relation']]
|
||||
views = submodel.with_context(**refs) \
|
||||
.load_views([(False, 'tree'), (False, 'form')])['fields_views']
|
||||
# embedded views should take the priority on externals
|
||||
views.update(descr['views'])
|
||||
|
||||
# if the default view is a kanban or a non-editable list, the
|
||||
# "edition controller" is the form view
|
||||
edition = views['form']
|
||||
if default_view == 'tree':
|
||||
subarch = etree.fromstring(views['tree']['arch'])
|
||||
if subarch.get('editable'):
|
||||
edition = views['tree']
|
||||
|
||||
self._process_fvg(submodel, edition)
|
||||
descr['views']['edition'] = edition
|
||||
|
||||
self._process_fvg(recordp, fvg)
|
||||
|
||||
# ordered?
|
||||
vals = dict.fromkeys(fvg['fields'], False)
|
||||
object.__setattr__(self, '_values', vals)
|
||||
object.__setattr__(self, '_changed', set())
|
||||
if recordp:
|
||||
assert recordp['id'], "editing unstored records is not supported"
|
||||
# always load the id
|
||||
vals['id'] = recordp['id']
|
||||
|
||||
self._init_from_values(recordp)
|
||||
else:
|
||||
self._init_from_defaults(self._model)
|
||||
|
||||
def _process_fvg(self, model, fvg):
|
||||
""" Post-processes to augment the fields_view_get with:
|
||||
|
||||
* an id field (may not be present if not in the view but needed)
|
||||
* pre-processed modifiers (map of modifier name to json-loaded domain)
|
||||
* pre-processed onchanges list
|
||||
"""
|
||||
fvg['fields']['id'] = {'type': 'id'}
|
||||
# pre-resolve modifiers & bind to arch toplevel
|
||||
modifiers = fvg['modifiers'] = {}
|
||||
contexts = fvg['contexts'] = {}
|
||||
for f in etree.fromstring(fvg['arch']).iter('field'):
|
||||
fname = f.get('name')
|
||||
modifiers[fname] = json.loads(f.get('modifiers'))
|
||||
ctx = f.get('context')
|
||||
if ctx:
|
||||
contexts[fname] = ctx
|
||||
fvg['modifiers']['id'] = {'required': False, 'readonly': True}
|
||||
fvg['onchange'] = model._onchange_spec(fvg)
|
||||
|
||||
def _init_from_defaults(self, model):
|
||||
vals = self._values
|
||||
fields = self._view['fields']
|
||||
def cleanup(k, v):
|
||||
if fields[k]['type'] == 'one2many':
|
||||
# o2m default gets a (6) at the start, makes no sense
|
||||
return [c for c in v if c[0] != 6]
|
||||
return v
|
||||
defaults = {
|
||||
k: cleanup(k, v)
|
||||
for k, v in model.default_get(list(fields)).items()
|
||||
if k in fields
|
||||
}
|
||||
vals.update(defaults)
|
||||
# m2m should all be rep'd as command list
|
||||
for k, v in vals.items():
|
||||
if not v:
|
||||
type_ = fields[k]['type']
|
||||
if type_ == 'many2many':
|
||||
vals[k] = [(6, False, [])]
|
||||
elif type_ == 'one2many':
|
||||
vals[k] = []
|
||||
|
||||
# TODO: check that only fields with default values should be sent
|
||||
self._perform_onchange(list(defaults.keys()))
|
||||
|
||||
def _init_from_values(self, values):
|
||||
self._values.update(
|
||||
record_to_values(self._view['fields'], values))
|
||||
|
||||
def __getattr__(self, field):
|
||||
descr = self._view['fields'].get(field)
|
||||
assert descr is not None, "%s was not found in the view" % field
|
||||
|
||||
v = self._values[field]
|
||||
if descr['type'] == 'many2one':
|
||||
Model = self._env[descr['relation']]
|
||||
if not v:
|
||||
return Model
|
||||
return Model.browse(v)
|
||||
elif descr['type'] == 'many2many':
|
||||
return M2MProxy(self, field)
|
||||
elif descr['type'] == 'one2many':
|
||||
return O2MProxy(self, field)
|
||||
return v
|
||||
|
||||
def _get_modifier(self, field, modifier, default=False):
|
||||
d = self._view['modifiers'][field].get(modifier, default)
|
||||
if isinstance(d, bool):
|
||||
return d
|
||||
|
||||
vals = self._values
|
||||
stack = []
|
||||
for it in reversed(d):
|
||||
if it == '!':
|
||||
stack.append(not stack.pop())
|
||||
elif it == '&':
|
||||
e1 = stack.pop()
|
||||
e2 = stack.pop()
|
||||
stack.append(e1 and e2)
|
||||
elif it == '|':
|
||||
e1 = stack.pop()
|
||||
e2 = stack.pop()
|
||||
stack.append(e1 or e2)
|
||||
elif isinstance(it, list):
|
||||
f, op, val = it
|
||||
field_val = vals[f]
|
||||
stack.append(self._OPS[op](field_val, val))
|
||||
else:
|
||||
raise ValueError("Unknown domain element %s" % it)
|
||||
[result] = stack
|
||||
return result
|
||||
_OPS = {
|
||||
'=': operator.eq,
|
||||
'==': operator.eq,
|
||||
'!=': operator.ne,
|
||||
'<': operator.lt,
|
||||
'<=': operator.le,
|
||||
'>=': operator.ge,
|
||||
'>': operator.gt,
|
||||
'in': lambda a, b: a in b,
|
||||
'not in': lambda a, b: a not in b
|
||||
}
|
||||
def _get_context(self, field):
|
||||
c = self._view['contexts'].get(field)
|
||||
if not c:
|
||||
return {}
|
||||
|
||||
return safe_eval(c, self._values)
|
||||
|
||||
def __setattr__(self, field, value):
|
||||
descr = self._view['fields'].get(field)
|
||||
assert descr is not None, "%s was not found in the view" % field
|
||||
assert descr['type'] not in ('many2many', 'one2many'),\
|
||||
"Can't set an o2m or m2m field, manipulate the corresponding proxies"
|
||||
|
||||
# TODO: consider invisible to be the same as readonly?
|
||||
assert not self._get_modifier(field, 'readonly'), \
|
||||
"can't write on readonly field {}".format(field)
|
||||
|
||||
if descr['type'] == 'many2one':
|
||||
assert isinstance(value, BaseModel) and value._name == descr['relation']
|
||||
# store just the id: that's the output of default_get & (more
|
||||
# or less) onchange.
|
||||
value = value.id
|
||||
|
||||
self._values[field] = value
|
||||
self._perform_onchange([field])
|
||||
|
||||
# enables with Form(...) as f: f.a = 1; f.b = 2; f.c = 3
|
||||
# q: how to get recordset?
|
||||
def __enter__(self):
|
||||
return self
|
||||
def __exit__(self, *_):
|
||||
self.save()
|
||||
|
||||
def save(self):
|
||||
id_ = self._values.get('id')
|
||||
values = self._values_to_save()
|
||||
if id_:
|
||||
r = self._model.browse(id_)
|
||||
if values:
|
||||
r.write(values)
|
||||
else:
|
||||
r = self._model.create(values)
|
||||
[data] = r.read(list(self._view['fields']))
|
||||
# FIXME: process relational fields
|
||||
# alternative: iterate on record & read from it directly? pb: would
|
||||
# provide recordsets for relational fields which may or may not be
|
||||
# what we ultimately want, but we've got to re-process them anyway so...?
|
||||
self._values.update(data)
|
||||
self._changed.clear()
|
||||
return r
|
||||
|
||||
def _values_to_save(self):
|
||||
""" Validates values and returns only fields modified since
|
||||
load/save
|
||||
"""
|
||||
values = {}
|
||||
for f in self._view['fields']:
|
||||
v = self._values[f]
|
||||
if self._get_modifier(f, 'required'):
|
||||
assert v, "{} is a required field".format(f)
|
||||
|
||||
# skip unmodified fields
|
||||
if f not in self._changed:
|
||||
continue
|
||||
if self._get_modifier(f, 'readonly'):
|
||||
continue
|
||||
# TODO: filter out (1, _, {}) from o2m values
|
||||
values[f] = v
|
||||
return values
|
||||
|
||||
def _perform_onchange(self, fields):
|
||||
assert isinstance(fields, list)
|
||||
|
||||
# marks any onchange source as changed (default_get or explicit set)
|
||||
self._changed.update(fields)
|
||||
|
||||
result = self._model.onchange(
|
||||
self._onchange_values(),
|
||||
fields,
|
||||
self._view['onchange'],
|
||||
)
|
||||
assert not result.get('warning')
|
||||
values = result.get('value', {})
|
||||
# mark onchange output as changed
|
||||
self._changed.update(values.keys())
|
||||
self._values.update(
|
||||
(k, self._cleanup_onchange(
|
||||
self._view['fields'][k],
|
||||
v, self._values[k],
|
||||
))
|
||||
for k, v in values.items()
|
||||
if k in self._view['fields']
|
||||
)
|
||||
|
||||
def _onchange_values(self):
|
||||
return dict(self._values)
|
||||
|
||||
def _cleanup_onchange(self, descr, value, current):
|
||||
if descr['type'] == 'many2one':
|
||||
if not value:
|
||||
return False
|
||||
# out of onchange, m2o are name-gotten
|
||||
return value[0]
|
||||
elif descr['type'] == 'one2many':
|
||||
# ignore o2ms nested in o2ms
|
||||
if not descr['views']:
|
||||
return []
|
||||
|
||||
v = []
|
||||
# which view should this be???
|
||||
subfields = descr['views']['edition']['fields']
|
||||
for command in value:
|
||||
# TODO: get existing sub-values so we can pass them along?
|
||||
if command[0] in (0, 1):
|
||||
v.append((command[0], command[1], {
|
||||
k: self._cleanup_onchange(
|
||||
subfields[k], v, None
|
||||
)
|
||||
for k, v in command[2].items()
|
||||
if k in subfields
|
||||
}))
|
||||
# TODO: should reuse existing values if not 5?
|
||||
return v
|
||||
elif descr['type'] == 'many2many':
|
||||
# onchange result is a bunch of commands, normalize to single 6
|
||||
if current is None:
|
||||
ids = []
|
||||
else:
|
||||
ids = list(current[0][2])
|
||||
for command in value:
|
||||
if command[0] == 3:
|
||||
ids.remove(command[1])
|
||||
elif command[0] == 4:
|
||||
ids.append(command[1])
|
||||
elif command[0] == 5:
|
||||
del ids[:]
|
||||
elif command[0] == 6:
|
||||
ids[:] = command[2]
|
||||
else:
|
||||
raise ValueError(
|
||||
"Unsupported M2M command %d" % command[0])
|
||||
return [(6, 0, ids)]
|
||||
|
||||
return value
|
||||
|
||||
class O2MForm(Form):
|
||||
# noinspection PyMissingConstructor
|
||||
def __init__(self, proxy, index=None):
|
||||
m = proxy._model
|
||||
object.__setattr__(self, '_proxy', proxy)
|
||||
object.__setattr__(self, '_index', index)
|
||||
|
||||
object.__setattr__(self, '_env', m.env)
|
||||
object.__setattr__(self, '_model', m)
|
||||
|
||||
# copy so we don't risk breaking it too much (?)
|
||||
fvg = dict(proxy._descr['views']['edition'])
|
||||
object.__setattr__(self, '_view', fvg)
|
||||
self._process_fvg(m, fvg)
|
||||
|
||||
vals = dict.fromkeys(fvg['fields'], False)
|
||||
object.__setattr__(self, '_values', vals)
|
||||
object.__setattr__(self, '_changed', set())
|
||||
if index is None:
|
||||
self._init_from_defaults(m)
|
||||
else:
|
||||
self._values.update(proxy._records[index])
|
||||
|
||||
def _onchange_values(self):
|
||||
values = super(O2MForm, self)._onchange_values()
|
||||
values[self._proxy._descr['relation_field']] = \
|
||||
self._proxy._parent._values
|
||||
return values
|
||||
|
||||
def save(self):
|
||||
proxy = self._proxy
|
||||
commands = proxy._parent._values[proxy._field]
|
||||
values = self._values_to_save()
|
||||
if self._index is None:
|
||||
commands.append((0, 0, values))
|
||||
else:
|
||||
(c, _, vs) = commands[proxy._command_index(self._index)]
|
||||
assert c in (0, 1)
|
||||
vs.update(values)
|
||||
|
||||
# FIXME: should be called when performing on change => value needs to be serialised into parent every time?
|
||||
proxy._parent._perform_onchange([proxy._field])
|
||||
|
||||
class X2MProxy(object):
|
||||
_parent = None
|
||||
_field = None
|
||||
def _assert_editable(self):
|
||||
assert not self._parent._get_modifier(self._field, 'readonly'),\
|
||||
'field %s is not editable' % self._field
|
||||
|
||||
class O2MProxy(X2MProxy):
|
||||
def __init__(self, parent, field):
|
||||
self._parent = parent
|
||||
self._field = field
|
||||
# reify records to a list so they can be manipulated easily?
|
||||
self._records = []
|
||||
model = self._model
|
||||
fields = self._descr['views']['edition']['fields']
|
||||
for (command, rid, values) in self._parent._values[self._field]:
|
||||
if command == 0:
|
||||
self._records.append(values)
|
||||
elif command == 1:
|
||||
# read based on view info
|
||||
r = model.browse(rid)
|
||||
record = record_to_values(fields, r)
|
||||
record.update(values)
|
||||
self._records.append(record)
|
||||
elif command == 2:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("O2M proxy only supports commands 0, 1 and 2")
|
||||
|
||||
@property
|
||||
def _model(self):
|
||||
model = self._parent._env[self._descr['relation']]
|
||||
ctx = self._parent._get_context(self._field)
|
||||
if ctx:
|
||||
model = model.with_context(**ctx)
|
||||
return model
|
||||
|
||||
@property
|
||||
def _descr(self):
|
||||
return self._parent._view['fields'][self._field]
|
||||
|
||||
def _command_index(self, for_record):
|
||||
""" Takes a record index and finds the corresponding record index
|
||||
(skips all 2s, basically)
|
||||
|
||||
:param int for_record:
|
||||
"""
|
||||
commands = self._parent._values[self._field]
|
||||
return next(
|
||||
cidx
|
||||
for ridx, cidx in enumerate(
|
||||
cidx for cidx, (c, _1, _2) in enumerate(commands)
|
||||
if c in (0, 1)
|
||||
)
|
||||
if ridx == for_record
|
||||
)
|
||||
|
||||
def new(self):
|
||||
self._assert_editable()
|
||||
return O2MForm(self)
|
||||
|
||||
def edit(self, index):
|
||||
self._assert_editable()
|
||||
return O2MForm(self, index)
|
||||
|
||||
def remove(self, index):
|
||||
self._assert_editable()
|
||||
# remove reified record from local list & either remove 0 from
|
||||
# commands list or replace 1 (update) by 2 (remove)
|
||||
cidx = self._command_index(index)
|
||||
commands = self._parent._values[self._field]
|
||||
(command, rid, _) = commands[cidx]
|
||||
if command == 0:
|
||||
# record not saved yet -> just remove the command
|
||||
del commands[cidx]
|
||||
elif command == 1:
|
||||
# record already saved, replace by 2
|
||||
commands[cidx] = (2, rid, 0)
|
||||
else:
|
||||
raise AssertionError("Expected command 0 or 1, got %s" % commands[cidx])
|
||||
# remove reified record
|
||||
del self._records[index]
|
||||
self._parent._perform_onchange([self._field])
|
||||
|
||||
class M2MProxy(X2MProxy):
|
||||
def __init__(self, parent, field):
|
||||
self._parent = parent
|
||||
self._field = field
|
||||
|
||||
def __getitem__(self, it):
|
||||
p = self._parent
|
||||
model = p._view['fields'][self._field]['relation']
|
||||
return p._env[model].browse(self._get_ids()[it])
|
||||
|
||||
def add(self, record):
|
||||
self._assert_editable()
|
||||
parent = self._parent
|
||||
assert isinstance(record, BaseModel) \
|
||||
and record._name == parent._view['fields'][self._field]['relation']
|
||||
self._get_ids().append(record.id)
|
||||
|
||||
parent._perform_onchange([self._field])
|
||||
|
||||
def _get_ids(self):
|
||||
return self._parent._values[self._field][0][2]
|
||||
|
||||
def remove(self, id=None, index=None):
|
||||
self._assert_editable()
|
||||
assert (id is None) ^ (index is None), \
|
||||
"can remove by either id or index"
|
||||
|
||||
if id is None:
|
||||
# remove by index
|
||||
del self._get_ids()[index]
|
||||
else:
|
||||
self._get_ids().remove(id)
|
||||
|
||||
self._parent._perform_onchange([self._field])
|
||||
|
||||
def record_to_values(fields, record):
|
||||
r = {}
|
||||
for f, descr in fields.items():
|
||||
v = record[f]
|
||||
if descr['type'] == 'many2one':
|
||||
assert v._name == descr['relation']
|
||||
v = v.id
|
||||
elif descr['type'] == 'many2many':
|
||||
assert v._name == descr['relation']
|
||||
v = [(6, 0, v.ids)]
|
||||
elif descr['type'] == 'one2many':
|
||||
v = [(1, r.id, {}) for r in v]
|
||||
r[f] = v
|
||||
return r
|
||||
|
||||
Reference in New Issue
Block a user