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.
- arelle/CntlrCmdLine.py +3 -0
- arelle/DisclosureSystem.py +2 -0
- arelle/UrlUtil.py +3 -0
- arelle/ValidateFilingText.py +3 -3
- arelle/_version.py +2 -2
- arelle/config/disclosuresystems.xsd +1 -0
- arelle/plugin/validate/DBA/rules/__init__.py +2 -2
- arelle/plugin/validate/EDINET/Constants.py +5 -1
- arelle/plugin/validate/EDINET/ContextRequirement.py +58 -0
- arelle/plugin/validate/EDINET/ControllerPluginData.py +69 -3
- arelle/plugin/validate/EDINET/PluginValidationDataExtension.py +1 -1
- arelle/plugin/validate/EDINET/__init__.py +10 -0
- arelle/plugin/validate/EDINET/resources/config.xml +2 -1
- arelle/plugin/validate/EDINET/rules/contexts.py +205 -2
- arelle/plugin/validate/EDINET/rules/frta.py +2 -2
- arelle/plugin/validate/EDINET/rules/gfm.py +140 -1
- arelle/plugin/validate/EDINET/rules/upload.py +68 -0
- arelle/plugin/validate/ESEF/ESEF_2021/DTS.py +5 -0
- arelle/plugin/validate/ESEF/ESEF_2021/Image.py +2 -2
- arelle/plugin/validate/ESEF/ESEF_2021/ValidateXbrlFinally.py +3 -3
- arelle/plugin/validate/ESEF/ESEF_Current/DTS.py +5 -0
- arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +23 -2
- arelle/plugin/validate/ESEF/resources/authority-validations.json +28 -4
- arelle/utils/validate/ESEFImage.py +3 -3
- {arelle_release-2.37.63.dist-info → arelle_release-2.37.65.dist-info}/METADATA +1 -1
- {arelle_release-2.37.63.dist-info → arelle_release-2.37.65.dist-info}/RECORD +30 -29
- {arelle_release-2.37.63.dist-info → arelle_release-2.37.65.dist-info}/WHEEL +0 -0
- {arelle_release-2.37.63.dist-info → arelle_release-2.37.65.dist-info}/entry_points.txt +0 -0
- {arelle_release-2.37.63.dist-info → arelle_release-2.37.65.dist-info}/licenses/LICENSE.md +0 -0
- {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)
|
arelle/DisclosureSystem.py
CHANGED
|
@@ -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
|
arelle/ValidateFilingText.py
CHANGED
|
@@ -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".*(<|<|<|<)[A-Za-z_]+[A-Za-z0-9_:]*[^>]*(/>|>|>|/>).*", 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
|
|
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
|
|
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.
|
|
32
|
-
__version_tuple__ = version_tuple = (2, 37,
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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'
|
|
235
|
-
"'
|
|
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 = []
|