diff --git a/modules/account_be/account.py b/modules/account_be/account.py index 81ee9e1eef..1472ea1b08 100644 --- a/modules/account_be/account.py +++ b/modules/account_be/account.py @@ -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) diff --git a/modules/account_export_winbooks/account.py b/modules/account_export_winbooks/account.py index ac8692b27a..fc396a5fbb 100644 --- a/modules/account_export_winbooks/account.py +++ b/modules/account_export_winbooks/account.py @@ -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 { diff --git a/modules/account_export_winbooks/party.py b/modules/account_export_winbooks/party.py index b03ca393bf..416dd4919e 100644 --- a/modules/account_export_winbooks/party.py +++ b/modules/account_export_winbooks/party.py @@ -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') diff --git a/modules/account_invoice/tests/scenario_invoice.rst b/modules/account_invoice/tests/scenario_invoice.rst index 6dffa295f2..350264d468 100644 --- a/modules/account_invoice/tests/scenario_invoice.rst +++ b/modules/account_invoice/tests/scenario_invoice.rst @@ -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) diff --git a/modules/account_payment_sepa/party.py b/modules/account_payment_sepa/party.py index 9e7b7f5752..72e2f81620 100644 --- a/modules/account_payment_sepa/party.py +++ b/modules/account_payment_sepa/party.py @@ -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() diff --git a/modules/account_stock_eu/stock.py b/modules/account_stock_eu/stock.py index 47f305285e..a76449c1f3 100644 --- a/modules/account_stock_eu/stock.py +++ b/modules/account_stock_eu/stock.py @@ -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: diff --git a/modules/document_incoming_ocr/document.py b/modules/document_incoming_ocr/document.py index 98f067f581..4c273a4138 100644 --- a/modules/document_incoming_ocr/document.py +++ b/modules/document_incoming_ocr/document.py @@ -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: diff --git a/modules/edocument_peppol/account.py b/modules/edocument_peppol/account.py index d06f5c7db9..0b36299ae0 100644 --- a/modules/edocument_peppol/account.py +++ b/modules/edocument_peppol/account.py @@ -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) diff --git a/modules/edocument_ubl/edocument.py b/modules/edocument_ubl/edocument.py index 13718613f0..4bc8209748 100644 --- a/modules/edocument_ubl/edocument.py +++ b/modules/edocument_ubl/edocument.py @@ -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): diff --git a/modules/edocument_ubl/template/2/base.xml b/modules/edocument_ubl/template/2/base.xml index c94dceaf95..f9e59bd646 100644 --- a/modules/edocument_ubl/template/2/base.xml +++ b/modules/edocument_ubl/template/2/base.xml @@ -13,7 +13,7 @@ this repository contains the full copyright notices and license terms. --> - ${identifier.code} + ${identifier.code_compact} ${Address(address, specification=specification)} @@ -23,7 +23,7 @@ this repository contains the full copyright notices and license terms. --> ${tax_identifier.vatin} - ${tax_identifier.code} + ${tax_identifier.code_compact} @@ -34,7 +34,7 @@ this repository contains the full copyright notices and license terms. --> ${party.name} - ${identifier.code} + ${identifier.code_compact} diff --git a/modules/edocument_uncefact/template/16B-CII/CrossIndustryInvoice.xml b/modules/edocument_uncefact/template/16B-CII/CrossIndustryInvoice.xml index ead5453aa4..a0d443505a 100644 --- a/modules/edocument_uncefact/template/16B-CII/CrossIndustryInvoice.xml +++ b/modules/edocument_uncefact/template/16B-CII/CrossIndustryInvoice.xml @@ -20,7 +20,7 @@ this repository contains the full copyright notices and license terms. --> ${TradeAddress(address)} - ${tax_identifier.code} + ${tax_identifier.code_compact} diff --git a/modules/party/CHANGELOG b/modules/party/CHANGELOG index ba988ac6a2..9d5812f319 100644 --- a/modules/party/CHANGELOG +++ b/modules/party/CHANGELOG @@ -1,3 +1,4 @@ +* Format identifier codes * Add Senegal Tax Number identifier * Add Azerbaijan Tax Number identifier * Add French Trade Registration Number identifier diff --git a/modules/party/party.py b/modules/party/party.py index d50651c61b..e4b8dc9cab 100644 --- a/modules/party/party.py +++ b/modules/party/party.py @@ -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() diff --git a/modules/stock_package_shipping_dpd/stock.py b/modules/stock_package_shipping_dpd/stock.py index 702062d05a..c96cd3417d 100644 --- a/modules/stock_package_shipping_dpd/stock.py +++ b/modules/stock_package_shipping_dpd/stock.py @@ -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'] = [ diff --git a/modules/stock_package_shipping_sendcloud/stock.py b/modules/stock_package_shipping_sendcloud/stock.py index f5a579beaa..a3fcba22ba 100644 --- a/modules/stock_package_shipping_sendcloud/stock.py +++ b/modules/stock_package_shipping_sendcloud/stock.py @@ -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( diff --git a/modules/web_shop_vue_storefront/party.py b/modules/web_shop_vue_storefront/party.py index a94144f5ad..f294d4c4e1 100644 --- a/modules/web_shop_vue_storefront/party.py +++ b/modules/web_shop_vue_storefront/party.py @@ -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 diff --git a/modules/web_shop_vue_storefront/web.py b/modules/web_shop_vue_storefront/web.py index e69a139117..9b0f2bac66 100644 --- a/modules/web_shop_vue_storefront/web.py +++ b/modules/web_shop_vue_storefront/web.py @@ -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')