def validateValue(modelXbrl, elt, attrTag, baseXsdType, value, isNillable=False, isNil=False, facets=None): if baseXsdType: try: ''' if (len(value) == 0 and attrTag is None and not isNillable and baseXsdType not in ("anyType", "string", "normalizedString", "token", "NMTOKEN", "anyURI", "noContent")): raise ValueError("missing value for not nillable element") ''' xValid = VALID whitespaceReplace = (baseXsdType == "normalizedString") whitespaceCollapse = (not whitespaceReplace and baseXsdType != "string") isList = baseXsdType in {"IDREFS", "ENTITIES", "NMTOKENS"} if isList: baseXsdType = baseXsdType[:-1] # remove plural if facets: if "minLength" not in facets: facets = facets.copy() facets["minLength"] = 1 else: facets = {"minLength": 1} pattern = baseXsdTypePatterns.get(baseXsdType) if facets: if "pattern" in facets: pattern = facets["pattern"] # note multiple patterns are or'ed togetner, which isn't yet implemented! if "whiteSpace" in facets: whitespaceReplace, whitespaceCollapse = { "preserve": (False, False), "replace": (True, False), "collapse": (False, True) }[facets["whiteSpace"]] if whitespaceReplace: value = normalizeWhitespacePattern.sub( ' ', value) # replace tab, line feed, return with space elif whitespaceCollapse: value = collapseWhitespacePattern.sub(' ', value).strip( ' ' ) # collapse multiple spaces, tabs, line feeds and returns to single space if baseXsdType == "noContent": if len(value) > 0 and not entirelyWhitespacePattern.match( value): # only xml schema pattern whitespaces removed raise ValueError("value content not permitted") # note that sValue and xValue are not innerText but only text elements on specific element (or attribute) xValue = sValue = None xValid = VALID_NO_CONTENT # notify others that element may contain subelements (for stringValue needs) elif not value and isNil and isNillable: # rest of types get None if nil/empty value xValue = sValue = None else: if pattern is not None: if ((isList and any( pattern.match(v) is None for v in value.split())) or (not isList and pattern.match(value) is None)): raise ValueError( "pattern facet " + facets["pattern"].pattern if facets and "pattern" in facets else "pattern mismatch") if facets: if "enumeration" in facets and value not in facets[ "enumeration"]: raise ValueError("{0} is not in {1}".format( value, facets["enumeration"].keys())) if "length" in facets and len(value) != facets["length"]: raise ValueError("length {0}, expected {1}".format( len(value), facets["length"])) if "minLength" in facets and len( value) < facets["minLength"]: raise ValueError("length {0}, minLength {1}".format( len(value), facets["minLength"])) if "maxLength" in facets and len( value) > facets["maxLength"]: raise ValueError("length {0}, maxLength {1}".format( len(value), facets["maxLength"])) if baseXsdType in { "string", "normalizedString", "language", "languageOrEmpty", "token", "NMTOKEN", "Name", "NCName", "IDREF", "ENTITY" }: xValue = sValue = value elif baseXsdType == "ID": xValue = sValue = value xValid = VALID_ID elif baseXsdType == "anyURI": if value: # allow empty strings to be valid anyURIs if UrlUtil.relativeUrlPattern.match(value) is None: raise ValueError("IETF RFC 2396 4.3 syntax") # encode PSVI xValue similarly to Xerces and other implementations xValue = anyURI(UrlUtil.anyUriQuoteForPSVI(value)) sValue = value elif baseXsdType in ("decimal", "float", "double", "XBRLI_NONZERODECIMAL"): if baseXsdType in ("decimal", "XBRLI_NONZERODECIMAL"): if decimalPattern.match(value) is None: raise ValueError("lexical pattern mismatch") xValue = Decimal(value) sValue = float( value ) # s-value uses Number (float) representation if sValue == 0 and baseXsdType == "XBRLI_NONZERODECIMAL": raise ValueError("zero is not allowed") else: if floatPattern.match(value) is None: raise ValueError("lexical pattern mismatch") xValue = sValue = float(value) if facets: if "totalDigits" in facets and len( value.replace(".", "")) > facets["totalDigits"]: raise ValueError("totalDigits facet {0}".format( facets["totalDigits"])) if "fractionDigits" in facets and ( '.' in value and len(value[value.index('.') + 1:]) > facets["fractionDigits"]): raise ValueError( "fraction digits facet {0}".format( facets["fractionDigits"])) if "maxInclusive" in facets and xValue > facets[ "maxInclusive"]: raise ValueError(" > maxInclusive {0}".format( facets["maxInclusive"])) if "maxExclusive" in facets and xValue >= facets[ "maxExclusive"]: raise ValueError(" >= maxInclusive {0}".format( facets["maxExclusive"])) if "minInclusive" in facets and xValue < facets[ "minInclusive"]: raise ValueError(" < minInclusive {0}".format( facets["minInclusive"])) if "minExclusive" in facets and xValue <= facets[ "minExclusive"]: raise ValueError(" <= minExclusive {0}".format( facets["minExclusive"])) elif baseXsdType in { "integer", "nonPositiveInteger", "negativeInteger", "nonNegativeInteger", "positiveInteger", "long", "unsignedLong", "int", "unsignedInt", "short", "unsignedShort", "byte", "unsignedByte" }: xValue = sValue = _INT(value) if ((baseXsdType in { "nonNegativeInteger", "unsignedLong", "unsignedInt" } and xValue < 0) or (baseXsdType == "nonPositiveInteger" and xValue > 0) or (baseXsdType == "positiveInteger" and xValue <= 0) or (baseXsdType == "byte" and not -128 <= xValue < 127) or (baseXsdType == "unsignedByte" and not 0 <= xValue < 255) or (baseXsdType == "short" and not -32768 <= xValue < 32767) or (baseXsdType == "unsignedShort" and not 0 <= xValue < 65535) or (baseXsdType == "positiveInteger" and xValue <= 0)): raise ValueError("{0} is not {1}".format( value, baseXsdType)) if facets: if "totalDigits" in facets and len( value.replace(".", "")) > facets["totalDigits"]: raise ValueError("totalDigits facet {0}".format( facets["totalDigits"])) if "fractionDigits" in facets and ( '.' in value and len(value[value.index('.') + 1:]) > facets["fractionDigits"]): raise ValueError( "fraction digits facet {0}".format( facets["fractionDigits"])) if "maxInclusive" in facets and xValue > facets[ "maxInclusive"]: raise ValueError(" > maxInclusive {0}".format( facets["maxInclusive"])) if "maxExclusive" in facets and xValue >= facets[ "maxExclusive"]: raise ValueError(" >= maxInclusive {0}".format( facets["maxExclusive"])) if "minInclusive" in facets and xValue < facets[ "minInclusive"]: raise ValueError(" < minInclusive {0}".format( facets["minInclusive"])) if "minExclusive" in facets and xValue <= facets[ "minExclusive"]: raise ValueError(" <= minExclusive {0}".format( facets["minExclusive"])) elif baseXsdType == "boolean": if value in ("true", "1"): xValue = sValue = True elif value in ("false", "0"): xValue = sValue = False else: raise ValueError elif baseXsdType == "QName": xValue = qnameEltPfxName(elt, value, prefixException=ValueError) #xValue = qname(elt, value, castException=ValueError, prefixException=ValueError) sValue = value ''' not sure here, how are explicitDimensions validated, but bad units not? if xValue.namespaceURI in modelXbrl.namespaceDocs: if (xValue not in modelXbrl.qnameConcepts and xValue not in modelXbrl.qnameTypes and xValue not in modelXbrl.qnameAttributes and xValue not in modelXbrl.qnameAttributeGroups): raise ValueError("qname not defined " + str(xValue)) ''' elif baseXsdType == "enumerationHrefs": xValue = [qnameHref(href) for href in value.split()] sValue = value elif baseXsdType == "enumerationQNames": xValue = [ qnameEltPfxName(elt, qn, prefixException=ValueError) for qn in value.split() ] sValue = value elif baseXsdType in ("XBRLI_DECIMALSUNION", "XBRLI_PRECISIONUNION"): xValue = sValue = value if value == "INF" else _INT(value) elif baseXsdType in ("XBRLI_NONZERODECIMAL"): xValue = sValue = _INT(value) if xValue == 0: raise ValueError("invalid value") elif baseXsdType == "regex-pattern": # for facet compiling try: sValue = value if value in xmlSchemaPatterns: xValue = xmlSchemaPatterns[value] else: if r"\i" in value or r"\c" in value: value = value.replace(r"[\i-[:]]", iNameChar).replace(r"\i", iNameChar) \ .replace(r"[\c-[:]]", cMinusCNameChar).replace(r"\c", cNameChar) xValue = re_compile(value + "$") # must match whole string except Exception as err: raise ValueError(err) elif baseXsdType == "fraction": numeratorStr, denominatorStr = elt.fractionValue if numeratorStr == INVALIDixVALUE or denominatorStr == INVALIDixVALUE: sValue = xValue = INVALIDixVALUE xValid = INVALID else: sValue = value numeratorNum = float(numeratorStr) denominatorNum = float(denominatorStr) if numeratorNum.is_integer( ) and denominatorNum.is_integer(): xValue = Fraction(int(numeratorNum), int(denominatorNum)) else: xValue = Fraction(numeratorNum / denominatorNum) else: if baseXsdType in lexicalPatterns: match = lexicalPatterns[baseXsdType].match(value) if match is None: raise ValueError("lexical pattern mismatch") if baseXsdType == "XBRLI_DATEUNION": xValue = dateTime(value, type=DATEUNION, castException=ValueError) sValue = value elif baseXsdType == "dateTime": xValue = dateTime(value, type=DATETIME, castException=ValueError) sValue = value elif baseXsdType == "date": xValue = dateTime(value, type=DATE, castException=ValueError) sValue = value elif baseXsdType == "gMonthDay": month, day, zSign, zHrMin, zHr, zMin = match.groups( ) if int(day) > { 2: 29, 4: 30, 6: 30, 9: 30, 11: 30, 1: 31, 3: 31, 5: 31, 7: 31, 8: 31, 10: 31, 12: 31 }[int(month)]: raise ValueError( "invalid day {0} for month {1}".format( day, month)) xValue = gMonthDay(month, day) elif baseXsdType == "gYearMonth": year, month, zSign, zHrMin, zHr, zMin = match.groups( ) xValue = gYearMonth(year, month) elif baseXsdType == "gYear": year, zSign, zHrMin, zHr, zMin = match.groups() xValue = gYear(year) elif baseXsdType == "gMonth": month, zSign, zHrMin, zHr, zMin = match.groups() xValue = gMonth(month) elif baseXsdType == "gDay": day, zSign, zHrMin, zHr, zMin = match.groups() xValue = gDay(day) elif baseXsdType == "duration": xValue = isoDuration(value) else: xValue = value else: # no lexical pattern, forget compiling value xValue = value sValue = value except (ValueError, InvalidOperation) as err: if ModelInlineValueObject is not None and isinstance( elt, ModelInlineValueObject): errElt = "{0} fact {1}".format(elt.elementQname, elt.qname) else: errElt = elt.elementQname if attrTag: modelXbrl.error( "xmlSchema:valueError", _("Element %(element)s attribute %(attribute)s type %(typeName)s value error: %(value)s, %(error)s" ), modelObject=elt, element=errElt, attribute=XmlUtil.clarkNotationToPrefixedName( elt, attrTag, isAttribute=True), typeName=baseXsdType, value=strTruncate(value, 30), error=err) else: modelXbrl.error( "xmlSchema:valueError", _("Element %(element)s type %(typeName)s value error: %(value)s, %(error)s" ), modelObject=elt, element=errElt, typeName=baseXsdType, value=strTruncate(value, 30), error=err) xValue = None sValue = value xValid = INVALID else: xValue = sValue = None xValid = UNKNOWN if attrTag: try: # dynamically allocate attributes (otherwise given shared empty set) xAttributes = elt.xAttributes except AttributeError: elt.xAttributes = xAttributes = {} xAttributes[attrTag] = ModelAttribute(elt, attrTag, xValid, xValue, sValue, value) else: elt.xValid = xValid elt.xValue = xValue elt.sValue = sValue
def validateValue(modelXbrl, elt, attrTag, baseXsdType, value, isNillable=False, isNil=False, facets=None): if baseXsdType: try: ''' if (len(value) == 0 and attrTag is None and not isNillable and baseXsdType not in ("anyType", "string", "normalizedString", "token", "NMTOKEN", "anyURI", "noContent")): raise ValueError("missing value for not nillable element") ''' xValid = VALID whitespaceReplace = (baseXsdType == "normalizedString") whitespaceCollapse = (not whitespaceReplace and baseXsdType != "string") isList = baseXsdType in {"IDREFS", "ENTITIES", "NMTOKENS"} if isList: baseXsdType = baseXsdType[:-1] # remove plural pattern = baseXsdTypePatterns.get(baseXsdType) if facets: if "pattern" in facets: pattern = facets["pattern"] # note multiple patterns are or'ed togetner, which isn't yet implemented! if "whiteSpace" in facets: whitespaceReplace, whitespaceCollapse = {"preserve":(False,False), "replace":(True,False), "collapse":(False,True)}[facets["whiteSpace"]] if whitespaceReplace: value = normalizeWhitespacePattern.sub(' ', value) elif whitespaceCollapse: value = collapseWhitespacePattern.sub(' ', value.strip()) if baseXsdType == "noContent": if len(value) > 0 and not value.isspace(): raise ValueError("value content not permitted") # note that sValue and xValue are not innerText but only text elements on specific element (or attribute) xValue = sValue = None xValid = VALID_NO_CONTENT # notify others that element may contain subelements (for stringValue needs) elif not value and isNil and isNillable: # rest of types get None if nil/empty value xValue = sValue = None else: if pattern is not None: if ((isList and any(pattern.match(v) is None for v in value.split())) or (not isList and pattern.match(value) is None)): raise ValueError("pattern facet " + facets["pattern"].pattern if facets and "pattern" in facets else "pattern mismatch") if facets: if "enumeration" in facets and value not in facets["enumeration"]: raise ValueError("{0} is not in {1}".format(value, facets["enumeration"].keys())) if "length" in facets and len(value) != facets["length"]: raise ValueError("length {0}, expected {1}".format(len(value), facets["length"])) if "minLength" in facets and len(value) < facets["minLength"]: raise ValueError("length {0}, minLength {1}".format(len(value), facets["minLength"])) if "maxLength" in facets and len(value) > facets["maxLength"]: raise ValueError("length {0}, maxLength {1}".format(len(value), facets["maxLength"])) if baseXsdType in {"string", "normalizedString", "language", "token", "NMTOKEN","Name","NCName","IDREF","ENTITY"}: xValue = sValue = value elif baseXsdType == "ID": xValue = sValue = value xValid = VALID_ID elif baseXsdType == "anyURI": if value: # allow empty strings to be valid anyURIs if UrlUtil.relativeUrlPattern.match(value) is None: raise ValueError("IETF RFC 2396 4.3 syntax") # encode PSVI xValue similarly to Xerces and other implementations xValue = anyURI(UrlUtil.anyUriQuoteForPSVI(value)) sValue = value elif baseXsdType in ("decimal", "float", "double"): if baseXsdType == "decimal": if decimalPattern.match(value) is None: raise ValueError("lexical pattern mismatch") xValue = Decimal(value) sValue = float(value) # s-value uses Number (float) representation else: if floatPattern.match(value) is None: raise ValueError("lexical pattern mismatch") xValue = sValue = float(value) if facets: if "totalDigits" in facets and len(value.replace(".","")) > facets["totalDigits"]: raise ValueError("totalDigits facet {0}".format(facets["totalDigits"])) if "fractionDigits" in facets and ( '.' in value and len(value[value.index('.') + 1:]) > facets["fractionDigits"]): raise ValueError("fraction digits facet {0}".format(facets["fractionDigits"])) if "maxInclusive" in facets and xValue > facets["maxInclusive"]: raise ValueError(" > maxInclusive {0}".format(facets["maxInclusive"])) if "maxExclusive" in facets and xValue >= facets["maxExclusive"]: raise ValueError(" >= maxInclusive {0}".format(facets["maxExclusive"])) if "minInclusive" in facets and xValue < facets["minInclusive"]: raise ValueError(" < minInclusive {0}".format(facets["minInclusive"])) if "minExclusive" in facets and xValue <= facets["minExclusive"]: raise ValueError(" <= minExclusive {0}".format(facets["minExclusive"])) elif baseXsdType in {"integer", "nonPositiveInteger","negativeInteger","nonNegativeInteger","positiveInteger", "long","unsignedLong", "int","unsignedInt", "short","unsignedShort", "byte","unsignedByte"}: xValue = sValue = _INT(value) if ((baseXsdType in {"nonNegativeInteger","unsignedLong","unsignedInt"} and xValue < 0) or (baseXsdType == "nonPositiveInteger" and xValue > 0) or (baseXsdType == "positiveInteger" and xValue <= 0) or (baseXsdType == "byte" and not -128 <= xValue < 127) or (baseXsdType == "unsignedByte" and not 0 <= xValue < 255) or (baseXsdType == "short" and not -32768 <= xValue < 32767) or (baseXsdType == "unsignedShort" and not 0 <= xValue < 65535) or (baseXsdType == "positiveInteger" and xValue <= 0)): raise ValueError("{0} is not {1}".format(value, baseXsdType)) if facets: if "totalDigits" in facets and len(value.replace(".","")) > facets["totalDigits"]: raise ValueError("totalDigits facet {0}".format(facets["totalDigits"])) if "fractionDigits" in facets and ( '.' in value and len(value[value.index('.') + 1:]) > facets["fractionDigits"]): raise ValueError("fraction digits facet {0}".format(facets["fractionDigits"])) if "maxInclusive" in facets and xValue > facets["maxInclusive"]: raise ValueError(" > maxInclusive {0}".format(facets["maxInclusive"])) if "maxExclusive" in facets and xValue >= facets["maxExclusive"]: raise ValueError(" >= maxInclusive {0}".format(facets["maxExclusive"])) if "minInclusive" in facets and xValue < facets["minInclusive"]: raise ValueError(" < minInclusive {0}".format(facets["minInclusive"])) if "minExclusive" in facets and xValue <= facets["minExclusive"]: raise ValueError(" <= minExclusive {0}".format(facets["minExclusive"])) elif baseXsdType == "boolean": if value in ("true", "1"): xValue = sValue = True elif value in ("false", "0"): xValue = sValue = False else: raise ValueError elif baseXsdType == "QName": xValue = qnameEltPfxName(elt, value, prefixException=ValueError) #xValue = qname(elt, value, castException=ValueError, prefixException=ValueError) sValue = value ''' not sure here, how are explicitDimensions validated, but bad units not? if xValue.namespaceURI in modelXbrl.namespaceDocs: if (xValue not in modelXbrl.qnameConcepts and xValue not in modelXbrl.qnameTypes and xValue not in modelXbrl.qnameAttributes and xValue not in modelXbrl.qnameAttributeGroups): raise ValueError("qname not defined " + str(xValue)) ''' elif baseXsdType in ("XBRLI_DECIMALSUNION", "XBRLI_PRECISIONUNION"): xValue = sValue = value if value == "INF" else _INT(value) elif baseXsdType in ("XBRLI_NONZERODECIMAL"): xValue = sValue = _INT(value) if xValue == 0: raise ValueError("invalid value") elif baseXsdType == "XBRLI_DATEUNION": xValue = dateTime(value, type=DATEUNION, castException=ValueError) sValue = value elif baseXsdType == "dateTime": xValue = dateTime(value, type=DATETIME, castException=ValueError) sValue = value elif baseXsdType == "date": xValue = dateTime(value, type=DATE, castException=ValueError) sValue = value elif baseXsdType == "regex-pattern": # for facet compiling try: sValue = value if value in xmlSchemaPatterns: xValue = xmlSchemaPatterns[value] else: if r"\i" in value or r"\c" in value: value = value.replace(r"\i", iNameChar).replace(r"\c", cNameChar) xValue = re_compile(value + "$") # must match whole string except Exception as err: raise ValueError(err) elif baseXsdType == "fraction": sValue = value xValue = Fraction("/".join(elt.fractionValue)) else: if baseXsdType in lexicalPatterns: match = lexicalPatterns[baseXsdType].match(value) if match is None: raise ValueError("lexical pattern mismatch") if baseXsdType == "gMonthDay": month, day, zSign, zHrMin, zHr, zMin = match.groups() if int(day) > {2:29, 4:30, 6:30, 9:30, 11:30, 1:31, 3:31, 5:31, 7:31, 8:31, 10:31, 12:31}[int(month)]: raise ValueError("invalid day {0} for month {1}".format(day, month)) xValue = gMonthDay(month, day) elif baseXsdType == "gYearMonth": year, month, zSign, zHrMin, zHr, zMin = match.groups() xValue = gYearMonth(year, month) elif baseXsdType == "gYear": year, zSign, zHrMin, zHr, zMin = match.groups() xValue = gYear(year) elif baseXsdType == "gMonth": month, zSign, zHrMin, zHr, zMin = match.groups() xValue = gMonth(month) elif baseXsdType == "gDay": day, zSign, zHrMin, zHr, zMin = match.groups() xValue = gDay(day) else: xValue = value else: # no lexical pattern, forget compiling value xValue = value sValue = value except (ValueError, InvalidOperation) as err: if ModelInlineValueObject is not None and isinstance(elt, ModelInlineValueObject): errElt = "{0} fact {1}".format(elt.elementQname, elt.qname) else: errElt = elt.elementQname if attrTag: modelXbrl.error("xmlSchema:valueError", _("Element %(element)s attribute %(attribute)s type %(typeName)s value error: %(value)s, %(error)s"), modelObject=elt, element=errElt, attribute=XmlUtil.clarkNotationToPrefixedName(elt,attrTag,isAttribute=True), typeName=baseXsdType, value=strTruncate(value, 30), error=err) else: modelXbrl.error("xmlSchema:valueError", _("Element %(element)s type %(typeName)s value error: %(value)s, %(error)s"), modelObject=elt, element=errElt, typeName=baseXsdType, value=strTruncate(value, 30), error=err) xValue = None sValue = value xValid = INVALID else: xValue = sValue = None xValid = UNKNOWN if attrTag: try: # dynamically allocate attributes (otherwise given shared empty set) xAttributes = elt.xAttributes except AttributeError: elt.xAttributes = xAttributes = {} xAttributes[attrTag] = ModelAttribute(elt, attrTag, xValid, xValue, sValue, value) else: elt.xValid = xValid elt.xValue = xValue elt.sValue = sValue
def validateXbrlFinally(val, *args, **kwargs): if not (val.validateROSplugin): return _xhtmlNs = "{{{}}}".format(xhtml) _xhtmlNsLen = len(_xhtmlNs) modelXbrl = val.modelXbrl modelDocument = modelXbrl.modelDocument if not modelDocument: return # never loaded properly _statusMsg = _("validating {0} filing rules").format( val.disclosureSystem.name) modelXbrl.profileActivity() modelXbrl.modelManager.showStatus(_statusMsg) if modelDocument.type == ModelDocument.Type.INSTANCE: modelXbrl.error("ROS:instanceMustBeInlineXBRL", _("ROS expects inline XBRL instances."), modelObject=modelXbrl) if modelDocument.type in (ModelDocument.Type.INLINEXBRL, ModelDocument.Type.INLINEXBRLDOCUMENTSET): for ixdsHtmlRootElt in modelXbrl.ixdsHtmlElements: # ix root elements for all ix docs in IXDS ixNStag = ixdsHtmlRootElt.modelDocument.ixNStag ixTags = set(ixNStag + ln for ln in ("nonNumeric", "nonFraction", "references", "relationship")) transformRegistryErrors = set() ixTargets = set() for ixdsHtmlRootElt in modelXbrl.ixdsHtmlElements: for elt in ixdsHtmlRootElt.iter(): if isinstance( elt, (_ElementTree, _Comment, _ProcessingInstruction)): continue # comment or other non-parsed element if isinstance(elt, ModelInlineFact): if elt.format is not None and elt.format.namespaceURI not in TRnamespaces: transformRegistryErrors.add(elt) if elt.get("escape") in ("true", "1"): modelXbrl.error( "ROS.escapedHTML", _("Escaped (x)html fact content is not supported: %(element)s" ), modelObject=elt, element=eltTag) eltTag = elt.tag if eltTag in ixTags: ixTargets.add(elt.get("target")) else: if eltTag.startswith(_xhtmlNs): eltTag = eltTag[_xhtmlNsLen:] if eltTag == "link" and elt.get("type") == "text/css": modelXbrl.error( "ROS.externalCssStyle", _("CSS must be embedded in the inline XBRL document: %(element)s" ), modelObject=elt, element=eltTag) elif ((eltTag in ("object", "script")) or (eltTag == "a" and "javascript:" in elt.get("href", "")) or (eltTag == "img" and "javascript:" in elt.get("src", ""))): modelXbrl.error( "ROS.embeddedCode", _("Inline XBRL documents MUST NOT contain embedded code: %(element)s" ), modelObject=elt, element=eltTag) elif eltTag == "img": src = elt.get("src", "").strip() if not src.startswith("data:image"): modelXbrl.warning( "ROS.embeddedCode", _("Images should be inlined as a base64-encoded string: %(element)s" ), modelObject=elt, element=eltTag) if len(ixTargets) > 1: modelXbrl.error( "ROS:singleOutputDocument", _("Multiple target instance documents are not supported: %(targets)s." ), modelObject=modelXbrl, targets=", ".join((t or "(default)") for t in ixTargets)) filingTypes = set() unexpectedTaxonomyReferences = set() numIxDocs = 0 for doc in modelXbrl.urlDocs.values(): if doc.type == ModelDocument.Type.INLINEXBRL: # base file extension _baseName, _baseExt = os.path.splitext(doc.basename) if _baseExt not in (".xhtml", ".html", "htm", "ixbrl", "xml", "xhtml"): modelXbrl.error( "ROS.fileNameExtension", _("The list of acceptable file extensions for upload is: html, htm, ixbrl, xml, xhtml: %(fileName)s" ), modelObject=doc, fileName=doc.basename) # document encoding if doc.documentEncoding.lower() != "utf-8": modelXbrl.error( "ROS.documentEncoding", _("iXBRL documents submitted to Revenue should be UTF-8 encoded: %(encoding)s" ), modelObject=doc, encoding=doc.documentEncoding) # identify type of filing for referencedDoc in doc.referencesDocument.keys(): if referencedDoc.type == ModelDocument.Type.SCHEMA: if referencedDoc.uri in taxonomyReferences: filingTypes.add(referencedDoc.uri) else: unexpectedTaxonomyReferences.add(referencedDoc.uri) # count of inline docs in IXDS numIxDocs += 1 if len(filingTypes) != 1: modelXbrl.error( "ROS:multipleFilingTypes", _("Multiple filing types detected: %(filingTypes)s."), modelObject=modelXbrl, filingTypes=", ".join(sorted(filingTypes))) if unexpectedTaxonomyReferences: modelXbrl.error( "ROS:unexpectedTaxonomyReferences", _("Referenced schema(s) does not map to a taxonomy supported by Revenue (schemaRef): %(unexpectedReferences)s." ), modelObject=modelXbrl, unexpectedReferences=", ".join( sorted(unexpectedTaxonomyReferences))) # single document IXDS if numIxDocs > 1: modelXbrl.warning( "ROS:multipleInlineDocuments", _("A single inline document should be submitted but %(numberDocs)s were found." ), modelObject=modelXbrl, numberDocs=numIxDocs) # build namespace maps nsMap = {} for prefix in ("ie-common", "bus", "uk-bus", "ie-dpl", "core"): if prefix in modelXbrl.prefixedNamespaces: nsMap[prefix] = modelXbrl.prefixedNamespaces[prefix] # build mandatory table by ns qname in use mandatory = set() for prefix in mandatoryElements: if prefix in nsMap: ns = nsMap[prefix] for localName in mandatoryElements[prefix]: mandatory.add(qname(ns, prefix + ":" + localName)) equivalentManatoryQNames = [[ qname(q, nsMap) for q in equivElts ] for equivElts in equivalentMandatoryElements] # document creator requirement if "bus" in nsMap: if qname("bus:NameProductionSoftware", nsMap) not in modelXbrl.factsByQname or qname( "bus:VersionProductionSoftware", nsMap) not in modelXbrl.factsByQname: modelXbrl.warning( "ROS:documentCreatorProductInformation", _("Please use the NameProductionSoftware tag to identify the software package and the VersionProductionSoftware tag to identify the version of the software package." ), modelObject=modelXbrl) elif "uk-bus" in nsMap: if qname("uk-bus:NameAuthor", nsMap) not in modelXbrl.factsByQname or qname( "uk-bus:DescriptionOrTitleAuthor", nsMap) not in modelXbrl.factsByQname: modelXbrl.warning( "ROS:documentCreatorProductInformation", _("Revenue request that vendor, product and version information is embedded in the generated inline XBRL document using a single XBRLDocumentAuthorGrouping tuple. The NameAuthor tag should be used to identify the name and version of the software package." ), modelObject=modelXbrl) schemeEntityIds = set() mapContext = {} # identify unique contexts and units mapUnit = {} uniqueContextHashes = {} hasCRO = False unsupportedSchemeContexts = [] mismatchIdentifierContexts = [] for context in modelXbrl.contexts.values(): schemeEntityIds.add(context.entityIdentifier) scheme, entityId = context.entityIdentifier if scheme not in schemePatterns: unsupportedSchemeContexts.append(context) elif not schemePatterns[scheme].match(entityId): mismatchIdentifierContexts.append(context) if scheme == "http://www.cro.ie/": hasCRO = True h = context.contextDimAwareHash if h in uniqueContextHashes: if context.isEqualTo(uniqueContextHashes[h]): mapContext[context] = uniqueContextHashes[h] else: uniqueContextHashes[h] = context del uniqueContextHashes if len(schemeEntityIds) > 1: modelXbrl.error( "ROS:differentContextEntityIdentifiers", _("Context entity identifier not all the same: %(schemeEntityIds)s." ), modelObject=modelXbrl, schemeEntityIds=", ".join( sorted(str(s) for s in schemeEntityIds))) if unsupportedSchemeContexts: modelXbrl.error( "ROS:unsupportedContextEntityIdentifierScheme", _("Context identifier scheme(s) is not supported: %(schemes)s." ), modelObject=unsupportedSchemeContexts, schemes=", ".join( sorted( set(c.entityIdentifier[0] for c in unsupportedSchemeContexts)))) if mismatchIdentifierContexts: modelXbrl.error( "ROS:invalidContextEntityIdentifier", _("Context entity identifier(s) lexically invalid: %(identifiers)s." ), modelObject=mismatchIdentifierContexts, identifiers=", ".join( sorted( set(c.entityIdentifier[1] for c in mismatchIdentifierContexts)))) uniqueUnitHashes = {} for unit in modelXbrl.units.values(): h = unit.hash if h in uniqueUnitHashes: if unit.isEqualTo(uniqueUnitHashes[h]): mapUnit[unit] = uniqueUnitHashes[h] else: uniqueUnitHashes[h] = unit del uniqueUnitHashes if hasCRO and "ie-common" in nsMap: mandatory.add( qname("ie-common:CompaniesRegistrationOfficeNumber", nsMap)) reportedMandatory = set() numFactsByConceptContextUnit = defaultdict(list) for qn, facts in modelXbrl.factsByQname.items(): if qn in mandatory: reportedMandatory.add(qn) for f in facts: if f.isNumeric and f.parentElement.qname == qnXbrliXbrl: numFactsByConceptContextUnit[(f.qname, mapContext.get( f.context, f.context), mapUnit.get( f.unit, f.unit))].append(f) missingElements = ( mandatory - reportedMandatory ) # | (reportedFootnoteIfNil - reportedFootnoteIfNil) for qnames in equivalentManatoryQNames: # remove missing elements for which an or-match was reported if any(qn in modelXbrl.factsByQname for qn in qnames): for qn in qnames: missingElements.discard(qn) if missingElements: modelXbrl.error( "ROS:missingRequiredElements", _("Required elements missing from document: %(elements)s."), modelObject=modelXbrl, elements=", ".join(sorted(str(qn) for qn in missingElements))) for fList in numFactsByConceptContextUnit.values(): if len(fList) > 1: f0 = fList[0] if any(f.isNil for f in fList): _inConsistent = not all(f.isNil for f in fList) elif all( inferredDecimals(f) == inferredDecimals(f0) for f in fList[1:]): # same decimals v0 = rangeValue(f0.value) _inConsistent = not all( rangeValue(f.value) == v0 for f in fList[1:]) else: # not all have same decimals aMax, bMin = rangeValue(f0.value, inferredDecimals(f0)) for f in fList[1:]: a, b = rangeValue(f.value, inferredDecimals(f)) if a > aMax: aMax = a if b < bMin: bMin = b _inConsistent = (bMin < aMax) if _inConsistent: modelXbrl.error( ("ROS.inconsistentDuplicateFacts"), "Inconsistent duplicate numeric facts: %(fact)s were used more than once in contexts equivalent to %(contextID)s: values %(values)s. ", modelObject=fList, fact=f0.qname, contextID=f0.contextID, values=", ".join( strTruncate(f.value, 128) for f in fList)) modelXbrl.profileActivity(_statusMsg, minTimeToShow=0.0) modelXbrl.modelManager.showStatus(None)
def validateXbrlFinally(val, *args, **kwargs): if not (val.validateESEFplugin): return _xhtmlNs = "{{{}}}".format(xhtml) _xhtmlNsLen = len(_xhtmlNs) modelXbrl = val.modelXbrl modelDocument = modelXbrl.modelDocument _statusMsg = _("validating {0} filing rules").format( val.disclosureSystem.name) modelXbrl.profileActivity() modelXbrl.modelManager.showStatus(_statusMsg) reportXmlLang = None firstRootmostXmlLangDepth = 9999999 _ifrsNs = None for targetNs in modelXbrl.namespaceDocs.keys(): if ifrsNsPattern.match(targetNs): _ifrsNs = targetNs if not _ifrsNs: modelXbrl.error("ESEF.RTS.ifrsRequired", _("RTS on ESEF requires IFRS taxonomy."), modelObject=modelXbrl) return esefPrimaryStatementPlaceholders = set( qname(_ifrsNs, n) for n in esefPrimaryStatementPlaceholderNames) esefMandatoryElements2020 = set( qname(_ifrsNs, n) for n in esefMandatoryElementNames2020) if modelDocument.type == ModelDocument.Type.INSTANCE: modelXbrl.error("ESEF.I.1.instanceShallBeInlineXBRL", _("RTS on ESEF requires inline XBRL instances."), modelObject=modelXbrl) checkFilingDimensions( val) # sets up val.primaryItems and val.domainMembers val.hasExtensionSchema = val.hasExtensionPre = val.hasExtensionCal = val.hasExtensionDef = val.hasExtensionLbl = False checkFilingDTS(val, modelXbrl.modelDocument, []) modelXbrl.profileActivity("... filer DTS checks", minTimeToShow=1.0) if not (val.hasExtensionSchema and val.hasExtensionPre and val.hasExtensionCal and val.hasExtensionDef and val.hasExtensionLbl): missingFiles = [] if not val.hasExtensionSchema: missingFiles.append("schema file") if not val.hasExtensionPre: missingFiles.append("presentation linkbase") if not val.hasExtensionCal: missingFiles.append("calculation linkbase") if not val.hasExtensionDef: missingFiles.append("definition linkbase") if not val.hasExtensionLbl: missingFiles.append("label linkbase") modelXbrl.warning( "ESEF.3.1.1.extensionTaxonomyWrongFilesStructure", _("Extension taxonomies MUST consist of at least a schema file and presentation, calculation, definition and label linkbases" ": missing %(missingFiles)s"), modelObject=modelXbrl, missingFiles=", ".join(missingFiles)) #if modelDocument.type == ModelDocument.Type.INLINEXBRLDOCUMENTSET: # # reports only under reports, none elsewhere # modelXbrl.fileSource.dir if modelDocument.type in (ModelDocument.Type.INLINEXBRL, ModelDocument.Type.INLINEXBRLDOCUMENTSET, ModelDocument.Type.INSTANCE): footnotesRelationshipSet = modelXbrl.relationshipSet("XBRL-footnotes") orphanedFootnotes = set() noLangFootnotes = set() factLangFootnotes = defaultdict(set) footnoteRoleErrors = set() transformRegistryErrors = set() def checkFootnote(elt, text): if text: # non-empty footnote must be linked to a fact if not empty if not any( isinstance(rel.fromModelObject, ModelFact) for rel in footnotesRelationshipSet.toModelObject(elt)): orphanedFootnotes.add(elt) lang = elt.xmlLang if not lang: noLangFootnotes.add(elt) else: for rel in footnotesRelationshipSet.toModelObject(elt): if rel.fromModelObject is not None: factLangFootnotes[rel.fromModelObject].add(lang) if elt.role != XbrlConst.footnote or not all( rel.arcrole == XbrlConst.factFootnote and rel.linkrole == XbrlConst.defaultLinkRole for rel in footnotesRelationshipSet.toModelObject(elt)): footnoteRoleErrors.add(elt) # check file name of each inline document (which might be below a top-level IXDS) for doc in modelXbrl.urlDocs.values(): if doc.type == ModelDocument.Type.INLINEXBRL: _baseName, _baseExt = os.path.splitext(doc.basename) if _baseExt not in (".xhtml", ".html"): modelXbrl.warning( "ESEF.RTS.Art.3.fileNameExtension", _("FileName SHOULD have the extension .xhtml or .html: %(fileName)s" ), modelObject=doc, fileName=doc.basename) docinfo = doc.xmlRootElement.getroottree().docinfo if " html" in docinfo.doctype: modelXbrl.warning( "ESEF.RTS.Art.3.htmlDoctype", _("Doctype SHOULD NOT be html: %(fileName)s"), modelObject=doc, fileName=doc.basename) if modelDocument.type in (ModelDocument.Type.INLINEXBRL, ModelDocument.Type.INLINEXBRLDOCUMENTSET): hiddenEltIds = {} presentedHiddenEltIds = defaultdict(list) eligibleForTransformHiddenFacts = [] requiredToDisplayFacts = [] requiredToDisplayFactIds = {} firstIxdsDoc = True for ixdsHtmlRootElt in modelXbrl.ixdsHtmlElements: # ix root elements for all ix docs in IXDS ixNStag = ixdsHtmlRootElt.modelDocument.ixNStag ixTags = set(ixNStag + ln for ln in ("nonNumeric", "nonFraction", "references", "relationship")) ixTextTags = set(ixNStag + ln for ln in ("nonFraction", "continuation", "footnote")) ixExcludeTag = ixNStag + "exclude" ixTupleTag = ixNStag + "tuple" ixFractionTag = ixNStag + "fraction" for elt, depth in etreeIterWithDepth(ixdsHtmlRootElt): eltTag = elt.tag if isinstance( elt, (_ElementTree, _Comment, _ProcessingInstruction)): continue # comment or other non-parsed element else: eltTag = elt.tag if eltTag.startswith(_xhtmlNs): eltTag = eltTag[_xhtmlNsLen:] if firstIxdsDoc and (not reportXmlLang or depth < firstRootmostXmlLangDepth): xmlLang = elt.get( "{http://www.w3.org/XML/1998/namespace}lang" ) if xmlLang: reportXmlLang = xmlLang firstRootmostXmlLangDepth = depth if ((eltTag in ("object", "script")) or (eltTag == "a" and "javascript:" in elt.get("href", "")) or (eltTag == "img" and "javascript:" in elt.get("src", ""))): modelXbrl.error( "ESEF.2.5.1.executableCodePresent", _("Inline XBRL documents MUST NOT contain executable code: %(element)s" ), modelObject=elt, element=eltTag) elif eltTag == "img": src = elt.get("src", "").strip() hasParentIxTextTag = False # check if image is in an ix text-bearing element _ancestorElt = elt while (_ancestorElt is not None): if _ancestorElt.tag == ixExcludeTag: # excluded from any parent text-bearing ix element break if _ancestorElt.tag in ixTextTags: hasParentIxTextTag = True break _ancestorElt = _ancestorElt.getparent() if scheme(src) in ("http", "https", "ftp"): modelXbrl.error( "ESEF.3.5.1.inlinXbrlContainsExternalReferences", _("Inline XBRL instance documents MUST NOT contain any reference pointing to resources outside the reporting package: %(element)s" ), modelObject=elt, element=eltTag) elif not src.startswith("data:image"): if hasParentIxTextTag: modelXbrl.error( "ESEF.2.5.1.imageInIXbrlElementNotEmbedded", _("Images appearing within an inline XBRL element MUST be embedded regardless of their size." ), modelObject=elt) else: # presume it to be an image file, check image contents try: base = elt.modelDocument.baseForElement( elt) normalizedUri = elt.modelXbrl.modelManager.cntlr.webCache.normalizeUrl( src, base) if not elt.modelXbrl.fileSource.isInArchive( normalizedUri): normalizedUri = elt.modelXbrl.modelManager.cntlr.webCache.getfilename( normalizedUri) imglen = 0 with elt.modelXbrl.fileSource.file( normalizedUri, binary=True)[0] as fh: imglen += len(fh.read()) if imglen < browserMaxBase64ImageLength: modelXbrl.error( "ESEF.2.5.1.embeddedImageNotUsingBase64Encoding", _("Images MUST be included in the XHTML document as a base64 encoded string unless their size exceeds support of browsers (%(maxImageSize)s): %(file)s." ), modelObject=elt, maxImageSize= browserMaxBase64ImageLength, file=os.path.basename( normalizedUri)) except IOError as err: modelXbrl.error( "ESEF.2.5.1.imageFileCannotBeLoaded", _("Image file which isn't openable '%(src)s', error: %(error)s" ), modelObject=elt, src=src, error=err) elif not any( src.startswith(m) for m in allowedImgMimeTypes): modelXbrl.error( "ESEF.2.5.1.embeddedImageNotUsingBase64Encoding", _("Images MUST be included in the XHTML document as a base64 encoded string, encoding disallowed: %(src)s." ), modelObject=elt, src=attrValue[:128]) elif eltTag == "a": href = elt.get("href", "").strip() if scheme(href) in ("http", "https", "ftp"): modelXbrl.error( "ESEF.3.5.1.inlinXbrlContainsExternalReferences", _("Inline XBRL instance documents MUST NOT contain any reference pointing to resources outside the reporting package: %(element)s" ), modelObject=elt, element=eltTag) elif eltTag == "base" or elt.tag == "{http://www.w3.org/XML/1998/namespace}base": modelXbrl.error( "ESEF.2.4.2.htmlOrXmlBaseUsed", _("The HTML <base> elements and xml:base attributes MUST NOT be used in the Inline XBRL document." ), modelObject=elt, element=eltTag) elif eltTag == "link" and elt.get( "type") == "text/css": if len(modelXbrl.ixdsHtmlElements) > 1: f = elt.get("href") if not f or isHttpUrl(f) or os.path.isabs(f): modelXbrl.warning( "ESEF.2.5.4.externalCssReportPackage", _("The CSS file should be physically stored within the report package: %{file}s." ), modelObject=elt, file=f) else: modelXbrl.error( "ESEF.2.5.4.externalCssFileForSingleIXbrlDocument", _("Where an Inline XBRL document set contains a single document, the CSS MUST be embedded within the document." ), modelObject=elt, element=eltTag) elif eltTag == "style" and elt.get( "type") == "text/css": if len(modelXbrl.ixdsHtmlElements) > 1: modelXbrl.warning( "ESEF.2.5.4.embeddedCssForMultiHtmlIXbrlDocumentSets", _("Where an Inline XBRL document set contains multiple documents, the CSS SHOULD be defined in a separate file." ), modelObject=elt, element=eltTag) if eltTag in ixTags and elt.get("target"): modelXbrl.error( "ESEF.2.5.3.targetAttributeUsed", _("Target attribute MUST not be used: element %(localName)s, target attribute %(target)s." ), modelObject=elt, localName=elt.elementQname, target=elt.get("target")) if eltTag == ixTupleTag: modelXbrl.error( "ESEF.2.4.1.tupleElementUsed", _("The ix:tuple element MUST not be used in the Inline XBRL document: %(qname)s." ), modelObject=elt, qname=elt.qname) if eltTag == ixFractionTag: modelXbrl.error( "ESEF.2.4.1.fractionElementUsed", _("The ix:fraction element MUST not be used in the Inline XBRL document." ), modelObject=elt) if elt.get("{http://www.w3.org/XML/1998/namespace}base" ) is not None: modelXbrl.error( "ESEF.2.4.1.xmlBaseUsed", _("xml:base attributes MUST NOT be used in the Inline XBRL document: element %(localName)s, base attribute %(base)s." ), modelObject=elt, localName=elt.elementQname, base=elt.get( "{http://www.w3.org/XML/1998/namespace}base")) if isinstance(elt, ModelInlineFootnote): checkFootnote(elt, elt.value) elif isinstance( elt, ModelResource ) and elt.qname == XbrlConst.qnLinkFootnote: checkFootnote(elt, elt.value) elif isinstance(elt, ModelInlineFact): if elt.format is not None and elt.format.namespaceURI not in IXT_NAMESPACES: transformRegistryErrors.add(elt) for ixHiddenElt in ixdsHtmlRootElt.iterdescendants( tag=ixNStag + "hidden"): for tag in (ixNStag + "nonNumeric", ixNStag + "nonFraction"): for ixElt in ixHiddenElt.iterdescendants(tag=tag): if ( getattr(ixElt, "xValid", 0) >= VALID # may not be validated ): # add future "and" conditions on elements which can be in hidden if (ixElt.concept.baseXsdType not in untransformableTypes and not ixElt.isNil): eligibleForTransformHiddenFacts.append( ixElt) elif ixElt.id is None: requiredToDisplayFacts.append(ixElt) if ixElt.id: hiddenEltIds[ixElt.id] = ixElt firstIxdsDoc = False if eligibleForTransformHiddenFacts: modelXbrl.warning( "ESEF.2.4.1.transformableElementIncludedInHiddenSection", _("The ix:hidden section of Inline XBRL document MUST not include elements eligible for transformation. " "%(countEligible)s fact(s) were eligible for transformation: %(elements)s" ), modelObject=eligibleForTransformHiddenFacts, countEligible=len(eligibleForTransformHiddenFacts), elements=", ".join( sorted( set( str(f.qname) for f in eligibleForTransformHiddenFacts)))) for ixdsHtmlRootElt in modelXbrl.ixdsHtmlElements: for ixElt in ixdsHtmlRootElt.getroottree().iterfind( "//{http://www.w3.org/1999/xhtml}*[@style]"): hiddenFactRefMatch = styleIxHiddenPattern.match( ixElt.get("style", "")) if hiddenFactRefMatch: hiddenFactRef = hiddenFactRefMatch.group(2) if hiddenFactRef not in hiddenEltIds: modelXbrl.error( "ESEF.2.4.1.esefIxHiddenStyleNotLinkingFactInHiddenSection", _("\"-esef-ix-hidden\" style identifies @id, %(id)s of a fact that is not in ix:hidden section." ), modelObject=ixElt, id=hiddenFactRef) else: presentedHiddenEltIds[hiddenFactRef].append(ixElt) for hiddenEltId, ixElt in hiddenEltIds.items(): if (hiddenEltId not in presentedHiddenEltIds and getattr(ixElt, "xValid", 0) >= VALID and # may not be validated (ixElt.concept.baseXsdType in untransformableTypes or ixElt.isNil)): requiredToDisplayFacts.append(ixElt) if requiredToDisplayFacts: modelXbrl.warning( "ESEF.2.4.1.factInHiddenSectionNotInReport", _("The ix:hidden section contains %(countUnreferenced)s fact(s) whose @id is not applied on any \"-esef-ix- hidden\" style: %(elements)s" ), modelObject=requiredToDisplayFacts, countUnreferenced=len(requiredToDisplayFacts), elements=", ".join( sorted( set(str(f.qname) for f in requiredToDisplayFacts)))) del eligibleForTransformHiddenFacts, hiddenEltIds, presentedHiddenEltIds, requiredToDisplayFacts elif modelDocument.type == ModelDocument.Type.INSTANCE: for elt in modelDocument.xmlRootElement.iter(): if elt.qname == XbrlConst.qnLinkFootnote: # for now assume no private elements extend link:footnote checkFootnote(elt, elt.stringValue) contextsWithDisallowedOCEs = [] contextsWithDisallowedOCEcontent = [] contextsWithPeriodTime = [] contextsWithPeriodTimeZone = [] contextIdentifiers = defaultdict(list) nonStandardTypedDimensions = defaultdict(set) for context in modelXbrl.contexts.values(): for elt in context.iterdescendants( "{http://www.xbrl.org/2003/instance}startDate", "{http://www.xbrl.org/2003/instance}endDate", "{http://www.xbrl.org/2003/instance}instant"): m = datetimePattern.match(elt.stringValue) if m: if m.group(1): contextsWithPeriodTime.append(context) if m.group(3): contextsWithPeriodTimeZone.append(context) for elt in context.iterdescendants( "{http://www.xbrl.org/2003/instance}segment"): contextsWithDisallowedOCEs.append(context) break for elt in context.iterdescendants( "{http://www.xbrl.org/2003/instance}scenario"): if isinstance(elt, ModelObject): if any(True for child in elt.iterchildren() if isinstance(child, ModelObject) and child.tag not in ( "{http://xbrl.org/2006/xbrldi}explicitMember", "{http://xbrl.org/2006/xbrldi}typedMember")): contextsWithDisallowedOCEcontent.append(context) # check periods here contextIdentifiers[context.entityIdentifier].append(context) if contextsWithDisallowedOCEs: modelXbrl.error( "ESEF.2.1.3.segmentUsed", _("xbrli:segment container MUST NOT be used in contexts: %(contextIds)s" ), modelObject=contextsWithDisallowedOCEs, contextIds=", ".join(c.id for c in contextsWithDisallowedOCEs)) if contextsWithDisallowedOCEcontent: modelXbrl.error( "ESEF.2.1.3.scenarioContainsNonDimensionalContent", _("xbrli:scenario in contexts MUST NOT contain any other content than defined in XBRL Dimensions specification: %(contextIds)s" ), modelObject=contextsWithDisallowedOCEcontent, contextIds=", ".join( c.id for c in contextsWithDisallowedOCEcontent)) if len(contextIdentifiers) > 1: modelXbrl.error( "ESEF.2.1.4.multipleIdentifiers", _("All entity identifiers in contexts MUST have identical content: %(contextIdentifiers)s" ), modelObject=modelXbrl, contextIds=", ".join(i[1] for i in contextIdentifiers)) for (contextScheme, contextIdentifier), contextElts in contextIdentifiers.items(): if contextScheme != iso17442: modelXbrl.warning( "ESEF.2.1.1.nonLEIContextScheme", _("The scheme attribute of the xbrli:identifier element should have \"%(leiScheme)s\" as its content: %(contextScheme)s" ), modelObject=contextElts, contextScheme=contextScheme, leiScheme=iso17442) else: leiValidity = LeiUtil.checkLei(contextIdentifier) if leiValidity == LeiUtil.LEI_INVALID_LEXICAL: modelXbrl.warning( "ESEF.2.1.1.invalidIdentifierFormat", _("The LEI context identifier has an invalid format: %(identifier)s" ), modelObject=contextElts, identifier=contextIdentifier) elif leiValidity == LeiUtil.LEI_INVALID_CHECKSUM: modelXbrl.warning( "ESEF.2.1.1.invalidIdentifier", _("The LEI context identifier has checksum error: %(identifier)s" ), modelObject=contextElts, identifier=contextIdentifier) if contextsWithPeriodTime: modelXbrl.warning( "ESEF.2.1.2.periodWithTimeContent", _("Context period startDate, endDate and instant elements should be in whole days without time: %(contextIds)s" ), modelObject=contextsWithPeriodTime, contextIds=", ".join(c.id for c in contextsWithPeriodTime)) if contextsWithPeriodTimeZone: modelXbrl.warning( "ESEF.2.1.2.periodWithTimeZone", _("Context period startDate, endDate and instant elements should be in whole days without a timezone: %(contextIds)s" ), modelObject=contextsWithPeriodTimeZone, contextIds=", ".join(c.id for c in contextsWithPeriodTimeZone)) # identify unique contexts and units mapContext = {} mapUnit = {} uniqueContextHashes = {} for context in modelXbrl.contexts.values(): h = context.contextDimAwareHash if h in uniqueContextHashes: if context.isEqualTo(uniqueContextHashes[h]): mapContext[context] = uniqueContextHashes[h] else: uniqueContextHashes[h] = context del uniqueContextHashes uniqueUnitHashes = {} utrValidator = ValidateUtr(modelXbrl) utrUnitIds = set( u.unitId for unitItemType in utrValidator.utrItemTypeEntries.values() for u in unitItemType.values()) for unit in modelXbrl.units.values(): h = unit.hash if h in uniqueUnitHashes: if unit.isEqualTo(uniqueUnitHashes[h]): mapUnit[unit] = uniqueUnitHashes[h] else: uniqueUnitHashes[h] = unit # check if any custom measure is in UTR for measureTerm in unit.measures: for measure in measureTerm: ns = measure.namespaceURI if ns != XbrlConst.iso4217 and not ns.startswith( "http://www.xbrl.org/"): if measure.localName in utrUnitIds: modelXbrl.error( "ESEF.RTS.III.1.G1-7-1.customUnitInUtr", _("Custom measure SHOULD NOT duplicate a UnitID of UTR: %(measure)s" ), modelObject=unit, measure=measure) del uniqueUnitHashes reportedMandatory = set() precisionFacts = set() numFactsByConceptContextUnit = defaultdict(list) textFactsByConceptContext = defaultdict(list) footnotesRelationshipSet = modelXbrl.relationshipSet( XbrlConst.factFootnote, XbrlConst.defaultLinkRole) noLangFacts = [] textFactsMissingReportLang = [] conceptsUsed = set() for qn, facts in modelXbrl.factsByQname.items(): if qn in mandatory: reportedMandatory.add(qn) for f in facts: if f.precision is not None: precisionFacts.add(f) if f.isNumeric: numFactsByConceptContextUnit[(f.qname, mapContext.get( f.context, f.context), mapUnit.get( f.unit, f.unit))].append(f) if f.concept is not None and not f.isNil and f.xValid >= VALID and f.xValue > 1 and f.concept.type is not None and ( f.concept.type.qname == PERCENT_TYPE or f.concept.type.isDerivedFrom(PERCENT_TYPE)): modelXbrl.warning( "ESEF.2.2.2.percentGreaterThan100", _("A percent fact should have value <= 100: %(element)s in context %(context)s value %(value)s" ), modelObject=f, element=f.qname, context=f.context.id, value=f.xValue) elif f.concept is not None and f.concept.type is not None: if f.concept.type.isOimTextFactType: if not f.xmlLang: noLangFacts.append(f) elif f.context is not None: textFactsByConceptContext[( f.qname, mapContext.get(f.context, f.context))].append(f) conceptsUsed.add(f.concept) if f.context is not None: for dim in f.context.qnameDims.values(): conceptsUsed.add(dim.dimension) if dim.isExplicit: conceptsUsed.add(dim.member) elif dim.isTyped: conceptsUsed.add(dim.typedMember) if noLangFacts: modelXbrl.error( "ESEF.2.5.2.undefinedLanguageForTextFact", _("Each tagged text fact MUST have the 'xml:lang' attribute assigned or inherited." ), modelObject=noLangFacts) # missing report lang text facts if reportXmlLang: for fList in textFactsByConceptContext.values(): if not any(f.xmlLang == reportXmlLang for f in fList): modelXbrl.error( "ESEF.2.5.2.taggedTextFactOnlyInLanguagesOtherThanLanguageOfAReport", _("Each tagged text fact MUST have the 'xml:lang' provided in at least the language of the report: %(element)s" ), modelObject=fList, element=fList[0].qname) # 2.2.4 test for fList in numFactsByConceptContextUnit.values(): if len(fList) > 1: f0 = fList[0] if any(f.isNil for f in fList): _inConsistent = not all(f.isNil for f in fList) elif all( inferredDecimals(f) == inferredDecimals(f0) for f in fList[1:]): # same decimals v0 = rangeValue(f0.value) _inConsistent = not all( rangeValue(f.value) == v0 for f in fList[1:]) else: # not all have same decimals aMax, bMin = rangeValue(f0.value, inferredDecimals(f0)) for f in fList[1:]: a, b = rangeValue(f.value, inferredDecimals(f)) if a > aMax: aMax = a if b < bMin: bMin = b _inConsistent = (bMin < aMax) if _inConsistent: modelXbrl.error( ("ESEF.2.2.4.inconsistentDuplicateNumericFactInInlineXbrlDocument" ), "Inconsistent duplicate numeric facts MUST NOT appear in the content of an inline XBRL document. %(fact)s that was used more than once in contexts equivalent to %(contextID)s: values %(values)s. ", modelObject=fList, fact=f0.qname, contextID=f0.contextID, values=", ".join( strTruncate(f.value, 128) for f in fList)) if precisionFacts: modelXbrl.warning( "ESEF.2.2.1.precisionAttributeUsed", _("The accuracy of numeric facts SHOULD be defined with the 'decimals' attribute rather than the 'precision' attribute: %(elements)s." ), modelObject=precisionFacts, elements=", ".join(sorted( str(e.qname) for e in precisionFacts))) missingElements = (mandatory - reportedMandatory) if missingElements: modelXbrl.error( "ESEF.???.missingRequiredElements", _("Required elements missing from document: %(elements)s."), modelObject=modelXbrl, elements=", ".join(sorted(str(qn) for qn in missingElements))) if transformRegistryErrors: modelXbrl.warning( "ESEF.2.2.3.transformRegistry", _("ESMA recommends applying the latest available version of the Transformation Rules Registry marked with 'Recommendation' status for these elements: %(elements)s." ), modelObject=transformRegistryErrors, elements=", ".join( sorted( str(fact.qname) for fact in transformRegistryErrors))) if orphanedFootnotes: modelXbrl.error( "ESEF.2.3.1.unusedFootnote", _("Non-empty footnotes must be connected to fact(s)."), modelObject=orphanedFootnotes) if noLangFootnotes: modelXbrl.error( "ESEF.2.3.1.undefinedLanguageForFootnote", _("Each footnote MUST have the 'xml:lang' attribute whose value corresponds to the language of the text in the content of the respective footnote." ), modelObject=noLangFootnotes) nonDefLangFtFacts = set(f for f, langs in factLangFootnotes.items() if reportXmlLang not in langs) if nonDefLangFtFacts: modelXbrl.error( "ESEF.2.3.1.footnoteOnlyInLanguagesOtherThanLanguageOfAReport", _("Each fact MUST have at least one footnote with 'xml:lang' attribute whose value corresponds to the language of the text in the content of the respective footnote: %(qnames)s." ), modelObject=nonDefLangFtFacts, qnames=", ".join( sorted(str(f.qname) for f in nonDefLangFtFacts))) del nonDefLangFtFacts if footnoteRoleErrors: modelXbrl.error( "ESEF.2.3.1.nonStandardRoleForFootnote", _("The xlink:role attribute of a link:footnote and link:footnoteLink element as well as xlink:arcrole attribute of a link:footnoteArc MUST be defined in the XBRL Specification 2.1." ), modelObject=footnoteRoleErrors) nonStdFootnoteElts = list() for modelLink in modelXbrl.baseSets[("XBRL-footnotes", None, None, None)]: for elt in modelLink.iterchildren(): if isinstance( elt, (_ElementTree, _Comment, _ProcessingInstruction)): continue # comment or other non-parsed element if elt.qname not in FOOTNOTE_LINK_CHILDREN: nonStdFootnoteElts.append(elt) if nonStdFootnoteElts: modelXbrl.error( "ESEF.2.3.2.nonStandardElementInFootnote", _("A link:footnoteLink element MUST have no children other than link:loc, link:footnote, and link:footnoteArc." ), modelObject=nonStdFootnoteElts) for qn in modelXbrl.qnameDimensionDefaults.values(): conceptsUsed.add(modelXbrl.qnameConcepts.get(qn)) # unused elements in linkbases for arcroles, err in ( ((parentChild, ), "elementsNotUsedForTaggingAppliedInPresentationLinkbase"), ((summationItem, ), "elementsNotUsedForTaggingAppliedInCalculationLinkbase"), ((dimensionDomain, domainMember), "elementsNotUsedForTaggingAppliedInDefinitionLinkbase")): unreportedLbElts = set() for arcrole in arcroles: for rel in modelXbrl.relationshipSet( arcrole).modelRelationships: fr = rel.fromModelObject to = rel.toModelObject if arcrole in (parentChild, summationItem): if fr is not None and not fr.isAbstract and fr not in conceptsUsed and isExtension( val, rel): unreportedLbElts.add(fr) if to is not None and not to.isAbstract and to not in conceptsUsed and isExtension( val, rel): unreportedLbElts.add(to) elif arcrole == dimensionDomain: # dimension, always abstract if fr is not None and fr not in conceptsUsed and isExtension( val, rel): unreportedLbElts.add(fr) if to is not None and rel.isUsable and to not in conceptsUsed and isExtension( val, rel): unreportedLbElts.add(to) elif arcrole == domainMember: if to is not None and not fr.isAbstract and rel.isUsable and to not in conceptsUsed and isExtension( val, rel): unreportedLbElts.add(to) if unreportedLbElts: modelXbrl.error( "ESEF.3.4.6." + err, _("All usable concepts in extension taxonomy relationships MUST be applied by tagged facts: %(elements)s." ), modelObject=unreportedLbElts, elements=", ".join( sorted((str(c.qname) for c in unreportedLbElts)))) # 3.4.4 check for presentation preferred labels missingConceptLabels = defaultdict(set) # by role pfsConceptsRootInPreLB = set() def checkLabels(parent, relSet, labelrole, visited): if not parent.label( labelrole, lang=reportXmlLang, fallbackToQname=False): if parent.name != "NotesAccountingPoliciesAndMandatoryTags": # TEMPORARY TBD remove missingConceptLabels[labelrole].add(parent) visited.add(parent) conceptRels = defaultdict( list) # counts for concepts without preferred label role for rel in relSet.fromModelObject(parent): child = rel.toModelObject if child is not None: labelrole = rel.preferredLabel if not labelrole: conceptRels[child].append(rel) if child not in visited: checkLabels(child, relSet, labelrole, visited) for concept, rels in conceptRels.items(): if len(rels) > 1: modelXbrl.warning( "ESEF.3.4.4.missingPreferredLabelRole", _("Preferred label role SHOULD be used when concept is duplicated in same presentation tree location: %(qname)s." ), modelObject=rels + [concept], qname=concept.qname) visited.remove(parent) for ELR in modelXbrl.relationshipSet(parentChild).linkRoleUris: relSet = modelXbrl.relationshipSet(parentChild, ELR) for rootConcept in relSet.rootConcepts: checkLabels(rootConcept, relSet, None, set()) # check for PFS element which isn't an orphan if rootConcept.qname in esefPrimaryStatementPlaceholders and relSet.fromModelObject( rootConcept): pfsConceptsRootInPreLB.add(rootConcept) for labelrole, concepts in missingConceptLabels.items(): modelXbrl.warning( "ESEF.3.4.5.missingLabelForRoleInReportLanguage", _("Label for %(role)s role SHOULD be available in report language for concepts: %(qnames)s." ), modelObject=concepts, qnames=", ".join(str(c.qname) for c in concepts), role=os.path.basename(labelrole) if labelrole else "standard") if not pfsConceptsRootInPreLB: # no PFS statements were recognized modelXbrl.error( "ESEF.RTS.Annex.II.Par.1.Par.7.missingPrimaryFinancialStatement", _("A primary financial statement placeholder element MUST be a root of a presentation linkbase tree." ), modelObject=modelXbrl) # dereference del missingConceptLabels, pfsConceptsRootInPreLB # mandatory factc RTS Annex II missingMandatoryElements = esefMandatoryElements2020 - modelXbrl.factsByQname.keys( ) if missingMandatoryElements: modelXbrl.error( "ESEF.RTS.Annex.II.Par.2.missingMandatoryMarkups", _("Mandatory elements to be marked up are missing: %(qnames)s." ), modelObject=missingMandatoryElements, qnames=", ".join( sorted(str(qn) for qn in missingMandatoryElements))) # duplicated core taxonomy elements for name, concepts in modelXbrl.nameConcepts.items(): if len(concepts) > 1: i = None # ifrs Concept for c in concepts: if c.qname.namespaceURI == _ifrsNs: i = c break if i is not None: for c in concepts: if c != i and c.balance == i.balance and c.periodType == i.periodType: modelXbrl.error( "ESEF.RTS.Annex.IV.Par.4.2.extensionElementDuplicatesCoreElement", _("Extension elements must not duplicate the existing elements from the core taxonomy and be identifiable %(qname)s." ), modelObject=(c, i), qname=c.qname) modelXbrl.profileActivity(_statusMsg, minTimeToShow=0.0) modelXbrl.modelManager.showStatus(None)
def validateXbrlFinally(val, *args, **kwargs): if not (val.validateESMAplugin): return _xhtmlNs = "{{{}}}".format(xhtml) _xhtmlNsLen = len(_xhtmlNs) modelXbrl = val.modelXbrl modelDocument = modelXbrl.modelDocument _statusMsg = _("validating {0} filing rules").format(val.disclosureSystem.name) modelXbrl.profileActivity() modelXbrl.modelManager.showStatus(_statusMsg) reportXmlLang = None firstRootmostXmlLangDepth = 9999999 if modelDocument.type == ModelDocument.Type.INSTANCE: modelXbrl.error("esma:instanceShallBeInlineXBRL", _("RTS on ESEF requires inline XBRL instances."), modelObject=modelXbrl) checkFilingDimensions(val) # sets up val.primaryItems and val.domainMembers val.hasExtensionSchema = val.hasExtensionPre = val.hasExtensionCal = val.hasExtensionDef = val.hasExtensionLbl = False checkFilingDTS(val, modelXbrl.modelDocument, []) modelXbrl.profileActivity("... filer DTS checks", minTimeToShow=1.0) if not (val.hasExtensionSchema and val.hasExtensionPre and val.hasExtensionCal and val.hasExtensionDef and val.hasExtensionLbl): missingFiles = [] if not val.hasExtensionSchema: missingFiles.append("schema file") if not val.hasExtensionPre: missingFiles.append("presentation linkbase") if not val.hasExtensionCal: missingFiles.append("calculation linkbase") if not val.hasExtensionDef: missingFiles.append("definition linkbase") if not val.hasExtensionLbl: missingFiles.append("label linkbase") modelXbrl.warning("esma:3.1.1.extensionTaxonomyWrongFilesStructure", _("Extension taxonomies MUST consist of at least a schema file and presentation, calculation, definition and label linkbases" ": missing %(missingFiles)s"), modelObject=modelXbrl, missingFiles=", ".join(missingFiles)) if modelDocument.type in (ModelDocument.Type.INLINEXBRL, ModelDocument.Type.INLINEXBRLDOCUMENTSET, ModelDocument.Type.INSTANCE): footnotesRelationshipSet = modelXbrl.relationshipSet("XBRL-footnotes") orphanedFootnotes = set() noLangFootnotes = set() footnoteRoleErrors = set() transformRegistryErrors = set() def checkFootnote(elt, text): if text: # non-empty footnote must be linked to a fact if not empty if not any(isinstance(rel.fromModelObject, ModelFact) for rel in footnotesRelationshipSet.toModelObject(elt)): orphanedFootnotes.add(elt) if not elt.xmlLang: noLangFootnotes.add(elt) if elt.role != XbrlConst.footnote or not all( rel.arcrole == XbrlConst.factFootnote and rel.linkrole == XbrlConst.defaultLinkRole for rel in footnotesRelationshipSet.toModelObject(elt)): footnoteRoleErrors.add(elt) # check file name of each inline document (which might be below a top-level IXDS) for doc in modelXbrl.urlDocs.values(): if doc.type == ModelDocument.Type.INLINEXBRL: _baseName, _baseExt = os.path.splitext(doc.basename) if _baseExt not in (".xhtml",): modelXbrl.warning("esma:TBD.fileNameExtension", _("FileName should have the extension .xhtml: %(fileName)s"), modelObject=doc, fileName=doc.basename) if modelDocument.type in (ModelDocument.Type.INLINEXBRL, ModelDocument.Type.INLINEXBRLDOCUMENTSET): ixNStag = modelXbrl.modelDocument.ixNStag ixTags = set(ixNStag + ln for ln in ("nonNumeric", "nonFraction", "references", "relationship")) ixTextTags = set(ixNStag + ln for ln in ("nonFraction", "continuation", "footnote")) ixExcludeTag = ixNStag + "exclude" ixTupleTag = ixNStag + "tuple" ixFractionTag = ixNStag + "fraction" hiddenEltIds = {} presentedHiddenEltIds = defaultdict(list) eligibleForTransformHiddenFacts = [] requiredToDisplayFacts = [] requiredToDisplayFactIds = {} firstIxdsDoc = True for ixdsHtmlRootElt in modelXbrl.ixdsHtmlElements: # ix root elements for all ix docs in IXDS for elt, depth in etreeIterWithDepth(ixdsHtmlRootElt): eltTag = elt.tag if isinstance(elt, ModelObject) and elt.namespaceURI == xhtml: eltTag = elt.localName if firstIxdsDoc and (not reportXmlLang or depth < firstRootmostXmlLangDepth): xmlLang = elt.get("{http://www.w3.org/XML/1998/namespace}lang") if xmlLang: reportXmlLang = xmlLang firstRootmostXmlLangDepth = depth elif isinstance(elt, (_ElementTree, _Comment, _ProcessingInstruction)): continue # comment or other non-parsed element else: eltTag = elt.tag if eltTag.startswith(_xhtmlNs): eltTag = eltTag[_xhtmlNsLen:] if ((eltTag in ("object", "script")) or (eltTag == "a" and "javascript:" in elt.get("href","")) or (eltTag == "img" and "javascript:" in elt.get("src",""))): modelXbrl.error("esma.2.5.1.executableCodePresent", _("Inline XBRL documents MUST NOT contain executable code: %(element)s"), modelObject=elt, element=eltTag) elif eltTag == "img": src = elt.get("src","").strip() hasParentIxTextTag = False # check if image is in an ix text-bearing element _ancestorElt = elt while (_ancestorElt is not None): if _ancestorElt.tag == ixExcludeTag: # excluded from any parent text-bearing ix element break if _ancestorElt.tag in ixTextTags: hasParentIxTextTag = True break _ancestorElt = _ancestorElt.getparent() if scheme(href) in ("http", "https", "ftp"): modelXbrl.error("esma.3.5.1.inlinXbrlContainsExternalReferences", _("Inline XBRL instance documents MUST NOT contain any reference pointing to resources outside the reporting package: %(element)s"), modelObject=elt, element=eltTag) if not src.startswith("data:image"): if hasParentIxTextTag: modelXbrl.error("esma.2.5.1.imageInIXbrlElementNotEmbedded", _("Images appearing within an inline XBRL element MUST be embedded regardless of their size."), modelObject=elt) else: # presume it to be an image file, check image contents try: base = elt.modelDocument.baseForElement(elt) normalizedUri = elt.modelXbrl.modelManager.cntlr.webCache.normalizeUrl(graphicFile, base) if not elt.modelXbrl.fileSource.isInArchive(normalizedUri): normalizedUri = elt.modelXbrl.modelManager.cntlr.webCache.getfilename(normalizedUri) imglen = 0 with elt.modelXbrl.fileSource.file(normalizedUri,binary=True)[0] as fh: imglen += len(fh.read()) if imglen < browserMaxBase64ImageLength: modelXbrl.error("esma.2.5.1.embeddedImageNotUsingBase64Encoding", _("Images MUST be included in the XHTML document as a base64 encoded string unless their size exceeds support of browsers."), modelObject=elt) except IOError as err: modelXbrl.error("esma.2.5.1.imageFileCannotBeLoaded", _("Image file which isn't openable '%(src)s', error: %(error)s"), modelObject=elt, src=src, error=err) elif not any(src.startswith(m) for m in allowedImgMimeTypes): modelXbrl.error("esma.2.5.1.embeddedImageNotUsingBase64Encoding", _("Images MUST be included in the XHTML document as a base64 encoded string, encoding disallowed: %(src)s."), modelObject=elt, src=attrValue[:128]) elif eltTag == "a": href = elt.get("href","").strip() if scheme(href) in ("http", "https", "ftp"): modelXbrl.error("esma.3.5.1.inlinXbrlContainsExternalReferences", _("Inline XBRL instance documents MUST NOT contain any reference pointing to resources outside the reporting package: %(element)s"), modelObject=elt, element=eltTag) elif eltTag == "base" or elt.tag == "{http://www.w3.org/XML/1998/namespace}base": modelXbrl.error("esma.2.4.2.htmlOrXmlBaseUsed", _("The HTML <base> elements and xml:base attributes MUST NOT be used in the Inline XBRL document."), modelObject=elt, element=eltTag) if eltTag in ixTags and elt.get("target"): modelXbrl.error("esma.2.5.3.targetAttributeUsed", _("Target attribute MUST not be used: element %(localName)s, target attribute %(target)s."), modelObject=elt, localName=elt.elementQname, target=elt.get("target")) if eltTag == ixTupleTag: modelXbrl.error("esma.2.4.1.tupleElementUsed", _("The ix:tuple element MUST not be used in the Inline XBRL document."), modelObject=elt) if eltTag == ixFractionTag: modelXbrl.error("esma.2.4.1.fractionElementUsed", _("The ix:fraction element MUST not be used in the Inline XBRL document."), modelObject=elt) if elt.get("{http://www.w3.org/XML/1998/namespace}base") is not None: modelXbrl.error("esma.2.4.1.xmlBaseUsed", _("xml:base attributes MUST NOT be used in the Inline XBRL document: element %(localName)s, base attribute %(base)s."), modelObject=elt, localName=elt.elementQname, base=elt.get("{http://www.w3.org/XML/1998/namespace}base")) if isinstance(elt, ModelInlineFootnote): checkFootnote(elt, elt.value) elif isinstance(elt, ModelResource) and elt.qname == XbrlConst.qnLinkFootnote: checkFootnote(elt, elt.value) elif isinstance(elt, ModelInlineFact): if elt.format is not None and elt.format.namespaceURI != 'http://www.xbrl.org/inlineXBRL/transformation/2015-02-26': transformRegistryErrors.add(elt) for ixHiddenElt in ixdsHtmlRootElt.iterdescendants(tag=ixNStag + "hidden"): for tag in (ixNStag + "nonNumeric", ixNStag+"nonFraction"): for ixElt in ixHiddenElt.iterdescendants(tag=tag): if (getattr(ixElt, "xValid", 0) >= VALID # may not be validated ): # add future "and" conditions on elements which can be in hidden if (ixElt.concept.baseXsdType not in untransformableTypes and not ixElt.isNil): eligibleForTransformHiddenFacts.append(ixElt) elif ixElt.id is None: requiredToDisplayFacts.append(ixElt) if ixElt.id: hiddenEltIds[ixElt.id] = ixElt firstIxdsDoc = False if eligibleForTransformHiddenFacts: modelXbrl.warning("esma.2.4.1.transformableElementIncludedInHiddenSection", _("The ix:hidden section of Inline XBRL document MUST not include elements eligible for transformation. " "%(countEligible)s fact(s) were eligible for transformation: %(elements)s"), modelObject=eligibleForTransformHiddenFacts, countEligible=len(eligibleForTransformHiddenFacts), elements=", ".join(sorted(set(str(f.qname) for f in eligibleForTransformHiddenFacts)))) for ixdsHtmlRootElt in modelXbrl.ixdsHtmlElements: for ixElt in ixdsHtmlRootElt.getroottree().iterfind("//{http://www.w3.org/1999/xhtml}*[@style]"): hiddenFactRefMatch = styleIxHiddenPattern.match(ixElt.get("style","")) if hiddenFactRefMatch: hiddenFactRef = hiddenFactRefMatch.group(2) if hiddenFactRef not in hiddenEltIds: modelXbrl.error("esma.2.4.1.esefIxHiddenStyleNotLinkingFactInHiddenSection", _("\"-esef-ix-hidden\" style identifies @id, %(id)s of a fact that is not in ix:hidden section."), modelObject=ixElt, id=hiddenFactRef) else: presentedHiddenEltIds[hiddenFactRef].append(ixElt) for hiddenEltId, ixElt in hiddenEltIds.items(): if (hiddenEltId not in presentedHiddenEltIds and getattr(ixElt, "xValid", 0) >= VALID and # may not be validated (ixElt.concept.baseXsdType in untransformableTypes or ixElt.isNil)): requiredToDisplayFacts.append(ixElt) if requiredToDisplayFacts: modelXbrl.warning("esma.2.4.1.factInHiddenSectionNotInReport", _("The ix:hidden section contains %(countUnreferenced)s fact(s) whose @id is not applied on any \"-esef-ix- hidden\" style: %(elements)s"), modelObject=requiredToDisplayFacts, countUnreferenced=len(requiredToDisplayFacts), elements=", ".join(sorted(set(str(f.qname) for f in requiredToDisplayFacts)))) del eligibleForTransformHiddenFacts, hiddenEltIds, presentedHiddenEltIds, requiredToDisplayFacts elif modelDocument.type == ModelDocument.Type.INSTANCE: for elt in modelDocument.xmlRootElement.iter(): if elt.qname == XbrlConst.qnLinkFootnote: # for now assume no private elements extend link:footnote checkFootnote(elt, elt.stringValue) contextsWithDisallowedOCEs = [] contextsWithDisallowedOCEcontent = [] contextsWithPeriodTime = [] contextsWithPeriodTimeZone = [] contextIdentifiers = defaultdict(list) nonStandardTypedDimensions = defaultdict(set) for context in modelXbrl.contexts.values(): if XmlUtil.hasChild(context, XbrlConst.xbrli, "segment"): contextsWithDisallowedOCEs.append(context) for segScenElt in context.iterdescendants("{http://www.xbrl.org/2003/instance}scenario"): if isinstance(segScenElt,ModelObject): if any(True for child in segScenElt.iterchildren() if isinstance(child,ModelObject) and child.tag not in ("{http://xbrl.org/2006/xbrldi}explicitMember", "{http://xbrl.org/2006/xbrldi}typedMember")): contextsWithDisallowedOCEcontent.append(context) # check periods here contextIdentifiers[context.entityIdentifier].append(context) if contextsWithDisallowedOCEs: modelXbrl.error("esma.2.1.3.segmentUsed", _("xbrli:segment container MUST NOT be used in contexts: %(contextIds)s"), modelObject=contextsWithDisallowedOCEs, contextIds=", ".join(c.id for c in contextsWithDisallowedOCEs)) if contextsWithDisallowedOCEcontent: modelXbrl.error("esma.2.1.3.scenarioContainsNonDimensionalContent", _("xbrli:scenario in contexts MUST NOT contain any other content than defined in XBRL Dimensions specification: %(contextIds)s"), modelObject=contextsWithDisallowedOCEcontent, contextIds=", ".join(c.id for c in contextsWithDisallowedOCEcontent)) if len(contextIdentifiers) > 1: modelXbrl.error("esma.2.1.4.multipleIdentifiers", _("All entity identifiers in contexts MUST have identical content: %(contextIdentifiers)s"), modelObject=modelXbrl, contextIds=", ".join(i[1] for i in contextIdentifiers)) for (contextScheme, contextIdentifier), contextElts in contextIdentifiers.items(): if contextScheme != "http://standards.iso.org/iso/17442": modelXbrl.warning("esma.2.1.1.nonLEIContextScheme", _("The scheme attribute of the xbrli:identifier element should have \"http://standards.iso.org/iso/17442\" as its content: %(scheme)s"), modelObject=contextElts, scheme=contextScheme) else: leiValidity = LeiUtil.checkLei(contextIdentifier) if leiValidity == LeiUtil.LEI_INVALID_LEXICAL: modelXbrl.warning("esma.2.1.1.invalidIdentifierFormat", _("The LEI context idenntifier has an invalid format: %(identifier)s"), modelObject=contextElts, identifier=contextIdentifier) elif leiValidity == LeiUtil.LEI_INVALID_CHECKSUM: modelXbrl.warning("esma.2.1.1.invalidIdentifier", _("The LEI context idenntifier has checksum error: %(identifier)s"), modelObject=contextElts, identifier=contextIdentifier) if contextsWithPeriodTime: modelXbrl.warning("esma.2.1.2.periodWithTimeContent", _("Context period startDate, endDate and instant elements should be in whole days without time: %(contextIds)s"), modelObject=contextsWithPeriodTime, contextIds=", ".join(c.id for c in contextsWithPeriodTime)) if contextsWithPeriodTimeZone: modelXbrl.warning("esma.2.1.2.periodWithTimeZone", _("Context period startDate, endDate and instant elements should be in whole days without a timezone: %(contextIds)s"), modelObject=contextsWithPeriodTimeZone, contextIds=", ".join(c.id for c in contextsWithPeriodTimeZone)) # identify unique contexts and units mapContext = {} mapUnit = {} uniqueContextHashes = {} for context in modelXbrl.contexts.values(): h = context.contextDimAwareHash if h in uniqueContextHashes: if context.isEqualTo(uniqueContextHashes[h]): mapContext[context] = uniqueContextHashes[h] else: uniqueContextHashes[h] = context del uniqueContextHashes uniqueUnitHashes = {} for unit in modelXbrl.units.values(): h = unit.hash if h in uniqueUnitHashes: if unit.isEqualTo(uniqueUnitHashes[h]): mapUnit[unit] = uniqueUnitHashes[h] else: uniqueUnitHashes[h] = unit del uniqueUnitHashes reportedMandatory = set() precisionFacts = set() numFactsByConceptContextUnit = defaultdict(list) textFactsByConceptContext = defaultdict(list) footnotesRelationshipSet = modelXbrl.relationshipSet(XbrlConst.factFootnote, XbrlConst.defaultLinkRole) noLangFacts = [] textFactsMissingReportLang = [] conceptsUsed = set() for qn, facts in modelXbrl.factsByQname.items(): if qn in mandatory: reportedMandatory.add(qn) for f in facts: if f.precision is not None: precisionFacts.add(f) if f.isNumeric: numFactsByConceptContextUnit[(f.qname, mapContext.get(f.context,f.context), mapUnit.get(f.unit, f.unit))].append(f) elif f.concept is not None and f.concept.type is not None: if f.concept.type.isOimTextFactType: if not f.xmlLang: noLangFacts.append(f) elif f.context is not None: textFactsByConceptContext[(f.qname, mapContext.get(f.context,f.context))].append(f) conceptsUsed.add(f.concept) if f.context is not None: for dim in f.context.qnameDims.values(): conceptsUsed.add(dim.dimension) if dim.isExplicit: conceptsUsed.add(dim.member) elif dim.isTyped: conceptsUsed.add(dim.typedMember) if noLangFacts: modelXbrl.error("esma.2.5.2.undefinedLanguageForTextFact", _("Each tagged text fact MUST have the 'xml:lang' attribute assigned or inherited."), modelObject=noLangFacts) # missing report lang text facts for fList in textFactsByConceptContext.values(): if not any(f.xmlLang == reportXmlLang for f in fList): modelXbrl.error("esma.2.5.2.taggedTextFactOnlyInLanguagesOtherThanLanguageOfAReport", _("Each tagged text fact MUST have the 'xml:lang' provided in at least the language of the report: %(element)s"), modelObject=fList, element=fList[0].qname) # 2.2.4 test for fList in numFactsByConceptContextUnit.values(): if len(fList) > 1: f0 = fList[0] if any(f.isNil for f in fList): _inConsistent = not all(f.isNil for f in fList) elif all(inferredDecimals(f) == inferredDecimals(f0) for f in fList[1:]): # same decimals v0 = rangeValue(f0.value) _inConsistent = not all(rangeValue(f.value) == v0 for f in fList[1:]) else: # not all have same decimals aMax, bMin = rangeValue(f0.value, inferredDecimals(f0)) for f in fList[1:]: a, b = rangeValue(f.value, inferredDecimals(f)) if a > aMax: aMax = a if b < bMin: bMin = b _inConsistent = (bMin < aMax) if _inConsistent: modelXbrl.error(("esma:2.2.4.inconsistentDuplicateNumericFactInInlineXbrlDocument"), "Inconsistent duplicate numeric facts MUST NOT appear in the content of an inline XBRL document. %(fact)s that was used more than once in contexts equivalent to %(contextID)s: values %(values)s. ", modelObject=fList, fact=f0.qname, contextID=f0.contextID, values=", ".join(strTruncate(f.value, 128) for f in fList)) if precisionFacts: modelXbrl.warning("esma:2.2.1.precisionAttributeUsed", _("The accuracy of numeric facts SHOULD be defined with the 'decimals' attribute rather than the 'precision' attribute: %(elements)s."), modelObject=precisionFacts, elements=", ".join(sorted(str(qn) for qn in precisionFacts))) missingElements = (mandatory - reportedMandatory) if missingElements: modelXbrl.error("esma:???.missingRequiredElements", _("Required elements missing from document: %(elements)s."), modelObject=modelXbrl, elements=", ".join(sorted(str(qn) for qn in missingElements))) if transformRegistryErrors: modelXbrl.warning("esma:2.2.3.transformRegistry", _("ESMA recommends applying the latest available version of the Transformation Rules Registry marked with 'Recommendation' status for these elements: %(elements)s."), modelObject=transformRegistryErrors, elements=", ".join(sorted(str(fact.qname) for fact in transformRegistryErrors))) if orphanedFootnotes: modelXbrl.error("esma.2.3.1.unusedFootnote", _("Non-empty footnotes must be connected to fact(s)."), modelObject=orphanedFootnotes) if noLangFootnotes: modelXbrl.error("esma.2.3.2.undefinedLanguageForFootnote", _("Each footnote MUST have the 'xml:lang' attribute whose value corresponds to the language of the text in the content of the respective footnote."), modelObject=noLangFootnotes) if footnoteRoleErrors: modelXbrl.error("esma.2.3.2.nonStandardRoleForFootnote", _("The xlink:role attribute of a link:footnote and link:footnoteLink element as well as xlink:arcrole attribute of a link:footnoteArc MUST be defined in the XBRL Specification 2.1."), modelObject=footnoteRoleErrors) nonStdFootnoteElts = list() for modelLink in modelXbrl.baseSets[("XBRL-footnotes",None,None,None)]: for elt in ixdsHtmlRootElt.iter(): if isinstance(elt, (_ElementTree, _Comment, _ProcessingInstruction)): continue # comment or other non-parsed element if elt.namespaceURI != link or elt.localName not in ("loc", "link", "footnoteArc"): nonStdFootnoteElts.append(elt) if nonStdFootnoteElts: modelXbrl.error("esma.2.3.2.nonStandardElementInFootnote", _("A link:footnoteLink element MUST have no children other than link:loc, link:footnote, and link:footnoteArc."), modelObject=nonStdFootnoteElts) for qn in modelXbrl.qnameDimensionDefaults.values(): conceptsUsed.add(modelXbrl.qnameConcepts.get(qn)) # unused elements in linkbases for arcroles, err in (((parentChild,), "elementsNotUsedForTaggingAppliedInPresentationLinkbase"), ((summationItem,), "elementsNotUsedForTaggingAppliedInCalculationLinkbase"), ((dimensionDomain,domainMember), "elementsNotUsedForTaggingAppliedInDefinitionLinkbase")): lbElts = set() for arcrole in arcroles: for rel in modelXbrl.relationshipSet(arcrole).modelRelationships: fr = rel.fromModelObject to = rel.toModelObject if arcrole in (parentChild, summationItem): if fr is not None and not fr.isAbstract: lbElts.add(fr) if to is not None and not to.isAbstract: lbElts.add(to) elif arcrole == dimensionDomain: if fr is not None: # dimension, always abstract lbElts.add(fr) if to is not None and rel.isUsable: lbElts.add(to) elif arcrole == domainMember: if to is not None and rel.isUsable: lbElts.add(to) unreportedLbElts = lbElts - conceptsUsed if unreportedLbElts: modelXbrl.error("esma.3.2.6." + err, _("All usable concepts in extension taxonomy relationships MUST be applied by tagged facts: %(elements)s."), modelObject=unreportedLbElts, elements=", ".join(sorted((str(c.qname) for c in unreportedLbElts)))) modelXbrl.profileActivity(_statusMsg, minTimeToShow=0.0) modelXbrl.modelManager.showStatus(None)