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: on:
push: push:
branches: branches:
- "**" - "8.0"
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - 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 - name: Detect package branch
run: | run: |
@@ -21,7 +29,9 @@ jobs:
import re import re
branch = ( 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 os.environ.get('GITHUB_HEAD_REF')
or '') or '')
publish = bool(re.fullmatch(r'\d+\.\d+', branch)) publish = bool(re.fullmatch(r'\d+\.\d+', branch))
@@ -75,7 +85,7 @@ jobs:
OWNER = 'tryton-do' OWNER = 'tryton-do'
PACKAGE_TYPE = 'pypi' PACKAGE_TYPE = 'pypi'
API_ROOT = 'https://gitea.joseagrc.com/api/v1' API_ROOT = 'https://code.unpaismejor.org.do/api/v1'
def normalize(name): def normalize(name):
return re.sub(r'[-_.]+', '-', name).lower() return re.sub(r'[-_.]+', '-', name).lower()
@@ -85,7 +95,6 @@ jobs:
cfg_match = re.search(r'(?m)^version=(.+)$', cfg_text) cfg_match = re.search(r'(?m)^version=(.+)$', cfg_text)
if not cfg_match: if not cfg_match:
raise SystemExit('tryton.cfg has no version entry') raise SystemExit('tryton.cfg has no version entry')
series = os.environ['PACKAGE_SERIES'] series = os.environ['PACKAGE_SERIES']
major, minor = map(int, series.split('.')) major, minor = map(int, series.split('.'))
@@ -188,5 +197,5 @@ jobs:
TWINE_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} TWINE_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: | run: |
python -m twine upload \ 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/* 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 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 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 This module extends ``account_invoice`` with operational fields commonly used
in Dominican Republic installations: in Dominican Republic installations:
* salesman on invoices;
* due date, elapsed days, overdue days, and paid amount helpers; * due date, elapsed days, overdue days, and paid amount helpers;
* supplier reimbursement party;
* informational invoice line tax amounts by Dominican tax kind; * 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 The tax amount columns are display helpers. They use the amounts computed by
amount and the selected taxes to make invoice entry easier to review; accounting Tryton's core tax engine and classify them by the stable Dominican tax groups.
tax posting remains handled by Tryton's core tax engine. 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 from . import invoice
__all__ = [ __all__ = [
'address',
'cron',
'invoice', '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 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 Cobertura funcional
------------------- -------------------
* Vendedor en factura. * Fecha final de vencimiento calculada desde el término de pago completo.
* Fecha de vencimiento calculada desde término de pago.
* Días transcurridos, días vencidos e importe pagado. * Días transcurridos, días vencidos e importe pagado.
* Totales por tipo de impuesto en líneas de factura. * Totales informativos por grupo fiscal dominicano en líneas de factura.
* Procesamiento cron de facturas publicadas con bitácora de errores. * Total de línea basado en los importes producidos por el motor fiscal de
Tryton.
Pruebas 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 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 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 dateutil.relativedelta import relativedelta
from decimal import Decimal 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): class Invoice(metaclass=PoolMeta):
__name__ = 'account.invoice' __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'), due_date = fields.Function(fields.Date('Due Date'),
'get_remainder_time') 'get_due_date')
amount_paid = fields.Function(Monetary( amount_paid = fields.Function(Monetary(
"Amount Paid", currency='currency', digits='currency'), "Amount Paid", currency='currency', digits='currency'),
@@ -36,17 +27,6 @@ class Invoice(metaclass=PoolMeta):
days_overdue = fields.Function(fields.Integer('Days Overdue'), days_overdue = fields.Function(fields.Integer('Days Overdue'),
'get_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 @classmethod
def search_rec_name(cls, name, clause): def search_rec_name(cls, name, clause):
domain = super().search_rec_name(name, clause) domain = super().search_rec_name(name, clause)
@@ -55,31 +35,49 @@ class Invoice(metaclass=PoolMeta):
] ]
def get_days_elapsed(self, name=None): def get_days_elapsed(self, name=None):
if self.state == 'paid' or not self.due_date or not self.invoice_date: return self._positive_age(self.invoice_date)
return None
days = (date.today() - self.invoice_date).days
return days if days > 0 else None
def get_days_overdue(self, name=None): 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 return None
days = (date.today() - self.due_date).days days = (self._today() - reference_date).days
return days if days > 0 else None return days if days > 0 else None
def get_remainder_time(self, name): def _today(self):
term = self.payment_term Date = Pool().get('ir.date')
if term and term.lines: company = self.company.id if self.company else None
for line in term.lines: with Transaction().set_context(company=company):
if (not line.relativedeltas return Date.today()
or not self.invoice_date):
break def _compute_due_date(self):
delta = line.relativedeltas[0] maturity_date = max((
return self.invoice_date + relativedelta( line.maturity_date for line in (self.lines_to_pay or [])
months=delta.months, if line.maturity_date), default=None)
weeks=delta.weeks, if maturity_date:
days=delta.days) 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 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 @classmethod
def get_amount_paid(cls, invoices, name): def get_amount_paid(cls, invoices, name):
res = {i.id: _ZERO for i in invoices} res = {i.id: _ZERO for i in invoices}
@@ -88,7 +86,7 @@ class Invoice(metaclass=PoolMeta):
continue continue
total = invoice.total_amount or _ZERO total = invoice.total_amount or _ZERO
to_pay = invoice.amount_to_pay or _ZERO to_pay = invoice.amount_to_pay or _ZERO
paid = abs(total - to_pay) paid = total - to_pay
if invoice.currency: if invoice.currency:
paid = invoice.currency.round(paid) paid = invoice.currency.round(paid)
res[invoice.id] = paid res[invoice.id] = paid
@@ -97,10 +95,14 @@ class Invoice(metaclass=PoolMeta):
@classmethod @classmethod
def __setup__(cls): def __setup__(cls):
super().__setup__() 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 = [ cls._order = [
('invoice_date', 'DESC'), ('invoice_date', 'DESC'),
('id', 'DESC'), *(order for order in cls._order
] if order[0] != 'invoice_date'),
]
_LINE_DEPENDS = [ _LINE_DEPENDS = [
@@ -116,6 +118,28 @@ _LINE_TOTAL_STATES = {
'invisible': ~Eval('type').in_(['line', 'subtotal']), '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): class InvoiceLine(metaclass=PoolMeta):
'Invoice Line' 'Invoice Line'
@@ -126,47 +150,13 @@ class InvoiceLine(metaclass=PoolMeta):
'on_change_with_product_type' 'on_change_with_product_type'
) )
itbis_invoiced_line = fields.Function(Monetary( itbis_invoiced_line = _amount_field("ITBIS Invoiced")
"ITBIS Invoiced", currency='currency', digits='currency', itbis_withholding_invoiced_line = _amount_field("ITBIS Withholding")
states=_LINE_STATES), isc_invoiced_line = _amount_field("ISC Invoiced")
'get_itbis_invoiced_line' cdt_invoiced_line = _amount_field("CDT Invoiced")
) isr_withholding_invoiced_line = _amount_field("ISR Withholding")
tip_invoiced_line = _amount_field("Tip Invoiced")
itbis_withholding_invoiced_line = fields.Function(Monetary( line_total = _amount_field("Total", _LINE_TOTAL_STATES)
"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'
)
@fields.depends('type', 'product') @fields.depends('type', 'product')
def on_change_with_product_type(self, name=None): def on_change_with_product_type(self, name=None):
@@ -177,7 +167,7 @@ class InvoiceLine(metaclass=PoolMeta):
@classmethod @classmethod
def _get_line_currency(cls, line): def _get_line_currency(cls, line):
invoice = getattr(line, 'invoice', None) 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 invoice.currency
return getattr(line, 'currency', None) return getattr(line, 'currency', None)
@@ -191,105 +181,109 @@ class InvoiceLine(metaclass=PoolMeta):
@classmethod @classmethod
def _get_base_line_amount(cls, line): def _get_base_line_amount(cls, line):
if line.type != 'line': if line.type != 'line':
return Decimal('0.0') return _ZERO
amount = None compute = getattr(line, 'on_change_with_amount', None)
if hasattr(line, 'on_change_with_amount'): amount = compute() if compute else None
amount = line.on_change_with_amount()
if amount is None: if amount is None:
amount = getattr(line, 'amount', None) amount = getattr(line, 'amount', None)
return amount or Decimal('0.0') return amount or _ZERO
@classmethod @classmethod
def _compute_tax_kind_amount(cls, line, tax_kind): def _is_withholding_tax(cls, tax):
if line.type != 'line': if not tax:
return Decimal('0.0') return False
taxes = getattr(line, 'taxes', None) if getattr(tax, 'type', None) == 'percentage':
if not taxes: return (tax.rate or _ZERO) < 0
return Decimal('0.0') if getattr(tax, 'type', None) == 'fixed':
base = cls._get_base_line_amount(line) return (tax.amount or _ZERO) < 0
total = Decimal('0.0') return False
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))
@classmethod @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': if line.type == 'line':
base = cls._get_base_line_amount(line) base = cls._get_base_line_amount(line)
tax_total = Decimal('0.0') tax_total = _ZERO
for tax in (getattr(line, 'taxes', None) or []): if getattr(line, 'taxes', None):
rate = getattr(tax, 'rate', None) or Decimal('0.0') for tax_line in line._get_taxes().values():
tax_total += base * Decimal(str(rate)) amount = tax_line.amount or _ZERO
return cls._round_amount(line, base + tax_total) tax_total += amount
if line.type == 'subtotal': kind = cls._tax_kind(tax_line)
subtotal = Decimal('0.0') if kind:
amounts[kind] += amount
line_total = base + tax_total
elif line.type == 'subtotal':
subtotal = _ZERO
invoice = getattr(line, 'invoice', None) invoice = getattr(line, 'invoice', None)
if not invoice or not hasattr(invoice, 'lines'): for invoice_line in getattr(invoice, 'lines', []):
return subtotal if invoice_line.type == 'line':
for line2 in invoice.lines: subtotal += cls._compute_amounts(invoice_line)['line_total']
if line2.type == 'line': elif invoice_line.type == 'subtotal':
subtotal += cls._compute_line_total_amount(line2) if line == invoice_line:
elif line2.type == 'subtotal':
if line == line2:
break break
subtotal = Decimal('0.0') subtotal = _ZERO
return cls._round_amount(line, subtotal) line_total = subtotal
return Decimal('0.0') 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 @classmethod
def get_itbis_invoiced_line(cls, lines, name): def get_amounts(cls, lines, names):
return {l.id: cls._compute_tax_kind_amount(l, 'itbis') for l in lines} single = isinstance(names, str)
if single:
@classmethod names = [names]
def get_itbis_withholding_invoiced_line(cls, lines, name): result = {name: {} for name in names}
return {l.id: cls._compute_tax_kind_amount(l, 'itbis_withholding') for l in lines} for line in lines:
amounts = cls._compute_amounts(line)
@classmethod for name in names:
def get_isc_invoiced_line(cls, lines, name): result[name][line.id] = amounts.get(
return {l.id: cls._compute_tax_kind_amount(l, 'isc') for l in lines} _TAX_FIELD_KINDS.get(name, name), _ZERO)
return result[names[0]] if single else result
@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}
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount']) @fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_itbis_invoiced_line(self, name=None): 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']) @fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_itbis_withholding_invoiced_line(self, name=None): 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']) @fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_isc_invoiced_line(self, name=None): 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']) @fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_cdt_invoiced_line(self, name=None): 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']) @fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_isr_withholding_invoiced_line(self, name=None): 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']) @fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_tip_invoiced_line(self, name=None): 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']) @fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_line_total(self, name=None): 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. --> this repository contains the full copyright notices and license terms. -->
<tryton> <tryton>
<data> <data>
<record model="ir.ui.view" id="invoice_view_form"> <record model="ir.ui.view" id="invoice_view_form">
<field name="model">account.invoice</field> <field name="model">account.invoice</field>
<field name="inherit" ref="account_invoice.invoice_view_form"/> <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-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
msgctxt "field:account.invoice,amount_paid:"
msgid "Amount Paid"
msgstr "Monto pagado"
msgctxt "field:account.invoice,days_elapsed:" msgctxt "field:account.invoice,days_elapsed:"
msgid "Days Elapsed" msgid "Days Elapsed"
msgstr "Días transcurridos" msgstr "Días transcurridos"
@@ -22,18 +26,38 @@ msgctxt "field:account.invoice,due_date:"
msgid "Due Date" msgid "Due Date"
msgstr "Fecha de vencimiento" 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:" msgctxt "field:account.invoice.line,product_type:"
msgid "Product Type" msgid "Product Type"
msgstr "Tipo de producto" 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:" msgctxt "model:account.invoice.line,string:"
msgid "Invoice Line" msgid "Invoice Line"
msgstr "Línea de factura" msgstr "Línea de factura"
@@ -54,6 +78,22 @@ msgctxt "model:ir.action,name:act_invoice_out_domain_paid"
msgid "Paid" msgid "Paid"
msgstr "Pagado" 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" msgctxt "model:ir.ui.view,name:invoice_line_view_form"
msgid "invoice_line_form" msgid "invoice_line_form"
msgstr "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" msgctxt "model:ir.ui.view,name:invoice_view_form"
msgid "invoice_form" msgid "invoice_form"
msgstr "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 "" msgstr ""
"Project-Id-Version: account_invoice_do 8.0.0\n" "Project-Id-Version: account_invoice_do 8.0.0\n"
"PO-Revision-Date: 2026-07-22 00:00+0000\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-Team: Spanish (Latin America)\n"
"Language: es_419\n" "Language: es_419\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@@ -12,51 +12,43 @@ msgstr ""
msgctxt "field:account.invoice,amount_paid:" msgctxt "field:account.invoice,amount_paid:"
msgid "Amount Paid" msgid "Amount Paid"
msgstr "Monto Pagado" msgstr "Monto pagado"
msgctxt "field:account.invoice,days_elapsed:" msgctxt "field:account.invoice,days_elapsed:"
msgid "Days Elapsed" msgid "Days Elapsed"
msgstr "Días Transcurridos" msgstr "Días transcurridos"
msgctxt "field:account.invoice,days_overdue:" msgctxt "field:account.invoice,days_overdue:"
msgid "Days Overdue" msgid "Days Overdue"
msgstr "Días Vencidos" msgstr "Días vencidos"
msgctxt "field:account.invoice,due_date:" msgctxt "field:account.invoice,due_date:"
msgid "Due Date" msgid "Due Date"
msgstr "Fecha de Vencimiento" 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"
msgctxt "field:account.invoice.line,cdt_invoiced_line:" msgctxt "field:account.invoice.line,cdt_invoiced_line:"
msgid "CDT Invoiced" msgid "CDT Invoiced"
msgstr "CDT Facturado" msgstr "CDT facturado"
msgctxt "field:account.invoice.line,isc_invoiced_line:" msgctxt "field:account.invoice.line,isc_invoiced_line:"
msgid "ISC Invoiced" msgid "ISC Invoiced"
msgstr "ISC Facturado" msgstr "ISC facturado"
msgctxt "field:account.invoice.line,isr_withholding_invoiced_line:" msgctxt "field:account.invoice.line,isr_withholding_invoiced_line:"
msgid "ISR Withholding" msgid "ISR Withholding"
msgstr "ISR Retenido" msgstr "ISR retenido"
msgctxt "field:account.invoice.line,itbis_invoiced_line:" msgctxt "field:account.invoice.line,itbis_invoiced_line:"
msgid "ITBIS Invoiced" msgid "ITBIS Invoiced"
msgstr "ITBIS Facturado" msgstr "ITBIS facturado"
msgctxt "field:account.invoice.line,itbis_withholding_invoiced_line:" msgctxt "field:account.invoice.line,itbis_withholding_invoiced_line:"
msgid "ITBIS Withholding" msgid "ITBIS Withholding"
msgstr "ITBIS Retenido" msgstr "ITBIS retenido"
msgctxt "field:account.invoice.line,line_total:" msgctxt "field:account.invoice.line,line_total:"
msgid "Total" msgid "Total"
msgstr "Total Línea" msgstr "Total de línea"
msgctxt "field:account.invoice.line,product_type:" msgctxt "field:account.invoice.line,product_type:"
msgid "Product Type" msgid "Product Type"
@@ -64,7 +56,7 @@ msgstr "Tipo de producto"
msgctxt "field:account.invoice.line,tip_invoiced_line:" msgctxt "field:account.invoice.line,tip_invoiced_line:"
msgid "Tip Invoiced" msgid "Tip Invoiced"
msgstr "Propina Legal" msgstr "Propina legal facturada"
msgctxt "model:account.invoice.line,string:" msgctxt "model:account.invoice.line,string:"
msgid "Invoice Line" msgid "Invoice Line"
@@ -124,28 +116,4 @@ msgstr "Bienes"
msgctxt "selection:account.invoice.line,product_type:" msgctxt "selection:account.invoice.line,product_type:"
msgid "Service" msgid "Service"
msgstr "Servicios" msgstr "Servicio"
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"
+5 -4
View File
@@ -3,11 +3,11 @@ requires = ['hatchling >= 1', 'hatch-tryton']
build-backend = 'hatchling.build' build-backend = 'hatchling.build'
[project] [project]
name = 'trytond_account_invoice_do' name = 'unpaismejor_account_invoice_do'
dynamic = ['version', 'dependencies', 'optional-dependencies', 'authors', 'readme'] dynamic = ['version', 'dependencies', 'optional-dependencies', 'authors', 'readme']
requires-python = '>=3.10' requires-python = '>=3.10'
maintainers = [ maintainers = [
{name = "Solutema"}, {name = "Fundación Un País Mejor", email = "jgarcia@unpaismejor.org.do"},
] ]
description = "Dominican Republic invoices for Tryton" description = "Dominican Republic invoices for Tryton"
license = 'GPL-3.0-or-later' license = 'GPL-3.0-or-later'
@@ -26,11 +26,12 @@ classifiers = [
account_invoice_do = 'trytond.modules.account_invoice_do' account_invoice_do = 'trytond.modules.account_invoice_do'
[project.urls] [project.urls]
homepage = "https://www.tryton.org/" homepage = "https://www.unpaismejor.org.do/"
repository = "https://gitea.joseagrc.com/tryton-do/account_invoice_do" repository = "https://code.unpaismejor.org.do/tryton-do/account_invoice_do"
[tool.hatch.build] [tool.hatch.build]
include = [ include = [
'CHANGELOG',
'COPYRIGHT', 'COPYRIGHT',
'LICENSE', 'LICENSE',
'README.rst', 'README.rst',
+93 -51
View File
@@ -1,11 +1,8 @@
import unittest
from datetime import date from datetime import date
from decimal import Decimal from decimal import Decimal
from types import SimpleNamespace 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 from trytond.modules.account_invoice_do.invoice import Invoice, InvoiceLine
@@ -15,68 +12,113 @@ class AccountInvoiceDoUnitTestCase(unittest.TestCase):
def round(self, amount): def round(self, amount):
return amount.quantize(Decimal('0.01')) return amount.quantize(Decimal('0.01'))
def test_due_date_from_payment_term(self): def _tax_line(
invoice = SimpleNamespace() self, code, amount, template_name=None, rate='0.10'):
invoice.payment_term = SimpleNamespace(lines=[ template = (SimpleNamespace(name=template_name)
SimpleNamespace(relativedeltas=[ if template_name else None)
relativedelta(months=1, days=5), tax = SimpleNamespace(
]), group=SimpleNamespace(code=code), template=template,
]) type='percentage', rate=Decimal(rate), amount=None)
invoice.invoice_date = date(2026, 1, 10) return SimpleNamespace(tax=tax, amount=Decimal(amount))
self.assertEqual(
Invoice.get_remainder_time(invoice, 'due_date'),
date(2026, 2, 15))
def test_amount_paid_only_for_posted_or_paid_invoices(self): def _line(self, *tax_lines, **values):
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):
line = SimpleNamespace( line = SimpleNamespace(
id=1, id=1,
type='line', type='line',
currency=self.Currency(),
invoice=None,
amount=Decimal('100.00'), amount=Decimal('100.00'),
taxes=[ taxes=[tax.tax for tax in tax_lines],
SimpleNamespace(tax_kind='itbis', rate=Decimal('0.18')), )
SimpleNamespace(tax_kind='tip', rate=Decimal('0.10')), line._get_taxes = lambda: {
], index: tax for index, tax in enumerate(tax_lines)}
currency=self.Currency()) 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( self.assertEqual(
InvoiceLine._compute_tax_kind_amount(line, 'itbis'), Invoice._compute_due_date(invoice), date(2026, 3, 10))
Decimal('18.00'))
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( self.assertEqual(
InvoiceLine.on_change_with_itbis_invoiced_line(line), Invoice._compute_due_date(invoice), date(2026, 4, 10))
Decimal('18.00'))
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( self.assertEqual(
InvoiceLine._compute_line_total_amount(line), Invoice.get_amount_paid([credit], 'amount_paid'),
Decimal('128.00')) {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): def test_subtotal_resets_after_previous_subtotal(self):
invoice = SimpleNamespace() invoice = SimpleNamespace()
line1 = SimpleNamespace( line1 = self._line()
type='line', amount=Decimal('100.00'), taxes=[], currency=None)
subtotal1 = SimpleNamespace( subtotal1 = SimpleNamespace(
id=1, type='subtotal', invoice=invoice, currency=None) id=1, type='subtotal', invoice=invoice, currency=None)
line2 = SimpleNamespace( line2 = self._line(amount=Decimal('50.00'))
type='line', amount=Decimal('50.00'), taxes=[], currency=None)
subtotal2 = SimpleNamespace( subtotal2 = SimpleNamespace(
id=2, type='subtotal', invoice=invoice, currency=None) id=2, type='subtotal', invoice=invoice, currency=None)
invoice.lines = [line1, subtotal1, line2, subtotal2] invoice.lines = [line1, subtotal1, line2, subtotal2]
self.assertEqual( self.assertEqual(
InvoiceLine._compute_line_total_amount(subtotal2), InvoiceLine._compute_amounts(subtotal2)['line_total'],
Decimal('50.00')) 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 import datetime
from decimal import Decimal from decimal import Decimal
from types import SimpleNamespace
import unittest
from trytond.modules.account.tests.test_module import ( from trytond.modules.account.tests.test_module import (
create_chart, get_fiscalyear) 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.modules.company.tests import create_company, set_company
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
@@ -15,7 +12,8 @@ class AccountInvoiceDoTestCase(ModuleTestCase):
"Test Account Invoice Do module" "Test Account Invoice Do module"
module = 'account_invoice_do' 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() pool = Pool()
FiscalYear = pool.get('account.fiscalyear') FiscalYear = pool.get('account.fiscalyear')
InvoiceSequence = pool.get('account.fiscalyear.invoice_sequence') InvoiceSequence = pool.get('account.fiscalyear.invoice_sequence')
@@ -83,59 +81,17 @@ class AccountInvoiceDoTestCase(ModuleTestCase):
return lines[0] return lines[0]
@with_transaction() @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' 'Post customer invoice and compute Dominican invoice helper fields'
pool = Pool() pool = Pool()
Account = pool.get('account.account')
FiscalYear = pool.get('account.fiscalyear')
InvoiceSequence = pool.get('account.fiscalyear.invoice_sequence')
Invoice = pool.get('account.invoice') Invoice = pool.get('account.invoice')
Journal = pool.get('account.journal') 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, term, today = self._prepare_invoice_company()
company = create_company() receivable = self._account(company, '110201')
create_chart(company) revenue = self._account(company, '4102')
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)
journal, = Journal.search([('type', '=', 'revenue')], limit=1) journal, = Journal.search([('type', '=', 'revenue')], limit=1)
party, = Party.create([{ party = self._party('Cliente local')
'name': 'Cliente local',
'addresses': [('create', [{}])],
}])
term, = PaymentTerm.create([{
'name': 'Contado',
'lines': [('create', [{'type': 'remainder'}])],
}])
with set_company(company): with set_company(company):
invoice, = Invoice.create([{ invoice, = Invoice.create([{
@@ -182,8 +138,8 @@ class AccountInvoiceDoTestCase(ModuleTestCase):
payable = self._account(company, '210101') payable = self._account(company, '210101')
revenue = self._account(company, '4102') revenue = self._account(company, '4102')
expense = self._account(company, '6103') expense = self._account(company, '6103')
sale_tax = self._tax(company, 'ITBIS 18% Ventas') sale_tax = self._tax(company, 'ITBIS 18% Sales')
purchase_tax = self._tax(company, 'ITBIS 18% Compras') purchase_tax = self._tax(company, 'ITBIS 18% Purchases')
revenue_journal, = Journal.search([('type', '=', 'revenue')], limit=1) revenue_journal, = Journal.search([('type', '=', 'revenue')], limit=1)
expense_journal, = Journal.search([('type', '=', 'expense')], limit=1) expense_journal, = Journal.search([('type', '=', 'expense')], limit=1)
@@ -237,61 +193,20 @@ class AccountInvoiceDoTestCase(ModuleTestCase):
in_invoice = Invoice(in_invoice.id) in_invoice = Invoice(in_invoice.id)
self.assertEqual(out_invoice.tax_amount, Decimal('180.00')) 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') out_tax_line = self._invoice_tax_line(out_invoice, '21020101')
self.assertEqual(out_tax_line.debit, Decimal('0.00')) self.assertEqual(out_tax_line.debit, Decimal('0.00'))
self.assertEqual(out_tax_line.credit, Decimal('180.00')) self.assertEqual(out_tax_line.credit, Decimal('180.00'))
self.assertEqual(in_invoice.tax_amount, 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') in_tax_line = self._invoice_tax_line(in_invoice, '11040101')
self.assertEqual(in_tax_line.debit, Decimal('180.00')) self.assertEqual(in_tax_line.debit, Decimal('180.00'))
self.assertEqual(in_tax_line.credit, Decimal('0.00')) self.assertEqual(in_tax_line.credit, Decimal('0.00'))
del ModuleTestCase 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] [register]
model: model:
cron.Cron
cron.Invoice
address.Address
invoice.Invoice invoice.Invoice
invoice.InvoiceLine 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 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. -->
<data> <data>
<xpath expr="/form/notebook/page[@id='info']/field[@name='company']" position="after"> <xpath
<label name="party_reimbursement"/> expr="/form/notebook/page[@id='payment']/label[@name='amount_to_pay_today']"
<field name="party_reimbursement"/> position="before">
</xpath>
<xpath expr="/form/notebook/page[@id='payment']/label[@name='amount_to_pay_today']" position="before">
<label name="amount_paid"/> <label name="amount_paid"/>
<field name="amount_paid"/> <field name="amount_paid"/>
</xpath> </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"/> <label name="due_date"/>
<field name="due_date"/> <field name="due_date"/>
<label name="days_elapsed"/> <label name="days_elapsed"/>
<field name="days_elapsed"/> <field name="days_elapsed"/>
<label name="days_overdue"/> <label name="days_overdue"/>
<field name="days_overdue"/> <field name="days_overdue"/>
</xpath> </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 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. -->
<data> <data>
<!-- <xpath expr="/tree" position="replace_attributes">
<tree editable="1"/>
</xpath> -->
<xpath expr="/tree/field[@name='taxes']" position="replace_attributes"> <xpath expr="/tree/field[@name='taxes']" position="replace_attributes">
<field name="taxes" optional="1"/> <field name="taxes" optional="1"/>
</xpath> </xpath>
@@ -23,5 +19,4 @@ this repository contains the full copyright notices and license terms. -->
<field name="tip_invoiced_line" optional="0"/> <field name="tip_invoiced_line" optional="0"/>
<field name="line_total" optional="0"/> <field name="line_total" optional="0"/>
</xpath> </xpath>
</data> </data>