arelle-release 2.37.48__py3-none-any.whl → 2.37.50__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.

@@ -4,10 +4,9 @@ See COPYRIGHT.md for copyright information.
4
4
  from __future__ import annotations
5
5
 
6
6
  from collections import defaultdict
7
+ from itertools import chain
7
8
  from typing import Any, Iterable
8
9
 
9
- import regex
10
-
11
10
  from arelle import XbrlConst
12
11
  from arelle.ModelDtsObject import ModelConcept
13
12
  from arelle.ValidateXbrl import ValidateXbrl
@@ -163,17 +162,15 @@ def rule_EC8054W(
163
162
  EDINET.EC8054W: For any context with ID containing "NonConsolidatedMember",
164
163
  the scenario element within must be set to "NonConsolidatedMember".
165
164
  """
166
- for context in val.modelXbrl.contexts.values():
167
- if pluginData.nonConsolidatedMemberQn.localName not in context.id:
165
+ allContexts = chain(val.modelXbrl.contexts.values(), val.modelXbrl.ixdsUnmappedContexts.values())
166
+ for context in allContexts:
167
+ if context.id is None or pluginData.nonConsolidatedMemberQn.localName not in context.id:
168
168
  continue
169
- memberQnames = set()
170
- for scenarioElt in context.iterdescendants(XbrlConst.qnXbrlScenario.clarkNotation):
171
- for memberElt in scenarioElt.iterdescendants(
172
- XbrlConst.qnXbrldiExplicitMember.clarkNotation,
173
- XbrlConst.qnXbrldiTypedMember.clarkNotation
174
- ):
175
- memberQnames.add(memberElt.xValue)
176
- if pluginData.nonConsolidatedMemberQn not in memberQnames:
169
+ member = context.dimMemberQname(
170
+ pluginData.consolidatedOrNonConsolidatedAxisQn,
171
+ includeDefaults=True
172
+ )
173
+ if member != pluginData.nonConsolidatedMemberQn:
177
174
  yield Validation.warning(
178
175
  codes='EDINET.EC8054W',
179
176
  msg=_("For the context ID (%(contextId)s), \"NonConsolidatedMember\" "
@@ -3,9 +3,7 @@ See COPYRIGHT.md for copyright information.
3
3
  """
4
4
  from __future__ import annotations
5
5
 
6
- from collections import defaultdict
7
- from decimal import Decimal
8
- from typing import Any, Iterable, cast
6
+ from typing import Any, Iterable
9
7
 
10
8
  from arelle import XbrlConst, ValidateDuplicateFacts
11
9
  from arelle.LinkbaseType import LinkbaseType
@@ -15,12 +13,71 @@ from arelle.typing import TypeGetText
15
13
  from arelle.utils.PluginHooks import ValidationHook
16
14
  from arelle.utils.validate.Decorator import validation
17
15
  from arelle.utils.validate.Validation import Validation
16
+ from ..Constants import AccountingStandard
18
17
  from ..DisclosureSystems import (DISCLOSURE_SYSTEM_EDINET)
19
18
  from ..PluginValidationDataExtension import PluginValidationDataExtension
19
+ from ..Statement import StatementType
20
20
 
21
21
  _: TypeGetText
22
22
 
23
23
 
24
+ @validation(
25
+ hook=ValidationHook.XBRL_FINALLY,
26
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
27
+ )
28
+ def rule_balances(
29
+ pluginData: PluginValidationDataExtension,
30
+ val: ValidateXbrl,
31
+ *args: Any,
32
+ **kwargs: Any,
33
+ ) -> Iterable[Validation]:
34
+ """
35
+ EDINET.EC8057W: On the consolidated balance sheet, the sum of all liabilities and
36
+ equity must equal the sum of all assets.
37
+ EDINET.EC8058W: On the nonconsolidated balance sheet, the sum of all liabilities and
38
+ equity must equal the sum of all assets.
39
+ EDINET.EC8062W: On the consolidated statement of financial position, the sum of all liabilities and
40
+ equity must equal the sum of all assets.
41
+ EDINET.EC8064W: On the nonconsolidated statement of financial position, the sum of all liabilities and
42
+ equity must equal the sum of all assets.
43
+ """
44
+ for statementInstance in pluginData.getStatementInstances(val.modelXbrl):
45
+ statement = statementInstance.statement
46
+ for balanceSheet in statementInstance.balanceSheets:
47
+ if balanceSheet.assetsTotal == balanceSheet.liabilitiesAndEquityTotal:
48
+ continue
49
+ code = None
50
+ if statement.statementType == StatementType.BALANCE_SHEET:
51
+ if statement.isConsolidated:
52
+ code = "EDINET.EC8057W"
53
+ else:
54
+ code = "EDINET.EC8058W"
55
+ elif statement.statementType == StatementType.STATEMENT_OF_FINANCIAL_POSITION:
56
+ if statement.isConsolidated:
57
+ code = "EDINET.EC8062W"
58
+ else:
59
+ code = "EDINET.EC8064W"
60
+ assert code is not None, "Unknown balance sheet encountered."
61
+ yield Validation.warning(
62
+ codes=code,
63
+ msg=_("The %(consolidated)s %(balanceSheet)s is not balanced. "
64
+ "The sum of all liabilities and equity must equal the sum of all assets. "
65
+ "Please correct the debit (%(liabilitiesAndEquitySum)s) and credit (%(assetSum)s) "
66
+ "values so that they match "
67
+ "<roleUri=%(roleUri)s> <contextID=%(contextId)s> <unitID=%(unitId)s>."),
68
+ consolidated=_("consolidated") if statement.isConsolidated
69
+ else _("nonconsolidated"),
70
+ balanceSheet=_("balance sheet") if statement.statementType == StatementType.BALANCE_SHEET
71
+ else _("statement of financial position"),
72
+ liabilitiesAndEquitySum=f"{balanceSheet.liabilitiesAndEquityTotal:,}",
73
+ assetSum=f"{balanceSheet.assetsTotal:,}",
74
+ roleUri=statement.roleUri,
75
+ contextId=balanceSheet.contextId,
76
+ unitId=balanceSheet.unitId,
77
+ modelObject=balanceSheet.facts,
78
+ )
79
+
80
+
24
81
  @validation(
25
82
  hook=ValidationHook.XBRL_FINALLY,
26
83
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
@@ -90,6 +147,35 @@ def rule_EC5002E(
90
147
  )
91
148
 
92
149
 
150
+ @validation(
151
+ hook=ValidationHook.XBRL_FINALLY,
152
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
153
+ )
154
+ def rule_EC5613W(
155
+ pluginData: PluginValidationDataExtension,
156
+ val: ValidateXbrl,
157
+ *args: Any,
158
+ **kwargs: Any,
159
+ ) -> Iterable[Validation]:
160
+ """
161
+ EDINET.EC5613W: Please set the DEI "Accounting Standard" value to one
162
+ of the following: "Japan GAAP", "US GAAP", "IFRS".
163
+ """
164
+ validAccountingStandards = {s.value for s in AccountingStandard}
165
+ errorFacts = [
166
+ fact for fact in pluginData.iterValidNonNilFacts(val.modelXbrl, pluginData.accountingStandardsDeiQn)
167
+ if fact.xValue not in validAccountingStandards
168
+ ]
169
+ if len(errorFacts) > 0:
170
+ yield Validation.warning(
171
+ codes='EDINET.EC5613W',
172
+ msg=_("Please set the DEI \"Accounting Standard\" value to one "
173
+ "of the following: %(values)s."),
174
+ values=', '.join(f'"{s.value}"' for s in AccountingStandard),
175
+ modelObject=errorFacts,
176
+ )
177
+
178
+
93
179
  @validation(
94
180
  hook=ValidationHook.XBRL_FINALLY,
95
181
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
@@ -179,67 +265,41 @@ def rule_EC8027W(
179
265
  hook=ValidationHook.XBRL_FINALLY,
180
266
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
181
267
  )
182
- def rule_EC8062W(
268
+ def rule_EC8075W(
183
269
  pluginData: PluginValidationDataExtension,
184
270
  val: ValidateXbrl,
185
271
  *args: Any,
186
272
  **kwargs: Any,
187
273
  ) -> Iterable[Validation]:
188
274
  """
189
- EDINET.EC8062W: The sum of all liabilities and equity must equal the sum of all assets.
275
+ EDINET.EC8075W: The percentage of female executives has not been tagged in detail. Ensure that there is
276
+ a nonnil value disclosed for jpcrp_cor:RatioOfFemaleDirectorsAndOtherOfficers.
190
277
  """
191
- deduplicatedFacts = pluginData.getDeduplicatedFacts(val.modelXbrl)
192
-
193
- factsByContextId = defaultdict(list)
194
- for fact in deduplicatedFacts:
195
- if fact.qname not in (pluginData.assetsIfrsQn, pluginData.liabilitiesAndEquityIfrsQn):
196
- continue
197
- if fact.contextID is None or not pluginData.contextIdPattern.fullmatch(fact.contextID):
198
- continue
199
- factsByContextId[fact.contextID].append(fact)
200
-
201
- for facts in factsByContextId.values():
202
- assetSum = Decimal(0)
203
- liabilitiesAndEquitySum = Decimal(0)
204
- for fact in facts:
205
- if isinstance(fact.xValue, float):
206
- value = Decimal(fact.xValue)
207
- else:
208
- value = cast(Decimal, fact.xValue)
209
- if fact.qname == pluginData.assetsIfrsQn:
210
- assetSum += value
211
- elif fact.qname == pluginData.liabilitiesAndEquityIfrsQn:
212
- liabilitiesAndEquitySum += value
213
- if assetSum != liabilitiesAndEquitySum:
278
+ if pluginData.isCorporateForm(val.modelXbrl):
279
+ if not pluginData.hasValidNonNilFact(val.modelXbrl, pluginData.ratioOfFemaleDirectorsAndOtherOfficersQn):
214
280
  yield Validation.warning(
215
- codes='EDINET.EC8062W',
216
- msg=_("The consolidated statement of financial position is not reconciled. "
217
- "The sum of all liabilities and equity must equal the sum of all assets. "
218
- "Please correct the debit (%(liabilitiesAndEquitySum)s) and credit (%(assetSum)s) "
219
- "values so that they match."),
220
- liabilitiesAndEquitySum=f"{liabilitiesAndEquitySum:,}",
221
- assetSum=f"{assetSum:,}",
222
- modelObject=facts,
223
- )
281
+ codes='EDINET.EC8075W',
282
+ msg=_("The percentage of female executives has not been tagged in detail."),
283
+ )
224
284
 
225
285
 
226
286
  @validation(
227
287
  hook=ValidationHook.XBRL_FINALLY,
228
288
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
229
289
  )
230
- def rule_EC8075W(
290
+ def rule_EC8076W(
231
291
  pluginData: PluginValidationDataExtension,
232
292
  val: ValidateXbrl,
233
293
  *args: Any,
234
294
  **kwargs: Any,
235
295
  ) -> Iterable[Validation]:
236
296
  """
237
- EDINET.EC8075W: The percentage of female executives has not been tagged in detail. Ensure that there is
238
- a nonnil value disclosed for jpcrp_cor:RatioOfFemaleDirectorsAndOtherOfficers.
297
+ EDINET.EC8076W: "Issued Shares, Total Number of Shares, etc. [Text Block]" (IssuedSharesTotalNumberOfSharesEtcTextBlock) is not tagged.
298
+ Applies to forms 3 and 4.
239
299
  """
240
- if pluginData.isCorporateForm(val.modelXbrl):
241
- if not pluginData.hasValidNonNilFact(val.modelXbrl, pluginData.ratioOfFemaleDirectorsAndOtherOfficersQn):
300
+ if pluginData.isStockForm(val.modelXbrl) and pluginData.isCorporateReport(val.modelXbrl):
301
+ if not pluginData.hasValidNonNilFact(val.modelXbrl, pluginData.issuedSharesTotalNumberOfSharesEtcQn):
242
302
  yield Validation.warning(
243
- codes='EDINET.EC8075W',
244
- msg=_("The percentage of female executives has not been tagged in detail."),
245
- )
303
+ codes='EDINET.EC8076W',
304
+ msg=_('"Issued Shares, Total Number of Shares, etc. [Text Block]" (IssuedSharesTotalNumberOfSharesEtcTextBlock) is not tagged.'),
305
+ )
@@ -3,8 +3,10 @@ See COPYRIGHT.md for copyright information.
3
3
  """
4
4
  from __future__ import annotations
5
5
 
6
+ import os
6
7
  from collections import defaultdict
7
8
  from datetime import timedelta
9
+ from lxml.etree import XML, DTD
8
10
  from typing import Any, cast, Iterable
9
11
 
10
12
  import regex
@@ -28,6 +30,7 @@ from arelle.utils.validate.Validation import Validation
28
30
  from arelle.utils.validate.ValidationUtil import etreeIterWithDepth
29
31
  from ..DisclosureSystems import (DISCLOSURE_SYSTEM_EDINET)
30
32
  from ..PluginValidationDataExtension import PluginValidationDataExtension
33
+ from ..Constants import xhtmlDtdExtension
31
34
 
32
35
  _: TypeGetText
33
36
 
@@ -254,13 +257,14 @@ def rule_gfm_1_2_8(
254
257
  EDINET.EC5700W: [GFM 1.2.8] Every xbrli:context element must appear in at least one
255
258
  contextRef attribute in the same instance.
256
259
  """
257
- unused_contexts = list(set(val.modelXbrl.contexts.values()) - set(val.modelXbrl.contextsInUse))
258
- unused_contexts.sort(key=lambda x: x.id)
259
- for context in unused_contexts:
260
+ unusedContexts = list(set(val.modelXbrl.contexts.values()) - set(val.modelXbrl.contextsInUse))
261
+ unusedContexts.extend(val.modelXbrl.ixdsUnmappedContexts.values())
262
+ unusedContexts.sort(key=lambda x: x.id if x.id is not None else "")
263
+ for context in unusedContexts:
260
264
  yield Validation.warning(
261
265
  codes='EDINET.EC5700W.GFM.1.2.8',
262
266
  msg=_('If you are not using a context, delete it if it is not needed.'),
263
- modelObject = context
267
+ modelObject=context
264
268
  )
265
269
 
266
270
 
@@ -349,6 +353,44 @@ def rule_gfm_1_2_13(
349
353
  )
350
354
 
351
355
 
356
+ @validation(
357
+ hook=ValidationHook.XBRL_FINALLY,
358
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
359
+ )
360
+ def rule_gfm_1_2_14(
361
+ pluginData: PluginValidationDataExtension,
362
+ val: ValidateXbrl,
363
+ *args: Any,
364
+ **kwargs: Any,
365
+ ) -> Iterable[Validation]:
366
+ """
367
+ EDINET.EC5700W: [GFM.1.2.14] The content of an element with a data type of nonnum:textBlockItemType is not well-formed XML
368
+ (a format that conforms to XML grammar, such as all start and end tags being paired, and the end tag of a nested tag not coming after the end tag of its parent tag, etc.).
369
+ Please modify it so that it is well-formed.
370
+ """
371
+ CDATApattern = regex.compile(r"<!\[CDATA\[(.+)\]\]")
372
+ dtd = DTD(os.path.join(val.modelXbrl.modelManager.cntlr.configDir, xhtmlDtdExtension))
373
+ htmlBodyTemplate = "<body><div>\n{0}\n</div></body>\n"
374
+ namedEntityPattern = regex.compile("&[_A-Za-z\xC0-\xD6\xD8-\xF6\xF8-\xFF\u0100-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]"
375
+ r"[_\-\.:"
376
+ "\xB7A-Za-z0-9\xC0-\xD6\xD8-\xF6\xF8-\xFF\u0100-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u0300-\u036F\u203F-\u2040]*;")
377
+ XMLpattern = regex.compile(r".*(<|&lt;|&#x3C;|&#60;)[A-Za-z_]+[A-Za-z0-9_:]*[^>]*(/>|>|&gt;|/&gt;).*", regex.DOTALL)
378
+
379
+ for fact in val.modelXbrl.facts:
380
+ concept = fact.concept
381
+ if not fact.isNil and concept is not None and concept.isTextBlock and XMLpattern.match(fact.value):
382
+ for xmlText in [fact.value] + CDATApattern.findall(fact.value):
383
+ xmlBodyWithoutEntities = htmlBodyTemplate.format(namedEntityPattern.sub("", xmlText).replace('&','&amp;'))
384
+ textblockXml = XML(xmlBodyWithoutEntities)
385
+ if not dtd.validate(textblockXml):
386
+ yield Validation.warning(
387
+ codes='EDINET.EC5700W.GFM.1.2.14',
388
+ msg=_('The content of an element with a data type of nonnum:textBlockItemType is not well-formed XML (a format that conforms to XML grammar, '
389
+ 'such as all start and end tags being in pairs, and the end tag of a nested tag not coming after the end tag of its parent tag). '
390
+ 'Correct the content so that it is well-formed.'),
391
+ modelObject = fact
392
+ )
393
+
352
394
 
353
395
  @validation(
354
396
  hook=ValidationHook.XBRL_FINALLY,
@@ -443,6 +485,7 @@ def rule_gfm_1_2_25(
443
485
  XbrlConst.qnXbrliEndDate.clarkNotation,
444
486
  XbrlConst.qnXbrliInstant.clarkNotation
445
487
  ):
488
+ elt = cast(ModelObject, elt)
446
489
  dateText = XmlUtil.text(elt)
447
490
  if not GFM_CONTEXT_DATE_PATTERN.match(dateText):
448
491
  errors.append(elt)
@@ -517,12 +560,14 @@ def rule_gfm_1_2_27(
517
560
  EDINET.EC5700W: [GFM 1.2.27] An instance must not contain unused units.
518
561
  """
519
562
  # TODO: Consolidate validations involving unused units
520
- unusedUnits = set(val.modelXbrl.units.values()) - {fact.unit for fact in val.modelXbrl.facts if fact.unit is not None}
563
+ unusedUnits = list(set(val.modelXbrl.units.values()) - set(val.modelXbrl.unitsInUse))
564
+ unusedUnits.extend(val.modelXbrl.ixdsUnmappedUnits.values())
565
+ unusedUnits.sort(key=lambda x: x.hash)
521
566
  if len(unusedUnits) > 0:
522
567
  yield Validation.warning(
523
568
  codes='EDINET.EC5700W.GFM.1.2.27',
524
569
  msg=_("Delete unused units from the instance."),
525
- modelObject=list(unusedUnits)
570
+ modelObject=unusedUnits
526
571
  )
527
572
 
528
573
 
@@ -529,11 +529,11 @@ def validateXbrlFinally(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None:
529
529
  if contextsWithDisallowedOCEs:
530
530
  modelXbrl.error("ESEF.2.1.3.segmentUsed",
531
531
  _("xbrli:segment container MUST NOT be used in contexts: %(contextIds)s"),
532
- modelObject=contextsWithDisallowedOCEs, contextIds=", ".join(c.id for c in contextsWithDisallowedOCEs))
532
+ modelObject=contextsWithDisallowedOCEs, contextIds=", ".join(c.id for c in contextsWithDisallowedOCEs if c.id is not None))
533
533
  if contextsWithDisallowedOCEcontent:
534
534
  modelXbrl.error("ESEF.2.1.3.scenarioContainsNonDimensionalContent",
535
535
  _("xbrli:scenario in contexts MUST NOT contain any other content than defined in XBRL Dimensions specification: %(contextIds)s"),
536
- modelObject=contextsWithDisallowedOCEcontent, contextIds=", ".join(c.id for c in contextsWithDisallowedOCEcontent))
536
+ modelObject=contextsWithDisallowedOCEcontent, contextIds=", ".join(c.id for c in contextsWithDisallowedOCEcontent if c.id is not None))
537
537
  if len(contextIdentifiers) > 1:
538
538
  modelXbrl.error("ESEF.2.1.4.multipleIdentifiers",
539
539
  _("All entity identifiers in contexts MUST have identical content: %(contextIds)s"),
@@ -589,11 +589,11 @@ def validateXbrlFinally(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None:
589
589
  if contextsWithDisallowedOCEs:
590
590
  modelXbrl.error("ESEF.2.1.3.segmentUsed",
591
591
  _("xbrli:segment container MUST NOT be used in contexts: %(contextIds)s"),
592
- modelObject=contextsWithDisallowedOCEs, contextIds=", ".join(c.id for c in contextsWithDisallowedOCEs))
592
+ modelObject=contextsWithDisallowedOCEs, contextIds=", ".join(c.id for c in contextsWithDisallowedOCEs if c.id is not None))
593
593
  if contextsWithDisallowedOCEcontent:
594
594
  modelXbrl.error("ESEF.2.1.3.scenarioContainsNonDimensionalContent",
595
595
  _("xbrli:scenario in contexts MUST NOT contain any other content than defined in XBRL Dimensions specification: %(contextIds)s"),
596
- modelObject=contextsWithDisallowedOCEcontent, contextIds=", ".join(c.id for c in contextsWithDisallowedOCEcontent))
596
+ modelObject=contextsWithDisallowedOCEcontent, contextIds=", ".join(c.id for c in contextsWithDisallowedOCEcontent if c.id is not None))
597
597
  if len(contextIdentifiers) > 1:
598
598
  modelXbrl.error("ESEF.2.1.4.multipleIdentifiers",
599
599
  _("All entity identifiers in contexts MUST have identical content: %(contextIds)s"),
@@ -265,7 +265,7 @@ class PluginValidationDataExtension(PluginData):
265
265
  contextsWithPeriodTimeZone.append(context)
266
266
  if context.hasSegment:
267
267
  contextsWithSegments.append(context)
268
- if context.nonDimValues("scenario"): # type: ignore[no-untyped-call]
268
+ if context.nonDimValues("scenario"):
269
269
  contextsWithImproperContent.append(context)
270
270
  return ContextData(
271
271
  contextsWithImproperContent=contextsWithImproperContent,
@@ -552,9 +552,9 @@ def rule_fr_nl_3_03(
552
552
  """
553
553
  FR-NL-3.03: An XBRL instance document MUST NOT contain unused contexts
554
554
  """
555
- unused_contexts = list(set(val.modelXbrl.contexts.values()) - set(val.modelXbrl.contextsInUse))
556
- unused_contexts.sort(key=lambda x: x.id)
557
- for context in unused_contexts:
555
+ unusedContexts = list(set(val.modelXbrl.contexts.values()) - set(val.modelXbrl.contextsInUse))
556
+ unusedContexts.sort(key=lambda x: x.id if x.id is not None else "")
557
+ for context in unusedContexts:
558
558
  yield Validation.error(
559
559
  codes='NL.FR-NL-3.03',
560
560
  msg=_('Unused context must not exist in XBRL instance document'),
@@ -637,10 +637,9 @@ def rule_fr_nl_4_02(
637
637
  """
638
638
  FR-NL-4.02: An XBRL instance document MUST NOT contain unused 'xbrli:unit' elements
639
639
  """
640
- unused_units_set = set(val.modelXbrl.units.values()) - {fact.unit for fact in val.modelXbrl.facts if fact.unit is not None}
641
- unused_units = list(unused_units_set)
642
- unused_units.sort(key=lambda x: x.hash)
643
- for unit in unused_units:
640
+ unusedUnits = list(set(val.modelXbrl.units.values()) - set(val.modelXbrl.unitsInUse))
641
+ unusedUnits.sort(key=lambda x: x.hash)
642
+ for unit in unusedUnits:
644
643
  yield Validation.error(
645
644
  codes='NL.FR-NL-4.02',
646
645
  msg=_('Unused unit must not exist in the XBRL instance document'),
@@ -7,12 +7,13 @@ from dataclasses import dataclass
7
7
  from dataclasses import field
8
8
  from enum import Enum
9
9
  from functools import cached_property
10
- from typing import Any, cast
10
+ from typing import Any, cast, Iterable
11
11
 
12
12
  import regex as re
13
13
 
14
14
  from arelle.ModelInstanceObject import ModelFact
15
15
  from arelle.ModelXbrl import ModelXbrl
16
+ from arelle.XmlValidateConst import VALID
16
17
 
17
18
  # Error codes
18
19
  CH_AUDIT = 'Char.Audit'
@@ -430,10 +431,11 @@ class ValidateUK:
430
431
  _codeResultMap: dict[str, CodeResult] = field(default_factory=dict)
431
432
 
432
433
  def _checkValidFact(self, fact: ModelFact ) -> bool:
433
- if fact is not None:
434
- if not fact.isNil:
435
- return True
436
- return False
434
+ return (
435
+ fact is not None and
436
+ not fact.isNil and
437
+ fact.context is not None
438
+ )
437
439
 
438
440
  def _errorOnMissingFact(self, conceptLocalName: str) -> None:
439
441
  """
@@ -628,16 +630,12 @@ class ValidateUK:
628
630
  elif code == CH_CHAR_FUND:
629
631
  concept = CONCEPT_CHARITY_FUNDS
630
632
  trading = False
631
- for fact in self._getFacts(CONCEPT_ENTITY_TRADING_STATUS):
632
- if fact is None or fact.context is None:
633
- continue
634
- for qname, value in fact.context.qnameDims.items():
635
- if qname.localName == CONCEPT_ENTITY_TRADING_STATUS_DIMENSION:
636
- if value.xValue.localName in {
637
- NotTrading.CONCEPT_ENTITY_NO_LONGER_TRADING.value,
638
- NotTrading.CONCEPT_ENTITY_HAS_NEVER_TRADED.value,
639
- }:
640
- trading = True
633
+ for value in self._getDimensionValues(CONCEPT_ENTITY_TRADING_STATUS, CONCEPT_ENTITY_TRADING_STATUS_DIMENSION):
634
+ if value in {
635
+ NotTrading.CONCEPT_ENTITY_NO_LONGER_TRADING.value,
636
+ NotTrading.CONCEPT_ENTITY_HAS_NEVER_TRADED.value,
637
+ }:
638
+ trading = True
641
639
  if not self._getAndCheckValidFacts([concept]) and not trading:
642
640
  return CodeResult(
643
641
  conceptLocalName=concept,
@@ -686,13 +684,11 @@ class ValidateUK:
686
684
  """
687
685
  Determines if the language is set to Welsh, otherwise defaults to English.
688
686
  """
689
- for fact in self._getFacts(CONCEPT_REPORT_PRINCIPAL_LANGUAGE):
690
- if fact is None or fact.context is None:
691
- continue
692
- for qname, value in fact.context.qnameDims.items():
693
- if qname.localName == CONCEPT_LANGUAGES_DIMENSION:
694
- if value.xValue.localName == CONCEPT_WELSH:
695
- return HmrcLang.WELSH
687
+ if any(
688
+ lang == CONCEPT_WELSH
689
+ for lang in self._getDimensionValues(CONCEPT_REPORT_PRINCIPAL_LANGUAGE, CONCEPT_LANGUAGES_DIMENSION)
690
+ ):
691
+ return HmrcLang.WELSH
696
692
  return HmrcLang.ENGLISH
697
693
 
698
694
  def _yieldErrorOrWarning(self, code: str, result: CodeResult) -> None:
@@ -725,47 +721,19 @@ class ValidateUK:
725
721
 
726
722
  @cached_property
727
723
  def accountStatus(self) -> str | None:
728
- facts = self._getFacts(CONCEPT_ACCOUNTS_STATUS)
729
- for fact in facts:
730
- if not self._checkValidFact(fact):
731
- continue
732
- for qname, value in fact.context.qnameDims.items():
733
- if qname.localName == CONCEPT_ACCOUNTS_STATUS_DIMENSION:
734
- return cast(str, value.xValue.localName)
735
- return None
724
+ return next(iter(self._getDimensionValues(CONCEPT_ACCOUNTS_STATUS, CONCEPT_ACCOUNTS_STATUS_DIMENSION)), None)
736
725
 
737
726
  @cached_property
738
727
  def accountsType(self) -> str | None:
739
- facts = self._getFacts(CONCEPT_ACCOUNTS_TYPE_FULL_OR_ABBREVIATED)
740
- for fact in facts:
741
- if not self._checkValidFact(fact):
742
- continue
743
- for qname, value in fact.context.qnameDims.items():
744
- if qname.localName == CONCEPT_ACCOUNTS_TYPE_DIMENSION:
745
- return cast(str, value.xValue.localName)
746
- return None
728
+ return next(iter(self._getDimensionValues(CONCEPT_ACCOUNTS_TYPE_FULL_OR_ABBREVIATED, CONCEPT_ACCOUNTS_TYPE_DIMENSION)), None)
747
729
 
748
730
  @cached_property
749
731
  def accountingStandardsApplied(self) -> str | None:
750
- facts = self._getFacts(CONCEPT_ACCOUNTING_STANDARDS_APPLIED)
751
- for fact in facts:
752
- if not self._checkValidFact(fact):
753
- continue
754
- for qname, value in fact.context.qnameDims.items():
755
- if qname.localName == CONCEPT_ACCOUNTING_STANDARDS_DIMENSION:
756
- return cast(str, value.xValue.localName)
757
- return None
732
+ return next(iter(self._getDimensionValues(CONCEPT_ACCOUNTING_STANDARDS_APPLIED, CONCEPT_ACCOUNTING_STANDARDS_DIMENSION)), None)
758
733
 
759
734
  @cached_property
760
735
  def applicableLegislation(self) -> str | None:
761
- facts = self._getFacts(CONCEPT_APPLICABLE_LEGISLATION)
762
- for fact in facts:
763
- if not self._checkValidFact(fact):
764
- continue
765
- for qname, value in fact.context.qnameDims.items():
766
- if qname.localName == CONCEPT_APPLICABLE_LEGISLATION_DIMENSION:
767
- return cast(str, value.xValue.localName)
768
- return None
736
+ return next(iter(self._getDimensionValues(CONCEPT_APPLICABLE_LEGISLATION, CONCEPT_APPLICABLE_LEGISLATION_DIMENSION)), None)
769
737
 
770
738
  @cached_property
771
739
  def isEntityDormant(self) -> bool:
@@ -776,25 +744,22 @@ class ValidateUK:
776
744
 
777
745
  @cached_property
778
746
  def legalFormEntity(self) -> str | None:
779
- facts = self._getFacts(CONCEPT_LEGAL_FORM_ENTIY)
780
- for fact in facts:
781
- if not self._checkValidFact(fact):
782
- continue
783
- for qname, value in fact.context.qnameDims.items():
784
- if qname.localName == CONCEPT_LEGAL_FORM_ENTIY_DIMENSION:
785
- return cast(str, value.xValue.localName)
786
- return None
747
+ return next(iter(self._getDimensionValues(CONCEPT_LEGAL_FORM_ENTIY, CONCEPT_LEGAL_FORM_ENTIY_DIMENSION)), None)
787
748
 
788
749
  @cached_property
789
750
  def scopeAccounts(self) -> str | None:
790
- facts = self._getFacts(CONCEPT_SCOPE_ACCOUNTS)
751
+ return next(iter(self._getDimensionValues(CONCEPT_SCOPE_ACCOUNTS, CONCEPT_SCOPE_ACCOUNTS_DIMENSION)), None)
752
+
753
+ def _getDimensionValues(self, conceptLocalName: str, dimensionLocalName: str) -> Iterable[str]:
754
+ facts = self._getFacts(conceptLocalName)
791
755
  for fact in facts:
792
756
  if not self._checkValidFact(fact):
793
757
  continue
794
758
  for qname, value in fact.context.qnameDims.items():
795
- if qname.localName == CONCEPT_SCOPE_ACCOUNTS_DIMENSION:
796
- return cast(str, value.xValue.localName)
797
- return None
759
+ if value.xValid < VALID:
760
+ continue
761
+ if qname.localName == dimensionLocalName:
762
+ yield cast(str, value.xValue.localName)
798
763
 
799
764
  def validate(self) -> None:
800
765
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arelle-release
3
- Version: 2.37.48
3
+ Version: 2.37.50
4
4
  Summary: An open source XBRL platform.
5
5
  Author-email: "arelle.org" <support@arelle.org>
6
6
  License-Expression: Apache-2.0
@@ -31,34 +31,38 @@ License-File: LICENSE.md
31
31
  Requires-Dist: bottle<0.14,>=0.13
32
32
  Requires-Dist: certifi
33
33
  Requires-Dist: filelock
34
- Requires-Dist: jsonschema==4.*
35
- Requires-Dist: isodate==0.*
36
- Requires-Dist: lxml<6,>=4
34
+ Requires-Dist: isodate<1,>=0
35
+ Requires-Dist: jsonschema<5,>=4
36
+ Requires-Dist: lxml!=6.0.0,<7,>=4
37
37
  Requires-Dist: numpy<3,>=1
38
- Requires-Dist: openpyxl==3.*
38
+ Requires-Dist: openpyxl<4,>=3
39
39
  Requires-Dist: pillow<12,>=10
40
- Requires-Dist: pyparsing==3.*
41
- Requires-Dist: python-dateutil==2.*
40
+ Requires-Dist: pyparsing<4,>=3
41
+ Requires-Dist: python-dateutil<3,>=2
42
42
  Requires-Dist: regex
43
- Requires-Dist: truststore==0.*; python_version > "3.9"
44
- Requires-Dist: typing-extensions==4.*
43
+ Requires-Dist: truststore<1,>=0; python_version > "3.9"
44
+ Requires-Dist: typing-extensions<5,>=4
45
45
  Provides-Extra: crypto
46
- Requires-Dist: pycryptodome==3.*; extra == "crypto"
46
+ Requires-Dist: pycryptodome<4,>=3; extra == "crypto"
47
47
  Provides-Extra: db
48
- Requires-Dist: pg8000==1.*; extra == "db"
49
- Requires-Dist: PyMySQL==1.*; extra == "db"
48
+ Requires-Dist: pg8000<2,>=1; extra == "db"
49
+ Requires-Dist: PyMySQL<2,>=1; extra == "db"
50
50
  Requires-Dist: pyodbc<6,>=4; extra == "db"
51
51
  Requires-Dist: rdflib<8,>=5; extra == "db"
52
52
  Provides-Extra: efm
53
- Requires-Dist: holidays==0.*; extra == "efm"
53
+ Requires-Dist: aniso8601<11,>=10; extra == "efm"
54
+ Requires-Dist: holidays<1,>=0; extra == "efm"
55
+ Requires-Dist: matplotlib<4,>=3; extra == "efm"
54
56
  Requires-Dist: pytz; extra == "efm"
55
57
  Provides-Extra: esef
56
- Requires-Dist: tinycss2==1.*; extra == "esef"
58
+ Requires-Dist: tinycss2<2,>=1; extra == "esef"
57
59
  Provides-Extra: objectmaker
58
- Requires-Dist: graphviz==0.*; extra == "objectmaker"
60
+ Requires-Dist: graphviz<1,>=0; extra == "objectmaker"
59
61
  Provides-Extra: webserver
60
62
  Requires-Dist: cheroot<11,>=8; extra == "webserver"
61
- Requires-Dist: tornado==6.*; extra == "webserver"
63
+ Requires-Dist: tornado<7,>=6; extra == "webserver"
64
+ Provides-Extra: xule
65
+ Requires-Dist: aniso8601<11,>=10; extra == "xule"
62
66
  Dynamic: license-file
63
67
 
64
68
  # Arelle