arelle-release 2.37.56__py3-none-any.whl → 2.37.58__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/Cntlr.py +1 -0
  2. arelle/CntlrCmdLine.py +1 -0
  3. arelle/DialogPluginManager.py +6 -4
  4. arelle/ErrorManager.py +7 -1
  5. arelle/PluginManager.py +129 -100
  6. arelle/ValidateDuplicateFacts.py +1 -1
  7. arelle/XbrlConst.py +1 -0
  8. arelle/_version.py +2 -2
  9. arelle/plugin/validate/EDINET/Constants.py +19 -18
  10. arelle/plugin/validate/EDINET/ControllerPluginData.py +11 -4
  11. arelle/plugin/validate/EDINET/CoverPageRequirements.py +118 -0
  12. arelle/plugin/validate/EDINET/FilingFormat.py +253 -0
  13. arelle/plugin/validate/EDINET/FormType.py +81 -0
  14. arelle/plugin/validate/EDINET/PluginValidationDataExtension.py +171 -33
  15. arelle/plugin/validate/EDINET/resources/cover-page-requirements.csv +27 -0
  16. arelle/plugin/validate/EDINET/rules/edinet.py +1 -1
  17. arelle/plugin/validate/EDINET/rules/gfm.py +271 -2
  18. arelle/plugin/validate/EDINET/rules/upload.py +183 -10
  19. arelle/plugin/validate/ROS/PluginValidationDataExtension.py +2 -0
  20. arelle/plugin/validate/ROS/ValidationPluginExtension.py +1 -0
  21. arelle/plugin/validate/ROS/rules/ros.py +37 -7
  22. {arelle_release-2.37.56.dist-info → arelle_release-2.37.58.dist-info}/METADATA +1 -1
  23. {arelle_release-2.37.56.dist-info → arelle_release-2.37.58.dist-info}/RECORD +27 -23
  24. {arelle_release-2.37.56.dist-info → arelle_release-2.37.58.dist-info}/WHEEL +0 -0
  25. {arelle_release-2.37.56.dist-info → arelle_release-2.37.58.dist-info}/entry_points.txt +0 -0
  26. {arelle_release-2.37.56.dist-info → arelle_release-2.37.58.dist-info}/licenses/LICENSE.md +0 -0
  27. {arelle_release-2.37.56.dist-info → arelle_release-2.37.58.dist-info}/top_level.txt +0 -0
arelle/Cntlr.py CHANGED
@@ -161,6 +161,7 @@ class Cntlr:
161
161
  disable_persistent_config: bool = False,
162
162
  betaFeatures: dict[str, bool] | None = None
163
163
  ) -> None:
164
+ self.logger = None
164
165
  if betaFeatures is None:
165
166
  betaFeatures = {}
166
167
  self.betaFeatures = {
arelle/CntlrCmdLine.py CHANGED
@@ -950,6 +950,7 @@ class CntlrCmdLine(Cntlr.Cntlr):
950
950
  if options.testcaseFilters:
951
951
  fo.testcaseFilters = options.testcaseFilters
952
952
  if options.testcaseResultsCaptureWarnings:
953
+ self.errorManager.setErrorCaptureLevel(logging._checkLevel("WARNING"))
953
954
  fo.testcaseResultsCaptureWarnings = True
954
955
  if options.testcaseResultOptions:
955
956
  fo.testcaseResultOptions = options.testcaseResultOptions
@@ -386,15 +386,17 @@ class DialogPluginManager(Toplevel):
386
386
  group = {
387
387
  "ixbrl-viewer": "1", # pip installed Arelle viewer
388
388
  "iXBRLViewerPlugin": "2", # git clone installed Arelle viewer
389
- "Edgar Renderer": "3",
389
+ "EDGAR": "3",
390
+ "Inline XBRL Document Set": "4",
391
+ "XBRL rule processor (xule)": "5",
390
392
  }.get(key)
391
393
  if not group:
392
394
  if key.startswith("Validate"):
393
- group = "4"
395
+ group = "6"
394
396
  elif key.startswith("xbrlDB"):
395
- group = "5"
397
+ group = "7"
396
398
  else:
397
- group = "6"
399
+ group = "8"
398
400
  return group + key.lower()
399
401
 
400
402
  @staticmethod
arelle/ErrorManager.py CHANGED
@@ -180,7 +180,10 @@ class ErrorManager:
180
180
  objectUrl = arg
181
181
  else:
182
182
  try:
183
- objectUrl = arg.modelDocument.displayUri
183
+ if isinstance(arg, ObjectPropertyViewWrapper):
184
+ objectUrl = arg.modelObject.modelDocument.displayUri
185
+ else:
186
+ objectUrl = arg.modelDocument.displayUri
184
187
  except AttributeError:
185
188
  try:
186
189
  objectUrl = arg.displayUri
@@ -313,3 +316,6 @@ class ErrorManager:
313
316
  if isinstance(argValue, dict):
314
317
  return dict((self._loggableValue(k), self._loggableValue(v)) for k, v in argValue.items())
315
318
  return str(argValue)
319
+
320
+ def setErrorCaptureLevel(self, errorCaptureLevel: int) -> None:
321
+ self._errorCaptureLevel = errorCaptureLevel
arelle/PluginManager.py CHANGED
@@ -1,31 +1,41 @@
1
1
  '''
2
2
  See COPYRIGHT.md for copyright information.
3
-
4
- based on pull request 4
5
-
6
3
  '''
7
4
  from __future__ import annotations
8
- import os, sys, types, time, ast, importlib, io, json, gettext, traceback
9
- from dataclasses import dataclass
10
- from importlib.metadata import entry_points, EntryPoint
5
+
6
+ import ast
7
+ import gettext
8
+ from glob import glob
11
9
  import importlib.util
10
+ import json
12
11
  import logging
13
-
12
+ import os
13
+ import sys
14
+ import time
15
+ import traceback
16
+ import types
17
+ from collections import defaultdict
18
+ from collections.abc import Callable, Iterator
19
+ from dataclasses import dataclass
20
+ from importlib.metadata import EntryPoint, entry_points
21
+ from numbers import Number
22
+ from pathlib import Path
14
23
  from types import ModuleType
15
24
  from typing import TYPE_CHECKING, Any, cast
16
- from arelle.Locale import getLanguageCodes
25
+
17
26
  import arelle.FileSource
27
+ from arelle.Locale import getLanguageCodes
18
28
  from arelle.PythonUtil import isLegacyAbs
29
+ from arelle.typing import TypeGetText
19
30
  from arelle.UrlUtil import isAbsolute
20
- from pathlib import Path
21
- from collections import OrderedDict, defaultdict
22
- from collections.abc import Callable, Iterator
23
-
24
31
 
25
32
  if TYPE_CHECKING:
26
33
  # Prevent potential circular import error
27
34
  from .Cntlr import Cntlr
28
35
 
36
+
37
+ _: TypeGetText
38
+
29
39
  PLUGIN_TRACE_FILE = None
30
40
  # PLUGIN_TRACE_FILE = "c:/temp/pluginerr.txt"
31
41
  PLUGIN_TRACE_LEVEL = logging.WARNING
@@ -58,7 +68,7 @@ def init(cntlr: Cntlr, loadPluginConfig: bool = True) -> None:
58
68
  if loadPluginConfig:
59
69
  try:
60
70
  pluginJsonFile = cntlr.userAppDir + os.sep + "plugins.json"
61
- with io.open(pluginJsonFile, 'rt', encoding='utf-8') as f:
71
+ with open(pluginJsonFile, encoding='utf-8') as f:
62
72
  pluginConfig = json.load(f)
63
73
  freshenModuleInfos()
64
74
  except Exception:
@@ -79,29 +89,45 @@ def reset() -> None: # force reloading modules and plugin infos
79
89
  pluginMethodsForClasses.clear() # dict by class of list of ordered callable function objects
80
90
 
81
91
  def orderedPluginConfig():
82
- return OrderedDict(
83
- (('modules',OrderedDict((moduleName,
84
- OrderedDict(sorted(moduleInfo.items(),
85
- key=lambda k: {'name': '01',
86
- 'status': '02',
87
- 'version': '03',
88
- 'fileDate': '04', 'version': '05',
89
- 'description': '05',
90
- 'moduleURL': '06',
91
- 'localeURL': '07',
92
- 'localeDomain': '08',
93
- 'license': '09',
94
- 'author': '10',
95
- 'copyright': '11',
96
- 'classMethods': '12'}.get(k[0],k[0]))))
97
- for moduleName, moduleInfo in sorted(pluginConfig['modules'].items()))),
98
- ('classes',OrderedDict(sorted(pluginConfig['classes'].items())))))
92
+ fieldOrder = [
93
+ 'name',
94
+ 'status',
95
+ 'fileDate',
96
+ 'version',
97
+ 'description',
98
+ 'moduleURL',
99
+ 'localeURL',
100
+ 'localeDomain',
101
+ 'license',
102
+ 'author',
103
+ 'copyright',
104
+ 'classMethods',
105
+ ]
106
+ priorityIndex = {k: i for i, k in enumerate(fieldOrder)}
107
+
108
+ def sortModuleInfo(moduleInfo):
109
+ # Prioritize known fields by the index in fieldOrder; sort others alphabetically
110
+ orderedKeys = sorted(
111
+ moduleInfo.keys(),
112
+ key=lambda k: (priorityIndex.get(k, len(priorityIndex)), k)
113
+ )
114
+ return {k: moduleInfo[k] for k in orderedKeys}
115
+
116
+ orderedModules = {
117
+ moduleName: sortModuleInfo(pluginConfig['modules'][moduleName])
118
+ for moduleName in sorted(pluginConfig['modules'].keys())
119
+ }
120
+
121
+ return {
122
+ 'modules': orderedModules,
123
+ 'classes': dict(sorted(pluginConfig['classes'].items()))
124
+ }
99
125
 
100
126
  def save(cntlr: Cntlr) -> None:
101
127
  global pluginConfigChanged
102
128
  if pluginConfigChanged and cntlr.hasFileSystem and not cntlr.disablePersistentConfig:
103
129
  pluginJsonFile = cntlr.userAppDir + os.sep + "plugins.json"
104
- with io.open(pluginJsonFile, 'wt', encoding='utf-8') as f:
130
+ with open(pluginJsonFile, 'w', encoding='utf-8') as f:
105
131
  jsonStr = str(json.dumps(orderedPluginConfig(), ensure_ascii=False, indent=2)) # might not be unicode in 2.7
106
132
  f.write(jsonStr)
107
133
  pluginConfigChanged = False
@@ -155,7 +181,6 @@ moduleInfo = {
155
181
 
156
182
  '''
157
183
 
158
-
159
184
  def logPluginTrace(message: str, level: Number) -> None:
160
185
  """
161
186
  If plugin trace file logging is configured, logs `message` to it.
@@ -274,6 +299,14 @@ def getModuleFilename(moduleURL: str, reload: bool, normalize: bool, base: str |
274
299
  if moduleFilename:
275
300
  # `moduleFilename` normalized to an existing script
276
301
  return moduleFilename, None
302
+ if base and not _isAbsoluteModuleURL(moduleURL):
303
+ # Search for a matching plugin deeper in the plugin directory tree.
304
+ # Handles cases where a plugin exists in a nested structure, such as
305
+ # when a developer clones an entire repository into the plugin directory.
306
+ # Example: arelle/plugin/xule/plugin/xule/__init__.py
307
+ for path in glob("**/" + moduleURL.replace('\\', '/'), recursive=True):
308
+ if normalizedPath := normalizeModuleFilename(path):
309
+ return normalizedPath, None
277
310
  # `moduleFilename` did not map to a local filepath or did not normalize to a script
278
311
  # Try using `moduleURL` to search for pip-installed entry point
279
312
  entryPointRef = EntryPointRef.get(moduleURL)
@@ -416,7 +449,7 @@ def moduleModuleInfo(
416
449
 
417
450
  if moduleFilename:
418
451
  try:
419
- logPluginTrace("Scanning module for plug-in info: {}".format(moduleFilename), logging.INFO)
452
+ logPluginTrace(f"Scanning module for plug-in info: {moduleFilename}", logging.INFO)
420
453
  moduleInfo = parsePluginInfo(moduleURL, moduleFilename, entryPoint)
421
454
  if moduleInfo is None:
422
455
  return None
@@ -426,38 +459,50 @@ def moduleModuleInfo(
426
459
  del moduleInfo["importURLs"]
427
460
  moduleImports = moduleInfo["moduleImports"]
428
461
  del moduleInfo["moduleImports"]
429
- _moduleImportsSubtree = False
462
+ moduleImportsSubtree = False
430
463
  mergedImportURLs = []
431
464
 
432
- for _url in importURLs:
433
- if _url.startswith("module_import"):
465
+ for url in importURLs:
466
+ if url.startswith("module_import"):
434
467
  for moduleImport in moduleImports:
435
468
  mergedImportURLs.append(moduleImport + ".py")
436
- if _url == "module_import_subtree":
437
- _moduleImportsSubtree = True
438
- elif _url == "module_subtree":
469
+ if url == "module_import_subtree":
470
+ moduleImportsSubtree = True
471
+ elif url == "module_subtree":
439
472
  for _dir in os.listdir(moduleDir):
440
- _subtreeModule = os.path.join(moduleDir,_dir)
441
- if os.path.isdir(_subtreeModule) and _dir != "__pycache__":
442
- mergedImportURLs.append(_subtreeModule)
473
+ subtreeModule = os.path.join(moduleDir,_dir)
474
+ if os.path.isdir(subtreeModule) and _dir != "__pycache__":
475
+ mergedImportURLs.append(subtreeModule)
443
476
  else:
444
- mergedImportURLs.append(_url)
445
- if parentImportsSubtree and not _moduleImportsSubtree:
446
- _moduleImportsSubtree = True
477
+ mergedImportURLs.append(url)
478
+ if parentImportsSubtree and not moduleImportsSubtree:
479
+ moduleImportsSubtree = True
447
480
  for moduleImport in moduleImports:
448
481
  mergedImportURLs.append(moduleImport + ".py")
449
482
  imports = []
450
- for _url in mergedImportURLs:
451
- if isAbsolute(_url) or isLegacyAbs(_url):
452
- _importURL = _url # URL is absolute http or local file system
453
- else: # check if exists relative to this module's directory
454
- _importURL = os.path.join(os.path.dirname(moduleURL), os.path.normpath(_url))
455
- if not os.path.exists(_importURL): # not relative to this plugin, assume standard plugin base
456
- _importURL = _url # moduleModuleInfo adjusts relative URL to plugin base
457
- _importModuleInfo = moduleModuleInfo(moduleURL=_importURL, reload=reload, parentImportsSubtree=_moduleImportsSubtree)
458
- if _importModuleInfo:
459
- _importModuleInfo["isImported"] = True
460
- imports.append(_importModuleInfo)
483
+ for url in mergedImportURLs:
484
+ importURL = url
485
+ if not _isAbsoluteModuleURL(url):
486
+ # Handle relative imports when plugin is loaded from external directory.
487
+ # When EDGAR/render imports EDGAR/validate, this works if EDGAR is in the plugin directory
488
+ # but fails if loaded externally (e.g., dev repo clone at /dev/path/to/EDGAR/).
489
+ # Solution: Find common path segments to resolve /dev/path/to/EDGAR/validate
490
+ # from the importing module at /dev/path/to/EDGAR/render.
491
+ modulePath = Path(moduleFilename)
492
+ importPath = Path(url)
493
+ if importPath.parts:
494
+ importFirstPart = importPath.parts[0]
495
+ for i, modulePathPart in enumerate(reversed(modulePath.parts)):
496
+ if modulePathPart != importFirstPart:
497
+ continue
498
+ # Found a potential branching point, construct and check a new path
499
+ candidateImportURL = str(modulePath.parents[i] / importPath)
500
+ if normalizeModuleFilename(candidateImportURL):
501
+ importURL = candidateImportURL
502
+ importModuleInfo = moduleModuleInfo(moduleURL=importURL, reload=reload, parentImportsSubtree=moduleImportsSubtree)
503
+ if importModuleInfo:
504
+ importModuleInfo["isImported"] = True
505
+ imports.append(importModuleInfo)
461
506
  moduleInfo["imports"] = imports
462
507
  logPluginTrace(f"Successful module plug-in info: {moduleFilename}", logging.INFO)
463
508
  return moduleInfo
@@ -477,43 +522,30 @@ def moduleInfo(pluginInfo):
477
522
  moduleInfo.getdefault('classes', []).append(name)
478
523
 
479
524
 
480
- def _get_name_dir_prefix(
481
- controller: Cntlr,
482
- pluginBase: str,
483
- moduleURL: str,
484
- packagePrefix: str = "",
485
- ) -> tuple[str, str, str] | tuple[None, None, None]:
486
- """Get the name, directory and prefix of a module."""
487
- moduleFilename: str
488
- moduleDir: str
489
- packageImportPrefix: str
525
+ def _isAbsoluteModuleURL(moduleURL: str) -> bool:
526
+ return isAbsolute(moduleURL) or isLegacyAbs(moduleURL)
490
527
 
491
- moduleFilename = controller.webCache.getfilename(
492
- url=moduleURL, normalize=True, base=pluginBase, allowTransformation=False
493
- )
494
528
 
495
- if moduleFilename:
496
- if os.path.basename(moduleFilename) == "__init__.py" and os.path.isfile(
497
- moduleFilename
498
- ):
499
- moduleFilename = os.path.dirname(
500
- moduleFilename
501
- ) # want just the dirpart of package
502
-
503
- if os.path.isdir(moduleFilename) and os.path.isfile(
504
- os.path.join(moduleFilename, "__init__.py")
505
- ):
506
- moduleDir = os.path.dirname(moduleFilename)
507
- moduleName = os.path.basename(moduleFilename)
508
- packageImportPrefix = moduleName + "."
509
- else:
510
- moduleName = os.path.basename(moduleFilename).partition(".")[0]
511
- moduleDir = os.path.dirname(moduleFilename)
512
- packageImportPrefix = packagePrefix
529
+ def _get_name_dir_prefix(modulePath: Path, packagePrefix: str = "") -> tuple[str, str, str] | tuple[None, None, None]:
530
+ """Get the name, directory and prefix of a module."""
531
+ moduleName = None
532
+ moduleDir = None
533
+ packageImportPrefix = None
534
+ initFileName = "__init__.py"
535
+
536
+ if modulePath.is_file() and modulePath.name == initFileName:
537
+ modulePath = modulePath.parent
513
538
 
514
- return (moduleName, moduleDir, packageImportPrefix)
539
+ if modulePath.is_dir() and (modulePath / initFileName).is_file():
540
+ moduleName = modulePath.name
541
+ moduleDir = str(modulePath.parent)
542
+ packageImportPrefix = moduleName + "."
543
+ elif modulePath.is_file() and modulePath.suffix == ".py":
544
+ moduleName = modulePath.stem
545
+ moduleDir = str(modulePath.parent)
546
+ packageImportPrefix = packagePrefix
515
547
 
516
- return (None, None, None)
548
+ return (moduleName, moduleDir, packageImportPrefix)
517
549
 
518
550
  def _get_location(moduleDir: str, moduleName: str) -> str:
519
551
  """Get the file name of a plugin."""
@@ -543,13 +575,9 @@ def _find_and_load_module(moduleDir: str, moduleName: str) -> ModuleType | None:
543
575
  def loadModule(moduleInfo: dict[str, Any], packagePrefix: str="") -> None:
544
576
  name = moduleInfo['name']
545
577
  moduleURL = moduleInfo['moduleURL']
578
+ modulePath = Path(moduleInfo['path'])
546
579
 
547
- moduleName, moduleDir, packageImportPrefix = _get_name_dir_prefix(
548
- controller=_cntlr,
549
- pluginBase=_pluginBase,
550
- moduleURL=moduleURL,
551
- packagePrefix=packagePrefix,
552
- )
580
+ moduleName, moduleDir, packageImportPrefix = _get_name_dir_prefix(modulePath, packagePrefix)
553
581
 
554
582
  if all(p is None for p in [moduleName, moduleDir, packageImportPrefix]):
555
583
  _cntlr.addToLog(message=_ERROR_MESSAGE_IMPORT_TEMPLATE.format(name), level=logging.ERROR)
@@ -566,10 +594,12 @@ def loadModule(moduleInfo: dict[str, Any], packagePrefix: str="") -> None:
566
594
  localeDir = os.path.dirname(module.__file__) + os.sep + pluginInfo['localeURL']
567
595
  try:
568
596
  _gettext = gettext.translation(pluginInfo['localeDomain'], localeDir, getLanguageCodes())
569
- except IOError:
570
- _gettext = lambda x: x # no translation
597
+ except OSError:
598
+ def _gettext(x):
599
+ return x # no translation
571
600
  else:
572
- _gettext = lambda x: x
601
+ def _gettext(x):
602
+ return x
573
603
  for key, value in pluginInfo.items():
574
604
  if key == 'name':
575
605
  if name:
@@ -627,8 +657,7 @@ def pluginClassMethods(className: str) -> Iterator[Callable[..., Any]]:
627
657
  if className in pluginInfo:
628
658
  pluginMethodsForClass.append(pluginInfo[className])
629
659
  pluginMethodsForClasses[className] = pluginMethodsForClass
630
- for method in pluginMethodsForClass:
631
- yield method
660
+ yield from pluginMethodsForClass
632
661
 
633
662
 
634
663
  def addPluginModule(name: str) -> dict[str, Any] | None:
@@ -402,7 +402,7 @@ def getAspectEqualFacts(
402
402
  fact.qname,
403
403
  (
404
404
  cast(str, fact.xmlLang or "").lower()
405
- if useLang and fact.concept.type.isWgnStringFactType
405
+ if useLang and fact.concept.type.isOimTextFactType
406
406
  else None
407
407
  ),
408
408
  )
arelle/XbrlConst.py CHANGED
@@ -139,6 +139,7 @@ qnXlLocatorType = qname("{http://www.xbrl.org/2003/XLink}xl:locatorType")
139
139
  qnXlResourceType = qname("{http://www.xbrl.org/2003/XLink}xl:resourceType")
140
140
  qnXlArcType = qname("{http://www.xbrl.org/2003/XLink}xl:arcType")
141
141
  xhtml = "http://www.w3.org/1999/xhtml"
142
+ qnXhtmlMeta = qname("{http://www.w3.org/1999/xhtml}meta")
142
143
  ixbrl = "http://www.xbrl.org/2008/inlineXBRL"
143
144
  ixbrl11 = "http://www.xbrl.org/2013/inlineXBRL"
144
145
  ixbrlAll = {ixbrl, ixbrl11}
arelle/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '2.37.56'
32
- __version_tuple__ = version_tuple = (2, 37, 56)
31
+ __version__ = version = '2.37.58'
32
+ __version_tuple__ = version_tuple = (2, 37, 58)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -10,25 +10,7 @@ class AccountingStandard(Enum):
10
10
  JAPAN_GAAP = 'Japan GAAP'
11
11
  US_GAAP = 'US GAAP'
12
12
 
13
- class FormType(Enum):
14
- FORM_2_4 = '第二号の四様式'
15
- FORM_2_7 = '第二号の七様式'
16
- FORM_3 = '第三号様式'
17
- FORM_4 = '第四号様式'
18
13
 
19
- @property
20
- def isStockReport(self) -> bool:
21
- return self in STOCK_REPORT_FORMS
22
-
23
- CORPORATE_FORMS =frozenset([
24
- FormType.FORM_2_4,
25
- FormType.FORM_2_7,
26
- FormType.FORM_3,
27
- ])
28
- STOCK_REPORT_FORMS = frozenset([
29
- FormType.FORM_3,
30
- FormType.FORM_4,
31
- ])
32
14
  qnEdinetManifestInsert = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}insert")
33
15
  qnEdinetManifestInstance = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}instance")
34
16
  qnEdinetManifestItem = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}item")
@@ -76,3 +58,22 @@ PROHIBITED_HTML_TAGS = frozenset({
76
58
  'select',
77
59
  'textarea',
78
60
  })
61
+
62
+ NUMERIC_LABEL_ROLES = frozenset({
63
+ 'http://www.xbrl.org/2003/role/positiveLabel',
64
+ 'http://www.xbrl.org/2003/role/positiveTerseLabel',
65
+ 'http://www.xbrl.org/2003/role/positiveVerboseLabel',
66
+ 'http://www.xbrl.org/2003/role/negativeLabel',
67
+ 'http://www.xbrl.org/2003/role/negativeTerseLabel',
68
+ 'http://www.xbrl.org/2003/role/negativeVerboseLabel',
69
+ 'http://www.xbrl.org/2003/role/zeroLabel',
70
+ 'http://www.xbrl.org/2003/role/zeroTerseLabel',
71
+ 'http://www.xbrl.org/2003/role/zeroVerboseLabel',
72
+ 'http://www.xbrl.org/2003/role/totalLabel',
73
+ 'http://www.xbrl.org/2009/role/negatedLabel',
74
+ 'http://www.xbrl.org/2009/role/negatedPeriodEndLabel',
75
+ 'http://www.xbrl.org/2009/role/negatedPeriodStartLabel',
76
+ 'http://www.xbrl.org/2009/role/negatedTotalLabel',
77
+ 'http://www.xbrl.org/2009/role/negatedNetLabel',
78
+ 'http://www.xbrl.org/2009/role/negatedTerseLabel',
79
+ })
@@ -12,9 +12,12 @@ from typing import TYPE_CHECKING
12
12
 
13
13
  from arelle.Cntlr import Cntlr
14
14
  from arelle.FileSource import FileSource
15
+ from arelle.ModelValue import QName
15
16
  from arelle.typing import TypeGetText
16
17
  from arelle.utils.PluginData import PluginData
17
18
  from . import Constants
19
+ from .CoverPageRequirements import CoverPageRequirements
20
+ from .FilingFormat import FilingFormat
18
21
  from .ReportFolderType import ReportFolderType
19
22
  from .UploadContents import UploadContents, UploadPathInfo
20
23
 
@@ -45,6 +48,10 @@ class ControllerPluginData(PluginData):
45
48
  """
46
49
  self._manifestInstancesById[manifestInstance.id] = manifestInstance
47
50
 
51
+ @lru_cache(1)
52
+ def getCoverPageRequirements(self, csvPath: Path, coverPageItems: tuple[QName, ...], filingFormats: tuple[FilingFormat, ...]) -> CoverPageRequirements:
53
+ return CoverPageRequirements(csvPath, coverPageItems, filingFormats)
54
+
48
55
  def getManifestInstances(self) -> list[ManifestInstance]:
49
56
  """
50
57
  Retrieve all loaded manifest instances.
@@ -147,10 +154,10 @@ class ControllerPluginData(PluginData):
147
154
  if (fileSource.fs is None or
148
155
  not isinstance(fileSource.fs, zipfile.ZipFile)):
149
156
  if fileSource.cntlr is not None:
150
- fileSource.cntlr.error(
151
- level="WARNING",
152
- codes="EDINET.uploadNotValidated",
153
- msg=_("The target file is not a zip file, so upload validation could not be performed.")
157
+ fileSource.cntlr.addToLog(
158
+ _("The target file is not a zip file, so upload validation was not performed."),
159
+ messageCode="EDINET.uploadNotValidated",
160
+ file=str(fileSource.url)
154
161
  )
155
162
  return False
156
163
  return True
@@ -0,0 +1,118 @@
1
+ """
2
+ See COPYRIGHT.md for copyright information.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ from enum import Enum
7
+ from pathlib import Path
8
+
9
+ from arelle.ModelValue import QName
10
+ from .FilingFormat import FilingFormat
11
+
12
+
13
+ # Cover page requirements parsing is designed so that the contents of Attachment #5
14
+ # in "(Appendix) Taxonomy Extension Guideline" (ESE140111.zip), or future versions,
15
+ # can be easily exported to a CSV file where the rows correspond to Cover Page items
16
+ # andt the columns correspond to different formats.
17
+ class CoverPageRequirements:
18
+ _coverPageItems: tuple[QName, ...]
19
+ _csvPath: Path
20
+ _data: dict[QName, dict[FilingFormat, CoverPageItemStatus | None]] | None
21
+ _filingFormats: tuple[FilingFormat, ...]
22
+
23
+ def __init__(self, csvPath: Path, coverPageItems: tuple[QName, ...], filingFormats: tuple[FilingFormat, ...]):
24
+ self._coverPageItems = coverPageItems
25
+ self._csvPath = csvPath
26
+ self._data = None
27
+ self._filingFormats = filingFormats
28
+
29
+ def _load(self) -> dict[QName, dict[FilingFormat, CoverPageItemStatus | None]]:
30
+ if self._data is None:
31
+ with open(self._csvPath, encoding='utf-8') as f:
32
+ data = [
33
+ [
34
+ CoverPageItemStatus.parse(cell) for cell in line.strip().split(',')
35
+ ]
36
+ for line in f.readlines()
37
+ ]
38
+ self._data = {}
39
+ assert len(data) == len(self._coverPageItems), \
40
+ "Unexpected number of rows in cover page requirements CSV."
41
+ for rowIndex, row in enumerate(data):
42
+ assert len(row) == len(self._filingFormats), \
43
+ f"Unexpected number of columns in cover page requirements CSV at row {rowIndex}."
44
+ coverPageItem = self._coverPageItems[rowIndex]
45
+ self._data[coverPageItem] = {}
46
+ for colIndex, cell in enumerate(row):
47
+ filingFormat = self._filingFormats[colIndex]
48
+ self._data[coverPageItem][filingFormat] = cell
49
+ return self._data
50
+
51
+
52
+ def get(self, coverPageItem: QName, filingFormat: FilingFormat) -> CoverPageItemStatus | None:
53
+ data = self._load()
54
+ if coverPageItem not in data:
55
+ return None
56
+ if filingFormat not in data[coverPageItem]:
57
+ return None
58
+ return data[coverPageItem][filingFormat]
59
+
60
+
61
+ class CoverPageItemStatus(Enum):
62
+ # The values of the enum correspond to the symbols used in the spreadsheet.
63
+ PROHIBITED = '×'
64
+ OPTIONAL = '△'
65
+ CONDITIONAL = '○'
66
+ REQUIRED = '◎'
67
+
68
+ @classmethod
69
+ def parse(cls, value: str) -> CoverPageItemStatus | None:
70
+ try:
71
+ return cls(value)
72
+ except ValueError:
73
+ return None
74
+
75
+ # The below values are based on Attachment #5 in "(Appendix) Taxonomy Extension Guideline" (ESE140111.zip).
76
+ # Column D lists the elements of the cover page. Rows purely for grouping are omitted.
77
+ # The order is preserved. The index is used to map to other data structures.
78
+ COVER_PAGE_ITEM_LOCAL_NAMES = (
79
+ # Submitter Information (提出者情報)
80
+ 'EDINETCodeDEI',
81
+ 'FundCodeDEI',
82
+ 'SecurityCodeDEI',
83
+ 'FilerNameInJapaneseDEI',
84
+ 'FilerNameInEnglishDEI',
85
+ 'FundNameInJapaneseDEI',
86
+ 'FundNameInEnglishDEI',
87
+
88
+ # Document Submission Information (提出書類情報)
89
+ 'CabinetOfficeOrdinanceDEI',
90
+ 'DocumentTypeDEI',
91
+ 'AccountingStandardsDEI',
92
+ 'WhetherConsolidatedFinancialStatementsArePreparedDEI',
93
+ 'IndustryCodeWhenConsolidatedFinancialStatementsArePreparedInAccordanceWithIndustrySpecificRegulationsDEI',
94
+ 'IndustryCodeWhenFinancialStatementsArePreparedInAccordanceWithIndustrySpecificRegulationsDEI',
95
+
96
+ # Current Fiscal Year (当会計期間)
97
+ 'CurrentFiscalYearStartDateDEI',
98
+ 'CurrentPeriodEndDateDEI',
99
+ 'TypeOfCurrentPeriodDEI',
100
+ 'CurrentFiscalYearEndDateDEI',
101
+
102
+ # Previous Fiscal Year (比較対象会計期間)
103
+ 'PreviousFiscalYearStartDateDEI',
104
+ 'ComparativePeriodEndDateDEI',
105
+ 'PreviousFiscalYearEndDateDEI',
106
+
107
+ # Next Fiscal Year (次の中間期の会計期間)
108
+ 'NextFiscalYearStartDateDEI',
109
+ 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI',
110
+
111
+ 'NumberOfSubmissionDEI',
112
+ 'AmendmentFlagDEI',
113
+ 'IdentificationOfDocumentSubjectToAmendmentDEI',
114
+
115
+ # Type of Correction (訂正の種類)
116
+ 'ReportAmendmentFlagDEI',
117
+ 'XBRLAmendmentFlagDEI',
118
+ )