diff --git a/addons/account_edi/models/account_edi_document.py b/addons/account_edi/models/account_edi_document.py
index d878907fc3d..a06fd937c4b 100644
--- a/addons/account_edi/models/account_edi_document.py
+++ b/addons/account_edi/models/account_edi_document.py
@@ -52,10 +52,10 @@ class AccountEdiDocument(models.Model):
config_errors = doc.edi_format_id._check_move_configuration(move)
if config_errors:
res = base64.b64encode('\n'.join(config_errors).encode('UTF-8'))
- elif move.is_invoice(include_receipts=True) and doc.edi_format_id._is_required_for_invoice(move):
- res = base64.b64encode(doc.edi_format_id._get_invoice_edi_content(doc.move_id))
- elif move.payment_id and doc.edi_format_id._is_required_for_payment(move):
- res = base64.b64encode(doc.edi_format_id._get_payment_edi_content(doc.move_id))
+ else:
+ move_applicability = doc.edi_format_id._get_move_applicability(move)
+ if move_applicability and move_applicability.get('edi_content'):
+ res = base64.b64encode(move_applicability['edi_content'](move))
doc.edi_content = res
def action_export_xml(self):
@@ -71,52 +71,44 @@ class AccountEdiDocument(models.Model):
doc_type (invoice or payment) and company_id AND the edi_format_id supports batching, they are grouped
into a single job.
- :returns: A list of tuples (documents, doc_type)
- * documents: The documents related to this job. If edi_format_id does not support batch, length is one
- * doc_type: Are the moves of this job invoice or payments ?
+ :returns: [{
+ 'documents': account.edi.document,
+ 'method_to_call': str,
+ }]
"""
-
# Classify jobs by (edi_format, edi_doc.state, doc_type, move.company_id, custom_key)
to_process = {}
- documents = self.filtered(lambda d: d.state in ('to_send', 'to_cancel') and d.blocking_level != 'error')
- for edi_doc in documents:
- move = edi_doc.move_id
- edi_format = edi_doc.edi_format_id
- if move.is_invoice(include_receipts=True):
- doc_type = 'invoice'
- elif move.payment_id or move.statement_line_id:
- doc_type = 'payment'
- else:
- continue
+ for state, edi_flow in (('to_send', 'post'), ('to_cancel', 'cancel')):
+ documents = self.filtered(lambda d: d.state == state and d.blocking_level != 'error')
+ for edi_doc in documents:
+ edi_format = edi_doc.edi_format_id
+ move = edi_doc.move_id
+ move_applicability = edi_doc.edi_format_id._get_move_applicability(move) or {}
- custom_key = edi_format._get_batch_key(edi_doc.move_id, edi_doc.state)
- key = (edi_format, edi_doc.state, doc_type, move.company_id, custom_key)
- to_process.setdefault(key, self.env['account.edi.document'])
- to_process[key] |= edi_doc
-
- # Order payments/invoice and create batches.
- invoices = []
- payments = []
- for key, documents in to_process.items():
- edi_format, state, doc_type, company_id, custom_key = key
- target = invoices if doc_type == 'invoice' else payments
- batch = self.env['account.edi.document']
- for doc in documents:
- if edi_format._support_batching(move=doc.move_id, state=state, company=company_id):
- batch |= doc
+ batching_key = [edi_format, state, move.company_id]
+ custom_batching_key = f'{edi_flow}_batching'
+ if move_applicability.get(custom_batching_key):
+ batching_key += list(move_applicability[custom_batching_key](move))
else:
- target.append((doc, doc_type))
- if batch:
- target.append((batch, doc_type))
- return invoices + payments
+ batching_key.append(move.id)
+
+ batch = to_process.setdefault(tuple(batching_key), {
+ 'documents': self.env['account.edi.document'],
+ 'method_to_call': move_applicability.get(edi_flow),
+ })
+ batch['documents'] |= edi_doc
+
+ return list(to_process.values())
@api.model
- def _process_job(self, documents, doc_type):
+ def _process_job(self, job):
"""Post or cancel move_id (invoice or payment) by calling the related methods on edi_format_id.
Invoices are processed before payments.
- :param documents: The documents related to this job. If edi_format_id does not support batch, length is one
- :param doc_type: Are the moves of this job invoice or payments ?
+ :param job: {
+ 'documents': account.edi.document,
+ 'method_to_call': str,
+ }
"""
def _postprocess_post_edi_results(documents, edi_result):
attachments_to_unlink = self.env['ir.attachment']
@@ -182,38 +174,36 @@ class AccountEdiDocument(models.Model):
# supposed to have any traceability from the user.
attachments_to_unlink.sudo().unlink()
+ documents = job['documents']
+ if job['method_to_call']:
+ method_to_call = job['method_to_call']
+ else:
+ method_to_call = lambda moves: {move: {'success': True} for move in moves}
documents.edi_format_id.ensure_one() # All account.edi.document of a job should have the same edi_format_id
documents.move_id.company_id.ensure_one() # All account.edi.document of a job should be from the same company
if len(set(doc.state for doc in documents)) != 1:
raise ValueError('All account.edi.document of a job should have the same state')
- edi_format = documents.edi_format_id
state = documents[0].state
documents.move_id.line_ids.flush_recordset() # manual flush for tax details
- if doc_type == 'invoice':
- if state == 'to_send':
- 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)
-
- elif doc_type == 'payment':
- if state == 'to_send':
- edi_result = edi_format._post_payment_edi(documents.move_id)
- _postprocess_post_edi_results(documents, edi_result)
- elif state == 'to_cancel':
- edi_result = edi_format._cancel_payment_edi(documents.move_id)
- _postprocess_cancel_edi_results(documents, edi_result)
+ moves = documents.move_id
+ if state == 'to_send':
+ if all(move.is_invoice(include_receipts=True) for move in moves):
+ with moves._send_only_when_ready():
+ edi_result = method_to_call(moves)
+ else:
+ edi_result = method_to_call(moves)
+ _postprocess_post_edi_results(documents, edi_result)
+ elif state == 'to_cancel':
+ edi_result = method_to_call(moves)
+ _postprocess_cancel_edi_results(documents, edi_result)
def _process_documents_no_web_services(self):
""" Post and cancel all the documents that don't need a web service.
"""
jobs = self.filtered(lambda d: not d.edi_format_id._needs_web_services())._prepare_jobs()
- for documents, doc_type in jobs:
- self._process_job(documents, doc_type)
+ for job in jobs:
+ self._process_job(job)
def _process_documents_web_services(self, job_count=None, with_commit=True):
''' Post and cancel all the documents that need a web service.
@@ -225,7 +215,8 @@ class AccountEdiDocument(models.Model):
all_jobs = self.filtered(lambda d: d.edi_format_id._needs_web_services())._prepare_jobs()
jobs_to_process = all_jobs[0:job_count] if job_count else all_jobs
- for documents, doc_type in jobs_to_process:
+ for job in jobs_to_process:
+ documents = job['documents']
move_to_lock = documents.move_id
attachments_potential_unlink = documents.attachment_id.filtered(lambda a: not a.res_model and not a.res_id)
try:
@@ -245,7 +236,7 @@ class AccountEdiDocument(models.Model):
continue
else:
raise e
- self._process_job(documents, doc_type)
+ self._process_job(job)
if with_commit and len(jobs_to_process) > 1:
self.env.cr.commit()
diff --git a/addons/account_edi/models/account_edi_format.py b/addons/account_edi/models/account_edi_format.py
index 93117c22cfd..ff7613ea955 100644
--- a/addons/account_edi/models/account_edi_format.py
+++ b/addons/account_edi/models/account_edi_format.py
@@ -28,7 +28,6 @@ class AccountEdiFormat(models.Model):
('unique_code', 'unique (code)', 'This code already exists')
]
-
####################################################
# Low-level methods
####################################################
@@ -51,37 +50,18 @@ class AccountEdiFormat(models.Model):
# Export method to override based on EDI Format
####################################################
- def _get_invoice_edi_content(self, move):
- ''' Create a bytes literal of the file content representing the invoice - to be overridden by the EDI Format
- :returns: bytes literal of the content generated (typically XML).
- '''
- return b''
+ def _get_move_applicability(self, move):
+ """ Core function for the EDI processing: it first checks whether the EDI format is applicable on a given
+ move, if so, it then returns a dictionary containing the functions to call for this move.
- def _get_payment_edi_content(self, move):
- ''' Create a bytes literal of the file content representing the payment - to be overridden by the EDI Format
- :returns: bytes literal of the content generated (typically XML).
- '''
- return b''
-
- def _is_required_for_invoice(self, invoice):
- """ Indicate if this EDI must be generated for the invoice passed as parameter.
-
- :param invoice: An account.move having the invoice type.
- :returns: True if the EDI must be generated, False otherwise.
+ :return: dict mapping str to function (callable)
+ * post: function called for edi.documents with state 'to_send' (post flow)
+ * cancel: function called for edi.documents with state 'to_cancel' (cancel flow)
+ * post_batching: function returning the batching key for the post flow
+ * cancel_batching: function returning the batching key for the cancel flow
+ * edi_content: function called when computing the edi_content for an edi.document
"""
- # TO OVERRIDE
self.ensure_one()
- return True
-
- def _is_required_for_payment(self, payment):
- """ Indicate if this EDI must be generated for the payment passed as parameter.
-
- :param payment: An account.move linked to either an account.payment, either an account.bank.statement.line.
- :returns: True if the EDI must be generated, False otherwise.
- """
- # TO OVERRIDE
- self.ensure_one()
- return False
def _needs_web_services(self):
""" Indicate if the EDI must be generated asynchronously through to some web services.
@@ -111,33 +91,6 @@ class AccountEdiFormat(models.Model):
"""
return True
- def _support_batching(self, move, state, company):
- """ Indicate if we can send multiple documents in the same time to the web services.
- If True, the _post_%s_edi methods will get multiple documents in the same time.
- Otherwise, these methods will be called with only one record at a time.
-
- :param move: The move that we are trying to batch.
- :param state: The EDI state of the move.
- :param company: The company with which we are sending the EDI.
- :returns: True if batching is supported, False otherwise.
- """
- # TO OVERRIDE
- return False
-
- def _get_batch_key(self, move, state):
- """ Returns a tuple that will be used as key to partitionnate the invoices/payments when creating batches
- with multiple invoices/payments.
- The type of move (invoice or payment), its company_id, its edi state and the edi_format are used by default, if
- no further partition is needed for this format, this method should return (). It's not necessary to repeat those
- fields in the custom key.
-
- :param move: The move to batch.
- :param state: The EDI state of the move.
- :returns: The key to be used when partitionning the batches.
- """
- move.ensure_one()
- return ()
-
def _check_move_configuration(self, move):
""" Checks the move and relevant records for potential error (missing data, etc).
@@ -147,59 +100,6 @@ class AccountEdiFormat(models.Model):
# TO OVERRIDE
return []
- def _post_invoice_edi(self, invoices):
- """ Create the file content representing the invoice (and calls web services if necessary).
-
- :param invoices: A list of invoices to post.
- :returns: A dictionary with the invoice as key and as value, another dictionary:
- * success: True if the edi was successfully posted.
- * attachment: The attachment representing the invoice in this edi_format.
- * error: An error if the edi was not successfully posted.
- * blocking_level: (optional) How bad is the error (how should the edi flow be blocked ?)
- """
- # TO OVERRIDE
- self.ensure_one()
- return {}
-
- def _cancel_invoice_edi(self, invoices):
- """Calls the web services to cancel the invoice of this document.
-
- :param invoices: A list of invoices to cancel.
- :returns: A dictionary with the invoice as key and as value, another dictionary:
- * success: True if the invoice was successfully cancelled.
- * error: An error if the edi was not successfully cancelled.
- * blocking_level: (optional) How bad is the error (how should the edi flow be blocked ?)
- """
- # TO OVERRIDE
- self.ensure_one()
- return {invoice: {'success': True} for invoice in invoices} # By default, cancel succeeds doing nothing.
-
- def _post_payment_edi(self, payments):
- """ Create the file content representing the payment (and calls web services if necessary).
-
- :param payments: The payments to post.
- :returns: A dictionary with the payment as key and as value, another dictionary:
- * attachment: The attachment representing the payment in this edi_format if the edi was successfully posted.
- * error: An error if the edi was not successfully posted.
- * blocking_level: (optional) How bad is the error (how should the edi flow be blocked ?)
- """
- # TO OVERRIDE
- self.ensure_one()
- return {}
-
- def _cancel_payment_edi(self, payments):
- """Calls the web services to cancel the payment of this document.
-
- :param payments: A list of payments to cancel.
- :returns: A dictionary with the payment as key and as value, another dictionary:
- * success: True if the payment was successfully cancelled.
- * error: An error if the edi was not successfully cancelled.
- * blocking_level: (optional) How bad is the error (how should the edi flow be blocked ?)
- """
- # TO OVERRIDE
- self.ensure_one()
- return {payment: {'success': True} for payment in payments} # By default, cancel succeeds doing nothing.
-
####################################################
# Import methods to override based on EDI Format
####################################################
diff --git a/addons/account_edi/models/account_move.py b/addons/account_edi/models/account_move.py
index 81fa9d8ff43..774ea6f2807 100644
--- a/addons/account_edi/models/account_move.py
+++ b/addons/account_edi/models/account_move.py
@@ -93,10 +93,11 @@ class AccountMove(models.Model):
for move in self:
for doc in move.edi_document_ids:
+ move_applicability = doc.edi_format_id._get_move_applicability(move)
if doc.edi_format_id._needs_web_services() \
- and doc.state in ('sent', 'to_cancel') \
- and move.is_invoice(include_receipts=True) \
- and doc.edi_format_id._is_required_for_invoice(move):
+ and doc.state in ('sent', 'to_cancel') \
+ and move_applicability \
+ and move_applicability.get('cancel'):
move.show_reset_to_draft_button = False
break
@@ -107,20 +108,28 @@ class AccountMove(models.Model):
move.edi_show_cancel_button = False
continue
- move.edi_show_cancel_button = any([doc.edi_format_id._needs_web_services()
- and doc.state == 'sent'
- and move.is_invoice(include_receipts=True)
- and doc.edi_format_id._is_required_for_invoice(move)
- for doc in move.edi_document_ids])
+ move.edi_show_cancel_button = False
+ for doc in move.edi_document_ids:
+ move_applicability = doc.edi_format_id._get_move_applicability(move)
+ if doc.edi_format_id._needs_web_services() \
+ and doc.state == 'sent' \
+ and move_applicability \
+ and move_applicability.get('cancel'):
+ move.edi_show_cancel_button = True
+ break
@api.depends('edi_document_ids.state')
def _compute_edi_show_abandon_cancel_button(self):
for move in self:
- move.edi_show_abandon_cancel_button = any(doc.edi_format_id._needs_web_services()
- and doc.state == 'to_cancel'
- and move.is_invoice(include_receipts=True)
- and doc.edi_format_id._is_required_for_invoice(move)
- for doc in move.edi_document_ids)
+ move.edi_show_abandon_cancel_button = False
+ for doc in move.edi_document_ids:
+ move_applicability = doc.edi_format_id._get_move_applicability(move)
+ if doc.edi_format_id._needs_web_services() \
+ and doc.state == 'to_cancel' \
+ and move_applicability \
+ and move_applicability.get('cancel'):
+ move.edi_show_abandon_cancel_button = True
+ break
####################################################
# Export Electronic Document
@@ -250,8 +259,9 @@ class AccountMove(models.Model):
edi_formats = self.env['account.edi.format'].browse(edi_formats.ids) # Avoid duplicates
for edi_format in edi_formats:
existing_edi_document = payment.edi_document_ids.filtered(lambda x: x.edi_format_id == edi_format)
+ move_applicability = edi_format._get_move_applicability(payment)
- if edi_format._is_required_for_payment(payment):
+ if move_applicability:
if existing_edi_document:
existing_edi_document.write({
'state': 'to_send',
@@ -293,9 +303,9 @@ class AccountMove(models.Model):
edi_document_vals_list = []
for move in posted:
for edi_format in move.journal_id.edi_format_ids:
- is_edi_needed = move.is_invoice(include_receipts=False) and edi_format._is_required_for_invoice(move)
+ move_applicability = edi_format._get_move_applicability(move)
- if is_edi_needed:
+ if move_applicability:
errors = edi_format._check_move_configuration(move)
if errors:
raise UserError(_("Invalid invoice configuration:\n\n%s") % '\n'.join(errors))
@@ -352,11 +362,11 @@ class AccountMove(models.Model):
for move in self:
is_move_marked = False
for doc in move.edi_document_ids:
+ move_applicability = doc.edi_format_id._get_move_applicability(move)
if doc.edi_format_id._needs_web_services() \
- and doc.attachment_id \
and doc.state == 'sent' \
- and move.is_invoice(include_receipts=True) \
- and doc.edi_format_id._is_required_for_invoice(move):
+ and move_applicability \
+ and move_applicability.get('cancel'):
to_cancel_documents |= doc
is_move_marked = True
if is_move_marked:
@@ -371,9 +381,8 @@ class AccountMove(models.Model):
for move in self:
is_move_marked = False
for doc in move.edi_document_ids:
- if doc.state == 'to_cancel' \
- and move.is_invoice(include_receipts=True) \
- and doc.edi_format_id._is_required_for_invoice(move):
+ move_applicability = doc.edi_format_id._get_move_applicability(move)
+ if doc.state == 'to_cancel' and move_applicability and move_applicability.get('cancel'):
documents |= doc
is_move_marked = True
if is_move_marked:
diff --git a/addons/account_edi/tests/common.py b/addons/account_edi/tests/common.py
index 14f08848ca8..f76b884c503 100644
--- a/addons/account_edi/tests/common.py
+++ b/addons/account_edi/tests/common.py
@@ -5,7 +5,6 @@ from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from contextlib import contextmanager
from unittest.mock import patch
-from unittest import mock
import base64
@@ -14,12 +13,17 @@ def _generate_mocked_needs_web_services(needs_web_services):
return lambda edi_format: needs_web_services
-def _generate_mocked_support_batching(support_batching):
- return lambda edi_format, move, state, company: support_batching
-
-
-def _mocked_get_batch_key(edi_format, move, state):
- return ()
+def _mocked_get_move_applicability(edi_format, move):
+ if move.is_invoice():
+ return {
+ 'post': edi_format._post_invoice_edi,
+ 'cancel': edi_format._cancel_invoice_edi,
+ }
+ elif move.payment_id or move.statement_line_id:
+ return {
+ 'post': edi_format._post_payment_edi,
+ 'cancel': edi_format._cancel_invoice_edi,
+ }
def _mocked_check_move_configuration_success(edi_format, move):
@@ -30,48 +34,10 @@ def _mocked_check_move_configuration_fail(edi_format, move):
return ['Fake error (mocked)']
-def _mocked_post(edi_format, invoices):
- res = {}
- for invoice in invoices:
- attachment = edi_format.env['ir.attachment'].create({
- 'name': 'mock_simple.xml',
- 'datas': base64.encodebytes(b""),
- 'mimetype': 'application/xml'
- })
- res[invoice] = {'success': True, 'attachment': attachment}
- return res
-
-
-def _mocked_post_two_steps(edi_format, invoices):
- # For this test, we use the field ref to know if the first step is already done or not.
- # Typically, a technical field for the reference of the upload to the web-service will
- # be saved on the invoice.
- invoices_no_ref = invoices.filtered(lambda i: not i.ref)
- if len(invoices_no_ref) == len(invoices): # first step
- invoices_no_ref.ref = 'test_ref'
- return {invoice: {} for invoice in invoices}
- elif len(invoices_no_ref) == 0: # second step
- res = {}
- for invoice in invoices:
- attachment = edi_format.env['ir.attachment'].create({
- 'name': 'mock_simple.xml',
- 'datas': base64.encodebytes(b""),
- 'mimetype': 'application/xml'
- })
- res[invoice] = {'success': True, 'attachment': attachment}
- return res
- else:
- raise ValueError('wrong use of "_mocked_post_two_steps"')
-
-
def _mocked_cancel_success(edi_format, invoices):
return {invoice: {'success': True} for invoice in invoices}
-def _mocked_cancel_failed(edi_format, invoices):
- return {invoice: {'error': 'Faked error (mocked)'} for invoice in invoices}
-
-
class AccountEdiTestCommon(AccountTestInvoicingCommon):
@classmethod
@@ -93,41 +59,33 @@ class AccountEdiTestCommon(AccountTestInvoicingCommon):
# EDI helpers
####################################################
+ def _create_fake_edi_attachment(self):
+ return self.env['ir.attachment'].create({
+ 'name': '_create_fake_edi_attachment.xml',
+ 'datas': base64.encodebytes(b""),
+ 'mimetype': 'application/xml'
+ })
+
+ @contextmanager
+ def with_custom_method(self, method_name, method_content):
+ path = f'odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat.{method_name}'
+ with patch(path, new=method_content, create=not hasattr(self.env['account.edi.format'], method_name)):
+ yield
+
@contextmanager
def mock_edi(self,
- _is_required_for_invoice_method=lambda edi_format, invoice: True,
- _is_required_for_payment_method=lambda edi_format, invoice: True,
- _support_batching_method=_generate_mocked_support_batching(False),
- _get_batch_key_method=_mocked_get_batch_key,
+ _get_move_applicability_method=_mocked_get_move_applicability,
_needs_web_services_method=_generate_mocked_needs_web_services(False),
_check_move_configuration_method=_mocked_check_move_configuration_success,
- _post_invoice_edi_method=_mocked_post,
- _cancel_invoice_edi_method=_mocked_cancel_success,
- _post_payment_edi_method=_mocked_post,
- _cancel_payment_edi_method=_mocked_cancel_success,
):
try:
- with patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._is_required_for_invoice',
- new=_is_required_for_invoice_method), \
- patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._is_required_for_payment',
- new=_is_required_for_payment_method), \
- patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._needs_web_services',
+ with patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._needs_web_services',
new=_needs_web_services_method), \
- patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._support_batching',
- new=_support_batching_method), \
- patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._get_batch_key',
- new=_get_batch_key_method), \
patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._check_move_configuration',
new=_check_move_configuration_method), \
- patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._post_invoice_edi',
- new=_post_invoice_edi_method), \
- patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._cancel_invoice_edi',
- new=_cancel_invoice_edi_method), \
- patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._post_payment_edi',
- new=_post_payment_edi_method), \
- patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._cancel_payment_edi',
- new=_cancel_payment_edi_method):
+ patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._get_move_applicability',
+ new=_get_move_applicability_method):
yield
finally:
diff --git a/addons/account_edi/tests/test_edi.py b/addons/account_edi/tests/test_edi.py
index b48a927adf5..d7441e45b27 100644
--- a/addons/account_edi/tests/test_edi.py
+++ b/addons/account_edi/tests/test_edi.py
@@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import Command
-from odoo.addons.account_edi.tests.common import AccountEdiTestCommon, _mocked_post_two_steps, _generate_mocked_needs_web_services, _mocked_cancel_failed, _generate_mocked_support_batching
-from unittest.mock import patch
+from odoo.addons.account_edi.tests.common import AccountEdiTestCommon
from odoo.addons.base.tests.test_ir_cron import CronMixinCase
from odoo.tests import tagged
@@ -15,197 +13,130 @@ class TestAccountEdi(AccountEdiTestCommon, CronMixinCase):
def setUpClass(cls, chart_template_ref=None, edi_format_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref, edi_format_ref=edi_format_ref)
- cls.invoice = cls.init_invoice('out_invoice', products=cls.product_a + cls.product_b)
+ cls.env['account.edi.document'].search([]).unlink()
+ cls.env['account.edi.format'].search([]).unlink()
+
+ cls.test_edi_format = cls.env['account.edi.format'].sudo().create({
+ 'name': 'test_edi_format',
+ 'code': 'test_edi_format',
+ })
+ cls.company_data['default_journal_sale'].edi_format_ids |= cls.test_edi_format
def test_export_edi(self):
- self.assertEqual(len(self.invoice.edi_document_ids), 0)
- self.invoice.action_post()
- self.assertEqual(len(self.invoice.edi_document_ids), 1)
+ invoice = self.init_invoice('out_invoice', products=self.product_a)
- def test_prepare_jobs(self):
+ self.assertEqual(len(invoice.edi_document_ids), 0)
+ with self.with_custom_method('_get_move_applicability', lambda edi_format, inv: {'post': edi_format._test_edi_post_invoice}), \
+ self.with_custom_method('_test_edi_post_invoice', lambda edi_format, inv: {inv: {'success': True}}):
+ invoice.action_post()
+ self.assertEqual(len(invoice.edi_document_ids), 1)
- edi_docs = self.env['account.edi.document']
- edi_docs |= self.create_edi_document(self.edi_format, 'to_send')
- edi_docs |= self.create_edi_document(self.edi_format, 'to_send')
+ def test_prepare_jobs_no_batching(self):
+ invoice1 = self.init_invoice('out_invoice', products=self.product_a)
+ invoice2 = self.init_invoice('out_invoice', products=self.product_a)
- to_process = edi_docs._prepare_jobs()
- self.assertEqual(len(to_process), 2)
+ with self.with_custom_method('_get_move_applicability', lambda edi_format, inv: {'post': edi_format._test_edi_post_invoice}), \
+ self.with_custom_method('_test_edi_post_invoice', lambda edi_format, inv: {inv: {'success': True}}), \
+ self.with_custom_method('_needs_web_services', lambda edi_format: True):
+ (invoice1 + invoice2).action_post()
- with patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._support_batching', return_value=True):
- to_process = edi_docs._prepare_jobs()
- self.assertEqual(len(to_process), 1)
+ jobs = (invoice1 + invoice2).edi_document_ids._prepare_jobs()
+ self.assertEqual(len(jobs), 2)
- other_edi = self.env['account.edi.format'].sudo().create({
- 'name': 'Batchable EDI format 2',
- 'code': 'test_batch_edi_2',
- })
+ def test_prepare_jobs_batching(self):
+ invoice1 = self.init_invoice('out_invoice', products=self.product_a)
+ invoice2 = self.init_invoice('out_invoice', products=self.product_a)
- edi_docs |= self.create_edi_document(other_edi, 'to_send')
- edi_docs |= self.create_edi_document(other_edi, 'to_send')
+ with self.with_custom_method('_get_move_applicability',
+ lambda edi_format, inv: {
+ 'post': edi_format._test_edi_post_invoice,
+ 'post_batching': lambda inv: (inv.partner_id,),
+ }), \
+ self.with_custom_method('_test_edi_post_invoice', lambda edi_format, inv: {inv: {'success': True}}), \
+ self.with_custom_method('_needs_web_services', lambda edi_format: True):
+ (invoice1 + invoice2).action_post()
- with patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._support_batching', return_value=True):
- to_process = edi_docs._prepare_jobs()
- self.assertEqual(len(to_process), 2)
+ jobs = (invoice1 + invoice2).edi_document_ids._prepare_jobs()
+ self.assertEqual(len(jobs), 1)
- @patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._post_invoice_edi', return_value={})
- def test_warning_is_retried(self, patched):
- with patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._needs_web_services',
- new=lambda edi_format: True):
- edi_docs = self.create_edi_document(self.edi_format, 'to_send')
- edi_docs.error = 'Test Error'
- edi_docs.blocking_level = 'warning'
+ def test_warning_is_retried(self):
+ invoice = self.init_invoice('out_invoice', products=self.product_a)
- edi_docs.move_id.action_process_edi_web_services()
- patched.assert_called_once()
+ with self.with_custom_method('_get_move_applicability', lambda edi_format, inv: {'post': edi_format._test_edi_post_invoice}), \
+ self.with_custom_method('_needs_web_services', lambda edi_format: True):
+
+ with self.with_custom_method('_test_edi_post_invoice',
+ lambda edi_format, inv: {inv: {'error': "turlututu", 'blocking_level': 'warning'}}):
+ invoice.action_post()
+ self.assertRecordValues(invoice.edi_document_ids, [{'state': 'to_send'}])
+
+ with self.with_custom_method('_test_edi_post_invoice', lambda edi_format, inv: {inv: {'success': True}}):
+ invoice.action_process_edi_web_services(with_commit=False)
+ self.assertRecordValues(invoice.edi_document_ids, [{'state': 'sent'}])
def test_edi_flow(self):
- with self.mock_edi():
- doc = self.invoice._get_edi_document(self.edi_format)
- self.assertFalse(doc)
- self.invoice.action_post()
- doc = self.invoice._get_edi_document(self.edi_format)
- self.assertEqual(len(doc), 1)
- self.assertEqual(doc.state, 'sent')
- self.invoice.button_draft()
- self.invoice.button_cancel()
- self.assertEqual(doc.state, 'cancelled')
+ invoice = self.init_invoice('out_invoice', products=self.product_a)
+
+ with self.with_custom_method('_get_move_applicability', lambda edi_format, inv: {
+ 'post': edi_format._test_edi_post_invoice,
+ 'cancel': edi_format._test_edi_cancel_invoice,
+ }), \
+ self.with_custom_method('_needs_web_services', lambda edi_format: True), \
+ self.with_custom_method('_test_edi_post_invoice', lambda edi_format, inv: {inv: {'success': True}}), \
+ self.with_custom_method('_test_edi_cancel_invoice', lambda edi_format, inv: {inv: {'success': True}}):
+ invoice.action_post()
+ self.assertRecordValues(invoice.edi_document_ids, [{'state': 'to_send'}])
+
+ invoice.action_process_edi_web_services(with_commit=False)
+ self.assertRecordValues(invoice.edi_document_ids, [{'state': 'sent'}])
+
+ invoice.button_cancel_posted_moves()
+ self.assertRecordValues(invoice.edi_document_ids, [{'state': 'to_cancel'}])
+
+ invoice.button_abandon_cancel_posted_posted_moves()
+ self.assertRecordValues(invoice.edi_document_ids, [{'state': 'sent'}])
+
+ invoice.button_cancel_posted_moves()
+ self.assertRecordValues(invoice.edi_document_ids, [{'state': 'to_cancel'}])
+
+ invoice.action_process_edi_web_services(with_commit=False)
+ self.assertRecordValues(invoice.edi_document_ids, [{'state': 'cancelled'}])
def test_edi_flow_two_steps(self):
- with self.mock_edi(_post_invoice_edi_method=_mocked_post_two_steps,
- _needs_web_services_method=_generate_mocked_needs_web_services(True)):
- doc = self.invoice._get_edi_document(self.edi_format)
- self.assertFalse(doc)
- self.invoice.action_post()
- doc = self.invoice._get_edi_document(self.edi_format)
- self.assertEqual(len(doc), 1)
- self.assertEqual(doc.state, 'to_send')
- doc._process_documents_web_services(with_commit=False)
- self.assertEqual(doc.state, 'to_send')
- doc._process_documents_web_services(with_commit=False)
- self.assertEqual(doc.state, 'sent')
+ def step1(edi_format, invoice):
+ return {invoice: {'error': "step1 done", 'blocking_level': 'info'}}
- def test_edi_flow_request_cancel_success(self):
- with self.mock_edi(_needs_web_services_method=_generate_mocked_needs_web_services(True)):
- self.assertEqual(self.invoice.state, 'draft')
- self.invoice.action_post()
- doc = self.invoice._get_edi_document(self.edi_format)
- self.assertEqual(doc.state, 'to_send')
- self.assertEqual(self.invoice.state, 'posted')
- doc._process_documents_web_services(with_commit=False)
- self.assertEqual(doc.state, 'sent')
- self.assertEqual(self.invoice.state, 'posted')
- self.invoice.button_cancel_posted_moves()
- self.assertEqual(doc.state, 'to_cancel')
- self.assertEqual(self.invoice.state, 'posted')
- doc._process_documents_web_services(with_commit=False)
- self.assertEqual(doc.state, 'cancelled')
- self.assertEqual(self.invoice.state, 'cancel')
+ def step2(edi_format, invoice):
+ return {invoice: {'success': True}}
- def test_edi_flow_request_cancel_failed(self):
- with self.mock_edi(_needs_web_services_method=_generate_mocked_needs_web_services(True),
- _cancel_invoice_edi_method=_mocked_cancel_failed):
- self.assertEqual(self.invoice.state, 'draft')
- self.invoice.action_post()
- doc = self.invoice._get_edi_document(self.edi_format)
- self.assertEqual(doc.state, 'to_send')
- self.assertEqual(self.invoice.state, 'posted')
- doc._process_documents_web_services(with_commit=False)
- self.assertEqual(doc.state, 'sent')
- self.assertEqual(self.invoice.state, 'posted')
- self.invoice.button_cancel_posted_moves()
- self.assertEqual(doc.state, 'to_cancel')
- self.assertEqual(self.invoice.state, 'posted')
- # Call off edi Cancellation
- self.invoice.button_abandon_cancel_posted_posted_moves()
- self.assertEqual(doc.state, 'sent')
- self.assertFalse(doc.error)
-
- # Failed cancel
- self.invoice.button_cancel_posted_moves()
- self.assertEqual(doc.state, 'to_cancel')
- self.assertEqual(self.invoice.state, 'posted')
- doc._process_documents_web_services(with_commit=False)
- self.assertEqual(doc.state, 'to_cancel')
- self.assertEqual(self.invoice.state, 'posted')
-
- # Call off edi Cancellation
- self.invoice.button_abandon_cancel_posted_posted_moves()
- self.assertEqual(doc.state, 'sent')
- self.assertIsNotNone(doc.error)
-
- def test_edi_flow_two_step_cancel_with_call_off_request(self):
- def _mock_cancel(edi_format, invoices):
- invoices_no_ref = invoices.filtered(lambda i: not i.ref)
- if len(invoices_no_ref) == len(invoices): # first step
- invoices_no_ref.ref = 'test_ref_cancel'
- return {invoice: {} for invoice in invoices}
- elif len(invoices_no_ref) == 0: # second step
- for invoice in invoices:
- invoice.ref = None
- return {invoice: {'success': True} for invoice in invoices}
+ def get_move_applicability(edi_format, invoice):
+ if "step1" in (invoice.edi_document_ids.error or ''):
+ return {'post': edi_format._test_edi_post_invoice_step2}
else:
- raise ValueError('wrong use of "_mocked_post_two_steps"')
+ return {'post': edi_format._test_edi_post_invoice_step1}
- def _is_needed_for_invoice(edi_format, invoice):
- return not bool(invoice.ref)
+ invoice = self.init_invoice('out_invoice', products=self.product_a)
- with self.mock_edi(_needs_web_services_method=_generate_mocked_needs_web_services(True),
- _is_required_for_invoice_method=_is_needed_for_invoice,
- _cancel_invoice_edi_method=_mock_cancel):
- self.invoice.action_post()
- doc = self.invoice._get_edi_document(self.edi_format)
- doc._process_documents_web_services(with_commit=False)
- self.assertEqual(doc.state, 'sent')
+ with self.with_custom_method('_get_move_applicability', get_move_applicability), \
+ self.with_custom_method('_needs_web_services', lambda edi_format: True), \
+ self.with_custom_method('_test_edi_post_invoice_step1', step1), \
+ self.with_custom_method('_test_edi_post_invoice_step2', step2):
+ invoice.action_post()
+ self.assertRecordValues(invoice.edi_document_ids, [{'state': 'to_send'}])
- # Request Cancellation
- self.invoice.button_cancel_posted_moves()
- doc._process_documents_web_services(with_commit=False) # first step of cancel
- self.assertEqual(doc.state, 'to_cancel')
+ invoice.action_process_edi_web_services(with_commit=False)
+ self.assertRecordValues(invoice.edi_document_ids, [{'state': 'to_send'}])
- # Call off edi Cancellation
- self.invoice.button_abandon_cancel_posted_posted_moves()
- self.assertEqual(doc.state, 'to_cancel')
-
- # If we cannot call off edi cancellation, only solution is to post again
- doc._process_documents_web_services(with_commit=False) # second step of cancel
- self.assertEqual(doc.state, 'cancelled')
- self.invoice.action_post()
- doc._process_documents_web_services(with_commit=False)
- self.assertEqual(doc.state, 'sent')
-
- def test_batches(self):
- def _get_batch_key_method(edi_format, move, state):
- return (move.ref)
-
- with self.mock_edi(_get_batch_key_method=_get_batch_key_method,
- _support_batching_method=_generate_mocked_support_batching(True)):
- edi_docs = self.env['account.edi.document']
- doc1 = self.create_edi_document(self.edi_format, 'to_send')
- edi_docs |= doc1
- doc2 = self.create_edi_document(self.edi_format, 'to_send')
- edi_docs |= doc2
- doc3 = self.create_edi_document(self.edi_format, 'to_send')
- edi_docs |= doc3
-
- to_process = edi_docs._prepare_jobs()
- self.assertEqual(len(to_process), 1)
-
- doc1.move_id.ref = 'batch1'
- doc2.move_id.ref = 'batch2'
- doc3.move_id.ref = 'batch3'
-
- to_process = edi_docs._prepare_jobs()
- self.assertEqual(len(to_process), 3)
-
- doc2.move_id.ref = 'batch1'
- to_process = edi_docs._prepare_jobs()
- self.assertEqual(len(to_process), 2)
+ invoice.action_process_edi_web_services(with_commit=False)
+ self.assertRecordValues(invoice.edi_document_ids, [{'state': 'sent'}])
def test_cron_triggers(self):
- with self.capture_triggers('account_edi.ir_cron_edi_network') as capt, \
- self.mock_edi(_needs_web_services_method=_generate_mocked_needs_web_services(True)):
- self.invoice._get_edi_document(self.edi_format)
- self.invoice.action_post()
+ invoice = self.init_invoice('out_invoice', products=self.product_a)
+ with self.with_custom_method('_get_move_applicability', lambda edi_format, inv: {'post': edi_format._test_edi_post_invoice}), \
+ self.with_custom_method('_needs_web_services', lambda edi_format: True), \
+ self.with_custom_method('_test_edi_post_invoice', lambda edi_format, inv: {inv: {'success': True}}), \
+ self.capture_triggers('account_edi.ir_cron_edi_network') as capt:
+ invoice.action_post()
capt.records.ensure_one()
def test_cron_self_trigger(self):
@@ -213,33 +144,27 @@ class TestAccountEdi(AccountEdiTestCommon, CronMixinCase):
edi_cron = self.env.ref('account_edi.ir_cron_edi_network')
edi_cron.code = 'model._cron_process_documents_web_services(job_count=1)'
- # Create invoices.
- 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})],
- } for i in range(4)])
-
- with self.capture_triggers('account_edi.ir_cron_edi_network') as capt, \
- self.mock_edi(_needs_web_services_method=_generate_mocked_needs_web_services(True)):
- invoices.action_post()
+ invoice1 = self.init_invoice('out_invoice', products=self.product_a)
+ invoice2 = self.init_invoice('out_invoice', products=self.product_a)
+ with self.with_custom_method('_get_move_applicability', lambda edi_format, inv: {'post': edi_format._test_edi_post_invoice}), \
+ self.with_custom_method('_needs_web_services', lambda edi_format: True), \
+ self.with_custom_method('_test_edi_post_invoice', lambda edi_format, inv: {inv: {'success': True}}), \
+ self.capture_triggers('account_edi.ir_cron_edi_network') as capt:
+ (invoice1 + invoice2).action_post()
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")
+ 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_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())
+ invoice = self.init_invoice('out_invoice', products=self.product_a)
+ with self.with_custom_method('_get_move_applicability', lambda edi_format, inv: {'post': edi_format._test_edi_post_invoice}), \
+ self.with_custom_method('_needs_web_services', lambda edi_format: True), \
+ self.with_custom_method('_test_edi_post_invoice', lambda edi_format, inv: {inv: {'success': True}}):
+ invoice.action_post()
+ self.assertFalse(invoice._is_ready_to_be_sent())
+ invoice.action_process_edi_web_services(with_commit=False)
+ self.assertTrue(invoice._is_ready_to_be_sent())
diff --git a/addons/account_edi_ubl_cii/models/account_edi_format.py b/addons/account_edi_ubl_cii/models/account_edi_format.py
index 920f0d3f765..7bfdae7aee0 100644
--- a/addons/account_edi_ubl_cii/models/account_edi_format.py
+++ b/addons/account_edi_ubl_cii/models/account_edi_format.py
@@ -74,14 +74,6 @@ class AccountEdiFormat(models.Model):
# Export: Account.edi.format override
####################################################
- def _is_required_for_invoice(self, invoice):
- # EXTENDS account_edi
- self.ensure_one()
- if self.code not in FORMAT_CODES:
- return super()._is_required_for_invoice(invoice)
-
- return self._is_ubl_cii_available(invoice.company_id) and invoice.move_type in ('out_invoice', 'out_refund')
-
def _is_compatible_with_journal(self, journal):
# EXTENDS account_edi
# the formats appear on the journal only if they are compatible (e.g. NLCIUS only appear for dutch companies)
@@ -98,39 +90,38 @@ class AccountEdiFormat(models.Model):
return super()._is_enabled_by_default_on_journal(journal)
return self.code == 'facturx_1_0_05'
- def _post_invoice_edi(self, invoices):
+ def _ubl_cii_post_invoice(self, invoice):
# EXTENDS account_edi
self.ensure_one()
+ builder = self._get_xml_builder(invoice.company_id)
+ # For now, the errors are not displayed anywhere, don't want to annoy the user
+ xml_content, _ = builder._export_invoice(invoice)
+
+ # DEBUG: send directly to the test platform (the one used by ecosio)
+ #response = self.env['account.edi.common']._check_xml_ecosio(invoice, xml_content, builder._export_invoice_ecosio_schematrons())
+
+ attachment_create_vals = {
+ 'name': builder._export_invoice_filename(invoice),
+ 'raw': xml_content,
+ 'mimetype': 'application/xml',
+ }
+ # we don't want the Factur-X and E-FFF xml to appear in the attachment of the invoice when confirming it
+ # E-FFF will appear after the pdf is generated, Factur-X will never appear (it's contained in the PDF)
+ if self.code not in ['facturx_1_0_05', 'efff_1']:
+ attachment_create_vals.update({'res_id': invoice.id, 'res_model': 'account.move'})
+
+ attachment = self.env['ir.attachment'].create(attachment_create_vals)
+ return {invoice: {'success': True, 'attachment': attachment}}
+
+ def _get_move_applicability(self, move):
+ # EXTENDS account_edi
+ self.ensure_one()
if self.code not in FORMAT_CODES:
- return super()._post_invoice_edi(invoices)
+ return super()._get_move_applicability(move)
- res = {}
- for invoice in invoices:
- builder = self._get_xml_builder(invoice.company_id)
- # For now, the errors are not displayed anywhere, don't want to annoy the user
- xml_content, errors = builder._export_invoice(invoice)
-
- # DEBUG: send directly to the test platform (the one used by ecosio)
- #response = self.env['account.edi.common']._check_xml_ecosio(invoice, xml_content, builder._export_invoice_ecosio_schematrons())
-
- attachment_create_vals = {
- 'name': builder._export_invoice_filename(invoice),
- 'raw': xml_content,
- 'mimetype': 'application/xml',
- }
- # we don't want the Factur-X, E-FFF and NLCIUS xml to appear in the attachment of the invoice when confirming it
- # E-FFF and NLCIUS will appear after the pdf is generated, Factur-X will never appear (it's contained in the PDF)
- if self.code not in ['facturx_1_0_05', 'efff_1', 'nlcius_1']:
- attachment_create_vals.update({'res_id': invoice.id, 'res_model': 'account.move'})
-
- attachment = self.env['ir.attachment'].create(attachment_create_vals)
- res[invoice] = {
- 'success': True,
- 'attachment': attachment,
- }
-
- return res
+ if self._is_ubl_cii_available(move.company_id) and move.move_type in ('out_invoice', 'out_refund'):
+ return {'post': self._ubl_cii_post_invoice}
def _is_embedding_to_invoice_pdf_needed(self):
# EXTENDS account_edi
diff --git a/addons/l10n_eg_edi_eta/models/account_edi_format.py b/addons/l10n_eg_edi_eta/models/account_edi_format.py
index a270cdea91a..d2b5c192754 100644
--- a/addons/l10n_eg_edi_eta/models/account_edi_format.py
+++ b/addons/l10n_eg_edi_eta/models/account_edi_format.py
@@ -311,6 +311,19 @@ class AccountEdiFormat(models.Model):
def _needs_web_services(self):
return self.code == 'eg_eta' or super()._needs_web_services()
+ def _get_move_applicability(self, move):
+ # EXTENDS account_edi
+ self.ensure_one()
+ if self.code != 'eg_eta':
+ return super()._get_move_applicability(move)
+
+ if move.is_invoice(include_receipts=True) and move.country_code == 'EG':
+ return {
+ 'post': self._l10n_eg_edi_post_invoice,
+ 'cancel': self._l10n_eg_edi_cancel_invoice,
+ 'edi_content': self._l10n_eg_edi_xml_invoice_content,
+ }
+
def _check_move_configuration(self, invoice):
errors = super()._check_move_configuration(invoice)
if self.code != 'eg_eta':
@@ -336,11 +349,7 @@ class AccountEdiFormat(models.Model):
errors.append(_("Please make sure the EGS/GS1 Barcode is set correctly on all products"))
return errors
- def _post_invoice_edi(self, invoices):
- if self.code != 'eg_eta':
- return super()._post_invoice_edi(invoices)
- invoice = invoices # Batching is disabled for this EDI.
-
+ def _l10n_eg_edi_post_invoice(self, invoice):
# In case we have already sent it, but have not got a final answer yet.
if invoice.l10n_eg_submission_number:
return {invoice: self._l10n_eg_get_einvoice_status(invoice)}
@@ -362,16 +371,11 @@ class AccountEdiFormat(models.Model):
}
return {invoice: self._l10n_eg_edi_post_invoice_web_service(invoice)}
- def _cancel_invoice_edi(self, invoices):
- if self.code != 'eg_eta':
- return super()._cancel_invoice_edi(invoices)
- invoice = invoices
+ def _l10n_eg_edi_cancel_invoice(self, invoice):
return {invoice: self._cancel_invoice_edi_eta(invoice)}
- def _get_invoice_edi_content(self, move):
- if self.code != 'eg_eta':
- return super()._get_invoice_edi_content(move)
- return json.dumps(self._l10n_eg_eta_prepare_eta_invoice(move)).encode()
+ def _l10n_eg_edi_xml_invoice_content(self, invoice):
+ return json.dumps(self._l10n_eg_eta_prepare_eta_invoice(invoice)).encode()
def _is_compatible_with_journal(self, journal):
# OVERRIDE
diff --git a/addons/l10n_es_edi_sii/models/account_edi_format.py b/addons/l10n_es_edi_sii/models/account_edi_format.py
index 2e596d19473..552f63208f1 100644
--- a/addons/l10n_es_edi_sii/models/account_edi_format.py
+++ b/addons/l10n_es_edi_sii/models/account_edi_format.py
@@ -440,7 +440,7 @@ class AccountEdiFormat(models.Model):
def _l10n_es_edi_call_web_service_sign(self, invoices, info_list):
company = invoices.company_id
- # All are sharing the same value, see '_get_batch_key'.
+ # All are sharing the same value.
csv_number = invoices.mapped('l10n_es_edi_csv')[0]
# Set registration date
@@ -573,36 +573,26 @@ class AccountEdiFormat(models.Model):
# EDI OVERRIDDEN METHODS
# -------------------------------------------------------------------------
- def _get_invoice_edi_content(self, move):
- if self.code != 'es_sii':
- return super()._get_invoice_edi_content(move)
- return json.dumps(self._l10n_es_edi_get_invoices_info(move)).encode()
+ def _l10n_es_edi_sii_xml_invoice_content(self, invoice):
+ return json.dumps(self._l10n_es_edi_get_invoices_info(invoice)).encode()
- def _is_required_for_invoice(self, invoice):
- # OVERRIDE
+ def _get_move_applicability(self, move):
+ # EXTENDS account_edi
+ self.ensure_one()
if self.code != 'es_sii':
- return super()._is_required_for_invoice(invoice)
+ return super()._get_move_applicability(move)
- return invoice.l10n_es_edi_is_required
+ if move.l10n_es_edi_is_required:
+ return {
+ 'post': self._l10n_es_edi_sii_post_invoices,
+ 'post_batching': lambda invoice: (invoice.move_type, invoice.l10n_es_edi_csv),
+ 'edi_content': self._l10n_es_edi_sii_xml_invoice_content,
+ }
def _needs_web_services(self):
# OVERRIDE
return self.code == 'es_sii' or super()._needs_web_services()
- def _support_batching(self, move=None, state=None, company=None):
- # OVERRIDE
- if self.code != 'es_sii':
- return super()._support_batching(move=move, state=state, company=company)
-
- return state == 'to_send' and move.is_invoice()
-
- def _get_batch_key(self, move, state):
- # OVERRIDE
- if self.code != 'es_sii':
- return super()._get_batch_key(move, state)
-
- return move.move_type, move.l10n_es_edi_csv
-
def _check_move_configuration(self, move):
# OVERRIDE
res = super()._check_move_configuration(move)
@@ -642,11 +632,7 @@ class AccountEdiFormat(models.Model):
return journal.country_code == 'ES'
- def _post_invoice_edi(self, invoices):
- # OVERRIDE
- if self.code != 'es_sii':
- return super()._post_invoice_edi(invoices)
-
+ def _l10n_es_edi_sii_post_invoices(self, invoices):
# Ensure a certificate is available.
certificate = invoices.company_id.l10n_es_edi_certificate_id
if not certificate:
diff --git a/addons/l10n_in_edi/models/account_edi_format.py b/addons/l10n_in_edi/models/account_edi_format.py
index f239348a557..2412df9301b 100644
--- a/addons/l10n_in_edi/models/account_edi_format.py
+++ b/addons/l10n_in_edi/models/account_edi_format.py
@@ -27,28 +27,31 @@ class AccountEdiFormat(models.Model):
return journal.company_id.country_id.code == 'IN'
return super()._is_enabled_by_default_on_journal(journal)
- def _is_required_for_invoice(self, invoice):
+ def _get_move_applicability(self, move):
+ # EXTENDS account_edi
self.ensure_one()
- if self.code == "in_einvoice_1_03":
- return invoice.is_sale_document() and invoice.country_code == 'IN' and invoice.l10n_in_gst_treatment in (
- "regular",
- "composition",
- "overseas",
- "special_economic_zone",
- "deemed_export",
- "uin_holders",
- )
- return super()._is_required_for_invoice(invoice)
+ if self.code != 'in_einvoice_1_03':
+ return super()._get_move_applicability(move)
+
+ if move.is_sale_document() and move.country_code == 'IN' and move.l10n_in_gst_treatment in (
+ "regular",
+ "composition",
+ "overseas",
+ "special_economic_zone",
+ "deemed_export",
+ ):
+ return {
+ 'post': self._l10n_in_edi_post_invoice,
+ 'cancel': self._l10n_in_edi_cancel_invoice,
+ 'edi_content': self._l10n_in_edi_xml_invoice_content,
+ }
def _needs_web_services(self):
self.ensure_one()
return self.code == "in_einvoice_1_03" or super()._needs_web_services()
- def _get_invoice_edi_content(self, move):
- if self.code != "in_einvoice_1_03":
- return super()._get_invoice_edi_content(move)
- json_dump = json.dumps(self._l10n_in_edi_generate_invoice_json(move))
- return json_dump.encode()
+ def _l10n_in_edi_xml_invoice_content(self, invoice):
+ return json.dumps(self._l10n_in_edi_generate_invoice_json(invoice)).encode()
def _l10n_in_edi_extract_digits(self, string):
if not string:
@@ -88,128 +91,118 @@ class AccountEdiFormat(models.Model):
_("Buy Credits")
)
- def _post_invoice_edi(self, invoices):
- if self.code != "in_einvoice_1_03":
- return super()._post_invoice_edi(invoices)
- response = {}
- res = {}
- generate_json = self._l10n_in_edi_generate_invoice_json(invoices)
- response = self._l10n_in_edi_generate(invoices.company_id, generate_json)
+ def _l10n_in_edi_post_invoice(self, invoice):
+ generate_json = self._l10n_in_edi_generate_invoice_json(invoice)
+ response = self._l10n_in_edi_generate(invoice.company_id, generate_json)
if response.get("error"):
error = response["error"]
error_codes = [e.get("code") for e in error]
if "1005" in error_codes:
# Invalid token eror then create new token and send generate request again.
# This happen when authenticate called from another odoo instance with same credentials (like. Demo/Test)
- authenticate_response = self._l10n_in_edi_authenticate(invoices.company_id)
+ authenticate_response = self._l10n_in_edi_authenticate(invoice.company_id)
if not authenticate_response.get("error"):
error = []
- response = self._l10n_in_edi_generate(invoices.company_id, generate_json)
+ response = self._l10n_in_edi_generate(invoice.company_id, generate_json)
if response.get("error"):
error = response["error"]
error_codes = [e.get("code") for e in error]
if "2150" in error_codes:
# Get IRN by details in case of IRN is already generated
# this happens when timeout from the Government portal but IRN is generated
- response = self._l10n_in_edi_get_irn_by_details(invoices.company_id, {
- "doc_type": invoices.move_type == "out_refund" and "CRN" or "INV",
- "doc_num": invoices.name,
- "doc_date": invoices.invoice_date and invoices.invoice_date.strftime("%d/%m/%Y") or False,
+ response = self._l10n_in_edi_get_irn_by_details(invoice.company_id, {
+ "doc_type": invoice.move_type == "out_refund" and "CRN" or "INV",
+ "doc_num": invoice.name,
+ "doc_date": invoice.invoice_date and invoice.invoice_date.strftime("%d/%m/%Y") or False,
})
if not response.get("error"):
error = []
odoobot = self.env.ref("base.partner_root")
- invoices.message_post(author_id=odoobot.id, body=_(
+ invoice.message_post(author_id=odoobot.id, body=_(
"Somehow this invoice had been submited to government before." \
"
Normally, this should not happen too often" \
"
Just verify value of invoice by uploade json to government website " \
"here."
))
if "no-credit" in error_codes:
- res[invoices] = {
+ return {invoice: {
"success": False,
- "error": self._l10n_in_edi_get_iap_buy_credits_message(invoices.company_id),
+ "error": self._l10n_in_edi_get_iap_buy_credits_message(invoice.company_id),
"blocking_level": "error",
- }
+ }}
elif error:
error_message = "
".join(["[%s] %s" % (e.get("code"), html_escape(e.get("message"))) for e in error])
- res[invoices] = {
+ return {invoice: {
"success": False,
"error": error_message,
"blocking_level": ("404" in error_codes) and "warning" or "error",
- }
+ }}
if not response.get("error"):
json_dump = json.dumps(response.get("data"))
- json_name = "%s_einvoice.json" % (invoices.name.replace("/", "_"))
+ json_name = "%s_einvoice.json" % (invoice.name.replace("/", "_"))
attachment = self.env["ir.attachment"].create({
"name": json_name,
"raw": json_dump.encode(),
"res_model": "account.move",
- "res_id": invoices.id,
+ "res_id": invoice.id,
"mimetype": "application/json",
})
- res[invoices] = {"success": True, "attachment": attachment}
- return res
+ return {invoice: {"success": True, "attachment": attachment}}
- def _cancel_invoice_edi(self, invoices):
- if self.code != "in_einvoice_1_03":
- return super()._cancel_invoice_edi(invoices)
- res = {}
- for invoice in invoices:
- l10n_in_edi_response_json = invoice._get_l10n_in_edi_response_json()
- cancel_json = {
- "Irn": l10n_in_edi_response_json.get("Irn"),
- "CnlRsn": invoice.l10n_in_edi_cancel_reason,
- "CnlRem": invoice.l10n_in_edi_cancel_remarks,
- }
- response = self._l10n_in_edi_cancel(invoice.company_id, cancel_json)
- if response.get("error"):
- error = response["error"]
- error_codes = [e.get("code") for e in error]
- if "1005" in error_codes:
- # Invalid token eror then create new token and send generate request again.
- # This happen when authenticate called from another odoo instance with same credentials (like. Demo/Test)
- authenticate_response = self._l10n_in_edi_authenticate(invoice.company_id)
- if not authenticate_response.get("error"):
- error = []
- response = self._l10n_in_edi_cancel(invoice.company_id, cancel_json)
- if response.get("error"):
- error = response["error"]
- error_codes = [e.get("code") for e in error]
- if "9999" in error_codes:
- response = {}
- odoobot = self.env.ref("base.partner_root")
- invoices.message_post(author_id=odoobot.id, body=_(
- "Somehow this invoice had been cancelled to government before." \
- "
Normally, this should not happen too often" \
- "
Just verify by logging into government website " \
- "here."
- ))
- if "no-credit" in error_codes:
- res[invoice] = {
- "success": False,
- "error": self._l10n_in_edi_get_iap_buy_credits_message(invoice.company_id),
- "blocking_level": "error",
- }
- else:
- error_message = "
".join(["[%s] %s" % (e.get("code"), html_escape(e.get("message"))) for e in error])
- res[invoice] = {
- "success": False,
- "error": error_message,
- "blocking_level": ("404" in error_codes) and "warning" or "error",
- }
- if not response.get("error"):
- json_dump = json.dumps(response.get("data", {}))
- json_name = "%s_cancel_einvoice.json" % (invoice.name.replace("/", "_"))
- attachment = self.env["ir.attachment"].create({
- "name": json_name,
- "raw": json_dump.encode(),
- "res_model": "account.move",
- "res_id": invoice.id,
- "mimetype": "application/json",
- })
- res[invoice] = {"success": True, "attachment": attachment}
- return res
+ def _l10n_in_edi_cancel_invoice(self, invoice):
+ l10n_in_edi_response_json = invoice._get_l10n_in_edi_response_json()
+ cancel_json = {
+ "Irn": l10n_in_edi_response_json.get("Irn"),
+ "CnlRsn": invoice.l10n_in_edi_cancel_reason,
+ "CnlRem": invoice.l10n_in_edi_cancel_remarks,
+ }
+ response = self._l10n_in_edi_cancel(invoice.company_id, cancel_json)
+ if response.get("error"):
+ error = response["error"]
+ error_codes = [e.get("code") for e in error]
+ if "1005" in error_codes:
+ # Invalid token eror then create new token and send generate request again.
+ # This happen when authenticate called from another odoo instance with same credentials (like. Demo/Test)
+ authenticate_response = self._l10n_in_edi_authenticate(invoice.company_id)
+ if not authenticate_response.get("error"):
+ error = []
+ response = self._l10n_in_edi_cancel(invoice.company_id, cancel_json)
+ if response.get("error"):
+ error = response["error"]
+ error_codes = [e.get("code") for e in error]
+ if "9999" in error_codes:
+ response = {}
+ odoobot = self.env.ref("base.partner_root")
+ invoice.message_post(author_id=odoobot.id, body=_(
+ "Somehow this invoice had been cancelled to government before." \
+ "
Normally, this should not happen too often" \
+ "
Just verify by logging into government website " \
+ "here."
+ ))
+ if "no-credit" in error_codes:
+ return {invoice: {
+ "success": False,
+ "error": self._l10n_in_edi_get_iap_buy_credits_message(invoice.company_id),
+ "blocking_level": "error",
+ }}
+ else:
+ error_message = "
".join(["[%s] %s" % (e.get("code"), html_escape(e.get("message"))) for e in error])
+ return {invoice: {
+ "success": False,
+ "error": error_message,
+ "blocking_level": ("404" in error_codes) and "warning" or "error",
+ }}
+ if not response.get("error"):
+ json_dump = json.dumps(response.get("data", {}))
+ json_name = "%s_cancel_einvoice.json" % (invoice.name.replace("/", "_"))
+ attachment = self.env["ir.attachment"].create({
+ "name": json_name,
+ "raw": json_dump.encode(),
+ "res_model": "account.move",
+ "res_id": invoice.id,
+ "mimetype": "application/json",
+ })
+ return {invoice: {"success": True, "attachment": attachment}}
def _l10n_in_validate_partner(self, partner, is_company=False):
self.ensure_one()
diff --git a/addons/l10n_it_edi/models/account_edi_format.py b/addons/l10n_it_edi/models/account_edi_format.py
index df2a1573cd5..acb1f8321fb 100644
--- a/addons/l10n_it_edi/models/account_edi_format.py
+++ b/addons/l10n_it_edi/models/account_edi_format.py
@@ -790,19 +790,20 @@ class AccountEdiFormat(models.Model):
return super()._is_compatible_with_journal(journal)
return journal.type in ('sale', 'purchase') and journal.country_code == 'IT'
- def _is_required_for_invoice(self, invoice):
+ def _get_move_applicability(self, move):
# OVERRIDE
self.ensure_one()
if self.code != 'fattura_pa':
- return super()._is_required_for_invoice(invoice)
+ return super()._get_move_applicability(move)
- is_self_invoice = self._l10n_it_edi_is_self_invoice(invoice)
- return (
- (invoice.is_sale_document() or (is_self_invoice and invoice.is_purchase_document()))
- and invoice.country_code == 'IT'
- )
+ is_it_purchase_document = self._l10n_it_edi_is_self_invoice(move) and move.is_purchase_document()
+ if move.country_code == 'IT' and (move.is_sale_document() or is_it_purchase_document):
+ return {
+ 'post': self._post_fattura_pa,
+ 'post_batching': lambda move: (move.move_type, bool(move.l10n_it_edi_transaction)),
+ }
- def _export_as_xml(self, invoice):
+ def _l10n_it_edi_export_invoice_as_xml(self, invoice):
''' Create the xml file content.
:return: The XML content as str.
'''
@@ -817,12 +818,6 @@ class AccountEdiFormat(models.Model):
))
return content
- def _get_invoice_edi_content(self, move):
- #OVERRIDE
- if self.code != 'fattura_pa':
- return super()._get_invoice_edi_content(move)
- return self._export_as_xml(move)
-
def _check_move_configuration(self, move):
# OVERRIDE
res = super()._check_move_configuration(move)
@@ -840,20 +835,6 @@ class AccountEdiFormat(models.Model):
self.ensure_one()
return self.code == 'fattura_pa' or super()._needs_web_services()
- def _support_batching(self, move=None, state=None, company=None):
- # OVERRIDE
- if self.code == 'fattura_pa':
- return state == 'to_send' and move.is_invoice()
-
- return super()._support_batching(move=move, state=state, company=company)
-
- def _get_batch_key(self, move, state):
- # OVERRIDE
- if self.code != 'fattura_pa':
- return super()._get_batch_key(move, state)
-
- return move.move_type, bool(move.l10n_it_edi_transaction)
-
def _l10n_it_post_invoices_step_1(self, invoices):
''' Send the invoices to the proxy.
'''
@@ -861,7 +842,7 @@ class AccountEdiFormat(models.Model):
to_send = {}
for invoice in invoices:
- xml = "" + str(self._export_as_xml(invoice))
+ xml = "" + str(self._l10n_it_edi_export_invoice_as_xml(invoice))
filename = self._l10n_it_edi_generate_electronic_invoice_filename(invoice)
attachment = self.env['ir.attachment'].create({
'name': filename,
@@ -1027,12 +1008,12 @@ class AccountEdiFormat(models.Model):
return to_return
- def _post_fattura_pa(self, invoices):
+ def _post_fattura_pa(self, invoice):
# OVERRIDE
- if not invoices[0].l10n_it_edi_transaction:
- return self._l10n_it_post_invoices_step_1(invoices)
+ if not invoice.l10n_it_edi_transaction:
+ return self._l10n_it_post_invoices_step_1(invoice)
else:
- return self._l10n_it_post_invoices_step_2(invoices)
+ return self._l10n_it_post_invoices_step_2(invoice)
def _post_invoice_edi(self, invoices):
# OVERRIDE
diff --git a/addons/l10n_it_edi/models/account_invoice.py b/addons/l10n_it_edi/models/account_invoice.py
index 7b738830064..865b389eb22 100644
--- a/addons/l10n_it_edi/models/account_invoice.py
+++ b/addons/l10n_it_edi/models/account_invoice.py
@@ -43,7 +43,7 @@ class AccountMove(models.Model):
self.ensure_one()
report_name = self.env['account.edi.format']._l10n_it_edi_generate_electronic_invoice_filename(self)
- data = "" + str(self._export_as_xml())
+ data = "" + str(self._l10n_it_edi_export_invoice_as_xml())
description = _('Italian invoice: %s', self.move_type)
attachment = self.env['ir.attachment'].create({
'name': report_name,
diff --git a/addons/l10n_it_edi/tests/test_edi_export.py b/addons/l10n_it_edi/tests/test_edi_export.py
index 259c2daed20..ee866d66396 100644
--- a/addons/l10n_it_edi/tests/test_edi_export.py
+++ b/addons/l10n_it_edi/tests/test_edi_export.py
@@ -399,7 +399,7 @@ class TestItEdiExport(TestItEdi):
2577.29
''')
- invoice_etree = etree.fromstring(self.edi_format._export_as_xml(self.price_included_invoice))
+ invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.price_included_invoice))
# Remove the attachment and its details
invoice_etree = self.with_applied_xpath(invoice_etree, "")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
@@ -408,7 +408,7 @@ class TestItEdiExport(TestItEdi):
# The EDI can account for discounts, but a line with, for example, a 100% discount should still have
# a corresponding tax with a base amount of 0
- invoice_etree = etree.fromstring(self.edi_format._export_as_xml(self.partial_discount_invoice))
+ invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.partial_discount_invoice))
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_basis_xml),
'''
@@ -465,7 +465,7 @@ class TestItEdiExport(TestItEdi):
self.assertXmlTreeEqual(invoice_etree, expected_etree)
def test_fully_discounted_inovice(self):
- invoice_etree = etree.fromstring(self.edi_format._export_as_xml(self.full_discount_invoice))
+ invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.full_discount_invoice))
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_basis_xml),
'''
@@ -502,7 +502,7 @@ class TestItEdiExport(TestItEdi):
self.assertXmlTreeEqual(invoice_etree, expected_etree)
def test_non_latin_and_latin_invoice(self):
- invoice_etree = etree.fromstring(self.edi_format._export_as_xml(self.non_latin_and_latin_invoice))
+ invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.non_latin_and_latin_invoice))
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_basis_xml),
'''
@@ -551,7 +551,7 @@ class TestItEdiExport(TestItEdi):
self.assertXmlTreeEqual(invoice_etree, expected_etree)
def test_below_400_codice_simplified_invoice(self):
- invoice_etree = etree.fromstring(self.edi_format._export_as_xml(self.below_400_codice_simplified_invoice))
+ invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.below_400_codice_simplified_invoice))
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_simplified_basis_xml),
'''
@@ -581,7 +581,7 @@ class TestItEdiExport(TestItEdi):
self.assertXmlTreeEqual(invoice_etree, expected_etree)
def test_total_400_VAT_simplified_invoice(self):
- invoice_etree = etree.fromstring(self.edi_format._export_as_xml(self.total_400_VAT_simplified_invoice))
+ invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.total_400_VAT_simplified_invoice))
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_simplified_basis_xml),
'''
@@ -619,7 +619,7 @@ class TestItEdiExport(TestItEdi):
self.assertEqual(res[self.pa_partner_invoice], {'attachment': self.pa_partner_invoice.l10n_it_edi_attachment_id, 'success': True})
def test_zero_percent_taxes(self):
- invoice_etree = etree.fromstring(self.edi_format._export_as_xml(self.zero_tax_invoice))
+ invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.zero_tax_invoice))
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_basis_xml),
'''
@@ -667,7 +667,7 @@ class TestItEdiExport(TestItEdi):
self.assertXmlTreeEqual(invoice_etree, expected_etree)
def test_negative_price_invoice(self):
- invoice_etree = etree.fromstring(self.edi_format._export_as_xml(self.negative_price_invoice))
+ invoice_etree = etree.fromstring(self.edi_format._l10n_it_edi_export_invoice_as_xml(self.negative_price_invoice))
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_basis_xml),
'''
diff --git a/addons/l10n_it_edi/tests/test_edi_reverse_charge.py b/addons/l10n_it_edi/tests/test_edi_reverse_charge.py
index 34b81b624e9..038654dc11a 100644
--- a/addons/l10n_it_edi/tests/test_edi_reverse_charge.py
+++ b/addons/l10n_it_edi/tests/test_edi_reverse_charge.py
@@ -181,7 +181,7 @@ class TestItEdiReverseCharge(TestItEdi):
)
def _test_invoice_with_sample_file(self, invoice, filename, xpaths_file=None, xpaths_result=None):
- result = self._cleanup_etree(self.edi_format._export_as_xml(invoice), xpaths_result)
+ result = self._cleanup_etree(self.edi_format._l10n_it_edi_export_invoice_as_xml(invoice), xpaths_result)
expected = self._cleanup_etree(self._get_test_file_content(filename), xpaths_file)
self.assertXmlTreeEqual(result, expected)