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