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. -->
+
+