arelle-release 2.37.44__py3-none-any.whl → 2.37.46__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.
- arelle/Cntlr.py +35 -0
- arelle/ErrorManager.py +306 -0
- arelle/ModelRelationshipSet.py +1 -1
- arelle/ModelXbrl.py +45 -233
- arelle/PluginManager.py +4 -0
- arelle/Validate.py +2 -0
- arelle/XbrlConst.py +1 -0
- arelle/_version.py +2 -2
- arelle/plugin/validate/EDINET/Constants.py +4 -4
- arelle/plugin/validate/EDINET/ControllerPluginData.py +80 -0
- arelle/plugin/validate/EDINET/{FormType.py → InstanceType.py} +8 -8
- arelle/plugin/validate/EDINET/{Manifest.py → ManifestInstance.py} +78 -33
- arelle/plugin/validate/EDINET/PluginValidationDataExtension.py +21 -10
- arelle/plugin/validate/EDINET/ValidationPluginExtension.py +20 -3
- arelle/plugin/validate/EDINET/__init__.py +4 -22
- arelle/plugin/validate/EDINET/rules/edinet.py +1 -1
- arelle/plugin/validate/EDINET/rules/manifests.py +88 -0
- arelle/plugin/validate/EDINET/rules/upload.py +51 -53
- arelle/utils/validate/ESEFImage.py +7 -7
- {arelle_release-2.37.44.dist-info → arelle_release-2.37.46.dist-info}/METADATA +1 -1
- {arelle_release-2.37.44.dist-info → arelle_release-2.37.46.dist-info}/RECORD +25 -22
- {arelle_release-2.37.44.dist-info → arelle_release-2.37.46.dist-info}/WHEEL +0 -0
- {arelle_release-2.37.44.dist-info → arelle_release-2.37.46.dist-info}/entry_points.txt +0 -0
- {arelle_release-2.37.44.dist-info → arelle_release-2.37.46.dist-info}/licenses/LICENSE.md +0 -0
- {arelle_release-2.37.44.dist-info → arelle_release-2.37.46.dist-info}/top_level.txt +0 -0
|
@@ -3,44 +3,59 @@ See COPYRIGHT.md for copyright information.
|
|
|
3
3
|
"""
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import traceback
|
|
6
7
|
import zipfile
|
|
8
|
+
from collections import defaultdict
|
|
7
9
|
from dataclasses import dataclass
|
|
8
10
|
from functools import lru_cache
|
|
9
11
|
from pathlib import Path
|
|
12
|
+
from typing import cast
|
|
10
13
|
|
|
11
14
|
from lxml import etree
|
|
12
15
|
from lxml.etree import _Element
|
|
13
16
|
|
|
14
17
|
from arelle import XbrlConst
|
|
18
|
+
from arelle.Cntlr import Cntlr
|
|
15
19
|
from arelle.FileSource import FileSource
|
|
16
20
|
from arelle.ModelValue import QName, qname
|
|
21
|
+
from arelle.typing import TypeGetText
|
|
17
22
|
from . import Constants
|
|
18
23
|
|
|
24
|
+
_: TypeGetText
|
|
25
|
+
|
|
19
26
|
|
|
20
27
|
@dataclass(frozen=True)
|
|
21
|
-
class
|
|
22
|
-
|
|
28
|
+
class ManifestInstance:
|
|
29
|
+
id: str
|
|
30
|
+
ixbrlFiles: list[Path]
|
|
23
31
|
path: Path
|
|
32
|
+
preferredFilename: str
|
|
24
33
|
titlesByLang: dict[str, str]
|
|
25
34
|
tocItems: list[ManifestTocItem]
|
|
35
|
+
type: str
|
|
26
36
|
|
|
27
37
|
|
|
28
38
|
@dataclass(frozen=True)
|
|
29
39
|
class ManifestTocItem:
|
|
40
|
+
element: _Element
|
|
30
41
|
extrole: str
|
|
31
42
|
childItems: list[ManifestTocItem]
|
|
32
43
|
itemIn: str
|
|
33
44
|
parent: QName | None
|
|
34
45
|
ref: str
|
|
35
46
|
start: QName | None
|
|
47
|
+
end: QName | None
|
|
36
48
|
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
def _invalidManifestError(cntlr: Cntlr, file: str, message: str | None = None) -> None:
|
|
51
|
+
if message is None:
|
|
52
|
+
message = _("Please correct the content.")
|
|
53
|
+
cntlr.error(
|
|
54
|
+
"EDINET.EC5800E.FATAL_ERROR_INVALID_MANIFEST",
|
|
55
|
+
_("The manifest file definition is incorrect <file=%(file)s>. %(message)s"),
|
|
56
|
+
file=file,
|
|
57
|
+
message=message,
|
|
58
|
+
)
|
|
44
59
|
|
|
45
60
|
|
|
46
61
|
def _parseManifestTocItems(parentElt: _Element, parentQName: QName | None) -> list[ManifestTocItem]:
|
|
@@ -51,28 +66,33 @@ def _parseManifestTocItems(parentElt: _Element, parentQName: QName | None) -> li
|
|
|
51
66
|
childParentQName = qname(insertElt.attrib.get("parent"), insertElt.nsmap) if insertElt.attrib.get("parent") else None
|
|
52
67
|
childTocItems.extend(_parseManifestTocItems(insertElt, childParentQName))
|
|
53
68
|
tocItems.append(ManifestTocItem(
|
|
69
|
+
element=itemElt,
|
|
54
70
|
extrole=itemElt.attrib.get("extrole", ""),
|
|
55
71
|
childItems=childTocItems,
|
|
56
72
|
parent=parentQName,
|
|
57
73
|
itemIn=itemElt.attrib.get("in", ""),
|
|
58
74
|
ref=itemElt.attrib.get("ref", ""),
|
|
59
75
|
start=qname(itemElt.attrib.get("start"), itemElt.nsmap) if itemElt.attrib.get("start") else None,
|
|
76
|
+
end=qname(itemElt.attrib.get("end"), itemElt.nsmap) if itemElt.attrib.get("end") else None,
|
|
60
77
|
))
|
|
61
78
|
return tocItems
|
|
62
79
|
|
|
63
80
|
|
|
64
|
-
def _parseManifestDoc(xmlRootElement: _Element, path: Path) ->
|
|
81
|
+
def _parseManifestDoc(xmlRootElement: _Element, path: Path) -> list[ManifestInstance]:
|
|
65
82
|
instances = []
|
|
66
83
|
titlesByLang = {}
|
|
67
84
|
base = path.parent
|
|
68
85
|
tocElts = list(xmlRootElement.iterchildren(tag=Constants.qnEdinetManifestTocComposition.clarkNotation))
|
|
69
|
-
assert len(tocElts) == 1, 'There should be exactly one tocComposition element in the manifest.'
|
|
86
|
+
assert len(tocElts) == 1, _('There should be exactly one tocComposition element in the manifest.')
|
|
70
87
|
for titleElt in tocElts[0].iterchildren(tag=Constants.qnEdinetManifestTitle.clarkNotation):
|
|
71
88
|
lang = titleElt.attrib.get(XbrlConst.qnXmlLang.clarkNotation, "")
|
|
72
89
|
titlesByLang[lang] = titleElt.text.strip() if titleElt.text else ""
|
|
73
90
|
tocItems = _parseManifestTocItems(tocElts[0], None)
|
|
91
|
+
tocItemsByRef = defaultdict(list)
|
|
92
|
+
for tocItem in tocItems:
|
|
93
|
+
tocItemsByRef[tocItem.ref].append(tocItem)
|
|
74
94
|
listElts = list(xmlRootElement.iterchildren(tag=Constants.qnEdinetManifestList.clarkNotation))
|
|
75
|
-
assert len(listElts) == 1, 'There should be exactly one list element in the manifest.'
|
|
95
|
+
assert len(listElts) == 1, _('There should be exactly one list element in the manifest.')
|
|
76
96
|
for instanceElt in listElts[0].iterchildren(tag=Constants.qnEdinetManifestInstance.clarkNotation):
|
|
77
97
|
instanceId = str(instanceElt.attrib.get("id", ""))
|
|
78
98
|
instanceType = str(instanceElt.attrib.get("type", ""))
|
|
@@ -85,40 +105,65 @@ def _parseManifestDoc(xmlRootElement: _Element, path: Path) -> Manifest:
|
|
|
85
105
|
instances.append(ManifestInstance(
|
|
86
106
|
id=instanceId,
|
|
87
107
|
ixbrlFiles=ixbrlFiles,
|
|
108
|
+
path=path,
|
|
88
109
|
preferredFilename=preferredFilename,
|
|
110
|
+
titlesByLang=titlesByLang,
|
|
111
|
+
tocItems=tocItemsByRef.get(instanceId, []),
|
|
89
112
|
type=instanceType,
|
|
90
113
|
))
|
|
91
|
-
return
|
|
92
|
-
instances=instances,
|
|
93
|
-
path=path,
|
|
94
|
-
titlesByLang=titlesByLang,
|
|
95
|
-
tocItems=tocItems,
|
|
96
|
-
)
|
|
114
|
+
return instances
|
|
97
115
|
|
|
98
116
|
|
|
99
117
|
@lru_cache(1)
|
|
100
|
-
def parseManifests(filesource: FileSource) -> list[
|
|
101
|
-
|
|
118
|
+
def parseManifests(filesource: FileSource) -> list[ManifestInstance]:
|
|
119
|
+
assert filesource.cntlr is not None, "FileSource controller must be set before parsing manifests."
|
|
120
|
+
cntlr = filesource.cntlr
|
|
121
|
+
instances: list[ManifestInstance] = []
|
|
102
122
|
if filesource.isArchive:
|
|
103
123
|
if filesource.isTaxonomyPackage:
|
|
104
|
-
return
|
|
124
|
+
return instances
|
|
105
125
|
if filesource.reportPackage is not None:
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"The EDINET plugin only supports archives in .zip format."
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
126
|
+
return instances
|
|
127
|
+
if not isinstance(filesource.fs, zipfile.ZipFile):
|
|
128
|
+
_invalidManifestError(
|
|
129
|
+
cntlr,
|
|
130
|
+
cast(str, filesource.url),
|
|
131
|
+
_("The EDINET plugin only supports archives in .zip format.")
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
for _archiveFile in (filesource.dir or ()):
|
|
135
|
+
if not Path(_archiveFile).stem.startswith('manifest'):
|
|
136
|
+
continue
|
|
137
|
+
try:
|
|
138
|
+
with filesource.fs.open(_archiveFile) as manifestDoc:
|
|
139
|
+
xmlRootElement = etree.fromstring(manifestDoc.read())
|
|
140
|
+
instances.extend(_parseManifestDoc(xmlRootElement, Path(_archiveFile)))
|
|
141
|
+
except AssertionError as err:
|
|
142
|
+
_invalidManifestError(cntlr, str(_archiveFile), str(err))
|
|
143
|
+
except Exception as err:
|
|
144
|
+
_invalidManifestError(cntlr, str(_archiveFile))
|
|
145
|
+
cntlr.addToLog(_("[Exception] Failed to parse manifest \"{0}\": \n{1} \n{2}").format(
|
|
146
|
+
str(_archiveFile),
|
|
147
|
+
err,
|
|
148
|
+
traceback.format_exc()
|
|
149
|
+
))
|
|
115
150
|
elif (dirpath := Path(str(filesource.url))).is_dir():
|
|
116
151
|
for file in dirpath.rglob("*"):
|
|
117
152
|
if not file.is_file():
|
|
118
153
|
continue
|
|
119
154
|
if not file.stem.startswith('manifest'):
|
|
120
155
|
continue
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
156
|
+
try:
|
|
157
|
+
with open(file, 'rb') as manifestDoc:
|
|
158
|
+
xmlRootElement = etree.fromstring(manifestDoc.read())
|
|
159
|
+
instances.extend(_parseManifestDoc(xmlRootElement, file))
|
|
160
|
+
except AssertionError as err:
|
|
161
|
+
_invalidManifestError(cntlr, str(file), str(err))
|
|
162
|
+
except Exception as err:
|
|
163
|
+
_invalidManifestError(cntlr, str(file))
|
|
164
|
+
cntlr.addToLog(_("[Exception] Failed to parse manifest \"{0}\": \n{1} \n{2}").format(
|
|
165
|
+
str(file),
|
|
166
|
+
err,
|
|
167
|
+
traceback.format_exc()
|
|
168
|
+
))
|
|
169
|
+
return instances
|
|
@@ -22,17 +22,19 @@ from arelle.ValidateXbrl import ValidateXbrl
|
|
|
22
22
|
from arelle.XmlValidate import VALID
|
|
23
23
|
from arelle.typing import TypeGetText
|
|
24
24
|
from arelle.utils.PluginData import PluginData
|
|
25
|
-
from .FormType import FormType
|
|
26
25
|
from .Constants import CORPORATE_FORMS
|
|
26
|
+
from .ControllerPluginData import ControllerPluginData
|
|
27
|
+
from .InstanceType import InstanceType
|
|
28
|
+
from .ManifestInstance import ManifestInstance
|
|
27
29
|
|
|
28
30
|
_: TypeGetText
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
@dataclass(frozen=True)
|
|
32
34
|
class UploadContents:
|
|
33
|
-
amendmentPaths: dict[
|
|
35
|
+
amendmentPaths: dict[InstanceType, frozenset[Path]]
|
|
34
36
|
directories: frozenset[Path]
|
|
35
|
-
|
|
37
|
+
instances: dict[InstanceType, frozenset[Path]]
|
|
36
38
|
unknownPaths: frozenset[Path]
|
|
37
39
|
|
|
38
40
|
|
|
@@ -142,6 +144,15 @@ class PluginValidationDataExtension(PluginData):
|
|
|
142
144
|
if isinstance(elt, (ModelObject, LinkPrototype))
|
|
143
145
|
]
|
|
144
146
|
|
|
147
|
+
@lru_cache(1)
|
|
148
|
+
def getManifestInstance(self, modelXbrl: ModelXbrl) -> ManifestInstance | None:
|
|
149
|
+
controllerPluginData = ControllerPluginData.get(modelXbrl.modelManager.cntlr, self.name)
|
|
150
|
+
return controllerPluginData.matchManifestInstance(modelXbrl.ixdsDocUrls)
|
|
151
|
+
|
|
152
|
+
@lru_cache(1)
|
|
153
|
+
def getManifestInstances(self, modelXbrl: ModelXbrl) -> list[ManifestInstance]:
|
|
154
|
+
controllerPluginData = ControllerPluginData.get(modelXbrl.modelManager.cntlr, self.name)
|
|
155
|
+
return controllerPluginData.getManifestInstances()
|
|
145
156
|
|
|
146
157
|
def getUploadFileSizes(self, modelXbrl: ModelXbrl) -> dict[Path, int]:
|
|
147
158
|
"""
|
|
@@ -172,14 +183,14 @@ class PluginValidationDataExtension(PluginData):
|
|
|
172
183
|
if parents[0] == 'XBRL':
|
|
173
184
|
if len(parents) > 1:
|
|
174
185
|
formName = parents[1]
|
|
175
|
-
|
|
176
|
-
if
|
|
177
|
-
forms[
|
|
186
|
+
instanceType = InstanceType.parse(formName)
|
|
187
|
+
if instanceType is not None:
|
|
188
|
+
forms[instanceType].append(path)
|
|
178
189
|
continue
|
|
179
190
|
formName = parents[0]
|
|
180
|
-
|
|
181
|
-
if
|
|
182
|
-
amendmentPaths[
|
|
191
|
+
instanceType = InstanceType.parse(formName)
|
|
192
|
+
if instanceType is not None:
|
|
193
|
+
amendmentPaths[instanceType].append(path)
|
|
183
194
|
continue
|
|
184
195
|
if len(path.suffix) == 0:
|
|
185
196
|
directories.append(path)
|
|
@@ -188,7 +199,7 @@ class PluginValidationDataExtension(PluginData):
|
|
|
188
199
|
return UploadContents(
|
|
189
200
|
amendmentPaths={k: frozenset(v) for k, v in amendmentPaths.items() if len(v) > 0},
|
|
190
201
|
directories=frozenset(directories),
|
|
191
|
-
|
|
202
|
+
instances={k: frozenset(v) for k, v in forms.items() if len(v) > 0},
|
|
192
203
|
unknownPaths=frozenset(unknownPaths)
|
|
193
204
|
)
|
|
194
205
|
|
|
@@ -5,16 +5,36 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
+
from arelle.FileSource import FileSource
|
|
8
9
|
from arelle.ValidateXbrl import ValidateXbrl
|
|
9
10
|
from arelle.typing import TypeGetText
|
|
10
11
|
from arelle.utils.validate.ValidationPlugin import ValidationPlugin
|
|
12
|
+
from .ControllerPluginData import ControllerPluginData
|
|
11
13
|
from .DisclosureSystems import DISCLOSURE_SYSTEM_EDINET
|
|
14
|
+
from .ManifestInstance import parseManifests
|
|
12
15
|
from .PluginValidationDataExtension import PluginValidationDataExtension
|
|
13
16
|
|
|
14
17
|
_: TypeGetText
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
class ValidationPluginExtension(ValidationPlugin):
|
|
21
|
+
|
|
22
|
+
def fileSourceEntrypointFiles(self, filesource: FileSource, *args: Any, **kwargs: Any) -> list[dict[str, Any]] | None:
|
|
23
|
+
instances = parseManifests(filesource)
|
|
24
|
+
if len(instances) == 0:
|
|
25
|
+
return None
|
|
26
|
+
assert filesource.cntlr is not None
|
|
27
|
+
pluginData = ControllerPluginData.get(filesource.cntlr, self.name)
|
|
28
|
+
entrypointFiles = []
|
|
29
|
+
for instance in instances:
|
|
30
|
+
pluginData.addManifestInstance(instance)
|
|
31
|
+
entrypoints = []
|
|
32
|
+
for ixbrlFile in instance.ixbrlFiles:
|
|
33
|
+
filesource.select(str(ixbrlFile))
|
|
34
|
+
entrypoints.append({"file": filesource.url})
|
|
35
|
+
entrypointFiles.append({'ixds': entrypoints, 'id': instance.id})
|
|
36
|
+
return entrypointFiles
|
|
37
|
+
|
|
18
38
|
def newPluginData(self, validateXbrl: ValidateXbrl) -> PluginValidationDataExtension:
|
|
19
39
|
disclosureSystem = validateXbrl.disclosureSystem.name
|
|
20
40
|
if disclosureSystem == DISCLOSURE_SYSTEM_EDINET:
|
|
@@ -24,6 +44,3 @@ class ValidationPluginExtension(ValidationPlugin):
|
|
|
24
44
|
return PluginValidationDataExtension(
|
|
25
45
|
self.name,
|
|
26
46
|
)
|
|
27
|
-
|
|
28
|
-
def modelXbrlLoadComplete(self, *args: Any, **kwargs: Any) -> None:
|
|
29
|
-
return None
|
|
@@ -5,17 +5,14 @@ See COPYRIGHT.md for copyright information.
|
|
|
5
5
|
"""
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from collections import defaultdict
|
|
9
8
|
from pathlib import Path
|
|
10
9
|
from typing import Any
|
|
11
10
|
|
|
12
|
-
from arelle.FileSource import FileSource
|
|
13
11
|
from arelle.ModelXbrl import ModelXbrl
|
|
14
12
|
from arelle.Version import authorLabel, copyrightLabel
|
|
15
13
|
from . import Constants
|
|
16
|
-
from .Manifest import Manifest, ManifestInstance, parseManifests
|
|
17
14
|
from .ValidationPluginExtension import ValidationPluginExtension
|
|
18
|
-
from .rules import contexts, edinet, frta, gfm, upload
|
|
15
|
+
from .rules import contexts, edinet, frta, gfm, manifests, upload
|
|
19
16
|
|
|
20
17
|
PLUGIN_NAME = "Validate EDINET"
|
|
21
18
|
DISCLOSURE_SYSTEM_VALIDATION_TYPE = "EDINET"
|
|
@@ -38,6 +35,7 @@ validationPlugin = ValidationPluginExtension(
|
|
|
38
35
|
edinet,
|
|
39
36
|
frta,
|
|
40
37
|
gfm,
|
|
38
|
+
manifests,
|
|
41
39
|
upload,
|
|
42
40
|
],
|
|
43
41
|
)
|
|
@@ -51,19 +49,8 @@ def disclosureSystemConfigURL(*args: Any, **kwargs: Any) -> str:
|
|
|
51
49
|
return validationPlugin.disclosureSystemConfigURL
|
|
52
50
|
|
|
53
51
|
|
|
54
|
-
def fileSourceEntrypointFiles(
|
|
55
|
-
|
|
56
|
-
if len(manifests) == 0:
|
|
57
|
-
return None
|
|
58
|
-
entrypointFiles = []
|
|
59
|
-
for manifest in manifests:
|
|
60
|
-
for instance in manifest.instances:
|
|
61
|
-
entrypoints = []
|
|
62
|
-
for ixbrlFile in instance.ixbrlFiles:
|
|
63
|
-
filesource.select(str(ixbrlFile))
|
|
64
|
-
entrypoints.append({"file": filesource.url})
|
|
65
|
-
entrypointFiles.append({'ixds': entrypoints})
|
|
66
|
-
return entrypointFiles
|
|
52
|
+
def fileSourceEntrypointFiles(*args: Any, **kwargs: Any) -> list[dict[str, Any]] | None:
|
|
53
|
+
return validationPlugin.fileSourceEntrypointFiles(*args, **kwargs)
|
|
67
54
|
|
|
68
55
|
|
|
69
56
|
def loggingSeverityReleveler(modelXbrl: ModelXbrl, level: str, messageCode: str, args: Any, **kwargs: Any) -> tuple[str | None, str | None]:
|
|
@@ -72,10 +59,6 @@ def loggingSeverityReleveler(modelXbrl: ModelXbrl, level: str, messageCode: str,
|
|
|
72
59
|
return level, messageCode
|
|
73
60
|
|
|
74
61
|
|
|
75
|
-
def modelXbrlLoadComplete(*args: Any, **kwargs: Any) -> None:
|
|
76
|
-
return validationPlugin.modelXbrlLoadComplete(*args, **kwargs)
|
|
77
|
-
|
|
78
|
-
|
|
79
62
|
def validateFinally(*args: Any, **kwargs: Any) -> None:
|
|
80
63
|
return validationPlugin.validateFinally(*args, **kwargs)
|
|
81
64
|
|
|
@@ -96,7 +79,6 @@ __pluginInfo__ = {
|
|
|
96
79
|
"DisclosureSystem.ConfigURL": disclosureSystemConfigURL,
|
|
97
80
|
"FileSource.EntrypointFiles": fileSourceEntrypointFiles,
|
|
98
81
|
"Logging.Severity.Releveler": loggingSeverityReleveler,
|
|
99
|
-
"ModelXbrl.LoadComplete": modelXbrlLoadComplete,
|
|
100
82
|
"Validate.XBRL.Finally": validateXbrlFinally,
|
|
101
83
|
"ValidateFormula.Finished": validateFinally,
|
|
102
84
|
}
|
|
@@ -154,7 +154,7 @@ def rule_EC8027W(
|
|
|
154
154
|
continue
|
|
155
155
|
arcroles = linkbaseType.getArcroles()
|
|
156
156
|
relSet = val.modelXbrl.relationshipSet(tuple(arcroles), roleType.roleURI)
|
|
157
|
-
relSetFrom = relSet.
|
|
157
|
+
relSetFrom = relSet.fromModelObjects()
|
|
158
158
|
rootConcepts = relSet.rootConcepts
|
|
159
159
|
if len(rootConcepts) < 2:
|
|
160
160
|
continue
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
See COPYRIGHT.md for copyright information.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, Iterable
|
|
7
|
+
|
|
8
|
+
from arelle.LinkbaseType import LinkbaseType
|
|
9
|
+
from arelle.ModelXbrl import ModelXbrl
|
|
10
|
+
from arelle.ValidateXbrl import ValidateXbrl
|
|
11
|
+
from arelle.typing import TypeGetText
|
|
12
|
+
from arelle.utils.PluginHooks import ValidationHook
|
|
13
|
+
from arelle.utils.validate.Decorator import validation
|
|
14
|
+
from arelle.utils.validate.Validation import Validation
|
|
15
|
+
from ..DisclosureSystems import (DISCLOSURE_SYSTEM_EDINET)
|
|
16
|
+
from ..ManifestInstance import ManifestTocItem
|
|
17
|
+
from ..PluginValidationDataExtension import PluginValidationDataExtension
|
|
18
|
+
|
|
19
|
+
_: TypeGetText
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _validateTocItem(modelXbrl: ModelXbrl, tocItem: ManifestTocItem, instanceId: str) -> Iterable[Validation]:
|
|
23
|
+
for childTocItem in tocItem.childItems:
|
|
24
|
+
yield from _validateTocItem(modelXbrl, childTocItem, instanceId)
|
|
25
|
+
relSet = modelXbrl.relationshipSet(tuple(LinkbaseType.PRESENTATION.getArcroles()), tocItem.extrole)
|
|
26
|
+
if len(relSet.fromModelObjects()) == 0:
|
|
27
|
+
yield Validation.error(
|
|
28
|
+
codes="EDINET.EC5800E.FATAL_ERROR_TOC_TREE_NOT_DEFINED",
|
|
29
|
+
msg=_("A table of contents item specified an extended link role "
|
|
30
|
+
"that does not exist in the presentation linkbase. "),
|
|
31
|
+
modelObject=tocItem.element,
|
|
32
|
+
)
|
|
33
|
+
for conceptQname in {tocItem.parent, tocItem.start, tocItem.end}:
|
|
34
|
+
if conceptQname is None:
|
|
35
|
+
continue
|
|
36
|
+
concept = modelXbrl.qnameConcepts.get(conceptQname)
|
|
37
|
+
if concept is None or not relSet.contains(concept):
|
|
38
|
+
yield Validation.error(
|
|
39
|
+
codes="EDINET.EC5800E.ERROR_ELEMENT_NOT_DEFINED_IN_EXTENDED_LINK_ROLE",
|
|
40
|
+
msg=_("A table of contents item specified an extended link role "
|
|
41
|
+
"that does not contain the element specified by the parent "
|
|
42
|
+
"attribute of the insert element or the start or end "
|
|
43
|
+
"attribute of the item element. Check the extended link "
|
|
44
|
+
"role and correct it."),
|
|
45
|
+
modelObject=tocItem.element,
|
|
46
|
+
)
|
|
47
|
+
if tocItem.end is not None:
|
|
48
|
+
if tocItem.start is None or \
|
|
49
|
+
not relSet.isRelated(tocItem.start, "child", tocItem.end):
|
|
50
|
+
yield Validation.error(
|
|
51
|
+
codes="EDINET.EC5800E.ERROR_ENDING_ELEMENT_NOT_DEFINED_UNDER_STARTING_ELEMENT",
|
|
52
|
+
msg=_("A table of contents item specified an end element that "
|
|
53
|
+
"is not a descendant of the start element within the "
|
|
54
|
+
"specified extended link role. Please check and correct "
|
|
55
|
+
"the table of contents items."),
|
|
56
|
+
modelObject=tocItem.element,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@validation(
|
|
61
|
+
hook=ValidationHook.XBRL_FINALLY,
|
|
62
|
+
disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
|
|
63
|
+
)
|
|
64
|
+
def rule_EC5800E(
|
|
65
|
+
pluginData: PluginValidationDataExtension,
|
|
66
|
+
val: ValidateXbrl,
|
|
67
|
+
*args: Any,
|
|
68
|
+
**kwargs: Any,
|
|
69
|
+
) -> Iterable[Validation]:
|
|
70
|
+
"""
|
|
71
|
+
EDINET.EC5800E.FATAL_ERROR_TOC_TREE_NOT_DEFINED:
|
|
72
|
+
A table of contents item specified an extended link role that does not
|
|
73
|
+
exist in the presentation linkbase.
|
|
74
|
+
EDINET.EC5800E.ERROR_ELEMENT_NOT_DEFINED_IN_EXTENDED_LINK_ROLE:
|
|
75
|
+
A table of contents item specified an extended link role that does not
|
|
76
|
+
contain the element specified by the parent attribute of the insert
|
|
77
|
+
element or the start or end attribute of the item element. Check the
|
|
78
|
+
extended link role and correct it.
|
|
79
|
+
EDINET.EC5800E.ERROR_ENDING_ELEMENT_NOT_DEFINED_UNDER_STARTING_ELEMENT:
|
|
80
|
+
A table of contents item specified an end element that is not a descendant
|
|
81
|
+
of the start element within the specified extended link role. Please check
|
|
82
|
+
and correct the table of contents items.
|
|
83
|
+
"""
|
|
84
|
+
instance = pluginData.getManifestInstance(val.modelXbrl)
|
|
85
|
+
if instance is None:
|
|
86
|
+
return
|
|
87
|
+
for tocItem in instance.tocItems:
|
|
88
|
+
yield from _validateTocItem(val.modelXbrl, tocItem, instance.id)
|
|
@@ -5,9 +5,8 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import re
|
|
7
7
|
from collections import defaultdict
|
|
8
|
-
from collections.abc import Iterable
|
|
9
8
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
9
|
+
from typing import Any, Iterable
|
|
11
10
|
|
|
12
11
|
from arelle.ValidateXbrl import ValidateXbrl
|
|
13
12
|
from arelle.typing import TypeGetText
|
|
@@ -15,8 +14,7 @@ from arelle.utils.PluginHooks import ValidationHook
|
|
|
15
14
|
from arelle.utils.validate.Decorator import validation
|
|
16
15
|
from arelle.utils.validate.Validation import Validation
|
|
17
16
|
from ..DisclosureSystems import (DISCLOSURE_SYSTEM_EDINET)
|
|
18
|
-
from ..
|
|
19
|
-
from ..Manifest import parseManifests
|
|
17
|
+
from ..InstanceType import InstanceType, HTML_EXTENSIONS, IMAGE_EXTENSIONS
|
|
20
18
|
from ..PluginValidationDataExtension import PluginValidationDataExtension
|
|
21
19
|
|
|
22
20
|
_: TypeGetText
|
|
@@ -162,19 +160,19 @@ def rule_EC0130E(
|
|
|
162
160
|
return
|
|
163
161
|
uploadContents = pluginData.getUploadContents(val.modelXbrl)
|
|
164
162
|
checks = []
|
|
165
|
-
for
|
|
163
|
+
for instanceType, amendmentPaths in uploadContents.amendmentPaths.items():
|
|
166
164
|
for amendmentPath in amendmentPaths:
|
|
167
|
-
isSubdirectory = amendmentPath.parent.name !=
|
|
168
|
-
checks.append((amendmentPath, True,
|
|
169
|
-
for
|
|
165
|
+
isSubdirectory = amendmentPath.parent.name != instanceType.value
|
|
166
|
+
checks.append((amendmentPath, True, instanceType, isSubdirectory))
|
|
167
|
+
for instanceType, formPaths in uploadContents.instances.items():
|
|
170
168
|
for amendmentPath in formPaths:
|
|
171
|
-
isSubdirectory = amendmentPath.parent.name !=
|
|
172
|
-
checks.append((amendmentPath, False,
|
|
173
|
-
for path, isAmendment,
|
|
169
|
+
isSubdirectory = amendmentPath.parent.name != instanceType.value
|
|
170
|
+
checks.append((amendmentPath, False, instanceType, isSubdirectory))
|
|
171
|
+
for path, isAmendment, instanceType, isSubdirectory in checks:
|
|
174
172
|
ext = path.suffix
|
|
175
173
|
if len(ext) == 0:
|
|
176
174
|
continue
|
|
177
|
-
validExtensions =
|
|
175
|
+
validExtensions = instanceType.getValidExtensions(isAmendment, isSubdirectory)
|
|
178
176
|
if validExtensions is None:
|
|
179
177
|
continue
|
|
180
178
|
if ext not in validExtensions:
|
|
@@ -209,17 +207,17 @@ def rule_EC0132E(
|
|
|
209
207
|
if not pluginData.shouldValidateUpload(val):
|
|
210
208
|
return
|
|
211
209
|
uploadContents = pluginData.getUploadContents(val.modelXbrl)
|
|
212
|
-
for
|
|
213
|
-
if
|
|
210
|
+
for instanceType in (InstanceType.AUDIT_DOC, InstanceType.PRIVATE_DOC, InstanceType.PUBLIC_DOC):
|
|
211
|
+
if instanceType not in uploadContents.instances:
|
|
214
212
|
continue
|
|
215
|
-
if
|
|
213
|
+
if instanceType.manifestPath in uploadContents.instances.get(instanceType, []):
|
|
216
214
|
continue
|
|
217
215
|
yield Validation.error(
|
|
218
216
|
codes='EDINET.EC0132E',
|
|
219
217
|
msg=_("'%(expectedManifestName)s' does not exist in '%(expectedManifestDirectory)s'. "
|
|
220
218
|
"Please store the manifest file (or cover file) directly under the relevant folder and upload it again. "),
|
|
221
|
-
expectedManifestName=
|
|
222
|
-
expectedManifestDirectory=str(
|
|
219
|
+
expectedManifestName=instanceType.manifestPath.name,
|
|
220
|
+
expectedManifestDirectory=str(instanceType.manifestPath.parent),
|
|
223
221
|
)
|
|
224
222
|
|
|
225
223
|
|
|
@@ -478,40 +476,40 @@ def rule_manifest_preferredFilename(
|
|
|
478
476
|
"""
|
|
479
477
|
if not pluginData.shouldValidateUpload(val):
|
|
480
478
|
return
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
for
|
|
479
|
+
instances = pluginData.getManifestInstances(val.modelXbrl)
|
|
480
|
+
preferredFilenames: dict[Path, set[str]] = defaultdict(set)
|
|
481
|
+
duplicateFilenames = defaultdict(set)
|
|
482
|
+
for instance in instances:
|
|
483
|
+
if len(instance.preferredFilename) == 0:
|
|
484
|
+
yield Validation.error(
|
|
485
|
+
codes='EDINET.EC5804E',
|
|
486
|
+
msg=_("The instance file name is not set. "
|
|
487
|
+
"Set the instance name as the preferredFilename attribute value "
|
|
488
|
+
"of the instance element in the manifest file. (manifest: '%(manifest)s', id: %(id)s)"),
|
|
489
|
+
manifest=str(instance.path),
|
|
490
|
+
id=instance.id,
|
|
491
|
+
)
|
|
492
|
+
continue
|
|
493
|
+
preferredFilename = Path(instance.preferredFilename)
|
|
494
|
+
if preferredFilename.suffix != '.xbrl':
|
|
495
|
+
yield Validation.error(
|
|
496
|
+
codes='EDINET.EC5805E',
|
|
497
|
+
msg=_("The instance file extension is not '.xbrl'. "
|
|
498
|
+
"File name: '%(preferredFilename)s'. "
|
|
499
|
+
"Please change the extension of the instance name set in the "
|
|
500
|
+
"preferredFilename attribute value of the instance element in "
|
|
501
|
+
"the manifest file to '.xbrl'. (manifest: '%(manifest)s', id: %(id)s)"),
|
|
502
|
+
preferredFilename=instance.preferredFilename,
|
|
503
|
+
manifest=str(instance.path),
|
|
504
|
+
id=instance.id,
|
|
505
|
+
)
|
|
506
|
+
continue
|
|
507
|
+
if instance.preferredFilename in preferredFilenames[instance.path]:
|
|
508
|
+
duplicateFilenames[instance.path].add(instance.preferredFilename)
|
|
509
|
+
continue
|
|
510
|
+
preferredFilenames[instance.path].add(instance.preferredFilename)
|
|
511
|
+
for path, filenames in duplicateFilenames.items():
|
|
512
|
+
for filename in filenames:
|
|
515
513
|
yield Validation.error(
|
|
516
514
|
codes='EDINET.EC5806E',
|
|
517
515
|
msg=_("The same instance file name is set multiple times. "
|
|
@@ -519,6 +517,6 @@ def rule_manifest_preferredFilename(
|
|
|
519
517
|
"The preferredFilename attribute value of the instance "
|
|
520
518
|
"element in the manifest file must be unique within the "
|
|
521
519
|
"same file. (manifest: '%(manifest)s')"),
|
|
522
|
-
manifest=str(
|
|
523
|
-
preferredFilename=
|
|
520
|
+
manifest=str(path),
|
|
521
|
+
preferredFilename=filename,
|
|
524
522
|
)
|
|
@@ -257,22 +257,22 @@ def checkSVGContentElt(
|
|
|
257
257
|
) -> Iterable[Validation]:
|
|
258
258
|
guidance = params.contentOtherThanXHTMLGuidance
|
|
259
259
|
rootElement = True
|
|
260
|
-
for
|
|
260
|
+
for childElt in elt.iter():
|
|
261
261
|
if rootElement:
|
|
262
|
-
if
|
|
262
|
+
if childElt.tag != "{http://www.w3.org/2000/svg}svg":
|
|
263
263
|
yield Validation.error((f"{guidance}.imageFileCannotBeLoaded", "NL.NL-KVK.3.5.1.imageFileCannotBeLoaded"),
|
|
264
264
|
_("Image SVG has root element which is not svg"),
|
|
265
265
|
modelObject=imgElts)
|
|
266
266
|
rootElement = False
|
|
267
267
|
# Comments, processing instructions, and maybe other special constructs don't have string tags.
|
|
268
|
-
if not isinstance(
|
|
268
|
+
if not isinstance(childElt.tag, str):
|
|
269
269
|
continue
|
|
270
|
-
eltTag =
|
|
270
|
+
eltTag = childElt.tag.rpartition("}")[2] # strip namespace
|
|
271
271
|
if eltTag == "image":
|
|
272
|
-
imgElts = [*imgElts,
|
|
273
|
-
yield from validateImage(baseUrl, getHref(
|
|
272
|
+
imgElts = [*imgElts, childElt]
|
|
273
|
+
yield from validateImage(baseUrl, getHref(childElt), modelXbrl, val, imgElts, "", params)
|
|
274
274
|
if eltTag in ("object", "script", "audio", "foreignObject", "iframe", "image", "use", "video"):
|
|
275
|
-
href =
|
|
275
|
+
href = childElt.get("href","")
|
|
276
276
|
if eltTag in ("object", "script") or "javascript:" in href:
|
|
277
277
|
yield Validation.error((f"{guidance}.executableCodePresent", "NL.NL-KVK.3.5.1.1.executableCodePresent"),
|
|
278
278
|
_("Inline XBRL images MUST NOT contain executable code: %(element)s"),
|