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
7 changes: 4 additions & 3 deletions sale_exception/models/sale_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ def sale_check_exception(self):
if orders:
orders._check_exception()

def _must_popup_exception(self):
return self.env.company.sale_exception_show_popup

def action_confirm(self):
if self.detect_exceptions():
if not self.env.company.sale_exception_show_popup:
return
return self._popup_exceptions()
return
return super().action_confirm()

def action_draft(self):
Expand Down
38 changes: 28 additions & 10 deletions sale_exception/models/sale_order_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import html

from odoo import api, fields, models
from odoo.api import Environment
from odoo.fields import Command
from odoo.tools import config


class SaleOrderLine(models.Model):
Expand Down Expand Up @@ -53,15 +55,31 @@ def _reverse_field(self):

def _detect_exceptions(self, rule):
records = super()._detect_exceptions(rule)
# Thanks to the new flush of odoo 13.0, queries will be optimized
# together at the end even if we update the exception_ids many times.
# On previous versions, this could be unoptimized.
lines_to_remove_exception = (self - records).filtered(
lambda line: rule.id in line.exception_ids.ids
test_mode = config["test_enable"] and not self.env.context.get(
"test_base_exception"
)
lines_to_remove_exception.exception_ids = [Command.unlink(rule.id)]
lines_to_add_exception = records.filtered(
lambda line: rule.id not in line.exception_ids.ids
)
lines_to_add_exception.exception_ids = [Command.link(rule.id)]
# 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
)
lines_to_remove_exception = (self - records).filtered(
lambda line: rule.id in line.exception_ids.ids
)
lines_to_remove_exception.with_env(new_env).exception_ids = [
Command.unlink(rule.id)
]
lines_to_add_exception = records.filtered(
lambda line: rule.id not in line.exception_ids.ids
)
lines_to_add_exception.with_env(new_env).exception_ids = [
Command.link(rule.id)
]
return records.mapped("order_id")

def _detect_exception_get_exc_class_values(self):
res = super()._detect_exception_get_exc_class_values()
return dict(res, target_model="sale.order")
26 changes: 26 additions & 0 deletions sale_exception/tests/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 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


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


@decorator
def patch_detect_exception_method_env(func, self):
with mock_detect_exception_method_env(self):
return func(self)
10 changes: 10 additions & 0 deletions sale_exception/tests/test_multi_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
from odoo import Command
from odoo.tests import TransactionCase

from odoo.addons.base_exception.tests.common import (
patch_base_exception_method_env,
swallow_base_exception_error,
)

from .common import patch_detect_exception_method_env


class TestSaleExceptionMultiRecord(TransactionCase):
@classmethod
Expand All @@ -17,6 +24,9 @@ def setUpClass(cls):
}
)

@patch_base_exception_method_env
@patch_detect_exception_method_env
@swallow_base_exception_error
def test_sale_order_exception(self):
exception_no_sol = self.env.ref("sale_exception.excep_no_sol")
exception_no_free = self.env.ref("sale_exception.excep_no_free")
Expand Down
68 changes: 62 additions & 6 deletions sale_exception/tests/test_sale_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,40 @@
# Copyright 2021 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)

from odoo import Command

from odoo import SUPERUSER_ID, Command
from odoo.api import Environment
from odoo.exceptions import UserError, ValidationError
from odoo.tests import Form, TransactionCase

from odoo.addons.base_exception.exceptions import BaseExceptionError
from odoo.addons.base_exception.tests.common import (
mock_base_exception_method_env,
patch_base_exception_method_env,
swallow_base_exception_error,
)

from .common import mock_detect_exception_method_env, patch_detect_exception_method_env


class TestSaleException(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.env = cls.env(
context=dict(
cls.env.context, test_base_exception=True, tracking_disable=True
)
)
cls.default_pl = cls.env["product.pricelist"].create(
{
"name": "Public Pricelist",
}
)

@patch_base_exception_method_env
@patch_detect_exception_method_env
@swallow_base_exception_error
def test_sale_order_exception(self):
self.sale_exception_confirm = self.env["sale.exception.confirm"]

Expand Down Expand Up @@ -146,6 +164,9 @@ def _create_sale_order(self, partner, product):
line_form.product_id = product
return order_form.save()

@patch_base_exception_method_env
@patch_detect_exception_method_env
@swallow_base_exception_error
def test_exception_partner_sale_warning(self):
exception = self.env.ref("sale_exception.exception_partner_sale_warning")
exception.active = True
Expand All @@ -157,13 +178,13 @@ def test_exception_partner_sale_warning(self):
partner.sale_warn = "warning"
sale_order2 = sale_order.copy()
self.env.company.sale_exception_show_popup = True
result = sale_order2.action_confirm()
self.assertEqual(
result.get("xml_id"), "sale_exception.action_sale_exception_confirm"
)
sale_order2.action_confirm()
self.assertEqual(sale_order2.state, "draft")
self.assertTrue(sale_order2.exception_ids.filtered(lambda x: x == exception))

@patch_base_exception_method_env
@patch_detect_exception_method_env
@swallow_base_exception_error
def test_exception_partner_sale_warning_no_popup(self):
exception = self.env.ref("sale_exception.exception_partner_sale_warning")
exception.active = True
Expand All @@ -180,6 +201,9 @@ def test_exception_partner_sale_warning_no_popup(self):
self.assertEqual(sale_order2.state, "draft")
self.assertTrue(sale_order2.exception_ids.filtered(lambda x: x == exception))

@patch_base_exception_method_env
@patch_detect_exception_method_env
@swallow_base_exception_error
def test_exception_product_sale_warning(self):
exception = self.env.ref("sale_exception.exception_product_sale_warning")
exception.active = True
Expand All @@ -193,6 +217,9 @@ def test_exception_product_sale_warning(self):
sale_order2.detect_exceptions()
self.assertTrue(sale_order2.exception_ids.filtered(lambda x: x == exception))

@patch_base_exception_method_env
@patch_detect_exception_method_env
@swallow_base_exception_error
def test_exception_no_free(self):
# No allow ignoring exceptions if the "is_blocking" field is checked
self.sale_exception_confirm = self.env["sale.exception.confirm"]
Expand Down Expand Up @@ -235,3 +262,32 @@ def test_exception_no_free(self):
so_except_confirm.action_confirm()
self.assertFalse(sale_order.ignore_exception)
self.assertTrue(sale_order.state == "draft")

def test_sale_order_line_exception_stored(self):
exception = self.env.ref("sale_exception.excep_no_dumping").sudo()
exception.active = True
partner = self.env.ref("base.res_partner_1")
product = self.env.ref("product.product_product_6")
product.standard_price = 10.0
sale_order = self._create_sale_order(partner=partner, product=product)
sale_order.order_line.price_unit = 5.0
self.registry.enter_test_mode(self.cr)
self.addCleanup(self.registry.leave_test_mode)
with (
self.registry.cursor() as new_cr,
):
new_env = Environment(new_cr, SUPERUSER_ID, {"module": "sale_exception"})
with (
# Use new_env created here instead of the one in base_exception_method
mock_base_exception_method_env(self, env=new_env),
mock_detect_exception_method_env(self, env=new_env),
self.assertRaises(BaseExceptionError),
):
sale_order.action_confirm()
new_cr._savepoint = None
self.assertFalse(sale_order.exception_ids)
self.assertTrue(sale_order.with_env(new_env).exception_ids)
self.assertFalse(sale_order.order_line.exception_ids)
self.assertTrue(sale_order.order_line.with_env(new_env).exception_ids)
self.assertNotEqual(sale_order.state, "sale")
self.assertNotEqual(sale_order.with_env(new_env).state, "sale")
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)

from odoo.addons.base.tests.common import BaseCommon
from odoo.addons.base_exception.tests.common import (
patch_base_exception_method_env,
swallow_base_exception_error,
)


class TestSaleException(BaseCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()

cls.env.context = dict(cls.env.context, test_base_exception=True)
cls.exception = cls.env.ref(
"sale_exception_product_sale_manufactured_for.exception_partner_can_order"
)
Expand Down Expand Up @@ -40,6 +44,8 @@ def setUpClass(cls):
}
)

@patch_base_exception_method_env
@swallow_base_exception_error
def test_commercial_partner_not_valid(self):
self.sale.partner_id.commercial_partner_id = self.env.ref("base.res_partner_2")
self.sale.action_confirm()
Expand All @@ -55,6 +61,8 @@ def test_commercial_partner_is_valid(self):
self.assertEqual(self.sale.state, "sale")
self.assertFalse(self.sale.exception_ids)

@patch_base_exception_method_env
@swallow_base_exception_error
def test_commercial_partner_empty(self):
self.sale.partner_id.commercial_partner_id = False
self.sale.action_confirm()
Expand Down
1 change: 1 addition & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
odoo-test-helper
odoo-addon-base_exception @ git+https://github.com/OCA/server-tools.git@refs/pull/3590/head#subdirectory=base_exception
Loading