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..e6ab0a9833 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
@@ -23,6 +24,7 @@
linkbaseRefTypes,
qnDomainItemTypes,
qnDomainItemTypes2023,
+ qnDomainItemTypes2024,
)
from ..Util import isChildOfNotes, isExtension, getDisclosureSystemYear
@@ -39,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:
@@ -49,6 +52,14 @@ def lc3wordAdjust(word: str) -> str:
return word[0].upper() + word[1:]
return word
+ 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]
+ esefTaxonomyYear = datetime.strptime(date, "%Y-%m-%d").year
+ break
+
if not isExtensionDoc:
pass
@@ -109,7 +120,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 esefDisclosureSystemYear < 2023:
+ esefDomainItemTypes = qnDomainItemTypes
+ elif esefDisclosureSystemYear == 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 +164,23 @@ def lc3wordAdjust(word: str) -> str:
extLineItemsWronglyAnchored.append(modelConcept)
if modelConcept.isMonetary and not modelConcept.balance:
extMonetaryConceptsWithoutBalance.append(modelConcept)
+ if esefDisclosureSystemYear >= 2024:
+ widerConcept = widerNarrowerRelSet.fromModelObject(modelConcept)
+ narrowerConcept = widerNarrowerRelSet.toModelObject(modelConcept)
+
+ # 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 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.typeQname, widerNarrowerType=widerNarrowerType)
+
# check all lang's of standard label
hasLc3Match = False
lc3names = []
@@ -204,9 +238,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:
+ if esefDisclosureSystemYear < 2023:
xbrlReference322 = "http://www.xbrl.org/dtr/type/nonNumeric-2009-12-16.xsd"
+ 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"
+
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 +316,12 @@ def lc3wordAdjust(word: str) -> str:
if linkEltName == "calculationLink":
val.hasExtensionCal = True
linkbasesFound.add(linkEltName)
+ 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",
+ _("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)
@@ -290,7 +334,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 788dea25d2..6408979326 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
@@ -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:
@@ -175,7 +176,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):
@@ -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 = {}
@@ -644,11 +654,24 @@ 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 esefDisclosureSystemYear >= 2024:
+ if not f.id:
+ factsMissingId.append(f)
+ escaped = f.get("escape") in ("true", "1")
+ 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)
+ 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 - fact %(conceptName)s"),
+ modelObject=f, value=f.value, conceptName=f.concept.qname)
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 +703,10 @@ def checkFootnote(elt: ModelInlineFootnote | ModelResource, text: str) -> None:
# conceptsUsed.add(dim.typedMember)
'''
+ 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]), )
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/__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,
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",
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">
+
+
+
+
+