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
264 changes: 264 additions & 0 deletions web_field_provenance/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
====================
Web Field Provenance
====================

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

.. |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%2Fweb-lightgray.png?logo=github
:target: https://github.com/OCA/web/tree/18.0/web_field_provenance
:alt: OCA/web
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/web-18-0/web-18-0-web_field_provenance
: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/web&target_branch=18.0
:alt: Try me on Runboat

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

Track and surface whether a field value was set by the user, assigned by
a rule/cascade/integration, or is still at its default — Salesforce/
SAP-style field provenance for Odoo.

Solves the OCA-wide bug class where
``compute=..., store=True, readonly=False`` ("computed-writable") fields
silently overwrite manual user overrides when an upstream dependency
changes and the compute re-fires through ``super()``. Provides:

1. A persistent per-record provenance map (``_provenance``) that
records, for every opted-in field, **how** it got its current value.
Three states:

- **default** — no entry. The field is still at the value it had on
creation. We never stamp the default state, which keeps the JSON
column compact.
- **rule / cascade / EDI** —
``{s:"r", b:<writer-id>, t:<unix>, r:<label>}``. Set by server-side
cascade code via ``_stamp_provenance(source="r", …)``.
- **user** — ``{s:"u", b:<login>, t:<unix>}``. Set by a regular user
write. Stamped automatically by the ORM hook.

2. A reusable ``_user_set(fname)`` helper that compute methods consult
to gate ``super()`` on real user intent rather than chain-residual
values.
3. A ``_stamp_provenance(keys, source, by, rule=None, when=None)``
public API for cascade methods, EDI connectors, and import loaders to
attribute their writes correctly.
4. A ``_provenance_for(fname)`` helper returning the badge tooltip dict
(also useful as a golden-output target in tests).
5. An OWL ``provenance_m2o`` widget that renders a small badge next to
opted-in fields with three icons:

- grey outline = default
- green cog = rule
- pencil = user Hovering shows the writer and timestamp. Clicking
promotes the value to user-anchored; the server sanitizes the
client payload so writer identity cannot be spoofed.

This module is plumbing. To surface the badge on a specific field, set
``track_provenance=True`` on its ``ir.model.fields`` record (see USAGE)
and replace the view tag with ``widget="provenance_m2o"``.

**Table of contents**

.. contents::
:local:

Usage
=====

1. Opt-in a field for provenance tracking
-----------------------------------------

In Settings → Technical → Database Structure → Fields, find the field
and tick **Track Provenance**. (For fields declared in Python — most
fields you'll want to track — the UI write is blocked by Odoo's
base-field protection; the documented workaround is a small admin
wizard, see ROADMAP.)

Programmatically (e.g. in a ``post_init_hook`` of a downstream module):

.. code:: python

self.env.cr.execute(
"UPDATE ir_model_fields SET track_provenance = TRUE "
"WHERE model = %s AND name = %s",
("sale.order", "payment_term_id"),
)
self.env.registry.clear_cache()

2. Preserve user values inside an override compute
--------------------------------------------------

In modules that layer overrides on Odoo core computes (e.g.
``sale_order_type``), guard the recompute on the new helper:

.. code:: python

@api.depends("type_id")
def _compute_payment_term_id(self):
# Records the user has explicitly touched are excluded from super().
preserved = self.filtered(lambda r: r._user_set("payment_term_id"))
super(SaleOrder, self - preserved)._compute_payment_term_id()
for order in (self - preserved).filtered("type_id.payment_term_id"):
order.payment_term_id = order.type_id.payment_term_id

3. Attribute cascade / rule / EDI writes correctly
--------------------------------------------------

When a non-user writer (a compute cascade, an EDI inbound, an import
loader) sets a value, call ``_stamp_provenance`` so the badge attributes
the value to the correct source instead of the env user:

.. code:: python

order._stamp_provenance(
["payment_term_id"],
source="r",
by="sot.cascade", # stable writer identifier
rule="Sale Order Type cascade", # optional human-readable label
)

The badge will then render the green-cog (rule) icon and the hover
tooltip will read "Set by *Sale Order Type cascade*".

4. Render the badge in views
----------------------------

Replace the field's view tag:

.. code:: xml

<field name="payment_term_id" widget="provenance_m2o"/>

A small icon appears next to the field with three states:

- **Grey outline** — *default*: field is still at its initial value. No
entry exists in the provenance map; the badge falls back to
``record.create_date`` for the tooltip.
- **Green cog** — *rule / cascade / EDI*: the value was set by
server-side logic (``_stamp_provenance`` called with ``source="r"``).
Tooltip names the writer.
- **Pencil** — *user*: the user has anchored this value. Recompute
cascades that consult ``_user_set`` will respect it.

Clicking the icon promotes the current value to user-anchored. The
server sanitizes the click (forces ``b = env.user.login``, drops any
client-supplied rule provenance) — there is no path for a malicious
client to spoof the writer identity.

5. Inspect provenance programmatically
--------------------------------------

.. code:: python

order._user_set("payment_term_id") # True/False
order._provenance_for("payment_term_id") # {state, label, by?, rule?, when}

The dict form is what the OWL widget renders; it's also useful for
golden-output tests of cascade flows.

Known issues / Roadmap
======================

- ☐ **Opt-in wizard for base fields.** Odoo blocks
``ir.model.fields.write()`` on ``state='base'`` fields, so
``track_provenance`` can't be toggled from the generic technical view
today. Ship a small wizard (Settings → Technical → Field Provenance
Setup) that picks (model, field) tuples and applies the flag via a SQL
UPDATE + ``registry.clear_cache()``. Tests already use this bypass.
- ☐ **provenance_selection** and **provenance_char** widget variants
(current MVP only ships ``provenance_m2o``).
- ☐ Pre-commit cascade banner: intercept the onchange RPC result in the
OWL form controller and surface "Field X is about to change from A → B
because Y changed" before commit. ProseMirror ``filterTransaction`` is
the conceptual reference.
- ☐ Draft-time cascade log panel (chatter-adjacent) listing every field
cascade with timestamp, prior value, new value, cause field.
- ☐ Undo toast (Linear-style) — let the cascade happen, then offer a
6-second window to revert just the overwritten field.
- ☐ Demo addon ``web_field_provenance_sale`` that wires ``sale.order`` +
``account.move`` fields touched by ``sale_order_type`` to the widget
and applies the ``_user_set`` guard in the seven mirror computes.
- ☐ Upstream RFC to Odoo S.A.: expose ``_user_set`` on the ORM base
class so this module becomes plumbing-only.
- ☐ **API / EDI integration** (sub-track — separate from the MVP):

- Documented
``_stamp_provenance(..., source='r', by='edi:<connector>')`` hook
for inbound EDI / XML-RPC writers. Connector modules call this when
applying values on behalf of an upstream system so the badge
attributes correctly.
- Conflict policy on re-import: when an inbound EDI rewrites a ``s=u``
field, decide between "preserve user (block)", "override and log
prior in chatter", or "override and re-stamp as ``r=edi:...``".
Per-integration setting.
- Provenance serializer ``_provenance_to_audit_dict`` for outbound
audit exports (UBL/Peppol BTG-65-equivalent "Originator Document
Reference" semantics).
- Bulk-import (``load()``, base_import) stamping with ``b='import'``
and optional batch identifier so audit can trace a row to a specific
import job.
- Benchmark JSON-column overhead at 100+ tracked fields per record;
consider migrating to a ``mail.tracking.value``-style external table
if the inline column starts dominating query costs.

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

Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/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/web/issues/new?body=module:%20web_field_provenance%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
-------

* Ledoweb

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

- Dan Kendall dkendall@ledoweb.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.

.. |maintainer-dkendall| image:: https://github.com/dkendall.png?size=40px
:target: https://github.com/dkendall
:alt: dkendall

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-dkendall|

This module is part of the `OCA/web <https://github.com/OCA/web/tree/18.0/web_field_provenance>`_ 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 web_field_provenance/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
27 changes: 27 additions & 0 deletions web_field_provenance/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright 2026 Ledoweb (Dan Kendall)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
{
"name": "Web Field Provenance",
"summary": "Track and surface whether a field value came from the user "
"or from a computed/derived default, SAP-style.",
"category": "Tools",
"version": "18.0.1.0.0",
"author": "Ledoweb, Odoo Community Association (OCA)",
"license": "AGPL-3",
"website": "https://github.com/OCA/web",
"depends": ["web"],
"data": [
"views/ir_model_fields_views.xml",
],
"demo": [
"demo/web_field_provenance_demo.xml",
],
"assets": {
"web.assets_backend": [
"web_field_provenance/static/src/**/*",
],
},
"maintainers": ["dkendall"],
"auto_install": False,
"installable": True,
}
23 changes: 23 additions & 0 deletions web_field_provenance/demo/web_field_provenance_demo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!--
Demo view: opt-in `res.partner.title` to the provenance badge so the
widget can be visually verified against the three states (default /
rule / user). `parent_id` is rendered with a specialized partner-m2o
widget in core that can't be replaced via inheritance, so `title`
serves as the demo target. This is loaded only with demo data and
is safe to install on a production DB — the underlying field's
`track_provenance` flag is the real opt-in switch and is OFF until
set explicitly by an admin.
-->
<record id="view_partner_form_provenance_demo" model="ir.ui.view">
<field name="name">res.partner.form.web_field_provenance.demo</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="arch" type="xml">
<field name="title" position="replace">
<field name="title" widget="provenance_m2o" />
</field>
</field>
</record>
</odoo>
2 changes: 2 additions & 0 deletions web_field_provenance/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import base_model
from . import ir_model_fields
Loading
Loading