first commit
This commit is contained in:
360
modules/account_tax_cash/account.py
Normal file
360
modules/account_tax_cash/account.py
Normal file
@@ -0,0 +1,360 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user