[FIX] fields: setting a one2many field deletes all its lines (#13082)

Setting the value of a one2many field (`rec.line_ids = ...`) converts the new
recordset to a list of commands, and passes it to `write`.

Currently the list of commands is like: `[(5,), (0, 0, vals), (1, id, vals), (4, id), ...]`.
Notice that the command `(5,)` is inappropriate here: it deletes all the
existing lines of the record. Reintroducing the same lines with `(4, id)`
commands crashes, because the corresponding `id` is no longer in database.

The fix serializes the recordset as a list of commands like
`[(6, 0, ids), (0, 0, vals), (1, id, vals), ...]`.
Only the lines that are not kept will be deleted in this case.
This commit is contained in:
Jairo Llopis
2016-10-14 14:54:02 +02:00
committed by Raphael Collet
parent 852e2c557e
commit 448521df64
6 changed files with 79 additions and 8 deletions
@@ -5,6 +5,7 @@ from . import test_new_fields
from . import test_onchange
from . import test_field_conversions
from . import test_attributes
from . import test_one2many
from . import test_ui
from . import test_no_infinite_recursion
from . import test_domain
@@ -176,7 +176,7 @@ class TestOnChange(common.TransactionCase):
self.assertEqual(values, {
'name': partner.name,
'partner': partner.id,
'lines': [(5,), (4, line.id)],
'lines': [(6, 0, [line.id])],
})
# modify 'partner'
@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
from openerp.tests.common import TransactionCase
class One2manyCase(TransactionCase):
def setUp(self):
super(One2manyCase, self).setUp()
self.Line = self.env["test_new_api.multi.line"]
self.multi = self.env["test_new_api.multi"].create({
"name": "What is up?"
})
def operations(self):
"""Run operations on o2m fields to check all works fine."""
# Check the lines first
self.assertItemsEqual(
self.multi.lines.mapped('name'),
map(str, range(10)))
# Modify the first line and drop the last one
self.multi.lines[0].name = "hello"
self.multi.lines = self.multi.lines[:-1]
self.assertEqual(len(self.multi.lines), 9)
self.assertIn("hello", self.multi.lines.mapped('name'))
# Invalidate the cache and check again; this crashes if the value
# of self.multi.lines in cache contains new records
self.multi.invalidate_cache()
self.assertEqual(len(self.multi.lines), 9)
self.assertIn("hello", self.multi.lines.mapped('name'))
def test_new_one_by_one(self):
"""Check lines created with ``new()`` and appended one by one."""
for name in range(10):
self.multi.lines |= self.Line.new({"name": str(name)})
self.operations()
def test_new_single(self):
"""Check lines created with ``new()`` and added in one step."""
self.multi.lines = self.Line.browse(
[self.Line.new({"name": str(name)}).id for name in range(10)]
)
self.operations()
def test_create_one_by_one(self):
"""Check lines created with ``create()`` and appended one by one."""
for name in range(10):
self.multi.lines |= self.Line.create({"name": str(name)})
self.operations()
def test_create_single(self):
"""Check lines created with ``create()`` and added in one step."""
self.multi.lines = self.Line.browse(
[self.Line.create({"name": str(name)}).id for name in range(10)]
)
self.operations()
def test_rpcstyle_one_by_one(self):
"""Check lines created with RPC style and appended one by one."""
for name in range(10):
self.multi.lines = [(0, 0, {"name": str(name)})]
self.operations()
def test_rpcstyle_single(self):
"""Check lines created with RPC style and added in one step"""
self.multi.lines = [(0, 0, {'name': str(name)}) for name in range(10)]
self.operations()
+6 -4
View File
@@ -861,9 +861,11 @@ class Field(object):
env.invalidate(spec)
else:
# simply write to the database, and update cache
# Write to database
record.write({self.name: self.convert_to_write(value)})
record._cache[self] = value
# Update the cache unless value contains a new record
if all(getattr(value, '_ids', ())):
record._cache[self] = value
############################################################################
#
@@ -1795,7 +1797,7 @@ class _RelationalMulti(_Relational):
def convert_to_write(self, value):
# make result with new and existing records
result = [(5,)]
result = [(6, 0, [])]
for record in value:
if not record.id:
values = dict(record._cache)
@@ -1806,7 +1808,7 @@ class _RelationalMulti(_Relational):
values = record._convert_to_write(values)
result.append((1, record.id, values))
else:
result.append((4, record.id))
result[0][2].append(record.id)
return result
def convert_to_onchange(self, value, fnames=None):
+1 -2
View File
@@ -3807,8 +3807,7 @@ class BaseModel(object):
``(6, _, ids)``
replaces all existing records in the set by the ``ids`` list,
equivalent to using the command ``5`` followed by a command
``4`` for each ``id`` in ``ids``. Can not be used on
:class:`~openerp.fields.One2many`.
``4`` for each ``id`` in ``ids``.
.. note:: Values marked as ``_`` in the list above are ignored and
can be anything, generally ``0`` or ``False``.
+5 -1
View File
@@ -863,7 +863,11 @@ class one2many(_column):
ids2 = act[2] or [0]
cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
ids3 = map(lambda x:x[0], cr.fetchall())
obj.write(cr, user, ids3, {self._fields_id:False}, context=context or {})
inverse_field = obj._fields.get(self._fields_id)
if getattr(inverse_field, "ondelete", None) == "cascade":
obj.unlink(cr, user, ids3, context=context)
else:
obj.write(cr, user, ids3, {self._fields_id: False}, context=context or {})
return result
def search(self, cr, obj, args, name, value, offset=0, limit=None, uid=None, operator='like', context=None):