arelle-release 2.37.21__py3-none-any.whl → 2.37.23__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 (28) hide show
  1. arelle/ModelRelationshipSet.py +3 -0
  2. arelle/ValidateDuplicateFacts.py +13 -7
  3. arelle/XbrlConst.py +3 -0
  4. arelle/_version.py +2 -2
  5. arelle/api/Session.py +88 -58
  6. arelle/plugin/validate/DBA/rules/fr.py +10 -10
  7. arelle/plugin/validate/DBA/rules/th.py +1 -1
  8. arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +21 -20
  9. arelle/plugin/validate/NL/DisclosureSystems.py +14 -0
  10. arelle/plugin/validate/NL/PluginValidationDataExtension.py +167 -3
  11. arelle/plugin/validate/NL/ValidationPluginExtension.py +10 -1
  12. arelle/plugin/validate/NL/resources/config.xml +12 -1
  13. arelle/plugin/validate/NL/rules/fr_kvk.py +1 -1
  14. arelle/plugin/validate/NL/rules/nl_kvk.py +680 -155
  15. arelle/utils/validate/DetectScriptsInXhtml.py +1 -4
  16. arelle/utils/validate/ESEFImage.py +274 -0
  17. {arelle_release-2.37.21.dist-info → arelle_release-2.37.23.dist-info}/METADATA +1 -1
  18. {arelle_release-2.37.21.dist-info → arelle_release-2.37.23.dist-info}/RECORD +27 -26
  19. tests/integration_tests/validation/conformance_suite_configs.py +2 -0
  20. tests/integration_tests/validation/conformance_suite_configurations/nl_inline_2024.py +30 -43
  21. tests/integration_tests/validation/conformance_suite_configurations/nl_inline_2024_gaap_other.py +244 -0
  22. tests/integration_tests/validation/discover_tests.py +2 -2
  23. tests/unit_tests/arelle/plugin/validate/ESEF/ESEF_Current/test_validate_css_url.py +10 -2
  24. arelle/plugin/validate/ESEF/ESEF_Current/Image.py +0 -213
  25. {arelle_release-2.37.21.dist-info → arelle_release-2.37.23.dist-info}/WHEEL +0 -0
  26. {arelle_release-2.37.21.dist-info → arelle_release-2.37.23.dist-info}/entry_points.txt +0 -0
  27. {arelle_release-2.37.21.dist-info → arelle_release-2.37.23.dist-info}/licenses/LICENSE.md +0 -0
  28. {arelle_release-2.37.21.dist-info → arelle_release-2.37.23.dist-info}/top_level.txt +0 -0
@@ -218,6 +218,9 @@ class ModelRelationshipSet:
218
218
  def __bool__(self): # some modelRelationships exist
219
219
  return len(self.modelRelationships) > 0
220
220
 
221
+ def contains(self, modelObject: ModelObject) -> bool:
222
+ return bool(self.fromModelObject(modelObject) or self.toModelObject(modelObject))
223
+
221
224
  @property
222
225
  def linkRoleUris(self):
223
226
  # order by document appearance of linkrole, required for Table Linkbase testcase 3220 v03
@@ -162,7 +162,7 @@ class DuplicateFactSet:
162
162
  decimalValues: dict[float | int, TypeXValue] = {}
163
163
  for fact in self.facts:
164
164
  value = fact.xValue
165
- if isnan(cast(SupportsFloat, value)):
165
+ if _isNanOrNone(value):
166
166
  # NaN values are not comparable, can't be equal/consistent.
167
167
  return False
168
168
  decimals = self.getDecimals(fact)
@@ -224,7 +224,7 @@ class DuplicateFactSet:
224
224
  groupLower = decimalsMap[decimalLower]
225
225
  for factA in groupLower:
226
226
  lowerA, upperA = self.getRange(factA)
227
- if isnan(cast(SupportsFloat, factA.xValue)):
227
+ if _isNanOrNone(factA.xValue):
228
228
  continue
229
229
  remove = False
230
230
  # Iterate through each higher decimals group
@@ -232,7 +232,7 @@ class DuplicateFactSet:
232
232
  groupHigher = decimalsMap[decimalHigher]
233
233
  for factB in groupHigher:
234
234
  lowerB, upperB = self.getRange(factB)
235
- if isnan(cast(SupportsFloat, factB.xValue)):
235
+ if _isNanOrNone(factB.xValue):
236
236
  continue
237
237
  if lowerB <= upperA and upperB >= lowerA:
238
238
  remove = True
@@ -342,6 +342,12 @@ DUPLICATE_TYPE_ARG_MAP = {
342
342
  }
343
343
 
344
344
 
345
+ def _isNanOrNone(value: TypeXValue) -> bool:
346
+ if value is None:
347
+ return True
348
+ return isnan(cast(SupportsFloat, value))
349
+
350
+
345
351
  def doesSetHaveDuplicateType(
346
352
  duplicateFacts: DuplicateFactSet, duplicateType: DuplicateType
347
353
  ) -> bool:
@@ -504,15 +510,15 @@ def getFactValueEqualityKey(fact: ModelFact) -> TypeFactValueEqualityKey:
504
510
  :param fact:
505
511
  :return: A key to be used for fact-value-equality comparison.
506
512
  """
507
- if fact.isNil:
508
- return FactValueEqualityType.DEFAULT, (None,)
509
513
  xValue = fact.xValue
514
+ if xValue is None or fact.isNil:
515
+ return FactValueEqualityType.DEFAULT, (None,)
510
516
  if fact.isNumeric:
511
- if isnan(cast(SupportsFloat, xValue)):
517
+ if _isNanOrNone(xValue):
512
518
  return FactValueEqualityType.DEFAULT, (float("nan"),)
513
519
  if fact.concept.isLanguage:
514
520
  return FactValueEqualityType.LANGUAGE, (
515
- cast(str, xValue).lower() if xValue is not None else None,
521
+ cast(str, xValue).lower(),
516
522
  )
517
523
  if isinstance(xValue, DateTime): # with/without time makes values unequal
518
524
  return FactValueEqualityType.DATETIME, (xValue, xValue.dateOnly)
arelle/XbrlConst.py CHANGED
@@ -20,6 +20,7 @@ _: TypeGetText
20
20
  _tuple = tuple # type: ignore[type-arg]
21
21
 
22
22
  xsd = "http://www.w3.org/2001/XMLSchema"
23
+ qnXsdComplexType = qname("{http://www.w3.org/2001/XMLSchema}xsd:complexType")
23
24
  qnXsdSchema = qname("{http://www.w3.org/2001/XMLSchema}xsd:schema")
24
25
  qnXsdAppinfo = qname("{http://www.w3.org/2001/XMLSchema}xsd:appinfo")
25
26
  qnXsdDefaultType = qname("{http://www.w3.org/2001/XMLSchema}xsd:anyType")
@@ -110,6 +111,8 @@ qnXbrldiExplicitMember = qname("{http://xbrl.org/2006/xbrldi}xbrldi:explicitMemb
110
111
  qnXbrldiTypedMember = qname("{http://xbrl.org/2006/xbrldi}xbrldi:typedMember")
111
112
  xlink = "http://www.w3.org/1999/xlink"
112
113
  qnXlinkArcRole = qname("{http://www.w3.org/1999/xlink}xlink:arcrole")
114
+ qnXlinkFrom = qname("{http://www.w3.org/1999/xlink}xlink:from")
115
+ qnXlinkType = qname("{http://www.w3.org/1999/xlink}xlink:type")
113
116
  xl = "http://www.xbrl.org/2003/XLink"
114
117
  qnXlExtended = qname("{http://www.xbrl.org/2003/XLink}xl:extended")
115
118
  qnXlLocator = qname("{http://www.xbrl.org/2003/XLink}xl:locator")
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.21'
21
- __version_tuple__ = version_tuple = (2, 37, 21)
20
+ __version__ = version = '2.37.23'
21
+ __version_tuple__ = version_tuple = (2, 37, 23)
arelle/api/Session.py CHANGED
@@ -6,6 +6,7 @@ The `arelle.api` module is the supported method for integrating Arelle into othe
6
6
  from __future__ import annotations
7
7
 
8
8
  import logging
9
+ import threading
9
10
  from types import TracebackType
10
11
  from typing import Any, BinaryIO
11
12
 
@@ -14,10 +15,35 @@ from arelle.CntlrCmdLine import CntlrCmdLine, createCntlrAndPreloadPlugins
14
15
  from arelle.ModelXbrl import ModelXbrl
15
16
  from arelle.RuntimeOptions import RuntimeOptions
16
17
 
18
+ _session_lock = threading.Lock()
19
+
17
20
 
18
21
  class Session:
22
+ """
23
+ CRITICAL THREAD SAFETY WARNING:
24
+
25
+ Arelle uses shared global state (PackageManager, PluginManager) which is NOT thread-safe.
26
+ Only ONE Session can run at a time across the entire process.
27
+
28
+ Safe usage:
29
+ - Use one Session at a time per process
30
+ - Use a process pool instead of thread pool for parallelism
31
+
32
+ Unsafe usage:
33
+ - Running multiple Sessions concurrently in any threads
34
+ - Threading.Thread with Session.run()
35
+ """
36
+
19
37
  def __init__(self) -> None:
20
38
  self._cntlr: CntlrCmdLine | None = None
39
+ self._thread_id = threading.get_ident()
40
+
41
+ def _check_thread(self) -> None:
42
+ """Ensure session is only used from the thread that created it."""
43
+ if threading.get_ident() != self._thread_id:
44
+ raise RuntimeError(
45
+ "Session objects cannot be shared between threads. Create a new Session instance in each thread."
46
+ )
21
47
 
22
48
  def __enter__(self) -> Any:
23
49
  return self
@@ -31,9 +57,11 @@ class Session:
31
57
  self.close()
32
58
 
33
59
  def close(self) -> None:
34
- if self._cntlr is not None:
35
- self._cntlr.close()
36
- PluginManager.close()
60
+ with _session_lock:
61
+ self._check_thread()
62
+ if self._cntlr is not None:
63
+ self._cntlr.close()
64
+ PluginManager.close()
37
65
 
38
66
  def get_log_messages(self) -> list[dict[str, Any]]:
39
67
  """
@@ -89,60 +117,62 @@ class Session:
89
117
  :param sourceZipStreamFileName: Optional file name to use for the passed zip stream.
90
118
  :return: True if the run was successful, False otherwise.
91
119
  """
92
- if sourceZipStreamFileName is not None and sourceZipStream is None:
93
- raise ValueError("sourceZipStreamFileName may only be provided if sourceZipStream is not None.")
94
- PackageManager.reset()
95
- PluginManager.reset()
96
- if self._cntlr is None:
97
- # Certain options must be passed into the controller constructor to have the intended effect
98
- self._cntlr = createCntlrAndPreloadPlugins(
99
- uiLang=options.uiLang,
100
- disablePersistentConfig=options.disablePersistentConfig,
101
- arellePluginModules={},
102
- )
103
- else:
104
- # Certain options passed into the controller constructor need to be updated
105
- if self._cntlr.uiLang != options.uiLang:
106
- self._cntlr.setUiLanguage(options.uiLang)
107
- self._cntlr.disablePersistentConfig = options.disablePersistentConfig or False
108
- logRefObjectProperties = True
109
- if options.logRefObjectProperties is not None:
110
- logRefObjectProperties = options.logRefObjectProperties
111
- if options.webserver:
112
- assert sourceZipStream is None, "Source streaming is not supported with webserver"
113
- assert responseZipStream is None, "Response streaming is not supported with webserver"
114
- if not self._cntlr.logger:
115
- self._cntlr.startLogging(
116
- logFileName='logToBuffer',
117
- logFilters=logFilters,
118
- logHandler=logHandler,
119
- logTextMaxLength=options.logTextMaxLength,
120
- logRefObjectProperties=logRefObjectProperties,
121
- logPropagate=options.logPropagate,
120
+ with _session_lock:
121
+ self._check_thread()
122
+ if sourceZipStreamFileName is not None and sourceZipStream is None:
123
+ raise ValueError("sourceZipStreamFileName may only be provided if sourceZipStream is not None.")
124
+ PackageManager.reset()
125
+ PluginManager.reset()
126
+ if self._cntlr is None:
127
+ # Certain options must be passed into the controller constructor to have the intended effect
128
+ self._cntlr = createCntlrAndPreloadPlugins(
129
+ uiLang=options.uiLang,
130
+ disablePersistentConfig=options.disablePersistentConfig,
131
+ arellePluginModules={},
122
132
  )
123
- self._cntlr.postLoggingInit()
124
- from arelle import CntlrWebMain
125
- CntlrWebMain.startWebserver(self._cntlr, options)
126
- return True
127
- else:
128
- if not self._cntlr.logger:
129
- self._cntlr.startLogging(
130
- logFileName=(options.logFile or "logToPrint"),
131
- logFileMode=options.logFileMode,
132
- logFormat=(options.logFormat or "[%(messageCode)s] %(message)s - %(file)s"),
133
- logLevel=(options.logLevel or "DEBUG"),
134
- logFilters=logFilters,
135
- logHandler=logHandler,
136
- logToBuffer=options.logFile == 'logToBuffer',
137
- logTextMaxLength=options.logTextMaxLength, # e.g., used by EDGAR/render to require buffered logging
138
- logRefObjectProperties=logRefObjectProperties,
139
- logXmlMaxAttributeLength=options.logXmlMaxAttributeLength,
140
- logPropagate=options.logPropagate,
133
+ else:
134
+ # Certain options passed into the controller constructor need to be updated
135
+ if self._cntlr.uiLang != options.uiLang:
136
+ self._cntlr.setUiLanguage(options.uiLang)
137
+ self._cntlr.disablePersistentConfig = options.disablePersistentConfig or False
138
+ logRefObjectProperties = True
139
+ if options.logRefObjectProperties is not None:
140
+ logRefObjectProperties = options.logRefObjectProperties
141
+ if options.webserver:
142
+ assert sourceZipStream is None, "Source streaming is not supported with webserver"
143
+ assert responseZipStream is None, "Response streaming is not supported with webserver"
144
+ if not self._cntlr.logger:
145
+ self._cntlr.startLogging(
146
+ logFileName='logToBuffer',
147
+ logFilters=logFilters,
148
+ logHandler=logHandler,
149
+ logTextMaxLength=options.logTextMaxLength,
150
+ logRefObjectProperties=logRefObjectProperties,
151
+ logPropagate=options.logPropagate,
152
+ )
153
+ self._cntlr.postLoggingInit()
154
+ from arelle import CntlrWebMain
155
+ CntlrWebMain.startWebserver(self._cntlr, options)
156
+ return True
157
+ else:
158
+ if not self._cntlr.logger:
159
+ self._cntlr.startLogging(
160
+ logFileName=(options.logFile or "logToPrint"),
161
+ logFileMode=options.logFileMode,
162
+ logFormat=(options.logFormat or "[%(messageCode)s] %(message)s - %(file)s"),
163
+ logLevel=(options.logLevel or "DEBUG"),
164
+ logFilters=logFilters,
165
+ logHandler=logHandler,
166
+ logToBuffer=options.logFile == 'logToBuffer',
167
+ logTextMaxLength=options.logTextMaxLength, # e.g., used by EDGAR/render to require buffered logging
168
+ logRefObjectProperties=logRefObjectProperties,
169
+ logXmlMaxAttributeLength=options.logXmlMaxAttributeLength,
170
+ logPropagate=options.logPropagate,
171
+ )
172
+ self._cntlr.postLoggingInit() # Cntlr options after logging is started
173
+ return self._cntlr.run(
174
+ options,
175
+ sourceZipStream=sourceZipStream,
176
+ responseZipStream=responseZipStream,
177
+ sourceZipStreamFileName=sourceZipStreamFileName,
141
178
  )
142
- self._cntlr.postLoggingInit() # Cntlr options after logging is started
143
- return self._cntlr.run(
144
- options,
145
- sourceZipStream=sourceZipStream,
146
- responseZipStream=responseZipStream,
147
- sourceZipStreamFileName=sourceZipStreamFileName,
148
- )
@@ -437,7 +437,7 @@ def rule_fr48(
437
437
  if len(foundFacts) > 0:
438
438
  yield Validation.warning(
439
439
  codes="DBA.FR48",
440
- msg=_("Annual reports with a start date of 1/1 2016 or later must not use the fields:"
440
+ msg=_("Annual reports with a start date of 1/1 2016 or later must not use the fields: "
441
441
  "'Extraordinary profit before tax', 'Extraordinary income', 'Extraordinary costs'."),
442
442
  modelObject=foundFacts
443
443
  )
@@ -729,7 +729,7 @@ def rule_fr57(
729
729
  for pair in equalityErrorPairs:
730
730
  yield Validation.error(
731
731
  codes="DBA.FR57.Equality",
732
- msg=_("The total of Assets (fsa:Assets) must be equal to the total of Liabilities and Equity (fsa:LiabilitiesAndEquity)."
732
+ msg=_("The total of Assets (fsa:Assets) must be equal to the total of Liabilities and Equity (fsa:LiabilitiesAndEquity). "
733
733
  "Assets: %(Assets)s Liabilities and Equity: %(LiabilitiesAndEquity)s"),
734
734
  Assets=pair[0].effectiveValue,
735
735
  LiabilitiesAndEquity=pair[1].effectiveValue,
@@ -745,7 +745,7 @@ def rule_fr57(
745
745
  for fact in negativeLiabilitiesAndEquityFacts:
746
746
  yield Validation.error(
747
747
  codes="DBA.FR57.NegativeLiabilitiesAndEquity",
748
- msg=_("Liabilities and Equity (fsa:LiabilitiesAndEquity) must not be negative."
748
+ msg=_("Liabilities and Equity (fsa:LiabilitiesAndEquity) must not be negative. "
749
749
  "Liabilities and Equity was tagged with the value: %(factValue)s"),
750
750
  factValue = fact.effectiveValue,
751
751
  modelObject=fact
@@ -753,7 +753,7 @@ def rule_fr57(
753
753
  if not len(liabilitiesAndEquityErrors) == 0:
754
754
  yield Validation.error(
755
755
  codes="DBA.FR57.LiabilitiesAndEquity",
756
- msg=_("Liabilities and equity (fsa:LiabilitiesAndEquity) in the balance sheet must be filled in."
756
+ msg=_("Liabilities and equity (fsa:LiabilitiesAndEquity) in the balance sheet must be filled in. "
757
757
  "There is a problem with the reporting period ending: %(periods)s"),
758
758
  periods = ", ".join([cast(datetime.datetime, dt).strftime("%Y-%m-%d") for dt in liabilitiesAndEquityErrors])
759
759
  )
@@ -761,7 +761,7 @@ def rule_fr57(
761
761
  for profitLossError in profitLossErrors:
762
762
  yield Validation.error(
763
763
  codes="DBA.FR57.ProfitLoss",
764
- msg=_("The profit for the year (fsa:ProfitLoss) in the income statement must be filled in."
764
+ msg=_("The profit for the year (fsa:ProfitLoss) in the income statement must be filled in. "
765
765
  "There is a problem with the reporting periods starting %(start)s and ending: %(end)s"),
766
766
  start = profitLossError[0],
767
767
  end = profitLossError[1]
@@ -995,7 +995,7 @@ def rule_fr73(
995
995
  return
996
996
  yield Validation.warning(
997
997
  codes='DBA.FR73',
998
- msg=_("When the field ReportingResponsibilitiesAccordingToTheDanishExecutiveOrderOnApprovedAuditorsReportsExtendedReview is completed"
998
+ msg=_("When the field ReportingResponsibilitiesAccordingToTheDanishExecutiveOrderOnApprovedAuditorsReportsExtendedReview is completed "
999
999
  "one or more of the sub-items below must be indicated: "
1000
1000
  "ReportingResponsibilitiesAccordingToTheDanishExecutiveOrderOnApprovedAuditorsReportsEspeciallyTheCriminalCodeAndFiscalTaxAndSubsidyLegislationExtendedReview "
1001
1001
  "ReportingResponsibilitiesAccordingToTheDanishExecutiveOrderOnApprovedAuditorsReportsEspeciallyTheCompaniesActOrEquivalentLegislationThatTheCompanyIsSubjectToExtendedReview "
@@ -1036,7 +1036,7 @@ def rule_fr74(
1036
1036
  if not cast(decimal.Decimal, liabilityFact.xValue) - cast(decimal.Decimal, equityFact.xValue) >= cast(decimal.Decimal, provisionFact.xValue) - ROUNDING_MARGIN:
1037
1037
  yield Validation.error(
1038
1038
  codes="DBA.FR74a",
1039
- msg=_("Provisions (fsa:Provisions) must be less than or equal to the balance sheet total (fsa:LiabilitiesAndEquity) minus equity (fsa:Equity)."
1039
+ msg=_("Provisions (fsa:Provisions) must be less than or equal to the balance sheet total (fsa:LiabilitiesAndEquity) minus equity (fsa:Equity). "
1040
1040
  "LiabilitiesAndEquity: %(liabilities)s, Equity: %(equity)s, Provisions: %(provisions)s"),
1041
1041
  equity=equityFact.effectiveValue,
1042
1042
  liabilities=liabilityFact.effectiveValue,
@@ -1047,7 +1047,7 @@ def rule_fr74(
1047
1047
  if not cast(decimal.Decimal, liabilityFact.xValue) - cast(decimal.Decimal, equityFact.xValue) >= cast(decimal.Decimal, liabilityOtherFact.xValue) - ROUNDING_MARGIN:
1048
1048
  yield Validation.error(
1049
1049
  codes="DBA.FR74b",
1050
- msg=_("Liabilities (fsa:LiabilitiesOtherThanProvisions) must be less than or equal to total assets (fsa:LiabilitiesAndEquity) minus equity (fsa:Equity)."
1050
+ msg=_("Liabilities (fsa:LiabilitiesOtherThanProvisions) must be less than or equal to total assets (fsa:LiabilitiesAndEquity) minus equity (fsa:Equity). "
1051
1051
  "LiabilitiesAndEquity: %(liabilities)s, Equity: %(equity)s, LiabilitiesOtherThanProvisions: %(liabilityOther)s"),
1052
1052
  equity=equityFact.effectiveValue,
1053
1053
  liabilityOther=liabilityOtherFact.effectiveValue,
@@ -1124,7 +1124,7 @@ def rule_fr77(
1124
1124
  if not cast(decimal.Decimal, liabilityFact.xValue) - cast(decimal.Decimal, equityFact.xValue) >= cast(decimal.Decimal, longLiabilityFact.xValue) - ROUNDING_MARGIN:
1125
1125
  yield Validation.error(
1126
1126
  codes="DBA.FR77a",
1127
- msg=_("Long-term liabilities (fsa:LongtermLiabilitiesOtherThanProvisions) must be less than or equal to the balance sheet total (fsa:LiabilitiesAndEquity) minus equity (fsa:Equity)."
1127
+ msg=_("Long-term liabilities (fsa:LongtermLiabilitiesOtherThanProvisions) must be less than or equal to the balance sheet total (fsa:LiabilitiesAndEquity) minus equity (fsa:Equity). "
1128
1128
  "LiabilitiesAndEquity: %(liabilities)s, Equity: %(equity)s, LongtermLiabilitiesOtherThanProvisions: %(longLiabilities)s"),
1129
1129
  equity=equityFact.effectiveValue,
1130
1130
  liabilities=liabilityFact.effectiveValue,
@@ -1135,7 +1135,7 @@ def rule_fr77(
1135
1135
  if not cast(decimal.Decimal, liabilityFact.xValue) - cast(decimal.Decimal, equityFact.xValue) >= cast(decimal.Decimal, shortLiabilityFact.xValue) - ROUNDING_MARGIN:
1136
1136
  yield Validation.error(
1137
1137
  codes="DBA.FR77b",
1138
- msg=_("Short-term liabilities (fsa:ShorttermLiabilitiesOtherThanProvisions) must be less than or equal to the balance sheet total (fsa:LiabilitiesAndEquity) minus equity (fsa:Equity)."
1138
+ msg=_("Short-term liabilities (fsa:ShorttermLiabilitiesOtherThanProvisions) must be less than or equal to the balance sheet total (fsa:LiabilitiesAndEquity) minus equity (fsa:Equity). "
1139
1139
  "LiabilitiesAndEquity: %(liabilities)s, Equity: %(equity)s, ShorttermLiabilitiesOtherThanProvisions: %(shortLiabilities)s"),
1140
1140
  equity=equityFact.effectiveValue,
1141
1141
  liabilities=liabilityFact.effectiveValue,
@@ -41,7 +41,7 @@ def rule_th01(
41
41
  else:
42
42
  yield Validation.error(
43
43
  codes="DBA.TH01",
44
- msg=_("The 'link:schemaRef' must contain '{}'."
44
+ msg=_("The 'link:schemaRef' must contain '{}'. "
45
45
  "The 'link:schemaRef' as reported is {}.").format(pluginData.schemaRefUri, href),
46
46
  modelObject=doc,
47
47
  )
@@ -26,6 +26,7 @@ from arelle.ModelValue import QName
26
26
  from arelle.ModelValue import qname
27
27
  from arelle.ModelXbrl import ModelXbrl
28
28
 
29
+ from arelle.utils.validate.ESEFImage import ImageValidationParameters, checkSVGContentElt, validateImageAndLog
29
30
  from arelle.utils.validate.ValidationUtil import etreeIterWithDepth
30
31
  from arelle.PythonUtil import isLegacyAbs, normalizeSpace
31
32
  from arelle.PythonUtil import strTruncate
@@ -50,7 +51,6 @@ from arelle.XbrlConst import (
50
51
  from arelle.XmlValidateConst import VALID
51
52
  from arelle.typing import TypeGetText
52
53
  from .DTS import checkFilingDTS
53
- from .Image import checkSVGContentElt, validateImage
54
54
  from ..Const import (
55
55
  DefaultDimensionLinkroles,
56
56
  FOOTNOTE_LINK_CHILDREN,
@@ -60,10 +60,10 @@ from ..Const import (
60
60
  docTypeXhtmlPattern,
61
61
  esefMandatoryElementNames2020,
62
62
  esefPrimaryStatementPlaceholderNames,
63
- esefStatementsOfMonetaryDeclarationNames,
64
63
  mandatory,
65
64
  styleCssHiddenPattern,
66
65
  styleIxHiddenPattern,
66
+ supportedImgTypes,
67
67
  untransformableTypes,
68
68
  )
69
69
  from ..Dimensions import checkFilingDimensions
@@ -133,7 +133,6 @@ def validateXbrlFinally(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None:
133
133
  esefNotesConcepts = getEsefNotesStatementConcepts(val.modelXbrl)
134
134
 
135
135
  esefPrimaryStatementPlaceholders = set(qname(_ifrsNs, n) for n in esefPrimaryStatementPlaceholderNames)
136
- esefStatementsOfMonetaryDeclaration = set(qname(_ifrsNs, n) for n in esefStatementsOfMonetaryDeclarationNames)
137
136
  esefMandatoryElements2020 = set(qname(_ifrsNs, n) for n in esefMandatoryElementNames2020)
138
137
 
139
138
  if modelDocument.type == ModelDocument.Type.INSTANCE and not val.unconsolidated:
@@ -302,9 +301,16 @@ def validateXbrlFinally(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None:
302
301
  presentedHiddenEltIds = defaultdict(list)
303
302
  eligibleForTransformHiddenFacts = []
304
303
  requiredToDisplayFacts = []
305
- requiredToDisplayFactIds: dict[Any, Any] = {}
306
304
  firstIxdsDoc = True
307
305
  contentOtherThanXHTMLGuidance = 'ESEF.2.5.1' if val.consolidated else 'ESEF.4.1.3' # Different reference for iXBRL and stand-alone XHTML
306
+ imageValidationParameters = ImageValidationParameters(
307
+ checkMinExternalResourceSize=True,
308
+ consolidated = val.consolidated,
309
+ contentOtherThanXHTMLGuidance=contentOtherThanXHTMLGuidance,
310
+ missingMimeTypeIsIncorrect=True,
311
+ recommendBase64EncodingEmbeddedImages=True,
312
+ supportedImgTypes=supportedImgTypes,
313
+ )
308
314
  # ModelDocument.load has None as a return type. For typing reasons, we need to guard against that here.
309
315
  assert modelXbrl.modelDocument is not None
310
316
  for ixdsHtmlRootElt in (modelXbrl.ixdsHtmlElements if val.consolidated else # ix root elements for all ix docs in IXDS
@@ -339,12 +345,11 @@ def validateXbrlFinally(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None:
339
345
  _("Inline XBRL documents SHOULD NOT contain any 'mailto' URI: %(element)s"),
340
346
  modelObject=elt, element=eltTag)
341
347
  elif eltTag == "{http://www.w3.org/2000/svg}svg":
342
- checkSVGContentElt(elt, elt.modelDocument.baseForElement(elt), modelXbrl, [elt],
343
- contentOtherThanXHTMLGuidance, val)
348
+ checkSVGContentElt(elt, elt.modelDocument.baseForElement(elt), modelXbrl, [elt], imageValidationParameters, val)
344
349
  elif eltTag == "img":
345
350
  src = elt.get("src","").strip()
346
351
  evaluatedMsg = _('On line {line}, "alt" attribute value: "{alt}"').format(line=elt.sourceline, alt=elt.get("alt"))
347
- validateImage(elt.modelDocument.baseForElement(elt), src, modelXbrl, val, elt, evaluatedMsg, contentOtherThanXHTMLGuidance)
352
+ validateImageAndLog(elt.modelDocument.baseForElement(elt), src, modelXbrl, val, elt, evaluatedMsg, imageValidationParameters)
348
353
  # links to external documents are allowed as of 2021 per G.2.5.1
349
354
  # Since ESEF is a format requirement and is not expected to impact the 'human readable layer' of a report,
350
355
  # this guidance should not be seen as limiting the inclusion of links to external websites, to other documents
@@ -371,7 +376,7 @@ def validateXbrlFinally(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None:
371
376
 
372
377
  with elt.modelXbrl.fileSource.file(normalizedUri, binary=True)[0] as fh:
373
378
  cssContents = fh.read()
374
- validateCssUrl(cssContents.decode(), normalizedUri, modelXbrl, val, elt, contentOtherThanXHTMLGuidance)
379
+ validateCssUrl(cssContents.decode(), normalizedUri, modelXbrl, val, elt, imageValidationParameters)
375
380
  cssContents = None
376
381
  if val.unconsolidated:
377
382
  modelXbrl.warning("ESEF.4.1.4.externalCssFileForXhtmlDocument",
@@ -388,7 +393,7 @@ def validateXbrlFinally(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None:
388
393
  _("Where an Inline XBRL document set contains a single document, the CSS SHOULD be embedded within the document."),
389
394
  modelObject=elt, element=eltTag)
390
395
  elif eltTag == "style" and elt.get("type") == "text/css":
391
- validateCssUrl(elt.stringValue, elt.modelDocument.baseForElement(elt), modelXbrl, val, elt, contentOtherThanXHTMLGuidance)
396
+ validateCssUrl(elt.stringValue, elt.modelDocument.baseForElement(elt), modelXbrl, val, elt, imageValidationParameters)
392
397
  if not val.unconsolidated:
393
398
  if len(modelXbrl.ixdsHtmlElements) > 1:
394
399
  modelXbrl.warning("ESEF.2.5.4.embeddedCssForMultiHtmlIXbrlDocumentSets",
@@ -495,7 +500,7 @@ def validateXbrlFinally(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None:
495
500
  for declaration in tinycss2.parse_blocks_contents(styleValue):
496
501
  if isinstance(declaration, tinycss2.ast.Declaration):
497
502
  validateCssUrlContent(declaration.value, ixElt.modelDocument.baseForElement(ixElt),
498
- modelXbrl, val, ixElt, contentOtherThanXHTMLGuidance)
503
+ modelXbrl, val, ixElt, imageValidationParameters)
499
504
  elif isinstance(declaration, tinycss2.ast.ParseError):
500
505
  modelXbrl.warning("ix.CssParsingError",
501
506
  _("The style attribute contains erroneous CSS declaration \"%(styleContent)s\": %(parseError)s"),
@@ -538,7 +543,6 @@ def validateXbrlFinally(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None:
538
543
  contextsWithPeriodTimeZone: list[ModelContext] = []
539
544
  contextsWithWrongInstantDate: list[ModelContext] = []
540
545
  contextIdentifiers = defaultdict(list)
541
- nonStandardTypedDimensions: dict[Any, Any] = defaultdict(set)
542
546
  for context in modelXbrl.contexts.values():
543
547
  for uncast_elt in context.iterdescendants("{http://www.xbrl.org/2003/instance}startDate",
544
548
  "{http://www.xbrl.org/2003/instance}endDate",
@@ -650,14 +654,11 @@ def validateXbrlFinally(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None:
650
654
  textFactsByConceptContext = defaultdict(list)
651
655
  footnotesRelationshipSet = modelXbrl.relationshipSet(XbrlConst.factFootnote, XbrlConst.defaultLinkRole)
652
656
  noLangFacts = []
653
- textFactsMissingReportLang: list[Any] = []
654
657
  conceptsUsed = set()
655
658
  langsUsedByTextFacts = set()
656
659
 
657
- hasNoFacts = True
658
660
  factsMissingId = []
659
661
  for qn, facts in modelXbrl.factsByQname.items():
660
- hasNoFacts = False
661
662
  if qn in mandatory:
662
663
  reportedMandatory.add(qn)
663
664
  for f in facts:
@@ -1099,7 +1100,7 @@ def validateXbrlFinally(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None:
1099
1100
  modelXbrl.modelManager.showStatus(None)
1100
1101
 
1101
1102
 
1102
- def validateCssUrl(cssContent:str, normalizedUri:str, modelXbrl: ModelXbrl, val: ValidateXbrl, elt: ModelObject, contentOtherThanXHTMLGuidance: str) -> None:
1103
+ def validateCssUrl(cssContent:str, normalizedUri:str, modelXbrl: ModelXbrl, val: ValidateXbrl, elt: ModelObject, params: ImageValidationParameters) -> None:
1103
1104
  css_elements = tinycss2.parse_stylesheet(cssContent)
1104
1105
  for css_element in css_elements:
1105
1106
  if isinstance(css_element, tinycss2.ast.AtRule):
@@ -1107,22 +1108,22 @@ def validateCssUrl(cssContent:str, normalizedUri:str, modelXbrl: ModelXbrl, val:
1107
1108
  for css_rule in css_element.content:
1108
1109
  if isinstance(css_rule, tinycss2.ast.URLToken) and "data:font" not in css_rule.value:
1109
1110
  modelXbrl.warning(
1110
- "ESEF.%s.fontIncludedAndNotEmbeddedAsBase64EncodedString" % contentOtherThanXHTMLGuidance,
1111
+ "ESEF.%s.fontIncludedAndNotEmbeddedAsBase64EncodedString" % params.contentOtherThanXHTMLGuidance,
1111
1112
  _("Fonts SHOULD be included in the XHTML document as a base64 encoded string: %(file)s."),
1112
1113
  modelObject=elt, file=css_rule.value)
1113
1114
  if isinstance(css_element, tinycss2.ast.QualifiedRule):
1114
- validateCssUrlContent(css_element.content, normalizedUri, modelXbrl, val, elt, contentOtherThanXHTMLGuidance)
1115
+ validateCssUrlContent(css_element.content, normalizedUri, modelXbrl, val, elt, params)
1115
1116
 
1116
1117
 
1117
- def validateCssUrlContent(cssRules: list[Any], normalizedUri:str, modelXbrl: ModelXbrl, val: ValidateXbrl, elt: ModelObject, contentOtherThanXHTMLGuidance: str) -> None:
1118
+ def validateCssUrlContent(cssRules: list[Any], normalizedUri:str, modelXbrl: ModelXbrl, val: ValidateXbrl, elt: ModelObject, params: ImageValidationParameters) -> None:
1118
1119
  for css_rule in cssRules:
1119
1120
  if isinstance(css_rule, tinycss2.ast.FunctionBlock):
1120
1121
  if css_rule.lower_name == "url":
1121
1122
  if len(css_rule.arguments):
1122
1123
  css_rule_url = css_rule.arguments[0].value # url or base64
1123
1124
  evaluatedMsg = _('On line {line}').format(line=1) #css_element.source_line)
1124
- validateImage(normalizedUri, css_rule_url, modelXbrl, val, elt, evaluatedMsg, contentOtherThanXHTMLGuidance)
1125
+ validateImageAndLog(normalizedUri, css_rule_url, modelXbrl, val, elt, evaluatedMsg, params)
1125
1126
  elif isinstance(css_rule, tinycss2.ast.URLToken):
1126
1127
  value = css_rule.value
1127
1128
  evaluatedMsg = _('On line {line}').format(line=1) #css_element.source_line)
1128
- validateImage(normalizedUri, value, modelXbrl, val, elt, evaluatedMsg, contentOtherThanXHTMLGuidance)
1129
+ validateImageAndLog(normalizedUri, value, modelXbrl, val, elt, evaluatedMsg, params)
@@ -3,3 +3,17 @@ DISCLOSURE_SYSTEM_NT17 = 'NT17'
3
3
  DISCLOSURE_SYSTEM_NT18 = 'NT18'
4
4
  DISCLOSURE_SYSTEM_NT19 = 'NT19'
5
5
  DISCLOSURE_SYSTEM_NL_INLINE_2024 = 'NL-INLINE-2024'
6
+ DISCLOSURE_SYSTEM_NL_INLINE_2024_GAAP_OTHER = 'NL-INLINE-2024-GAAP-OTHER'
7
+
8
+ ALL_NL_INLINE_DISCLOSURE_SYSTEMS = [
9
+ DISCLOSURE_SYSTEM_NL_INLINE_2024,
10
+ DISCLOSURE_SYSTEM_NL_INLINE_2024_GAAP_OTHER,
11
+ ]
12
+
13
+ NL_INLINE_GAAP_IFRS_DISCLOSURE_SYSTEMS = [
14
+ DISCLOSURE_SYSTEM_NL_INLINE_2024,
15
+ ]
16
+
17
+ NL_INLINE_GAAP_OTHER_DISCLOSURE_SYSTEMS = [
18
+ DISCLOSURE_SYSTEM_NL_INLINE_2024_GAAP_OTHER,
19
+ ]