arelle-release 2.37.46__py3-none-any.whl → 2.38.0__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.
Files changed (204) hide show
  1. arelle/BetaFeatures.py +0 -21
  2. arelle/Cntlr.py +15 -8
  3. arelle/CntlrCmdLine.py +121 -56
  4. arelle/CntlrWinMain.py +143 -70
  5. arelle/DialogFind.py +1 -1
  6. arelle/DialogPluginManager.py +6 -4
  7. arelle/DisclosureSystem.py +7 -0
  8. arelle/ErrorManager.py +21 -6
  9. arelle/FileSource.py +11 -4
  10. arelle/FunctionIxt.py +16 -11
  11. arelle/HtmlUtil.py +5 -4
  12. arelle/LeiUtil.py +63 -43
  13. arelle/ModelDocument.py +20 -15
  14. arelle/ModelDtsObject.py +8 -0
  15. arelle/ModelInstanceObject.py +1 -1
  16. arelle/ModelObject.py +16 -18
  17. arelle/ModelObjectFactory.py +35 -17
  18. arelle/ModelXbrl.py +28 -11
  19. arelle/PluginManager.py +130 -105
  20. arelle/RuntimeOptions.py +1 -0
  21. arelle/UrlUtil.py +14 -0
  22. arelle/Validate.py +17 -12
  23. arelle/ValidateDuplicateFacts.py +3 -1
  24. arelle/ValidateFileSource.py +38 -0
  25. arelle/ValidateFilingText.py +3 -3
  26. arelle/ValidateXbrl.py +5 -2
  27. arelle/ValidateXbrlCalcs.py +210 -186
  28. arelle/ValidateXbrlDTS.py +1 -1
  29. arelle/ViewFile.py +1 -0
  30. arelle/ViewFileFactTable.py +2 -2
  31. arelle/ViewWinDTS.py +4 -1
  32. arelle/WebCache.py +28 -24
  33. arelle/XbrlConst.py +22 -0
  34. arelle/XmlUtil.py +16 -21
  35. arelle/XmlValidate.py +6 -9
  36. arelle/_version.py +16 -3
  37. arelle/api/Session.py +11 -2
  38. arelle/config/disclosuresystems.xsd +2 -0
  39. arelle/config/rosettaEntitlements.plist +8 -0
  40. arelle/conformance/CSVTestcaseLoader.py +1 -1
  41. arelle/formula/XPathContext.py +3 -3
  42. arelle/logging/formatters/LogFormatter.py +3 -1
  43. arelle/packages/report/ReportPackage.py +26 -13
  44. arelle/packages/report/ReportPackageConst.py +0 -1
  45. arelle/plugin/inlineXbrlDocumentSet.py +19 -5
  46. arelle/plugin/validate/DBA/DisclosureSystems.py +19 -1
  47. arelle/plugin/validate/DBA/PluginValidationDataExtension.py +2 -4
  48. arelle/plugin/validate/DBA/ValidationPluginExtension.py +2 -1
  49. arelle/plugin/validate/DBA/resources/config.xml +5 -0
  50. arelle/plugin/validate/DBA/rules/__init__.py +2 -2
  51. arelle/plugin/validate/DBA/rules/fr.py +19 -2
  52. arelle/plugin/validate/DBA/rules/tc.py +2 -0
  53. arelle/plugin/validate/DBA/rules/th.py +6 -0
  54. arelle/plugin/validate/DBA/rules/tm.py +18 -5
  55. arelle/plugin/validate/DBA/rules/tr.py +11 -5
  56. arelle/plugin/validate/EDINET/Constants.py +193 -9
  57. arelle/plugin/validate/EDINET/ContextRequirement.py +58 -0
  58. arelle/plugin/validate/EDINET/ControllerPluginData.py +220 -1
  59. arelle/plugin/validate/EDINET/CoverItemRequirements.py +42 -0
  60. arelle/plugin/validate/EDINET/DeiRequirements.py +118 -0
  61. arelle/plugin/validate/EDINET/FilingFormat.py +275 -0
  62. arelle/plugin/validate/EDINET/FormType.py +134 -0
  63. arelle/plugin/validate/EDINET/ManifestInstance.py +72 -5
  64. arelle/plugin/validate/EDINET/NamespaceConfig.py +50 -0
  65. arelle/plugin/validate/EDINET/PluginValidationDataExtension.py +493 -132
  66. arelle/plugin/validate/EDINET/{InstanceType.py → ReportFolderType.py} +72 -15
  67. arelle/plugin/validate/EDINET/Statement.py +139 -0
  68. arelle/plugin/validate/EDINET/TableOfContentsBuilder.py +595 -0
  69. arelle/plugin/validate/EDINET/UploadContents.py +48 -0
  70. arelle/plugin/validate/EDINET/ValidationPluginExtension.py +20 -2
  71. arelle/plugin/validate/EDINET/__init__.py +31 -6
  72. arelle/plugin/validate/EDINET/resources/config.xml +8 -1
  73. arelle/plugin/validate/EDINET/resources/cover-item-requirements.json +793 -0
  74. arelle/plugin/validate/EDINET/resources/dei-requirements.csv +27 -0
  75. arelle/plugin/validate/EDINET/resources/edinet-taxonomies.xml +2 -0
  76. arelle/plugin/validate/EDINET/rules/contexts.py +375 -14
  77. arelle/plugin/validate/EDINET/rules/edinet.py +1934 -45
  78. arelle/plugin/validate/EDINET/rules/frta.py +122 -3
  79. arelle/plugin/validate/EDINET/rules/gfm.py +1907 -11
  80. arelle/plugin/validate/EDINET/rules/upload.py +989 -141
  81. arelle/plugin/validate/ESEF/Const.py +3 -1
  82. arelle/plugin/validate/ESEF/ESEF_2021/DTS.py +5 -0
  83. arelle/plugin/validate/ESEF/ESEF_2021/Image.py +2 -2
  84. arelle/plugin/validate/ESEF/ESEF_2021/ValidateXbrlFinally.py +23 -20
  85. arelle/plugin/validate/ESEF/ESEF_Current/DTS.py +47 -14
  86. arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +100 -25
  87. arelle/plugin/validate/ESEF/__init__.py +20 -6
  88. arelle/plugin/validate/ESEF/resources/authority-validations.json +76 -9
  89. arelle/plugin/validate/ESEF/resources/config.xml +20 -0
  90. arelle/plugin/validate/NL/DisclosureSystems.py +22 -0
  91. arelle/plugin/validate/NL/PluginValidationDataExtension.py +27 -9
  92. arelle/plugin/validate/NL/ValidationPluginExtension.py +51 -7
  93. arelle/plugin/validate/NL/resources/config.xml +18 -0
  94. arelle/plugin/validate/NL/rules/br_kvk.py +17 -61
  95. arelle/plugin/validate/NL/rules/fg_nl.py +7 -38
  96. arelle/plugin/validate/NL/rules/fr_kvk.py +7 -42
  97. arelle/plugin/validate/NL/rules/fr_nl.py +31 -147
  98. arelle/plugin/validate/NL/rules/nl_kvk.py +142 -28
  99. arelle/plugin/validate/ROS/PluginValidationDataExtension.py +2 -0
  100. arelle/plugin/validate/ROS/ValidationPluginExtension.py +4 -1
  101. arelle/plugin/validate/ROS/rules/ros.py +41 -9
  102. arelle/plugin/validate/UK/ValidateUK.py +130 -66
  103. arelle/plugin/validate/UK/__init__.py +89 -103
  104. arelle/utils/EntryPointDetection.py +79 -13
  105. arelle/utils/PluginHooks.py +125 -0
  106. arelle/utils/validate/ESEFImage.py +6 -6
  107. arelle/utils/validate/Validation.py +18 -0
  108. arelle/utils/validate/ValidationPlugin.py +76 -11
  109. arelle/utils/validate/ValidationUtil.py +35 -3
  110. {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/METADATA +30 -20
  111. {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/RECORD +115 -191
  112. {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/licenses/LICENSE.md +0 -3
  113. arelle/archive/CustomLogger.py +0 -43
  114. arelle/archive/LoadEFMvalidate.py +0 -32
  115. arelle/archive/LoadSavePreLbCsv.py +0 -26
  116. arelle/archive/LoadValidate.cs +0 -31
  117. arelle/archive/LoadValidate.py +0 -36
  118. arelle/archive/LoadValidateCmdLine.java +0 -69
  119. arelle/archive/LoadValidatePostedZip.java +0 -57
  120. arelle/archive/LoadValidateWebService.java +0 -34
  121. arelle/archive/SaveTableToExelle.py +0 -140
  122. arelle/archive/TR3toTR4.py +0 -88
  123. arelle/archive/plugin/ESEF_2022/__init__.py +0 -47
  124. arelle/archive/plugin/bigInstance.py +0 -394
  125. arelle/archive/plugin/cmdWebServerExtension.py +0 -43
  126. arelle/archive/plugin/crashTest.py +0 -38
  127. arelle/archive/plugin/functionsXmlCreation.py +0 -106
  128. arelle/archive/plugin/hello_i18n.pot +0 -26
  129. arelle/archive/plugin/hello_i18n.py +0 -32
  130. arelle/archive/plugin/importTestChild1.py +0 -21
  131. arelle/archive/plugin/importTestChild2.py +0 -22
  132. arelle/archive/plugin/importTestGrandchild1.py +0 -21
  133. arelle/archive/plugin/importTestGrandchild2.py +0 -21
  134. arelle/archive/plugin/importTestImported1.py +0 -23
  135. arelle/archive/plugin/importTestImported11.py +0 -22
  136. arelle/archive/plugin/importTestParent.py +0 -48
  137. arelle/archive/plugin/instanceInfo.py +0 -306
  138. arelle/archive/plugin/loadFromOIM-2018.py +0 -1282
  139. arelle/archive/plugin/locale/fr/LC_MESSAGES/hello_i18n.po +0 -25
  140. arelle/archive/plugin/objectmaker.py +0 -285
  141. arelle/archive/plugin/packagedImportTest/__init__.py +0 -47
  142. arelle/archive/plugin/packagedImportTest/importTestChild1.py +0 -21
  143. arelle/archive/plugin/packagedImportTest/importTestChild2.py +0 -22
  144. arelle/archive/plugin/packagedImportTest/importTestGrandchild1.py +0 -21
  145. arelle/archive/plugin/packagedImportTest/importTestGrandchild2.py +0 -21
  146. arelle/archive/plugin/packagedImportTest/importTestImported1.py +0 -24
  147. arelle/archive/plugin/packagedImportTest/importTestImported11.py +0 -21
  148. arelle/archive/plugin/packagedImportTest/subdir/importTestImported111.py +0 -21
  149. arelle/archive/plugin/packagedImportTest/subdir/subsubdir/importTestImported1111.py +0 -21
  150. arelle/archive/plugin/sakaCalendar.py +0 -215
  151. arelle/archive/plugin/saveInstanceInfoset.py +0 -121
  152. arelle/archive/plugin/sphinx/FormulaGenerator.py +0 -823
  153. arelle/archive/plugin/sphinx/SphinxContext.py +0 -404
  154. arelle/archive/plugin/sphinx/SphinxEvaluator.py +0 -783
  155. arelle/archive/plugin/sphinx/SphinxMethods.py +0 -1287
  156. arelle/archive/plugin/sphinx/SphinxParser.py +0 -1093
  157. arelle/archive/plugin/sphinx/SphinxValidator.py +0 -163
  158. arelle/archive/plugin/sphinx/US-GAAP Ratios Example.xsr +0 -52
  159. arelle/archive/plugin/sphinx/__init__.py +0 -285
  160. arelle/archive/plugin/streamingExtensions.py +0 -335
  161. arelle/archive/plugin/updateTableLB.py +0 -242
  162. arelle/archive/plugin/validate/SBRnl/CustomLoader.py +0 -19
  163. arelle/archive/plugin/validate/SBRnl/DTS.py +0 -305
  164. arelle/archive/plugin/validate/SBRnl/Dimensions.py +0 -357
  165. arelle/archive/plugin/validate/SBRnl/Document.py +0 -799
  166. arelle/archive/plugin/validate/SBRnl/Filing.py +0 -467
  167. arelle/archive/plugin/validate/SBRnl/__init__.py +0 -75
  168. arelle/archive/plugin/validate/SBRnl/config.xml +0 -26
  169. arelle/archive/plugin/validate/SBRnl/sbr-nl-taxonomies.xml +0 -754
  170. arelle/archive/plugin/validate/USBestPractices.py +0 -570
  171. arelle/archive/plugin/validate/USCorpAction.py +0 -557
  172. arelle/archive/plugin/validate/USSecTagging.py +0 -337
  173. arelle/archive/plugin/validate/XDC/__init__.py +0 -77
  174. arelle/archive/plugin/validate/XDC/config.xml +0 -20
  175. arelle/archive/plugin/validate/XFsyntax/__init__.py +0 -64
  176. arelle/archive/plugin/validate/XFsyntax/xf.py +0 -2227
  177. arelle/archive/plugin/validate/calc2.py +0 -536
  178. arelle/archive/plugin/validateSchemaLxml.py +0 -156
  179. arelle/archive/plugin/validateTableInfoset.py +0 -52
  180. arelle/archive/us-gaap-dei-docType-extraction-frm.xml +0 -90
  181. arelle/archive/us-gaap-dei-ratio-cash-frm.xml +0 -150
  182. arelle/examples/plugin/formulaSuiteConverter.py +0 -212
  183. arelle/examples/plugin/functionsCustom.py +0 -59
  184. arelle/examples/plugin/hello_dolly.py +0 -64
  185. arelle/examples/plugin/multi.py +0 -58
  186. arelle/examples/plugin/rssSaveOim.py +0 -96
  187. arelle/examples/plugin/validate/XYZ/DisclosureSystems.py +0 -2
  188. arelle/examples/plugin/validate/XYZ/PluginValidationDataExtension.py +0 -10
  189. arelle/examples/plugin/validate/XYZ/ValidationPluginExtension.py +0 -49
  190. arelle/examples/plugin/validate/XYZ/__init__.py +0 -75
  191. arelle/examples/plugin/validate/XYZ/resources/config.xml +0 -16
  192. arelle/examples/plugin/validate/XYZ/rules/__init__.py +0 -0
  193. arelle/examples/plugin/validate/XYZ/rules/rules01.py +0 -110
  194. arelle/examples/plugin/validate/XYZ/rules/rules02.py +0 -59
  195. arelle/model/CommentBase.py +0 -9
  196. arelle/model/ElementBase.py +0 -11
  197. arelle/model/PIBase.py +0 -10
  198. arelle/model/__init__.py +0 -15
  199. arelle/scripts-macOS/startWebServer.command +0 -3
  200. arelle/scripts-unix/startWebServer.sh +0 -1
  201. arelle/scripts-windows/startWebServer.bat +0 -5
  202. {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/WHEEL +0 -0
  203. {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/entry_points.txt +0 -0
  204. {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/top_level.txt +0 -0
@@ -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'
@@ -24,6 +25,7 @@ CO_AUDIT = 'Co.Audit'
24
25
  CO_AUDIT_NR = 'Co.AuditNR'
25
26
  CO_DIR_REP = 'Co.DirReport'
26
27
  CO_DIR_RESP = 'Co.DirResp'
28
+ CO_GROUP = 'Co.Group'
27
29
  CO_MED_CO = 'Co.MedCo'
28
30
  CO_MICRO = 'Co.Micro'
29
31
  CO_MISSING_ELEMENT = 'Co.MissingElement'
@@ -34,6 +36,7 @@ CO_SEC_477 = 'Co.Sec477'
34
36
  CO_SEC_480 = 'Co.Sec480'
35
37
  LP_ABRID = 'Lp.Abrid'
36
38
  LP_AUDIT = 'Lp.Audit'
39
+ LP_GROUP = 'Lp.Group'
37
40
  LP_MED_LP = 'Lp.MedLp'
38
41
  LP_MEM_RESP = 'Lp.MemResp'
39
42
  LP_MICRO = 'Lp.Micro'
@@ -56,19 +59,23 @@ CONCEPT_ACCOUNTS_STATUS_DIMENSION = 'AccountsStatusDimension'
56
59
  CONCEPT_ACCOUNTS_TYPE_FULL_OR_ABBREVIATED = 'AccountsTypeFullOrAbbreviated' # DEPRECATED IN 2022+ taxonomies. No replacement yet.
57
60
  CONCEPT_ACCOUNTS_TYPE_DIMENSION = 'AccountsTypeDimension'
58
61
  CONCEPT_ADVERSE_OPINION = 'AdverseOpinion'
62
+ CONCEPT_BALANCE_SHEET_DATE = 'BalanceSheetDate'
59
63
  CONCEPT_CHARITY_FUNDS = 'CharityFunds'
60
64
  CONCEPT_CHARITY_REGISTRATION_NUMBER_ENGLAND_WALES = 'CharityRegistrationNumberEnglandWales'
61
65
  CONCEPT_CHARITY_REGISTRATION_NUMBER_NORTH_IRELAND = 'CharityRegistrationNumberNorthernIreland'
62
66
  CONCEPT_CHARITY_REGISTRATION_NUMBER_SCOTLAND = 'CharityRegistrationNumberScotland'
67
+ CONCEPT_CONSOLIDATED = 'Consolidated'
63
68
  CONCEPT_DATE_AUDITOR_REPORT = 'DateAuditorsReport'
64
69
  CONCEPT_DATE_CHARITY_AUDITORS_REPORT = 'DateCharityAuditorsReport'
65
70
  CONCEPT_DATE_SIGNING_DIRECTOR_REPORT = 'DateSigningDirectorsReport'
66
71
  CONCEPT_DATE_SIGNING_TRUSTEES_REPORT = 'DateSigningTrusteesAnnualReport'
67
72
  CONCEPT_DIRECTOR_SIGNING_DIRECTORS_REPORT = 'DirectorSigningDirectorsReport'
68
73
  CONCEPT_DISCLAIMER_OPINION = 'DisclaimerOpinion'
74
+ CONCEPT_END_DATE_FOR_PERIOD_COVERED_BY_REPORT = 'EndDateForPeriodCoveredByReport'
69
75
  CONCEPT_ENTITY_DORMANT = 'EntityDormantTruefalse'
70
76
  CONCEPT_ENTITY_TRADING_STATUS = 'EntityTradingStatus'
71
77
  CONCEPT_ENTITY_TRADING_STATUS_DIMENSION = 'EntityTradingStatusDimension'
78
+ CONCEPT_GROUP_COMPANY_DATA_DIMENSION = 'GroupCompanyDataDimension'
72
79
  CONCEPT_LANGUAGES_DIMENSION = 'LanguagesDimension'
73
80
  CONCEPT_MEDIUM_COMPANY = 'StatementThatCompanyHasPreparedAccountsUnderProvisionsRelatingToMedium-sizedCompanies'
74
81
  CONCEPT_MEDIUM_COMPANIES_REGIME_FOR_ACCOUNTS = 'Medium-sizedCompaniesRegimeForAccounts'
@@ -430,10 +437,11 @@ class ValidateUK:
430
437
  _codeResultMap: dict[str, CodeResult] = field(default_factory=dict)
431
438
 
432
439
  def _checkValidFact(self, fact: ModelFact ) -> bool:
433
- if fact is not None:
434
- if not fact.isNil:
435
- return True
436
- return False
440
+ return (
441
+ fact is not None and
442
+ not fact.isNil and
443
+ fact.context is not None
444
+ )
437
445
 
438
446
  def _errorOnMissingFact(self, conceptLocalName: str) -> None:
439
447
  """
@@ -628,16 +636,12 @@ class ValidateUK:
628
636
  elif code == CH_CHAR_FUND:
629
637
  concept = CONCEPT_CHARITY_FUNDS
630
638
  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
639
+ for value in self._getDimensionValues(CONCEPT_ENTITY_TRADING_STATUS, CONCEPT_ENTITY_TRADING_STATUS_DIMENSION):
640
+ if value in {
641
+ NotTrading.CONCEPT_ENTITY_NO_LONGER_TRADING.value,
642
+ NotTrading.CONCEPT_ENTITY_HAS_NEVER_TRADED.value,
643
+ }:
644
+ trading = True
641
645
  if not self._getAndCheckValidFacts([concept]) and not trading:
642
646
  return CodeResult(
643
647
  conceptLocalName=concept,
@@ -648,6 +652,39 @@ class ValidateUK:
648
652
  )
649
653
  return CodeResult()
650
654
 
655
+ def _evaluateGroupFacts(self) -> CodeResult:
656
+ """
657
+ BalanceSheetDate with GroupCompanyDataDimension and Consolidated must equal BalanceSheetDate with default
658
+ dimensions. Both facts must be non-nil.
659
+ """
660
+ consolidatedFact = None
661
+ defaultFact = None
662
+ endDateFact = None
663
+ for fact in self._getFacts(CONCEPT_END_DATE_FOR_PERIOD_COVERED_BY_REPORT):
664
+ if not fact.context.qnameDims:
665
+ endDateFact = fact
666
+ break
667
+ if endDateFact is None:
668
+ return CodeResult()
669
+ for balanceSheetDateFact in self._getFacts(CONCEPT_BALANCE_SHEET_DATE):
670
+ if self._checkValidFact(balanceSheetDateFact):
671
+ if not balanceSheetDateFact.context.qnameDims and balanceSheetDateFact.context.instantDate == endDateFact.context.instantDate:
672
+ defaultFact = balanceSheetDateFact
673
+ for qname, value in balanceSheetDateFact.context.qnameDims.items():
674
+ if value.xValid < VALID:
675
+ continue
676
+ if qname.localName == CONCEPT_GROUP_COMPANY_DATA_DIMENSION and cast(str, value.xValue.localName) == CONCEPT_CONSOLIDATED:
677
+ consolidatedFact = balanceSheetDateFact
678
+ break
679
+ if consolidatedFact is None or defaultFact is None or consolidatedFact.xValue != defaultFact.xValue:
680
+ return CodeResult(
681
+ conceptLocalName=CONCEPT_BALANCE_SHEET_DATE,
682
+ success=False,
683
+ message="A fact tagged with BalanceSheetDate with the dimension of GroupCompanyDataDimension/Consolidated "
684
+ "must equal a fact tagged with BalanceSheetDate with the default dimension."
685
+ )
686
+ return CodeResult()
687
+
651
688
  def _evaluateCode(self, code: str) -> CodeResult:
652
689
  """
653
690
  Evaluates whether the conditions associated with the given code pass.
@@ -669,6 +706,8 @@ class ValidateUK:
669
706
  result = self._evaluateProfLossOrCharityFundsFact(code)
670
707
  elif code == CH_AUDIT:
671
708
  result = self._evaluateCharAuditFacts()
709
+ elif code in (CO_GROUP, LP_GROUP):
710
+ result = self._evaluateGroupFacts()
672
711
  return self._setCode(code, result)
673
712
 
674
713
  def _getFacts(self, conceptLocalName: str) -> list[ModelFact]:
@@ -686,13 +725,11 @@ class ValidateUK:
686
725
  """
687
726
  Determines if the language is set to Welsh, otherwise defaults to English.
688
727
  """
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
728
+ if any(
729
+ lang == CONCEPT_WELSH
730
+ for lang in self._getDimensionValues(CONCEPT_REPORT_PRINCIPAL_LANGUAGE, CONCEPT_LANGUAGES_DIMENSION)
731
+ ):
732
+ return HmrcLang.WELSH
696
733
  return HmrcLang.ENGLISH
697
734
 
698
735
  def _yieldErrorOrWarning(self, code: str, result: CodeResult) -> None:
@@ -725,47 +762,19 @@ class ValidateUK:
725
762
 
726
763
  @cached_property
727
764
  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
765
+ return next(iter(self._getDimensionValues(CONCEPT_ACCOUNTS_STATUS, CONCEPT_ACCOUNTS_STATUS_DIMENSION)), None)
736
766
 
737
767
  @cached_property
738
768
  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
769
+ return next(iter(self._getDimensionValues(CONCEPT_ACCOUNTS_TYPE_FULL_OR_ABBREVIATED, CONCEPT_ACCOUNTS_TYPE_DIMENSION)), None)
747
770
 
748
771
  @cached_property
749
772
  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
773
+ return next(iter(self._getDimensionValues(CONCEPT_ACCOUNTING_STANDARDS_APPLIED, CONCEPT_ACCOUNTING_STANDARDS_DIMENSION)), None)
758
774
 
759
775
  @cached_property
760
776
  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
777
+ return next(iter(self._getDimensionValues(CONCEPT_APPLICABLE_LEGISLATION, CONCEPT_APPLICABLE_LEGISLATION_DIMENSION)), None)
769
778
 
770
779
  @cached_property
771
780
  def isEntityDormant(self) -> bool:
@@ -776,25 +785,22 @@ class ValidateUK:
776
785
 
777
786
  @cached_property
778
787
  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
788
+ return next(iter(self._getDimensionValues(CONCEPT_LEGAL_FORM_ENTIY, CONCEPT_LEGAL_FORM_ENTIY_DIMENSION)), None)
787
789
 
788
790
  @cached_property
789
791
  def scopeAccounts(self) -> str | None:
790
- facts = self._getFacts(CONCEPT_SCOPE_ACCOUNTS)
792
+ return next(iter(self._getDimensionValues(CONCEPT_SCOPE_ACCOUNTS, CONCEPT_SCOPE_ACCOUNTS_DIMENSION)), None)
793
+
794
+ def _getDimensionValues(self, conceptLocalName: str, dimensionLocalName: str) -> Iterable[str]:
795
+ facts = self._getFacts(conceptLocalName)
791
796
  for fact in facts:
792
797
  if not self._checkValidFact(fact):
793
798
  continue
794
799
  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
800
+ if value.xValid < VALID:
801
+ continue
802
+ if qname.localName == dimensionLocalName:
803
+ yield cast(str, value.xValue.localName)
798
804
 
799
805
  def validate(self) -> None:
800
806
  """
@@ -866,6 +872,14 @@ class ValidateUK:
866
872
  self.validateAuditedOtherLLP()
867
873
  else:
868
874
  self.validateAuditedOtherCompany()
875
+ elif self.scopeAccounts in {
876
+ ScopeAccounts.GROUP_ONLY.value,
877
+ ScopeAccounts.CONSOLIDATED_GROUP.value,
878
+ }:
879
+ if self.legalFormEntity == CONCEPT_LLP:
880
+ self.validateAuditedGroupLLP()
881
+ else:
882
+ self.validateAuditedGroupCompany()
869
883
 
870
884
  def validateCharities(self) -> None:
871
885
  """
@@ -1306,3 +1320,53 @@ class ValidateUK:
1306
1320
  result = self._evaluateCode(CO_SM_CO)
1307
1321
  if not result.success:
1308
1322
  self._errorOnMissingFactText(CO_SM_CO, result)
1323
+
1324
+ def validateAuditedGroupCompany(self) -> None:
1325
+ """
1326
+ Checks conditions applicable to audited group companies:
1327
+ Co.Audit AND ((Co.DirReport AND Co.ProfLoss AND Co.Group) OR Co.SmCo).
1328
+ """
1329
+ result = self._evaluateCode(CO_AUDIT)
1330
+ if not result.success:
1331
+ self._yieldErrorOrWarning(CO_AUDIT, result)
1332
+
1333
+ result = self._evaluateCode(CO_SM_CO)
1334
+ if not result.success:
1335
+ failedOr = False
1336
+ dirRepResult = self._evaluateCode(CO_DIR_REP)
1337
+ if not dirRepResult.success:
1338
+ self._yieldErrorOrWarning(CO_DIR_REP, dirRepResult)
1339
+ failedOr = True
1340
+ profLossResult = self._evaluateCode(CO_PROF_LOSS)
1341
+ if not profLossResult.success:
1342
+ self._yieldErrorOrWarning(CO_PROF_LOSS, profLossResult)
1343
+ failedOr = True
1344
+ groupResult = self._evaluateCode(CO_GROUP)
1345
+ if not groupResult.success:
1346
+ self._yieldErrorOrWarning(CO_GROUP, groupResult)
1347
+ failedOr = True
1348
+ if failedOr:
1349
+ self._errorOnMissingFactText(CO_SM_CO, result)
1350
+
1351
+ def validateAuditedGroupLLP(self) -> None:
1352
+ """
1353
+ Checks conditions applicable to audited group companies:
1354
+ LP.Audit AND ((LP.ProfLoss+LP.Group) OR LP.SmLp)
1355
+ """
1356
+ result = self._evaluateCode(LP_AUDIT)
1357
+ if not result.success:
1358
+ self._yieldErrorOrWarning(LP_AUDIT, result)
1359
+
1360
+ result = self._evaluateCode(LP_SM_LP)
1361
+ if not result.success:
1362
+ failedOr = False
1363
+ profLossResult = self._evaluateCode(LP_PROF_LOSS)
1364
+ if not profLossResult.success:
1365
+ self._yieldErrorOrWarning(LP_PROF_LOSS, profLossResult)
1366
+ failedOr = True
1367
+ groupResult = self._evaluateCode(LP_GROUP)
1368
+ if not groupResult.success:
1369
+ self._yieldErrorOrWarning(LP_GROUP, groupResult)
1370
+ failedOr = True
1371
+ if failedOr:
1372
+ self._errorOnMissingFactText(LP_SM_LP, result)
@@ -8,15 +8,16 @@ References:
8
8
  - [HMRC CT Inline XBRL Style Guide](https://www.gov.uk/government/uploads/system/uploads/attachment_data/file/434588/xbrl-style-guide.pdf)
9
9
  """
10
10
  import os
11
- from math import isnan
12
11
  from arelle import ModelDocument, XmlUtil
13
- from arelle.ModelValue import qname, dateTime, DATE
14
- from arelle.ValidateXbrlCalcs import inferredDecimals, rangeValue, insignificantDigits
12
+ from arelle.ModelValue import qname, dateTime, DATE, dateUnionEqual
13
+ from arelle.ValidateDuplicateFacts import getDuplicateFactSets
14
+ from arelle.ValidateXbrlCalcs import insignificantDigits
15
15
  from arelle.Version import authorLabel, copyrightLabel
16
16
  from arelle.XbrlConst import xbrli, qnXbrliXbrl
17
17
  import regex as re
18
18
  import tinycss2.ast
19
19
  from collections import defaultdict
20
+ from arelle.XmlValidate import VALID
20
21
 
21
22
  from .ValidateUK import ValidateUK
22
23
 
@@ -38,12 +39,18 @@ IMG_URL_CSS_PROPERTIES = frozenset([
38
39
  EMPTYDICT = {}
39
40
  _6_APR_2008 = dateTime("2008-04-06", type=DATE)
40
41
 
41
- COMMON_GENERIC_DIMENSIONS = {
42
+ GENERIC_DIMENSION_VALIDATIONS = {
43
+ # "LocalName": (range of numbers if any, first item name, 2nd choice item name if any)
44
+ "SpecificDiscontinuedOperation": (1, 8, "DescriptionDiscontinuedOperationOrNon-currentAssetsOrDisposalGroupHeldForSale"),
45
+ "SpecificNon-currentAssetsDisposalGroupHeldForSale": (1, 8, "DescriptionDiscontinuedOperationOrNon-currentAssetsOrDisposalGroupHeldForSale"),
42
46
  "Chairman": ("NameEntityOfficer",),
43
47
  "ChiefExecutive": ("NameEntityOfficer",),
44
48
  "ChairmanChiefExecutive": ("NameEntityOfficer",),
45
49
  "SeniorPartnerLimitedLiabilityPartnership": ("NameEntityOfficer",),
46
- "HighestPaidDirector": ("NameEntityOfficer",),
50
+ "CorporateTrustee": (1, 3, "NameEntityOfficer"),
51
+ "DirectorOfCorporateTrustee": ("NameEntityOfficer",),
52
+ "CustodianTrustee": ("NameEntityOfficer",),
53
+ "Trustee": (1, 20, "NameEntityOfficer",),
47
54
  "CompanySecretary": (1, 2, "NameEntityOfficer",),
48
55
  "CompanySecretaryDirector": (1, 2, "NameEntityOfficer",),
49
56
  "Director": (1, 40, "NameEntityOfficer",),
@@ -60,7 +67,7 @@ COMMON_GENERIC_DIMENSIONS = {
60
67
  "UnconsolidatedStructuredEntity": (1, 5, "NameUnconsolidatedStructuredEntity"),
61
68
  "IntermediateParent": (1, 5, "NameOrDescriptionRelatedPartyIfNotDefinedByAnotherTag"),
62
69
  "EntityWithJointControlOrSignificantInfluence": (1, 5, "NameOrDescriptionRelatedPartyIfNotDefinedByAnotherTag"),
63
- "AnotherGroupMember": (1, 8, "NameOrDescriptionRelatedPartyIfNotDefinedByAnotherTag"),
70
+ "OtherGroupMember": (1, 8, "NameOrDescriptionRelatedPartyIfNotDefinedByAnotherTag"),
64
71
  "KeyManagementIndividualGroup": (1, 5, "NameOrDescriptionRelatedPartyIfNotDefinedByAnotherTag"),
65
72
  "CloseFamilyMember": (1, 5, "NameOrDescriptionRelatedPartyIfNotDefinedByAnotherTag"),
66
73
  "EntityControlledByKeyManagementPersonnel": (1, 5, "NameOrDescriptionRelatedPartyIfNotDefinedByAnotherTag"),
@@ -77,6 +84,7 @@ COMMON_GENERIC_DIMENSIONS = {
77
84
  "OtherPost-employmentBenefitPlan": (1, 2, "NameDefinedContributionPlan", "NameDefinedBenefitPlan"),
78
85
  "OtherContractType": (1, 2, "DescriptionOtherContractType"),
79
86
  "OtherDurationType": (1, 2, "DescriptionOtherContractDurationType"),
87
+ "OtherChannelType": (1, 2, "DescriptionOtherSalesChannelType"),
80
88
  "SalesChannel": (1, 2, "DescriptionOtherSalesChannelType"),
81
89
  }
82
90
 
@@ -115,33 +123,6 @@ MUST_HAVE_ONE_ITEM = {
115
123
  }
116
124
  }
117
125
 
118
- GENERIC_DIMENSION_VALIDATION = {
119
- # "taxonomyType": { "LocalName": (range of numbers if any, first item name, 2nd choice item name if any)
120
- "ukGAAP": COMMON_GENERIC_DIMENSIONS,
121
- "ukIFRS": COMMON_GENERIC_DIMENSIONS,
122
- "charities": {
123
- **COMMON_GENERIC_DIMENSIONS,
124
- **{
125
- "Trustee": (1, 20, "NameEntityOfficer"),
126
- "CorporateTrustee": (1, 3, "NameEntityOfficer"),
127
- "CustodianTrustee": (1, 3, "NameEntityOfficer"),
128
- "Director1CorporateTrustee": ("NameEntityOfficer",),
129
- "Director2CorporateTrustee": ("NameEntityOfficer",),
130
- "Director3CorporateTrustee": ("NameEntityOfficer",),
131
- "TrusteeTrustees": (1, 5, "NameOrDescriptionRelatedPartyIfNotDefinedByAnotherTag"),
132
- "CloseFamilyMemberTrusteeTrustees": (1, 5, "NameOrDescriptionRelatedPartyIfNotDefinedByAnotherTag"),
133
- "EntityControlledTrustees": (1, 5, "NameOrDescriptionRelatedPartyIfNotDefinedByAnotherTag"),
134
- "Activity": (1, 50, "DescriptionActivity"),
135
- "MaterialFund": (1, 50, "DescriptionsMaterialFund"),
136
- "LinkedCharity": (1, 5, "DescriptionActivitiesLinkedCharity"),
137
- "NameGrantRecipient": (1, 50, "NameSpecificInstitutionalGrantRecipient"),
138
- "ConcessionaryLoan": (1, 50, "DescriptionConcessionaryLoan"),
139
- }
140
- },
141
- "FRS": COMMON_GENERIC_DIMENSIONS,
142
- "FRS-2022": COMMON_GENERIC_DIMENSIONS
143
- }
144
-
145
126
  ALLOWED_IMG_MIME_TYPES = (
146
127
  "data:image/gif;base64",
147
128
  "data:image/jpeg;base64", "data:image/jpg;base64", # note both jpg and jpeg are in use
@@ -221,9 +202,9 @@ def validateXbrlFinally(val, *args, **kwargs):
221
202
  scheme, identifier = c1.entityIdentifier
222
203
  if scheme == "http://www.companieshouse.gov.uk/":
223
204
  companyReferenceNumberContexts[identifier].append(c1.id)
224
- atLeastOneFacts = {}
205
+ atLeastOneFacts = defaultdict(set)
225
206
  uniqueFacts = {} # key = (qname, context hash, unit hash, lang)
226
- mandatoryFacts = {}
207
+ mandatoryFacts = defaultdict(set)
227
208
  mandatoryGDV = defaultdict(set)
228
209
  factForConceptContextUnitHash = defaultdict(list)
229
210
  hasCompaniesHouseContext = any(cntx.entityIdentifier[0] == "http://www.companieshouse.gov.uk/"
@@ -243,7 +224,7 @@ def validateXbrlFinally(val, *args, **kwargs):
243
224
  else:
244
225
  l = _memName
245
226
  n = None
246
- gdv = GENERIC_DIMENSION_VALIDATION.get(val.txmyType, EMPTYDICT).get(l)
227
+ gdv = GENERIC_DIMENSION_VALIDATIONS.get(l)
247
228
  if (gdv and (n is None or
248
229
  (isinstance(gdv[0],int) and isinstance(gdv[1],int) and n >= gdv[0] and n <= gdv[1]))):
249
230
  gdvFacts = [f for f in gdv if isinstance(f,str)]
@@ -270,12 +251,12 @@ def validateXbrlFinally(val, *args, **kwargs):
270
251
  factNamespaceURI = f.qname.namespaceURI
271
252
  factLocalName = f.qname.localName
272
253
  if factLocalName in MANDATORY_ITEMS[val.txmyType]:
273
- mandatoryFacts[factLocalName] = f
254
+ mandatoryFacts[factLocalName].add(f)
274
255
  if val.txmyType in MUST_HAVE_ONE_ITEM and factLocalName in MUST_HAVE_ONE_ITEM[val.txmyType]:
275
- atLeastOneFacts[factLocalName] = f
256
+ atLeastOneFacts[factLocalName].add(f)
276
257
  if factLocalName == "UKCompaniesHouseRegisteredNumber" and val.isAccounts:
277
258
  if hasCompaniesHouseContext:
278
- mandatoryFacts[factLocalName] = f
259
+ mandatoryFacts[factLocalName].add(f)
279
260
  for _cntx in contextsUsed:
280
261
  _scheme, _identifier = _cntx.entityIdentifier
281
262
  if _scheme == "http://www.companieshouse.gov.uk/" and f.xValue != _identifier:
@@ -346,17 +327,72 @@ def validateXbrlFinally(val, *args, **kwargs):
346
327
  checkFacts(modelXbrl.facts)
347
328
 
348
329
  if val.isAccounts:
349
- _missingItems = MANDATORY_ITEMS[val.txmyType] - mandatoryFacts.keys()
350
- if hasCompaniesHouseContext and "UKCompaniesHouseRegisteredNumber" not in mandatoryFacts:
351
- _missingItems.add("UKCompaniesHouseRegisteredNumber")
330
+ startDate = None
331
+ endDate = None
332
+ _missingItems = []
333
+ for fact in mandatoryFacts.get('EndDateForPeriodCoveredByReport', set()):
334
+ if (fact is not None and
335
+ fact.xValid >= VALID and
336
+ fact.context is not None and
337
+ fact.context.isInstantPeriod and
338
+ dateUnionEqual(fact.context.instantDate, fact.xValue)):
339
+ endDate = fact.xValue
340
+ break
341
+ else:
342
+ _missingItems.append('EndDateForPeriodCoveredByReport')
343
+
344
+ for fact in mandatoryFacts.get('StartDateForPeriodCoveredByReport', set()):
345
+ if (fact is not None and
346
+ fact.xValid >= VALID and
347
+ fact.context is not None and
348
+ fact.context.isInstantPeriod and
349
+ dateUnionEqual(fact.context.instantDate, endDate)):
350
+ startDate = fact.xValue
351
+ break
352
+ else:
353
+ _missingItems.append('StartDateForPeriodCoveredByReport')
354
+
355
+ if startDate is not None and endDate is not None:
356
+ mandatoryConceptsToCheck = MANDATORY_ITEMS[val.txmyType]
357
+ if hasCompaniesHouseContext:
358
+ mandatoryConceptsToCheck = MANDATORY_ITEMS[val.txmyType] | {"UKCompaniesHouseRegisteredNumber"}
359
+
360
+ for mandatoryConcept in mandatoryConceptsToCheck:
361
+ if mandatoryConcept in ['StartDateForPeriodCoveredByReport', 'EndDateForPeriodCoveredByReport']:
362
+ continue
363
+ foundFact = False
364
+ for fact in mandatoryFacts.get(mandatoryConcept, set()):
365
+ if (fact is not None and fact.context is not None and fact.xValid >= VALID and
366
+ ((fact.context.isInstantPeriod and dateUnionEqual(fact.context.instantDate, endDate) or
367
+ (fact.context.isStartEndPeriod and dateUnionEqual(fact.context.startDatetime, startDate) and
368
+ dateUnionEqual(fact.context.endDate, endDate, instantEndDate=True))))):
369
+ foundFact = True
370
+ break
371
+ if not foundFact:
372
+ _missingItems.append(mandatoryConcept)
373
+
352
374
  if _missingItems:
353
375
  modelXbrl.error("JFCVC.3312",
354
- _("Facts are MANDATORY: %(missingItems)s"),
376
+ _("The following mandatory concepts are either not tagged on a fact or are tagged on facts that "
377
+ "have contexts that do not align with the dates as reported in "
378
+ "'StartDateForPeriodCoveredByReport' and 'EndDateForPeriodCoveredByReport': %(missingItems)s"),
355
379
  modelObject=modelXbrl, missingItems=", ".join(sorted(_missingItems)))
356
- if not atLeastOneFacts and val.txmyType in MUST_HAVE_ONE_ITEM:
357
- modelXbrl.error("JFCVC.3312.atLeastOne",
358
- _("At least one of the facts is MANDATORY: %(missingItems)s"),
359
- modelObject=modelXbrl, missingItems=", ".join(sorted(MUST_HAVE_ONE_ITEM[val.txmyType])))
380
+
381
+ if startDate is not None and endDate is not None and val.txmyType in MUST_HAVE_ONE_ITEM:
382
+ foundFact = False
383
+ for mustHaveOneConcept in MUST_HAVE_ONE_ITEM[val.txmyType]:
384
+ for fact in atLeastOneFacts.get(mustHaveOneConcept, set()):
385
+ if (fact is not None and fact.context is not None and fact.xValid >= VALID and
386
+ ((fact.context.isInstantPeriod and dateUnionEqual(fact.context.instantDate, endDate) or
387
+ (fact.context.isStartEndPeriod and dateUnionEqual(fact.context.startDatetime, startDate) and
388
+ dateUnionEqual(fact.context.endDate, endDate, instantEndDate=True))))):
389
+ foundFact = True
390
+ break
391
+
392
+ if not foundFact:
393
+ modelXbrl.error("JFCVC.3312.atLeastOne",
394
+ _("At least one of the facts is MANDATORY: %(missingItems)s"),
395
+ modelObject=modelXbrl, missingItems=", ".join(sorted(MUST_HAVE_ONE_ITEM[val.txmyType])))
360
396
 
361
397
  ''' removed with JFCVC v4.0 2020-06-09
362
398
  f = mandatoryFacts.get("StartDateForPeriodCoveredByReport")
@@ -374,62 +410,12 @@ def validateXbrlFinally(val, *args, **kwargs):
374
410
  _("Generic dimension members have no associated name or description item, member names (name or description item): %(memberNames)s"),
375
411
  modelObject=modelXbrl, memberNames=", ".join(sorted(memLocalNamesMissing)))
376
412
 
377
-
378
-
379
- aspectEqualFacts = defaultdict(dict) # dict [(qname,lang)] of dict(cntx,unit) of [fact, fact]
380
- for hashEquivalentFacts in factForConceptContextUnitHash.values():
381
- if len(hashEquivalentFacts) > 1:
382
- for f in hashEquivalentFacts: # check for hash collision by value checks on context and unit
383
- if getattr(f,"xValid", 0) >= 4:
384
- cuDict = aspectEqualFacts[(f.qname,
385
- (f.xmlLang or "").lower() if f.concept.type.isWgnStringFactType else None)]
386
- _matched = False
387
- for (_cntx,_unit),fList in cuDict.items():
388
- if (((_cntx is None and f.context is None) or (f.context is not None and f.context.isEqualTo(_cntx))) and
389
- ((_unit is None and f.unit is None) or (f.unit is not None and f.unit.isEqualTo(_unit)))):
390
- _matched = True
391
- fList.append(f)
392
- break
393
- if not _matched:
394
- cuDict[(f.context,f.unit)] = [f]
395
- decVals = {}
396
- for cuDict in aspectEqualFacts.values(): # dups by qname, lang
397
- for fList in cuDict.values(): # dups by equal-context equal-unit
398
- if len(fList) > 1:
399
- f0 = fList[0]
400
- if f0.concept.isNumeric:
401
- if any(f.isNil for f in fList):
402
- _inConsistent = not all(f.isNil for f in fList)
403
- else: # not all have same decimals
404
- _d = inferredDecimals(f0)
405
- _v = f0.xValue
406
- _inConsistent = isnan(_v) # NaN is incomparable, always makes dups inconsistent
407
- decVals[_d] = _v
408
- aMax, bMin, _inclA, _inclB = rangeValue(_v, _d)
409
- for f in fList[1:]:
410
- _d = inferredDecimals(f)
411
- _v = f.xValue
412
- if isnan(_v):
413
- _inConsistent = True
414
- break
415
- if _d in decVals:
416
- _inConsistent |= _v != decVals[_d]
417
- else:
418
- decVals[_d] = _v
419
- a, b, _inclA, _inclB = rangeValue(_v, _d)
420
- if a > aMax: aMax = a
421
- if b < bMin: bMin = b
422
- if not _inConsistent:
423
- _inConsistent = (bMin < aMax)
424
- decVals.clear()
425
- else:
426
- _inConsistent = any(not f.isVEqualTo(f0) for f in fList[1:])
427
- if _inConsistent:
428
- modelXbrl.error("JFCVC.3314",
429
- "Inconsistent duplicate fact values %(fact)s: %(values)s.",
430
- modelObject=fList, fact=f0.qname, contextID=f0.contextID, values=", ".join(f.value for f in fList))
431
- aspectEqualFacts.clear()
432
- del factForConceptContextUnitHash, aspectEqualFacts
413
+ for duplicateFactSet in getDuplicateFactSets(modelXbrl.facts, includeSingles=False):
414
+ if duplicateFactSet.areAnyInconsistent:
415
+ f0 = duplicateFactSet.facts[0]
416
+ modelXbrl.error("JFCVC.3314",
417
+ "Inconsistent duplicate fact values %(fact)s: %(values)s.",
418
+ modelObject=duplicateFactSet.facts, fact=f0.qname, values=", ".join(f'"{f.value}"' for f in duplicateFactSet))
433
419
 
434
420
  if modelXbrl.modelDocument.type == ModelDocument.Type.INLINEXBRL:
435
421
  rootElt = modelXbrl.modelDocument.xmlRootElement