From 95973bbb0c903a5b3675119ec1cb5b7a57575242 Mon Sep 17 00:00:00 2001 From: "Denis Robinet (ACSONE)" Date: Wed, 28 Aug 2024 08:22:48 +0200 Subject: [PATCH 1/9] ESEF 2024: Add new rules --- arelle/plugin/validate/ESEF/Const.py | 4 +++ .../plugin/validate/ESEF/ESEF_Current/DTS.py | 29 +++++++++++++++++-- .../ESEF/ESEF_Current/ValidateXbrlFinally.py | 25 +++++++++++++++- .../plugin/validate/ESEF/resources/config.xml | 21 ++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/arelle/plugin/validate/ESEF/Const.py b/arelle/plugin/validate/ESEF/Const.py index ef8b85eaaf..f75230ffd0 100644 --- a/arelle/plugin/validate/ESEF/Const.py +++ b/arelle/plugin/validate/ESEF/Const.py @@ -90,6 +90,10 @@ qname("{http://www.xbrl.org/dtr/type/2020-01-21}nonnum:domainItemType"), )) +qnDomainItemTypes2024 = frozenset(( + qname("{http://www.xbrl.org/dtr/type/2022-03-31}nonnum:domainItemType"), +)) + linkbaseRefTypes = { "http://www.xbrl.org/2003/role/calculationLinkbaseRef": "cal", diff --git a/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py b/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py index 0c7fab2e03..fa8c0f6323 100644 --- a/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py +++ b/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py @@ -23,6 +23,7 @@ linkbaseRefTypes, qnDomainItemTypes, qnDomainItemTypes2023, + qnDomainItemTypes2024, ) from ..Util import isChildOfNotes, isExtension, getDisclosureSystemYear @@ -109,7 +110,13 @@ def lc3wordAdjust(word: str) -> str: val.modelXbrl.error("ESEF.3.4.3.extensionTaxonomyDimensionNotAssignedDefaultMemberInDedicatedPlaceholder", _("Each dimension in an issuer specific extension taxonomy MUST be assigned to a default member in the ELR with role URI http://www.esma.europa.eu/xbrl/role/core/ifrs-dim_role-990000 defined in esef_cor.xsd schema file. %(qname)s"), modelObject=modelConcept, qname=modelConcept.qname) - esefDomainItemTypes = qnDomainItemTypes if getDisclosureSystemYear(val.modelXbrl) < 2023 else qnDomainItemTypes2023 + + if getDisclosureSystemYear(val.modelXbrl) < 2023: + esefDomainItemTypes = qnDomainItemTypes + elif getDisclosureSystemYear(val.modelXbrl) == 2023: + esefDomainItemTypes = qnDomainItemTypes2023 + else: + esefDomainItemTypes = qnDomainItemTypes2024 if modelConcept.isDomainMember and modelConcept in val.domainMembers and modelConcept.typeQname not in esefDomainItemTypes: domainMembersWrongType.append(modelConcept) if modelConcept.isPrimaryItem and not modelConcept.isAbstract: @@ -147,6 +154,14 @@ def lc3wordAdjust(word: str) -> str: extLineItemsWronglyAnchored.append(modelConcept) if modelConcept.isMonetary and not modelConcept.balance: extMonetaryConceptsWithoutBalance.append(modelConcept) + widerConcept = widerNarrowerRelSet.fromModelObject(modelConcept) + narrowerConcept = widerNarrowerRelSet.toModelObject(modelConcept) + if getDisclosureSystemYear(val.modelXbrl) >= 2024 and (widerConcept and all(modelConcept.baseXbrliType != r.toModelObject.baseXbrliType for r in widerConcept)) or (narrowerConcept and all(modelConcept.baseXbrliType != r.fromModelObject.baseXbrliType for r in narrowerConcept)): + val.modelXbrl.warning("ESEF.1.4.1.differentExtensionDataType", + _("Issuers should anchor their extension elements to ESEF core taxonomy elements sharing the same data type. %(qname)s"), + modelObject=modelConcept, qname=modelConcept.qname) + + # check all lang's of standard label hasLc3Match = False lc3names = [] @@ -204,9 +219,13 @@ def lc3wordAdjust(word: str) -> str: _("Extension taxonomy MUST NOT define typed dimensions: %(concepts)s."), modelObject=typedDimsInExtTxmy, concepts=", ".join(str(c.qname) for c in typedDimsInExtTxmy)) if domainMembersWrongType: - xbrlReference322 = "https://www.xbrl.org/dtr/type/2020-01-21/types.xsd" if getDisclosureSystemYear(val.modelXbrl) < 2023: xbrlReference322 = "http://www.xbrl.org/dtr/type/nonNumeric-2009-12-16.xsd" + elif getDisclosureSystemYear(val.modelXbrl) == 2023: + xbrlReference322 = "https://www.xbrl.org/dtr/type/2020-01-21/types.xsd" + else: + xbrlReference322 = "https://www.xbrl.org/dtr/type/2022-03-31/types.xsd" + val.modelXbrl.error("ESEF.3.2.2.domainMemberWrongDataType", _("Domain members MUST have domainItemType data type as defined in \"%(xbrlReference)s\": concept %(concepts)s."), modelObject=domainMembersWrongType, xbrlReference=xbrlReference322, @@ -278,6 +297,12 @@ def lc3wordAdjust(word: str) -> str: if linkEltName == "calculationLink": val.hasExtensionCal = True linkbasesFound.add(linkEltName) + if getDisclosureSystemYear(val.modelXbrl) >= 2024: + for arc in linkElt.iterdescendants(tag="{http://www.xbrl.org/2003/linkbase}calculationArc"): + if arc.get("{http://www.w3.org/1999/xlink}arcrole") != "https://www.xbrl.org/2023/arcrole/summation-item": + val.modelXbrl.error("ESEF.3.4.1.IncorrectSummationItemArcroleUsed", + _("Starting from the ESEF 2024 taxonomy, only calculation linkbases using the arcrole 'https://www.xbrl.org/2023/arcrole/summation-item' are permitted. Arcrole %(arcrole)s has been found."), + arcrole=arc.get("{http://www.w3.org/1999/xlink}arcrole")) if linkEltName == "definitionLink": val.hasExtensionDef = True linkbasesFound.add(linkEltName) diff --git a/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py b/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py index a4554ac246..ba02eb1b84 100644 --- a/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +++ b/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py @@ -6,7 +6,7 @@ import os import zipfile from collections import defaultdict -from datetime import datetime +from datetime import datetime, timedelta from math import isnan from typing import Any, List, cast @@ -644,11 +644,30 @@ def checkFootnote(elt: ModelInlineFootnote | ModelResource, text: str) -> None: langsUsedByTextFacts = set() hasNoFacts = True + factsMissingId = [] for qn, facts in modelXbrl.factsByQname.items(): hasNoFacts = False if qn in mandatory: reportedMandatory.add(qn) for f in facts: + if getDisclosureSystemYear(modelXbrl) >= 2024: + if not f.id: + factsMissingId.append(f) + escaped = f.get("escape") in ("true", "1") + if (f.concept.type.isTextBlock and not escaped) or (not f.concept.type.isTextBlock and escaped): + modelXbrl.error("ESEF.2.2.7.improperApplicationOfEscapeAttribute", + _("Facts with datatype 'dtr-types:textBlockItemType' MUST use the 'escape' attribute set to 'true'. Facts with any other datatype MUST use the 'escape' attribute set to true 'false'"), + modelObject=f) + if f.effectiveValue == "0" and f.xValue != 0: + modelXbrl.warning("ESEF.2.2.5.roundedValueBelowScaleNotNull", + _("A value that has been rounded and is below the scale should show a value of zero. It has been found to have the value %(value)s"), + modelObject=f, value=f.value) + if f.concept.periodType == "instant": + if f.context.instantDate.day == 1 and f.context.instantDate.month == 1: + modelXbrl.error("ESEF.2.1.2.inappropriateInstantDate", + _("Instant date %(actualValue)s shall be replaced by %(expectedValue)s to ensure a better comparability between the facts."), + modelObject=f, actualValue=f.context.instantDate, expectedValue=f.context.instantDate - timedelta(days=1)) + pass if f.precision is not None: precisionFacts.add(f) if f.isNumeric and f.concept is not None and getattr(f, "xValid", 0) >= VALID: @@ -680,6 +699,10 @@ def checkFootnote(elt: ModelInlineFootnote | ModelResource, text: str) -> None: # conceptsUsed.add(dim.typedMember) ''' + if getDisclosureSystemYear(modelXbrl) >= 2024 and factsMissingId: + modelXbrl.warning("ESEF.2.2.8.missingFactID", + _("All facts should have a unique identifier. Facts %(elements)s have no identifier."), + modelObject=f, elements=", ".join([str(f.qname) for f in factsMissingId]), ) if noLangFacts: modelXbrl.error("ESEF.2.5.2.undefinedLanguageForTextFact", _("Each tagged text fact MUST have the 'xml:lang' attribute assigned or inherited."), diff --git a/arelle/plugin/validate/ESEF/resources/config.xml b/arelle/plugin/validate/ESEF/resources/config.xml index 334388b716..f0f2c5cf11 100644 --- a/arelle/plugin/validate/ESEF/resources/config.xml +++ b/arelle/plugin/validate/ESEF/resources/config.xml @@ -4,6 +4,27 @@ xsi:noNamespaceSchemaLocation="../../../../config/disclosuresystems.xsd"> + + + + + Date: Thu, 29 Aug 2024 13:35:26 +0200 Subject: [PATCH 2/9] First review of ESEF 2024 changes --- .../plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py | 4 ++-- arelle/plugin/validate/ESEF/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py b/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py index ba02eb1b84..0f65fc464c 100644 --- a/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +++ b/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py @@ -175,7 +175,7 @@ def validateXbrlFinally(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None: if ifrs_year != esef_year: # this check isn't precise enough, but the list of available concept isn't available in esef_cor modelXbrl.warning("ESEF.1.2.IFRSNotYetIncluded", - _("Elements available in the IFRS Taxonomy that were not yet included in the ESEF taxonomy sould not be used."), + _("Elements available in the IFRS Taxonomy that were not yet included in the ESEF taxonomy should not be used."), modelObject=modelXbrl) if val.consolidated and not (val.hasExtensionSchema and val.hasExtensionPre and val.hasExtensionCal and val.hasExtensionDef and val.hasExtensionLbl): @@ -656,7 +656,7 @@ def checkFootnote(elt: ModelInlineFootnote | ModelResource, text: str) -> None: escaped = f.get("escape") in ("true", "1") if (f.concept.type.isTextBlock and not escaped) or (not f.concept.type.isTextBlock and escaped): modelXbrl.error("ESEF.2.2.7.improperApplicationOfEscapeAttribute", - _("Facts with datatype 'dtr-types:textBlockItemType' MUST use the 'escape' attribute set to 'true'. Facts with any other datatype MUST use the 'escape' attribute set to true 'false'"), + _("Facts with datatype 'dtr-types:textBlockItemType' MUST use the 'escape' attribute set to 'true'. Facts with any other datatype MUST use the 'escape' attribute set to 'false'"), modelObject=f) if f.effectiveValue == "0" and f.xValue != 0: modelXbrl.warning("ESEF.2.2.5.roundedValueBelowScaleNotNull", diff --git a/arelle/plugin/validate/ESEF/__init__.py b/arelle/plugin/validate/ESEF/__init__.py index 1e59b32113..b479078688 100644 --- a/arelle/plugin/validate/ESEF/__init__.py +++ b/arelle/plugin/validate/ESEF/__init__.py @@ -369,7 +369,7 @@ def modelTestcaseVariationReportPackageIxdsOptions( "Validate ESMA ESEF-2022", "validate/ESEF_2022", ], - "version": "1.2023.00", + "version": "1.2024.00", "description": """ESMA ESEF Filer Manual and RTS Validations.""", "license": "Apache-2", "author": authorLabel, From 6b2733cb5c85d11b5da00cac6c475731490d351f Mon Sep 17 00:00:00 2001 From: "Gregorio Mongelli (ACSONE)" Date: Thu, 29 Aug 2024 14:54:24 +0200 Subject: [PATCH 3/9] For ESEF 2024, update list of permitted taxonomy URLs --- .../ESEF/resources/authority-validations.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/arelle/plugin/validate/ESEF/resources/authority-validations.json b/arelle/plugin/validate/ESEF/resources/authority-validations.json index 36656ffb92..1791fc9a13 100644 --- a/arelle/plugin/validate/ESEF/resources/authority-validations.json +++ b/arelle/plugin/validate/ESEF/resources/authority-validations.json @@ -130,6 +130,22 @@ "https://www.esma.europa.eu/taxonomy/2022-03-24/esef_cor.xsd" ] }, + "ESEF-2024": { + "outdatedTaxonomyURLs": [ + "http://www.esma.europa.eu/taxonomy/2017-03-31/esef_cor.xsd", + "https://www.esma.europa.eu/taxonomy/2017-03-31/esef_cor.xsd", + "http://www.esma.europa.eu/taxonomy/2019-03-27/esef_cor.xsd", + "https://www.esma.europa.eu/taxonomy/2019-03-27/esef_cor.xsd", + "http://www.esma.europa.eu/taxonomy/2020-03-16/esef_cor.xsd", + "https://www.esma.europa.eu/taxonomy/2020-03-16/esef_cor.xsd", + "http://www.esma.europa.eu/taxonomy/2021-03-24/esef_cor.xsd", + "https://www.esma.europa.eu/taxonomy/2021-03-24/esef_cor.xsd" + ], + "effectiveTaxonomyURLs": [ + "http://www.esma.europa.eu/taxonomy/2022-03-24/esef_cor.xsd", + "https://www.esma.europa.eu/taxonomy/2022-03-24/esef_cor.xsd" + ] + }, "AT": { "name": "Austria", "reportPackageMaxMB": "100", From 2938027ae66e132625229ef473e750738a99c847 Mon Sep 17 00:00:00 2001 From: "Denis Robinet (ACSONE)" Date: Thu, 29 Aug 2024 16:31:49 +0200 Subject: [PATCH 4/9] ESEF 2024: better error message and esef version detection --- arelle/plugin/validate/ESEF/ESEF_Current/DTS.py | 12 ++++++++++-- .../ESEF/ESEF_Current/ValidateXbrlFinally.py | 12 ++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py b/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py index fa8c0f6323..4a06b6dee5 100644 --- a/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py +++ b/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py @@ -5,6 +5,7 @@ import unicodedata from collections import defaultdict +from datetime import datetime import regex as re @@ -50,6 +51,13 @@ def lc3wordAdjust(word: str) -> str: return word[0].upper() + word[1:] return word + esefYear = None + for url in val.modelXbrl.namespaceDocs.keys(): + match = re.match("http[s]?://www.esma.europa.eu/taxonomy/([0-9]{4}-[0-9]{2}-[0-9]{2})/.*", url) + if match: + date = match.groups()[0] + esefYear = datetime.strptime(date, "%Y-%m-%d").year + if not isExtensionDoc: pass @@ -221,7 +229,7 @@ def lc3wordAdjust(word: str) -> str: if domainMembersWrongType: if getDisclosureSystemYear(val.modelXbrl) < 2023: xbrlReference322 = "http://www.xbrl.org/dtr/type/nonNumeric-2009-12-16.xsd" - elif getDisclosureSystemYear(val.modelXbrl) == 2023: + elif getDisclosureSystemYear(val.modelXbrl) == 2023 or esefYear < 2024: xbrlReference322 = "https://www.xbrl.org/dtr/type/2020-01-21/types.xsd" else: xbrlReference322 = "https://www.xbrl.org/dtr/type/2022-03-31/types.xsd" @@ -297,7 +305,7 @@ def lc3wordAdjust(word: str) -> str: if linkEltName == "calculationLink": val.hasExtensionCal = True linkbasesFound.add(linkEltName) - if getDisclosureSystemYear(val.modelXbrl) >= 2024: + if esefYear >= 2024: for arc in linkElt.iterdescendants(tag="{http://www.xbrl.org/2003/linkbase}calculationArc"): if arc.get("{http://www.w3.org/1999/xlink}arcrole") != "https://www.xbrl.org/2023/arcrole/summation-item": val.modelXbrl.error("ESEF.3.4.1.IncorrectSummationItemArcroleUsed", diff --git a/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py b/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py index 0f65fc464c..1a4237e0e6 100644 --- a/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +++ b/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py @@ -656,17 +656,17 @@ def checkFootnote(elt: ModelInlineFootnote | ModelResource, text: str) -> None: escaped = f.get("escape") in ("true", "1") if (f.concept.type.isTextBlock and not escaped) or (not f.concept.type.isTextBlock and escaped): modelXbrl.error("ESEF.2.2.7.improperApplicationOfEscapeAttribute", - _("Facts with datatype 'dtr-types:textBlockItemType' MUST use the 'escape' attribute set to 'true'. Facts with any other datatype MUST use the 'escape' attribute set to 'false'"), - modelObject=f) + _("Facts with datatype 'dtr-types:textBlockItemType' MUST use the 'escape' attribute set to 'true'. Facts with any other datatype MUST use the 'escape' attribute set to 'false' - fact %(conceptName)s"), + modelObject=f, conceptName=f.concept.qname) if f.effectiveValue == "0" and f.xValue != 0: modelXbrl.warning("ESEF.2.2.5.roundedValueBelowScaleNotNull", - _("A value that has been rounded and is below the scale should show a value of zero. It has been found to have the value %(value)s"), - modelObject=f, value=f.value) + _("A value that has been rounded and is below the scale should show a value of zero. It has been found to have the value %(value)s - fact %(conceptName)s"), + modelObject=f, value=f.value, conceptName=f.concept.qname) if f.concept.periodType == "instant": if f.context.instantDate.day == 1 and f.context.instantDate.month == 1: modelXbrl.error("ESEF.2.1.2.inappropriateInstantDate", - _("Instant date %(actualValue)s shall be replaced by %(expectedValue)s to ensure a better comparability between the facts."), - modelObject=f, actualValue=f.context.instantDate, expectedValue=f.context.instantDate - timedelta(days=1)) + _("Instant date %(actualValue)s in context %(contextID)s shall be replaced by %(expectedValue)s to ensure a better comparability between the facts."), + modelObject=f, actualValue=f.context.instantDate, expectedValue=f.context.instantDate - timedelta(days=1), contextID=f.context.id) pass if f.precision is not None: precisionFacts.add(f) From cca5af124357e9b11b659f62f1e1160f9259d927 Mon Sep 17 00:00:00 2001 From: "Denis Robinet (ACSONE)" Date: Mon, 2 Sep 2024 10:36:48 +0200 Subject: [PATCH 5/9] ESEF 2024: Ensure that the esef year alwayse have a value --- arelle/plugin/validate/ESEF/ESEF_Current/DTS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py b/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py index 4a06b6dee5..082ef0c3e7 100644 --- a/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py +++ b/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py @@ -51,7 +51,7 @@ def lc3wordAdjust(word: str) -> str: return word[0].upper() + word[1:] return word - esefYear = None + esefYear = getDisclosureSystemYear(val.modelXbrl) # if the taxonomy isn't recognised, take the disclosure system for url in val.modelXbrl.namespaceDocs.keys(): match = re.match("http[s]?://www.esma.europa.eu/taxonomy/([0-9]{4}-[0-9]{2}-[0-9]{2})/.*", url) if match: From 0f8ae8104ac8f7c87bfaf458dfd1c9abdc154842 Mon Sep 17 00:00:00 2001 From: "Denis Robinet (ACSONE)" Date: Mon, 2 Sep 2024 12:48:46 +0200 Subject: [PATCH 6/9] ESEF 2024: fix and/or priority for ESEF.1.4.1.differentExtensionDataType --- arelle/plugin/validate/ESEF/ESEF_Current/DTS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py b/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py index 082ef0c3e7..e32d1d08dc 100644 --- a/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py +++ b/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py @@ -164,7 +164,7 @@ def lc3wordAdjust(word: str) -> str: extMonetaryConceptsWithoutBalance.append(modelConcept) widerConcept = widerNarrowerRelSet.fromModelObject(modelConcept) narrowerConcept = widerNarrowerRelSet.toModelObject(modelConcept) - if getDisclosureSystemYear(val.modelXbrl) >= 2024 and (widerConcept and all(modelConcept.baseXbrliType != r.toModelObject.baseXbrliType for r in widerConcept)) or (narrowerConcept and all(modelConcept.baseXbrliType != r.fromModelObject.baseXbrliType for r in narrowerConcept)): + if getDisclosureSystemYear(val.modelXbrl) >= 2024 and ((widerConcept and all(modelConcept.baseXbrliType != r.toModelObject.baseXbrliType for r in widerConcept)) or (narrowerConcept and all(modelConcept.baseXbrliType != r.fromModelObject.baseXbrliType for r in narrowerConcept))): val.modelXbrl.warning("ESEF.1.4.1.differentExtensionDataType", _("Issuers should anchor their extension elements to ESEF core taxonomy elements sharing the same data type. %(qname)s"), modelObject=modelConcept, qname=modelConcept.qname) From ad3b366b26a9b241ccec1abddf7201068c805771 Mon Sep 17 00:00:00 2001 From: "Denis Robinet (ACSONE)" Date: Mon, 16 Sep 2024 09:19:03 +0200 Subject: [PATCH 7/9] [ESEF 2024] Changes after review --- .../plugin/validate/ESEF/ESEF_Current/DTS.py | 40 ++++++++++++------- .../ESEF/ESEF_Current/ValidateXbrlFinally.py | 26 +++++++----- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py b/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py index e32d1d08dc..1fba615efe 100644 --- a/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py +++ b/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py @@ -41,7 +41,8 @@ def checkFilingDTS(val: ValidateXbrl, modelDocument: ModelDocument, esefNotesCon isExtensionDoc = isExtension(val, modelDocument) filenamePattern = filenameRegex = None - anchorAbstractExtensionElements = getDisclosureSystemYear(val.modelXbrl) < 2023 and val.authParam["extensionElementsAnchoring"] == "include abstract" + esefDisclosureSystemYear = getDisclosureSystemYear(val.modelXbrl) + anchorAbstractExtensionElements = esefDisclosureSystemYear < 2023 and val.authParam["extensionElementsAnchoring"] == "include abstract" allowCapsInLc3Words = val.authParam["LC3AllowCapitalsInWord"] def lc3wordAdjust(word: str) -> str: @@ -51,12 +52,13 @@ def lc3wordAdjust(word: str) -> str: return word[0].upper() + word[1:] return word - esefYear = getDisclosureSystemYear(val.modelXbrl) # if the taxonomy isn't recognised, take the disclosure system + esefTaxonomyYear = esefDisclosureSystemYear # if the taxonomy isn't recognised, take the disclosure system for url in val.modelXbrl.namespaceDocs.keys(): match = re.match("http[s]?://www.esma.europa.eu/taxonomy/([0-9]{4}-[0-9]{2}-[0-9]{2})/.*", url) if match: date = match.groups()[0] - esefYear = datetime.strptime(date, "%Y-%m-%d").year + esefTaxonomyYear = datetime.strptime(date, "%Y-%m-%d").year + break if not isExtensionDoc: pass @@ -119,9 +121,9 @@ def lc3wordAdjust(word: str) -> str: _("Each dimension in an issuer specific extension taxonomy MUST be assigned to a default member in the ELR with role URI http://www.esma.europa.eu/xbrl/role/core/ifrs-dim_role-990000 defined in esef_cor.xsd schema file. %(qname)s"), modelObject=modelConcept, qname=modelConcept.qname) - if getDisclosureSystemYear(val.modelXbrl) < 2023: + if esefDisclosureSystemYear < 2023: esefDomainItemTypes = qnDomainItemTypes - elif getDisclosureSystemYear(val.modelXbrl) == 2023: + elif esefDisclosureSystemYear == 2023: esefDomainItemTypes = qnDomainItemTypes2023 else: esefDomainItemTypes = qnDomainItemTypes2024 @@ -162,13 +164,21 @@ def lc3wordAdjust(word: str) -> str: extLineItemsWronglyAnchored.append(modelConcept) if modelConcept.isMonetary and not modelConcept.balance: extMonetaryConceptsWithoutBalance.append(modelConcept) - widerConcept = widerNarrowerRelSet.fromModelObject(modelConcept) - narrowerConcept = widerNarrowerRelSet.toModelObject(modelConcept) - if getDisclosureSystemYear(val.modelXbrl) >= 2024 and ((widerConcept and all(modelConcept.baseXbrliType != r.toModelObject.baseXbrliType for r in widerConcept)) or (narrowerConcept and all(modelConcept.baseXbrliType != r.fromModelObject.baseXbrliType for r in narrowerConcept))): - val.modelXbrl.warning("ESEF.1.4.1.differentExtensionDataType", - _("Issuers should anchor their extension elements to ESEF core taxonomy elements sharing the same data type. %(qname)s"), - modelObject=modelConcept, qname=modelConcept.qname) + if esefDisclosureSystemYear >= 2024: + widerConcept = widerNarrowerRelSet.fromModelObject(modelConcept) + narrowerConcept = widerNarrowerRelSet.toModelObject(modelConcept) + widerTypes = set(r.toModelObject.baseXbrliType for r in widerConcept) + narrowerTypes = set(r.fromModelObject.baseXbrliType for r in narrowerConcept) + + if (narrowerTypes and modelConcept.baseXbrliType not in narrowerTypes) or (widerTypes and modelConcept.baseXbrliType not in widerTypes): + widerNarrowerType = "{} {}".format( + "Wider: {}".format(", ".join(widerTypes)) if widerTypes else "", + "Narrower: {}".format(", ".join(narrowerTypes)) if narrowerTypes else "" + ) + val.modelXbrl.warning("ESEF.1.4.1.differentExtensionDataType", + _("Issuers should anchor their extension elements to ESEF core taxonomy elements sharing the same data type. Concept: %(qname)s type: %(type)s %(widerNarrowerType)s"), + modelObject=modelConcept, qname=modelConcept.qname, type=modelConcept.baseXbrliType, widerNarrowerType=widerNarrowerType) # check all lang's of standard label hasLc3Match = False @@ -227,9 +237,9 @@ def lc3wordAdjust(word: str) -> str: _("Extension taxonomy MUST NOT define typed dimensions: %(concepts)s."), modelObject=typedDimsInExtTxmy, concepts=", ".join(str(c.qname) for c in typedDimsInExtTxmy)) if domainMembersWrongType: - if getDisclosureSystemYear(val.modelXbrl) < 2023: + if esefDisclosureSystemYear < 2023: xbrlReference322 = "http://www.xbrl.org/dtr/type/nonNumeric-2009-12-16.xsd" - elif getDisclosureSystemYear(val.modelXbrl) == 2023 or esefYear < 2024: + elif esefDisclosureSystemYear == 2023 or esefTaxonomyYear < 2024: xbrlReference322 = "https://www.xbrl.org/dtr/type/2020-01-21/types.xsd" else: xbrlReference322 = "https://www.xbrl.org/dtr/type/2022-03-31/types.xsd" @@ -305,7 +315,7 @@ def lc3wordAdjust(word: str) -> str: if linkEltName == "calculationLink": val.hasExtensionCal = True linkbasesFound.add(linkEltName) - if esefYear >= 2024: + if esefTaxonomyYear >= 2024: for arc in linkElt.iterdescendants(tag="{http://www.xbrl.org/2003/linkbase}calculationArc"): if arc.get("{http://www.w3.org/1999/xlink}arcrole") != "https://www.xbrl.org/2023/arcrole/summation-item": val.modelXbrl.error("ESEF.3.4.1.IncorrectSummationItemArcroleUsed", @@ -323,7 +333,7 @@ def lc3wordAdjust(word: str) -> str: if arcrole not in esefDefinitionArcroles: disallowedArcroles[arcrole].append(arcElt) - if linkEltName in ("definitionLink", ) and getDisclosureSystemYear(val.modelXbrl) == 2023 and val.authParam["validate1_9_1"] in ("true", "True", 1): + if linkEltName in ("definitionLink", ) and esefDisclosureSystemYear == 2023 and val.authParam["validate1_9_1"] in ("true", "True", 1): for locElt in linkElt.iterchildren("{http://www.xbrl.org/2003/linkbase}loc"): refObject = locElt.dereference() if (isinstance(refObject, ModelConcept) diff --git a/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py b/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py index 1a4237e0e6..0fba8db0ba 100644 --- a/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +++ b/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py @@ -146,7 +146,8 @@ def validateXbrlFinally(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None: checkFilingDTS(val, modelXbrl.modelDocument, esefNotesConcepts, [], ifrsNses=_ifrsNses) modelXbrl.profileActivity("... filer DTS checks", minTimeToShow=1.0) - if getDisclosureSystemYear(modelXbrl) >= 2023: + esefDisclosureSystemYear = getDisclosureSystemYear(modelXbrl) + if esefDisclosureSystemYear >= 2023: if val.unconsolidated and modelXbrl.fileSource.dir: instanceNumber = 0 for file in modelXbrl.fileSource.dir: @@ -283,7 +284,7 @@ def checkFootnote(elt: ModelInlineFootnote | ModelResource, text: str) -> None: modelObject=doc, doctype=docinfo.doctype) reportIncorrectlyPlacedInPackageRef = "https://www.xbrl.org/Specification/report-package/CR-2023-05-03/report-package-CR-2023-05-03.html" - if getDisclosureSystemYear(modelXbrl) < 2023: + if esefDisclosureSystemYear < 2023: reportIncorrectlyPlacedInPackageRef = "http://www.xbrl.org/WGN/report-packages/WGN-2018-08-14/report-packages-WGN-2018-08-14.html" if len(ixdsDocDirs) > 1 and val.consolidated: @@ -411,7 +412,7 @@ def checkFootnote(elt: ModelInlineFootnote | ModelResource, text: str) -> None: modelObject=elt, qname=elt.qname) elif any(character in elt.stringValue for character in ['<', '&', '&', '<']): if not (hasattr(elt, 'attrib')) or ('escape' not in elt.attrib or elt.attrib.get('escape').lower() != 'true'): - modelXbrl.error("ESEF.2.2.6.escapedHTMLUsedInBlockTagWithSpecialCharacters" if getDisclosureSystemYear(modelXbrl) < 2023 else "ESEF.2.2.7.escapedHTMLUsedInBlockTagWithSpecialCharacters", + modelXbrl.error("ESEF.2.2.6.escapedHTMLUsedInBlockTagWithSpecialCharacters" if esefDisclosureSystemYear < 2023 else "ESEF.2.2.7.escapedHTMLUsedInBlockTagWithSpecialCharacters", _("A text block containing '&' or '<' character MUST have an 'escape' attribute: %(qname)s."), modelObject=elt, qname=elt.qname) # Check that continuation elements are in the order of html text as rendered to user @@ -534,6 +535,7 @@ def checkFootnote(elt: ModelInlineFootnote | ModelResource, text: str) -> None: contextsWithDisallowedOCEcontent = [] contextsWithPeriodTime: list[ModelContext] = [] contextsWithPeriodTimeZone: list[ModelContext] = [] + contextsWithWrongInstantDate: list[ModelContext] = [] contextIdentifiers = defaultdict(list) nonStandardTypedDimensions: dict[Any, Any] = defaultdict(set) for context in modelXbrl.contexts.values(): @@ -548,6 +550,9 @@ def checkFootnote(elt: ModelInlineFootnote | ModelResource, text: str) -> None: contextsWithPeriodTime.append(context) if m.group(3): contextsWithPeriodTimeZone.append(context) + + if esefDisclosureSystemYear >= 2024 and context.instantDate and context.instantDate.day == 1 and context.instantDate.month == 1: + contextsWithWrongInstantDate.append(context) for elt in context.iterdescendants("{http://www.xbrl.org/2003/instance}segment"): contextsWithDisallowedOCEs.append(context) break @@ -597,6 +602,11 @@ def checkFootnote(elt: ModelInlineFootnote | ModelResource, text: str) -> None: modelXbrl.error("ESEF.2.1.2.periodWithTimeZone", _("The xbrli:startDate, xbrli:endDate and xbrli:instant elements MUST identify periods using whole days (i.e. specified without a time zone): %(contextIds)s"), modelObject=contextsWithPeriodTimeZone, contextIds=", ".join(c.id for c in contextsWithPeriodTimeZone if c.id)) + if contextsWithWrongInstantDate: + for context in contextsWithWrongInstantDate: + modelXbrl.error("ESEF.2.1.2.inappropriateInstantDate", + _("Instant date %(actualValue)s in context %(contextID)s shall be replaced by %(expectedValue)s to ensure a better comparability between the facts."), + modelObject=contextsWithWrongInstantDate, actualValue=context.instantDate, expectedValue=context.instantDate - timedelta(days=1), contextID=context.id) # identify unique contexts and units mapContext = {} @@ -650,7 +660,7 @@ def checkFootnote(elt: ModelInlineFootnote | ModelResource, text: str) -> None: if qn in mandatory: reportedMandatory.add(qn) for f in facts: - if getDisclosureSystemYear(modelXbrl) >= 2024: + if esefDisclosureSystemYear >= 2024: if not f.id: factsMissingId.append(f) escaped = f.get("escape") in ("true", "1") @@ -662,12 +672,6 @@ def checkFootnote(elt: ModelInlineFootnote | ModelResource, text: str) -> None: modelXbrl.warning("ESEF.2.2.5.roundedValueBelowScaleNotNull", _("A value that has been rounded and is below the scale should show a value of zero. It has been found to have the value %(value)s - fact %(conceptName)s"), modelObject=f, value=f.value, conceptName=f.concept.qname) - if f.concept.periodType == "instant": - if f.context.instantDate.day == 1 and f.context.instantDate.month == 1: - modelXbrl.error("ESEF.2.1.2.inappropriateInstantDate", - _("Instant date %(actualValue)s in context %(contextID)s shall be replaced by %(expectedValue)s to ensure a better comparability between the facts."), - modelObject=f, actualValue=f.context.instantDate, expectedValue=f.context.instantDate - timedelta(days=1), contextID=f.context.id) - pass if f.precision is not None: precisionFacts.add(f) if f.isNumeric and f.concept is not None and getattr(f, "xValid", 0) >= VALID: @@ -699,7 +703,7 @@ def checkFootnote(elt: ModelInlineFootnote | ModelResource, text: str) -> None: # conceptsUsed.add(dim.typedMember) ''' - if getDisclosureSystemYear(modelXbrl) >= 2024 and factsMissingId: + if esefDisclosureSystemYear >= 2024 and factsMissingId: modelXbrl.warning("ESEF.2.2.8.missingFactID", _("All facts should have a unique identifier. Facts %(elements)s have no identifier."), modelObject=f, elements=", ".join([str(f.qname) for f in factsMissingId]), ) From 63dc7ae5cc400de64eea24eb5b6139621b1b702e Mon Sep 17 00:00:00 2001 From: "Denis Robinet (ACSONE)" Date: Wed, 25 Sep 2024 13:47:09 +0200 Subject: [PATCH 8/9] [ESEF 2024] Update ESEF.3.4.1.IncorrectSummationItemArcroleUsed to check the disclosure system instead of the taxonomy year --- arelle/plugin/validate/ESEF/ESEF_Current/DTS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py b/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py index 1fba615efe..fc2c2d2860 100644 --- a/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py +++ b/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py @@ -315,7 +315,7 @@ def lc3wordAdjust(word: str) -> str: if linkEltName == "calculationLink": val.hasExtensionCal = True linkbasesFound.add(linkEltName) - if esefTaxonomyYear >= 2024: + if esefDisclosureSystemYear >= 2024: for arc in linkElt.iterdescendants(tag="{http://www.xbrl.org/2003/linkbase}calculationArc"): if arc.get("{http://www.w3.org/1999/xlink}arcrole") != "https://www.xbrl.org/2023/arcrole/summation-item": val.modelXbrl.error("ESEF.3.4.1.IncorrectSummationItemArcroleUsed", From 598662d325b5495337749b8e08b742b02fc973c9 Mon Sep 17 00:00:00 2001 From: "Denis Robinet (ACSONE)" Date: Thu, 26 Sep 2024 13:43:59 +0200 Subject: [PATCH 9/9] [ESEF 2024] Restrict ESEF.1.4.1.differentExtensionDataType to only allow a single type in the wider/narrower relationship --- arelle/plugin/validate/ESEF/ESEF_Current/DTS.py | 9 +++++---- .../validate/ESEF/ESEF_Current/ValidateXbrlFinally.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py b/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py index fc2c2d2860..e6ab0a9833 100644 --- a/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py +++ b/arelle/plugin/validate/ESEF/ESEF_Current/DTS.py @@ -168,17 +168,18 @@ def lc3wordAdjust(word: str) -> str: widerConcept = widerNarrowerRelSet.fromModelObject(modelConcept) narrowerConcept = widerNarrowerRelSet.toModelObject(modelConcept) - widerTypes = set(r.toModelObject.baseXbrliType for r in widerConcept) - narrowerTypes = set(r.fromModelObject.baseXbrliType for r in narrowerConcept) + # Transform the qname to str for the later join() + widerTypes = set(str(r.toModelObject.typeQname) for r in widerConcept) + narrowerTypes = set(str(r.fromModelObject.typeQname) for r in narrowerConcept) - if (narrowerTypes and modelConcept.baseXbrliType not in narrowerTypes) or (widerTypes and modelConcept.baseXbrliType not in widerTypes): + if (narrowerTypes and narrowerTypes != {str(modelConcept.typeQname)}) or (widerTypes and widerTypes != {str(modelConcept.typeQname)}): widerNarrowerType = "{} {}".format( "Wider: {}".format(", ".join(widerTypes)) if widerTypes else "", "Narrower: {}".format(", ".join(narrowerTypes)) if narrowerTypes else "" ) val.modelXbrl.warning("ESEF.1.4.1.differentExtensionDataType", _("Issuers should anchor their extension elements to ESEF core taxonomy elements sharing the same data type. Concept: %(qname)s type: %(type)s %(widerNarrowerType)s"), - modelObject=modelConcept, qname=modelConcept.qname, type=modelConcept.baseXbrliType, widerNarrowerType=widerNarrowerType) + modelObject=modelConcept, qname=modelConcept.qname, type=modelConcept.typeQname, widerNarrowerType=widerNarrowerType) # check all lang's of standard label hasLc3Match = False diff --git a/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py b/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py index 0fba8db0ba..c9a896f12a 100644 --- a/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +++ b/arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py @@ -664,7 +664,7 @@ def checkFootnote(elt: ModelInlineFootnote | ModelResource, text: str) -> None: if not f.id: factsMissingId.append(f) escaped = f.get("escape") in ("true", "1") - if (f.concept.type.isTextBlock and not escaped) or (not f.concept.type.isTextBlock and escaped): + if escaped != f.concept.type.isTextBlock: modelXbrl.error("ESEF.2.2.7.improperApplicationOfEscapeAttribute", _("Facts with datatype 'dtr-types:textBlockItemType' MUST use the 'escape' attribute set to 'true'. Facts with any other datatype MUST use the 'escape' attribute set to 'false' - fact %(conceptName)s"), modelObject=f, conceptName=f.concept.qname)