# Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import tools from odoo.tests.common import tagged from odoo.exceptions import UserError from odoo.addons.account.tests.test_account_move_send import TestAccountMoveSendCommon from odoo.addons.l10n_hu_edi.tests.common import L10nHuEdiTestCommon import requests from unittest import mock from freezegun import freeze_time import contextlib @tagged('post_install_l10n', '-at_install', 'post_install') class L10nHuEdiTestFlowsMocked(L10nHuEdiTestCommon, TestAccountMoveSendCommon): """ Test the Hungarian EDI flows using mocked data from the test servers. """ @classmethod def setUpClass(cls): with freeze_time('2024-01-25T15:28:53Z'): super().setUpClass() def test_send_invoice_and_credit_note(self): with self.patch_post(), \ freeze_time('2024-01-25T15:28:53Z'): invoice = self.create_invoice_simple() invoice.action_post() send_and_print = self.create_send_and_print(invoice) self.assertTrue(send_and_print.extra_edi_checkboxes and send_and_print.extra_edi_checkboxes.get('hu_nav_30', {}).get('checked')) self.assertFalse(invoice._l10n_hu_edi_check_invoices()) send_and_print.action_send_and_print() self.assertRecordValues(invoice, [{'l10n_hu_edi_state': 'confirmed', 'l10n_hu_invoice_chain_index': -1}]) credit_note = self.create_reversal(invoice) credit_note.action_post() send_and_print = self.create_send_and_print(credit_note) self.assertTrue(send_and_print.extra_edi_checkboxes and send_and_print.extra_edi_checkboxes.get('hu_nav_30', {}).get('checked')) self.assertFalse(credit_note._l10n_hu_edi_check_invoices()) send_and_print.action_send_and_print() self.assertRecordValues(credit_note, [{'l10n_hu_edi_state': 'confirmed', 'l10n_hu_invoice_chain_index': 1}]) def test_send_invoice_warning(self): with tools.file_open('l10n_hu_edi/tests/mocked_requests/queryTransactionStatus_response_warning.xml', 'r') as response_file: response_data = response_file.read() with self.patch_post({'queryTransactionStatus': response_data}), \ freeze_time('2024-01-25T15:28:53Z'): invoice = self.create_invoice_simple() invoice.action_post() send_and_print = self.create_send_and_print(invoice) self.assertTrue(send_and_print.extra_edi_checkboxes and send_and_print.extra_edi_checkboxes.get('hu_nav_30', {}).get('checked')) self.assertFalse(invoice._l10n_hu_edi_check_invoices()) send_and_print.action_send_and_print() self.assertRecordValues(invoice, [{'l10n_hu_edi_state': 'confirmed_warning', 'l10n_hu_invoice_chain_index': -1}]) def test_send_invoice_error(self): with tools.file_open('l10n_hu_edi/tests/mocked_requests/queryTransactionStatus_response_error.xml', 'r') as response_file: response_data = response_file.read() with self.patch_post({'queryTransactionStatus': response_data}), \ freeze_time('2024-01-25T15:28:53Z'): invoice = self.create_invoice_simple() invoice.action_post() send_and_print = self.create_send_and_print(invoice) self.assertTrue(send_and_print.extra_edi_checkboxes and send_and_print.extra_edi_checkboxes.get('hu_nav_30', {}).get('checked')) self.assertFalse(invoice._l10n_hu_edi_check_invoices()) with contextlib.suppress(UserError): send_and_print.action_send_and_print() self.assertRecordValues(invoice, [{'l10n_hu_edi_state': 'rejected', 'l10n_hu_invoice_chain_index': 0}]) def test_timeout_recovery_fail(self): with freeze_time('2024-01-25T15:28:53Z'), \ self.patch_post({'manageInvoice': requests.Timeout()}): invoice = self.create_invoice_simple() invoice.action_post() send_and_print = self.create_send_and_print(invoice) self.assertTrue(send_and_print.extra_edi_checkboxes and send_and_print.extra_edi_checkboxes.get('hu_nav_30', {}).get('checked')) self.assertFalse(invoice._l10n_hu_edi_check_invoices()) send_and_print.action_send_and_print() self.assertRecordValues(invoice, [{'l10n_hu_edi_state': 'send_timeout', 'l10n_hu_invoice_chain_index': -1}]) with tools.file_open('l10n_hu_edi/tests/mocked_requests/queryTransactionStatus_response_original.xml', 'r') as response_file: response_data = response_file.read() # Advance 10 minutes so the timeout recovery mechanism triggers. with freeze_time('2024-01-25T15:38:53Z'), \ self.patch_post({'queryTransactionStatus': response_data}): with contextlib.suppress(UserError): invoice.l10n_hu_edi_button_update_status() self.assertRecordValues(invoice, [{'l10n_hu_edi_state': 'rejected', 'l10n_hu_invoice_chain_index': 0}]) def test_timeout_recovery_success(self): with freeze_time('2024-01-25T15:28:53Z'), \ self.patch_post({'manageInvoice': requests.Timeout()}): invoice = self.create_invoice_simple() invoice.name = 'INV/2024/00999' # This matches the invoice name in the XML returned by queryTransactionStatus. invoice.action_post() send_and_print = self.create_send_and_print(invoice) self.assertTrue(send_and_print.extra_edi_checkboxes and send_and_print.extra_edi_checkboxes.get('hu_nav_30', {}).get('checked')) self.assertFalse(invoice._l10n_hu_edi_check_invoices()) send_and_print.action_send_and_print() self.assertRecordValues(invoice, [{'l10n_hu_edi_state': 'send_timeout', 'l10n_hu_invoice_chain_index': -1}]) # This returns an original XML with name INV/2024/00999 with tools.file_open('l10n_hu_edi/tests/mocked_requests/queryTransactionStatus_response_original.xml', 'r') as response_file: response_data = response_file.read() # Advance 10 minutes so the timeout recovery mechanism triggers. with freeze_time('2024-01-25T15:38:53Z'), \ self.patch_post({'queryTransactionStatus': response_data}): invoice.l10n_hu_edi_button_update_status() self.assertRecordValues(invoice, [{'l10n_hu_edi_state': 'confirmed', 'l10n_hu_invoice_chain_index': -1}]) def test_cancel_invoice_error(self): with freeze_time('2024-01-25T15:28:53Z'): with self.patch_post(): invoice, cancel_wizard = self.create_cancel_wizard() self.assertRecordValues(invoice, [{'l10n_hu_edi_state': 'confirmed'}]) with tools.file_open('l10n_hu_edi/tests/mocked_requests/queryTransactionStatus_response_error.xml', 'r') as response_file: response_data = response_file.read() with self.patch_post({'queryTransactionStatus': response_data}): with contextlib.suppress(UserError): cancel_wizard.button_request_cancel() self.assertRecordValues(invoice, [{'l10n_hu_edi_state': 'confirmed_warning', 'l10n_hu_invoice_chain_index': -1}]) def test_cancel_invoice_pending(self): with freeze_time('2024-01-25T15:28:53Z'): with self.patch_post(): invoice, cancel_wizard = self.create_cancel_wizard() self.assertRecordValues(invoice, [{'l10n_hu_edi_state': 'confirmed'}]) with tools.file_open('l10n_hu_edi/tests/mocked_requests/queryTransactionStatus_response_annulment_pending.xml', 'r') as response_file: response_data = response_file.read() with self.patch_post({'queryTransactionStatus': response_data}): cancel_wizard.button_request_cancel() self.assertRecordValues(invoice, [{'l10n_hu_edi_state': 'cancel_pending', 'l10n_hu_invoice_chain_index': -1}]) def test_cancel_invoice_done(self): with freeze_time('2024-01-25T15:28:53Z'): with self.patch_post(): invoice, cancel_wizard = self.create_cancel_wizard() self.assertRecordValues(invoice, [{'l10n_hu_edi_state': 'confirmed', 'l10n_hu_invoice_chain_index': -1}]) with tools.file_open('l10n_hu_edi/tests/mocked_requests/queryTransactionStatus_response_annulment_done.xml', 'r') as response_file: response_data = response_file.read() with self.patch_post({'queryTransactionStatus': response_data}): cancel_wizard.button_request_cancel() self.assertRecordValues(invoice, [{'l10n_hu_edi_state': 'cancelled', 'state': 'cancel', 'l10n_hu_invoice_chain_index': 0}]) def test_cancel_and_resend(self): """ Test the sending, annulment and re-sending of an invoice + credit note + modif. invoice """ with freeze_time('2024-01-25T15:28:53Z'): with self.patch_post(): invoice, cancel_wizard = self.create_cancel_wizard() self.assertRecordValues(invoice, [{'l10n_hu_edi_state': 'confirmed', 'l10n_hu_invoice_chain_index': -1}]) new_invoice = self.create_reversal(invoice, is_modify=True) self.assertRecordValues(new_invoice, [{'debit_origin_id': invoice.id}]) new_invoice.action_post() credit_note = invoice.reversal_move_ids send_and_print = self.create_send_and_print(credit_note) self.assertTrue(send_and_print.extra_edi_checkboxes and send_and_print.extra_edi_checkboxes.get('hu_nav_30', {}).get('checked')) self.assertFalse(credit_note._l10n_hu_edi_check_invoices()) send_and_print.action_send_and_print() self.assertRecordValues(credit_note, [{'l10n_hu_edi_state': 'confirmed', 'l10n_hu_invoice_chain_index': 1}]) send_and_print = self.create_send_and_print(new_invoice) self.assertTrue(send_and_print.extra_edi_checkboxes and send_and_print.extra_edi_checkboxes.get('hu_nav_30', {}).get('checked')) self.assertFalse(new_invoice._l10n_hu_edi_check_invoices()) send_and_print.action_send_and_print() self.assertRecordValues(new_invoice, [{'l10n_hu_edi_state': 'confirmed', 'l10n_hu_invoice_chain_index': 2}]) with tools.file_open('l10n_hu_edi/tests/mocked_requests/queryTransactionStatus_response_annulment_done.xml', 'r') as response_file: response_data = response_file.read() with self.patch_post({'queryTransactionStatus': response_data}): cancel_wizard.button_request_cancel() self.assertRecordValues(invoice, [{'l10n_hu_edi_state': 'cancelled', 'state': 'cancel', 'l10n_hu_invoice_chain_index': 0}]) self.assertRecordValues(credit_note, [{'l10n_hu_edi_state': 'cancelled', 'state': 'cancel', 'l10n_hu_invoice_chain_index': 0}]) self.assertRecordValues(new_invoice, [{'l10n_hu_edi_state': 'cancelled', 'state': 'cancel', 'l10n_hu_invoice_chain_index': 0}]) (invoice | credit_note | new_invoice).button_draft() invoice.action_post() credit_note.action_post() new_invoice.action_post() with self.patch_post(): send_and_print = self.create_send_and_print(invoice) self.assertTrue(send_and_print.extra_edi_checkboxes and send_and_print.extra_edi_checkboxes.get('hu_nav_30', {}).get('checked')) self.assertFalse(invoice._l10n_hu_edi_check_invoices()) send_and_print.action_send_and_print() self.assertRecordValues(invoice, [{'l10n_hu_edi_state': 'confirmed', 'l10n_hu_invoice_chain_index': -1}]) send_and_print = self.create_send_and_print(credit_note) self.assertTrue(send_and_print.extra_edi_checkboxes and send_and_print.extra_edi_checkboxes.get('hu_nav_30', {}).get('checked')) self.assertFalse(credit_note._l10n_hu_edi_check_invoices()) send_and_print.action_send_and_print() self.assertRecordValues(credit_note, [{'l10n_hu_edi_state': 'confirmed', 'l10n_hu_invoice_chain_index': 1}]) send_and_print = self.create_send_and_print(new_invoice) self.assertTrue(send_and_print.extra_edi_checkboxes and send_and_print.extra_edi_checkboxes.get('hu_nav_30', {}).get('checked')) self.assertFalse(new_invoice._l10n_hu_edi_check_invoices()) send_and_print.action_send_and_print() self.assertRecordValues(new_invoice, [{'l10n_hu_edi_state': 'confirmed', 'l10n_hu_invoice_chain_index': 2}]) # === Helpers === # @contextlib.contextmanager def patch_post(self, responses=None): """ Patch requests.Session in l10n_hu_edi.connection. :param responses: If specified, a dict {service: response} that gives, for any service, bytes that should be served as response data, or an Exception that should be raised. Otherwise, will use the default responses stored under mocked_requests/{service}_response.xml """ test_case = self class MockedSession: def post(self, url, data, headers, timeout=None): prod_url = 'https://api.onlineszamla.nav.gov.hu/invoiceService/v3' demo_url = 'https://api-test.onlineszamla.nav.gov.hu/invoiceService/v3' mocked_requests = ['manageInvoice', 'queryTaxpayer', 'tokenExchange', 'queryTransactionStatus', 'queryTransactionList', 'manageAnnulment'] base_url, __, service = url.rpartition('/') if base_url not in (prod_url, demo_url) or service not in mocked_requests: test_case.fail(f'Invalid POST url: {url}') with tools.file_open(f'l10n_hu_edi/tests/mocked_requests/{service}_request.xml', 'rb') as expected_request_file: test_case.assertXmlTreeEqual( test_case.get_xml_tree_from_string(data), test_case.get_xml_tree_from_string(expected_request_file.read()), ) mock_response = mock.Mock(spec=requests.Response) mock_response.status_code = 200 mock_response.headers = '' if responses and service in responses: if isinstance(responses[service], Exception): raise responses[service] mock_response.text = responses[service] else: with tools.file_open(f'l10n_hu_edi/tests/mocked_requests/{service}_response.xml', 'r') as response_file: mock_response.text = response_file.read() return mock_response def close(self): pass with mock.patch('odoo.addons.l10n_hu_edi.models.l10n_hu_edi_connection.requests.Session', side_effect=MockedSession, autospec=True): yield