Format the party's identifier codes

Closes #14485
This commit is contained in:
Cédric Krier
2026-01-16 16:56:47 +01:00
parent 86d2fdfe50
commit b0ff152120
17 changed files with 115 additions and 36 deletions
+2 -2
View File
@@ -81,10 +81,10 @@ class BEVATCustomer(ModelSQL, ModelView):
where = ((invoice.company == context.get('company'))
& (period.fiscalyear == context.get('fiscalyear')))
where &= invoice.type == 'out'
where &= ((company_identifier.code.ilike('BE%')
where &= ((company_identifier.code_compact.ilike('BE%')
& (company_identifier.type == 'eu_vat'))
| (company_identifier.type == 'be_vat'))
where &= ((party_identifier.code.ilike('BE%')
where &= ((party_identifier.code_compact.ilike('BE%')
& (party_identifier.type == 'eu_vat'))
| (party_identifier.type == 'be_vat'))
where &= tax.group.in_(groups)
+2 -2
View File
@@ -309,11 +309,11 @@ class MoveExport(metaclass=PoolMeta):
if line.account.type.receivable and line.party:
doctype = DOCTYPE.CUSTOMER
if identifier := line.party.winbooks_customer_identifier:
accountrp = identifier.code
accountrp = identifier.code_compact
elif line.account.type.payable and line.party:
doctype = DOCTYPE.SUPPLIER
if identifier := line.party.winbooks_supplier_identifier:
accountrp = identifier.code
accountrp = identifier.code_compact
else:
doctype = DOCTYPE.GENERAL
return {
+10 -2
View File
@@ -54,11 +54,19 @@ class Identifier(metaclass=PoolMeta):
& (t.active == Literal(True))),
'account_export_winbooks.'
'msg_party_identifier_winbooks_party_unique'),
('winbooks_code_unique',
Exclude(t, (t.code, Equal),
('winbooks_code_compact_unique',
Exclude(t, (t.code_compact, Equal),
where=t.type.in_(
['winbooks_supplier', 'winbooks_customer'])
& (t.active == Literal(True))),
'account_export_winbooks.'
'msg_party_identifier_winbooks_code_unique'),
]
@classmethod
def __register__(cls, module):
table_h = cls.__table_handler__(module)
super().__register__(module)
# Migration from 7.8: replace winbooks_code_unique
table_h.drop_constraint('winbooks_code_unique')
@@ -189,7 +189,7 @@ Post invoice::
>>> assertEqual(invoice.posted_by, employee)
>>> invoice.state
'posted'
>>> invoice.tax_identifier.code
>>> invoice.tax_identifier.code_compact
'BE0897290877'
>>> assertTrue(iso11649.validate(invoice.customer_payment_reference))
>>> bool(invoice.has_report_cache)
+1 -1
View File
@@ -28,7 +28,7 @@ class Party(metaclass=PoolMeta):
def get_sepa_creditor_identifier_used(self, name):
for identifier in self.identifiers:
if identifier.type == 'eu_at_02':
return identifier.code
return identifier.code_compact
def get_sepa_identifier(self, name):
pool = Pool()
+1 -1
View File
@@ -319,7 +319,7 @@ class Move(metaclass=PoolMeta):
if not fallback:
fallback = identifier
if (self.intrastat_country
and identifier.code.startswith(
and identifier.code_compact.startswith(
self.intrastat_country.code)):
break
else:
+11 -3
View File
@@ -91,7 +91,9 @@ class IncomingSupplierInvoice(metaclass=PoolMeta):
tax_identifier_types = Party.tax_identifier_types()
for identifier in invoice.party.identifiers:
if (identifier.type in tax_identifier_types
and identifier.code == tax_identifier):
and (
identifier.code == tax_identifier
or identifier.code_compact == tax_identifier)):
invoice.party_tax_identifier = identifier
currency = invoice_data.get('currency')
@@ -302,7 +304,10 @@ class IncomingSupplierInvoice(metaclass=PoolMeta):
tax_identifier = invoice_data.get('company_tax_identifier')
if tax_identifier:
identifiers = Identifier.search([
('code', '=', tax_identifier),
['OR',
('code', '=', tax_identifier),
('code_compact', '=', tax_identifier),
],
('type', 'in', Party.tax_identifier_types()),
])
if len(identifiers) == 1:
@@ -342,7 +347,10 @@ class IncomingSupplierInvoice(metaclass=PoolMeta):
tax_identifier = invoice_data.get('tax_identifier')
if tax_identifier:
identifiers = Identifier.search([
('code', '=', tax_identifier),
['OR',
('code', '=', tax_identifier),
('code_compact', '=', tax_identifier),
],
('type', 'in', Party.tax_identifier_types()),
])
if len(identifiers) == 1:
+1 -1
View File
@@ -24,7 +24,7 @@ class Invoice(metaclass=PoolMeta):
return (identifier
and (identifier.type == 'be_vat'
or (identifier.type == 'eu_vat'
and identifier.code.startswith('BE'))))
and identifier.code_compact.startswith('BE'))))
if (self.invoice_date
and self.invoice_date.year >= 2026
and is_be_vat(self.tax_identifier)
+3 -3
View File
@@ -881,7 +881,7 @@ class Invoice(Model):
):
if identifier.text:
domain = [
('code', '=', identifier.text),
('code_compact', '=', identifier.text),
]
if schemeId := identifier.get('schemeID'):
if type := ISO6523.get(schemeId):
@@ -999,7 +999,7 @@ class Invoice(Model):
for identifier in party.identifiers:
if (identifier.type in tax_identifier_types
and identifier.iso_6523 == scheme_id
and identifier.code == value):
and identifier.code_compact == value):
return identifier
else:
if create and scheme_id in ISO6523:
@@ -1042,7 +1042,7 @@ class Invoice(Model):
):
if identifier.text:
domain = [
('code', '=', identifier.text),
('code_compact', '=', identifier.text),
]
if schemeId := identifier.get('schemeID'):
if type := ISO6523.get(schemeId):
+3 -3
View File
@@ -13,7 +13,7 @@ this repository contains the full copyright notices and license terms. -->
</py:with>
<py:for each="identifier in identifications or party.identifiers">
<cac:PartyIdentification py:if="identifier and identifier.iso_6523">
<cbc:ID py:attrs="{'schemeID': identifier.iso_6523}">${identifier.code}</cbc:ID>
<cbc:ID py:attrs="{'schemeID': identifier.iso_6523}">${identifier.code_compact}</cbc:ID>
</cac:PartyIdentification>
</py:for>
<cac:PostalAddress py:if="address">${Address(address, specification=specification)}</cac:PostalAddress>
@@ -23,7 +23,7 @@ this repository contains the full copyright notices and license terms. -->
<cbc:CompanyID>${tax_identifier.vatin}</cbc:CompanyID>
</py:when>
<py:otherwise>
<cbc:CompanyID py:attrs="{'schemeID': tax_identifier.iso_6523}">${tax_identifier.code}</cbc:CompanyID>
<cbc:CompanyID py:attrs="{'schemeID': tax_identifier.iso_6523}">${tax_identifier.code_compact}</cbc:CompanyID>
</py:otherwise>
</py:choose>
<cac:TaxScheme>
@@ -34,7 +34,7 @@ this repository contains the full copyright notices and license terms. -->
<cbc:RegistrationName>${party.name}</cbc:RegistrationName>
<py:with vars="identifier = party.identifier_iso6523">
<py:if test="identifier">
<cbc:CompanyID py:attrs="{'schemeID': identifier.iso_6523}">${identifier.code}</cbc:CompanyID>
<cbc:CompanyID py:attrs="{'schemeID': identifier.iso_6523}">${identifier.code_compact}</cbc:CompanyID>
</py:if>
</py:with>
</cac:PartyLegalEntity>
@@ -20,7 +20,7 @@ this repository contains the full copyright notices and license terms. -->
</ram:SpecifiedLegalOrganization>
<ram:PostalTradeAddress py:if="address">${TradeAddress(address)}</ram:PostalTradeAddress>
<ram:SpecifiedTaxRegistration py:if="tax_identifier and tax_identifier.type == 'eu_vat'">
<ram:ID schemeID='VA'>${tax_identifier.code}</ram:ID>
<ram:ID schemeID='VA'>${tax_identifier.code_compact}</ram:ID>
</ram:SpecifiedTaxRegistration>
</py:def>
<py:def function="TradeAddress(address)">
+1
View File
@@ -1,3 +1,4 @@
* Format identifier codes
* Add Senegal Tax Number identifier
* Add Azerbaijan Tax Number identifier
* Add French Trade Registration Number identifier
+69 -7
View File
@@ -343,6 +343,7 @@ class Party(
return [bool_op,
('code', operator, code_value, *extra),
('identifiers.code', operator, code_value, *extra),
('identifiers.code_compact', operator, code_value, *extra),
('name', operator, operand, *extra),
('contact_mechanisms.rec_name', operator, operand, *extra),
]
@@ -843,9 +844,12 @@ class Identifier(sequence_ordered(), DeactivableMixin, ModelSQL, ModelView):
type_address = fields.Function(
fields.Boolean("Type of Address"), 'on_change_with_type_address')
code = fields.Char('Code', required=True)
code_compact = fields.Char("Code Compact", readonly=True, required=True)
@classmethod
def __setup__(cls):
cls.code.search_unaccented = False
cls.code_compact.search_unaccented = False
super().__setup__()
cls.__access__.add('party')
@@ -853,6 +857,11 @@ class Identifier(sequence_ordered(), DeactivableMixin, ModelSQL, ModelView):
def __register__(cls, module_name):
cursor = Transaction().connection.cursor()
table = cls.__table__()
table_h = cls.__table_handler__(module_name)
fill_code_compact = (
table_h.column_exist('code')
and not table_h.column_exist('code_compact'))
super().__register__(module_name)
@@ -871,6 +880,10 @@ class Identifier(sequence_ordered(), DeactivableMixin, ModelSQL, ModelView):
cursor.execute(*table.update([table.type], ['co_nit'],
where=(table.type == 'co_rut')))
# Migration from 7.8: Fill code_compact
if fill_code_compact:
cursor.execute(*table.update([table.code_compact], [table.code]))
@classmethod
def get_types(cls):
pool = Pool()
@@ -893,14 +906,20 @@ class Identifier(sequence_ordered(), DeactivableMixin, ModelSQL, ModelView):
@fields.depends('type', 'code')
def on_change_with_code(self):
code = self.code
if self.type and '_' in self.type:
module = get_cc_module(*self.type.split('_', 1))
if module:
try:
return module.compact(self.code)
except stdnum.exceptions.ValidationError:
pass
return self.code
if module := get_cc_module(*self.type.split('_', 1)):
if hasattr(module, 'compact'):
try:
code = module.compact(code)
except stdnum.exceptions.ValidationError:
pass
if hasattr(module, 'format'):
try:
code = module.format(code)
except stdnum.exceptions.ValidationError:
pass
return code
def pre_validate(self):
super().pre_validate()
@@ -922,6 +941,49 @@ class Identifier(sequence_ordered(), DeactivableMixin, ModelSQL, ModelView):
code=self.code,
party=party))
@classmethod
def preprocess_values(cls, mode, values):
values = super().preprocess_values(mode, values)
if mode == 'create':
values['code_compact'] = values.get('code')
if ((type := values.get('type')) and '_' in type
and (code := values.get('code'))):
if module := get_cc_module(*type.split('_', 1)):
if hasattr(module, 'format'):
try:
values['code'] = module.format(code)
except stdnum.exceptions.ValidationError:
pass
if hasattr(module, 'compact'):
try:
values['code_compact'] = module.compact(code)
except stdnum.exceptions.ValidationError:
pass
return values
def compute_fields(self, field_names=None):
values = super().compute_fields(field_names=field_names)
if field_names is None or {'type', 'code'} & field_names:
code = getattr(self, 'code', None)
code_compact = getattr(self, 'code_compact', None)
if (type := getattr(self, 'type', None)) and '_' in type and code:
if module := get_cc_module(*type.split('_', 1)):
if hasattr(module, 'format'):
code = module.format(code)
if hasattr(module, 'compact'):
code_compact = module.compact(code)
else:
code_compact = code
if getattr(self, 'code', None) != code:
values['code'] = code
if getattr(self, 'code_compact', None) != code_compact:
values['code_compact'] = code_compact
elif code_compact != code:
values['code_compact'] = code
elif code_compact != code:
values['code_compact'] = code
return values
@fields.depends(methods=['_notify_duplicate'])
def on_change_notify(self):
notifications = super().on_change_notify()
+2 -2
View File
@@ -385,7 +385,7 @@ class CreateDPDShipping_Customs(metaclass=PoolMeta):
if customs_agent := shipment.customs_agent:
international.update({
'commercialInvoiceConsigneeVatNumber': (
customs_agent.tax_identifier.code)[:20],
customs_agent.tax_identifier.code_compact)[:20],
'commercialInvoiceConsignee': self.shipping_party(
customs_agent.party,
customs_agent.address,
@@ -393,7 +393,7 @@ class CreateDPDShipping_Customs(metaclass=PoolMeta):
})
if shipment.tax_identifier:
international['commercialInvoiceConsignorVatNumber'] = (
shipment.tax_identifier.code[:17])
shipment.tax_identifier.code_compact[:17])
international['commercialInvoiceConsignor'] = self.shipping_party(
shipment.company.party, shipment.customs_from_address)
international['additionalInvoiceLines'] = [
@@ -281,32 +281,32 @@ class CreateShippingSendcloud_Customs(metaclass=PoolMeta):
yield {
'name': 'CNP',
'country_code': 'BR',
'value': tax_identifier.code[:100],
'value': tax_identifier.code_compact[:100],
}
elif tax_identifier.type == 'ru_vat':
yield {
'name': 'INN',
'country_code': 'RU',
'value': tax_identifier.code[:100],
'value': tax_identifier.code_compact[:100],
}
elif tax_identifier.type == 'eu_vat':
yield {
'name': 'VAT',
'country_code': tax_identifier.code[:2],
'value': tax_identifier.code[2:][:100],
'value': tax_identifier.code_compact[2:][:100],
}
elif tax_identifier.type.endswith('_vat'):
yield {
'name': 'VAT',
'country_code': tax_identifier.type[:2].upper(),
'value': tax_identifier.code[:100],
'value': tax_identifier.code_compact[:100],
}
elif tax_identifier.type in {'us_ein', 'us_ssn'}:
country, name = tax_identifier.type.upper().split('_')
yield {
'name': name,
'country_code': country,
'value': tax_identifier.code[:100],
'value': tax_identifier.code_compact[:100],
}
def get_parcel_item(
+1 -1
View File
@@ -53,7 +53,7 @@ class Address(metaclass=PoolMeta):
if for_party:
address['company'] = self.party.name
address['vat_id'] = (
self.party.tax_identifier.code
self.party.tax_identifier.code_compact
if self.party.tax_identifier else None)
return address
+1 -1
View File
@@ -647,7 +647,7 @@ class User(metaclass=PoolMeta):
if address_data.get('company'):
for company_party in self.secondary_parties:
tax_code = (
company_party.tax_identifier.code
company_party.tax_identifier.code_compact
if company_party.tax_identifier else '')
if (company_party.name == address_data['company']
and (not address_data.get('vat_id')