[IMP] account_edi: merge account_edi_extended

TASK ID: 2457873

closes odoo/odoo#65745

Related: odoo/enterprise#16226
Related: odoo/upgrade#2144
Signed-off-by: Laurent Smet <smetl@users.noreply.github.com>
This commit is contained in:
Benjamin Frantzen (bfr)
2021-02-11 08:08:18 +00:00
parent 99ce4c3ff8
commit e3bd3fec38
22 changed files with 409 additions and 741 deletions
@@ -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.
+14 -16
View File
@@ -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']
+66 -4
View File
@@ -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'
@@ -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()
+102 -2
View File
@@ -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"<?xml version='1.0' encoding='UTF-8'?><Invoice/>"),
'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"<?xml version='1.0' encoding='UTF-8'?><Invoice/>"),
'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):
+155 -6
View File
@@ -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)
@@ -5,9 +5,13 @@
<field name="name">Account.edi.document.tree</field>
<field name="model">account.edi.document</field>
<field name="arch" type="xml">
<tree create="false" delete="false" edit="false">
<tree create="false" delete="false" edit="false"
decoration-info="blocking_level == 'info'"
decoration-warning="blocking_level == 'warning'"
decoration-danger="blocking_level == 'error'">
<field name="edi_format_name" />
<field name="error" />
<field name="blocking_level" invisible="1" />
</tree>
</field>
</record>
@@ -16,13 +16,21 @@
<field name="arch" type="xml">
<xpath expr="//button[@name='button_cancel']" position="after">
<field name="edi_show_cancel_button" invisible="1"/>
<field name="edi_show_abandon_cancel_button" invisible="1"/>
<button name="button_cancel_posted_moves"
string="Request EDI Cancellation"
type="object"
groups="account.group_account_invoice"
attrs="{'invisible' : [('edi_show_cancel_button', '=', False)]}"/>
<button name="button_abandon_cancel_posted_posted_moves"
string="Call off EDI Cancellation"
type="object"
groups="account.group_account_invoice"
attrs="{'invisible' : [('edi_show_abandon_cancel_button', '=', False)]}"/>
</xpath>
<xpath expr="//header" position="after">
<field name="edi_blocking_level" invisible="1" />
<field name="edi_error_count" invisible="1" />
<div class="alert alert-info" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': ['|', ('edi_web_services_to_process', 'in', ['', False]), ('state', '=', 'draft')]}">
<div>The invoice will be sent asynchronously to :
@@ -30,11 +38,29 @@
</div>
<button name="action_process_edi_web_services" type="object" class="oe_link" string="Send now" />
</div>
<div class="alert alert-danger" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': ['|', ('edi_error_count', '=', 0), ('edi_blocking_level', '!=', 'error')]}">
<div class="o_row">
<field name="edi_error_message" />
<button name="%(account_edi.action_open_edi_documents)d" string="⇒ See errors" type="action" class="oe_link" attrs="{'invisible': [('edi_error_count', '=', 1)]}" />
<button name="action_retry_edi_documents_error" type="object" class="oe_link oe_inline" string="Retry" />
</div>
</div>
<div class="alert alert-warning" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': [('edi_error_count', '=', 0)]}">
<field name="edi_error_count"/>
Electronic invoicing error(s)
<button name="%(account_edi.action_open_edi_documents)d" type="action" class="oe_link oe_inline" string="⇒ See errors" />
attrs="{'invisible': ['|', ('edi_error_count', '=', 0), ('edi_blocking_level', '!=', 'warning')]}">
<div class="o_row">
<field name="edi_error_message" />
<button name="%(account_edi.action_open_edi_documents)d" string="⇒ See errors" type="action" class="oe_link" attrs="{'invisible': [('edi_error_count', '=', 1)]}" />
<button name="action_retry_edi_documents_error" type="object" class="oe_link oe_inline" string="Retry" />
</div>
</div>
<div class="alert alert-info" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': ['|', ('edi_error_count', '=', 0), ('edi_blocking_level', '!=', 'info')]}">
<div class="o_row">
<field name="edi_error_message" />
<button name="%(account_edi.action_open_edi_documents)d" string="⇒ See errors" type="action" class="oe_link" attrs="{'invisible': [('edi_error_count', '=', 1)]}" />
<button name="action_retry_edi_documents_error" type="object" class="oe_link oe_inline" string="Retry" />
</div>
</div>
</xpath>
<xpath expr="//div[@name='journal_div']" position="after">
@@ -15,6 +15,8 @@
<field name="inherit_id" ref="account.view_account_payment_form" />
<field name="arch" type="xml">
<xpath expr="//header" position="after">
<field name="edi_blocking_level" invisible="1" />
<field name="edi_error_count" invisible="1" />
<div class="alert alert-info" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': ['|', ('edi_web_services_to_process', 'in', ['', False]), ('state', '=', 'draft')]}">
<div>The payment will be sent asynchronously to :
@@ -22,11 +24,27 @@
</div>
<button name="action_process_edi_web_services" type="object" class="oe_link" string="Send now"/>
</div>
<div class="alert alert-danger" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': ['|', ('edi_error_count', '=', 0), ('edi_blocking_level', '!=', 'error')]}">
<div class="o_row">
<field name="edi_error_message" />
<button name="%(account_edi.action_open_edi_documents)d" string="⇒ See errors" type="action" class="oe_link" attrs="{'invisible': [('edi_error_count', '=', 1)]}" />
<button name="action_retry_edi_documents_error" type="object" class="oe_link oe_inline" string="Retry" />
</div>
</div>
<div class="alert alert-warning" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': [('edi_error_count', '=', 0)]}">
<field name="edi_error_count"/>
Electronic invoicing error(s)
<button name="%(account_edi.action_open_payment_edi_documents)d" type="action" class="oe_link oe_inline" string="⇒ See errors" />
attrs="{'invisible': ['|', ('edi_error_count', '=', 0), ('edi_blocking_level', '!=', 'warning')]}">
<div class="o_row">
<field name="edi_error_message" />
<button name="%(account_edi.action_open_edi_documents)d" string="⇒ See errors" type="action" class="oe_link" attrs="{'invisible': [('edi_error_count', '=', 1)]}" />
</div>
</div>
<div class="alert alert-info" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': ['|', ('edi_error_count', '=', 0), ('edi_blocking_level', '!=', 'info')]}">
<div class="o_row">
<field name="edi_error_message" />
<button name="%(account_edi.action_open_edi_documents)d" string="⇒ See errors" type="action" class="oe_link" attrs="{'invisible': [('edi_error_count', '=', 1)]}" />
</div>
</div>
</xpath>
<xpath expr="//field[@name='journal_id']" position="after">
-13
View File
@@ -1,13 +0,0 @@
# -*- encoding: utf-8 -*-
from . import models
from odoo import api, SUPERUSER_ID
def account_edi_block_level(cr, registery):
''' The default value for blocking_level is 'error', but without this module,
the behavior is the same as a blocking_level of 'warning' so we need to set
all documents in error.
'''
env = api.Environment(cr, SUPERUSER_ID, {})
env['account.edi.document'].search([('error', '!=', False)]).write({'blocking_level': 'warning'})
@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
{
'name' : 'Additionnal features for account_edi',
'description':"""
This module add features to account_edi to support new Edi formats.
""",
'version' : '1.0',
'category': 'Accounting/Accounting',
'depends' : ['account_edi'],
'data': [
'views/account_edi_document_views.xml',
'views/account_move_views.xml',
'views/account_payment_views.xml',
],
'installable': True,
'application': False,
'auto_install': False,
'post_init_hook': 'account_edi_block_level',
}
@@ -1,151 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_edi_extended
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-05 11:07+0000\n"
"PO-Revision-Date: 2021-01-05 11:07+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: account_edi_extended
#: code:addons/account_edi_extended/models/account_move.py:0
#, python-format
msgid " Electronic invoicing error(s)"
msgstr ""
#. module: account_edi_extended
#: code:addons/account_edi_extended/models/account_move.py:0
#, python-format
msgid " Electronic invoicing info(s)"
msgstr ""
#. module: account_edi_extended
#: code:addons/account_edi_extended/models/account_move.py:0
#, python-format
msgid " Electronic invoicing warning(s)"
msgstr ""
#. module: account_edi_extended
#: code:addons/account_edi_extended/models/account_move.py:0
#, python-format
msgid "A request for cancellation of the EDI has been called off."
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields,field_description:account_edi_extended.field_account_edi_document__blocking_level
msgid "Blocking Level"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields,help:account_edi_extended.field_account_edi_document__blocking_level
msgid ""
"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."
msgstr ""
#. module: account_edi_extended
#: model_terms:ir.ui.view,arch_db:account_edi_extended.view_move_form_inherit
msgid "Call off EDI Cancellation"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields,field_description:account_edi_extended.field_account_edi_document__display_name
#: model:ir.model.fields,field_description:account_edi_extended.field_account_move__display_name
#: model:ir.model.fields,field_description:account_edi_extended.field_account_payment__display_name
msgid "Display Name"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields,field_description:account_edi_extended.field_account_bank_statement_line__edi_blocking_level
#: model:ir.model.fields,field_description:account_edi_extended.field_account_move__edi_blocking_level
#: model:ir.model.fields,field_description:account_edi_extended.field_account_payment__edi_blocking_level
msgid "Edi Blocking Level"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields,field_description:account_edi_extended.field_account_bank_statement_line__edi_error_message
#: model:ir.model.fields,field_description:account_edi_extended.field_account_move__edi_error_message
#: model:ir.model.fields,field_description:account_edi_extended.field_account_payment__edi_error_message
msgid "Edi Error Message"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields,field_description:account_edi_extended.field_account_bank_statement_line__edi_show_abandon_cancel_button
#: model:ir.model.fields,field_description:account_edi_extended.field_account_move__edi_show_abandon_cancel_button
#: model:ir.model.fields,field_description:account_edi_extended.field_account_payment__edi_show_abandon_cancel_button
msgid "Edi Show Abandon Cancel Button"
msgstr ""
#. module: account_edi_extended
#: model:ir.model,name:account_edi_extended.model_account_edi_document
msgid "Electronic Document for an account.move"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields.selection,name:account_edi_extended.selection__account_edi_document__blocking_level__error
#: model:ir.model.fields.selection,name:account_edi_extended.selection__account_move__edi_blocking_level__error
msgid "Error"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields,field_description:account_edi_extended.field_account_edi_document__id
#: model:ir.model.fields,field_description:account_edi_extended.field_account_move__id
#: model:ir.model.fields,field_description:account_edi_extended.field_account_payment__id
msgid "ID"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields.selection,name:account_edi_extended.selection__account_edi_document__blocking_level__info
#: model:ir.model.fields.selection,name:account_edi_extended.selection__account_move__edi_blocking_level__info
msgid "Info"
msgstr ""
#. module: account_edi_extended
#: code:addons/account_edi_extended/models/account_edi_document.py:0
#, python-format
msgid "Invalid configuration:"
msgstr ""
#. module: account_edi_extended
#: model:ir.model,name:account_edi_extended.model_account_move
msgid "Journal Entry"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields,field_description:account_edi_extended.field_account_edi_document____last_update
#: model:ir.model.fields,field_description:account_edi_extended.field_account_move____last_update
#: model:ir.model.fields,field_description:account_edi_extended.field_account_payment____last_update
msgid "Last Modified on"
msgstr ""
#. module: account_edi_extended
#: model:ir.model,name:account_edi_extended.model_account_payment
msgid "Payments"
msgstr ""
#. module: account_edi_extended
#: model_terms:ir.ui.view,arch_db:account_edi_extended.view_move_form_inherit
#: model_terms:ir.ui.view,arch_db:account_edi_extended.view_payment_form_inherit
msgid "Retry"
msgstr ""
#. module: account_edi_extended
#: model:ir.model.fields.selection,name:account_edi_extended.selection__account_edi_document__blocking_level__warning
#: model:ir.model.fields.selection,name:account_edi_extended.selection__account_move__edi_blocking_level__warning
msgid "Warning"
msgstr ""
#. module: account_edi_extended
#: model_terms:ir.ui.view,arch_db:account_edi_extended.view_move_form_inherit
msgid "⇒ See errors"
msgstr ""
@@ -1,3 +0,0 @@
from . import account_edi_document
from . import account_move
from . import account_payment
@@ -1,17 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
import logging
_logger = logging.getLogger(__name__)
DEFAULT_BLOCKING_LEVEL = 'warning' # Keep previous behavior. TODO : when account_edi_extended is merged with account_edi, should be 'error' (document will not be processed again until forced retry or reset to draft)
class AccountEdiDocument(models.Model):
_inherit = 'account.edi.document'
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.")
@@ -1,81 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, fields, _
class AccountMove(models.Model):
_inherit = 'account.move'
edi_show_abandon_cancel_button = fields.Boolean(
compute='_compute_edi_show_abandon_cancel_button')
edi_error_message = fields.Html(compute='_compute_edi_error_message')
edi_blocking_level = fields.Selection(selection=[('info', 'Info'), ('warning', 'Warning'), ('error', 'Error')], compute='_compute_edi_error_message')
@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):
# OVERRIDE to take blocking_level into account
for move in self:
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)
@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)
@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'
def action_retry_edi_documents_error(self):
self.edi_document_ids.write({'error': False, 'blocking_level': False})
self.action_process_edi_web_services()
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'})
@@ -1,12 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class AccountPayment(models.Model):
_inherit = 'account.payment'
def action_retry_edi_documents_error(self):
self.ensure_one()
return self.move_id.action_retry_edi_documents_error()
@@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common
from . import test_edi
-114
View File
@@ -1,114 +0,0 @@
# -*- 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 contextlib import contextmanager
from unittest.mock import patch
import base64
# TODO for the test to work, we need a chart template (COA) but we don't have any and don't want to add dependency (create empty coa ?)
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"<?xml version='1.0' encoding='UTF-8'?><Invoice/>"),
'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"<?xml version='1.0' encoding='UTF-8'?><Invoice/>"),
'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 AccountEdiExtendedTestCommon(AccountEdiTestCommon):
@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
@@ -1,157 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.account_edi_extended.tests.common import AccountEdiExtendedTestCommon, _mocked_post_two_steps, _generate_mocked_needs_web_services, _mocked_cancel_failed, _generate_mocked_support_batching
class TestAccountEdi(AccountEdiExtendedTestCommon):
@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_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)
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_tree_account_edi_document_inherit" model="ir.ui.view">
<field name="name">Account.edi.document.tree.inherit</field>
<field name="model">account.edi.document</field>
<field name="inherit_id" ref="account_edi.view_tree_account_edi_document"/>
<field name="arch" type="xml">
<xpath expr="//tree" position="inside">
<field name="blocking_level" invisible="1" />
</xpath>
<xpath expr="//tree" position="attributes">
<attribute name="decoration-info">blocking_level == 'info'</attribute>
<attribute name="decoration-warning">blocking_level == 'warning'</attribute>
<attribute name="decoration-danger">blocking_level == 'error'</attribute>
</xpath>
</field>
</record>
</data>
</odoo>
@@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_move_form_inherit" model="ir.ui.view">
<field name="name">account.move.form.inherit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account_edi.view_move_form_inherit" />
<field name="arch" type="xml">
<xpath expr="//button[@name='%(account_edi.action_open_edi_documents)d']" position="after">
<button name="action_retry_edi_documents_error" type="object" class="oe_link oe_inline" string="Retry" />
</xpath>
<xpath expr="//button[@name='button_cancel_posted_moves']" position="after">
<field name="edi_show_abandon_cancel_button" invisible="1"/>
<button name="button_abandon_cancel_posted_posted_moves"
string="Call off EDI Cancellation"
type="object"
groups="account.group_account_invoice"
attrs="{'invisible' : [('edi_show_abandon_cancel_button', '=', False)]}"/>
</xpath>
<!-- Nasty xpath to replace the error count warning banner. In master, it will be merged. -->
<xpath expr="//div[hasclass('alert-warning')]" position="replace">
<field name="edi_blocking_level" invisible="1" />
<field name="edi_error_count" invisible="1" />
<div class="alert alert-danger" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': ['|', ('edi_error_count', '=', 0), ('edi_blocking_level', '!=', 'error')]}">
<div class="o_row">
<field name="edi_error_message" />
<button name="%(account_edi.action_open_edi_documents)d" string="⇒ See errors" type="action" class="oe_link" attrs="{'invisible': [('edi_error_count', '=', 1)]}" />
<button name="action_retry_edi_documents_error" type="object" class="oe_link oe_inline" string="Retry" />
</div>
</div>
<div class="alert alert-warning" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': ['|', ('edi_error_count', '=', 0), ('edi_blocking_level', '!=', 'warning')]}">
<div class="o_row">
<field name="edi_error_message" />
<button name="%(account_edi.action_open_edi_documents)d" string="⇒ See errors" type="action" class="oe_link" attrs="{'invisible': [('edi_error_count', '=', 1)]}" />
</div>
</div>
<div class="alert alert-info" role="alert" style="margin-bottom:0px;"
attrs="{'invisible': ['|', ('edi_error_count', '=', 0), ('edi_blocking_level', '!=', 'info')]}">
<div class="o_row">
<field name="edi_error_message" />
<button name="%(account_edi.action_open_edi_documents)d" string="⇒ See errors" type="action" class="oe_link" attrs="{'invisible': [('edi_error_count', '=', 1)]}" />
</div>
</div>
</xpath>
</field>
</record>
</data>
</odoo>
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_payment_form_inherit" model="ir.ui.view">
<field name="name">account.payment.form.inherit</field>
<field name="model">account.payment</field>
<field name="inherit_id" ref="account_edi.view_payment_form_inherit" />
<field name="arch" type="xml">
<xpath expr="//button[@name='%(account_edi.action_open_payment_edi_documents)d']" position="after">
<button name="action_retry_edi_documents_error" type="object" class="oe_link oe_inline" string="Retry" />
</xpath>
</field>
</record>
</data>
</odoo>