From 24bb0b1d08c321620f63ec7b60e1639f0f48aeab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20=C3=89vrard?= Date: Fri, 23 Jan 2015 10:41:05 +0100 Subject: [PATCH] Add unit_price modifying taxes issue4115 review12451002 --- CHANGELOG | 1 + doc/index.rst | 2 + tax.py | 124 ++++++++++++++++++----- tests/test_account.py | 198 ++++++++++++++++++++++++++++++++++++- view/tax_form.xml | 3 + view/tax_template_form.xml | 3 + 6 files changed, 304 insertions(+), 27 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 891a859e8d..5016c5c918 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Allow tax modifying the unit price * Add reverse_compute on Tax * Add fix_party script * Allow to display only the balance in the General Ledger diff --git a/doc/index.rst b/doc/index.rst index 7905b8221a..358267724e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -205,6 +205,8 @@ the following fields: - Amount: If Type is *Fixed*, defines a fix amount for the tax. - Percentage: If Type is *Percentage*, defines the percentage of the tax. +- Update Unit Price: If checked then the unit price for further tax calculation + will be increased by the amount of this tax. - Parent, Children: Parent and children taxes - Company: The company for which the tax is defined. - Invoice Account: The account to use when creating move lines for diff --git a/tax.py b/tax.py index 1d5d54f89d..b00772069a 100644 --- a/tax.py +++ b/tax.py @@ -6,6 +6,7 @@ from decimal import Decimal from sql import Null from sql.aggregate import Sum +from itertools import groupby from trytond.model import ModelView, ModelSQL, MatchMixin, fields from trytond.wizard import Wizard, StateView, StateAction, Button @@ -369,6 +370,11 @@ class TaxTemplate(ModelSQL, ModelView): ('fixed', 'Fixed'), ('none', 'None'), ], 'Type', required=True) + update_unit_price = fields.Boolean('Update Unit Price', + states={ + 'invisible': Bool(Eval('parent')), + }, + depends=['parent']) parent = fields.Many2One('account.tax.template', 'Parent') childs = fields.One2Many('account.tax.template', 'parent', 'Children') invoice_account = fields.Many2One('account.account.template', @@ -397,6 +403,10 @@ class TaxTemplate(ModelSQL, ModelView): super(TaxTemplate, cls).__setup__() cls._order.insert(0, ('sequence', 'ASC')) cls._order.insert(0, ('account', 'ASC')) + cls._error_messages.update({ + 'update_unit_price_with_parent': ('"Update Unit Price" can ' + 'not be set on tax "%(template)s" which has a parent.'), + }) @classmethod def __register__(cls, module_name): @@ -419,6 +429,18 @@ class TaxTemplate(ModelSQL, ModelView): values=[sql_table.percentage / 100])) table.drop_column('percentage') + @classmethod + def validate(cls, tax_templates): + super(TaxTemplate, cls).validate(tax_templates) + for tax_template in tax_templates: + tax_template.check_update_unit_price() + + def check_update_unit_price(self): + if self.update_unit_price and self.parent: + self.raise_user_error('update_unit_price_with_parent', { + 'template': self.rec_name, + }) + @staticmethod def order_sequence(tables): table, _ = tables[None] @@ -448,6 +470,10 @@ class TaxTemplate(ModelSQL, ModelView): def default_credit_note_tax_sign(): return Decimal('1') + @staticmethod + def default_update_unit_price(): + return False + def _get_tax_value(self, tax=None): ''' Set values for tax creation. @@ -456,7 +482,7 @@ class TaxTemplate(ModelSQL, ModelView): 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'): + 'start_date', 'end_date', 'update_unit_price'): if not tax or getattr(tax, field) != getattr(self, field): res[field] = getattr(self, field) for field in ('group',): @@ -598,6 +624,13 @@ class Tax(ModelSQL, ModelView): ('fixed', 'Fixed'), ('none', 'None'), ], 'Type', required=True) + update_unit_price = fields.Boolean('Update Unit Price', + states={ + 'invisible': Bool(Eval('parent')), + }, + depends=['parent'], + 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') childs = fields.One2Many('account.tax', 'parent', 'Children') company = fields.Many2One('company.company', 'Company', required=True, @@ -692,6 +725,10 @@ class Tax(ModelSQL, ModelView): def __setup__(cls): super(Tax, cls).__setup__() cls._order.insert(0, ('sequence', 'ASC')) + cls._error_messages.update({ + 'update_unit_price_with_parent': ('"Update Unit Price" can ' + 'not be set on tax "%(template)s" which has a parent.'), + }) @classmethod def __register__(cls, module_name): @@ -714,6 +751,18 @@ class Tax(ModelSQL, ModelView): values=[sql_table.percentage / 100])) table.drop_column('percentage') + @classmethod + def validate(cls, taxes): + super(Tax, cls).validate(taxes) + for tax in taxes: + tax.check_update_unit_price() + + def check_update_unit_price(self): + if self.parent and self.update_unit_price: + self.raise_user_error('update_unit_price_with_parent', { + 'tax': self.rec_name, + }) + @staticmethod def order_sequence(tables): table, _ = tables[None] @@ -747,6 +796,10 @@ class Tax(ModelSQL, ModelView): def default_credit_note_tax_sign(): return Decimal('1') + @staticmethod + def default_update_unit_price(): + return False + @staticmethod def default_company(): return Transaction().context.get('company') @@ -773,24 +826,33 @@ class Tax(ModelSQL, ModelView): '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 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)) + 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 + if not (start_date <= date <= end_date): + continue + if tax.type != 'none': + value = tax._process_tax(price_unit) + res.append(value) + if tax.update_unit_price: + unit_price_variation += value['amount'] + if len(tax.childs): + res.extend(cls._unit_compute(tax.childs, price_unit, date)) + price_unit += unit_price_variation return res @classmethod def _reverse_rate_amount(cls, taxes, date): - rate = None - amount = 0 + 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 @@ -798,30 +860,40 @@ class Tax(ModelSQL, ModelView): continue if tax.type == 'percentage': - if rate is None: - rate = tax.rate - else: - rate += tax.rate + rate += tax.rate elif tax.type == 'fixed': amount += tax.amount if tax.childs: child_rate, child_amount = cls._reverse_rate_amount( tax.childs, date) - if rate is None: - rate = child_rate - else: - rate += child_rate + rate += child_rate amount += child_amount return rate, amount @classmethod def _reverse_unit_compute(cls, price_unit, taxes, date): - rate, amount = cls._reverse_rate_amount(taxes, date) - price_unit -= amount - if rate is not None: - price_unit = price_unit / (1 + rate) - return price_unit + 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): @@ -860,7 +932,7 @@ class Tax(ModelSQL, ModelView): Date = pool.get('ir.date') if date is None: date = Date.today() - taxes = cls.sort_taxes(taxes, reverse=True) + taxes = cls.sort_taxes(taxes) return cls._reverse_unit_compute(price_unit, taxes, date) def update_tax(self, template2tax_code, template2account, diff --git a/tests/test_account.py b/tests/test_account.py index f554981270..51c5fd41fa 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -467,7 +467,6 @@ class AccountTestCase(unittest.TestCase): ecotax1.amount = Decimal(5) ecotax1.invoice_account = tax_account ecotax1.credit_note_account = tax_account - ecotax1.update_unit_price = True ecotax1.sequence = 10 ecotax1.save() @@ -508,6 +507,203 @@ class AccountTestCase(unittest.TestCase): self.tax.reverse_compute(Decimal(135), [vat0, ecotax1, vat1]), Decimal(100)) + def test0045tax_compute_with_update_unit_price(self): + 'Test tax compute with unit_price modifying tax' + with Transaction().start(DB_NAME, USER, context=CONTEXT): + tax_account, = self.account.search([ + ('name', '=', 'Main Tax'), + ]) + ecotax1 = self.tax() + ecotax1.name = ecotax1.description = 'EcoTax 1' + ecotax1.type = 'fixed' + ecotax1.amount = Decimal(5) + ecotax1.invoice_account = tax_account + ecotax1.credit_note_account = tax_account + ecotax1.update_unit_price = True + ecotax1.sequence = 10 + ecotax1.save() + + vat1 = self.tax() + vat1.name = vat1.description = 'VAT1' + vat1.type = 'percentage' + vat1.rate = Decimal('0.2') + vat1.invoice_account = tax_account + vat1.credit_note_account = tax_account + vat1.sequence = 20 + vat1.save() + + self.assertEqual( + self.tax.compute([ecotax1, vat1], Decimal(100), 5), + [{ + 'base': Decimal(500), + 'amount': Decimal(25), + 'tax': ecotax1, + }, { + 'base': Decimal(525), + 'amount': Decimal(105), + 'tax': vat1, + }]) + self.assertEqual( + self.tax.reverse_compute(Decimal(126), [ecotax1, vat1]), + Decimal(100)) + + ecotax2 = self.tax() + ecotax2.name = ecotax2.description = 'EcoTax 2' + ecotax2.type = 'percentage' + ecotax2.rate = Decimal('0.5') + ecotax2.invoice_account = tax_account + ecotax2.credit_note_account = tax_account + ecotax2.update_unit_price = True + ecotax2.sequence = 10 + ecotax2.save() + + self.assertEqual( + self.tax.compute([ecotax1, ecotax2, vat1], Decimal(100), 1), + [{ + 'base': Decimal(100), + 'amount': Decimal(5), + 'tax': ecotax1, + }, { + 'base': Decimal(100), + 'amount': Decimal(50), + 'tax': ecotax2, + }, { + 'base': Decimal(155), + 'amount': Decimal(31), + 'tax': vat1, + }]) + self.assertEqual( + self.tax.reverse_compute(Decimal(186), + [ecotax1, ecotax2, vat1]), + Decimal(100)) + + vat0 = self.tax() + vat0.name = vat0.description = 'VAT0' + vat0.type = 'percentage' + vat0.rate = Decimal('0.1') + vat0.invoice_account = tax_account + vat0.credit_note_account = tax_account + vat0.sequence = 5 + vat0.save() + + self.assertEqual( + self.tax.compute([vat0, ecotax1, vat1], Decimal(100), 1), + [{ + 'base': Decimal(100), + 'amount': Decimal(10), + 'tax': vat0, + }, { + 'base': Decimal(100), + 'amount': Decimal(5), + 'tax': ecotax1, + }, { + 'base': Decimal(105), + 'amount': Decimal(21), + 'tax': vat1, + }]) + self.assertEqual( + self.tax.reverse_compute(Decimal(136), + [vat0, ecotax1, vat1]), + Decimal(100)) + + self.assertEqual( + self.tax.compute([vat0, ecotax1, ecotax2, vat1], + Decimal(100), 1), + [{ + 'base': Decimal(100), + 'amount': Decimal(10), + 'tax': vat0, + }, { + 'base': Decimal(100), + 'amount': Decimal(5), + 'tax': ecotax1, + }, { + 'base': Decimal(100), + 'amount': Decimal(50), + 'tax': ecotax2, + }, { + 'base': Decimal(155), + 'amount': Decimal(31), + 'tax': vat1, + }]) + self.assertEqual( + self.tax.reverse_compute(Decimal(196), + [vat0, ecotax1, ecotax2, vat1]), + Decimal(100)) + + vat2 = self.tax() + vat2.name = vat2.description = 'VAT2' + vat2.type = 'percentage' + vat2.rate = Decimal('0.3') + vat2.invoice_account = tax_account + vat2.credit_note_account = tax_account + vat2.sequence = 30 + vat2.save() + + self.assertEqual( + self.tax.compute([vat0, ecotax1, vat1, vat2], + Decimal(100), 1), + [{ + 'base': Decimal(100), + 'amount': Decimal(10), + 'tax': vat0, + }, { + 'base': Decimal(100), + 'amount': Decimal(5), + 'tax': ecotax1, + }, { + 'base': Decimal(105), + 'amount': Decimal(21), + 'tax': vat1, + }, { + 'base': Decimal(105), + 'amount': Decimal('31.5'), + 'tax': vat2, + }]) + self.assertEqual( + self.tax.reverse_compute(Decimal('167.5'), + [vat0, ecotax1, vat1, vat2]), + Decimal(100)) + + ecotax3 = self.tax() + ecotax3.name = ecotax3.description = 'ECOTAX3' + ecotax3.type = 'percentage' + ecotax3.rate = Decimal('0.4') + ecotax3.invoice_account = tax_account + ecotax3.credit_note_account = tax_account + ecotax3.update_unit_price = True + ecotax3.sequence = 25 + ecotax3.save() + + self.assertEqual( + self.tax.compute([vat0, ecotax1, vat1, ecotax3, vat2], + Decimal(100), 1), + [{ + 'base': Decimal(100), + 'amount': Decimal(10), + 'tax': vat0, + }, { + 'base': Decimal(100), + 'amount': Decimal(5), + 'tax': ecotax1, + }, { + 'base': Decimal(105), + 'amount': Decimal(21), + 'tax': vat1, + }, { + 'base': Decimal(105), + 'amount': Decimal('42'), + 'tax': ecotax3, + }, { + 'base': Decimal(147), + 'amount': Decimal('44.1'), + 'tax': vat2 + }]) + self.assertEqual( + self.tax.reverse_compute(Decimal('222.1'), + [vat0, ecotax1, vat1, ecotax3, vat2]), + Decimal(100)) + def test0050_receivable_payable(self): 'Test party receivable payable' with Transaction().start(DB_NAME, USER, context=CONTEXT): diff --git a/view/tax_form.xml b/view/tax_form.xml index dc1fb2e40c..24b8463cae 100644 --- a/view/tax_form.xml +++ b/view/tax_form.xml @@ -34,6 +34,9 @@ this repository contains the full copyright notices and license terms. -->