Disclosure: I used AI to help draft this article. I reviewed the content and tested the script myself. I work at an accounting software partner in Dubai, and this post is a general tutorial.
The UAE is moving to structured electronic invoices. Instead of emailing a PDF, a system produces invoice data in a defined format, and an accredited service provider sends it to the customer. The Ministry of Finance publishes the list of accredited providers.
In practice, most rejected invoices are not caused by software bugs. They come from bad master data: a customer with no tax number, an item called "Services", an address stored in one text box. This post shows a small script that finds these problems in a CSV export before you send anything.
What the script checks
- Customer tax registration numbers (TRN) exist and are 15 digits
- Customer address has a street, a city, and a country
- Item names are not generic words such as "Services" or "Goods"
- Unit codes come from an allowed list
- VAT category codes come from an allowed list
The data
Export two CSV files from your accounting system. customers.csv:
name,trn,street,city,country
Alpha Trading LLC,100123456789012,Sheikh Zayed Road,Dubai,AE
Beta Logistics,,Industrial Area 3,Sharjah,AE
Gamma Foods,12345,,Abu Dhabi,AE
And items.csv:
name,unit,vat_category
Monthly IT support retainer,HUR,S
Services,Box,S
Steel pipe 6 metre,MTR,Z
Goods,EA,X
The script
import csv
import re
import sys
# Edit these to match the specification your provider gives you.
GENERIC_NAMES = {"services", "service", "goods", "item", "items", "product",
"products", "misc", "other", "general", "supply"}
ALLOWED_UNITS = {"EA", "C62", "KGM", "LTR", "MTR", "MTQ", "HUR", "DAY"}
ALLOWED_VAT_CATEGORIES = {"S", "Z", "E", "O"}
TRN_PATTERN = re.compile(r"^\d{15}$") # UAE TRNs are 15 digits
def check_customers(path):
problems = []
with open(path, newline="", encoding="utf-8") as f:
for row_no, row in enumerate(csv.DictReader(f), start=2):
name = row.get("name", "").strip() or f"row {row_no}"
trn = row.get("trn", "").strip()
if not trn:
problems.append((name, "missing TRN"))
elif not TRN_PATTERN.match(trn):
problems.append((name, f"TRN is not 15 digits: {trn}"))
for field in ("street", "city", "country"):
if not row.get(field, "").strip():
problems.append((name, f"missing {field}"))
return problems
def check_items(path):
problems = []
with open(path, newline="", encoding="utf-8") as f:
for row_no, row in enumerate(csv.DictReader(f), start=2):
name = row.get("name", "").strip()
label = name or f"row {row_no}"
if name.lower() in GENERIC_NAMES or len(name) < 4:
problems.append((label, "item name too generic"))
unit = row.get("unit", "").strip().upper()
if unit not in ALLOWED_UNITS:
problems.append((label, f"unit '{unit}' is not an allowed code"))
cat = row.get("vat_category", "").strip().upper()
if cat not in ALLOWED_VAT_CATEGORIES:
problems.append((label, f"VAT category '{cat}' not recognised"))
return problems
if __name__ == "__main__":
customers, items = sys.argv[1], sys.argv[2]
all_problems = check_customers(customers) + check_items(items)
for record, issue in all_problems:
print(f"{record}: {issue}")
print(f"\n{len(all_problems)} issue(s) found")
Running it
python validate_data.py customers.csv items.csv
With the sample data above, the output is:
Beta Logistics: missing TRN
Gamma Foods: TRN is not 15 digits: 12345
Gamma Foods: missing street
Services: item name too generic
Services: unit 'BOX' is not an allowed code
Goods: item name too generic
Goods: VAT category 'X' not recognised
7 issue(s) found
Limits worth knowing
This script checks format only. It does not confirm that a TRN belongs to the customer, and it does not replace the validation your accredited provider performs. The allowed lists at the top are examples. Use the code lists in the specification your provider supplies, and update them when the specification changes.
Start with customers you invoice most often. Fixing a small group of high-volume records usually removes most of the problem.
If you adapt this for your own system, I would be interested to hear which checks you added.
Top comments (0)