odoo-addon-base-write-diff 17.0.1.0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo/addons/base_write_diff/README.rst +191 -0
- odoo/addons/base_write_diff/__init__.py +1 -0
- odoo/addons/base_write_diff/__manifest__.py +14 -0
- odoo/addons/base_write_diff/i18n/base_write_diff.pot +19 -0
- odoo/addons/base_write_diff/models/__init__.py +1 -0
- odoo/addons/base_write_diff/models/base.py +117 -0
- odoo/addons/base_write_diff/readme/CONTRIBUTORS.md +1 -0
- odoo/addons/base_write_diff/readme/DESCRIPTION.md +2 -0
- odoo/addons/base_write_diff/readme/USAGE.md +101 -0
- odoo/addons/base_write_diff/static/description/icon.png +0 -0
- odoo/addons/base_write_diff/static/description/index.html +524 -0
- odoo/addons/base_write_diff/tests/__init__.py +1 -0
- odoo/addons/base_write_diff/tests/test_base_write_diff.py +423 -0
- odoo_addon_base_write_diff-17.0.1.0.0.2.dist-info/METADATA +207 -0
- odoo_addon_base_write_diff-17.0.1.0.0.2.dist-info/RECORD +17 -0
- odoo_addon_base_write_diff-17.0.1.0.0.2.dist-info/WHEEL +5 -0
- odoo_addon_base_write_diff-17.0.1.0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
# Copyright 2025 Camptocamp SA
|
|
2
|
+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
|
3
|
+
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
|
|
6
|
+
from odoo_test_helper import FakeModelLoader
|
|
7
|
+
|
|
8
|
+
from odoo import api, fields, models
|
|
9
|
+
from odoo.tests import TransactionCase
|
|
10
|
+
from odoo.tools.misc import mute_logger
|
|
11
|
+
|
|
12
|
+
from odoo.addons.base.tests.common import DISABLED_MAIL_CONTEXT
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestRecordDiffCommon(TransactionCase):
|
|
16
|
+
@classmethod
|
|
17
|
+
def setUpClass(cls):
|
|
18
|
+
super().setUpClass()
|
|
19
|
+
# Setup env
|
|
20
|
+
cls.env = cls.env["base"].with_context(**DISABLED_MAIL_CONTEXT).env
|
|
21
|
+
|
|
22
|
+
# ``_register_hook()`` is usually called at the end of the test process, but
|
|
23
|
+
# we need to be able to test it here
|
|
24
|
+
cls.env["base"]._register_hook()
|
|
25
|
+
|
|
26
|
+
# Load test model
|
|
27
|
+
cls.loader = FakeModelLoader(cls.env, cls.__module__)
|
|
28
|
+
cls.loader.backup_registry()
|
|
29
|
+
|
|
30
|
+
class BWDTestModel(models.Model):
|
|
31
|
+
_name = "bwd.test.model"
|
|
32
|
+
_description = "Base Write Diff - Test Model"
|
|
33
|
+
_test_logger = getLogger("bwd.test.model.log")
|
|
34
|
+
|
|
35
|
+
# To test a non-relational field
|
|
36
|
+
name = fields.Char()
|
|
37
|
+
# To test single-relational fields
|
|
38
|
+
m2o_id = fields.Many2one("bwd.test.model")
|
|
39
|
+
# To test multi-relational fields
|
|
40
|
+
o2m_ids = fields.One2many("bwd.test.model", inverse_name="m2o_id")
|
|
41
|
+
m2m_ids = fields.Many2many("bwd.test.model", "test_rel", "id_1", "id_2")
|
|
42
|
+
# To test computed fields
|
|
43
|
+
# ``perimeter``: computed, stored field that depends on stored fields
|
|
44
|
+
# ``area``: computed, non-stored field that depends on stored fields
|
|
45
|
+
# ``volume``: computed, non-stored field that depends on non-stored fields
|
|
46
|
+
length = fields.Integer() # pylint: disable=W8105 (Pylint complains this?)
|
|
47
|
+
width = fields.Integer()
|
|
48
|
+
height = fields.Integer()
|
|
49
|
+
perimeter = fields.Integer(compute="_compute_perimeter", store=True)
|
|
50
|
+
area = fields.Integer(compute="_compute_area", store=False)
|
|
51
|
+
volume = fields.Integer(compute="_compute_volume", store=False)
|
|
52
|
+
|
|
53
|
+
@api.depends("length", "width")
|
|
54
|
+
def _compute_perimeter(self):
|
|
55
|
+
self._test_logger.warning("Computing perimeter")
|
|
56
|
+
for rec in self:
|
|
57
|
+
rec.perimeter = 2 * (rec.length + rec.width)
|
|
58
|
+
|
|
59
|
+
@api.depends("length", "width")
|
|
60
|
+
def _compute_area(self):
|
|
61
|
+
self._test_logger.warning("Computing area")
|
|
62
|
+
for rec in self:
|
|
63
|
+
rec.area = rec.length * rec.width
|
|
64
|
+
|
|
65
|
+
@api.depends("area", "height")
|
|
66
|
+
def _compute_volume(self):
|
|
67
|
+
self._test_logger.warning("Computing volume")
|
|
68
|
+
for rec in self:
|
|
69
|
+
rec.volume = rec.area * rec.height
|
|
70
|
+
|
|
71
|
+
cls.loader.update_registry([BWDTestModel])
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def tearDownClass(cls):
|
|
75
|
+
cls.loader.restore_registry()
|
|
76
|
+
super().tearDownClass()
|
|
77
|
+
|
|
78
|
+
def _create_records(self, count=1):
|
|
79
|
+
records = self.env["bwd.test.model"].create([{} for _ in range(1, count + 1)])
|
|
80
|
+
for rec in records:
|
|
81
|
+
rec.name = f"Record {rec.id}"
|
|
82
|
+
return records
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestRecordDiff(TestRecordDiffCommon):
|
|
86
|
+
@mute_logger("bwd.test.model.log")
|
|
87
|
+
def test_00_get_write_diff_values_simple(self):
|
|
88
|
+
"""Test ``_get_write_diff_values()`` on fields that are not multi-relational"""
|
|
89
|
+
record = self._create_records()
|
|
90
|
+
# Try to write the same value on non-relational field
|
|
91
|
+
# => ``_get_write_diff_values()`` returns an empty dict
|
|
92
|
+
vals = {"name": record.name}
|
|
93
|
+
self.assertEqual(record._get_write_diff_values(vals), {})
|
|
94
|
+
# Try to write another value on non-relational field
|
|
95
|
+
# => ``_get_write_diff_values()`` returns the same dict
|
|
96
|
+
vals = {"name": record.name + " something else"}
|
|
97
|
+
self.assertEqual(record._get_write_diff_values(vals), vals)
|
|
98
|
+
# Try to write the same value on M2O field
|
|
99
|
+
# => ``_get_write_diff_values()`` returns an empty dict
|
|
100
|
+
vals = {"m2o_id": record.m2o_id.id}
|
|
101
|
+
self.assertEqual(record._get_write_diff_values(vals), {})
|
|
102
|
+
# Try to write another value on M2O field
|
|
103
|
+
# => ``_get_write_diff_values()`` returns the same dict
|
|
104
|
+
vals = {"m2o_id": self._create_records().id}
|
|
105
|
+
self.assertEqual(record._get_write_diff_values(vals), vals)
|
|
106
|
+
|
|
107
|
+
@mute_logger("bwd.test.model.log")
|
|
108
|
+
def test_10_get_write_diff_values_x2many_command_create(self):
|
|
109
|
+
"""Test ``_get_write_diff_values()`` on fields.Command.create()
|
|
110
|
+
|
|
111
|
+
``_get_write_diff_values()`` always returns the original dict, even after the
|
|
112
|
+
corecords are actually created (because the values will create a new, different
|
|
113
|
+
corecord if used on ``write()`` again)
|
|
114
|
+
"""
|
|
115
|
+
record = self._create_records()
|
|
116
|
+
vals = {
|
|
117
|
+
"o2m_ids": [fields.Command.create({"name": "O2M Co-record"})],
|
|
118
|
+
"m2m_ids": [fields.Command.create({"name": "M2M Co-record"})],
|
|
119
|
+
}
|
|
120
|
+
self.assertEqual(record._get_write_diff_values(vals), vals)
|
|
121
|
+
record.write(vals) # Do the real update => the diff is not empty anyway
|
|
122
|
+
self.assertEqual(record._get_write_diff_values(vals), vals)
|
|
123
|
+
|
|
124
|
+
@mute_logger("bwd.test.model.log")
|
|
125
|
+
def test_11_get_write_diff_values_x2many_command_update(self):
|
|
126
|
+
"""Test ``_get_write_diff_values()`` on fields.Command.update()
|
|
127
|
+
|
|
128
|
+
``_get_write_diff_values()`` returns only the subset of IDs/values that should
|
|
129
|
+
be updated
|
|
130
|
+
"""
|
|
131
|
+
record = self._create_records()
|
|
132
|
+
# Create and assign 2 corecords to each X2M field
|
|
133
|
+
record.o2m_ids = o2m_corecords = self._create_records(2)
|
|
134
|
+
record.m2m_ids = m2m_corecords = self._create_records(2)
|
|
135
|
+
# Set vals to update 1 corecord on each X2M field
|
|
136
|
+
vals = {
|
|
137
|
+
"o2m_ids": [
|
|
138
|
+
fields.Command.update(o2m_corecords[0].id, {"name": "O2M Corec"}),
|
|
139
|
+
fields.Command.update(
|
|
140
|
+
o2m_corecords[1].id, {"name": o2m_corecords[1].name}
|
|
141
|
+
),
|
|
142
|
+
],
|
|
143
|
+
"m2m_ids": [
|
|
144
|
+
fields.Command.update(
|
|
145
|
+
m2m_corecords[0].id, {"name": m2m_corecords[0].name}
|
|
146
|
+
),
|
|
147
|
+
fields.Command.update(m2m_corecords[1].id, {"name": "M2M Corec"}),
|
|
148
|
+
],
|
|
149
|
+
}
|
|
150
|
+
# The diff should include only the IDs we want to update, and the fields we are
|
|
151
|
+
# actually different on them
|
|
152
|
+
self.assertEqual(
|
|
153
|
+
record._get_write_diff_values(vals),
|
|
154
|
+
{
|
|
155
|
+
"o2m_ids": [
|
|
156
|
+
fields.Command.update(o2m_corecords[0].id, {"name": "O2M Corec"})
|
|
157
|
+
],
|
|
158
|
+
"m2m_ids": [
|
|
159
|
+
fields.Command.update(m2m_corecords[1].id, {"name": "M2M Corec"})
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
record.write(vals) # Do the real update => the diff should be empty now
|
|
164
|
+
self.assertEqual(record._get_write_diff_values(vals), {})
|
|
165
|
+
|
|
166
|
+
@mute_logger("bwd.test.model.log")
|
|
167
|
+
def test_12_get_write_diff_values_x2many_command_delete(self):
|
|
168
|
+
"""Test ``_get_write_diff_values()`` on fields.Command.delete()
|
|
169
|
+
|
|
170
|
+
``_get_write_diff_values()`` returns only the subset of IDs that should be
|
|
171
|
+
deleted/unlinked
|
|
172
|
+
"""
|
|
173
|
+
record = self._create_records()
|
|
174
|
+
# Create and assign 2 corecords to each X2M field
|
|
175
|
+
record.o2m_ids = o2m_corecords = self._create_records(2)
|
|
176
|
+
record.m2m_ids = m2m_corecords = self._create_records(2)
|
|
177
|
+
# Set vals to delete 1 corecord in each X2M field
|
|
178
|
+
vals = {
|
|
179
|
+
"o2m_ids": [fields.Command.delete(o2m_corecords[0].id)],
|
|
180
|
+
"m2m_ids": [fields.Command.delete(m2m_corecords[1].id)],
|
|
181
|
+
}
|
|
182
|
+
# The diff should include only the IDs we want to delete
|
|
183
|
+
self.assertEqual(
|
|
184
|
+
record._get_write_diff_values(vals),
|
|
185
|
+
# Odoo assigns command "delete" or "unlink" according to the field type
|
|
186
|
+
# and its definition (not important for our purposes here)
|
|
187
|
+
{
|
|
188
|
+
"o2m_ids": [fields.Command.delete(o2m_corecords[0].id)],
|
|
189
|
+
"m2m_ids": [fields.Command.unlink(m2m_corecords[1].id)],
|
|
190
|
+
},
|
|
191
|
+
)
|
|
192
|
+
record.write(vals) # Do the real update => the diff should be empty now
|
|
193
|
+
self.assertEqual(record._get_write_diff_values(vals), {})
|
|
194
|
+
|
|
195
|
+
@mute_logger("bwd.test.model.log")
|
|
196
|
+
def test_13_get_write_diff_values_x2many_command_unlink(self):
|
|
197
|
+
"""Test ``_get_write_diff_values()`` on fields.Command.unlink()
|
|
198
|
+
|
|
199
|
+
``_get_write_diff_values()`` returns only the subset of IDs that should be
|
|
200
|
+
deleted/unlinked
|
|
201
|
+
"""
|
|
202
|
+
record = self._create_records()
|
|
203
|
+
# Create and assign 2 corecords to each X2M field
|
|
204
|
+
record.o2m_ids = o2m_corecords = self._create_records(2)
|
|
205
|
+
record.m2m_ids = m2m_corecords = self._create_records(2)
|
|
206
|
+
# Set vals to unlink 1 corecord in each X2M field
|
|
207
|
+
vals = {
|
|
208
|
+
"o2m_ids": [fields.Command.unlink(o2m_corecords[0].id)],
|
|
209
|
+
"m2m_ids": [fields.Command.unlink(m2m_corecords[1].id)],
|
|
210
|
+
}
|
|
211
|
+
# The diff should include only the IDs we want to unlink
|
|
212
|
+
self.assertEqual(
|
|
213
|
+
record._get_write_diff_values(vals),
|
|
214
|
+
# Odoo assigns command "delete" or "unlink" according to the field type
|
|
215
|
+
# and its definition (not important for our purposes here)
|
|
216
|
+
{
|
|
217
|
+
"o2m_ids": [fields.Command.delete(o2m_corecords[0].id)],
|
|
218
|
+
"m2m_ids": [fields.Command.unlink(m2m_corecords[1].id)],
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
record.write(vals) # Do the real update => the diff should be empty now
|
|
222
|
+
self.assertEqual(record._get_write_diff_values(vals), {})
|
|
223
|
+
|
|
224
|
+
@mute_logger("bwd.test.model.log")
|
|
225
|
+
def test_14_get_write_diff_values_x2many_command_link(self):
|
|
226
|
+
"""Test ``_get_write_diff_values()`` on fields.Command.link()
|
|
227
|
+
|
|
228
|
+
``_get_write_diff_values()`` returns only the subset of IDs that should be
|
|
229
|
+
linked
|
|
230
|
+
"""
|
|
231
|
+
record = self._create_records()
|
|
232
|
+
# Create 2 corecords
|
|
233
|
+
o2m_corecords = self._create_records(2)
|
|
234
|
+
m2m_corecords = self._create_records(2)
|
|
235
|
+
# Assign 1 corecord to each X2M field
|
|
236
|
+
record.write(
|
|
237
|
+
{
|
|
238
|
+
"o2m_ids": [fields.Command.set(o2m_corecords[0].ids)],
|
|
239
|
+
"m2m_ids": [fields.Command.set(m2m_corecords[1].ids)],
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
# Set vals to link all corecords on each X2M field
|
|
243
|
+
vals = {
|
|
244
|
+
"o2m_ids": [fields.Command.link(i) for i in o2m_corecords.ids],
|
|
245
|
+
"m2m_ids": [fields.Command.link(i) for i in m2m_corecords.ids],
|
|
246
|
+
}
|
|
247
|
+
# The diff should include only the IDs we want to link that are not already
|
|
248
|
+
# linked
|
|
249
|
+
self.assertEqual(
|
|
250
|
+
record._get_write_diff_values(vals),
|
|
251
|
+
# Odoo will update the commands to include the {"id": corecord.id} in them
|
|
252
|
+
{
|
|
253
|
+
"o2m_ids": [
|
|
254
|
+
(
|
|
255
|
+
fields.Command.LINK,
|
|
256
|
+
o2m_corecords[1].id,
|
|
257
|
+
{"id": o2m_corecords[1].id},
|
|
258
|
+
)
|
|
259
|
+
],
|
|
260
|
+
"m2m_ids": [
|
|
261
|
+
(
|
|
262
|
+
fields.Command.LINK,
|
|
263
|
+
m2m_corecords[0].id,
|
|
264
|
+
{"id": m2m_corecords[0].id},
|
|
265
|
+
)
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
record.write(vals) # Do the real update => the diff should be empty now
|
|
270
|
+
self.assertEqual(record._get_write_diff_values(vals), {})
|
|
271
|
+
|
|
272
|
+
@mute_logger("bwd.test.model.log")
|
|
273
|
+
def test_15_get_write_diff_values_x2many_command_clear(self):
|
|
274
|
+
"""Test ``_get_write_diff_values()`` on fields.Command.clear()
|
|
275
|
+
|
|
276
|
+
``_get_write_diff_values()`` returns only the subset of IDs that should be
|
|
277
|
+
deleted/unlinked
|
|
278
|
+
"""
|
|
279
|
+
record = self._create_records()
|
|
280
|
+
# Create and assign 2 corecords to each X2M field
|
|
281
|
+
record.o2m_ids = o2m_corecords = self._create_records(2)
|
|
282
|
+
record.m2m_ids = m2m_corecords = self._create_records(2)
|
|
283
|
+
# Set vals to clear each X2M field
|
|
284
|
+
vals = {
|
|
285
|
+
"o2m_ids": [fields.Command.clear()],
|
|
286
|
+
"m2m_ids": [fields.Command.clear()],
|
|
287
|
+
}
|
|
288
|
+
self.assertEqual(
|
|
289
|
+
record._get_write_diff_values(vals),
|
|
290
|
+
# Odoo assigns command "delete" or "unlink" according to the field type
|
|
291
|
+
# and its definition (not important for our purposes here)
|
|
292
|
+
{
|
|
293
|
+
"o2m_ids": [fields.Command.delete(i) for i in o2m_corecords.ids],
|
|
294
|
+
"m2m_ids": [fields.Command.unlink(i) for i in m2m_corecords.ids],
|
|
295
|
+
},
|
|
296
|
+
)
|
|
297
|
+
record.write(vals) # Do the real update => the diff should be empty now
|
|
298
|
+
self.assertEqual(record._get_write_diff_values(vals), {})
|
|
299
|
+
|
|
300
|
+
@mute_logger("bwd.test.model.log")
|
|
301
|
+
def test_16_get_write_diff_values_x2many_command_set(self):
|
|
302
|
+
"""Test ``_get_write_diff_values()`` on fields.Command.set()
|
|
303
|
+
|
|
304
|
+
``_get_write_diff_values()`` behavior depends on various cases
|
|
305
|
+
"""
|
|
306
|
+
record = self._create_records()
|
|
307
|
+
# Create 3 corecords for each X2M field
|
|
308
|
+
o2m_corecords = self._create_records(3)
|
|
309
|
+
m2m_corecords = self._create_records(3)
|
|
310
|
+
|
|
311
|
+
# Case 1:
|
|
312
|
+
# - X2M fields contain no corecords
|
|
313
|
+
# - we want to assign them some corecords
|
|
314
|
+
# => ``_get_write_diff_values()`` should return a ``fields.Command.link()``
|
|
315
|
+
# command for each corecord to add
|
|
316
|
+
self.assertEqual(
|
|
317
|
+
record._get_write_diff_values(
|
|
318
|
+
{
|
|
319
|
+
"o2m_ids": [fields.Command.set(o2m_corecords.ids)],
|
|
320
|
+
"m2m_ids": [fields.Command.set(m2m_corecords.ids)],
|
|
321
|
+
},
|
|
322
|
+
),
|
|
323
|
+
# Odoo will update the commands to "link", and it will add the
|
|
324
|
+
# {"id": corecord.id} in them
|
|
325
|
+
{
|
|
326
|
+
"o2m_ids": [
|
|
327
|
+
(fields.Command.LINK, i, {"id": i}) for i in o2m_corecords.ids
|
|
328
|
+
],
|
|
329
|
+
"m2m_ids": [
|
|
330
|
+
(fields.Command.LINK, i, {"id": i}) for i in m2m_corecords.ids
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Case 2:
|
|
336
|
+
# - X2M fields contain some corecords
|
|
337
|
+
# - we want to replace them with different corecords
|
|
338
|
+
# => ``_get_write_diff_values()`` should return a
|
|
339
|
+
# ``fields.Command.[delete|unlink]()`` command for each corecord to remove,
|
|
340
|
+
# and a ``fields.Command.link()`` command for each corecord to add
|
|
341
|
+
record.o2m_ids = o2m_corecords[:1]
|
|
342
|
+
record.m2m_ids = m2m_corecords[:2]
|
|
343
|
+
self.assertEqual(
|
|
344
|
+
record._get_write_diff_values(
|
|
345
|
+
{
|
|
346
|
+
"o2m_ids": [fields.Command.set(o2m_corecords[1:].ids)],
|
|
347
|
+
"m2m_ids": [fields.Command.set(m2m_corecords[2:].ids)],
|
|
348
|
+
},
|
|
349
|
+
),
|
|
350
|
+
# Odoo will update the commands to "unlink", "delete" and "link" (with the
|
|
351
|
+
# {"id": corecord.id} in the "link" ones)
|
|
352
|
+
{
|
|
353
|
+
"o2m_ids": [
|
|
354
|
+
(fields.Command.DELETE, i, 0) for i in o2m_corecords[:1].ids
|
|
355
|
+
]
|
|
356
|
+
+ [(fields.Command.LINK, i, {"id": i}) for i in o2m_corecords[1:].ids],
|
|
357
|
+
"m2m_ids": [
|
|
358
|
+
(fields.Command.UNLINK, i, 0) for i in m2m_corecords[:2].ids
|
|
359
|
+
]
|
|
360
|
+
+ [(fields.Command.LINK, i, {"id": i}) for i in m2m_corecords[2:].ids],
|
|
361
|
+
},
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Case 3:
|
|
365
|
+
# - X2M fields contain some corecords
|
|
366
|
+
# - we want to reassign the same corecords
|
|
367
|
+
# => ``_get_write_diff_values()`` should return nothing
|
|
368
|
+
record.o2m_ids = o2m_corecords
|
|
369
|
+
record.m2m_ids = m2m_corecords
|
|
370
|
+
self.assertEqual(
|
|
371
|
+
record._get_write_diff_values(
|
|
372
|
+
{
|
|
373
|
+
"o2m_ids": [fields.Command.set(o2m_corecords.ids)],
|
|
374
|
+
"m2m_ids": [fields.Command.set(m2m_corecords.ids)],
|
|
375
|
+
},
|
|
376
|
+
),
|
|
377
|
+
{},
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Case 4:
|
|
381
|
+
# - X2M fields contain some corecords
|
|
382
|
+
# - we want to remove all corecords
|
|
383
|
+
# => ``_get_write_diff_values()`` should return a
|
|
384
|
+
# ``fields.Command.[delete|unlink]()`` command for each linked corecord
|
|
385
|
+
self.assertEqual(
|
|
386
|
+
record._get_write_diff_values(
|
|
387
|
+
{
|
|
388
|
+
"o2m_ids": [fields.Command.set([])],
|
|
389
|
+
"m2m_ids": [fields.Command.set([])],
|
|
390
|
+
},
|
|
391
|
+
),
|
|
392
|
+
# Odoo will update the commands to "unlink" and "delete"
|
|
393
|
+
{
|
|
394
|
+
"o2m_ids": [(fields.Command.DELETE, i, 0) for i in o2m_corecords.ids],
|
|
395
|
+
"m2m_ids": [(fields.Command.UNLINK, i, 0) for i in m2m_corecords.ids],
|
|
396
|
+
},
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# pylint: disable=W0104
|
|
400
|
+
def test_20_write_diff_computed_fields(self):
|
|
401
|
+
"""Checks cache behavior for computed fields when diff-writing their deps"""
|
|
402
|
+
# Prepare the record, its fields values and the cache
|
|
403
|
+
record = self._create_records()
|
|
404
|
+
vals = {"length": 5, "width": 3, "height": 2}
|
|
405
|
+
record.write(vals)
|
|
406
|
+
fnames = ("perimeter", "area", "volume")
|
|
407
|
+
for fname in fnames:
|
|
408
|
+
with mute_logger("bwd.test.model.log"):
|
|
409
|
+
record[fname] # Dummy read: set fields in cache
|
|
410
|
+
|
|
411
|
+
# Use ``write`` w/ the same values: Odoo will need to recompute the computed
|
|
412
|
+
# fields values as soon as they're read
|
|
413
|
+
record.write(vals)
|
|
414
|
+
for fname in fnames:
|
|
415
|
+
with self.assertLogs("bwd.test.model.log", level="WARNING"):
|
|
416
|
+
record[fname] # Dummy read: check the compute method is triggered
|
|
417
|
+
|
|
418
|
+
# Use ``write_diff`` w/ the same values: Odoo won't need to recompute the
|
|
419
|
+
# computed fields values
|
|
420
|
+
record.write_diff(vals)
|
|
421
|
+
for fname in fnames:
|
|
422
|
+
with self.assertNoLogs("bwd.test.model.log", level="WARNING"):
|
|
423
|
+
record[fname] # Dummy read: check the compute method is not triggered
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: odoo-addon-base_write_diff
|
|
3
|
+
Version: 17.0.1.0.0.2
|
|
4
|
+
Requires-Python: >=3.10
|
|
5
|
+
Requires-Dist: odoo>=17.0a,<17.1dev
|
|
6
|
+
Summary: Prevents updates on fields whose values won't change anyway
|
|
7
|
+
Home-page: https://github.com/OCA/server-tools
|
|
8
|
+
License: AGPL-3
|
|
9
|
+
Author: Camptocamp, Odoo Community Association (OCA)
|
|
10
|
+
Author-email: support@odoo-community.org
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Framework :: Odoo
|
|
13
|
+
Classifier: Framework :: Odoo :: 17.0
|
|
14
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
15
|
+
Description-Content-Type: text/x-rst
|
|
16
|
+
|
|
17
|
+
.. image:: https://odoo-community.org/readme-banner-image
|
|
18
|
+
:target: https://odoo-community.org/get-involved?utm_source=readme
|
|
19
|
+
:alt: Odoo Community Association
|
|
20
|
+
|
|
21
|
+
=================
|
|
22
|
+
Base - Write Diff
|
|
23
|
+
=================
|
|
24
|
+
|
|
25
|
+
..
|
|
26
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
27
|
+
!! This file is generated by oca-gen-addon-readme !!
|
|
28
|
+
!! changes will be overwritten. !!
|
|
29
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
30
|
+
!! source digest: sha256:71745a37619c24019b67f8558eb6c7be1b974ad0031b6ce40892e7eec305bcb3
|
|
31
|
+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
32
|
+
|
|
33
|
+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
|
34
|
+
:target: https://odoo-community.org/page/development-status
|
|
35
|
+
:alt: Beta
|
|
36
|
+
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
|
|
37
|
+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
|
38
|
+
:alt: License: AGPL-3
|
|
39
|
+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github
|
|
40
|
+
:target: https://github.com/OCA/server-tools/tree/17.0/base_write_diff
|
|
41
|
+
:alt: OCA/server-tools
|
|
42
|
+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
|
43
|
+
:target: https://translation.odoo-community.org/projects/server-tools-17-0/server-tools-17-0-base_write_diff
|
|
44
|
+
:alt: Translate me on Weblate
|
|
45
|
+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
|
46
|
+
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=17.0
|
|
47
|
+
:alt: Try me on Runboat
|
|
48
|
+
|
|
49
|
+
|badge1| |badge2| |badge3| |badge4| |badge5|
|
|
50
|
+
|
|
51
|
+
This module allows filtering values to update on records according to
|
|
52
|
+
whether they are actually different from the records' current values.
|
|
53
|
+
|
|
54
|
+
**Table of contents**
|
|
55
|
+
|
|
56
|
+
.. contents::
|
|
57
|
+
:local:
|
|
58
|
+
|
|
59
|
+
Usage
|
|
60
|
+
=====
|
|
61
|
+
|
|
62
|
+
**Summary**
|
|
63
|
+
|
|
64
|
+
This module allows you to update records by filtering out fields whose
|
|
65
|
+
values are going to be left unchanged by ``BaseModel.write()``; for
|
|
66
|
+
example, let's assume you have:
|
|
67
|
+
|
|
68
|
+
.. code:: python
|
|
69
|
+
|
|
70
|
+
>>> self
|
|
71
|
+
sale.order.line(1,)
|
|
72
|
+
>>> self.price_unit
|
|
73
|
+
10.00
|
|
74
|
+
|
|
75
|
+
If you use ``self.write({"price_unit": 10.00})`` or
|
|
76
|
+
``self.price_unit = 10.00``, Odoo may end up executing unnecessary
|
|
77
|
+
operations, like triggering the update on the field, recompute computed
|
|
78
|
+
fields that depend on ``price_unit``, and so on, even if the value is
|
|
79
|
+
actually unchanged.
|
|
80
|
+
|
|
81
|
+
By using this module, you can prevent all of that.
|
|
82
|
+
|
|
83
|
+
You can use this module in 3 different ways. All of them require you to
|
|
84
|
+
add this module as a dependency of your module.
|
|
85
|
+
|
|
86
|
+
**1 - Context key ``"write_use_diff_values"``**
|
|
87
|
+
|
|
88
|
+
By adding ``write_use_diff_values=True`` to the context when updating a
|
|
89
|
+
field value, the ``BaseModel.write()`` patch will take care of filtering
|
|
90
|
+
out the fields' values that are the same as the record's current ones.
|
|
91
|
+
|
|
92
|
+
⚠️ Beware: the context key is propagated down to other ``write()`` calls
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
|
|
96
|
+
.. code:: python
|
|
97
|
+
|
|
98
|
+
from odoo import models
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ProductTemplate(models.Model):
|
|
102
|
+
_inherit = "product.template"
|
|
103
|
+
|
|
104
|
+
def write(self, vals):
|
|
105
|
+
# Update only fields that are actually different
|
|
106
|
+
self = self.with_context(write_use_diff_values=True)
|
|
107
|
+
return super().write(vals)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ProductProduct(models.Model):
|
|
111
|
+
_inherit = "product.product"
|
|
112
|
+
|
|
113
|
+
def update_code_if_necessary(self, code: str):
|
|
114
|
+
# Update ``default_code`` only if different from the current value
|
|
115
|
+
self.with_context(write_use_diff_values=True).default_code = code
|
|
116
|
+
|
|
117
|
+
**2 - Method ``BaseModel.write_diff()``**
|
|
118
|
+
|
|
119
|
+
It is the same as calling ``write()``, but it automatically enables the
|
|
120
|
+
``"write_use_diff_values"`` context flag: ``self.write_diff(vals)`` is a
|
|
121
|
+
shortcut for
|
|
122
|
+
``self.with_context(write_use_diff_values=True).write(vals)``
|
|
123
|
+
|
|
124
|
+
⚠️ Beware: the context key is propagated down to other ``write()`` calls
|
|
125
|
+
|
|
126
|
+
**3 - Method ``BaseModel._get_write_diff_values(vals)``**
|
|
127
|
+
|
|
128
|
+
This method accepts a write-like ``dict`` as param, and returns a new
|
|
129
|
+
``dict`` made of the fields who will actually update the record's
|
|
130
|
+
values. This allows for a more flexible and customizable behavior than
|
|
131
|
+
the context key usage, because:
|
|
132
|
+
|
|
133
|
+
- you'll be able to filter out specific fields, instead of filtering out
|
|
134
|
+
all the fields whose values won't be changed after the update;
|
|
135
|
+
- you'll be able to execute the filtering on specific models, instead of
|
|
136
|
+
executing it on all the models involved in the stack of ``write()``
|
|
137
|
+
calls from the first usage of the context key down to the base method
|
|
138
|
+
``BaseModel.write()``.
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
|
|
142
|
+
.. code:: python
|
|
143
|
+
|
|
144
|
+
from collections import defaultdict
|
|
145
|
+
|
|
146
|
+
from odoo import api, models
|
|
147
|
+
from odoo.tools.misc import frozendict
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ProductProduct(models.Model):
|
|
151
|
+
_inherit = "product.product"
|
|
152
|
+
|
|
153
|
+
def write(self, vals):
|
|
154
|
+
# OVERRIDE: ``odoo.addons.product.models.product_product.Product.write()``
|
|
155
|
+
# override will clear the whole registry cache if either 'active' or
|
|
156
|
+
# 'product_template_attribute_value_ids' are found in the ``vals`` dictionary:
|
|
157
|
+
# remove them unless it's necessary to update them
|
|
158
|
+
fnames = {"active", "product_template_attribute_value_ids"}
|
|
159
|
+
if vals_to_check := {f: vals.pop(f) for f in fnames.intersection(vals)}:
|
|
160
|
+
groups = defaultdict(lambda: self.browse())
|
|
161
|
+
for prod in self:
|
|
162
|
+
groups[frozendict(prod._get_write_diff_values(vals_to_check))] += prod
|
|
163
|
+
for diff_vals, prods in groups.items():
|
|
164
|
+
if res_vals := (vals | dict(diff_vals)):
|
|
165
|
+
super(ProductProduct, prods).write(res_vals)
|
|
166
|
+
return True
|
|
167
|
+
return super().write(vals)
|
|
168
|
+
|
|
169
|
+
Bug Tracker
|
|
170
|
+
===========
|
|
171
|
+
|
|
172
|
+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-tools/issues>`_.
|
|
173
|
+
In case of trouble, please check there if your issue has already been reported.
|
|
174
|
+
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
|
175
|
+
`feedback <https://github.com/OCA/server-tools/issues/new?body=module:%20base_write_diff%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
|
176
|
+
|
|
177
|
+
Do not contact contributors directly about support or help with technical issues.
|
|
178
|
+
|
|
179
|
+
Credits
|
|
180
|
+
=======
|
|
181
|
+
|
|
182
|
+
Authors
|
|
183
|
+
-------
|
|
184
|
+
|
|
185
|
+
* Camptocamp
|
|
186
|
+
|
|
187
|
+
Contributors
|
|
188
|
+
------------
|
|
189
|
+
|
|
190
|
+
- Silvio Gregorini <silvio.gregorini@camptocamp.com>
|
|
191
|
+
|
|
192
|
+
Maintainers
|
|
193
|
+
-----------
|
|
194
|
+
|
|
195
|
+
This module is maintained by the OCA.
|
|
196
|
+
|
|
197
|
+
.. image:: https://odoo-community.org/logo.png
|
|
198
|
+
:alt: Odoo Community Association
|
|
199
|
+
:target: https://odoo-community.org
|
|
200
|
+
|
|
201
|
+
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
|
202
|
+
mission is to support the collaborative development of Odoo features and
|
|
203
|
+
promote its widespread use.
|
|
204
|
+
|
|
205
|
+
This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/17.0/base_write_diff>`_ project on GitHub.
|
|
206
|
+
|
|
207
|
+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
odoo/addons/base_write_diff/README.rst,sha256=sZW-2soYGCfprm9XKjIgeeWjI2-9O6uOLgVcGZiZokk,6949
|
|
2
|
+
odoo/addons/base_write_diff/__init__.py,sha256=X9EJGOE2GtZbS0G82PtSXmWSZ_R8jEM0rlJTDliQjp4,21
|
|
3
|
+
odoo/addons/base_write_diff/__manifest__.py,sha256=wlJDjQpcjcz5Rbue3P_BXHS8s39-H-VLgkrSWxy4mYs,464
|
|
4
|
+
odoo/addons/base_write_diff/i18n/base_write_diff.pot,sha256=G8ybp7izelJrFwRs4Dq3QeOjXqNSQD0WYhZeX8e86OI,463
|
|
5
|
+
odoo/addons/base_write_diff/models/__init__.py,sha256=9pIQnc7H3xgVNpi7n3I18hg04S4IkX5mnAfl9cv7RBE,19
|
|
6
|
+
odoo/addons/base_write_diff/models/base.py,sha256=m_BvhzFy-Z1hFMe4mtI0g5za-WVXhRiy_CBRmG-DIVY,6022
|
|
7
|
+
odoo/addons/base_write_diff/readme/CONTRIBUTORS.md,sha256=I2EZz-IBsF_bZyhR1U8QoNZmB-m5qd7CGXRRRT0tkQE,57
|
|
8
|
+
odoo/addons/base_write_diff/readme/DESCRIPTION.md,sha256=KGpIKiOVAfAVofJx1WzWT69_Bge7AifVh4LvMG5n1GI,140
|
|
9
|
+
odoo/addons/base_write_diff/readme/USAGE.md,sha256=nRas9M95YRiVXh6FVUvTJ60YMBB6Fh2jhwBqvhM-2q8,3742
|
|
10
|
+
odoo/addons/base_write_diff/static/description/icon.png,sha256=CgnOEZCwoe6f1vlLwkqFVfc2q_uwBMU0UnXN8j6X5ag,10254
|
|
11
|
+
odoo/addons/base_write_diff/static/description/index.html,sha256=BPyYUioBiOJvIMe358jde59Xj4p-9jwJrQrrRoT3Eho,23403
|
|
12
|
+
odoo/addons/base_write_diff/tests/__init__.py,sha256=54LbOvo7gqDhCdt6fz1qGm7h946smek_vCcKGC1s-UI,35
|
|
13
|
+
odoo/addons/base_write_diff/tests/test_base_write_diff.py,sha256=n94okgQBw0-HGR-F8jWD0hKVZTN69605U-lhCucI7iY,18176
|
|
14
|
+
odoo_addon_base_write_diff-17.0.1.0.0.2.dist-info/METADATA,sha256=WfkSpbaPsvQqbqemPY5zi5mAVobMxr4OyYOpXkJlgJ4,7537
|
|
15
|
+
odoo_addon_base_write_diff-17.0.1.0.0.2.dist-info/WHEEL,sha256=ZhOvUsYhy81Dx67gN3TV0RchQWBIIzutDZaJODDg2Vo,81
|
|
16
|
+
odoo_addon_base_write_diff-17.0.1.0.0.2.dist-info/top_level.txt,sha256=QE6RBQ0QX5f4eFuUcGgU5Kbq1A_qJcDs-e_vpr6pmfU,4
|
|
17
|
+
odoo_addon_base_write_diff-17.0.1.0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
odoo
|