Files

990 lines
41 KiB
Python
Raw Permalink Normal View History

2026-07-22 19:12:44 -04:00
import unittest
import datetime
2026-07-22 19:12:44 -04:00
from collections import Counter
from configparser import ConfigParser
2026-07-22 19:12:44 -04:00
from decimal import Decimal
from pathlib import Path
from xml.etree import ElementTree as ET
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
2026-08-09 13:13:22 -04:00
TAX_CODE_LINE_AUDIT = {
'do_tcl_itbis18v_inv': '+',
'do_tcl_itbis18v_cr': '-',
'do_tcl_itbis18c_inv': '-',
'do_tcl_itbis18c_cr': '+',
'do_tcl_ret_itbis_inv': '-',
'do_tcl_ret_itbis_cr': '+',
'do_tcl_ret_itbis_inf75_inv': '-',
'do_tcl_ret_itbis_inf75_cr': '+',
'do_tcl_isr_hon_inv': '-',
'do_tcl_isr_hon_cr': '+',
'do_tcl_isr_est_inv': '+',
'do_tcl_isr_est_cr': '-',
'do_tcl_isc_comb_inv': '+',
'do_tcl_isc_comb_cr': '-',
'do_tcl_chq_inv': '+',
'do_tcl_chq_cr': '-',
'do_tcl_chq020_inv': '+',
'do_tcl_chq020_cr': '-',
}
IFRS_REQUIRED_ACCOUNT_CODES = {
'1', '11', '12',
'1101', '1102', '1103', '1104', '1105', '1106', '1107', '1110',
'110101', '110103', '110104', '110201', '110205', '110301',
'110305', '110601', '110701', '110702', '110703', '111001',
'111002',
'1201', '1205', '1206', '1207', '1208', '1209',
'120101', '120191', '120192', '120501', '120591', '120592',
'120601', '120701', '120702', '120703', '120801', '120891',
'120892', '120901', '120991', '120992',
'2', '21', '22', '23',
'2101', '2104', '2105', '2106', '2201', '2301', '2302', '2303',
'2304',
'210101', '210401', '210501', '210601', '220101', '230201',
'230202', '230301', '230401',
'3', '31', '3101', '3104', '3105', '3106', '310601', '310604',
'4', '41', '42', '4101', '4102', '4103', '4104', '4201', '4208',
'4209', '4210',
'5', '51', '5101', '5102', '5106',
'6', '61', '62', '63', '6101', '6103', '6111', '6112', '6115',
'6116', '6117', '6118', '6119', '6120', '6201', '6203', '6206',
'6207', '6301', '6302', '6303',
'7', '7101', '7102',
}
IFRS_ACCOUNT_TYPE_AUDIT = {
'111002': 'do_type_current_asset',
'120192': 'do_type_fixed_asset',
'120292': 'do_type_fixed_asset',
'120392': 'do_type_fixed_asset',
'120591': 'do_type_rou_asset',
'120592': 'do_type_rou_asset',
'120891': 'do_type_investment_property',
'120892': 'do_type_investment_property',
'120992': 'do_type_intangible',
'7101': 'do_type_control',
'7102': 'do_type_control',
}
IFRS_POLICY_COVERAGE = {
'NIC 1': {'11', '12', '21', '23', '3106', '7101', '7102'},
'NIC 2': {'1103', '110305', '5101', '5106'},
'NIIF 9': {'110205', '110701', '110702', '110703', '120701',
'120702', '120703', '4208', '6206', '310604'},
'NIIF 15': {'110601', '210501', '4101', '4102', '4103', '4104'},
'NIIF 16': {'120501', '120591', '120592', '230201', '230202',
'6112', '6203'},
'NIC 12': {'120601', '230301', '6301', '6302', '6303'},
'NIC 16': {'120101', '120191', '120192', '6110', '6116'},
'NIC 36': {'111002', '120192', '120292', '120392', '120592',
'120892', '120992', '6116', '6117', '4206'},
'NIC 37': {'220101', '230401', '6118', '4207'},
'NIC 38': {'120901', '120991', '120992', '6111', '6117'},
'NIC 40': {'120801', '120891', '120892', '4209', '6207'},
'NIIF 5': {'111001', '111002', '210601', '4210', '6120'},
}
2026-07-22 19:12:44 -04:00
def _iter_xml_records(*filenames):
for filename in filenames:
path = MODULE_DIR / filename
root = ET.parse(path).getroot()
for record in root.findall('.//record'):
english = filename.endswith('_en.xml')
2026-07-22 19:12:44 -04:00
values = {
field.get('name'): (
field.get('ref') or field.get('eval') or field.text or '')
for field in record.findall('field')
}
record_id = record.get('id')
if english:
record_id = record_id.removesuffix('_en')
values = {
name: value.removesuffix('_en')
if name not in {'name', 'description'} else value
for name, value in values.items()
}
yield filename, record_id, record.get('model'), values
2026-07-22 19:12:44 -04:00
class AccountDoTestCase(ModuleTestCase):
"Test account_do module"
module = 'account_do'
extras = [
'account_asset',
'account_deposit',
'account_stock_continental',
]
2026-07-22 19:12:44 -04:00
@with_transaction()
def test_chart_template_is_loaded_by_default(self):
'Test English Dominican chart is loaded in an English database'
2026-07-22 19:12:44 -04:00
pool = Pool()
ModelData = pool.get('ir.model.data')
self.assertTrue(ModelData.get_id('account_do', 'do_account_root_en'))
@with_transaction()
def test_default_properties_ignore_unrelated_chart(self):
'Test account_do does not override defaults for another chart'
pool = Pool()
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')
CreateChart = pool.get('account.create_chart', type='wizard')
currency, = Currency.create([{
'name': 'Test Currency',
'code': 'TST',
'symbol': 'T',
'digits': 2,
'rounding': '0.01',
}])
party, = Party.create([{'name': 'Unrelated Chart Company'}])
company, = Company.create([{
'party': party.id,
'currency': currency.id,
}])
template = AccountTemplate(ModelData.get_id(
'account', 'account_template_root_en'))
session_id, _start, _end = CreateChart.create()
chart = CreateChart(session_id)
chart.account.account_template = template
chart.account.company = company
defaults = chart.default_properties([
'company', 'account_receivable', 'account_payable'])
self.assertEqual(defaults['company'], company.id)
self.assertIsNone(defaults['account_receivable'])
self.assertIsNone(defaults['account_payable'])
CreateChart.delete(session_id)
@with_transaction()
def test_spanish_chart_creates_spanish_accounts_and_taxes(self):
'Test the es_419 chart creates a complete independent chart'
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')
CreateChart = pool.get('account.create_chart', type='wizard')
currency, = Currency.create([{
'name': 'Peso dominicano',
'code': 'DOP',
'symbol': 'RD$',
'digits': 2,
'rounding': '0.01',
}])
party, = Party.create([{'name': 'Empresa Dominicana'}])
company, = Company.create([{
'party': party.id,
'currency': currency.id,
}])
try:
template_id = ModelData.get_id(
'account_do', 'do_account_root_es_419')
except KeyError:
self.skipTest('The es_419 chart is loaded only for es_419 databases')
template = AccountTemplate(template_id)
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'),
], limit=1)
payable, = Account.search([
('company', '=', company.id),
('code', '=', '210101'),
], limit=1)
defaults = chart.default_properties([
'company', 'account_receivable', 'account_payable'])
self.assertEqual(receivable.name, 'Cuentas por cobrar clientes')
self.assertEqual(payable.name, 'Cuentas por pagar proveedores')
self.assertEqual(defaults['account_receivable'], receivable.id)
self.assertEqual(defaults['account_payable'], payable.id)
self.assertTrue(Tax.search([
('company', '=', company.id),
('description', '=', 'ITBIS 18% Ventas'),
('rate', '=', Decimal('0.18')),
], limit=1))
self.assertTrue(TaxCode.search([
('company', '=', company.id),
('name', '=', 'Otros Impuestos y Contribuciones'),
], limit=1))
CreateChart.delete(session_id)
2026-07-22 19:12:44 -04:00
@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')
2026-08-09 13:13:22 -04:00
TaxCodeLine = pool.get('account.tax.code.line')
2026-07-22 19:12:44 -04:00
TaxRule = pool.get('account.tax.rule')
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_en'))
2026-07-22 19:12:44 -04:00
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)
defaults = chart.default_properties([
'company', 'account_receivable', 'account_payable'])
self.assertEqual(defaults['company'], company.id)
self.assertEqual(defaults['account_receivable'], receivable.id)
self.assertEqual(defaults['account_payable'], payable.id)
2026-07-22 19:12:44 -04:00
for code in [
2026-08-09 13:13:22 -04:00
'111002', '120192', '120292', '120392', '120591',
'120592', '120891', '120892', '120992', '7101', '7102',
'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']:
2026-07-22 19:12:44 -04:00
with self.subTest(code=code):
self.assertTrue(Account.search([
('company', '=', company.id),
('code', '=', code),
('closed', '!=', True),
], limit=1))
2026-08-09 13:13:22 -04:00
company_ifrs_types = {
'111002': 'Current Assets',
'120591': 'Right-of-use Assets',
'120592': 'Right-of-use Assets',
'120891': 'Investment Property',
'120892': 'Investment Property',
'120992': 'Intangible assets',
'7101': 'Closing and control accounts',
'7102': 'Closing and control accounts',
2026-08-09 13:13:22 -04:00
}
for code, account_type in company_ifrs_types.items():
with self.subTest(code=code, account_type=account_type):
account, = Account.search([
('company', '=', company.id),
('code', '=', code),
('closed', '!=', True),
], limit=1)
self.assertEqual(account.type.name, account_type)
2026-07-22 19:12:44 -04:00
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 — Net Balance (Debit Credit)'),
2026-07-22 19:12:44 -04:00
], 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.assertTrue(Tax.search([
('company', '=', company.id),
('description', '=', 'ITBIS 18% Sales'),
2026-07-22 19:12:44 -04:00
('rate', '=', Decimal('0.18')),
], limit=1))
self.assertTrue(Tax.search([
('company', '=', company.id),
('description', '=', 'ITBIS Withholding Large Taxpayer 30%'),
2026-07-22 19:12:44 -04:00
('rate', '=', Decimal('-0.054')),
], limit=1))
self.assertTrue(Tax.search([
('company', '=', company.id),
('description', '=', 'ITBIS Withholding Informal Supplier 75%'),
('rate', '=', Decimal('-0.135')),
], limit=1))
check_015, = Tax.search([
('company', '=', company.id),
('description', '=', 'Checks and Transfers Tax 0.15%'),
], limit=1)
check_020, = Tax.search([
('company', '=', company.id),
('description', '=', 'Checks and Transfers Tax 0.20%'),
], limit=1)
bank_rule, = TaxRule.search([
('company', '=', company.id),
('name', '=', (
'Banking Rule Checks / Electronic Transfers '
'(RD)')),
], limit=1)
self.assertEqual(check_015.end_date, datetime.date(2026, 7, 2))
self.assertEqual(check_020.start_date, datetime.date(2026, 7, 3))
self.assertEqual(
bank_rule.apply(check_015, {
'date': datetime.date(2026, 7, 2),
}),
[check_015.id])
self.assertEqual(
bank_rule.apply(check_015, {
'date': datetime.date(2026, 7, 3),
}),
[check_020.id])
2026-08-09 13:13:22 -04:00
real_tax_code_signs = {
('ITBIS 18% Sales', 'invoice'): '+',
('ITBIS 18% Sales', 'credit'): '-',
('ITBIS 18% Purchases', 'invoice'): '-',
('ITBIS 18% Purchases', 'credit'): '+',
('ITBIS Withholding Large Taxpayer 30%', 'invoice'): '-',
('ITBIS Withholding Large Taxpayer 30%', 'credit'): '+',
('ISR Withholding Individual 10%', 'invoice'): '-',
('ISR Withholding Individual 10%', 'credit'): '+',
('ISR Withholding State 1.5%', 'invoice'): '+',
('ISR Withholding State 1.5%', 'credit'): '-',
('Checks and Transfers Tax 0.20%', 'invoice'): '+',
('Checks and Transfers Tax 0.20%', 'credit'): '-',
2026-08-09 13:13:22 -04:00
}
for (description, line_type), operator in real_tax_code_signs.items():
with self.subTest(description=description, line_type=line_type):
tax, = Tax.search([
('company', '=', company.id),
('description', '=', description),
], limit=1)
line, = TaxCodeLine.search([
('tax', '=', tax.id),
('type', '=', line_type),
('amount', '=', 'tax'),
], limit=1)
self.assertEqual(line.operator, operator)
2026-07-22 19:12:44 -04:00
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 SpanishAccountDoTestCase(ModuleTestCase):
"Test the Latin American Spanish account_do data set"
module = 'account_do'
extras = AccountDoTestCase.extras
language = 'es_419'
test_spanish_chart_creates_spanish_accounts_and_taxes = (
AccountDoTestCase.test_spanish_chart_creates_spanish_accounts_and_taxes)
@with_transaction()
def test_migrate_pre_language_identifiers(self):
'Test 8.0.0 identifiers migrate without deleting referenced records'
pool = Pool()
AccountTemplate = pool.get('account.account.template')
ModelData = pool.get('ir.model.data')
root_id = ModelData.get_id(
'account_do', 'do_account_root_es_419')
root_data, = ModelData.search([
('module', '=', 'account_do'),
('fs_id', '=', 'do_account_root_es_419'),
])
ModelData.write([root_data], {'fs_id': 'do_account_root'})
legacy, = AccountTemplate.create([{
'name': 'Legacy Obsolete Tax Placeholder',
'parent': root_id,
}])
ModelData.create([{
'module': 'account_do',
'fs_id': 'do_tax_itbis_8_venta',
'model': 'account.account.template',
'db_id': legacy.id,
'noupdate': False,
}])
ModelData._migrate_localization_identifiers()
self.assertEqual(
ModelData.get_id('account_do', 'do_account_root_es_419'),
root_id)
self.assertFalse(ModelData.search([
('module', '=', 'account_do'),
('fs_id', '=', 'do_tax_itbis_8_venta'),
]))
2026-07-22 19:12:44 -04:00
class AccountDoUnitTestCase(unittest.TestCase):
def test_static_project_configuration_is_complete(self):
config = ConfigParser()
config.read(MODULE_DIR / 'tryton.cfg', encoding='utf-8')
def lines(section, option):
return [
line.strip()
for line in config.get(section, option).splitlines()
if line.strip()
]
2026-08-10 00:12:02 -04:00
self.assertEqual(config.get('tryton', 'version'), '8.1.0')
self.assertEqual(
set(lines('tryton', 'depends')),
{'account', 'ir'})
self.assertEqual(
lines('tryton', 'xml'),
[
'account_chart_do_en.xml',
'tax_do_en.xml',
'tax_code_do_en.xml',
'tax_rule_do_en.xml',
'account_chart_do_es_419.xml',
'tax_do_es_419.xml',
'tax_code_do_es_419.xml',
'tax_rule_do_es_419.xml',
])
self.assertEqual(lines('register', 'model'), ['ir.ModelData'])
self.assertEqual(lines('register', 'wizard'), ['account.CreateChart'])
for filename in lines('tryton', 'xml'):
self.assertTrue((MODULE_DIR / filename).is_file())
self.assertTrue((MODULE_DIR / 'account.py').is_file())
pyproject = (MODULE_DIR / 'pyproject.toml').read_text(encoding='utf-8')
self.assertIn("name = 'trytond_account_do'", pyproject)
self.assertIn(
"account_do = 'trytond.modules.account_do'", pyproject)
self.assertIn(
'{name = "Fundación Un País Mejor"}', pyproject)
self.assertIn(
'homepage = "https://www.unpaismejor.org.do"', pyproject)
self.assertIn(
('repository = "https://code.unpaismejor.org.do/'
'tryton-do/account_do"'), pyproject)
self.assertIn("exclude = ['doc']", pyproject)
for package_file in ['**/*.xml', 'tests/**/*.rst']:
self.assertIn(package_file, pyproject)
for filename in [
'__init__.py',
'account.py',
'doc/conf.py',
'tests/test_scenario.py',
]:
with self.subTest(filename=filename):
source = (MODULE_DIR / filename).read_text(encoding='utf-8')
self.assertIn('This file is part of Tryton.', source)
account = (MODULE_DIR / 'account.py').read_text(encoding='utf-8')
self.assertIn("class CreateChart(metaclass=PoolMeta):", account)
self.assertIn(
"'account_do.do_account_110201_en'", account)
self.assertIn(
"'account_do.do_account_210101_en'", account)
readme = (MODULE_DIR / 'README.rst').read_text(encoding='utf-8')
self.assertNotIn('100% de la base estructural', readme)
self.assertNotIn('catalogo_cuentas_niif_rd_tryton_completo', readme)
for filename in [
'doc/conf.py',
'doc/design.rst',
'doc/reference.rst',
'doc/releases.rst',
]:
self.assertTrue((MODULE_DIR / filename).is_file())
def test_xml_record_inventory_is_explicit(self):
expected = {
'account_chart_do_en.xml': {
'account.account.type.template': 36,
'account.account.template': 281,
},
'tax_do_en.xml': {
'account.tax.group': 7,
'account.tax.template': 50,
},
'tax_code_do_en.xml': {
'account.tax.code.template': 44,
'account.tax.code.line.template': 93,
},
'tax_rule_do_en.xml': {
'account.tax.rule.template': 23,
'account.tax.rule.line.template': 30,
},
'account_chart_do_es_419.xml': {
'account.account.type.template': 36,
'account.account.template': 281,
},
'tax_do_es_419.xml': {
'account.tax.group': 7,
'account.tax.template': 50,
},
'tax_code_do_es_419.xml': {
'account.tax.code.template': 44,
'account.tax.code.line.template': 93,
},
'tax_rule_do_es_419.xml': {
'account.tax.rule.template': 23,
'account.tax.rule.line.template': 30,
},
}
for filename, expected_counts in expected.items():
records = list(_iter_xml_records(filename))
with self.subTest(filename=filename):
self.assertEqual(
dict(Counter(model for _, _, model, _ in records)),
expected_counts)
self.assertFalse([
(record_id, model)
for _, record_id, model, _ in records
if not record_id or not model])
duplicate_ids = [
record_id
for record_id, count in Counter(
record_id for _, record_id, _, _ in records).items()
if count > 1]
self.assertEqual(duplicate_ids, [])
def test_xml_data_is_scoped_to_its_language(self):
'Every data section follows the language-specific chart convention'
for language in ['en', 'es_419']:
for stem in [
'account_chart_do', 'tax_do', 'tax_code_do',
'tax_rule_do']:
filename = f'{stem}_{language}.xml'
root = ET.parse(MODULE_DIR / filename).getroot()
with self.subTest(filename=filename):
self.assertTrue(root.findall('data'))
self.assertEqual(
{data.get('language') for data in root.findall('data')},
{language})
def test_english_accounting_terminology_is_consistent(self):
'English datasets avoid literal Spanish accounting translations'
text = '\n'.join(
field.text.strip()
for filename in [
'account_chart_do_en.xml', 'tax_do_en.xml',
'tax_code_do_en.xml', 'tax_rule_do_en.xml']
for field in ET.parse(MODULE_DIR / filename).getroot().findall(
'.//field')
if field.get('name') in {'name', 'description'} and field.text)
for phrase in [
'advance itbis', 'assets for right of use',
'charged for paying', 'clients tax rule',
'collected to pay', 'deterioration', 'in favor',
'itbis supported', 'other income and profits',
'physical persons', 'retention', 'withheld status',
]:
with self.subTest(phrase=phrase):
self.assertNotIn(phrase, text.lower())
def test_spanish_chart_has_complete_parallel_identifiers(self):
pairs = [
('account_chart_do_en.xml', 'account_chart_do_es_419.xml'),
('tax_do_en.xml', 'tax_do_es_419.xml'),
('tax_code_do_en.xml', 'tax_code_do_es_419.xml'),
('tax_rule_do_en.xml', 'tax_rule_do_es_419.xml'),
]
for english_file, spanish_file in pairs:
english = {
record_id: model
for _, record_id, model, _ in _iter_xml_records(english_file)
}
spanish = {
record_id: model
for _, record_id, model, _ in _iter_xml_records(spanish_file)
}
with self.subTest(spanish_file=spanish_file):
self.assertEqual(
spanish,
{record_id + '_es_419': model
for record_id, model in english.items()})
spanish_accounts = {
record_id: values
for _, record_id, model, values in _iter_xml_records(
'account_chart_do_es_419.xml')
if model == 'account.account.template'
}
spanish_taxes = {
record_id: values
for _, record_id, model, values in _iter_xml_records(
'tax_do_es_419.xml')
if model == 'account.tax.template'
}
self.assertEqual(
spanish_accounts['do_account_root_es_419']['name'],
'Plan de Cuentas NIIF - República Dominicana')
self.assertEqual(
spanish_accounts['do_account_110201_es_419']['name'],
'Cuentas por cobrar clientes')
self.assertEqual(
spanish_taxes['do_tax_itbis_18_venta_es_419']['description'],
'ITBIS 18% Ventas')
2026-07-22 19:12:44 -04:00
def test_xml_references_are_resolved_inside_module(self):
records = list(_iter_xml_records(
'account_chart_do_en.xml',
'tax_do_en.xml',
'tax_code_do_en.xml',
'tax_rule_do_en.xml',
'account_chart_do_es_419.xml',
'tax_do_es_419.xml',
'tax_code_do_es_419.xml',
'tax_rule_do_es_419.xml',
2026-07-22 19:12:44 -04:00
))
ids = set()
for filename in {
filename for filename, _record_id, _model, _values in records}:
root = ET.parse(MODULE_DIR / filename).getroot()
ids.update(
record.get('id') for record in root.findall('.//record'))
2026-07-22 19:12:44 -04:00
missing = []
for filename in [
'account_chart_do_en.xml',
'tax_do_en.xml',
'tax_code_do_en.xml',
'tax_rule_do_en.xml',
'account_chart_do_es_419.xml',
'tax_do_es_419.xml',
'tax_code_do_es_419.xml',
'tax_rule_do_es_419.xml',
2026-07-22 19:12:44 -04:00
]:
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_en.xml')
2026-07-22 19:12:44 -04:00
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_en.xml')
2026-07-22 19:12:44 -04:00
if model == 'account.account.template'
}
types = {
record_id: values
for _, record_id, model, values in _iter_xml_records(
'account_chart_do_en.xml')
2026-07-22 19:12:44 -04:00
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')
2026-08-09 13:13:22 -04:00
def test_ifrs_chart_is_complete_and_typed(self):
accounts = {
values['code']: values
for _, _, model, values in _iter_xml_records(
'account_chart_do_en.xml')
2026-08-09 13:13:22 -04:00
if model == 'account.account.template' and values.get('code')
}
types = {
record_id: values
for _, record_id, model, values in _iter_xml_records(
'account_chart_do_en.xml')
2026-08-09 13:13:22 -04:00
if model == 'account.account.type.template'
}
self.assertLessEqual(IFRS_REQUIRED_ACCOUNT_CODES, set(accounts))
self.assertEqual(types['do_type_control']['statement'], 'off-balance')
self.assertEqual(
types['do_type_deferred_tax_asset']['statement'], 'balance')
self.assertEqual(
types['do_type_deferred_tax_liability']['statement'], 'balance')
self.assertEqual(types['do_type_rou_asset']['statement'], 'balance')
self.assertEqual(
types['do_type_investment_property']['statement'], 'balance')
self.assertEqual(types['do_type_intangible']['statement'], 'balance')
missing_type = [
(code, values['name'])
for code, values in accounts.items()
if values.get('closed') != 'True' and not values.get('type')
]
self.assertEqual(missing_type, [])
for code, account_type in IFRS_ACCOUNT_TYPE_AUDIT.items():
with self.subTest(code=code):
self.assertEqual(accounts[code]['type'], account_type)
def test_ifrs_policy_matrix_is_represented_in_chart(self):
accounts = {
values['code']
for _, _, model, values in _iter_xml_records(
'account_chart_do_en.xml')
2026-08-09 13:13:22 -04:00
if model == 'account.account.template' and values.get('code')
}
for standard, codes in IFRS_POLICY_COVERAGE.items():
with self.subTest(standard=standard):
self.assertLessEqual(codes, accounts)
2026-07-22 19:12:44 -04:00
def test_no_spanish_technical_other_tax_ids_remain(self):
ids = {
record_id
for _, record_id, _, _ in _iter_xml_records(
'tax_do_en.xml', 'tax_code_do_en.xml')
2026-07-22 19:12:44 -04:00
}
self.assertFalse([record_id for record_id in ids if 'otros' in record_id])
def test_tax_templates_are_complete(self):
2026-07-22 19:12:44 -04:00
taxes = {
record_id: values
for _, record_id, model, values in _iter_xml_records('tax_do_en.xml')
2026-07-22 19:12:44 -04:00
if model == 'account.tax.template'
}
2026-08-09 13:13:22 -04:00
accounts = {
record_id: values
for _, record_id, model, values in _iter_xml_records(
'account_chart_do_en.xml')
2026-08-09 13:13:22 -04:00
if model == 'account.account.template'
}
self.assertEqual(len(taxes), 50)
for record_id, values in taxes.items():
with self.subTest(record_id=record_id):
self.assertEqual(
values.get('credit_note_account'),
values.get('invoice_account'))
account_id = values.get('invoice_account')
if values['type'] != 'none':
self.assertTrue(account_id)
2026-08-09 13:13:22 -04:00
self.assertIn(account_id, accounts)
self.assertTrue(
values.get('rate') or values.get('amount'))
2026-08-09 13:13:22 -04:00
else:
self.assertFalse(values.get('rate'))
self.assertTrue(values.get('legal_notice'))
2026-07-22 19:12:44 -04:00
self.assertIn(
"Decimal('-5.4')/100",
taxes['do_tax_ret_itbis_30']['rate'])
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.assertIn(
"Decimal('10')/100",
taxes['do_tax_isc_bebidas_alc']['rate'])
self.assertEqual(
taxes['do_tax_ret_isr_bovine_1']['start_date'],
'datetime.date(2025, 6, 20)')
self.assertIn(
'arts. 401-405; art. 404',
taxes['do_tax_activos_1']['legal_notice'])
self.assertIn(
'art. 7 Ley 173-07',
taxes['do_tax_iti_3']['legal_notice'])
self.assertIn(
'art. 228', taxes['do_tax_propina_10']['legal_notice'])
2026-07-22 19:12:44 -04:00
self.assertEqual(
taxes['do_tax_isc_vehiculos']['group'],
'do_tax_group_others')
self.assertNotIn('do_tax_itbis_8_venta', taxes)
self.assertNotIn('do_tax_itbis_9_venta', taxes)
self.assertEqual(
taxes['do_tax_cheques_015']['end_date'],
'datetime.date(2026, 7, 2)')
self.assertEqual(
taxes['do_tax_cheques_020']['start_date'],
'datetime.date(2026, 7, 3)')
self.assertEqual(
taxes['do_tax_cheques_020']['invoice_account'],
'do_account_21021902')
2026-07-22 19:12:44 -04:00
self.assertIn(
"Decimal('0.20')/100",
taxes['do_tax_cheques_020']['rate'])
def test_tax_validation_register_is_complete(self):
'Every tax template is inventoried in the validation register'
taxes = {
record_id
for _, record_id, model, _ in _iter_xml_records('tax_do_en.xml')
if model == 'account.tax.template'
}
documentation = (MODULE_DIR / 'doc' / 'tax_validation.rst').read_text(
encoding='utf-8')
documented = {
token.strip('`')
for token in documentation.split()
if token.startswith('``do_tax_') and token.endswith('``')
}
self.assertEqual(documented, taxes)
2026-07-22 19:12:44 -04:00
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_en.xml')
2026-07-22 19:12:44 -04:00
if model == 'account.tax.code.template'
}
lines = {
record_id: values
for _, record_id, model, values in _iter_xml_records(
'tax_code_do_en.xml')
2026-07-22 19:12:44 -04:00
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: Withholdings')
2026-07-22 19:12:44 -04:00
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')
2026-08-09 13:13:22 -04:00
for record_id, operator in TAX_CODE_LINE_AUDIT.items():
with self.subTest(record_id=record_id):
self.assertEqual(lines[record_id]['operator'], operator)
2026-07-22 19:12:44 -04:00
self.assertEqual(lines['do_tcl_itbis18v_inv']['operator'], '+')
self.assertEqual(lines['do_tcl_itbis18c_inv']['operator'], '-')
self.assertEqual(lines['do_tcl_ret_itbis_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'], '+')
2026-07-22 19:12:44 -04:00
self.assertEqual(lines['do_tcl_prop_inv']['code'], 'do_tc_others_tip')
def test_tax_rule_templates_include_date_sensitive_bank_tax(self):
rules = {
record_id: values
for _, record_id, model, values in _iter_xml_records(
'tax_rule_do_en.xml')
if model == 'account.tax.rule.template'
}
lines = {
record_id: values
for _, record_id, model, values in _iter_xml_records(
'tax_rule_do_en.xml')
if model == 'account.tax.rule.line.template'
}
self.assertIn('do_tax_rule_bank_check_transfer', rules)
self.assertEqual(
lines['do_trline_bank_check_transfer_015']['origin_tax'],
'do_tax_cheques_015')
self.assertEqual(
lines['do_trline_bank_check_transfer_015']['tax'],
'do_tax_cheques_015')
self.assertEqual(
lines['do_trline_bank_check_transfer_015']['end_date'],
'datetime.date(2026, 7, 2)')
self.assertEqual(
lines['do_trline_bank_check_transfer_020']['origin_tax'],
'do_tax_cheques_015')
self.assertEqual(
lines['do_trline_bank_check_transfer_020']['tax'],
'do_tax_cheques_020')
self.assertEqual(
lines['do_trline_bank_check_transfer_020']['start_date'],
'datetime.date(2026, 7, 3)')
2026-07-22 19:12:44 -04:00
del ModuleTestCase