arelle-release 2.37.55__py3-none-any.whl → 2.37.57__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 +1 -0
- arelle/ErrorManager.py +3 -0
- arelle/ValidateDuplicateFacts.py +1 -1
- arelle/XbrlConst.py +1 -0
- arelle/_version.py +2 -2
- arelle/plugin/validate/EDINET/Constants.py +0 -18
- arelle/plugin/validate/EDINET/ControllerPluginData.py +43 -20
- arelle/plugin/validate/EDINET/CoverPageRequirements.py +118 -0
- arelle/plugin/validate/EDINET/FilingFormat.py +253 -0
- arelle/plugin/validate/EDINET/FormType.py +81 -0
- arelle/plugin/validate/EDINET/PluginValidationDataExtension.py +146 -34
- arelle/plugin/validate/EDINET/UploadContents.py +18 -2
- arelle/plugin/validate/EDINET/ValidationPluginExtension.py +1 -0
- arelle/plugin/validate/EDINET/resources/cover-page-requirements.csv +27 -0
- arelle/plugin/validate/EDINET/rules/edinet.py +1 -1
- arelle/plugin/validate/EDINET/rules/gfm.py +216 -0
- arelle/plugin/validate/EDINET/rules/upload.py +295 -35
- arelle/plugin/validate/ROS/PluginValidationDataExtension.py +2 -0
- arelle/plugin/validate/ROS/ValidationPluginExtension.py +1 -0
- arelle/plugin/validate/ROS/rules/ros.py +39 -9
- {arelle_release-2.37.55.dist-info → arelle_release-2.37.57.dist-info}/METADATA +1 -1
- {arelle_release-2.37.55.dist-info → arelle_release-2.37.57.dist-info}/RECORD +26 -22
- {arelle_release-2.37.55.dist-info → arelle_release-2.37.57.dist-info}/WHEEL +0 -0
- {arelle_release-2.37.55.dist-info → arelle_release-2.37.57.dist-info}/entry_points.txt +0 -0
- {arelle_release-2.37.55.dist-info → arelle_release-2.37.57.dist-info}/licenses/LICENSE.md +0 -0
- {arelle_release-2.37.55.dist-info → arelle_release-2.37.57.dist-info}/top_level.txt +0 -0
|
@@ -6,20 +6,24 @@ from __future__ import annotations
|
|
|
6
6
|
import re
|
|
7
7
|
from collections import defaultdict
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Any, Iterable, TYPE_CHECKING
|
|
9
|
+
from typing import Any, Iterable, TYPE_CHECKING, cast
|
|
10
10
|
|
|
11
11
|
import regex
|
|
12
12
|
|
|
13
|
-
from arelle import UrlUtil
|
|
13
|
+
from arelle import UrlUtil, XbrlConst
|
|
14
14
|
from arelle.Cntlr import Cntlr
|
|
15
15
|
from arelle.FileSource import FileSource
|
|
16
|
+
from arelle.ModelInstanceObject import ModelFact
|
|
16
17
|
from arelle.ValidateXbrl import ValidateXbrl
|
|
18
|
+
from arelle.XmlValidateConst import VALID
|
|
17
19
|
from arelle.typing import TypeGetText
|
|
18
20
|
from arelle.utils.PluginHooks import ValidationHook
|
|
19
21
|
from arelle.utils.validate.Decorator import validation
|
|
20
22
|
from arelle.utils.validate.Validation import Validation
|
|
21
23
|
from .. import Constants
|
|
24
|
+
from ..CoverPageRequirements import CoverPageItemStatus
|
|
22
25
|
from ..DisclosureSystems import (DISCLOSURE_SYSTEM_EDINET)
|
|
26
|
+
from ..FilingFormat import FILING_FORMATS
|
|
23
27
|
from ..ReportFolderType import ReportFolderType, HTML_EXTENSIONS, IMAGE_EXTENSIONS
|
|
24
28
|
from ..PluginValidationDataExtension import PluginValidationDataExtension
|
|
25
29
|
|
|
@@ -122,8 +126,10 @@ def rule_EC0100E(
|
|
|
122
126
|
For this implementation, we will allow all directories that may be valid for at least one submission type.
|
|
123
127
|
This allows for a false-negative outcome when a non-correction submission has a correction-only root directory.
|
|
124
128
|
"""
|
|
125
|
-
uploadContents = pluginData.getUploadContents(
|
|
126
|
-
|
|
129
|
+
uploadContents = pluginData.getUploadContents()
|
|
130
|
+
if uploadContents is None:
|
|
131
|
+
return
|
|
132
|
+
for path, pathInfo in uploadContents.uploadPathsByPath.items():
|
|
127
133
|
if pathInfo.isRoot and path.name not in ALLOWED_ROOT_FOLDERS:
|
|
128
134
|
yield Validation.error(
|
|
129
135
|
codes='EDINET.EC0100E',
|
|
@@ -153,8 +159,8 @@ def rule_EC0124E_EC0187E(
|
|
|
153
159
|
"""
|
|
154
160
|
uploadFilepaths = pluginData.getUploadFilepaths(fileSource)
|
|
155
161
|
emptyDirectories = []
|
|
156
|
-
for path in uploadFilepaths:
|
|
157
|
-
if
|
|
162
|
+
for path, zipPath in uploadFilepaths.items():
|
|
163
|
+
if not zipPath.is_dir():
|
|
158
164
|
continue
|
|
159
165
|
if not any(path in p.parents for p in uploadFilepaths):
|
|
160
166
|
emptyDirectories.append(path)
|
|
@@ -227,8 +233,10 @@ def rule_EC0130E(
|
|
|
227
233
|
"""
|
|
228
234
|
EDINET.EC0130E: File extensions must match the file extensions allowed in Figure 2-1-3 and Figure 2-1-5.
|
|
229
235
|
"""
|
|
230
|
-
uploadContents = pluginData.getUploadContents(
|
|
231
|
-
|
|
236
|
+
uploadContents = pluginData.getUploadContents()
|
|
237
|
+
if uploadContents is None:
|
|
238
|
+
return
|
|
239
|
+
for path, pathInfo in uploadContents.uploadPathsByPath.items():
|
|
232
240
|
if pathInfo.reportFolderType is None or pathInfo.isDirectory:
|
|
233
241
|
continue
|
|
234
242
|
validExtensions = pathInfo.reportFolderType.getValidExtensions(pathInfo.isCorrection, pathInfo.isSubdirectory)
|
|
@@ -265,7 +273,9 @@ def rule_EC0132E(
|
|
|
265
273
|
"""
|
|
266
274
|
EDINET.EC0132E: Store the manifest file directly under the relevant folder.
|
|
267
275
|
"""
|
|
268
|
-
uploadContents = pluginData.getUploadContents(
|
|
276
|
+
uploadContents = pluginData.getUploadContents()
|
|
277
|
+
if uploadContents is None:
|
|
278
|
+
return
|
|
269
279
|
for reportFolderType, paths in uploadContents.reports.items():
|
|
270
280
|
if reportFolderType.isAttachment:
|
|
271
281
|
continue
|
|
@@ -353,8 +363,10 @@ def rule_EC0192E(
|
|
|
353
363
|
PublicDoc cover file. Please delete the cover file from PrivateDoc and upload
|
|
354
364
|
it again.
|
|
355
365
|
"""
|
|
356
|
-
uploadContents = pluginData.getUploadContents(
|
|
357
|
-
|
|
366
|
+
uploadContents = pluginData.getUploadContents()
|
|
367
|
+
if uploadContents is None:
|
|
368
|
+
return
|
|
369
|
+
for path, pathInfo in uploadContents.uploadPathsByPath.items():
|
|
358
370
|
if not pathInfo.isCoverPage:
|
|
359
371
|
continue
|
|
360
372
|
# Only applies to PrivateDoc correction reports
|
|
@@ -383,8 +395,8 @@ def rule_EC0198E(
|
|
|
383
395
|
"""
|
|
384
396
|
fileCounts: dict[Path, int] = defaultdict(int)
|
|
385
397
|
uploadFilepaths = pluginData.getUploadFilepaths(fileSource)
|
|
386
|
-
for path in uploadFilepaths:
|
|
387
|
-
if
|
|
398
|
+
for path, zipPath in uploadFilepaths.items():
|
|
399
|
+
if zipPath.is_dir():
|
|
388
400
|
continue
|
|
389
401
|
for directory in FILE_COUNT_LIMITS.keys():
|
|
390
402
|
if directory in path.parents:
|
|
@@ -421,10 +433,12 @@ def rule_EC0233E(
|
|
|
421
433
|
NOTE: This includes files in subdirectories. For example, PublicDoc/00000000_images/image.png
|
|
422
434
|
comes before PublicDoc/0000000_header_*.htm
|
|
423
435
|
"""
|
|
424
|
-
uploadContents = pluginData.getUploadContents(
|
|
436
|
+
uploadContents = pluginData.getUploadContents()
|
|
437
|
+
if uploadContents is None:
|
|
438
|
+
return
|
|
425
439
|
directories = defaultdict(list)
|
|
426
440
|
for path in uploadContents.sortedPaths:
|
|
427
|
-
pathInfo = uploadContents.
|
|
441
|
+
pathInfo = uploadContents.uploadPathsByPath[path]
|
|
428
442
|
if pathInfo.isDirectory:
|
|
429
443
|
continue
|
|
430
444
|
if pathInfo.reportFolderType in (ReportFolderType.PRIVATE_DOC, ReportFolderType.PUBLIC_DOC):
|
|
@@ -462,8 +476,10 @@ def rule_EC0234E(
|
|
|
462
476
|
"""
|
|
463
477
|
EDINET.EC0234E: A cover file exists in an unsupported subdirectory.
|
|
464
478
|
"""
|
|
465
|
-
uploadContents = pluginData.getUploadContents(
|
|
466
|
-
|
|
479
|
+
uploadContents = pluginData.getUploadContents()
|
|
480
|
+
if uploadContents is None:
|
|
481
|
+
return
|
|
482
|
+
for path, pathInfo in uploadContents.uploadPathsByPath.items():
|
|
467
483
|
if pathInfo.isDirectory:
|
|
468
484
|
continue
|
|
469
485
|
if pathInfo.reportFolderType not in (ReportFolderType.PRIVATE_DOC, ReportFolderType.PUBLIC_DOC):
|
|
@@ -549,14 +565,16 @@ def rule_EC0349E(
|
|
|
549
565
|
EDINET.EC0349E: An unexpected directory or file exists in the XBRL directory.
|
|
550
566
|
Only PublicDoc, PrivateDoc, or AuditDoc directories may exist beneath the XBRL directory.
|
|
551
567
|
"""
|
|
552
|
-
|
|
568
|
+
uploadContents = pluginData.getUploadContents()
|
|
569
|
+
if uploadContents is None:
|
|
570
|
+
return
|
|
553
571
|
xbrlDirectoryPath = Path('XBRL')
|
|
554
572
|
allowedPaths = {p.xbrlDirectory for p in (
|
|
555
573
|
ReportFolderType.AUDIT_DOC,
|
|
556
574
|
ReportFolderType.PRIVATE_DOC,
|
|
557
575
|
ReportFolderType.PUBLIC_DOC,
|
|
558
576
|
)}
|
|
559
|
-
for path, pathInfo in
|
|
577
|
+
for path, pathInfo in uploadContents.uploadPathsByPath.items():
|
|
560
578
|
if path.parent != xbrlDirectoryPath:
|
|
561
579
|
continue
|
|
562
580
|
if path not in allowedPaths:
|
|
@@ -583,8 +601,10 @@ def rule_EC0352E(
|
|
|
583
601
|
"""
|
|
584
602
|
EDINET.EC0352E: An XBRL file with an invalid name exists.
|
|
585
603
|
"""
|
|
586
|
-
|
|
587
|
-
|
|
604
|
+
uploadContents = pluginData.getUploadContents()
|
|
605
|
+
if uploadContents is None:
|
|
606
|
+
return
|
|
607
|
+
for path, pathInfo in uploadContents.uploadPathsByPath.items():
|
|
588
608
|
if (
|
|
589
609
|
pathInfo.isDirectory or
|
|
590
610
|
pathInfo.isCorrection or
|
|
@@ -603,6 +623,127 @@ def rule_EC0352E(
|
|
|
603
623
|
)
|
|
604
624
|
|
|
605
625
|
|
|
626
|
+
@validation(
|
|
627
|
+
hook=ValidationHook.XBRL_FINALLY,
|
|
628
|
+
disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
|
|
629
|
+
)
|
|
630
|
+
def rules_cover_page(
|
|
631
|
+
pluginData: PluginValidationDataExtension,
|
|
632
|
+
val: ValidateXbrl,
|
|
633
|
+
*args: Any,
|
|
634
|
+
**kwargs: Any,
|
|
635
|
+
) -> Iterable[Validation]:
|
|
636
|
+
"""
|
|
637
|
+
EDINET.EC1000E: Cover page must contain "【表紙】".
|
|
638
|
+
EDINET.EC1001E: A required item is missing from the cover page.
|
|
639
|
+
EDINET.EC1002E: A duplicate item is included on the cover page.
|
|
640
|
+
EDINET.EC1003E: An unnecessary item is included on the cover page.
|
|
641
|
+
EDINET.EC1004E: An item on the cover page is out of order.
|
|
642
|
+
EDINET.EC1005E: A required item on the cover page is missing a valid value.
|
|
643
|
+
"""
|
|
644
|
+
uploadContents = pluginData.getUploadContents(val.modelXbrl)
|
|
645
|
+
if uploadContents is None:
|
|
646
|
+
return
|
|
647
|
+
for url, doc in val.modelXbrl.urlDocs.items():
|
|
648
|
+
path = Path(url)
|
|
649
|
+
pathInfo = uploadContents.uploadPathsByFullPath.get(path)
|
|
650
|
+
if pathInfo is None or not pathInfo.isCoverPage:
|
|
651
|
+
continue
|
|
652
|
+
rootElt = doc.xmlRootElement
|
|
653
|
+
coverPageTextFound = False
|
|
654
|
+
for elt in rootElt.iterdescendants():
|
|
655
|
+
if not coverPageTextFound and elt.text and '【表紙】' in elt.text:
|
|
656
|
+
coverPageTextFound = True
|
|
657
|
+
break
|
|
658
|
+
if not coverPageTextFound:
|
|
659
|
+
yield Validation.error(
|
|
660
|
+
codes='EDINET.EC1000E',
|
|
661
|
+
msg=_("There is no '【表紙】' on the cover page. "
|
|
662
|
+
"File name: '%(file)s'. "
|
|
663
|
+
"Please add '【表紙】' to the relevant file."),
|
|
664
|
+
file=doc.basename,
|
|
665
|
+
)
|
|
666
|
+
filingFormat = pluginData.getFilingFormat(val.modelXbrl)
|
|
667
|
+
if filingFormat is None:
|
|
668
|
+
return
|
|
669
|
+
coverPageRequirements = pluginData.getCoverPageRequirements(val.modelXbrl)
|
|
670
|
+
currentLineNumber = 0
|
|
671
|
+
for qname in pluginData.coverPageItems:
|
|
672
|
+
foundFacts = []
|
|
673
|
+
validNonNilFacts = []
|
|
674
|
+
for fact in pluginData.iterFacts(val.modelXbrl, qname):
|
|
675
|
+
if fact.modelDocument != doc:
|
|
676
|
+
continue
|
|
677
|
+
if fact.qname.prefix is not None and filingFormat.includesTaxonomyPrefix(fact.qname.prefix):
|
|
678
|
+
foundFacts.append(fact)
|
|
679
|
+
if fact.xValid >= VALID and not fact.isNil:
|
|
680
|
+
validNonNilFacts.append(fact)
|
|
681
|
+
|
|
682
|
+
for fact in sorted(foundFacts, key=lambda f: cast(int, f.sourceline)):
|
|
683
|
+
if (sourceLine := cast(int, fact.sourceline)) <= currentLineNumber:
|
|
684
|
+
yield Validation.error(
|
|
685
|
+
codes='EDINET.EC1004E',
|
|
686
|
+
msg=_("Cover item %(localName)s is not in the correct order. "
|
|
687
|
+
"File name: '%(file)s'. "
|
|
688
|
+
"Please correct the order of cover items in the appropriate file."),
|
|
689
|
+
localName=qname.localName,
|
|
690
|
+
file=doc.basename,
|
|
691
|
+
modelObject=fact,
|
|
692
|
+
)
|
|
693
|
+
else:
|
|
694
|
+
currentLineNumber = sourceLine
|
|
695
|
+
|
|
696
|
+
if len(foundFacts) > 1:
|
|
697
|
+
yield Validation.error(
|
|
698
|
+
codes='EDINET.EC1002E',
|
|
699
|
+
msg=_("Cover item %(localName)s is duplicated. "
|
|
700
|
+
"File name: '%(file)s'. "
|
|
701
|
+
"Please check the cover item %(localName)s of the relevant file "
|
|
702
|
+
"and make sure there are no duplicates."),
|
|
703
|
+
localName=qname.localName,
|
|
704
|
+
file=doc.basename,
|
|
705
|
+
modelObject=foundFacts,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
status = coverPageRequirements.get(qname, filingFormat)
|
|
709
|
+
if status is None:
|
|
710
|
+
continue
|
|
711
|
+
if status == CoverPageItemStatus.REQUIRED:
|
|
712
|
+
if len(foundFacts) == 0:
|
|
713
|
+
yield Validation.error(
|
|
714
|
+
codes='EDINET.EC1001E',
|
|
715
|
+
msg=_("Cover item %(localName)s is missing. "
|
|
716
|
+
"File name: '%(file)s'. "
|
|
717
|
+
"Please add the cover item %(localName)s to the relevant file."),
|
|
718
|
+
localName=qname.localName,
|
|
719
|
+
file=doc.basename,
|
|
720
|
+
)
|
|
721
|
+
elif len(validNonNilFacts) == 0:
|
|
722
|
+
yield Validation.error(
|
|
723
|
+
codes='EDINET.EC1005E',
|
|
724
|
+
msg=_("Cover item %(localName)s is missing a valid value. "
|
|
725
|
+
"File name: '%(file)s'. "
|
|
726
|
+
"Please enter a valid value for %(localName)s in the relevant file."),
|
|
727
|
+
localName=qname.localName,
|
|
728
|
+
file=doc.basename,
|
|
729
|
+
modelObject=foundFacts,
|
|
730
|
+
)
|
|
731
|
+
elif status == CoverPageItemStatus.PROHIBITED:
|
|
732
|
+
for fact in foundFacts:
|
|
733
|
+
if fact.isNil:
|
|
734
|
+
continue # Prohibited cover pages facts are allowed, only if nil.
|
|
735
|
+
yield Validation.error(
|
|
736
|
+
codes='EDINET.EC1003E',
|
|
737
|
+
msg=_("Cover item %(localName)s is not necessary. "
|
|
738
|
+
"File name: '%(file)s' (line %(line)s). "
|
|
739
|
+
"Please add the cover item %(localName)s to the relevant file."),
|
|
740
|
+
localName=qname.localName,
|
|
741
|
+
file=doc.basename,
|
|
742
|
+
line=fact.sourceline,
|
|
743
|
+
modelObject=fact,
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
|
|
606
747
|
@validation(
|
|
607
748
|
hook=ValidationHook.XBRL_FINALLY,
|
|
608
749
|
disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
|
|
@@ -644,8 +785,13 @@ def rule_uri_references(
|
|
|
644
785
|
"""
|
|
645
786
|
EDINET.EC1007E: The URI in the HTML specifies a URL or absolute path.
|
|
646
787
|
EDINET.EC1013E: The URI in the HTML specifies a path not under a subdirectory.
|
|
647
|
-
EDINET.EC1014E: The URI in the HTML specifies a path to a
|
|
788
|
+
EDINET.EC1014E: The URI in the HTML specifies a path to a directory.
|
|
789
|
+
EDINET.EC1021E: The URI in the HTML specifies a path to a file that doesn't exist.
|
|
790
|
+
EDINET.EC1023E: The URI in the HTML specifies a path to a PDF file.
|
|
648
791
|
"""
|
|
792
|
+
uploadContents = pluginData.getUploadContents(val.modelXbrl)
|
|
793
|
+
if uploadContents is None:
|
|
794
|
+
return
|
|
649
795
|
for uriReference in pluginData.uriReferences:
|
|
650
796
|
if UrlUtil.isAbsolute(uriReference.attributeValue):
|
|
651
797
|
yield Validation.error(
|
|
@@ -671,10 +817,11 @@ def rule_uri_references(
|
|
|
671
817
|
)
|
|
672
818
|
continue
|
|
673
819
|
fullPath = Path(uriReference.document.uri).parent / path
|
|
674
|
-
|
|
820
|
+
pathInfo = uploadContents.uploadPathsByFullPath.get(fullPath)
|
|
821
|
+
if pathInfo is not None and pathInfo.isDirectory:
|
|
675
822
|
yield Validation.error(
|
|
676
823
|
codes='EDINET.EC1014E',
|
|
677
|
-
msg=_("The URI in the HTML specifies a path to a
|
|
824
|
+
msg=_("The URI in the HTML specifies a path to a directory. "
|
|
678
825
|
"File name: '%(file)s' (line %(line)s). "
|
|
679
826
|
"Please update the URI to reference a file."),
|
|
680
827
|
file=uriReference.document.basename,
|
|
@@ -682,6 +829,100 @@ def rule_uri_references(
|
|
|
682
829
|
modelObject=uriReference.element,
|
|
683
830
|
)
|
|
684
831
|
continue
|
|
832
|
+
if path.suffix.lower() == '.pdf':
|
|
833
|
+
yield Validation.error(
|
|
834
|
+
codes='EDINET.EC1023E',
|
|
835
|
+
msg=_("The URI in the HTML specifies a path to a PDF file. "
|
|
836
|
+
"File name: '%(file)s' (line %(line)s). "
|
|
837
|
+
"Please remove the link from the relevant file."),
|
|
838
|
+
file=uriReference.document.basename,
|
|
839
|
+
line=uriReference.element.sourceline,
|
|
840
|
+
modelObject=uriReference.element,
|
|
841
|
+
)
|
|
842
|
+
continue
|
|
843
|
+
if not val.modelXbrl.fileSource.exists(str(fullPath)):
|
|
844
|
+
yield Validation.error(
|
|
845
|
+
codes='EDINET.EC1021E',
|
|
846
|
+
msg=_("The linked file ('%(path)s') does not exist. "
|
|
847
|
+
"File name: '%(file)s' (line %(line)s). "
|
|
848
|
+
"Please update the URI to reference a file."),
|
|
849
|
+
path=str(path),
|
|
850
|
+
file=uriReference.document.basename,
|
|
851
|
+
line=uriReference.element.sourceline,
|
|
852
|
+
modelObject=uriReference.element,
|
|
853
|
+
)
|
|
854
|
+
continue
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
@validation(
|
|
858
|
+
hook=ValidationHook.FILESOURCE,
|
|
859
|
+
disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
|
|
860
|
+
)
|
|
861
|
+
def rule_EC1009R(
|
|
862
|
+
pluginData: ControllerPluginData,
|
|
863
|
+
cntlr: Cntlr,
|
|
864
|
+
fileSource: FileSource,
|
|
865
|
+
*args: Any,
|
|
866
|
+
**kwargs: Any,
|
|
867
|
+
) -> Iterable[Validation]:
|
|
868
|
+
"""
|
|
869
|
+
EDINET.EC1009R: The HTML file size must be 2.5MB (megabytes) or less.
|
|
870
|
+
"""
|
|
871
|
+
for path, size in pluginData.getUploadFileSizes(fileSource).items():
|
|
872
|
+
if path.suffix not in HTML_EXTENSIONS:
|
|
873
|
+
continue
|
|
874
|
+
if size > 2_500_000:
|
|
875
|
+
yield Validation.warning(
|
|
876
|
+
codes='EDINET.EC1009R',
|
|
877
|
+
msg=_("The HTML file size exceeds the maximum limit. "
|
|
878
|
+
"File name: '%(path)s'. "
|
|
879
|
+
"Please split the file so that the file size is 2.5MB or less."),
|
|
880
|
+
path=str(path),
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
@validation(
|
|
886
|
+
hook=ValidationHook.XBRL_FINALLY,
|
|
887
|
+
disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
|
|
888
|
+
)
|
|
889
|
+
def rule_EC1010E(
|
|
890
|
+
pluginData: PluginValidationDataExtension,
|
|
891
|
+
val: ValidateXbrl,
|
|
892
|
+
*args: Any,
|
|
893
|
+
**kwargs: Any,
|
|
894
|
+
) -> Iterable[Validation]:
|
|
895
|
+
"""
|
|
896
|
+
EDINET.EC1010E: The charset specification in the content attribute of the HTML <meta> tag must be UTF-8.
|
|
897
|
+
"""
|
|
898
|
+
for modelDocument in val.modelXbrl.urlDocs.values():
|
|
899
|
+
path = Path(modelDocument.uri)
|
|
900
|
+
if path.suffix not in HTML_EXTENSIONS:
|
|
901
|
+
continue
|
|
902
|
+
rootElt = modelDocument.xmlRootElement
|
|
903
|
+
matchingElt = None
|
|
904
|
+
missingElts = []
|
|
905
|
+
for metaElt in rootElt.iterdescendants(tag=XbrlConst.qnXhtmlMeta.clarkNotation):
|
|
906
|
+
if metaElt.qname.localName != 'meta':
|
|
907
|
+
continue
|
|
908
|
+
content = metaElt.get('content')
|
|
909
|
+
if content is None:
|
|
910
|
+
continue
|
|
911
|
+
charset = content.split('charset=')[-1].strip().lower()
|
|
912
|
+
if charset == 'utf-8':
|
|
913
|
+
matchingElt = metaElt
|
|
914
|
+
else:
|
|
915
|
+
missingElts.append(metaElt)
|
|
916
|
+
|
|
917
|
+
if matchingElt is None or len(missingElts) > 0:
|
|
918
|
+
yield Validation.error(
|
|
919
|
+
codes='EDINET.EC1010E',
|
|
920
|
+
msg=_("The charset specification in the content attribute of the HTML <meta> tag is not UTF-8. "
|
|
921
|
+
"File name: '%(path)s'. "
|
|
922
|
+
"Please change the character code of the file to UTF-8."),
|
|
923
|
+
path=str(path),
|
|
924
|
+
modelObject=missingElts
|
|
925
|
+
)
|
|
685
926
|
|
|
686
927
|
|
|
687
928
|
@validation(
|
|
@@ -727,10 +968,12 @@ def rule_EC1017E(
|
|
|
727
968
|
"""
|
|
728
969
|
EDINET.EC1017E: There is an unused file.
|
|
729
970
|
"""
|
|
730
|
-
uploadContents = pluginData.getUploadContents(
|
|
971
|
+
uploadContents = pluginData.getUploadContents()
|
|
972
|
+
if uploadContents is None:
|
|
973
|
+
return
|
|
731
974
|
existingSubdirectoryFilepaths = {
|
|
732
975
|
path
|
|
733
|
-
for path, pathInfo in uploadContents.
|
|
976
|
+
for path, pathInfo in uploadContents.uploadPathsByPath.items()
|
|
734
977
|
if pathInfo.isSubdirectory and not pathInfo.isDirectory
|
|
735
978
|
}
|
|
736
979
|
usedFilepaths = pluginData.getUsedFilepaths()
|
|
@@ -749,20 +992,22 @@ def rule_EC1017E(
|
|
|
749
992
|
hook=ValidationHook.XBRL_FINALLY,
|
|
750
993
|
disclosureSystems=[DISCLOSURE_SYSTEM_EDINET],
|
|
751
994
|
)
|
|
752
|
-
def
|
|
995
|
+
def rule_html_elements(
|
|
753
996
|
pluginData: PluginValidationDataExtension,
|
|
754
997
|
val: ValidateXbrl,
|
|
755
998
|
*args: Any,
|
|
756
999
|
**kwargs: Any,
|
|
757
1000
|
) -> Iterable[Validation]:
|
|
758
1001
|
"""
|
|
1002
|
+
EDINET.EC1011E: The HTML lang attribute is not Japanese.
|
|
759
1003
|
EDINET.EC1020E: When writing a DOCTYPE declaration, do not define it multiple times.
|
|
760
|
-
|
|
1004
|
+
Also, please modify the relevant file so that there is only one html tag, one head tag, and one body tag each.
|
|
761
1005
|
|
|
762
|
-
Note: Some violations of
|
|
1006
|
+
Note: Some violations of EC1020E (such as multiple DOCTYPE declarations) prevent Arelle from parsing
|
|
763
1007
|
the XML at all, and thus an XML schema error will be triggered rather than this validation error.
|
|
764
1008
|
"""
|
|
765
1009
|
checkNames = frozenset({'body', 'head', 'html'})
|
|
1010
|
+
langAttributeValues = frozenset({'ja', 'jp', 'ja-jp', 'JA', 'JP', 'JA-JP'})
|
|
766
1011
|
for modelDocument in val.modelXbrl.urlDocs.values():
|
|
767
1012
|
path = Path(modelDocument.uri)
|
|
768
1013
|
if path.suffix not in HTML_EXTENSIONS:
|
|
@@ -773,10 +1018,22 @@ def rule_EC1020E(
|
|
|
773
1018
|
}
|
|
774
1019
|
for elt in rootElt.iterdescendants():
|
|
775
1020
|
name = elt.qname.localName
|
|
776
|
-
if name
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1021
|
+
if name in checkNames:
|
|
1022
|
+
eltCounts[name] = eltCounts.get(name, 0) + 1
|
|
1023
|
+
if not isinstance(elt, ModelFact):
|
|
1024
|
+
lang = elt.get(XbrlConst.qnXmlLang.clarkNotation)
|
|
1025
|
+
if lang is not None and lang not in langAttributeValues:
|
|
1026
|
+
yield Validation.error(
|
|
1027
|
+
codes='EDINET.EC1011E',
|
|
1028
|
+
msg=_("The language setting is not Japanese. "
|
|
1029
|
+
"File name: %(file)s (line %(line)s). "
|
|
1030
|
+
"Please set the lang attribute on the given line of the "
|
|
1031
|
+
"relevant file to one of the following: %(langValues)s."),
|
|
1032
|
+
file=modelDocument.basename,
|
|
1033
|
+
line=elt.sourceline,
|
|
1034
|
+
langValues=', '.join(langAttributeValues),
|
|
1035
|
+
)
|
|
1036
|
+
|
|
780
1037
|
if any(count > 1 for count in eltCounts.values()):
|
|
781
1038
|
yield Validation.error(
|
|
782
1039
|
codes='EDINET.EC1020E',
|
|
@@ -810,7 +1067,7 @@ def rule_EC1031E(
|
|
|
810
1067
|
msg=_("Prohibited attribute '%(attributeName)s' is used in HTML. "
|
|
811
1068
|
"File name: %(file)s (line %(line)s). "
|
|
812
1069
|
"Please correct the tag attributes of the relevant file."),
|
|
813
|
-
attributeName=
|
|
1070
|
+
attributeName=attributeName,
|
|
814
1071
|
file=doc.basename,
|
|
815
1072
|
line=elt.sourceline,
|
|
816
1073
|
modelObject=elt,
|
|
@@ -838,7 +1095,10 @@ def rule_filenames(
|
|
|
838
1095
|
than those allowed (alphanumeric characters, '-' and '_').
|
|
839
1096
|
Note: Applies ONLY to files directly beneath non-correction report folders.
|
|
840
1097
|
"""
|
|
841
|
-
|
|
1098
|
+
uploadContents = pluginData.getUploadContents()
|
|
1099
|
+
if uploadContents is None:
|
|
1100
|
+
return
|
|
1101
|
+
for path, pathInfo in uploadContents.uploadPathsByPath.items():
|
|
842
1102
|
isReportFile = (
|
|
843
1103
|
not pathInfo.isAttachment and
|
|
844
1104
|
not pathInfo.isCorrection and
|
|
@@ -35,6 +35,8 @@ SCHEMA_PATTERNS = {
|
|
|
35
35
|
"http://www.cro.ie/": re.compile(r"^\d{1,6}$")
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
UK_REF_NS_PATTERN = re.compile(r"^http://xbrl.frc.org.uk/general/\d{4,}-\d{2,}-\d{2,}/ref$")
|
|
39
|
+
|
|
38
40
|
TR_NAMESPACES = {
|
|
39
41
|
"http://www.xbrl.org/inlineXBRL/transformation/2010-04-20",
|
|
40
42
|
"http://www.xbrl.org/inlineXBRL/transformation/2011-07-31",
|
|
@@ -12,6 +12,7 @@ from .PluginValidationDataExtension import PluginValidationDataExtension
|
|
|
12
12
|
|
|
13
13
|
_: TypeGetText
|
|
14
14
|
|
|
15
|
+
CURRENCIES_DIMENSION = 'CurrenciesDimension'
|
|
15
16
|
EQUITY = 'Equity'
|
|
16
17
|
IE_PROFIT_LOSS = 'ProfitLossBeforeTax'
|
|
17
18
|
IE_PROFIT_LOSS_ORDINARY = 'ProfitLossOnOrdinaryActivitiesBeforeTax'
|
|
@@ -10,6 +10,7 @@ from collections import Counter
|
|
|
10
10
|
from collections.abc import Iterable
|
|
11
11
|
from decimal import Decimal
|
|
12
12
|
|
|
13
|
+
from arelle import XbrlConst
|
|
13
14
|
from arelle.typing import TypeGetText
|
|
14
15
|
from arelle.ValidateXbrl import ValidateXbrl
|
|
15
16
|
from collections import defaultdict
|
|
@@ -28,8 +29,8 @@ from arelle.utils.validate.Validation import Validation
|
|
|
28
29
|
from arelle.ValidateXbrlCalcs import inferredDecimals, rangeValue
|
|
29
30
|
from arelle.XbrlConst import qnXbrliMonetaryItemType, qnXbrliXbrl, xhtml
|
|
30
31
|
from arelle.XmlValidateConst import VALID
|
|
31
|
-
from ..ValidationPluginExtension import EQUITY, PRINCIPAL_CURRENCY, TURNOVER_REVENUE
|
|
32
|
-
from ..PluginValidationDataExtension import MANDATORY_ELEMENTS,
|
|
32
|
+
from ..ValidationPluginExtension import CURRENCIES_DIMENSION, EQUITY, PRINCIPAL_CURRENCY, TURNOVER_REVENUE
|
|
33
|
+
from ..PluginValidationDataExtension import MANDATORY_ELEMENTS, SCHEMA_PATTERNS, TR_NAMESPACES, UK_REF_NS_PATTERN, PluginValidationDataExtension
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
def checkFileEncoding(modelXbrl: ModelXbrl) -> None:
|
|
@@ -302,14 +303,14 @@ def rule_ros19(
|
|
|
302
303
|
equity_facts = val.modelXbrl.factsByLocalName.get(EQUITY, set())
|
|
303
304
|
largest_equity_fact = None
|
|
304
305
|
for e_fact in equity_facts:
|
|
305
|
-
if e_fact.xValid >= VALID:
|
|
306
|
+
if e_fact.xValid >= VALID and e_fact.xValue is not None:
|
|
306
307
|
if largest_equity_fact is None or abs(convertToDecimal(e_fact.xValue)) > abs(convertToDecimal(largest_equity_fact.xValue)):
|
|
307
308
|
largest_equity_fact = e_fact
|
|
308
309
|
|
|
309
310
|
turnover_facts = val.modelXbrl.factsByLocalName.get(TURNOVER_REVENUE, set())
|
|
310
311
|
largest_turnover_fact = None
|
|
311
312
|
for t_fact in turnover_facts:
|
|
312
|
-
if t_fact.xValid >= VALID:
|
|
313
|
+
if t_fact.xValid >= VALID and t_fact.xValue is not None:
|
|
313
314
|
if largest_turnover_fact is None or convertToDecimal(t_fact.xValue) > convertToDecimal(largest_turnover_fact.xValue):
|
|
314
315
|
largest_turnover_fact = t_fact
|
|
315
316
|
|
|
@@ -337,27 +338,56 @@ def rule_ros20(
|
|
|
337
338
|
used for the majority of monetary facts.
|
|
338
339
|
"""
|
|
339
340
|
principal_currency_facts = val.modelXbrl.factsByLocalName.get(PRINCIPAL_CURRENCY, set())
|
|
340
|
-
principal_currency_values =
|
|
341
|
+
principal_currency_values = {
|
|
342
|
+
currencyDimensionCode
|
|
343
|
+
for pc_fact in principal_currency_facts
|
|
344
|
+
if (currencyDimensionCode := _getCurrencyDimensionCode(val.modelXbrl, pc_fact))
|
|
345
|
+
}
|
|
341
346
|
if len(principal_currency_values) != 1:
|
|
342
347
|
yield Validation.error(
|
|
343
348
|
"ROS.20",
|
|
344
349
|
_("'PrincipalCurrencyUsedInBusinessReport' must exist and have a single value. Values found: %(principal_currency_values)s."),
|
|
345
350
|
modelObject=principal_currency_facts,
|
|
346
|
-
principal_currency_values=principal_currency_values,
|
|
351
|
+
principal_currency_values=sorted(principal_currency_values),
|
|
347
352
|
)
|
|
348
353
|
else:
|
|
349
354
|
principal_currency_value = principal_currency_values.pop()
|
|
350
355
|
monetary_facts = list(val.modelXbrl.factsByDatatype(False, qnXbrliMonetaryItemType))
|
|
351
|
-
monetary_units = [list(fact.utrEntries)[0].
|
|
356
|
+
monetary_units = [list(fact.utrEntries)[0].unitId for fact in monetary_facts if fact.unit is not None]
|
|
352
357
|
unit_counts = Counter(monetary_units)
|
|
353
358
|
principal_currency_value_count = unit_counts[principal_currency_value]
|
|
354
|
-
for
|
|
359
|
+
for count in unit_counts.values():
|
|
355
360
|
if count > principal_currency_value_count:
|
|
356
361
|
yield Validation.warning(
|
|
357
362
|
"ROS.20",
|
|
358
|
-
_("'PrincipalCurrencyUsedInBusinessReport' has a value of %(principal_currency_value)s, "
|
|
363
|
+
_("'PrincipalCurrencyUsedInBusinessReport' has a %(currencies_dimension)s value of %(principal_currency_value)s, "
|
|
359
364
|
"which must match the functional(majority) unit of the financial statement."),
|
|
360
365
|
modelObject=principal_currency_facts,
|
|
366
|
+
currencies_dimension=CURRENCIES_DIMENSION,
|
|
361
367
|
principal_currency_value=principal_currency_value,
|
|
362
368
|
)
|
|
363
369
|
break
|
|
370
|
+
|
|
371
|
+
def _getCurrencyDimensionCode(modelXbrl: ModelXbrl, fact: ModelInlineFact) -> str | None:
|
|
372
|
+
if fact.context is None:
|
|
373
|
+
return None
|
|
374
|
+
for dim, mem in fact.context.qnameDims.items():
|
|
375
|
+
if dim.localName != CURRENCIES_DIMENSION:
|
|
376
|
+
continue
|
|
377
|
+
if mem.xValid < VALID:
|
|
378
|
+
return None
|
|
379
|
+
mem_concept = modelXbrl.qnameConcepts.get(mem.xValue)
|
|
380
|
+
if mem_concept is None:
|
|
381
|
+
return None
|
|
382
|
+
for ref_rel in modelXbrl.relationshipSet(XbrlConst.conceptReference).fromModelObject(mem_concept):
|
|
383
|
+
concept_ref = ref_rel.toModelObject
|
|
384
|
+
uk_ref_ns = None
|
|
385
|
+
for ns in concept_ref.nsmap.values():
|
|
386
|
+
if UK_REF_NS_PATTERN.match(ns):
|
|
387
|
+
uk_ref_ns = ns
|
|
388
|
+
break
|
|
389
|
+
if uk_ref_ns is None:
|
|
390
|
+
continue
|
|
391
|
+
if code := concept_ref.findtext(f"{{{uk_ref_ns}}}Code"):
|
|
392
|
+
return code.strip()
|
|
393
|
+
return None
|