diff --git a/modules/account_invoice/invoice.py b/modules/account_invoice/invoice.py
index 903e7ea127..ea7b7e6acd 100644
--- a/modules/account_invoice/invoice.py
+++ b/modules/account_invoice/invoice.py
@@ -1383,9 +1383,12 @@ class Invoice(Workflow, ModelSQL, ModelView, TaxableMixin, InvoiceReportMixin):
"Returns the field name of invoice_sequence to use"
return f'{self.type}_{self.sequence_type}_sequence'
- def get_tax_identifier(self):
+ def get_tax_identifier(self, pattern=None):
"Return the default computed tax identifier"
- return self.company.party.tax_identifier
+ pattern = pattern.copy() if pattern is not None else {}
+ if self.invoice_address and self.invoice_address.country:
+ pattern.setdefault('country', self.invoice_address.country.id)
+ return self.company.get_tax_identifier(pattern)
@property
def invoice_report_versioned(self):
diff --git a/modules/company/CHANGELOG b/modules/company/CHANGELOG
index 05f8394d54..6f7fa32225 100644
--- a/modules/company/CHANGELOG
+++ b/modules/company/CHANGELOG
@@ -1,3 +1,4 @@
+* Add tax identifiers to company
Version 7.4.0 - 2024-11-04
--------------------------
diff --git a/modules/company/__init__.py b/modules/company/__init__.py
index ccfd0a39f1..46ce2d0111 100644
--- a/modules/company/__init__.py
+++ b/modules/company/__init__.py
@@ -12,6 +12,7 @@ __all__ = ['register', 'CompanyReport']
def register():
Pool.register(
company.Company,
+ company.CompanyTaxIdentifier,
company.CompanyLogoCache,
company.Employee,
company.CompanyConfigStart,
diff --git a/modules/company/company.py b/modules/company/company.py
index a819411d8d..69eb8c2333 100644
--- a/modules/company/company.py
+++ b/modules/company/company.py
@@ -12,10 +12,11 @@ except ImportError:
PIL = None
from trytond.config import config
-from trytond.model import ModelSQL, ModelView, Unique, fields
+from trytond.model import (
+ MatchMixin, ModelSQL, ModelView, Unique, fields, sequence_ordered)
from trytond.model.exceptions import SQLConstraintError
from trytond.pool import Pool
-from trytond.pyson import Eval, If
+from trytond.pyson import Bool, Eval, If
from trytond.report import Report
from trytond.tools import timezone as tz
from trytond.transaction import Transaction
@@ -60,6 +61,10 @@ class Company(ModelSQL, ModelView):
help="Used to compute the today date.")
employees = fields.One2Many('company.employee', 'company', 'Employees',
help="Add employees to the company.")
+ tax_identifiers = fields.One2Many(
+ 'company.company.tax_identifier', 'company', "Tax Identifiers",
+ help="Uses the first identifier that matches the criteria.\n"
+ "If none match, uses the party's tax identifier.")
@property
def header_used(self):
@@ -195,6 +200,80 @@ class Company(ModelSQL, ModelView):
caches = [c for r in companies for c in r.logo_cache]
Cache.delete(caches)
+ def get_tax_identifier(self, pattern=None):
+ if pattern is None:
+ pattern = {}
+ for record in self.tax_identifiers:
+ if record.match(pattern):
+ return record.tax_identifier
+ return self.party.tax_identifier
+
+
+class CompanyTaxIdentifier(
+ MatchMixin, sequence_ordered(), ModelSQL, ModelView):
+ __name__ = 'company.company.tax_identifier'
+
+ company = fields.Many2One(
+ 'company.company', "Company", required=True, ondelete='CASCADE')
+ company_party = fields.Function(
+ fields.Many2One(
+ 'party.party', "Company Party",
+ context={
+ 'company': Eval('company', None),
+ }),
+ 'on_change_with_company_party')
+
+ country = fields.Many2One(
+ 'country.country', "Country", ondelete='RESTRICT',
+ states={
+ 'invisible': Bool(Eval('organization')),
+ },
+ help="Applies only to addresses in this country.")
+ organization = fields.Many2One(
+ 'country.organization', "Organization",
+ states={
+ 'invisible': Bool(Eval('country')),
+ },
+ help="Applies only to addresses in this organization.")
+
+ tax_identifier = fields.Many2One(
+ 'party.identifier', "Tax Identifier",
+ help="The identifier used for tax report.")
+
+ @classmethod
+ def __setup__(cls):
+ pool = Pool()
+ Party = pool.get('party.party')
+ super().__setup__()
+ cls.__access__.add('company')
+ tax_identifier_types = Party.tax_identifier_types()
+ cls.tax_identifier.domain = [
+ ('party', '=', Eval('company_party', -1)),
+ ('type', 'in', tax_identifier_types),
+ ]
+
+ @classmethod
+ def default_company(cls):
+ return Transaction().context.get('company')
+
+ @fields.depends('company', '_parent_company.party')
+ def on_change_with_company_party(self, name=None):
+ if self.company:
+ return self.company.party
+
+ def match(self, pattern, match_none=False):
+ if 'country' in pattern:
+ pattern = pattern.copy()
+ country = pattern.pop('country')
+ if country is not None or match_none:
+ if self.country and self.country.id != country:
+ return False
+ if (self.organization
+ and country not in [
+ c.id for c in self.organization.countries]):
+ return False
+ return super().match(pattern, match_none=match_none)
+
class CompanyLogoCache(ModelSQL):
__name__ = 'company.company.logo.cache'
diff --git a/modules/company/company.xml b/modules/company/company.xml
index 7bf38e5ea2..aeb9d3f513 100644
--- a/modules/company/company.xml
+++ b/modules/company/company.xml
@@ -96,6 +96,18 @@ this repository contains the full copyright notices and license terms. -->
+
+ company.company.tax_identifier
+ form
+ company_tax_identifier_form
+
+
+
+ company.company.tax_identifier
+ tree
+ company_tax_identifier_list
+
+
res.user
diff --git a/modules/company/tests/test_module.py b/modules/company/tests/test_module.py
index 7beaa523f8..5db7d58fc0 100644
--- a/modules/company/tests/test_module.py
+++ b/modules/company/tests/test_module.py
@@ -432,5 +432,68 @@ class CompanyTestCase(
datetime.now.assert_called_with(tz.ZoneInfo('CET'))
+ @with_transaction()
+ def test_company_tax_identifier(self):
+ "Test company tax identifier"
+ pool = Pool()
+ Country = pool.get('country.country')
+ Organization = pool.get('country.organization')
+ Identifier = pool.get('party.identifier')
+ TaxIdentifier = pool.get('company.company.tax_identifier')
+
+ company = create_company()
+
+ fr_vat = Identifier(
+ party=company.party, type='fr_vat', code="FR23334175221")
+ fr_vat.save()
+ be_vat = Identifier(
+ party=company.party, type='be_vat', code="BE403019261")
+ be_vat.save()
+
+ belgium = Country(name="Belgium")
+ belgium.save()
+ france = Country(name="France")
+ france.save()
+ germany = Country(name="Germany")
+ germany.save()
+ switzerland = Country(name="Switzerland")
+ switzerland.save()
+ europe = Organization(
+ name="Europe",
+ members=[
+ {'country': belgium},
+ {'country': france},
+ {'country': germany},
+ ])
+ europe.save()
+
+ TaxIdentifier.create([{
+ 'company': company,
+ 'country': france,
+ 'tax_identifier': fr_vat,
+ }, {
+ 'company': company,
+ 'country': belgium,
+ 'tax_identifier': be_vat,
+ }, {
+ 'company': company,
+ 'organization': europe,
+ 'tax_identifier': be_vat,
+ }])
+
+ self.assertEqual(company.party.tax_identifier, fr_vat)
+ self.assertEqual(company.get_tax_identifier({
+ 'country': france.id,
+ }), fr_vat)
+ self.assertEqual(company.get_tax_identifier({
+ 'country': belgium.id,
+ }), be_vat)
+ self.assertEqual(company.get_tax_identifier({
+ 'country': germany.id,
+ }), be_vat)
+ self.assertEqual(company.get_tax_identifier({
+ 'country': switzerland,
+ }), fr_vat)
+
del ModuleTestCase
diff --git a/modules/company/view/company_form.xml b/modules/company/view/company_form.xml
index 3cf985f3f6..8791870021 100644
--- a/modules/company/view/company_form.xml
+++ b/modules/company/view/company_form.xml
@@ -12,6 +12,9 @@ this repository contains the full copyright notices and license terms. -->
+
+
+
diff --git a/modules/company/view/company_tax_identifier_form.xml b/modules/company/view/company_tax_identifier_form.xml
new file mode 100644
index 0000000000..7c98ac0e7d
--- /dev/null
+++ b/modules/company/view/company_tax_identifier_form.xml
@@ -0,0 +1,19 @@
+
+
+
diff --git a/modules/company/view/company_tax_identifier_list.xml b/modules/company/view/company_tax_identifier_list.xml
new file mode 100644
index 0000000000..80750dca3a
--- /dev/null
+++ b/modules/company/view/company_tax_identifier_list.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/modules/sale/invoice.py b/modules/sale/invoice.py
index 211a67e03b..abc3cc5a1e 100644
--- a/modules/sale/invoice.py
+++ b/modules/sale/invoice.py
@@ -64,6 +64,17 @@ class Invoice(metaclass=PoolMeta):
return [('lines.origin.sale' + clause[0][len(name):],
*clause[1:3], 'sale.line', *clause[3:])]
+ def get_tax_identifier(self, pattern=None):
+ if self.sales:
+ pattern = pattern.copy() if pattern is not None else {}
+ shipment_countries = {
+ s.shipment_address.country for s in self.sales
+ if s.shipment_address and s.shipment_address.country}
+ if len(shipment_countries) == 1:
+ shipment_country, = shipment_countries
+ pattern.setdefault('country', shipment_country.id)
+ return super().get_tax_identifier(pattern=pattern)
+
@classmethod
@process_sale
def delete(cls, invoices):