OIOUBL faktura-XML — elektronisk fakturering til det offentlige
OIOUBL UBL 2.1 Invoice XML til elektronisk fakturering: struktur, påkrævede felter, EAN-nummer, NemHandel-indsendelse, validering og Python-generering til almene boligselskaber.
Af M-Bus Gateway
OIOUBL (Offentlig Information Online UBL) er krævet ved fakturering til det offentlige og almene boligselskaber i Danmark. Her er implementeringen.
Hvornår kræves OIOUBL?
OIOUBL er krævet ved fakturering til:
✅ Statslige institutioner og styrelser
✅ Kommuner og regioner
✅ Almene boligselskaber (LBF-regulerede)
✅ Selvejende institutioner med offentlig finansiering
Ikke krævet til:
✗ Private udlejere (landlord-rolle)
✗ Andelsboligforeninger (private)
✗ Ejerforeninger
Lovgrundlag:
→ Bekendtgørelse om elektronisk afregning med offentlige myndigheder
→ EAN-nummer (GLN) identificerer modtager-institution
→ Sendes via NemHandel OIORASP-netværk
→ Alternativ: PEPPOL BIS Billing 3.0 (EU-kompatibel)
EAN-nummer:
→ 13-cifret GS1-nummer (Global Location Number)
→ Hentes fra OIORASP-adresseregistret eller CVR
→ Eksempel EAN for boligselskab: 5790001234567
OIOUBL Invoice XML struktur
<?xml version="1.0" encoding="UTF-8"?>
<!-- OIOUBL Invoice 2.02 — UBL 2.1 subset -->
<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">
<!-- Dokumenthoved -->
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
<cbc:CustomizationID>OIOUBL-2.02</cbc:CustomizationID>
<cbc:ProfileID>Procurement-BilSimR-1.0</cbc:ProfileID>
<cbc:ID>2026-AFREG-001234</cbc:ID> <!-- Fakturanummer -->
<cbc:IssueDate>2026-09-30</cbc:IssueDate>
<cbc:DueDate>2026-10-30</cbc:DueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode> <!-- 380 = Handelsfaktura -->
<cbc:DocumentCurrencyCode>DKK</cbc:DocumentCurrencyCode>
<!-- Sælger (M-Bus Gateway / udlejer) -->
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="GLN">5790001111111</cbc:EndpointID>
<cac:PartyName><cbc:Name>Ejendomsselskabet ApS</cbc:Name></cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Testgade 1</cbc:StreetName>
<cbc:CityName>København</cbc:CityName>
<cbc:PostalZone>1000</cbc:PostalZone>
<cac:Country><cbc:IdentificationCode>DK</cbc:IdentificationCode></cac:Country>
</cac:PostalAddress>
<cac:PartyTaxScheme>
<cbc:CompanyID>DK12345678</cbc:CompanyID> <!-- CVR med DK-præfix -->
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
</cac:PartyTaxScheme>
</cac:Party>
</cac:AccountingSupplierParty>
<!-- Køber (boligselskab — EAN-nummer) -->
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="GLN">5790001234567</cbc:EndpointID>
<cac:PartyName><cbc:Name>Almene Boliger A/S</cbc:Name></cac:PartyName>
</cac:Party>
</cac:AccountingCustomerParty>
<!-- Fakturalinjer -->
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="DKK">45230.50</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Description>Varmeafregning 2025-2026, Testvej 1, 8000 Aarhus</cbc:Description>
<cbc:Name>Årsafregning varmeforbrug</cbc:Name>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="DKK">45230.50</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
<!-- Totaler -->
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="DKK">45230.50</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="DKK">45230.50</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="DKK">45230.50</cbc:TaxInclusiveAmount>
<cbc:PayableAmount currencyID="DKK">45230.50</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
</Invoice>
Python XML-generering med lxml
# server/src/oioubl/generator.py
from lxml import etree
from datetime import date, timedelta
from server.src.db.models import Settlement
NS = {
None: "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2",
"cac": "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
"cbc": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
}
CAC = "{urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2}"
CBC = "{urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2}"
def generate_oioubl_invoice(settlement: Settlement) -> bytes:
"""
Generér OIOUBL 2.02 Invoice XML fra en Settlement.
Kræver: settlement.property.ean_number, settlement.total_amount
"""
root = etree.Element("Invoice", nsmap=NS)
def cbc(tag: str, text: str, **attrib) -> etree.Element:
el = etree.SubElement(root, f"{CBC}{tag}", **attrib)
el.text = text
return el
cbc("UBLVersionID", "2.1")
cbc("CustomizationID", "OIOUBL-2.02")
cbc("ProfileID", "Procurement-BilSimR-1.0")
cbc("ID", f"MBUS-{settlement.id[:8].upper()}")
cbc("IssueDate", date.today().isoformat())
cbc("DueDate", (date.today() + timedelta(days=30)).isoformat())
cbc("InvoiceTypeCode", "380")
cbc("DocumentCurrencyCode", settlement.currency or "DKK")
# Note = varmeperiode:
note = etree.SubElement(root, f"{CBC}Note")
note.text = (
f"Varmeafregning {settlement.period_start.strftime('%d.%m.%Y')} "
f"– {settlement.period_end.strftime('%d.%m.%Y')}"
)
# Returnér som bytes med XML declaration:
return etree.tostring(
root,
xml_declaration=True,
encoding="UTF-8",
pretty_print=True,
)
FastAPI endpoint: OIOUBL download
# server/src/settlements/router.py (udsnit)
from fastapi import Response
@router.get("/{settlement_id}/oioubl")
async def download_oioubl(
settlement_id: str,
session: AsyncSession = Depends(get_session),
user: TokenPayload = Depends(require_role("landlord", "super_admin")),
):
"""Download OIOUBL Invoice XML til NemHandel-indsendelse."""
settlement = await session.get(Settlement, settlement_id)
if not settlement or settlement.tenant_id != user.tenant_id:
raise HTTPException(404)
prop = await session.get(Property, settlement.property_id)
if not prop.ean_number:
raise HTTPException(400, "EAN-nummer mangler på ejendomsprofilen")
xml_bytes = generate_oioubl_invoice(settlement)
return Response(
content=xml_bytes,
media_type="application/xml",
headers={
"Content-Disposition": f'attachment; filename="faktura-{settlement_id[:8]}.xml"',
},
)
Validering: OIOUBL schema check
# Valider XML mod officielt OIOUBL XSD-schema:
from lxml import etree
import httpx
async def validate_oioubl(xml_bytes: bytes) -> list[str]:
"""
Valider OIOUBL XML mod officielt schema.
Returnér liste af fejlmeddelelser (tom = valid).
"""
# Download schema første gang (cache lokalt):
schema_path = Path("server/schemas/OIOUBL_Invoice_2p02.xsd")
if not schema_path.exists():
async with httpx.AsyncClient() as client:
r = await client.get(
"https://nemhandel.dk/schemas/oioubl/2.02/OIOUBL_Invoice_2p02.xsd"
)
schema_path.write_bytes(r.content)
xsd = etree.XMLSchema(etree.parse(str(schema_path)))
doc = etree.fromstring(xml_bytes)
if not xsd.validate(doc):
return [str(e) for e in xsd.error_log]
return []
Konklusion
OIOUBL kræves ved fakturering til almene boligselskaber og offentlige institutioner i Danmark. XML-strukturen er UBL 2.1 med OIOUBL-profil. EAN-nummer (GLN) på ejendommen er forudsætning. M-Bus Gateway platformen genererer OIOUBL XML via /api/v1/settlements/{id}/oioubl — klar til NemHandel-indsendelse. lxml giver hurtig, typesikker XML-generering med XSD-validering.
Se BEK 563 varmefordeling guide eller årsafregning skabelon guide.