diff --git a/sale_order_type_automation/models/sale_order.py b/sale_order_type_automation/models/sale_order.py index 61fe83d61..14bac6dd2 100644 --- a/sale_order_type_automation/models/sale_order.py +++ b/sale_order_type_automation/models/sale_order.py @@ -59,31 +59,29 @@ def run_invoicing_atomation(self): ): if rec.env.context.get("commit_invoice_automation"): rec.env.cr.commit() + if rec.type_id.invoice_validate_domain: + domain = safe_eval( + rec.type_id.invoice_validate_domain, + { + "datetime": safe_eval_datetime, + "context_today": lambda: fields.Date.context_today(rec), + "relativedelta": safe_eval_dateutil.relativedelta.relativedelta, + }, + ) + invoices_to_validate = (invoices - invoices.filtered_domain(domain)) if domain else invoices + else: + invoices_to_validate = invoices try: - if rec.type_id.invoice_validate_domain: - domain = safe_eval( - rec.type_id.invoice_validate_domain, - { - "datetime": safe_eval_datetime, - "context_today": lambda: fields.Date.context_today(rec), - "relativedelta": safe_eval_dateutil.relativedelta.relativedelta, - }, - ) - invoices_to_validate = (invoices - invoices.filtered_domain(domain)) if domain else invoices - else: - invoices_to_validate = invoices - invoices_to_validate.sudo().action_post() - for invoice in invoices_to_validate: # to avoid "expected singleton" error - if ( - invoice.name - and not invoice.line_ids.mapped("move_name") - and invoice.name not in invoice.line_ids.mapped("move_name") - ): - invoice.env.add_to_compute(invoice.line_ids._fields["move_name"], invoice.line_ids) + with rec.env.cr.savepoint(): + invoices_to_validate.sudo().action_post() + for invoice in invoices_to_validate: # to avoid "expected singleton" error + if ( + invoice.name + and not invoice.line_ids.mapped("move_name") + and invoice.name not in invoice.line_ids.mapped("move_name") + ): + invoice.env.add_to_compute(invoice.line_ids._fields["move_name"], invoice.line_ids) except Exception as error: - rec.env.cr.rollback() - if not rec.env.context.get("commit_invoice_automation"): - raise error message = _( "We couldn't validate the automatically created " "invoices (ids %s), you will need to validate them" diff --git a/sale_order_type_automation/tests/__init__.py b/sale_order_type_automation/tests/__init__.py index 53cf081b2..b82a80716 100644 --- a/sale_order_type_automation/tests/__init__.py +++ b/sale_order_type_automation/tests/__init__.py @@ -4,3 +4,4 @@ ############################################################################## from . import test_sale_order_type_automation +from . import test_invoice_automation_error diff --git a/sale_order_type_automation/tests/test_invoice_automation_error.py b/sale_order_type_automation/tests/test_invoice_automation_error.py new file mode 100644 index 000000000..a0648ac82 --- /dev/null +++ b/sale_order_type_automation/tests/test_invoice_automation_error.py @@ -0,0 +1,74 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from unittest.mock import patch + +from odoo.addons.sale.tests.common import TestSaleCommon +from odoo.tests import tagged + + +@tagged("post_install", "-at_install") +class TestInvoiceAutomationError(TestSaleCommon): + @classmethod + def setup_independent_user(cls): + # Keep superuser context for setup in deployments with stricter product ACLs. + return None + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.validate_invoice_type = cls.env["sale.order.type"].create( + { + "name": "Test Validate Invoice Automation", + "invoicing_atomation": "validate_invoice", + } + ) + sale_exception_installed = cls.env["sale.order"]._fields.get("ignore_exception") + if sale_exception_installed: + cls.env["exception.rule"].search([("active", "=", True)]).write({"active": False}) + + def _create_so(self): + product = self.company_data["product_service_order"] + return self.env["sale.order"].create( + { + "partner_id": self.partner_a.id, + "type_id": self.validate_invoice_type.id, + "order_line": [ + ( + 0, + 0, + { + "product_id": product.id, + "product_uom_qty": 1.0, + "price_unit": 100.0, + }, + ) + ], + } + ) + + def test_arca_timeout_keeps_invoice_and_logs_error(self): + """Al fallar action_post (ej. timeout ARCA), la factura queda en draft y + se registra el error en el chatter de la factura y de la orden de venta.""" + so = self._create_so() + AccountMove = self.env["account.move"] + with patch.object( + type(AccountMove), + "action_post", + side_effect=Exception("ARCA timeout"), + ): + so.action_confirm() + + self.assertEqual(len(so.invoice_ids), 1, "La factura debe haberse creado") + invoice = so.invoice_ids + self.assertTrue(invoice.exists(), "La factura no debe haberse borrado por el savepoint") + self.assertEqual(invoice.state, "draft", "La factura debe quedar en draft si action_post falla") + self.assertTrue( + any("ARCA timeout" in (m.body or "") for m in invoice.message_ids), + "Se esperaba mensaje de error en el chatter de la factura", + ) + self.assertTrue( + any("ARCA timeout" in (m.body or "") for m in so.message_ids), + "Se esperaba mensaje de error en el chatter de la orden de venta", + )