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

arelle/XbrlConst.py CHANGED
@@ -22,6 +22,7 @@ _tuple = tuple # type: ignore[type-arg]
22
22
  xsd = "http://www.w3.org/2001/XMLSchema"
23
23
  qnXsdComplexType = qname("{http://www.w3.org/2001/XMLSchema}xsd:complexType")
24
24
  qnXsdDocumentation = qname("{http://www.w3.org/2001/XMLSchema}xsd:documentation")
25
+ qnXsdInclude = qname("{http://www.w3.org/2001/XMLSchema}xsd:include")
25
26
  qnXsdImport = qname("{http://www.w3.org/2001/XMLSchema}xsd:import")
26
27
  qnXsdSchema = qname("{http://www.w3.org/2001/XMLSchema}xsd:schema")
27
28
  qnXsdAppinfo = qname("{http://www.w3.org/2001/XMLSchema}xsd:appinfo")
arelle/_version.py CHANGED
@@ -1,7 +1,14 @@
1
1
  # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
3
 
4
- __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
5
12
 
6
13
  TYPE_CHECKING = False
7
14
  if TYPE_CHECKING:
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
9
16
  from typing import Union
10
17
 
11
18
  VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
12
20
  else:
13
21
  VERSION_TUPLE = object
22
+ COMMIT_ID = object
14
23
 
15
24
  version: str
16
25
  __version__: str
17
26
  __version_tuple__: VERSION_TUPLE
18
27
  version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
19
30
 
20
- __version__ = version = '2.37.50'
21
- __version_tuple__ = version_tuple = (2, 37, 50)
31
+ __version__ = version = '2.37.52'
32
+ __version_tuple__ = version_tuple = (2, 37, 52)
33
+
34
+ __commit_id__ = commit_id = None
@@ -37,3 +37,5 @@ qnEdinetManifestList = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}
37
37
  qnEdinetManifestTitle = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}title")
38
38
  qnEdinetManifestTocComposition = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}tocComposition")
39
39
  xhtmlDtdExtension = "xhtml1-strict-ix.dtd"
40
+
41
+ COVER_PAGE_FILENAME_PREFIX = "0000000_header_"
@@ -12,11 +12,11 @@ from typing import TYPE_CHECKING
12
12
 
13
13
  from arelle.Cntlr import Cntlr
14
14
  from arelle.FileSource import FileSource
15
- from arelle.ModelXbrl import ModelXbrl
16
15
  from arelle.typing import TypeGetText
17
16
  from arelle.utils.PluginData import PluginData
18
- from .InstanceType import InstanceType
19
- from .UploadContents import UploadContents
17
+ from . import Constants
18
+ from .ReportFolderType import ReportFolderType
19
+ from .UploadContents import UploadContents, UploadPathInfo
20
20
 
21
21
  if TYPE_CHECKING:
22
22
  from .ManifestInstance import ManifestInstance
@@ -50,35 +50,46 @@ class ControllerPluginData(PluginData):
50
50
  @lru_cache(1)
51
51
  def getUploadContents(self, fileSource: FileSource) -> UploadContents:
52
52
  uploadFilepaths = self.getUploadFilepaths(fileSource)
53
- amendmentPaths = defaultdict(list)
54
- unknownPaths = []
55
- directories = []
56
- forms = defaultdict(list)
53
+ reports = defaultdict(list)
54
+ uploadPaths = {}
57
55
  for path in uploadFilepaths:
58
- parents = list(reversed([p.name for p in path.parents if len(p.name) > 0]))
59
- if len(parents) == 0:
60
- continue
61
- if parents[0] == 'XBRL':
62
- if len(parents) > 1:
63
- formName = parents[1]
64
- instanceType = InstanceType.parse(formName)
65
- if instanceType is not None:
66
- forms[instanceType].append(path)
67
- continue
68
- formName = parents[0]
69
- instanceType = InstanceType.parse(formName)
70
- if instanceType is not None:
71
- amendmentPaths[instanceType].append(path)
56
+ if len(path.parts) == 0:
72
57
  continue
73
- if len(path.suffix) == 0:
74
- directories.append(path)
75
- continue
76
- unknownPaths.append(path)
58
+ parents = list(reversed([p.name for p in path.parents if len(p.name) > 0]))
59
+ reportFolderType = None
60
+ isCorrection = True
61
+ isDirectory = len(path.suffix) == 0
62
+ isInSubdirectory = False
63
+ reportPath = None
64
+ if len(parents) > 0:
65
+ isCorrection = parents[0] != 'XBRL'
66
+ if not isCorrection:
67
+ if len(parents) > 1:
68
+ formName = parents[1]
69
+ isInSubdirectory = len(parents) > 2
70
+ reportFolderType = ReportFolderType.parse(formName)
71
+ if reportFolderType is None:
72
+ formName = parents[0]
73
+ isInSubdirectory = len(parents) > 1
74
+ reportFolderType = ReportFolderType.parse(formName)
75
+ if reportFolderType is not None:
76
+ reportPath = Path(reportFolderType.value) if isCorrection else Path("XBRL") / reportFolderType.value
77
+ if not isCorrection:
78
+ reports[reportFolderType].append(path)
79
+ uploadPaths[path] = UploadPathInfo(
80
+ isAttachment=reportFolderType is not None and reportFolderType.isAttachment,
81
+ isCorrection=isCorrection,
82
+ isCoverPage=not isDirectory and path.stem.startswith(Constants.COVER_PAGE_FILENAME_PREFIX),
83
+ isDirectory=len(path.suffix) == 0,
84
+ isRoot=len(path.parts) == 1,
85
+ isSubdirectory=isInSubdirectory or (isDirectory and reportFolderType is not None),
86
+ path=path,
87
+ reportFolderType=reportFolderType,
88
+ reportPath=reportPath,
89
+ )
77
90
  return UploadContents(
78
- amendmentPaths={k: frozenset(v) for k, v in amendmentPaths.items() if len(v) > 0},
79
- directories=frozenset(directories),
80
- instances={k: frozenset(v) for k, v in forms.items() if len(v) > 0},
81
- unknownPaths=frozenset(unknownPaths)
91
+ reports={k: frozenset(v) for k, v in reports.items() if len(v) > 0},
92
+ uploadPaths=uploadPaths
82
93
  )
83
94
 
84
95
  @lru_cache(1)
@@ -7,15 +7,17 @@ from collections import defaultdict
7
7
  from dataclasses import dataclass
8
8
  from decimal import Decimal
9
9
  from functools import lru_cache
10
+ from lxml.etree import DTD, XML
10
11
  from operator import attrgetter
11
12
  from typing import Callable, Hashable, Iterable, cast
12
13
 
14
+ import os
13
15
  import regex
14
16
 
15
17
  from arelle.LinkbaseType import LinkbaseType
16
18
  from arelle.ModelDocument import Type as ModelDocumentType
17
19
  from arelle.ModelDtsObject import ModelConcept
18
- from arelle.ModelInstanceObject import ModelFact, ModelUnit, ModelContext
20
+ from arelle.ModelInstanceObject import ModelFact, ModelUnit, ModelContext, ModelInlineFact
19
21
  from arelle.ModelObject import ModelObject
20
22
  from arelle.ModelValue import QName, qname
21
23
  from arelle.ModelXbrl import ModelXbrl
@@ -24,7 +26,7 @@ from arelle.ValidateDuplicateFacts import getDeduplicatedFacts, DeduplicationTyp
24
26
  from arelle.XmlValidate import VALID
25
27
  from arelle.typing import TypeGetText
26
28
  from arelle.utils.PluginData import PluginData
27
- from .Constants import CORPORATE_FORMS, FormType
29
+ from .Constants import CORPORATE_FORMS, FormType, xhtmlDtdExtension
28
30
  from .ControllerPluginData import ControllerPluginData
29
31
  from .ManifestInstance import ManifestInstance
30
32
  from .Statement import Statement, STATEMENTS, BalanceSheet, StatementInstance, StatementType
@@ -166,6 +168,22 @@ class PluginValidationDataExtension(PluginData):
166
168
  )
167
169
  return balanceSheets
168
170
 
171
+ def getProblematicTextBlocks(self, modelXbrl: ModelXbrl) -> list[ModelInlineFact]:
172
+ problematicTextBlocks: list[ModelInlineFact] = []
173
+ dtd = DTD(os.path.join(modelXbrl.modelManager.cntlr.configDir, xhtmlDtdExtension))
174
+ htmlBodyTemplate = "<body><div>\n{0}\n</div></body>\n"
175
+ for fact in modelXbrl.facts:
176
+ concept = fact.concept
177
+ if isinstance(fact, ModelInlineFact) and not fact.isNil and concept is not None and concept.isTextBlock and not fact.isEscaped:
178
+ xmlBody = htmlBodyTemplate.format(fact.value)
179
+ try:
180
+ textblockXml = XML(xmlBody)
181
+ if not dtd.validate(textblockXml):
182
+ problematicTextBlocks.append(fact)
183
+ except Exception:
184
+ problematicTextBlocks.append(fact)
185
+ return problematicTextBlocks
186
+
169
187
  @lru_cache(1)
170
188
  def getStatementInstance(self, modelXbrl: ModelXbrl, statement: Statement) -> StatementInstance | None:
171
189
  if statement.roleUri not in modelXbrl.roleTypes:
@@ -242,7 +260,7 @@ class PluginValidationDataExtension(PluginData):
242
260
  return controllerPluginData.matchManifestInstance(modelXbrl.ixdsDocUrls)
243
261
 
244
262
  def hasValidNonNilFact(self, modelXbrl: ModelXbrl, qname: QName) -> bool:
245
- return any(fact is not None for fact in self.iterValidNonNilFacts(modelXbrl, qname))
263
+ return any(True for fact in self.iterValidNonNilFacts(modelXbrl, qname))
246
264
 
247
265
  def isStandardTaxonomyUrl(self, uri: str, modelXbrl: ModelXbrl) -> bool:
248
266
  return modelXbrl.modelManager.disclosureSystem.hrefValidForDisclosureSystem(uri)
@@ -8,16 +8,15 @@ from functools import cached_property, lru_cache
8
8
  from pathlib import Path
9
9
 
10
10
 
11
- class InstanceType(Enum):
11
+ class ReportFolderType(Enum):
12
12
  ATTACH_DOC = "AttachDoc"
13
13
  AUDIT_DOC = "AuditDoc"
14
- ENGLISH_DOC = "EnglishDoc"
15
14
  PRIVATE_ATTACH = "PrivateAttach"
16
15
  PRIVATE_DOC = "PrivateDoc"
17
16
  PUBLIC_DOC = "PublicDoc"
18
17
 
19
18
  @classmethod
20
- def parse(cls, value: str) -> InstanceType | None:
19
+ def parse(cls, value: str) -> ReportFolderType | None:
21
20
  try:
22
21
  return cls(value)
23
22
  except ValueError:
@@ -27,6 +26,10 @@ class InstanceType(Enum):
27
26
  def extensionCategory(self) -> ExtensionCategory | None:
28
27
  return FORM_TYPE_EXTENSION_CATEGORIES.get(self, None)
29
28
 
29
+ @cached_property
30
+ def isAttachment(self) -> bool:
31
+ return "Attach" in self.value
32
+
30
33
  @cached_property
31
34
  def manifestName(self) -> str:
32
35
  return f'manifest_{self.value}.xml'
@@ -49,7 +52,6 @@ class InstanceType(Enum):
49
52
  class ExtensionCategory(Enum):
50
53
  ATTACH = 'ATTACH'
51
54
  DOC = 'DOC'
52
- ENGLISH_DOC = 'ENGLISH_DOC'
53
55
 
54
56
  def getValidExtensions(self, isAmendment: bool, isSubdirectory: bool) -> frozenset[str] | None:
55
57
  amendmentMap = VALID_EXTENSIONS[isAmendment]
@@ -60,12 +62,11 @@ class ExtensionCategory(Enum):
60
62
 
61
63
 
62
64
  FORM_TYPE_EXTENSION_CATEGORIES = {
63
- InstanceType.ATTACH_DOC: ExtensionCategory.ATTACH,
64
- InstanceType.AUDIT_DOC: ExtensionCategory.DOC,
65
- InstanceType.ENGLISH_DOC: ExtensionCategory.ENGLISH_DOC,
66
- InstanceType.PRIVATE_ATTACH: ExtensionCategory.ATTACH,
67
- InstanceType.PRIVATE_DOC: ExtensionCategory.DOC,
68
- InstanceType.PUBLIC_DOC: ExtensionCategory.DOC,
65
+ ReportFolderType.ATTACH_DOC: ExtensionCategory.ATTACH,
66
+ ReportFolderType.AUDIT_DOC: ExtensionCategory.DOC,
67
+ ReportFolderType.PRIVATE_ATTACH: ExtensionCategory.ATTACH,
68
+ ReportFolderType.PRIVATE_DOC: ExtensionCategory.DOC,
69
+ ReportFolderType.PUBLIC_DOC: ExtensionCategory.DOC,
69
70
  }
70
71
 
71
72
 
@@ -74,7 +75,6 @@ IMAGE_EXTENSIONS = frozenset({'.jpeg', '.jpg', '.gif', '.png'})
74
75
  ASSET_EXTENSIONS = frozenset(HTML_EXTENSIONS | IMAGE_EXTENSIONS)
75
76
  XBRL_EXTENSIONS = frozenset(HTML_EXTENSIONS | {'.xml', '.xsd'})
76
77
  ATTACH_EXTENSIONS = frozenset(HTML_EXTENSIONS | {'.pdf', })
77
- ENGLISH_DOC_EXTENSIONS = frozenset(ASSET_EXTENSIONS | frozenset({'.pdf', '.xml', '.txt'}))
78
78
 
79
79
  # Is Amendment -> Category -> Is Subdirectory
80
80
  VALID_EXTENSIONS = {
@@ -97,9 +97,5 @@ VALID_EXTENSIONS = {
97
97
  False: HTML_EXTENSIONS,
98
98
  True: ASSET_EXTENSIONS,
99
99
  },
100
- ExtensionCategory.ENGLISH_DOC: {
101
- False: ENGLISH_DOC_EXTENSIONS,
102
- True: ENGLISH_DOC_EXTENSIONS,
103
- },
104
100
  },
105
101
  }
@@ -6,12 +6,27 @@ from __future__ import annotations
6
6
  from dataclasses import dataclass
7
7
  from pathlib import Path
8
8
 
9
- from .InstanceType import InstanceType
9
+ from .ReportFolderType import ReportFolderType
10
10
 
11
11
 
12
12
  @dataclass(frozen=True)
13
13
  class UploadContents:
14
- amendmentPaths: dict[InstanceType, frozenset[Path]]
15
- directories: frozenset[Path]
16
- instances: dict[InstanceType, frozenset[Path]]
17
- unknownPaths: frozenset[Path]
14
+ reports: dict[ReportFolderType, frozenset[Path]]
15
+ uploadPaths: dict[Path, UploadPathInfo]
16
+
17
+ @property
18
+ def sortedPaths(self) -> list[Path]:
19
+ return sorted(self.uploadPaths.keys())
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class UploadPathInfo:
24
+ isAttachment: bool
25
+ isCorrection: bool
26
+ isCoverPage: bool
27
+ isDirectory: bool
28
+ isRoot: bool
29
+ isSubdirectory: bool
30
+ path: Path
31
+ reportFolderType: ReportFolderType | None
32
+ reportPath: Path | None
@@ -3,10 +3,8 @@ See COPYRIGHT.md for copyright information.
3
3
  """
4
4
  from __future__ import annotations
5
5
 
6
- import os
7
6
  from collections import defaultdict
8
7
  from datetime import timedelta
9
- from lxml.etree import XML, DTD
10
8
  from typing import Any, cast, Iterable
11
9
 
12
10
  import regex
@@ -30,7 +28,7 @@ from arelle.utils.validate.Validation import Validation
30
28
  from arelle.utils.validate.ValidationUtil import etreeIterWithDepth
31
29
  from ..DisclosureSystems import (DISCLOSURE_SYSTEM_EDINET)
32
30
  from ..PluginValidationDataExtension import PluginValidationDataExtension
33
- from ..Constants import xhtmlDtdExtension
31
+
34
32
 
35
33
  _: TypeGetText
36
34
 
@@ -368,28 +366,15 @@ def rule_gfm_1_2_14(
368
366
  (a format that conforms to XML grammar, such as all start and end tags being paired, and the end tag of a nested tag not coming after the end tag of its parent tag, etc.).
369
367
  Please modify it so that it is well-formed.
370
368
  """
371
- CDATApattern = regex.compile(r"<!\[CDATA\[(.+)\]\]")
372
- dtd = DTD(os.path.join(val.modelXbrl.modelManager.cntlr.configDir, xhtmlDtdExtension))
373
- htmlBodyTemplate = "<body><div>\n{0}\n</div></body>\n"
374
- namedEntityPattern = regex.compile("&[_A-Za-z\xC0-\xD6\xD8-\xF6\xF8-\xFF\u0100-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]"
375
- r"[_\-\.:"
376
- "\xB7A-Za-z0-9\xC0-\xD6\xD8-\xF6\xF8-\xFF\u0100-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\u0300-\u036F\u203F-\u2040]*;")
377
- XMLpattern = regex.compile(r".*(<|&lt;|&#x3C;|&#60;)[A-Za-z_]+[A-Za-z0-9_:]*[^>]*(/>|>|&gt;|/&gt;).*", regex.DOTALL)
378
-
379
- for fact in val.modelXbrl.facts:
380
- concept = fact.concept
381
- if not fact.isNil and concept is not None and concept.isTextBlock and XMLpattern.match(fact.value):
382
- for xmlText in [fact.value] + CDATApattern.findall(fact.value):
383
- xmlBodyWithoutEntities = htmlBodyTemplate.format(namedEntityPattern.sub("", xmlText).replace('&','&amp;'))
384
- textblockXml = XML(xmlBodyWithoutEntities)
385
- if not dtd.validate(textblockXml):
386
- yield Validation.warning(
387
- codes='EDINET.EC5700W.GFM.1.2.14',
388
- msg=_('The content of an element with a data type of nonnum:textBlockItemType is not well-formed XML (a format that conforms to XML grammar, '
389
- 'such as all start and end tags being in pairs, and the end tag of a nested tag not coming after the end tag of its parent tag). '
390
- 'Correct the content so that it is well-formed.'),
391
- modelObject = fact
392
- )
369
+ problematicFacts = pluginData.getProblematicTextBlocks(val.modelXbrl)
370
+ if len(problematicFacts) > 0:
371
+ yield Validation.warning(
372
+ codes='EDINET.EC5700W.GFM.1.2.14',
373
+ 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, '
374
+ '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). '
375
+ 'Correct the content so that it is well-formed.'),
376
+ modelObject = problematicFacts
377
+ )
393
378
 
394
379
 
395
380
  @validation(
@@ -628,3 +613,31 @@ def rule_gfm_1_2_30(
628
613
  msg=_("A context must not contain the xbrli:forever element."),
629
614
  modelObject=errors
630
615
  )
616
+
617
+
618
+ @validation(
619
+ hook=ValidationHook.XBRL_FINALLY,
620
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
621
+ )
622
+ def rule_gfm_1_3_1(
623
+ pluginData: PluginValidationDataExtension,
624
+ val: ValidateXbrl,
625
+ *args: Any,
626
+ **kwargs: Any,
627
+ ) -> Iterable[Validation]:
628
+ """
629
+ EDINET.EC5700W: [GFM 1.3.1] The submitter-specific taxonomy contains include elements.
630
+ """
631
+ warnings = []
632
+ for modelDocument in val.modelXbrl.urlDocs.values():
633
+ if pluginData.isStandardTaxonomyUrl(modelDocument.uri, val.modelXbrl):
634
+ continue
635
+ rootElt = modelDocument.xmlRootElement
636
+ for elt in rootElt.iterdescendants(XbrlConst.qnXsdInclude.clarkNotation):
637
+ warnings.append(elt)
638
+ if len(warnings) > 0:
639
+ yield Validation.warning(
640
+ codes='EDINET.EC5700W.GFM.1.3.1',
641
+ msg=_("The submitter-specific taxonomy contains include elements."),
642
+ modelObject=warnings
643
+ )
@@ -8,6 +8,8 @@ from collections import defaultdict
8
8
  from pathlib import Path
9
9
  from typing import Any, Iterable, TYPE_CHECKING
10
10
 
11
+ import regex
12
+
11
13
  from arelle.Cntlr import Cntlr
12
14
  from arelle.FileSource import FileSource
13
15
  from arelle.ValidateXbrl import ValidateXbrl
@@ -15,8 +17,9 @@ from arelle.typing import TypeGetText
15
17
  from arelle.utils.PluginHooks import ValidationHook
16
18
  from arelle.utils.validate.Decorator import validation
17
19
  from arelle.utils.validate.Validation import Validation
20
+ from .. import Constants
18
21
  from ..DisclosureSystems import (DISCLOSURE_SYSTEM_EDINET)
19
- from ..InstanceType import InstanceType, HTML_EXTENSIONS, IMAGE_EXTENSIONS
22
+ from ..ReportFolderType import ReportFolderType, HTML_EXTENSIONS, IMAGE_EXTENSIONS
20
23
  from ..PluginValidationDataExtension import PluginValidationDataExtension
21
24
 
22
25
  if TYPE_CHECKING:
@@ -24,6 +27,16 @@ if TYPE_CHECKING:
24
27
 
25
28
  _: TypeGetText
26
29
 
30
+ ALLOWED_ROOT_FOLDERS = {
31
+ "AttachDoc",
32
+ "AuditDoc",
33
+ "PrivateAttach",
34
+ "PrivateDoc",
35
+ "PublicAttach",
36
+ "PublicDoc",
37
+ "XBRL",
38
+ }
39
+
27
40
  FILE_COUNT_LIMITS = {
28
41
  Path("AttachDoc"): 990,
29
42
  Path("AuditDoc"): 990,
@@ -38,12 +51,54 @@ FILE_COUNT_LIMITS = {
38
51
 
39
52
  FILENAME_STEM_PATTERN = re.compile(r'[a-zA-Z0-9_-]*')
40
53
 
54
+ PATTERN_CODE = r'(?P<code>[A-Za-z\d]*)'
55
+ PATTERN_CONSOLIDATED = r'(?P<consolidated>c|n)'
56
+ PATTERN_COUNT = r'(?P<count>\d{2})'
57
+ PATTERN_DATE1 = r'(?P<year1>\d{4})-(?P<month1>\d{2})-(?P<day1>\d{2})'
58
+ PATTERN_DATE2 = r'(?P<year2>\d{4})-(?P<month2>\d{2})-(?P<day2>\d{2})'
59
+ PATTERN_FORM = r'(?P<form>\d{6})'
60
+ PATTERN_LINKBASE = r'(?P<linkbase>lab|lab-en|gla|pre|def|cal)'
61
+ PATTERN_MAIN = r'(?P<main>\d{7})'
62
+ PATTERN_NAME = r'(?P<name>[a-z]{6})'
63
+ PATTERN_ORDINANCE = r'(?P<ordinance>[a-z]*)'
64
+ PATTERN_PERIOD = r'(?P<period>c|p)' # TODO: Have only seen "c" in sample/public filings, assuming "p" for previous.
65
+ PATTERN_REPORT = r'(?P<report>[a-z]*)'
66
+ PATTERN_REPORT_SERIAL = r'(?P<report_serial>\d{3})'
67
+ PATTERN_SERIAL = r'(?P<serial>\d{3})'
68
+
69
+ PATTERN_AUDIT_REPORT_PREFIX = rf'jpaud-{PATTERN_REPORT}-{PATTERN_PERIOD}{PATTERN_CONSOLIDATED}'
70
+ PATTERN_REPORT_PREFIX = rf'jp{PATTERN_ORDINANCE}{PATTERN_FORM}-{PATTERN_REPORT}'
71
+ PATTERN_SUFFIX = rf'{PATTERN_REPORT_SERIAL}_{PATTERN_CODE}-{PATTERN_SERIAL}_{PATTERN_DATE1}_{PATTERN_COUNT}_{PATTERN_DATE2}'
72
+
73
+ PATTERNS = list(regex.compile(p) for p in (
74
+ # Schema file for report
75
+ # Example: jpcrp050300-esr-001_X99007-000_2025-04-10_01_2025-04-10.xsd
76
+ rf'{PATTERN_REPORT_PREFIX}-{PATTERN_SUFFIX}.xsd',
77
+ # Schema file for audit report
78
+ # Example: jpaud-aar-cn-001_X99001-000_2025-03-31_01_2025-06-28.xsd
79
+ rf'{PATTERN_AUDIT_REPORT_PREFIX}-{PATTERN_SUFFIX}.xsd',
80
+ # Linkbase file for report
81
+ # Example: jpcrp020000-srs-001_X99001-000_2025-03-31_01_2025-11-20_cal.xml
82
+ rf'{PATTERN_REPORT_PREFIX}-{PATTERN_SUFFIX}_{PATTERN_LINKBASE}.xml',
83
+ # Linkbase file for audit report
84
+ # Example: jpaud-qrr-cc-001_X99001-000_2025-03-31_01_2025-11-20_pre.xml
85
+ rf'{PATTERN_AUDIT_REPORT_PREFIX}-{PATTERN_SUFFIX}_{PATTERN_LINKBASE}.xml',
86
+ # Cover page file for report
87
+ # Example: 0000000_header_jpcrp020000-srs-001_X99001-000_2025-03-31_01_2025-11-20_ixbrl.htm
88
+ rf'{Constants.COVER_PAGE_FILENAME_PREFIX}{PATTERN_REPORT_PREFIX}-{PATTERN_SUFFIX}_ixbrl.htm',
89
+ # Main file for report
90
+ # Example: 0205020_honbun_jpcrp020000-srs-001_X99001-000_2025-03-31_01_2025-11-20_ixbrl.htm
91
+ rf'{PATTERN_MAIN}_{PATTERN_NAME}_{PATTERN_REPORT_PREFIX}-{PATTERN_SUFFIX}_ixbrl.htm',
92
+ # Main file for audit report
93
+ # Example: jpaud-qrr-cc-001_X99001-000_2025-03-31_01_2025-11-20_pre.xml
94
+ rf'{PATTERN_AUDIT_REPORT_PREFIX}-{PATTERN_SUFFIX}_ixbrl.htm',
95
+ ))
41
96
 
42
97
  @validation(
43
98
  hook=ValidationHook.FILESOURCE,
44
99
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
45
100
  )
46
- def rule_EC0121E(
101
+ def rule_EC0100E(
47
102
  pluginData: ControllerPluginData,
48
103
  cntlr: Cntlr,
49
104
  fileSource: FileSource,
@@ -51,30 +106,32 @@ def rule_EC0121E(
51
106
  **kwargs: Any,
52
107
  ) -> Iterable[Validation]:
53
108
  """
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.
109
+ EDINET.EC0100E: An illegal directory is found directly under the transferred directory.
110
+ Only the following root folders are allowed:
111
+ AttachDoc
112
+ AuditDoc*
113
+ PrivateAttach
114
+ PrivateDoc*
115
+ PublicAttach
116
+ PublicDoc*
117
+ XBRL
118
+ * Only when reporting corrections
119
+
120
+ NOTE: since we do not have access to the submission type, we can't determine if the submission is a correction or not.
121
+ For this implementation, we will allow all directories that may be valid for at least one submission type.
122
+ This allows for a false-negative outcome when a non-correction submission has a correction-only root directory.
61
123
  """
62
124
  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):
125
+ for path, pathInfo in uploadContents.uploadPaths.items():
126
+ if pathInfo.isRoot and path.name not in ALLOWED_ROOT_FOLDERS:
68
127
  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)
128
+ codes='EDINET.EC0100E',
129
+ msg=_("An illegal directory is found directly under the transferred directory. "
130
+ "Directory name or file name: '%(rootDirectory)s'. "
131
+ "Delete all folders except the following folders that exist directly "
132
+ "under the root folder, and then upload again: %(allowedDirectories)s."),
133
+ rootDirectory=path.name,
134
+ allowedDirectories=', '.join(f"'{d}'" for d in ALLOWED_ROOT_FOLDERS)
78
135
  )
79
136
 
80
137
 
@@ -82,7 +139,7 @@ def rule_EC0121E(
82
139
  hook=ValidationHook.FILESOURCE,
83
140
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
84
141
  )
85
- def rule_EC0124E(
142
+ def rule_EC0124E_EC0187E(
86
143
  pluginData: ControllerPluginData,
87
144
  cntlr: Cntlr,
88
145
  fileSource: FileSource,
@@ -90,7 +147,8 @@ def rule_EC0124E(
90
147
  **kwargs: Any,
91
148
  ) -> Iterable[Validation]:
92
149
  """
93
- EDINET.EC0124E: There are no empty directories.
150
+ EDINET.EC0124E: There are no empty root directories.
151
+ EDINET.EC0187E: There are no empty subdirectories.
94
152
  """
95
153
  uploadFilepaths = pluginData.getUploadFilepaths(fileSource)
96
154
  emptyDirectories = []
@@ -98,15 +156,24 @@ def rule_EC0124E(
98
156
  if path.suffix:
99
157
  continue
100
158
  if not any(path in p.parents for p in uploadFilepaths):
101
- emptyDirectories.append(str(path))
159
+ emptyDirectories.append(path)
102
160
  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
- )
161
+ if len(emptyDirectory.parts) <= 1:
162
+ yield Validation.error(
163
+ codes='EDINET.EC0124E',
164
+ msg=_("There is no file directly under '%(emptyDirectory)s'. "
165
+ "No empty root folders. "
166
+ "Please store the file in the appropriate folder or delete the folder and upload again."),
167
+ emptyDirectory=str(emptyDirectory),
168
+ )
169
+ else:
170
+ yield Validation.error(
171
+ codes='EDINET.EC0187E',
172
+ msg=_("'%(parentDirectory)s' contains a subordinate directory ('%(emptyDirectory)s') with no files. "
173
+ "Please store the file in the corresponding subfolder or delete the subfolder and upload again."),
174
+ parentDirectory=str(emptyDirectory.parent),
175
+ emptyDirectory=str(emptyDirectory),
176
+ )
110
177
 
111
178
 
112
179
  @validation(
@@ -160,22 +227,13 @@ def rule_EC0130E(
160
227
  EDINET.EC0130E: File extensions must match the file extensions allowed in Figure 2-1-3 and Figure 2-1-5.
161
228
  """
162
229
  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:
230
+ for path, pathInfo in uploadContents.uploadPaths.items():
231
+ if pathInfo.reportFolderType is None or pathInfo.isDirectory:
175
232
  continue
176
- validExtensions = instanceType.getValidExtensions(isAmendment, isSubdirectory)
233
+ validExtensions = pathInfo.reportFolderType.getValidExtensions(pathInfo.isCorrection, pathInfo.isSubdirectory)
177
234
  if validExtensions is None:
178
235
  continue
236
+ ext = path.suffix
179
237
  if ext not in validExtensions:
180
238
  yield Validation.error(
181
239
  codes='EDINET.EC0130E',
@@ -207,18 +265,17 @@ def rule_EC0132E(
207
265
  EDINET.EC0132E: Store the manifest file directly under the relevant folder.
208
266
  """
209
267
  uploadContents = pluginData.getUploadContents(fileSource)
210
- for instanceType in (InstanceType.AUDIT_DOC, InstanceType.PRIVATE_DOC, InstanceType.PUBLIC_DOC):
211
- if instanceType not in uploadContents.instances:
212
- continue
213
- if instanceType.manifestPath in uploadContents.instances.get(instanceType, []):
268
+ for reportFolderType, paths in uploadContents.reports.items():
269
+ if reportFolderType.isAttachment:
214
270
  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
- )
271
+ if reportFolderType.manifestPath not in paths:
272
+ yield Validation.error(
273
+ codes='EDINET.EC0132E',
274
+ msg=_("'%(expectedManifestName)s' does not exist in '%(expectedManifestDirectory)s'. "
275
+ "Please store the manifest file (or cover file) directly under the relevant folder and upload it again. "),
276
+ expectedManifestName=reportFolderType.manifestPath.name,
277
+ expectedManifestDirectory=str(reportFolderType.manifestPath.parent),
278
+ )
222
279
 
223
280
 
224
281
  @validation(
@@ -279,6 +336,36 @@ def rule_EC0188E(
279
336
  )
280
337
 
281
338
 
339
+ @validation(
340
+ hook=ValidationHook.FILESOURCE,
341
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
342
+ )
343
+ def rule_EC0192E(
344
+ pluginData: ControllerPluginData,
345
+ cntlr: Cntlr,
346
+ fileSource: FileSource,
347
+ *args: Any,
348
+ **kwargs: Any,
349
+ ) -> Iterable[Validation]:
350
+ """
351
+ EDINET.EC0192E: The cover file for PrivateDoc cannot be set because it uses a
352
+ PublicDoc cover file. Please delete the cover file from PrivateDoc and upload
353
+ it again.
354
+ """
355
+ uploadContents = pluginData.getUploadContents(fileSource)
356
+ for path, pathInfo in uploadContents.uploadPaths.items():
357
+ if not pathInfo.isCoverPage:
358
+ continue
359
+ # Only applies to PrivateDoc correction reports
360
+ if pathInfo.isCorrection and pathInfo.reportFolderType == ReportFolderType.PRIVATE_DOC:
361
+ yield Validation.error(
362
+ codes='EDINET.EC0192E',
363
+ msg=_("The cover file for PrivateDoc ('%(file)s') cannot be set because it uses a PublicDoc cover file. "
364
+ "Please delete the cover file from PrivateDoc and upload it again."),
365
+ file=str(path),
366
+ )
367
+
368
+
282
369
  @validation(
283
370
  hook=ValidationHook.FILESOURCE,
284
371
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
@@ -315,6 +402,82 @@ def rule_EC0198E(
315
402
  )
316
403
 
317
404
 
405
+ @validation(
406
+ hook=ValidationHook.FILESOURCE,
407
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
408
+ )
409
+ def rule_EC0233E(
410
+ pluginData: ControllerPluginData,
411
+ cntlr: Cntlr,
412
+ fileSource: FileSource,
413
+ *args: Any,
414
+ **kwargs: Any,
415
+ ) -> Iterable[Validation]:
416
+ """
417
+ EDINET.EC0233E: There is a file in the report directory that comes before the cover file
418
+ in file name sort order.
419
+
420
+ NOTE: This includes files in subdirectories. For example, PublicDoc/00000000_images/image.png
421
+ comes before PublicDoc/0000000_header_*.htm
422
+ """
423
+ uploadContents = pluginData.getUploadContents(fileSource)
424
+ directories = defaultdict(list)
425
+ for path in uploadContents.sortedPaths:
426
+ pathInfo = uploadContents.uploadPaths[path]
427
+ if pathInfo.isDirectory:
428
+ continue
429
+ if pathInfo.reportFolderType in (ReportFolderType.PRIVATE_DOC, ReportFolderType.PUBLIC_DOC):
430
+ directories[pathInfo.reportPath].append(pathInfo)
431
+ for reportPath, pathInfos in directories.items():
432
+ coverPagePath = next(iter(p for p in pathInfos if p.isCoverPage), None)
433
+ if coverPagePath is None:
434
+ continue
435
+ errorPathInfos = pathInfos[:pathInfos.index(coverPagePath)]
436
+ for pathInfo in errorPathInfos:
437
+ yield Validation.error(
438
+ codes='EDINET.EC0233E',
439
+ msg=_("There is a file in the report directory in '%(reportPath)s' that comes before the cover "
440
+ "file ('%(coverPage)s') in file name sort order. "
441
+ "Directory name or file name: '%(path)s'. "
442
+ "Please make sure that there are no files that come before the cover file in the file "
443
+ "name sort order, and then upload again."),
444
+ reportPath=str(reportPath),
445
+ coverPage=str(coverPagePath.path.name),
446
+ path=str(pathInfo.path),
447
+ )
448
+
449
+
450
+ @validation(
451
+ hook=ValidationHook.FILESOURCE,
452
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
453
+ )
454
+ def rule_EC0234E(
455
+ pluginData: ControllerPluginData,
456
+ cntlr: Cntlr,
457
+ fileSource: FileSource,
458
+ *args: Any,
459
+ **kwargs: Any,
460
+ ) -> Iterable[Validation]:
461
+ """
462
+ EDINET.EC0234E: A cover file exists in an unsupported subdirectory.
463
+ """
464
+ uploadContents = pluginData.getUploadContents(fileSource)
465
+ for path, pathInfo in uploadContents.uploadPaths.items():
466
+ if pathInfo.isDirectory:
467
+ continue
468
+ if pathInfo.reportFolderType not in (ReportFolderType.PRIVATE_DOC, ReportFolderType.PUBLIC_DOC):
469
+ continue
470
+ if pathInfo.isSubdirectory and pathInfo.isCoverPage:
471
+ yield Validation.error(
472
+ codes='EDINET.EC0234E',
473
+ msg=_("A cover file ('%(coverPage)s') exists in an unsupported subdirectory. "
474
+ "Directory: '%(directory)s'. "
475
+ "Please make sure there is no cover file in the subfolder and upload again."),
476
+ coverPage=str(path.name),
477
+ directory=str(path.parent),
478
+ )
479
+
480
+
318
481
  @validation(
319
482
  hook=ValidationHook.FILESOURCE,
320
483
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
@@ -370,6 +533,75 @@ def rule_EC0206E(
370
533
  )
371
534
 
372
535
 
536
+ @validation(
537
+ hook=ValidationHook.FILESOURCE,
538
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
539
+ )
540
+ def rule_EC0349E(
541
+ pluginData: ControllerPluginData,
542
+ cntlr: Cntlr,
543
+ fileSource: FileSource,
544
+ *args: Any,
545
+ **kwargs: Any,
546
+ ) -> Iterable[Validation]:
547
+ """
548
+ EDINET.EC0349E: An unexpected directory or file exists in the XBRL directory.
549
+ Only PublicDoc, PrivateDoc, or AuditDoc directories may exist beneath the XBRL directory.
550
+ """
551
+ uploadContent = pluginData.getUploadContents(fileSource)
552
+ xbrlDirectoryPath = Path('XBRL')
553
+ allowedPaths = {p.xbrlDirectory for p in (
554
+ ReportFolderType.AUDIT_DOC,
555
+ ReportFolderType.PRIVATE_DOC,
556
+ ReportFolderType.PUBLIC_DOC,
557
+ )}
558
+ for path, pathInfo in uploadContent.uploadPaths.items():
559
+ if path.parent != xbrlDirectoryPath:
560
+ continue
561
+ if path not in allowedPaths:
562
+ if not any(pattern.fullmatch(path.name) for pattern in PATTERNS):
563
+ yield Validation.error(
564
+ codes='EDINET.EC0349E',
565
+ msg=_("An unexpected directory or file exists in the XBRL directory. "
566
+ "Directory or file name: '%(file)s'."),
567
+ file=path.name,
568
+ )
569
+
570
+
571
+ @validation(
572
+ hook=ValidationHook.FILESOURCE,
573
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
574
+ )
575
+ def rule_EC0352E(
576
+ pluginData: ControllerPluginData,
577
+ cntlr: Cntlr,
578
+ fileSource: FileSource,
579
+ *args: Any,
580
+ **kwargs: Any,
581
+ ) -> Iterable[Validation]:
582
+ """
583
+ EDINET.EC0352E: An XBRL file with an invalid name exists.
584
+ """
585
+ uploadContent = pluginData.getUploadContents(fileSource)
586
+ for path, pathInfo in uploadContent.uploadPaths.items():
587
+ if (
588
+ pathInfo.isDirectory or
589
+ pathInfo.isCorrection or
590
+ pathInfo.isSubdirectory or
591
+ pathInfo.isAttachment or
592
+ pathInfo.reportFolderType is None or
593
+ any(path == t.manifestPath for t in ReportFolderType)
594
+ ):
595
+ continue
596
+ if not any(pattern.fullmatch(path.name) for pattern in PATTERNS):
597
+ yield Validation.error(
598
+ codes='EDINET.EC0352E',
599
+ msg=_("A file with an invalid name exists. "
600
+ "File path: '%(path)s'."),
601
+ path=str(path),
602
+ )
603
+
604
+
373
605
  @validation(
374
606
  hook=ValidationHook.FILESOURCE,
375
607
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
@@ -444,6 +676,61 @@ def rule_EC1020E(
444
676
  )
445
677
 
446
678
 
679
+ @validation(
680
+ hook=ValidationHook.FILESOURCE,
681
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
682
+ )
683
+ def rule_filenames(
684
+ pluginData: ControllerPluginData,
685
+ cntlr: Cntlr,
686
+ fileSource: FileSource,
687
+ *args: Any,
688
+ **kwargs: Any,
689
+ ) -> Iterable[Validation]:
690
+ """
691
+ EDINET.EC0121E: There is a directory or file that contains
692
+ more than 31 characters or uses characters other than those allowed (alphanumeric characters,
693
+ '-' and '_').
694
+ Note: Applies to everything EXCEPT files directly beneath non-correction report folders.
695
+
696
+ EDINET.EC0200E: There is a file that uses characters other
697
+ than those allowed (alphanumeric characters, '-' and '_').
698
+ Note: Applies ONLY to files directly beneath non-correction report folders.
699
+ """
700
+ for path, pathInfo in pluginData.getUploadContents(fileSource).uploadPaths.items():
701
+ isReportFile = (
702
+ not pathInfo.isAttachment and
703
+ not pathInfo.isCorrection and
704
+ not pathInfo.isDirectory and
705
+ not pathInfo.isSubdirectory
706
+ )
707
+ charactersAreValid = FILENAME_STEM_PATTERN.fullmatch(path.stem)
708
+ lengthIsValid = isReportFile or (len(path.name) <= 31)
709
+ if charactersAreValid and lengthIsValid:
710
+ continue
711
+ if isReportFile:
712
+ yield Validation.error(
713
+ codes='EDINET.EC0200E',
714
+ msg=_("There is a file inside the XBRL directory that uses characters "
715
+ "other than those allowed (alphanumeric characters, '-' and '_'). "
716
+ "File: '%(path)s'. "
717
+ "Please change the filename to usable characters, and upload again."),
718
+ path=str(path)
719
+ )
720
+ else:
721
+ yield Validation.error(
722
+ codes='EDINET.EC0121E',
723
+ msg=_("There is a directory or file in '%(directory)s' that contains more "
724
+ "than 31 characters or uses characters other than those allowed "
725
+ "(alphanumeric characters, '-' and '_'). "
726
+ "Directory or filename: '%(basename)s'. "
727
+ "Please change the file name (or folder name) to within 31 characters and to usable "
728
+ "characters, and upload again."),
729
+ directory=str(path.parent),
730
+ basename=path.name,
731
+ )
732
+
733
+
447
734
  @validation(
448
735
  hook=ValidationHook.FILESOURCE,
449
736
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
@@ -8,10 +8,10 @@ References:
8
8
  - [HMRC CT Inline XBRL Style Guide](https://www.gov.uk/government/uploads/system/uploads/attachment_data/file/434588/xbrl-style-guide.pdf)
9
9
  """
10
10
  import os
11
- from math import isnan
12
11
  from arelle import ModelDocument, XmlUtil
13
12
  from arelle.ModelValue import qname, dateTime, DATE
14
- from arelle.ValidateXbrlCalcs import inferredDecimals, rangeValue, insignificantDigits
13
+ from arelle.ValidateDuplicateFacts import getDuplicateFactSets
14
+ from arelle.ValidateXbrlCalcs import insignificantDigits
15
15
  from arelle.Version import authorLabel, copyrightLabel
16
16
  from arelle.XbrlConst import xbrli, qnXbrliXbrl
17
17
  import regex as re
@@ -374,62 +374,12 @@ def validateXbrlFinally(val, *args, **kwargs):
374
374
  _("Generic dimension members have no associated name or description item, member names (name or description item): %(memberNames)s"),
375
375
  modelObject=modelXbrl, memberNames=", ".join(sorted(memLocalNamesMissing)))
376
376
 
377
-
378
-
379
- aspectEqualFacts = defaultdict(dict) # dict [(qname,lang)] of dict(cntx,unit) of [fact, fact]
380
- for hashEquivalentFacts in factForConceptContextUnitHash.values():
381
- if len(hashEquivalentFacts) > 1:
382
- for f in hashEquivalentFacts: # check for hash collision by value checks on context and unit
383
- if getattr(f,"xValid", 0) >= 4:
384
- cuDict = aspectEqualFacts[(f.qname,
385
- (f.xmlLang or "").lower() if f.concept.type.isWgnStringFactType else None)]
386
- _matched = False
387
- for (_cntx,_unit),fList in cuDict.items():
388
- if (((_cntx is None and f.context is None) or (f.context is not None and f.context.isEqualTo(_cntx))) and
389
- ((_unit is None and f.unit is None) or (f.unit is not None and f.unit.isEqualTo(_unit)))):
390
- _matched = True
391
- fList.append(f)
392
- break
393
- if not _matched:
394
- cuDict[(f.context,f.unit)] = [f]
395
- decVals = {}
396
- for cuDict in aspectEqualFacts.values(): # dups by qname, lang
397
- for fList in cuDict.values(): # dups by equal-context equal-unit
398
- if len(fList) > 1:
399
- f0 = fList[0]
400
- if f0.concept.isNumeric:
401
- if any(f.isNil for f in fList):
402
- _inConsistent = not all(f.isNil for f in fList)
403
- else: # not all have same decimals
404
- _d = inferredDecimals(f0)
405
- _v = f0.xValue
406
- _inConsistent = isnan(_v) # NaN is incomparable, always makes dups inconsistent
407
- decVals[_d] = _v
408
- aMax, bMin, _inclA, _inclB = rangeValue(_v, _d)
409
- for f in fList[1:]:
410
- _d = inferredDecimals(f)
411
- _v = f.xValue
412
- if isnan(_v):
413
- _inConsistent = True
414
- break
415
- if _d in decVals:
416
- _inConsistent |= _v != decVals[_d]
417
- else:
418
- decVals[_d] = _v
419
- a, b, _inclA, _inclB = rangeValue(_v, _d)
420
- if a > aMax: aMax = a
421
- if b < bMin: bMin = b
422
- if not _inConsistent:
423
- _inConsistent = (bMin < aMax)
424
- decVals.clear()
425
- else:
426
- _inConsistent = any(not f.isVEqualTo(f0) for f in fList[1:])
427
- if _inConsistent:
428
- modelXbrl.error("JFCVC.3314",
429
- "Inconsistent duplicate fact values %(fact)s: %(values)s.",
430
- modelObject=fList, fact=f0.qname, contextID=f0.contextID, values=", ".join(f.value for f in fList))
431
- aspectEqualFacts.clear()
432
- del factForConceptContextUnitHash, aspectEqualFacts
377
+ for duplicateFactSet in getDuplicateFactSets(modelXbrl.facts, includeSingles=False):
378
+ if duplicateFactSet.areAnyInconsistent:
379
+ f0 = duplicateFactSet.facts[0]
380
+ modelXbrl.error("JFCVC.3314",
381
+ "Inconsistent duplicate fact values %(fact)s: %(values)s.",
382
+ modelObject=duplicateFactSet, fact=f0.qname, values=", ".join(f'"{f.value}"' for f in duplicateFactSet))
433
383
 
434
384
  if modelXbrl.modelDocument.type == ModelDocument.Type.INLINEXBRL:
435
385
  rootElt = modelXbrl.modelDocument.xmlRootElement
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arelle-release
3
- Version: 2.37.50
3
+ Version: 2.37.52
4
4
  Summary: An open source XBRL platform.
5
5
  Author-email: "arelle.org" <support@arelle.org>
6
6
  License-Expression: Apache-2.0
@@ -116,7 +116,7 @@ arelle/ViewWinVersReport.py,sha256=aYfsOgynVZpMzl6f2EzQCBLzdihYGycwb5SiTghkgMQ,9
116
116
  arelle/ViewWinXml.py,sha256=4ZGKtjaoCwU9etKYm9ZAS7jSmUxba1rqNEdv0OIyjTY,1250
117
117
  arelle/WatchRss.py,sha256=5Ih4igH2MM4hpOuAXy9eO0QAyZ7jZR3S5bPzo2sdFpw,14097
118
118
  arelle/WebCache.py,sha256=SLk-S5StYUIucm5bd2BqT-o8ZA0NdYw2Xl4O9vIt7O8,45257
119
- arelle/XbrlConst.py,sha256=_K3__pmzcJKvSt72n4Vybo2qUaKeOGP-HbiiqCoBpqQ,58491
119
+ arelle/XbrlConst.py,sha256=p5GV8x7ZLNVT-lMo9EEtw8CkVICITxJ4GZXXQFmZQEk,58561
120
120
  arelle/XbrlUtil.py,sha256=s2Vmrh-sZI5TeuqsziKignOc3ao-uUgnCNoelP4dDj0,9212
121
121
  arelle/XhtmlValidate.py,sha256=0gtm7N-kXK0RB5o3c1AQXjfFuRp1w2fKZZAeyruNANw,5727
122
122
  arelle/XmlUtil.py,sha256=1VToOOylF8kbEorEdZLThmq35j9bmuF_DS2q9NthnHU,58774
@@ -125,7 +125,7 @@ arelle/XmlValidateConst.py,sha256=U_wN0Q-nWKwf6dKJtcu_83FXPn9c6P8JjzGA5b0w7P0,33
125
125
  arelle/XmlValidateParticles.py,sha256=Mn6vhFl0ZKC_vag1mBwn1rH_x2jmlusJYqOOuxFPO2k,9231
126
126
  arelle/XmlValidateSchema.py,sha256=6frtZOc1Yrx_5yYF6V6oHbScnglWrVbWr6xW4EHtLQI,7428
127
127
  arelle/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
128
- arelle/_version.py,sha256=njDexQGIu5W1_Bx2XXI0APU6EFVbL-MHc7OHXGD6xDg,515
128
+ arelle/_version.py,sha256=gK8aRhTgf0JSYF4-ziM2ZwD_sWJNac2Cf_ZEEYaXGq4,708
129
129
  arelle/typing.py,sha256=PRe-Fxwr2SBqYYUVPCJ3E7ddDX0_oOISNdT5Q97EbRM,1246
130
130
  arelle/api/Session.py,sha256=kgSxS7VckA1sQ7xp0pJiK7IK-vRxAdAZKUo8gEx27s8,7549
131
131
  arelle/config/creationSoftwareNames.json,sha256=5MK7XUjfDJ9OpRCCHXeOErJ1SlTBZji4WEcEOdOacx0,3128
@@ -312,14 +312,14 @@ arelle/plugin/validate/DBA/rules/tm.py,sha256=ui9oKBqlAForwkQ9kk9KBiUogTJE5pv1Rb
312
312
  arelle/plugin/validate/DBA/rules/tr.py,sha256=4TootFjl0HXsKZk1XNBCyj-vnjRs4lg35hfiz_b_4wU,14684
313
313
  arelle/plugin/validate/EBA/__init__.py,sha256=x3zXNcdSDJ3kHfL7kMs0Ve0Vs9oWbzNFVf1TK4Avmy8,45924
314
314
  arelle/plugin/validate/EBA/config.xml,sha256=37wMVUAObK-XEqakqD8zPNog20emYt4a_yfL1AKubF8,2022
315
- arelle/plugin/validate/EDINET/Constants.py,sha256=bYLMdzEFw6X1jchW5iN5pUAQ8K7-cGtiZRN22r1LP6g,1339
316
- arelle/plugin/validate/EDINET/ControllerPluginData.py,sha256=PrEhllue_lh-sLeUy9w8I9XLMLgpo5eW-c7ZKgNyGLE,6256
315
+ arelle/plugin/validate/EDINET/Constants.py,sha256=H_OX8hq7nns6TbKPpmDlg1_pNOd7P-XMFF03MMZBAuk,1387
316
+ arelle/plugin/validate/EDINET/ControllerPluginData.py,sha256=T4m8GFiVg9tNoLcC3HxeegNnKVqAqZdc6kNdxM5Vz1Y,7005
317
317
  arelle/plugin/validate/EDINET/DisclosureSystems.py,sha256=3rKG42Eg-17Xx_KXU_V5yHW6I3LTwQunvf4a44C9k_4,36
318
- arelle/plugin/validate/EDINET/InstanceType.py,sha256=aLKb4-AJ6nDZKMOLCp7u08E9VD64ExeZy9_oGth-LTk,3207
319
318
  arelle/plugin/validate/EDINET/ManifestInstance.py,sha256=SkQV-aOsYn3CTgCkH4IXNdM3QKoiz8okwb29ftMtV3Q,6882
320
- arelle/plugin/validate/EDINET/PluginValidationDataExtension.py,sha256=NBCXANYep0AvQVe2zm2eaKzklvLCC1zeQ_oa8L6cryM,11498
319
+ arelle/plugin/validate/EDINET/PluginValidationDataExtension.py,sha256=EkUJvkjk96ehgfnnoUxBAv9x9aauntvF9oXFOyr81Yo,12447
320
+ arelle/plugin/validate/EDINET/ReportFolderType.py,sha256=Q-9a-5tJfhK-cjY8WUB2AT1NI-Nn9cFtARVOIJoLRGU,2979
321
321
  arelle/plugin/validate/EDINET/Statement.py,sha256=0Mw5IB7LMtvUZ-2xKZfxmq67xF_dCgJo3eNLweLFRHU,9350
322
- arelle/plugin/validate/EDINET/UploadContents.py,sha256=L0u5171cLBKX7NT-_szRqOfkiy4Gv1xPsfpPVgPhtu0,409
322
+ arelle/plugin/validate/EDINET/UploadContents.py,sha256=IKQYl6lXYTfAZKLIDzRRGSRt3FXoL2Eldbx3Dh7T2I4,712
323
323
  arelle/plugin/validate/EDINET/ValidationPluginExtension.py,sha256=oMY0ntLr1qIh3uMi1W_M-bT5bhXPDx048X3oDFP5zOY,2042
324
324
  arelle/plugin/validate/EDINET/__init__.py,sha256=OZ7gMknCHd0M-9nt8UOmjEZW50YQzbvSLongr9O7Yi0,3022
325
325
  arelle/plugin/validate/EDINET/resources/config.xml,sha256=7uT4GcRgk5veMLpFhPPQJxbGKiQvM52P8EMrjn0qd0g,646
@@ -328,9 +328,9 @@ arelle/plugin/validate/EDINET/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQ
328
328
  arelle/plugin/validate/EDINET/rules/contexts.py,sha256=KPoyWfRaURvxoGVcWP64mTMTAKPMSmQSX06RClCLddw,7590
329
329
  arelle/plugin/validate/EDINET/rules/edinet.py,sha256=VYrDZaKbsQuQEvOY5F0Pv4Jzk9YZ4iETOkAFOggrhEY,12632
330
330
  arelle/plugin/validate/EDINET/rules/frta.py,sha256=N0YglHYZuLD2IuwE26viR2ViwUYjneBuMFU9vlrS0aQ,7616
331
- arelle/plugin/validate/EDINET/rules/gfm.py,sha256=Ty5zDqIRkXwnOQTzK5JD2F6jbK1xEiseltosVfYUrVM,24772
331
+ arelle/plugin/validate/EDINET/rules/gfm.py,sha256=Elyd0Vqooj_rC0yDWC8NneWCQ_Ckb9IZy1XCn7m1_IE,24389
332
332
  arelle/plugin/validate/EDINET/rules/manifests.py,sha256=MoT9R_a4BzuYdQVbF7RC5wz134Ve68svSdJ3NlpO_AU,4026
333
- arelle/plugin/validate/EDINET/rules/upload.py,sha256=k1o12K_vMN2N5bAXPxLRwyKjghoOGrgfLijE_j_5ilQ,19811
333
+ arelle/plugin/validate/EDINET/rules/upload.py,sha256=jfBt-WjUiJqh8QSkaLMxfsz9dP6fT3z63GT-ws514YY,31415
334
334
  arelle/plugin/validate/ESEF/Const.py,sha256=JujF_XV-_TNsxjGbF-8SQS4OOZIcJ8zhCMnr-C1O5Ho,22660
335
335
  arelle/plugin/validate/ESEF/Dimensions.py,sha256=MOJM7vwNPEmV5cu-ZzPrhx3347ZvxgD6643OB2HRnIk,10597
336
336
  arelle/plugin/validate/ESEF/Util.py,sha256=QH3btcGqBpr42M7WSKZLSdNXygZaZLfEiEjlxoG21jE,7950
@@ -367,7 +367,7 @@ arelle/plugin/validate/ROS/resources/config.xml,sha256=HXWume5HlrAqOx5AtiWWqgADb
367
367
  arelle/plugin/validate/ROS/rules/__init__.py,sha256=wW7BUAIb7sRkOxC1Amc_ZKrz03FM-Qh1TyZe6wxYaAU,1567
368
368
  arelle/plugin/validate/ROS/rules/ros.py,sha256=Dk5BkfKQYItImdx5FcFvkMWT5BlJ1r_L7Vn-EsCG85A,19870
369
369
  arelle/plugin/validate/UK/ValidateUK.py,sha256=h7-tnCubHme8Meaif-o55TV2rCfMWuikfpZCcK6NNDs,56447
370
- arelle/plugin/validate/UK/__init__.py,sha256=KE6s_B-EvrHDCtWQz2N_wQwyx_ZbWhYNV2GfQnluxMw,30655
370
+ arelle/plugin/validate/UK/__init__.py,sha256=0X4_J9Ug0O0xiEm-JYx7LEnGdpXUKZ7D-5eh9mjjqR8,27456
371
371
  arelle/plugin/validate/UK/config.xml,sha256=mUFhWDfBzGTn7v0ZSmf4HaweQTMJh_4ZcJmD9mzCHrA,1547
372
372
  arelle/plugin/validate/UK/consistencyChecksByName.json,sha256=BgB9YAWzmcsX-_rU74RBkABwEsS75vrMlwBHsYCz2R0,25247
373
373
  arelle/plugin/validate/UK/hmrc-taxonomies.xml,sha256=3lR-wb2sooAddQkVqqRzG_VqLuHq_MQ8kIaXAQs1KVk,9623
@@ -680,9 +680,9 @@ arelle/utils/validate/ValidationUtil.py,sha256=9vmSvShn-EdQy56dfesyV8JjSRVPj7txr
680
680
  arelle/utils/validate/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
681
681
  arelle/webserver/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
682
682
  arelle/webserver/bottle.py,sha256=P-JECd9MCTNcxCnKoDUvGcoi03ezYVOgoWgv2_uH-6M,362
683
- arelle_release-2.37.50.dist-info/licenses/LICENSE.md,sha256=Q0tn6q0VUbr-NM8916513NCIG8MNzo24Ev-sxMUBRZc,3959
684
- arelle_release-2.37.50.dist-info/METADATA,sha256=4w922-d1vIeCmfCpQ18ImtcI_0C7_qguifO2WlU-yYo,9327
685
- arelle_release-2.37.50.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
686
- arelle_release-2.37.50.dist-info/entry_points.txt,sha256=Uj5niwfwVsx3vaQ3fYj8hrZ1xpfCJyTUA09tYKWbzpo,111
687
- arelle_release-2.37.50.dist-info/top_level.txt,sha256=fwU7SYawL4_r-sUMRg7r1nYVGjFMSDvRWx8VGAXEw7w,7
688
- arelle_release-2.37.50.dist-info/RECORD,,
683
+ arelle_release-2.37.52.dist-info/licenses/LICENSE.md,sha256=Q0tn6q0VUbr-NM8916513NCIG8MNzo24Ev-sxMUBRZc,3959
684
+ arelle_release-2.37.52.dist-info/METADATA,sha256=k76rs-k1Xh7_AxEk4QnW0pEKI3JdJxEYvE-9mU9SZIY,9327
685
+ arelle_release-2.37.52.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
686
+ arelle_release-2.37.52.dist-info/entry_points.txt,sha256=Uj5niwfwVsx3vaQ3fYj8hrZ1xpfCJyTUA09tYKWbzpo,111
687
+ arelle_release-2.37.52.dist-info/top_level.txt,sha256=fwU7SYawL4_r-sUMRg7r1nYVGjFMSDvRWx8VGAXEw7w,7
688
+ arelle_release-2.37.52.dist-info/RECORD,,