Skip to content
Closed
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
291 changes: 291 additions & 0 deletions global_undo/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
==================
Global Undo & Redo
==================

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

.. |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/global_undo
: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-global_undo
: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 journals what a user does in the backend and lets them take
it back with **Ctrl+Z** (**Cmd+Z** on macOS), or replay it with
**Ctrl+Shift+Z**:

- creations, updates and deletions, grouped one step per saved form, so
that a form and its one2many lines are a single undo;
- business actions that have a known inverse, such as confirming a sales
order or posting a journal entry;
- a history view and a trash from which deleted records can be restored.

Permissions, record rules, multi-company and accounting integrity are
all checked before anything is replayed. When a step cannot be replayed
safely the module refuses with a clear message rather than leaving the
database inconsistent.

Odoo core is not modified. The CRUD hooks come from an
``_inherit = "base"`` model and the business action hooks from
``_register_hook``, the same technique ``base_automation`` uses.

**Table of contents**

.. contents::
:local:

Configuration
=============

Groups
------

- **Global Undo: User**, implied by *Internal User*. Only the operations
of users in this group are journalled. Removing the group disables
recording for that user entirely.
- **Global Undo: Administrator**, implied by *Settings*. Can review and
undo the operations of every user, and read the stored snapshots.

Undoable actions
----------------

*Settings > Global Undo > Configuration > Undoable Actions* declares
which business methods may be undone and which methods revert them, in
order.

Three are shipped and activate themselves when their module is
installed:

+--------------------+--------------------+----------------------------+
| Model | Action | Reverted with |
+====================+====================+============================+
| ``sale.order`` | ``action_confirm`` | ``_action_cancel``, |
| | | ``action_draft`` |
+--------------------+--------------------+----------------------------+
| ``purchase.order`` | ``button_confirm`` | ``button_cancel``, |
| | | ``button_draft`` |
+--------------------+--------------------+----------------------------+
| ``account.move`` | ``action_post`` | ``button_draft`` |
+--------------------+--------------------+----------------------------+

Add a line to cover a method of your own, or to change the inverse of
one of the three. Archiving a line that matches a shipped default
switches that default off. Changes take effect when the registry
reloads.

Excluded models
---------------

*Settings > Global Undo > Configuration > Excluded Models* takes any
model out of the journal, on top of the technical and ledger models the
module always leaves alone.

Retention
---------

A daily cron purges the history. Two separate windows, because
forgetting that something was deleted is a nuisance while losing the
only copy of it is data loss:

- ``global_undo.retention_days`` (30 by default) for the history;
- ``global_undo.trash_retention_days`` (180 by default) for the steps
still holding recoverable records. Such a step survives the first
window even though it can no longer be undone.

Usage
=====

Press **Ctrl+Z** to undo the last thing you did, **Ctrl+Shift+Z** to
redo it. The shortcuts are deliberately registered without
``bypassEditableProtection``: inside an ``input`` or a ``textarea``,
Ctrl+Z keeps its native meaning of "undo what I just typed" and never
reaches the server.

The systray also carries an undo icon, a redo icon and a history
dropdown with the ten most recent steps. Each button names in its
tooltip the step it would act on and is disabled when there is none, so
the shortcut never quietly undoes something other than what it
announces.

The undo stack behaves like the one in any editor. Ctrl+Z takes the most
recently applied step, Ctrl+Shift+Z takes the oldest undone one, so
repeated redos walk back up the stack in the order it was unwound. Any
new operation discards the redo stack.

Trash
-----

Deleted records are listed under *Settings > Global Undo > Trash* and
can be restored from there. To bring back a parent and its children,
select them all and restore them in one go: the trash applies the same
ordering and the same id remapping as an undo, so the children come back
pointing at the parent's new id. Restoring only a child leaves it
orphaned.

History
-------

*Settings > Global Undo > Undo History* lists every recorded step, its
operations, and lets a step be undone or redone from the form view.
Ordinary users see only their own; a Global Undo Administrator sees
everyone's.

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

Some operations cannot be undone. The module prefers to refuse with a
clear message over leaving the database inconsistent.

Never recorded
--------------

These do not appear in the history at all.

- **Technical models.** Everything under ``ir.``, ``bus.``, ``mail.``,
``base.``, ``report.``, ``iap.``, plus ``res.users.log`` and
``res.users.settings``. This covers module installation, views, crons,
sequences, chatter messages, activities and notifications.
- **Ledger rows**, where the accounting and stock truth lives and which
may only change through the business layer that owns it:
``account.move.line``, ``account.payment``,
``account.partial.reconcile``, ``account.full.reconcile``,
``account.bank.statement.line``, ``account.tax.repartition.line``,
``stock.move``, ``stock.move.line``, ``stock.quant``,
``stock.valuation.layer``, ``pos.order`` and its lines, and
``product.price.history``.
- **Transient and abstract models**, which have no rows to put back.
- **Operations touching more than 200 records at once.** A mass update
is cheaper to redo by hand than to journal.
- **Anything done as superuser or with ``sudo()``**, during a module
install or upgrade, or while the registry is not ready.
- **Anything that is not the user editing data through the web client.**
Only the client's save, delete and archive calls and its button
presses count. Imports, XML-RPC, the portal, the website and the
client's own housekeeping calls are not journalled, so they never land
on top of somebody's undo stack.

Recorded, but the undo is refused
---------------------------------

- **The record changed afterwards.** Every operation stores the
``write_date`` it read right after running, and stores it again on
every replay. If it no longer matches exactly, somebody else touched
the record and undoing would discard their work. This also applies to
undoing a creation: deleting the record would take the other user's
edits with it.
- **The permission is no longer there.** Undoing a creation requires
delete rights, undoing a deletion requires create rights. Both
model-level access and record-level rules are checked.
- **The record belongs to a company outside your allowed companies.**
- **The record is gone**, or, for something being re-created, already
exists.
- **Journal entries.** An ``account.move`` carrying an
``inalterable_hash`` cannot be touched at all. A posted entry refuses
plain field changes, since the only reversible thing about it is the
posting itself. A reconciled or paid entry refuses to be set back to
draft. Beyond that, Odoo's own ``button_draft`` rules (lock dates,
sequence, hashed journals) apply and their error is propagated as is.
- **Transfers already done.** A ``stock.picking`` in state ``done`` is
not reverted: the correct reversal is a return, not a deletion.
- **Another user's operations**, unless you are a Global Undo
Administrator.

Undone, with a caveat worth knowing
-----------------------------------

- **A restored record gets a new database id.** The ORM will not reuse a
deleted one. The new id is kept in ``restored_res_id`` and later
replays follow it, but any external reference to the old id stays
broken -- and in most cases already was, since the deletion cascaded
or nulled it.
- **One2many children do not come back on their own.** They have their
own lifecycle and were deleted as separate operations. Restore them
together with their parent from the trash and the link is rebuilt.
- **Binary fields are stored only up to 512 KB.** Larger attachments and
images are dropped rather than bloat the journal. The computed
variants of an image are not stored either, since they are regenerated
from the original.
- **Read-only computed fields and related fields are not stored**,
because they are recomputed from the fields that are.
- **Sequences are consumed.** Undoing the creation of an invoice does
not give its number back to ``ir.sequence``. A redo restores the
original number from the snapshot.
- **External effects are not reverted.** Sent emails, webhooks, third
party API calls and files written to disk are outside the database.
- **The chatter is not cleaned up.** Messages and followers created by
the original operation stay, because ``mail.*`` is not recorded.

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:%20global_undo%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
-------

* Pol Reig
* QubiQ

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

- `QubiQ <https://www.qubiq.es>`__:

- Pol Reig <pol.reig@qubiq.es>

Other credits
-------------

The business action hooks follow the method wrapping technique used by
Odoo's own ``base_automation`` module.

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-polreig| image:: https://github.com/polreig.png?size=40px
:target: https://github.com/polreig
:alt: polreig

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

|maintainer-polreig|

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

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
4 changes: 4 additions & 0 deletions global_undo/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2026 Pol Reig <pol.reig@qubiq.es>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from . import models
38 changes: 38 additions & 0 deletions global_undo/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2026 Pol Reig <pol.reig@qubiq.es>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Global Undo & Redo",
"summary": "Undo and redo backend operations with Ctrl+Z, with history and trash",
"version": "18.0.1.0.0",
"category": "Tools",
"author": "Pol Reig, QubiQ, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/server-tools",
"license": "AGPL-3",
"development_status": "Beta",
"maintainers": ["polreig"],
"application": False,
"installable": True,
"depends": ["base", "web"],
"data": [
"security/global_undo_security.xml",
"security/ir.model.access.csv",
"data/ir_cron_data.xml",
"data/ir_config_parameter_data.xml",
"views/global_undo_transaction_views.xml",
"views/global_undo_operation_views.xml",
"views/global_undo_action_views.xml",
"views/global_undo_exclusion_views.xml",
"views/global_undo_menus.xml",
],
"assets": {
"web.assets_backend": [
"global_undo/static/src/global_undo_systray/global_undo_systray.scss",
"global_undo/static/src/global_undo_service/global_undo_service.esm.js",
"global_undo/static/src/global_undo_systray/global_undo_systray.esm.js",
"global_undo/static/src/global_undo_systray/global_undo_systray.xml",
],
"web.assets_tests": [
"global_undo/static/tests/tours/global_undo_tour.esm.js",
],
},
}
21 changes: 21 additions & 0 deletions global_undo/data/ir_config_parameter_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2026 Pol Reig <pol.reig@qubiq.es>
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo noupdate="1">
<record
id="ir_config_parameter_global_undo_retention_days"
model="ir.config_parameter"
>
<field name="key">global_undo.retention_days</field>
<field name="value">30</field>
</record>

<!-- Longer on purpose: past this, a deleted record is gone for good. -->
<record
id="ir_config_parameter_global_undo_trash_retention_days"
model="ir.config_parameter"
>
<field name="key">global_undo.trash_retention_days</field>
<field name="value">180</field>
</record>
</odoo>
14 changes: 14 additions & 0 deletions global_undo/data/ir_cron_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2026 Pol Reig <pol.reig@qubiq.es>
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo noupdate="1">
<record id="ir_cron_global_undo_vacuum" model="ir.cron">
<field name="name">Global Undo: purge old history</field>
<field name="model_id" ref="model_global_undo_transaction" />
<field name="state">code</field>
<field name="code">model._gu_vacuum()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="active" eval="True" />
</record>
</odoo>
Loading
Loading