arelle-release 2.37.63__py3-none-any.whl → 2.37.65__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 (30) hide show
  1. arelle/CntlrCmdLine.py +3 -0
  2. arelle/DisclosureSystem.py +2 -0
  3. arelle/UrlUtil.py +3 -0
  4. arelle/ValidateFilingText.py +3 -3
  5. arelle/_version.py +2 -2
  6. arelle/config/disclosuresystems.xsd +1 -0
  7. arelle/plugin/validate/DBA/rules/__init__.py +2 -2
  8. arelle/plugin/validate/EDINET/Constants.py +5 -1
  9. arelle/plugin/validate/EDINET/ContextRequirement.py +58 -0
  10. arelle/plugin/validate/EDINET/ControllerPluginData.py +69 -3
  11. arelle/plugin/validate/EDINET/PluginValidationDataExtension.py +1 -1
  12. arelle/plugin/validate/EDINET/__init__.py +10 -0
  13. arelle/plugin/validate/EDINET/resources/config.xml +2 -1
  14. arelle/plugin/validate/EDINET/rules/contexts.py +205 -2
  15. arelle/plugin/validate/EDINET/rules/frta.py +2 -2
  16. arelle/plugin/validate/EDINET/rules/gfm.py +140 -1
  17. arelle/plugin/validate/EDINET/rules/upload.py +68 -0
  18. arelle/plugin/validate/ESEF/ESEF_2021/DTS.py +5 -0
  19. arelle/plugin/validate/ESEF/ESEF_2021/Image.py +2 -2
  20. arelle/plugin/validate/ESEF/ESEF_2021/ValidateXbrlFinally.py +3 -3
  21. arelle/plugin/validate/ESEF/ESEF_Current/DTS.py +5 -0
  22. arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +23 -2
  23. arelle/plugin/validate/ESEF/resources/authority-validations.json +28 -4
  24. arelle/utils/validate/ESEFImage.py +3 -3
  25. {arelle_release-2.37.63.dist-info → arelle_release-2.37.65.dist-info}/METADATA +1 -1
  26. {arelle_release-2.37.63.dist-info → arelle_release-2.37.65.dist-info}/RECORD +30 -29
  27. {arelle_release-2.37.63.dist-info → arelle_release-2.37.65.dist-info}/WHEEL +0 -0
  28. {arelle_release-2.37.63.dist-info → arelle_release-2.37.65.dist-info}/entry_points.txt +0 -0
  29. {arelle_release-2.37.63.dist-info → arelle_release-2.37.65.dist-info}/licenses/LICENSE.md +0 -0
  30. {arelle_release-2.37.63.dist-info → arelle_release-2.37.65.dist-info}/top_level.txt +0 -0
arelle/CntlrCmdLine.py CHANGED
@@ -826,6 +826,9 @@ class CntlrCmdLine(Cntlr.Cntlr):
826
826
  else:
827
827
  self.modelManager.disclosureSystem.select(None) # just load ordinary mappings
828
828
  self.modelManager.validateDisclosureSystem = False
829
+ if self.modelManager.disclosureSystem.keepOpen:
830
+ # Force keepOpen if specified by disclosure system.
831
+ options.keepOpen = True
829
832
  if options.baseTaxonomyValidationMode is not None:
830
833
  self.modelManager.baseTaxonomyValidationMode = ValidateBaseTaxonomiesMode.fromName(options.baseTaxonomyValidationMode)
831
834
  self.modelManager.validateXmlOim = bool(options.validateXmlOim)
@@ -80,6 +80,7 @@ class DisclosureSystem:
80
80
  self.utrUrl = ["http://www.xbrl.org/utr/utr.xml"]
81
81
  self.utrStatusFilters = None
82
82
  self.utrTypeEntries = None
83
+ self.keepOpen: bool = False
83
84
  self.identifierSchemePattern = None
84
85
  self.identifierValuePattern = None
85
86
  self.identifierValueName = None
@@ -214,6 +215,7 @@ class DisclosureSystem:
214
215
  self.utrUrl = [self.modelManager.cntlr.webCache.normalizeUrl(u, url)
215
216
  for u in dsElt.get("utrUrl").split()]
216
217
  self.utrStatusFilters = dsElt.get("utrStatusFilters")
218
+ self.keepOpen = dsElt.get("keepOpen") == "true"
217
219
  self.identifierSchemePattern = compileAttrPattern(dsElt,"identifierSchemePattern")
218
220
  self.identifierValuePattern = compileAttrPattern(dsElt,"identifierValuePattern")
219
221
  self.identifierValueName = dsElt.get("identifierValueName")
arelle/UrlUtil.py CHANGED
@@ -31,6 +31,9 @@ def authority(url: str, includeScheme: bool=True) -> str:
31
31
  def scheme(url: str) -> str | None: # returns None if no scheme part
32
32
  return (url or "").rpartition(":")[0] or None
33
33
 
34
+ def isExternalUrl(url: str) -> bool:
35
+ return scheme(url) in ("http", "https", "ftp")
36
+
34
37
  absoluteUrlPattern = None
35
38
  # http://www.ietf.org/rfc/rfc2396.txt section 4.3
36
39
  # this pattern doesn't allow some valid unicode characters
@@ -13,7 +13,7 @@ from arelle.PythonUtil import isLegacyAbs
13
13
  from arelle.XbrlConst import ixbrlAll, xhtml
14
14
  from arelle.XmlUtil import setXmlns, xmlstring, xmlDeclarationPattern, XmlDeclarationLocationException
15
15
  from arelle.ModelObject import ModelObject
16
- from arelle.UrlUtil import decodeBase64DataImage, isHttpUrl, scheme
16
+ from arelle.UrlUtil import decodeBase64DataImage, isExternalUrl, isHttpUrl, scheme
17
17
 
18
18
  XMLpattern = re.compile(r".*(<|&lt;|&#x3C;|&#60;)[A-Za-z_]+[A-Za-z0-9_:]*[^>]*(/>|>|&gt;|/&gt;).*", re.DOTALL)
19
19
  CDATApattern = re.compile(r"<!\[CDATA\[(.+)\]\]")
@@ -631,7 +631,7 @@ def validateTextBlockFacts(modelXbrl):
631
631
  attribute=attrTag, element=eltTag)
632
632
  elif eltTag == "a" and (not allowedExternalHrefPattern or allowedExternalHrefPattern.match(attrValue)):
633
633
  pass
634
- elif scheme(attrValue) in ("http", "https", "ftp"):
634
+ elif isExternalUrl(attrValue):
635
635
  modelXbrl.error(("EFM.6.05.16.externalReference", "FERC.6.05.16.externalReference"),
636
636
  _("Fact %(fact)s of context %(contextID)s has an invalid external reference in '%(attribute)s' for <%(element)s>"),
637
637
  modelObject=f1, fact=f1.qname, contextID=f1.contextID,
@@ -773,7 +773,7 @@ def validateHtmlContent(modelXbrl, referenceElt, htmlEltTree, validatedObjectLab
773
773
  messageCodes=("EFM.6.05.34.activeContent", "EFM.5.02.05.activeContent", "FERC.6.05.34.activeContent", "FERC.5.02.05.activeContent"))
774
774
  elif eltTag == "a" and (not allowedExternalHrefPattern or allowedExternalHrefPattern.match(attrValue)):
775
775
  pass
776
- elif scheme(attrValue) in ("http", "https", "ftp"):
776
+ elif isExternalUrl(attrValue):
777
777
  modelXbrl.error(messageCodePrefix + "externalReference",
778
778
  _("%(validatedObjectLabel)s has an invalid external reference in '%(attribute)s' for <%(element)s>: %(value)s"),
779
779
  modelObject=elt, validatedObjectLabel=validatedObjectLabel,
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.63'
32
- __version_tuple__ = version_tuple = (2, 37, 63)
31
+ __version__ = version = '2.37.65'
32
+ __version_tuple__ = version_tuple = (2, 37, 65)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -51,6 +51,7 @@
51
51
  <xs:attribute name="deiFilerNameElement" use="optional" type="xs:NCName"/>
52
52
  <xs:attribute name="deiNamespacePattern" use="optional"/>
53
53
  <xs:attribute name="description" />
54
+ <xs:attribute name="keepOpen" type="xs:boolean"/>
54
55
  <xs:attribute name="identifierSchemePattern" use="optional"/>
55
56
  <xs:attribute name="identifierValueName" use="optional"/>
56
57
  <xs:attribute name="identifierValuePattern" use="optional"/>
@@ -14,7 +14,7 @@ from arelle.ModelInstanceObject import ModelFact
14
14
  from arelle.ModelValue import QName
15
15
  from arelle.ModelXbrl import ModelXbrl
16
16
  from arelle.typing import TypeGetText
17
- from arelle.UrlUtil import scheme
17
+ from arelle.UrlUtil import isExternalUrl
18
18
  from arelle.utils.Contexts import ContextHashKey
19
19
  from arelle.utils.validate.Validation import Validation
20
20
  from arelle.ValidateFilingText import parseImageDataURL
@@ -73,7 +73,7 @@ def errorOnForbiddenImage(
73
73
  """
74
74
  invalidImages = []
75
75
  for image in images:
76
- if scheme(image) in ("http", "https", "ftp"):
76
+ if isExternalUrl(image):
77
77
  invalidImages.append(image)
78
78
  elif image.startswith("data:image"):
79
79
  dataURLParts = parseImageDataURL(image)
@@ -159,7 +159,7 @@ REPORT_IXBRL_FILENAME_PATTERN = regex.compile(rf'{PATTERN_MAIN}_{PATTERN_NAME}_{
159
159
  AUDIT_IXBRL_FILENAME_PATTERN = regex.compile(rf'{PATTERN_AUDIT_REPORT_PREFIX}-{PATTERN_SUFFIX}_ixbrl\.htm')
160
160
 
161
161
  PATTERN_CONTEXT_RELATIVE_PERIOD = r'(?P<relative_period>Prior[1-9]Year|CurrentYear|Prior[1-9]Interim|Interim)'
162
- PATTERN_CONTEXT_PERIOD = r'(?P<relative_period>Prior[1-9]Year|CurrentYear|Prior[1-9]Interim|Interim|FilingDate|RecordDate)'
162
+ PATTERN_CONTEXT_PERIOD = r'(?P<relative_period>Prior[1-9]Year|CurrentYear|Prior[1-9]Interim|Prior[1-9]Quarter|Prior[1-9]YTD|Interim|FilingDate|RecordDate|CurrentQuarter|CurrentYTD)'
163
163
  PATTERN_CONTEXT_DURATION = r'(?P<duration>Duration|Instant)'
164
164
  PATTERN_CONTEXT_MEMBERS = r'(?P<context_members>(?:_[a-zA-Z][a-zA-Z0-9-]+)+)*'
165
165
  PATTERN_CONTEXT_NUMBER = r'(?P<context_number>[0-9]{3})'
@@ -172,5 +172,9 @@ CONTEXT_ID_PATTERN = regex.compile(rf'{PATTERN_CONTEXT_PERIOD}{PATTERN_CONTEXT_D
172
172
  # Example: Prior2YearDuration
173
173
  FINANCIAL_STATEMENT_CONTEXT_ID_PATTERN = regex.compile(rf'^{PATTERN_CONTEXT_RELATIVE_PERIOD}{PATTERN_CONTEXT_DURATION}.*')
174
174
 
175
+ # Context IDs for facts associated with individual (non-consolidated) Financial Statements
176
+ # Example: Prior2YearDuration_NonConsolidatedMember
177
+ INDIVIDUAL_CONTEXT_ID_PATTERN = regex.compile(rf'^{PATTERN_CONTEXT_RELATIVE_PERIOD}{PATTERN_CONTEXT_DURATION}_NonConsolidatedMember.*')
178
+
175
179
  # Accepted language codes for Japan
176
180
  JAPAN_LANGUAGE_CODES = frozenset({'ja', 'jp', 'ja-jp', 'JA', 'JP', 'JA-JP'})
@@ -0,0 +1,58 @@
1
+ """
2
+ See COPYRIGHT.md for copyright information.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+ from typing import Literal
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ContextRequirement:
12
+ """
13
+ If {elementExists} is set, and {elementDoesNotExist} is not set,
14
+ any context with ID starting with {contextId} must have {element} value
15
+ matching the {elementMatch} DEI value (adjusted by {dayAdjustment} days).
16
+ """
17
+ contextId: str
18
+ element: Literal['endDate', 'instant', 'startDate']
19
+ elementExists: str | None
20
+ elementDoesNotExist: str | None
21
+ elementMatch: str
22
+ dayAdjustment: int = 0 # days to adjust by (e.g. -1 for "day before")
23
+
24
+
25
+ # See section 3-4-2 in the Validation Guidelines.
26
+ CONTEXT_REQUIREMENTS = [
27
+ ContextRequirement('CurrentYearInstant', 'instant', None, None, 'CurrentFiscalYearEndDateDEI'),
28
+ ContextRequirement('Prior1YearInstant', 'instant', 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', None, 'CurrentFiscalYearEndDateDEI'),
29
+ ContextRequirement('Prior1YearInstant', 'instant', None, 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', 'PreviousFiscalYearEndDateDEI'),
30
+ ContextRequirement('Prior2YearInstant', 'instant', 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', None, 'PreviousFiscalYearEndDateDEI'),
31
+ ContextRequirement('Prior2YearInstant', 'instant', None, 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', 'PreviousFiscalYearStartDateDEI', -1),
32
+ ContextRequirement('CurrentQuarterInstant', 'instant', 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', None, 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI'),
33
+ ContextRequirement('CurrentQuarterInstant', 'instant', None, 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', 'CurrentPeriodEndDateDEI'),
34
+ ContextRequirement('Prior1QuarterInstant', 'instant', None, 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', 'ComparativePeriodEndDateDEI'),
35
+ ContextRequirement('InterimInstant', 'instant', 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', None, 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI'),
36
+ ContextRequirement('InterimInstant', 'instant', None, 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', 'CurrentPeriodEndDateDEI'),
37
+ ContextRequirement('Prior1InterimInstant', 'instant', None, 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', 'ComparativePeriodEndDateDEI'),
38
+ ContextRequirement('CurrentYearDuration', 'startDate', None, None, 'CurrentFiscalYearStartDateDEI'),
39
+ ContextRequirement('Prior1YearDuration', 'startDate', 'NextFiscalYearStartDateDEI', None, 'CurrentFiscalYearStartDateDEI'),
40
+ ContextRequirement('Prior1YearDuration', 'startDate', None, 'NextFiscalYearStartDateDEI', 'PreviousFiscalYearStartDateDEI'),
41
+ ContextRequirement('Prior2YearDuration', 'startDate', 'NextFiscalYearStartDateDEI', None, 'PreviousFiscalYearStartDateDEI'),
42
+ ContextRequirement('CurrentYTDDuration', 'startDate', 'NextFiscalYearStartDateDEI', None, 'NextFiscalYearStartDateDEI'),
43
+ ContextRequirement('CurrentYTDDuration', 'startDate', None, 'NextFiscalYearStartDateDEI', 'CurrentFiscalYearStartDateDEI'),
44
+ ContextRequirement('Prior1YTDDuration', 'startDate', None, 'NextFiscalYearStartDateDEI', 'PreviousFiscalYearStartDateDEI'),
45
+ ContextRequirement('InterimDuration', 'startDate', 'NextFiscalYearStartDateDEI', None, 'NextFiscalYearStartDateDEI'),
46
+ ContextRequirement('InterimDuration', 'startDate', None, 'NextFiscalYearStartDateDEI', 'CurrentFiscalYearStartDateDEI'),
47
+ ContextRequirement('Prior1InterimDuration', 'startDate', None, 'NextFiscalYearStartDateDEI', 'PreviousFiscalYearStartDateDEI'),
48
+ ContextRequirement('CurrentYearDuration', 'endDate', None, None, 'CurrentFiscalYearEndDateDEI'),
49
+ ContextRequirement('Prior1YearDuration', 'endDate', 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', None, 'CurrentFiscalYearEndDateDEI'),
50
+ ContextRequirement('Prior1YearDuration', 'endDate', None, 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', 'PreviousFiscalYearEndDateDEI'),
51
+ ContextRequirement('Prior2YearDuration', 'endDate', 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', None, 'PreviousFiscalYearEndDateDEI'),
52
+ ContextRequirement('CurrentYTDDuration', 'endDate', 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', None, 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI'),
53
+ ContextRequirement('CurrentYTDDuration', 'endDate', None, 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', 'CurrentPeriodEndDateDEI'),
54
+ ContextRequirement('Prior1YTDDuration', 'endDate', None, 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', 'ComparativePeriodEndDateDEI'),
55
+ ContextRequirement('InterimDuration', 'endDate', 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', None, 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI'),
56
+ ContextRequirement('InterimDuration', 'endDate', None, 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', 'CurrentPeriodEndDateDEI'),
57
+ ContextRequirement('Prior1InterimDuration', 'endDate', None, 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI', 'ComparativePeriodEndDateDEI'),
58
+ ]
@@ -12,12 +12,14 @@ 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
+ from arelle.ModelValue import QName, TypeXValue
16
+ from arelle.ModelXbrl import ModelXbrl
17
+ from arelle.XmlValidateConst import VALID
16
18
  from arelle.typing import TypeGetText
17
19
  from arelle.utils.PluginData import PluginData
18
20
  from . import Constants
19
21
  from .CoverItemRequirements import CoverItemRequirements
20
- from .DeiRequirements import DeiRequirements
22
+ from .DeiRequirements import DeiRequirements, DEI_LOCAL_NAMES
21
23
  from .FilingFormat import FilingFormat
22
24
  from .ReportFolderType import ReportFolderType
23
25
  from .TableOfContentsBuilder import TableOfContentsBuilder
@@ -31,6 +33,8 @@ _: TypeGetText
31
33
 
32
34
  @dataclass
33
35
  class ControllerPluginData(PluginData):
36
+ _deiValues: dict[str, TypeXValue]
37
+ _loadedModelXbrls: list[ModelXbrl]
34
38
  _manifestInstancesById: dict[str, ManifestInstance]
35
39
  _tocBuilder: TableOfContentsBuilder
36
40
  _uploadContents: UploadContents | None
@@ -38,6 +42,8 @@ class ControllerPluginData(PluginData):
38
42
 
39
43
  def __init__(self, name: str):
40
44
  super().__init__(name)
45
+ self._deiValues = {}
46
+ self._loadedModelXbrls = []
41
47
  self._manifestInstancesById = {}
42
48
  self._tocBuilder = TableOfContentsBuilder()
43
49
  self._usedFilepaths = set()
@@ -52,6 +58,10 @@ class ControllerPluginData(PluginData):
52
58
  """
53
59
  self._manifestInstancesById[manifestInstance.id] = manifestInstance
54
60
 
61
+ def addModelXbrl(self, modelXbrl: ModelXbrl) -> None:
62
+ self._loadedModelXbrls.append(modelXbrl)
63
+ self.setDeiValues(modelXbrl)
64
+
55
65
  @lru_cache(1)
56
66
  def getCoverItemRequirements(self, jsonPath: Path) -> CoverItemRequirements:
57
67
  return CoverItemRequirements(jsonPath)
@@ -60,6 +70,9 @@ class ControllerPluginData(PluginData):
60
70
  def getDeiRequirements(self, csvPath: Path, deiItems: tuple[QName, ...], filingFormats: tuple[FilingFormat, ...]) -> DeiRequirements:
61
71
  return DeiRequirements(csvPath, deiItems, filingFormats)
62
72
 
73
+ def getDeiValue(self, localName: str) -> TypeXValue:
74
+ return self._deiValues.get(localName)
75
+
63
76
  def getManifestInstances(self) -> list[ManifestInstance]:
64
77
  """
65
78
  Retrieve all loaded manifest instances.
@@ -159,6 +172,28 @@ class ControllerPluginData(PluginData):
159
172
  def getUsedFilepaths(self) -> frozenset[Path]:
160
173
  return frozenset(self._usedFilepaths)
161
174
 
175
+ def isConsolidated(self) -> bool | None:
176
+ """
177
+ Is this a consolidated (not individual) filing?
178
+ Looks for the DEI fact 'WhetherConsolidatedFinancialStatementsArePreparedDEI'
179
+ within PublicDoc instances. If an explicit True/False value is found, it is returned.
180
+ If no non-nil value exists, None is returned, which indicates a not applicable state.
181
+ :return:
182
+ """
183
+ for modelXbrl in self.loadedModelXbrls:
184
+ manifestInstance = self.getManifestInstance(modelXbrl)
185
+ if manifestInstance is None:
186
+ continue
187
+ if manifestInstance.type != ReportFolderType.PUBLIC_DOC.value:
188
+ continue
189
+ facts = modelXbrl.factsByLocalName.get('WhetherConsolidatedFinancialStatementsArePreparedDEI', set())
190
+ for fact in facts:
191
+ if fact.xValue == True:
192
+ return True
193
+ if fact.xValue == False:
194
+ return False
195
+ return None
196
+
162
197
  @lru_cache(1)
163
198
  def isUpload(self, fileSource: FileSource) -> bool:
164
199
  fileSource.open() # Make sure file source is open
@@ -173,13 +208,23 @@ class ControllerPluginData(PluginData):
173
208
  return False
174
209
  return True
175
210
 
176
- def matchManifestInstance(self, ixdsDocUrls: list[str]) -> ManifestInstance | None:
211
+ @property
212
+ def loadedModelXbrls(self) -> list[ModelXbrl]:
213
+ """
214
+ TODO: Only necessary because cntlr.modelManager.loadedModelXbrls is not reliable
215
+ in the current conformance suite runner. Remove when that is fixed/replaced.
216
+ """
217
+ return self._loadedModelXbrls
218
+
219
+ @lru_cache(1)
220
+ def getManifestInstance(self, modelXbrl: ModelXbrl) -> ManifestInstance | None:
177
221
  """
178
222
  Match a manifest instance based on the provided ixdsDocUrls.
179
223
  A one-to-one mapping must exist between the model's IXDS document URLs and the manifest instance's IXBRL files.
180
224
  :param ixdsDocUrls: A model's list of IXDS document URLs.
181
225
  :return: A matching ManifestInstance if found, otherwise None.
182
226
  """
227
+ ixdsDocUrls = modelXbrl.ixdsDocUrls
183
228
  modelUrls = set(ixdsDocUrls)
184
229
  matchedInstance = None
185
230
  for instance in self._manifestInstancesById.values():
@@ -204,6 +249,27 @@ class ControllerPluginData(PluginData):
204
249
  break
205
250
  return matchedInstance
206
251
 
252
+ def setDeiValue(self, localName: str, value: TypeXValue) -> None:
253
+ if localName in self._deiValues:
254
+ # Duplicate DEI values will be caught by validations.
255
+ return
256
+ self._deiValues[localName] = value
257
+
258
+ def setDeiValues(self, modelXbrl: ModelXbrl) -> None:
259
+ """
260
+ Set DEI values from the provided modelXbrl.
261
+ Some EDINET validations rely on both DEI values defined in one instance
262
+ and other values in a separate instance, so we collect DEI values from
263
+ all instances and collect them at a controller level.
264
+ :param modelXbrl:
265
+ :return:
266
+ """
267
+ for localName in DEI_LOCAL_NAMES:
268
+ for fact in modelXbrl.factsByLocalName.get(localName, ()):
269
+ if fact.isNil or fact.xValid < VALID:
270
+ continue
271
+ self.setDeiValue(localName, fact.xValue)
272
+
207
273
  def addUsedFilepath(self, path: Path) -> None:
208
274
  self._usedFilepaths.add(path)
209
275
 
@@ -568,7 +568,7 @@ class PluginValidationDataExtension(PluginData):
568
568
  @lru_cache(1)
569
569
  def getManifestInstance(self, modelXbrl: ModelXbrl) -> ManifestInstance | None:
570
570
  controllerPluginData = ControllerPluginData.get(modelXbrl.modelManager.cntlr, self.name)
571
- return controllerPluginData.matchManifestInstance(modelXbrl.ixdsDocUrls)
571
+ return controllerPluginData.getManifestInstance(modelXbrl)
572
572
 
573
573
  @lru_cache(1)
574
574
  def getProhibitedAttributeElements(self, modelDocument: ModelDocument) -> list[tuple[ModelObject, str]]:
@@ -9,8 +9,10 @@ from pathlib import Path
9
9
  from typing import Any
10
10
 
11
11
  from arelle.ModelXbrl import ModelXbrl
12
+ from arelle.ValidateXbrl import ValidateXbrl
12
13
  from arelle.Version import authorLabel, copyrightLabel
13
14
  from . import Constants
15
+ from .ControllerPluginData import ControllerPluginData
14
16
  from .ValidationPluginExtension import ValidationPluginExtension
15
17
  from .rules import contexts, edinet, frta, gfm, manifests, upload
16
18
 
@@ -84,6 +86,13 @@ def validateFinally(*args: Any, **kwargs: Any) -> None:
84
86
  return validationPlugin.validateFinally(*args, **kwargs)
85
87
 
86
88
 
89
+ def validateXbrlStart(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None:
90
+ # TODO: See ControllerPluginData.loadedModelXbrls comment
91
+ controllerPluginData = ControllerPluginData.get(val.modelXbrl.modelManager.cntlr, PLUGIN_NAME)
92
+ controllerPluginData.addModelXbrl(val.modelXbrl)
93
+ return validationPlugin.validateXbrlStart(val, *args, **kwargs)
94
+
95
+
87
96
  def validateXbrlFinally(*args: Any, **kwargs: Any) -> None:
88
97
  return validationPlugin.validateXbrlFinally(*args, **kwargs)
89
98
 
@@ -103,5 +112,6 @@ __pluginInfo__ = {
103
112
  "Validate.Complete": validateComplete,
104
113
  "Validate.FileSource": validateFileSource,
105
114
  "Validate.XBRL.Finally": validateXbrlFinally,
115
+ 'Validate.XBRL.Start': validateXbrlStart,
106
116
  "ValidateFormula.Finished": validateFinally,
107
117
  }
@@ -13,9 +13,10 @@
13
13
  "https://xbrl.org/2023/arcrole/summation-item": ["none", "EDINET.EC5700W.GFM.1.7.4"]
14
14
  }'
15
15
  defaultXmlLang="ja"
16
+ keepOpen="true"
16
17
  blockDisallowedReferences="true"
18
+ standardTaxonomiesUrl="edinet-taxonomies.xml"
17
19
  validationType="EDINET"
18
- validTaxonomiesUrl="edinet-taxonomies.xml"
19
20
  exclusiveTypesPattern="EFM|GFM|FERC|HMRC|SBR.NL|EBA|EIOPA|ESEF"
20
21
  />
21
22
  </DisclosureSystems>
@@ -3,21 +3,30 @@ See COPYRIGHT.md for copyright information.
3
3
  """
4
4
  from __future__ import annotations
5
5
 
6
+ import datetime
6
7
  from collections import defaultdict
8
+
9
+ from dateutil.relativedelta import relativedelta
7
10
  from itertools import chain
8
- from typing import Any, Iterable
11
+ from typing import Any, Iterable, cast
9
12
 
10
13
  from arelle import XbrlConst
14
+ from arelle.Cntlr import Cntlr
15
+ from arelle.FileSource import FileSource
11
16
  from arelle.LinkbaseType import LinkbaseType
12
17
  from arelle.ModelDtsObject import ModelConcept
13
18
  from arelle.ValidateXbrl import ValidateXbrl
19
+ from arelle.XmlValidateConst import VALID
14
20
  from arelle.typing import TypeGetText
15
21
  from arelle.utils.PluginHooks import ValidationHook
16
22
  from arelle.utils.validate.Decorator import validation
17
23
  from arelle.utils.validate.Validation import Validation
18
- from ..Constants import FINANCIAL_STATEMENT_CONTEXT_ID_PATTERN, CONTEXT_ID_PATTERN
24
+ from ..Constants import FINANCIAL_STATEMENT_CONTEXT_ID_PATTERN, CONTEXT_ID_PATTERN, INDIVIDUAL_CONTEXT_ID_PATTERN
25
+ from ..ContextRequirement import CONTEXT_REQUIREMENTS
26
+ from ..ControllerPluginData import ControllerPluginData
19
27
  from ..DisclosureSystems import (DISCLOSURE_SYSTEM_EDINET)
20
28
  from ..PluginValidationDataExtension import PluginValidationDataExtension
29
+ from ..ReportFolderType import ReportFolderType
21
30
 
22
31
  _: TypeGetText
23
32
 
@@ -150,6 +159,200 @@ def rule_EC8013W(
150
159
  )
151
160
 
152
161
 
162
+ @validation(
163
+ hook=ValidationHook.COMPLETE,
164
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
165
+ )
166
+ def rule_EC8014W(
167
+ pluginData: ControllerPluginData,
168
+ cntlr: Cntlr,
169
+ fileSource: FileSource,
170
+ *args: Any,
171
+ **kwargs: Any,
172
+ ) -> Iterable[Validation]:
173
+ """
174
+ EDINET.EC8014W: For individual (non-consolidated) reports, there must be a context that represents an individual.
175
+
176
+ Individual reports identified by presence of WhetherConsolidatedFinancialStatementsArePreparedDEI with False value.
177
+ Individual context identified by context ID matching pattern with "_NonConsolidatedMember".
178
+ """
179
+ if pluginData.isConsolidated() != False:
180
+ return
181
+ if not any(
182
+ INDIVIDUAL_CONTEXT_ID_PATTERN.fullmatch(contextID)
183
+ for modelXbrl in pluginData.loadedModelXbrls
184
+ for contextID in modelXbrl.contexts
185
+ ):
186
+ yield Validation.warning(
187
+ codes='EDINET.EC8014W',
188
+ msg=_("There is no context ID in the inline XBRL file that represents an individual. "
189
+ "Please set a context ID that indicates individual financial "
190
+ "statements in the inline XBRL file. "
191
+ "If you are not including individual financial statements, "
192
+ "please check the \"WhetherConsolidatedFinancialStatementsArePreparedDEI\" "
193
+ "value of the DEI information."),
194
+ )
195
+
196
+
197
+ @validation(
198
+ hook=ValidationHook.COMPLETE,
199
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
200
+ )
201
+ def rule_EC8015W(
202
+ pluginData: ControllerPluginData,
203
+ cntlr: Cntlr,
204
+ fileSource: FileSource,
205
+ *args: Any,
206
+ **kwargs: Any,
207
+ ) -> Iterable[Validation]:
208
+ """
209
+ EDINET.EC8015W: For consolidated reports, there must not be a context that represents an individual.
210
+
211
+ Consolidated reports identified by presence of WhetherConsolidatedFinancialStatementsArePreparedDEI with True value.
212
+ Individual context identified by context ID matching pattern with "_NonConsolidatedMember".
213
+ """
214
+ if pluginData.isConsolidated() != True:
215
+ return
216
+ individualContexts = [
217
+ context
218
+ for modelXbrl in pluginData.loadedModelXbrls
219
+ for contextId, context in modelXbrl.contexts.items()
220
+ if INDIVIDUAL_CONTEXT_ID_PATTERN.fullmatch(contextId)
221
+ ]
222
+ if len(individualContexts) > 0:
223
+ yield Validation.warning(
224
+ codes='EDINET.EC8015W',
225
+ msg=_("There is a context ID in the inline XBRL file that represents an individual. "
226
+ "If you do not want to enter information related to individual financial statements, "
227
+ "delete the context ID that indicates individual. "
228
+ "If you want to enter individual financial statements, "
229
+ "check the \"WhetherConsolidatedFinancialStatementsArePreparedDEI\" status in the DEI "
230
+ "information. "
231
+ "* If there is a change from non-consolidated to consolidated, even if the data content "
232
+ "is normal, it may be recognized as an exception and a warning may be displayed."),
233
+ modelObject=individualContexts,
234
+ )
235
+
236
+
237
+ @validation(
238
+ hook=ValidationHook.COMPLETE,
239
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
240
+ )
241
+ def rule_contextDeiRequirements(
242
+ pluginData: ControllerPluginData,
243
+ cntlr: Cntlr,
244
+ fileSource: FileSource,
245
+ *args: Any,
246
+ **kwargs: Any,
247
+ ) -> Iterable[Validation]:
248
+ """
249
+ EDINET.EC8018W, EDINET.EC8019W, and EDINET.EC8020W assert that context instant, startDate, and endDate values
250
+ (respectively) match certain DEI values based on the presence of DEI values and patterns in the context ID.
251
+ See section 3-4-2 in the Validation Guidelines.
252
+ """
253
+ for modelXbrl in pluginData.loadedModelXbrls:
254
+ for contextId, context in modelXbrl.contexts.items():
255
+ for contextDeiRequirement in CONTEXT_REQUIREMENTS:
256
+ if not contextId.startswith(contextDeiRequirement.contextId):
257
+ continue
258
+ if contextDeiRequirement.elementDoesNotExist is not None:
259
+ if pluginData.getDeiValue(contextDeiRequirement.elementDoesNotExist) is not None:
260
+ continue
261
+ if contextDeiRequirement.elementExists is not None:
262
+ if pluginData.getDeiValue(contextDeiRequirement.elementExists) is None:
263
+ continue
264
+
265
+ if contextDeiRequirement.element == 'startDate':
266
+ contextValue = context.startDatetime
267
+ code = "EDINET.EC8019W"
268
+ elif contextDeiRequirement.element == 'endDate':
269
+ contextValue = context.endDatetime
270
+ code = "EDINET.EC8020W"
271
+ else:
272
+ assert contextDeiRequirement.element == 'instant'
273
+ contextValue = context.instantDatetime
274
+ code = "EDINET.EC8018W"
275
+
276
+ deiValue = pluginData.getDeiValue(contextDeiRequirement.elementMatch)
277
+ if deiValue is None:
278
+ continue
279
+ deiValue = cast(datetime.datetime, deiValue)
280
+
281
+ if contextDeiRequirement.element in ('instant', 'endDate'):
282
+ # Instant and end dates are parsed as the beginning of the day after the date specified by the value
283
+ # DEI values need to be adjusted by 1 day to match this.
284
+ deiValue = cast(datetime.datetime, deiValue) + datetime.timedelta(1)
285
+ if contextDeiRequirement.dayAdjustment:
286
+ deiValue = cast(datetime.datetime, deiValue) + datetime.timedelta(contextDeiRequirement.dayAdjustment)
287
+
288
+ if contextValue != deiValue:
289
+ yield Validation.warning(
290
+ codes=code,
291
+ msg=_("The context %(element)s element does not match the information in DEI "
292
+ "\"%(elementMatch)s\". "
293
+ "Context ID: '%(contextId)s'. "
294
+ "DEI value: '%(deiValue)s'. "
295
+ "Context value: '%(contextValue)s'. "
296
+ "Please set the same value for the %(element)s element value of the corresponding "
297
+ "context ID and the DEI information value."),
298
+ element=contextDeiRequirement.element,
299
+ elementMatch=contextDeiRequirement.elementMatch,
300
+ contextId=contextId,
301
+ deiValue=deiValue.date().isoformat(),
302
+ contextValue=contextValue.date().isoformat(),
303
+ )
304
+
305
+
306
+ @validation(
307
+ hook=ValidationHook.COMPLETE,
308
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
309
+ )
310
+ def rule_EC8021W(
311
+ pluginData: ControllerPluginData,
312
+ cntlr: Cntlr,
313
+ fileSource: FileSource,
314
+ *args: Any,
315
+ **kwargs: Any,
316
+ ) -> Iterable[Validation]:
317
+ """
318
+ EDINET.EC8021W: "EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI" or "CurrentPeriodEndDateDEI"
319
+ must not be more than one year earlier than "FilingDateCoverPage".
320
+
321
+
322
+ If the "EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI" of the DEI information is present,
323
+ then its value must not be more than one year earlier than "FilingDateCoverPage".
324
+ If "EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI" is not present, but "CurrentPeriodEndDateDEI"
325
+ is present, then its value must not be more than one year earlier than "FilingDateCoverPage".
326
+ """
327
+ localName = 'EndDateOfQuarterlyOrSemiAnnualPeriodOfNextFiscalYearDEI'
328
+ targetDate = pluginData.getDeiValue(localName)
329
+ if not isinstance(targetDate, datetime.datetime):
330
+ localName = 'CurrentPeriodEndDateDEI'
331
+ targetDate = pluginData.getDeiValue(localName)
332
+ if not isinstance(targetDate, datetime.datetime):
333
+ return
334
+ compareDate = cast(datetime.datetime, targetDate + relativedelta(years=1))
335
+ for modelXbrl in pluginData.loadedModelXbrls:
336
+ for fact in modelXbrl.factsByLocalName.get('FilingDateCoverPage', set()):
337
+ if fact.isNil or fact.xValid < VALID:
338
+ continue
339
+ submissionDate = cast(datetime.datetime, fact.xValue)
340
+ if compareDate < submissionDate:
341
+ yield Validation.warning(
342
+ codes='EDINET.EC8021W',
343
+ msg=_("The DEI '%(localName)s' information is set to a date that is "
344
+ "more than one year earlier than 'FilingDateCoverPage'. "
345
+ "Please set the '%(localName)s' value to a value that is less "
346
+ "than one year earlier than the value of 'FilingDateCoverPage'. "
347
+ "%(localName)s: '%(targetDate)s'. "
348
+ "FilingDateCoverPage: '%(submissionDate)s'. "),
349
+ localName=localName,
350
+ targetDate=targetDate.date().isoformat(),
351
+ submissionDate=submissionDate.date().isoformat(),
352
+ modelObject=fact,
353
+ )
354
+
355
+
153
356
  @validation(
154
357
  hook=ValidationHook.XBRL_FINALLY,
155
358
  disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
@@ -231,8 +231,8 @@ def rule_frta_4_2_4(
231
231
  if rootElt.get('elementFormDefault') != 'qualified' or rootElt.get('attributeFormDefault') != 'unqualified':
232
232
  yield Validation.warning(
233
233
  codes='EDINET.EC5710W.FRTA.4.2.4',
234
- msg=_("The XMLSchema root in taxonomy schema files must have the 'elementFormDefault' atribute set as "
235
- "'qulaified' and the 'attributeFormDefault' attribute set as 'unqualified'"),
234
+ msg=_("The XMLSchema root in taxonomy schema files must have the 'elementFormDefault' attribute set as "
235
+ "'qualified' and the 'attributeFormDefault' attribute set as 'unqualified'"),
236
236
  modelObject=modelDocument,
237
237
  )
238
238
  formUsages = []