Varmeregnskab i storskala ejendomsportefølje — professionel administration
Professionel varmeafregning for store ejendomsporteføljer: batch-generering, portfolio KPI, automatisk á conto-justering, OIOUBL bulk-eksport, revision og SLA-overvågning.
Af M-Bus Gateway
Store ejendomsporteføljer med 50-500+ ejendomme kræver automatisering, batch-processer og konsoliderede KPI'er. Her er arkitekturen bag professionel storskala-administration.
Skala og kompleksitet
Porteføljestørrelser og krav:
Lille (1-10 ejendomme):
→ Manuel administration mulig
→ Én landlord-bruger
→ Afregning én gang pr. år
Mellemstor (10-50 ejendomme):
→ Behov for batch-generering
→ 2-5 administratorer (landlord-roller)
→ Portefølje-KPI ønskes
Stor (50-500 ejendomme):
→ Fuld automatisering nødvendig
→ Ekstern admin + landlord pr. ejendomsklynge
→ Batch-afregning + bulk-OIOUBL
→ Revisoradgang + separat tenant pr. CVR
Enterprise (500+ ejendomme):
→ Multi-tenant holding-struktur
→ API-integration til ERP (e-conomic, Business Central)
→ Webhook-baseret regnskabssystem-opdatering
→ SLA-krav på gateway-leveringsrate
Batch-generering af årsafregninger
# server/src/settlement/batch_generator.py
import asyncio
from datetime import date
from typing import AsyncIterator
from sqlmodel.ext.asyncio.session import AsyncSession
from server.src.models import Property, AnnualInput
async def generate_settlements_for_portfolio(
tenant_id: str,
period_end: date,
db: AsyncSession,
concurrency: int = 5,
) -> dict[str, str]:
"""
Generer årsafregninger for hele porteføljen.
concurrency=5: Maks 5 properties parallelt (undgå DB overload).
Returnerer: {property_id: status}
"""
# Find alle ejendomme klar til afregning
properties = await get_properties_ready_for_settlement(
tenant_id, period_end, db
)
results: dict[str, str] = {}
semaphore = asyncio.Semaphore(concurrency)
async def generate_one(property_id: str) -> None:
async with semaphore:
try:
await generate_settlement(property_id, period_end, db)
results[property_id] = "success"
except Exception as e:
results[property_id] = f"error: {e}"
await asyncio.gather(*[generate_one(p.id) for p in properties])
return results
async def get_properties_ready_for_settlement(
tenant_id: str,
period_end: date,
db: AsyncSession,
) -> list[Property]:
"""
Returnerer ejendomme der opfylder:
1. Har AnnualInput for den ønskede periode
2. Har aktive lejemål
3. Har aflæsninger inden for perioden
4. Har ikke allerede en settlement for perioden
"""
from sqlmodel import select, and_, not_, exists
from server.src.models import Settlement
stmt = (
select(Property)
.where(
and_(
Property.tenant_id == tenant_id,
Property.deleted_at.is_(None),
exists(
select(AnnualInput.id).where(
and_(
AnnualInput.property_id == Property.id,
AnnualInput.period_end == period_end,
AnnualInput.deleted_at.is_(None),
)
)
),
not_(
exists(
select(Settlement.id).where(
and_(
Settlement.property_id == Property.id,
Settlement.period_end == period_end,
Settlement.deleted_at.is_(None),
)
)
)
),
)
)
)
result = await db.exec(stmt)
return list(result.all())
Portfolio KPI — executive overblik
Konsoliderede KPI'er for storportefølje:
Afregnings-KPI (pr. sæson):
→ Total genererede afregninger: 412/430 ejendomme (95,8%)
→ Udestående (>4 måneder): 18 ejendomme (KRITISK)
→ Sendte afregninger: 398 (96,6%)
→ Åbningsrate: 78% inden for 7 dage
→ Indsigelses-rate: 3,2% (branche-benchmark: 4-6%)
Gateway-KPI:
→ Portefølje leverings-SLA (24t): 94,2%
→ Gateways offline > 7 dage: 3 stk (ALARM)
→ Stille målere (>72t): 47 stk
→ AES-dækning: 89% (mangler nøgler: 23 Engelmann HCA)
Energi-KPI:
→ Portefølje-gns: 124 kWh/m²/år
→ Outliers > 2σ: 12 ejendomme (ANALYSE ANBEFALET)
→ Graddage-normaliseret ÅTÅ ændring: -3,2% (forbedring)
Platform endpoints:
GET /api/v1/portfolio/kpi → Executive snapshot
GET /api/v1/portfolio/kpi/export → CSV til ledelsesrapport
GET /api/v1/portfolio/alerts → Handlingskrævende items
Automatisk á conto-justering
# server/src/aconto/auto_adjuster.py
# Kør efter afregningssæson: Justér á conto for kommende år
from decimal import Decimal, ROUND_HALF_UP
from dataclasses import dataclass
@dataclass
class AContoAdjustment:
unit_id: str
current_monthly: Decimal
actual_annual_cost: Decimal
recommended_monthly: Decimal
change_pct: Decimal
action: str # "increase", "decrease", "ok"
def calculate_aconto_adjustment(
unit_id: str,
current_monthly_aconto: Decimal,
actual_annual_cost: Decimal,
buffer_pct: Decimal = Decimal("0.10"),
) -> AContoAdjustment:
"""
Beregn anbefalet á conto for næste år baseret på faktisk forbrug.
Buffer: 10% over faktisk forbrug (undgå efterbetaling).
"""
# Anbefalet annual = faktisk + buffer
recommended_annual = actual_annual_cost * (Decimal("1") + buffer_pct)
recommended_monthly = (recommended_annual / Decimal("12")).quantize(
Decimal("0.01"), rounding=ROUND_HALF_UP
)
current_annual = current_monthly_aconto * Decimal("12")
change_pct = (
(recommended_annual - current_annual) / current_annual * 100
).quantize(Decimal("0.1"), rounding=ROUND_HALF_UP)
if change_pct > Decimal("5"):
action = "increase"
elif change_pct < Decimal("-5"):
action = "decrease"
else:
action = "ok"
return AContoAdjustment(
unit_id=unit_id,
current_monthly=current_monthly_aconto,
actual_annual_cost=actual_annual_cost,
recommended_monthly=recommended_monthly,
change_pct=change_pct,
action=action,
)
OIOUBL bulk-eksport
OIOUBL batch-eksport til erhvervslejere:
Trigger:
→ Manuelt fra portal: "Eksportér alle OIOUBL"
→ API: POST /api/v1/settlements/oioubl/batch
→ Celery task: Asynkron generering
Output:
→ ZIP-fil med én .xml pr. settlement med EAN-nummer
→ Navngivning: OIOUBL_{settlement_id}_{date}.xml
→ Download-link via Hetzner Object Storage (pre-signed URL, 24t gyldighed)
Valideringsrapport (vedlagt i ZIP):
→ validation_report.csv
→ Kolonner: settlement_id, ean, status, error_message
→ Status: valid / invalid_ean / missing_cvr / schema_error
Automatisk NemHandel-upload (fremtid):
→ Peppol Access Point integration
→ AS4 transport protokol
→ Automatisk kvittering fra modtager
Tidsforbrug (benchmark):
→ 1 OIOUBL XML: ~50ms (lxml generering)
→ 100 OIOUBL XML: ~5 sekunder (Celery single worker)
→ 500 OIOUBL XML: ~25 sekunder (Celery 4 workers)
Revisionseksport og dokumentation
Revisoradgang til storportefølje:
Hvad revisoren kræver (BEK 563 + bogføringsloven):
✅ Årsafregninger som PDF (5-7 år)
✅ Fjernvarmefakturaer (AnnualInput PDF-bilag)
✅ HCA-aflæsningslister med datoer
✅ Pro-rata beregninger ved ind-/fraflytning
✅ Audit-log for godkendelser og ændringer
✅ OIOUBL XML ved erhvervslejemål
✅ MID-certifikat-dokumentation
Platform-revisionseksport:
GET /api/v1/export/audit → Audit-log CSV (filter pr. periode)
GET /api/v1/export/settlements → Alle afregninger pr. tenant
GET /api/v1/export/annual-inputs → Alle AnnualInput med bilag-links
GET /api/v1/export/readings → Råaflæsninger pr. periode
Revisor-brugerrolle:
→ Oprettes som landlord med read-only flag
→ Kan eksportere men ikke ændre
→ Audit-log: Revisors adgange logges (GDPR-compliance)
SLA-dokumentation til bestyrelsesrapport:
→ Gateway leverings-SLA pr. ejendom
→ Indsigels-rate og -behandlingstid
→ Energiforbrug kWh/m² vs. benchmark
Konklusion
Storskala-porteføljeadministration kræver batch-generering med concurrency-styring, konsoliderede portfolio-KPI'er, automatisk á conto-justering baseret på faktisk forbrug og bulk-OIOUBL-eksport til erhvervslejere. Platformen er bygget til dette fra dag 1: multi-tenant med external_admin, separate tenant pr. CVR, portfolio-endpoints og Celery-baseret async batch-processing. Revisoreksport dækker alle BEK 563- og bogføringskrav.