diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index a3fcaca19ee..2032a085192 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -7,6 +7,7 @@ from odoo.tools.misc import formatLang, format_date, get_lang from datetime import date, timedelta from collections import defaultdict +from contextlib import contextmanager from itertools import zip_longest from hashlib import sha256 from json import dumps @@ -2758,6 +2759,30 @@ class AccountMove(models.Model): return reverse_moves + def _action_invoice_ready_to_be_sent(self): + """ Hook allowing custom code when an invoice becomes ready to be sent by mail to the customer. + For example, when an EDI document must be sent to the government and be signed by it. + """ + + def _is_ready_to_be_sent(self): + """ Helper telling if a journal entry is ready to be sent by mail to the customer. + + :return: True if the invoice is ready, False otherwise. + """ + self.ensure_one() + return True + + @contextmanager + def _send_only_when_ready(self): + moves_not_ready = self.filtered(lambda x: not x._is_ready_to_be_sent()) + + try: + yield + finally: + moves_now_ready = moves_not_ready.filtered(lambda x: x._is_ready_to_be_sent()) + if moves_now_ready: + moves_now_ready._action_invoice_ready_to_be_sent() + def open_reconcile_view(self): return self.line_ids.open_reconcile_view() diff --git a/addons/account_edi/models/account_edi_document.py b/addons/account_edi/models/account_edi_document.py index 3ccdc8f8b80..be316c70e94 100644 --- a/addons/account_edi/models/account_edi_document.py +++ b/addons/account_edi/models/account_edi_document.py @@ -161,8 +161,10 @@ class AccountEdiDocument(models.Model): state = documents[0].state if doc_type == 'invoice': if state == 'to_send': - edi_result = edi_format._post_invoice_edi(documents.move_id) - _postprocess_post_edi_results(documents, edi_result) + invoices = documents.move_id + with invoices._send_only_when_ready(): + edi_result = edi_format._post_invoice_edi(invoices) + _postprocess_post_edi_results(documents, edi_result) elif state == 'to_cancel': edi_result = edi_format._cancel_invoice_edi(documents.move_id) _postprocess_cancel_edi_results(documents, edi_result) diff --git a/addons/account_edi/models/account_move.py b/addons/account_edi/models/account_move.py index a32422054bc..170f0621054 100644 --- a/addons/account_edi/models/account_move.py +++ b/addons/account_edi/models/account_move.py @@ -429,6 +429,17 @@ class AccountMove(models.Model): self.env['account.edi.document'].create(edi_document_vals_list) self.edi_document_ids._process_documents_no_web_services() + def _is_ready_to_be_sent(self): + # OVERRIDE + # Prevent a mail to be sent to the customer if the EDI document is not sent. + res = super()._is_ready_to_be_sent() + + if not res: + return False + + edi_documents_to_send = self.edi_document_ids.filtered(lambda x: x.state == 'to_send') + return not bool(edi_documents_to_send) + def _post(self, soft=True): # OVERRIDE # Set the electronic document to be posted and post immediately for synchronous formats. diff --git a/addons/account_edi/tests/test_edi.py b/addons/account_edi/tests/test_edi.py index 4811e7a1f37..d1177237f6a 100644 --- a/addons/account_edi/tests/test_edi.py +++ b/addons/account_edi/tests/test_edi.py @@ -227,3 +227,68 @@ class TestAccountEdi(AccountEdiTestCommon, CronMixinCase): self.env.ref('account_edi.ir_cron_edi_network').method_direct_trigger() self.assertEqual(len(capt.records), 2, "Not all records have been processed in this run, the cron should " "re-trigger itself to process some more later") + + def test_prepare_edi_vals_to_export(self): + """ + Test _prepare_edi_vals_to_export return values + in the proper format + + tag_ids should be a set of id and tags a proper recordset + """ + account_tag = self.env['account.account.tag'].create({ + "applicability": "taxes", + "country_id": self.env.ref('base.us').id, + "name": "Test Tag", + }) + tax = self.env['account.tax'].create({ + "amount": 15, + "amount_type": "percent", + "description": "15%", + "country_id": self.env.ref('base.us').id, + "invoice_repartition_line_ids": [ + Command.create({ + "factor_percent": 100, + "repartition_type": "base", + "sequence": 1, + }), + Command.create({ + "factor_percent": 100, + "repartition_type": "tax", + "sequence": 1, + "tag_ids": [Command.link(account_tag.id)] + }) + ], + "name": "Test", + "tax_exigibility": "on_invoice", + "type_tax_use": "sale" + }) + + invoices = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'invoice_date': '2019-01-01', + 'date': '2019-01-01', + 'partner_id': self.partner_a.id, + 'invoice_line_ids': [Command.create({ + 'product_id': self.product_a.id, + 'tax_ids': [Command.link(tax.id)] + })], + }) + vals = invoices._prepare_edi_vals_to_export() + self.assertEqual( + vals['invoice_line_vals_list'][0]['tax_detail_vals_list'][0]['tag_ids'], set(account_tag.ids)) + self.assertEqual( + vals['invoice_line_vals_list'][0]['tax_detail_vals_list'][0]['tags'].name, "Test Tag") + + def test_invoice_ready_to_be_sent(self): + def _is_needed_for_invoice(edi_format, invoice): + return True + + with self.mock_edi( + _needs_web_services_method=_generate_mocked_needs_web_services(True), + _is_required_for_invoice_method=_is_needed_for_invoice, + ): + self.invoice.action_post() + doc = self.invoice._get_edi_document(self.edi_format) + self.assertFalse(self.invoice._is_ready_to_be_sent()) + doc._process_documents_web_services(with_commit=False) + self.assertTrue(self.invoice._is_ready_to_be_sent())