296 lines
10 KiB
Python
Executable File
296 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 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')
|
|
|
|
|
|
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')
|
|
|
|
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')
|
|
|
|
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)
|
|
return domain + [
|
|
('description', *clause[1:]),
|
|
]
|
|
|
|
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
|
|
|
|
def get_days_overdue(self, name=None):
|
|
if self.state == 'paid' or not self.due_date or not self.invoice_date:
|
|
return None
|
|
days = (date.today() - self.due_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)
|
|
return None
|
|
|
|
@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 = abs(total - to_pay)
|
|
if invoice.currency:
|
|
paid = invoice.currency.round(paid)
|
|
res[invoice.id] = paid
|
|
return res
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._order = [
|
|
('invoice_date', 'DESC'),
|
|
('id', 'DESC'),
|
|
]
|
|
|
|
|
|
_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']),
|
|
}
|
|
|
|
|
|
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 = 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'
|
|
)
|
|
|
|
@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 hasattr(invoice, 'currency') and invoice.currency:
|
|
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 Decimal('0.0')
|
|
amount = None
|
|
if hasattr(line, 'on_change_with_amount'):
|
|
amount = line.on_change_with_amount()
|
|
if amount is None:
|
|
amount = getattr(line, 'amount', None)
|
|
return amount or Decimal('0.0')
|
|
|
|
@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))
|
|
|
|
@classmethod
|
|
def _compute_line_total_amount(cls, line):
|
|
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')
|
|
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:
|
|
break
|
|
subtotal = Decimal('0.0')
|
|
return cls._round_amount(line, subtotal)
|
|
return Decimal('0.0')
|
|
|
|
@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}
|
|
|
|
@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')
|
|
|
|
@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')
|
|
|
|
@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')
|
|
|
|
@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')
|
|
|
|
@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')
|
|
|
|
@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')
|
|
|
|
@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)
|