Add unit_price modifying taxes

issue4115
review12451002
This commit is contained in:
Nicolas Évrard committed 2015-01-23 10:41:05 +01:00
1 parent a768d62f7d
commit 24bb0b1d08
6 files changed
+304 -27

No files matched your search

+1
View File
@@ -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
+2
View File
@@ -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
+98 -26
View File
@@ -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,
+197 -1
View File
@@ -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):
+3
View File
@@ -34,6 +34,9 @@ this repository contains the full copyright notices and license terms. -->
<label name="company"/>
<field name="company"/>
<newline/>
<label name="update_unit_price"/>
<field name="update_unit_price"/>
<newline/>
<label name="invoice_account"/>
<field name="invoice_account"/>
<newline/>
+3
View File
@@ -32,6 +32,9 @@ this repository contains the full copyright notices and license terms. -->
</group>
</group>
<newline/>
<label name="update_unit_price"/>
<field name="update_unit_price"/>
<newline/>
<label name="invoice_account"/>
<field name="invoice_account"/>
<newline/>