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.
- arelle/ModelDocument.py +16 -14
- arelle/ModelInstanceObject.py +1 -1
- arelle/ModelXbrl.py +21 -10
- arelle/WebCache.py +26 -16
- arelle/_version.py +16 -3
- arelle/api/Session.py +5 -2
- arelle/plugin/validate/DBA/PluginValidationDataExtension.py +0 -1
- arelle/plugin/validate/EDINET/Constants.py +11 -0
- arelle/plugin/validate/EDINET/ControllerPluginData.py +40 -29
- arelle/plugin/validate/EDINET/PluginValidationDataExtension.py +30 -5
- arelle/plugin/validate/EDINET/{InstanceType.py → ReportFolderType.py} +11 -15
- arelle/plugin/validate/EDINET/UploadContents.py +20 -5
- arelle/plugin/validate/EDINET/rules/contexts.py +4 -2
- arelle/plugin/validate/EDINET/rules/edinet.py +22 -0
- arelle/plugin/validate/EDINET/rules/gfm.py +20 -29
- arelle/plugin/validate/EDINET/rules/upload.py +230 -57
- arelle/plugin/validate/ESEF/ESEF_2021/ValidateXbrlFinally.py +2 -2
- arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +2 -2
- arelle/plugin/validate/NL/PluginValidationDataExtension.py +1 -1
- arelle/plugin/validate/NL/rules/fr_nl.py +6 -7
- arelle/plugin/validate/UK/ValidateUK.py +31 -66
- {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/METADATA +20 -18
- {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/RECORD +27 -27
- {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/WHEEL +0 -0
- {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/entry_points.txt +0 -0
- {arelle_release-2.37.49.dist-info → arelle_release-2.37.51.dist-info}/licenses/LICENSE.md +0 -0
- {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
|
-
|
|
1615
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1749
|
-
if
|
|
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
|
-
|
|
1753
|
-
if
|
|
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]:
|
arelle/ModelInstanceObject.py
CHANGED
|
@@ -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,
|
|
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
|
|
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) ->
|
|
872
|
-
|
|
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
|
|
881
|
-
|
|
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
|
-
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
# RFC 3986: https://www.ietf.org/rfc/rfc3986.txt
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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__ = [
|
|
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.
|
|
21
|
-
__version_tuple__ = version_tuple = (2, 37,
|
|
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) ->
|
|
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 .
|
|
19
|
-
from .
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
directories = []
|
|
56
|
-
forms = defaultdict(list)
|
|
53
|
+
reports = defaultdict(list)
|
|
54
|
+
uploadPaths = {}
|
|
57
55
|
for path in uploadFilepaths:
|
|
58
|
-
|
|
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(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
|
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) ->
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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 .
|
|
9
|
+
from .ReportFolderType import ReportFolderType
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@dataclass(frozen=True)
|
|
13
13
|
class UploadContents:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
+
)
|