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
@@ -0,0 +1,595 @@
1
+ """
2
+ See COPYRIGHT.md for copyright information.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ from typing import Iterable
7
+
8
+ from collections import defaultdict
9
+ from jaconv import jaconv
10
+ from lxml.etree import _Element
11
+
12
+ from arelle import XbrlConst
13
+ from arelle.ModelDocument import ModelDocument
14
+ from arelle.ModelObject import ModelObject
15
+ from arelle.typing import TypeGetText
16
+ from arelle.utils.validate.Validation import Validation
17
+
18
+ _: TypeGetText
19
+
20
+ # Table of content number sets per EDINET documentation:
21
+ # Figure "3-4-5 設定可能な目次番号の一覧" in "File Specification for EDINET Filing"
22
+ # https://disclosure2dl.edinet-fsa.go.jp/guide/static/disclosure/download/ESE140104.pdf
23
+ TOC_DIGITS = [
24
+ '一',
25
+ '二',
26
+ '三',
27
+ '四',
28
+ '五',
29
+ '六',
30
+ '七',
31
+ '八',
32
+ '九',
33
+ '十',
34
+ ]
35
+ FULL_WIDTH_DIGIT_MAP = {
36
+ str(d): jaconv.h2z(str(d), kana=True, ascii=True, digit=True)
37
+ for d in range(0, 10)
38
+ }
39
+ KATAKANA_GOJUON_SEQUENCE = [
40
+ # a-column
41
+ 'ア', 'イ', 'ウ', 'エ', 'オ',
42
+ # k-column
43
+ 'カ', 'キ', 'ク', 'ケ', 'コ',
44
+ # s-column
45
+ 'サ', 'シ', 'ス', 'セ', 'ソ',
46
+ # t-column
47
+ 'タ', 'チ', 'ツ', 'テ', 'ト',
48
+ # n-column
49
+ 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ',
50
+ # h-column
51
+ 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ',
52
+ # m-column
53
+ 'マ', 'ミ', 'ム', 'メ', 'モ',
54
+ # y-column
55
+ 'ヤ', 'ユ', 'ヨ',
56
+ # r-column
57
+ 'ラ', 'リ', 'ル', 'レ', 'ロ',
58
+ # w-column
59
+ 'ワ', 'ヲ',
60
+ # n-row
61
+ 'ン'
62
+ ]
63
+ TOC_NUMBER_SETS = {
64
+ 1: {
65
+ '第一部': [f'第{d}部' for d in TOC_DIGITS] + [f'第十{d}部' for d in TOC_DIGITS[:-1]] + ['第二十部']
66
+ },
67
+ 2: {
68
+ '第1': [
69
+ '第' + ''.join(FULL_WIDTH_DIGIT_MAP[ddd] for ddd in dd)
70
+ for dd in [
71
+ str(d) for d in range(1, 61)
72
+ ]
73
+ ],
74
+ },
75
+ 3: {
76
+ '1': [
77
+ ''.join(FULL_WIDTH_DIGIT_MAP[ddd] for ddd in dd)
78
+ for dd in [
79
+ str(d) for d in range(1, 61)
80
+ ]
81
+ ],
82
+ },
83
+ 4: {
84
+ '(1)': [
85
+ '(' + ''.join(FULL_WIDTH_DIGIT_MAP[ddd] for ddd in dd) + ')'
86
+ for dd in [
87
+ str(d) for d in range(1, 61)
88
+ ]
89
+ ],
90
+ },
91
+ 5: {
92
+ '①': [chr(ord('①') + d) for d in range(0, 20)],
93
+ f'({KATAKANA_GOJUON_SEQUENCE[0]})': [f'({d})' for d in KATAKANA_GOJUON_SEQUENCE],
94
+ KATAKANA_GOJUON_SEQUENCE[0]: KATAKANA_GOJUON_SEQUENCE,
95
+ '(a)': ['(' + chr(ord('a') + d) + ')' for d in range(0, 26)],
96
+ 'a': [chr(ord('a') + d) for d in range(0, 26)],
97
+ },
98
+ }
99
+ SHALLOWEST_LEVEL = min(TOC_NUMBER_SETS.keys())
100
+ DEEPEST_LEVEL = max(TOC_NUMBER_SETS.keys())
101
+
102
+ PROHIBITED_BETWEEN_TAGS = frozenset({
103
+ XbrlConst.qnXhtmlDel.localName,
104
+ XbrlConst.qnXhtmlImg.localName,
105
+ XbrlConst.qnXhtmlDel.clarkNotation,
106
+ XbrlConst.qnXhtmlImg.clarkNotation,
107
+ })
108
+
109
+
110
+ class TableOfContentsBuilder:
111
+ _currentDocument: ModelDocument | None
112
+ _currentLevel: int
113
+ _documents: list[ModelDocument]
114
+ _floatingLevel: int | None
115
+ _levelLabels: dict[int, set[str]]
116
+ _levelPositions: dict[int, int]
117
+ _levelSequences: dict[int, list[str] | None]
118
+ _tocEntryCount: int
119
+ _tocSequence: list[tuple[str, str, ModelObject]]
120
+ _validations: list[Validation]
121
+
122
+ def __init__(self) -> None:
123
+ self._currentDocument = None
124
+ self._currentLevel = SHALLOWEST_LEVEL
125
+ self._floatingLevel = None
126
+ self._levelLabels = defaultdict(set)
127
+ self._levelPositions = defaultdict(int)
128
+ self._levelSequences = {}
129
+ self._documents = []
130
+ self._tocEntryCount = 0
131
+ self._tocSequence = []
132
+ self._validations = []
133
+
134
+ def _build(self) -> None:
135
+ documents = sorted(self._documents, key=lambda doc: doc.basename)
136
+ for document in documents:
137
+ rootElt = document.xmlRootElement
138
+ for elt in rootElt.iterdescendants():
139
+ if not isinstance(elt, ModelObject):
140
+ continue
141
+ if elt.elementQname.localName == 'title':
142
+ continue
143
+ if elt.text is not None:
144
+ self._element(elt)
145
+
146
+ def _checkCurrentLevelNext(self, number: str, nextPosition: int, currentSequence: list[str] | None) -> bool:
147
+
148
+ # NEXT IN SEQUENCE
149
+ # We can only move ahead in the sequence if it has been established AND
150
+ # we have not reached the end of the allowed numbers in the sequence.
151
+ if currentSequence is None:
152
+ # No sequence established at this level.
153
+ return False
154
+ if nextPosition >= len(currentSequence):
155
+ # We have reached the end of the sequence at this level.
156
+ return False
157
+ nextNumberInSequence = currentSequence[nextPosition]
158
+ if number == nextNumberInSequence:
159
+ # Increment the position at the current level.
160
+ self._levelPositions[self._currentLevel] = self._levelPositions[self._currentLevel] + 1
161
+ # Reset the floating status.
162
+ self._floatingLevel = None
163
+ return True
164
+ return False
165
+
166
+ def _checkDeepLevel(self, number: str) -> bool:
167
+ # STARTING DEEPER SEQUENCE
168
+ # We can move deeper one or more levels.
169
+ if self._floatingLevel is not None:
170
+ # We don't move deeper if we are floating.
171
+ return False
172
+ deeperStartingNumbers = {
173
+ startingNumber: level
174
+ for level in range(min(self._currentLevel + 1, DEEPEST_LEVEL), DEEPEST_LEVEL + 1)
175
+ for startingNumber in TOC_NUMBER_SETS[level]
176
+ }
177
+ if number not in deeperStartingNumbers:
178
+ # The number is not the first entry in a deeper level sequence.
179
+ return False
180
+ # Set the level to the deeper level.
181
+ self._currentLevel = deeperStartingNumbers[number]
182
+ # Establish the sequence at the deeper level.
183
+ self._levelSequences[self._currentLevel] = TOC_NUMBER_SETS[self._currentLevel][number]
184
+ # Reset the duplicates at the deeper level.
185
+ self._levelLabels[self._currentLevel].clear()
186
+ # Reset the position at the deeper level.
187
+ self._levelPositions[self._currentLevel] = 1
188
+ return True
189
+
190
+ def _checkShallowLevel(self, number: str) -> bool:
191
+ # RESUMING SHALLOWER SEQUENCE
192
+ # We may be moving back up one or more levels.
193
+ nextLevel = None
194
+ for shallowLevel in reversed(range(SHALLOWEST_LEVEL, self._currentLevel)):
195
+ shallowSequence = self._levelSequences.get(shallowLevel)
196
+ # For each level, should we be checking earlier and later numbers in the sequence
197
+ # to fire EC3002E or EC3003E?
198
+ if shallowSequence is not None and number == shallowSequence[self._levelPositions[shallowLevel]]:
199
+ # This is the next number in the upper sequence.
200
+ # Move up one level.
201
+ nextLevel = shallowLevel
202
+ break
203
+ shallowLevel -= 1
204
+ if nextLevel is None:
205
+ return False
206
+ # For each level shallower than the new level down to the current level...
207
+ for i in range(nextLevel + 1, self._currentLevel + 1):
208
+ # Reset the position.
209
+ self._levelPositions[i] = 0
210
+ # Reset the duplicates.
211
+ self._levelLabels[i].clear()
212
+ # Reset the sequence.
213
+ self._levelSequences[i] = None
214
+ # Set the level to the shallower level.
215
+ self._currentLevel = nextLevel
216
+ # Increment the position at the shallower level.
217
+ self._levelPositions[self._currentLevel] = self._levelPositions[self._currentLevel] + 1
218
+ # Reset the floating status.
219
+ self._floatingLevel = None
220
+ return True
221
+
222
+ def _closeDocument(self) -> None:
223
+ assert self._currentDocument is not None, "No document is currently open."
224
+ # EDINET.EC2001E: There must be at least one table of contents entry in each file.
225
+ if self._tocEntryCount == 0:
226
+ self._validations.append(Validation.error(
227
+ codes='EDINET.EC2001E',
228
+ msg=_("The table of contents is not listed at the beginning. "
229
+ "File name: '%(path)s'. "
230
+ "Please provide the table of contents entry for the file."),
231
+ path=self._currentDocument.basename,
232
+ ))
233
+ self._currentDocument = None
234
+
235
+ def _element(self, elt: ModelObject) -> None:
236
+ # New document, close previous.
237
+ if self._currentDocument is not None and self._currentDocument != elt.document:
238
+ self._closeDocument()
239
+ # First or new document, open document.
240
+ if self._currentDocument is None:
241
+ self._openDocument(elt.document)
242
+
243
+ number, label, tail, eltsInLabel, eltsBetweenNumAndLabel = self._getTextParts(elt)
244
+
245
+ textValue = ''.join(elt.textNodes())
246
+ if (
247
+ (label is None and ('【' in textValue or '】' in textValue)) or
248
+ (tail is not None and ('【' in tail or '】' in tail))
249
+ ):
250
+ self._validations.append(Validation.error(
251
+ codes='EDINET.EC2011E',
252
+ msg=_("\"【\" or \"】\" is used in the text. "
253
+ "File name: '%(path)s' (line %(line)s). "
254
+ "Corner brackets (【】) cannot be used in the main text "
255
+ "except for the table of contents. Please delete the "
256
+ "corner brackets (【】) in the relevant file."),
257
+ path=elt.document.basename,
258
+ line=elt.sourceline,
259
+ modelObject=elt,
260
+ ))
261
+
262
+ if label is None:
263
+ return
264
+
265
+ if len(eltsInLabel) > 0:
266
+ self._validations.append(Validation.error(
267
+ codes='EDINET.EC2008E',
268
+ msg=_("The table of contents label contains HTML tags. "
269
+ "File name: '%(path)s' (line %(line)s). "
270
+ "HTML tags are not allowed in table of contents labels. "
271
+ "Please remove the HTML tags from the relevant file."),
272
+ path=elt.document.basename,
273
+ line=elt.sourceline,
274
+ modelObject=elt,
275
+ ))
276
+
277
+ if any(
278
+ e.tag in PROHIBITED_BETWEEN_TAGS
279
+ for e in eltsBetweenNumAndLabel
280
+ ):
281
+ self._validations.append(Validation.error(
282
+ codes='EDINET.EC2009E',
283
+ msg=_("An invalid tag is used between the table of contents number "
284
+ "and the table of contents item. "
285
+ "File name: '%(path)s' (line %(line)s). "
286
+ "Please delete the tag (\"del\" or \"img\") used between the "
287
+ "table of contents number and the table of contents item of "
288
+ "the relevant file."),
289
+ path=elt.document.basename,
290
+ line=elt.sourceline,
291
+ modelObject=elt,
292
+ ))
293
+
294
+ if '【' in label[1:]:
295
+ self._validations.append(Validation.error(
296
+ codes='EDINET.EC2004E',
297
+ msg=_("The opening bracket (【) is repeated. "
298
+ "File name: '%(path)s' (line %(line)s). "
299
+ "When viewed in a browser, it is not possible to display "
300
+ "more than one table of contents item on one line. Please "
301
+ "delete the brackets (【) in the relevant file."),
302
+ path=elt.document.basename,
303
+ line=elt.sourceline,
304
+ modelObject=elt,
305
+ ))
306
+
307
+ if '】' not in label:
308
+ # EDINET.EC2007E: The table of contents entries must be enclosed in square brackets (】).
309
+ self._validations.append(Validation.error(
310
+ codes='EDINET.EC2007E',
311
+ msg=_("The table of contents entry is not closed with '】'. "
312
+ "File name: '%(path)s' (line %(line)s). "
313
+ "Add a closing bracket (】) to match the open bracket (【) "
314
+ "in the table of contents entry for the file in question."),
315
+ path=elt.document.basename,
316
+ line=elt.sourceline,
317
+ modelObject=elt,
318
+ ))
319
+
320
+ self._tocSequence.append((number, label, elt))
321
+ self._tocEntryCount += 1
322
+
323
+ def _getTextParts(
324
+ self,
325
+ elt: ModelObject
326
+ ) -> tuple[str, str | None, str | None, list[ModelObject], list[ModelObject]]:
327
+ """
328
+ Determines the TOC number, label, and tail of a given element (if set correctly)
329
+ based on the text nodes directly beneath the element.
330
+ Also captures misplaced elements in provided lists for error handling.
331
+ """
332
+ eltsInLabel = []
333
+ eltsBetweenNumAndLabel = []
334
+ textParts = [(None, elt.text)] + [
335
+ (child, child.tail)
336
+ for child in elt.iterchildren()
337
+ ]
338
+
339
+ number = ''
340
+ label = None
341
+ tail = None
342
+ while len(textParts) > 0:
343
+ textElt, text = textParts.pop(0)
344
+
345
+ # If we're iterating over an element, check if it's misplaced
346
+ if textElt is not None:
347
+ if number and label is None:
348
+ eltsBetweenNumAndLabel.append(textElt)
349
+ if label is not None and '】' not in label and tail is None:
350
+ eltsInLabel.append(textElt)
351
+
352
+ # If no text, move on
353
+ if not text:
354
+ continue
355
+
356
+ if label is None:
357
+ # We're building the number
358
+ start, sep, end = text.partition('【')
359
+ if sep:
360
+ number += start
361
+ label = sep # Start the label
362
+ # Process the remainder of this text node next
363
+ textParts.insert(0, (None, end))
364
+ else:
365
+ number += start
366
+ elif tail is None:
367
+ # We're building the label
368
+ start, sep, end = text.partition('】')
369
+ if sep:
370
+ label += start + sep
371
+ tail = '' # Start the tail
372
+ # Process the remainder of this text node next
373
+ textParts.insert(0, (None, end))
374
+ else:
375
+ label += start
376
+ else:
377
+ # We're building the tail
378
+ tail += text
379
+
380
+ return (
381
+ number.strip(),
382
+ label.strip() if label else label,
383
+ tail.strip() if tail else tail,
384
+ eltsInLabel,
385
+ eltsBetweenNumAndLabel,
386
+ )
387
+
388
+ def _isFloating(self) -> bool:
389
+ return self._floatingLevel is not None or self._currentLevel >= DEEPEST_LEVEL
390
+
391
+ def _normalizeNumber(self, number: str) -> str:
392
+ # EDINET does not support:
393
+ # - Mixture of half-width and full-width digits within a number.
394
+ # - Mixture of half-width and full-width parentheses within a number.
395
+ # EDINET does support:
396
+ # - Mixture of half-width digits with full-width parentheses, and vice versa.
397
+ # We will normalize to full-width parantheses and digits for number validation.
398
+ if "(" in number and ")" in number: # Half-width (, full-width )
399
+ return number
400
+ if "(" in number and ")" in number: # Full-width (, half-width )
401
+ return number
402
+ paranthesesFullWidth: bool | None = None
403
+ numbersFullWidth: bool | None = None
404
+ for c in number:
405
+ if c in ("(", ")"):
406
+ if paranthesesFullWidth == True:
407
+ return number # Mix of half/full-width parantheses
408
+ paranthesesFullWidth = False
409
+ elif c in ("(", " )"):
410
+ if paranthesesFullWidth == False:
411
+ return number # Mix of half/full-width parantheses
412
+ paranthesesFullWidth = True
413
+ elif c in FULL_WIDTH_DIGIT_MAP:
414
+ if numbersFullWidth == True:
415
+ return number # Mix of half/full-width digits
416
+ numbersFullWidth = False
417
+ elif c in FULL_WIDTH_DIGIT_MAP.values():
418
+ if numbersFullWidth == False:
419
+ return number # Mix of half/full-width digits
420
+ numbersFullWidth = True
421
+ return jaconv.h2z(number, kana=True, ascii=True, digit=True)
422
+
423
+ def _openDocument(self, modelDocument: ModelDocument) -> None:
424
+ assert self._currentDocument is None, "Close current document before opening another."
425
+ self._tocEntryCount = 0
426
+ self._currentDocument = modelDocument
427
+
428
+ def _validateItem(self, number: str, label: str, elt: ModelObject) -> Iterable[Validation]:
429
+ # Convert to full-width, ONLY if fully half-width.
430
+ # EDINET does not support a mixture of half-width and full-width digits in TOC numbers.
431
+ number = self._normalizeNumber(number)
432
+ nextPosition = self._levelPositions[self._currentLevel]
433
+ currentSequence = self._levelSequences.get(self._currentLevel)
434
+
435
+ # UN-NUMBERED (FLOATING) ITEM
436
+ # Floating items trigger special behavior for following items
437
+ # until the current or shallower level is resumed.
438
+ if number == "":
439
+ # Only trigger floating/warning if we are not already floating, and we
440
+ # aren't already at the deepest level.
441
+ if not self._isFloating():
442
+ # EDINET.EC2002W: The table of contents number must be present.
443
+ # Note from documentation: Even if the data content is normal, it may be identified as an
444
+ # exception and a warning may be displayed.
445
+ yield Validation.warning(
446
+ codes='EDINET.EC2002W',
447
+ msg=_("The table of contents number is not listed. "
448
+ "File name: '%(path)s' (line %(line)s). "
449
+ "Please include the table of contents number of the relevant file."),
450
+ path=elt.document.basename,
451
+ line=elt.sourceline,
452
+ modelObject=elt,
453
+ )
454
+ self._floatingLevel = self._currentLevel
455
+ return
456
+
457
+ if self._checkCurrentLevelNext(number, nextPosition, currentSequence):
458
+ return
459
+ if self._checkDeepLevel(number):
460
+ return
461
+ if self._checkShallowLevel(number):
462
+ return
463
+
464
+ # OTHER NUMBER IN CURRENT SEQUENCE
465
+ if currentSequence is not None and number in currentSequence:
466
+ numberIndex = currentSequence.index(number)
467
+ assert numberIndex != nextPosition
468
+ # Is it repeating a previous number?
469
+ if numberIndex < nextPosition:
470
+ # EDINET.EC3002E: Table of contents numbers for table of contents entries
471
+ # must not be repeated within the same hierarchy.
472
+ yield Validation.error(
473
+ codes='EDINET.EC3002E',
474
+ msg=_("The table of contents number of the table of contents "
475
+ "item is duplicated in the same hierarchy. "
476
+ "File name: '%(path)s' (line %(line)s). "
477
+ "Please correct the table of contents number in the "
478
+ "table of contents entry of the relevant file."),
479
+ path=elt.document.basename,
480
+ line=elt.sourceline,
481
+ modelObject=elt,
482
+ )
483
+ return
484
+ # Is it skipping ahead?
485
+ if numberIndex > nextPosition:
486
+ # EDINET.EC3003E: There must be no gaps in the table of contents numbers
487
+ # within the same hierarchy.
488
+ yield Validation.error(
489
+ codes='EDINET.EC3003E',
490
+ msg=_("There is a gap in the table of contents number "
491
+ "within the same hierarchy. "
492
+ "File name: '%(path)s' (line %(line)s). "
493
+ "Please enter the missing table of contents number "
494
+ "in the appropriate file."),
495
+ number=number,
496
+ label=label,
497
+ path=elt.document.basename,
498
+ line=elt.sourceline,
499
+ modelObject=elt,
500
+ )
501
+ # Jump ahead to minimize further errors.
502
+ # Increment the position at the current level.
503
+ self._levelPositions[self._currentLevel] = numberIndex + 1
504
+ # Reset the floating status.
505
+ self._floatingLevel = None
506
+ return
507
+
508
+ # INVALID NUMBER
509
+ # Not un-numbered (floating), not next in sequence, not starting deeper sequence,
510
+ # not resuming shallower sequence.
511
+ if self._floatingLevel is None:
512
+ # The difference between EC3004W and EC3005E is unclear based on documentation.
513
+ # We will implement the higher level severity version of the two.
514
+ # EDINET.EC3004W: The table of contents number of the table of contents
515
+ # item must be set.
516
+ # EDINET.EC3005E: The table of contents numbers for the table of contents
517
+ # entries must be as specified in the format.
518
+ yield Validation.error(
519
+ codes='EDINET.EC3005E',
520
+ msg=_("The table of contents number for item '%(number)s %(label)s' is incorrect. "
521
+ "File name: '%(path)s' (line %(line)s). "
522
+ "Please correct the table of contents number of the "
523
+ "table of contents item of the corresponding file."),
524
+ number=number,
525
+ label=label,
526
+ path=elt.document.basename,
527
+ line=elt.sourceline,
528
+ modelObject=elt,
529
+ )
530
+
531
+ def addDocument(self, modelDocument: ModelDocument) -> None:
532
+ self._documents.append(modelDocument)
533
+
534
+ def validate(self) -> Iterable[Validation]:
535
+ self._build()
536
+ if self._currentDocument is not None:
537
+ self._closeDocument()
538
+ # Yield errors encountered during loading/build.
539
+ yield from self._validations
540
+
541
+ # Tracks the current level.
542
+ self._currentLevel = SHALLOWEST_LEVEL
543
+ # Tracks the current position in the sequence at each level.
544
+ self._levelPositions: dict[int, int] = defaultdict(int)
545
+ # Tracks the active number set at each level.
546
+ self._levelSequences: dict[int, list[str] | None] = {
547
+ self._currentLevel: next(iter(TOC_NUMBER_SETS[self._currentLevel].values()))
548
+ }
549
+ # Tracks unique labels within a sequence.
550
+ self._levelLabels = defaultdict(set)
551
+ # Tracks floating status.
552
+ self._floatingLevel = None
553
+ for number, label, elt in self._tocSequence:
554
+ yield from self._validateItem(number, label, elt)
555
+
556
+ # We are only concerned about duplicates if we are not floating.
557
+ if not self._isFloating():
558
+ if label in self._levelLabels[self._currentLevel]:
559
+ # EDINET.EC2005E: Table of contents entries must not be duplicated.
560
+ # Note: Sample filings suggest this applies to entries that are
561
+ # siblings within the hierarchy.
562
+ yield Validation.error(
563
+ codes='EDINET.EC2005E',
564
+ msg=_("The table of contents item ('%(label)s') is duplicated. "
565
+ "File name: '%(path)s' (line %(line)s). "
566
+ "Please remove the duplicate table of contents in the appropriate file."),
567
+ label=label,
568
+ path=elt.document.basename,
569
+ line=elt.sourceline,
570
+ modelObject=elt,
571
+ )
572
+ else:
573
+ self._levelLabels[self._currentLevel].add(label)
574
+
575
+ if not self._isFloating():
576
+ # EDINET.EC2003E: The table of contents must be no longer than 384 bytes
577
+ # (equivalent to 128 full-width characters).
578
+ if len(label.encode('utf-8')) > 384:
579
+ yield Validation.error(
580
+ codes='EDINET.EC2003E',
581
+ msg=_("The table of contents entry exceeds 384 bytes. "
582
+ "File name: '%(path)s' (line %(line)s). "
583
+ "Please modify the table of contents of the relevant "
584
+ "file so that it is within 384B (bytes) (equivalent to "
585
+ "128 full-width characters)."),
586
+ path=elt.document.basename,
587
+ line=elt.sourceline,
588
+ modelObject=elt,
589
+ )
590
+
591
+ # Uncomment for debugging output of TOC structure.
592
+ # print(
593
+ # f'{" " if self._floatingLevel is not None else "|"}\t' * (self._currentLevel - 1) +
594
+ # f"{number} [{label}] \t{elt.document.basename}"
595
+ # )
@@ -0,0 +1,48 @@
1
+ """
2
+ See COPYRIGHT.md for copyright information.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+ from functools import cached_property
8
+ from pathlib import Path
9
+
10
+ from .ReportFolderType import ReportFolderType
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class UploadContents:
15
+ reports: dict[ReportFolderType, frozenset[Path]]
16
+ uploadPaths: list[UploadPathInfo]
17
+
18
+ @property
19
+ def sortedPaths(self) -> list[Path]:
20
+ return sorted(uploadPath.path for uploadPath in self.uploadPaths)
21
+
22
+ @cached_property
23
+ def uploadPathsByFullPath(self) -> dict[Path, UploadPathInfo]:
24
+ return {
25
+ uploadPath.fullPath: uploadPath
26
+ for uploadPath in self.uploadPaths
27
+ }
28
+
29
+ @cached_property
30
+ def uploadPathsByPath(self) -> dict[Path, UploadPathInfo]:
31
+ return {
32
+ uploadPath.path: uploadPath
33
+ for uploadPath in self.uploadPaths
34
+ }
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class UploadPathInfo:
39
+ fullPath: Path
40
+ isAttachment: bool
41
+ isCorrection: bool
42
+ isCoverPage: bool
43
+ isDirectory: bool
44
+ isRoot: bool
45
+ isSubdirectory: bool
46
+ path: Path
47
+ reportFolderType: ReportFolderType | None
48
+ reportPath: Path | None
@@ -3,11 +3,14 @@ See COPYRIGHT.md for copyright information.
3
3
  """
4
4
  from __future__ import annotations
5
5
 
6
+ import logging
6
7
  from typing import Any
7
8
 
9
+ from arelle.Cntlr import Cntlr
8
10
  from arelle.FileSource import FileSource
9
11
  from arelle.ValidateXbrl import ValidateXbrl
10
12
  from arelle.typing import TypeGetText
13
+ from arelle.utils.PluginData import PluginData
11
14
  from arelle.utils.validate.ValidationPlugin import ValidationPlugin
12
15
  from .ControllerPluginData import ControllerPluginData
13
16
  from .DisclosureSystems import DISCLOSURE_SYSTEM_EDINET
@@ -24,7 +27,17 @@ class ValidationPluginExtension(ValidationPlugin):
24
27
  if len(instances) == 0:
25
28
  return None
26
29
  assert filesource.cntlr is not None
30
+ filesource.cntlr.addToLog(
31
+ _("EDINET manifest(s) detected (%(manifests)s). Loading %(count)s instances (%(instances)s)."),
32
+ messageCode="info",
33
+ messageArgs={
34
+ "manifests": ', '.join(instance.type for instance in instances),
35
+ "count": len(instances),
36
+ "instances": ', '.join(instance.id for instance in instances),
37
+ }, level=logging.INFO
38
+ )
27
39
  pluginData = ControllerPluginData.get(filesource.cntlr, self.name)
40
+ pluginData.setUploadContents(filesource)
28
41
  entrypointFiles = []
29
42
  for instance in instances:
30
43
  pluginData.addManifestInstance(instance)
@@ -35,12 +48,17 @@ class ValidationPluginExtension(ValidationPlugin):
35
48
  entrypointFiles.append({'ixds': entrypoints, 'id': instance.id})
36
49
  return entrypointFiles
37
50
 
38
- def newPluginData(self, validateXbrl: ValidateXbrl) -> PluginValidationDataExtension:
39
- disclosureSystem = validateXbrl.disclosureSystem.name
51
+ def newPluginData(self, cntlr: Cntlr, validateXbrl: ValidateXbrl | None) -> PluginData:
52
+ if validateXbrl is None:
53
+ return ControllerPluginData.get(cntlr, self.name)
54
+ disclosureSystem = DISCLOSURE_SYSTEM_EDINET
55
+ if validateXbrl is not None:
56
+ disclosureSystem = str(validateXbrl.disclosureSystem.name)
40
57
  if disclosureSystem == DISCLOSURE_SYSTEM_EDINET:
41
58
  pass
42
59
  else:
43
60
  raise ValueError(f'Invalid EDINET disclosure system: {disclosureSystem}')
44
61
  return PluginValidationDataExtension(
45
62
  self.name,
63
+ validateXbrl
46
64
  )