diff --git a/addons/account_edi/models/account_edi_document.py b/addons/account_edi/models/account_edi_document.py
index bcc5e8e6012..6a44e7f3ea7 100644
--- a/addons/account_edi/models/account_edi_document.py
+++ b/addons/account_edi/models/account_edi_document.py
@@ -2,12 +2,13 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api
-from odoo.addons.account_edi_extended.models.account_edi_document import DEFAULT_BLOCKING_LEVEL
from psycopg2 import OperationalError
import logging
_logger = logging.getLogger(__name__)
+DEFAULT_BLOCKING_LEVEL = 'error'
+
class AccountEdiDocument(models.Model):
_name = 'account.edi.document'
@@ -19,6 +20,12 @@ class AccountEdiDocument(models.Model):
attachment_id = fields.Many2one('ir.attachment', help='The file generated by edi_format_id when the invoice is posted (and this document is processed).')
state = fields.Selection([('to_send', 'To Send'), ('sent', 'Sent'), ('to_cancel', 'To Cancel'), ('cancelled', 'Cancelled')])
error = fields.Html(help='The text of the last error that happened during Electronic Invoice operation.')
+ blocking_level = fields.Selection(
+ selection=[('info', 'Info'), ('warning', 'Warning'), ('error', 'Error')],
+ help="Blocks the document current operation depending on the error severity :\n"
+ " * Info: the document is not blocked and everything is working as it should.\n"
+ " * Warning : there is an error that doesn't prevent the current Electronic Invoicing operation to succeed.\n"
+ " * Error : there is an error that blocks the current Electronic Invoicing operation.")
# == Not stored fields ==
name = fields.Char(related='attachment_id.name')
@@ -32,14 +39,6 @@ class AccountEdiDocument(models.Model):
),
]
- def write(self, vals):
- ''' If account_edi_extended is not installed, a default behaviour is used instead.
- '''
- if 'blocking_level' in vals and 'blocking_level' not in self.env['account.edi.document']._fields:
- vals.pop('blocking_level')
-
- return super().write(vals)
-
def _prepare_jobs(self):
"""Creates a list of jobs to be performed by '_process_job' for the documents in self.
Each document represent a job, BUT if multiple documents have the same state, edi_format_id,
@@ -53,10 +52,7 @@ class AccountEdiDocument(models.Model):
# Classify jobs by (edi_format, edi_doc.state, doc_type, move.company_id, custom_key)
to_process = {}
- if 'blocking_level' in self.env['account.edi.document']._fields:
- documents = self.filtered(lambda d: d.state in ('to_send', 'to_cancel') and d.blocking_level != 'error')
- else:
- documents = self.filtered(lambda d: d.state in ('to_send', 'to_cancel'))
+ 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
@@ -88,36 +84,6 @@ class AccountEdiDocument(models.Model):
target.append((batch, doc_type))
return invoices + payments
- @api.model
- def _convert_to_old_jobs_format(self, jobs):
- """ See '_prepare_jobs' :
- Old format : ((edi_format, state, doc_type, company_id), documents)
- Since edi_format, state and company_id can be deduced from documents, this is redundant and more prone to unexpected behaviours.
- New format : (doc_type, documents).
-
- However, for backward compatibility of 'process_jobs', we need a way to convert back to the old format.
- """
- return [(
- (documents.edi_format_id, documents[0].state, doc_type, documents.move_id.company_id),
- documents
- ) for documents, doc_type in jobs]
-
- @api.model
- def _process_jobs(self, to_process):
- """ Deprecated, use _process_job instead.
-
- :param to_process: A list of tuples (key, documents)
- * key: A tuple (edi_format_id, state, doc_type, company_id)
- ** edi_format_id: The format to perform the operation with
- ** state: The state of the documents of this job
- ** doc_type: Are the moves of this job invoice or payments ?
- ** company_id: The company the moves belong to
- * documents: The documents related to this job. If edi_format_id does not support batch, length is one
- """
- for key, documents in to_process:
- dummy, dummy, doc_type, dummy = key
- self._process_job(documents, doc_type)
-
@api.model
def _process_job(self, documents, doc_type):
"""Post or cancel move_id (invoice or payment) by calling the related methods on edi_format_id.
@@ -220,7 +186,8 @@ class AccountEdiDocument(models.Model):
""" 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()
- self._process_jobs(self._convert_to_old_jobs_format(jobs))
+ for documents, doc_type in jobs:
+ self._process_job(documents, doc_type)
def _process_documents_web_services(self, job_count=None, with_commit=True):
""" Post and cancel all the documents that need a web service. This is called by CRON.
diff --git a/addons/account_edi/models/account_edi_format.py b/addons/account_edi/models/account_edi_format.py
index 40e4311279c..1f9059486b2 100644
--- a/addons/account_edi/models/account_edi_format.py
+++ b/addons/account_edi/models/account_edi_format.py
@@ -98,12 +98,15 @@ class AccountEdiFormat(models.Model):
# TO OVERRIDE
return False
- def _support_batching(self, move=None, state=None, company=None):
+ 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.
- :returns: True if batching is supported, False otherwise.
+ :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
@@ -112,8 +115,11 @@ class AccountEdiFormat(models.Model):
""" 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 ().
+ 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()
@@ -122,7 +128,7 @@ class AccountEdiFormat(models.Model):
def _check_move_configuration(self, move):
""" Checks the move and relevant records for potential error (missing data, etc).
- :param invoice: The move to check.
+ :param move: The move to check.
:returns: A list of error messages.
"""
# TO OVERRIDE
@@ -136,7 +142,7 @@ class AccountEdiFormat(models.Model):
:returns: A dictionary with the invoice as key and as value, another dictionary:
* attachment: The attachment representing the invoice in this edi_format if the edi was successfully posted.
* error: An error if the edi was not successfully posted.
- * blocking_level: (optional, requires account_edi_extended) How bad is the error (how should the edi flow be blocked ?)
+ * blocking_level: (optional) How bad is the error (how should the edi flow be blocked ?)
"""
# TO OVERRIDE
self.ensure_one()
@@ -150,7 +156,7 @@ class AccountEdiFormat(models.Model):
: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, requires account_edi_extended) How bad is the error (how should the edi flow be blocked ?)
+ * blocking_level: (optional) How bad is the error (how should the edi flow be blocked ?)
"""
# TO OVERRIDE
self.ensure_one()
@@ -164,7 +170,7 @@ class AccountEdiFormat(models.Model):
: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, requires account_edi_extended) How bad is the error (how should the edi flow be blocked ?)
+ * blocking_level: (optional) How bad is the error (how should the edi flow be blocked ?)
"""
# TO OVERRIDE
self.ensure_one()
@@ -178,7 +184,7 @@ class AccountEdiFormat(models.Model):
: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, requires account_edi_extended) How bad is the error (how should the edi flow be blocked ?)
+ * blocking_level: (optional) How bad is the error (how should the edi flow be blocked ?)
"""
# TO OVERRIDE
self.ensure_one()
@@ -376,10 +382,6 @@ class AccountEdiFormat(models.Model):
except Exception as e:
_logger.exception("Error importing attachment \"%s\" as invoice with format \"%s\"", file_data['filename'], edi_format.name, str(e))
if res:
- if 'extract_state' in res:
- # Bypass the OCR to prevent overwriting data when an EDI was succesfully imported.
- # TODO : remove when we integrate the OCR to the EDI flow.
- res.write({'extract_state': 'done'})
return res
return self.env['account.move']
@@ -401,10 +403,6 @@ class AccountEdiFormat(models.Model):
except Exception as e:
_logger.exception("Error importing attachment \"%s\" as invoice with format \"%s\"", file_data['filename'], edi_format.name, str(e))
if res:
- if 'extract_state' in res:
- # Bypass the OCR to prevent overwriting data when an EDI was succesfully imported.
- # TODO : remove when we integrate the OCR to the EDI flow.
- res.write({'extract_state': 'done'})
return res
return self.env['account.move']
diff --git a/addons/account_edi/models/account_move.py b/addons/account_edi/models/account_move.py
index 75a917b5ee9..9d0194be268 100644
--- a/addons/account_edi/models/account_move.py
+++ b/addons/account_edi/models/account_move.py
@@ -20,11 +20,18 @@ class AccountMove(models.Model):
edi_error_count = fields.Integer(
compute='_compute_edi_error_count',
help='How many EDIs are in error for this move ?')
+ edi_blocking_level = fields.Selection(
+ selection=[('info', 'Info'), ('warning', 'Warning'), ('error', 'Error')],
+ compute='_compute_edi_error_message')
+ edi_error_message = fields.Html(
+ compute='_compute_edi_error_message')
edi_web_services_to_process = fields.Text(
compute='_compute_edi_web_services_to_process',
help="Technical field to display the documents that will be processed by the CRON")
edi_show_cancel_button = fields.Boolean(
compute='_compute_edi_show_cancel_button')
+ edi_show_abandon_cancel_button = fields.Boolean(
+ compute='_compute_edi_show_abandon_cancel_button')
@api.depends('edi_document_ids.state')
def _compute_edi_state(self):
@@ -46,14 +53,37 @@ class AccountMove(models.Model):
for move in self:
move.edi_error_count = len(move.edi_document_ids.filtered(lambda d: d.error))
+ @api.depends('edi_error_count', 'edi_document_ids.error', 'edi_document_ids.blocking_level')
+ def _compute_edi_error_message(self):
+ for move in self:
+ if move.edi_error_count == 0:
+ move.edi_error_message = None
+ move.edi_blocking_level = None
+ elif move.edi_error_count == 1:
+ error_doc = move.edi_document_ids.filtered(lambda d: d.error)
+ move.edi_error_message = error_doc.error
+ move.edi_blocking_level = error_doc.blocking_level
+ else:
+ error_levels = set([doc.blocking_level for doc in move.edi_document_ids])
+ if 'error' in error_levels:
+ move.edi_error_message = str(move.edi_error_count) + _(" Electronic invoicing error(s)")
+ move.edi_blocking_level = 'error'
+ elif 'warning' in error_levels:
+ move.edi_error_message = str(move.edi_error_count) + _(" Electronic invoicing warning(s)")
+ move.edi_blocking_level = 'warning'
+ else:
+ move.edi_error_message = str(move.edi_error_count) + _(" Electronic invoicing info(s)")
+ move.edi_blocking_level = 'info'
+
@api.depends(
'edi_document_ids',
'edi_document_ids.state',
+ 'edi_document_ids.blocking_level',
'edi_document_ids.edi_format_id',
'edi_document_ids.edi_format_id.name')
def _compute_edi_web_services_to_process(self):
for move in self:
- to_process = move.edi_document_ids.filtered(lambda d: d.state in ['to_send', 'to_cancel'])
+ to_process = move.edi_document_ids.filtered(lambda d: d.state in ['to_send', 'to_cancel'] and d.blocking_level != 'error')
format_web_services = to_process.edi_format_id.filtered(lambda f: f._needs_web_services())
move.edi_web_services_to_process = ', '.join(f.name for f in format_web_services)
@@ -89,6 +119,18 @@ class AccountMove(models.Model):
and doc.edi_format_id._is_required_for_invoice(move)
for doc in move.edi_document_ids])
+ @api.depends(
+ 'state',
+ 'edi_document_ids.state',
+ 'edi_document_ids.attachment_id')
+ 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)
+
####################################################
# Export Electronic Document
####################################################
@@ -206,6 +248,23 @@ class AccountMove(models.Model):
to_cancel_documents.write({'state': 'to_cancel', 'error': False, 'blocking_level': False})
+ def button_abandon_cancel_posted_posted_moves(self):
+ '''Cancel the request for cancellation of the EDI.
+ '''
+ documents = self.env['account.edi.document']
+ 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):
+ documents |= doc
+ is_move_marked = True
+ if is_move_marked:
+ move.message_post(body=_("A request for cancellation of the EDI has been called off."))
+
+ documents.write({'state': 'sent'})
+
def _get_edi_document(self, edi_format):
return self.edi_document_ids.filtered(lambda d: d.edi_format_id == edi_format)
@@ -233,11 +292,14 @@ class AccountMove(models.Model):
####################################################
def action_process_edi_web_services(self):
- docs = self.edi_document_ids.filtered(lambda d: d.state in ('to_send', 'to_cancel'))
- if 'blocking_level' in self.env['account.edi.document']._fields:
- docs = docs.filtered(lambda d: d.blocking_level != 'error')
+ docs = self.edi_document_ids.filtered(lambda d: d.state in ('to_send', 'to_cancel') and d.blocking_level != 'error')
docs._process_documents_web_services(with_commit=False)
+ def action_retry_edi_documents_error(self):
+ self.edi_document_ids.write({'error': False, 'blocking_level': False})
+ self.action_process_edi_web_services()
+
+
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
diff --git a/addons/account_edi/models/account_payment.py b/addons/account_edi/models/account_payment.py
index 8b8a691d63e..ce746c91705 100644
--- a/addons/account_edi/models/account_payment.py
+++ b/addons/account_edi/models/account_payment.py
@@ -9,3 +9,7 @@ class AccountPayment(models.Model):
def action_process_edi_web_services(self):
return self.move_id.action_process_edi_web_services()
+
+ def action_retry_edi_documents_error(self):
+ self.ensure_one()
+ return self.move_id.action_retry_edi_documents_error()
diff --git a/addons/account_edi/tests/common.py b/addons/account_edi/tests/common.py
index 28eb26d3def..6059f569bf7 100644
--- a/addons/account_edi/tests/common.py
+++ b/addons/account_edi/tests/common.py
@@ -10,6 +10,68 @@ from unittest import mock
import base64
+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_check_move_configuration_success(edi_format, move):
+ return []
+
+
+def _mocked_check_move_configuration_fail(edi_format, move):
+ return ['Fake error (mocked)']
+
+
+def _mocked_post(edi_format, invoices, test_mode):
+ 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] = {'attachment': attachment}
+ return res
+
+
+def _mocked_post_two_steps(edi_format, invoices, test_mode):
+ # 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] = {'attachment': attachment}
+ return res
+ else:
+ raise ValueError('wrong use of "_mocked_post_two_steps"')
+
+
+def _mocked_cancel_success(edi_format, invoices, test_mode):
+ return {invoice: {'success': True} for invoice in invoices}
+
+
+def _mocked_cancel_failed(edi_format, invoices, test_mode):
+ return {invoice: {'error': 'Faked error (mocked)'} for invoice in invoices}
+
+
class AccountEdiTestCommon(AccountTestInvoicingCommon):
@classmethod
@@ -31,6 +93,46 @@ class AccountEdiTestCommon(AccountTestInvoicingCommon):
# EDI helpers
####################################################
+ @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,
+ _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',
+ 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):
+
+ yield
+ finally:
+ pass
+
def edi_cron(self):
self.env['account.edi.document'].sudo().with_context(edi_test_mode=True).search([('state', 'in', ('to_send', 'to_cancel'))])._process_documents_web_services(with_commit=False)
@@ -39,8 +141,6 @@ class AccountEdiTestCommon(AccountTestInvoicingCommon):
'move_type': 'in_invoice',
'journal_id': self.company_data['default_journal_purchase'].id,
})
- if 'extract_state' in invoice._fields:
- invoice.extract_state = 'done' # prevent ocr
return invoice
def update_invoice_from_file(self, module_name, subfolder, filename, invoice):
diff --git a/addons/account_edi/tests/test_edi.py b/addons/account_edi/tests/test_edi.py
index 85baee809b4..bfd8f6f8518 100644
--- a/addons/account_edi/tests/test_edi.py
+++ b/addons/account_edi/tests/test_edi.py
@@ -1,17 +1,22 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo.addons.account_edi.tests.common import AccountEdiTestCommon
+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
class TestAccountEdi(AccountEdiTestCommon):
+ @classmethod
+ 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)
+
def test_export_edi(self):
- invoice = self.init_invoice('out_invoice', products=self.product_a)
- self.assertEqual(len(invoice.edi_document_ids), 0)
- invoice.action_post()
- self.assertEqual(len(invoice.edi_document_ids), 1)
+ self.assertEqual(len(self.invoice.edi_document_ids), 0)
+ self.invoice.action_post()
+ self.assertEqual(len(self.invoice.edi_document_ids), 1)
def test_prepare_jobs(self):
@@ -39,11 +44,155 @@ class TestAccountEdi(AccountEdiTestCommon):
self.assertEqual(len(to_process), 2)
@patch('odoo.addons.account_edi.models.account_edi_format.AccountEdiFormat._post_invoice_edi')
- def test_error(self, patched):
+ 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'
edi_docs.move_id.action_process_edi_web_services()
patched.assert_called_once()
+
+ 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')
+
+ 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 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()
+ self.assertEqual(doc.state, 'cancelled')
+ self.assertEqual(self.invoice.state, 'cancel')
+
+ 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()
+ 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, test_mode):
+ 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}
+ else:
+ raise ValueError('wrong use of "_mocked_post_two_steps"')
+
+ def _is_needed_for_invoice(edi_format, invoice):
+ return not bool(invoice.ref)
+
+ 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')
+
+ # 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')
+
+ # 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)
diff --git a/addons/account_edi/views/account_edi_document_views.xml b/addons/account_edi/views/account_edi_document_views.xml
index 9b16ff60deb..37354094d73 100644
--- a/addons/account_edi/views/account_edi_document_views.xml
+++ b/addons/account_edi/views/account_edi_document_views.xml
@@ -5,9 +5,13 @@
Account.edi.document.treeaccount.edi.document
-
+
+
diff --git a/addons/account_edi/views/account_move_views.xml b/addons/account_edi/views/account_move_views.xml
index 96321487225..4b5c36b6d9b 100644
--- a/addons/account_edi/views/account_move_views.xml
+++ b/addons/account_edi/views/account_move_views.xml
@@ -16,13 +16,21 @@
+
+
+
+
The invoice will be sent asynchronously to :
@@ -30,11 +38,29 @@