arelle-release 2.37.22__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.

@@ -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")
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.22'
21
- __version_tuple__ = version_tuple = (2, 37, 22)
20
+ __version__ = version = '2.37.23'
21
+ __version_tuple__ = version_tuple = (2, 37, 23)
@@ -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)
@@ -7,7 +7,7 @@ from collections import defaultdict
7
7
  from dataclasses import dataclass
8
8
  from functools import lru_cache
9
9
  from pathlib import Path
10
- from typing import Any, TYPE_CHECKING, cast, Iterable
10
+ from typing import Any, cast, Iterable
11
11
 
12
12
  import regex as re
13
13
  from lxml.etree import _Comment, _ElementTree, _Entity, _ProcessingInstruction, _Element
@@ -15,8 +15,9 @@ from lxml.etree import _Comment, _ElementTree, _Entity, _ProcessingInstruction,
15
15
  from arelle import XbrlConst
16
16
  from arelle.FunctionIxt import ixtNamespaces
17
17
  from arelle.ModelDocument import ModelDocument, Type as ModelDocumentType
18
- from arelle.ModelDtsObject import ModelConcept
18
+ from arelle.ModelDtsObject import ModelConcept, ModelRelationship
19
19
  from arelle.ModelInstanceObject import ModelContext, ModelFact, ModelInlineFootnote, ModelUnit, ModelInlineFact
20
+ from arelle.ModelRelationshipSet import ModelRelationshipSet
20
21
  from arelle.ModelObject import ModelObject
21
22
  from arelle.ModelValue import QName, qname
22
23
  from arelle.ModelXbrl import ModelXbrl
@@ -68,6 +69,10 @@ EFFECTIVE_KVK_GAAP_OTHER_ENTRYPOINT_FILES = frozenset((
68
69
  'https://www.nltaxonomie.nl/kvk/2024-12-31/kvk-annual-report-other-gaap.xsd',
69
70
  ))
70
71
 
72
+ NON_DIMENSIONALIZED_LINE_ITEM_LINKROLES = frozenset((
73
+ 'https://www.nltaxonomie.nl/kvk/role/lineitems-nondimensional-usage',
74
+ ))
75
+
71
76
  TAXONOMY_URLS_BY_YEAR = {
72
77
  '2024': {
73
78
  'https://www.nltaxonomie.nl/kvk/2024-12-31/kvk-annual-report-nlgaap-ext.xsd',
@@ -103,6 +108,21 @@ QN_DOMAIN_ITEM_TYPES = frozenset((
103
108
  qname("{http://www.xbrl.org/dtr/type/2022-03-31}nonnum:domainItemType"),
104
109
  ))
105
110
 
111
+ SUPPORTED_IMAGE_TYPES_BY_IS_FILE = {
112
+ True: ('gif', 'jpg', 'jpeg', 'png'),
113
+ False: ('gif', 'jpeg', 'png'),
114
+ }
115
+
116
+
117
+ @dataclass(frozen=True)
118
+ class AnchorData:
119
+ anchorsInDimensionalElrs: dict[str, frozenset[ModelRelationship]]
120
+ anchorsNotInBase: frozenset[ModelRelationship]
121
+ anchorsWithDimensionItem: frozenset[ModelRelationship]
122
+ anchorsWithDomainItem: frozenset[ModelRelationship]
123
+ extLineItemsNotAnchored: frozenset[ModelConcept]
124
+ extLineItemsWronglyAnchored: frozenset[ModelConcept]
125
+
106
126
 
107
127
  @dataclass(frozen=True)
108
128
  class ContextData:
@@ -112,6 +132,13 @@ class ContextData:
112
132
  contextsWithSegments: list[ModelContext | None]
113
133
 
114
134
 
135
+ @dataclass(frozen=True)
136
+ class DimensionalData:
137
+ domainMembers: frozenset[ModelConcept]
138
+ elrPrimaryItems: dict[str, set[ModelConcept]]
139
+ primaryItems: frozenset[ModelConcept]
140
+
141
+
115
142
  @dataclass(frozen=True)
116
143
  class ExtensionData:
117
144
  extensionConcepts: list[ModelConcept]
@@ -249,6 +276,23 @@ class PluginValidationDataExtension(PluginData):
249
276
  contextsWithSegments=contextsWithSegments,
250
277
  )
251
278
 
279
+ def checkLabels(self, issues: set[ModelConcept| None], modelXbrl: ModelXbrl, parent: ModelConcept, relSet: ModelRelationshipSet, labelrole: str | None, visited: set[ModelConcept]) -> set[ModelConcept| None]:
280
+ visited.add(parent)
281
+ conceptRels = defaultdict(list) # counts for concepts without preferred label role
282
+ for rel in relSet.fromModelObject(parent):
283
+ child = rel.toModelObject
284
+ if child is not None:
285
+ labelrole = rel.preferredLabel
286
+ if not labelrole:
287
+ conceptRels[child].append(rel)
288
+ if child not in visited:
289
+ self.checkLabels(issues, modelXbrl, child, relSet, labelrole, visited)
290
+ for concept, rels in conceptRels.items():
291
+ if len(rels) > 1:
292
+ issues.add(concept)
293
+ visited.remove(parent)
294
+ return issues
295
+
252
296
  @lru_cache(1)
253
297
  def checkHiddenElements(self, modelXbrl: ModelXbrl) -> HiddenElementsData:
254
298
  cssHiddenFacts = set()
@@ -366,6 +410,70 @@ class PluginValidationDataExtension(PluginData):
366
410
  factLangs.add(fact.xmlLang)
367
411
  return factLangs
368
412
 
413
+ @lru_cache(1)
414
+ def getAnchorData(self, modelXbrl: ModelXbrl) -> AnchorData:
415
+ extLineItemsNotAnchored = set()
416
+ extLineItemsWronglyAnchored = set()
417
+ widerNarrowerRelSet = modelXbrl.relationshipSet(XbrlConst.widerNarrower)
418
+ generalSpecialRelSet = modelXbrl.relationshipSet(XbrlConst.generalSpecial)
419
+ calcRelSet = modelXbrl.relationshipSet(XbrlConst.summationItems)
420
+ dimensionalData = self.getDimensionalData(modelXbrl)
421
+ primaryItems = dimensionalData.primaryItems
422
+ domainMembers = dimensionalData.domainMembers
423
+ extensionData = self.getExtensionData(modelXbrl)
424
+ for concept in extensionData.extensionConcepts:
425
+ extLineItem = False
426
+ if concept.isPrimaryItem and \
427
+ not concept.isAbstract and \
428
+ concept in primaryItems and \
429
+ not widerNarrowerRelSet.contains(concept) and \
430
+ not calcRelSet.fromModelObject(concept):
431
+ extLineItem = True
432
+ elif concept.isAbstract and \
433
+ concept not in domainMembers and \
434
+ concept.type is not None and \
435
+ not concept.type.isDomainItemType and \
436
+ not concept.isHypercubeItem and \
437
+ not concept.isDimensionItem and \
438
+ not widerNarrowerRelSet.contains(concept):
439
+ extLineItem = True
440
+ if extLineItem:
441
+ if not generalSpecialRelSet.contains(concept):
442
+ extLineItemsNotAnchored.add(concept)
443
+ else:
444
+ extLineItemsWronglyAnchored.add(concept)
445
+ elrsContainingDimensionalRelationships = set(
446
+ ELR
447
+ for arcrole, ELR, linkqname, arcqname in modelXbrl.baseSets.keys()
448
+ if arcrole == "XBRL-dimensions" and ELR is not None)
449
+ anchorsNotInBase = set()
450
+ anchorsWithDomainItem = set()
451
+ anchorsWithDimensionItem = set()
452
+ anchorsInDimensionalElrs = defaultdict(set)
453
+ for anchoringRel in widerNarrowerRelSet.modelRelationships:
454
+ elr = anchoringRel.linkrole
455
+ fromObj = anchoringRel.fromModelObject
456
+ toObj = anchoringRel.toModelObject
457
+ if fromObj is not None and toObj is not None and fromObj.type is not None and toObj.type is not None:
458
+ if not ((not self.isExtensionUri(fromObj.modelDocument.uri, modelXbrl)) ^ (not self.isExtensionUri(toObj.modelDocument.uri, modelXbrl))):
459
+ anchorsNotInBase.add(anchoringRel)
460
+ if fromObj.type.isDomainItemType or toObj.type.isDomainItemType:
461
+ anchorsWithDomainItem.add(anchoringRel)
462
+ elif fromObj.isDimensionItem or toObj.isDimensionItem:
463
+ anchorsWithDimensionItem.add(anchoringRel)
464
+ else:
465
+ if elr in elrsContainingDimensionalRelationships:
466
+ anchorsInDimensionalElrs[elr].add(anchoringRel)
467
+ return AnchorData(
468
+ anchorsInDimensionalElrs={x: frozenset(y) for x, y in anchorsInDimensionalElrs.items()},
469
+ anchorsNotInBase=frozenset(anchorsNotInBase),
470
+ anchorsWithDimensionItem=frozenset(anchorsWithDimensionItem),
471
+ anchorsWithDomainItem=frozenset(anchorsWithDomainItem),
472
+ extLineItemsNotAnchored=frozenset(extLineItemsNotAnchored),
473
+ extLineItemsWronglyAnchored=frozenset(extLineItemsWronglyAnchored),
474
+ )
475
+
476
+
369
477
  def getBaseElements(self, modelXbrl: ModelXbrl) -> set[Any | None]:
370
478
  return self.checkInlineHTMLElements(modelXbrl).baseElements
371
479
 
@@ -399,10 +507,13 @@ class PluginValidationDataExtension(PluginData):
399
507
  _getDocumentsInDts(modelXbrl.modelDocument)
400
508
  return modelDocuments
401
509
 
402
- def getDomainMembers(self, modelXbrl: ModelXbrl) -> set[ModelConcept]:
510
+ @lru_cache(1)
511
+ def getDimensionalData(self, modelXbrl: ModelXbrl) -> DimensionalData:
403
512
  domainMembers = set() # concepts which are dimension domain members
513
+ elrPrimaryItems = defaultdict(set)
404
514
  hcPrimaryItems: set[ModelConcept] = set()
405
515
  hcMembers: set[Any] = set()
516
+ primaryItems: set[ModelConcept] = set()
406
517
  for hasHypercubeArcrole in (XbrlConst.all, XbrlConst.notAll):
407
518
  hasHypercubeRelationships = modelXbrl.relationshipSet(hasHypercubeArcrole).fromModelObjects()
408
519
  for hasHcRels in hasHypercubeRelationships.values():
@@ -413,6 +524,7 @@ class PluginValidationDataExtension(PluginData):
413
524
  for domMbrRel in modelXbrl.relationshipSet(XbrlConst.domainMember).fromModelObject(sourceConcept):
414
525
  if domMbrRel.consecutiveLinkrole == hasHcRel.linkrole: # only those related to this hc
415
526
  self.addDomMbrs(modelXbrl, domMbrRel.toModelObject, domMbrRel.consecutiveLinkrole, hcPrimaryItems)
527
+ primaryItems.update(hcPrimaryItems)
416
528
  hc = hasHcRel.toModelObject
417
529
  for hcDimRel in modelXbrl.relationshipSet(XbrlConst.hypercubeDimension, hasHcRel.consecutiveLinkrole).fromModelObject(hc):
418
530
  dim = hcDimRel.toModelObject
@@ -422,7 +534,18 @@ class PluginValidationDataExtension(PluginData):
422
534
  if isinstance(dom, ModelConcept):
423
535
  self.addDomMbrs(modelXbrl, dom, dimDomRel.consecutiveLinkrole, hcMembers)
424
536
  domainMembers.update(hcMembers)
425
- return domainMembers
537
+ if hasHcRel.linkrole in NON_DIMENSIONALIZED_LINE_ITEM_LINKROLES or hcMembers:
538
+ for hcPrimaryItem in hcPrimaryItems:
539
+ if not hcPrimaryItem.isAbstract:
540
+ elrPrimaryItems[hasHcRel.linkrole].add(hcPrimaryItem)
541
+ elrPrimaryItems["*"].add(hcPrimaryItem) # members of any ELR
542
+ hcPrimaryItems.clear()
543
+ hcMembers.clear()
544
+ return DimensionalData(
545
+ domainMembers=frozenset(domainMembers),
546
+ elrPrimaryItems=elrPrimaryItems,
547
+ primaryItems=frozenset(primaryItems),
548
+ )
426
549
 
427
550
  def getEligibleForTransformHiddenFacts(self, modelXbrl: ModelXbrl) -> set[ModelInlineFact]:
428
551
  return self.checkHiddenElements(modelXbrl).eligibleForTransformHiddenFacts
@@ -475,6 +598,7 @@ class PluginValidationDataExtension(PluginData):
475
598
  def getIxdsDocBasenames(self, modelXbrl: ModelXbrl) -> set[str]:
476
599
  return set(Path(url).name for url in getattr(modelXbrl, "ixdsDocUrls", []))
477
600
 
601
+ @lru_cache(1)
478
602
  def getExtensionConcepts(self, modelXbrl: ModelXbrl) -> list[ModelConcept]:
479
603
  """
480
604
  Returns a list of extension concepts in the DTS.
@@ -7,30 +7,36 @@
7
7
  names="NL-INLINE-2024|nl-inline-2024|kvk-inline-2024-preview"
8
8
  description="Checks for NL INLINE 2024. For use with NL GAAP and NL IFRS"
9
9
  validationType="NL"
10
+ exclusiveTypesPattern="EFM|GFM|FERC|HMRC|SBR.NL|EBA|EIOPA|ESEF"
10
11
  />
11
12
  <DisclosureSystem
12
13
  names="NL-INLINE-2024-GAAP-OTHER|nl-inline-2024-gaap-other|kvk-inline-2024-gaap-other-preview"
13
14
  description="Checks for NL INLINE 2024. For use with other GAAP."
14
15
  validationType="NL"
16
+ exclusiveTypesPattern="EFM|GFM|FERC|HMRC|SBR.NL|EBA|EIOPA|ESEF"
15
17
  />
16
18
  <DisclosureSystem
17
19
  names="NT19|nt19|NT19-preview|nt19-preview"
18
20
  description="Checks for NT19"
19
21
  validationType="NL"
22
+ exclusiveTypesPattern="EFM|GFM|FERC|HMRC|SBR.NL|EBA|EIOPA|ESEF"
20
23
  />
21
24
  <DisclosureSystem
22
25
  names="NT18|nt18|NT18-preview|nt18-preview"
23
26
  description="Checks for NT18"
24
27
  validationType="NL"
28
+ exclusiveTypesPattern="EFM|GFM|FERC|HMRC|SBR.NL|EBA|EIOPA|ESEF"
25
29
  />
26
30
  <DisclosureSystem
27
31
  names="NT17|nt17|NT17-preview|nt17-preview"
28
32
  description="Checks for NT17"
29
33
  validationType="NL"
34
+ exclusiveTypesPattern="EFM|GFM|FERC|HMRC|SBR.NL|EBA|EIOPA|ESEF"
30
35
  />
31
36
  <DisclosureSystem
32
37
  names="NT16|nt16|NT16-preview|nt16-preview"
33
38
  description="Checks for NT16"
34
39
  validationType="NL"
40
+ exclusiveTypesPattern="EFM|GFM|FERC|HMRC|SBR.NL|EBA|EIOPA|ESEF"
35
41
  />
36
42
  </DisclosureSystems>
@@ -120,7 +120,7 @@ def rule_fr_kvk_2_02(
120
120
  if fact.xmlLang and fact.xmlLang != lang:
121
121
  yield Validation.error(
122
122
  codes='NL.FR-KVK-2.02',
123
- msg=_('The attribute \'xml:lang\' can be reported on different elements within an XBRL instance document.'
123
+ msg=_('The attribute \'xml:lang\' can be reported on different elements within an XBRL instance document. '
124
124
  'The attribute \'xml:lang\' must always contain the same value within an XBRL instance document. '
125
125
  'It is not allowed to report different values here. Document language: %(documentLang)s Element language: %(additionalLang)s'),
126
126
  modelObject=fact,