449 lines
21 KiB
Python
449 lines
21 KiB
Python
from datetime import datetime, timedelta
|
|
from unittest.mock import patch
|
|
from markupsafe import Markup
|
|
import contextlib
|
|
|
|
from odoo import fields
|
|
from odoo.exceptions import UserError
|
|
from odoo.tests import freeze_time, tagged
|
|
|
|
from .common import TestCoDianCommon
|
|
|
|
|
|
@freeze_time('2024-01-30')
|
|
@tagged('post_install_l10n', 'post_install', '-at_install')
|
|
class TestDianFlows(TestCoDianCommon):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.invoice = cls._create_move()
|
|
|
|
def _send_test_set_async(self):
|
|
self.invoice.company_id.l10n_co_dian_test_environment = True
|
|
self.invoice.company_id.l10n_co_dian_certification_process = True
|
|
|
|
# Step 1: SendTestSetAsync
|
|
self._mock_send_and_print(move=self.invoice, response_file='SendTestSetAsync.xml')
|
|
|
|
self.assertRecordValues(self.invoice.l10n_co_dian_document_ids, [{
|
|
'zip_key': '42424242',
|
|
'state': 'invoice_pending',
|
|
'message': "<p>Invoice is being processed by the DIAN.</p>",
|
|
}])
|
|
self.assertEqual(self.invoice.l10n_co_dian_state, 'invoice_pending')
|
|
|
|
# ==== Get Status Zip ==== #
|
|
|
|
def test_get_status_zip_warnings(self):
|
|
""" SendTestSetAsync then GetStatusZip which returns warnings but the document is valid. """
|
|
# Step 1: SendTestSetAsync
|
|
self._send_test_set_async()
|
|
# Step 2: GetStatusZip
|
|
self._mock_get_status_zip(move=self.invoice, response_file='GetStatusZip_warnings.xml')
|
|
|
|
# checks on the document
|
|
self.assertEqual(len(self.invoice.l10n_co_dian_document_ids), 1)
|
|
self.assertRecordValues(self.invoice.l10n_co_dian_document_ids, [{
|
|
'zip_key': '42424242',
|
|
'state': 'invoice_accepted',
|
|
'message': (
|
|
"<p>Procesado Correctamente.</p>"
|
|
"<ul><li>Regla: FAJ44b, Notificación: Nit o Documento de Identificación BlaBlaBla.</li>"
|
|
"<li>Regla: FAJ43b, Notificación: Nombre informado No corresponde al BlaBlaBla.</li></ul>"
|
|
),
|
|
}])
|
|
# checks on the invoice
|
|
self.assertTrue(self.invoice.l10n_co_dian_attachment_id)
|
|
self.assertEqual(self.invoice.l10n_co_dian_state, 'invoice_accepted')
|
|
|
|
def test_get_status_zip_habilitation(self):
|
|
""" SendTestSetAsync then GetStatusZip which inform that they passed the habitation process. """
|
|
# Step 1: SendTestSetAsync
|
|
self._send_test_set_async()
|
|
# Step 2: GetStatusZip
|
|
self._mock_get_status_zip(move=self.invoice, response_file='GetStatusZip_habilitation.xml')
|
|
|
|
# checks on the document
|
|
self.assertEqual(len(self.invoice.l10n_co_dian_document_ids), 1)
|
|
self.assertRecordValues(self.invoice.l10n_co_dian_document_ids, [{
|
|
'zip_key': '42424242',
|
|
'state': 'invoice_rejected',
|
|
'message': "<p>Set de prueba con identificador 123456 se encuentra Aceptado.</p>",
|
|
}])
|
|
# checks on the invoice
|
|
self.assertFalse(self.invoice.l10n_co_dian_attachment_id)
|
|
self.assertEqual(self.invoice.l10n_co_dian_state, 'invoice_rejected')
|
|
|
|
def test_get_status_zip_pending(self):
|
|
""" SendTestSetAsync then GetStatusZip which responds 'Batch is being processed' (need to check again later)"""
|
|
# Step 1: SendTestSetAsync
|
|
self._send_test_set_async()
|
|
# Step 2: GetStatusZip
|
|
self._mock_get_status_zip(move=self.invoice, response_file='GetStatusZip_pending.xml')
|
|
|
|
# checks on the document
|
|
self.assertEqual(len(self.invoice.l10n_co_dian_document_ids), 1)
|
|
self.assertRecordValues(self.invoice.l10n_co_dian_document_ids, [{
|
|
'zip_key': '42424242',
|
|
'state': 'invoice_pending',
|
|
'message': "<p>Batch en proceso de validación.</p>",
|
|
'show_button_get_status': True,
|
|
}])
|
|
# checks on the invoice
|
|
self.assertFalse(self.invoice.l10n_co_dian_attachment_id)
|
|
self.assertEqual(self.invoice.l10n_co_dian_state, 'invoice_pending')
|
|
|
|
# Step 3: GetStatusZip again (this time the document is processed)
|
|
self._mock_get_status_zip(move=self.invoice, response_file='GetStatusZip_warnings.xml')
|
|
|
|
# checks on the document
|
|
self.assertEqual(len(self.invoice.l10n_co_dian_document_ids), 1)
|
|
self.assertRecordValues(self.invoice.l10n_co_dian_document_ids, [{
|
|
'zip_key': '42424242',
|
|
'state': 'invoice_accepted',
|
|
'message': (
|
|
"<p>Procesado Correctamente.</p>"
|
|
"<ul><li>Regla: FAJ44b, Notificación: Nit o Documento de Identificación BlaBlaBla.</li>"
|
|
"<li>Regla: FAJ43b, Notificación: Nombre informado No corresponde al BlaBlaBla.</li></ul>"
|
|
),
|
|
}])
|
|
# checks on the invoice
|
|
self.assertTrue(self.invoice.l10n_co_dian_attachment_id)
|
|
self.assertEqual(self.invoice.l10n_co_dian_state, 'invoice_accepted')
|
|
|
|
# ==== Send Bill Sync ==== #
|
|
|
|
def test_send_bill_sync_warnings(self):
|
|
""" SendBillSync returning warnings but the document is valid. """
|
|
self._mock_send_and_print(move=self.invoice, response_file='SendBillSync_warnings.xml')
|
|
|
|
self.assertTrue(self.invoice.l10n_co_dian_attachment_id)
|
|
self.assertRecordValues(self.invoice.l10n_co_dian_document_ids, [{
|
|
'zip_key': False,
|
|
'state': 'invoice_accepted',
|
|
'message': (
|
|
"<p>Procesado Correctamente.</p>"
|
|
"<ul><li>Regla: FAJ44b, Notificación: Nit o Documento de Identificación informado No BlaBlaBla.</li>"
|
|
"<li>Regla: FAJ43b, Notificación: Nombre informado No corresponde al registrado en el BlaBlaBla.</li></ul>"
|
|
),
|
|
}])
|
|
|
|
def test_send_bill_sync_errors(self):
|
|
""" SendBillSync returning errors blocking the Send & Print flow. """
|
|
error_raised = False
|
|
try:
|
|
self._mock_send_and_print(move=self.invoice, response_file='SendBillSync_errors.xml')
|
|
except UserError as e:
|
|
error_raised = True
|
|
self.assertEqual(e.args[0], "Error(s) when sending the document to the DIAN:\n"
|
|
"- Regla: ZB01, Rechazo: Fallo en el esquema XML del archivo")
|
|
|
|
self.assertTrue(error_raised)
|
|
self.assertRecordValues(self.invoice.l10n_co_dian_document_ids, [{
|
|
'zip_key': False,
|
|
'state': 'invoice_rejected',
|
|
'message': "<p>Documento con errores en campos mandatorios.</p>"
|
|
"<ul><li>Regla: ZB01, Rechazo: Fallo en el esquema XML del archivo</li></ul>",
|
|
}])
|
|
|
|
def test_send_bill_sync_errors_nsmap(self):
|
|
""" SendBillSync returning errors due to a missing namespace, blocking the Send & Print flow. """
|
|
error_raised = False
|
|
try:
|
|
self._mock_send_and_print(move=self.invoice, response_file='SendBillSync_errors_nsmap.xml')
|
|
except UserError as e:
|
|
error_raised = True
|
|
self.assertEqual(e.args[0], "Error(s) when sending the document to the DIAN:\n- Error al parsear xml. Namespace prefix 'sts' is not defined.")
|
|
|
|
self.assertTrue(error_raised)
|
|
self.assertRecordValues(self.invoice.l10n_co_dian_document_ids, [{
|
|
'zip_key': False,
|
|
'state': 'invoice_rejected',
|
|
'message': "<p>Error al parsear xml. Namespace prefix 'sts' is not defined.</p>",
|
|
}])
|
|
|
|
def test_send_bill_sync_duplicated_identifier_accepted(self):
|
|
""" SendBillSync returning 'Regla: 90, Rechazo: Documento procesado anteriormente.'
|
|
This means an invoice with the same CUFE has already been accepted by the DIAN.
|
|
The invoice on DIAN has the same partner and issue date than the one being sent,
|
|
therefore it is accepted.
|
|
"""
|
|
error_raised = False
|
|
try:
|
|
xml = self._read_file('l10n_co_dian/tests/attachments/invoice_alcohol.xml', 'rb')
|
|
with patch(f'{self.document_path}._get_xml_by_document_key', return_value=xml):
|
|
self._mock_send_and_print(move=self.invoice, response_file='SendBillSync_duplicated.xml')
|
|
except UserError:
|
|
error_raised = True
|
|
|
|
self.assertFalse(error_raised)
|
|
self.assertRecordValues(self.invoice.l10n_co_dian_document_ids, [{
|
|
'zip_key': False,
|
|
'state': 'invoice_accepted',
|
|
'message': ("<p>Validación contiene errores en campos mandatorios.</p>"
|
|
"<ul><li>Regla: 90, Rechazo: Documento procesado anteriormente.</li></ul>"),
|
|
}])
|
|
|
|
def test_send_bill_sync_duplicated_identifier_rejected(self):
|
|
""" SendBillSync returning 'Regla: 90, Rechazo: Documento procesado anteriormente.'
|
|
This means an invoice with the same CUFE has already been accepted by the DIAN.
|
|
The invoice on DIAN has the same partner but a different issue time than the one
|
|
being sent, therefore it is rejected.
|
|
"""
|
|
error_raised = False
|
|
try:
|
|
xml = self._read_file('l10n_co_dian/tests/attachments/invoice_alcohol.xml', 'rb')
|
|
# change the issue time in the XML so that the invoice should be rejected
|
|
xml = xml.replace(
|
|
b'<cbc:IssueTime>19:00:00-05:00</cbc:IssueTime>',
|
|
b'<cbc:IssueTime>08:00:00-05:00</cbc:IssueTime>'
|
|
)
|
|
with patch(f'{self.document_path}._get_xml_by_document_key', return_value=xml):
|
|
self._mock_send_and_print(move=self.invoice, response_file='SendBillSync_duplicated.xml')
|
|
except UserError:
|
|
error_raised = True
|
|
|
|
self.assertTrue(error_raised)
|
|
self.assertRecordValues(self.invoice.l10n_co_dian_document_ids, [{
|
|
'zip_key': False,
|
|
'state': 'invoice_rejected',
|
|
'message': Markup("<p>Validación contiene errores en campos mandatorios.</p>"
|
|
"<ul><li>Regla: 90, Rechazo: Documento procesado anteriormente.</li></ul>"),
|
|
}])
|
|
|
|
def test_send_bill_sync_second_attempt(self):
|
|
""" First attempt SendBillSync returns errors, second attempt is successful. """
|
|
# 1st attempt (fail)
|
|
self.test_send_bill_sync_errors()
|
|
|
|
self.assertFalse(self.invoice.l10n_co_dian_attachment_id)
|
|
self.assertRecordValues(self.invoice.l10n_co_dian_document_ids, [{
|
|
'zip_key': False,
|
|
'state': 'invoice_rejected',
|
|
'message': "<p>Documento con errores en campos mandatorios.</p>"
|
|
"<ul><li>Regla: ZB01, Rechazo: Fallo en el esquema XML del archivo</li></ul>",
|
|
}])
|
|
|
|
# 2nd attempt (success without errors or warnings)
|
|
with self.patched_document('_parse_errors', []):
|
|
self._mock_send_and_print(move=self.invoice, response_file='SendBillSync_warnings.xml')
|
|
|
|
self.assertEqual(len(self.invoice.l10n_co_dian_document_ids), 1) # no need to keep the rejected documents
|
|
self.assertTrue(self.invoice.l10n_co_dian_attachment_id)
|
|
self.assertRecordValues(self.invoice.l10n_co_dian_document_ids, [{
|
|
'zip_key': False,
|
|
'state': 'invoice_accepted',
|
|
'message': "<p>Procesado Correctamente.</p>",
|
|
}])
|
|
|
|
def test_send_bill_sync_get_status_error(self):
|
|
"""
|
|
The attempt SendBillSync is accepted, but getStatus got a non 200 status code
|
|
=> status of the invoice should still be invoice_accepted
|
|
"""
|
|
with (
|
|
self.patched_document('_get_status', self._mocked_response('GetStatus_invoice.xml', 503)),
|
|
self._mock_build_and_send_request('SendBillSync_warnings.xml'),
|
|
self._disable_get_acquirer_call(),
|
|
self.assertRaisesRegex(UserError, "Error\\(s\\) when generating the Attached Document"),
|
|
):
|
|
self.env['account.move.send.wizard'].with_context(
|
|
active_model=self.invoice._name,
|
|
active_ids=self.invoice.ids
|
|
).create({}).action_send_and_print()
|
|
|
|
self.assertRecordValues(self.invoice.l10n_co_dian_document_ids, [{
|
|
'state': 'invoice_accepted',
|
|
}])
|
|
|
|
def test_send_bill_sync_unknown_error(self):
|
|
""" Handle the generic error returned when using wrong certificates (e.g. the demo certificate). """
|
|
error_raised = False
|
|
try:
|
|
self._mock_send_and_print(move=self.invoice, response_file='unknown_error.xml', response_code=500)
|
|
except UserError as e:
|
|
error_raised = True
|
|
self.assertEqual(e.args[0], "Error(s) when sending the document to the DIAN:\n"
|
|
"- The creator of this fault did not specify a Reason. "
|
|
"(This might be caused by using incorrect certificates)")
|
|
self.assertTrue(error_raised)
|
|
self.assertRecordValues(self.invoice.l10n_co_dian_document_ids, [{
|
|
'zip_key': False,
|
|
'state': 'invoice_sending_failed',
|
|
'message': "<p>The creator of this fault did not specify a Reason. "
|
|
"(This might be caused by using incorrect certificates)</p>",
|
|
}])
|
|
|
|
def test_send_bill_sync_support_document(self):
|
|
""" Send Support Document. """
|
|
bill = self._create_move(
|
|
move_type='in_invoice',
|
|
invoice_date=datetime.today(),
|
|
journal_id=self.support_document_journal.id,
|
|
)
|
|
with (
|
|
patch(f'{self.utils_path}._build_and_send_request', return_value=self._mocked_response('SendBillSync_warnings.xml', 200)),
|
|
self._disable_get_acquirer_call(),
|
|
):
|
|
bill.l10n_co_dian_action_send_bill_support_document()
|
|
|
|
self.assertTrue(bill.l10n_co_dian_attachment_id)
|
|
self.assertRecordValues(bill.l10n_co_dian_document_ids, [{
|
|
'zip_key': False,
|
|
'state': 'invoice_accepted',
|
|
'message': "<p>Procesado Correctamente.</p>"
|
|
"<ul><li>Regla: FAJ44b, Notificación: Nit o Documento de Identificación informado No BlaBlaBla.</li>"
|
|
"<li>Regla: FAJ43b, Notificación: Nombre informado No corresponde al registrado en el BlaBlaBla.</li></ul>",
|
|
}])
|
|
|
|
def test_get_acquirer(self):
|
|
""" Get Partner Data """
|
|
partner = self._create_partner(
|
|
country_id=self.env.ref('base.co').id,
|
|
vat='213123432-1',
|
|
email='test@mail.com',
|
|
)
|
|
|
|
with patch(f'{self.utils_path}._build_and_send_request', return_value=self._mocked_response('GetAcquirer_partner.xml', 200)):
|
|
partner.button_l10n_co_dian_refresh_data()
|
|
|
|
self.assertTrue(partner.child_ids)
|
|
self.assertRecordValues(partner.child_ids[0], [{
|
|
'name': 'Real Company Name',
|
|
'email': 'company@mail.com',
|
|
}])
|
|
|
|
def test_get_aquirer_constraints(self):
|
|
"""Refresh is rejected on a child contact and on a partner that already has an invoicing child."""
|
|
partner = self._create_partner(
|
|
country_id=self.env.ref('base.co').id,
|
|
vat='213123432-1',
|
|
email='test@mail.com',
|
|
)
|
|
|
|
self.env['res.partner'].create({
|
|
'name': 'Child Contact',
|
|
'parent_id': partner.id,
|
|
'type': 'invoice',
|
|
'email': 'existing@mail.com',
|
|
})
|
|
with patch(f'{self.utils_path}._build_and_send_request', return_value=self._mocked_response('GetAcquirer_partner.xml', 200)):
|
|
with self.assertRaisesRegex(UserError, "This contact has already been updated with DIAN information"):
|
|
partner.button_l10n_co_dian_refresh_data()
|
|
|
|
def test_invoice_date_constraints_dian(self):
|
|
"""Test that invoices date older than 6 days or more than 6 days ahead trigger the constraint."""
|
|
bogota_today = fields.Date.context_today(self.env.user.with_context(tz='America/Bogota'))
|
|
|
|
valid_invoice = self._create_move(invoice_date=bogota_today - timedelta(days=6))
|
|
self._mock_send_and_print(move=valid_invoice, response_file='SendBillSync_warnings.xml')
|
|
|
|
invalid_invoice = self._create_move(invoice_date=bogota_today - timedelta(days=7))
|
|
with self.assertRaisesRegex(UserError, "The issue date can not be older than 6 days or more than 6 days in the future."):
|
|
self._mock_send_and_print(move=invalid_invoice, response_file='SendBillSync_warnings.xml')
|
|
|
|
def test_dian_state_machine(self):
|
|
"""
|
|
Test how move.l10n_co_dian_state and move.l10n_co_dian_commercial_state change when
|
|
communicating with DIAN.
|
|
Communication with DIAN is always represented by a l10n_co_dian.document, which in turn
|
|
could alter the states of the account.move.
|
|
The tested sequence is as follows:
|
|
1. sending failure because of a 500 response
|
|
2. invoice rejected by dian
|
|
3. invoice accepted by dian
|
|
4. status updated with a response GetStatusEvent_no_events
|
|
"""
|
|
# we are not testing for the UseError in this scope, so we ignore it.
|
|
with contextlib.suppress(UserError):
|
|
self._mock_send_and_print(self.invoice, 'unknown_error.xml', response_code=500)
|
|
self.assertEqual(self.invoice.l10n_co_dian_state, 'invoice_sending_failed')
|
|
self.assertFalse(self.invoice.l10n_co_dian_commercial_state)
|
|
|
|
# we are not testing for the UseError in this scope, so we ignore it.
|
|
with contextlib.suppress(UserError):
|
|
self._mock_send_and_print(self.invoice, 'SendBillSync_errors.xml')
|
|
self.assertEqual(self.invoice.l10n_co_dian_state, 'invoice_rejected')
|
|
self.assertFalse(self.invoice.l10n_co_dian_commercial_state)
|
|
|
|
self._mock_send_and_print(self.invoice, 'SendBillSync_warnings.xml')
|
|
self.assertEqual(self.invoice.l10n_co_dian_state, 'invoice_accepted')
|
|
self.assertEqual(self.invoice.l10n_co_dian_commercial_state, 'pending')
|
|
|
|
track_id = self.invoice.l10n_co_edi_cufe_cude_ref
|
|
with self._mock_build_and_send_request('GetStatusEvent_no_events.xml'):
|
|
self.env['l10n_co_dian.document']._send_get_status_event(self.invoice, track_id)
|
|
|
|
docs = self.invoice.l10n_co_dian_document_ids.sorted()
|
|
self.assertEqual(docs[0].state, 'invoice_rejected')
|
|
self.assertEqual(docs[1].state, 'invoice_accepted')
|
|
self.assertEqual(self.invoice.l10n_co_dian_state, 'invoice_accepted')
|
|
|
|
|
|
@freeze_time('2024-01-30')
|
|
@tagged('post_install_l10n', 'post_install', '-at_install')
|
|
class TestDianFlowsSettings(TestCoDianCommon):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.settings = cls.env['res.config.settings'].create({})
|
|
cls.settings.l10n_co_dian_test_environment = True
|
|
cls.settings.l10n_co_dian_certification_process = True
|
|
cls.settings.l10n_co_dian_cert_invoice_count = 2
|
|
cls.settings.l10n_co_dian_cert_credit_count = 2
|
|
cls.settings.l10n_co_dian_cert_debit_count = 2
|
|
|
|
def _mocked_build_and_send_request(self, record, payload, service, company):
|
|
"""Build_and_send_request is a module level function in xml_util that contains a parameter
|
|
self. To be able to mock it from within the test class we need an additional parameter to
|
|
make sure it can be called."""
|
|
if service == 'SendTestSetAsync':
|
|
return self._mocked_response('SendTestSetAsync.xml', 200)
|
|
if service == 'GetStatusZip':
|
|
return self._mocked_response('GetStatusZip_habilitation.xml', 200)
|
|
return self._mocked_response('', 200)
|
|
|
|
def test_certification_process(self):
|
|
""" Test the certification process with the DIAN. """
|
|
with (
|
|
patch(f'{self.utils_path}._build_and_send_request', autospec=True, side_effect=self._mocked_build_and_send_request),
|
|
self._disable_get_acquirer_call(),
|
|
):
|
|
self.settings.action_l10n_co_certify_with_dian()
|
|
# Check that the journals were created or updated.
|
|
journals = self.env['account.journal'].search([('code', 'in', ['SETP', 'NC', 'ND'])])
|
|
self.assertEqual(len(journals), 3)
|
|
|
|
# Unique journal values
|
|
self.assertRecordValues(journals, [
|
|
{'code': 'SETP', 'l10n_co_edi_min_range_number': 990000000, 'l10n_co_edi_max_range_number': 995000000, 'l10n_co_edi_debit_note': False},
|
|
{'code': 'NC', 'l10n_co_edi_min_range_number': 1, 'l10n_co_edi_max_range_number': 1000, 'l10n_co_edi_debit_note': False},
|
|
{'code': 'ND', 'l10n_co_edi_min_range_number': 1, 'l10n_co_edi_max_range_number': 1000, 'l10n_co_edi_debit_note': True},
|
|
])
|
|
|
|
# Shared journal values
|
|
self.assertRecordValues(journals, [{
|
|
'l10n_co_dian_technical_key': 'fc8eac422eba16e22ffd8c6f94b3f40a6e38162c',
|
|
'l10n_co_edi_dian_authorization_date': fields.Date.from_string('2019-01-19'),
|
|
'l10n_co_edi_dian_authorization_end_date': fields.Date.from_string('2030-01-19'),
|
|
'l10n_co_edi_dian_authorization_number': '18760000001',
|
|
}] * 3)
|
|
|
|
# Check that the moves were created
|
|
moves = self.env['account.move'].search([
|
|
('move_type', 'in', ['out_invoice', 'out_refund']),
|
|
('state', '=', 'posted'),
|
|
('journal_id', 'in', journals.ids),
|
|
])
|
|
self.assertEqual(len(moves), 6)
|
|
|
|
# The test file we use is one already accepted so it should all be marked as already accepted.
|
|
for move in moves:
|
|
self.assertRecordValues(move.l10n_co_dian_document_ids, [{
|
|
'zip_key': '42424242',
|
|
'state': 'invoice_rejected',
|
|
'message': "<p>Set de prueba con identificador 123456 se encuentra Aceptado.</p>",
|
|
}])
|