6 Commits
Author SHA1 Message Date
José Arturo García 76d4c3a649 ci: add package publishing workflow
Publish Package / publish (push) Canceled after 5m9s
2026-08-12 18:10:32 -04:00
joseagrc 29e987520d Update Dominican withholding catalog
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.
2026-08-10 09:10:05 -04:00
joseagrc c3ba620b74 Harden localization data and validation 2026-08-10 08:42:52 -04:00
joseagrc 6581a4b8af Update foreign withholding and tax rules 2026-08-10 08:18:26 -04:00
joseagrc 22281ed4a6 Classify foreign ISR withholdings separately
Assign payments to non-residents to the dedicated ISREXT group in both localization datasets and tax rules. Add regression checks for national and foreign ISR groups.
2026-08-10 02:23:01 -04:00
José Arturo García d708024c58 Start account_do 8.1 series 2026-08-10 00:12:02 -04:00
22 changed files with 126 additions and 780 deletions
+4 -2
View File
@@ -45,7 +45,7 @@ jobs:
if: env.PUBLISH_PACKAGE == 'true' if: env.PUBLISH_PACKAGE == 'true'
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
python -m pip install 'build>=1.2' 'twine>=7' 'packaging>=26' python -m pip install build twine
- name: Check publishing credentials - name: Check publishing credentials
if: env.PUBLISH_PACKAGE == 'true' if: env.PUBLISH_PACKAGE == 'true'
@@ -71,6 +71,7 @@ jobs:
import json import json
import os import os
import re import re
import tomllib
OWNER = 'tryton-do' OWNER = 'tryton-do'
PACKAGE_TYPE = 'pypi' PACKAGE_TYPE = 'pypi'
@@ -88,7 +89,8 @@ jobs:
series = os.environ['PACKAGE_SERIES'] series = os.environ['PACKAGE_SERIES']
major, minor = map(int, series.split('.')) major, minor = map(int, series.split('.'))
package_name = 'unpaismejor_account_do' pyproject = tomllib.loads(Path('pyproject.toml').read_text())
package_name = pyproject['project']['name']
normalized_name = normalize(package_name) normalized_name = normalize(package_name)
username = os.environ['TWINE_USERNAME'] username = os.environ['TWINE_USERNAME']
+1 -26
View File
@@ -1,31 +1,6 @@
Version 8.0.5 - 2026-08-10 Version 8.1.0 - 2026-08-10
--------------------------
* Translate every field and selection introduced by the module into Spanish
and Latin American Spanish.
* Include translation catalogs in built distributions.
Version 8.0.4 - 2026-08-10
--------------------------
* Display the Dominican tax classification on tax and tax-template forms and
lists.
Version 8.0.3 - 2026-08-10
--------------------------
* Restore the shared Dominican tax classification required by invoicing, NCF
management and electronic fiscal documents.
* Populate tax classifications for existing charts and language-scoped tax
templates.
Version 8.0.2 - 2026-08-10
--------------------------
* Structure income-statement types under current-period profit or loss, with
operating and non-operating sections compatible with Tryton core reports.
Version 8.0.1 - 2026-08-09
-------------------------- --------------------------
* Initial release. * Initial release.
* Include project metadata and review documentation in the wheel so the
installed package and its test suite are reproducible.
* Document tax requirements and official source provenance. * Document tax requirements and official source provenance.
* Correct asset, real-estate transfer, legal-tip, vehicle and bovine tax data. * Correct asset, real-estate transfer, legal-tip, vehicle and bovine tax data.
* Remove unsupported or unsafe ITBIS templates and their dependent records. * Remove unsupported or unsafe ITBIS templates and their dependent records.
-19
View File
@@ -1,21 +1,2 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of # This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms. # this repository contains the full copyright notices and license terms.
from trytond.pool import Pool
from . import account, invoice, ir, tax
__all__ = ['register']
def register():
Pool.register(
ir.ModelData,
invoice.Invoice,
invoice.InvoiceLine,
tax.TaxTemplate,
tax.Tax,
module='account_do', type_='model')
Pool.register(
account.CreateChart,
module='account_do', type_='wizard')
+15 -40
View File
@@ -191,70 +191,45 @@
<field name="sequence" eval="30"/> <field name="sequence" eval="30"/>
<field name="statement">balance</field> <field name="statement">balance</field>
</record> </record>
<record id="do_type_profit_loss_en" model="account.account.type.template">
<field name="name">Current Period Profit or Loss</field>
<field name="parent" ref="do_type_equity_en"/>
<field name="sequence" eval="40"/>
<field name="statement">income</field>
</record>
<record id="do_type_operating_section_en" model="account.account.type.template">
<field name="name">Operating profit or loss</field>
<field name="parent" ref="do_type_profit_loss_en"/>
<field name="sequence" eval="10"/>
<field name="statement">income</field>
</record>
<record id="do_type_revenue_en" model="account.account.type.template"> <record id="do_type_revenue_en" model="account.account.type.template">
<field name="name">Revenue</field> <field name="name">Revenue</field>
<field name="parent" ref="do_type_operating_section_en"/> <field name="parent" ref="do_type_root_en"/>
<field name="sequence" eval="10"/> <field name="sequence" eval="40"/>
<field name="statement">income</field> <field name="statement">income</field>
<field name="revenue" eval="True"/> <field name="revenue" eval="True"/>
</record> </record>
<record id="do_type_expenses_en" model="account.account.type.template"> <record id="do_type_other_income_en" model="account.account.type.template">
<field name="name">Operating costs and expenses</field> <field name="name">Other Income and Gains</field>
<field name="parent" ref="do_type_operating_section_en"/> <field name="parent" ref="do_type_root_en"/>
<field name="sequence" eval="20"/> <field name="sequence" eval="50"/>
<field name="statement">income</field> <field name="statement">income</field>
<field name="expense" eval="True"/> <field name="revenue" eval="True"/>
</record> </record>
<record id="do_type_cogs_en" model="account.account.type.template"> <record id="do_type_cogs_en" model="account.account.type.template">
<field name="name">Cost of Sales and Services</field> <field name="name">Cost of Sales and Services</field>
<field name="parent" ref="do_type_expenses_en"/> <field name="parent" ref="do_type_root_en"/>
<field name="sequence" eval="10"/> <field name="sequence" eval="60"/>
<field name="statement">income</field> <field name="statement">income</field>
<field name="expense" eval="True"/> <field name="expense" eval="True"/>
</record> </record>
<record id="do_type_expense_en" model="account.account.type.template"> <record id="do_type_expense_en" model="account.account.type.template">
<field name="name">Operational expenses</field> <field name="name">Operational expenses</field>
<field name="parent" ref="do_type_expenses_en"/> <field name="parent" ref="do_type_root_en"/>
<field name="sequence" eval="20"/> <field name="sequence" eval="70"/>
<field name="statement">income</field> <field name="statement">income</field>
<field name="expense" eval="True"/> <field name="expense" eval="True"/>
</record> </record>
<record id="do_type_non_operating_section_en" model="account.account.type.template">
<field name="name">Non-operating profit or loss</field>
<field name="parent" ref="do_type_profit_loss_en"/>
<field name="sequence" eval="20"/>
<field name="statement">income</field>
</record>
<record id="do_type_other_income_en" model="account.account.type.template">
<field name="name">Other Income and Gains</field>
<field name="parent" ref="do_type_non_operating_section_en"/>
<field name="sequence" eval="10"/>
<field name="statement">income</field>
<field name="revenue" eval="True"/>
</record>
<record id="do_type_financial_expense_en" model="account.account.type.template"> <record id="do_type_financial_expense_en" model="account.account.type.template">
<field name="name">Financial expenses</field> <field name="name">Financial expenses</field>
<field name="parent" ref="do_type_non_operating_section_en"/> <field name="parent" ref="do_type_root_en"/>
<field name="sequence" eval="20"/> <field name="sequence" eval="80"/>
<field name="statement">income</field> <field name="statement">income</field>
<field name="expense" eval="True"/> <field name="expense" eval="True"/>
</record> </record>
<record id="do_type_income_tax_en" model="account.account.type.template"> <record id="do_type_income_tax_en" model="account.account.type.template">
<field name="name">Income Tax</field> <field name="name">Income Tax</field>
<field name="parent" ref="do_type_non_operating_section_en"/> <field name="parent" ref="do_type_root_en"/>
<field name="sequence" eval="30"/> <field name="sequence" eval="90"/>
<field name="statement">income</field> <field name="statement">income</field>
<field name="expense" eval="True"/> <field name="expense" eval="True"/>
</record> </record>
+15 -40
View File
@@ -189,70 +189,45 @@
<field name="sequence" eval="30" /> <field name="sequence" eval="30" />
<field name="statement">balance</field> <field name="statement">balance</field>
</record> </record>
<record id="do_type_profit_loss_es_419" model="account.account.type.template">
<field name="name">Resultado del período actual</field>
<field name="parent" ref="do_type_equity_es_419" />
<field name="sequence" eval="40" />
<field name="statement">income</field>
</record>
<record id="do_type_operating_section_es_419" model="account.account.type.template">
<field name="name">Resultado de operación</field>
<field name="parent" ref="do_type_profit_loss_es_419" />
<field name="sequence" eval="10" />
<field name="statement">income</field>
</record>
<record id="do_type_revenue_es_419" model="account.account.type.template"> <record id="do_type_revenue_es_419" model="account.account.type.template">
<field name="name">Ingresos de actividades ordinarias</field> <field name="name">Ingresos de actividades ordinarias</field>
<field name="parent" ref="do_type_operating_section_es_419" /> <field name="parent" ref="do_type_root_es_419" />
<field name="sequence" eval="10" /> <field name="sequence" eval="40" />
<field name="statement">income</field> <field name="statement">income</field>
<field name="revenue" eval="True" /> <field name="revenue" eval="True" />
</record> </record>
<record id="do_type_expenses_es_419" model="account.account.type.template"> <record id="do_type_other_income_es_419" model="account.account.type.template">
<field name="name">Costos y gastos de operación</field> <field name="name">Otros ingresos y ganancias</field>
<field name="parent" ref="do_type_operating_section_es_419" /> <field name="parent" ref="do_type_root_es_419" />
<field name="sequence" eval="20" /> <field name="sequence" eval="50" />
<field name="statement">income</field> <field name="statement">income</field>
<field name="expense" eval="True" /> <field name="revenue" eval="True" />
</record> </record>
<record id="do_type_cogs_es_419" model="account.account.type.template"> <record id="do_type_cogs_es_419" model="account.account.type.template">
<field name="name">Costo de ventas y servicios</field> <field name="name">Costo de ventas y servicios</field>
<field name="parent" ref="do_type_expenses_es_419" /> <field name="parent" ref="do_type_root_es_419" />
<field name="sequence" eval="10" /> <field name="sequence" eval="60" />
<field name="statement">income</field> <field name="statement">income</field>
<field name="expense" eval="True" /> <field name="expense" eval="True" />
</record> </record>
<record id="do_type_expense_es_419" model="account.account.type.template"> <record id="do_type_expense_es_419" model="account.account.type.template">
<field name="name">Gastos operacionales</field> <field name="name">Gastos operacionales</field>
<field name="parent" ref="do_type_expenses_es_419" /> <field name="parent" ref="do_type_root_es_419" />
<field name="sequence" eval="20" /> <field name="sequence" eval="70" />
<field name="statement">income</field> <field name="statement">income</field>
<field name="expense" eval="True" /> <field name="expense" eval="True" />
</record> </record>
<record id="do_type_non_operating_section_es_419" model="account.account.type.template">
<field name="name">Resultado no operativo</field>
<field name="parent" ref="do_type_profit_loss_es_419" />
<field name="sequence" eval="20" />
<field name="statement">income</field>
</record>
<record id="do_type_other_income_es_419" model="account.account.type.template">
<field name="name">Otros ingresos y ganancias</field>
<field name="parent" ref="do_type_non_operating_section_es_419" />
<field name="sequence" eval="10" />
<field name="statement">income</field>
<field name="revenue" eval="True" />
</record>
<record id="do_type_financial_expense_es_419" model="account.account.type.template"> <record id="do_type_financial_expense_es_419" model="account.account.type.template">
<field name="name">Gastos financieros</field> <field name="name">Gastos financieros</field>
<field name="parent" ref="do_type_non_operating_section_es_419" /> <field name="parent" ref="do_type_root_es_419" />
<field name="sequence" eval="20" /> <field name="sequence" eval="80" />
<field name="statement">income</field> <field name="statement">income</field>
<field name="expense" eval="True" /> <field name="expense" eval="True" />
</record> </record>
<record id="do_type_income_tax_es_419" model="account.account.type.template"> <record id="do_type_income_tax_es_419" model="account.account.type.template">
<field name="name">Impuesto a las ganancias</field> <field name="name">Impuesto a las ganancias</field>
<field name="parent" ref="do_type_non_operating_section_es_419" /> <field name="parent" ref="do_type_root_es_419" />
<field name="sequence" eval="30" /> <field name="sequence" eval="90" />
<field name="statement">income</field> <field name="statement">income</field>
<field name="expense" eval="True" /> <field name="expense" eval="True" />
</record> </record>
+13 -4
View File
@@ -14,22 +14,31 @@ else:
def get_info(): def get_info():
import configparser import configparser
import json
import subprocess
module_dir = os.path.dirname(os.path.dirname(__file__)) module_dir = os.path.dirname(os.path.dirname(__file__))
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read_file(open(os.path.join(module_dir, 'tryton.cfg'))) config.read_file(open(os.path.join(module_dir, 'tryton.cfg')))
info = dict(config.items('tryton')) info = dict(config.items('tryton'))
info['name'] = 'unpaismejor_account_do' metadata_cmd = 'python3 -m build -qq --metadata'
info['description'] = 'Dominican Republic accounting for Tryton' if os.environ.get('DOC_NO_ISOLATION'):
major_version, minor_version, _ = info['version'].split('.', 2) metadata_cmd += ' --no-isolation'
metadata = subprocess.check_output(
metadata_cmd, shell=True, encoding='utf-8', cwd=module_dir).strip()
metadata = json.loads(metadata)
info['name'] = metadata['name']
info['description'] = metadata['summary']
major_version, minor_version, _ = metadata['version'].split('.', 2)
major_version = int(major_version) major_version = int(major_version)
minor_version = int(minor_version) minor_version = int(minor_version)
if minor_version % 2: if minor_version % 2:
info['series'] = 'latest' info['series'] = 'latest'
info['branch'] = 'branch/default' info['branch'] = 'branch/default'
else: else:
info['series'] = '.'.join(info['version'].split('.', 2)[:2]) info['series'] = '.'.join(metadata['version'].split('.', 2)[:2])
info['branch'] = 'branch/' + info['series'] info['branch'] = 'branch/' + info['series']
for key in {'depends', 'extras_depend'}: for key in {'depends', 'extras_depend'}:
+1 -1
View File
@@ -1,7 +1,7 @@
Releases Releases
======== ========
8.0.1 8.1.0
----- -----
Initial release with the Dominican chart of accounts, fiscal accounts, taxes, Initial release with the Dominican chart of accounts, fiscal accounts, taxes,
-39
View File
@@ -1,39 +0,0 @@
"""Dominican invoice tax rounding policy.
Tryton's generic taxable mixin compensates the aggregate currency-rounding
remainder on the first non-zero tax. That keeps the rounded aggregate equal
to the aggregate of the unrounded taxes, but it can change the reported value
of a specific Dominican tax (ITBIS, ISC or CDT).
Dominican fiscal documents state every tax in currency precision. Each tax
must therefore retain its own rounded result; no residual is redistributed
between different tax liabilities or tax credits.
"""
from trytond.model import fields
from trytond.pool import PoolMeta
def round_taxes_independently(currency, taxes):
"""Round every tax amount without reallocating the aggregate residual."""
if not currency:
return
for taxline in taxes.values():
taxline.amount = currency.round(taxline.amount)
class IndependentTaxRoundingMixin:
__slots__ = ()
@fields.depends('currency')
def _round_taxes(self, taxes):
round_taxes_independently(self.currency, taxes)
class Invoice(IndependentTaxRoundingMixin, metaclass=PoolMeta):
__name__ = 'account.invoice'
class InvoiceLine(IndependentTaxRoundingMixin, metaclass=PoolMeta):
__name__ = 'account.invoice.line'
-83
View File
@@ -1,83 +0,0 @@
#
msgid ""
msgstr ""
"Project-Id-Version: account_do 8.0\n"
"PO-Revision-Date: 2026-08-10 00:00-0400\n"
"Last-Translator: Fundación Un País Mejor\n"
"Language-Team: Spanish\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgctxt "field:account.tax,tax_kind:"
msgid "Tax Kind"
msgstr "Clasificación fiscal"
msgctxt "field:account.tax.template,tax_kind:"
msgid "Tax Kind"
msgstr "Clasificación fiscal"
msgctxt "selection:account.tax,tax_kind:"
msgid "ITBIS"
msgstr "ITBIS"
msgctxt "selection:account.tax,tax_kind:"
msgid "ITBIS Withholding"
msgstr "Retención de ITBIS"
msgctxt "selection:account.tax,tax_kind:"
msgid "ITBIS Acquirer Withholding"
msgstr "Retención de ITBIS por adquirencia"
msgctxt "selection:account.tax,tax_kind:"
msgid "ISR Withholding"
msgstr "Retención de ISR"
msgctxt "selection:account.tax,tax_kind:"
msgid "Selective Consumption Tax (ISC)"
msgstr "Impuesto Selectivo al Consumo (ISC)"
msgctxt "selection:account.tax,tax_kind:"
msgid "INDOTEL Contribution (CDT)"
msgstr "Contribución al Desarrollo de las Telecomunicaciones (CDT)"
msgctxt "selection:account.tax,tax_kind:"
msgid "Statutory Tip"
msgstr "Propina legal"
msgctxt "selection:account.tax,tax_kind:"
msgid "Other Taxes"
msgstr "Otros impuestos"
msgctxt "selection:account.tax.template,tax_kind:"
msgid "ITBIS"
msgstr "ITBIS"
msgctxt "selection:account.tax.template,tax_kind:"
msgid "ITBIS Withholding"
msgstr "Retención de ITBIS"
msgctxt "selection:account.tax.template,tax_kind:"
msgid "ITBIS Acquirer Withholding"
msgstr "Retención de ITBIS por adquirencia"
msgctxt "selection:account.tax.template,tax_kind:"
msgid "ISR Withholding"
msgstr "Retención de ISR"
msgctxt "selection:account.tax.template,tax_kind:"
msgid "Selective Consumption Tax (ISC)"
msgstr "Impuesto Selectivo al Consumo (ISC)"
msgctxt "selection:account.tax.template,tax_kind:"
msgid "INDOTEL Contribution (CDT)"
msgstr "Contribución al Desarrollo de las Telecomunicaciones (CDT)"
msgctxt "selection:account.tax.template,tax_kind:"
msgid "Statutory Tip"
msgstr "Propina legal"
msgctxt "selection:account.tax.template,tax_kind:"
msgid "Other Taxes"
msgstr "Otros impuestos"
-83
View File
@@ -1,83 +0,0 @@
#
msgid ""
msgstr ""
"Project-Id-Version: account_do 8.0\n"
"PO-Revision-Date: 2026-08-10 00:00-0400\n"
"Last-Translator: Fundación Un País Mejor\n"
"Language-Team: Spanish (Latin America)\n"
"Language: es_419\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgctxt "field:account.tax,tax_kind:"
msgid "Tax Kind"
msgstr "Clasificación fiscal"
msgctxt "field:account.tax.template,tax_kind:"
msgid "Tax Kind"
msgstr "Clasificación fiscal"
msgctxt "selection:account.tax,tax_kind:"
msgid "ITBIS"
msgstr "ITBIS"
msgctxt "selection:account.tax,tax_kind:"
msgid "ITBIS Withholding"
msgstr "Retención de ITBIS"
msgctxt "selection:account.tax,tax_kind:"
msgid "ITBIS Acquirer Withholding"
msgstr "Retención de ITBIS por adquirencia"
msgctxt "selection:account.tax,tax_kind:"
msgid "ISR Withholding"
msgstr "Retención de ISR"
msgctxt "selection:account.tax,tax_kind:"
msgid "Selective Consumption Tax (ISC)"
msgstr "Impuesto Selectivo al Consumo (ISC)"
msgctxt "selection:account.tax,tax_kind:"
msgid "INDOTEL Contribution (CDT)"
msgstr "Contribución al Desarrollo de las Telecomunicaciones (CDT)"
msgctxt "selection:account.tax,tax_kind:"
msgid "Statutory Tip"
msgstr "Propina legal"
msgctxt "selection:account.tax,tax_kind:"
msgid "Other Taxes"
msgstr "Otros impuestos"
msgctxt "selection:account.tax.template,tax_kind:"
msgid "ITBIS"
msgstr "ITBIS"
msgctxt "selection:account.tax.template,tax_kind:"
msgid "ITBIS Withholding"
msgstr "Retención de ITBIS"
msgctxt "selection:account.tax.template,tax_kind:"
msgid "ITBIS Acquirer Withholding"
msgstr "Retención de ITBIS por adquirencia"
msgctxt "selection:account.tax.template,tax_kind:"
msgid "ISR Withholding"
msgstr "Retención de ISR"
msgctxt "selection:account.tax.template,tax_kind:"
msgid "Selective Consumption Tax (ISC)"
msgstr "Impuesto Selectivo al Consumo (ISC)"
msgctxt "selection:account.tax.template,tax_kind:"
msgid "INDOTEL Contribution (CDT)"
msgstr "Contribución al Desarrollo de las Telecomunicaciones (CDT)"
msgctxt "selection:account.tax.template,tax_kind:"
msgid "Statutory Tip"
msgstr "Propina legal"
msgctxt "selection:account.tax.template,tax_kind:"
msgid "Other Taxes"
msgstr "Otros impuestos"
+59
View File
@@ -0,0 +1,59 @@
[build-system]
requires = ['hatchling >= 1', 'hatch-tryton']
build-backend = 'hatchling.build'
[project]
name = 'trytond_account_do'
dynamic = ['version', 'dependencies', 'optional-dependencies', 'authors', 'readme']
requires-python = '>=3.10'
maintainers = [
{name = "Fundación Un País Mejor"},
]
description = "Dominican Republic accounting for Tryton"
license = 'GPL-3.0-or-later'
license-files = ['LICENSE', 'COPYRIGHT']
keywords = ["tryton", "accounting", "coa", "dominican-republic"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Plugins",
"Framework :: Tryton",
"Topic :: Office/Business",
"Topic :: Office/Business :: Financial :: Accounting",
"Topic :: Software Development :: Libraries :: Python Modules",
]
[project.entry-points.'trytond.modules']
account_do = 'trytond.modules.account_do'
[project.urls]
homepage = "https://www.unpaismejor.org.do"
documentation = "https://docs.tryton.org/modules-account-do/"
changelog = "https://docs.tryton.org/modules-account-do/releases.html"
forum = "https://www.tryton.org/forum"
issues = "https://bugs.tryton.org/tryton"
repository = "https://code.unpaismejor.org.do/tryton-do/account_do"
funding = "https://www.tryton.org/donate"
[tool.hatch.build]
include = [
'**/tryton.cfg',
'**/*.py',
'**/*.xml',
'view/**/*.xml',
'**/*.fodt',
'icons/**/*.svg',
'tests/**/*.rst',
'tests/**/*.json',
]
exclude = ['doc']
[tool.hatch.build.targets.wheel.sources]
"" = "trytond/modules/account_do"
[tool.hatch.metadata.hooks.tryton]
dependencies = []
copyright = 'COPYRIGHT'
readme = 'README.rst'
[tool.hatch.metadata.hooks.tryton.tryton-optional-dependencies]
test = ['proteus']
-74
View File
@@ -1,74 +0,0 @@
from configparser import ConfigParser
from pathlib import Path
from setuptools import setup
MODULE = 'account_do'
PREFIX = 'trytond.modules'
PACKAGE = f'{PREFIX}.{MODULE}'
def read(path):
return Path(path).read_text(encoding='utf-8')
config = ConfigParser()
config.read('tryton.cfg')
version = config.get('tryton', 'version')
major, minor, _ = version.split('.', 2)
series = f'{major}.{minor}'
next_major = f'{major}.{int(minor) + 1}'
setup(
name='unpaismejor_account_do',
version=version,
description='Dominican Republic accounting for Tryton',
long_description=read('README.rst'),
long_description_content_type='text/x-rst',
author='Fundación Un País Mejor',
maintainer='Fundación Un País Mejor',
url='https://code.unpaismejor.org.do/tryton-do/account_do',
keywords='tryton accounting coa dominican-republic',
license='GPL-3.0-or-later',
python_requires='>=3.8',
packages=[PACKAGE],
package_dir={PACKAGE: '.'},
package_data={
PACKAGE: [
'tryton.cfg',
'*.xml',
'view/*.xml',
'locale/*.po',
'doc/*.rst',
],
},
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Plugins',
'Framework :: Tryton',
'Topic :: Office/Business',
'Topic :: Office/Business :: Financial :: Accounting',
'Topic :: Software Development :: Libraries :: Python Modules',
],
install_requires=[
f'trytond >= {series}, < {next_major}',
f'trytond_account >= {series}, < {next_major}',
f'trytond_account_invoice >= {series}, < {next_major}',
],
extras_require={
'test': [
f'proteus >= {series}, < {next_major}',
f'trytond_account_asset >= {series}, < {next_major}',
f'trytond_account_deposit >= {series}, < {next_major}',
f'trytond_account_stock_continental >= {series}, < {next_major}',
],
},
entry_points={
'trytond.modules': [
f'{MODULE} = {PACKAGE}',
],
},
zip_safe=False,
)
-162
View File
@@ -1,162 +0,0 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from sql import Null, Table
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
TAX_KIND = [
('', ''),
('itbis', 'ITBIS'),
('itbis_withholding', 'ITBIS Withholding'),
('itbis_withholding_acquirer', 'ITBIS Acquirer Withholding'),
('isr_withholding', 'ISR Withholding'),
('isc', 'Selective Consumption Tax (ISC)'),
('cdt', 'INDOTEL Contribution (CDT)'),
('tip', 'Statutory Tip'),
('others', 'Other Taxes'),
]
TAX_TEMPLATE_KINDS = {
'itbis': {
'do_tax_itbis_18_venta', 'do_tax_itbis_16_venta',
'do_tax_itbis_18_compra', 'do_tax_itbis_16_compra',
'do_tax_itbis_exento', 'do_tax_itbis_tasa_cero',
},
'itbis_withholding': {
'do_tax_ret_itbis_30', 'do_tax_ret_itbis_100_inf',
'do_tax_ret_itbis_75_inf', 'do_tax_ret_itbis_75_inf_16',
'do_tax_ret_itbis_100_goods_18',
'do_tax_ret_itbis_100_goods_16', 'do_tax_ret_itbis_rst_18',
'do_tax_ret_itbis_rst_16', 'do_tax_ret_itbis_insurance_100',
'do_tax_ret_itbis_airline_100',
'do_tax_ret_itbis_society_30_suf', 'do_tax_ret_itbis_hotel_100',
'do_tax_ret_itbis_state_100',
},
'isr_withholding': {
'do_tax_ret_isr_hon_5', 'do_tax_ret_isr_serv_10',
'do_tax_ret_isr_serv_15', 'do_tax_ret_isr_div_10',
'do_tax_ret_isr_int_10', 'do_tax_ret_isr_alq_10',
'do_tax_ret_isr_alq_15', 'do_tax_ret_isr_est_15',
'do_tax_ret_isr_est_5', 'do_tax_ret_isr_bovine_1',
'do_tax_ret_isr_exporter_25', 'do_tax_ret_isr_int_pj_1',
'do_tax_ret_isr_premios_25', 'do_tax_ret_isr_premios_10',
'do_tax_ret_isr_premios_15', 'do_tax_ret_isr_betting_15',
'do_tax_ret_isr_tragamonedas_10',
'do_tax_ret_isr_tragamonedas_15',
'do_tax_ret_isr_other_income_10',
'do_tax_ret_isr_other_income_15', 'do_tax_ret_isr_ext_27',
'do_tax_ret_isr_ext_15', 'do_tax_ret_isr_ext_10',
},
'isc': {
'do_tax_isc_bebidas_alc', 'do_tax_isc_tabaco',
'do_tax_isc_telecom', 'do_tax_isc_combustibles_16',
'do_tax_isc_avtur_65', 'do_tax_isc_fuel_rd2_gallon',
'do_tax_ret_isc_insurance_100', 'do_tax_isc_vehiculos',
},
'cdt': {'do_tax_cdt_indotel'},
'tip': {'do_tax_propina_10'},
'others': {
'do_tax_cheques_015', 'do_tax_cheques_020',
'do_tax_activos_1', 'do_tax_iti_3',
},
}
def _set_template_kinds(table):
cursor = Transaction().connection.cursor()
model_data = Table('ir_model_data')
kind_by_identifier = {
identifier + suffix: tax_kind
for tax_kind, identifiers in TAX_TEMPLATE_KINDS.items()
for identifier in identifiers
for suffix in ('', '_en', '_es_419')
}
cursor.execute(*model_data.select(
model_data.db_id, model_data.fs_id,
where=model_data.module == 'account_do'))
for template_id, identifier in cursor.fetchall():
tax_kind = kind_by_identifier.get(identifier)
if not tax_kind:
continue
cursor.execute(*table.update(
[table.tax_kind], [tax_kind],
where=(table.id == template_id)
& ((table.tax_kind == Null) | (table.tax_kind == ''))))
def _kind_from_values(values):
pool = Pool()
TaxGroup = pool.get('account.tax.group')
group = values.get('group')
if isinstance(group, int):
group = TaxGroup(group)
code = getattr(group, 'code', None)
if code == 'ITBIS':
amount = values.get('rate') or values.get('amount') or 0
return 'itbis_withholding' if amount < 0 else 'itbis'
if code in {'ISR', 'ISREXT'}:
return 'isr_withholding'
if code == 'ISC':
return 'isc'
if code == 'CDT':
return 'cdt'
if code == 'OTROS':
name = (values.get('name') or '').casefold()
if 'propina' in name or 'tip' in name:
return 'tip'
if 'vehículo' in name or 'vehicle' in name:
return 'isc'
return 'others'
return ''
class TaxTemplate(metaclass=PoolMeta):
__name__ = 'account.tax.template'
tax_kind = fields.Selection(TAX_KIND, 'Tax Kind', sort=False)
@classmethod
def __register__(cls, module_name):
super().__register__(module_name)
_set_template_kinds(cls.__table__())
@classmethod
def create(cls, vlist):
vlist = [values.copy() for values in vlist]
for values in vlist:
values.setdefault('tax_kind', _kind_from_values(values))
return super().create(vlist)
def _get_tax_value(self, tax=None):
values = super()._get_tax_value(tax=tax)
if not tax or tax.tax_kind != self.tax_kind:
values['tax_kind'] = self.tax_kind
return values
class Tax(metaclass=PoolMeta):
__name__ = 'account.tax'
tax_kind = fields.Selection(TAX_KIND, 'Tax Kind', sort=False)
@classmethod
def __register__(cls, module_name):
super().__register__(module_name)
table = cls.__table__()
template = Table('account_tax_template')
cursor = Transaction().connection.cursor()
cursor.execute(*table.join(
template, condition=template.id == table.template).select(
table.id, template.tax_kind,
where=(table.template != Null)
& (template.tax_kind != Null)
& (template.tax_kind != '')
& ((table.tax_kind == Null) | (table.tax_kind == ''))))
for tax_id, tax_kind in cursor.fetchall():
cursor.execute(*table.update(
[table.tax_kind], [tax_kind],
where=table.id == tax_id))
-27
View File
@@ -1,27 +0,0 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="tax_view_form">
<field name="model">account.tax</field>
<field name="inherit" ref="account.tax_view_form"/>
<field name="name">tax_form</field>
</record>
<record model="ir.ui.view" id="tax_view_list">
<field name="model">account.tax</field>
<field name="inherit" ref="account.tax_view_list"/>
<field name="name">tax_list</field>
</record>
<record model="ir.ui.view" id="tax_template_view_form">
<field name="model">account.tax.template</field>
<field name="inherit" ref="account.tax_template_view_form"/>
<field name="name">tax_template_form</field>
</record>
<record model="ir.ui.view" id="tax_template_view_list">
<field name="model">account.tax.template</field>
<field name="inherit" ref="account.tax_template_view_list"/>
<field name="name">tax_template_list</field>
</record>
</data>
</tryton>
+1 -2
View File
@@ -11,8 +11,7 @@ Imports::
Install account_do:: Install account_do::
>>> config = activate_modules('account_do') >>> config = activate_modules('account_do', create_company)
>>> _ = create_company()
Get the test company:: Get the test company::
+15 -136
View File
@@ -4,15 +4,12 @@ from collections import Counter
from configparser import ConfigParser from configparser import ConfigParser
from decimal import Decimal from decimal import Decimal
from pathlib import Path from pathlib import Path
from types import SimpleNamespace
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
from trytond.pool import Pool from trytond.pool import Pool
from trytond.tests.test_tryton import ModuleTestCase, with_transaction from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.transaction import Transaction from trytond.transaction import Transaction
from trytond.modules.account_do.invoice import round_taxes_independently
MODULE_DIR = Path(__file__).resolve().parent.parent MODULE_DIR = Path(__file__).resolve().parent.parent
TAX_CODE_LINE_AUDIT = { TAX_CODE_LINE_AUDIT = {
@@ -128,56 +125,6 @@ class AccountDoTestCase(ModuleTestCase):
'account_stock_continental', 'account_stock_continental',
] ]
@with_transaction()
def test_invoice_taxes_round_independently(self):
"""A rounding residual must not change ITBIS, ISC or CDT."""
pool = Pool()
Currency = pool.get('currency.currency')
currency, = Currency.create([{
'name': 'Peso dominicano',
'code': 'DOP',
'symbol': 'RD$',
'digits': 2,
'rounding': Decimal('0.01'),
}])
taxes = {
'itbis': SimpleNamespace(amount=Decimal('91.8036')),
'isc': SimpleNamespace(amount=Decimal('51.0020')),
'cdt': SimpleNamespace(amount=Decimal('9.9000')),
}
round_taxes_independently(currency, taxes)
self.assertEqual(taxes['itbis'].amount, Decimal('91.80'))
self.assertEqual(taxes['isc'].amount, Decimal('51.00'))
self.assertEqual(taxes['cdt'].amount, Decimal('9.90'))
self.assertEqual(
sum(tax.amount for tax in taxes.values()), Decimal('152.70'))
@with_transaction()
def test_invoice_credit_note_taxes_round_independently(self):
"""The same policy applies symmetrically to credit notes."""
pool = Pool()
Currency = pool.get('currency.currency')
currency, = Currency.create([{
'name': 'Peso dominicano',
'code': 'DOP',
'symbol': 'RD$',
'digits': 2,
'rounding': Decimal('0.01'),
}])
taxes = {
'itbis': SimpleNamespace(amount=Decimal('-91.8036')),
'isc': SimpleNamespace(amount=Decimal('-51.0020')),
'cdt': SimpleNamespace(amount=Decimal('-9.9000')),
}
round_taxes_independently(currency, taxes)
self.assertEqual(taxes['itbis'].amount, Decimal('-91.80'))
self.assertEqual(taxes['isc'].amount, Decimal('-51.00'))
self.assertEqual(taxes['cdt'].amount, Decimal('-9.90'))
@with_transaction() @with_transaction()
def test_chart_template_is_loaded_by_default(self): def test_chart_template_is_loaded_by_default(self):
'Test English Dominican chart is loaded in an English database' 'Test English Dominican chart is loaded in an English database'
@@ -282,7 +229,6 @@ class AccountDoTestCase(ModuleTestCase):
('company', '=', company.id), ('company', '=', company.id),
('description', '=', 'ITBIS 18% Ventas'), ('description', '=', 'ITBIS 18% Ventas'),
('rate', '=', Decimal('0.18')), ('rate', '=', Decimal('0.18')),
('tax_kind', '=', 'itbis'),
], limit=1)) ], limit=1))
self.assertTrue(TaxCode.search([ self.assertTrue(TaxCode.search([
('company', '=', company.id), ('company', '=', company.id),
@@ -416,14 +362,12 @@ class AccountDoTestCase(ModuleTestCase):
('company', '=', company.id), ('company', '=', company.id),
('description', '=', 'ITBIS 18% Sales'), ('description', '=', 'ITBIS 18% Sales'),
('rate', '=', Decimal('0.18')), ('rate', '=', Decimal('0.18')),
('tax_kind', '=', 'itbis'),
], limit=1)) ], limit=1))
self.assertTrue(Tax.search([ self.assertTrue(Tax.search([
('company', '=', company.id), ('company', '=', company.id),
('description', '=', ('description', '=',
'ITBIS Withholding Large Taxpayer 30%'), 'ITBIS Withholding Large Taxpayer 30%'),
('rate', '=', Decimal('-0.054')), ('rate', '=', Decimal('-0.054')),
('tax_kind', '=', 'itbis_withholding'),
], limit=1)) ], limit=1))
self.assertTrue(Tax.search([ self.assertTrue(Tax.search([
('company', '=', company.id), ('company', '=', company.id),
@@ -621,10 +565,10 @@ class AccountDoUnitTestCase(unittest.TestCase):
if line.strip() if line.strip()
] ]
self.assertEqual(config.get('tryton', 'version'), '7.0.0') self.assertEqual(config.get('tryton', 'version'), '8.1.0')
self.assertEqual( self.assertEqual(
set(lines('tryton', 'depends')), set(lines('tryton', 'depends')),
{'account', 'account_invoice', 'ir'}) {'account', 'ir'})
self.assertEqual( self.assertEqual(
lines('tryton', 'xml'), lines('tryton', 'xml'),
[ [
@@ -636,40 +580,26 @@ class AccountDoUnitTestCase(unittest.TestCase):
'tax_do_es_419.xml', 'tax_do_es_419.xml',
'tax_code_do_es_419.xml', 'tax_code_do_es_419.xml',
'tax_rule_do_es_419.xml', 'tax_rule_do_es_419.xml',
'tax.xml',
])
self.assertEqual(
lines('register', 'model'),
[
'ir.ModelData',
'invoice.Invoice',
'invoice.InvoiceLine',
'tax.TaxTemplate',
'tax.Tax',
]) ])
self.assertEqual(lines('register', 'model'), ['ir.ModelData'])
self.assertEqual(lines('register', 'wizard'), ['account.CreateChart']) self.assertEqual(lines('register', 'wizard'), ['account.CreateChart'])
for filename in lines('tryton', 'xml'): for filename in lines('tryton', 'xml'):
self.assertTrue((MODULE_DIR / filename).is_file()) self.assertTrue((MODULE_DIR / filename).is_file())
self.assertTrue((MODULE_DIR / 'account.py').is_file()) self.assertTrue((MODULE_DIR / 'account.py').is_file())
self.assertTrue((MODULE_DIR / 'tax.py').is_file()) pyproject = (MODULE_DIR / 'pyproject.toml').read_text(encoding='utf-8')
setup_py = (MODULE_DIR / 'setup.py').read_text(encoding='utf-8') self.assertIn("name = 'trytond_account_do'", pyproject)
self.assertIn("name='unpaismejor_account_do'", setup_py)
self.assertIn( self.assertIn(
"f'{MODULE} = {PACKAGE}'", setup_py) "account_do = 'trytond.modules.account_do'", pyproject)
self.assertIn( self.assertIn(
"maintainer='Fundación Un País Mejor'", setup_py) '{name = "Fundación Un País Mejor"}', pyproject)
self.assertIn( self.assertIn(
"url='https://code.unpaismejor.org.do/tryton-do/account_do'", 'homepage = "https://www.unpaismejor.org.do"', pyproject)
setup_py)
self.assertIn( self.assertIn(
"python_requires='>=3.8'", setup_py) ('repository = "https://code.unpaismejor.org.do/'
for package_file in [ 'tryton-do/account_do"'), pyproject)
"'*.xml'", self.assertIn("exclude = ['doc']", pyproject)
"'view/*.xml'", for package_file in ['**/*.xml', 'tests/**/*.rst']:
"'locale/*.po'", self.assertIn(package_file, pyproject)
"'doc/*.rst'",
]:
self.assertIn(package_file, setup_py)
for filename in [ for filename in [
'__init__.py', '__init__.py',
'account.py', 'account.py',
@@ -710,7 +640,7 @@ class AccountDoUnitTestCase(unittest.TestCase):
def test_xml_record_inventory_is_explicit(self): def test_xml_record_inventory_is_explicit(self):
expected = { expected = {
'account_chart_do_en.xml': { 'account_chart_do_en.xml': {
'account.account.type.template': 40, 'account.account.type.template': 36,
'account.account.template': 287, 'account.account.template': 287,
}, },
'tax_do_en.xml': { 'tax_do_en.xml': {
@@ -726,7 +656,7 @@ class AccountDoUnitTestCase(unittest.TestCase):
'account.tax.rule.line.template': 34, 'account.tax.rule.line.template': 34,
}, },
'account_chart_do_es_419.xml': { 'account_chart_do_es_419.xml': {
'account.account.type.template': 40, 'account.account.type.template': 36,
'account.account.template': 287, 'account.account.template': 287,
}, },
'tax_do_es_419.xml': { 'tax_do_es_419.xml': {
@@ -812,33 +742,6 @@ class AccountDoUnitTestCase(unittest.TestCase):
with self.subTest(marker=marker): with self.subTest(marker=marker):
self.assertNotIn(marker, text) self.assertNotIn(marker, text)
def test_spanish_translations_cover_module_strings(self):
translations = {
'Tax Kind': 'Clasificación fiscal',
'ITBIS': 'ITBIS',
'ITBIS Withholding': 'Retención de ITBIS',
'ITBIS Acquirer Withholding': (
'Retención de ITBIS por adquirencia'),
'ISR Withholding': 'Retención de ISR',
'Selective Consumption Tax (ISC)': (
'Impuesto Selectivo al Consumo (ISC)'),
'INDOTEL Contribution (CDT)': (
'Contribución al Desarrollo de las Telecomunicaciones (CDT)'),
'Statutory Tip': 'Propina legal',
'Other Taxes': 'Otros impuestos',
}
for language in ['es', 'es_419']:
text = (MODULE_DIR / 'locale' / f'{language}.po').read_text(
encoding='utf-8')
with self.subTest(language=language):
for source, target in translations.items():
self.assertIn(f'msgid "{source}"', text)
self.assertIn(f'msgstr "{target}"', text)
self.assertIn(
'msgctxt "field:account.tax,tax_kind:"', text)
self.assertIn(
'msgctxt "field:account.tax.template,tax_kind:"', text)
def test_spanish_chart_has_complete_parallel_identifiers(self): def test_spanish_chart_has_complete_parallel_identifiers(self):
pairs = [ pairs = [
('account_chart_do_en.xml', 'account_chart_do_es_419.xml'), ('account_chart_do_en.xml', 'account_chart_do_es_419.xml'),
@@ -970,30 +873,6 @@ class AccountDoUnitTestCase(unittest.TestCase):
types['do_type_retained_earnings']['statement'], 'balance') types['do_type_retained_earnings']['statement'], 'balance')
self.assertEqual(types['do_type_revenue']['statement'], 'income') self.assertEqual(types['do_type_revenue']['statement'], 'income')
self.assertEqual(types['do_type_expense']['statement'], 'income') self.assertEqual(types['do_type_expense']['statement'], 'income')
self.assertEqual(
types['do_type_profit_loss']['parent'], 'do_type_equity')
self.assertEqual(
types['do_type_operating_section']['parent'],
'do_type_profit_loss')
self.assertEqual(
types['do_type_revenue']['parent'],
'do_type_operating_section')
self.assertEqual(
types['do_type_expenses']['parent'],
'do_type_operating_section')
self.assertEqual(
types['do_type_cogs']['parent'], 'do_type_expenses')
self.assertEqual(
types['do_type_expense']['parent'], 'do_type_expenses')
self.assertEqual(
types['do_type_non_operating_section']['parent'],
'do_type_profit_loss')
for account_type in [
'do_type_other_income', 'do_type_financial_expense',
'do_type_income_tax']:
self.assertEqual(
types[account_type]['parent'],
'do_type_non_operating_section')
self.assertEqual( self.assertEqual(
accounts['do_account_110201']['type'], 'do_type_receivable') accounts['do_account_110201']['type'], 'do_type_receivable')
self.assertEqual( self.assertEqual(
+1 -1
View File
@@ -1,5 +1,5 @@
[tox] [tox]
envlist = {py38,py39,py310,py311}-{sqlite,postgresql} envlist = {py310,py311,py312,py313,py314}-{sqlite,postgresql}
[testenv] [testenv]
changedir = {env_site_packages_dir} changedir = {env_site_packages_dir}
+1 -7
View File
@@ -1,8 +1,7 @@
[tryton] [tryton]
version=7.0.0 version=8.1.0
depends: depends:
account account
account_invoice
ir ir
xml: xml:
account_chart_do_en.xml account_chart_do_en.xml
@@ -13,14 +12,9 @@ xml:
tax_do_es_419.xml tax_do_es_419.xml
tax_code_do_es_419.xml tax_code_do_es_419.xml
tax_rule_do_es_419.xml tax_rule_do_es_419.xml
tax.xml
[register] [register]
model: model:
ir.ModelData ir.ModelData
invoice.Invoice
invoice.InvoiceLine
tax.TaxTemplate
tax.Tax
wizard: wizard:
account.CreateChart account.CreateChart
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="//page[@id='general']/field[@name='group']" position="after">
<label name="tax_kind"/>
<field name="tax_kind"/>
</xpath>
</data>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/tree/field[@name='group']" position="after">
<field name="tax_kind" optional="1"/>
</xpath>
</data>
-9
View File
@@ -1,9 +0,0 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="//page[@id='general']/field[@name='group']" position="after">
<label name="tax_kind"/>
<field name="tax_kind"/>
</xpath>
</data>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/tree/field[@name='group']" position="after">
<field name="tax_kind" optional="1"/>
</xpath>
</data>