arelle-release 2.37.48__py3-none-any.whl → 2.37.50__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of arelle-release might be problematic. Click here for more details.

arelle/ModelDocument.py CHANGED
@@ -1579,12 +1579,6 @@ def inlineIxdsDiscover(modelXbrl, modelIxdsDocument, setTargetModelXbrl=False, *
1579
1579
  modelXbrl.targetRoleRefs = {} # roleRefs used by selected target
1580
1580
  modelXbrl.targetArcroleRefs = {} # arcroleRefs used by selected target
1581
1581
  modelXbrl.targetRelationships = set() # relationship elements used by selected target
1582
- targetModelXbrl = modelXbrl if setTargetModelXbrl else None # modelXbrl of target for contexts/units in multi-target/multi-instance situation
1583
- assignUnusedContextsUnits = (not setTargetModelXbrl and not ixdsTarget and
1584
- not getattr(modelXbrl, "supplementalModelXbrls", ()) and (
1585
- not getattr(modelXbrl, "targetIXDSesToLoad", ()) or
1586
- set(e.modelDocument for e in modelXbrl.ixdsHtmlElements) ==
1587
- set(x.modelDocument for e in getattr(modelXbrl, "targetIXDSesToLoad", ()) for x in e[1])))
1588
1582
  hasResources = hasHeader = False
1589
1583
  for htmlElement in modelXbrl.ixdsHtmlElements:
1590
1584
  mdlDoc = htmlElement.modelDocument
@@ -1611,8 +1605,12 @@ def inlineIxdsDiscover(modelXbrl, modelIxdsDocument, setTargetModelXbrl=False, *
1611
1605
  for modelInlineFact in htmlElement.iterdescendants(ixNStag + "nonNumeric", ixNStag + "nonFraction", ixNStag + "fraction"):
1612
1606
  if isinstance(modelInlineFact,ModelObject):
1613
1607
  _target = modelInlineFact.get("target")
1614
- factTargetContextRefs[_target].add(modelInlineFact.get("contextRef"))
1615
- factTargetUnitRefs[_target].add(modelInlineFact.get("unitRef"))
1608
+ contextRef = modelInlineFact.get("contextRef")
1609
+ if contextRef is not None:
1610
+ factTargetContextRefs[_target].add(contextRef.strip())
1611
+ unitRef = modelInlineFact.get("unitRef")
1612
+ if unitRef is not None:
1613
+ factTargetUnitRefs[_target].add(unitRef.strip())
1616
1614
  if modelInlineFact.id:
1617
1615
  factsByFactID[modelInlineFact.id] = modelInlineFact
1618
1616
  for elt in htmlElement.iterdescendants(tag=ixNStag + "continuation"):
@@ -1734,9 +1732,9 @@ def inlineIxdsDiscover(modelXbrl, modelIxdsDocument, setTargetModelXbrl=False, *
1734
1732
  targetRoleUris[_target].add(footnoteRole)
1735
1733
 
1736
1734
  contextRefs = factTargetContextRefs[ixdsTarget]
1735
+ contextRefsForAllTargets = {ref for refs in factTargetContextRefs.values() for ref in refs}
1737
1736
  unitRefs = factTargetUnitRefs[ixdsTarget]
1738
- allContextRefs = set.union(*factTargetContextRefs.values())
1739
- allUnitRefs = set.union(*factTargetUnitRefs.values())
1737
+ unitRefsForAllTargets = {ref for refs in factTargetUnitRefs.values() for ref in refs}
1740
1738
 
1741
1739
  # discovery of contexts, units and roles which are used by target document
1742
1740
  for htmlElement in modelXbrl.ixdsHtmlElements:
@@ -1745,13 +1743,17 @@ def inlineIxdsDiscover(modelXbrl, modelIxdsDocument, setTargetModelXbrl=False, *
1745
1743
 
1746
1744
  for inlineElement in htmlElement.iterdescendants(tag=ixNStag + "resources"):
1747
1745
  for elt in inlineElement.iterchildren("{http://www.xbrl.org/2003/instance}context"):
1748
- id = elt.get("id")
1749
- if id in contextRefs or (assignUnusedContextsUnits and id not in allContextRefs):
1746
+ contextId = elt.get("id")
1747
+ if contextId in contextRefs:
1750
1748
  modelIxdsDocument.contextDiscover(elt, setTargetModelXbrl)
1749
+ elif contextId not in contextRefsForAllTargets:
1750
+ modelXbrl.ixdsUnmappedContexts[contextId] = elt
1751
1751
  for elt in inlineElement.iterchildren("{http://www.xbrl.org/2003/instance}unit"):
1752
- id = elt.get("id")
1753
- if id in unitRefs or (assignUnusedContextsUnits and id not in allUnitRefs):
1752
+ unitId = elt.get("id")
1753
+ if unitId in unitRefs:
1754
1754
  modelIxdsDocument.unitDiscover(elt, setTargetModelXbrl)
1755
+ elif unitId not in unitRefsForAllTargets:
1756
+ modelXbrl.ixdsUnmappedUnits[unitId] = elt
1755
1757
  for refElement in inlineElement.iterchildren("{http://www.xbrl.org/2003/linkbase}roleRef"):
1756
1758
  r = refElement.get("roleURI")
1757
1759
  if r in targetRoleUris[ixdsTarget]:
@@ -1159,7 +1159,7 @@ class ModelContext(ModelObject):
1159
1159
  self._dimsHash = hash( frozenset(self.qnameDims.values()) )
1160
1160
  return self._dimsHash
1161
1161
 
1162
- def nonDimValues(self, contextElement):
1162
+ def nonDimValues(self, contextElement: str | int) -> list[ModelObject]:
1163
1163
  """([ModelObject]) -- ContextElement is either string or Aspect code for segment or scenario, returns nonXDT ModelObject children of context element.
1164
1164
 
1165
1165
  :param contextElement: one of 'segment', 'scenario', Aspect.NON_XDT_SEGMENT, Aspect.NON_XDT_SCENARIO, Aspect.COMPLETE_SEGMENT, Aspect.COMPLETE_SCENARIO
arelle/ModelXbrl.py CHANGED
@@ -12,7 +12,7 @@ from collections import defaultdict
12
12
  from typing import TYPE_CHECKING, Any, TypeVar, Union, cast, Optional
13
13
 
14
14
  import regex as re
15
- from collections.abc import Iterable
15
+ from collections.abc import Iterable, Iterator
16
16
 
17
17
  import arelle
18
18
  from arelle import FileSource, ModelRelationshipSet, XmlUtil, ModelValue, XbrlConst, XmlValidate
@@ -335,8 +335,12 @@ class ModelXbrl:
335
335
  self.facts: list[ModelFact] = []
336
336
  self.factsInInstance: set[ModelFact] = set()
337
337
  self.undefinedFacts: list[ModelFact] = [] # elements presumed to be facts but not defined
338
- self.contexts: dict[str, ModelDocumentClass.xmlRootElement] = {}
338
+ self.contexts: dict[str, ModelContext] = {}
339
+ self.ixdsUnmappedContexts: dict[str, ModelContext] = {}
340
+ self._contextsInUseMarked = False
339
341
  self.units: dict[str, ModelUnit] = {}
342
+ self.ixdsUnmappedUnits: dict[str, ModelUnit] = {}
343
+ self._unitsInUseMarked = False
340
344
  self.modelObjects: list[ModelObject] = []
341
345
  self.qnameParameters: dict[QName, Any] = {}
342
346
  self.modelVariableSets: set[ModelVariableSet] = set()
@@ -604,7 +608,7 @@ class ModelXbrl:
604
608
  for cOCCs,mOCCs in ((c.nonDimValues(segAspect),segOCCs),
605
609
  (c.nonDimValues(scenAspect),scenOCCs)))
606
610
  ):
607
- return cast('ModelContext', c)
611
+ return c
608
612
  return None
609
613
 
610
614
  def createContext(
@@ -868,17 +872,24 @@ class ModelXbrl:
868
872
  return fbdq[memQname]
869
873
 
870
874
  @property
871
- def contextsInUse(self) -> Any:
872
- try:
873
- if self._contextsInUseMarked:
874
- return (cntx for cntx in self.contexts.values() if getattr(cntx, "_inUse", False))
875
- except AttributeError:
875
+ def contextsInUse(self) -> Iterator[ModelContext]:
876
+ if not self._contextsInUseMarked:
876
877
  for fact in self.factsInInstance:
877
878
  cntx = fact.context
878
879
  if cntx is not None:
879
880
  cntx._inUse = True
880
- self._contextsInUseMarked: bool = True
881
- return self.contextsInUse
881
+ self._contextsInUseMarked = True
882
+ return (cntx for cntx in self.contexts.values() if getattr(cntx, "_inUse", False))
883
+
884
+ @property
885
+ def unitsInUse(self) -> Iterator[ModelUnit]:
886
+ if not self._unitsInUseMarked:
887
+ for fact in self.factsInInstance:
888
+ unit = fact.unit
889
+ if unit is not None:
890
+ unit._inUse = True
891
+ self._unitsInUseMarked = True
892
+ return (unit for unit in self.units.values() if getattr(unit, "_inUse", False))
882
893
 
883
894
  @property
884
895
  def dimensionsInUse(self) -> set[Any]:
arelle/WebCache.py CHANGED
@@ -6,20 +6,28 @@ e.g., User-Agent: Sample Company Name AdminContact@<sample company domain>.com
6
6
 
7
7
  '''
8
8
  from __future__ import annotations
9
- import contextlib
10
-
11
9
 
12
- from filelock import FileLock, Timeout
10
+ import calendar
11
+ import contextlib
12
+ import io
13
+ import json
14
+ import logging
15
+ import os
16
+ import posixpath
17
+ import shutil
18
+ import sys
19
+ import time
20
+ import zlib
21
+ from http.client import IncompleteRead
13
22
  from pathlib import Path
14
23
  from typing import TYPE_CHECKING, Any
15
- import os, posixpath, sys, time, calendar, io, json, logging, shutil, zlib
16
- import regex as re
17
- from urllib.parse import quote, unquote
18
- from urllib.error import URLError, HTTPError, ContentTooShortError
19
- from http.client import IncompleteRead
20
24
  from urllib import request as proxyhandlers
25
+ from urllib.error import ContentTooShortError, HTTPError, URLError
26
+ from urllib.parse import quote, unquote, urlsplit, urlunsplit
21
27
 
22
28
  import certifi
29
+ import regex as re
30
+ from filelock import FileLock, Timeout
23
31
 
24
32
  from arelle.PythonUtil import isLegacyAbs
25
33
 
@@ -567,14 +575,16 @@ class WebCache:
567
575
  :param url:
568
576
  :return: `url` with scheme-specific-part quoted except for parameter separators
569
577
  """
570
- urlScheme, schemeSep, urlSchemeSpecificPart = url.partition("://")
571
- urlPath, querySep, query = urlSchemeSpecificPart.partition("?")
572
- # RFC 3986: https://www.ietf.org/rfc/rfc3986.txt
573
- querySafeChars = ';/?'
574
- pathSafeChars = querySafeChars + ':@&=+$,'
575
- quotedUrlPath = quote(urlPath, safe=pathSafeChars)
576
- quotedQuery = quote(query, safe=querySafeChars)
577
- return urlScheme + schemeSep + quotedUrlPath + querySep + quotedQuery
578
+ parts = urlsplit(url)
579
+
580
+ # RFC 3986 safe characters: https://www.ietf.org/rfc/rfc3986.txt
581
+ pathSafe = "/:@!$&'()*+,;=" # path allows sub-delims, ":" and "@"
582
+ querySafe = "&=:/?@!$'()*+,;[]" # query allows pchar + "/" + "?"
583
+
584
+ quotedPath = quote(parts.path, safe=pathSafe)
585
+ quotedQuery = quote(parts.query, safe=querySafe)
586
+
587
+ return urlunsplit((parts.scheme, parts.netloc, quotedPath, quotedQuery, parts.fragment))
578
588
 
579
589
  @staticmethod
580
590
  def _getFileTimestamp(path: str) -> float:
arelle/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.37.48'
21
- __version_tuple__ = version_tuple = (2, 37, 48)
20
+ __version__ = version = '2.37.50'
21
+ __version_tuple__ = version_tuple = (2, 37, 50)
arelle/api/Session.py CHANGED
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
  import logging
9
9
  import threading
10
10
  from types import TracebackType
11
- from typing import Any, BinaryIO
11
+ from typing import Any, BinaryIO, TypeVar
12
12
 
13
13
  from arelle import PackageManager, PluginManager
14
14
  from arelle.CntlrCmdLine import CntlrCmdLine, createCntlrAndPreloadPlugins
@@ -18,6 +18,9 @@ from arelle.RuntimeOptions import RuntimeOptions
18
18
 
19
19
  _session_lock = threading.Lock()
20
20
 
21
+ # typing.Self can be used once Python 3.10 support is dropped.
22
+ Self = TypeVar("Self", bound="Session")
23
+
21
24
 
22
25
  class Session:
23
26
  """
@@ -46,7 +49,7 @@ class Session:
46
49
  "Session objects cannot be shared between threads. Create a new Session instance in each thread."
47
50
  )
48
51
 
49
- def __enter__(self) -> Any:
52
+ def __enter__(self: Self) -> Self:
50
53
  return self
51
54
 
52
55
  def __exit__(
@@ -146,7 +146,6 @@ class PluginValidationDataExtension(PluginData):
146
146
  return self._reportingPeriodContexts
147
147
  contexts = []
148
148
  for context in modelXbrl.contexts.values():
149
- context = cast(ModelContext, context)
150
149
  if context.isInstantPeriod or context.isForeverPeriod:
151
150
  continue # Reporting period contexts can't be instant/forever contexts
152
151
  if len(context.qnameDims) > 0:
@@ -5,16 +5,30 @@ from enum import Enum
5
5
 
6
6
  from arelle.ModelValue import qname
7
7
 
8
+ class AccountingStandard(Enum):
9
+ IFRS = 'IFRS'
10
+ JAPAN_GAAP = 'Japan GAAP'
11
+ US_GAAP = 'US GAAP'
12
+
8
13
  class FormType(Enum):
9
14
  FORM_2_4 = '第二号の四様式'
10
15
  FORM_2_7 = '第二号の七様式'
11
16
  FORM_3 = '第三号様式'
17
+ FORM_4 = '第四号様式'
18
+
19
+ @property
20
+ def isStockReport(self) -> bool:
21
+ return self in STOCK_REPORT_FORMS
12
22
 
13
23
  CORPORATE_FORMS =frozenset([
14
24
  FormType.FORM_2_4,
15
25
  FormType.FORM_2_7,
16
26
  FormType.FORM_3,
17
27
  ])
28
+ STOCK_REPORT_FORMS = frozenset([
29
+ FormType.FORM_3,
30
+ FormType.FORM_4,
31
+ ])
18
32
  qnEdinetManifestInsert = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}insert")
19
33
  qnEdinetManifestInstance = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}instance")
20
34
  qnEdinetManifestItem = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}item")
@@ -22,3 +36,4 @@ qnEdinetManifestIxbrl = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest
22
36
  qnEdinetManifestList = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}list")
23
37
  qnEdinetManifestTitle = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}title")
24
38
  qnEdinetManifestTocComposition = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}tocComposition")
39
+ xhtmlDtdExtension = "xhtml1-strict-ix.dtd"
@@ -3,15 +3,19 @@ See COPYRIGHT.md for copyright information.
3
3
  """
4
4
  from __future__ import annotations
5
5
 
6
- import zipfile
6
+ from collections import defaultdict
7
7
  from dataclasses import dataclass
8
+ from decimal import Decimal
8
9
  from functools import lru_cache
9
- from pathlib import Path
10
+ from operator import attrgetter
11
+ from typing import Callable, Hashable, Iterable, cast
10
12
 
11
13
  import regex
12
14
 
15
+ from arelle.LinkbaseType import LinkbaseType
13
16
  from arelle.ModelDocument import Type as ModelDocumentType
14
- from arelle.ModelInstanceObject import ModelFact
17
+ from arelle.ModelDtsObject import ModelConcept
18
+ from arelle.ModelInstanceObject import ModelFact, ModelUnit, ModelContext
15
19
  from arelle.ModelObject import ModelObject
16
20
  from arelle.ModelValue import QName, qname
17
21
  from arelle.ModelXbrl import ModelXbrl
@@ -20,21 +24,26 @@ from arelle.ValidateDuplicateFacts import getDeduplicatedFacts, DeduplicationTyp
20
24
  from arelle.XmlValidate import VALID
21
25
  from arelle.typing import TypeGetText
22
26
  from arelle.utils.PluginData import PluginData
23
- from .Constants import CORPORATE_FORMS
27
+ from .Constants import CORPORATE_FORMS, FormType
24
28
  from .ControllerPluginData import ControllerPluginData
25
29
  from .ManifestInstance import ManifestInstance
30
+ from .Statement import Statement, STATEMENTS, BalanceSheet, StatementInstance, StatementType
26
31
 
27
32
  _: TypeGetText
28
33
 
29
34
 
35
+ _DEBIT_QNAME_PATTERN = regex.compile('.*(Liability|Liabilities|Equity)')
36
+
37
+
30
38
  @dataclass
31
39
  class PluginValidationDataExtension(PluginData):
40
+ accountingStandardsDeiQn: QName
32
41
  assetsIfrsQn: QName
42
+ consolidatedOrNonConsolidatedAxisQn: QName
33
43
  documentTypeDeiQn: QName
34
44
  jpcrpEsrFilingDateCoverPageQn: QName
35
45
  jpcrpFilingDateCoverPageQn: QName
36
46
  jpspsFilingDateCoverPageQn: QName
37
- liabilitiesAndEquityIfrsQn: QName
38
47
  nonConsolidatedMemberQn: QName
39
48
  ratioOfFemaleDirectorsAndOtherOfficersQn: QName
40
49
 
@@ -45,19 +54,21 @@ class PluginValidationDataExtension(PluginData):
45
54
  def __init__(self, name: str):
46
55
  super().__init__(name)
47
56
  jpcrpEsrNamespace = "http://disclosure.edinet-fsa.go.jp/taxonomy/jpcrp-esr/2024-11-01/jpcrp-esr_cor"
48
- jpcrpNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jpcrp/2024-11-01/jpcrp_cor'
57
+ self.jpcrpNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jpcrp/2024-11-01/jpcrp_cor'
49
58
  jpdeiNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jpdei/2013-08-31/jpdei_cor'
50
59
  jpigpNamespace = "http://disclosure.edinet-fsa.go.jp/taxonomy/jpigp/2024-11-01/jpigp_cor"
51
60
  jppfsNamespace = "http://disclosure.edinet-fsa.go.jp/taxonomy/jppfs/2024-11-01/jppfs_cor"
52
61
  jpspsNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jpsps/2024-11-01/jpsps_cor'
62
+ self.accountingStandardsDeiQn = qname(jpdeiNamespace, 'AccountingStandardsDEI')
53
63
  self.assetsIfrsQn = qname(jpigpNamespace, 'AssetsIFRS')
64
+ self.consolidatedOrNonConsolidatedAxisQn = qname(jppfsNamespace, 'ConsolidatedOrNonConsolidatedAxis')
54
65
  self.documentTypeDeiQn = qname(jpdeiNamespace, 'DocumentTypeDEI')
66
+ self.issuedSharesTotalNumberOfSharesEtcQn = qname(self.jpcrpNamespace, 'IssuedSharesTotalNumberOfSharesEtcTextBlock')
55
67
  self.jpcrpEsrFilingDateCoverPageQn = qname(jpcrpEsrNamespace, 'FilingDateCoverPage')
56
- self.jpcrpFilingDateCoverPageQn = qname(jpcrpNamespace, 'FilingDateCoverPage')
68
+ self.jpcrpFilingDateCoverPageQn = qname(self.jpcrpNamespace, 'FilingDateCoverPage')
57
69
  self.jpspsFilingDateCoverPageQn = qname(jpspsNamespace, 'FilingDateCoverPage')
58
- self.liabilitiesAndEquityIfrsQn = qname(jpigpNamespace, "LiabilitiesAndEquityIFRS")
59
70
  self.nonConsolidatedMemberQn = qname(jppfsNamespace, "NonConsolidatedMember")
60
- self.ratioOfFemaleDirectorsAndOtherOfficersQn = qname(jpcrpNamespace, "RatioOfFemaleDirectorsAndOtherOfficers")
71
+ self.ratioOfFemaleDirectorsAndOtherOfficersQn = qname(self.jpcrpNamespace, "RatioOfFemaleDirectorsAndOtherOfficers")
61
72
 
62
73
  self.contextIdPattern = regex.compile(r'(Prior[1-9]Year|CurrentYear|Prior[1-9]Interim|Interim)(Duration|Instant)')
63
74
 
@@ -65,6 +76,27 @@ class PluginValidationDataExtension(PluginData):
65
76
  def __hash__(self) -> int:
66
77
  return id(self)
67
78
 
79
+ @lru_cache(1)
80
+ def _contextMatchesStatement(self, modelXbrl: ModelXbrl, contextId: str, statement: Statement) -> bool:
81
+ """
82
+ :return: Whether the context's facts are applicable to the given statement.
83
+ """
84
+ if 'Interim' in contextId:
85
+ # valid06.zip suggests "interim"" contexts are not considered for balance sheets.
86
+ return False
87
+ context = modelXbrl.contexts[contextId]
88
+ if not all(dimQn == self.consolidatedOrNonConsolidatedAxisQn for dimQn in context.qnameDims):
89
+ return False
90
+ memberValue = context.dimMemberQname(self.consolidatedOrNonConsolidatedAxisQn, includeDefaults=True)
91
+ contextIsConsolidated = memberValue != self.nonConsolidatedMemberQn
92
+ return bool(statement.isConsolidated == contextIsConsolidated)
93
+
94
+ def _isDebitConcept(self, concept: ModelConcept) -> bool:
95
+ """
96
+ :return: Whether the given concept is a debit concept.
97
+ """
98
+ return bool(_DEBIT_QNAME_PATTERN.match(concept.qname.localName))
99
+
68
100
  @lru_cache(1)
69
101
  def isCorporateForm(self, modelXbrl: ModelXbrl) -> bool:
70
102
  documentTypes = self.getDocumentTypes(modelXbrl)
@@ -72,6 +104,85 @@ class PluginValidationDataExtension(PluginData):
72
104
  return True
73
105
  return False
74
106
 
107
+ def isCorporateReport(self, modelXbrl: ModelXbrl) -> bool:
108
+ return self.jpcrpNamespace in modelXbrl.namespaceDocs
109
+
110
+ def isStockForm(self, modelXbrl: ModelXbrl) -> bool:
111
+ documentTypes = self.getDocumentTypes(modelXbrl)
112
+ return any(documentType == form.value for form in FormType if form.isStockReport for documentType in documentTypes)
113
+
114
+ def getBalanceSheets(self, modelXbrl: ModelXbrl, statement: Statement) -> list[BalanceSheet]:
115
+ """
116
+ :return: Balance sheet data for each context/unit pairing the given statement.
117
+ """
118
+ balanceSheets: list[BalanceSheet] = []
119
+ if statement.roleUri not in modelXbrl.roleTypes:
120
+ return balanceSheets
121
+ if statement.statementType not in (
122
+ StatementType.BALANCE_SHEET,
123
+ StatementType.STATEMENT_OF_FINANCIAL_POSITION
124
+ ):
125
+ return balanceSheets
126
+
127
+ relSet = modelXbrl.relationshipSet(
128
+ tuple(LinkbaseType.CALCULATION.getArcroles()),
129
+ linkrole=statement.roleUri
130
+ )
131
+ rootConcepts = relSet.rootConcepts
132
+ if len(rootConcepts) == 0:
133
+ return balanceSheets
134
+
135
+ # GFM 1.2.7 and 1.2.10 asserts no duplicate contexts and units, respectively,
136
+ # so context and unit IDs can be used as a key.
137
+ factsByContextIdAndUnitId = self.getFactsByContextAndUnit(
138
+ modelXbrl,
139
+ attrgetter("id"),
140
+ attrgetter("id"),
141
+ tuple(concept.qname for concept in rootConcepts)
142
+ )
143
+
144
+ for (contextId, unitId), facts in factsByContextIdAndUnitId.items():
145
+ if not self._contextMatchesStatement(modelXbrl, contextId, statement):
146
+ continue
147
+ assetSum = Decimal(0)
148
+ liabilitiesAndEquitySum = Decimal(0)
149
+ for fact in facts:
150
+ if isinstance(fact.xValue, float):
151
+ value = Decimal(fact.xValue)
152
+ else:
153
+ value = cast(Decimal, fact.xValue)
154
+ if self._isDebitConcept(fact.concept):
155
+ liabilitiesAndEquitySum += value
156
+ else:
157
+ assetSum += value
158
+ balanceSheets.append(
159
+ BalanceSheet(
160
+ assetsTotal=assetSum,
161
+ contextId=str(contextId),
162
+ facts=facts,
163
+ liabilitiesAndEquityTotal=liabilitiesAndEquitySum,
164
+ unitId=str(unitId),
165
+ )
166
+ )
167
+ return balanceSheets
168
+
169
+ @lru_cache(1)
170
+ def getStatementInstance(self, modelXbrl: ModelXbrl, statement: Statement) -> StatementInstance | None:
171
+ if statement.roleUri not in modelXbrl.roleTypes:
172
+ return None
173
+ return StatementInstance(
174
+ balanceSheets=self.getBalanceSheets(modelXbrl, statement),
175
+ statement=statement,
176
+ )
177
+
178
+ @lru_cache(1)
179
+ def getStatementInstances(self, modelXbrl: ModelXbrl) -> list[StatementInstance]:
180
+ return [
181
+ statementInstance
182
+ for statement in STATEMENTS
183
+ if (statementInstance := self.getStatementInstance(modelXbrl, statement)) is not None
184
+ ]
185
+
75
186
  @lru_cache(1)
76
187
  def getDeduplicatedFacts(self, modelXbrl: ModelXbrl) -> list[ModelFact]:
77
188
  return getDeduplicatedFacts(modelXbrl, DeduplicationType.CONSISTENT_PAIRS)
@@ -85,6 +196,24 @@ class PluginValidationDataExtension(PluginData):
85
196
  documentTypes.add(fact.textValue)
86
197
  return documentTypes
87
198
 
199
+ def getFactsByContextAndUnit(
200
+ self, modelXbrl: ModelXbrl,
201
+ getContextKey: Callable[[ModelContext], Hashable],
202
+ getUnitKey: Callable[[ModelUnit], Hashable],
203
+ qnames: tuple[QName, ...] | None = None,
204
+ ) -> dict[tuple[Hashable, Hashable], list[ModelFact]]:
205
+ deduplicatedFacts = self.getDeduplicatedFacts(modelXbrl)
206
+ getFactsByContextAndUnit = defaultdict(list)
207
+ for fact in deduplicatedFacts:
208
+ if qnames is not None and fact.qname not in qnames:
209
+ continue
210
+ if fact.context is None or fact.unit is None:
211
+ continue
212
+ contextKey = getContextKey(fact.context)
213
+ unitKey = getUnitKey(fact.unit)
214
+ getFactsByContextAndUnit[(contextKey, unitKey)].append(fact)
215
+ return dict(getFactsByContextAndUnit)
216
+
88
217
  @lru_cache(1)
89
218
  def getFootnoteLinkElements(self, modelXbrl: ModelXbrl) -> list[ModelObject | LinkPrototype]:
90
219
  # TODO: Consolidate with similar implementations in EDGAR and FERC
@@ -113,8 +242,13 @@ class PluginValidationDataExtension(PluginData):
113
242
  return controllerPluginData.matchManifestInstance(modelXbrl.ixdsDocUrls)
114
243
 
115
244
  def hasValidNonNilFact(self, modelXbrl: ModelXbrl, qname: QName) -> bool:
116
- requiredFacts = modelXbrl.factsByQname.get(qname, set())
117
- return any(fact.xValid >= VALID and not fact.isNil for fact in requiredFacts)
245
+ return any(fact is not None for fact in self.iterValidNonNilFacts(modelXbrl, qname))
118
246
 
119
247
  def isStandardTaxonomyUrl(self, uri: str, modelXbrl: ModelXbrl) -> bool:
120
248
  return modelXbrl.modelManager.disclosureSystem.hrefValidForDisclosureSystem(uri)
249
+
250
+ def iterValidNonNilFacts(self, modelXbrl: ModelXbrl, qname: QName) -> Iterable[ModelFact]:
251
+ facts = modelXbrl.factsByQname.get(qname, set())
252
+ for fact in facts:
253
+ if fact.xValid >= VALID and not fact.isNil:
254
+ yield fact
@@ -0,0 +1,139 @@
1
+ from dataclasses import dataclass
2
+ from decimal import Decimal
3
+ from enum import Enum
4
+
5
+ from regex import regex
6
+
7
+ from arelle.ModelInstanceObject import ModelFact
8
+
9
+ CONSOLIDATED_ROLE_URI_PATTERN = regex.compile(r'.*rol_[\w]*Consolidated')
10
+
11
+ STATEMENT_ROLE_URIS = frozenset([
12
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterPeriodConsolidatedStatementOfComprehensiveIncomeIFRS',
13
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterPeriodConsolidatedStatementOfComprehensiveIncomeSingleStatementIFRS',
14
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterPeriodConsolidatedStatementOfProfitOrLossIFRS',
15
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterPeriodStatementOfComprehensiveIncomeIFRS',
16
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterPeriodStatementOfComprehensiveIncomeSingleStatementIFRS',
17
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterPeriodStatementOfProfitOrLossIFRS',
18
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterlyConsolidatedStatementOfCashFlowsIFRS',
19
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterlyConsolidatedStatementOfChangesInEquityIFRS',
20
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterlyConsolidatedStatementOfComprehensiveIncomeIFRS',
21
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterlyConsolidatedStatementOfComprehensiveIncomeSingleStatementIFRS',
22
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterlyConsolidatedStatementOfFinancialPositionIFRS',
23
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterlyConsolidatedStatementOfProfitOrLossIFRS',
24
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterlyStatementOfCashFlowsIFRS',
25
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterlyStatementOfChangesInEquityIFRS',
26
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterlyStatementOfComprehensiveIncomeIFRS',
27
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterlyStatementOfComprehensiveIncomeSingleStatementIFRS',
28
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterlyStatementOfFinancialPositionIFRS',
29
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedQuarterlyStatementOfProfitOrLossIFRS',
30
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedSemiAnnualConsolidatedStatementOfCashFlowsIFRS',
31
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedSemiAnnualConsolidatedStatementOfChangesInEquityIFRS',
32
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedSemiAnnualConsolidatedStatementOfComprehensiveIncomeIFRS',
33
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedSemiAnnualConsolidatedStatementOfComprehensiveIncomeSingleStatementIFRS',
34
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedSemiAnnualConsolidatedStatementOfFinancialPositionIFRS',
35
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedSemiAnnualConsolidatedStatementOfProfitOrLossIFRS',
36
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedSemiAnnualStatementOfCashFlowsIFRS',
37
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedSemiAnnualStatementOfChangesInEquityIFRS',
38
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedSemiAnnualStatementOfComprehensiveIncomeIFRS',
39
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedSemiAnnualStatementOfComprehensiveIncomeSingleStatementIFRS',
40
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedSemiAnnualStatementOfFinancialPositionIFRS',
41
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedSemiAnnualStatementOfProfitOrLossIFRS',
42
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedYearToQuarterEndConsolidatedStatementOfComprehensiveIncomeIFRS',
43
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedYearToQuarterEndConsolidatedStatementOfComprehensiveIncomeSingleStatementIFRS',
44
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedYearToQuarterEndConsolidatedStatementOfProfitOrLossIFRS',
45
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedYearToQuarterEndStatementOfComprehensiveIncomeIFRS',
46
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedYearToQuarterEndStatementOfComprehensiveIncomeSingleStatementIFRS',
47
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_CondensedYearToQuarterEndStatementOfProfitOrLossIFRS',
48
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_ConsolidatedStatementOfCashFlowsIFRS',
49
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_ConsolidatedStatementOfChangesInEquityIFRS',
50
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_ConsolidatedStatementOfComprehensiveIncomeIFRS',
51
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_ConsolidatedStatementOfComprehensiveIncomeSingleStatementIFRS',
52
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_ConsolidatedStatementOfFinancialPositionIFRS',
53
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_ConsolidatedStatementOfProfitOrLossIFRS',
54
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_StatementOfCashFlowsIFRS',
55
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_StatementOfChangesInEquityIFRS',
56
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_StatementOfComprehensiveIncomeIFRS',
57
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_StatementOfComprehensiveIncomeSingleStatementIFRS',
58
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_StatementOfFinancialPositionIFRS',
59
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_StatementOfProfitOrLossIFRS',
60
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_std_ConsolidatedStatementOfCashFlowsIFRS',
61
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_std_ConsolidatedStatementOfChangesInEquityIFRS',
62
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_std_ConsolidatedStatementOfComprehensiveIncomeIFRS',
63
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_std_ConsolidatedStatementOfFinancialPositionIFRS',
64
+ 'http://disclosure.edinet-fsa.go.jp/role/jpigp/rol_std_ConsolidatedStatementOfProfitOrLossIFRS',
65
+ 'http://disclosure.edinet-fsa.go.jp/role/jppfs/rol_BalanceSheet',
66
+ 'http://disclosure.edinet-fsa.go.jp/role/jppfs/rol_ConsolidatedBalanceSheet',
67
+ 'http://disclosure.edinet-fsa.go.jp/role/jppfs/rol_QuarterlyBalanceSheet',
68
+ 'http://disclosure.edinet-fsa.go.jp/role/jppfs/rol_QuarterlyConsolidatedBalanceSheet',
69
+ 'http://disclosure.edinet-fsa.go.jp/role/jppfs/rol_SemiAnnualBalanceSheet',
70
+ 'http://disclosure.edinet-fsa.go.jp/role/jppfs/rol_SemiAnnualConsolidatedBalanceSheet',
71
+ 'http://disclosure.edinet-fsa.go.jp/role/jppfs/rol_Type1SemiAnnualBalanceSheet',
72
+ 'http://disclosure.edinet-fsa.go.jp/role/jppfs/rol_Type1SemiAnnualConsolidatedBalanceSheet',
73
+ 'http://disclosure.edinet-fsa.go.jp/role/jppfs/rol_std_BalanceSheet',
74
+ 'http://disclosure.edinet-fsa.go.jp/role/jppfs/rol_std_ConsolidatedBalanceSheet',
75
+ 'http://disclosure.edinet-fsa.go.jp/role/jppfs/rol_std_QuarterlyBalanceSheet',
76
+ 'http://disclosure.edinet-fsa.go.jp/role/jppfs/rol_std_QuarterlyConsolidatedBalanceSheet',
77
+ 'http://disclosure.edinet-fsa.go.jp/role/jppfs/rol_std_SemiAnnualBalanceSheet',
78
+ 'http://disclosure.edinet-fsa.go.jp/role/jppfs/rol_std_SemiAnnualConsolidatedBalanceSheet',
79
+ 'http://disclosure.edinet-fsa.go.jp/role/jppfs/rol_std_Type1SemiAnnualBalanceSheet',
80
+ 'http://disclosure.edinet-fsa.go.jp/role/jppfs/rol_std_Type1SemiAnnualConsolidatedBalanceSheet',
81
+ ])
82
+
83
+
84
+ class StatementType(Enum):
85
+ BALANCE_SHEET = 'BalanceSheet'
86
+ CONSOLIDATED_BALANCE_SHEET = 'ConsolidatedBalanceSheetIFRS'
87
+ STATEMENT_OF_CASH_FLOWS = 'StatementOfCashFlowsIFRS'
88
+ STATEMENT_OF_CHANGES_IN_EQUITY = 'StatementOfChangesInEquityIFRS'
89
+ STATEMENT_OF_COMPREHENSIVE_INCOME = 'StatementOfComprehensiveIncomeIFRS'
90
+ STATEMENT_OF_COMPREHENSIVE_INCOME_SINGLE_STATEMENT = 'StatementOfComprehensiveIncomeSingleStatementIFRS'
91
+ STATEMENT_OF_FINANCIAL_POSITION = 'StatementOfFinancialPositionIFRS'
92
+ STATEMENT_OF_PROFIT_OR_LOSS = 'StatementOfProfitOrLossIFRS'
93
+
94
+
95
+ @dataclass(frozen=True)
96
+ class Statement:
97
+ isConsolidated: bool
98
+ roleUri: str
99
+ statementType: StatementType
100
+
101
+
102
+ @dataclass(frozen=True)
103
+ class BalanceSheet:
104
+ assetsTotal: Decimal
105
+ contextId: str
106
+ facts: list[ModelFact]
107
+ liabilitiesAndEquityTotal: Decimal
108
+ unitId: str
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class StatementInstance:
113
+ balanceSheets: list[BalanceSheet]
114
+ statement: Statement
115
+
116
+ def _buildStatements() -> frozenset[Statement]:
117
+ """
118
+ Build a frozenset of Statement objects from the STATEMENT_ROLE_URIS.
119
+ This is done to avoid re-evaluating the set comprehension multiple times.
120
+ """
121
+ statements = []
122
+ for roleUri in STATEMENT_ROLE_URIS:
123
+ isConsolidated = bool(CONSOLIDATED_ROLE_URI_PATTERN.match(roleUri))
124
+ statementType=next(
125
+ statementType
126
+ for statementType in StatementType
127
+ if roleUri.endswith(statementType.value)
128
+ )
129
+ statements.append(
130
+ Statement(
131
+ isConsolidated=isConsolidated,
132
+ roleUri=roleUri,
133
+ statementType=statementType
134
+ )
135
+ )
136
+ return frozenset(statements)
137
+
138
+
139
+ STATEMENTS = _buildStatements()