Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ addon | version | maintainers | summary
[autovacuum_message_attachment](autovacuum_message_attachment/) | 18.0.1.0.1 | <a href='https://github.com/florian-dacosta'><img src='https://github.com/florian-dacosta.png' width='32' height='32' style='border-radius:50%;' alt='florian-dacosta'/></a> | Automatically delete old mail messages and attachments
[base_conditional_image](base_conditional_image/) | 18.0.1.0.0 | | This module extends the functionality to support conditional images
[base_cron_exclusion](base_cron_exclusion/) | 18.0.1.0.1 | <a href='https://github.com/LoisRForgeFlow'><img src='https://github.com/LoisRForgeFlow.png' width='32' height='32' style='border-radius:50%;' alt='LoisRForgeFlow'/></a> <a href='https://github.com/ChrisOForgeFlow'><img src='https://github.com/ChrisOForgeFlow.png' width='32' height='32' style='border-radius:50%;' alt='ChrisOForgeFlow'/></a> | Allow you to select scheduled actions that should not run simultaneously.
[base_exception](base_exception/) | 18.0.1.1.1 | <a href='https://github.com/hparfr'><img src='https://github.com/hparfr.png' width='32' height='32' style='border-radius:50%;' alt='hparfr'/></a> <a href='https://github.com/sebastienbeau'><img src='https://github.com/sebastienbeau.png' width='32' height='32' style='border-radius:50%;' alt='sebastienbeau'/></a> | This module provide an abstract model to manage customizable exceptions to be applied on different models (sale order, invoice, ...)
[base_exception](base_exception/) | 18.0.1.1.2 | <a href='https://github.com/hparfr'><img src='https://github.com/hparfr.png' width='32' height='32' style='border-radius:50%;' alt='hparfr'/></a> <a href='https://github.com/sebastienbeau'><img src='https://github.com/sebastienbeau.png' width='32' height='32' style='border-radius:50%;' alt='sebastienbeau'/></a> | This module provide an abstract model to manage customizable exceptions to be applied on different models (sale order, invoice, ...)
[base_fontawesome](base_fontawesome/) | 18.0.2.0.0 | | Up to date Fontawesome resources.
[base_fontawesome_web_editor](base_fontawesome_web_editor/) | 18.0.1.0.0 | | Integration between base_fontawesome and web_editor for FontAwesome >= 6.7.2 support.
[base_force_record_noupdate](base_force_record_noupdate/) | 18.0.1.0.0 | | Manually force noupdate=True on models
Expand Down
3 changes: 2 additions & 1 deletion base_exception/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Exception Rule
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:f26be2731bb07680ada96c7a82498292ac0bc49c68965087476b3d354e6acc9e
!! source digest: sha256:bf43c9d6f42f7195b276ef11de74a1a145886fbc5928d55fe35e8763fb5fd866
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png
Expand Down Expand Up @@ -91,6 +91,7 @@ Contributors
- Kevin Khao <kevin.khao@akretion.com>
- Laurent Mignon <laurent.mignon@acsone.eu>
- Do Anh Duy <duyda@trobz.com>
- Akim Juillerat <akim.juillerat@camptocamp.com>

Other credits
-------------
Expand Down
7 changes: 6 additions & 1 deletion base_exception/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Exception Rule",
"version": "18.0.1.1.1",
"version": "18.0.1.1.2",
"development_status": "Mature",
"category": "Generic Modules",
"summary": """
Expand All @@ -24,4 +24,9 @@
"views/base_exception_view.xml",
],
"installable": True,
"assets": {
"web.assets_backend": [
"base_exception/static/src/js/base_exception.esm.js",
],
},
}
8 changes: 8 additions & 0 deletions base_exception/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copyright 2025 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)

from odoo.exceptions import UserError


class BaseExceptionError(UserError):
pass
23 changes: 14 additions & 9 deletions base_exception/models/base_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,22 @@ def _compute_exceptions_summary(self):
else:
rec.exceptions_summary = False

def _must_popup_exception(self):
"""Hook to redefine if the exception pop up must be shown"""
return False

def action_popup_exceptions(self):
if self._must_popup_exception():
return self._popup_exceptions()
return {"type": "ir.actions.client", "tag": "soft_reload"}

def _popup_exceptions(self):
"""This method is used to show the popup action view.
Used in several dependent modules."""
record = self._get_popup_action()
action = record.sudo().read()[0]
action = {
field: value
for field, value in action.items()
if field in record._get_readable_fields()
}
action.update(
# TODO: When migrating, use _for_xml_id instead of this
action = self._get_popup_action()
action_dict = action.sudo()._get_action_dict()
action_dict.update(
{
"context": {
"active_id": self.ids[0],
Expand All @@ -85,7 +90,7 @@ def _popup_exceptions(self):
}
}
)
return action
return action_dict

@api.model
def _get_popup_action(self):
Expand Down
57 changes: 52 additions & 5 deletions base_exception/models/base_exception_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@
# Copyright 2020 Hibou Corp.
# Copyright 2023 ACSONE SA/NV (http://acsone.eu)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import json
import logging
from collections import defaultdict

from odoo import _, api, models
from odoo import Command, _, api, models
from odoo.api import Environment
from odoo.exceptions import UserError
from odoo.osv import expression
from odoo.tools import config
from odoo.tools.safe_eval import safe_eval

from ..exceptions import BaseExceptionError

_logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -84,12 +89,54 @@ def detect_exceptions(self):
# the "to remove" part generates one DELETE per rule on the relation
# table
# and the "to add" part generates one INSERT (with unnest) per rule.
for rule_id, records in rules_to_remove.items():
records.write({"exception_ids": [(3, rule_id)]})
for rule_id, records in rules_to_add.items():
records.write({"exception_ids": [(4, rule_id)]})
raise_exception = False
test_mode = config["test_enable"] and not self.env.context.get(
"test_base_exception"
)
# Write exceptions in a new transaction to be committed so that we can
# rollback the ongoing one while keeping the exceptions stored
with self.env.registry.cursor() as new_cr:
new_env = (
Environment(new_cr, self.env.uid, self.env.context)
if not test_mode
else self.env
)
for rule_id, records in rules_to_remove.items():
records.with_env(new_env).write(
{"exception_ids": [Command.unlink(rule_id)]}
)
for rule_id, records in rules_to_add.items():
records.with_env(new_env).write(
{"exception_ids": [Command.link(rule_id)]}
)
# In case we have new exception, or exceptions that were not ignored yet, or
# blocking exceptions, we need to raise an exception to rollback the
# ongoing transaction
self_new_env = self.with_env(new_env)
if rules_to_add or self_new_env._must_raise_exception_after_detection():
raise_exception = True
if raise_exception:
raise BaseExceptionError(
json.dumps(self._detect_exception_get_exc_class_values())
)
return all_exception_ids

def _detect_exception_get_exc_class_values(self):
return {
"src_model": self._name,
"target_model": self._name,
}

def _must_raise_exception_after_detection(self):
main_records = self._get_main_records()
all_ignore_exception = all(
main_records.filtered("exception_ids").mapped("ignore_exception")
)
any_blocking_exception = any(
rule.is_blocking for rule in main_records.exception_ids
)
return not all_ignore_exception or any_blocking_exception

@api.model
def _exception_rule_eval_context(self, rec):
return {
Expand Down
1 change: 1 addition & 0 deletions base_exception/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
- Kevin Khao \<<kevin.khao@akretion.com>\>
- Laurent Mignon \<<laurent.mignon@acsone.eu>\>
- Do Anh Duy \<<duyda@trobz.com>\>
- Akim Juillerat \<<akim.juillerat@camptocamp.com>\>
3 changes: 2 additions & 1 deletion base_exception/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ <h1>Exception Rule</h1>
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:f26be2731bb07680ada96c7a82498292ac0bc49c68965087476b3d354e6acc9e
!! source digest: sha256:bf43c9d6f42f7195b276ef11de74a1a145886fbc5928d55fe35e8763fb5fd866
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Mature" src="https://img.shields.io/badge/maturity-Mature-brightgreen.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/server-tools/tree/18.0/base_exception"><img alt="OCA/server-tools" src="https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/server-tools-18-0/server-tools-18-0-base_exception"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/server-tools&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module provide an abstract model to manage customizable exceptions
Expand Down Expand Up @@ -438,6 +438,7 @@ <h3><a class="toc-backref" href="#toc-entry-5">Contributors</a></h3>
<li>Kevin Khao &lt;<a class="reference external" href="mailto:kevin.khao&#64;akretion.com">kevin.khao&#64;akretion.com</a>&gt;</li>
<li>Laurent Mignon &lt;<a class="reference external" href="mailto:laurent.mignon&#64;acsone.eu">laurent.mignon&#64;acsone.eu</a>&gt;</li>
<li>Do Anh Duy &lt;<a class="reference external" href="mailto:duyda&#64;trobz.com">duyda&#64;trobz.com</a>&gt;</li>
<li>Akim Juillerat &lt;<a class="reference external" href="mailto:akim.juillerat&#64;camptocamp.com">akim.juillerat&#64;camptocamp.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="other-credits">
Expand Down
44 changes: 44 additions & 0 deletions base_exception/static/src/js/base_exception.esm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {registry} from "@web/core/registry";

/* eslint-disable no-unused-vars */
async function popUpException(env, _action) {
/* eslint-enable no-unused-vars */
const controller = env.services.action.currentController;
const orm = env.services.orm;
const resId = controller.currentState?.resId;
const resModel = controller.props.resModel;
if (!resModel || !resId) return;
const popupAction = await orm.call(resModel, "action_popup_exceptions", [[resId]]);
if (!popupAction) return;
// Do a soft reload before displaying the popup to display the exception
// on the Form view
await env.services.action.restore(controller.jsId);
await env.services.action.doAction(popupAction);
}

function baseExceptionErrorHandler(env, uncaughtError, originalError) {
const controller = env.services.action.currentController;
if (
originalError.exceptionName ===
"odoo.addons.base_exception.exceptions.BaseExceptionError"
) {
const excData = JSON.parse(originalError.data.message);
if (excData.target_model === controller.props.resModel) {
env.services.action.doAction({
type: "ir.actions.client",
tag: "popup_exception",
});
} else {
env.services.action.doAction({
type: "ir.actions.client",
tag: "soft_reload",
});
}
return true;
}
}

registry.category("actions").add("popup_exception", popUpException);
registry
.category("error_handlers")
.add("base_exception_error", baseExceptionErrorHandler, {sequence: 0});
40 changes: 40 additions & 0 deletions base_exception/tests/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright 2026 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)

try:
from decorator import decoratorx as decorator
except ImportError:
from decorator import decorator

from contextlib import contextmanager
from unittest.mock import patch

from ..exceptions import BaseExceptionError


@decorator
def swallow_base_exception_error(func, self):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except BaseExceptionError:
return None

return wrapper


@contextmanager
def mock_base_exception_method_env(self, env=None):
if env is None:
env = self.env
with patch(
"odoo.addons.base_exception.models.base_exception_method.Environment"
) as mocked_env:
mocked_env.return_value = env
yield


@decorator
def patch_base_exception_method_env(func, self):
with mock_base_exception_method_env(self):
return func(self)
5 changes: 5 additions & 0 deletions base_exception/tests/purchase_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ def button_confirm(self):
self.write({"state": "purchase"})
return True

def button_detect_and_confirm(self):
if self.detect_exceptions():
return
return self.button_confirm()

def button_cancel(self):
self.write({"state": "cancel"})

Expand Down
Loading
Loading