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

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

Potentially problematic release.


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

Files changed (27) hide show
  1. arelle/ModelDocument.py +16 -14
  2. arelle/ModelInstanceObject.py +1 -1
  3. arelle/ModelXbrl.py +21 -10
  4. arelle/WebCache.py +26 -16
  5. arelle/_version.py +16 -3
  6. arelle/api/Session.py +5 -2
  7. arelle/plugin/validate/DBA/PluginValidationDataExtension.py +0 -1
  8. arelle/plugin/validate/EDINET/Constants.py +11 -0
  9. arelle/plugin/validate/EDINET/ControllerPluginData.py +40 -29
  10. arelle/plugin/validate/EDINET/PluginValidationDataExtension.py +30 -5
  11. arelle/plugin/validate/EDINET/{InstanceType.py → ReportFolderType.py} +11 -15
  12. arelle/plugin/validate/EDINET/UploadContents.py +20 -5
  13. arelle/plugin/validate/EDINET/rules/contexts.py +4 -2
  14. arelle/plugin/validate/EDINET/rules/edinet.py +22 -0
  15. arelle/plugin/validate/EDINET/rules/gfm.py +20 -29
  16. arelle/plugin/validate/EDINET/rules/upload.py +230 -57
  17. arelle/plugin/validate/ESEF/ESEF_2021/ValidateXbrlFinally.py +2 -2
  18. arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +2 -2
  19. arelle/plugin/validate/NL/PluginValidationDataExtension.py +1 -1
  20. arelle/plugin/validate/NL/rules/fr_nl.py +6 -7
  21. arelle/plugin/validate/UK/ValidateUK.py +31 -66
  22. {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/METADATA +20 -18
  23. {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/RECORD +27 -27
  24. {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/WHEEL +0 -0
  25. {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/entry_points.txt +0 -0
  26. {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/licenses/LICENSE.md +0 -0
  27. {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/top_level.txt +0 -0
arelle/ModelDocument.py CHANGED
@@ -1579,12 +1579,6 @@ def inlineIxdsDiscover(modelXbrl, modelIxdsDocument, setTargetModelXbrl=False, *
1579
1579
  modelXbrl.targetRoleRefs = {} # roleRefs used by selected target
1580
1580
  modelXbrl.targetArcroleRefs = {} # arcroleRefs used by selected target
1581
1581
  modelXbrl.targetRelationships = set() # relationship elements used by selected target
1582
- targetModelXbrl = modelXbrl if setTargetModelXbrl else None # modelXbrl of target for contexts/units in multi-target/multi-instance situation
1583
- assignUnusedContextsUnits = (not setTargetModelXbrl and not ixdsTarget and
1584
- not getattr(modelXbrl, "supplementalModelXbrls", ()) and (
1585
- not getattr(modelXbrl, "targetIXDSesToLoad", ()) or
1586
- set(e.modelDocument for e in modelXbrl.ixdsHtmlElements) ==
1587
- set(x.modelDocument for e in getattr(modelXbrl, "targetIXDSesToLoad", ()) for x in e[1])))
1588
1582
  hasResources = hasHeader = False
1589
1583
  for htmlElement in modelXbrl.ixdsHtmlElements:
1590
1584
  mdlDoc = htmlElement.modelDocument
@@ -1611,8 +1605,12 @@ def inlineIxdsDiscover(modelXbrl, modelIxdsDocument, setTargetModelXbrl=False, *
1611
1605
  for modelInlineFact in htmlElement.iterdescendants(ixNStag + "nonNumeric", ixNStag + "nonFraction", ixNStag + "fraction"):
1612
1606
  if isinstance(modelInlineFact,ModelObject):
1613
1607
  _target = modelInlineFact.get("target")
1614
- factTargetContextRefs[_target].add(modelInlineFact.get("contextRef"))
1615
- factTargetUnitRefs[_target].add(modelInlineFact.get("unitRef"))
1608
+ contextRef = modelInlineFact.get("contextRef")
1609
+ if contextRef is not None:
1610
+ factTargetContextRefs[_target].add(contextRef.strip())
1611
+ unitRef = modelInlineFact.get("unitRef")
1612
+ if unitRef is not None:
1613
+ factTargetUnitRefs[_target].add(unitRef.strip())
1616
1614
  if modelInlineFact.id:
1617
1615
  factsByFactID[modelInlineFact.id] = modelInlineFact
1618
1616
  for elt in htmlElement.iterdescendants(tag=ixNStag + "continuation"):
@@ -1734,9 +1732,9 @@ def inlineIxdsDiscover(modelXbrl, modelIxdsDocument, setTargetModelXbrl=False, *
1734
1732
  targetRoleUris[_target].add(footnoteRole)
1735
1733
 
1736
1734
  contextRefs = factTargetContextRefs[ixdsTarget]
1735
+ contextRefsForAllTargets = {ref for refs in factTargetContextRefs.values() for ref in refs}
1737
1736
  unitRefs = factTargetUnitRefs[ixdsTarget]
1738
- allContextRefs = set.union(*factTargetContextRefs.values())
1739
- allUnitRefs = set.union(*factTargetUnitRefs.values())
1737
+ unitRefsForAllTargets = {ref for refs in factTargetUnitRefs.values() for ref in refs}
1740
1738
 
1741
1739
  # discovery of contexts, units and roles which are used by target document
1742
1740
  for htmlElement in modelXbrl.ixdsHtmlElements:
@@ -1745,13 +1743,17 @@ def inlineIxdsDiscover(modelXbrl, modelIxdsDocument, setTargetModelXbrl=False, *
1745
1743
 
1746
1744
  for inlineElement in htmlElement.iterdescendants(tag=ixNStag + "resources"):
1747
1745
  for elt in inlineElement.iterchildren("{http://www.xbrl.org/2003/instance}context"):
1748
- id = elt.get("id")
1749
- if id in contextRefs or (assignUnusedContextsUnits and id not in allContextRefs):
1746
+ contextId = elt.get("id")
1747
+ if contextId in contextRefs:
1750
1748
  modelIxdsDocument.contextDiscover(elt, setTargetModelXbrl)
1749
+ elif contextId not in contextRefsForAllTargets:
1750
+ modelXbrl.ixdsUnmappedContexts[contextId] = elt
1751
1751
  for elt in inlineElement.iterchildren("{http://www.xbrl.org/2003/instance}unit"):
1752
- id = elt.get("id")
1753
- if id in unitRefs or (assignUnusedContextsUnits and id not in allUnitRefs):
1752
+ unitId = elt.get("id")
1753
+ if unitId in unitRefs:
1754
1754
  modelIxdsDocument.unitDiscover(elt, setTargetModelXbrl)
1755
+ elif unitId not in unitRefsForAllTargets:
1756
+ modelXbrl.ixdsUnmappedUnits[unitId] = elt
1755
1757
  for refElement in inlineElement.iterchildren("{http://www.xbrl.org/2003/linkbase}roleRef"):
1756
1758
  r = refElement.get("roleURI")
1757
1759
  if r in targetRoleUris[ixdsTarget]:
@@ -1159,7 +1159,7 @@ class ModelContext(ModelObject):
1159
1159
  self._dimsHash = hash( frozenset(self.qnameDims.values()) )
1160
1160
  return self._dimsHash
1161
1161
 
1162
- def nonDimValues(self, contextElement):
1162
+ def nonDimValues(self, contextElement: str | int) -> list[ModelObject]:
1163
1163
  """([ModelObject]) -- ContextElement is either string or Aspect code for segment or scenario, returns nonXDT ModelObject children of context element.
1164
1164
 
1165
1165
  :param contextElement: one of 'segment', 'scenario', Aspect.NON_XDT_SEGMENT, Aspect.NON_XDT_SCENARIO, Aspect.COMPLETE_SEGMENT, Aspect.COMPLETE_SCENARIO
arelle/ModelXbrl.py CHANGED
@@ -12,7 +12,7 @@ from collections import defaultdict
12
12
  from typing import TYPE_CHECKING, Any, TypeVar, Union, cast, Optional
13
13
 
14
14
  import regex as re
15
- from collections.abc import Iterable
15
+ from collections.abc import Iterable, Iterator
16
16
 
17
17
  import arelle
18
18
  from arelle import FileSource, ModelRelationshipSet, XmlUtil, ModelValue, XbrlConst, XmlValidate
@@ -335,8 +335,12 @@ class ModelXbrl:
335
335
  self.facts: list[ModelFact] = []
336
336
  self.factsInInstance: set[ModelFact] = set()
337
337
  self.undefinedFacts: list[ModelFact] = [] # elements presumed to be facts but not defined
338
- self.contexts: dict[str, ModelDocumentClass.xmlRootElement] = {}
338
+ self.contexts: dict[str, ModelContext] = {}
339
+ self.ixdsUnmappedContexts: dict[str, ModelContext] = {}
340
+ self._contextsInUseMarked = False
339
341
  self.units: dict[str, ModelUnit] = {}
342
+ self.ixdsUnmappedUnits: dict[str, ModelUnit] = {}
343
+ self._unitsInUseMarked = False
340
344
  self.modelObjects: list[ModelObject] = []
341
345
  self.qnameParameters: dict[QName, Any] = {}
342
346
  self.modelVariableSets: set[ModelVariableSet] = set()
@@ -604,7 +608,7 @@ class ModelXbrl:
604
608
  for cOCCs,mOCCs in ((c.nonDimValues(segAspect),segOCCs),
605
609
  (c.nonDimValues(scenAspect),scenOCCs)))
606
610
  ):
607
- return cast('ModelContext', c)
611
+ return c
608
612
  return None
609
613
 
610
614
  def createContext(
@@ -868,17 +872,24 @@ class ModelXbrl:
868
872
  return fbdq[memQname]
869
873
 
870
874
  @property
871
- def contextsInUse(self) -> Any:
872
- try:
873
- if self._contextsInUseMarked:
874
- return (cntx for cntx in self.contexts.values() if getattr(cntx, "_inUse", False))
875
- except AttributeError:
875
+ def contextsInUse(self) -> Iterator[ModelContext]:
876
+ if not self._contextsInUseMarked:
876
877
  for fact in self.factsInInstance:
877
878
  cntx = fact.context
878
879
  if cntx is not None:
879
880
  cntx._inUse = True
880
- self._contextsInUseMarked: bool = True
881
- return self.contextsInUse
881
+ self._contextsInUseMarked = True
882
+ return (cntx for cntx in self.contexts.values() if getattr(cntx, "_inUse", False))
883
+
884
+ @property
885
+ def unitsInUse(self) -> Iterator[ModelUnit]:
886
+ if not self._unitsInUseMarked:
887
+ for fact in self.factsInInstance:
888
+ unit = fact.unit
889
+ if unit is not None:
890
+ unit._inUse = True
891
+ self._unitsInUseMarked = True
892
+ return (unit for unit in self.units.values() if getattr(unit, "_inUse", False))
882
893
 
883
894
  @property
884
895
  def dimensionsInUse(self) -> set[Any]:
arelle/WebCache.py CHANGED
@@ -6,20 +6,28 @@ e.g., User-Agent: Sample Company Name AdminContact@<sample company domain>.com
6
6
 
7
7
  '''
8
8
  from __future__ import annotations
9
- import contextlib
10
-
11
9
 
12
- from filelock import FileLock, Timeout
10
+ import calendar
11
+ import contextlib
12
+ import io
13
+ import json
14
+ import logging
15
+ import os
16
+ import posixpath
17
+ import shutil
18
+ import sys
19
+ import time
20
+ import zlib
21
+ from http.client import IncompleteRead
13
22
  from pathlib import Path
14
23
  from typing import TYPE_CHECKING, Any
15
- import os, posixpath, sys, time, calendar, io, json, logging, shutil, zlib
16
- import regex as re
17
- from urllib.parse import quote, unquote
18
- from urllib.error import URLError, HTTPError, ContentTooShortError
19
- from http.client import IncompleteRead
20
24
  from urllib import request as proxyhandlers
25
+ from urllib.error import ContentTooShortError, HTTPError, URLError
26
+ from urllib.parse import quote, unquote, urlsplit, urlunsplit
21
27
 
22
28
  import certifi
29
+ import regex as re
30
+ from filelock import FileLock, Timeout
23
31
 
24
32
  from arelle.PythonUtil import isLegacyAbs
25
33
 
@@ -567,14 +575,16 @@ class WebCache:
567
575
  :param url:
568
576
  :return: `url` with scheme-specific-part quoted except for parameter separators
569
577
  """
570
- urlScheme, schemeSep, urlSchemeSpecificPart = url.partition("://")
571
- urlPath, querySep, query = urlSchemeSpecificPart.partition("?")
572
- # RFC 3986: https://www.ietf.org/rfc/rfc3986.txt
573
- querySafeChars = ';/?'
574
- pathSafeChars = querySafeChars + ':@&=+$,'
575
- quotedUrlPath = quote(urlPath, safe=pathSafeChars)
576
- quotedQuery = quote(query, safe=querySafeChars)
577
- return urlScheme + schemeSep + quotedUrlPath + querySep + quotedQuery
578
+ parts = urlsplit(url)
579
+
580
+ # RFC 3986 safe characters: https://www.ietf.org/rfc/rfc3986.txt
581
+ pathSafe = "/:@!$&'()*+,;=" # path allows sub-delims, ":" and "@"
582
+ querySafe = "&=:/?@!$'()*+,;[]" # query allows pchar + "/" + "?"
583
+
584
+ quotedPath = quote(parts.path, safe=pathSafe)
585
+ quotedQuery = quote(parts.query, safe=querySafe)
586
+
587
+ return urlunsplit((parts.scheme, parts.netloc, quotedPath, quotedQuery, parts.fragment))
578
588
 
579
589
  @staticmethod
580
590
  def _getFileTimestamp(path: str) -> float:
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.49'
21
- __version_tuple__ = version_tuple = (2, 37, 49)
31
+ __version__ = version = '2.37.51'
32
+ __version_tuple__ = version_tuple = (2, 37, 51)
33
+
34
+ __commit_id__ = commit_id = None
arelle/api/Session.py CHANGED
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
  import logging
9
9
  import threading
10
10
  from types import TracebackType
11
- from typing import Any, BinaryIO
11
+ from typing import Any, BinaryIO, TypeVar
12
12
 
13
13
  from arelle import PackageManager, PluginManager
14
14
  from arelle.CntlrCmdLine import CntlrCmdLine, createCntlrAndPreloadPlugins
@@ -18,6 +18,9 @@ from arelle.RuntimeOptions import RuntimeOptions
18
18
 
19
19
  _session_lock = threading.Lock()
20
20
 
21
+ # typing.Self can be used once Python 3.10 support is dropped.
22
+ Self = TypeVar("Self", bound="Session")
23
+
21
24
 
22
25
  class Session:
23
26
  """
@@ -46,7 +49,7 @@ class Session:
46
49
  "Session objects cannot be shared between threads. Create a new Session instance in each thread."
47
50
  )
48
51
 
49
- def __enter__(self) -> Any:
52
+ def __enter__(self: Self) -> Self:
50
53
  return self
51
54
 
52
55
  def __exit__(
@@ -146,7 +146,6 @@ class PluginValidationDataExtension(PluginData):
146
146
  return self._reportingPeriodContexts
147
147
  contexts = []
148
148
  for context in modelXbrl.contexts.values():
149
- context = cast(ModelContext, context)
150
149
  if context.isInstantPeriod or context.isForeverPeriod:
151
150
  continue # Reporting period contexts can't be instant/forever contexts
152
151
  if len(context.qnameDims) > 0:
@@ -14,12 +14,21 @@ class FormType(Enum):
14
14
  FORM_2_4 = '第二号の四様式'
15
15
  FORM_2_7 = '第二号の七様式'
16
16
  FORM_3 = '第三号様式'
17
+ FORM_4 = '第四号様式'
18
+
19
+ @property
20
+ def isStockReport(self) -> bool:
21
+ return self in STOCK_REPORT_FORMS
17
22
 
18
23
  CORPORATE_FORMS =frozenset([
19
24
  FormType.FORM_2_4,
20
25
  FormType.FORM_2_7,
21
26
  FormType.FORM_3,
22
27
  ])
28
+ STOCK_REPORT_FORMS = frozenset([
29
+ FormType.FORM_3,
30
+ FormType.FORM_4,
31
+ ])
23
32
  qnEdinetManifestInsert = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}insert")
24
33
  qnEdinetManifestInstance = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}instance")
25
34
  qnEdinetManifestItem = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}item")
@@ -28,3 +37,5 @@ qnEdinetManifestList = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}
28
37
  qnEdinetManifestTitle = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}title")
29
38
  qnEdinetManifestTocComposition = qname("{http://disclosure.edinet-fsa.go.jp/2013/manifest}tocComposition")
30
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
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
@@ -54,7 +56,7 @@ class PluginValidationDataExtension(PluginData):
54
56
  def __init__(self, name: str):
55
57
  super().__init__(name)
56
58
  jpcrpEsrNamespace = "http://disclosure.edinet-fsa.go.jp/taxonomy/jpcrp-esr/2024-11-01/jpcrp-esr_cor"
57
- jpcrpNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jpcrp/2024-11-01/jpcrp_cor'
59
+ self.jpcrpNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jpcrp/2024-11-01/jpcrp_cor'
58
60
  jpdeiNamespace = 'http://disclosure.edinet-fsa.go.jp/taxonomy/jpdei/2013-08-31/jpdei_cor'
59
61
  jpigpNamespace = "http://disclosure.edinet-fsa.go.jp/taxonomy/jpigp/2024-11-01/jpigp_cor"
60
62
  jppfsNamespace = "http://disclosure.edinet-fsa.go.jp/taxonomy/jppfs/2024-11-01/jppfs_cor"
@@ -63,11 +65,12 @@ class PluginValidationDataExtension(PluginData):
63
65
  self.assetsIfrsQn = qname(jpigpNamespace, 'AssetsIFRS')
64
66
  self.consolidatedOrNonConsolidatedAxisQn = qname(jppfsNamespace, 'ConsolidatedOrNonConsolidatedAxis')
65
67
  self.documentTypeDeiQn = qname(jpdeiNamespace, 'DocumentTypeDEI')
68
+ self.issuedSharesTotalNumberOfSharesEtcQn = qname(self.jpcrpNamespace, 'IssuedSharesTotalNumberOfSharesEtcTextBlock')
66
69
  self.jpcrpEsrFilingDateCoverPageQn = qname(jpcrpEsrNamespace, 'FilingDateCoverPage')
67
- self.jpcrpFilingDateCoverPageQn = qname(jpcrpNamespace, 'FilingDateCoverPage')
70
+ self.jpcrpFilingDateCoverPageQn = qname(self.jpcrpNamespace, 'FilingDateCoverPage')
68
71
  self.jpspsFilingDateCoverPageQn = qname(jpspsNamespace, 'FilingDateCoverPage')
69
72
  self.nonConsolidatedMemberQn = qname(jppfsNamespace, "NonConsolidatedMember")
70
- self.ratioOfFemaleDirectorsAndOtherOfficersQn = qname(jpcrpNamespace, "RatioOfFemaleDirectorsAndOtherOfficers")
73
+ self.ratioOfFemaleDirectorsAndOtherOfficersQn = qname(self.jpcrpNamespace, "RatioOfFemaleDirectorsAndOtherOfficers")
71
74
 
72
75
  self.contextIdPattern = regex.compile(r'(Prior[1-9]Year|CurrentYear|Prior[1-9]Interim|Interim)(Duration|Instant)')
73
76
 
@@ -103,6 +106,12 @@ class PluginValidationDataExtension(PluginData):
103
106
  return True
104
107
  return False
105
108
 
109
+ def isCorporateReport(self, modelXbrl: ModelXbrl) -> bool:
110
+ return self.jpcrpNamespace in modelXbrl.namespaceDocs
111
+
112
+ def isStockForm(self, modelXbrl: ModelXbrl) -> bool:
113
+ documentTypes = self.getDocumentTypes(modelXbrl)
114
+ return any(documentType == form.value for form in FormType if form.isStockReport for documentType in documentTypes)
106
115
 
107
116
  def getBalanceSheets(self, modelXbrl: ModelXbrl, statement: Statement) -> list[BalanceSheet]:
108
117
  """
@@ -159,6 +168,22 @@ class PluginValidationDataExtension(PluginData):
159
168
  )
160
169
  return balanceSheets
161
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
+
162
187
  @lru_cache(1)
163
188
  def getStatementInstance(self, modelXbrl: ModelXbrl, statement: Statement) -> StatementInstance | None:
164
189
  if statement.roleUri not in modelXbrl.roleTypes:
@@ -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
@@ -4,6 +4,7 @@ See COPYRIGHT.md for copyright information.
4
4
  from __future__ import annotations
5
5
 
6
6
  from collections import defaultdict
7
+ from itertools import chain
7
8
  from typing import Any, Iterable
8
9
 
9
10
  from arelle import XbrlConst
@@ -161,8 +162,9 @@ def rule_EC8054W(
161
162
  EDINET.EC8054W: For any context with ID containing "NonConsolidatedMember",
162
163
  the scenario element within must be set to "NonConsolidatedMember".
163
164
  """
164
- for context in val.modelXbrl.contexts.values():
165
- if pluginData.nonConsolidatedMemberQn.localName not in context.id:
165
+ allContexts = chain(val.modelXbrl.contexts.values(), val.modelXbrl.ixdsUnmappedContexts.values())
166
+ for context in allContexts:
167
+ if context.id is None or pluginData.nonConsolidatedMemberQn.localName not in context.id:
166
168
  continue
167
169
  member = context.dimMemberQname(
168
170
  pluginData.consolidatedOrNonConsolidatedAxisQn,
@@ -281,3 +281,25 @@ def rule_EC8075W(
281
281
  codes='EDINET.EC8075W',
282
282
  msg=_("The percentage of female executives has not been tagged in detail."),
283
283
  )
284
+
285
+
286
+ @validation(
287
+ hook=ValidationHook.XBRL_FINALLY,
288
+ disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
289
+ )
290
+ def rule_EC8076W(
291
+ pluginData: PluginValidationDataExtension,
292
+ val: ValidateXbrl,
293
+ *args: Any,
294
+ **kwargs: Any,
295
+ ) -> Iterable[Validation]:
296
+ """
297
+ EDINET.EC8076W: "Issued Shares, Total Number of Shares, etc. [Text Block]" (IssuedSharesTotalNumberOfSharesEtcTextBlock) is not tagged.
298
+ Applies to forms 3 and 4.
299
+ """
300
+ if pluginData.isStockForm(val.modelXbrl) and pluginData.isCorporateReport(val.modelXbrl):
301
+ if not pluginData.hasValidNonNilFact(val.modelXbrl, pluginData.issuedSharesTotalNumberOfSharesEtcQn):
302
+ yield Validation.warning(
303
+ codes='EDINET.EC8076W',
304
+ msg=_('"Issued Shares, Total Number of Shares, etc. [Text Block]" (IssuedSharesTotalNumberOfSharesEtcTextBlock) is not tagged.'),
305
+ )