danielhjerresen's picture
Upload 18 files
a6d7bb2 verified
Raw
History Blame Contribute Delete
7.44 kB
# xbrl_processing/parser.py
from .arelle_loader import load_model
from .fact_extractor import get_fact
from .taxonomy_map import (
REVISION_TYPE,
AUDITOR_DESCRIPTION,
MAIN_ACTIVITY,
MATERIAL_ERROR_CORRECTION,
GOING_CONCERN,
ACCOUNTING_CLASS,
ACCOUNTING_CLASS_UPGRADE,
)
def _get_ixbrl_non_numeric(model, names):
"""
Extract text from ix:nonNumeric facts in XHTML/iXBRL.
"""
if not hasattr(model, "ixFacts"):
return None
for ix in model.ixFacts:
local = getattr(ix.qname, "localName", None)
if not local:
continue
if local in names:
text = ix.value
if text:
return text.strip()
return None
def _find_first(model, names):
# Try normal XBRL facts first
for name in names:
val = get_fact(model, name)
if val:
return val
# Fallback: look in iXBRL nonNumeric tags
ix_val = _get_ixbrl_non_numeric(model, names)
if ix_val:
return ix_val
return None
def _clean_activity(text: str) -> str:
if not text:
return text
# Remove soft hyphens and weird invisible characters
text = text.replace("\u00AD", "") # soft hyphen
text = text.replace("\u2011", "-") # non-breaking hyphen
# Normalize whitespace
t = " ".join(text.split())
# Case 1 — prefix at the beginning
prefix = "Selskabets væsentligste aktiviteter"
if t.startswith(prefix):
t = t[len(prefix):].lstrip(" .")
# Case 2 — prefix concatenated inside the string ("aktiviteterSelskabets")
t = t.replace(prefix + " ", "")
t = t.replace(prefix, "")
# Final trim
return t.strip()
def _normalize_revisionstype(value: str) -> str:
"""
Normalize various wording forms of revision types into:
- 'Revision'
- 'Udvidet Gennemgang'
- 'Assistance'
- 'Ingen bistand'
- 'Review' (rare but supported)
"""
if not value:
return None
v = value.lower().strip()
# -------------------------
# 1) INGEN BISTAND
# -------------------------
if any(x in v for x in [
"ingen bistand",
"ingen", # generic
"uden revisor",
"ikke revideret",
"uden revision",
"uden gennemgang",
"uden erklæring"
]):
return "Ingen bistand"
# -------------------------
# 2) ASSISTANCE (uden sikkerhed)
# -------------------------
if any(x in v for x in [
"andre erklæringer uden sikkerhed",
"uden sikkerhed",
"assistance",
"revisorassistance",
"udarbejdet med bistand",
"udarbejdet uden", # e.g. "udarbejdet uden revisor"
"kompilering"
]):
return "Assistance"
# -------------------------
# 3) UDV. GENNEMGANG
# -------------------------
if any(x in v for x in [
"erklæring om udvidet gennemgang",
"udvidet gennemgang",
"udv. gennemgang",
"gennemgangserklæring",
"gennemgang" # careful but safe: most Danish uses are "udvidet gennemgang"
]):
return "Udvidet Gennemgang"
# -------------------------
# 4) REVISION
# -------------------------
if any(x in v for x in [
"revisionspåtegning",
"revision",
"revideret regnskab",
"aflagt med revision",
"revisionsfirma",
"audited financial statements",
]):
return "Revision"
# -------------------------
# 5) REVIEW (rare, English)
# -------------------------
if "review" in v:
return "Review"
# -------------------------
# Fallback — unknown label
# -------------------------
return value
def _normalize_revisortype(value: str) -> str:
"""
Normalize wording of 'Revisortype' into:
- 'Statsautoriseret revisor'
- 'Registreret revisor'
- 'Ingen revisor'
- 'Andet'
"""
if not value:
return None
v = value.lower().strip()
# -------------------------
# 1) INGEN REVISOR
# -------------------------
if any(x in v for x in [
"ingen",
"uden revisor",
"ikke revideret",
"ikke valgt revisor",
"fravalgt revision",
"fravalg af revisor",
"uden revision"
]):
return "Ingen revisor"
# -------------------------
# 2) STATSautorisERet REVIsor
# -------------------------
if any(x in v for x in [
"statsautoriseret",
"state-authorised",
"state authorised",
"statsaut."
]):
return "Statsautoriseret revisor"
# -------------------------
# 3) REGISTRERET REVISOR
# -------------------------
if any(x in v for x in [
"registreret revisor",
"reg. revisor",
"registreret"
]):
return "Registreret revisor"
# -------------------------
# 4) ANDRE – side cases
# -------------------------
# Some reports include the firm name but no type:
# e.g. "Deloitte", "PwC", "BDO"
if any(x in v for x in [
"deloitte", "pwc", "bdo", "ey", "grant thornton",
"martinsen", "kpmg"
]):
# Usually a statsaut. firm but we cannot assume.
return "Andet"
# -------------------------
# Fallback
# -------------------------
return "Andet"
def _extract_periods_from_dcca_tags(model):
"""
Extract CY/PY using Danish GAAP period tags.
Same logic as in financial_parser.
"""
cy_start = None
cy_end = None
py_start = None
py_end = None
for fact in model.facts:
name = fact.qname.localName
if name == "ReportingPeriodStartDate":
cy_start = fact.value
elif name == "ReportingPeriodEndDate":
cy_end = fact.value
elif name == "PrecedingReportingPeriodStartDate":
py_start = fact.value
elif name == "PredingReportingPeriodEndDate": # DCCA typo
py_end = fact.value
return {
"CY": {"start": cy_start, "end": cy_end},
"PY": {"start": py_start, "end": py_end},
}
def extract_xbrl_data(filepath: str) -> dict:
"""
Parse XBRL/iXBRL file with Arelle and extract general qualitative facts.
No ML, no SBERT — pure taxonomy-based extraction.
"""
try:
model = load_model(filepath)
data = {
# Revision info
"Revisionstype": _normalize_revisionstype(_find_first(model, REVISION_TYPE)),
"Revisortype": _normalize_revisortype(_find_first(model, AUDITOR_DESCRIPTION)),
# Company activity description
"Væsentlig aktivitet": _clean_activity(_find_first(model, MAIN_ACTIVITY)),
# Corrections of material errors
"Korrektion af væsentlig fejl": _find_first(model, MATERIAL_ERROR_CORRECTION),
# Going concern
"Going concern usikkerhed": _find_first(model, GOING_CONCERN),
# Accounting class
"Anvendt regnskabsklasse": _find_first(model, ACCOUNTING_CLASS),
# Optional use of higher accounting class
"Tilvalg af højere regnskabsklasse": _find_first(model, ACCOUNTING_CLASS_UPGRADE),
}
data["Years"] = _extract_periods_from_dcca_tags(model)
return data
except Exception as e:
print("[Fejl] XBRL parsing:", e)
return {"Fejl": str(e)}