Migrate account invoice DO to Tryton 8.0
Publish Package / publish (push) Successful in 1m57s

This commit is contained in:
José Arturo García
2026-08-14 20:54:23 -04:00
parent 4e70fd9733
commit e2340bd5bf
20 changed files with 420 additions and 514 deletions
+15 -6
View File
@@ -3,16 +3,24 @@ name: Publish Package
on:
push:
branches:
- "**"
- "8.0"
workflow_dispatch:
permissions:
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: https://gitea.com/actions/checkout@v4
with:
token: ${{ secrets.GITEA_TOKEN }}
github-server-url: "http://unpaismejor-gitea-app:3000"
ref: "8.0"
fetch-depth: 0
- name: Detect package branch
run: |
@@ -21,7 +29,9 @@ jobs:
import re
branch = (
os.environ.get('GITHUB_REF_NAME')
os.environ.get('GITEA_REF_NAME')
or os.environ.get('GITEA_HEAD_REF')
or os.environ.get('GITHUB_REF_NAME')
or os.environ.get('GITHUB_HEAD_REF')
or '')
publish = bool(re.fullmatch(r'\d+\.\d+', branch))
@@ -75,7 +85,7 @@ jobs:
OWNER = 'tryton-do'
PACKAGE_TYPE = 'pypi'
API_ROOT = 'https://gitea.joseagrc.com/api/v1'
API_ROOT = 'https://code.unpaismejor.org.do/api/v1'
def normalize(name):
return re.sub(r'[-_.]+', '-', name).lower()
@@ -85,7 +95,6 @@ jobs:
cfg_match = re.search(r'(?m)^version=(.+)$', cfg_text)
if not cfg_match:
raise SystemExit('tryton.cfg has no version entry')
series = os.environ['PACKAGE_SERIES']
major, minor = map(int, series.split('.'))
@@ -188,5 +197,5 @@ jobs:
TWINE_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: |
python -m twine upload \
--repository-url https://gitea.joseagrc.com/api/packages/tryton-do/pypi \
--repository-url https://code.unpaismejor.org.do/api/packages/tryton-do/pypi \
dist/*
+11
View File
@@ -0,0 +1,11 @@
Version 8.0.0 - 2026-08-14
--------------------------
* Align invoice, tax and payment helpers with Tryton 8.0 core APIs.
* Derive informational Dominican tax amounts from the core tax engine.
* Preserve signed paid amounts for invoices and credit notes.
* Compute the final due date from all payment-term maturities.
* Remove unsafe global address defaults and filesystem-writing cron jobs.
* Preserve core and third-party invoice ordering criteria.
* Declare Fundación Un País Mejor as maintainer and package publisher.
* Add Gitea Actions publishing for the 8.0 series.
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (C) 2022 Solutema
Copyright (C) 2022 Fundación Un País Mejor
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
+17 -6
View File
@@ -6,13 +6,24 @@ Dominican Republic invoice helpers for Tryton.
This module extends ``account_invoice`` with operational fields commonly used
in Dominican Republic installations:
* salesman on invoices;
* due date, elapsed days, overdue days, and paid amount helpers;
* supplier reimbursement party;
* informational invoice line tax amounts by Dominican tax kind;
* optional cron methods to process posted customer and supplier invoices.
The tax amount columns are display helpers. They are computed from the line
amount and the selected taxes to make invoice entry easier to review; accounting
tax posting remains handled by Tryton's core tax engine.
The tax amount columns are display helpers. They use the amounts computed by
Tryton's core tax engine and classify them by the stable Dominican tax groups.
Accounting tax posting remains handled exclusively by the core engine.
Installing
**********
Install the package and activate the module in the database::
pip install unpaismejor_account_invoice_do
trytond-admin -d <database> -u account_invoice_do
Publishing
**********
The Gitea Actions workflow builds and publishes the package from the ``8.0``
series branch. Configure ``REGISTRY_USER`` and ``REGISTRY_PASSWORD`` as
repository secrets.
-4
View File
@@ -1,9 +1,5 @@
from . import address
from . import cron
from . import invoice
__all__ = [
'address',
'cron',
'invoice',
]
-11
View File
@@ -1,11 +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 trytond.pool import PoolMeta
class Address(metaclass=PoolMeta):
__name__ = 'party.address'
@staticmethod
def default_invoice():
return True
-91
View File
@@ -1,91 +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.
import logging
import os
from trytond.pool import PoolMeta
logger = logging.getLogger(__name__)
def _build_error_message(inv, error):
return (
f"ID: {inv.id}"
f" | Numero: {getattr(inv, 'number', 'N/D')}"
f" | NCF: {getattr(inv, 'ncf_number', 'N/D')}"
f" | Fecha: {getattr(inv, 'invoice_date', 'N/D')}"
f" | Descripcion: {getattr(inv, 'description', 'N/D')}"
f" | Error: {error}"
)
def _write_error_log(path, header, error_lines):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'a', encoding='utf-8') as f:
f.write(f"===== {header} =====\n")
for line in error_lines:
f.write(line + '\n')
class Invoice(metaclass=PoolMeta):
__name__ = 'account.invoice'
@classmethod
def _cron_process_invoices(cls, invoice_type, error_log_filename):
invoices = cls.search([
('type', '=', invoice_type),
('state', '=', 'posted'),
])
logger.info(
"cron_process_posted_%s_invoices: found %d invoices",
'customer' if invoice_type == 'out' else 'supplier',
len(invoices),
)
errors = []
total = len(invoices)
for idx, inv in enumerate(invoices, start=1):
try:
logger.info(
"Processing invoice %d/%d id=%s number=%s",
idx, total, inv.id, getattr(inv, 'number', 'N/D'),
)
cls.process([inv])
except Exception as e:
msg = _build_error_message(inv, e)
logger.error(msg, exc_info=True)
errors.append(msg)
if errors:
log_dir = os.path.expanduser('~/legna/tryton')
_write_error_log(
os.path.join(log_dir, error_log_filename),
f"ERRORES CRON {'CUSTOMER' if invoice_type == 'out' else 'SUPPLIER'} INVOICES",
errors,
)
logger.warning(
"cron finished with %d error(s)", len(errors))
else:
logger.info("cron finished successfully")
@classmethod
def cron_process_posted_customer_invoices(cls):
cls._cron_process_invoices('out', 'invoice_out_cron_errors.log')
@classmethod
def cron_process_posted_supplier_invoices(cls):
cls._cron_process_invoices('in', 'invoice_in_cron_errors.log')
class Cron(metaclass=PoolMeta):
__name__ = 'ir.cron'
@classmethod
def __setup__(cls):
super().__setup__()
cls.method.selection.extend([
('account.invoice|cron_process_posted_customer_invoices',
"Process Posted Customer Invoices"),
('account.invoice|cron_process_posted_supplier_invoices',
"Process Posted Supplier Invoices"),
])
+11
View File
@@ -1,3 +1,14 @@
******
Design
******
The module does not reimplement tax formulas. Invoice line helper amounts are
obtained from ``account.invoice.line._get_taxes`` and classified with the tax
group codes supplied by ``account_do``. This preserves Tryton's handling of
fixed, included, compound, dated and rounded taxes.
All informational tax fields and the line total share one multi-field getter,
so the core tax result is classified only once per invoice line read.
The displayed due date is the last maturity date. For posted invoices it is
read from the payable move lines. For draft invoices it is computed by the
complete payment term using the invoice amount and currency.
+6 -5
View File
@@ -6,13 +6,14 @@ Extensiones dominicanas para facturación.
Cobertura funcional
-------------------
* Vendedor en factura.
* Fecha de vencimiento calculada desde término de pago.
* Fecha final de vencimiento calculada desde el término de pago completo.
* Días transcurridos, días vencidos e importe pagado.
* Totales por tipo de impuesto en líneas de factura.
* Procesamiento cron de facturas publicadas con bitácora de errores.
* Totales informativos por grupo fiscal dominicano en líneas de factura.
* Total de línea basado en los importes producidos por el motor fiscal de
Tryton.
Pruebas
-------
Las pruebas cubren vencimiento, importe pagado, cálculo por tipo de impuesto, subtotal y formato de errores del cron.
Las pruebas cubren vencimientos múltiples, notas de crédito, clasificación
fiscal, subtotales y contabilización real de ITBIS en ventas y compras.
+8
View File
@@ -1,3 +1,11 @@
*****
Usage
*****
Select taxes provided by ``account_do`` on an invoice line. The informational
columns show ITBIS, ITBIS withholding, ISC, CDT, ISR withholding and legal tip
amounts. These values are read-only and do not alter accounting entries.
The invoice payment page displays the final due date, elapsed days, overdue
days and the signed amount paid. Credit notes therefore retain their negative
sign.
+155 -161
View File
@@ -1,31 +1,22 @@
# 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 dateutil.relativedelta import relativedelta
from decimal import Decimal
from datetime import date
from trytond.model import fields
from trytond.pool import PoolMeta
from trytond.modules.product.product import TYPES
from trytond.pyson import Eval
from trytond.modules.currency.fields import Monetary
_ZERO = Decimal('0.00')
from trytond.model import fields
from trytond.modules.currency.fields import Monetary
from trytond.modules.product.product import TYPES
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.transaction import Transaction
_ZERO = Decimal(0)
class Invoice(metaclass=PoolMeta):
__name__ = 'account.invoice'
employee = fields.Many2One('company.employee', 'Salesman',
domain=[
('company', '=', Eval('company', -1)),
],
states={
'readonly': Eval('state') != 'draft',
},
depends=['state', 'company'])
due_date = fields.Function(fields.Date('Due Date'),
'get_remainder_time')
'get_due_date')
amount_paid = fields.Function(Monetary(
"Amount Paid", currency='currency', digits='currency'),
@@ -36,17 +27,6 @@ class Invoice(metaclass=PoolMeta):
days_overdue = fields.Function(fields.Integer('Days Overdue'),
'get_days_overdue')
party_reimbursement = fields.Many2One(
'party.party', 'Party Reimbursement',
states={
'invisible': Eval('type') != 'in',
'readonly': Eval('state') != 'draft',
},
context={
'company': Eval('company', -1),
},
depends=['company'])
@classmethod
def search_rec_name(cls, name, clause):
domain = super().search_rec_name(name, clause)
@@ -55,31 +35,49 @@ class Invoice(metaclass=PoolMeta):
]
def get_days_elapsed(self, name=None):
if self.state == 'paid' or not self.due_date or not self.invoice_date:
return None
days = (date.today() - self.invoice_date).days
return days if days > 0 else None
return self._positive_age(self.invoice_date)
def get_days_overdue(self, name=None):
if self.state == 'paid' or not self.due_date or not self.invoice_date:
return self._positive_age(self.due_date)
def _positive_age(self, reference_date):
if self.state == 'paid' or not reference_date:
return None
days = (date.today() - self.due_date).days
days = (self._today() - reference_date).days
return days if days > 0 else None
def get_remainder_time(self, name):
term = self.payment_term
if term and term.lines:
for line in term.lines:
if (not line.relativedeltas
or not self.invoice_date):
break
delta = line.relativedeltas[0]
return self.invoice_date + relativedelta(
months=delta.months,
weeks=delta.weeks,
days=delta.days)
def _today(self):
Date = Pool().get('ir.date')
company = self.company.id if self.company else None
with Transaction().set_context(company=company):
return Date.today()
def _compute_due_date(self):
maturity_date = max((
line.maturity_date for line in (self.lines_to_pay or [])
if line.maturity_date), default=None)
if maturity_date:
return maturity_date
payment_date = self.payment_term_date or self.invoice_date
if (self.payment_term and payment_date and self.currency
and self.total_amount is not None):
terms = self.payment_term.compute(
self.total_amount, self.currency, payment_date)
if terms:
return max(date for date, _ in terms)
return None
def get_due_date(self, name=None):
return self._compute_due_date()
@fields.depends(
'payment_term', 'payment_term_date', 'invoice_date', 'currency',
'total_amount', 'lines_to_pay',
methods=['_compute_due_date'])
def on_change_with_due_date(self, name=None):
return self._compute_due_date()
@classmethod
def get_amount_paid(cls, invoices, name):
res = {i.id: _ZERO for i in invoices}
@@ -88,7 +86,7 @@ class Invoice(metaclass=PoolMeta):
continue
total = invoice.total_amount or _ZERO
to_pay = invoice.amount_to_pay or _ZERO
paid = abs(total - to_pay)
paid = total - to_pay
if invoice.currency:
paid = invoice.currency.round(paid)
res[invoice.id] = paid
@@ -97,10 +95,14 @@ class Invoice(metaclass=PoolMeta):
@classmethod
def __setup__(cls):
super().__setup__()
# Keep the ordering criteria installed by account_invoice and other
# modules. Replacing the whole list would silently discard extension
# hooks such as the core invoice-number ordering.
cls._order = [
('invoice_date', 'DESC'),
('id', 'DESC'),
]
*(order for order in cls._order
if order[0] != 'invoice_date'),
]
_LINE_DEPENDS = [
@@ -116,6 +118,28 @@ _LINE_TOTAL_STATES = {
'invisible': ~Eval('type').in_(['line', 'subtotal']),
}
_TAX_FIELD_KINDS = {
'itbis_invoiced_line': 'itbis',
'itbis_withholding_invoiced_line': 'itbis_withholding',
'isc_invoiced_line': 'isc',
'cdt_invoiced_line': 'cdt',
'isr_withholding_invoiced_line': 'isr_withholding',
'tip_invoiced_line': 'tip',
}
_TAX_GROUP_KINDS = {
'ISC': 'isc',
'CDT': 'cdt',
'ISR': 'isr_withholding',
'ISREXT': 'isr_withholding',
}
def _amount_field(label, states=_LINE_STATES):
return fields.Function(Monetary(
label, currency='currency', digits='currency', states=states),
'get_amounts')
class InvoiceLine(metaclass=PoolMeta):
'Invoice Line'
@@ -126,47 +150,13 @@ class InvoiceLine(metaclass=PoolMeta):
'on_change_with_product_type'
)
itbis_invoiced_line = fields.Function(Monetary(
"ITBIS Invoiced", currency='currency', digits='currency',
states=_LINE_STATES),
'get_itbis_invoiced_line'
)
itbis_withholding_invoiced_line = fields.Function(Monetary(
"ITBIS Withholding", currency='currency', digits='currency',
states=_LINE_STATES),
'get_itbis_withholding_invoiced_line'
)
isc_invoiced_line = fields.Function(Monetary(
"ISC Invoiced", currency='currency', digits='currency',
states=_LINE_STATES),
'get_isc_invoiced_line'
)
cdt_invoiced_line = fields.Function(Monetary(
"CDT Invoiced", currency='currency', digits='currency',
states=_LINE_STATES),
'get_cdt_invoiced_line'
)
isr_withholding_invoiced_line = fields.Function(Monetary(
"ISR Withholding", currency='currency', digits='currency',
states=_LINE_STATES),
'get_isr_withholding_invoiced_line'
)
tip_invoiced_line = fields.Function(Monetary(
"Tip Invoiced", currency='currency', digits='currency',
states=_LINE_STATES),
'get_tip_invoiced_line'
)
line_total = fields.Function(Monetary(
"Total", currency='currency', digits='currency',
states=_LINE_TOTAL_STATES),
'get_line_total'
)
itbis_invoiced_line = _amount_field("ITBIS Invoiced")
itbis_withholding_invoiced_line = _amount_field("ITBIS Withholding")
isc_invoiced_line = _amount_field("ISC Invoiced")
cdt_invoiced_line = _amount_field("CDT Invoiced")
isr_withholding_invoiced_line = _amount_field("ISR Withholding")
tip_invoiced_line = _amount_field("Tip Invoiced")
line_total = _amount_field("Total", _LINE_TOTAL_STATES)
@fields.depends('type', 'product')
def on_change_with_product_type(self, name=None):
@@ -177,7 +167,7 @@ class InvoiceLine(metaclass=PoolMeta):
@classmethod
def _get_line_currency(cls, line):
invoice = getattr(line, 'invoice', None)
if invoice and hasattr(invoice, 'currency') and invoice.currency:
if invoice and getattr(invoice, 'currency', None):
return invoice.currency
return getattr(line, 'currency', None)
@@ -191,105 +181,109 @@ class InvoiceLine(metaclass=PoolMeta):
@classmethod
def _get_base_line_amount(cls, line):
if line.type != 'line':
return Decimal('0.0')
amount = None
if hasattr(line, 'on_change_with_amount'):
amount = line.on_change_with_amount()
return _ZERO
compute = getattr(line, 'on_change_with_amount', None)
amount = compute() if compute else None
if amount is None:
amount = getattr(line, 'amount', None)
return amount or Decimal('0.0')
return amount or _ZERO
@classmethod
def _compute_tax_kind_amount(cls, line, tax_kind):
if line.type != 'line':
return Decimal('0.0')
taxes = getattr(line, 'taxes', None)
if not taxes:
return Decimal('0.0')
base = cls._get_base_line_amount(line)
total = Decimal('0.0')
for tax in taxes:
if getattr(tax, 'tax_kind', None) == tax_kind:
rate = getattr(tax, 'rate', None) or Decimal('0.0')
total += base * Decimal(str(rate))
return cls._round_amount(line, abs(total))
def _is_withholding_tax(cls, tax):
if not tax:
return False
if getattr(tax, 'type', None) == 'percentage':
return (tax.rate or _ZERO) < 0
if getattr(tax, 'type', None) == 'fixed':
return (tax.amount or _ZERO) < 0
return False
@classmethod
def _compute_line_total_amount(cls, line):
def _tax_kind(cls, tax_line):
tax = getattr(tax_line, 'tax', None)
if not tax:
return None
group_code = getattr(getattr(tax, 'group', None), 'code', None)
if group_code == 'ITBIS':
return ('itbis_withholding'
if cls._is_withholding_tax(tax) else 'itbis')
if group_code in _TAX_GROUP_KINDS:
return _TAX_GROUP_KINDS[group_code]
if group_code == 'OTROS' and getattr(tax, 'template', None):
name = (tax.template.name or '').casefold()
if 'propina' in name or 'tip' in name:
return 'tip'
@classmethod
def _compute_amounts(cls, line):
amounts = dict.fromkeys(_TAX_FIELD_KINDS.values(), _ZERO)
if line.type == 'line':
base = cls._get_base_line_amount(line)
tax_total = Decimal('0.0')
for tax in (getattr(line, 'taxes', None) or []):
rate = getattr(tax, 'rate', None) or Decimal('0.0')
tax_total += base * Decimal(str(rate))
return cls._round_amount(line, base + tax_total)
if line.type == 'subtotal':
subtotal = Decimal('0.0')
tax_total = _ZERO
if getattr(line, 'taxes', None):
for tax_line in line._get_taxes().values():
amount = tax_line.amount or _ZERO
tax_total += amount
kind = cls._tax_kind(tax_line)
if kind:
amounts[kind] += amount
line_total = base + tax_total
elif line.type == 'subtotal':
subtotal = _ZERO
invoice = getattr(line, 'invoice', None)
if not invoice or not hasattr(invoice, 'lines'):
return subtotal
for line2 in invoice.lines:
if line2.type == 'line':
subtotal += cls._compute_line_total_amount(line2)
elif line2.type == 'subtotal':
if line == line2:
for invoice_line in getattr(invoice, 'lines', []):
if invoice_line.type == 'line':
subtotal += cls._compute_amounts(invoice_line)['line_total']
elif invoice_line.type == 'subtotal':
if line == invoice_line:
break
subtotal = Decimal('0.0')
return cls._round_amount(line, subtotal)
return Decimal('0.0')
subtotal = _ZERO
line_total = subtotal
else:
line_total = _ZERO
amounts = {
kind: cls._round_amount(line, abs(amount))
for kind, amount in amounts.items()}
amounts['line_total'] = cls._round_amount(line, line_total)
return amounts
@classmethod
def get_itbis_invoiced_line(cls, lines, name):
return {l.id: cls._compute_tax_kind_amount(l, 'itbis') for l in lines}
@classmethod
def get_itbis_withholding_invoiced_line(cls, lines, name):
return {l.id: cls._compute_tax_kind_amount(l, 'itbis_withholding') for l in lines}
@classmethod
def get_isc_invoiced_line(cls, lines, name):
return {l.id: cls._compute_tax_kind_amount(l, 'isc') for l in lines}
@classmethod
def get_cdt_invoiced_line(cls, lines, name):
return {l.id: cls._compute_tax_kind_amount(l, 'cdt') for l in lines}
@classmethod
def get_isr_withholding_invoiced_line(cls, lines, name):
return {l.id: cls._compute_tax_kind_amount(l, 'isr_withholding') for l in lines}
@classmethod
def get_tip_invoiced_line(cls, lines, name):
return {l.id: cls._compute_tax_kind_amount(l, 'tip') for l in lines}
@classmethod
def get_line_total(cls, lines, name):
return {l.id: cls._compute_line_total_amount(l) for l in lines}
def get_amounts(cls, lines, names):
single = isinstance(names, str)
if single:
names = [names]
result = {name: {} for name in names}
for line in lines:
amounts = cls._compute_amounts(line)
for name in names:
result[name][line.id] = amounts.get(
_TAX_FIELD_KINDS.get(name, name), _ZERO)
return result[names[0]] if single else result
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_itbis_invoiced_line(self, name=None):
return InvoiceLine._compute_tax_kind_amount(self, 'itbis')
return self._compute_amounts(self)['itbis']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_itbis_withholding_invoiced_line(self, name=None):
return InvoiceLine._compute_tax_kind_amount(self, 'itbis_withholding')
return self._compute_amounts(self)['itbis_withholding']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_isc_invoiced_line(self, name=None):
return InvoiceLine._compute_tax_kind_amount(self, 'isc')
return self._compute_amounts(self)['isc']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_cdt_invoiced_line(self, name=None):
return InvoiceLine._compute_tax_kind_amount(self, 'cdt')
return self._compute_amounts(self)['cdt']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_isr_withholding_invoiced_line(self, name=None):
return InvoiceLine._compute_tax_kind_amount(self, 'isr_withholding')
return self._compute_amounts(self)['isr_withholding']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_tip_invoiced_line(self, name=None):
return InvoiceLine._compute_tax_kind_amount(self, 'tip')
return self._compute_amounts(self)['tip']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_line_total(self, name=None):
return InvoiceLine._compute_line_total_amount(self)
return self._compute_amounts(self)['line_total']
-1
View File
@@ -3,7 +3,6 @@
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="invoice_view_form">
<field name="model">account.invoice</field>
<field name="inherit" ref="account_invoice.invoice_view_form"/>
+60 -8
View File
@@ -10,6 +10,10 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgctxt "field:account.invoice,amount_paid:"
msgid "Amount Paid"
msgstr "Monto pagado"
msgctxt "field:account.invoice,days_elapsed:"
msgid "Days Elapsed"
msgstr "Días transcurridos"
@@ -22,18 +26,38 @@ msgctxt "field:account.invoice,due_date:"
msgid "Due Date"
msgstr "Fecha de vencimiento"
msgctxt "field:account.invoice,employee:"
msgid "Salesman"
msgstr "Vendedor"
msgctxt "field:account.invoice,party_reimbursement:"
msgid "Party Reimbursement"
msgstr "Tercero para reembolso"
msgctxt "field:account.invoice.line,product_type:"
msgid "Product Type"
msgstr "Tipo de producto"
msgctxt "field:account.invoice.line,cdt_invoiced_line:"
msgid "CDT Invoiced"
msgstr "CDT facturado"
msgctxt "field:account.invoice.line,isc_invoiced_line:"
msgid "ISC Invoiced"
msgstr "ISC facturado"
msgctxt "field:account.invoice.line,isr_withholding_invoiced_line:"
msgid "ISR Withholding"
msgstr "ISR retenido"
msgctxt "field:account.invoice.line,itbis_invoiced_line:"
msgid "ITBIS Invoiced"
msgstr "ITBIS facturado"
msgctxt "field:account.invoice.line,itbis_withholding_invoiced_line:"
msgid "ITBIS Withholding"
msgstr "ITBIS retenido"
msgctxt "field:account.invoice.line,line_total:"
msgid "Total"
msgstr "Total de línea"
msgctxt "field:account.invoice.line,tip_invoiced_line:"
msgid "Tip Invoiced"
msgstr "Propina legal facturada"
msgctxt "model:account.invoice.line,string:"
msgid "Invoice Line"
msgstr "Línea de factura"
@@ -54,6 +78,22 @@ msgctxt "model:ir.action,name:act_invoice_out_domain_paid"
msgid "Paid"
msgstr "Pagado"
msgctxt "model:ir.action.act_window.domain,name:act_invoice_in_domain_cancelled"
msgid "Cancelled"
msgstr "Cancelada"
msgctxt "model:ir.action.act_window.domain,name:act_invoice_in_domain_paid"
msgid "Paid"
msgstr "Pagada"
msgctxt "model:ir.action.act_window.domain,name:act_invoice_out_domain_cancelled"
msgid "Cancelled"
msgstr "Cancelada"
msgctxt "model:ir.action.act_window.domain,name:act_invoice_out_domain_paid"
msgid "Paid"
msgstr "Pagada"
msgctxt "model:ir.ui.view,name:invoice_line_view_form"
msgid "invoice_line_form"
msgstr "invoice_line_form"
@@ -65,3 +105,15 @@ msgstr "invoice_line_tree_sequence"
msgctxt "model:ir.ui.view,name:invoice_view_form"
msgid "invoice_form"
msgstr "invoice_form"
msgctxt "selection:account.invoice.line,product_type:"
msgid "Assets"
msgstr "Activos"
msgctxt "selection:account.invoice.line,product_type:"
msgid "Goods"
msgstr "Bienes"
msgctxt "selection:account.invoice.line,product_type:"
msgid "Service"
msgstr "Servicio"
+13 -45
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: account_invoice_do 8.0.0\n"
"PO-Revision-Date: 2026-07-22 00:00+0000\n"
"Last-Translator: Solutema\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"
@@ -12,51 +12,43 @@ msgstr ""
msgctxt "field:account.invoice,amount_paid:"
msgid "Amount Paid"
msgstr "Monto Pagado"
msgstr "Monto pagado"
msgctxt "field:account.invoice,days_elapsed:"
msgid "Days Elapsed"
msgstr "Días Transcurridos"
msgstr "Días transcurridos"
msgctxt "field:account.invoice,days_overdue:"
msgid "Days Overdue"
msgstr "Días Vencidos"
msgstr "Días vencidos"
msgctxt "field:account.invoice,due_date:"
msgid "Due Date"
msgstr "Fecha de Vencimiento"
msgctxt "field:account.invoice,employee:"
msgid "Salesman"
msgstr "Vendedor"
msgctxt "field:account.invoice,party_reimbursement:"
msgid "Party Reimbursement"
msgstr "Tercero de Reembolso"
msgstr "Fecha de vencimiento"
msgctxt "field:account.invoice.line,cdt_invoiced_line:"
msgid "CDT Invoiced"
msgstr "CDT Facturado"
msgstr "CDT facturado"
msgctxt "field:account.invoice.line,isc_invoiced_line:"
msgid "ISC Invoiced"
msgstr "ISC Facturado"
msgstr "ISC facturado"
msgctxt "field:account.invoice.line,isr_withholding_invoiced_line:"
msgid "ISR Withholding"
msgstr "ISR Retenido"
msgstr "ISR retenido"
msgctxt "field:account.invoice.line,itbis_invoiced_line:"
msgid "ITBIS Invoiced"
msgstr "ITBIS Facturado"
msgstr "ITBIS facturado"
msgctxt "field:account.invoice.line,itbis_withholding_invoiced_line:"
msgid "ITBIS Withholding"
msgstr "ITBIS Retenido"
msgstr "ITBIS retenido"
msgctxt "field:account.invoice.line,line_total:"
msgid "Total"
msgstr "Total Línea"
msgstr "Total de línea"
msgctxt "field:account.invoice.line,product_type:"
msgid "Product Type"
@@ -64,7 +56,7 @@ msgstr "Tipo de producto"
msgctxt "field:account.invoice.line,tip_invoiced_line:"
msgid "Tip Invoiced"
msgstr "Propina Legal"
msgstr "Propina legal facturada"
msgctxt "model:account.invoice.line,string:"
msgid "Invoice Line"
@@ -124,28 +116,4 @@ msgstr "Bienes"
msgctxt "selection:account.invoice.line,product_type:"
msgid "Service"
msgstr "Servicios"
msgctxt "selection:ir.cron,method:"
msgid "Process Posted Customer Invoices"
msgstr "Procesar Facturas de Clientes Contabilizadas"
msgctxt "selection:ir.cron,method:"
msgid "Process Posted Supplier Invoices"
msgstr "Procesar Facturas de Proveedores Contabilizadas"
msgctxt "selection:product.template,type:"
msgid "Assets"
msgstr "Activos"
msgctxt "selection:product.template,type:"
msgid "Goods"
msgstr "Bienes"
msgctxt "selection:product.template,type:"
msgid "Service"
msgstr "Servicios"
msgctxt "view:account.invoice:"
msgid "Accounting"
msgstr "Contabilidad"
msgstr "Servicio"
+5 -4
View File
@@ -3,11 +3,11 @@ requires = ['hatchling >= 1', 'hatch-tryton']
build-backend = 'hatchling.build'
[project]
name = 'trytond_account_invoice_do'
name = 'unpaismejor_account_invoice_do'
dynamic = ['version', 'dependencies', 'optional-dependencies', 'authors', 'readme']
requires-python = '>=3.10'
maintainers = [
{name = "Solutema"},
{name = "Fundación Un País Mejor", email = "jgarcia@unpaismejor.org.do"},
]
description = "Dominican Republic invoices for Tryton"
license = 'GPL-3.0-or-later'
@@ -26,11 +26,12 @@ classifiers = [
account_invoice_do = 'trytond.modules.account_invoice_do'
[project.urls]
homepage = "https://www.tryton.org/"
repository = "https://gitea.joseagrc.com/tryton-do/account_invoice_do"
homepage = "https://www.unpaismejor.org.do/"
repository = "https://code.unpaismejor.org.do/tryton-do/account_invoice_do"
[tool.hatch.build]
include = [
'CHANGELOG',
'COPYRIGHT',
'LICENSE',
'README.rst',
+93 -51
View File
@@ -1,11 +1,8 @@
import unittest
from datetime import date
from decimal import Decimal
from types import SimpleNamespace
import unittest
from dateutil.relativedelta import relativedelta
from trytond.modules.account_invoice_do.cron import _build_error_message
from trytond.modules.account_invoice_do.invoice import Invoice, InvoiceLine
@@ -15,68 +12,113 @@ class AccountInvoiceDoUnitTestCase(unittest.TestCase):
def round(self, amount):
return amount.quantize(Decimal('0.01'))
def test_due_date_from_payment_term(self):
invoice = SimpleNamespace()
invoice.payment_term = SimpleNamespace(lines=[
SimpleNamespace(relativedeltas=[
relativedelta(months=1, days=5),
]),
])
invoice.invoice_date = date(2026, 1, 10)
self.assertEqual(
Invoice.get_remainder_time(invoice, 'due_date'),
date(2026, 2, 15))
def _tax_line(
self, code, amount, template_name=None, rate='0.10'):
template = (SimpleNamespace(name=template_name)
if template_name else None)
tax = SimpleNamespace(
group=SimpleNamespace(code=code), template=template,
type='percentage', rate=Decimal(rate), amount=None)
return SimpleNamespace(tax=tax, amount=Decimal(amount))
def test_amount_paid_only_for_posted_or_paid_invoices(self):
posted = SimpleNamespace(
id=1, state='posted', total_amount=Decimal('118.00'),
amount_to_pay=Decimal('18.00'), currency=self.Currency())
draft = SimpleNamespace(
id=2, state='draft', total_amount=Decimal('118.00'),
amount_to_pay=Decimal('0.00'), currency=self.Currency())
self.assertEqual(Invoice.get_amount_paid([posted, draft], 'amount_paid'), {
1: Decimal('100.00'),
2: Decimal('0.00'),
})
def test_tax_kind_amount_and_line_total(self):
def _line(self, *tax_lines, **values):
line = SimpleNamespace(
id=1,
type='line',
currency=self.Currency(),
invoice=None,
amount=Decimal('100.00'),
taxes=[
SimpleNamespace(tax_kind='itbis', rate=Decimal('0.18')),
SimpleNamespace(tax_kind='tip', rate=Decimal('0.10')),
],
currency=self.Currency())
taxes=[tax.tax for tax in tax_lines],
)
line._get_taxes = lambda: {
index: tax for index, tax in enumerate(tax_lines)}
for name, value in values.items():
setattr(line, name, value)
return line
def test_due_date_uses_last_payment_term_maturity(self):
term = SimpleNamespace(compute=lambda amount, currency, start: [
(date(2026, 2, 10), Decimal('40.00')),
(date(2026, 3, 10), Decimal('60.00')),
])
invoice = SimpleNamespace(
lines_to_pay=[], payment_term=term, payment_term_date=None,
invoice_date=date(2026, 1, 10), currency=self.Currency(),
total_amount=Decimal('100.00'))
self.assertEqual(
InvoiceLine._compute_tax_kind_amount(line, 'itbis'),
Decimal('18.00'))
Invoice._compute_due_date(invoice), date(2026, 3, 10))
def test_due_date_prefers_posted_maturity_dates(self):
invoice = SimpleNamespace(lines_to_pay=[
SimpleNamespace(maturity_date=date(2026, 2, 10)),
SimpleNamespace(maturity_date=date(2026, 4, 10)),
])
self.assertEqual(
InvoiceLine.on_change_with_itbis_invoiced_line(line),
Decimal('18.00'))
Invoice._compute_due_date(invoice), date(2026, 4, 10))
def test_amount_paid_preserves_credit_note_sign(self):
credit = SimpleNamespace(
id=1, state='posted', total_amount=Decimal('-118.00'),
amount_to_pay=Decimal('-18.00'), currency=self.Currency())
self.assertEqual(
InvoiceLine._compute_line_total_amount(line),
Decimal('128.00'))
Invoice.get_amount_paid([credit], 'amount_paid'),
{1: Decimal('-100.00')})
def test_days_elapsed_does_not_require_payment_term(self):
invoice = SimpleNamespace(
state='posted', invoice_date=date(2026, 1, 1), due_date=None,
_today=lambda: date(2026, 1, 11))
invoice._positive_age = lambda reference: Invoice._positive_age(
invoice, reference)
self.assertEqual(Invoice.get_days_elapsed(invoice), 10)
self.assertIsNone(Invoice.get_days_overdue(invoice))
def test_tax_amounts_use_tryton_computed_tax_lines(self):
line = self._line(
self._tax_line('ITBIS', '18.00'),
self._tax_line('ITBIS', '-5.40', rate='-0.054'),
self._tax_line('ISC', '10.00'),
self._tax_line('CDT', '2.00'),
self._tax_line('ISR', '-10.00'),
self._tax_line('OTROS', '10.00', 'Legal Tip 10%'))
self.assertEqual(InvoiceLine._compute_amounts(line), {
'itbis': Decimal('18.00'),
'itbis_withholding': Decimal('5.40'),
'isc': Decimal('10.00'),
'cdt': Decimal('2.00'),
'isr_withholding': Decimal('10.00'),
'tip': Decimal('10.00'),
'line_total': Decimal('124.60'),
})
def test_credit_note_itbis_is_not_classified_as_withholding(self):
line = self._line(self._tax_line('ITBIS', '-18.00', rate='0.18'))
self.assertEqual(InvoiceLine._compute_amounts(line), {
'itbis': Decimal('18.00'),
'itbis_withholding': Decimal('0.00'),
'isc': Decimal('0.00'),
'cdt': Decimal('0.00'),
'isr_withholding': Decimal('0.00'),
'tip': Decimal('0.00'),
'line_total': Decimal('82.00'),
})
def test_tax_kind_ignores_incomplete_external_tax_lines(self):
self.assertIsNone(InvoiceLine._tax_kind(SimpleNamespace(tax=None)))
tax = SimpleNamespace(
group=None, template=None, type='percentage',
rate=Decimal('0.18'), amount=None)
self.assertIsNone(InvoiceLine._tax_kind(SimpleNamespace(tax=tax)))
def test_subtotal_resets_after_previous_subtotal(self):
invoice = SimpleNamespace()
line1 = SimpleNamespace(
type='line', amount=Decimal('100.00'), taxes=[], currency=None)
line1 = self._line()
subtotal1 = SimpleNamespace(
id=1, type='subtotal', invoice=invoice, currency=None)
line2 = SimpleNamespace(
type='line', amount=Decimal('50.00'), taxes=[], currency=None)
line2 = self._line(amount=Decimal('50.00'))
subtotal2 = SimpleNamespace(
id=2, type='subtotal', invoice=invoice, currency=None)
invoice.lines = [line1, subtotal1, line2, subtotal2]
self.assertEqual(
InvoiceLine._compute_line_total_amount(subtotal2),
InvoiceLine._compute_amounts(subtotal2)['line_total'],
Decimal('50.00'))
def test_cron_error_message_uses_fallbacks(self):
inv = SimpleNamespace(id=10)
message = _build_error_message(inv, ValueError('boom'))
self.assertIn('ID: 10', message)
self.assertIn('Numero: N/D', message)
self.assertIn('Error: boom', message)
+15 -100
View File
@@ -1,11 +1,8 @@
import datetime
from decimal import Decimal
from types import SimpleNamespace
import unittest
from trytond.modules.account.tests.test_module import (
create_chart, get_fiscalyear)
from trytond.modules.account_invoice_do.invoice import InvoiceLine
from trytond.modules.company.tests import create_company, set_company
from trytond.pool import Pool
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
@@ -15,7 +12,8 @@ class AccountInvoiceDoTestCase(ModuleTestCase):
"Test Account Invoice Do module"
module = 'account_invoice_do'
def _prepare_invoice_company(self, chart='account_do.do_account_root'):
def _prepare_invoice_company(
self, chart='account_do.do_account_root_en'):
pool = Pool()
FiscalYear = pool.get('account.fiscalyear')
InvoiceSequence = pool.get('account.fiscalyear.invoice_sequence')
@@ -83,59 +81,17 @@ class AccountInvoiceDoTestCase(ModuleTestCase):
return lines[0]
@with_transaction()
def test_customer_invoice_without_product_posts_and_computes_do_fields(self):
def test_customer_invoice_without_product_computes_do_fields(self):
'Post customer invoice and compute Dominican invoice helper fields'
pool = Pool()
Account = pool.get('account.account')
FiscalYear = pool.get('account.fiscalyear')
InvoiceSequence = pool.get('account.fiscalyear.invoice_sequence')
Invoice = pool.get('account.invoice')
Journal = pool.get('account.journal')
Party = pool.get('party.party')
PaymentTerm = pool.get('account.invoice.payment_term')
Sequence = pool.get('ir.sequence.strict')
SequenceType = pool.get('ir.sequence.type')
today = datetime.date.today()
company = create_company()
create_chart(company)
fiscalyear = get_fiscalyear(company, today=today)
sequence_type, = SequenceType.search([
('name', '=', 'Invoice'),
], limit=1)
invoice_sequence, = Sequence.create([{
'name': fiscalyear.name,
'sequence_type': sequence_type.id,
'company': company.id,
}])
fiscalyear.invoice_sequences = [InvoiceSequence(
company=company,
in_invoice_sequence=invoice_sequence,
in_credit_note_sequence=invoice_sequence,
out_invoice_sequence=invoice_sequence,
out_credit_note_sequence=invoice_sequence)]
fiscalyear.save()
FiscalYear.create_period([fiscalyear])
receivable, = Account.search([
('company', '=', company.id),
('type.receivable', '=', True),
('party_required', '=', True),
], limit=1)
revenue, = Account.search([
('company', '=', company.id),
('type.revenue', '=', True),
('closed', '!=', True),
], limit=1)
company, term, today = self._prepare_invoice_company()
receivable = self._account(company, '110201')
revenue = self._account(company, '4102')
journal, = Journal.search([('type', '=', 'revenue')], limit=1)
party, = Party.create([{
'name': 'Cliente local',
'addresses': [('create', [{}])],
}])
term, = PaymentTerm.create([{
'name': 'Contado',
'lines': [('create', [{'type': 'remainder'}])],
}])
party = self._party('Cliente local')
with set_company(company):
invoice, = Invoice.create([{
@@ -182,8 +138,8 @@ class AccountInvoiceDoTestCase(ModuleTestCase):
payable = self._account(company, '210101')
revenue = self._account(company, '4102')
expense = self._account(company, '6103')
sale_tax = self._tax(company, 'ITBIS 18% Ventas')
purchase_tax = self._tax(company, 'ITBIS 18% Compras')
sale_tax = self._tax(company, 'ITBIS 18% Sales')
purchase_tax = self._tax(company, 'ITBIS 18% Purchases')
revenue_journal, = Journal.search([('type', '=', 'revenue')], limit=1)
expense_journal, = Journal.search([('type', '=', 'expense')], limit=1)
@@ -237,61 +193,20 @@ class AccountInvoiceDoTestCase(ModuleTestCase):
in_invoice = Invoice(in_invoice.id)
self.assertEqual(out_invoice.tax_amount, Decimal('180.00'))
out_line, = out_invoice.lines
self.assertEqual(out_line.itbis_invoiced_line, Decimal('180.00'))
self.assertEqual(out_line.line_total, Decimal('1180.00'))
out_tax_line = self._invoice_tax_line(out_invoice, '21020101')
self.assertEqual(out_tax_line.debit, Decimal('0.00'))
self.assertEqual(out_tax_line.credit, Decimal('180.00'))
self.assertEqual(in_invoice.tax_amount, Decimal('180.00'))
in_line, = in_invoice.lines
self.assertEqual(in_line.itbis_invoiced_line, Decimal('180.00'))
self.assertEqual(in_line.line_total, Decimal('1180.00'))
in_tax_line = self._invoice_tax_line(in_invoice, '11040101')
self.assertEqual(in_tax_line.debit, Decimal('180.00'))
self.assertEqual(in_tax_line.credit, Decimal('0.00'))
del ModuleTestCase
class AccountInvoiceDoUnitTestCase(unittest.TestCase):
def _currency(self):
return SimpleNamespace(round=lambda amount: amount.quantize(
Decimal('0.01')))
def _tax(self, kind, rate):
return SimpleNamespace(tax_kind=kind, rate=Decimal(rate))
def _line(self, **values):
defaults = {
'id': 1,
'type': 'line',
'product': None,
'taxes': [],
'currency': self._currency(),
'amount': Decimal('100.00'),
}
defaults.update(values)
return SimpleNamespace(**defaults)
def test_product_type_is_empty_without_product(self):
line = self._line()
self.assertEqual(InvoiceLine.on_change_with_product_type(line), '')
def test_tax_kind_amounts_are_computed_from_line_base(self):
line = self._line(taxes=[
self._tax('itbis', '0.18'),
self._tax('isc', '0.10'),
])
self.assertEqual(
InvoiceLine._compute_tax_kind_amount(line, 'itbis'),
Decimal('18.00'))
self.assertEqual(
InvoiceLine._compute_tax_kind_amount(line, 'isc'),
Decimal('10.00'))
def test_line_total_includes_all_line_taxes(self):
line = self._line(taxes=[
self._tax('itbis', '0.18'),
self._tax('cdt', '0.02'),
])
self.assertEqual(
InvoiceLine._compute_line_total_amount(line),
Decimal('120.00'))
-3
View File
@@ -10,8 +10,5 @@ xml:
[register]
model:
cron.Cron
cron.Invoice
address.Address
invoice.Invoice
invoice.InvoiceLine
+10 -12
View File
@@ -2,23 +2,21 @@
<!-- 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="/form/notebook/page[@id='info']/field[@name='company']" position="after">
<label name="party_reimbursement"/>
<field name="party_reimbursement"/>
</xpath>
<xpath expr="/form/notebook/page[@id='payment']/label[@name='amount_to_pay_today']" position="before">
<xpath
expr="/form/notebook/page[@id='payment']/label[@name='amount_to_pay_today']"
position="before">
<label name="amount_paid"/>
<field name="amount_paid"/>
<field name="amount_paid"/>
</xpath>
<xpath expr="/form/notebook/page[@id='payment']/field[@name='amount_to_pay']" position="after">
<xpath
expr="/form/notebook/page[@id='payment']/field[@name='amount_to_pay']"
position="after">
<label name="due_date"/>
<field name="due_date"/>
<label name="days_elapsed"/>
<field name="days_elapsed"/>
<field name="days_elapsed"/>
<label name="days_overdue"/>
<field name="days_overdue"/>
<field name="days_overdue"/>
</xpath>
</data>
</data>
-5
View File
@@ -2,10 +2,6 @@
<!-- 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" position="replace_attributes">
<tree editable="1"/>
</xpath> -->
<xpath expr="/tree/field[@name='taxes']" position="replace_attributes">
<field name="taxes" optional="1"/>
</xpath>
@@ -23,5 +19,4 @@ this repository contains the full copyright notices and license terms. -->
<field name="tip_invoiced_line" optional="0"/>
<field name="line_total" optional="0"/>
</xpath>
</data>