290 lines
10 KiB
Python
Executable File
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']
|