From e3bd3fec383a074ef8eaea8da7665251a5f95da2 Mon Sep 17 00:00:00 2001 From: "Benjamin Frantzen (bfr)" Date: Mon, 8 Feb 2021 16:13:06 +0000 Subject: [PATCH] [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 --- .../models/account_edi_document.py | 55 ++---- .../account_edi/models/account_edi_format.py | 30 ++-- addons/account_edi/models/account_move.py | 70 +++++++- addons/account_edi/models/account_payment.py | 4 + addons/account_edi/tests/common.py | 104 ++++++++++- addons/account_edi/tests/test_edi.py | 161 +++++++++++++++++- .../views/account_edi_document_views.xml | 6 +- .../account_edi/views/account_move_views.xml | 34 +++- .../views/account_payment_views.xml | 26 ++- addons/account_edi_extended/__init__.py | 13 -- addons/account_edi_extended/__manifest__.py | 19 --- .../i18n/account_edi_extended.pot | 151 ---------------- .../account_edi_extended/models/__init__.py | 3 - .../models/account_edi_document.py | 17 -- .../models/account_move.py | 81 --------- .../models/account_payment.py | 12 -- addons/account_edi_extended/tests/__init__.py | 5 - addons/account_edi_extended/tests/common.py | 114 ------------- addons/account_edi_extended/tests/test_edi.py | 157 ----------------- .../views/account_edi_document_views.xml | 21 --- .../views/account_move_views.xml | 52 ------ .../views/account_payment_views.xml | 15 -- 22 files changed, 409 insertions(+), 741 deletions(-) delete mode 100644 addons/account_edi_extended/__init__.py delete mode 100644 addons/account_edi_extended/__manifest__.py delete mode 100644 addons/account_edi_extended/i18n/account_edi_extended.pot delete mode 100644 addons/account_edi_extended/models/__init__.py delete mode 100644 addons/account_edi_extended/models/account_edi_document.py delete mode 100644 addons/account_edi_extended/models/account_move.py delete mode 100644 addons/account_edi_extended/models/account_payment.py delete mode 100644 addons/account_edi_extended/tests/__init__.py delete mode 100644 addons/account_edi_extended/tests/common.py delete mode 100644 addons/account_edi_extended/tests/test_edi.py delete mode 100644 addons/account_edi_extended/views/account_edi_document_views.xml delete mode 100644 addons/account_edi_extended/views/account_move_views.xml delete mode 100644 addons/account_edi_extended/views/account_payment_views.xml 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.tree account.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 @@ +