diff --git a/CHANGELOG b/CHANGELOG index fd8840208b..4eaa8d15e0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Add subdivision as criteria + Version 5.2.0 - 2019-05-06 * Bug fixes (see mercurial logs for details) diff --git a/__init__.py b/__init__.py index ba2393797c..b840bc158e 100644 --- a/__init__.py +++ b/__init__.py @@ -10,6 +10,7 @@ from . import stock def register(): Pool.register( + account.TaxRule, account.TaxRuleLineTemplate, account.TaxRuleLine, module='account_tax_rule_country', type_='model') diff --git a/account.py b/account.py index fa27a39ec6..a101bce6e6 100644 --- a/account.py +++ b/account.py @@ -3,16 +3,62 @@ from trytond.pool import Pool, PoolMeta from trytond.model import fields - -__all__ = ['TaxRuleLineTemplate', 'TaxRuleLine', 'InvoiceLine'] +from trytond.pyson import Eval -class TaxRuleLineTemplate(metaclass=PoolMeta): +class TaxRule(metaclass=PoolMeta): + __name__ = 'account.tax.rule' + + def apply(self, tax, pattern): + pool = Pool() + Subdivision = pool.get('country.subdivision') + + def parents(subdivision): + while subdivision: + yield subdivision.id + subdivision = subdivision.parent + + pattern = pattern.copy() + for name in ['from_subdivision', 'to_subdivision']: + subdivision = pattern.pop(name, None) + if not subdivision: + continue + subdivision = Subdivision(subdivision) + pattern[name] = list(parents(subdivision)) + return super().apply(tax, pattern) + + +class _TaxRuleLineMixin: + from_country = fields.Many2One( + 'country.country', 'From Country', ondelete='RESTRICT', + help="Apply only to addresses of this country.") + from_subdivision = fields.Many2One( + 'country.subdivision', "From Subdivision", ondelete='RESTRICT', + domain=[ + ('country', '=', Eval('from_country', -1)), + ], + states={ + 'invisible': ~Eval('from_country'), + }, + depends=['from_country'], + help="Apply only to addresses in this subdivision.") + to_country = fields.Many2One( + 'country.country', 'To Country', ondelete='RESTRICT', + help="Apply only to addresses of this country.") + to_subdivision = fields.Many2One( + 'country.subdivision', "To Subdivision", ondelete='RESTRICT', + domain=[ + ('country', '=', Eval('to_country', -1)), + ], + states={ + 'invisible': ~Eval('to_country'), + }, + depends=['to_country'], + help="Apply only to addresses in this subdivision.") + + +class TaxRuleLineTemplate(_TaxRuleLineMixin, metaclass=PoolMeta): __name__ = 'account.tax.rule.line.template' - from_country = fields.Many2One('country.country', 'From Country', - ondelete='RESTRICT') - to_country = fields.Many2One('country.country', 'To Country', - ondelete='RESTRICT') def _get_tax_rule_line_value(self, rule_line=None): value = super(TaxRuleLineTemplate, self)._get_tax_rule_line_value( @@ -20,18 +66,37 @@ class TaxRuleLineTemplate(metaclass=PoolMeta): if not rule_line or rule_line.from_country != self.from_country: value['from_country'] = ( self.from_country.id if self.from_country else None) + if (not rule_line + or rule_line.from_subdivision != self.from_subdivision): + value['from_subdivision'] = ( + self.from_subdivision.id if self.from_subdivision else None) if not rule_line or rule_line.to_country != self.to_country: value['to_country'] = ( self.to_country.id if self.to_country else None) + if (not rule_line + or rule_line.to_subdivision != self.to_subdivision): + value['to_subdivision'] = ( + self.to_subdivision.id if self.to_subdivision else None) return value -class TaxRuleLine(metaclass=PoolMeta): +class TaxRuleLine(_TaxRuleLineMixin, metaclass=PoolMeta): __name__ = 'account.tax.rule.line' - from_country = fields.Many2One('country.country', 'From Country', - ondelete='RESTRICT') - to_country = fields.Many2One('country.country', 'To Country', - ondelete='RESTRICT') + + def match(self, pattern): + for name in ['from_subdivision', 'to_subdivision']: + subdivision = getattr(self, name) + if name not in pattern: + if subdivision: + return False + else: + continue + pattern = pattern.copy() + subdivisions = pattern.pop(name) + if (subdivision is not None + and subdivision.id not in subdivisions): + return False + return super().match(pattern) class InvoiceLine(metaclass=PoolMeta): @@ -45,16 +110,28 @@ class InvoiceLine(metaclass=PoolMeta): pattern = super(InvoiceLine, self)._get_tax_rule_pattern() - from_country, to_country = None, None + from_country = from_subdivision = to_country = to_subdivision = None if isinstance(self.origin, SaleLine): - if self.origin.warehouse.address: - from_country = self.origin.warehouse.address.country - to_country = self.origin.sale.shipment_address.country + warehouse_address = self.origin.warehouse.address + if warehouse_address: + from_country = warehouse_address.country + from_subdivision = warehouse_address.subdivision + shipment_address = self.origin.sale.shipment_address + to_country = shipment_address.country + to_subdivision = shipment_address.subdivision elif isinstance(self.origin, PurchaseLine): - from_country = self.origin.purchase.invoice_address.country - if self.origin.purchase.warehouse.address: - to_country = self.origin.purchase.warehouse.address.country + invoice_address = self.origin.purchase.invoice_address + from_country = invoice_address.country + from_subdivision = invoice_address.subdivision + warehouse_address = self.origin.purchase.warehouse.address + if warehouse_address: + to_country = warehouse_address.country + to_subdivision = warehouse_address.subdivision pattern['from_country'] = from_country.id if from_country else None + pattern['from_subdivision'] = ( + from_subdivision.id if from_subdivision else None) pattern['to_country'] = to_country.id if to_country else None + pattern['to_subdivision'] = ( + to_subdivision.id if to_subdivision else None) return pattern diff --git a/doc/index.rst b/doc/index.rst index 1c9748054e..ba42051fa1 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -2,31 +2,33 @@ Account Tax Rule Country ######################## The account_tax_rule module extends the tax rule to add origin and destination -countries as criteria. +countries and subdivisions as criteria. Tax Rule Line ************* -Two criteria fields are added: +Four criteria fields are added: - From Country: The country of origin +- From Subdivision: The subdivision of origin - To Country: The country of destination +- To Subdivision: The subdivision of destination The countries are picked from the origin document: - Sale: - - The origin country comes from the address of the warehouse. - - The destination country comes from the shipping address. + - The origin country and subdivision come from the address of the warehouse. + - The destination country and subdivision come from the shipping address. - Purchase: - - The origin country comes from the invoice address. - - The destination country comes from the address of the warehouse. + - The origin country and subdivision come from the invoice address. + - The destination country and subdivision come from the address of the warehouse. - Stock Consignment: - - The origin country comes from the warehouse's address of the location or - the delivery address for returned customer shipment. - - The destination country comes from the warehouse's address of the location - or the delivery address for customer shipment. + - The origin country and subdivision come from the warehouse's address of the + location or the delivery address for returned customer shipment. + - The destination country and subdivision come from the warehouse's address + of the location or the delivery address for customer shipment. diff --git a/purchase.py b/purchase.py index 05a2a102d0..6e593d9301 100644 --- a/purchase.py +++ b/purchase.py @@ -28,14 +28,20 @@ class PurchaseLine(metaclass=PoolMeta): def _get_tax_rule_pattern(self): pattern = super(PurchaseLine, self)._get_tax_rule_pattern() - from_country, to_country = None, None + from_country = from_subdivision = to_country = to_subdivision = None if self.purchase: if self.purchase.invoice_address: from_country = self.purchase.invoice_address.country + from_subdivision = self.purchase.invoice_address.subdivision warehouse = self.purchase.warehouse if warehouse and warehouse.address: to_country = warehouse.address.country + to_subdivision = warehouse.address.subdivision pattern['from_country'] = from_country.id if from_country else None + pattern['from_subdivision'] = ( + from_subdivision.id if from_subdivision else None) pattern['to_country'] = to_country.id if to_country else None + pattern['to_subdivision'] = ( + to_subdivision.id if to_subdivision else None) return pattern diff --git a/sale.py b/sale.py index 8ad5c0d290..f9e1c39d75 100644 --- a/sale.py +++ b/sale.py @@ -31,7 +31,7 @@ class SaleLine(metaclass=PoolMeta): pattern = super(SaleLine, self)._get_tax_rule_pattern() - from_country, to_country = None, None + from_country = from_subdivision = to_country = to_subdivision = None if self.id is None or self.id < 0: warehouse = self.get_warehouse('warehouse') if warehouse: @@ -40,9 +40,15 @@ class SaleLine(metaclass=PoolMeta): warehouse = self.warehouse if warehouse and warehouse.address: from_country = warehouse.address.country + from_subdivision = warehouse.address.subdivision if self.sale and self.sale.shipment_address: to_country = self.sale.shipment_address.country + to_subdivision = self.sale.shipment_address.subdivision pattern['from_country'] = from_country.id if from_country else None + pattern['from_subdivision'] = ( + from_subdivision.id if from_subdivision else None) pattern['to_country'] = to_country.id if to_country else None + pattern['to_subdivision'] = ( + to_subdivision.id if to_subdivision else None) return pattern diff --git a/stock.py b/stock.py index 6cb7f29363..302cd70484 100644 --- a/stock.py +++ b/stock.py @@ -14,18 +14,30 @@ class Move(metaclass=PoolMeta): pattern = super(Move, self)._get_tax_rule_pattern() - from_country, to_country = None, None + from_country = from_subdivision = to_country = to_subdivision = None if self.from_location.warehouse: - if self.from_location.warehouse.address: - from_country = self.from_location.warehouse.address.country + warehouse_address = self.from_location.warehouse.address + if warehouse_address: + from_country = warehouse_address.country + from_subdivision = warehouse_address.subdivision elif isinstance(self.origin, ShipmentOutReturn): - from_country = self.origin.delivery_address.country + delivery_address = self.origin.delivery_address + from_country = delivery_address.country + from_subdivision = delivery_address.subdivision if self.to_location.warehouse: - if self.to_location.warehouse.address: - to_country = self.to_location.warehouse.address.country + warehouse_address = self.to_location.warehouse.address + if warehouse_address: + to_country = warehouse_address.country + to_subdivision = warehouse_address.subdivision elif isinstance(self.origin, ShipmentOut): - to_country = self.origin.delivery_address.country + delivery_address = self.origin.delivery_address + to_country = delivery_address.country + to_subdivision = delivery_address.subdivision pattern['from_country'] = from_country.id if from_country else None + pattern['from_subdivision'] = ( + from_subdivision.id if from_subdivision else None) pattern['to_country'] = to_country.id if to_country else None + pattern['to_subdivision'] = ( + to_subdivision.id if to_subdivision else None) return pattern diff --git a/tests/test_account_tax_rule_country.py b/tests/test_account_tax_rule_country.py index 449b8dcc23..ab49d767a4 100644 --- a/tests/test_account_tax_rule_country.py +++ b/tests/test_account_tax_rule_country.py @@ -32,6 +32,142 @@ class AccountTaxRuleCountryTestCase(ModuleTestCase): update_chart.start.account = root update_chart.transition_update() + @classmethod + def _create_countries(cls): + pool = Pool() + Country = pool.get('country.country') + Subdivision = pool.get('country.subdivision') + + country1 = Country(name="Country 1") + country1.save() + subdivision1 = Subdivision( + country=country1, name="Subdivision 1", code="SUB1", + type='province') + subdivision1.save() + subdivision11 = Subdivision( + country=country1, parent=subdivision1, + name="Sub-Subdivision 1", code="SUBSUB1", type='province') + subdivision11.save() + country2 = Country(name="Country 2") + country2.save() + subdivision2 = Subdivision( + country=country2, name="Subdivision 2", code="SUB2", + type='province') + subdivision2.save() + + return [country1, country2] + + def _get_taxes(cls): + pool = Pool() + Tax = pool.get('account.tax') + tax, = Tax.search([]) + target_tax, = Tax.copy([tax]) + return [tax, target_tax] + + def _create_rule(cls, lines): + pool = Pool() + TaxRule = pool.get('account.tax.rule') + return TaxRule.create([{ + 'name': 'Test', + 'kind': 'both', + 'lines': [('create', lines)], + }])[0] + + @with_transaction() + def test_tax_rule(self): + "Test tax rule" + country1, country2 = self._create_countries()[:2] + subdivision1 = country1.subdivisions[0] + subdivision2 = country2.subdivisions[0] + company = create_company() + with set_company(company): + create_chart(company, tax=True) + tax, target_tax = self._get_taxes()[:2] + tax_rule = self._create_rule([{ + 'from_country': country1.id, + 'from_subdivision': subdivision1.id, + 'to_country': country2.id, + 'to_subdivision': subdivision2.id, + 'origin_tax': tax.id, + 'tax': target_tax.id, + }]) + pattern = { + 'from_country': country1.id, + 'from_subdivision': subdivision1.id, + 'to_country': country2.id, + 'to_subdivision': subdivision2.id, + } + + self.assertListEqual(tax_rule.apply(tax, pattern), [target_tax.id]) + + @with_transaction() + def test_tax_rule_children(self): + "Test tax rule with children subdivision" + country = self._create_countries()[0] + parent_subdivision = [ + s for s in country.subdivisions if not s.parent][0] + subdivision = [ + s for s in country.subdivisions + if s.parent == parent_subdivision][0] + company = create_company() + with set_company(company): + create_chart(company, tax=True) + tax, target_tax = self._get_taxes()[:2] + tax_rule = self._create_rule([{ + 'to_country': country.id, + 'to_subdivision': parent_subdivision.id, + 'origin_tax': tax.id, + 'tax': target_tax.id, + }]) + pattern = { + 'to_country': country.id, + 'to_subdivision': subdivision.id, + } + + self.assertListEqual(tax_rule.apply(tax, pattern), [target_tax.id]) + + @with_transaction() + def test_tax_rule_no_subdivision(self): + "Test tax rule without subdivision" + country = self._create_countries()[0] + subdivision = country.subdivisions[0] + company = create_company() + with set_company(company): + create_chart(company, tax=True) + tax, target_tax = self._get_taxes()[:2] + tax_rule = self._create_rule([{ + 'to_country': country.id, + 'origin_tax': tax.id, + 'tax': target_tax.id, + }]) + pattern = { + 'to_country': country.id, + 'to_subdivision': subdivision.id, + } + + self.assertListEqual(tax_rule.apply(tax, pattern), [target_tax.id]) + + @with_transaction() + def test_tax_rule_no_subdivision_pattern(self): + "Test tax rule without subdivision in pattern" + country = self._create_countries()[0] + subdivision = country.subdivisions[0] + company = create_company() + with set_company(company): + create_chart(company, tax=True) + tax, target_tax = self._get_taxes()[:2] + tax_rule = self._create_rule([{ + 'to_country': country.id, + 'to_subdivision': subdivision.id, + 'origin_tax': tax.id, + 'tax': target_tax.id, + }]) + pattern = { + 'to_country': country.id, + } + + self.assertListEqual(tax_rule.apply(tax, pattern), [tax.id]) + def suite(): suite = trytond.tests.test_tryton.suite() diff --git a/view/tax_rule_line_form.xml b/view/tax_rule_line_form.xml index ccdac9f480..9f6007a727 100644 --- a/view/tax_rule_line_form.xml +++ b/view/tax_rule_line_form.xml @@ -5,7 +5,11 @@ this repository contains the full copyright notices and license terms. --> diff --git a/view/tax_rule_line_template_form.xml b/view/tax_rule_line_template_form.xml index ccdac9f480..9f6007a727 100644 --- a/view/tax_rule_line_template_form.xml +++ b/view/tax_rule_line_template_form.xml @@ -5,7 +5,11 @@ this repository contains the full copyright notices and license terms. --> diff --git a/view/tax_rule_line_template_tree.xml b/view/tax_rule_line_template_tree.xml index 75e995a5a4..4f2ecf41ed 100644 --- a/view/tax_rule_line_template_tree.xml +++ b/view/tax_rule_line_template_tree.xml @@ -4,6 +4,8 @@ this repository contains the full copyright notices and license terms. --> + + diff --git a/view/tax_rule_line_tree.xml b/view/tax_rule_line_tree.xml index 75e995a5a4..4f2ecf41ed 100644 --- a/view/tax_rule_line_tree.xml +++ b/view/tax_rule_line_tree.xml @@ -4,6 +4,8 @@ this repository contains the full copyright notices and license terms. --> + + diff --git a/view/tax_rule_line_tree_sequence.xml b/view/tax_rule_line_tree_sequence.xml index 75e995a5a4..4f2ecf41ed 100644 --- a/view/tax_rule_line_tree_sequence.xml +++ b/view/tax_rule_line_tree_sequence.xml @@ -4,6 +4,8 @@ this repository contains the full copyright notices and license terms. --> + +