[FIX] l10n_ro_edi: country code prefix on VAT / company_registry

This commit makes the CIUS-RO e-invoice emitted by a Romanian company
without VAT number compliant with the CIUS-RO requirements.

The validator for CIUS-RO (which extends the validations from the BIS3
Schematron) asserts a rule `[BR-CO-09]` where:

if the PartyTaxScheme/TaxScheme/ID == 'VAT',
  CompanyID must start with a country code prefix.

In Romania however, there are multiple types of "Tax IDs", and it is perfectly
valid in Romania to have a Tax ID without RO (country code prefix) in front of
them. They are not a subject to paying VAT, and it should still be possible to
generate CIUS-RO XML with their tax identifications.

We have to handle their cases by changing the TaxScheme/ID to 'something other
than VAT', preventing the trigger of the rule and allow Romanian companies
without prefixed VAT to use CIUS-RO.

```xml
<rule context="//cac:PartyTaxScheme[cac:TaxScheme/normalize-space(upper-case(cbc:ID))='VAT']">
  <assert id="BR-CO-09" flag="fatal" test="( contains( ' 1A AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH EL ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS XI YE YT ZA ZM ZW ',substring(cbc:CompanyID,1,2) ) )">[BR-CO-09]-The Seller VAT identifier (BT-31), the Seller tax representative VAT identifier (BT-63) and the Buyer VAT identifier (BT-48) shall have a prefix in accordance with ISO code ISO 3166-1 alpha-2 by which the country of issue may be identified. Nevertheless, Greece may use the prefix ‘EL’.</assert>
</rule>
```

This commit also fixes and clean some of the irrelevant constraints and tests
previously written in `l10n_ro_edi`.

closes odoo/odoo#155252

Task-id: 3649426
Signed-off-by: Brice Bartoletti (bib) <bib@odoo.com>
This commit is contained in:
Yosua Nicolaus
2024-02-28 19:49:05 +00:00
parent d111e66525
commit 95a9cd2a46
3 changed files with 202 additions and 27 deletions
@@ -42,12 +42,17 @@ class AccountEdiXmlUBLRO(models.AbstractModel):
vals_list = super()._get_partner_party_tax_scheme_vals_list(partner, role)
if not partner.vat and partner.company_registry:
return [{
'company_id': partner.company_registry,
'tax_scheme_vals': {
'id': 'VAT',
},
}]
# Use company_registry (Company ID) as the VAT replacement
vals_list = [{'company_id': partner.company_registry, 'tax_scheme_vals': {'id': 'VAT'}}]
# The validator for CIUS-RO (which extends the validations from the BIS3 Schematron) asserts a rule where:
# [BR-CO-09] if the PartyTaxScheme/TaxScheme/ID == 'VAT', CompanyID must start with a country code prefix.
# In Romania however, the CompanyID can be with or without country code prefix and still be perfectly valid.
# We have to handle their cases by changing the TaxScheme/ID to 'something other than VAT',
# preventing the trigger of the rule and allow Romanian companies without prefixed VAT to use CIUS-RO.
for vals in vals_list:
if partner.country_code == 'RO' and not vals['company_id'].upper().startswith('RO'):
vals['tax_scheme_vals']['id'] = 'NO_VAT'
return vals_list
@@ -98,12 +103,6 @@ class AccountEdiXmlUBLRO(models.AbstractModel):
"At least one of them is required. ",
partner.name)
if (not partner.vat and partner.company_registry
and not partner.company_registry.startswith(partner.country_code)):
constraints[f"ciusro_{partner_type}_country_code_company_registry_required"] = _(
"The following partner's doesn't have a country code prefix in their Company ID: %s.",
partner.name)
if (partner.country_code == 'RO'
and partner.state_id
and partner.state_id.code == 'B'
@@ -0,0 +1,166 @@
<?xml version='1.0' encoding='UTF-8'?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1</cbc:CustomizationID>
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
<cbc:ID>INV/2017/00001</cbc:ID>
<cbc:IssueDate>2017-01-01</cbc:IssueDate>
<cbc:DueDate>2017-02-28</cbc:DueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:Note>test narration</cbc:Note>
<cbc:DocumentCurrencyCode>RON</cbc:DocumentCurrencyCode>
<cbc:TaxCurrencyCode>RON</cbc:TaxCurrencyCode>
<cbc:BuyerReference>ref_partner_a</cbc:BuyerReference>
<cac:OrderReference>
<cbc:ID>ref_move</cbc:ID>
</cac:OrderReference>
<cac:AdditionalDocumentReference>
<cbc:ID>INV_2017_00001.pdf</cbc:ID>
<cac:Attachment>
<cbc:EmbeddedDocumentBinaryObject mimeCode="application/pdf" filename="INV_2017_00001.pdf">___ignore___</cbc:EmbeddedDocumentBinaryObject>
</cac:Attachment>
</cac:AdditionalDocumentReference>
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="9947">___ignore___</cbc:EndpointID>
<cac:PartyName>
<cbc:Name>Hudson Construction</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Strada Kunst, 3</cbc:StreetName>
<cbc:CityName>SECTOR1</cbc:CityName>
<cbc:PostalZone>010101</cbc:PostalZone>
<cbc:CountrySubentity>RO-B</cbc:CountrySubentity>
<cac:Country>
<cbc:IdentificationCode>RO</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>1234567897</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>NO_VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Hudson Construction</cbc:RegistrationName>
<cbc:CompanyID>1234567897</cbc:CompanyID>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Name>Hudson Construction</cbc:Name>
<cbc:Telephone>+40 123 456 789</cbc:Telephone>
</cac:Contact>
</cac:Party>
</cac:AccountingSupplierParty>
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="9947">___ignore___</cbc:EndpointID>
<cac:PartyName>
<cbc:Name>Roasted Romanian Roller</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Rolling Roast, 88</cbc:StreetName>
<cbc:CityName>SECTOR3</cbc:CityName>
<cbc:PostalZone>010101</cbc:PostalZone>
<cbc:CountrySubentity>RO-B</cbc:CountrySubentity>
<cac:Country>
<cbc:IdentificationCode>RO</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>1234567897</cbc:CompanyID>
<cac:TaxScheme>
<cbc:ID>NO_VAT</cbc:ID>
</cac:TaxScheme>
</cac:PartyTaxScheme>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Roasted Romanian Roller</cbc:RegistrationName>
<cbc:CompanyID>1234567897</cbc:CompanyID>
</cac:PartyLegalEntity>
<cac:Contact>
<cbc:Name>Roasted Romanian Roller</cbc:Name>
<cbc:Telephone>+40 123 456 780</cbc:Telephone>
</cac:Contact>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:Delivery>
<cac:DeliveryLocation>
<cac:Address>
<cbc:StreetName>Rolling Roast, 88</cbc:StreetName>
<cbc:CityName>SECTOR3</cbc:CityName>
<cbc:PostalZone>010101</cbc:PostalZone>
<cbc:CountrySubentity>RO-B</cbc:CountrySubentity>
<cac:Country>
<cbc:IdentificationCode>RO</cbc:IdentificationCode>
</cac:Country>
</cac:Address>
</cac:DeliveryLocation>
</cac:Delivery>
<cac:PaymentMeans>
<cbc:PaymentMeansCode name="credit transfer">30</cbc:PaymentMeansCode>
<cbc:PaymentID>INV/2017/00001</cbc:PaymentID>
<cac:PayeeFinancialAccount>
<cbc:ID>RO98RNCB1234567890123456</cbc:ID>
</cac:PayeeFinancialAccount>
</cac:PaymentMeans>
<cac:PaymentTerms>
<cbc:Note>Payment terms: 30% Advance End of Following Month</cbc:Note>
</cac:PaymentTerms>
<cac:TaxTotal>
<cbc:TaxAmount currencyID="RON">285.00</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="RON">1500.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="RON">285.00</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>19.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="RON">1500.00</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="RON">1500.00</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="RON">1785.00</cbc:TaxInclusiveAmount>
<cbc:PrepaidAmount currencyID="RON">0.00</cbc:PrepaidAmount>
<cbc:PayableAmount currencyID="RON">1785.00</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<cac:InvoiceLine>
<cbc:ID>___ignore___</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">1.0</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="RON">500.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>product_a</cbc:Description>
<cbc:Name>product_a</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>19.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="RON">500.0</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<cac:InvoiceLine>
<cbc:ID>___ignore___</cbc:ID>
<cbc:InvoicedQuantity unitCode="DZN">1.0</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="RON">1000.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>product_b</cbc:Description>
<cbc:Name>product_b</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>19.0</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="RON">1000.0</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</Invoice>
+25 -15
View File
@@ -97,29 +97,39 @@ class TestUBLRO(TestUBLCommon):
self._assert_invoice_attachment(attachment, xpaths=None, expected_file_path='from_odoo/ciusro_out_invoice_different_currency.xml')
self.currency_data['currency'] = self.env.ref('base.RON')
def test_export_no_vat_but_have_company_id_without_prefix(self):
self.company_data['company'].write({
'vat': None,
'company_registry': '1234567897',
})
invoice = self.create_move("out_invoice", send=False)
with self.assertRaisesRegex(UserError, "doesn't have a country code prefix in their Company ID"):
invoice._generate_pdf_and_send_invoice(self.move_template, allow_fallback_pdf=False)
def test_export_invoice_without_country_code_prefix_in_vat(self):
self.company_data['company'].write({'vat': '1234567897'})
self.partner_a.write({'vat': '1234567897'})
invoice = self.create_move("out_invoice")
attachment = self.get_attachment(invoice)
self._assert_invoice_attachment(attachment, xpaths=None, expected_file_path='from_odoo/ciusro_out_invoice_no_prefix_vat.xml')
def test_export_no_vat_but_have_company_id_with_prefix(self):
self.company_data['company'].write({
'vat': None,
'company_registry': 'RO1234567897',
})
def test_export_no_vat_but_have_company_registry(self):
self.company_data['company'].write({'vat': False, 'company_registry': 'RO1234567897'})
self.partner_a.write({'vat': False, 'company_registry': 'RO1234567897'})
invoice = self.create_move("out_invoice")
attachment = self.get_attachment(invoice)
self._assert_invoice_attachment(attachment, xpaths=None, expected_file_path='from_odoo/ciusro_out_invoice.xml')
def test_export_no_vat_but_have_company_registry_without_prefix(self):
self.company_data['company'].write({'vat': False, 'company_registry': '1234567897'})
self.partner_a.write({'vat': False, 'company_registry': '1234567897'})
invoice = self.create_move("out_invoice")
attachment = self.get_attachment(invoice)
self._assert_invoice_attachment(attachment, xpaths=None, expected_file_path='from_odoo/ciusro_out_invoice_no_prefix_vat.xml')
def test_export_no_vat_and_no_company_registry_raises_error(self):
self.company_data['company'].write({'vat': False, 'company_registry': False})
self.partner_a.write({'vat': False, 'company_registry': False})
invoice = self.create_move("out_invoice", send=False)
with self.assertRaisesRegex(UserError, "doesn't have a VAT nor Company ID"):
invoice._generate_pdf_and_send_invoice(self.move_template, allow_fallback_pdf=False)
def test_export_constraints(self):
self.company_data['company'].company_registry = None
self.company_data['company'].company_registry = False
for required_field in ('city', 'street', 'state_id', 'vat'):
prev_val = self.company_data["company"][required_field]
self.company_data["company"][required_field] = None
self.company_data["company"][required_field] = False
invoice = self.create_move("out_invoice", send=False)
with self.assertRaisesRegex(UserError, "required"):
invoice._generate_pdf_and_send_invoice(self.move_template, allow_fallback_pdf=False)