import unittest from collections import Counter from datetime import date from decimal import Decimal from pathlib import Path from xml.etree import ElementTree as ET from trytond.modules.account_do.tax import ( TAX_APPLICATION, TAX_FISCAL_TYPE, TAX_KIND) from trytond.pool import Pool from trytond.tests.test_tryton import ModuleTestCase, with_transaction from trytond.transaction import Transaction MODULE_DIR = Path(__file__).resolve().parent.parent def _iter_xml_records(*filenames): for filename in filenames: path = MODULE_DIR / filename root = ET.parse(path).getroot() for record in root.findall('.//record'): values = { field.get('name'): ( field.get('ref') or field.get('eval') or field.text or '') for field in record.findall('field') } yield filename, record.get('id'), record.get('model'), values class AccountDoTestCase(ModuleTestCase): "Test account_do module" module = 'account_do' @with_transaction() def test_chart_template_is_loaded_by_default(self): 'Test Dominican chart template is loaded without Spanish language setup' pool = Pool() ModelData = pool.get('ir.model.data') self.assertTrue(ModelData.get_id('account_do', 'do_account_root')) @with_transaction() def test_chart_creates_company_accounts_and_taxes(self): 'Test Dominican chart creates usable accounts and taxes' pool = Pool() Account = pool.get('account.account') AccountTemplate = pool.get('account.account.template') Company = pool.get('company.company') Currency = pool.get('currency.currency') ModelData = pool.get('ir.model.data') Party = pool.get('party.party') Tax = pool.get('account.tax') TaxCode = pool.get('account.tax.code') TaxRule = pool.get('account.tax.rule') DgiiTaxMapping = pool.get('account.do.dgii.tax.mapping') CreateChart = pool.get('account.create_chart', type='wizard') currency, = Currency.create([{ 'name': 'Dominican Peso', 'code': 'DOP', 'symbol': 'RD$', 'digits': 2, 'rounding': '0.01', }]) party, = Party.create([{ 'name': 'Empresa Dominicana', }]) company, = Company.create([{ 'party': party.id, 'currency': currency.id, }]) template = AccountTemplate(ModelData.get_id( 'account_do', 'do_account_root')) session_id, _start, _end = CreateChart.create() chart = CreateChart(session_id) chart.account.account_template = template chart.account.company = company chart.transition_create_account() receivable, = Account.search([ ('company', '=', company.id), ('code', '=', '110201'), ('type.receivable', '=', True), ('party_required', '=', True), ('closed', '!=', True), ], limit=1) payable, = Account.search([ ('company', '=', company.id), ('code', '=', '210101'), ('type.payable', '=', True), ('party_required', '=', True), ('closed', '!=', True), ], limit=1) for code in [ '110401', '110406', '110407', '110408', '11040101', '11040102', '11040103', '11040104', '11040601', '11040801', '11040802', '11040803', '210201', '210202', '210203', '210206', '210207', '210208', '210209', '210210', '210211', '210212', '210213', '210214', '210215', '210216', '210217', '210218', '210219', '21020101', '21020102', '21020103', '21020104', '21020105', '21020201', '21020501', '21020502', '21020601', '21020701', '21020702', '21020801', '21020802', '21020803', '21020804', '21020805', '21020901', '21021001', '21021101', '21021201', '21021301', '21021302', '21021401', '21021501', '21021502', '21021701', '21021801', '21021901', '21021902', '6208']: with self.subTest(code=code): self.assertTrue(Account.search([ ('company', '=', company.id), ('code', '=', code), ('closed', '!=', True), ], limit=1)) self.assertGreaterEqual(len(Account.search([ ('company', '=', company.id), ])), 200) self.assertGreaterEqual(len(Tax.search([ ('company', '=', company.id), ])), 25) self.assertTrue(TaxCode.search([ ('company', '=', company.id), ('name', '=', 'ITBIS — Balance Neto (Débito − Crédito)'), ], limit=1)) self.assertTrue(TaxRule.search([ ('company', '=', company.id), ('kind', '=', 'sale'), ], limit=1)) self.assertTrue(TaxRule.search([ ('company', '=', company.id), ('kind', '=', 'purchase'), ], limit=1)) self.assertEqual(DgiiTaxMapping.search_count([]), 55) self.assertTrue(DgiiTaxMapping.search([ ('tax_template', '=', ModelData.get_id( 'account_do', 'do_tax_cheques_020')), ('form', '=', 'ir2'), ('box', '=', 'financial_expenses_check_transfer_tax_020'), ('effective_from', '=', date(2026, 7, 3)), ], limit=1)) self.assertTrue(Tax.search([ ('company', '=', company.id), ('description', '=', 'ITBIS 18% Ventas'), ('tax_kind', '=', 'itbis'), ('rate', '=', Decimal('0.18')), ], limit=1)) self.assertTrue(Tax.search([ ('company', '=', company.id), ('description', '=', 'Retención ITBIS Gran Contribuyente 30%'), ('tax_kind', '=', 'itbis_withholding'), ('tax_fiscal_type', '=', 'itbis_withholding_30'), ('rate', '=', Decimal('-0.054')), ], limit=1)) self.assertTrue(Tax.search([ ('company', '=', company.id), ('description', '=', 'Retención ITBIS Proveedor Informal 75%'), ('tax_kind', '=', 'itbis_withholding'), ('tax_fiscal_type', '=', 'itbis_withholding_75_informal'), ('rate', '=', Decimal('-0.135')), ], limit=1)) chart.properties.company = company chart.properties.account_receivable = receivable chart.properties.account_payable = payable with Transaction().set_context(company=company.id): chart.transition_create_properties() CreateChart.delete(session_id) class AccountDoUnitTestCase(unittest.TestCase): def test_xml_references_are_resolved_inside_module(self): records = list(_iter_xml_records( 'account_chart_do.xml', 'tax_do.xml', 'tax_code_do.xml', 'tax_rule_do.xml', )) ids = {record_id for _, record_id, _, _ in records} missing = [] for filename in [ 'account_chart_do.xml', 'tax_do.xml', 'tax_code_do.xml', 'tax_rule_do.xml', ]: root = ET.parse(MODULE_DIR / filename).getroot() for record in root.findall('.//record'): for field in record.findall('field'): ref = field.get('ref') if ref and '.' not in ref and ref not in ids: missing.append( (filename, record.get('id'), field.get('name'), ref)) self.assertEqual(missing, []) def test_chart_template_codes_are_unique(self): accounts = [ values for _, _, model, values in _iter_xml_records( 'account_chart_do.xml') if model == 'account.account.template' ] codes = [values['code'] for values in accounts if values.get('code')] duplicates = [ code for code, count in Counter(codes).items() if count > 1] self.assertEqual(duplicates, []) def test_chart_root_and_statement_classification(self): accounts = { record_id: values for _, record_id, model, values in _iter_xml_records( 'account_chart_do.xml') if model == 'account.account.template' } types = { record_id: values for _, record_id, model, values in _iter_xml_records( 'account_chart_do.xml') if model == 'account.account.type.template' } self.assertEqual([ accounts[f'do_account_{code}']['code'] for code in range(1, 8) ], ['1', '2', '3', '4', '5', '6', '7']) self.assertEqual( types['do_type_retained_earnings']['statement'], 'balance') self.assertEqual(types['do_type_revenue']['statement'], 'income') self.assertEqual(types['do_type_expense']['statement'], 'income') self.assertEqual( accounts['do_account_110201']['type'], 'do_type_receivable') self.assertEqual( accounts['do_account_210101']['type'], 'do_type_payable') def test_no_spanish_technical_other_tax_ids_remain(self): ids = { record_id for _, record_id, _, _ in _iter_xml_records( 'tax_do.xml', 'tax_code_do.xml') } self.assertFalse([record_id for record_id in ids if 'otros' in record_id]) def test_tax_templates_are_classified(self): taxes = { record_id: values for _, record_id, model, values in _iter_xml_records('tax_do.xml') if model == 'account.tax.template' } self.assertEqual(len(taxes), 55) self.assertFalse([ record_id for record_id, values in taxes.items() if not values.get('tax_kind')]) self.assertFalse([ record_id for record_id, values in taxes.items() if not values.get('tax_fiscal_type')]) self.assertFalse([ record_id for record_id, values in taxes.items() if not values.get('tax_application')]) self.assertEqual( taxes['do_tax_itbis_18_venta']['invoice_account'], 'do_account_21020101') self.assertEqual( taxes['do_tax_itbis_18_compra']['invoice_account'], 'do_account_11040101') expected_accounts = { 'do_tax_itbis_18_venta': 'do_account_21020101', 'do_tax_itbis_16_venta': 'do_account_21020102', 'do_tax_itbis_9_venta': 'do_account_21020103', 'do_tax_itbis_8_venta': 'do_account_21020104', 'do_tax_itbis_tasa_cero': 'do_account_21020105', 'do_tax_itbis_18_compra': 'do_account_11040101', 'do_tax_itbis_16_compra': 'do_account_11040102', 'do_tax_itbis_9_compra': 'do_account_11040103', 'do_tax_itbis_8_compra': 'do_account_11040104', 'do_tax_ret_itbis_2_adq': 'do_account_11040601', 'do_tax_ret_itbis_30': 'do_account_21021101', 'do_tax_ret_itbis_100_inf': 'do_account_21020201', 'do_tax_ret_itbis_75_inf': 'do_account_21021201', 'do_tax_ret_itbis_75_inf_16': 'do_account_21021202', 'do_tax_ret_itbis_100_goods_18': 'do_account_21021203', 'do_tax_ret_itbis_100_goods_16': 'do_account_21021204', 'do_tax_ret_itbis_rst_18': 'do_account_21020202', 'do_tax_ret_itbis_rst_16': 'do_account_21020203', 'do_tax_ret_itbis_insurance_100': 'do_account_21020204', 'do_tax_ret_itbis_airline_100': 'do_account_11040701', 'do_tax_ret_itbis_society_30_suf': 'do_account_11040702', 'do_tax_ret_itbis_hotel_100': 'do_account_11040703', 'do_tax_ret_itbis_state_100': 'do_account_11040704', 'do_tax_ret_isr_hon_5': 'do_account_21021301', 'do_tax_ret_isr_serv_10': 'do_account_21021302', 'do_tax_ret_isr_div_10': 'do_account_21020601', 'do_tax_ret_isr_int_10': 'do_account_21021501', 'do_tax_ret_isr_int_pj_1': 'do_account_21021502', 'do_tax_ret_isr_alq_10': 'do_account_21021401', 'do_tax_ret_isr_est_15': 'do_account_11040801', 'do_tax_ret_isr_est_5': 'do_account_11040802', 'do_tax_ret_isr_exporter_25': 'do_account_11040803', 'do_tax_ret_isr_bovine_1': 'do_account_21021801', 'do_tax_ret_isr_premios_25': 'do_account_21021701', 'do_tax_ret_isr_premios_10': 'do_account_21021702', 'do_tax_ret_isr_premios_15': 'do_account_21021703', 'do_tax_ret_isr_tragamonedas_10': 'do_account_21021704', 'do_tax_ret_isr_other_income_10': 'do_account_21020301', 'do_tax_ret_isr_ext_27': 'do_account_21020701', 'do_tax_ret_isr_ext_10': 'do_account_21020702', 'do_tax_isc_bebidas_alc': 'do_account_21020801', 'do_tax_isc_tabaco': 'do_account_21020802', 'do_tax_isc_telecom': 'do_account_21020803', 'do_tax_isc_combustibles_16': 'do_account_21020804', 'do_tax_isc_avtur_65': 'do_account_21020807', 'do_tax_isc_fuel_rd2_gallon': 'do_account_21020808', 'do_tax_ret_isc_insurance_100': 'do_account_21020806', 'do_tax_isc_vehiculos': 'do_account_21020805', 'do_tax_cdt_indotel': 'do_account_21020901', 'do_tax_propina_10': 'do_account_21020501', 'do_tax_cheques_015': 'do_account_21021901', 'do_tax_cheques_020': 'do_account_21021902', 'do_tax_activos_1': 'do_account_21020502', 'do_tax_iti_3': 'do_account_21021001', } for record_id, account_id in expected_accounts.items(): with self.subTest(record_id=record_id): self.assertEqual(taxes[record_id]['invoice_account'], account_id) self.assertEqual( taxes[record_id]['credit_note_account'], account_id) self.assertEqual( taxes['do_tax_ret_itbis_30']['tax_kind'], 'itbis_withholding') self.assertIn( "Decimal('-5.4')/100", taxes['do_tax_ret_itbis_30']['rate']) self.assertEqual( taxes['do_tax_ret_itbis_75_inf']['tax_fiscal_type'], 'itbis_withholding_75_informal') self.assertIn( "Decimal('-13.5')/100", taxes['do_tax_ret_itbis_75_inf']['rate']) for record_id in [ 'do_tax_ret_isr_hon_5', 'do_tax_ret_isr_serv_10', 'do_tax_ret_isr_alq_10', 'do_tax_ret_isr_est_5', 'do_tax_ret_isr_bovine_1', 'do_tax_ret_isr_exporter_25', 'do_tax_ret_isr_ext_27', ]: self.assertIn("Decimal('-", taxes[record_id]['rate']) self.assertEqual( taxes['do_tax_itbis_9_venta']['tax_fiscal_type'], 'itbis_sale_9') self.assertIn( "Decimal('9')/100", taxes['do_tax_itbis_9_compra']['rate']) self.assertEqual( taxes['do_tax_isc_bebidas_alc']['tax_fiscal_type'], 'isc_alcohol_10') self.assertIn( "Decimal('10')/100", taxes['do_tax_isc_bebidas_alc']['rate']) self.assertEqual( taxes['do_tax_isc_combustibles_16']['tax_fiscal_type'], 'isc_fossil_fuel_16') self.assertEqual( taxes['do_tax_cheques_020']['tax_kind'], 'others') self.assertEqual( taxes['do_tax_cheques_020']['invoice_account'], 'do_account_21021902') self.assertIn( "Decimal('0.20')/100", taxes['do_tax_cheques_020']['rate']) def test_dgii_tax_mapping_formalizes_every_tax_template(self): taxes = { record_id: values for _, record_id, model, values in _iter_xml_records('tax_do.xml') if model == 'account.tax.template' } mappings = { record_id: values for _, record_id, model, values in _iter_xml_records( 'dgii_tax_mapping_data.xml') if model == 'account.do.dgii.tax.mapping' } mapped_taxes = { values['tax_template'] for values in mappings.values()} self.assertEqual(len(mappings), 55) self.assertEqual(mapped_taxes, set(taxes)) for record_id, values in mappings.items(): with self.subTest(record_id=record_id): self.assertTrue(values.get('form')) self.assertTrue(values.get('box')) self.assertTrue(values.get('box_label')) self.assertTrue(values.get('amount_source')) self.assertIn(values.get('operator'), {'+', '-', 'info'}) self.assertTrue(values.get('flow')) self.assertTrue(values.get('legal_basis')) tax = taxes[values['tax_template']] if tax.get('type') != 'none': self.assertTrue(values.get('account_template')) by_tax = { values['tax_template']: values for values in mappings.values() } self.assertEqual(by_tax['do_tax_itbis_18_venta']['form'], 'it1') self.assertEqual(by_tax['do_tax_itbis_18_venta']['box'], '16') self.assertEqual(by_tax['do_tax_itbis_18_compra']['box'], '22') self.assertEqual(by_tax['do_tax_itbis_exento']['form'], 'it1_annex_a') self.assertEqual(by_tax['do_tax_itbis_exento']['box'], '04') self.assertEqual(by_tax['do_tax_ret_itbis_75_inf']['box'], '56') self.assertEqual(by_tax['do_tax_ret_isr_ext_27']['form'], '609') self.assertEqual(by_tax['do_tax_cheques_015']['effective_to'], '2026-07-02') self.assertEqual(by_tax['do_tax_cheques_020']['effective_from'], '2026-07-03') def test_tax_code_templates_use_expected_signs(self): codes = { record_id: values for _, record_id, model, values in _iter_xml_records( 'tax_code_do.xml') if model == 'account.tax.code.template' } lines = { record_id: values for _, record_id, model, values in _iter_xml_records( 'tax_code_do.xml') if model == 'account.tax.code.line.template' } for record_id in [ 'do_tc_itbis', 'do_tc_isr', 'do_tc_isc', 'do_tc_cdt', 'do_tc_others', ]: self.assertEqual(codes[record_id].get('parent'), 'None') self.assertEqual(codes['do_tc_isr']['name'], 'ISR - Retenciones') self.assertEqual( lines['do_tcl_isr_int_pf_inv']['code'], 'do_tc_isr_intereses_pf') self.assertEqual( lines['do_tcl_isr_int_pf_cr']['code'], 'do_tc_isr_intereses_pf') self.assertEqual( lines['do_tcl_isr_int_pj_inv']['code'], 'do_tc_isr_intereses') self.assertEqual( lines['do_tcl_isr_int_pj_cr']['code'], 'do_tc_isr_intereses') self.assertEqual( lines['do_tcl_chq020_inv']['code'], 'do_tc_others_checks_020') self.assertEqual( lines['do_tcl_chq020_cr']['code'], 'do_tc_others_checks_020') self.assertEqual(lines['do_tcl_itbis18v_inv']['operator'], '+') self.assertEqual(lines['do_tcl_itbis18c_inv']['operator'], '-') self.assertEqual(lines['do_tcl_itbis9v_inv']['operator'], '+') self.assertEqual(lines['do_tcl_itbis9c_inv']['operator'], '-') self.assertEqual(lines['do_tcl_ret_itbis_inv']['operator'], '-') self.assertEqual(lines['do_tcl_ret_itbis_adq_inv']['operator'], '+') self.assertEqual(lines['do_tcl_ret_itbis_inf75_inv']['operator'], '-') self.assertEqual(lines['do_tcl_isr_hon_inv']['operator'], '-') self.assertEqual(lines['do_tcl_isr_est_inv']['operator'], '+') self.assertEqual(lines['do_tcl_isr_est5_inv']['operator'], '+') self.assertEqual(lines['do_tcl_isr_bovine_inv']['operator'], '-') self.assertEqual(lines['do_tcl_isr_exporter_inv']['operator'], '+') self.assertEqual(lines['do_tcl_isc_comb_inv']['operator'], '+') self.assertEqual(lines['do_tcl_prop_inv']['code'], 'do_tc_others_tip') def test_tax_kind_contains_dominican_tax_categories(self): codes = {code for code, _ in TAX_KIND} self.assertEqual(codes, { '', 'itbis', 'itbis_withholding', 'itbis_withholding_acquirer', 'isr_withholding', 'isc', 'cdt', 'tip', 'others', }) def test_tax_kind_codes_are_unique(self): codes = [code for code, _ in TAX_KIND] self.assertEqual(len(codes), len(set(codes))) def test_fiscal_classification_codes_are_unique(self): fiscal_codes = [code for code, _ in TAX_FISCAL_TYPE] application_codes = [code for code, _ in TAX_APPLICATION] self.assertEqual(len(fiscal_codes), len(set(fiscal_codes))) self.assertEqual(len(application_codes), len(set(application_codes))) del ModuleTestCase