from freezegun import freeze_time import random import requests from base64 import b64encode from contextlib import contextmanager from datetime import datetime, date from os.path import join as opj from unittest.mock import Mock, patch import uuid from odoo import Command from odoo.addons.account.tests.common import AccountTestInvoicingCommon from odoo.tools import file_open from odoo.addons.l10n_co_dian.models.res_partner import ResPartner from odoo.addons.l10n_co_edi.models.res_partner import FINAL_CONSUMER_VAT class TestCoDianCommon(AccountTestInvoicingCommon): @classmethod @AccountTestInvoicingCommon.setup_country('co') def setUpClass(cls): super().setUpClass() cls.frozen_today = datetime(year=2024, month=1, day=30) cls.document_path = 'odoo.addons.l10n_co_dian.models.l10n_co_dian_document.L10n_Co_DianDocument' cls.utils_path = 'odoo.addons.l10n_co_dian.xml_utils' with freeze_time(cls.frozen_today): cls.key_demo = cls.env["certificate.key"].create({ "name": "Test DIAN Key", "content": b64encode(cls._read_file('l10n_co_dian/demo/demo_key.pem', 'rb')), "company_id": cls.company_data['company'].id, }) cls.certificate_demo = cls.env["certificate.certificate"].create({ "name": "Test DIAN certificate", "content": b64encode(cls._read_file('l10n_co_dian/demo/demo_cert.crt', 'rb')), "private_key_id": cls.key_demo.id, "company_id": cls.company_data['company'].id, }) city_bogota = cls.env.ref('l10n_co_edi.city_co_150') cls.company_data['company'].write({ 'name': "CO Company Test", 'vat': "1018419008-5", 'zip': "110110", 'street': "CL 12A", 'city': city_bogota.name, 'state_id': city_bogota.state_id.id, 'country_id': cls.env.ref('base.co').id, 'l10n_co_edi_header_actividad_economica': '0114', 'l10n_co_dian_operation_mode_ids': [ Command.create({ 'dian_software_operation_mode': 'invoice', 'dian_software_id': 'Odoo', 'dian_software_security_code': '12345', 'dian_testing_id': 'test_id', 'company_id': cls.company_data['company'].id, }), Command.create({ 'dian_software_operation_mode': 'bill', 'dian_software_id': 'Odoo', 'dian_software_security_code': '12345', 'dian_testing_id': 'test_id', 'company_id': cls.company_data['company'].id, }), ], 'l10n_co_dian_certificate_ids': [Command.set(cls.certificate_demo.ids)], 'l10n_co_dian_test_environment': True, 'l10n_co_dian_provider': 'dian', }) cls.company_data['company'].partner_id.write({ 'city_id': city_bogota.id, 'l10n_co_edi_obligation_type_ids': [Command.set(cls.env.ref('l10n_co_edi.obligation_type_1').ids)], 'l10n_latam_identification_type_id': cls.env.ref('l10n_co.rut').id, }) # Journals dian_vals = { 'l10n_co_edi_dian_authorization_number': "18760000001", 'l10n_co_edi_dian_authorization_date': date(2020, 1, 19), 'l10n_co_edi_dian_authorization_end_date': date(2030, 1, 19), 'l10n_co_edi_min_range_number': "990000000", 'l10n_co_edi_max_range_number': "995000000", 'l10n_co_dian_technical_key': "dummy_journal_technical_key", } cls.company_data['default_journal_sale'].write({ 'code': 'SETP', **dian_vals, }) cls.debit_note_journal = cls.env['account.journal'].create({ 'name': "DIAN Debit Note", 'l10n_co_edi_debit_note': True, 'code': "DN", 'type': "sale", **dian_vals, }) cls.support_document_journal = cls.env['account.journal'].create({ 'name': "DIAN Support Document", 'l10n_co_edi_is_support_document': True, 'code': "DS", 'type': "purchase", **dian_vals, }) # Partners cls.partner_co = cls.env['res.partner'].create({ 'name': "ADQUIRIENTE DE EJEMPLO", 'property_account_receivable_id': cls.company_data['default_account_receivable'].id, 'property_account_payable_id': cls.company_data['default_account_payable'].id, 'street': "CARRERA 8 No 20-14/40", 'city': city_bogota.name, 'city_id': city_bogota.id, 'state_id': city_bogota.state_id.id, 'zip': "110110", 'country_id': cls.env.ref('base.co').id, 'vat': "900108281", 'bank_ids': [Command.create({'acc_number': "0123456789"})], 'l10n_co_edi_obligation_type_ids': cls.env.ref('l10n_co_edi.obligation_type_1'), 'l10n_latam_identification_type_id': cls.env.ref('l10n_co.rut').id, }) cls.final_consumer = cls.env['res.partner'].create({ 'name': "consumidor o usuario final", 'country_id': cls.env.ref('base.co').id, 'l10n_latam_identification_type_id': cls.env.ref('l10n_co.national_citizen_id').id, 'vat': FINAL_CONSUMER_VAT, 'l10n_co_edi_obligation_type_ids': cls.env.ref('l10n_co_edi.obligation_type_5'), 'l10n_co_edi_fiscal_regimen': '49', }) # Taxes cls.tax_iva_5 = cls.env["account.chart.template"].ref('l10n_co_tax_0') cls.tax_iva_19 = cls.env["account.chart.template"].ref('l10n_co_tax_8') cls.tax_ret_ica_1_104 = cls.env["account.chart.template"].ref('l10n_co_tax_45') cls.tax_ret_ica_0414 = cls.env["account.chart.template"].ref('l10n_co_tax_57') cls.tax_ret_iva_2_85 = cls.env["account.chart.template"].ref('l10n_co_tax_12') # Sugar Taxes (need to fill 'l10n_co_edi_ref_nominal_tax' on the product !) cls.sugar_tax_1 = cls.env['account.tax'].create({ 'name': "IBUA >10gr 3500ml", 'amount_type': 'fixed', 'amount': 35 * 35, # rate of the tax = 35 (for a product with >10gr of sugar per 100ml) 'l10n_co_edi_type': cls.env.ref('l10n_co_edi.tax_type_20').id, # IBUA }) cls.sugar_tax_2 = cls.sugar_tax_1.copy({ 'name': "IBUA >6gr & <10gr 100ml", 'amount': 36, # rate of the tax = 36 (for a product with >10gr of sugar per 100ml) }) # Products cls.product_a.barcode = "562438192" cls.product_b.default_code = "prod_b" cls.product_sugar_1 = cls._create_product( name="Coca cola 3.5L", l10n_co_edi_ref_nominal_tax=3500, default_code='P1111', ) cls.product_sugar_2 = cls._create_product( name="Sprite 100mL", l10n_co_edi_ref_nominal_tax=100, default_code='P2222', ) @classmethod def _create_move(cls, **kwargs): with freeze_time(kwargs.pop('freeze_date', None) or cls.frozen_today): invoice = cls.env['account.move'].create({ 'partner_id': cls.partner_co.id, 'move_type': 'out_invoice', 'journal_id': cls.company_data['default_journal_sale'].id, 'invoice_line_ids': [ Command.create({'product_id': cls.product_a.id, 'tax_ids': [Command.set(cls.tax_iva_5.ids)]}), ], **kwargs, }) invoice.action_post() return invoice @classmethod def _create_product(cls, **kwargs): return cls.env['product.product'].create({ 'name': 'Product', 'uom_id': cls.env.ref('uom.product_uom_unit').id, 'property_account_income_id': cls.company_data['default_account_revenue'].id, 'property_account_expense_id': cls.company_data['default_account_expense'].id, **kwargs, }) def _generate_xml(self, invoice, with_error=False): with self._disable_get_acquirer_call(): result = self.env['account.edi.xml.ubl_dian']._export_invoice(invoice) if with_error: return result else: return result[0] def _assert_document_dian(self, xml, file): expected_dian = self._read_file(file, 'rb') self.assertXmlTreeEqual( self.get_xml_tree_from_string(xml), self.get_xml_tree_from_string(expected_dian), ) @classmethod def _get_last_sent_document(cls, move): move.ensure_one() return cls.env['l10n_co_dian.document'] if not move.l10n_co_dian_document_ids else move.l10n_co_dian_document_ids.sorted()[0] @classmethod def _read_file(cls, path, *args): with file_open(path, *args) as f: content = f.read() return content def _update_invoice_from_file(self, module_name, subfolder, filename, invoice): """ Create an attachment from a file and post it on the invoice """ file_path = opj(module_name, subfolder, filename) file = self._read_file(file_path, 'rb') attachment = self.env['ir.attachment'].create({ 'name': filename, 'datas': b64encode(file), 'res_id': invoice.id, 'res_model': 'account.move', }) invoice.message_post(message_type='comment', attachment_ids=[attachment.id]) def _assert_imported_invoice_from_file(self, subfolder, filename, invoice_vals): """ Create an empty account.move, update the xml file, and then check the invoice values. """ journal = self.company_data['default_journal_purchase'] invoice = self.env['account.move'].create({'move_type': 'in_invoice', 'journal_id': journal.id}) self._update_invoice_from_file( module_name='l10n_co_dian', subfolder=subfolder, filename=filename, invoice=invoice, ) invoice_vals = invoice_vals.copy() invoice_lines = invoice_vals.pop('invoice_lines', False) self.assertRecordValues(invoice, [invoice_vals]) if invoice_lines: self.assertRecordValues(invoice.invoice_line_ids, invoice_lines) @contextmanager def _mock_uuid_generation(self): rnd = random.Random(42) with patch(f'{self.utils_path}._uuid1', lambda: uuid.UUID(int=rnd.getrandbits(128))): yield @contextmanager def patched_document(self, method_name, return_value): with patch.object( self.registry['l10n_co_dian.document'], method_name, return_value=return_value, ): yield def _mocked_request(self, response_file, status_code): mock_response = Mock(spec=requests.Response) mock_response.status_code = status_code mock_response.text = self._read_file(f'l10n_co_dian/tests/responses/{response_file}') return mock_response def _mocked_response(self, response_file, status_code): return { 'response': self._read_file(f'l10n_co_dian/tests/responses/{response_file}'), 'status_code': status_code, } def _mock_get_status(self): return self.patched_document('_get_status', self._mocked_response('GetStatus_invoice.xml', 200)) def _mock_build_and_send_request(self, response_file, status_code=200): return patch(f'{self.utils_path}._build_and_send_request', return_value=self._mocked_response(response_file, status_code)) def _disable_get_acquirer_call(self): return patch.object(ResPartner, attribute='_l10n_co_dian_call_get_acquirer', return_value=dict()) def _mock_send_and_print(self, move, response_file, response_code=200): with ( self._mock_get_status(), self._mock_build_and_send_request(response_file, response_code), self._disable_get_acquirer_call(), ): self.env['account.move.send.wizard'] \ .with_context(active_model=move._name, active_ids=move.ids) \ .create({}) \ .action_send_and_print() def _mock_get_status_zip(self, move, response_file, response_code=200): with patch.object(requests, 'post', return_value=self._mocked_request(response_file, response_code)): move.l10n_co_dian_document_ids._get_status_zip() def _mock_button_l10n_co_dian_fetch_numbering_range(self, journal, response_file, response_code=200): with patch.object(requests, 'post', return_value=self._mocked_request(response_file, response_code)): return journal.button_l10n_co_dian_fetch_numbering_range()