| |
| 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): |
| |
| for name in names: |
| val = get_fact(model, name) |
| if val: |
| return val |
|
|
| |
| 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 |
|
|
| |
| text = text.replace("\u00AD", "") |
| text = text.replace("\u2011", "-") |
|
|
| |
| t = " ".join(text.split()) |
|
|
| |
| prefix = "Selskabets væsentligste aktiviteter" |
| if t.startswith(prefix): |
| t = t[len(prefix):].lstrip(" .") |
|
|
| |
| t = t.replace(prefix + " ", "") |
| t = t.replace(prefix, "") |
|
|
| |
| 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() |
|
|
| |
| |
| |
| if any(x in v for x in [ |
| "ingen bistand", |
| "ingen", |
| "uden revisor", |
| "ikke revideret", |
| "uden revision", |
| "uden gennemgang", |
| "uden erklæring" |
| ]): |
| return "Ingen bistand" |
|
|
| |
| |
| |
| if any(x in v for x in [ |
| "andre erklæringer uden sikkerhed", |
| "uden sikkerhed", |
| "assistance", |
| "revisorassistance", |
| "udarbejdet med bistand", |
| "udarbejdet uden", |
| "kompilering" |
| ]): |
| return "Assistance" |
|
|
| |
| |
| |
| if any(x in v for x in [ |
| "erklæring om udvidet gennemgang", |
| "udvidet gennemgang", |
| "udv. gennemgang", |
| "gennemgangserklæring", |
| "gennemgang" |
| ]): |
| return "Udvidet Gennemgang" |
|
|
| |
| |
| |
| if any(x in v for x in [ |
| "revisionspåtegning", |
| "revision", |
| "revideret regnskab", |
| "aflagt med revision", |
| "revisionsfirma", |
| "audited financial statements", |
| ]): |
| return "Revision" |
|
|
| |
| |
| |
| if "review" in v: |
| return "Review" |
|
|
| |
| |
| |
| 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() |
|
|
| |
| |
| |
| 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" |
|
|
| |
| |
| |
| if any(x in v for x in [ |
| "statsautoriseret", |
| "state-authorised", |
| "state authorised", |
| "statsaut." |
| ]): |
| return "Statsautoriseret revisor" |
|
|
| |
| |
| |
| if any(x in v for x in [ |
| "registreret revisor", |
| "reg. revisor", |
| "registreret" |
| ]): |
| return "Registreret revisor" |
|
|
| |
| |
| |
| |
| |
| if any(x in v for x in [ |
| "deloitte", "pwc", "bdo", "ey", "grant thornton", |
| "martinsen", "kpmg" |
| ]): |
| |
| return "Andet" |
|
|
| |
| |
| |
| 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": |
| 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 = { |
| |
| "Revisionstype": _normalize_revisionstype(_find_first(model, REVISION_TYPE)), |
| "Revisortype": _normalize_revisortype(_find_first(model, AUDITOR_DESCRIPTION)), |
|
|
| |
| "Væsentlig aktivitet": _clean_activity(_find_first(model, MAIN_ACTIVITY)), |
|
|
| |
| "Korrektion af væsentlig fejl": _find_first(model, MATERIAL_ERROR_CORRECTION), |
|
|
| |
| "Going concern usikkerhed": _find_first(model, GOING_CONCERN), |
|
|
| |
| "Anvendt regnskabsklasse": _find_first(model, 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)} |
|
|