DEV Community

Cover image for Integrating UAE Peppol E-Invoicing with Odoo — What Developers and Implementers Need to Know
Perfonec Computers
Perfonec Computers

Posted on

Integrating UAE Peppol E-Invoicing with Odoo — What Developers and Implementers Need to Know

UAE mandatory e-invoicing went live in pilot phase on 1 July 2026. Every UAE VAT-registered business sending B2B invoices must now route them through the UAE Peppol network as PINT AE XML. For Odoo developers and implementers working with UAE clients, this creates a specific integration challenge — Odoo does not ship with a native UAE Peppol access point. The transmission layer has to be added externally.

This post covers what the UAE Peppol infrastructure looks like technically, what PINT AE XML requires at the field level, how the Odoo data model maps to those fields, where data quality issues typically appear, and what a complete implementation involves from a developer perspective.

The 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-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 Kilogram
LTR Litre
MTR Metre
HUR Hour (services billed hourly)
DAY Day
MTK Square metre
Mapping PINT AE Fields to the Odoo Data Model

Here is the direct mapping from PINT AE BT codes to Odoo models and fields:

python

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_uom on account.move.line

to uom.uom — requires custom l10n_ae_uom_code field on uom.uom

VAT category code maps through tax_ids on account.move.line

to account.tax — requires custom l10n_ae_vat_type field on account.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)

python

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.

python

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.

python

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'
)
Enter fullscreen mode Exit fullscreen mode

Then populate via a data XML file:

xml

EA


KGM

Issue 4 — account.tax records without PINT AE category

python
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')
Enter fullscreen mode Exit fullscreen mode

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.

python

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:

python
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()
Enter fullscreen mode Exit fullscreen mode

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:

python
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 category S

Test 2 — Zero-rated export invoice
BT-55 buyer country ≠ AE
Tax line with category Z

Test 3 — Credit note
move_type = out_refund
BT-3 = 381
BT-25 = original invoice name

Test 4 — Mixed-tax invoice
Lines with S and Z categories 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 from sale.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.

Top comments (0)