arelle-release 2.37.49__py3-none-any.whl → 2.37.51__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of arelle-release might be problematic. Click here for more details.

Files changed (27) hide show
  1. arelle/ModelDocument.py +16 -14
  2. arelle/ModelInstanceObject.py +1 -1
  3. arelle/ModelXbrl.py +21 -10
  4. arelle/WebCache.py +26 -16
  5. arelle/_version.py +16 -3
  6. arelle/api/Session.py +5 -2
  7. arelle/plugin/validate/DBA/PluginValidationDataExtension.py +0 -1
  8. arelle/plugin/validate/EDINET/Constants.py +11 -0
  9. arelle/plugin/validate/EDINET/ControllerPluginData.py +40 -29
  10. arelle/plugin/validate/EDINET/PluginValidationDataExtension.py +30 -5
  11. arelle/plugin/validate/EDINET/{InstanceType.py → ReportFolderType.py} +11 -15
  12. arelle/plugin/validate/EDINET/UploadContents.py +20 -5
  13. arelle/plugin/validate/EDINET/rules/contexts.py +4 -2
  14. arelle/plugin/validate/EDINET/rules/edinet.py +22 -0
  15. arelle/plugin/validate/EDINET/rules/gfm.py +20 -29
  16. arelle/plugin/validate/EDINET/rules/upload.py +230 -57
  17. arelle/plugin/validate/ESEF/ESEF_2021/ValidateXbrlFinally.py +2 -2
  18. arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +2 -2
  19. arelle/plugin/validate/NL/PluginValidationDataExtension.py +1 -1
  20. arelle/plugin/validate/NL/rules/fr_nl.py +6 -7
  21. arelle/plugin/validate/UK/ValidateUK.py +31 -66
  22. {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/METADATA +20 -18
  23. {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/RECORD +27 -27
  24. {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/WHEEL +0 -0
  25. {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/entry_points.txt +0 -0
  26. {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/licenses/LICENSE.md +0 -0
  27. {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/top_level.txt +0 -0
@@ -30,7 +30,7 @@ from arelle.utils.validate.Validation import Validation
30
30
  from arelle.utils.validate.ValidationUtil import etreeIterWithDepth
31
31
  from ..DisclosureSystems import (DISCLOSURE_SYSTEM_EDINET)
32
32
  from ..PluginValidationDataExtension import PluginValidationDataExtension
33
- from ..Constants import xhtmlDtdExtension
33
+
34
34
 
35
35
  _: TypeGetText
36
36
 
@@ -257,13 +257,14 @@ def rule_gfm_1_2_8(
257
257
  EDINET.EC5700W: [GFM 1.2.8] Every xbrli:context element must appear in at least one
258
258
  contextRef attribute in the same instance.
259
259
  """
260
- unused_contexts = list(set(val.modelXbrl.contexts.values()) - set(val.modelXbrl.contextsInUse))
261
- unused_contexts.sort(key=lambda x: x.id)
262
- 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:
263
264
  yield Validation.warning(
264
265
  codes='EDINET.EC5700W.GFM.1.2.8',
265
266
  msg=_('If you are not using a context, delete it if it is not needed.'),
266
- modelObject = context
267
+ modelObject=context
267
268
  )
268
269
 
269
270
 
@@ -367,28 +368,15 @@ def rule_gfm_1_2_14(
367
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.).
368
369
  Please modify it so that it is well-formed.
369
370
  """
370
- CDATApattern = regex.compile(r"<!\[CDATA\[(.+)\]\]")
371
- dtd = DTD(os.path.join(val.modelXbrl.modelManager.cntlr.configDir, xhtmlDtdExtension))
372
- htmlBodyTemplate = "<body><div>\n{0}\n</div></body>\n"
373
- 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]"
374
- r"[_\-\.:"
375
- "\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]*;")
376
- XMLpattern = regex.compile(r".*(<|&lt;|&#x3C;|&#60;)[A-Za-z_]+[A-Za-z0-9_:]*[^>]*(/>|>|&gt;|/&gt;).*", regex.DOTALL)
377
-
378
- for fact in val.modelXbrl.facts:
379
- concept = fact.concept
380
- if not fact.isNil and concept is not None and concept.isTextBlock and XMLpattern.match(fact.value):
381
- for xmlText in [fact.value] + CDATApattern.findall(fact.value):
382
- xmlBodyWithoutEntities = htmlBodyTemplate.format(namedEntityPattern.sub("", xmlText).replace('&','&amp;'))
383
- textblockXml = XML(xmlBodyWithoutEntities)
384
- if not dtd.validate(textblockXml):
385
- yield Validation.warning(
386
- codes='EDINET.EC5700W.GFM.1.2.14',
387
- 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, '
388
- '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). '
389
- 'Correct the content so that it is well-formed.'),
390
- modelObject = fact
391
- )
371
+ problematicFacts = pluginData.getProblematicTextBlocks(val.modelXbrl)
372
+ if len(problematicFacts) > 0:
373
+ yield Validation.warning(
374
+ codes='EDINET.EC5700W.GFM.1.2.14',
375
+ 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, '
376
+ '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). '
377
+ 'Correct the content so that it is well-formed.'),
378
+ modelObject = problematicFacts
379
+ )
392
380
 
393
381
 
394
382
  @validation(
@@ -484,6 +472,7 @@ def rule_gfm_1_2_25(
484
472
  XbrlConst.qnXbrliEndDate.clarkNotation,
485
473
  XbrlConst.qnXbrliInstant.clarkNotation
486
474
  ):
475
+ elt = cast(ModelObject, elt)
487
476
  dateText = XmlUtil.text(elt)
488
477
  if not GFM_CONTEXT_DATE_PATTERN.match(dateText):
489
478
  errors.append(elt)
@@ -558,12 +547,14 @@ def rule_gfm_1_2_27(
558
547
  EDINET.EC5700W: [GFM 1.2.27] An instance must not contain unused units.
559
548
  """
560
549
  # TODO: Consolidate validations involving unused units
561
- unusedUnits = set(val.modelXbrl.units.values()) - {fact.unit for fact in val.modelXbrl.facts if fact.unit is not None}
550
+ unusedUnits = list(set(val.modelXbrl.units.values()) - set(val.modelXbrl.unitsInUse))
551
+ unusedUnits.extend(val.modelXbrl.ixdsUnmappedUnits.values())
552
+ unusedUnits.sort(key=lambda x: x.hash)
562
553
  if len(unusedUnits) > 0:
563
554
  yield Validation.warning(
564
555
  codes='EDINET.EC5700W.GFM.1.2.27',
565
556
  msg=_("Delete unused units from the instance."),
566
- modelObject=list(unusedUnits)
557
+ modelObject=unusedUnits
567
558
  )
568
559
 
569
560
 
@@ -16,7 +16,7 @@ from arelle.utils.PluginHooks import ValidationHook
16
16
  from arelle.utils.validate.Decorator import validation
17
17
  from arelle.utils.validate.Validation import Validation
18
18
  from ..DisclosureSystems import (DISCLOSURE_SYSTEM_EDINET)
19
- from ..InstanceType import InstanceType, HTML_EXTENSIONS, IMAGE_EXTENSIONS
19
+ from ..ReportFolderType import ReportFolderType, HTML_EXTENSIONS, IMAGE_EXTENSIONS
20
20
  from ..PluginValidationDataExtension import PluginValidationDataExtension
21
21
 
22
22
  if TYPE_CHECKING:
@@ -24,6 +24,16 @@ if TYPE_CHECKING:
24
24
 
25
25
  _: TypeGetText
26
26
 
27
+ ALLOWED_ROOT_FOLDERS = {
28
+ "AttachDoc",
29
+ "AuditDoc",
30
+ "PrivateAttach",
31
+ "PrivateDoc",
32
+ "PublicAttach",
33
+ "PublicDoc",
34
+ "XBRL",
35
+ }
36
+
27
37
  FILE_COUNT_LIMITS = {
28
38
  Path("AttachDoc"): 990,
29
39
  Path("AuditDoc"): 990,
@@ -43,7 +53,7 @@ FILENAME_STEM_PATTERN = re.compile(r'[a-zA-Z0-9_-]*')
43
53
  hook=ValidationHook.FILESOURCE,
44
54
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
45
55
  )
46
- def rule_EC0121E(
56
+ def rule_EC0100E(
47
57
  pluginData: ControllerPluginData,
48
58
  cntlr: Cntlr,
49
59
  fileSource: FileSource,
@@ -51,30 +61,32 @@ def rule_EC0121E(
51
61
  **kwargs: Any,
52
62
  ) -> Iterable[Validation]:
53
63
  """
54
- EDINET.EC0121E: There is a directory or file that contains more than 31 characters
55
- or uses characters other than those allowed (alphanumeric characters, '-' and '_').
56
-
57
- Note: Sample instances from EDINET almost always violate this rule based on our
58
- current interpretation. The exception being files placed outside the XBRL directory,
59
- i.e. amendment documents. For now, we will only check amendment documents, directory
60
- names, or other files in unexpected locations.
64
+ EDINET.EC0100E: An illegal directory is found directly under the transferred directory.
65
+ Only the following root folders are allowed:
66
+ AttachDoc
67
+ AuditDoc*
68
+ PrivateAttach
69
+ PrivateDoc*
70
+ PublicAttach
71
+ PublicDoc*
72
+ XBRL
73
+ * Only when reporting corrections
74
+
75
+ NOTE: since we do not have access to the submission type, we can't determine if the submission is a correction or not.
76
+ For this implementation, we will allow all directories that may be valid for at least one submission type.
77
+ This allows for a false-negative outcome when a non-correction submission has a correction-only root directory.
61
78
  """
62
79
  uploadContents = pluginData.getUploadContents(fileSource)
63
- paths = set(uploadContents.directories | uploadContents.unknownPaths)
64
- for amendmentPaths in uploadContents.amendmentPaths.values():
65
- paths.update(amendmentPaths)
66
- for path in paths:
67
- if len(str(path.name)) > 31 or not FILENAME_STEM_PATTERN.match(path.stem):
80
+ for path, pathInfo in uploadContents.uploadPaths.items():
81
+ if pathInfo.isRoot and path.name not in ALLOWED_ROOT_FOLDERS:
68
82
  yield Validation.error(
69
- codes='EDINET.EC0121E',
70
- msg=_("There is a directory or file in '%(directory)s' that contains more than 31 characters "
71
- "or uses characters other than those allowed (alphanumeric characters, '-' and '_'). "
72
- "Directory or file name: '%(basename)s'. "
73
- "Please change the file name (or folder name) to within 31 characters and to usable "
74
- "characters, and upload again."),
75
- directory=str(path.parent),
76
- basename=path.name,
77
- file=str(path)
83
+ codes='EDINET.EC0100E',
84
+ msg=_("An illegal directory is found directly under the transferred directory. "
85
+ "Directory name or file name: '%(rootDirectory)s'. "
86
+ "Delete all folders except the following folders that exist directly "
87
+ "under the root folder, and then upload again: %(allowedDirectories)s."),
88
+ rootDirectory=path.name,
89
+ allowedDirectories=', '.join(f"'{d}'" for d in ALLOWED_ROOT_FOLDERS)
78
90
  )
79
91
 
80
92
 
@@ -82,7 +94,7 @@ def rule_EC0121E(
82
94
  hook=ValidationHook.FILESOURCE,
83
95
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
84
96
  )
85
- def rule_EC0124E(
97
+ def rule_EC0124E_EC0187E(
86
98
  pluginData: ControllerPluginData,
87
99
  cntlr: Cntlr,
88
100
  fileSource: FileSource,
@@ -90,7 +102,8 @@ def rule_EC0124E(
90
102
  **kwargs: Any,
91
103
  ) -> Iterable[Validation]:
92
104
  """
93
- EDINET.EC0124E: There are no empty directories.
105
+ EDINET.EC0124E: There are no empty root directories.
106
+ EDINET.EC0187E: There are no empty subdirectories.
94
107
  """
95
108
  uploadFilepaths = pluginData.getUploadFilepaths(fileSource)
96
109
  emptyDirectories = []
@@ -98,15 +111,24 @@ def rule_EC0124E(
98
111
  if path.suffix:
99
112
  continue
100
113
  if not any(path in p.parents for p in uploadFilepaths):
101
- emptyDirectories.append(str(path))
114
+ emptyDirectories.append(path)
102
115
  for emptyDirectory in emptyDirectories:
103
- yield Validation.error(
104
- codes='EDINET.EC0124E',
105
- msg=_("There is no file directly under '%(emptyDirectory)s'. "
106
- "No empty folders. "
107
- "Please store the file in the appropriate folder or delete the folder and upload again."),
108
- emptyDirectory=emptyDirectory,
109
- )
116
+ if len(emptyDirectory.parts) <= 1:
117
+ yield Validation.error(
118
+ codes='EDINET.EC0124E',
119
+ msg=_("There is no file directly under '%(emptyDirectory)s'. "
120
+ "No empty root folders. "
121
+ "Please store the file in the appropriate folder or delete the folder and upload again."),
122
+ emptyDirectory=str(emptyDirectory),
123
+ )
124
+ else:
125
+ yield Validation.error(
126
+ codes='EDINET.EC0187E',
127
+ msg=_("'%(parentDirectory)s' contains a subordinate directory ('%(emptyDirectory)s') with no files. "
128
+ "Please store the file in the corresponding subfolder or delete the subfolder and upload again."),
129
+ parentDirectory=str(emptyDirectory.parent),
130
+ emptyDirectory=str(emptyDirectory),
131
+ )
110
132
 
111
133
 
112
134
  @validation(
@@ -160,22 +182,13 @@ def rule_EC0130E(
160
182
  EDINET.EC0130E: File extensions must match the file extensions allowed in Figure 2-1-3 and Figure 2-1-5.
161
183
  """
162
184
  uploadContents = pluginData.getUploadContents(fileSource)
163
- checks = []
164
- for instanceType, amendmentPaths in uploadContents.amendmentPaths.items():
165
- for amendmentPath in amendmentPaths:
166
- isSubdirectory = amendmentPath.parent.name != instanceType.value
167
- checks.append((amendmentPath, True, instanceType, isSubdirectory))
168
- for instanceType, formPaths in uploadContents.instances.items():
169
- for amendmentPath in formPaths:
170
- isSubdirectory = amendmentPath.parent.name != instanceType.value
171
- checks.append((amendmentPath, False, instanceType, isSubdirectory))
172
- for path, isAmendment, instanceType, isSubdirectory in checks:
173
- ext = path.suffix
174
- if len(ext) == 0:
185
+ for path, pathInfo in uploadContents.uploadPaths.items():
186
+ if pathInfo.reportFolderType is None or pathInfo.isDirectory:
175
187
  continue
176
- validExtensions = instanceType.getValidExtensions(isAmendment, isSubdirectory)
188
+ validExtensions = pathInfo.reportFolderType.getValidExtensions(pathInfo.isCorrection, pathInfo.isSubdirectory)
177
189
  if validExtensions is None:
178
190
  continue
191
+ ext = path.suffix
179
192
  if ext not in validExtensions:
180
193
  yield Validation.error(
181
194
  codes='EDINET.EC0130E',
@@ -207,18 +220,17 @@ def rule_EC0132E(
207
220
  EDINET.EC0132E: Store the manifest file directly under the relevant folder.
208
221
  """
209
222
  uploadContents = pluginData.getUploadContents(fileSource)
210
- for instanceType in (InstanceType.AUDIT_DOC, InstanceType.PRIVATE_DOC, InstanceType.PUBLIC_DOC):
211
- if instanceType not in uploadContents.instances:
223
+ for reportFolderType, paths in uploadContents.reports.items():
224
+ if reportFolderType.isAttachment:
212
225
  continue
213
- if instanceType.manifestPath in uploadContents.instances.get(instanceType, []):
214
- continue
215
- yield Validation.error(
216
- codes='EDINET.EC0132E',
217
- msg=_("'%(expectedManifestName)s' does not exist in '%(expectedManifestDirectory)s'. "
218
- "Please store the manifest file (or cover file) directly under the relevant folder and upload it again. "),
219
- expectedManifestName=instanceType.manifestPath.name,
220
- expectedManifestDirectory=str(instanceType.manifestPath.parent),
221
- )
226
+ if reportFolderType.manifestPath not in paths:
227
+ yield Validation.error(
228
+ codes='EDINET.EC0132E',
229
+ msg=_("'%(expectedManifestName)s' does not exist in '%(expectedManifestDirectory)s'. "
230
+ "Please store the manifest file (or cover file) directly under the relevant folder and upload it again. "),
231
+ expectedManifestName=reportFolderType.manifestPath.name,
232
+ expectedManifestDirectory=str(reportFolderType.manifestPath.parent),
233
+ )
222
234
 
223
235
 
224
236
  @validation(
@@ -279,6 +291,36 @@ def rule_EC0188E(
279
291
  )
280
292
 
281
293
 
294
+ @validation(
295
+ hook=ValidationHook.FILESOURCE,
296
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
297
+ )
298
+ def rule_EC0192E(
299
+ pluginData: ControllerPluginData,
300
+ cntlr: Cntlr,
301
+ fileSource: FileSource,
302
+ *args: Any,
303
+ **kwargs: Any,
304
+ ) -> Iterable[Validation]:
305
+ """
306
+ EDINET.EC0192E: The cover file for PrivateDoc cannot be set because it uses a
307
+ PublicDoc cover file. Please delete the cover file from PrivateDoc and upload
308
+ it again.
309
+ """
310
+ uploadContents = pluginData.getUploadContents(fileSource)
311
+ for path, pathInfo in uploadContents.uploadPaths.items():
312
+ if not pathInfo.isCoverPage:
313
+ continue
314
+ # Only applies to PrivateDoc correction reports
315
+ if pathInfo.isCorrection and pathInfo.reportFolderType == ReportFolderType.PRIVATE_DOC:
316
+ yield Validation.error(
317
+ codes='EDINET.EC0192E',
318
+ msg=_("The cover file for PrivateDoc ('%(file)s') cannot be set because it uses a PublicDoc cover file. "
319
+ "Please delete the cover file from PrivateDoc and upload it again."),
320
+ file=str(path),
321
+ )
322
+
323
+
282
324
  @validation(
283
325
  hook=ValidationHook.FILESOURCE,
284
326
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
@@ -315,6 +357,82 @@ def rule_EC0198E(
315
357
  )
316
358
 
317
359
 
360
+ @validation(
361
+ hook=ValidationHook.FILESOURCE,
362
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
363
+ )
364
+ def rule_EC0233E(
365
+ pluginData: ControllerPluginData,
366
+ cntlr: Cntlr,
367
+ fileSource: FileSource,
368
+ *args: Any,
369
+ **kwargs: Any,
370
+ ) -> Iterable[Validation]:
371
+ """
372
+ EDINET.EC0233E: There is a file in the report directory that comes before the cover file
373
+ in file name sort order.
374
+
375
+ NOTE: This includes files in subdirectories. For example, PublicDoc/00000000_images/image.png
376
+ comes before PublicDoc/0000000_header_*.htm
377
+ """
378
+ uploadContents = pluginData.getUploadContents(fileSource)
379
+ directories = defaultdict(list)
380
+ for path in uploadContents.sortedPaths:
381
+ pathInfo = uploadContents.uploadPaths[path]
382
+ if pathInfo.isDirectory:
383
+ continue
384
+ if pathInfo.reportFolderType in (ReportFolderType.PRIVATE_DOC, ReportFolderType.PUBLIC_DOC):
385
+ directories[pathInfo.reportPath].append(pathInfo)
386
+ for reportPath, pathInfos in directories.items():
387
+ coverPagePath = next(iter(p for p in pathInfos if p.isCoverPage), None)
388
+ if coverPagePath is None:
389
+ continue
390
+ errorPathInfos = pathInfos[:pathInfos.index(coverPagePath)]
391
+ for pathInfo in errorPathInfos:
392
+ yield Validation.error(
393
+ codes='EDINET.EC0233E',
394
+ msg=_("There is a file in the report directory in '%(reportPath)s' that comes before the cover "
395
+ "file ('%(coverPage)s') in file name sort order. "
396
+ "Directory name or file name: '%(path)s'. "
397
+ "Please make sure that there are no files that come before the cover file in the file "
398
+ "name sort order, and then upload again."),
399
+ reportPath=str(reportPath),
400
+ coverPage=str(coverPagePath.path.name),
401
+ path=str(pathInfo.path),
402
+ )
403
+
404
+
405
+ @validation(
406
+ hook=ValidationHook.FILESOURCE,
407
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
408
+ )
409
+ def rule_EC0234E(
410
+ pluginData: ControllerPluginData,
411
+ cntlr: Cntlr,
412
+ fileSource: FileSource,
413
+ *args: Any,
414
+ **kwargs: Any,
415
+ ) -> Iterable[Validation]:
416
+ """
417
+ EDINET.EC0234E: A cover file exists in an unsupported subdirectory.
418
+ """
419
+ uploadContents = pluginData.getUploadContents(fileSource)
420
+ for path, pathInfo in uploadContents.uploadPaths.items():
421
+ if pathInfo.isDirectory:
422
+ continue
423
+ if pathInfo.reportFolderType not in (ReportFolderType.PRIVATE_DOC, ReportFolderType.PUBLIC_DOC):
424
+ continue
425
+ if pathInfo.isSubdirectory and pathInfo.isCoverPage:
426
+ yield Validation.error(
427
+ codes='EDINET.EC0234E',
428
+ msg=_("A cover file ('%(coverPage)s') exists in an unsupported subdirectory. "
429
+ "Directory: '%(directory)s'. "
430
+ "Please make sure there is no cover file in the subfolder and upload again."),
431
+ coverPage=str(path.name),
432
+ directory=str(path.parent),
433
+ )
434
+
435
+
318
436
  @validation(
319
437
  hook=ValidationHook.FILESOURCE,
320
438
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
@@ -444,6 +562,61 @@ def rule_EC1020E(
444
562
  )
445
563
 
446
564
 
565
+ @validation(
566
+ hook=ValidationHook.FILESOURCE,
567
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
568
+ )
569
+ def rule_filenames(
570
+ pluginData: ControllerPluginData,
571
+ cntlr: Cntlr,
572
+ fileSource: FileSource,
573
+ *args: Any,
574
+ **kwargs: Any,
575
+ ) -> Iterable[Validation]:
576
+ """
577
+ EDINET.EC0121E: There is a directory or file that contains
578
+ more than 31 characters or uses characters other than those allowed (alphanumeric characters,
579
+ '-' and '_').
580
+ Note: Applies to everything EXCEPT files directly beneath non-correction report folders.
581
+
582
+ EDINET.EC0200E: There is a file that uses characters other
583
+ than those allowed (alphanumeric characters, '-' and '_').
584
+ Note: Applies ONLY to files directly beneath non-correction report folders.
585
+ """
586
+ for path, pathInfo in pluginData.getUploadContents(fileSource).uploadPaths.items():
587
+ isReportFile = (
588
+ not pathInfo.isAttachment and
589
+ not pathInfo.isCorrection and
590
+ not pathInfo.isDirectory and
591
+ not pathInfo.isSubdirectory
592
+ )
593
+ charactersAreValid = FILENAME_STEM_PATTERN.fullmatch(path.stem)
594
+ lengthIsValid = isReportFile or (len(path.name) <= 31)
595
+ if charactersAreValid and lengthIsValid:
596
+ continue
597
+ if isReportFile:
598
+ yield Validation.error(
599
+ codes='EDINET.EC0200E',
600
+ msg=_("There is a file inside the XBRL directory that uses characters "
601
+ "other than those allowed (alphanumeric characters, '-' and '_'). "
602
+ "File: '%(path)s'. "
603
+ "Please change the filename to usable characters, and upload again."),
604
+ path=str(path)
605
+ )
606
+ else:
607
+ yield Validation.error(
608
+ codes='EDINET.EC0121E',
609
+ msg=_("There is a directory or file in '%(directory)s' that contains more "
610
+ "than 31 characters or uses characters other than those allowed "
611
+ "(alphanumeric characters, '-' and '_'). "
612
+ "Directory or filename: '%(basename)s'. "
613
+ "Please change the file name (or folder name) to within 31 characters and to usable "
614
+ "characters, and upload again."),
615
+ directory=str(path.parent),
616
+ basename=path.name,
617
+ )
618
+
619
+
447
620
  @validation(
448
621
  hook=ValidationHook.FILESOURCE,
449
622
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
@@ -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'),