This commit is contained in:
@@ -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/*
|
||||||
|
|||||||
@@ -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,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
@@ -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.
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
from . import address
|
|
||||||
from . import cron
|
|
||||||
from . import invoice
|
from . import invoice
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'address',
|
|
||||||
'cron',
|
|
||||||
'invoice',
|
'invoice',
|
||||||
]
|
]
|
||||||
|
|||||||
-11
@@ -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
|
|
||||||
@@ -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"),
|
|
||||||
])
|
|
||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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']
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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',
|
||||||
|
|||||||
@@ -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
@@ -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'))
|
|
||||||
|
|||||||
@@ -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
@@ -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>
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user