Files

290 lines
10 KiB
Python
Raw Permalink Normal View History

2026-07-22 22:29:04 -04:00
# 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 decimal import Decimal
2026-08-14 20:54:23 -04: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)
2026-07-22 22:29:04 -04:00
class Invoice(metaclass=PoolMeta):
__name__ = 'account.invoice'
due_date = fields.Function(fields.Date('Due Date'),
2026-08-14 20:54:23 -04:00
'get_due_date')
2026-07-22 22:29:04 -04:00
amount_paid = fields.Function(Monetary(
"Amount Paid", currency='currency', digits='currency'),
'get_amount_paid')
days_elapsed = fields.Function(fields.Integer('Days Elapsed'),
'get_days_elapsed')
days_overdue = fields.Function(fields.Integer('Days Overdue'),
'get_days_overdue')
@classmethod
def search_rec_name(cls, name, clause):
domain = super().search_rec_name(name, clause)
return domain + [
('description', *clause[1:]),
]
def get_days_elapsed(self, name=None):
2026-08-14 20:54:23 -04:00
return self._positive_age(self.invoice_date)
2026-07-22 22:29:04 -04:00
def get_days_overdue(self, name=None):
2026-08-14 20:54:23 -04:00
return self._positive_age(self.due_date)
def _positive_age(self, reference_date):
if self.state == 'paid' or not reference_date:
2026-07-22 22:29:04 -04:00
return None
2026-08-14 20:54:23 -04:00
days = (self._today() - reference_date).days
2026-07-22 22:29:04 -04:00
return days if days > 0 else None
2026-08-14 20:54:23 -04:00
def _today(self):
Date = Pool().get('ir.date')
company = self.company.id if self.company else None
with Transaction().set_context(company=company):
return Date.today()
def _compute_due_date(self):
maturity_date = max((
line.maturity_date for line in (self.lines_to_pay or [])
if line.maturity_date), default=None)
if maturity_date:
return maturity_date
payment_date = self.payment_term_date or self.invoice_date
if (self.payment_term and payment_date and self.currency
and self.total_amount is not None):
terms = self.payment_term.compute(
self.total_amount, self.currency, payment_date)
if terms:
return max(date for date, _ in terms)
2026-07-22 22:29:04 -04:00
return None
2026-08-14 20:54:23 -04:00
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()
2026-07-22 22:29:04 -04:00
@classmethod
def get_amount_paid(cls, invoices, name):
res = {i.id: _ZERO for i in invoices}
for invoice in invoices:
if invoice.state not in ('posted', 'paid'):
continue
total = invoice.total_amount or _ZERO
to_pay = invoice.amount_to_pay or _ZERO
2026-08-14 20:54:23 -04:00
paid = total - to_pay
2026-07-22 22:29:04 -04:00
if invoice.currency:
paid = invoice.currency.round(paid)
res[invoice.id] = paid
return res
@classmethod
def __setup__(cls):
super().__setup__()
2026-08-14 20:54:23 -04:00
# 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.
2026-07-22 22:29:04 -04:00
cls._order = [
('invoice_date', 'DESC'),
2026-08-14 20:54:23 -04:00
*(order for order in cls._order
if order[0] != 'invoice_date'),
]
2026-07-22 22:29:04 -04:00
_LINE_DEPENDS = [
'type', 'unit_price', 'quantity', 'taxes',
'invoice', '_parent_invoice.currency', 'currency', 'product',
]
_LINE_STATES = {
'invisible': ~Eval('type').in_(['line']),
}
_LINE_TOTAL_STATES = {
'invisible': ~Eval('type').in_(['line', 'subtotal']),
}
2026-08-14 20:54:23 -04:00
_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')
2026-07-22 22:29:04 -04:00
class InvoiceLine(metaclass=PoolMeta):
'Invoice Line'
__name__ = 'account.invoice.line'
product_type = fields.Function(
fields.Selection([('', '')] + TYPES, "Product Type"),
'on_change_with_product_type'
)
2026-08-14 20:54:23 -04:00
itbis_invoiced_line = _amount_field("ITBIS Invoiced")
itbis_withholding_invoiced_line = _amount_field("ITBIS Withholding")
isc_invoiced_line = _amount_field("ISC Invoiced")
cdt_invoiced_line = _amount_field("CDT Invoiced")
isr_withholding_invoiced_line = _amount_field("ISR Withholding")
tip_invoiced_line = _amount_field("Tip Invoiced")
line_total = _amount_field("Total", _LINE_TOTAL_STATES)
2026-07-22 22:29:04 -04:00
@fields.depends('type', 'product')
def on_change_with_product_type(self, name=None):
if self.type == 'line' and self.product:
return self.product.type
return ''
@classmethod
def _get_line_currency(cls, line):
invoice = getattr(line, 'invoice', None)
2026-08-14 20:54:23 -04:00
if invoice and getattr(invoice, 'currency', None):
2026-07-22 22:29:04 -04:00
return invoice.currency
return getattr(line, 'currency', None)
@classmethod
def _round_amount(cls, line, amount):
currency = cls._get_line_currency(line)
if currency:
return currency.round(amount)
return amount
@classmethod
def _get_base_line_amount(cls, line):
if line.type != 'line':
2026-08-14 20:54:23 -04:00
return _ZERO
compute = getattr(line, 'on_change_with_amount', None)
amount = compute() if compute else None
2026-07-22 22:29:04 -04:00
if amount is None:
amount = getattr(line, 'amount', None)
2026-08-14 20:54:23 -04:00
return amount or _ZERO
2026-07-22 22:29:04 -04:00
@classmethod
2026-08-14 20:54:23 -04:00
def _is_withholding_tax(cls, tax):
if not tax:
return False
if getattr(tax, 'type', None) == 'percentage':
return (tax.rate or _ZERO) < 0
if getattr(tax, 'type', None) == 'fixed':
return (tax.amount or _ZERO) < 0
return False
2026-07-22 22:29:04 -04:00
@classmethod
2026-08-14 20:54:23 -04:00
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)
2026-07-22 22:29:04 -04:00
if line.type == 'line':
base = cls._get_base_line_amount(line)
2026-08-14 20:54:23 -04:00
tax_total = _ZERO
if getattr(line, 'taxes', None):
for tax_line in line._get_taxes().values():
amount = tax_line.amount or _ZERO
tax_total += amount
kind = cls._tax_kind(tax_line)
if kind:
amounts[kind] += amount
line_total = base + tax_total
elif line.type == 'subtotal':
subtotal = _ZERO
2026-07-22 22:29:04 -04:00
invoice = getattr(line, 'invoice', None)
2026-08-14 20:54:23 -04:00
for invoice_line in getattr(invoice, 'lines', []):
if invoice_line.type == 'line':
subtotal += cls._compute_amounts(invoice_line)['line_total']
elif invoice_line.type == 'subtotal':
if line == invoice_line:
2026-07-22 22:29:04 -04:00
break
2026-08-14 20:54:23 -04:00
subtotal = _ZERO
line_total = subtotal
else:
line_total = _ZERO
amounts = {
kind: cls._round_amount(line, abs(amount))
for kind, amount in amounts.items()}
amounts['line_total'] = cls._round_amount(line, line_total)
return amounts
2026-07-22 22:29:04 -04:00
@classmethod
2026-08-14 20:54:23 -04:00
def get_amounts(cls, lines, names):
single = isinstance(names, str)
if single:
names = [names]
result = {name: {} for name in names}
for line in lines:
amounts = cls._compute_amounts(line)
for name in names:
result[name][line.id] = amounts.get(
_TAX_FIELD_KINDS.get(name, name), _ZERO)
return result[names[0]] if single else result
2026-07-22 22:29:04 -04:00
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
2026-07-22 22:40:12 -04:00
def on_change_with_itbis_invoiced_line(self, name=None):
2026-08-14 20:54:23 -04:00
return self._compute_amounts(self)['itbis']
2026-07-22 22:29:04 -04:00
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
2026-07-22 22:40:12 -04:00
def on_change_with_itbis_withholding_invoiced_line(self, name=None):
2026-08-14 20:54:23 -04:00
return self._compute_amounts(self)['itbis_withholding']
2026-07-22 22:29:04 -04:00
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
2026-07-22 22:40:12 -04:00
def on_change_with_isc_invoiced_line(self, name=None):
2026-08-14 20:54:23 -04:00
return self._compute_amounts(self)['isc']
2026-07-22 22:29:04 -04:00
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
2026-07-22 22:40:12 -04:00
def on_change_with_cdt_invoiced_line(self, name=None):
2026-08-14 20:54:23 -04:00
return self._compute_amounts(self)['cdt']
2026-07-22 22:29:04 -04:00
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
2026-07-22 22:40:12 -04:00
def on_change_with_isr_withholding_invoiced_line(self, name=None):
2026-08-14 20:54:23 -04:00
return self._compute_amounts(self)['isr_withholding']
2026-07-22 22:29:04 -04:00
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
2026-07-22 22:40:12 -04:00
def on_change_with_tip_invoiced_line(self, name=None):
2026-08-14 20:54:23 -04:00
return self._compute_amounts(self)['tip']
2026-07-22 22:29:04 -04:00
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_line_total(self, name=None):
2026-08-14 20:54:23 -04:00
return self._compute_amounts(self)['line_total']