Apply the Article 309 rates introduced by Law 30-26 with dated historical templates and rules. Complete tax-code reversals, improve English terminology, harden migrations, and expand structural and functional tests.
1204 lines
50 KiB
Python
1204 lines
50 KiB
Python
import datetime
|
||
import unittest
|
||
from collections import Counter
|
||
from configparser import ConfigParser
|
||
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
|
||
|
||
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'},
|
||
}
|
||
|
||
|
||
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')
|
||
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
|
||
|
||
|
||
class AccountDoTestCase(ModuleTestCase):
|
||
"Test account_do module"
|
||
module = 'account_do'
|
||
extras = [
|
||
'account_asset',
|
||
'account_deposit',
|
||
'account_stock_continental',
|
||
]
|
||
|
||
@with_transaction()
|
||
def test_chart_template_is_loaded_by_default(self):
|
||
'Test English Dominican chart is loaded in an English database'
|
||
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)
|
||
|
||
@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')
|
||
TaxCodeLine = pool.get('account.tax.code.line')
|
||
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'))
|
||
|
||
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)
|
||
for code in [
|
||
'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', '21020703',
|
||
'21020801',
|
||
'21020802', '21020803', '21020804', '21020805',
|
||
'21020901', '21021001', '21021101', '21021201',
|
||
'21020302', '21021301', '21021302', '21021303',
|
||
'21021401', '21021402', '21021501',
|
||
'21021502', '21021701', '21021705', '21021706',
|
||
'21021801', '21021901',
|
||
'21021902', '6208']:
|
||
with self.subTest(code=code):
|
||
self.assertTrue(Account.search([
|
||
('company', '=', company.id),
|
||
('code', '=', code),
|
||
('closed', '!=', True),
|
||
], limit=1))
|
||
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',
|
||
}
|
||
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)
|
||
|
||
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)'),
|
||
], 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'),
|
||
('rate', '=', Decimal('0.18')),
|
||
], limit=1))
|
||
self.assertTrue(Tax.search([
|
||
('company', '=', company.id),
|
||
('description', '=',
|
||
'ITBIS Withholding Large Taxpayer 30%'),
|
||
('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))
|
||
foreign_15, = Tax.search([
|
||
('company', '=', company.id),
|
||
('description', '=', 'Foreign ISR Withholding 15%'),
|
||
], limit=1)
|
||
self.assertEqual(foreign_15.rate, Decimal('-0.15'))
|
||
self.assertEqual(
|
||
foreign_15.start_date, datetime.date(2026, 7, 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])
|
||
foreign_27, = Tax.search([
|
||
('company', '=', company.id),
|
||
('description', '=', 'Foreign ISR Withholding 27%'),
|
||
], limit=1)
|
||
foreign_rule, = TaxRule.search([
|
||
('company', '=', company.id),
|
||
('name', '=', (
|
||
'Foreign Royalties, Software and Online '
|
||
'Services 15% Rule (RD)')),
|
||
], limit=1)
|
||
self.assertEqual(
|
||
foreign_rule.apply(foreign_27, {
|
||
'date': datetime.date(2026, 6, 30),
|
||
}),
|
||
[foreign_27.id])
|
||
self.assertEqual(
|
||
foreign_rule.apply(foreign_27, {
|
||
'date': datetime.date(2026, 7, 1),
|
||
}),
|
||
[foreign_15.id])
|
||
service_10, = Tax.search([
|
||
('company', '=', company.id),
|
||
('description', '=', 'ISR Withholding Individual 10%'),
|
||
], limit=1)
|
||
service_15, = Tax.search([
|
||
('company', '=', company.id),
|
||
('description', '=', 'ISR Withholding Individual 15%'),
|
||
], limit=1)
|
||
service_rule, = TaxRule.search([
|
||
('company', '=', company.id),
|
||
('name', '=', 'Individual Services ISR Rule (RD)'),
|
||
], limit=1)
|
||
self.assertEqual(
|
||
service_rule.apply(service_10, {
|
||
'date': datetime.date(2026, 6, 30),
|
||
}),
|
||
[service_10.id])
|
||
self.assertEqual(
|
||
service_rule.apply(service_10, {
|
||
'date': datetime.date(2026, 7, 1),
|
||
}),
|
||
[service_15.id])
|
||
itbis_sale, = Tax.search([
|
||
('company', '=', company.id),
|
||
('description', '=', 'ITBIS 18% Sales'),
|
||
], limit=1)
|
||
society_withholding, = Tax.search([
|
||
('company', '=', company.id),
|
||
('description', '=', 'ITBIS Withheld Companies 30%'),
|
||
], limit=1)
|
||
society_rule, = TaxRule.search([
|
||
('company', '=', company.id),
|
||
('name', '=', (
|
||
'ITBIS Withheld by Companies 30% Customer '
|
||
'Rule (RD)')),
|
||
], limit=1)
|
||
self.assertEqual(
|
||
society_rule.apply(itbis_sale, {}),
|
||
[society_withholding.id, itbis_sale.id])
|
||
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 Individual 15%', 'invoice'): '-',
|
||
('ISR Withholding Individual 15%', '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'): '-',
|
||
}
|
||
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)
|
||
|
||
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()
|
||
# A retried module update must be harmless.
|
||
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'),
|
||
]))
|
||
|
||
|
||
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()
|
||
]
|
||
|
||
self.assertEqual(config.get('tryton', 'version'), '8.0.1')
|
||
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())
|
||
self.assertNotIn(
|
||
'Solu' + 'tema',
|
||
'\n'.join(
|
||
path.read_text(encoding='utf-8')
|
||
for path in MODULE_DIR.rglob('*')
|
||
if path.is_file()
|
||
and '.git' not in path.parts
|
||
and 'dist' not in path.parts
|
||
and (path.suffix in {'.py', '.rst', '.toml', '.xml'}
|
||
or path.name in {
|
||
'CHANGELOG', 'COPYRIGHT', 'tryton.cfg'})))
|
||
|
||
def test_xml_record_inventory_is_explicit(self):
|
||
expected = {
|
||
'account_chart_do_en.xml': {
|
||
'account.account.type.template': 36,
|
||
'account.account.template': 287,
|
||
},
|
||
'tax_do_en.xml': {
|
||
'account.tax.group': 6,
|
||
'account.tax.template': 56,
|
||
},
|
||
'tax_code_do_en.xml': {
|
||
'account.tax.code.template': 44,
|
||
'account.tax.code.line.template': 112,
|
||
},
|
||
'tax_rule_do_en.xml': {
|
||
'account.tax.rule.template': 28,
|
||
'account.tax.rule.line.template': 34,
|
||
},
|
||
'account_chart_do_es_419.xml': {
|
||
'account.account.type.template': 36,
|
||
'account.account.template': 287,
|
||
},
|
||
'tax_do_es_419.xml': {
|
||
'account.tax.group': 6,
|
||
'account.tax.template': 56,
|
||
},
|
||
'tax_code_do_es_419.xml': {
|
||
'account.tax.code.template': 44,
|
||
'account.tax.code.line.template': 112,
|
||
},
|
||
'tax_rule_do_es_419.xml': {
|
||
'account.tax.rule.template': 28,
|
||
'account.tax.rule.line.template': 34,
|
||
},
|
||
}
|
||
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', 'legal_notice'} 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',
|
||
' aseguradoras ', ' bienes ', ' casilla ',
|
||
' combustibles ', ' intereses ', ' ley ', ' pagos ',
|
||
' personas ', ' premios ', ' retención ', ' tasa ',
|
||
]:
|
||
with self.subTest(phrase=phrase):
|
||
self.assertNotIn(phrase, text.lower())
|
||
|
||
def test_english_xml_comments_are_in_english(self):
|
||
text = '\n'.join(
|
||
(MODULE_DIR / filename).read_text(encoding='utf-8').lower()
|
||
for filename in [
|
||
'account_chart_do_en.xml', 'tax_do_en.xml',
|
||
'tax_code_do_en.xml', 'tax_rule_do_en.xml'])
|
||
for marker in [
|
||
'á', 'é', 'í', 'ó', 'ú', 'ñ',
|
||
' cuenta ', ' impuesto ', ' proveedor ', ' regla ',
|
||
' retención ', ' servicios ', ' tasa ',
|
||
]:
|
||
with self.subTest(marker=marker):
|
||
self.assertNotIn(marker, text)
|
||
|
||
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_accounts['do_account_110406_es_419']['name'],
|
||
'ITBIS retenido en adquisiciones a recuperar')
|
||
self.assertEqual(
|
||
spanish_taxes['do_tax_itbis_18_venta_es_419']['description'],
|
||
'ITBIS 18% Ventas')
|
||
|
||
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',
|
||
))
|
||
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'))
|
||
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',
|
||
]:
|
||
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_every_tax_group_is_used(self):
|
||
"""Do not install tax groups that no tax template can produce."""
|
||
records = list(_iter_xml_records('tax_do_en.xml'))
|
||
groups = {
|
||
record_id for _, record_id, model, _values in records
|
||
if model == 'account.tax.group'
|
||
}
|
||
used_groups = {
|
||
values['group'] for _, _, model, values in records
|
||
if model == 'account.tax.template' and values.get('group')
|
||
}
|
||
self.assertEqual(groups, used_groups)
|
||
|
||
def test_chart_template_codes_are_unique(self):
|
||
accounts = [
|
||
values for _, _, model, values in _iter_xml_records(
|
||
'account_chart_do_en.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_en.xml')
|
||
if model == 'account.account.template'
|
||
}
|
||
types = {
|
||
record_id: values
|
||
for _, record_id, model, values in _iter_xml_records(
|
||
'account_chart_do_en.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_ifrs_chart_is_complete_and_typed(self):
|
||
accounts = {
|
||
values['code']: values
|
||
for _, _, model, values in _iter_xml_records(
|
||
'account_chart_do_en.xml')
|
||
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')
|
||
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')
|
||
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)
|
||
|
||
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')
|
||
}
|
||
self.assertFalse([
|
||
record_id for record_id in ids if 'otros' in record_id])
|
||
|
||
def test_tax_templates_are_complete(self):
|
||
taxes = {
|
||
record_id: values
|
||
for _, record_id, model, values
|
||
in _iter_xml_records('tax_do_en.xml')
|
||
if model == 'account.tax.template'
|
||
}
|
||
accounts = {
|
||
record_id: values
|
||
for _, record_id, model, values in _iter_xml_records(
|
||
'account_chart_do_en.xml')
|
||
if model == 'account.account.template'
|
||
}
|
||
self.assertEqual(len(taxes), 56)
|
||
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)
|
||
self.assertIn(account_id, accounts)
|
||
self.assertTrue(
|
||
values.get('rate') or values.get('amount'))
|
||
else:
|
||
self.assertFalse(values.get('rate'))
|
||
self.assertTrue(values.get('legal_notice'))
|
||
|
||
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_hon_5']['group'], 'do_tax_group_isr')
|
||
self.assertEqual(
|
||
taxes['do_tax_ret_isr_serv_10']['group'], 'do_tax_group_isr')
|
||
self.assertEqual(
|
||
taxes['do_tax_ret_isr_ext_27']['group'],
|
||
'do_tax_group_isr_ext')
|
||
self.assertEqual(
|
||
taxes['do_tax_ret_isr_ext_10']['group'],
|
||
'do_tax_group_isr_ext')
|
||
self.assertEqual(
|
||
taxes['do_tax_ret_isr_bovine_1']['start_date'],
|
||
'datetime.date(2025, 6, 20)')
|
||
for old_id in [
|
||
'do_tax_ret_isr_serv_10',
|
||
'do_tax_ret_isr_alq_10',
|
||
'do_tax_ret_isr_premios_10',
|
||
'do_tax_ret_isr_premios_15',
|
||
'do_tax_ret_isr_tragamonedas_10',
|
||
'do_tax_ret_isr_other_income_10',
|
||
]:
|
||
self.assertEqual(
|
||
taxes[old_id]['end_date'], 'datetime.date(2026, 6, 30)')
|
||
for current_id in [
|
||
'do_tax_ret_isr_serv_15',
|
||
'do_tax_ret_isr_alq_15',
|
||
'do_tax_ret_isr_betting_15',
|
||
'do_tax_ret_isr_tragamonedas_15',
|
||
'do_tax_ret_isr_other_income_15',
|
||
]:
|
||
self.assertEqual(
|
||
taxes[current_id]['start_date'],
|
||
'datetime.date(2026, 7, 1)')
|
||
self.assertIn(
|
||
'Tax Code Arts. 401-405; Art. 404',
|
||
taxes['do_tax_activos_1']['legal_notice'])
|
||
self.assertIn(
|
||
'Law 173-07 Art. 7',
|
||
taxes['do_tax_iti_3']['legal_notice'])
|
||
self.assertIn(
|
||
'Art. 228', taxes['do_tax_propina_10']['legal_notice'])
|
||
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')
|
||
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)
|
||
|
||
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')
|
||
if model == 'account.tax.code.template'
|
||
}
|
||
lines = {
|
||
record_id: values
|
||
for _, record_id, model, values in _iter_xml_records(
|
||
'tax_code_do_en.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: Withholdings')
|
||
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')
|
||
for record_id, operator in TAX_CODE_LINE_AUDIT.items():
|
||
with self.subTest(record_id=record_id):
|
||
self.assertEqual(lines[record_id]['operator'], operator)
|
||
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'], '+')
|
||
self.assertEqual(lines['do_tcl_prop_inv']['code'], 'do_tc_others_tip')
|
||
|
||
def test_tax_code_lines_reverse_credit_notes(self):
|
||
"""Every reported amount must have an inverse credit-note line."""
|
||
pairs = {}
|
||
for _, record_id, model, values in _iter_xml_records(
|
||
'tax_code_do_en.xml'):
|
||
if model != 'account.tax.code.line.template':
|
||
continue
|
||
key = (values['tax'], values['amount'])
|
||
pair = pairs.setdefault(key, {})
|
||
self.assertNotIn(values['type'], pair)
|
||
pair[values['type']] = (
|
||
record_id, values['operator'])
|
||
for key, lines in pairs.items():
|
||
with self.subTest(tax=key[0], amount=key[1]):
|
||
self.assertEqual(set(lines), {'invoice', 'credit'})
|
||
self.assertNotEqual(
|
||
lines['invoice'][1], lines['credit'][1])
|
||
|
||
def test_every_tax_is_reported_by_a_tax_code(self):
|
||
taxes = {
|
||
record_id for _, record_id, model, _values
|
||
in _iter_xml_records('tax_do_en.xml')
|
||
if model == 'account.tax.template'
|
||
}
|
||
reported = {
|
||
values['tax'] for _, _, model, values
|
||
in _iter_xml_records('tax_code_do_en.xml')
|
||
if model == 'account.tax.code.line.template'
|
||
}
|
||
self.assertEqual(taxes, reported)
|
||
|
||
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_supp_ext_isr27']['group'],
|
||
'do_tax_group_isr_ext')
|
||
self.assertEqual(
|
||
lines['do_trline_supp_ext_isr15']['tax'],
|
||
'do_tax_ret_isr_ext_15')
|
||
self.assertEqual(
|
||
lines['do_trline_supp_ext_isr15']['start_date'],
|
||
'datetime.date(2026, 7, 1)')
|
||
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)')
|
||
|
||
def test_tax_rule_lines_have_reachable_match_patterns(self):
|
||
"""A rule must not contain two lines with the same match pattern."""
|
||
patterns = {}
|
||
for _, record_id, model, values in _iter_xml_records(
|
||
'tax_rule_do_en.xml'):
|
||
if model != 'account.tax.rule.line.template':
|
||
continue
|
||
pattern = tuple(values.get(field) for field in [
|
||
'rule', 'group', 'origin_tax', 'start_date', 'end_date'])
|
||
self.assertNotIn(
|
||
pattern, patterns,
|
||
msg=f'{record_id} is shadowed by {patterns.get(pattern)}')
|
||
patterns[pattern] = record_id
|
||
|
||
|
||
del ModuleTestCase
|