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.

Files changed (40) hide show
  1. arelle/CntlrCmdLine.py +1 -66
  2. arelle/ModelDtsObject.py +1 -1
  3. arelle/ModelInstanceObject.py +9 -0
  4. arelle/ModelTestcaseObject.py +8 -1
  5. arelle/Validate.py +58 -23
  6. arelle/ValidateDuplicateFacts.py +4 -10
  7. arelle/_version.py +2 -2
  8. arelle/formula/XPathParser.py +9 -3
  9. arelle/packages/report/ReportPackage.py +7 -3
  10. arelle/packages/report/ReportPackageValidator.py +4 -3
  11. arelle/plugin/inlineXbrlDocumentSet.py +1 -1
  12. arelle/plugin/validate/DBA/PluginValidationDataExtension.py +9 -1
  13. arelle/plugin/validate/DBA/ValidationPluginExtension.py +4 -2
  14. arelle/plugin/validate/DBA/rules/fr.py +30 -32
  15. arelle/plugin/validate/DBA/rules/tm.py +22 -12
  16. arelle/plugin/validate/ESEF/ESEF_2021/ValidateXbrlFinally.py +3 -3
  17. arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +4 -5
  18. arelle/plugin/validate/NL/PluginValidationDataExtension.py +163 -32
  19. arelle/plugin/validate/NL/__init__.py +0 -11
  20. arelle/plugin/validate/NL/rules/nl_kvk.py +322 -12
  21. arelle/plugin/validate/ROS/rules/ros.py +1 -1
  22. arelle/typing.py +8 -1
  23. arelle/utils/EntryPointDetection.py +73 -0
  24. {arelle_release-2.37.12.dist-info → arelle_release-2.37.14.dist-info}/METADATA +1 -1
  25. {arelle_release-2.37.12.dist-info → arelle_release-2.37.14.dist-info}/RECORD +40 -38
  26. {arelle_release-2.37.12.dist-info → arelle_release-2.37.14.dist-info}/WHEEL +1 -1
  27. tests/integration_tests/ui_tests/ArelleGUITest/ArelleGUITest/ArelleGUITest.csproj +1 -1
  28. tests/integration_tests/validation/conformance_suite_configurations/nl_inline_2024.py +14 -21
  29. tests/integration_tests/validation/conformance_suite_configurations/xbrl_report_packages_1_0.py +3 -1
  30. tests/resources/conformance_suites/dba/fr/fr7-invalid.xhtml +1 -1
  31. tests/resources/conformance_suites/dba/fr/fr83-invalid.xbrl +1 -1
  32. tests/resources/conformance_suites/dba/fr/fr89-testcase.xml +10 -0
  33. tests/resources/conformance_suites/dba/fr/fr89-valid.xhtml +6816 -0
  34. tests/resources/conformance_suites/dba/fr/fr91-invalid.xhtml +1 -2
  35. tests/resources/conformance_suites/dba/tm/tm29-invalid.xhtml +0 -1
  36. tests/resources/conformance_suites/dba/tm/tm31-invalid.xhtml +10 -1
  37. tests/resources/conformance_suites/dba/tr/tr06-invalid.xhtml +1 -1
  38. {arelle_release-2.37.12.dist-info → arelle_release-2.37.14.dist-info}/entry_points.txt +0 -0
  39. {arelle_release-2.37.12.dist-info → arelle_release-2.37.14.dist-info}/licenses/LICENSE.md +0 -0
  40. {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 convertable to float, or None if not specified"""
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:
@@ -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:
@@ -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
- for report in filesource.reportPackage.reports or []:
412
- assert isinstance(filesource.basefile, str)
413
- modelXbrl = ModelXbrl.load(self.modelXbrl.modelManager,
414
- report.primary,
415
- _("validating"),
416
- useFileSource=filesource,
417
- base=filesource.basefile + "/",
418
- errorCaptureLevel=errorCaptureLevel,
419
- ixdsTarget=modelTestcaseVariation.ixdsTarget,
420
- errors=preLoadingErrors)
421
- loadedModels.append(modelXbrl)
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(modelXbrl.modelManager.cntlr.webCache.normalizeUrl(uri, baseForElement)
496
- for d in modelTestcaseVariation.dataUris.values() for uri in d
497
- if not UrlUtil.isAbsolute(uri))
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
- if expectedDataFiles - foundDataFiles:
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 expectedDataFiles - foundDataFiles)))
517
- if foundDataFiles - expectedDataFiles:
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 foundDataFiles - expectedDataFiles)))
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)): # string or assertion id counts dict
754
+ elif isinstance(expected, (QName, str, dict, list)) or expectedWarnings:
731
755
  status = "fail"
732
756
  _passCount = 0
733
- _expectedList = expected.copy() if isinstance(expected, list) else [expected]
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:
@@ -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
- tuple[QName, str | None], dict[tuple[ModelContext, ModelUnit], list[ModelFact]]
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
@@ -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.12'
21
- __version_tuple__ = version_tuple = (2, 37, 12)
20
+ __version__ = version = '2.37.14'
21
+ __version_tuple__ = version_tuple = (2, 37, 14)
@@ -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(format_string(modelManager.locale,
910
- _("Formula xpath2 grammar initialized in %.2f secs"),
911
- time.time() - startedAt))
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, Counter, cast
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, Counter
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
- byBaseDir = Counter(report.baseDir for report in reportEntries)
150
- if byBaseDir:
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
- modelXbrl = val.modelXbrl
1270
- auditorFacts = modelXbrl.factsByQname.get(pluginData.typeOfAuditorAssistanceQn)
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
- classFacts = []
1279
- facts = modelXbrl.factsByQname.get(pluginData.classOfReportingEntityQn)
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
- if fact.xValue in [
1284
- pluginData.reportingClassCLargeDanish,
1285
- pluginData.reportingClassCLargeEnglish,
1286
- pluginData.reportingClassCMediumDanish,
1287
- pluginData.reportingClassCMediumEnglish,
1288
- pluginData.reportingClassDDanish,
1289
- pluginData.reportingClassDEnglish,
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:DateOfApprovalOfReport), the values must be the same.
1317
- """
1318
- approvalOfReportFact = None
1319
- generalMeetingFact = None
1320
- approvalFacts = (val.modelXbrl.factsByQname.get(pluginData.dateOfApprovalOfReportQn, set()))
1321
- if len(approvalFacts) > 0:
1322
- approvalOfReportFact = next(iter(approvalFacts), None)
1323
- meetingFacts = val.modelXbrl.factsByQname.get(pluginData.dateOfGeneralMeetingQn, set())
1324
- if len(meetingFacts) > 0:
1325
- generalMeetingFact = next(iter(meetingFacts), None)
1326
- if generalMeetingFact is not None and generalMeetingFact.xValid >= VALID and approvalOfReportFact is not None and approvalOfReportFact.xValid >= VALID and generalMeetingFact.xValue != approvalOfReportFact.xValue:
1327
- yield Validation.error(
1328
- codes='DBA.FR91',
1329
- msg=_("The annual report contains information about both the general meeting date (gsd:DateOfGeneralMeeting) and the annual accounts meeting date (gsd:DateOfApprovalOfReport), the values must be the same."),
1330
- modelObject=[generalMeetingFact, approvalOfReportFact]
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:DateOfApprovalOfReport must be specified
242
+ DBA.TM29: Either gsd:DateOfGeneralMeeting or gsd:DateOfApprovalOfAnnualReport must be specified
243
243
  """
244
- modelXbrl = val.modelXbrl
245
- meeting_facts = modelXbrl.factsByQname.get(pluginData.dateOfGeneralMeetingQn, set())
246
- approval_facts = modelXbrl.factsByQname.get(pluginData.dateOfApprovalOfReportQn, 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
- )
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:DateOfApprovalOfReport must only be tagged once if tagged
280
- """
281
- return errorOnMultipleFacts(val.modelXbrl, pluginData.dateOfApprovalOfReportQn, 'DBA.TM31')
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
- XHTMLExtensionGuidance = "2.6.1"
191
+ errorCode = "ESEF.2.6.1.incorrectFileExtension"
192
192
  reportType = _("Inline XBRL document included within a ESEF report package")
193
193
  else:
194
- XHTMLExtensionGuidance = "4.1.1"
194
+ errorCode = "ESEF.4.1.1.incorrectFileExtension"
195
195
  reportType = _("Stand-alone XHTML document")
196
- modelXbrl.error(f"ESEF.{XHTMLExtensionGuidance}.incorrectFileExtension",
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