361 lines
12 KiB
Python
361 lines
12 KiB
Python
# 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 collections import defaultdict
|
|
from itertools import groupby
|
|
|
|
from sql import Literal, Null
|
|
from sql.operators import Equal
|
|
|
|
from trytond.i18n import gettext
|
|
from trytond.model import (
|
|
Exclude, ModelSQL, ModelView, Workflow, dualmethod, fields)
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.pyson import Eval
|
|
from trytond.tools import sortable_values
|
|
from trytond.transaction import Transaction
|
|
|
|
from .exceptions import ClosePeriodWarning
|
|
|
|
|
|
def _tax_group(tax):
|
|
while tax.parent:
|
|
tax = tax.parent
|
|
return tax.group
|
|
|
|
|
|
class FiscalYear(metaclass=PoolMeta):
|
|
__name__ = 'account.fiscalyear'
|
|
|
|
tax_group_on_cash_basis = fields.Many2Many(
|
|
'account.tax.group.cash', 'fiscalyear', 'tax_group',
|
|
"Tax Group On Cash Basis",
|
|
help="The tax group reported on cash basis for this fiscal year.")
|
|
|
|
|
|
class Period(metaclass=PoolMeta):
|
|
__name__ = 'account.period'
|
|
|
|
tax_group_on_cash_basis = fields.Many2Many(
|
|
'account.tax.group.cash', 'period', 'tax_group',
|
|
"Tax Group On Cash Basis",
|
|
help="The tax group reported on cash basis for this period.")
|
|
|
|
def is_on_cash_basis(self, tax):
|
|
if not tax:
|
|
return False
|
|
group = _tax_group(tax)
|
|
return (group in self.tax_group_on_cash_basis
|
|
or group in self.fiscalyear.tax_group_on_cash_basis)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('closed')
|
|
def close(cls, periods):
|
|
pool = Pool()
|
|
MoveLine = pool.get('account.move.line')
|
|
Warning = pool.get('res.user.warning')
|
|
super().close(periods)
|
|
for period in periods:
|
|
if (period.tax_group_on_cash_basis
|
|
or period.fiscalyear.tax_group_on_cash_basis):
|
|
move_lines = MoveLine.search([
|
|
('move.period', '=', period.id),
|
|
('reconciliation', '=', None),
|
|
('invoice_payment', '=', None),
|
|
['OR', [
|
|
('account.type.receivable', '=', True),
|
|
('credit', '>', 0),
|
|
], [
|
|
('account.type.payable', '=', True),
|
|
('debit', '<', 0),
|
|
],
|
|
],
|
|
])
|
|
if move_lines:
|
|
warning_name = Warning.format(
|
|
'period_close_line_payment', move_lines)
|
|
if Warning.check(warning_name):
|
|
raise ClosePeriodWarning(warning_name,
|
|
gettext('account_tax_cash'
|
|
'.msg_close_period_line_payment',
|
|
period=period.rec_name))
|
|
|
|
|
|
class TaxGroupCash(ModelSQL):
|
|
__name__ = 'account.tax.group.cash'
|
|
|
|
fiscalyear = fields.Many2One(
|
|
'account.fiscalyear', "Fiscal Year", ondelete='CASCADE')
|
|
period = fields.Many2One(
|
|
'account.period', "Period", ondelete='CASCADE')
|
|
party = fields.Many2One(
|
|
'party.party', "Party", ondelete='CASCADE')
|
|
tax_group = fields.Many2One(
|
|
'account.tax.group', "Tax Group", ondelete='CASCADE', required=True)
|
|
|
|
|
|
class Tax(metaclass=PoolMeta):
|
|
__name__ = 'account.tax'
|
|
|
|
@classmethod
|
|
def _amount_where(cls, tax_line, move_line, move):
|
|
context = Transaction().context
|
|
periods = context.get('periods', [])
|
|
where = super()._amount_where(tax_line, move_line, move)
|
|
return ((where
|
|
& (tax_line.on_cash_basis == Literal(False))
|
|
| (tax_line.on_cash_basis == Null))
|
|
| ((tax_line.period.in_(periods) if periods else Literal(False))
|
|
& (tax_line.on_cash_basis == Literal(True))))
|
|
|
|
@classmethod
|
|
def _amount_domain(cls):
|
|
context = Transaction().context
|
|
periods = context.get('periods', [])
|
|
domain = super()._amount_domain()
|
|
return ['OR',
|
|
[domain,
|
|
('on_cash_basis', '=', False),
|
|
],
|
|
[('period', 'in', periods),
|
|
('on_cash_basis', '=', True),
|
|
]]
|
|
|
|
|
|
class TaxLine(metaclass=PoolMeta):
|
|
__name__ = 'account.tax.line'
|
|
|
|
on_cash_basis = fields.Boolean("On Cash Basis")
|
|
period = fields.Many2One('account.period', "Period",
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
states={
|
|
'invisible': ~Eval('on_cash_basis', False),
|
|
})
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
|
|
t = cls.__table__()
|
|
cls._sql_constraints = [
|
|
('tax_type_move_line_cash_basis_no_period',
|
|
Exclude(
|
|
t,
|
|
(t.tax, Equal),
|
|
(t.type, Equal),
|
|
(t.move_line, Equal),
|
|
where=(t.on_cash_basis == Literal(True))
|
|
& (t.period == Null)),
|
|
'account_tax_cash.'
|
|
'msg_tax_type_move_line_cash_basis_no_period_unique'),
|
|
]
|
|
|
|
@classmethod
|
|
def default_on_cash_basis(cls):
|
|
return False
|
|
|
|
@property
|
|
def period_checked(self):
|
|
period = super().period_checked
|
|
if self.on_cash_basis:
|
|
period = self.period
|
|
return period
|
|
|
|
@classmethod
|
|
def group_cash_basis_key(cls, line):
|
|
return (
|
|
('tax', line.tax),
|
|
('type', line.type),
|
|
('move_line', line.move_line),
|
|
('on_cash_basis', line.on_cash_basis),
|
|
)
|
|
|
|
@classmethod
|
|
def update_cash_basis(cls, lines, ratio, period):
|
|
if not lines:
|
|
return
|
|
to_save = []
|
|
lines = cls.browse(sorted(
|
|
lines, key=sortable_values(cls.group_cash_basis_key)))
|
|
for key, lines in groupby(lines, key=cls.group_cash_basis_key):
|
|
key = dict(key)
|
|
if not key['on_cash_basis']:
|
|
continue
|
|
lines = list(lines)
|
|
company = lines[0].company
|
|
line_no_periods = [l for l in lines if not l.period]
|
|
if line_no_periods:
|
|
line_no_period, = line_no_periods
|
|
else:
|
|
line_no_period = None
|
|
total = sum(l.amount for l in lines)
|
|
amount = total * ratio - sum(l.amount for l in lines if l.period)
|
|
amount = company.currency.round(amount)
|
|
if amount:
|
|
if line_no_period and line_no_period.amount == amount:
|
|
line_no_period.period = period
|
|
else:
|
|
line = cls(**key, amount=amount)
|
|
if line_no_period:
|
|
line_no_period.amount -= line.amount
|
|
line.period = period
|
|
if line.amount:
|
|
to_save.append(line)
|
|
if line_no_period:
|
|
to_save.append(line_no_period)
|
|
cls.save(to_save)
|
|
|
|
|
|
class Move(metaclass=PoolMeta):
|
|
__name__ = 'account.move'
|
|
|
|
@dualmethod
|
|
@ModelView.button
|
|
def post(cls, moves):
|
|
pool = Pool()
|
|
TaxLine = pool.get('account.tax.line')
|
|
super().post(moves)
|
|
|
|
tax_lines = []
|
|
for move in moves:
|
|
period = move.period
|
|
for line in move.lines:
|
|
for tax_line in line.tax_lines:
|
|
if (not tax_line.on_cash_basis
|
|
and period.is_on_cash_basis(tax_line.tax)):
|
|
tax_lines.append(tax_line)
|
|
TaxLine.write(tax_lines, {
|
|
'on_cash_basis': True,
|
|
})
|
|
|
|
|
|
class Invoice(metaclass=PoolMeta):
|
|
__name__ = 'account.invoice'
|
|
|
|
tax_group_on_cash_basis = fields.Many2Many(
|
|
'account.invoice.tax.group.cash', 'invoice', 'tax_group',
|
|
"Tax Group On Cash Basis",
|
|
states={
|
|
'invisible': Eval('type') != 'in',
|
|
},
|
|
help="The tax group reported on cash basis for this invoice.")
|
|
|
|
@fields.depends('party', 'type', 'tax_group_on_cash_basis')
|
|
def on_change_party(self):
|
|
super().on_change_party()
|
|
if self.type == 'in' and self.party:
|
|
self.tax_group_on_cash_basis = (
|
|
self.party.supplier_tax_group_on_cash_basis)
|
|
else:
|
|
self.tax_group_on_cash_basis = []
|
|
|
|
def get_move(self):
|
|
move = super().get_move()
|
|
if self.tax_group_on_cash_basis:
|
|
for line in move.lines:
|
|
for tax_line in getattr(line, 'tax_lines', []):
|
|
tax = tax_line.tax
|
|
if tax and _tax_group(tax) in self.tax_group_on_cash_basis:
|
|
tax_line.on_cash_basis = True
|
|
return move
|
|
|
|
@property
|
|
def cash_paid_ratio(self):
|
|
amount = sum(l.debit - l.credit for l in self.lines_to_pay)
|
|
if self.state == 'paid':
|
|
ratio = 1
|
|
elif self.state == 'cancelled':
|
|
ratio = 0
|
|
else:
|
|
payment_amount = sum(
|
|
l.debit - l.credit for l in self.payment_lines
|
|
if not l.reconciliation)
|
|
payment_amount -= sum(
|
|
l.debit - l.credit for l in self.lines_to_pay
|
|
if l.reconciliation)
|
|
if amount:
|
|
ratio = abs(payment_amount / amount)
|
|
else:
|
|
ratio = 0
|
|
assert 0 <= ratio <= 1
|
|
return ratio
|
|
|
|
@classmethod
|
|
def on_modification(cls, mode, invoices, field_names=None):
|
|
super().on_modification(mode, invoices, field_names=field_names)
|
|
if mode == 'write' and 'payment_lines' in field_names:
|
|
cls._update_tax_cash_basis(invoices)
|
|
|
|
@classmethod
|
|
def process(cls, invoices):
|
|
super().process(invoices)
|
|
cls._update_tax_cash_basis(invoices)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('cancelled')
|
|
def cancel(cls, invoices):
|
|
super().cancel(invoices)
|
|
cls._update_tax_cash_basis(invoices)
|
|
|
|
@classmethod
|
|
def _update_tax_cash_basis(cls, invoices):
|
|
pool = Pool()
|
|
TaxLine = pool.get('account.tax.line')
|
|
Date = pool.get('ir.date')
|
|
Period = pool.get('account.period')
|
|
|
|
# Call update_cash_basis grouped per period and ratio only because
|
|
# group_cash_basis_key already group per move_line.
|
|
to_update = defaultdict(list)
|
|
periods = {}
|
|
for invoice in invoices:
|
|
if not invoice.move:
|
|
continue
|
|
if invoice.company not in periods:
|
|
with Transaction().set_context(company=invoice.company.id):
|
|
date = Transaction().context.get(
|
|
'payment_date', Date.today())
|
|
periods[invoice.company] = Period.find(
|
|
invoice.company, date=date)
|
|
period = periods[invoice.company]
|
|
ratio = invoice.cash_paid_ratio
|
|
for line in invoice.move.lines:
|
|
to_update[(period, ratio)].extend(line.tax_lines)
|
|
for (period, ratio), tax_lines in to_update.items():
|
|
TaxLine.update_cash_basis(tax_lines, ratio, period)
|
|
|
|
|
|
class InvoiceTax(metaclass=PoolMeta):
|
|
__name__ = 'account.invoice.tax'
|
|
|
|
@property
|
|
def on_cash_basis(self):
|
|
pool = Pool()
|
|
Period = pool.get('account.period')
|
|
if self.invoice and self.tax:
|
|
if self.tax.group in self.invoice.tax_group_on_cash_basis:
|
|
return True
|
|
if self.invoice.move:
|
|
period = self.invoice.move.period
|
|
else:
|
|
accounting_date = (
|
|
self.invoice.accounting_date or self.invoice.invoice_date)
|
|
period = Period.find(
|
|
self.invoice.company, date=accounting_date)
|
|
return period.is_on_cash_basis(self.tax)
|
|
|
|
|
|
class InvoiceTaxGroupCash(ModelSQL):
|
|
__name__ = 'account.invoice.tax.group.cash'
|
|
|
|
invoice = fields.Many2One(
|
|
'account.invoice', "Invoice", ondelete='CASCADE',
|
|
domain=[
|
|
('type', '=', 'in'),
|
|
])
|
|
tax_group = fields.Many2One(
|
|
'account.tax.group', "Tax Group", ondelete='CASCADE', required=True)
|