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.
@@ -0,0 +1,191 @@
1
+ .. image:: https://odoo-community.org/readme-banner-image
2
+ :target: https://odoo-community.org/get-involved?utm_source=readme
3
+ :alt: Odoo Community Association
4
+
5
+ =================
6
+ Base - Write Diff
7
+ =================
8
+
9
+ ..
10
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
11
+ !! This file is generated by oca-gen-addon-readme !!
12
+ !! changes will be overwritten. !!
13
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
14
+ !! source digest: sha256:71745a37619c24019b67f8558eb6c7be1b974ad0031b6ce40892e7eec305bcb3
15
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
16
+
17
+ .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
18
+ :target: https://odoo-community.org/page/development-status
19
+ :alt: Beta
20
+ .. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
21
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
22
+ :alt: License: AGPL-3
23
+ .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github
24
+ :target: https://github.com/OCA/server-tools/tree/17.0/base_write_diff
25
+ :alt: OCA/server-tools
26
+ .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
27
+ :target: https://translation.odoo-community.org/projects/server-tools-17-0/server-tools-17-0-base_write_diff
28
+ :alt: Translate me on Weblate
29
+ .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
30
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=17.0
31
+ :alt: Try me on Runboat
32
+
33
+ |badge1| |badge2| |badge3| |badge4| |badge5|
34
+
35
+ This module allows filtering values to update on records according to
36
+ whether they are actually different from the records' current values.
37
+
38
+ **Table of contents**
39
+
40
+ .. contents::
41
+ :local:
42
+
43
+ Usage
44
+ =====
45
+
46
+ **Summary**
47
+
48
+ This module allows you to update records by filtering out fields whose
49
+ values are going to be left unchanged by ``BaseModel.write()``; for
50
+ example, let's assume you have:
51
+
52
+ .. code:: python
53
+
54
+ >>> self
55
+ sale.order.line(1,)
56
+ >>> self.price_unit
57
+ 10.00
58
+
59
+ If you use ``self.write({"price_unit": 10.00})`` or
60
+ ``self.price_unit = 10.00``, Odoo may end up executing unnecessary
61
+ operations, like triggering the update on the field, recompute computed
62
+ fields that depend on ``price_unit``, and so on, even if the value is
63
+ actually unchanged.
64
+
65
+ By using this module, you can prevent all of that.
66
+
67
+ You can use this module in 3 different ways. All of them require you to
68
+ add this module as a dependency of your module.
69
+
70
+ **1 - Context key ``"write_use_diff_values"``**
71
+
72
+ By adding ``write_use_diff_values=True`` to the context when updating a
73
+ field value, the ``BaseModel.write()`` patch will take care of filtering
74
+ out the fields' values that are the same as the record's current ones.
75
+
76
+ ⚠️ Beware: the context key is propagated down to other ``write()`` calls
77
+
78
+ Example:
79
+
80
+ .. code:: python
81
+
82
+ from odoo import models
83
+
84
+
85
+ class ProductTemplate(models.Model):
86
+ _inherit = "product.template"
87
+
88
+ def write(self, vals):
89
+ # Update only fields that are actually different
90
+ self = self.with_context(write_use_diff_values=True)
91
+ return super().write(vals)
92
+
93
+
94
+ class ProductProduct(models.Model):
95
+ _inherit = "product.product"
96
+
97
+ def update_code_if_necessary(self, code: str):
98
+ # Update ``default_code`` only if different from the current value
99
+ self.with_context(write_use_diff_values=True).default_code = code
100
+
101
+ **2 - Method ``BaseModel.write_diff()``**
102
+
103
+ It is the same as calling ``write()``, but it automatically enables the
104
+ ``"write_use_diff_values"`` context flag: ``self.write_diff(vals)`` is a
105
+ shortcut for
106
+ ``self.with_context(write_use_diff_values=True).write(vals)``
107
+
108
+ ⚠️ Beware: the context key is propagated down to other ``write()`` calls
109
+
110
+ **3 - Method ``BaseModel._get_write_diff_values(vals)``**
111
+
112
+ This method accepts a write-like ``dict`` as param, and returns a new
113
+ ``dict`` made of the fields who will actually update the record's
114
+ values. This allows for a more flexible and customizable behavior than
115
+ the context key usage, because:
116
+
117
+ - you'll be able to filter out specific fields, instead of filtering out
118
+ all the fields whose values won't be changed after the update;
119
+ - you'll be able to execute the filtering on specific models, instead of
120
+ executing it on all the models involved in the stack of ``write()``
121
+ calls from the first usage of the context key down to the base method
122
+ ``BaseModel.write()``.
123
+
124
+ Example:
125
+
126
+ .. code:: python
127
+
128
+ from collections import defaultdict
129
+
130
+ from odoo import api, models
131
+ from odoo.tools.misc import frozendict
132
+
133
+
134
+ class ProductProduct(models.Model):
135
+ _inherit = "product.product"
136
+
137
+ def write(self, vals):
138
+ # OVERRIDE: ``odoo.addons.product.models.product_product.Product.write()``
139
+ # override will clear the whole registry cache if either 'active' or
140
+ # 'product_template_attribute_value_ids' are found in the ``vals`` dictionary:
141
+ # remove them unless it's necessary to update them
142
+ fnames = {"active", "product_template_attribute_value_ids"}
143
+ if vals_to_check := {f: vals.pop(f) for f in fnames.intersection(vals)}:
144
+ groups = defaultdict(lambda: self.browse())
145
+ for prod in self:
146
+ groups[frozendict(prod._get_write_diff_values(vals_to_check))] += prod
147
+ for diff_vals, prods in groups.items():
148
+ if res_vals := (vals | dict(diff_vals)):
149
+ super(ProductProduct, prods).write(res_vals)
150
+ return True
151
+ return super().write(vals)
152
+
153
+ Bug Tracker
154
+ ===========
155
+
156
+ Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-tools/issues>`_.
157
+ In case of trouble, please check there if your issue has already been reported.
158
+ If you spotted it first, help us to smash it by providing a detailed and welcomed
159
+ `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**>`_.
160
+
161
+ Do not contact contributors directly about support or help with technical issues.
162
+
163
+ Credits
164
+ =======
165
+
166
+ Authors
167
+ -------
168
+
169
+ * Camptocamp
170
+
171
+ Contributors
172
+ ------------
173
+
174
+ - Silvio Gregorini <silvio.gregorini@camptocamp.com>
175
+
176
+ Maintainers
177
+ -----------
178
+
179
+ This module is maintained by the OCA.
180
+
181
+ .. image:: https://odoo-community.org/logo.png
182
+ :alt: Odoo Community Association
183
+ :target: https://odoo-community.org
184
+
185
+ OCA, or the Odoo Community Association, is a nonprofit organization whose
186
+ mission is to support the collaborative development of Odoo features and
187
+ promote its widespread use.
188
+
189
+ This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/17.0/base_write_diff>`_ project on GitHub.
190
+
191
+ You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
@@ -0,0 +1 @@
1
+ from . import models
@@ -0,0 +1,14 @@
1
+ # Copyright 2025 Camptocamp SA
2
+ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3
+
4
+ {
5
+ "name": "Base - Write Diff",
6
+ "summary": "Prevents updates on fields whose values won't change anyway",
7
+ "version": "17.0.1.0.0",
8
+ "author": "Camptocamp, Odoo Community Association (OCA)",
9
+ "license": "AGPL-3",
10
+ "category": "Hidden",
11
+ "website": "https://github.com/OCA/server-tools",
12
+ "installable": True,
13
+ "depends": ["base", "web"],
14
+ }
@@ -0,0 +1,19 @@
1
+ # Translation of Odoo Server.
2
+ # This file contains the translation of the following modules:
3
+ # * base_write_diff
4
+ #
5
+ msgid ""
6
+ msgstr ""
7
+ "Project-Id-Version: Odoo Server 17.0\n"
8
+ "Report-Msgid-Bugs-To: \n"
9
+ "Last-Translator: \n"
10
+ "Language-Team: \n"
11
+ "MIME-Version: 1.0\n"
12
+ "Content-Type: text/plain; charset=UTF-8\n"
13
+ "Content-Transfer-Encoding: \n"
14
+ "Plural-Forms: \n"
15
+
16
+ #. module: base_write_diff
17
+ #: model:ir.model,name:base_write_diff.model_base
18
+ msgid "Base"
19
+ msgstr ""
@@ -0,0 +1 @@
1
+ from . import base
@@ -0,0 +1,117 @@
1
+ # Copyright 2025 Camptocamp SA (https://www.camptocamp.com).
2
+ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3
+
4
+ from collections import defaultdict
5
+
6
+ from odoo import Command, models
7
+ from odoo.tools.misc import frozendict
8
+
9
+ from odoo.addons.web.models.models import RecordSnapshot
10
+
11
+
12
+ class BaseModel(models.BaseModel):
13
+ _inherit = "base"
14
+
15
+ def write(self, vals):
16
+ # OVERRIDE: when using the ``write_use_diff_values`` context key, remove values
17
+ # that won't be changed before/after the ``write()`` itself.
18
+ # If ``write()`` is called on an empty recordset or with no value, ignore
19
+ # everything and shortcut to ``super()``.
20
+ if not (self and vals and self.env.context.get("write_use_diff_values")):
21
+ return super().write(vals)
22
+ recs_by_vals = defaultdict(lambda: self.browse())
23
+ for rec in self:
24
+ recs_by_vals[frozendict(rec._get_write_diff_values(vals))] += rec
25
+ for rec_vals, recs in recs_by_vals.items():
26
+ if rec_vals: # Don't trigger ``write()`` if there is nothing to update
27
+ super(BaseModel, recs).write(dict(rec_vals))
28
+ return True
29
+
30
+ def write_diff(self, vals: dict) -> bool:
31
+ """Executes a ``write()`` only on fields that actually need to be updated"""
32
+ return self.with_context(write_use_diff_values=True).write(vals)
33
+
34
+ def _get_write_diff_values(self, vals: dict) -> dict:
35
+ """Compares record values with the values to write
36
+
37
+ Returns a dictionary containing only the fields that actually needs to be
38
+ updated on ``self``, filtering out those which contain a value that is the same
39
+ as the current record's field value.
40
+ For example:
41
+ >>> self.name = "A"
42
+ >>> self.code = "a"
43
+ >>> self._get_write_diff_values({"name": "A", "code": "a"})
44
+ {}
45
+ >>> self._get_write_diff_values({"name": "B", "code": "a"})
46
+ {"name": "B"}
47
+ >>> self._get_write_diff_values({"name": "B", "code": "b"})
48
+ {"name": "B", "code": "b"}
49
+ """
50
+ self.ensure_one()
51
+ diff_values = {}
52
+
53
+ # Step 1: group fields according to whether they're multi-relational or not
54
+ x2many_fields_values, simple_fields_values = {}, {}
55
+ for fname, fvalue in vals.items():
56
+ if self._fields[fname].type in ("one2many", "many2many"):
57
+ x2many_fields_values[fname] = fvalue
58
+ else:
59
+ simple_fields_values[fname] = fvalue
60
+
61
+ # Step 2: prepare fields to update by checking simple fields first
62
+ if simple_fields_values:
63
+ simple_fields_specs = {f: {} for f in simple_fields_values}
64
+ snapshot0 = self._do_snapshot({}, simple_fields_specs)
65
+ snapshot1 = self._do_snapshot(simple_fields_values, simple_fields_specs)
66
+ diff_values.update(snapshot1.diff(snapshot0))
67
+
68
+ # Step 3: prepare fields to update by checking multi-relational fields
69
+ # For each multi-relational field, prepare a new list of values by checking
70
+ # the original commands:
71
+ # - if it's an update command, check whether something actually changes on
72
+ # the corecord by calling ``_get_write_diff_values()`` recursively
73
+ # - else, add the original command to the new list: all commands except "update"
74
+ # will modify the record-corecords relation by creating/[un]linking/deleting
75
+ # corecords
76
+ # Then, check the new list of values to decide if the field needs updating:
77
+ # - at least 1 creation/update => add the full list of commands for simplicity
78
+ # - else => check whether the new values will effectively change the
79
+ # record-corecords relationship
80
+ for fname, fvalues in x2many_fields_values.items():
81
+ # Prepare the new list of commands/values according to the original command
82
+ new_fvalues = []
83
+ for fvalue in fvalues:
84
+ if fvalue[0] == Command.UPDATE:
85
+ cmd, corec_id, corec_vals = fvalue
86
+ corec = self.env[self._fields[fname].comodel_name].browse(corec_id)
87
+ if corec_diff_vals := corec._get_write_diff_values(corec_vals):
88
+ new_fvalues.append((cmd, corec_id, corec_diff_vals))
89
+ else:
90
+ new_fvalues.append(fvalue)
91
+ # Check whether we actually need to include the new list in the diff values
92
+ if any(v[0] in (Command.CREATE, Command.UPDATE) for v in new_fvalues):
93
+ diff_values[fname] = new_fvalues
94
+ else:
95
+ x2many_snapshot0 = self._do_snapshot({}, {fname: {}})
96
+ x2many_snapshot1 = self._do_snapshot({fname: new_fvalues}, {fname: {}})
97
+ if x2many_diff_values := x2many_snapshot1.diff(x2many_snapshot0):
98
+ diff_values.update(x2many_diff_values)
99
+
100
+ return diff_values
101
+
102
+ def _do_snapshot(self, vals: dict, specs: dict) -> "RecordSnapshot":
103
+ """Prepares a ``RecordSnapshot`` object with the specified params"""
104
+ self.ensure_one()
105
+ # Align ``vals`` and ``specs`` to make sure they both contain the same fields:
106
+ # - if a field in ``specs`` is missing from ``vals``, we read its current value
107
+ # from the record and convert it to a ``write()``-able format to prevent cache
108
+ # issues and inconsistencies
109
+ # - if a field in ``vals`` is missing from ``specs``, we add it with the default
110
+ # value of ``{}`` to allow ``RecordSnapshot`` to handle it properly
111
+ vals_fnames_not_in_specs = set(vals) - set(specs)
112
+ specs_fnames_not_in_vals = set(specs) - set(vals)
113
+ for fname in vals_fnames_not_in_specs:
114
+ specs[fname] = {}
115
+ for fname in specs_fnames_not_in_vals:
116
+ vals[fname] = self._fields[fname].convert_to_write(self[fname], self)
117
+ return RecordSnapshot(self.new(values=vals, origin=self), fields_spec=specs)
@@ -0,0 +1 @@
1
+ - Silvio Gregorini \<<silvio.gregorini@camptocamp.com>\>
@@ -0,0 +1,2 @@
1
+ This module allows filtering values to update on records according to whether they are
2
+ actually different from the records' current values.
@@ -0,0 +1,101 @@
1
+ **Summary**
2
+
3
+ This module allows you to update records by filtering out fields whose values are going
4
+ to be left unchanged by ``BaseModel.write()``; for example, let's assume you have:
5
+
6
+ ```python
7
+ >>> self
8
+ sale.order.line(1,)
9
+ >>> self.price_unit
10
+ 10.00
11
+ ```
12
+
13
+ If you use ``self.write({"price_unit": 10.00})`` or ``self.price_unit = 10.00``, Odoo
14
+ may end up executing unnecessary operations, like triggering the update on the field,
15
+ recompute computed fields that depend on ``price_unit``, and so on, even if the value
16
+ is actually unchanged.
17
+
18
+ By using this module, you can prevent all of that.
19
+
20
+ You can use this module in 3 different ways. All of them require you to add this module
21
+ as a dependency of your module.
22
+
23
+ **1 - Context key ``"write_use_diff_values"``**
24
+
25
+ By adding ``write_use_diff_values=True`` to the context when updating a field value,
26
+ the ``BaseModel.write()`` patch will take care of filtering out the fields' values
27
+ that are the same as the record's current ones.
28
+
29
+ ⚠️ Beware: the context key is propagated down to other ``write()`` calls
30
+
31
+ Example:
32
+
33
+ ```python
34
+ from odoo import models
35
+
36
+
37
+ class ProductTemplate(models.Model):
38
+ _inherit = "product.template"
39
+
40
+ def write(self, vals):
41
+ # Update only fields that are actually different
42
+ self = self.with_context(write_use_diff_values=True)
43
+ return super().write(vals)
44
+
45
+
46
+ class ProductProduct(models.Model):
47
+ _inherit = "product.product"
48
+
49
+ def update_code_if_necessary(self, code: str):
50
+ # Update ``default_code`` only if different from the current value
51
+ self.with_context(write_use_diff_values=True).default_code = code
52
+ ```
53
+
54
+ **2 - Method ``BaseModel.write_diff()``**
55
+
56
+ It is the same as calling ``write()``, but it automatically enables the
57
+ ``"write_use_diff_values"`` context flag: ``self.write_diff(vals)`` is a shortcut for
58
+ ``self.with_context(write_use_diff_values=True).write(vals)``
59
+
60
+ ⚠️ Beware: the context key is propagated down to other ``write()`` calls
61
+
62
+ **3 - Method ``BaseModel._get_write_diff_values(vals)``**
63
+
64
+ This method accepts a write-like ``dict`` as param, and returns a new ``dict`` made of
65
+ the fields who will actually update the record's values. This allows for a more
66
+ flexible and customizable behavior than the context key usage, because:
67
+
68
+ - you'll be able to filter out specific fields, instead of filtering out all the fields
69
+ whose values won't be changed after the update;
70
+ - you'll be able to execute the filtering on specific models, instead of executing it
71
+ on all the models involved in the stack of ``write()`` calls from the first usage of
72
+ the context key down to the base method ``BaseModel.write()``.
73
+
74
+ Example:
75
+
76
+ ```python
77
+ from collections import defaultdict
78
+
79
+ from odoo import api, models
80
+ from odoo.tools.misc import frozendict
81
+
82
+
83
+ class ProductProduct(models.Model):
84
+ _inherit = "product.product"
85
+
86
+ def write(self, vals):
87
+ # OVERRIDE: ``odoo.addons.product.models.product_product.Product.write()``
88
+ # override will clear the whole registry cache if either 'active' or
89
+ # 'product_template_attribute_value_ids' are found in the ``vals`` dictionary:
90
+ # remove them unless it's necessary to update them
91
+ fnames = {"active", "product_template_attribute_value_ids"}
92
+ if vals_to_check := {f: vals.pop(f) for f in fnames.intersection(vals)}:
93
+ groups = defaultdict(lambda: self.browse())
94
+ for prod in self:
95
+ groups[frozendict(prod._get_write_diff_values(vals_to_check))] += prod
96
+ for diff_vals, prods in groups.items():
97
+ if res_vals := (vals | dict(diff_vals)):
98
+ super(ProductProduct, prods).write(res_vals)
99
+ return True
100
+ return super().write(vals)
101
+ ```