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
+155 -161
View File
@@ -1,31 +1,22 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from dateutil.relativedelta import relativedelta
from decimal import Decimal
from datetime import date
from trytond.model import fields
from trytond.pool import PoolMeta
from trytond.modules.product.product import TYPES
from trytond.pyson import Eval
from trytond.modules.currency.fields import Monetary
_ZERO = Decimal('0.00')
from trytond.model import fields
from trytond.modules.currency.fields import Monetary
from trytond.modules.product.product import TYPES
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.transaction import Transaction
_ZERO = Decimal(0)
class Invoice(metaclass=PoolMeta):
__name__ = 'account.invoice'
employee = fields.Many2One('company.employee', 'Salesman',
domain=[
('company', '=', Eval('company', -1)),
],
states={
'readonly': Eval('state') != 'draft',
},
depends=['state', 'company'])
due_date = fields.Function(fields.Date('Due Date'),
'get_remainder_time')
'get_due_date')
amount_paid = fields.Function(Monetary(
"Amount Paid", currency='currency', digits='currency'),
@@ -36,17 +27,6 @@ class Invoice(metaclass=PoolMeta):
days_overdue = fields.Function(fields.Integer('Days Overdue'),
'get_days_overdue')
party_reimbursement = fields.Many2One(
'party.party', 'Party Reimbursement',
states={
'invisible': Eval('type') != 'in',
'readonly': Eval('state') != 'draft',
},
context={
'company': Eval('company', -1),
},
depends=['company'])
@classmethod
def search_rec_name(cls, name, clause):
domain = super().search_rec_name(name, clause)
@@ -55,31 +35,49 @@ class Invoice(metaclass=PoolMeta):
]
def get_days_elapsed(self, name=None):
if self.state == 'paid' or not self.due_date or not self.invoice_date:
return None
days = (date.today() - self.invoice_date).days
return days if days > 0 else None
return self._positive_age(self.invoice_date)
def get_days_overdue(self, name=None):
if self.state == 'paid' or not self.due_date or not self.invoice_date:
return self._positive_age(self.due_date)
def _positive_age(self, reference_date):
if self.state == 'paid' or not reference_date:
return None
days = (date.today() - self.due_date).days
days = (self._today() - reference_date).days
return days if days > 0 else None
def get_remainder_time(self, name):
term = self.payment_term
if term and term.lines:
for line in term.lines:
if (not line.relativedeltas
or not self.invoice_date):
break
delta = line.relativedeltas[0]
return self.invoice_date + relativedelta(
months=delta.months,
weeks=delta.weeks,
days=delta.days)
def _today(self):
Date = Pool().get('ir.date')
company = self.company.id if self.company else None
with Transaction().set_context(company=company):
return Date.today()
def _compute_due_date(self):
maturity_date = max((
line.maturity_date for line in (self.lines_to_pay or [])
if line.maturity_date), default=None)
if maturity_date:
return maturity_date
payment_date = self.payment_term_date or self.invoice_date
if (self.payment_term and payment_date and self.currency
and self.total_amount is not None):
terms = self.payment_term.compute(
self.total_amount, self.currency, payment_date)
if terms:
return max(date for date, _ in terms)
return None
def get_due_date(self, name=None):
return self._compute_due_date()
@fields.depends(
'payment_term', 'payment_term_date', 'invoice_date', 'currency',
'total_amount', 'lines_to_pay',
methods=['_compute_due_date'])
def on_change_with_due_date(self, name=None):
return self._compute_due_date()
@classmethod
def get_amount_paid(cls, invoices, name):
res = {i.id: _ZERO for i in invoices}
@@ -88,7 +86,7 @@ class Invoice(metaclass=PoolMeta):
continue
total = invoice.total_amount or _ZERO
to_pay = invoice.amount_to_pay or _ZERO
paid = abs(total - to_pay)
paid = total - to_pay
if invoice.currency:
paid = invoice.currency.round(paid)
res[invoice.id] = paid
@@ -97,10 +95,14 @@ class Invoice(metaclass=PoolMeta):
@classmethod
def __setup__(cls):
super().__setup__()
# Keep the ordering criteria installed by account_invoice and other
# modules. Replacing the whole list would silently discard extension
# hooks such as the core invoice-number ordering.
cls._order = [
('invoice_date', 'DESC'),
('id', 'DESC'),
]
*(order for order in cls._order
if order[0] != 'invoice_date'),
]
_LINE_DEPENDS = [
@@ -116,6 +118,28 @@ _LINE_TOTAL_STATES = {
'invisible': ~Eval('type').in_(['line', 'subtotal']),
}
_TAX_FIELD_KINDS = {
'itbis_invoiced_line': 'itbis',
'itbis_withholding_invoiced_line': 'itbis_withholding',
'isc_invoiced_line': 'isc',
'cdt_invoiced_line': 'cdt',
'isr_withholding_invoiced_line': 'isr_withholding',
'tip_invoiced_line': 'tip',
}
_TAX_GROUP_KINDS = {
'ISC': 'isc',
'CDT': 'cdt',
'ISR': 'isr_withholding',
'ISREXT': 'isr_withholding',
}
def _amount_field(label, states=_LINE_STATES):
return fields.Function(Monetary(
label, currency='currency', digits='currency', states=states),
'get_amounts')
class InvoiceLine(metaclass=PoolMeta):
'Invoice Line'
@@ -126,47 +150,13 @@ class InvoiceLine(metaclass=PoolMeta):
'on_change_with_product_type'
)
itbis_invoiced_line = fields.Function(Monetary(
"ITBIS Invoiced", currency='currency', digits='currency',
states=_LINE_STATES),
'get_itbis_invoiced_line'
)
itbis_withholding_invoiced_line = fields.Function(Monetary(
"ITBIS Withholding", currency='currency', digits='currency',
states=_LINE_STATES),
'get_itbis_withholding_invoiced_line'
)
isc_invoiced_line = fields.Function(Monetary(
"ISC Invoiced", currency='currency', digits='currency',
states=_LINE_STATES),
'get_isc_invoiced_line'
)
cdt_invoiced_line = fields.Function(Monetary(
"CDT Invoiced", currency='currency', digits='currency',
states=_LINE_STATES),
'get_cdt_invoiced_line'
)
isr_withholding_invoiced_line = fields.Function(Monetary(
"ISR Withholding", currency='currency', digits='currency',
states=_LINE_STATES),
'get_isr_withholding_invoiced_line'
)
tip_invoiced_line = fields.Function(Monetary(
"Tip Invoiced", currency='currency', digits='currency',
states=_LINE_STATES),
'get_tip_invoiced_line'
)
line_total = fields.Function(Monetary(
"Total", currency='currency', digits='currency',
states=_LINE_TOTAL_STATES),
'get_line_total'
)
itbis_invoiced_line = _amount_field("ITBIS Invoiced")
itbis_withholding_invoiced_line = _amount_field("ITBIS Withholding")
isc_invoiced_line = _amount_field("ISC Invoiced")
cdt_invoiced_line = _amount_field("CDT Invoiced")
isr_withholding_invoiced_line = _amount_field("ISR Withholding")
tip_invoiced_line = _amount_field("Tip Invoiced")
line_total = _amount_field("Total", _LINE_TOTAL_STATES)
@fields.depends('type', 'product')
def on_change_with_product_type(self, name=None):
@@ -177,7 +167,7 @@ class InvoiceLine(metaclass=PoolMeta):
@classmethod
def _get_line_currency(cls, line):
invoice = getattr(line, 'invoice', None)
if invoice and hasattr(invoice, 'currency') and invoice.currency:
if invoice and getattr(invoice, 'currency', None):
return invoice.currency
return getattr(line, 'currency', None)
@@ -191,105 +181,109 @@ class InvoiceLine(metaclass=PoolMeta):
@classmethod
def _get_base_line_amount(cls, line):
if line.type != 'line':
return Decimal('0.0')
amount = None
if hasattr(line, 'on_change_with_amount'):
amount = line.on_change_with_amount()
return _ZERO
compute = getattr(line, 'on_change_with_amount', None)
amount = compute() if compute else None
if amount is None:
amount = getattr(line, 'amount', None)
return amount or Decimal('0.0')
return amount or _ZERO
@classmethod
def _compute_tax_kind_amount(cls, line, tax_kind):
if line.type != 'line':
return Decimal('0.0')
taxes = getattr(line, 'taxes', None)
if not taxes:
return Decimal('0.0')
base = cls._get_base_line_amount(line)
total = Decimal('0.0')
for tax in taxes:
if getattr(tax, 'tax_kind', None) == tax_kind:
rate = getattr(tax, 'rate', None) or Decimal('0.0')
total += base * Decimal(str(rate))
return cls._round_amount(line, abs(total))
def _is_withholding_tax(cls, tax):
if not tax:
return False
if getattr(tax, 'type', None) == 'percentage':
return (tax.rate or _ZERO) < 0
if getattr(tax, 'type', None) == 'fixed':
return (tax.amount or _ZERO) < 0
return False
@classmethod
def _compute_line_total_amount(cls, line):
def _tax_kind(cls, tax_line):
tax = getattr(tax_line, 'tax', None)
if not tax:
return None
group_code = getattr(getattr(tax, 'group', None), 'code', None)
if group_code == 'ITBIS':
return ('itbis_withholding'
if cls._is_withholding_tax(tax) else 'itbis')
if group_code in _TAX_GROUP_KINDS:
return _TAX_GROUP_KINDS[group_code]
if group_code == 'OTROS' and getattr(tax, 'template', None):
name = (tax.template.name or '').casefold()
if 'propina' in name or 'tip' in name:
return 'tip'
@classmethod
def _compute_amounts(cls, line):
amounts = dict.fromkeys(_TAX_FIELD_KINDS.values(), _ZERO)
if line.type == 'line':
base = cls._get_base_line_amount(line)
tax_total = Decimal('0.0')
for tax in (getattr(line, 'taxes', None) or []):
rate = getattr(tax, 'rate', None) or Decimal('0.0')
tax_total += base * Decimal(str(rate))
return cls._round_amount(line, base + tax_total)
if line.type == 'subtotal':
subtotal = Decimal('0.0')
tax_total = _ZERO
if getattr(line, 'taxes', None):
for tax_line in line._get_taxes().values():
amount = tax_line.amount or _ZERO
tax_total += amount
kind = cls._tax_kind(tax_line)
if kind:
amounts[kind] += amount
line_total = base + tax_total
elif line.type == 'subtotal':
subtotal = _ZERO
invoice = getattr(line, 'invoice', None)
if not invoice or not hasattr(invoice, 'lines'):
return subtotal
for line2 in invoice.lines:
if line2.type == 'line':
subtotal += cls._compute_line_total_amount(line2)
elif line2.type == 'subtotal':
if line == line2:
for invoice_line in getattr(invoice, 'lines', []):
if invoice_line.type == 'line':
subtotal += cls._compute_amounts(invoice_line)['line_total']
elif invoice_line.type == 'subtotal':
if line == invoice_line:
break
subtotal = Decimal('0.0')
return cls._round_amount(line, subtotal)
return Decimal('0.0')
subtotal = _ZERO
line_total = subtotal
else:
line_total = _ZERO
amounts = {
kind: cls._round_amount(line, abs(amount))
for kind, amount in amounts.items()}
amounts['line_total'] = cls._round_amount(line, line_total)
return amounts
@classmethod
def get_itbis_invoiced_line(cls, lines, name):
return {l.id: cls._compute_tax_kind_amount(l, 'itbis') for l in lines}
@classmethod
def get_itbis_withholding_invoiced_line(cls, lines, name):
return {l.id: cls._compute_tax_kind_amount(l, 'itbis_withholding') for l in lines}
@classmethod
def get_isc_invoiced_line(cls, lines, name):
return {l.id: cls._compute_tax_kind_amount(l, 'isc') for l in lines}
@classmethod
def get_cdt_invoiced_line(cls, lines, name):
return {l.id: cls._compute_tax_kind_amount(l, 'cdt') for l in lines}
@classmethod
def get_isr_withholding_invoiced_line(cls, lines, name):
return {l.id: cls._compute_tax_kind_amount(l, 'isr_withholding') for l in lines}
@classmethod
def get_tip_invoiced_line(cls, lines, name):
return {l.id: cls._compute_tax_kind_amount(l, 'tip') for l in lines}
@classmethod
def get_line_total(cls, lines, name):
return {l.id: cls._compute_line_total_amount(l) for l in lines}
def get_amounts(cls, lines, names):
single = isinstance(names, str)
if single:
names = [names]
result = {name: {} for name in names}
for line in lines:
amounts = cls._compute_amounts(line)
for name in names:
result[name][line.id] = amounts.get(
_TAX_FIELD_KINDS.get(name, name), _ZERO)
return result[names[0]] if single else result
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_itbis_invoiced_line(self, name=None):
return InvoiceLine._compute_tax_kind_amount(self, 'itbis')
return self._compute_amounts(self)['itbis']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_itbis_withholding_invoiced_line(self, name=None):
return InvoiceLine._compute_tax_kind_amount(self, 'itbis_withholding')
return self._compute_amounts(self)['itbis_withholding']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_isc_invoiced_line(self, name=None):
return InvoiceLine._compute_tax_kind_amount(self, 'isc')
return self._compute_amounts(self)['isc']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_cdt_invoiced_line(self, name=None):
return InvoiceLine._compute_tax_kind_amount(self, 'cdt')
return self._compute_amounts(self)['cdt']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_isr_withholding_invoiced_line(self, name=None):
return InvoiceLine._compute_tax_kind_amount(self, 'isr_withholding')
return self._compute_amounts(self)['isr_withholding']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_tip_invoiced_line(self, name=None):
return InvoiceLine._compute_tax_kind_amount(self, 'tip')
return self._compute_amounts(self)['tip']
@fields.depends(*_LINE_DEPENDS, methods=['on_change_with_amount'])
def on_change_with_line_total(self, name=None):
return InvoiceLine._compute_line_total_amount(self)
return self._compute_amounts(self)['line_total']