# 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")