Files
tryton/tax.py
T
2014-12-19 19:42:37 +01:00

1535 lines
58 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 datetime
from collections import namedtuple
from decimal import Decimal
from sql import Null
from sql.aggregate import Sum
from trytond.model import ModelView, ModelSQL, MatchMixin, fields
from trytond.wizard import Wizard, StateView, StateAction, Button
from trytond import backend
from trytond.pyson import Eval, If, Bool, PYSONEncoder
from trytond.transaction import Transaction
from trytond.pool import Pool, PoolMeta
__all__ = ['TaxGroup', 'TaxCodeTemplate', 'TaxCode',
'OpenChartTaxCodeStart', 'OpenChartTaxCode',
'TaxTemplate', 'Tax', 'TaxLine', 'TaxRuleTemplate', 'TaxRule',
'TaxRuleLineTemplate', 'TaxRuleLine',
'OpenTaxCode',
'AccountTemplateTaxTemplate', 'AccountTemplate2', 'AccountTax', 'Account2']
__metaclass__ = PoolMeta
KINDS = [
('sale', 'Sale'),
('purchase', 'Purchase'),
('both', 'Both'),
]
class TaxGroup(ModelSQL, ModelView):
'Tax Group'
__name__ = 'account.tax.group'
name = fields.Char('Name', size=None, required=True, translate=True)
code = fields.Char('Code', size=None, required=True)
kind = fields.Selection(KINDS, 'Kind', required=True)
@classmethod
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
super(TaxGroup, cls).__register__(module_name)
cursor = Transaction().cursor
table = TableHandler(cursor, cls, module_name)
# Migration from 1.4 drop code_uniq constraint
table.drop_constraint('code_uniq')
@staticmethod
def default_kind():
return 'both'
class TaxCodeTemplate(ModelSQL, ModelView):
'Tax Code Template'
__name__ = 'account.tax.code.template'
name = fields.Char('Name', required=True, translate=True)
code = fields.Char('Code')
parent = fields.Many2One('account.tax.code.template', 'Parent')
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', translate=True)
@classmethod
def __setup__(cls):
super(TaxCodeTemplate, cls).__setup__()
cls._order.insert(0, ('code', 'ASC'))
cls._order.insert(0, ('account', 'ASC'))
@classmethod
def validate(cls, templates):
super(TaxCodeTemplate, cls).validate(templates)
cls.check_recursion(templates)
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.description != self.description:
res['description'] = self.description
if not code or code.template.id != self.id:
res['template'] = self.id
return res
def create_tax_code(self, company_id, template2tax_code=None,
parent_id=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.
Return the id of the tax code created
'''
pool = Pool()
TaxCode = pool.get('account.tax.code')
Lang = pool.get('ir.lang')
Config = pool.get('ir.configuration')
if template2tax_code is None:
template2tax_code = {}
if self.id not in template2tax_code:
vals = self._get_tax_code_value()
vals['company'] = company_id
vals['parent'] = parent_id
new_tax_code, = TaxCode.create([vals])
prev_lang = self._context.get('language') or Config.get_language()
prev_data = {}
for field_name, field in self._fields.iteritems():
if getattr(field, 'translate', False):
prev_data[field_name] = getattr(self, field_name)
for lang in Lang.get_translatable_languages():
if lang == prev_lang:
continue
with Transaction().set_context(language=lang):
template = self.__class__(self.id)
data = {}
for field_name, field in template._fields.iteritems():
if (getattr(field, 'translate', False)
and (getattr(template, field_name) !=
prev_data[field_name])):
data[field_name] = getattr(template, field_name)
if data:
TaxCode.write([new_tax_code], data)
template2tax_code[self.id] = new_tax_code.id
new_id = template2tax_code[self.id]
new_childs = []
for child in self.childs:
new_childs.append(child.create_tax_code(company_id,
template2tax_code=template2tax_code, parent_id=new_id))
return new_id
class TaxCode(ModelSQL, ModelView):
'Tax Code'
__name__ = 'account.tax.code'
name = fields.Char('Name', size=None, required=True, select=True,
translate=True)
code = fields.Char('Code', size=None, select=True)
active = fields.Boolean('Active', select=True)
company = fields.Many2One('company.company', 'Company', required=True,
select=True)
parent = fields.Many2One('account.tax.code', 'Parent', select=True,
domain=[('company', '=', Eval('company', 0))], depends=['company'])
childs = fields.One2Many('account.tax.code', 'parent', 'Children',
domain=[('company', '=', Eval('company', 0))], depends=['company'])
currency_digits = fields.Function(fields.Integer('Currency Digits'),
'on_change_with_currency_digits')
sum = fields.Function(fields.Numeric('Sum',
digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']),
'get_sum')
template = fields.Many2One('account.tax.code.template', 'Template')
description = fields.Text('Description', translate=True)
@classmethod
def __setup__(cls):
super(TaxCode, cls).__setup__()
cls._order.insert(0, ('code', 'ASC'))
@classmethod
def validate(cls, codes):
super(TaxCode, cls).validate(codes)
cls.check_recursion(codes)
@staticmethod
def default_active():
return True
@staticmethod
def default_company():
return Transaction().context.get('company')
@fields.depends('company')
def on_change_with_currency_digits(self, name=None):
if self.company:
return self.company.currency.digits
return 2
@classmethod
def get_sum(cls, codes, name):
cursor = Transaction().cursor
res = {}
pool = Pool()
MoveLine = pool.get('account.move.line')
TaxLine = pool.get('account.tax.line')
code = cls.__table__()
tax_line = TaxLine.__table__()
move_line = MoveLine.__table__()
childs = cls.search([
('parent', 'child_of', [c.id for c in codes]),
])
all_codes = list(set(codes) | set(childs))
line_query, _ = MoveLine.query_get(move_line)
cursor.execute(*code.join(tax_line, condition=tax_line.code == code.id
).join(move_line, condition=tax_line.move_line == move_line.id
).select(code.id, Sum(tax_line.amount),
where=code.id.in_([c.id for c in all_codes])
& code.active & line_query,
group_by=code.id))
code_sum = {}
for code_id, sum in cursor.fetchall():
# SQLite uses float for SUM
if not isinstance(sum, Decimal):
sum = Decimal(str(sum))
code_sum[code_id] = sum
for code in codes:
res.setdefault(code.id, Decimal('0.0'))
childs = cls.search([
('parent', 'child_of', [code.id]),
])
for child in childs:
res[code.id] += code.company.currency.round(
code_sum.get(child.id, Decimal('0.0')))
res[code.id] = code.company.currency.round(res[code.id])
return res
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):
return ['OR',
('code',) + tuple(clause[1:]),
(cls._rec_name,) + tuple(clause[1:]),
]
@classmethod
def delete(cls, codes):
codes = cls.search([
('parent', 'child_of', [c.id for c in codes]),
])
super(TaxCode, cls).delete(codes)
def update_tax_code(self, 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
'''
pool = Pool()
Lang = pool.get('ir.lang')
Config = pool.get('ir.configuration')
if template2tax_code is None:
template2tax_code = {}
if self.template:
vals = self.template._get_tax_code_value(code=self)
if vals:
self.write([self], vals)
prev_lang = self._context.get('language') or Config.get_language()
prev_data = {}
for field_name, field in self.template._fields.iteritems():
if getattr(field, 'translate', False):
prev_data[field_name] = getattr(self.template, field_name)
for lang in Lang.get_translatable_languages():
if lang == prev_lang:
continue
with Transaction().set_context(language=lang):
code = self.__class__(self.id)
data = {}
for field_name, field in code.template._fields.iteritems():
if (getattr(field, 'translate', False)
and (getattr(code.template, field_name) !=
prev_data[field_name])):
data[field_name] = getattr(code.template,
field_name)
if data:
self.write([code], data)
template2tax_code[self.template.id] = self.id
for child in self.childs:
child.update_tax_code(template2tax_code=template2tax_code)
class OpenChartTaxCodeStart(ModelView):
'Open Chart of Tax Codes'
__name__ = 'account.tax.code.open_chart.start'
method = fields.Selection([
('fiscalyear', 'By Fiscal Year'),
('periods', 'By Periods'),
], 'Method', required=True)
fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscal Year',
help='Leave empty for all open fiscal year',
states={
'invisible': Eval('method') != 'fiscalyear',
}, depends=['method'])
periods = fields.Many2Many('account.period', None, None, 'Periods',
help='Leave empty for all periods of all open fiscal year',
states={
'invisible': Eval('method') != 'periods',
}, depends=['method'])
@staticmethod
def default_method():
return 'periods'
class OpenChartTaxCode(Wizard):
'Open Chart of Tax Codes'
__name__ = 'account.tax.code.open_chart'
start = StateView('account.tax.code.open_chart.start',
'account.tax_code_open_chart_start_view_form', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Open', 'open_', 'tryton-ok', default=True),
])
open_ = StateAction('account.act_tax_code_tree2')
def do_open_(self, action):
if self.start.method == 'fiscalyear':
action['pyson_context'] = PYSONEncoder().encode({
'fiscalyear': (self.start.fiscalyear.id
if self.start.fiscalyear else None),
})
if self.start.fiscalyear:
action['name'] += ' - %s' % self.start.fiscalyear.rec_name
else:
action['pyson_context'] = PYSONEncoder().encode({
'periods': [x.id for x in self.start.periods],
})
period_str = ', '.join(p.rec_name for p in self.start.periods)
if period_str:
action['name'] += ' (%s)' % period_str
return action, {}
def transition_open_(self):
return 'end'
class TaxTemplate(ModelSQL, ModelView):
'Account Tax Template'
__name__ = 'account.tax.template'
name = fields.Char('Name', required=True, translate=True)
description = fields.Char('Description', required=True, translate=True)
group = fields.Many2One('account.tax.group', 'Group')
sequence = fields.Integer('Sequence')
start_date = fields.Date('Starting Date')
end_date = fields.Date('Ending Date')
amount = fields.Numeric('Amount', digits=(16, 8),
states={
'required': Eval('type') == 'fixed',
'invisible': Eval('type') != 'fixed',
},
depends=['type'])
rate = fields.Numeric('Rate', digits=(14, 10),
states={
'required': Eval('type') == 'percentage',
'invisible': Eval('type') != 'percentage',
}, depends=['type'])
type = fields.Selection([
('percentage', 'Percentage'),
('fixed', 'Fixed'),
('none', 'None'),
], 'Type', required=True)
parent = fields.Many2One('account.tax.template', 'Parent')
childs = fields.One2Many('account.tax.template', 'parent', 'Children')
invoice_account = fields.Many2One('account.account.template',
'Invoice Account')
credit_note_account = fields.Many2One('account.account.template',
'Credit Note Account')
invoice_base_code = fields.Many2One('account.tax.code.template',
'Invoice Base Code')
invoice_base_sign = fields.Numeric('Invoice Base Sign', digits=(2, 0))
invoice_tax_code = fields.Many2One('account.tax.code.template',
'Invoice Tax Code')
invoice_tax_sign = fields.Numeric('Invoice Tax Sign', digits=(2, 0))
credit_note_base_code = fields.Many2One('account.tax.code.template',
'Credit Note Base Code')
credit_note_base_sign = fields.Numeric('Credit Note Base Sign',
digits=(2, 0))
credit_note_tax_code = fields.Many2One('account.tax.code.template',
'Credit Note Tax Code')
credit_note_tax_sign = fields.Numeric('Credit Note Tax Sign',
digits=(2, 0))
account = fields.Many2One('account.account.template', 'Account Template',
domain=[('parent', '=', None)], required=True)
@classmethod
def __setup__(cls):
super(TaxTemplate, cls).__setup__()
cls._order.insert(0, ('sequence', 'ASC'))
cls._order.insert(0, ('account', 'ASC'))
@classmethod
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
super(TaxTemplate, cls).__register__(module_name)
cursor = Transaction().cursor
table = TableHandler(cursor, cls, module_name)
# Migration from 1.0 group is no more required
table.not_null_action('group', action='remove')
# Migration from 2.4: drop required on sequence
table.not_null_action('sequence', action='remove')
# Migration from 2.8: rename percentage into rate
if table.column_exist('percentage'):
sql_table = cls.__table__()
cursor.execute(*sql_table.update(
columns=[sql_table.rate],
values=[sql_table.percentage / 100]))
table.drop_column('percentage')
@staticmethod
def order_sequence(tables):
table, _ = tables[None]
return [table.sequence == Null, table.sequence]
@staticmethod
def default_type():
return 'percentage'
@staticmethod
def default_include_base_amount():
return False
@staticmethod
def default_invoice_base_sign():
return Decimal('1')
@staticmethod
def default_invoice_tax_sign():
return Decimal('1')
@staticmethod
def default_credit_note_base_sign():
return Decimal('1')
@staticmethod
def default_credit_note_tax_sign():
return Decimal('1')
def _get_tax_value(self, tax=None):
'''
Set values for tax creation.
'''
res = {}
for field in ('name', 'description', 'sequence', 'amount',
'rate', 'type', 'invoice_base_sign', 'invoice_tax_sign',
'credit_note_base_sign', 'credit_note_tax_sign',
'start_date', 'end_date'):
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
def create_tax(self, company_id, template2tax_code, template2account,
template2tax=None, parent_id=None):
'''
Create recursively taxes based on template.
template2tax_code is a dictionary with tax code template id as key and
tax code id as value, used to convert tax code template into tax code.
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.
Return id of the tax created
'''
pool = Pool()
Tax = pool.get('account.tax')
Lang = pool.get('ir.lang')
Config = pool.get('ir.configuration')
if template2tax is None:
template2tax = {}
if self.id not in template2tax:
vals = self._get_tax_value()
vals['company'] = company_id
vals['parent'] = parent_id
if self.invoice_account:
vals['invoice_account'] = \
template2account[self.invoice_account.id]
else:
vals['invoice_account'] = None
if self.credit_note_account:
vals['credit_note_account'] = \
template2account[self.credit_note_account.id]
else:
vals['credit_note_account'] = None
if self.invoice_base_code:
vals['invoice_base_code'] = \
template2tax_code[self.invoice_base_code.id]
else:
vals['invoice_base_code'] = None
if self.invoice_tax_code:
vals['invoice_tax_code'] = \
template2tax_code[self.invoice_tax_code.id]
else:
vals['invoice_tax_code'] = None
if self.credit_note_base_code:
vals['credit_note_base_code'] = \
template2tax_code[self.credit_note_base_code.id]
else:
vals['credit_note_base_code'] = None
if self.credit_note_tax_code:
vals['credit_note_tax_code'] = \
template2tax_code[self.credit_note_tax_code.id]
else:
vals['credit_note_tax_code'] = None
new_tax, = Tax.create([vals])
prev_lang = self._context.get('language') or Config.get_language()
prev_data = {}
for field_name, field in self._fields.iteritems():
if getattr(field, 'translate', False):
prev_data[field_name] = getattr(self, field_name)
for lang in Lang.get_translatable_languages():
if lang == prev_lang:
continue
with Transaction().set_context(language=lang):
template = self.__class__(self.id)
data = {}
for field_name, field in template._fields.iteritems():
if (getattr(field, 'translate', False)
and (getattr(template, field_name)
!= prev_data[field_name])):
data[field_name] = getattr(template, field_name)
if data:
Tax.write([new_tax], data)
template2tax[self.id] = new_tax.id
new_id = template2tax[self.id]
new_childs = []
for child in self.childs:
new_childs.append(child.create_tax(company_id, template2tax_code,
template2account, template2tax=template2tax,
parent_id=new_id))
return new_id
class Tax(ModelSQL, ModelView):
'''
Account Tax
Type:
percentage: tax = price * rate
fixed: tax = amount
none: tax = none
'''
__name__ = 'account.tax'
name = fields.Char('Name', required=True, translate=True)
description = fields.Char('Description', required=True, translate=True,
help="The name that will be used in reports")
group = fields.Many2One('account.tax.group', 'Group',
states={
'invisible': Bool(Eval('parent')),
}, depends=['parent'])
active = fields.Boolean('Active')
sequence = fields.Integer('Sequence', help='Use to order the taxes')
start_date = fields.Date('Starting Date')
end_date = fields.Date('Ending Date')
currency_digits = fields.Function(fields.Integer('Currency Digits'),
'on_change_with_currency_digits')
amount = fields.Numeric('Amount', digits=(16, Eval('currency_digits', 2)),
states={
'required': Eval('type') == 'fixed',
'invisible': Eval('type') != 'fixed',
}, help='In company\'s currency',
depends=['type', 'currency_digits'])
rate = fields.Numeric('Rate', digits=(14, 10),
states={
'required': Eval('type') == 'percentage',
'invisible': Eval('type') != 'percentage',
}, depends=['type'])
type = fields.Selection([
('percentage', 'Percentage'),
('fixed', 'Fixed'),
('none', 'None'),
], 'Type', required=True)
parent = fields.Many2One('account.tax', 'Parent', ondelete='CASCADE')
childs = fields.One2Many('account.tax', 'parent', 'Children')
company = fields.Many2One('company.company', 'Company', required=True,
domain=[
('id', If(Eval('context', {}).contains('company'), '=', '!='),
Eval('context', {}).get('company', -1)),
], select=True)
invoice_account = fields.Many2One('account.account', 'Invoice Account',
domain=[
('company', '=', Eval('company')),
('kind', 'not in', ['view', 'receivable', 'payable']),
],
states={
'readonly': (Eval('type') == 'none') | ~Eval('company'),
'required': (Eval('type') != 'none') & Eval('company'),
},
depends=['company', 'type'])
credit_note_account = fields.Many2One('account.account',
'Credit Note Account',
domain=[
('company', '=', Eval('company')),
('kind', 'not in', ['view', 'receivable', 'payable']),
],
states={
'readonly': (Eval('type') == 'none') | ~Eval('company'),
'required': (Eval('type') != 'none') & Eval('company'),
},
depends=['company', 'type'])
invoice_base_code = fields.Many2One('account.tax.code',
'Invoice Base Code',
domain=[
('company', '=', Eval('company', -1)),
],
states={
'readonly': Eval('type') == 'none',
},
depends=['type', 'company'])
invoice_base_sign = fields.Numeric('Invoice Base Sign', digits=(2, 0),
help='Usualy 1 or -1',
states={
'required': Eval('type') != 'none',
'readonly': Eval('type') == 'none',
}, depends=['type'])
invoice_tax_code = fields.Many2One('account.tax.code',
'Invoice Tax Code',
domain=[
('company', '=', Eval('company', -1)),
],
states={
'readonly': Eval('type') == 'none',
},
depends=['type', 'company'])
invoice_tax_sign = fields.Numeric('Invoice Tax Sign', digits=(2, 0),
help='Usualy 1 or -1',
states={
'required': Eval('type') != 'none',
'readonly': Eval('type') == 'none',
}, depends=['type'])
credit_note_base_code = fields.Many2One('account.tax.code',
'Credit Note Base Code',
states={
'readonly': Eval('type') == 'none',
}, depends=['type'])
credit_note_base_sign = fields.Numeric('Credit Note Base Sign',
digits=(2, 0), help='Usualy 1 or -1',
domain=[
('company', '=', Eval('company', -1)),
],
states={
'required': Eval('type') != 'none',
'readonly': Eval('type') == 'none',
},
depends=['type', 'company'])
credit_note_tax_code = fields.Many2One('account.tax.code',
'Credit Note Tax Code',
domain=[
('company', '=', Eval('company', -1)),
],
states={
'readonly': Eval('type') == 'none',
},
depends=['type', 'company'])
credit_note_tax_sign = fields.Numeric('Credit Note Tax Sign',
digits=(2, 0), help='Usualy 1 or -1',
states={
'required': Eval('type') != 'none',
'readonly': Eval('type') == 'none',
}, depends=['type'])
template = fields.Many2One('account.tax.template', 'Template')
@classmethod
def __setup__(cls):
super(Tax, cls).__setup__()
cls._order.insert(0, ('sequence', 'ASC'))
@classmethod
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
super(Tax, cls).__register__(module_name)
cursor = Transaction().cursor
table = TableHandler(cursor, cls, module_name)
# Migration from 1.0 group is no more required
table.not_null_action('group', action='remove')
# Migration from 2.4: drop required on sequence
table.not_null_action('sequence', action='remove')
# Migration from 2.8: rename percentage into rate
if table.column_exist('percentage'):
sql_table = cls.__table__()
cursor.execute(*sql_table.update(
columns=[sql_table.rate],
values=[sql_table.percentage / 100]))
table.drop_column('percentage')
@staticmethod
def order_sequence(tables):
table, _ = tables[None]
return [table.sequence == Null, table.sequence]
@staticmethod
def default_active():
return True
@staticmethod
def default_type():
return 'percentage'
@staticmethod
def default_include_base_amount():
return False
@staticmethod
def default_invoice_base_sign():
return Decimal('1')
@staticmethod
def default_invoice_tax_sign():
return Decimal('1')
@staticmethod
def default_credit_note_base_sign():
return Decimal('1')
@staticmethod
def default_credit_note_tax_sign():
return Decimal('1')
@staticmethod
def default_company():
return Transaction().context.get('company')
@fields.depends('company')
def on_change_with_currency_digits(self, name=None):
if self.company:
return self.company.currency.digits
return 2
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,
}
@classmethod
def _unit_compute(cls, taxes, price_unit, date):
res = []
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 != 'none':
res.append(tax._process_tax(price_unit))
if len(tax.childs):
res.extend(cls._unit_compute(tax.childs, price_unit, date))
return res
@classmethod
def sort_taxes(cls, taxes):
'''
Return a list of taxes sorted
'''
return sorted(taxes, key=lambda t: (t.sequence, t.id))
@classmethod
def compute(cls, taxes, price_unit, quantity, date=None):
'''
Compute taxes for price_unit and quantity at the date.
Return list of dict for each taxes and their childs with:
base
amount
tax
'''
pool = Pool()
Date = pool.get('ir.date')
if date is None:
date = Date.today()
taxes = cls.sort_taxes(taxes)
res = cls._unit_compute(taxes, price_unit, date)
quantity = Decimal(str(quantity or 0.0))
for row in res:
row['base'] *= quantity
row['amount'] *= quantity
return res
def update_tax(self, template2tax_code, template2account,
template2tax=None):
'''
Update recursively taxes based on template.
template2tax_code is a dictionary with tax code template id as key and
tax code id as value, used to convert tax code template into tax code.
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()
Lang = pool.get('ir.lang')
Config = pool.get('ir.configuration')
if template2tax is None:
template2tax = {}
if self.template:
vals = self.template._get_tax_value(tax=self)
invoice_account_id = (self.invoice_account.id
if self.invoice_account else None)
if (self.template.invoice_account and
invoice_account_id != template2account.get(
self.template.invoice_account.id)):
vals['invoice_account'] = template2account.get(
self.template.invoice_account.id)
elif (not self.template.invoice_account
and self.invoice_account):
vals['invoice_account'] = None
credit_note_account_id = (self.credit_note_account.id
if self.credit_note_account else None)
if (self.template.credit_note_account and
credit_note_account_id != template2account.get(
self.template.credit_note_account.id)):
vals['credit_note_account'] = template2account.get(
self.template.credit_note_account.id)
elif (not self.template.credit_note_account
and self.credit_note_account):
vals['credit_note_account'] = None
invoice_base_code_id = (self.invoice_base_code.id
if self.invoice_base_code else None)
if (self.template.invoice_base_code and
invoice_base_code_id != template2tax_code.get(
self.template.invoice_base_code.id)):
vals['invoice_base_code'] = template2tax_code.get(
self.template.invoice_base_code.id)
elif (not self.template.invoice_base_code
and self.invoice_base_code):
vals['invoice_base_code'] = None
invoice_tax_code_id = (self.invoice_tax_code.id
if self.invoice_tax_code else None)
if (self.template.invoice_tax_code
and invoice_tax_code_id != template2tax_code.get(
self.template.invoice_tax_code.id)):
vals['invoice_tax_code'] = template2tax_code.get(
self.template.invoice_tax_code.id)
elif (not self.template.invoice_tax_code
and self.invoice_tax_code):
vals['invoice_tax_code'] = None
credit_note_base_code_id = (self.credit_note_base_code.id
if self.credit_note_base_code else None)
if (self.template.credit_note_base_code
and credit_note_base_code_id != template2tax_code.get(
self.template.credit_note_base_code.id)):
vals['credit_note_base_code'] = template2tax_code.get(
self.template.credit_note_base_code.id)
elif (not self.template.credit_note_base_code
and self.credit_note_base_code):
vals['credit_note_base_code'] = None
credit_note_tax_code_id = (self.credit_note_tax_code.id
if self.credit_note_tax_code else None)
if (self.template.credit_note_tax_code
and credit_note_tax_code_id != template2tax_code.get(
self.template.credit_note_tax_code.id)):
vals['credit_note_tax_code'] = template2tax_code.get(
self.template.credit_note_tax_code.id)
elif (not self.template.credit_note_tax_code
and self.credit_note_tax_code):
vals['credit_note_tax_code'] = None
if vals:
self.write([self], vals)
prev_lang = self._context.get('language') or Config.get_language()
prev_data = {}
for field_name, field in self.template._fields.iteritems():
if getattr(field, 'translate', False):
prev_data[field_name] = getattr(self.template, field_name)
for lang in Lang.get_translatable_languages():
if lang == prev_lang:
continue
with Transaction().set_context(language=lang):
tax = self.__class__(self.id)
data = {}
for field_name, field in tax.template._fields.iteritems():
if (getattr(field, 'translate', False)
and (getattr(tax.template, field_name)
!= prev_data[field_name])):
data[field_name] = getattr(tax.template,
field_name)
if data:
self.write([tax], data)
template2tax[self.template.id] = self.id
for child in self.childs:
child.update_tax(template2tax_code, template2account,
template2tax=template2tax)
class _TaxKey(dict):
def __init__(self, **kwargs):
self.update(kwargs)
def _key(self):
return (self['base_code'], self['base_sign'],
self['tax_code'], self['tax_sign'],
self['account'], self['tax'])
def __eq__(self, other):
if isinstance(other, _TaxKey):
return self._key() == other._key()
return self._key() == other
def __hash__(self):
return hash(self._key())
_TaxableLine = namedtuple('_TaxableLine', ('taxes', 'unit_price', 'quantity'))
class TaxableMixin(object):
@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
"""
return []
@property
def tax_type(self):
"The taxation type it can be 'invoice' or 'credit_note'"
return None
@property
def currency(self):
"The currency used by the taxable object"
return None
@property
def tax_date(self):
"Date to use when computing the tax"
pool = Pool()
Date = pool.get('ir.date')
return Date.today()
def _get_tax_context(self):
return {}
@staticmethod
def _compute_tax_line(type_, amount, base, tax):
assert type_ in ('invoice', 'credit_note')
line = {}
line['manual'] = False
line['description'] = tax.description
line['base'] = base
line['amount'] = amount
line['tax'] = tax.id if tax else None
for attribute in ['base_code', 'tax_code', 'account']:
value = getattr(tax, '%s_%s' % (type_, attribute), None)
line[attribute] = value.id if value else None
for attribute in ['base_sign', 'tax_sign']:
value = getattr(tax, '%s_%s' % (type_, attribute), None)
line[attribute] = value
return _TaxKey(**line)
def _round_taxes(self, taxes):
if not self.currency:
return
for taxline in taxes.itervalues():
for attribute in ('base', 'amount'):
taxline[attribute] = self.currency.round(taxline[attribute])
def _get_taxes(self):
pool = Pool()
Tax = pool.get('account.tax')
Configuration = pool.get('account.configuration')
config = Configuration(1)
taxes = {}
with Transaction().set_context(self._get_tax_context()):
taxable_lines = [_TaxableLine(*params)
for params in self.taxable_lines]
for line in taxable_lines:
l_taxes = Tax.compute(line.taxes, line.unit_price,
line.quantity, self.tax_date)
for tax in l_taxes:
taxline = self._compute_tax_line(self.tax_type, **tax)
if taxline not in taxes:
taxes[taxline] = taxline
else:
taxes[taxline]['base'] += taxline['base']
taxes[taxline]['amount'] += taxline['amount']
if config.tax_rounding == 'line':
self._round_taxes(taxes)
if config.tax_rounding == 'document':
self._round_taxes(taxes)
return taxes
class TaxLine(ModelSQL, ModelView):
'Tax Line'
__name__ = 'account.tax.line'
_rec_name = 'amount'
currency_digits = fields.Function(fields.Integer('Currency Digits'),
'on_change_with_currency_digits')
amount = fields.Numeric('Amount', digits=(16, Eval('currency_digits', 2)),
required=True, depends=['currency_digits'])
code = fields.Many2One('account.tax.code', 'Code', select=True,
required=True,
domain=[
('company', '=', Eval('company', -1)),
],
depends=['company'])
tax = fields.Many2One('account.tax', 'Tax', select=True,
ondelete='RESTRICT',
domain=[
('company', '=', Eval('company', -1)),
],
depends=['company'])
move_line = fields.Many2One('account.move.line', 'Move Line',
required=True, select=True, ondelete='CASCADE')
company = fields.Function(fields.Many2One('company.company', 'Company'),
'on_change_with_company')
@fields.depends('move_line')
def on_change_with_currency_digits(self, name=None):
if self.move_line:
return self.move_line.currency_digits
return 2
@fields.depends('tax')
def on_change_tax(self):
self.code = None
@fields.depends('_parent_move_line.account')
def on_change_with_company(self, name=None):
if self.move_line:
return self.move_line.account.company.id
class TaxRuleTemplate(ModelSQL, ModelView):
'Tax Rule Template'
__name__ = 'account.tax.rule.template'
name = fields.Char('Name', required=True, translate=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
def create_rule(self, 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.
Return id of the tax rule created
'''
pool = Pool()
Rule = pool.get('account.tax.rule')
Lang = pool.get('ir.lang')
Config = pool.get('ir.configuration')
if template2rule is None:
template2rule = {}
if self.id not in template2rule:
vals = self._get_tax_rule_value()
vals['company'] = company_id
new_rule, = Rule.create([vals])
prev_lang = self._context.get('language') or Config.get_language()
prev_data = {}
for field_name, field in self._fields.iteritems():
if getattr(field, 'translate', False):
prev_data[field_name] = getattr(self, field_name)
for lang in Lang.get_translatable_languages():
if lang == prev_lang:
continue
with Transaction().set_context(language=lang):
template = self.__class__(self.id)
data = {}
for field_name, field in template._fields.iteritems():
if (getattr(field, 'translate', False)
and (getattr(self, field_name)
!= prev_data[field_name])):
data[field_name] = getattr(self, field_name)
if data:
Rule.write([new_rule], data)
template2rule[self.id] = new_rule.id
return template2rule[self.id]
class TaxRule(ModelSQL, ModelView):
'Tax Rule'
__name__ = 'account.tax.rule'
name = fields.Char('Name', required=True, translate=True)
kind = fields.Selection(KINDS, 'Kind', required=True)
company = fields.Many2One('company.company', 'Company', required=True,
select=True, domain=[
('id', If(Eval('context', {}).contains('company'), '=', '!='),
Eval('context', {}).get('company', -1)),
])
lines = fields.One2Many('account.tax.rule.line', 'rule', 'Lines')
template = fields.Many2One('account.tax.rule.template', 'Template')
@staticmethod
def default_kind():
return 'both'
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()
return tax and [tax.id] or None
def update_rule(self, 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.
'''
pool = Pool()
Lang = pool.get('ir.lang')
Config = pool.get('ir.configuration')
if template2rule is None:
template2rule = {}
if self.template:
vals = self.template._get_tax_rule_value(rule=self)
if vals:
self.write([self], vals)
prev_lang = self._context.get('language') or Config.get_language()
prev_data = {}
for field_name, field in self.template._fields.iteritems():
if getattr(field, 'translate', False):
prev_data[field_name] = getattr(self.template, field_name)
for lang in Lang.get_translatable_languages():
if lang == prev_lang:
continue
with Transaction().set_context(language=lang):
rule = self.__class__(self.id)
data = {}
for field_name, field in rule.template._fields.iteritems():
if (getattr(field, 'translate', False)
and (getattr(rule.template, field_name)
!= prev_data[field_name])):
data[field_name] = getattr(rule.template,
field_name)
if data:
self.write([rule], data)
template2rule[self.template.id] = self.id
class TaxRuleLineTemplate(ModelSQL, ModelView):
'Tax Rule Line Template'
__name__ = 'account.tax.rule.line.template'
rule = fields.Many2One('account.tax.rule.template', 'Rule', required=True,
ondelete='CASCADE')
group = fields.Many2One('account.tax.group', 'Tax Group',
ondelete='RESTRICT')
origin_tax = fields.Many2One('account.tax.template', 'Original Tax',
domain=[
('account', '=', Eval('_parent_rule', {}).get('account', 0)),
('group', '=', Eval('group')),
['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=['group'],
ondelete='RESTRICT')
tax = fields.Many2One('account.tax.template', 'Substitution Tax',
domain=[
('account', '=', Eval('_parent_rule', {}).get('account', 0)),
('group', '=', Eval('group')),
['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=['group'],
ondelete='RESTRICT')
sequence = fields.Integer('Sequence')
@classmethod
def __setup__(cls):
super(TaxRuleLineTemplate, cls).__setup__()
cls._order.insert(0, ('rule', 'ASC'))
cls._order.insert(0, ('sequence', 'ASC'))
@classmethod
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
cursor = Transaction().cursor
table = TableHandler(cursor, cls, module_name)
super(TaxRuleLineTemplate, cls).__register__(module_name)
# Migration from 2.4: drop required on sequence
table.not_null_action('sequence', action='remove')
@staticmethod
def order_sequence(tables):
table, _ = tables[None]
return [table.sequence == Null, table.sequence]
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.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.template != self:
res['template'] = self.id
return res
def create_rule_line(self, 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.
Return id of the tax rule line created
'''
RuleLine = Pool().get('account.tax.rule.line')
if template2rule_line is None:
template2rule_line = {}
if self.id not in template2rule_line:
vals = self._get_tax_rule_line_value()
vals['rule'] = template2rule[self.rule.id]
if self.origin_tax:
vals['origin_tax'] = template2tax[self.origin_tax.id]
else:
vals['origin_tax'] = None
if self.tax:
vals['tax'] = template2tax[self.tax.id]
else:
vals['tax'] = None
new_rule_line, = RuleLine.create([vals])
template2rule_line[self.id] = new_rule_line.id
return template2rule_line[self.id]
class TaxRuleLine(ModelSQL, ModelView, MatchMixin):
'Tax Rule Line'
__name__ = 'account.tax.rule.line'
_rec_name = 'tax'
rule = fields.Many2One('account.tax.rule', 'Rule', required=True,
select=True, ondelete='CASCADE')
group = fields.Many2One('account.tax.group', 'Tax Group',
ondelete='RESTRICT')
origin_tax = fields.Many2One('account.tax', 'Original Tax',
domain=[
('company', '=', Eval('_parent_rule', {}).get('company')),
('group', '=', Eval('group')),
['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=['group'],
ondelete='RESTRICT')
tax = fields.Many2One('account.tax', 'Substitution Tax',
domain=[
('company', '=', Eval('_parent_rule', {}).get('company')),
('group', '=', Eval('group')),
['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=['group'],
ondelete='RESTRICT')
sequence = fields.Integer('Sequence')
template = fields.Many2One('account.tax.rule.line.template', 'Template')
@classmethod
def __setup__(cls):
super(TaxRuleLine, cls).__setup__()
cls._order.insert(0, ('rule', 'ASC'))
cls._order.insert(0, ('sequence', 'ASC'))
@classmethod
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
cursor = Transaction().cursor
table = TableHandler(cursor, cls, module_name)
super(TaxRuleLine, cls).__register__(module_name)
# Migration from 2.4: drop required on sequence
table.not_null_action('sequence', action='remove')
@staticmethod
def order_sequence(tables):
table, _ = tables[None]
return [table.sequence == Null, table.sequence]
def match(self, pattern):
if 'group' in pattern and not self.group:
if pattern['group']:
return False
return super(TaxRuleLine, self).match(pattern)
def get_taxes(self):
'''
Return list of taxes for a line
'''
if self.tax:
return [self.tax.id]
return None
def update_rule_line(self, 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 = {}
if self.template:
vals = self.template._get_tax_rule_line_value(rule_line=self)
if self.rule.id != template2rule[self.template.rule.id]:
vals['rule'] = template2rule[self.template.rule.id]
if self.origin_tax:
if self.template.origin_tax:
if (self.origin_tax.id !=
template2tax[self.template.origin_tax.id]):
vals['origin_tax'] = template2tax[
self.template.origin_tax.id]
else:
vals['origin_tax'] = None
elif self.template.origin_tax:
vals['origin_tax'] = template2tax[self.template.origin_tax.id]
if self.tax:
if self.template.tax:
if self.tax.id != template2tax[self.template.tax.id]:
vals['tax'] = template2tax[self.template.tax.id]
else:
vals['tax'] = None
elif self.template.tax:
vals['tax'] = template2tax[self.template.tax.id]
if vals:
self.write([self], vals)
template2rule_line[self.template.id] = self.id
class OpenTaxCode(Wizard):
'Open Code'
__name__ = 'account.tax.open_code'
start_state = 'open_'
open_ = StateAction('account.act_tax_line_form')
def do_open_(self, action):
pool = Pool()
FiscalYear = pool.get('account.fiscalyear')
Period = pool.get('account.period')
if not Transaction().context.get('fiscalyear'):
fiscalyears = FiscalYear.search([
('state', '=', 'open'),
])
else:
fiscalyears = [FiscalYear(Transaction().context['fiscalyear'])]
periods = []
if not Transaction().context.get('periods'):
periods = Period.search([
('fiscalyear', 'in', [f.id for f in fiscalyears]),
])
else:
periods = Period.browse(Transaction().context['periods'])
action['pyson_domain'] = PYSONEncoder().encode([
('move_line.period', 'in', [p.id for p in periods]),
('code', '=', Transaction().context['active_id']),
])
if Transaction().context.get('fiscalyear'):
action['pyson_context'] = PYSONEncoder().encode({
'fiscalyear': Transaction().context['fiscalyear'],
})
else:
action['pyson_context'] = PYSONEncoder().encode({
'periods': [p.id for p in periods],
})
return action, {}
class AccountTemplateTaxTemplate(ModelSQL):
'Account Template - Tax Template'
__name__ = 'account.account.template-account.tax.template'
_table = 'account_account_template_tax_rel'
account = fields.Many2One('account.account.template', 'Account Template',
ondelete='CASCADE', select=True, required=True)
tax = fields.Many2One('account.tax.template', 'Tax Template',
ondelete='RESTRICT', select=True, required=True)
class AccountTemplate2:
__name__ = 'account.account.template'
taxes = fields.Many2Many('account.account.template-account.tax.template',
'account', 'tax', 'Default Taxes',
domain=[('parent', '=', None)])
class AccountTax(ModelSQL):
'Account - Tax'
__name__ = 'account.account-account.tax'
_table = 'account_account_tax_rel'
account = fields.Many2One('account.account', 'Account', ondelete='CASCADE',
select=True, required=True)
tax = fields.Many2One('account.tax', 'Tax', ondelete='RESTRICT',
select=True, required=True)
class Account2:
__name__ = 'account.account'
taxes = fields.Many2Many('account.account-account.tax',
'account', 'tax', 'Default Taxes',
domain=[
('company', '=', Eval('company')),
('parent', '=', None),
],
help=('Default tax for manual encoding of move lines \n'
'for journal types: "expense" and "revenue"'),
depends=['company'])