diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml index 683b45d..1224c6c 100644 --- a/.gitea/workflows/publish.yml +++ b/.gitea/workflows/publish.yml @@ -1,14 +1,53 @@ -name: Publish Package +name: Test and Publish Package on: push: branches: - "**" + pull_request: + branches: + - "**" workflow_dispatch: jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install package and test dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e '.[test]' + + - name: Compile Python sources + run: | + python -m compileall . + + - name: Run module tests + run: | + python -m unittest discover -s tests -p 'test*.py' + + - name: Build package + run: | + python -m pip install build twine + python -m build + + - name: Check package + run: | + python -m twine check dist/* + publish: runs-on: ubuntu-latest + needs: test + if: github.event_name != 'pull_request' steps: - name: Checkout diff --git a/README.rst b/README.rst index 1bd8494..526ed48 100644 --- a/README.rst +++ b/README.rst @@ -23,7 +23,7 @@ Pruebas Las pruebas validan la instalación del módulo, la creación del plan para una compañía, la integridad de referencias XML, la clasificación contable principal, los códigos fiscales y las categorías fiscales dominicanas. Cumplimiento base NIIF ---------------------- +---------------------- ``account_do`` cubre el 100% de la base estructural que corresponde a una localización contable: catálogo NIIF/RD, tipos de presentación, cuentas fiscales dominicanas, impuestos, reglas, metadata legal y pruebas de instalación. La medición NIIF, estimaciones, cierres, estados financieros, notas y revelaciones dependen de políticas contables y módulos operativos complementarios. @@ -49,15 +49,33 @@ Matriz de auditoría La cobertura de auditoría fiscal queda formalizada en pruebas para evitar cambios silenciosos en cuentas, clasificación o vigencias. -=============================== =============================================== ============================================== -Área Cobertura Prueba -=============================== =============================================== ============================================== -Impuesto contra cuenta 55 plantillas de ``tax_do.xml`` contra cuenta ``account_do.tests.test_module.TAX_TEMPLATE_AUDIT`` -Clasificación fiscal ``tax_kind``, ``tax_fiscal_type`` y aplicación ``test_tax_templates_are_classified`` -Metadata legal y vigencia Referencia, artículo, formulario, estado y fecha ``test_tax_templates_are_classified`` -Signos de tax codes Plantillas y códigos reales creados por compañía ``test_tax_code_templates_use_expected_signs`` y chart real -Reglas con sustitución Impuesto cheques 0.15% hasta 2026-07-02 y 0.20% desde 2026-07-03 ``test_tax_rule_templates_include_date_sensitive_bank_tax`` -Documentación contra XML La matriz apunta a XML real y falla ante ids/campos faltantes ``test_xml_references_are_resolved_inside_module`` -Plan NIIF contra XML Códigos NIIF críticos, tipos y políticas ``test_ifrs_chart_is_complete_and_typed`` y ``test_ifrs_policy_matrix_is_represented_in_chart`` -Facturas reales Factura venta/compra posteada con ITBIS 18% ``account_invoice_do.tests.test_module`` -=============================== =============================================== ============================================== +.. list-table:: + :header-rows: 1 + + * - Área + - Cobertura + - Prueba + * - Impuesto contra cuenta + - 55 plantillas de ``tax_do.xml`` contra cuenta. + - ``account_do.tests.test_module.TAX_TEMPLATE_AUDIT`` + * - Clasificación fiscal + - ``tax_kind``, ``tax_fiscal_type`` y aplicación. + - ``test_tax_templates_are_classified`` + * - Metadata legal y vigencia + - Referencia, artículo, formulario, estado y fecha. + - ``test_tax_templates_are_classified`` + * - Signos de tax codes + - Plantillas y códigos reales creados por compañía. + - ``test_tax_code_templates_use_expected_signs`` y chart real. + * - Reglas con sustitución + - Impuesto cheques 0.15% hasta 2026-07-02 y 0.20% desde 2026-07-03. + - ``test_tax_rule_templates_include_date_sensitive_bank_tax`` + * - Documentación contra XML + - La matriz apunta a XML real y falla ante ids/campos faltantes. + - ``test_xml_references_are_resolved_inside_module`` + * - Plan NIIF contra XML + - Códigos NIIF críticos, tipos y políticas. + - ``test_ifrs_chart_is_complete_and_typed`` y ``test_ifrs_policy_matrix_is_represented_in_chart`` + * - Facturas reales + - Factura venta/compra posteada con ITBIS 18%. + - ``account_invoice_do.tests.test_module`` diff --git a/doc/publishing.rst b/doc/publishing.rst index a0082fc..17d4cb6 100644 --- a/doc/publishing.rst +++ b/doc/publishing.rst @@ -1,29 +1,30 @@ Publishing ========== -Every pushed commit builds, validates, and publishes the package to the Gitea -PyPI-compatible package registry. The workflow intentionally does not use -``actions/upload-artifact`` because current Gitea Actions runners do not support -the GitHub artifact API required by ``upload-artifact@v4``. +Every pushed commit installs the package, compiles the Python sources, runs the +module tests, builds the distribution, and validates it with ``twine check``. +Only branches named with the Tryton series, such as ``8.0``, continue to the +publishing step after those checks pass. -The publishing workflow creates a CI-only package version by appending the run -number to the module version from ``tryton.cfg``. For example, a module version -of ``8.0.0`` is published as ``8.0.0.post123`` on run number ``123``. +The publishing workflow creates a CI-only package version by reading the +published versions in Gitea's PyPI-compatible package registry and assigning the +next patch version for the branch series. For example, branch ``8.0`` publishes +``8.0.0`` when no package exists yet, then ``8.0.1``, ``8.0.2``, and so on. This is necessary because Gitea's PyPI-compatible registry is a custom package repository for Twine and does not support ``twine upload --skip-existing``. Required repository secrets: -* ``GITEA_PACKAGE_USER`` -* ``GITEA_PACKAGE_TOKEN`` +* ``REGISTRY_USER`` +* ``REGISTRY_PASSWORD`` Publish a release branch: .. code-block:: console - git switch -c 8.0.0 - git push origin 8.0.0 + git switch 8.0 + git push origin 8.0 Install from Gitea: diff --git a/locale/es.po b/locale/es.po index 4061208..a140bbc 100644 --- a/locale/es.po +++ b/locale/es.po @@ -1074,6 +1074,10 @@ msgctxt "model:account.account.type.template,name:do_type_income_tax" msgid "Impuesto a las ganancias" msgstr "Impuesto a las ganancias" +msgctxt "model:account.account.type.template,name:do_type_control" +msgid "Cuentas de cierre y control" +msgstr "Cuentas de cierre y control" + msgctxt "model:account.account.type.template,name:do_type_intangible" msgid "Activos intangibles" msgstr "Activos intangibles" diff --git a/locale/es_419.po b/locale/es_419.po index b6b5e6c..da3adf8 100644 --- a/locale/es_419.po +++ b/locale/es_419.po @@ -1066,6 +1066,10 @@ msgctxt "model:account.account.type.template,name:do_type_income_tax" msgid "Impuesto a las ganancias" msgstr "Impuesto a las ganancias" +msgctxt "model:account.account.type.template,name:do_type_control" +msgid "Cuentas de cierre y control" +msgstr "Cuentas de cierre y control" + msgctxt "model:account.account.type.template,name:do_type_intangible" msgid "Activos intangibles" msgstr "Activos intangibles" diff --git a/tests/test_module.py b/tests/test_module.py index a3756cf..63fba50 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -1,6 +1,7 @@ import unittest import datetime from collections import Counter +from configparser import ConfigParser from decimal import Decimal from pathlib import Path from xml.etree import ElementTree as ET @@ -237,6 +238,21 @@ def _iter_xml_records(*filenames): yield filename, record.get('id'), record.get('model'), values +def _po_entries(filename): + entries = [] + context = msgid = msgstr = None + for line in (MODULE_DIR / filename).read_text(encoding='utf-8').splitlines(): + if line.startswith('msgctxt '): + context = line.split(' ', 1)[1].strip().strip('"') + elif line.startswith('msgid '): + msgid = line.split(' ', 1)[1].strip().strip('"') + elif line.startswith('msgstr '): + msgstr = line.split(' ', 1)[1].strip().strip('"') + entries.append((context, msgid, msgstr)) + context = msgid = msgstr = None + return entries + + class AccountDoTestCase(ModuleTestCase): "Test account_do module" module = 'account_do' @@ -448,6 +464,229 @@ class AccountDoTestCase(ModuleTestCase): 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.0') + self.assertEqual( + set(lines('tryton', 'depends')), + {'account', 'company', 'currency'}) + self.assertEqual( + lines('tryton', 'xml'), + [ + 'account_chart_do.xml', + 'tax_do.xml', + 'tax_view.xml', + 'tax_code_do.xml', + 'tax_rule_do.xml', + ]) + self.assertEqual( + lines('register', 'model'), + [ + 'tax.TaxTemplate', + 'tax.Tax', + 'tax.TaxCode', + 'tax.TaxCodeLine', + ]) + for filename in lines('tryton', 'xml'): + self.assertTrue((MODULE_DIR / filename).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) + for package_file in [ + 'README.rst', + 'IMPUESTOS_RD.md', + 'doc/**/*.rst', + 'locale/**/*.po', + 'tests/**/*.rst', + ]: + self.assertIn(package_file, pyproject) + self.assertEqual( + (MODULE_DIR / '__init__.py').read_text(encoding='utf-8'), '') + + def test_xml_record_inventory_is_explicit(self): + expected = { + 'account_chart_do.xml': { + 'account.account.type.template': 36, + 'account.account.template': 281, + }, + 'tax_do.xml': { + 'account.tax.group': 7, + 'account.tax.template': 55, + }, + 'tax_code_do.xml': { + 'account.tax.code.template': 49, + 'account.tax.code.line.template': 103, + }, + 'tax_rule_do.xml': { + 'account.tax.rule.template': 28, + 'account.tax.rule.line.template': 35, + }, + 'tax_view.xml': { + 'ir.ui.view': 4, + }, + } + 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_gitea_workflow_tests_before_publish(self): + workflow = ( + MODULE_DIR / '.gitea' / 'workflows' / 'publish.yml' + ).read_text(encoding='utf-8') + + self.assertIn('test:', workflow) + self.assertIn('publish:', workflow) + self.assertIn('needs: test', workflow) + self.assertIn("python -m pip install -e '.[test]'", workflow) + self.assertIn('python -m compileall .', workflow) + self.assertIn( + "python -m unittest discover -s tests -p 'test*.py'", workflow) + self.assertIn('python -m build', workflow) + self.assertLess( + workflow.index('test:'), + workflow.index('publish:')) + self.assertLess( + workflow.index("python -m unittest discover -s tests -p 'test*.py'"), + workflow.index('Set CI package version')) + + def test_tax_views_expose_all_localization_fields(self): + localization_fields = [ + 'tax_kind', + 'tax_fiscal_type', + 'tax_application', + 'dgii_fiscal_status', + 'dgii_form_hint', + 'dgii_legal_reference', + 'dgii_legal_article', + ] + form = ET.parse(MODULE_DIR / 'view/tax_form.xml').getroot() + list_ = ET.parse(MODULE_DIR / 'view/tax_list.xml').getroot() + form_fields = [ + element.get('name') + for element in form.findall('.//field') + if element.get('name') in localization_fields + ] + list_fields = [ + element.get('name') + for element in list_.findall('.//field') + if element.get('name') in localization_fields + ] + self.assertEqual(form_fields, localization_fields) + self.assertEqual(list_fields, localization_fields[:3]) + + view_records = { + (values['model'], values['name'], values['inherit']) + for _, _, model, values in _iter_xml_records('tax_view.xml') + if model == 'ir.ui.view' + } + self.assertEqual(view_records, { + ('account.tax.template', 'tax_form', + 'account.tax_template_view_form'), + ('account.tax', 'tax_form', 'account.tax_view_form'), + ('account.tax.template', 'tax_list', + 'account.tax_template_view_list'), + ('account.tax', 'tax_list', 'account.tax_view_list'), + }) + + def test_translation_catalogs_cover_module_terms(self): + type_templates = { + record_id: values['name'] + for _, record_id, model, values in _iter_xml_records( + 'account_chart_do.xml') + if model == 'account.account.type.template' + } + account_templates = { + record_id: values['name'] + for _, record_id, model, values in _iter_xml_records( + 'account_chart_do.xml') + if model == 'account.account.template' + } + required_field_contexts = { + 'field:account.tax,tax_kind:', + 'field:account.tax.template,tax_kind:', + 'field:account.tax,tax_fiscal_type:', + 'field:account.tax.template,tax_fiscal_type:', + 'field:account.tax,tax_application:', + 'field:account.tax.template,tax_application:', + 'field:account.tax,dgii_fiscal_status:', + 'field:account.tax.template,dgii_fiscal_status:', + 'field:account.tax,dgii_form_hint:', + 'field:account.tax.template,dgii_form_hint:', + 'field:account.tax,dgii_legal_article:', + 'field:account.tax.template,dgii_legal_article:', + 'field:account.tax,dgii_legal_reference:', + 'field:account.tax.template,dgii_legal_reference:', + } + for filename in ['locale/es.po', 'locale/es_419.po']: + entries = _po_entries(filename) + contexts = {context for context, _msgid, _msgstr in entries} + entry_keys = [(context, msgid) for context, msgid, _ in entries] + with self.subTest(filename=filename): + self.assertEqual( + [key for key, count in Counter(entry_keys).items() + if count > 1], + []) + self.assertLessEqual(required_field_contexts, contexts) + for record_id in type_templates: + self.assertIn( + 'model:account.account.type.template,name:%s' + % record_id, + contexts) + for record_id in [ + 'do_account_110201', + 'do_account_210101', + 'do_account_21021901', + 'do_account_21021902', + ]: + self.assertIn( + 'model:account.account.template,name:%s' % record_id, + contexts) + + def test_migration_metadata_is_explicit(self): + from trytond.modules.account_do.tax import ( + MODEL_DATA_RENAMES, ROOT_TAX_CODE_CHILDREN) + + self.assertEqual(MODEL_DATA_RENAMES, { + 'do_tax_group_otros': 'do_tax_group_others', + 'do_tc_otros': 'do_tc_others', + 'do_tc_otros_propina': 'do_tc_others_tip', + 'do_tc_otros_cheques': 'do_tc_others_checks', + 'do_tc_otros_activos': 'do_tc_others_assets', + 'do_tc_otros_iti': 'do_tc_others_iti', + }) + self.assertEqual(ROOT_TAX_CODE_CHILDREN, [ + 'ITBIS — Balance Neto (Débito − Crédito)', + 'ISR - Retenciones', + 'ISR - Retenciones en la Fuente', + 'ISC - Impuesto Selectivo al Consumo', + 'CDT INDOTEL 2% (Ley 153-98)', + 'Otros Impuestos y Contribuciones', + ]) + def test_xml_references_are_resolved_inside_module(self): records = list(_iter_xml_records( 'account_chart_do.xml',