This commit is contained in:
+155
-161
@@ -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']
|
||||
|
||||
Reference in New Issue
Block a user