arelle-release 2.37.13__py3-none-any.whl → 2.37.14__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/ModelTestcaseObject.py +7 -0
- arelle/Validate.py +14 -2
- arelle/ValidateDuplicateFacts.py +4 -10
- arelle/_version.py +2 -2
- arelle/plugin/validate/DBA/PluginValidationDataExtension.py +9 -1
- arelle/plugin/validate/DBA/ValidationPluginExtension.py +4 -2
- arelle/plugin/validate/DBA/rules/fr.py +30 -32
- arelle/plugin/validate/DBA/rules/tm.py +22 -12
- arelle/plugin/validate/NL/PluginValidationDataExtension.py +154 -26
- arelle/plugin/validate/NL/rules/nl_kvk.py +265 -15
- {arelle_release-2.37.13.dist-info → arelle_release-2.37.14.dist-info}/METADATA +1 -1
- {arelle_release-2.37.13.dist-info → arelle_release-2.37.14.dist-info}/RECORD +25 -24
- {arelle_release-2.37.13.dist-info → arelle_release-2.37.14.dist-info}/WHEEL +1 -1
- tests/integration_tests/validation/conformance_suite_configurations/nl_inline_2024.py +11 -15
- tests/resources/conformance_suites/dba/fr/fr7-invalid.xhtml +1 -1
- tests/resources/conformance_suites/dba/fr/fr83-invalid.xbrl +1 -1
- tests/resources/conformance_suites/dba/fr/fr89-testcase.xml +10 -0
- tests/resources/conformance_suites/dba/fr/fr89-valid.xhtml +6816 -0
- tests/resources/conformance_suites/dba/fr/fr91-invalid.xhtml +1 -2
- tests/resources/conformance_suites/dba/tm/tm29-invalid.xhtml +0 -1
- tests/resources/conformance_suites/dba/tm/tm31-invalid.xhtml +10 -1
- tests/resources/conformance_suites/dba/tr/tr06-invalid.xhtml +1 -1
- {arelle_release-2.37.13.dist-info → arelle_release-2.37.14.dist-info}/entry_points.txt +0 -0
- {arelle_release-2.37.13.dist-info → arelle_release-2.37.14.dist-info}/licenses/LICENSE.md +0 -0
- {arelle_release-2.37.13.dist-info → arelle_release-2.37.14.dist-info}/top_level.txt +0 -0
arelle/ModelTestcaseObject.py
CHANGED
|
@@ -353,6 +353,13 @@ class ModelTestcaseVariation(ModelObject):
|
|
|
353
353
|
|
|
354
354
|
return None
|
|
355
355
|
|
|
356
|
+
@property
|
|
357
|
+
def expectedWarnings(self):
|
|
358
|
+
warningElements = XmlUtil.descendants(self, None, "warning")
|
|
359
|
+
if isinstance(warningElements, list) and len(warningElements) > 0:
|
|
360
|
+
return [w.stringValue for w in warningElements]
|
|
361
|
+
return None
|
|
362
|
+
|
|
356
363
|
@property
|
|
357
364
|
def match(self) -> str | None:
|
|
358
365
|
resultElement = XmlUtil.descendant(self, None, "result")
|
arelle/Validate.py
CHANGED
|
@@ -710,6 +710,7 @@ class Validate:
|
|
|
710
710
|
testcaseExpectedErrors = self.modelXbrl.modelManager.formulaOptions.testcaseExpectedErrors or {}
|
|
711
711
|
matchAllExpected = testcaseResultOptions == "match-all" or modelTestcaseVariation.match == 'all'
|
|
712
712
|
expectedReportCount = modelTestcaseVariation.expectedReportCount
|
|
713
|
+
expectedWarnings = modelTestcaseVariation.expectedWarnings if self.modelXbrl.modelManager.formulaOptions.testcaseResultsCaptureWarnings else []
|
|
713
714
|
if expectedReportCount is not None and validateModelCount is not None and expectedReportCount != validateModelCount:
|
|
714
715
|
errors.append("conf:testcaseExpectedReportCountError")
|
|
715
716
|
_blockedMessageCodes = modelTestcaseVariation.blockedMessageCodes # restricts codes examined when provided
|
|
@@ -750,10 +751,21 @@ class Validate:
|
|
|
750
751
|
status = "fail" if numErrors == 0 else "pass"
|
|
751
752
|
elif expected in (None, []) and numErrors == 0:
|
|
752
753
|
status = "pass"
|
|
753
|
-
elif isinstance(expected,(QName,str,dict,list))
|
|
754
|
+
elif isinstance(expected, (QName, str, dict, list)) or expectedWarnings:
|
|
754
755
|
status = "fail"
|
|
755
756
|
_passCount = 0
|
|
756
|
-
|
|
757
|
+
if isinstance(expected, list):
|
|
758
|
+
_expectedList = expected.copy()
|
|
759
|
+
elif not expected:
|
|
760
|
+
_expectedList = []
|
|
761
|
+
else:
|
|
762
|
+
_expectedList = [expected]
|
|
763
|
+
if expectedWarnings:
|
|
764
|
+
_expectedList.extend(expectedWarnings)
|
|
765
|
+
if expectedCount is not None:
|
|
766
|
+
expectedCount += len(expectedWarnings)
|
|
767
|
+
else:
|
|
768
|
+
expectedCount = len(expectedWarnings)
|
|
757
769
|
if not isinstance(expected, list):
|
|
758
770
|
expected = [expected]
|
|
759
771
|
for testErr in _errors:
|
arelle/ValidateDuplicateFacts.py
CHANGED
|
@@ -380,7 +380,7 @@ def areFactsValueEqual(factA: ModelFact, factB: ModelFact) -> bool:
|
|
|
380
380
|
|
|
381
381
|
|
|
382
382
|
def getAspectEqualFacts(
|
|
383
|
-
hashEquivalentFacts: list[ModelFact], includeSingles: bool
|
|
383
|
+
hashEquivalentFacts: list[ModelFact], includeSingles: bool, useLang: bool = True
|
|
384
384
|
) -> Iterator[list[ModelFact]]:
|
|
385
385
|
"""
|
|
386
386
|
Given a list of concept/context/unit hash-equivalent facts,
|
|
@@ -389,20 +389,14 @@ def getAspectEqualFacts(
|
|
|
389
389
|
:param includeSingles: Whether to include lists of single facts (with no duplicates).
|
|
390
390
|
:return: Lists of aspect-equal facts.
|
|
391
391
|
"""
|
|
392
|
-
aspectEqualFacts: dict[
|
|
393
|
-
|
|
394
|
-
] = defaultdict(dict)
|
|
395
|
-
for (
|
|
396
|
-
fact
|
|
397
|
-
) in (
|
|
398
|
-
hashEquivalentFacts
|
|
399
|
-
): # check for hash collision by value checks on context and unit
|
|
392
|
+
aspectEqualFacts: dict[tuple[QName, str | None], dict[tuple[ModelContext, ModelUnit], list[ModelFact]]] = defaultdict(dict)
|
|
393
|
+
for fact in hashEquivalentFacts: # check for hash collision by value checks on context and unit
|
|
400
394
|
contextUnitDict = aspectEqualFacts[
|
|
401
395
|
(
|
|
402
396
|
fact.qname,
|
|
403
397
|
(
|
|
404
398
|
cast(str, fact.xmlLang or "").lower()
|
|
405
|
-
if fact.concept.type.isWgnStringFactType
|
|
399
|
+
if useLang and fact.concept.type.isWgnStringFactType
|
|
406
400
|
else None
|
|
407
401
|
),
|
|
408
402
|
)
|
arelle/_version.py
CHANGED
|
@@ -13,6 +13,7 @@ from arelle.ModelInstanceObject import ModelFact, ModelContext
|
|
|
13
13
|
from arelle.ModelValue import QName
|
|
14
14
|
from arelle.ModelXbrl import ModelXbrl
|
|
15
15
|
from arelle.utils.PluginData import PluginData
|
|
16
|
+
from arelle.XmlValidateConst import VALID
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
@dataclass
|
|
@@ -47,7 +48,6 @@ class PluginValidationDataExtension(PluginData):
|
|
|
47
48
|
consolidatedSoloDimensionQn: QName
|
|
48
49
|
cpr_regex: regex.regex.Pattern[str]
|
|
49
50
|
dateOfApprovalOfAnnualReportQn: QName
|
|
50
|
-
dateOfApprovalOfReportQn: QName
|
|
51
51
|
dateOfExtraordinaryDividendDistributedAfterEndOfReportingPeriod: QName
|
|
52
52
|
dateOfGeneralMeetingQn: QName
|
|
53
53
|
descriptionOfQualificationsOfAssuranceEngagementPerformedQn: QName
|
|
@@ -157,3 +157,11 @@ class PluginValidationDataExtension(PluginData):
|
|
|
157
157
|
contexts.append(context)
|
|
158
158
|
self._reportingPeriodContexts = sorted(contexts, key=lambda c: c.endDatetime)
|
|
159
159
|
return self._reportingPeriodContexts
|
|
160
|
+
|
|
161
|
+
def isAnnualReport(self, modelXbrl: ModelXbrl) -> bool:
|
|
162
|
+
"""
|
|
163
|
+
:return: Return True if Type of Submitted Report value is in the annual report types
|
|
164
|
+
"""
|
|
165
|
+
reportTypeFacts = modelXbrl.factsByQname.get(self.informationOnTypeOfSubmittedReportQn, set())
|
|
166
|
+
filteredReportTypeFacts = [f for f in reportTypeFacts if f.xValid >= VALID and f.xValue in self.annualReportTypes]
|
|
167
|
+
return len(filteredReportTypeFacts) > 0
|
|
@@ -161,7 +161,10 @@ class ValidationPluginExtension(ValidationPlugin):
|
|
|
161
161
|
annualReportTypes=frozenset([
|
|
162
162
|
'Årsrapport',
|
|
163
163
|
'årsrapport',
|
|
164
|
-
'Annual report'
|
|
164
|
+
'Annual report',
|
|
165
|
+
'Likvidationsregnskab',
|
|
166
|
+
'likvidationsregnskab',
|
|
167
|
+
'Liquidation accounts',
|
|
165
168
|
]),
|
|
166
169
|
assetsQn=qname(f'{{{NAMESPACE_FSA}}}Assets'),
|
|
167
170
|
auditedAssuranceReportsDanish='Andre erklæringer med sikkerhed',
|
|
@@ -282,7 +285,6 @@ class ValidationPluginExtension(ValidationPlugin):
|
|
|
282
285
|
consolidatedSoloDimensionQn=qname(f'{{{NAMESPACE_CMN}}}ConsolidatedSoloDimension'),
|
|
283
286
|
cpr_regex=re.compile(r'^([0-2][0-9]|3[0-1])(0[1-9]|1[0-2])[0-9]{2}-[0-9]{4}'),
|
|
284
287
|
dateOfApprovalOfAnnualReportQn=qname(f'{{{NAMESPACE_SOB}}}DateOfApprovalOfAnnualReport'),
|
|
285
|
-
dateOfApprovalOfReportQn=qname(f'{{{NAMESPACE_GSD}}}DateOfApprovalOfReport'),
|
|
286
288
|
dateOfExtraordinaryDividendDistributedAfterEndOfReportingPeriod=qname(f'{{{NAMESPACE_FSA}}}DateOfExtraordinaryDividendDistributedAfterEndOfReportingPeriod'),
|
|
287
289
|
dateOfGeneralMeetingQn=qname(f'{{{NAMESPACE_GSD}}}DateOfGeneralMeeting'),
|
|
288
290
|
declarationObligationQns=frozenset([
|
|
@@ -1266,29 +1266,26 @@ def rule_fr89(
|
|
|
1266
1266
|
Regnskabsklasse D // Reporting class D
|
|
1267
1267
|
Then TypeOfAuditorAssistance should be: Revisionspåtegning // Auditor's report on audited financial statements
|
|
1268
1268
|
"""
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
if auditorFacts is not None:
|
|
1269
|
+
if pluginData.isAnnualReport(val.modelXbrl):
|
|
1270
|
+
auditorFacts = val.modelXbrl.factsByQname.get(pluginData.typeOfAuditorAssistanceQn, set())
|
|
1272
1271
|
for auditorFact in auditorFacts:
|
|
1273
1272
|
if auditorFact.xValid >= VALID and auditorFact.xValue in [
|
|
1274
1273
|
pluginData.auditedFinancialStatementsDanish,
|
|
1275
1274
|
pluginData.auditedFinancialStatementsEnglish
|
|
1276
1275
|
]:
|
|
1277
1276
|
return
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
if facts is not None:
|
|
1277
|
+
classFacts = []
|
|
1278
|
+
facts = val.modelXbrl.factsByQname.get(pluginData.classOfReportingEntityQn, set())
|
|
1281
1279
|
for fact in facts:
|
|
1282
|
-
if fact.xValid >= VALID
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
classFacts.append(fact)
|
|
1280
|
+
if fact.xValid >= VALID and fact.xValue in [
|
|
1281
|
+
pluginData.reportingClassCLargeDanish,
|
|
1282
|
+
pluginData.reportingClassCLargeEnglish,
|
|
1283
|
+
pluginData.reportingClassCMediumDanish,
|
|
1284
|
+
pluginData.reportingClassCMediumEnglish,
|
|
1285
|
+
pluginData.reportingClassDDanish,
|
|
1286
|
+
pluginData.reportingClassDEnglish
|
|
1287
|
+
]:
|
|
1288
|
+
classFacts.append(fact)
|
|
1292
1289
|
if len(classFacts) > 0:
|
|
1293
1290
|
yield Validation.error(
|
|
1294
1291
|
codes='DBA.FR89',
|
|
@@ -1313,22 +1310,23 @@ def rule_fr91(
|
|
|
1313
1310
|
) -> Iterable[Validation]:
|
|
1314
1311
|
"""
|
|
1315
1312
|
DBA.FR91: If the annual report contains information about both the general meeting date
|
|
1316
|
-
(gsd:DateOfGeneralMeeting) and the annual accounts meeting date (gsd:
|
|
1317
|
-
"""
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1313
|
+
(gsd:DateOfGeneralMeeting) and the annual accounts meeting date (gsd:DateOfApprovalOfAnnualReport), the values must be the same.
|
|
1314
|
+
"""
|
|
1315
|
+
if pluginData.isAnnualReport(val.modelXbrl):
|
|
1316
|
+
approvalOfReportFact = None
|
|
1317
|
+
generalMeetingFact = None
|
|
1318
|
+
approvalFacts = (val.modelXbrl.factsByQname.get(pluginData.dateOfApprovalOfAnnualReportQn, set()))
|
|
1319
|
+
if len(approvalFacts) > 0:
|
|
1320
|
+
approvalOfReportFact = next(iter(approvalFacts), None)
|
|
1321
|
+
meetingFacts = val.modelXbrl.factsByQname.get(pluginData.dateOfGeneralMeetingQn, set())
|
|
1322
|
+
if len(meetingFacts) > 0:
|
|
1323
|
+
generalMeetingFact = next(iter(meetingFacts), None)
|
|
1324
|
+
if generalMeetingFact is not None and generalMeetingFact.xValid >= VALID and approvalOfReportFact is not None and approvalOfReportFact.xValid >= VALID and generalMeetingFact.xValue != approvalOfReportFact.xValue:
|
|
1325
|
+
yield Validation.error(
|
|
1326
|
+
codes='DBA.FR91',
|
|
1327
|
+
msg=_("The annual report contains information about both the general meeting date (gsd:DateOfGeneralMeeting) and the annual accounts meeting date (gsd:DateOfApprovalOfAnnualReport), the values must be the same."),
|
|
1328
|
+
modelObject=[generalMeetingFact, approvalOfReportFact]
|
|
1329
|
+
)
|
|
1332
1330
|
|
|
1333
1331
|
|
|
1334
1332
|
@validation(
|
|
@@ -239,16 +239,16 @@ def rule_tm29(
|
|
|
239
239
|
**kwargs: Any,
|
|
240
240
|
) -> Iterable[Validation]:
|
|
241
241
|
"""
|
|
242
|
-
DBA.TM29: Either gsd:DateOfGeneralMeeting or gsd:
|
|
242
|
+
DBA.TM29: Either gsd:DateOfGeneralMeeting or gsd:DateOfApprovalOfAnnualReport must be specified
|
|
243
243
|
"""
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
244
|
+
if pluginData.isAnnualReport(val.modelXbrl):
|
|
245
|
+
meeting_facts = val.modelXbrl.factsByQname.get(pluginData.dateOfGeneralMeetingQn, set())
|
|
246
|
+
approval_facts = val.modelXbrl.factsByQname.get(pluginData.dateOfApprovalOfAnnualReportQn, set())
|
|
247
|
+
if len(meeting_facts) == 0 and len(approval_facts) == 0:
|
|
248
|
+
yield Validation.error(
|
|
249
|
+
'DBA.TM29',
|
|
250
|
+
_('Either DateOfGeneralMeeting or DateOfApprovalOfReport must be tagged in the document.')
|
|
251
|
+
)
|
|
252
252
|
|
|
253
253
|
|
|
254
254
|
@validation(
|
|
@@ -276,9 +276,19 @@ def rule_tm31(
|
|
|
276
276
|
**kwargs: Any,
|
|
277
277
|
) -> Iterable[Validation]:
|
|
278
278
|
"""
|
|
279
|
-
DBA.TM31: gsd:
|
|
280
|
-
"""
|
|
281
|
-
|
|
279
|
+
DBA.TM31: gsd:DateOfApprovalOfAnnualReport must only be tagged once if tagged
|
|
280
|
+
"""
|
|
281
|
+
if pluginData.isAnnualReport(val.modelXbrl):
|
|
282
|
+
dateFacts = val.modelXbrl.factsByQname.get(pluginData.dateOfApprovalOfAnnualReportQn, set())
|
|
283
|
+
if len(dateFacts) > 1:
|
|
284
|
+
yield Validation.error(
|
|
285
|
+
'DBA.TM31',
|
|
286
|
+
_('{} must only be tagged once. {} facts were found.').format(
|
|
287
|
+
pluginData.dateOfApprovalOfAnnualReportQn.localName,
|
|
288
|
+
len(dateFacts)
|
|
289
|
+
),
|
|
290
|
+
modelObject=dateFacts
|
|
291
|
+
)
|
|
282
292
|
|
|
283
293
|
|
|
284
294
|
@validation(
|
|
@@ -6,20 +6,23 @@ from __future__ import annotations
|
|
|
6
6
|
from collections import defaultdict
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from functools import lru_cache
|
|
9
|
+
from pathlib import Path
|
|
9
10
|
from typing import Any, TYPE_CHECKING, cast
|
|
10
11
|
|
|
11
12
|
import regex as re
|
|
12
13
|
from lxml.etree import _Comment, _ElementTree, _Entity, _ProcessingInstruction
|
|
13
14
|
|
|
14
15
|
from arelle.FunctionIxt import ixtNamespaces
|
|
15
|
-
from arelle.ModelInstanceObject import ModelContext, ModelFact, ModelInlineFootnote, ModelUnit
|
|
16
|
+
from arelle.ModelInstanceObject import ModelContext, ModelFact, ModelInlineFootnote, ModelUnit, ModelInlineFact
|
|
16
17
|
from arelle.ModelObject import ModelObject
|
|
17
18
|
from arelle.ModelValue import QName
|
|
18
19
|
from arelle.ModelXbrl import ModelXbrl
|
|
19
20
|
from arelle.typing import assert_type
|
|
20
21
|
from arelle.utils.PluginData import PluginData
|
|
21
22
|
from arelle.utils.validate.ValidationUtil import etreeIterWithDepth
|
|
23
|
+
from arelle.XbrlConst import ixbrl11
|
|
22
24
|
from arelle.XmlValidate import lexicalPatterns
|
|
25
|
+
from arelle.XmlValidateConst import VALID
|
|
23
26
|
|
|
24
27
|
XBRLI_IDENTIFIER_PATTERN = re.compile(r"^(?!00)\d{8}$")
|
|
25
28
|
XBRLI_IDENTIFIER_SCHEMA = 'http://www.kvk.nl/kvk-id'
|
|
@@ -29,6 +32,25 @@ DISALLOWED_IXT_NAMESPACES = frozenset((
|
|
|
29
32
|
ixtNamespaces["ixt v2"],
|
|
30
33
|
ixtNamespaces["ixt v3"],
|
|
31
34
|
))
|
|
35
|
+
UNTRANSFORMABLE_TYPES = frozenset((
|
|
36
|
+
"anyURI",
|
|
37
|
+
"base64Binary",
|
|
38
|
+
"duration",
|
|
39
|
+
"hexBinary",
|
|
40
|
+
"NOTATION",
|
|
41
|
+
"QName",
|
|
42
|
+
"time",
|
|
43
|
+
"token",
|
|
44
|
+
"language",
|
|
45
|
+
))
|
|
46
|
+
STYLE_IX_HIDDEN_PATTERN = re.compile(r"(.*[^\w]|^)ix-hidden\s*:\s*([\w.-]+).*")
|
|
47
|
+
|
|
48
|
+
ALLOWABLE_LANGUAGES = frozenset((
|
|
49
|
+
'nl',
|
|
50
|
+
'en',
|
|
51
|
+
'de',
|
|
52
|
+
'fr'
|
|
53
|
+
))
|
|
32
54
|
|
|
33
55
|
@dataclass(frozen=True)
|
|
34
56
|
class ContextData:
|
|
@@ -38,10 +60,18 @@ class ContextData:
|
|
|
38
60
|
contextsWithSegments: list[ModelContext | None]
|
|
39
61
|
|
|
40
62
|
@dataclass(frozen=True)
|
|
41
|
-
class
|
|
42
|
-
|
|
63
|
+
class HiddenElementsData:
|
|
64
|
+
eligibleForTransformHiddenFacts: set[ModelInlineFact]
|
|
65
|
+
hiddenFactsOutsideHiddenSection: set[ModelInlineFact]
|
|
66
|
+
requiredToDisplayFacts: set[ModelInlineFact]
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class InlineHTMLData:
|
|
43
70
|
noMatchLangFootnotes: set[ModelInlineFootnote]
|
|
44
71
|
orphanedFootnotes: set[ModelInlineFootnote]
|
|
72
|
+
tupleElements: set[tuple[Any]]
|
|
73
|
+
factLangFootnotes: dict[ModelInlineFootnote, set[str]]
|
|
74
|
+
fractionElements: set[Any]
|
|
45
75
|
|
|
46
76
|
@dataclass
|
|
47
77
|
class PluginValidationDataExtension(PluginData):
|
|
@@ -103,33 +133,83 @@ class PluginValidationDataExtension(PluginData):
|
|
|
103
133
|
)
|
|
104
134
|
|
|
105
135
|
@lru_cache(1)
|
|
106
|
-
def
|
|
136
|
+
def checkHiddenElements(self, modelXbrl: ModelXbrl) -> HiddenElementsData:
|
|
137
|
+
eligibleForTransformHiddenFacts = set()
|
|
138
|
+
hiddenEltIds = {}
|
|
139
|
+
hiddenFactsOutsideHiddenSection = set()
|
|
140
|
+
presentedHiddenEltIds = defaultdict(list)
|
|
141
|
+
requiredToDisplayFacts = set()
|
|
142
|
+
for ixdsHtmlRootElt in modelXbrl.ixdsHtmlElements:
|
|
143
|
+
ixNStag = getattr(ixdsHtmlRootElt.modelDocument, "ixNStag", ixbrl11)
|
|
144
|
+
for ixHiddenElt in ixdsHtmlRootElt.iterdescendants(tag=ixNStag + "hidden"):
|
|
145
|
+
for tag in (ixNStag + "nonNumeric", ixNStag+"nonFraction"):
|
|
146
|
+
for ixElt in ixHiddenElt.iterdescendants(tag=tag):
|
|
147
|
+
if getattr(ixElt, "xValid", 0) >= VALID:
|
|
148
|
+
if ixElt.concept.baseXsdType not in UNTRANSFORMABLE_TYPES and not ixElt.isNil:
|
|
149
|
+
eligibleForTransformHiddenFacts.add(ixElt)
|
|
150
|
+
if ixElt.id:
|
|
151
|
+
hiddenEltIds[ixElt.id] = ixElt
|
|
152
|
+
for ixdsHtmlRootElt in modelXbrl.ixdsHtmlElements:
|
|
153
|
+
for ixElt in ixdsHtmlRootElt.getroottree().iterfind(".//{http://www.w3.org/1999/xhtml}*[@style]"):
|
|
154
|
+
styleValue = ixElt.get("style","")
|
|
155
|
+
hiddenFactRefMatch = STYLE_IX_HIDDEN_PATTERN.match(styleValue)
|
|
156
|
+
if hiddenFactRefMatch:
|
|
157
|
+
hiddenFactRef = hiddenFactRefMatch.group(2)
|
|
158
|
+
if hiddenFactRef not in hiddenEltIds:
|
|
159
|
+
hiddenFactsOutsideHiddenSection.add(ixElt)
|
|
160
|
+
else:
|
|
161
|
+
presentedHiddenEltIds[hiddenFactRef].append(ixElt)
|
|
162
|
+
for hiddenEltId, ixElt in hiddenEltIds.items():
|
|
163
|
+
if (hiddenEltId not in presentedHiddenEltIds and
|
|
164
|
+
getattr(ixElt, "xValid", 0) >= VALID and # may not be validated
|
|
165
|
+
(ixElt.concept.baseXsdType in UNTRANSFORMABLE_TYPES or ixElt.isNil)):
|
|
166
|
+
requiredToDisplayFacts.add(ixElt)
|
|
167
|
+
return HiddenElementsData(
|
|
168
|
+
eligibleForTransformHiddenFacts=eligibleForTransformHiddenFacts,
|
|
169
|
+
hiddenFactsOutsideHiddenSection=hiddenFactsOutsideHiddenSection,
|
|
170
|
+
requiredToDisplayFacts=requiredToDisplayFacts,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
@lru_cache(1)
|
|
174
|
+
def checkInlineHTMLElements(self, modelXbrl: ModelXbrl) -> InlineHTMLData:
|
|
107
175
|
factLangs = self.factLangs(modelXbrl)
|
|
108
176
|
footnotesRelationshipSet = modelXbrl.relationshipSet("XBRL-footnotes")
|
|
109
177
|
factLangFootnotes = defaultdict(set)
|
|
110
|
-
|
|
178
|
+
fractionElements = set()
|
|
111
179
|
noMatchLangFootnotes = set()
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if elt.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
180
|
+
tupleElements = set()
|
|
181
|
+
orphanedFootnotes = set()
|
|
182
|
+
for ixdsHtmlRootElt in modelXbrl.ixdsHtmlElements:
|
|
183
|
+
ixNStag = getattr(ixdsHtmlRootElt.modelDocument, "ixNStag", ixbrl11)
|
|
184
|
+
ixTupleTag = ixNStag + "tuple"
|
|
185
|
+
ixFractionTag = ixNStag + "fraction"
|
|
186
|
+
for elts in modelXbrl.ixdsEltById.values(): # type: ignore[attr-defined]
|
|
187
|
+
for elt in elts:
|
|
188
|
+
if isinstance(elt, ModelInlineFootnote):
|
|
189
|
+
if elt.textValue is not None:
|
|
190
|
+
if not any(isinstance(rel.fromModelObject, ModelFact)
|
|
191
|
+
for rel in footnotesRelationshipSet.toModelObject(elt)):
|
|
192
|
+
orphanedFootnotes.add(elt)
|
|
193
|
+
if elt.xmlLang not in factLangs:
|
|
194
|
+
noMatchLangFootnotes.add(elt)
|
|
195
|
+
if elt.xmlLang is not None:
|
|
196
|
+
for rel in footnotesRelationshipSet.toModelObject(elt):
|
|
197
|
+
if rel.fromModelObject is not None:
|
|
198
|
+
fromObj = cast(ModelObject, rel.fromModelObject)
|
|
199
|
+
lang = cast(str, elt.xmlLang)
|
|
200
|
+
factLangFootnotes[fromObj].add(lang)
|
|
201
|
+
if elt.tag == ixTupleTag:
|
|
202
|
+
tupleElements.add(elt)
|
|
203
|
+
if elt.tag == ixFractionTag:
|
|
204
|
+
fractionElements.add(elt)
|
|
127
205
|
factLangFootnotes.default_factory = None
|
|
128
206
|
assert_type(factLangFootnotes, defaultdict[ModelObject, set[str]])
|
|
129
|
-
return
|
|
130
|
-
factLangFootnotes=cast(dict[
|
|
207
|
+
return InlineHTMLData(
|
|
208
|
+
factLangFootnotes=cast(dict[ModelInlineFootnote, set[str]], factLangFootnotes),
|
|
209
|
+
fractionElements=fractionElements,
|
|
131
210
|
noMatchLangFootnotes=noMatchLangFootnotes,
|
|
132
211
|
orphanedFootnotes=orphanedFootnotes,
|
|
212
|
+
tupleElements=tupleElements,
|
|
133
213
|
)
|
|
134
214
|
|
|
135
215
|
@lru_cache(1)
|
|
@@ -164,14 +244,57 @@ class PluginValidationDataExtension(PluginData):
|
|
|
164
244
|
def getContextsWithSegments(self, modelXbrl: ModelXbrl) -> list[ModelContext | None]:
|
|
165
245
|
return self.checkContexts(modelXbrl).contextsWithSegments
|
|
166
246
|
|
|
167
|
-
def
|
|
168
|
-
return self.
|
|
247
|
+
def getEligibleForTransformHiddenFacts(self, modelXbrl: ModelXbrl) -> set[ModelInlineFact]:
|
|
248
|
+
return self.checkHiddenElements(modelXbrl).eligibleForTransformHiddenFacts
|
|
249
|
+
|
|
250
|
+
def getFactLangFootnotes(self, modelXbrl: ModelXbrl) -> dict[ModelInlineFootnote, set[str]]:
|
|
251
|
+
return self.checkInlineHTMLElements(modelXbrl).factLangFootnotes
|
|
252
|
+
|
|
253
|
+
def getFractionElements(self, modelXbrl: ModelXbrl) -> set[Any]:
|
|
254
|
+
return self.checkInlineHTMLElements(modelXbrl).fractionElements
|
|
255
|
+
|
|
256
|
+
def getHiddenFactsOutsideHiddenSection(self, modelXbrl: ModelXbrl) -> set[ModelInlineFact]:
|
|
257
|
+
return self.checkHiddenElements(modelXbrl).hiddenFactsOutsideHiddenSection
|
|
258
|
+
|
|
259
|
+
@lru_cache(1)
|
|
260
|
+
def getFilenameAllowedCharactersPattern(self) -> re.Pattern[str]:
|
|
261
|
+
return re.compile(
|
|
262
|
+
r"^[\w\.-]*$",
|
|
263
|
+
flags=re.ASCII
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
@lru_cache(1)
|
|
267
|
+
def getFilenameFormatPattern(self) -> re.Pattern[str]:
|
|
268
|
+
return re.compile(
|
|
269
|
+
r"^(?<base>[^-]*)"
|
|
270
|
+
r"-(?<year>\d{4})-(?<month>0[1-9]|1[012])-(?<day>0?[1-9]|[12][0-9]|3[01])"
|
|
271
|
+
r"-(?<lang>[^-]*)"
|
|
272
|
+
r"\.(?<extension>html|htm|xhtml)$",
|
|
273
|
+
flags=re.ASCII
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
@lru_cache(1)
|
|
277
|
+
def getFilenameParts(self, filename: str) -> dict[str, Any] | None:
|
|
278
|
+
match = self.getFilenameFormatPattern().match(filename)
|
|
279
|
+
if match:
|
|
280
|
+
return match.groupdict()
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
@lru_cache(1)
|
|
284
|
+
def getIxdsDocBasenames(self, modelXbrl: ModelXbrl) -> set[str]:
|
|
285
|
+
return set(Path(url).name for url in modelXbrl.ixdsDocUrls)
|
|
169
286
|
|
|
170
287
|
def getNoMatchLangFootnotes(self, modelXbrl: ModelXbrl) -> set[ModelInlineFootnote]:
|
|
171
|
-
return self.
|
|
288
|
+
return self.checkInlineHTMLElements(modelXbrl).noMatchLangFootnotes
|
|
172
289
|
|
|
173
290
|
def getOrphanedFootnotes(self, modelXbrl: ModelXbrl) -> set[ModelInlineFootnote]:
|
|
174
|
-
return self.
|
|
291
|
+
return self.checkInlineHTMLElements(modelXbrl).orphanedFootnotes
|
|
292
|
+
|
|
293
|
+
def getRequiredToDisplayFacts(self, modelXbrl: ModelXbrl) -> set[ModelInlineFact]:
|
|
294
|
+
return self.checkHiddenElements(modelXbrl).requiredToDisplayFacts
|
|
295
|
+
|
|
296
|
+
def getTupleElements(self, modelXbrl: ModelXbrl) -> set[tuple[Any]]:
|
|
297
|
+
return self.checkInlineHTMLElements(modelXbrl).tupleElements
|
|
175
298
|
|
|
176
299
|
@lru_cache(1)
|
|
177
300
|
def getReportXmlLang(self, modelXbrl: ModelXbrl) -> str | None:
|
|
@@ -188,6 +311,11 @@ class PluginValidationDataExtension(PluginData):
|
|
|
188
311
|
firstRootmostXmlLangDepth = depth
|
|
189
312
|
return reportXmlLang
|
|
190
313
|
|
|
314
|
+
@lru_cache(1)
|
|
315
|
+
def isFilenameValidCharacters(self, filename: str) -> bool:
|
|
316
|
+
match = self.getFilenameAllowedCharactersPattern().match(filename)
|
|
317
|
+
return match is not None
|
|
318
|
+
|
|
191
319
|
@lru_cache(1)
|
|
192
320
|
def unitsByDocument(self, modelXbrl: ModelXbrl) -> dict[str, list[ModelUnit]]:
|
|
193
321
|
unitsByDocument = defaultdict(list)
|