M-Bus Gateway
← Tilbage til blog
· OIOUBL· NemHandel· faktura· XML· UBL· offentlig sektor· EAN· elektronisk fakturering· Python· boligselskab

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.