arelle-release 2.37.12__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/CntlrCmdLine.py +1 -66
- arelle/ModelDtsObject.py +1 -1
- arelle/ModelInstanceObject.py +9 -0
- arelle/ModelTestcaseObject.py +8 -1
- arelle/Validate.py +58 -23
- arelle/ValidateDuplicateFacts.py +4 -10
- arelle/_version.py +2 -2
- arelle/formula/XPathParser.py +9 -3
- arelle/packages/report/ReportPackage.py +7 -3
- arelle/packages/report/ReportPackageValidator.py +4 -3
- arelle/plugin/inlineXbrlDocumentSet.py +1 -1
- 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/ESEF/ESEF_2021/ValidateXbrlFinally.py +3 -3
- arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +4 -5
- arelle/plugin/validate/NL/PluginValidationDataExtension.py +163 -32
- arelle/plugin/validate/NL/__init__.py +0 -11
- arelle/plugin/validate/NL/rules/nl_kvk.py +322 -12
- arelle/plugin/validate/ROS/rules/ros.py +1 -1
- arelle/typing.py +8 -1
- arelle/utils/EntryPointDetection.py +73 -0
- {arelle_release-2.37.12.dist-info → arelle_release-2.37.14.dist-info}/METADATA +1 -1
- {arelle_release-2.37.12.dist-info → arelle_release-2.37.14.dist-info}/RECORD +40 -38
- {arelle_release-2.37.12.dist-info → arelle_release-2.37.14.dist-info}/WHEEL +1 -1
- tests/integration_tests/ui_tests/ArelleGUITest/ArelleGUITest/ArelleGUITest.csproj +1 -1
- tests/integration_tests/validation/conformance_suite_configurations/nl_inline_2024.py +14 -21
- tests/integration_tests/validation/conformance_suite_configurations/xbrl_report_packages_1_0.py +3 -1
- 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.12.dist-info → arelle_release-2.37.14.dist-info}/entry_points.txt +0 -0
- {arelle_release-2.37.12.dist-info → arelle_release-2.37.14.dist-info}/licenses/LICENSE.md +0 -0
- {arelle_release-2.37.12.dist-info → arelle_release-2.37.14.dist-info}/top_level.txt +0 -0
arelle/CntlrCmdLine.py
CHANGED
|
@@ -56,6 +56,7 @@ from arelle.RuntimeOptions import RuntimeOptions, RuntimeOptionsException
|
|
|
56
56
|
from arelle.SocketUtils import INTERNET_CONNECTIVITY, OFFLINE
|
|
57
57
|
from arelle.SystemInfo import PlatformOS, getSystemInfo, getSystemWordSize, hasWebServer, isCGI, isGAE
|
|
58
58
|
from arelle.typing import TypeGetText
|
|
59
|
+
from arelle.utils.EntryPointDetection import filesourceEntrypointFiles
|
|
59
60
|
from arelle.UrlUtil import isHttpUrl
|
|
60
61
|
from arelle.ValidateXbrlDTS import ValidateBaseTaxonomiesMode
|
|
61
62
|
from arelle.WebCache import proxyTuple
|
|
@@ -585,72 +586,6 @@ def configAndRunCntlr(options, arellePluginModules):
|
|
|
585
586
|
return cntlr
|
|
586
587
|
|
|
587
588
|
|
|
588
|
-
def filesourceEntrypointFiles(filesource, entrypointFiles=None, inlineOnly=False):
|
|
589
|
-
if entrypointFiles is None:
|
|
590
|
-
entrypointFiles = []
|
|
591
|
-
if filesource.isArchive:
|
|
592
|
-
if filesource.isTaxonomyPackage: # if archive is also a taxonomy package, activate mappings
|
|
593
|
-
filesource.loadTaxonomyPackageMappings()
|
|
594
|
-
# HF note: a web api request to load a specific file from archive is ignored, is this right?
|
|
595
|
-
del entrypointFiles[:] # clear out archive from entrypointFiles
|
|
596
|
-
if reportPackage := filesource.reportPackage:
|
|
597
|
-
assert isinstance(filesource.basefile, str)
|
|
598
|
-
for report in reportPackage.reports or []:
|
|
599
|
-
if report.isInline:
|
|
600
|
-
reportEntries = [{"file": f} for f in report.fullPathFiles]
|
|
601
|
-
ixdsDiscovered = False
|
|
602
|
-
for pluginXbrlMethod in PluginManager.pluginClassMethods("InlineDocumentSet.Discovery"):
|
|
603
|
-
pluginXbrlMethod(filesource, reportEntries)
|
|
604
|
-
ixdsDiscovered = True
|
|
605
|
-
if not ixdsDiscovered and len(reportEntries) > 1:
|
|
606
|
-
raise RuntimeError(_("Loading error. Inline document set encountered. Enable 'InlineXbrlDocumentSet' plug-in to load this filing: {0}").format(filesource.url))
|
|
607
|
-
entrypointFiles.extend(reportEntries)
|
|
608
|
-
elif not inlineOnly:
|
|
609
|
-
entrypointFiles.append({"file": report.fullPathPrimary})
|
|
610
|
-
else:
|
|
611
|
-
# attempt to find inline XBRL files before instance files, .xhtml before probing others (ESMA)
|
|
612
|
-
urlsByType = {}
|
|
613
|
-
for _archiveFile in (filesource.dir or ()): # .dir might be none if IOerror
|
|
614
|
-
filesource.select(_archiveFile)
|
|
615
|
-
identifiedType = ModelDocument.Type.identify(filesource, filesource.url)
|
|
616
|
-
if identifiedType in (ModelDocument.Type.INSTANCE, ModelDocument.Type.INLINEXBRL, ModelDocument.Type.HTML):
|
|
617
|
-
urlsByType.setdefault(identifiedType, []).append(filesource.url)
|
|
618
|
-
# use inline instances, if any, else non-inline instances
|
|
619
|
-
for identifiedType in ((ModelDocument.Type.INLINEXBRL,) if inlineOnly else (ModelDocument.Type.INLINEXBRL, ModelDocument.Type.INSTANCE)):
|
|
620
|
-
for url in urlsByType.get(identifiedType, []):
|
|
621
|
-
entrypointFiles.append({"file":url})
|
|
622
|
-
if entrypointFiles:
|
|
623
|
-
if identifiedType == ModelDocument.Type.INLINEXBRL:
|
|
624
|
-
for pluginXbrlMethod in PluginManager.pluginClassMethods("InlineDocumentSet.Discovery"):
|
|
625
|
-
pluginXbrlMethod(filesource, entrypointFiles) # group into IXDS if plugin feature is available
|
|
626
|
-
break # found inline (or non-inline) entrypoint files, don't look for any other type
|
|
627
|
-
# for ESEF non-consolidated xhtml documents accept an xhtml entry point
|
|
628
|
-
if not entrypointFiles and not inlineOnly:
|
|
629
|
-
for url in urlsByType.get(ModelDocument.Type.HTML, []):
|
|
630
|
-
entrypointFiles.append({"file":url})
|
|
631
|
-
if not entrypointFiles and filesource.taxonomyPackage is not None:
|
|
632
|
-
for packageEntry in filesource.taxonomyPackage.get('entryPoints', {}).values():
|
|
633
|
-
for _resolvedUrl, remappedUrl, _closest in packageEntry:
|
|
634
|
-
entrypointFiles.append({"file": remappedUrl})
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
elif os.path.isdir(filesource.url):
|
|
638
|
-
del entrypointFiles[:] # clear list
|
|
639
|
-
hasInline = False
|
|
640
|
-
for _file in os.listdir(filesource.url):
|
|
641
|
-
_path = os.path.join(filesource.url, _file)
|
|
642
|
-
if os.path.isfile(_path):
|
|
643
|
-
identifiedType = ModelDocument.Type.identify(filesource, _path)
|
|
644
|
-
if identifiedType == ModelDocument.Type.INLINEXBRL:
|
|
645
|
-
hasInline = True
|
|
646
|
-
if identifiedType in (ModelDocument.Type.INSTANCE, ModelDocument.Type.INLINEXBRL):
|
|
647
|
-
entrypointFiles.append({"file":_path})
|
|
648
|
-
if hasInline: # group into IXDS if plugin feature is available
|
|
649
|
-
for pluginXbrlMethod in PluginManager.pluginClassMethods("InlineDocumentSet.Discovery"):
|
|
650
|
-
pluginXbrlMethod(filesource, entrypointFiles)
|
|
651
|
-
|
|
652
|
-
return entrypointFiles
|
|
653
|
-
|
|
654
589
|
class ParserForDynamicPlugins:
|
|
655
590
|
def __init__(self, options):
|
|
656
591
|
self._long_opt = {}
|
arelle/ModelDtsObject.py
CHANGED
|
@@ -1846,7 +1846,7 @@ class ModelRelationship(ModelObject):
|
|
|
1846
1846
|
|
|
1847
1847
|
@property
|
|
1848
1848
|
def orderDecimal(self):
|
|
1849
|
-
"""(decimal) -- Value of xlink:order attribute, NaN if not
|
|
1849
|
+
"""(decimal) -- Value of xlink:order attribute, NaN if not convertible to float, or None if not specified"""
|
|
1850
1850
|
try:
|
|
1851
1851
|
return decimal.Decimal(self.order)
|
|
1852
1852
|
except decimal.InvalidOperation:
|
arelle/ModelInstanceObject.py
CHANGED
|
@@ -795,6 +795,15 @@ class ModelInlineFact(ModelInlineValueObject, ModelFact):
|
|
|
795
795
|
for inline root element, the xbrli:xbrl element is substituted for by the inline root element"""
|
|
796
796
|
return getattr(self, "_ixFactParent") # set by ModelDocument locateFactInTuple for the inline target's root element
|
|
797
797
|
|
|
798
|
+
@property
|
|
799
|
+
def isEscaped(self):
|
|
800
|
+
"""(bool) -- if true, the fact is escaped"""
|
|
801
|
+
try:
|
|
802
|
+
return self._isEscaped
|
|
803
|
+
except AttributeError:
|
|
804
|
+
self._isEscaped = self.get("escape") in ("true", "1")
|
|
805
|
+
return self._isEscaped
|
|
806
|
+
|
|
798
807
|
def ixIter(self, childOnly=False):
|
|
799
808
|
"""(ModelObject) -- child elements (tuple facts) of the inline target instance document"""
|
|
800
809
|
for fact in self.modelTupleFacts:
|
arelle/ModelTestcaseObject.py
CHANGED
|
@@ -158,7 +158,7 @@ class ModelTestcaseVariation(ModelObject):
|
|
|
158
158
|
except AttributeError:
|
|
159
159
|
self._dataUris = defaultdict(list) # may contain instances, schemas, linkbases
|
|
160
160
|
for dataElement in XmlUtil.descendants(self, None, ("data", "input")):
|
|
161
|
-
for elt in XmlUtil.descendants(dataElement, None, ("xsd", "schema", "linkbase", "instance")):
|
|
161
|
+
for elt in XmlUtil.descendants(dataElement, None, ("xsd", "schema", "linkbase", "instance", "taxonomyPackage")):
|
|
162
162
|
self._dataUris["schema" if elt.localName == "xsd" else elt.localName].append(elt.textValue.strip())
|
|
163
163
|
return self._dataUris
|
|
164
164
|
|
|
@@ -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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'''
|
|
2
2
|
See COPYRIGHT.md for copyright information.
|
|
3
3
|
'''
|
|
4
|
+
import bisect
|
|
4
5
|
import fnmatch
|
|
5
6
|
import os, sys, traceback, logging
|
|
6
7
|
import time
|
|
@@ -25,6 +26,7 @@ from arelle.PluginManager import pluginClassMethods
|
|
|
25
26
|
from arelle.packages.report.DetectReportPackage import isReportPackageExtension
|
|
26
27
|
from arelle.packages.report.ReportPackageValidator import ReportPackageValidator
|
|
27
28
|
from arelle.rendering import RenderingEvaluator
|
|
29
|
+
from arelle.utils.EntryPointDetection import filesourceEntrypointFiles
|
|
28
30
|
from arelle.XmlUtil import collapseWhitespace, xmlstring
|
|
29
31
|
|
|
30
32
|
def validate(modelXbrl):
|
|
@@ -166,7 +168,6 @@ class Validate:
|
|
|
166
168
|
filesource = FileSource.openFileSource(rssItemUrl, self.modelXbrl.modelManager.cntlr)
|
|
167
169
|
if filesource and not filesource.selection and filesource.isArchive:
|
|
168
170
|
try:
|
|
169
|
-
from arelle.CntlrCmdLine import filesourceEntrypointFiles
|
|
170
171
|
entrypoints = filesourceEntrypointFiles(filesource)
|
|
171
172
|
if entrypoints:
|
|
172
173
|
# resolve an IXDS in entrypoints
|
|
@@ -394,13 +395,12 @@ class Validate:
|
|
|
394
395
|
filesource.loadTaxonomyPackageMappings(errors=preLoadingErrors, expectTaxonomyPackage=expectTaxonomyPackage)
|
|
395
396
|
filesource.select(None) # must select loadable reports (not the taxonomy package itself)
|
|
396
397
|
elif not filesource.isReportPackage:
|
|
397
|
-
from arelle.CntlrCmdLine import filesourceEntrypointFiles
|
|
398
398
|
entrypoints = filesourceEntrypointFiles(filesource)
|
|
399
399
|
if entrypoints:
|
|
400
400
|
# resolve an IXDS in entrypoints
|
|
401
401
|
for pluginXbrlMethod in pluginClassMethods("ModelTestcaseVariation.ArchiveIxds"):
|
|
402
402
|
pluginXbrlMethod(self, filesource,entrypoints)
|
|
403
|
-
filesource.select(entrypoints[0].get("file", None)
|
|
403
|
+
filesource.select(entrypoints[0].get("file", None))
|
|
404
404
|
except Exception as err:
|
|
405
405
|
self.modelXbrl.error("exception:" + type(err).__name__,
|
|
406
406
|
_("Testcase variation validation exception: %(error)s, entry URL: %(instance)s"),
|
|
@@ -408,17 +408,20 @@ class Validate:
|
|
|
408
408
|
return [] # don't try to load this entry URL
|
|
409
409
|
if filesource and filesource.isReportPackage and not _rptPkgIxdsOptions:
|
|
410
410
|
if not reportPackageErrors:
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
411
|
+
assert isinstance(filesource.basefile, str)
|
|
412
|
+
if entrypoints := filesourceEntrypointFiles(filesource):
|
|
413
|
+
for pluginXbrlMethod in pluginClassMethods("ModelTestcaseVariation.ArchiveIxds"):
|
|
414
|
+
pluginXbrlMethod(self, filesource, entrypoints)
|
|
415
|
+
for entrypoint in entrypoints:
|
|
416
|
+
filesource.select(entrypoint.get("file", None))
|
|
417
|
+
modelXbrl = ModelXbrl.load(self.modelXbrl.modelManager,
|
|
418
|
+
filesource,
|
|
419
|
+
_("validating"),
|
|
420
|
+
base=filesource.basefile + "/",
|
|
421
|
+
errorCaptureLevel=errorCaptureLevel,
|
|
422
|
+
ixdsTarget=modelTestcaseVariation.ixdsTarget,
|
|
423
|
+
errors=preLoadingErrors)
|
|
424
|
+
loadedModels.append(modelXbrl)
|
|
422
425
|
else:
|
|
423
426
|
if _rptPkgIxdsOptions and filesource.isTaxonomyPackage:
|
|
424
427
|
# Legacy ESEF conformance suite logic.
|
|
@@ -492,9 +495,17 @@ class Validate:
|
|
|
492
495
|
# validate schema, linkbase, or instance
|
|
493
496
|
formulaOutputInstance = None
|
|
494
497
|
modelXbrl = inputDTSes[None][0]
|
|
495
|
-
expectedDataFiles = set(
|
|
496
|
-
|
|
497
|
-
|
|
498
|
+
expectedDataFiles = set()
|
|
499
|
+
expectedTaxonomyPackages = []
|
|
500
|
+
for localName, d in modelTestcaseVariation.dataUris.items():
|
|
501
|
+
for uri in d:
|
|
502
|
+
if not UrlUtil.isAbsolute(uri):
|
|
503
|
+
normalizedUri = self.modelXbrl.modelManager.cntlr.webCache.normalizeUrl(uri, baseForElement)
|
|
504
|
+
if localName == "taxonomyPackage":
|
|
505
|
+
expectedTaxonomyPackages.append(normalizedUri)
|
|
506
|
+
else:
|
|
507
|
+
expectedDataFiles.add(normalizedUri)
|
|
508
|
+
expectedTaxonomyPackages.sort()
|
|
498
509
|
foundDataFiles = set()
|
|
499
510
|
variationBase = os.path.dirname(baseForElement)
|
|
500
511
|
for dtsName, inputDTS in inputDTSes.items(): # input instances are also parameters
|
|
@@ -509,16 +520,28 @@ class Validate:
|
|
|
509
520
|
if docUrl.replace("-formula.xml", ".xf") in expectedDataFiles:
|
|
510
521
|
docUrl = docUrl.replace("-formula.xml", ".xf")
|
|
511
522
|
foundDataFiles.add(docUrl)
|
|
512
|
-
|
|
523
|
+
|
|
524
|
+
foundDataFilesInTaxonomyPackages = set()
|
|
525
|
+
foundTaxonomyPackages = set()
|
|
526
|
+
for f in foundDataFiles:
|
|
527
|
+
if i := bisect.bisect(expectedTaxonomyPackages, f):
|
|
528
|
+
package = expectedTaxonomyPackages[i-1]
|
|
529
|
+
if f.startswith(package + "/"):
|
|
530
|
+
foundDataFilesInTaxonomyPackages.add(f)
|
|
531
|
+
foundTaxonomyPackages.add(package)
|
|
532
|
+
|
|
533
|
+
expectedNotFound = expectedDataFiles.union(expectedTaxonomyPackages) - foundDataFiles - foundTaxonomyPackages
|
|
534
|
+
if expectedNotFound:
|
|
513
535
|
modelXbrl.info("arelle:testcaseDataNotUsed",
|
|
514
536
|
_("Variation %(id)s %(name)s data files not used: %(missingDataFiles)s"),
|
|
515
537
|
modelObject=modelTestcaseVariation, name=modelTestcaseVariation.name, id=modelTestcaseVariation.id,
|
|
516
|
-
missingDataFiles=", ".join(sorted(os.path.basename(f) for f in
|
|
517
|
-
|
|
538
|
+
missingDataFiles=", ".join(sorted(os.path.basename(f) for f in expectedNotFound)))
|
|
539
|
+
foundNotExpected = foundDataFiles - expectedDataFiles - foundDataFilesInTaxonomyPackages
|
|
540
|
+
if foundNotExpected:
|
|
518
541
|
modelXbrl.info("arelle:testcaseDataUnexpected",
|
|
519
542
|
_("Variation %(id)s %(name)s files not in variation data: %(unexpectedDataFiles)s"),
|
|
520
543
|
modelObject=modelTestcaseVariation, name=modelTestcaseVariation.name, id=modelTestcaseVariation.id,
|
|
521
|
-
unexpectedDataFiles=", ".join(sorted(os.path.basename(f) for f in
|
|
544
|
+
unexpectedDataFiles=", ".join(sorted(os.path.basename(f) for f in foundNotExpected)))
|
|
522
545
|
if modelXbrl.hasTableRendering or modelTestcaseVariation.resultIsTable:
|
|
523
546
|
try:
|
|
524
547
|
RenderingEvaluator.init(modelXbrl)
|
|
@@ -687,6 +710,7 @@ class Validate:
|
|
|
687
710
|
testcaseExpectedErrors = self.modelXbrl.modelManager.formulaOptions.testcaseExpectedErrors or {}
|
|
688
711
|
matchAllExpected = testcaseResultOptions == "match-all" or modelTestcaseVariation.match == 'all'
|
|
689
712
|
expectedReportCount = modelTestcaseVariation.expectedReportCount
|
|
713
|
+
expectedWarnings = modelTestcaseVariation.expectedWarnings if self.modelXbrl.modelManager.formulaOptions.testcaseResultsCaptureWarnings else []
|
|
690
714
|
if expectedReportCount is not None and validateModelCount is not None and expectedReportCount != validateModelCount:
|
|
691
715
|
errors.append("conf:testcaseExpectedReportCountError")
|
|
692
716
|
_blockedMessageCodes = modelTestcaseVariation.blockedMessageCodes # restricts codes examined when provided
|
|
@@ -727,10 +751,21 @@ class Validate:
|
|
|
727
751
|
status = "fail" if numErrors == 0 else "pass"
|
|
728
752
|
elif expected in (None, []) and numErrors == 0:
|
|
729
753
|
status = "pass"
|
|
730
|
-
elif isinstance(expected,(QName,str,dict,list))
|
|
754
|
+
elif isinstance(expected, (QName, str, dict, list)) or expectedWarnings:
|
|
731
755
|
status = "fail"
|
|
732
756
|
_passCount = 0
|
|
733
|
-
|
|
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)
|
|
734
769
|
if not isinstance(expected, list):
|
|
735
770
|
expected = [expected]
|
|
736
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
arelle/formula/XPathParser.py
CHANGED
|
@@ -3,6 +3,7 @@ See COPYRIGHT.md for copyright information.
|
|
|
3
3
|
'''
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import logging
|
|
6
7
|
import sys
|
|
7
8
|
import time
|
|
8
9
|
import traceback
|
|
@@ -906,9 +907,14 @@ def initializeParser(modelManager: ModelManager) -> bool:
|
|
|
906
907
|
modelManager.showStatus(_("initializing formula xpath2 grammar"))
|
|
907
908
|
startedAt = time.time()
|
|
908
909
|
xpathExpr.parse_string("0", parseAll=True)
|
|
909
|
-
modelManager.addToLog(
|
|
910
|
-
|
|
911
|
-
|
|
910
|
+
modelManager.addToLog(
|
|
911
|
+
format_string(
|
|
912
|
+
modelManager.locale,
|
|
913
|
+
_("Formula xpath2 grammar initialized in %.2f secs"),
|
|
914
|
+
time.time() - startedAt
|
|
915
|
+
),
|
|
916
|
+
level=logging.DEBUG
|
|
917
|
+
)
|
|
912
918
|
modelManager.showStatus(None)
|
|
913
919
|
isInitialized = True
|
|
914
920
|
return True # was initialized on this call
|
|
@@ -7,10 +7,10 @@ from __future__ import annotations
|
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
9
|
import zipfile
|
|
10
|
-
from collections import defaultdict
|
|
10
|
+
from collections import Counter, defaultdict
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from pathlib import Path, PurePosixPath
|
|
13
|
-
from typing import TYPE_CHECKING, Any,
|
|
13
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
14
14
|
|
|
15
15
|
from arelle.packages import PackageUtils
|
|
16
16
|
from arelle.packages.report import ReportPackageConst as Const
|
|
@@ -162,6 +162,10 @@ class ReportEntry:
|
|
|
162
162
|
def isTopLevel(self) -> bool:
|
|
163
163
|
return len(PurePosixPath(self.primary).parts) == 3
|
|
164
164
|
|
|
165
|
+
@property
|
|
166
|
+
def dir(self) -> str:
|
|
167
|
+
return PurePosixPath(self.primary).parent.name
|
|
168
|
+
|
|
165
169
|
|
|
166
170
|
class ReportPackage:
|
|
167
171
|
def __init__(
|
|
@@ -204,7 +208,7 @@ class ReportPackage:
|
|
|
204
208
|
reports = getAllReportEntries(filesource, stld)
|
|
205
209
|
if reportPackageJsonFile is None and reports is None:
|
|
206
210
|
return None
|
|
207
|
-
reportEntriesBySubDir = Counter(dir for report in reports or [] if not report.isTopLevel)
|
|
211
|
+
reportEntriesBySubDir = Counter(report.dir for report in reports or [] if not report.isTopLevel)
|
|
208
212
|
if reports is not None and any(report.isTopLevel for report in reports):
|
|
209
213
|
reports = [report for report in reports if report.isTopLevel]
|
|
210
214
|
if any(subdirCount > 1 for subdirCount in reportEntriesBySubDir.values()):
|
|
@@ -4,9 +4,10 @@ See COPYRIGHT.md for copyright information.
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
from collections import Counter
|
|
7
8
|
from collections.abc import Generator
|
|
8
9
|
from pathlib import Path, PurePosixPath
|
|
9
|
-
from typing import TYPE_CHECKING
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
10
11
|
|
|
11
12
|
from arelle.packages import PackageValidation
|
|
12
13
|
from arelle.packages.PackageType import PackageType
|
|
@@ -146,8 +147,8 @@ class ReportPackageValidator:
|
|
|
146
147
|
_("Report package must contain at least one report"),
|
|
147
148
|
)
|
|
148
149
|
if len(reportEntries) > 1 and not any(report.isTopLevel for report in reportEntries):
|
|
149
|
-
|
|
150
|
-
if
|
|
150
|
+
reportEntriesBySubDir = Counter(report.dir for report in reportEntries or [] if not report.isTopLevel)
|
|
151
|
+
if any(subdirCount > 1 for subdirCount in reportEntriesBySubDir.values()):
|
|
151
152
|
return Validation.error(
|
|
152
153
|
"rpe:multipleReportsInSubdirectory",
|
|
153
154
|
_("Report package must contain only one report"),
|
|
@@ -131,7 +131,6 @@ import regex as re
|
|
|
131
131
|
from lxml.etree import XML, XMLSyntaxError
|
|
132
132
|
|
|
133
133
|
from arelle import FileSource, ModelXbrl, ValidateDuplicateFacts, ValidateXbrlDimensions, XbrlConst
|
|
134
|
-
from arelle.CntlrCmdLine import filesourceEntrypointFiles
|
|
135
134
|
from arelle.FileSource import archiveFilenameParts, archiveFilenameSuffixes
|
|
136
135
|
from arelle.ModelDocument import ModelDocument, ModelDocumentReference, Type, create, inlineIxdsDiscover, load
|
|
137
136
|
from arelle.ModelInstanceObject import ModelInlineFootnote
|
|
@@ -142,6 +141,7 @@ from arelle.PrototypeDtsObject import ArcPrototype, LocPrototype
|
|
|
142
141
|
from arelle.PythonUtil import attrdict, isLegacyAbs
|
|
143
142
|
from arelle.RuntimeOptions import RuntimeOptions
|
|
144
143
|
from arelle.UrlUtil import isHttpUrl
|
|
144
|
+
from arelle.utils.EntryPointDetection import filesourceEntrypointFiles
|
|
145
145
|
from arelle.ValidateDuplicateFacts import DeduplicationType
|
|
146
146
|
from arelle.ValidateFilingText import CDATApattern
|
|
147
147
|
from arelle.Version import authorLabel, copyrightLabel
|
|
@@ -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(
|
|
@@ -188,12 +188,12 @@ def validateXbrlFinally(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None:
|
|
|
188
188
|
_baseName, _baseExt = os.path.splitext(doc.basename)
|
|
189
189
|
if _baseExt not in (".xhtml",".html"):
|
|
190
190
|
if val.consolidated:
|
|
191
|
-
|
|
191
|
+
errorCode = "ESEF.2.6.1.incorrectFileExtension"
|
|
192
192
|
reportType = _("Inline XBRL document included within a ESEF report package")
|
|
193
193
|
else:
|
|
194
|
-
|
|
194
|
+
errorCode = "ESEF.4.1.1.incorrectFileExtension"
|
|
195
195
|
reportType = _("Stand-alone XHTML document")
|
|
196
|
-
modelXbrl.error(
|
|
196
|
+
modelXbrl.error(errorCode,
|
|
197
197
|
_("%(reportType)s MUST have a .html or .xhtml extension: %(fileName)s"),
|
|
198
198
|
modelObject=doc, fileName=doc.basename, reportType=reportType)
|
|
199
199
|
docinfo = doc.xmlRootElement.getroottree().docinfo
|