Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions frameworks/rs-gaap/packages/rs-gaap/v1/taxonomy.jsonld
Original file line number Diff line number Diff line change
Expand Up @@ -38676,6 +38676,12 @@
"@type": "xsd:boolean",
"@value": true
}
],
"rdfs:label": [
{
"@value": "Income Tax Expense (Benefit), Continuing Operations",
"@language": "en"
}
]
},
{
Expand Down Expand Up @@ -59070,6 +59076,12 @@
"@type": "xsd:boolean",
"@value": true
}
],
"rdfs:label": [
{
"@value": "Liabilities and Partners' Capital",
"@language": "en"
}
]
},
{
Expand Down Expand Up @@ -65214,6 +65226,12 @@
"@type": "xsd:boolean",
"@value": true
}
],
"rdfs:label": [
{
"@value": "Net Interest Income (Loss) After Provision for Loan Losses",
"@language": "en"
}
]
},
{
Expand Down Expand Up @@ -104607,6 +104625,12 @@
"@type": "xsd:boolean",
"@value": true
}
],
"rdfs:label": [
{
"@value": "Secondary Processing Revenue",
"@language": "en"
}
]
},
{
Expand Down Expand Up @@ -111512,6 +111536,12 @@
"@type": "xsd:boolean",
"@value": true
}
],
"rdfs:label": [
{
"@value": "Vehicle Toll Revenue",
"@language": "en"
}
]
},
{
Expand Down
29 changes: 26 additions & 3 deletions robosystems/operations/serialization/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ def build_report_bundle(
from robosystems.models.core.graph.graph import Graph
from robosystems.models.extensions.association import Association
from robosystems.models.extensions.element import Element
from robosystems.models.extensions.element_label import ElementLabel
from robosystems.models.extensions.entity import Entity
from robosystems.models.extensions.roboledger.fact import Fact
from robosystems.models.extensions.roboledger.fact_set import FactSet
Expand Down Expand Up @@ -553,8 +554,27 @@ def build_report_bundle(
country=entity.address_country,
)

# Standard display labels (role=standard, en) for the bundled concepts — the
# XBRL label-linkbase + JSON-LD skos:prefLabel source. element.name is the
# human label for most concepts but the bare localname for single-word
# fundamentals (Assets, Cash, Liabilities, Revenues, Goodwill, …); sourcing
# the standard label here keeps those from collapsing to the QName in arelle.
standard_label_by_id: dict[str, str] = {}
if elements_by_id:
standard_label_by_id = {
str(eid): txt
for eid, txt in session.execute(
select(ElementLabel.element_id, ElementLabel.text).where(
ElementLabel.element_id.in_(list(elements_by_id)),
ElementLabel.role == "standard",
ElementLabel.language == "en",
)
)
}

schema_concepts = [
_element_to_bundle(elements_by_id[eid]) for eid in sorted(elements_by_id)
_element_to_bundle(elements_by_id[eid], standard_label_by_id.get(eid))
for eid in sorted(elements_by_id)
]
linkbases = _associations_to_linkbases(associations, structures_by_id, elements_by_id)
period_nodes, period_ref_for_fact = _mint_periods(facts)
Expand Down Expand Up @@ -656,13 +676,16 @@ def _period_metas_for_report(report: Any, fact_sets: list[Any]) -> list[PeriodMe
return []


def _element_to_bundle(e: Any) -> BundleElement:
def _element_to_bundle(e: Any, standard_label: str | None = None) -> BundleElement:
return BundleElement(
id=str(e.id),
qname=str(e.qname or e.name),
namespace=e.namespace,
name=str(e.name),
label=e.description,
# The standard label linkbase entry is the authoritative display label
# (skos:prefLabel / XBRL standard label); fall back to the element's
# description only when no standard label exists.
label=standard_label or e.description,
balance_type=e.balance_type if e.balance_type in {"debit", "credit"} else None,
period_type=str(e.period_type),
is_abstract=bool(e.is_abstract),
Expand Down
17 changes: 17 additions & 0 deletions tests/operations/serialization/test_bundle_producer.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,23 @@ def test_drops_invalid_balance_type(self) -> None:
b = _element_to_bundle(e)
assert b.balance_type is None

def test_standard_label_overrides_description(self) -> None:
"""The standard label-linkbase entry is authoritative over the element's
description as the concept's display label."""
e = _FakeElement(
id="elem_01", qname="rs-gaap:Assets", name="Assets", description="Total Assets"
)
assert _element_to_bundle(e, "Assets").label == "Assets"

def test_standard_label_sourced_when_name_equals_localname(self) -> None:
"""Regression: a single-word fundamental (name == QName localpart, no
description) carries no label without a sourced standard label — which is
why Assets/Cash/Liabilities/Revenues collapsed to their QName in the
XBRL/JSON-LD export. With the standard label sourced, it survives."""
e = _FakeElement(id="elem_01", qname="rs-gaap:Assets", name="Assets")
assert _element_to_bundle(e, None).label is None # pre-fix behaviour
assert _element_to_bundle(e, "Assets").label == "Assets" # the fix


# ── _associations_to_linkbases ────────────────────────────────────────

Expand Down
12 changes: 12 additions & 0 deletions tests/operations/serialization/test_xbrl_emitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,18 @@ def test_label_resource_text_and_arc_wiring(self) -> None:
assert arc.get(f"{{{NS_XLINK}}}from") == loc.get(f"{{{NS_XLINK}}}label")
assert arc.get(f"{{{NS_XLINK}}}to") == label_el.get(f"{{{NS_XLINK}}}label")

def test_single_word_label_equal_to_localname_is_emitted(self) -> None:
# Regression: a fundamental whose standard label equals its QName local
# part ("Assets" for rs-gaap:Assets) must still emit a label. The bundle
# now sources the standard label (bundle._element_to_bundle), so arelle
# shows "Assets" rather than falling back to "rs-gaap:Assets".
bundle = _single_concept_bundle(label="Assets", facts=[_fact("f1", 100.0)])
root = _parse(serialize_to_xbrl_21(bundle), "report-lab.xml")
link = root.find(f"{{{NS_LINK}}}labelLink")
assert link is not None
label_el = link.find(f"{{{NS_LINK}}}label")
assert label_el is not None and label_el.text == "Assets"


class TestFactDedup:
def _facts_in(self, zip_bytes: bytes) -> list[etree._Element]:
Expand Down