Add tax identifiers to company

This commit is contained in:
Cédric Krier
2025-01-30 19:10:23 +01:00
parent 993c6dbf26
commit 424328610a
10 changed files with 205 additions and 4 deletions
+5 -2
View File
@@ -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):
+1
View File
@@ -1,3 +1,4 @@
* Add tax identifiers to company
Version 7.4.0 - 2024-11-04
--------------------------
+1
View File
@@ -12,6 +12,7 @@ __all__ = ['register', 'CompanyReport']
def register():
Pool.register(
company.Company,
company.CompanyTaxIdentifier,
company.CompanyLogoCache,
company.Employee,
company.CompanyConfigStart,
+81 -2
View File
@@ -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'
+12
View File
@@ -96,6 +96,18 @@ this repository contains the full copyright notices and license terms. -->
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.ui.view" id="company_tax_identifier_view_form">
<field name="model">company.company.tax_identifier</field>
<field name="type">form</field>
<field name="name">company_tax_identifier_form</field>
</record>
<record model="ir.ui.view" id="company_tax_identifier_view_list">
<field name="model">company.company.tax_identifier</field>
<field name="type">tree</field>
<field name="name">company_tax_identifier_list</field>
</record>
<record model="ir.ui.view" id="user_view_form">
<field name="model">res.user</field>
<field name="inherit" ref="res.user_view_form"/>
+63
View File
@@ -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
+3
View File
@@ -12,6 +12,9 @@ this repository contains the full copyright notices and license terms. -->
<page name="employees" col="1">
<field name="employees"/>
</page>
<page name="tax_identifiers">
<field name="tax_identifiers" colspan="4"/>
</page>
<page string="Reports" col="1" id="reports">
<separator name="logo"/>
<field name="logo" widget="image" width="800" height="400"/>
@@ -0,0 +1,19 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form cursor="tax_identifier">
<label name="company"/>
<field name="company"/>
<label name="sequence"/>
<field name="sequence"/>
<label name="tax_identifier"/>
<field name="tax_identifier" colspan="3"/>
<separator id="pattern" colspan="4"/>
<label name="country"/>
<field name="country"/>
<label name="organization"/>
<field name="organization"/>
</form>
@@ -0,0 +1,9 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tree sequence="sequence">
<field name="company" expand="1"/>
<field name="country" expand="1"/>
<field name="organization" expand="1"/>
<field name="tax_identifier" expand="2"/>
</tree>
+11
View File
@@ -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):