Integrating UAE Peppol E-Invoicing with Odoo - What Developers and Implementers Need to Know
UAE Peppol Network - Four-Corner Architecture
UAE e-invoicing uses the standard Peppol four-corner model:
- Corner 1 - Seller ERP (Odoo)
- Corner 2 - Seller ASP (Accredited Service Provider)
- Corner 3 - Buyer ASP
- Corner 4 - Buyer ERP
The ASP at Corner 2 is the developer integration point. It validates the invoice against the PINT AE schema, handles the Peppol transmission, and returns a status response. For Odoo, this means building or configuring a connector between the account.move model and the ASP API.
The standard integration pattern is:
account.move (state = posted) → intercept confirmation event → read invoice data from Odoo ORM → map to PINT AE fields → serialize to XML → POST to ASP API endpoint → receive status response → write status back to account.move
PINT AE XML - Schema and Mandatory Fields
PINT AE is the UAE's national extension of Peppol BIS Billing 3.0. It is defined under the UAE Ministry of Finance Electronic Invoicing Guidelines Version 1.1, published June 2026. Every B2B invoice must pass schema validation before the ASP will transmit it.
The 17 mandatory PINT AE fields and their BT codes are:
| BT Code | Field | Notes |
|---|---|---|
| BT-1 | Invoice number | |
| BT-2 | Invoice date | ISO 8601 - YYYY-MM-DD |
| BT-3 | Invoice type code | 380 = standard invoice, 381 = credit note |
| BT-5 | Invoice currency code | ISO 4217 |
| BT-27 | Supplier name | |
| BT-29 | Supplier TRN | 15-digit UAE VAT number |
| BT-35 | Supplier street address | |
| BT-37 | Supplier city | |
| BT-40 | Supplier country code | Must be AE |
| BT-44 | Buyer name | |
| BT-46 | Buyer TRN | 15-digit UAE VAT number |
| BT-50 | Buyer street address | |
| BT-52 | Buyer city | |
| BT-55 | Buyer country code | |
| BT-110 | Sum of invoice line net amounts | |
| BT-112 | Invoice total VAT amount | |
| BT-131 | Invoice line net amount (per line) |
Every invoice line must also carry one of four PINT AE VAT category codes:
- S - Standard rate - 5% UAE VAT
- Z - Zero-rated - exports, certain food, international transport
- E - Exempt - bare land, residential property resale
- O - Out of scope - outside UAE territory, certain financial services
And every unit of measure must use UN/CEFACT codes:
EA- Each (countable items)KGM- KilogramLTR- LitreMTR- MetreHUR- Hour (services billed hourly)DAY- DayMTK- Square metre
Mapping PINT AE Fields to the Odoo Data Model
Direct mapping from PINT AE BT codes to Odoo models and fields:
# PINT AE → Odoo field mapping
PINT_AE_MAPPING = {
'BT-1': ('account.move', 'name'),
'BT-2': ('account.move', 'invoice_date'),
'BT-3': ('account.move', 'move_type'),
'BT-5': ('account.move', 'currency_id'),
'BT-27': ('res.company', 'name'),
'BT-29': ('res.company', 'vat'),
'BT-35': ('res.company', 'street'),
'BT-37': ('res.company', 'city'),
'BT-40': ('res.company', 'country_id'),
'BT-44': ('res.partner', 'name'),
'BT-46': ('res.partner', 'vat'),
'BT-50': ('res.partner', 'street'),
'BT-52': ('res.partner', 'city'),
'BT-55': ('res.partner', 'country_id'),
'BT-110': ('account.move', 'amount_untaxed'),
'BT-112': ('account.move', 'amount_tax'),
'BT-131': ('account.move.line', 'price_subtotal'),
}
- Unit of measure maps through
product_uomonaccount.move.linetouom.uom- requires customl10n_ae_uom_codefield onuom.uom - VAT category code maps through
tax_idsonaccount.move.linetoaccount.tax- requires customl10n_ae_vat_typefield onaccount.tax
The Most Common Data Quality Issues
In practice, the PINT AE schema is not the hard part. The hard part is data quality in existing Odoo databases. These are the five failures that appear most consistently across UAE Odoo implementations:
Issue 1 - BT-46 missing (buyer TRN)
# Check for partners without TRN
partners_without_trn = env['res.partner'].search([
('customer_rank', '>', 0),
('vat', '=', False),
('country_id.code', '=', 'AE'),
])
Typical result: 20-40% of customer records.
Fix: Bulk update via partner import, add a constrains check on res.partner to require vat when country_id = AE and customer_rank > 0.
Issue 2 - BT-50/BT-52 combined in one field
Many UAE Odoo accounts have the full address stored in street as a single string - "Office 401, Al Barsha 1, Dubai". PINT AE requires street and city as separate fields.
# Identify affected partners
partners_with_combined_address = env['res.partner'].search([
('customer_rank', '>', 0),
('city', '=', False),
('street', '!=', False),
])
Fix: Write a migration script to split the address or use partner import with separate columns.
Issue 3 - Missing UN/CEFACT codes on uom.uom
Standard Odoo UoM records do not include UN/CEFACT codes. The PINT AE XML serialiser needs this to populate the unit code on each invoice line.
# Add custom field to uom.uom
class UoM(models.Model):
_inherit = 'uom.uom'
l10n_ae_uom_code = fields.Char(
string='UN/CEFACT Code',
help='Required for UAE PINT AE e-invoicing'
)
Then populate via a data XML file:
<record id="uom_unit" model="uom.uom">
<field name="l10n_ae_uom_code">EA</field>
</record>
<record id="uom_kgm" model="uom.uom">
<field name="l10n_ae_uom_code">KGM</field>
</record>
Issue 4 - account.tax records without PINT AE category
class AccountTax(models.Model):
_inherit = 'account.tax'
l10n_ae_vat_type = fields.Selection([
('S', 'Standard Rate (5%)'),
('Z', 'Zero Rated'),
('E', 'Exempt'),
('O', 'Out of Scope'),
], string='PINT AE VAT Category')
Issue 5 - Fiscal position not assigned to customer records
UAE fiscal position must be set on res.partner for all UAE VAT-registered customers. Without it, tax lines will not carry the correct PINT AE category.
# Bulk assign UAE fiscal position
uae_fiscal_pos = env['account.fiscal.position'].search([
('name', 'ilike', 'UAE')
], limit=1)
uae_partners = env['res.partner'].search([
('customer_rank', '>', 0),
('country_id.code', '=', 'AE'),
('property_account_position_id', '=', False),
])
uae_partners.write({
'property_account_position_id': uae_fiscal_pos.id
})
The ASP Integration - Request and Response Pattern
The ASP API call follows this pattern for most UAE FTA-approved ASPs:
import requests
import json
def submit_to_asp(invoice_xml: str, invoice_id: str) -> dict:
"""
Submit PINT AE XML to ASP endpoint.
Returns status dict with transmission result.
"""
headers = {
'Content-Type': 'application/xml',
'Authorization': f'Bearer {ASP_API_KEY}',
'X-Invoice-ID': invoice_id,
}
response = requests.post(
url=ASP_ENDPOINT,
data=invoice_xml.encode('utf-8'),
headers=headers,
timeout=30,
)
if response.status_code == 200:
return {
'status': 'sent',
'peppol_id': response.json().get('peppolId'),
'timestamp': response.json().get('sentAt'),
}
elif response.status_code == 422:
return {
'status': 'rejected',
'errors': response.json().get('validationErrors', []),
}
else:
response.raise_for_status()
The status response should be written back to account.move via a custom field and surfaced in the invoice chatter so the accounts team can see it without going to a separate dashboard.
Credit Notes - Type 381
Credit notes in PINT AE use document type code 381 and must reference the original invoice:
def get_invoice_type_code(move):
"""Map Odoo move_type to PINT AE BT-3 value."""
if move.move_type == 'out_invoice':
return '380'
elif move.move_type == 'out_refund':
return '381'
raise ValueError(f'Unexpected move_type: {move.move_type}')
def get_preceding_invoice_ref(move):
"""Get original invoice reference for credit notes (BT-25)."""
if move.move_type == 'out_refund' and move.reversed_entry_id:
return move.reversed_entry_id.name
return None
Testing Sequence for the Pilot Phase
The UAE e-invoicing pilot phase runs from 1 July 2026 with no financial penalties for validation failures. This is the right window to run a complete integration test.
The recommended sequence:
-
Test 1 - Standard B2B invoice
All 17 BT fields populated, tax line with categoryS -
Test 2 - Zero-rated export invoice
BT-55buyer country ≠AE, tax line with categoryZ -
Test 3 - Credit note
move_type = out_refund,BT-3 = 381,BT-25 =original invoice name -
Test 4 - Mixed-tax invoice
Lines withSandZcategories on same invoice -
Test 5 - Free zone invoice
Custom beneficiary field populated for DMCC/JAFZA/IFZA/DIFC -
Test 6 - Multi-currency invoice
currency_id ≠ AED, AED equivalent amounts included per Version 1.1 -
Test 7 - Advance payment invoice
Per UAE E-Invoicing Guidelines Version 1.1, separate document flow fromsale.order
Each test should be run against the ASP's sandbox Peppol endpoint - not a mock - to catch schema validation errors that local pre-validation may not catch.
Mandatory Deadlines for Implementers
- Go-live (revenue AED 50M+): 1 January 2027
- Fines begin (large biz): 2 January 2027
- Go-live (all other SMEs): 1 July 2027
- Fines begin (SMEs): 2 July 2027
- Pilot phase (no penalties): From 1 July 2026 - open now
Penalty under Cabinet Decision No. 106 of 2025:
- AED 5,000/month for non-implementation
- AED 100/non-compliant invoice capped at AED 5,000/month
- Maximum combined: AED 10,000/month
Summary
UAE Peppol e-invoicing for Odoo is a well-scoped integration problem. The PINT AE schema is clearly defined. The Odoo data model maps cleanly to the BT fields once the custom extensions are in place. The real work is data quality - specifically BT-46 (buyer TRN), address structure (BT-50/BT-52), UN/CEFACT unit codes, and fiscal position assignment.
A pre-validation step before the ASP call is worth implementing - it catches the most common data issues at confirmation time and reduces ASP API errors significantly. The pilot phase is the right time to run the full test sequence. No penalties apply during pilot regardless of how many validation failures occur.
Based on UAE Ministry of Finance Electronic Invoicing Guidelines Version 1.1 (June 2026) and Peppol BIS Billing 3.0. For technical reference only - not legal advice. Written by Akanksha Surana - Odoo Ready Partner with Odoo Middle East.
Comments
No comments yet. Start the discussion.