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"> + + + + +