# 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']