Files
José Arturo García e2340bd5bf
Publish Package / publish (push) Successful in 1m57s
Migrate account invoice DO to Tryton 8.0
2026-08-14 20:54:23 -04:00

290 lines
10 KiB
Python
Executable File

# 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
from trytond.model import fields
from trytond.modules.currency.fields import Monetary
from trytond.modules.product.product import TYPES
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.transaction import Transaction
_ZERO = Decimal(0)
class Invoice(metaclass=PoolMeta):
__name__ = 'account.invoice'
due_date = fields.Function(fields.Date('Due Date'),
'get_due_date')
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):
return self._positive_age(self.invoice_date)
def get_days_overdue(self, name=None):
return self._positive_age(self.due_date)
def _positive_age(self, reference_date):
if self.state == 'paid' or not reference_date:
return None
days = (self._today() - reference_date).days
return days if days > 0 else None
def _today(self):
Date = Pool().get('ir.date')
company = self.company.id if self.company else None
with Transaction().set_context(company=company):
return Date.today()
def _compute_due_date(self):
maturity_date = max((
line.maturity_date for line in (self.lines_to_pay or [])
if line.maturity_date), default=None)
if maturity_date:
return maturity_date
payment_date = self.payment_term_date or self.invoice_date
if (self.payment_term and payment_date and self.currency
and self.total_amount is not None):
terms = self.payment_term.compute(
self.total_amount, self.currency, payment_date)
if terms:
return max(date for date, _ in terms)
return None
def get_due_date(self, name=None):
return self._compute_due_date()
@fields.depends(
'payment_term', 'payment_term_date', 'invoice_date', 'currency',
'total_amount', 'lines_to_pay',
methods=['_compute_due_date'])
def on_change_with_due_date(self, name=None):
return self._compute_due_date()
@classmethod
def get_amount_paid(cls, invoices, name):
res = {i.id: _ZERO for i in invoices}
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
paid = total - to_pay
if invoice.currency:
paid = invoice.currency.round(paid)
res[invoice.id] = paid
return res
@classmethod
def __setup__(cls):
super().__setup__()
# Keep the ordering criteria installed by account_invoice and other
# modules. Replacing the whole list would silently discard extension
# hooks such as the core invoice-number ordering.
cls._order = [
('invoice_date', 'DESC'),
*(order for order in cls._order
if order[0] != 'invoice_date'),
]
_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']),
}
_TAX_FIELD_KINDS = {
'itbis_invoiced_line': 'itbis',
'itbis_withholding_invoiced_line': 'itbis_withholding',
'isc_invoiced_line': 'isc',
'cdt_invoiced_line': 'cdt',
'isr_withholding_invoiced_line': 'isr_withholding',
'tip_invoiced_line': 'tip',
}
_TAX_GROUP_KINDS = {
'ISC': 'isc',
'CDT': 'cdt',
'ISR': 'isr_withholding',
'ISREXT': 'isr_withholding',
}
def _amount_field(label, states=_LINE_STATES):
return fields.Function(Monetary(
label, currency='currency', digits='currency', states=states),
'get_amounts')
class InvoiceLine(metaclass=PoolMeta):
'Invoice Line'
__name__ = 'account.invoice.line'
product_type = fields.Function(
fields.Selection([('', '')] + TYPES, "Product Type"),
'on_change_with_product_type'
)
itbis_invoiced_line = _amount_field("ITBIS Invoiced")
itbis_withholding_invoiced_line = _amount_field("ITBIS Withholding")
isc_invoiced_line = _amount_field("ISC Invoiced")
cdt_invoiced_line = _amount_field("CDT Invoiced")
isr_withholding_invoiced_line = _amount_field("ISR Withholding")
tip_invoiced_line = _amount_field("Tip Invoiced")
line_total = _amount_field("Total", _LINE_TOTAL_STATES)
@fields.depends('type', 'product')
def on_change_with_product_type(self, name=None):
if self.type == 'line' and self.product:
return self.product.type
return ''
@classmethod
def _get_line_currency(cls, line):
invoice = getattr(line, 'invoice', None)
if invoice and getattr(invoice, 'currency', None):
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':
return _ZERO
compute = getattr(line, 'on_change_with_amount', None)
amount = compute() if compute else None
if amount is None:
amount = getattr(line, 'amount', None)
return amount or _ZERO
@classmethod
def _is_withholding_tax(cls, tax):
if not tax:
return False
if getattr(tax, 'type', None) == 'percentage':
return (tax.rate or _ZERO) < 0
if getattr(tax, 'type', None) == 'fixed':
return (tax.amount or _ZERO) < 0
return False
@classmethod
def _tax_kind(cls, tax_line):
tax = getattr(tax_line, 'tax', None)
if not tax:
return None
group_code = getattr(getattr(tax, 'group', None), 'code', None)
if group_code == 'ITBIS':
return ('itbis_withholding'
if cls._is_withholding_tax(tax) else 'itbis')
if group_code in _TAX_GROUP_KINDS:
return _TAX_GROUP_KINDS[group_code]
if group_code == 'OTROS' and getattr(tax, 'template', None):
name = (tax.template.name or '').casefold()
if 'propina' in name or 'tip' in name:
return 'tip'
@classmethod
def _compute_amounts(cls, line):
amounts = dict.fromkeys(_TAX_FIELD_KINDS.values(), _ZERO)
if line.type == 'line':
base = cls._get_base_line_amount(line)
tax_total = _ZERO
if getattr(line, 'taxes', None):
for tax_line in line._get_taxes().values():
amount = tax_line.amount or _ZERO
tax_total += amount
kind = cls._tax_kind(tax_line)
if kind:
amounts[kind] += amount
line_total = base + tax_total
elif line.type == 'subtotal':
subtotal = _ZERO
invoice = getattr(line, 'invoice', None)
for invoice_line in getattr(invoice, 'lines', []):
if invoice_line.type == 'line':
subtotal += cls._compute_amounts(invoice_line)['line_total']
elif invoice_line.type == 'subtotal':
if line == invoice_line:
break
subtotal = _ZERO
line_total = subtotal
else:
line_total = _ZERO
amounts = {
kind: cls._round_amount(line, abs(amount))
for kind, amount in amounts.items()}
amounts['line_total'] = cls._round_amount(line, line_total)
return amounts
@classmethod
def get_amounts(cls, lines, names):
single = isinstance(names, str)
if single:
names = [names]
result = {name: {} for name in names}
for line in lines:
amounts = cls._compute_amounts(line)
for name in names:
result[name][line.id] = amounts.get(
_TAX_FIELD_KINDS.get(name, name), _ZERO)
return result[names[0]] if single else result
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_itbis_invoiced_line(self, name=None):
return self._compute_amounts(self)['itbis']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_itbis_withholding_invoiced_line(self, name=None):
return self._compute_amounts(self)['itbis_withholding']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_isc_invoiced_line(self, name=None):
return self._compute_amounts(self)['isc']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_cdt_invoiced_line(self, name=None):
return self._compute_amounts(self)['cdt']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_isr_withholding_invoiced_line(self, name=None):
return self._compute_amounts(self)['isr_withholding']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_tip_invoiced_line(self, name=None):
return self._compute_amounts(self)['tip']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_line_total(self, name=None):
return self._compute_amounts(self)['line_total']