1943 lines
70 KiB
Python
1943 lines
70 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.
|
|
import copy
|
|
import datetime
|
|
from collections import defaultdict, namedtuple
|
|
from decimal import Decimal
|
|
from itertools import cycle, groupby
|
|
|
|
from sql import Literal
|
|
from sql.aggregate import Sum
|
|
from sql.conditionals import Case
|
|
|
|
from trytond import backend
|
|
from trytond.i18n import gettext
|
|
from trytond.model import (
|
|
DeactivableMixin, Index, MatchMixin, ModelSQL, ModelView, fields,
|
|
sequence_ordered, tree)
|
|
from trytond.model.exceptions import AccessError
|
|
from trytond.modules.currency.fields import Monetary
|
|
from trytond.pool import Pool
|
|
from trytond.pyson import Bool, Eval, If, PYSONEncoder
|
|
from trytond.tools import (
|
|
cursor_dict, is_full_text, lstrip_wildcard, sqlite_apply_types)
|
|
from trytond.transaction import Transaction
|
|
from trytond.wizard import Button, StateAction, StateView, Wizard
|
|
|
|
from .common import ActivePeriodMixin, ContextCompanyMixin, PeriodMixin
|
|
from .exceptions import PeriodNotFoundError
|
|
|
|
KINDS = [
|
|
('sale', 'Sale'),
|
|
('purchase', 'Purchase'),
|
|
('both', 'Both'),
|
|
]
|
|
|
|
|
|
class TaxGroup(ModelSQL, ModelView):
|
|
__name__ = 'account.tax.group'
|
|
name = fields.Char('Name', size=None, required=True)
|
|
code = fields.Char('Code', size=None, required=True)
|
|
kind = fields.Selection(KINDS, 'Kind', required=True)
|
|
|
|
@staticmethod
|
|
def default_kind():
|
|
return 'both'
|
|
|
|
|
|
class TaxCodeTemplate(PeriodMixin, tree(), ModelSQL, ModelView):
|
|
__name__ = 'account.tax.code.template'
|
|
name = fields.Char('Name', required=True)
|
|
code = fields.Char('Code')
|
|
parent = fields.Many2One('account.tax.code.template', 'Parent')
|
|
lines = fields.One2Many('account.tax.code.line.template', 'code', "Lines")
|
|
childs = fields.One2Many('account.tax.code.template', 'parent', 'Children')
|
|
account = fields.Many2One('account.account.template', 'Account Template',
|
|
domain=[('parent', '=', None)], required=True)
|
|
description = fields.Text('Description')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._order.insert(0, ('code', 'ASC'))
|
|
cls._order.insert(0, ('account', 'ASC'))
|
|
|
|
def _get_tax_code_value(self, code=None):
|
|
'''
|
|
Set values for tax code creation.
|
|
'''
|
|
res = {}
|
|
if not code or code.name != self.name:
|
|
res['name'] = self.name
|
|
if not code or code.code != self.code:
|
|
res['code'] = self.code
|
|
if not code or code.start_date != self.start_date:
|
|
res['start_date'] = self.start_date
|
|
if not code or code.end_date != self.end_date:
|
|
res['end_date'] = self.end_date
|
|
if not code or code.description != self.description:
|
|
res['description'] = self.description
|
|
if not code or code.template.id != self.id:
|
|
res['template'] = self.id
|
|
return res
|
|
|
|
@classmethod
|
|
def create_tax_code(cls, account_id, company_id, template2tax_code=None):
|
|
'''
|
|
Create recursively tax codes based on template.
|
|
template2tax_code is a dictionary with tax code template id as key and
|
|
tax code id as value, used to convert template id into tax code. The
|
|
dictionary is filled with new tax codes.
|
|
'''
|
|
pool = Pool()
|
|
TaxCode = pool.get('account.tax.code')
|
|
|
|
if template2tax_code is None:
|
|
template2tax_code = {}
|
|
|
|
def create(templates):
|
|
values = []
|
|
created = []
|
|
for template in templates:
|
|
if template.id not in template2tax_code:
|
|
vals = template._get_tax_code_value()
|
|
vals['company'] = company_id
|
|
if template.parent:
|
|
vals['parent'] = template2tax_code[template.parent.id]
|
|
else:
|
|
vals['parent'] = None
|
|
values.append(vals)
|
|
created.append(template)
|
|
|
|
tax_codes = TaxCode.create(values)
|
|
for template, tax_code in zip(created, tax_codes):
|
|
template2tax_code[template.id] = tax_code.id
|
|
|
|
childs = cls.search([
|
|
('account', '=', account_id),
|
|
('parent', '=', None),
|
|
])
|
|
while childs:
|
|
create(childs)
|
|
childs = sum((c.childs for c in childs), ())
|
|
|
|
|
|
class TaxCode(
|
|
ContextCompanyMixin, ActivePeriodMixin, tree(), ModelSQL, ModelView):
|
|
__name__ = 'account.tax.code'
|
|
_states = {
|
|
'readonly': (Bool(Eval('template', -1))
|
|
& ~Eval('template_override', False)),
|
|
}
|
|
name = fields.Char('Name', required=True, states=_states)
|
|
code = fields.Char('Code', states=_states)
|
|
company = fields.Many2One('company.company', 'Company', required=True)
|
|
parent = fields.Many2One(
|
|
'account.tax.code', "Parent", ondelete='RESTRICT',
|
|
states=_states,
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
])
|
|
lines = fields.One2Many('account.tax.code.line', 'code', "Lines")
|
|
childs = fields.One2Many(
|
|
'account.tax.code', 'parent', "Children",
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
])
|
|
currency = fields.Function(fields.Many2One('currency.currency',
|
|
'Currency'), 'on_change_with_currency')
|
|
amount = fields.Function(Monetary(
|
|
"Amount", currency='currency', digits='currency'),
|
|
'get_amount')
|
|
template = fields.Many2One('account.tax.code.template', 'Template')
|
|
template_override = fields.Boolean('Override Template',
|
|
help="Check to override template definition",
|
|
states={
|
|
'invisible': ~Bool(Eval('template', -1)),
|
|
})
|
|
description = fields.Text('Description')
|
|
del _states
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
cls.code.search_unaccented = False
|
|
super().__setup__()
|
|
t = cls.__table__()
|
|
cls._sql_indexes.add(
|
|
Index(t, (t.code, Index.Similarity())))
|
|
for date in [cls.start_date, cls.end_date]:
|
|
date.states = {
|
|
'readonly': (Bool(Eval('template', -1))
|
|
& ~Eval('template_override', False)),
|
|
}
|
|
cls._order.insert(0, ('code', 'ASC'))
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@classmethod
|
|
def default_template_override(cls):
|
|
return False
|
|
|
|
@fields.depends('company')
|
|
def on_change_with_currency(self, name=None):
|
|
return self.company.currency if self.company else None
|
|
|
|
@classmethod
|
|
def get_amount(cls, codes, name):
|
|
result = {}
|
|
|
|
parents = {}
|
|
childs = cls.search([
|
|
('parent', 'child_of', [c.id for c in codes]),
|
|
])
|
|
for code in childs:
|
|
result[code.id] = code.currency.round(
|
|
sum((l.value for l in code.lines), Decimal(0)))
|
|
parents[code.id] = code.parent.id if code.parent else None
|
|
|
|
ids = set(map(int, childs))
|
|
leafs = ids - set(parents.values())
|
|
while leafs:
|
|
for code in leafs:
|
|
ids.remove(code)
|
|
parent = parents.get(code)
|
|
if parent in result:
|
|
result[parent] += result[code]
|
|
next_leafs = set(ids)
|
|
for code in ids:
|
|
parent = parents.get(code)
|
|
if not parent:
|
|
continue
|
|
if parent in next_leafs and parent in ids:
|
|
next_leafs.remove(parent)
|
|
leafs = next_leafs
|
|
return result
|
|
|
|
def get_rec_name(self, name):
|
|
if self.code:
|
|
return self.code + ' - ' + self.name
|
|
else:
|
|
return self.name
|
|
|
|
@classmethod
|
|
def search_rec_name(cls, name, clause):
|
|
_, operator, operand, *extra = clause
|
|
if operator.startswith('!') or operator.startswith('not '):
|
|
bool_op = 'AND'
|
|
else:
|
|
bool_op = 'OR'
|
|
code_value = operand
|
|
if operator.endswith('like') and is_full_text(operand):
|
|
code_value = lstrip_wildcard(operand)
|
|
return [bool_op,
|
|
('code', operator, code_value, *extra),
|
|
(cls._rec_name, operator, operand, *extra),
|
|
]
|
|
|
|
@classmethod
|
|
def copy(cls, codes, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
default.setdefault('template', None)
|
|
return super().copy(codes, default=default)
|
|
|
|
@classmethod
|
|
def update_tax_code(cls, company_id, template2tax_code=None):
|
|
'''
|
|
Update recursively tax code based on template.
|
|
template2tax_code is a dictionary with tax code template id as key and
|
|
tax code id as value, used to convert template id into tax code. The
|
|
dictionary is filled with new tax codes
|
|
'''
|
|
if template2tax_code is None:
|
|
template2tax_code = {}
|
|
|
|
values = []
|
|
childs = cls.search([
|
|
('company', '=', company_id),
|
|
('parent', '=', None),
|
|
])
|
|
while childs:
|
|
for child in childs:
|
|
if child.template:
|
|
if not child.template_override:
|
|
vals = child.template._get_tax_code_value(code=child)
|
|
if vals:
|
|
values.append([child])
|
|
values.append(vals)
|
|
template2tax_code[child.template.id] = child.id
|
|
childs = sum((c.childs for c in childs), ())
|
|
if values:
|
|
cls.write(*values)
|
|
|
|
# Update parent
|
|
to_save = []
|
|
childs = cls.search([
|
|
('company', '=', company_id),
|
|
('parent', '=', None),
|
|
])
|
|
while childs:
|
|
for child in childs:
|
|
if child.template:
|
|
if not child.template_override:
|
|
if child.template.parent:
|
|
parent = template2tax_code.get(
|
|
child.template.parent.id)
|
|
else:
|
|
parent = None
|
|
old_parent = (
|
|
child.parent.id if child.parent else None)
|
|
if parent != old_parent:
|
|
child.parent = parent
|
|
to_save.append(child)
|
|
childs = sum((c.childs for c in childs), ())
|
|
cls.save(to_save)
|
|
|
|
|
|
class TaxCodeLineTemplate(ModelSQL, ModelView):
|
|
__name__ = 'account.tax.code.line.template'
|
|
|
|
code = fields.Many2One(
|
|
'account.tax.code.template', "Code", required=True, ondelete='CASCADE')
|
|
operator = fields.Selection([
|
|
('+', "+"),
|
|
('-', "-"),
|
|
], "Operator", required=True)
|
|
tax = fields.Many2One(
|
|
'account.tax.template', "Tax", required=True, ondelete='CASCADE')
|
|
amount = fields.Selection([
|
|
('tax', "Tax"),
|
|
('base', "Base"),
|
|
], "Amount", required=True)
|
|
type = fields.Selection([
|
|
('invoice', "Invoice"),
|
|
('credit', "Credit"),
|
|
], "Type", required=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('code')
|
|
|
|
def _get_tax_code_line_value(self, line=None):
|
|
value = {}
|
|
for name in ['operator', 'amount', 'type']:
|
|
if not line or getattr(line, name) != getattr(self, name):
|
|
value[name] = getattr(self, name)
|
|
if not line or line.template != self:
|
|
value['template'] = self.id
|
|
return value
|
|
|
|
@classmethod
|
|
def create_tax_code_line(cls, account_id, template2tax, template2tax_code,
|
|
template2tax_code_line=None):
|
|
"Create tax code lines based on template"
|
|
pool = Pool()
|
|
TaxCodeLine = pool.get('account.tax.code.line')
|
|
|
|
if template2tax_code_line is None:
|
|
template2tax_code_line = {}
|
|
|
|
values = []
|
|
created = []
|
|
for template in cls.search([('code.account', '=', account_id)]):
|
|
if template.id not in template2tax_code_line:
|
|
value = template._get_tax_code_line_value()
|
|
value['code'] = template2tax_code.get(template.code.id)
|
|
value['tax'] = template2tax.get(template.tax.id)
|
|
values.append(value)
|
|
created.append(template)
|
|
|
|
lines = TaxCodeLine.create(values)
|
|
for template, line in zip(created, lines):
|
|
template2tax_code_line[template.id] = line.id
|
|
|
|
|
|
class TaxCodeLine(ModelSQL, ModelView):
|
|
__name__ = 'account.tax.code.line'
|
|
_states = {
|
|
'readonly': (Bool(Eval('template', -1))
|
|
& ~Eval('template_override', False)),
|
|
}
|
|
|
|
code = fields.Many2One('account.tax.code', "Code", required=True)
|
|
operator = fields.Selection([
|
|
('+', "+"),
|
|
('-', "-"),
|
|
], "Operator", required=True, states=_states)
|
|
tax = fields.Many2One(
|
|
'account.tax', "Tax", required=True, states=_states,
|
|
domain=[
|
|
('company', '=', Eval('_parent_code', {}).get('company', -1)),
|
|
],
|
|
depends={'code'})
|
|
amount = fields.Selection([
|
|
('tax', "Tax"),
|
|
('base', "Base"),
|
|
], "Amount", required=True, states=_states)
|
|
type = fields.Selection([
|
|
('invoice', "Invoice"),
|
|
('credit', "Credit"),
|
|
], "Type", required=True, states=_states)
|
|
|
|
template = fields.Many2One('account.tax.code.line.template', "Template")
|
|
template_override = fields.Boolean('Override Template',
|
|
help="Check to override template definition",
|
|
states={
|
|
'invisible': ~Bool(Eval('template', -1)),
|
|
})
|
|
|
|
del _states
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('code')
|
|
|
|
@classmethod
|
|
def default_operator(cls):
|
|
return '+'
|
|
|
|
@property
|
|
def value(self):
|
|
value = getattr(self.tax, '%s_%s_amount' % (self.type, self.amount))
|
|
if self.type == 'credit':
|
|
value *= -1
|
|
if self.operator == '-':
|
|
value *= -1
|
|
return value
|
|
|
|
@property
|
|
def _line_domain(self):
|
|
domain = [
|
|
('tax', '=', self.tax.id),
|
|
('type', '=', self.amount),
|
|
]
|
|
if self.type == 'invoice':
|
|
domain.append(['OR',
|
|
[('amount', '>', 0), ['OR',
|
|
('move_line.debit', '>', 0),
|
|
('move_line.credit', '>', 0),
|
|
]],
|
|
[('amount', '<', 0), ['OR',
|
|
('move_line.debit', '<', 0),
|
|
('move_line.credit', '<', 0),
|
|
]],
|
|
])
|
|
elif self.type == 'credit':
|
|
domain.append(['OR',
|
|
[('amount', '<', 0), ['OR',
|
|
('move_line.debit', '>', 0),
|
|
('move_line.credit', '>', 0),
|
|
]],
|
|
[('amount', '>', 0), ['OR',
|
|
('move_line.debit', '<', 0),
|
|
('move_line.credit', '<', 0),
|
|
]],
|
|
])
|
|
return domain
|
|
|
|
@classmethod
|
|
def update_tax_code_line(cls, company_id, template2tax, template2tax_code,
|
|
template2tax_code_line=None):
|
|
"Update tax code lines based on template."
|
|
if template2tax_code_line is None:
|
|
template2tax_code_line = {}
|
|
|
|
values = []
|
|
for line in cls.search([('tax.company', '=', company_id)]):
|
|
if line.template:
|
|
if not line.template_override:
|
|
template = line.template
|
|
value = template._get_tax_code_line_value(line=line)
|
|
if line.code.id != template2tax_code.get(template.code.id):
|
|
value['code'] = template2tax_code.get(template.code.id)
|
|
if line.tax.id != template2tax.get(template.tax.id):
|
|
value['tax'] = template2tax.get(template.tax.id)
|
|
if value:
|
|
values.append([line])
|
|
values.append(value)
|
|
template2tax_code_line[line.template.id] = line.id
|
|
if values:
|
|
cls.write(*values)
|
|
|
|
|
|
class TaxCodeContext(ModelView):
|
|
__name__ = 'account.tax.code.context'
|
|
|
|
company = fields.Many2One('company.company', "Company", required=True)
|
|
method = fields.Selection([
|
|
('fiscalyear', "By Fiscal Year"),
|
|
('period', "By Period"),
|
|
('periods', "Over Periods"),
|
|
], 'Method', required=True)
|
|
|
|
fiscalyear = fields.Many2One(
|
|
'account.fiscalyear', "Fiscal Year",
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
],
|
|
states={
|
|
'invisible': Eval('method') != 'fiscalyear',
|
|
'required': Eval('method') == 'fiscalyear',
|
|
})
|
|
|
|
period = fields.Many2One(
|
|
'account.period', "Period",
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
('type', '=', 'standard'),
|
|
],
|
|
states={
|
|
'invisible': Eval('method') != 'period',
|
|
'required': Eval('method') == 'period',
|
|
})
|
|
|
|
start_period = fields.Many2One(
|
|
'account.period', "Start Period",
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
('type', '=', 'standard'),
|
|
('start_date', '<=', (Eval('end_period'), 'start_date')),
|
|
],
|
|
states={
|
|
'invisible': Eval('method') != 'periods',
|
|
'required': Eval('method') == 'periods',
|
|
})
|
|
end_period = fields.Many2One(
|
|
'account.period', "End Period",
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
('type', '=', 'standard'),
|
|
('start_date', '>=', (Eval('start_period'), 'start_date')),
|
|
],
|
|
states={
|
|
'invisible': Eval('method') != 'periods',
|
|
'required': Eval('method') == 'periods',
|
|
})
|
|
|
|
periods = fields.Many2Many(
|
|
'account.period', None, None, "Periods",
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
('type', '=', 'standard'),
|
|
])
|
|
|
|
@classmethod
|
|
def default_company(cls):
|
|
return Transaction().context.get('company')
|
|
|
|
@fields.depends(
|
|
'company', 'fiscalyear', 'period', 'periods',
|
|
methods=['on_change_with_periods'])
|
|
def on_change_company(self):
|
|
if self.fiscalyear and self.fiscalyear.company != self.company:
|
|
self.fiscalyear = None
|
|
if self.period and self.period.company != self.company:
|
|
self.period = None
|
|
self.periods = self.on_change_with_periods()
|
|
|
|
@classmethod
|
|
def default_method(cls):
|
|
return 'period'
|
|
|
|
@classmethod
|
|
def default_period(cls):
|
|
pool = Pool()
|
|
Period = pool.get('account.period')
|
|
try:
|
|
period = Period.find(cls.default_company(), test_state=False)
|
|
except PeriodNotFoundError:
|
|
return None
|
|
return period.id
|
|
|
|
@fields.depends(
|
|
'method', 'company',
|
|
'fiscalyear', 'period', 'start_period', 'end_period')
|
|
def on_change_with_periods(self):
|
|
pool = Pool()
|
|
Period = pool.get('account.period')
|
|
periods = []
|
|
if self.method == 'fiscalyear' and self.fiscalyear:
|
|
periods.extend(
|
|
p for p in self.fiscalyear.periods if p.type == 'standard')
|
|
elif self.method == 'period' and self.period:
|
|
periods.append(self.period)
|
|
elif (self.method == 'periods'
|
|
and self.company
|
|
and self.start_period and self.end_period):
|
|
periods = Period.search([
|
|
('start_date', '>=', self.start_period.start_date),
|
|
('start_date', '<=', self.end_period.start_date),
|
|
('company', '=', self.company.id),
|
|
('type', '=', 'standard'),
|
|
])
|
|
return periods
|
|
|
|
|
|
class TaxTemplate(sequence_ordered(), ModelSQL, ModelView, DeactivableMixin):
|
|
__name__ = 'account.tax.template'
|
|
name = fields.Char('Name', required=True)
|
|
description = fields.Char('Description', required=True)
|
|
group = fields.Many2One(
|
|
'account.tax.group', 'Group',
|
|
states={
|
|
'invisible': Bool(Eval('parent')),
|
|
})
|
|
start_date = fields.Date("Start Date")
|
|
end_date = fields.Date("End Date")
|
|
amount = fields.Numeric(
|
|
"Amount", digits=(None, 8),
|
|
states={
|
|
'required': Eval('type') == 'fixed',
|
|
'invisible': Eval('type') != 'fixed',
|
|
})
|
|
rate = fields.Numeric(
|
|
"Rate", digits=(None, 10),
|
|
states={
|
|
'required': Eval('type') == 'percentage',
|
|
'invisible': Eval('type') != 'percentage',
|
|
})
|
|
type = fields.Selection([
|
|
('percentage', 'Percentage'),
|
|
('fixed', 'Fixed'),
|
|
('none', 'None'),
|
|
], 'Type', required=True)
|
|
update_unit_price = fields.Boolean('Update Unit Price',
|
|
states={
|
|
'invisible': Bool(Eval('parent')),
|
|
})
|
|
parent = fields.Many2One('account.tax.template', 'Parent')
|
|
childs = fields.One2Many('account.tax.template', 'parent', 'Children')
|
|
invoice_account = fields.Many2One(
|
|
'account.account.template', 'Invoice Account',
|
|
domain=[
|
|
('type.statement', '=', 'balance'),
|
|
('closed', '!=', True),
|
|
],
|
|
states={
|
|
'required': Eval('type') != 'none',
|
|
})
|
|
credit_note_account = fields.Many2One(
|
|
'account.account.template', 'Credit Note Account',
|
|
domain=[
|
|
('type.statement', '=', 'balance'),
|
|
('closed', '!=', True),
|
|
],
|
|
states={
|
|
'required': Eval('type') != 'none',
|
|
})
|
|
account = fields.Many2One('account.account.template', 'Account Template',
|
|
domain=[('parent', '=', None)], required=True)
|
|
legal_notice = fields.Text("Legal Notice")
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._order.insert(0, ('account', 'ASC'))
|
|
|
|
@classmethod
|
|
def validate_fields(cls, tax_templates, field_names):
|
|
super().validate_fields(tax_templates, field_names)
|
|
cls.check_update_unit_price(tax_templates, field_names)
|
|
|
|
@classmethod
|
|
def check_update_unit_price(cls, tax_templates, field_names=None):
|
|
if field_names and not (field_names & {'update_unit_price', 'parent'}):
|
|
return
|
|
for tax in tax_templates:
|
|
if tax.update_unit_price and tax.parent:
|
|
raise AccessError(gettext(
|
|
'account.msg_tax_update_unit_price_with_parent',
|
|
tax=tax.rec_name))
|
|
|
|
@staticmethod
|
|
def default_type():
|
|
return 'percentage'
|
|
|
|
@staticmethod
|
|
def default_update_unit_price():
|
|
return False
|
|
|
|
def _get_tax_value(self, tax=None):
|
|
'''
|
|
Set values for tax creation.
|
|
'''
|
|
res = {}
|
|
for field in ['name', 'description', 'sequence', 'amount', 'rate',
|
|
'type', 'start_date', 'end_date', 'update_unit_price',
|
|
'legal_notice', 'active']:
|
|
if not tax or getattr(tax, field) != getattr(self, field):
|
|
res[field] = getattr(self, field)
|
|
for field in ('group',):
|
|
if not tax or getattr(tax, field) != getattr(self, field):
|
|
value = getattr(self, field)
|
|
if value:
|
|
res[field] = getattr(self, field).id
|
|
else:
|
|
res[field] = None
|
|
if not tax or tax.template != self:
|
|
res['template'] = self.id
|
|
return res
|
|
|
|
@classmethod
|
|
def create_tax(cls, account_id, company_id,
|
|
template2account, template2tax=None):
|
|
'''
|
|
Create recursively taxes based on template.
|
|
|
|
template2account is a dictionary with account template id as key and
|
|
account id as value, used to convert account template into account
|
|
code.
|
|
template2tax is a dictionary with tax template id as key and tax id as
|
|
value, used to convert template id into tax. The dictionary is filled
|
|
with new taxes.
|
|
'''
|
|
pool = Pool()
|
|
Tax = pool.get('account.tax')
|
|
|
|
if template2tax is None:
|
|
template2tax = {}
|
|
|
|
def create(templates):
|
|
values = []
|
|
created = []
|
|
for template in templates:
|
|
if template.id not in template2tax:
|
|
vals = template._get_tax_value()
|
|
vals['company'] = company_id
|
|
if template.parent:
|
|
vals['parent'] = template2tax[template.parent.id]
|
|
else:
|
|
vals['parent'] = None
|
|
if template.invoice_account:
|
|
vals['invoice_account'] = \
|
|
template2account[template.invoice_account.id]
|
|
else:
|
|
vals['invoice_account'] = None
|
|
if template.credit_note_account:
|
|
vals['credit_note_account'] = \
|
|
template2account[template.credit_note_account.id]
|
|
else:
|
|
vals['credit_note_account'] = None
|
|
values.append(vals)
|
|
created.append(template)
|
|
|
|
taxes = Tax.create(values)
|
|
for template, tax in zip(created, taxes):
|
|
template2tax[template.id] = tax.id
|
|
|
|
childs = cls.search([
|
|
('account', '=', account_id),
|
|
('parent', '=', None),
|
|
])
|
|
while childs:
|
|
create(childs)
|
|
childs = sum((c.childs for c in childs), ())
|
|
|
|
|
|
class Tax(sequence_ordered(), ModelSQL, ModelView, DeactivableMixin):
|
|
"""Type:
|
|
percentage: tax = price * rate
|
|
fixed: tax = amount
|
|
none: tax = none"""
|
|
__name__ = 'account.tax'
|
|
_states = {
|
|
'readonly': (Bool(Eval('template', -1))
|
|
& ~Eval('template_override', False)),
|
|
}
|
|
name = fields.Char('Name', required=True, states=_states)
|
|
description = fields.Char('Description', required=True, translate=True,
|
|
help="The name that will be used in reports.", states=_states)
|
|
group = fields.Many2One('account.tax.group', 'Group',
|
|
states={
|
|
'invisible': Bool(Eval('parent')),
|
|
'readonly': _states['readonly'],
|
|
})
|
|
start_date = fields.Date("Start Date", states=_states)
|
|
end_date = fields.Date("End Date", states=_states)
|
|
amount = fields.Numeric(
|
|
"Amount", digits=(None, 8),
|
|
states={
|
|
'required': Eval('type') == 'fixed',
|
|
'invisible': Eval('type') != 'fixed',
|
|
'readonly': _states['readonly'],
|
|
}, help='In company\'s currency.')
|
|
rate = fields.Numeric(
|
|
"Rate", digits=(None, 10),
|
|
states={
|
|
'required': Eval('type') == 'percentage',
|
|
'invisible': Eval('type') != 'percentage',
|
|
'readonly': _states['readonly'],
|
|
})
|
|
type = fields.Selection([
|
|
('percentage', 'Percentage'),
|
|
('fixed', 'Fixed'),
|
|
('none', 'None'),
|
|
], 'Type', required=True, states=_states)
|
|
update_unit_price = fields.Boolean('Update Unit Price',
|
|
states={
|
|
'invisible': Bool(Eval('parent')),
|
|
'readonly': _states['readonly'],
|
|
},
|
|
help=('If checked then the unit price for further tax computation will'
|
|
' be modified by this tax.'))
|
|
parent = fields.Many2One(
|
|
'account.tax', "Parent", ondelete='CASCADE', states=_states,
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
])
|
|
childs = fields.One2Many(
|
|
'account.tax', 'parent', "Children",
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
])
|
|
company = fields.Many2One('company.company', "Company", required=True)
|
|
invoice_account = fields.Many2One('account.account', 'Invoice Account',
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
('type.statement', '=', 'balance'),
|
|
('closed', '!=', True),
|
|
],
|
|
states={
|
|
'readonly': _states['readonly'],
|
|
'required': Eval('type') != 'none',
|
|
})
|
|
credit_note_account = fields.Many2One('account.account',
|
|
'Credit Note Account',
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
('type.statement', '=', 'balance'),
|
|
('closed', '!=', True),
|
|
],
|
|
states={
|
|
'readonly': _states['readonly'],
|
|
'required': Eval('type') != 'none',
|
|
})
|
|
legal_notice = fields.Text("Legal Notice", translate=True,
|
|
states=_states)
|
|
template = fields.Many2One('account.tax.template', 'Template',
|
|
ondelete='RESTRICT')
|
|
template_override = fields.Boolean('Override Template',
|
|
help="Check to override template definition",
|
|
states={
|
|
'invisible': ~Bool(Eval('template', -1)),
|
|
})
|
|
|
|
invoice_base_amount = fields.Function(Monetary(
|
|
"Invoice Base Amount", currency='currency', digits='currency'),
|
|
'get_amount')
|
|
invoice_tax_amount = fields.Function(Monetary(
|
|
"Invoice Tax Amount", currency='currency', digits='currency'),
|
|
'get_amount')
|
|
credit_base_amount = fields.Function(Monetary(
|
|
"Credit Base Amount", currency='currency', digits='currency'),
|
|
'get_amount')
|
|
credit_tax_amount = fields.Function(Monetary(
|
|
"Credit Tax Amount", currency='currency', digits='currency'),
|
|
'get_amount')
|
|
|
|
currency = fields.Function(fields.Many2One(
|
|
'currency.currency', "Currency"),
|
|
'on_change_with_currency')
|
|
|
|
del _states
|
|
|
|
@classmethod
|
|
def validate_fields(cls, taxes, field_names):
|
|
super().validate_fields(taxes, field_names)
|
|
cls.check_update_unit_price(taxes, field_names)
|
|
|
|
@classmethod
|
|
def check_update_unit_price(cls, taxes, field_names=None):
|
|
if field_names and not (field_names & {'parent', 'update_unit_price'}):
|
|
return
|
|
for tax in taxes:
|
|
if tax.parent and tax.update_unit_price:
|
|
raise AccessError(gettext(
|
|
'account.msg_tax_update_unit_price_with_parent',
|
|
tax=tax.rec_name))
|
|
|
|
@staticmethod
|
|
def default_type():
|
|
return 'percentage'
|
|
|
|
@staticmethod
|
|
def default_update_unit_price():
|
|
return False
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@classmethod
|
|
def default_template_override(cls):
|
|
return False
|
|
|
|
@classmethod
|
|
def get_amount(cls, taxes, names):
|
|
pool = Pool()
|
|
Move = pool.get('account.move')
|
|
MoveLine = pool.get('account.move.line')
|
|
TaxLine = pool.get('account.tax.line')
|
|
cursor = Transaction().connection.cursor()
|
|
|
|
move = Move.__table__()
|
|
move_line = MoveLine.__table__()
|
|
tax_line = TaxLine.__table__()
|
|
|
|
tax_ids = list(map(int, taxes))
|
|
result = {}
|
|
for name in names:
|
|
result[name] = dict.fromkeys(tax_ids, Decimal(0))
|
|
|
|
columns = []
|
|
amount = tax_line.amount
|
|
debit = move_line.debit
|
|
credit = move_line.credit
|
|
if backend.name == 'sqlite':
|
|
amount = TaxLine.amount.sql_cast(tax_line.amount)
|
|
debit = MoveLine.debit.sql_cast(debit)
|
|
credit = MoveLine.credit.sql_cast(credit)
|
|
is_invoice = (
|
|
((amount > 0) & ((debit > 0) | (credit > 0)))
|
|
| ((amount < 0) & ((debit < 0) | (credit < 0)))
|
|
)
|
|
is_credit = (
|
|
((amount < 0) & ((debit > 0) | (credit > 0)))
|
|
| ((amount > 0) & ((debit < 0) | (credit < 0)))
|
|
)
|
|
for name, clause in [
|
|
('invoice_base_amount',
|
|
is_invoice & (tax_line.type == 'base')),
|
|
('invoice_tax_amount',
|
|
is_invoice & (tax_line.type == 'tax')),
|
|
('credit_base_amount',
|
|
is_credit & (tax_line.type == 'base')),
|
|
('credit_tax_amount',
|
|
is_credit & (tax_line.type == 'tax')),
|
|
]:
|
|
if name not in names:
|
|
continue
|
|
if backend.name == 'postgresql': # FIXME
|
|
columns.append(Sum(amount, filter_=clause).as_(name))
|
|
else:
|
|
columns.append(Sum(Case([clause, amount])).as_(name))
|
|
|
|
where = cls._amount_where(tax_line, move_line, move)
|
|
query = (tax_line
|
|
.join(move_line, condition=tax_line.move_line == move_line.id)
|
|
.join(move, condition=move_line.move == move.id)
|
|
.select(tax_line.tax.as_('tax'),
|
|
*columns,
|
|
where=tax_line.tax.in_(tax_ids)
|
|
& (move_line.state != 'draft')
|
|
& where,
|
|
group_by=tax_line.tax)
|
|
)
|
|
|
|
if backend.name == 'sqlite':
|
|
sqlite_apply_types(query, [None] + ['NUMERIC'] * len(names))
|
|
|
|
cursor.execute(*query)
|
|
for row in cursor_dict(cursor):
|
|
for name in names:
|
|
result[name][row['tax']] = row[name] or 0
|
|
return result
|
|
|
|
@classmethod
|
|
def _amount_where(cls, tax_line, move_line, move):
|
|
context = Transaction().context
|
|
periods = context.get('periods', [])
|
|
if periods:
|
|
return move.period.in_(periods)
|
|
else:
|
|
return Literal(False)
|
|
|
|
@classmethod
|
|
def _amount_domain(cls):
|
|
context = Transaction().context
|
|
periods = context.get('periods', [])
|
|
return [('move_line.move.period', 'in', periods)]
|
|
|
|
@fields.depends('company')
|
|
def on_change_with_currency(self, name=None):
|
|
return self.company.currency if self.company else None
|
|
|
|
@classmethod
|
|
def copy(cls, taxes, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
default.setdefault('template', None)
|
|
return super().copy(taxes, default=default)
|
|
|
|
def _process_tax(self, price_unit):
|
|
if self.type == 'percentage':
|
|
amount = price_unit * self.rate
|
|
return {
|
|
'base': price_unit,
|
|
'amount': amount,
|
|
'tax': self,
|
|
}
|
|
if self.type == 'fixed':
|
|
amount = self.amount
|
|
return {
|
|
'base': price_unit,
|
|
'amount': amount,
|
|
'tax': self,
|
|
}
|
|
|
|
def _group_taxes(self):
|
|
'Key method used to group taxes'
|
|
return (self.sequence,)
|
|
|
|
@classmethod
|
|
def _unit_compute(cls, taxes, price_unit, date):
|
|
res = []
|
|
for _, group_taxes in groupby(taxes, key=cls._group_taxes):
|
|
unit_price_variation = 0
|
|
for tax in group_taxes:
|
|
start_date = tax.start_date or datetime.date.min
|
|
end_date = tax.end_date or datetime.date.max
|
|
values = []
|
|
if not (start_date <= date <= end_date):
|
|
continue
|
|
if tax.type != 'none':
|
|
values.append(tax._process_tax(price_unit))
|
|
if len(tax.childs):
|
|
values.extend(
|
|
cls._unit_compute(tax.childs, price_unit, date))
|
|
if tax.update_unit_price:
|
|
for value in values:
|
|
unit_price_variation += value['amount']
|
|
res.extend(values)
|
|
price_unit += unit_price_variation
|
|
return res
|
|
|
|
@classmethod
|
|
def _reverse_rate_amount(cls, taxes, date):
|
|
rate, amount = 0, 0
|
|
for tax in taxes:
|
|
start_date = tax.start_date or datetime.date.min
|
|
end_date = tax.end_date or datetime.date.max
|
|
if not (start_date <= date <= end_date):
|
|
continue
|
|
|
|
if tax.type == 'percentage':
|
|
rate += tax.rate
|
|
elif tax.type == 'fixed':
|
|
amount += tax.amount
|
|
|
|
if tax.childs:
|
|
child_rate, child_amount = cls._reverse_rate_amount(
|
|
tax.childs, date)
|
|
rate += child_rate
|
|
amount += child_amount
|
|
return rate, amount
|
|
|
|
@classmethod
|
|
def _reverse_unit_compute(cls, price_unit, taxes, date):
|
|
rate, amount = 0, 0
|
|
update_unit_price = False
|
|
unit_price_variation_amount = 0
|
|
unit_price_variation_rate = 0
|
|
for _, group_taxes in groupby(taxes, key=cls._group_taxes):
|
|
group_taxes = list(group_taxes)
|
|
g_rate, g_amount = cls._reverse_rate_amount(group_taxes, date)
|
|
if update_unit_price:
|
|
g_amount += unit_price_variation_amount * g_rate
|
|
g_rate += unit_price_variation_rate * g_rate
|
|
|
|
g_update_unit_price = any(t.update_unit_price for t in group_taxes)
|
|
update_unit_price |= g_update_unit_price
|
|
if g_update_unit_price:
|
|
unit_price_variation_amount += g_amount
|
|
unit_price_variation_rate += g_rate
|
|
|
|
rate += g_rate
|
|
amount += g_amount
|
|
|
|
return (price_unit - amount) / (1 + rate)
|
|
|
|
@classmethod
|
|
def sort_taxes(cls, taxes, reverse=False):
|
|
'''
|
|
Return a list of taxes sorted
|
|
'''
|
|
def key(tax):
|
|
return 0 if tax.sequence is None else 1, tax.sequence or 0, tax.id
|
|
return sorted(taxes, key=key, reverse=reverse)
|
|
|
|
@classmethod
|
|
def compute(cls, taxes, price_unit, quantity, date):
|
|
'''
|
|
Compute taxes for price_unit and quantity at the date.
|
|
Return list of dict for each taxes and their childs with:
|
|
base
|
|
amount
|
|
tax
|
|
'''
|
|
taxes = cls.sort_taxes(taxes)
|
|
res = cls._unit_compute(taxes, price_unit, date)
|
|
quantity = Decimal(str(quantity or 0))
|
|
for row in res:
|
|
row['base'] *= quantity
|
|
row['amount'] *= quantity
|
|
return res
|
|
|
|
@classmethod
|
|
def reverse_compute(cls, price_unit, taxes, date):
|
|
'''
|
|
Reverse compute the price_unit for taxes at the date.
|
|
'''
|
|
taxes = cls.sort_taxes(taxes)
|
|
return cls._reverse_unit_compute(price_unit, taxes, date)
|
|
|
|
@classmethod
|
|
def update_tax(cls, company_id, template2account, template2tax=None):
|
|
'''
|
|
Update recursively taxes based on template.
|
|
template2account is a dictionary with account template id as key and
|
|
account id as value, used to convert account template into account
|
|
code.
|
|
template2tax is a dictionary with tax template id as key and tax id as
|
|
value, used to convert template id into tax. The dictionary is filled
|
|
with new taxes.
|
|
'''
|
|
if template2tax is None:
|
|
template2tax = {}
|
|
|
|
values = []
|
|
childs = cls.search([
|
|
('company', '=', company_id),
|
|
('parent', '=', None),
|
|
])
|
|
while childs:
|
|
for child in childs:
|
|
if child.template:
|
|
if not child.template_override:
|
|
vals = child.template._get_tax_value(tax=child)
|
|
invoice_account_id = (child.invoice_account.id
|
|
if child.invoice_account else None)
|
|
if (child.template.invoice_account
|
|
and invoice_account_id != template2account.get(
|
|
child.template.invoice_account.id)):
|
|
vals['invoice_account'] = template2account.get(
|
|
child.template.invoice_account.id)
|
|
elif (not child.template.invoice_account
|
|
and child.invoice_account):
|
|
vals['invoice_account'] = None
|
|
credit_note_account_id = (child.credit_note_account.id
|
|
if child.credit_note_account else None)
|
|
if (child.template.credit_note_account
|
|
and credit_note_account_id
|
|
!= template2account.get(
|
|
child.template.credit_note_account.id)):
|
|
vals['credit_note_account'] = template2account.get(
|
|
child.template.credit_note_account.id)
|
|
elif (not child.template.credit_note_account
|
|
and child.credit_note_account):
|
|
vals['credit_note_account'] = None
|
|
if vals:
|
|
values.append([child])
|
|
values.append(vals)
|
|
template2tax[child.template.id] = child.id
|
|
childs = sum((c.childs for c in childs), ())
|
|
if values:
|
|
cls.write(*values)
|
|
|
|
# Update parent
|
|
to_save = []
|
|
childs = cls.search([
|
|
('company', '=', company_id),
|
|
('parent', '=', None),
|
|
])
|
|
while childs:
|
|
for child in childs:
|
|
if child.template:
|
|
if not child.template_override:
|
|
if child.template.parent:
|
|
parent = template2tax.get(child.template.parent.id)
|
|
else:
|
|
parent = None
|
|
old_parent = (
|
|
child.parent.id if child.parent else None)
|
|
if parent != old_parent:
|
|
child.parent = parent
|
|
to_save.append(child)
|
|
childs = sum((c.childs for c in childs), ())
|
|
cls.save(to_save)
|
|
|
|
|
|
class _TaxLine:
|
|
__slots__ = ('base', 'amount', 'tax', 'account')
|
|
|
|
def __init__(self, **kwargs):
|
|
for k, v in kwargs.items():
|
|
setattr(self, k, v)
|
|
|
|
def values(self):
|
|
for k in self.__slots__:
|
|
yield k, getattr(self, k)
|
|
|
|
@property
|
|
def _key(self):
|
|
return (self.account, self.tax, self.base >= 0)
|
|
|
|
def __add__(self, other):
|
|
if (not isinstance(other, _TaxLine)
|
|
or self._key != other._key):
|
|
return NotImplemented
|
|
new = copy.copy(self)
|
|
for key in ['base', 'amount']:
|
|
setattr(new, key, getattr(self, key) + getattr(other, key))
|
|
return new
|
|
|
|
def __iadd__(self, other):
|
|
if (not isinstance(other, _TaxLine)
|
|
or self._key != other._key):
|
|
return NotImplemented
|
|
for key in ['base', 'amount']:
|
|
setattr(self, key, getattr(self, key) + getattr(other, key))
|
|
return self
|
|
|
|
|
|
class _TaxableLine(namedtuple(
|
|
'_TaxableLine',
|
|
('taxes', 'unit_price', 'quantity', 'tax_date'))):
|
|
@property
|
|
def _key(self):
|
|
base = self.unit_price * Decimal(str(self.quantity or 0))
|
|
return (tuple(self.taxes), base >= 0)
|
|
|
|
|
|
class TaxableMixin(object):
|
|
__slots__ = ()
|
|
|
|
@property
|
|
def taxable_lines(self):
|
|
"""A list of tuples where
|
|
- the first element is the taxes applicable
|
|
- the second element is the line unit price
|
|
- the third element is the line quantity
|
|
- the forth element is the optional tax date
|
|
"""
|
|
return []
|
|
|
|
@property
|
|
def tax_date(self):
|
|
"Date to use when computing the tax"
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
with Transaction().set_context(company=self.company.id):
|
|
return Date.today()
|
|
|
|
def _get_tax_context(self):
|
|
return {}
|
|
|
|
def _compute_tax_line(self, amount, base, tax):
|
|
if base >= 0:
|
|
type_ = 'invoice'
|
|
else:
|
|
type_ = 'credit_note'
|
|
# Base must always be rounded per line as there will be
|
|
# one tax line per taxable_lines
|
|
if self.currency:
|
|
base = self.currency.round(base)
|
|
return _TaxLine(
|
|
base=base,
|
|
amount=amount,
|
|
tax=tax,
|
|
account=getattr(tax, '%s_account' % type_),
|
|
)
|
|
|
|
@fields.depends('currency')
|
|
def _round_taxes(self, taxes):
|
|
if not self.currency:
|
|
return
|
|
|
|
remainder = 0
|
|
for taxline in taxes.values():
|
|
rounded_amount = self.currency.round(taxline.amount)
|
|
remainder += rounded_amount - taxline.amount
|
|
taxline.amount = rounded_amount
|
|
|
|
# We need to compensate the rounding we did
|
|
remainder = self.currency.round(remainder, opposite=True)
|
|
if abs(remainder) >= self.currency.rounding:
|
|
offset_amount = self.currency.rounding.copy_sign(remainder)
|
|
|
|
for tax in cycle(taxes.values()):
|
|
if tax.amount:
|
|
tax.amount -= offset_amount
|
|
remainder -= offset_amount
|
|
if abs(remainder) < self.currency.rounding:
|
|
break
|
|
|
|
@fields.depends('company', methods=['_get_tax_context', '_round_taxes'])
|
|
def _get_taxes(self):
|
|
pool = Pool()
|
|
Tax = pool.get('account.tax')
|
|
Configuration = pool.get('account.configuration')
|
|
context = Transaction().context
|
|
|
|
all_taxes = {}
|
|
with Transaction().set_context(self._get_tax_context()):
|
|
config = Configuration(1)
|
|
tax_rounding = config.get_multivalue(
|
|
'tax_rounding',
|
|
company=self.company.id if self.company
|
|
else context.get('company'))
|
|
taxable_lines = defaultdict(list)
|
|
for params in self.taxable_lines:
|
|
taxable_line = _TaxableLine(*params)
|
|
taxable_lines[taxable_line._key].append(taxable_line)
|
|
for grouped_taxable_lines in taxable_lines.values():
|
|
taxes = {}
|
|
for line in grouped_taxable_lines:
|
|
assert all(t.company == self.company for t in line.taxes)
|
|
l_taxes = Tax.compute(
|
|
Tax.browse(line.taxes), line.unit_price,
|
|
line.quantity, line.tax_date or self.tax_date)
|
|
current_taxes = {}
|
|
for tax in l_taxes:
|
|
taxline = self._compute_tax_line(**tax)
|
|
key = taxline._key
|
|
if taxline not in current_taxes:
|
|
current_taxes[key] = taxline
|
|
else:
|
|
current_taxes[key] += taxline
|
|
if tax_rounding == 'line':
|
|
self._round_taxes(current_taxes)
|
|
for key, taxline in current_taxes.items():
|
|
if key not in taxes:
|
|
taxes[key] = taxline
|
|
else:
|
|
taxes[key] += taxline
|
|
if tax_rounding == 'document':
|
|
self._round_taxes(taxes)
|
|
for key, taxline in taxes.items():
|
|
if key not in all_taxes:
|
|
all_taxes[key] = taxline
|
|
else:
|
|
all_taxes[key] += taxline
|
|
return all_taxes
|
|
|
|
|
|
class TaxLine(ModelSQL, ModelView):
|
|
__name__ = 'account.tax.line'
|
|
currency = fields.Function(fields.Many2One(
|
|
'currency.currency', "Currency"),
|
|
'on_change_with_currency')
|
|
amount = Monetary(
|
|
"Amount", currency='currency', digits='currency', required=True)
|
|
type = fields.Selection([
|
|
('tax', "Tax"),
|
|
('base', "Base"),
|
|
], "Type", required=True)
|
|
tax = fields.Many2One(
|
|
'account.tax', "Tax", ondelete='RESTRICT', required=True,
|
|
domain=[
|
|
('company', '=', Eval('company', -1)),
|
|
])
|
|
move_line = fields.Many2One(
|
|
'account.move.line', "Move Line", required=True, ondelete='CASCADE')
|
|
company = fields.Function(fields.Many2One('company.company', 'Company'),
|
|
'on_change_with_company')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('move_line')
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
pool = Pool()
|
|
Tax = pool.get('account.tax')
|
|
transaction = Transaction()
|
|
table = cls.__table__()
|
|
tax = Tax.__table__()
|
|
|
|
migrate_type = False
|
|
if backend.TableHandler.table_exist(cls._table):
|
|
table_h = cls.__table_handler__(module_name)
|
|
migrate_type = not table_h.column_exist('type')
|
|
|
|
super().__register__(module_name)
|
|
|
|
table_h = cls.__table_handler__(module_name)
|
|
|
|
# Migrate from 4.6: remove code and fill type
|
|
table_h.not_null_action('code', action='remove')
|
|
if migrate_type:
|
|
# XXX base on no tax code is used for both tax and base
|
|
cursor = transaction.connection.cursor()
|
|
cursor.execute(*tax.select(
|
|
tax.id, tax.company,
|
|
tax.invoice_base_code, tax.invoice_tax_code,
|
|
tax.credit_note_base_code, tax.credit_note_tax_code))
|
|
update = transaction.connection.cursor()
|
|
for (tax, company,
|
|
invoice_base_code, invoice_tax_code,
|
|
credit_note_base_code, credit_note_tax_code) in cursor:
|
|
update.execute(*table.update(
|
|
[table.type], ['tax'],
|
|
where=(table.tax == tax)
|
|
& (table.code.in_(
|
|
[invoice_tax_code, credit_note_tax_code]))))
|
|
update.execute(*table.update(
|
|
[table.type], ['base'],
|
|
where=(table.tax == tax)
|
|
& (table.code.in_(
|
|
[invoice_base_code, credit_note_base_code]))))
|
|
|
|
@fields.depends('move_line', '_parent_move_line.currency')
|
|
def on_change_with_currency(self, name=None):
|
|
return self.move_line.currency if self.move_line else None
|
|
|
|
@fields.depends('_parent_move_line.account', 'move_line')
|
|
def on_change_with_company(self, name=None):
|
|
if self.move_line and self.move_line.account:
|
|
return self.move_line.account.company
|
|
|
|
def get_rec_name(self, name):
|
|
name = super().get_rec_name(name)
|
|
if self.tax:
|
|
name = self.tax.rec_name
|
|
return name
|
|
|
|
@classmethod
|
|
def search_rec_name(cls, name, clause):
|
|
return [('tax',) + tuple(clause[1:])]
|
|
|
|
@property
|
|
def period_checked(self):
|
|
return self.move_line.period
|
|
|
|
@classmethod
|
|
def check_modification(cls, mode, lines, values=None, external=False):
|
|
super().check_modification(
|
|
mode, lines, values=values, external=external)
|
|
for line in lines:
|
|
period = line.period_checked
|
|
if period and period.state != 'open':
|
|
raise AccessError(gettext(
|
|
'account.msg_modify_tax_line_closed_period',
|
|
period=period.rec_name))
|
|
|
|
|
|
class TaxRuleTemplate(ModelSQL, ModelView):
|
|
__name__ = 'account.tax.rule.template'
|
|
name = fields.Char('Name', required=True)
|
|
kind = fields.Selection(KINDS, 'Kind', required=True)
|
|
lines = fields.One2Many('account.tax.rule.line.template', 'rule', 'Lines')
|
|
account = fields.Many2One('account.account.template', 'Account Template',
|
|
domain=[('parent', '=', None)], required=True)
|
|
|
|
@staticmethod
|
|
def default_kind():
|
|
return 'both'
|
|
|
|
def _get_tax_rule_value(self, rule=None):
|
|
'''
|
|
Set values for tax rule creation.
|
|
'''
|
|
res = {}
|
|
if not rule or rule.name != self.name:
|
|
res['name'] = self.name
|
|
if not rule or rule.kind != self.kind:
|
|
res['kind'] = self.kind
|
|
if not rule or rule.template.id != self.id:
|
|
res['template'] = self.id
|
|
return res
|
|
|
|
@classmethod
|
|
def create_rule(cls, account_id, company_id, template2rule=None):
|
|
'''
|
|
Create tax rule based on template.
|
|
template2rule is a dictionary with tax rule template id as key and tax
|
|
rule id as value, used to convert template id into tax rule. The
|
|
dictionary is filled with new tax rules.
|
|
'''
|
|
pool = Pool()
|
|
Rule = pool.get('account.tax.rule')
|
|
|
|
if template2rule is None:
|
|
template2rule = {}
|
|
|
|
templates = cls.search([
|
|
('account', '=', account_id),
|
|
])
|
|
|
|
values = []
|
|
created = []
|
|
for template in templates:
|
|
if template.id not in template2rule:
|
|
vals = template._get_tax_rule_value()
|
|
vals['company'] = company_id
|
|
values.append(vals)
|
|
created.append(template)
|
|
|
|
rules = Rule.create(values)
|
|
for template, rule in zip(created, rules):
|
|
template2rule[template.id] = rule.id
|
|
|
|
|
|
class TaxRule(ModelSQL, ModelView):
|
|
__name__ = 'account.tax.rule'
|
|
_states = {
|
|
'readonly': (Bool(Eval('template', -1))
|
|
& ~Eval('template_override', False)),
|
|
}
|
|
name = fields.Char('Name', required=True, states=_states)
|
|
kind = fields.Selection(KINDS, 'Kind', required=True, states=_states)
|
|
company = fields.Many2One('company.company', "Company", required=True,)
|
|
lines = fields.One2Many('account.tax.rule.line', 'rule', 'Lines')
|
|
template = fields.Many2One('account.tax.rule.template', 'Template')
|
|
template_override = fields.Boolean("Override Template",
|
|
help="Check to override template definition",
|
|
states={
|
|
'invisible': ~Bool(Eval('template', -1)),
|
|
})
|
|
del _states
|
|
|
|
@staticmethod
|
|
def default_kind():
|
|
return 'both'
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@classmethod
|
|
def copy(cls, rules, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
default.setdefault('template', None)
|
|
return super().copy(rules, default=default)
|
|
|
|
def apply(self, tax, pattern):
|
|
'''
|
|
Apply rule on tax
|
|
pattern is a dictonary with rule line field as key and match value as
|
|
value.
|
|
Return a list of the tax id to use or None
|
|
'''
|
|
pattern = pattern.copy()
|
|
pattern['group'] = tax.group.id if tax and tax.group else None
|
|
pattern['origin_tax'] = tax.id if tax else None
|
|
|
|
for line in self.lines:
|
|
if line.match(pattern):
|
|
return line.get_taxes(tax)
|
|
return tax and [tax.id] or None
|
|
|
|
@classmethod
|
|
def update_rule(cls, company_id, template2rule=None):
|
|
'''
|
|
Update tax rule based on template.
|
|
template2rule is a dictionary with tax rule template id as key and tax
|
|
rule id as value, used to convert template id into tax rule. The
|
|
dictionary is filled with new tax rules.
|
|
'''
|
|
if template2rule is None:
|
|
template2rule = {}
|
|
|
|
values = []
|
|
rules = cls.search([
|
|
('company', '=', company_id),
|
|
])
|
|
for rule in rules:
|
|
if rule.template:
|
|
template2rule[rule.template.id] = rule.id
|
|
if rule.template_override:
|
|
continue
|
|
vals = rule.template._get_tax_rule_value(rule=rule)
|
|
if vals:
|
|
values.append([rule])
|
|
values.append(vals)
|
|
if values:
|
|
cls.write(*values)
|
|
|
|
|
|
class TaxRuleLineTemplate(sequence_ordered(), ModelSQL, ModelView):
|
|
__name__ = 'account.tax.rule.line.template'
|
|
rule = fields.Many2One('account.tax.rule.template', 'Rule', required=True,
|
|
ondelete='CASCADE')
|
|
start_date = fields.Date("Start Date")
|
|
end_date = fields.Date("End Date")
|
|
group = fields.Many2One('account.tax.group', 'Tax Group',
|
|
ondelete='RESTRICT')
|
|
origin_tax = fields.Many2One('account.tax.template', 'Original Tax',
|
|
domain=[
|
|
('parent', '=', None),
|
|
('account', '=', Eval('_parent_rule', {}).get('account', -1)),
|
|
('group', '=', Eval('group', -1)),
|
|
['OR',
|
|
('group', '=', None),
|
|
If(Eval('_parent_rule', {}).get('kind', 'both') == 'sale',
|
|
('group.kind', 'in', ['sale', 'both']),
|
|
If(Eval('_parent_rule', {}).get('kind', 'both')
|
|
== 'purchase',
|
|
('group.kind', 'in', ['purchase', 'both']),
|
|
('group.kind', 'in', ['sale', 'purchase', 'both']))),
|
|
],
|
|
],
|
|
help=('If the original tax template is filled, the rule will be '
|
|
'applied only for this tax template.'),
|
|
depends={'rule'},
|
|
ondelete='RESTRICT')
|
|
keep_origin = fields.Boolean("Keep Origin",
|
|
help="Check to append the original tax to substituted tax.")
|
|
tax = fields.Many2One('account.tax.template', 'Substitution Tax',
|
|
domain=[
|
|
('parent', '=', None),
|
|
('account', '=', Eval('_parent_rule', {}).get('account', 0)),
|
|
('group', '=', Eval('group', -1)),
|
|
['OR',
|
|
('group', '=', None),
|
|
If(Eval('_parent_rule', {}).get('kind', 'both') == 'sale',
|
|
('group.kind', 'in', ['sale', 'both']),
|
|
If(Eval('_parent_rule', {}).get('kind', 'both')
|
|
== 'purchase',
|
|
('group.kind', 'in', ['purchase', 'both']),
|
|
('group.kind', 'in', ['sale', 'purchase', 'both']))),
|
|
],
|
|
],
|
|
depends={'rule'},
|
|
ondelete='RESTRICT')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('rule')
|
|
cls._order.insert(1, ('rule', 'ASC'))
|
|
|
|
def _get_tax_rule_line_value(self, rule_line=None):
|
|
'''
|
|
Set values for tax rule line creation.
|
|
'''
|
|
res = {}
|
|
if not rule_line or rule_line.start_date != self.start_date:
|
|
res['start_date'] = self.start_date
|
|
if not rule_line or rule_line.end_date != self.end_date:
|
|
res['end_date'] = self.end_date
|
|
if not rule_line or rule_line.group != self.group:
|
|
res['group'] = self.group.id if self.group else None
|
|
if not rule_line or rule_line.sequence != self.sequence:
|
|
res['sequence'] = self.sequence
|
|
if not rule_line or rule_line.keep_origin != self.keep_origin:
|
|
res['keep_origin'] = self.keep_origin
|
|
if not rule_line or rule_line.template != self:
|
|
res['template'] = self.id
|
|
return res
|
|
|
|
@classmethod
|
|
def create_rule_line(cls, account_id, template2tax, template2rule,
|
|
template2rule_line=None):
|
|
'''
|
|
Create tax rule line based on template.
|
|
template2tax is a dictionary with tax template id as key and tax id as
|
|
value, used to convert template id into tax.
|
|
template2rule is a dictionary with tax rule template id as key and tax
|
|
rule id as value, used to convert template id into tax rule.
|
|
template2rule_line is a dictionary with tax rule line template id as
|
|
key and tax rule line id as value, used to convert template id into tax
|
|
rule line. The dictionary is filled with new tax rule lines.
|
|
'''
|
|
RuleLine = Pool().get('account.tax.rule.line')
|
|
|
|
if template2rule_line is None:
|
|
template2rule_line = {}
|
|
|
|
templates = cls.search([
|
|
('rule.account', '=', account_id),
|
|
])
|
|
|
|
values = []
|
|
created = []
|
|
for template in templates:
|
|
if template.id not in template2rule_line:
|
|
vals = template._get_tax_rule_line_value()
|
|
vals['rule'] = template2rule[template.rule.id]
|
|
if template.origin_tax:
|
|
vals['origin_tax'] = template2tax[template.origin_tax.id]
|
|
else:
|
|
vals['origin_tax'] = None
|
|
if template.tax:
|
|
vals['tax'] = template2tax[template.tax.id]
|
|
else:
|
|
vals['tax'] = None
|
|
values.append(vals)
|
|
created.append(template)
|
|
|
|
rule_lines = RuleLine.create(values)
|
|
for template, rule_line in zip(created, rule_lines):
|
|
template2rule_line[template.id] = rule_line.id
|
|
|
|
|
|
class TaxRuleLine(sequence_ordered(), ModelSQL, ModelView, MatchMixin):
|
|
__name__ = 'account.tax.rule.line'
|
|
_states = {
|
|
'readonly': (Bool(Eval('template', -1))
|
|
& ~Eval('template_override', False)),
|
|
}
|
|
rule = fields.Many2One(
|
|
'account.tax.rule', "Rule",
|
|
required=True, ondelete='CASCADE', states=_states)
|
|
start_date = fields.Date("Start Date")
|
|
end_date = fields.Date("End Date")
|
|
group = fields.Many2One('account.tax.group', 'Tax Group',
|
|
ondelete='RESTRICT', states=_states)
|
|
origin_tax = fields.Many2One('account.tax', 'Original Tax',
|
|
domain=[
|
|
('parent', '=', None),
|
|
('company', '=', Eval('_parent_rule', {}).get('company', -1)),
|
|
('group', '=', Eval('group', -1)),
|
|
['OR',
|
|
('group', '=', None),
|
|
If(Eval('_parent_rule', {}).get('kind', 'both') == 'sale',
|
|
('group.kind', 'in', ['sale', 'both']),
|
|
If(Eval('_parent_rule', {}).get('kind', 'both')
|
|
== 'purchase',
|
|
('group.kind', 'in', ['purchase', 'both']),
|
|
('group.kind', 'in', ['sale', 'purchase', 'both']))),
|
|
],
|
|
],
|
|
help=('If the original tax is filled, the rule will be applied '
|
|
'only for this tax.'),
|
|
depends={'rule'},
|
|
ondelete='RESTRICT', states=_states)
|
|
keep_origin = fields.Boolean("Keep Origin", states=_states,
|
|
help="Check to append the original tax to substituted tax.")
|
|
tax = fields.Many2One('account.tax', 'Substitution Tax',
|
|
domain=[
|
|
('parent', '=', None),
|
|
('company', '=', Eval('_parent_rule', {}).get('company', -1)),
|
|
('group', '=', Eval('group', -1)),
|
|
['OR',
|
|
('group', '=', None),
|
|
If(Eval('_parent_rule', {}).get('kind', 'both') == 'sale',
|
|
('group.kind', 'in', ['sale', 'both']),
|
|
If(Eval('_parent_rule', {}).get('kind', 'both')
|
|
== 'purchase',
|
|
('group.kind', 'in', ['purchase', 'both']),
|
|
('group.kind', 'in', ['sale', 'purchase', 'both']))),
|
|
],
|
|
],
|
|
depends={'rule'},
|
|
ondelete='RESTRICT', states=_states)
|
|
template = fields.Many2One(
|
|
'account.tax.rule.line.template', 'Template', ondelete='CASCADE')
|
|
template_override = fields.Boolean("Override Template",
|
|
help="Check to override template definition",
|
|
states={
|
|
'invisible': ~Bool(Eval('template', -1)),
|
|
})
|
|
del _states
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('rule')
|
|
cls._order.insert(1, ('rule', 'ASC'))
|
|
|
|
@classmethod
|
|
def copy(cls, lines, default=None):
|
|
if default is None:
|
|
default = {}
|
|
else:
|
|
default = default.copy()
|
|
default.setdefault('template', None)
|
|
return super().copy(lines, default=default)
|
|
|
|
def match(self, pattern):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
with Transaction().set_context(company=self.rule.company.id):
|
|
today = Date.today()
|
|
pattern = pattern.copy()
|
|
if 'group' in pattern and not self.group:
|
|
if pattern['group']:
|
|
return False
|
|
date = pattern.pop('date', None) or today
|
|
if self.start_date and date < self.start_date:
|
|
return False
|
|
if self.end_date and date > self.end_date:
|
|
return False
|
|
return super().match(pattern)
|
|
|
|
def get_taxes(self, origin_tax):
|
|
'''
|
|
Return list of taxes for a line
|
|
'''
|
|
if self.tax:
|
|
taxes = [self.tax.id]
|
|
if self.keep_origin and origin_tax:
|
|
taxes.append(origin_tax.id)
|
|
return taxes
|
|
return None
|
|
|
|
@classmethod
|
|
def update_rule_line(cls, company_id, template2tax, template2rule,
|
|
template2rule_line=None):
|
|
'''
|
|
Update tax rule line based on template.
|
|
template2tax is a dictionary with tax template id as key and tax id as
|
|
value, used to convert template id into tax.
|
|
template2rule is a dictionary with tax rule template id as key and tax
|
|
rule id as value, used to convert template id into tax rule.
|
|
template2rule_line is a dictionary with tax rule line template id as
|
|
key and tax rule line id as value, used to convert template id into tax
|
|
rule line. The dictionary is filled with new tax rule lines.
|
|
'''
|
|
if template2rule_line is None:
|
|
template2rule_line = {}
|
|
|
|
values = []
|
|
lines = cls.search([
|
|
('rule.company', '=', company_id),
|
|
])
|
|
for line in lines:
|
|
if line.template:
|
|
template2rule_line[line.template.id] = line.id
|
|
if line.template_override:
|
|
continue
|
|
vals = line.template._get_tax_rule_line_value(rule_line=line)
|
|
if line.rule.id != template2rule[line.template.rule.id]:
|
|
vals['rule'] = template2rule[line.template.rule.id]
|
|
if line.origin_tax:
|
|
if line.template.origin_tax:
|
|
if (line.origin_tax.id
|
|
!= template2tax[line.template.origin_tax.id]):
|
|
vals['origin_tax'] = template2tax[
|
|
line.template.origin_tax.id]
|
|
else:
|
|
vals['origin_tax'] = None
|
|
elif line.template.origin_tax:
|
|
vals['origin_tax'] = template2tax[
|
|
line.template.origin_tax.id]
|
|
if line.tax:
|
|
if line.template.tax:
|
|
if line.tax.id != template2tax[line.template.tax.id]:
|
|
vals['tax'] = template2tax[line.template.tax.id]
|
|
else:
|
|
vals['tax'] = None
|
|
elif line.template.tax:
|
|
vals['tax'] = template2tax[line.template.tax.id]
|
|
if vals:
|
|
values.append([line])
|
|
values.append(vals)
|
|
if values:
|
|
cls.write(*values)
|
|
|
|
|
|
class OpenTaxCode(Wizard):
|
|
__name__ = 'account.tax.open_code'
|
|
start_state = 'open_'
|
|
_readonly = True
|
|
open_ = StateAction('account.act_tax_line_form')
|
|
|
|
def do_open_(self, action):
|
|
pool = Pool()
|
|
Tax = pool.get('account.tax')
|
|
if self.record.lines:
|
|
domain = ['OR'] + [l._line_domain for l in self.record.lines]
|
|
else:
|
|
domain = ('id', '=', None)
|
|
if self.record:
|
|
action['name'] += ' (%s)' % self.record.rec_name
|
|
action['pyson_domain'] = PYSONEncoder().encode([
|
|
Tax._amount_domain(),
|
|
domain,
|
|
])
|
|
return action, {}
|
|
|
|
|
|
class TestTax(Wizard):
|
|
__name__ = 'account.tax.test'
|
|
start_state = 'test'
|
|
test = StateView(
|
|
'account.tax.test', 'account.tax_test_view_form',
|
|
[Button('Close', 'end', 'tryton-close', default=True)])
|
|
|
|
def default_test(self, fields):
|
|
default = {}
|
|
if (self.model
|
|
and self.model.__name__ == 'account.tax'
|
|
and self.records):
|
|
default['taxes'] = list(map(int, self.records))
|
|
return default
|
|
|
|
|
|
class TestTaxView(ModelView, TaxableMixin):
|
|
__name__ = 'account.tax.test'
|
|
tax_date = fields.Date("Date")
|
|
taxes = fields.One2Many('account.tax', None, "Taxes",
|
|
domain=[
|
|
('parent', '=', None),
|
|
])
|
|
unit_price = fields.Numeric("Unit Price")
|
|
quantity = fields.Numeric("Quantity")
|
|
currency = fields.Many2One('currency.currency', 'Currency')
|
|
result = fields.One2Many(
|
|
'account.tax.test.result', None, "Result", readonly=True)
|
|
|
|
@classmethod
|
|
def default_tax_date(cls):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
return Date.today()
|
|
|
|
@classmethod
|
|
def default_quantity(cls):
|
|
return 1
|
|
|
|
@classmethod
|
|
def default_currency(cls):
|
|
pool = Pool()
|
|
Company = pool.get('company.company')
|
|
company_id = Transaction().context.get('company')
|
|
if company_id is not None and company_id >= 0:
|
|
company = Company(company_id)
|
|
return company.currency.id
|
|
|
|
@property
|
|
def company(self):
|
|
pool = Pool()
|
|
Company = pool.get('company.company')
|
|
company_id = Transaction().context.get('company')
|
|
if company_id is not None and company_id >= 0:
|
|
return Company(company_id)
|
|
|
|
@property
|
|
def taxable_lines(self):
|
|
return [(self.taxes, self.unit_price, self.quantity, self.tax_date)]
|
|
|
|
@fields.depends(
|
|
'tax_date', 'taxes', 'unit_price', 'quantity', 'currency', 'result')
|
|
def on_change_with_result(self):
|
|
pool = Pool()
|
|
Result = pool.get('account.tax.test.result')
|
|
result = []
|
|
if all([self.tax_date, self.unit_price, self.quantity, self.currency]):
|
|
for taxline in self._get_taxes().values():
|
|
result.append(Result(**dict(taxline.values())))
|
|
return result
|
|
|
|
|
|
class TestTaxViewResult(ModelView):
|
|
__name__ = 'account.tax.test.result'
|
|
tax = fields.Many2One('account.tax', "Tax")
|
|
account = fields.Many2One('account.account', "Account")
|
|
base = fields.Numeric("Base")
|
|
amount = fields.Numeric("Amount")
|