Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions base_write_diff/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
=================
Base - Write Diff
=================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:71745a37619c24019b67f8558eb6c7be1b974ad0031b6ce40892e7eec305bcb3
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github
:target: https://github.com/OCA/server-tools/tree/18.0/base_write_diff
:alt: OCA/server-tools
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/server-tools-18-0/server-tools-18-0-base_write_diff
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=18.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module allows filtering values to update on records according to
whether they are actually different from the records' current values.

**Table of contents**

.. contents::
:local:

Usage
=====

**Summary**

This module allows you to update records by filtering out fields whose
values are going to be left unchanged by ``BaseModel.write()``; for
example, let's assume you have:

.. code:: python

>>> self
sale.order.line(1,)
>>> self.price_unit
10.00

If you use ``self.write({"price_unit": 10.00})`` or
``self.price_unit = 10.00``, Odoo may end up executing unnecessary
operations, like triggering the update on the field, recompute computed
fields that depend on ``price_unit``, and so on, even if the value is
actually unchanged.

By using this module, you can prevent all of that.

You can use this module in 3 different ways. All of them require you to
add this module as a dependency of your module.

**1 - Context key ``"write_use_diff_values"``**

By adding ``write_use_diff_values=True`` to the context when updating a
field value, the ``BaseModel.write()`` patch will take care of filtering
out the fields' values that are the same as the record's current ones.

⚠️ Beware: the context key is propagated down to other ``write()`` calls

Example:

.. code:: python

from odoo import models


class ProductTemplate(models.Model):
_inherit = "product.template"

def write(self, vals):
# Update only fields that are actually different
self = self.with_context(write_use_diff_values=True)
return super().write(vals)


class ProductProduct(models.Model):
_inherit = "product.product"

def update_code_if_necessary(self, code: str):
# Update ``default_code`` only if different from the current value
self.with_context(write_use_diff_values=True).default_code = code

**2 - Method ``BaseModel.write_diff()``**

It is the same as calling ``write()``, but it automatically enables the
``"write_use_diff_values"`` context flag: ``self.write_diff(vals)`` is a
shortcut for
``self.with_context(write_use_diff_values=True).write(vals)``

⚠️ Beware: the context key is propagated down to other ``write()`` calls

**3 - Method ``BaseModel._get_write_diff_values(vals)``**

This method accepts a write-like ``dict`` as param, and returns a new
``dict`` made of the fields who will actually update the record's
values. This allows for a more flexible and customizable behavior than
the context key usage, because:

- you'll be able to filter out specific fields, instead of filtering out
all the fields whose values won't be changed after the update;
- you'll be able to execute the filtering on specific models, instead of
executing it on all the models involved in the stack of ``write()``
calls from the first usage of the context key down to the base method
``BaseModel.write()``.

Example:

.. code:: python

from collections import defaultdict

from odoo import api, models
from odoo.tools.misc import frozendict


class ProductProduct(models.Model):
_inherit = "product.product"

def write(self, vals):
# OVERRIDE: ``odoo.addons.product.models.product_product.Product.write()``
# override will clear the whole registry cache if either 'active' or
# 'product_template_attribute_value_ids' are found in the ``vals`` dictionary:
# remove them unless it's necessary to update them
fnames = {"active", "product_template_attribute_value_ids"}
if vals_to_check := {f: vals.pop(f) for f in fnames.intersection(vals)}:
groups = defaultdict(lambda: self.browse())
for prod in self:
groups[frozendict(prod._get_write_diff_values(vals_to_check))] += prod
for diff_vals, prods in groups.items():
if res_vals := (vals | dict(diff_vals)):
super(ProductProduct, prods).write(res_vals)
return True
return super().write(vals)

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-tools/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/server-tools/issues/new?body=module:%20base_write_diff%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* Camptocamp

Contributors
------------

- Silvio Gregorini <silvio.gregorini@camptocamp.com>

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/18.0/base_write_diff>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions base_write_diff/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
14 changes: 14 additions & 0 deletions base_write_diff/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2025 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

{
"name": "Base - Write Diff",
"summary": "Prevents updates on fields whose values won't change anyway",
"version": "18.0.1.0.0",
"author": "Camptocamp, Odoo Community Association (OCA)",
"license": "AGPL-3",
"category": "Hidden",
"website": "https://github.com/OCA/server-tools",
"installable": True,
"depends": ["base", "web"],
}
19 changes: 19 additions & 0 deletions base_write_diff/i18n/base_write_diff.pot
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * base_write_diff
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"

#. module: base_write_diff
#: model:ir.model,name:base_write_diff.model_base
msgid "Base"
msgstr ""
22 changes: 22 additions & 0 deletions base_write_diff/i18n/it.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * base_write_diff
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-12-09 12:43+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.10.4\n"

#. module: base_write_diff
#: model:ir.model,name:base_write_diff.model_base
msgid "Base"
msgstr "Base"
1 change: 1 addition & 0 deletions base_write_diff/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import base
117 changes: 117 additions & 0 deletions base_write_diff/models/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Copyright 2025 Camptocamp SA (https://www.camptocamp.com).
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from collections import defaultdict

from odoo import Command, models
from odoo.tools.misc import frozendict

from odoo.addons.web.models.models import RecordSnapshot


class BaseModel(models.BaseModel):
_inherit = "base"

def write(self, vals):
# OVERRIDE: when using the ``write_use_diff_values`` context key, remove values
# that won't be changed before/after the ``write()`` itself.
# If ``write()`` is called on an empty recordset or with no value, ignore
# everything and shortcut to ``super()``.
if not (self and vals and self.env.context.get("write_use_diff_values")):
return super().write(vals)
recs_by_vals = defaultdict(lambda: self.browse())
for rec in self:
recs_by_vals[frozendict(rec._get_write_diff_values(vals))] += rec
for rec_vals, recs in recs_by_vals.items():
if rec_vals: # Don't trigger ``write()`` if there is nothing to update
super(BaseModel, recs).write(dict(rec_vals))
return True

def write_diff(self, vals: dict) -> bool:
"""Executes a ``write()`` only on fields that actually need to be updated"""
return self.with_context(write_use_diff_values=True).write(vals)

def _get_write_diff_values(self, vals: dict) -> dict:
"""Compares record values with the values to write

Returns a dictionary containing only the fields that actually needs to be
updated on ``self``, filtering out those which contain a value that is the same
as the current record's field value.
For example:
>>> self.name = "A"
>>> self.code = "a"
>>> self._get_write_diff_values({"name": "A", "code": "a"})
{}
>>> self._get_write_diff_values({"name": "B", "code": "a"})
{"name": "B"}
>>> self._get_write_diff_values({"name": "B", "code": "b"})
{"name": "B", "code": "b"}
"""
self.ensure_one()
diff_values = {}

# Step 1: group fields according to whether they're multi-relational or not
x2many_fields_values, simple_fields_values = {}, {}
for fname, fvalue in vals.items():
if self._fields[fname].type in ("one2many", "many2many"):
x2many_fields_values[fname] = fvalue
else:
simple_fields_values[fname] = fvalue

# Step 2: prepare fields to update by checking simple fields first
if simple_fields_values:
simple_fields_specs = {f: {} for f in simple_fields_values}
snapshot0 = self._do_snapshot({}, simple_fields_specs)
snapshot1 = self._do_snapshot(simple_fields_values, simple_fields_specs)
diff_values.update(snapshot1.diff(snapshot0))

# Step 3: prepare fields to update by checking multi-relational fields
# For each multi-relational field, prepare a new list of values by checking
# the original commands:
# - if it's an update command, check whether something actually changes on
# the corecord by calling ``_get_write_diff_values()`` recursively
# - else, add the original command to the new list: all commands except "update"
# will modify the record-corecords relation by creating/[un]linking/deleting
# corecords
# Then, check the new list of values to decide if the field needs updating:
# - at least 1 creation/update => add the full list of commands for simplicity
# - else => check whether the new values will effectively change the
# record-corecords relationship
for fname, fvalues in x2many_fields_values.items():
# Prepare the new list of commands/values according to the original command
new_fvalues = []
for fvalue in fvalues:
if fvalue[0] == Command.UPDATE:
cmd, corec_id, corec_vals = fvalue
corec = self.env[self._fields[fname].comodel_name].browse(corec_id)
if corec_diff_vals := corec._get_write_diff_values(corec_vals):
new_fvalues.append((cmd, corec_id, corec_diff_vals))
else:
new_fvalues.append(fvalue)
# Check whether we actually need to include the new list in the diff values
if any(v[0] in (Command.CREATE, Command.UPDATE) for v in new_fvalues):
diff_values[fname] = new_fvalues
else:
x2many_snapshot0 = self._do_snapshot({}, {fname: {}})
x2many_snapshot1 = self._do_snapshot({fname: new_fvalues}, {fname: {}})
if x2many_diff_values := x2many_snapshot1.diff(x2many_snapshot0):
diff_values.update(x2many_diff_values)

return diff_values

def _do_snapshot(self, vals: dict, specs: dict) -> "RecordSnapshot":
"""Prepares a ``RecordSnapshot`` object with the specified params"""
self.ensure_one()
# Align ``vals`` and ``specs`` to make sure they both contain the same fields:
# - if a field in ``specs`` is missing from ``vals``, we read its current value
# from the record and convert it to a ``write()``-able format to prevent cache
# issues and inconsistencies
# - if a field in ``vals`` is missing from ``specs``, we add it with the default
# value of ``{}`` to allow ``RecordSnapshot`` to handle it properly
vals_fnames_not_in_specs = set(vals) - set(specs)
specs_fnames_not_in_vals = set(specs) - set(vals)
for fname in vals_fnames_not_in_specs:
specs[fname] = {}
for fname in specs_fnames_not_in_vals:
vals[fname] = self._fields[fname].convert_to_write(self[fname], self)
return RecordSnapshot(self.new(values=vals, origin=self), fields_spec=specs)
3 changes: 3 additions & 0 deletions base_write_diff/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
1 change: 1 addition & 0 deletions base_write_diff/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Silvio Gregorini \<<silvio.gregorini@camptocamp.com>\>
2 changes: 2 additions & 0 deletions base_write_diff/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
This module allows filtering values to update on records according to whether they are
actually different from the records' current values.
Loading