diff --git a/base_exception_user/__init__.py b/base_exception_user/__init__.py
new file mode 100644
index 00000000..c7120225
--- /dev/null
+++ b/base_exception_user/__init__.py
@@ -0,0 +1,4 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from . import models
+from . import wizard
diff --git a/base_exception_user/__manifest__.py b/base_exception_user/__manifest__.py
new file mode 100644
index 00000000..ee78d3af
--- /dev/null
+++ b/base_exception_user/__manifest__.py
@@ -0,0 +1,24 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+{
+ 'name': 'Exception Rule User',
+ 'version': '15.0.1.0.0',
+ 'author': 'Hibou Corp.',
+ 'license': 'OPL-1',
+ 'category': 'Generic Modules',
+ 'summary': 'Allow users to ignore exceptions',
+ 'description': """
+Allow users to ignore exceptions
+""",
+ 'website': 'https://hibou.io/',
+ 'depends': [
+ 'base_exception',
+ ],
+ 'data': [
+ 'security/base_exception_security.xml',
+ 'views/base_exception_views.xml',
+ 'wizard/base_exception_confirm_view.xml',
+ ],
+ 'installable': True,
+ 'auto_install': False,
+}
diff --git a/base_exception_user/models/__init__.py b/base_exception_user/models/__init__.py
new file mode 100644
index 00000000..3e60ba17
--- /dev/null
+++ b/base_exception_user/models/__init__.py
@@ -0,0 +1,3 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from . import base_exception
diff --git a/base_exception_user/models/base_exception.py b/base_exception_user/models/base_exception.py
new file mode 100644
index 00000000..09ebf775
--- /dev/null
+++ b/base_exception_user/models/base_exception.py
@@ -0,0 +1,28 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+import html
+from odoo import api, models, fields
+
+
+class ExceptionRule(models.Model):
+ _inherit = 'exception.rule'
+
+ allow_user_ignore = fields.Boolean('Allow User Ignore')
+
+
+class BaseException(models.AbstractModel):
+ _inherit = 'base.exception'
+
+ @api.depends("exception_ids", "ignore_exception")
+ def _compute_exceptions_summary(self):
+ for rec in self:
+ if rec.exception_ids and not rec.ignore_exception:
+ rec.exceptions_summary = "
" % "".join(
+ [
+ "%s: %s"
+ % tuple(map(html.escape, (e.name, e.description or '')))
+ for e in rec.exception_ids
+ ]
+ )
+ else:
+ rec.exceptions_summary = False
diff --git a/base_exception_user/security/base_exception_security.xml b/base_exception_user/security/base_exception_security.xml
new file mode 100644
index 00000000..0dfbf591
--- /dev/null
+++ b/base_exception_user/security/base_exception_security.xml
@@ -0,0 +1,13 @@
+
+
+
+
+ Exception user
+
+
+
+
+
+
+
+
diff --git a/base_exception_user/tests/__init__.py b/base_exception_user/tests/__init__.py
new file mode 100644
index 00000000..e598bf62
--- /dev/null
+++ b/base_exception_user/tests/__init__.py
@@ -0,0 +1,3 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from . import test_base_exception_user
diff --git a/base_exception_user/tests/common.py b/base_exception_user/tests/common.py
new file mode 100644
index 00000000..0e2f24b7
--- /dev/null
+++ b/base_exception_user/tests/common.py
@@ -0,0 +1,17 @@
+# Copyright 2017 ACSONE SA/NV ()
+# Copyright 2020 Hibou Corp.
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+
+
+def setup_test_model(env, model_clses):
+ model_names = set()
+ for model_cls in model_clses:
+ model = model_cls._build_model(env.registry, env.cr)
+ model_names.add(model._name)
+
+ env.registry.setup_models(env.cr)
+ env.registry.init_models(
+ env.cr,
+ model_names,
+ dict(env.context, update_custom_fields=True),
+ )
diff --git a/base_exception_user/tests/purchase_test.py b/base_exception_user/tests/purchase_test.py
new file mode 100644
index 00000000..e682b25d
--- /dev/null
+++ b/base_exception_user/tests/purchase_test.py
@@ -0,0 +1,31 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+class PurchaseUserTest(models.Model):
+ _name = 'base.exception.test.purchase'
+ _inherit = 'base.exception.test.purchase'
+
+ def button_confirm(self):
+ if self.detect_exceptions():
+ return self._popup_exceptions()
+ super(PurchaseUserTest, self).button_confirm()
+
+ @api.model
+ def _get_popup_action(self):
+ return self.env['ir.actions.act_window'].sudo().create(
+ {'name': 'Outstanding exceptions to manage',
+ 'type': 'ir.actions.act_window',
+ 'view_id': self.env.ref('base_exception.view_exception_rule_confirm').id,
+ 'res_model': 'purchase.test.exception.rule.confirm',
+ 'target': 'new',
+ 'view_mode': 'form',
+ })
+
+
+class PurchaseTestExceptionRuleConfirm(models.TransientModel):
+ _name = 'purchase.test.exception.rule.confirm'
+ _description = 'Exception Rule Confirm Wizard'
+ _inherit = 'exception.rule.confirm'
+
+ related_model_id = fields.Many2one('base.exception.test.purchase')
diff --git a/base_exception_user/tests/test_base_exception_user.py b/base_exception_user/tests/test_base_exception_user.py
new file mode 100644
index 00000000..e078b6ba
--- /dev/null
+++ b/base_exception_user/tests/test_base_exception_user.py
@@ -0,0 +1,128 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from odoo import fields
+from odoo.tests import common, Form
+from odoo.addons.base_exception.tests.purchase_test import PurchaseTest, LineTest
+
+from .common import setup_test_model
+from .purchase_test import PurchaseUserTest, PurchaseTestExceptionRuleConfirm
+
+
+@common.tagged("post_install", "-at_install")
+class TestBaseExceptionUser(common.SavepointCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestBaseExceptionUser, cls).setUpClass()
+ setup_test_model(cls.env, [PurchaseTest, PurchaseUserTest, LineTest, PurchaseTestExceptionRuleConfirm])
+
+ group_id = cls.env.ref('base_exception_user.group_exception_rule_user').id
+ user_dict = {
+ "name": "User test",
+ "login": "tua@example.com",
+ "password": "base-test-passwd",
+ "email": "armande.hruser@example.com",
+ "groups_id": [(6, 0, [group_id])],
+ }
+ cls.user_test = cls.env['res.users'].create(user_dict)
+
+ cls.env['ir.model.access'].create([
+ {'name': 'Test PO Access',
+ 'model_id': cls.env['ir.model'].search([('model', '=', 'base.exception.test.purchase')]).id,
+ 'group_id': group_id,
+ 'perm_read': True,
+ 'perm_write': True,
+ 'perm_create': True,
+ 'perm_unlink': True,
+ },
+ {'name': 'Test PO Line Access',
+ 'model_id': cls.env['ir.model'].search([('model', '=', 'base.exception.test.purchase.line')]).id,
+ 'group_id': group_id,
+ 'perm_read': True,
+ 'perm_write': True,
+ 'perm_create': True,
+ 'perm_unlink': True,
+ },
+ {'name': 'Test PO Wizard Access',
+ 'model_id': cls.env['ir.model'].search([('model', '=', 'purchase.test.exception.rule.confirm')]).id,
+ 'group_id': group_id,
+ 'perm_read': True,
+ 'perm_write': True,
+ 'perm_create': True,
+ 'perm_unlink': True,
+ },
+ ])
+
+
+ cls.base_exception = cls.env["base.exception"]
+ cls.exception_rule = cls.env["exception.rule"]
+ if "test_purchase_ids" not in cls.exception_rule._fields:
+ field = fields.Many2many("base.exception.test.purchase")
+ cls.exception_rule._add_field("test_purchase_ids", field)
+ cls.exception_rule._fields["test_purchase_ids"].depends_context = None
+ cls.exception_confirm = cls.env["exception.rule.confirm"]
+ cls.exception_rule._fields["model"].selection.append(
+ ("base.exception.test.purchase", "Purchase Order")
+ )
+
+ cls.exception_rule._fields["model"].selection.append(
+ ("base.exception.test.purchase.line", "Purchase Order Line")
+ )
+
+ cls.exceptionnozip = cls.env["exception.rule"].create(
+ {
+ "name": "No ZIP code on destination",
+ "sequence": 10,
+ "model": "base.exception.test.purchase",
+ "code": "if not self.partner_id.zip: failed=True",
+ "allow_user_ignore": False,
+ }
+ )
+
+ cls.exceptionno_minorder = cls.env["exception.rule"].create(
+ {
+ "name": "Min order except",
+ "sequence": 10,
+ "model": "base.exception.test.purchase",
+ "code": "if self.amount_total <= 200.0: failed=True",
+ "allow_user_ignore": False,
+ }
+ )
+
+ cls.exceptionno_lineqty = cls.env["exception.rule"].create(
+ {
+ "name": "Qty > 0",
+ "sequence": 10,
+ "model": "base.exception.test.purchase.line",
+ "code": "if obj.qty <= 0: failed=True",
+ "allow_user_ignore": False,
+ }
+ )
+
+ def test_purchase_order_exception_ignore(self):
+ partner = self.env.ref("base.res_partner_1")
+ partner.zip = False
+ potest1 = self.env['base.exception.test.purchase'].with_user(self.user_test).create(
+ {
+ "name": "Test base exception to basic purchase",
+ "partner_id": partner.id,
+ "line_ids": [
+ (0, 0, {"name": "line test", "amount": 120.0, "qty": 1.5})
+ ],
+ }
+ )
+ # Block because of exception during validation: return exception wizard
+ action = potest1.button_confirm()
+ self.assertEqual(action.get('res_model'), 'purchase.test.exception.rule.confirm')
+ wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
+ self.assertFalse(wizard.show_ignore_button)
+ self.assertFalse(wizard.action_ignore())
+
+ self.exceptionnozip.allow_user_ignore = True
+ self.exceptionno_minorder.allow_user_ignore = True
+ self.exceptionno_lineqty.allow_user_ignore = True
+
+ action = potest1.button_confirm()
+ wizard = Form(self.env[action['res_model']].with_user(self.user_test).with_context(action['context'])).save()
+ self.assertTrue(wizard.show_ignore_button)
+ action = wizard.action_ignore()
+ self.assertEqual(action.get('type'), 'ir.actions.act_window_close')
diff --git a/base_exception_user/views/base_exception_views.xml b/base_exception_user/views/base_exception_views.xml
new file mode 100644
index 00000000..08e5f6b1
--- /dev/null
+++ b/base_exception_user/views/base_exception_views.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ exception.rule.tree.inherit.user
+ exception.rule
+
+
+
+
+
+
+
+
+
+ exception.rule.form.inherit.user
+ exception.rule
+
+
+
+
+
+
+
+
+
diff --git a/base_exception_user/wizard/__init__.py b/base_exception_user/wizard/__init__.py
new file mode 100644
index 00000000..8436ddb2
--- /dev/null
+++ b/base_exception_user/wizard/__init__.py
@@ -0,0 +1,3 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+from . import base_exception_confirm
diff --git a/base_exception_user/wizard/base_exception_confirm.py b/base_exception_user/wizard/base_exception_confirm.py
new file mode 100644
index 00000000..97f7b9e1
--- /dev/null
+++ b/base_exception_user/wizard/base_exception_confirm.py
@@ -0,0 +1,38 @@
+# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
+
+import html
+from odoo import api, fields, models
+
+
+class ExceptionRuleConfirm(models.AbstractModel):
+ _inherit = 'exception.rule.confirm'
+
+ show_ignore_button = fields.Boolean('Allow User Ignore', compute='_compute_show_ignore_button')
+
+ @api.depends('exception_ids')
+ def _compute_show_ignore_button(self):
+ for wiz in self:
+ wiz.show_ignore_button = (self.env.user.has_group('base_exception_user.group_exception_rule_user') and
+ all(wiz.exception_ids.mapped('allow_user_ignore')))
+
+ def action_confirm(self):
+ if self.ignore and 'message_ids' in self.related_model_id:
+ exceptions_summary = '' % ''.join(
+ ['%s: %s' % tuple(map(html.escape, (e.name, e.description or ''))) for e in
+ self.exception_ids])
+ msg = 'Exceptions ignored:
' + exceptions_summary
+ self.related_model_id.message_post(body=msg)
+ return super().action_confirm()
+
+
+ def action_ignore(self):
+ self.ensure_one()
+ if self.show_ignore_button:
+ if 'message_ids' in self.related_model_id:
+ msg = 'Exceptions ignored:
' + self.related_model_id.exceptions_summary
+ self.related_model_id.message_post(body=msg)
+ return self._action_ignore()
+ return False
+
+ def _action_ignore(self):
+ return {'type': 'ir.actions.act_window_close'}
diff --git a/base_exception_user/wizard/base_exception_confirm_view.xml b/base_exception_user/wizard/base_exception_confirm_view.xml
new file mode 100644
index 00000000..4252ff15
--- /dev/null
+++ b/base_exception_user/wizard/base_exception_confirm_view.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ Exceptions Rules
+ exception.rule.confirm
+
+
+
+
+
+
+
+
+
+
+
+