arelle-release 2.36.27__py3-none-any.whl → 2.36.29__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of arelle-release might be problematic. Click here for more details.
- arelle/ValidateDuplicateFacts.py +112 -49
- arelle/_version.py +2 -2
- arelle/plugin/saveHtmlEBAtables.py +290 -215
- arelle/utils/PluginHooks.py +27 -0
- {arelle_release-2.36.27.dist-info → arelle_release-2.36.29.dist-info}/METADATA +1 -1
- {arelle_release-2.36.27.dist-info → arelle_release-2.36.29.dist-info}/RECORD +11 -10
- tests/integration_tests/scripts/tests/eba_tablesets.py +119 -0
- {arelle_release-2.36.27.dist-info → arelle_release-2.36.29.dist-info}/LICENSE.md +0 -0
- {arelle_release-2.36.27.dist-info → arelle_release-2.36.29.dist-info}/WHEEL +0 -0
- {arelle_release-2.36.27.dist-info → arelle_release-2.36.29.dist-info}/entry_points.txt +0 -0
- {arelle_release-2.36.27.dist-info → arelle_release-2.36.29.dist-info}/top_level.txt +0 -0
arelle/ValidateDuplicateFacts.py
CHANGED
|
@@ -1,31 +1,38 @@
|
|
|
1
1
|
"""
|
|
2
2
|
See COPYRIGHT.md for copyright information.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
from __future__ import annotations
|
|
5
6
|
|
|
6
|
-
from _decimal import Decimal
|
|
7
7
|
from collections import defaultdict
|
|
8
8
|
from collections.abc import Iterator
|
|
9
9
|
from dataclasses import dataclass, field
|
|
10
|
-
from enum import
|
|
10
|
+
from enum import Enum, Flag, auto
|
|
11
11
|
from functools import cached_property
|
|
12
12
|
from math import isnan
|
|
13
|
-
from typing import
|
|
13
|
+
from typing import Any, SupportsFloat, cast
|
|
14
|
+
|
|
15
|
+
from _decimal import Decimal
|
|
14
16
|
|
|
15
17
|
from arelle import XmlValidateConst
|
|
16
|
-
from arelle.ModelInstanceObject import
|
|
18
|
+
from arelle.ModelInstanceObject import ModelContext, ModelFact, ModelUnit
|
|
17
19
|
from arelle.ModelValue import DateTime, QName, TypeXValue
|
|
18
20
|
from arelle.ModelXbrl import ModelXbrl
|
|
19
|
-
from arelle.ValidateXbrlCalcs import rangeValue, inferredDecimals
|
|
20
21
|
from arelle.typing import TypeGetText
|
|
22
|
+
from arelle.ValidateXbrlCalcs import inferredDecimals, rangeValue
|
|
23
|
+
|
|
21
24
|
_: TypeGetText
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
@dataclass(frozen=True)
|
|
25
28
|
class DuplicateFactSet:
|
|
26
29
|
facts: list[ModelFact]
|
|
27
|
-
_inferredDecimals: dict[ModelFact, float | int | None] = field(
|
|
28
|
-
|
|
30
|
+
_inferredDecimals: dict[ModelFact, float | int | None] = field(
|
|
31
|
+
init=False, default_factory=dict
|
|
32
|
+
)
|
|
33
|
+
_ranges: dict[ModelFact, tuple[Decimal, Decimal]] = field(
|
|
34
|
+
init=False, default_factory=dict
|
|
35
|
+
)
|
|
29
36
|
|
|
30
37
|
def __iter__(self) -> Iterator[ModelFact]:
|
|
31
38
|
return iter(self.facts)
|
|
@@ -80,7 +87,9 @@ class DuplicateFactSet:
|
|
|
80
87
|
"""
|
|
81
88
|
:return: Whether any facts in the set are complete duplicates of each other.
|
|
82
89
|
"""
|
|
83
|
-
decimalsValueMap: dict[float | int | None, set[TypeFactValueEqualityKey]] =
|
|
90
|
+
decimalsValueMap: dict[float | int | None, set[TypeFactValueEqualityKey]] = (
|
|
91
|
+
defaultdict(set)
|
|
92
|
+
)
|
|
84
93
|
for fact in self.facts:
|
|
85
94
|
decimals = self.getDecimals(fact)
|
|
86
95
|
value = getFactValueEqualityKey(fact)
|
|
@@ -181,7 +190,10 @@ class DuplicateFactSet:
|
|
|
181
190
|
seenKeys = set()
|
|
182
191
|
results = []
|
|
183
192
|
for fact in self.facts:
|
|
184
|
-
|
|
193
|
+
xValue = (
|
|
194
|
+
tuple(fact.xValue) if isinstance(fact.xValue, list) else fact.xValue
|
|
195
|
+
)
|
|
196
|
+
key = (fact.decimals, xValue)
|
|
185
197
|
if key in seenKeys:
|
|
186
198
|
continue
|
|
187
199
|
seenKeys.add(key)
|
|
@@ -208,7 +220,7 @@ class DuplicateFactSet:
|
|
|
208
220
|
sortedDecimals = sorted(decimalsMap.keys())
|
|
209
221
|
results = set(facts)
|
|
210
222
|
|
|
211
|
-
for a, decimalLower in enumerate(sortedDecimals[:len(sortedDecimals)-1]):
|
|
223
|
+
for a, decimalLower in enumerate(sortedDecimals[: len(sortedDecimals) - 1]):
|
|
212
224
|
groupLower = decimalsMap[decimalLower]
|
|
213
225
|
for factA in groupLower:
|
|
214
226
|
lowerA, upperA = self.getRange(factA)
|
|
@@ -216,7 +228,7 @@ class DuplicateFactSet:
|
|
|
216
228
|
continue
|
|
217
229
|
remove = False
|
|
218
230
|
# Iterate through each higher decimals group
|
|
219
|
-
for b, decimalHigher in enumerate(sortedDecimals[a+1:]):
|
|
231
|
+
for b, decimalHigher in enumerate(sortedDecimals[a + 1 :]):
|
|
220
232
|
groupHigher = decimalsMap[decimalHigher]
|
|
221
233
|
for factB in groupHigher:
|
|
222
234
|
lowerB, upperB = self.getRange(factB)
|
|
@@ -241,7 +253,7 @@ class DuplicateFactSet:
|
|
|
241
253
|
return self.deduplicateCompleteSubsets(), None
|
|
242
254
|
if not self.areAllConsistent:
|
|
243
255
|
# If facts are not all consistent, we will only perform complete deduplication
|
|
244
|
-
return self.deduplicateCompleteSubsets(),
|
|
256
|
+
return self.deduplicateCompleteSubsets(), "Set has inconsistent facts"
|
|
245
257
|
selectedFact = self.facts[0]
|
|
246
258
|
maxDecimals = self.getDecimals(selectedFact)
|
|
247
259
|
assert maxDecimals is not None
|
|
@@ -259,9 +271,11 @@ class DuplicateFactSet:
|
|
|
259
271
|
:param fact:
|
|
260
272
|
:return: Retrieve cached inferred decimals value for the provided fact.
|
|
261
273
|
"""
|
|
262
|
-
assert fact in self.facts,
|
|
274
|
+
assert fact in self.facts, "Attempted to get decimals for fact not in set"
|
|
263
275
|
if fact not in self._inferredDecimals:
|
|
264
|
-
self._inferredDecimals[fact] =
|
|
276
|
+
self._inferredDecimals[fact] = (
|
|
277
|
+
None if fact.decimals is None else inferredDecimals(fact)
|
|
278
|
+
)
|
|
265
279
|
return self._inferredDecimals[fact]
|
|
266
280
|
|
|
267
281
|
def getRange(self, fact: ModelFact) -> tuple[Decimal, Decimal]:
|
|
@@ -270,8 +284,8 @@ class DuplicateFactSet:
|
|
|
270
284
|
:param fact:
|
|
271
285
|
:return: Retrieve cached range values for the provided fact.
|
|
272
286
|
"""
|
|
273
|
-
assert fact in self.facts,
|
|
274
|
-
assert fact.isNumeric,
|
|
287
|
+
assert fact in self.facts, "Attempted to get range for fact not in set"
|
|
288
|
+
assert fact.isNumeric, "Attempted to get range for non-numeric fact"
|
|
275
289
|
if fact not in self._ranges:
|
|
276
290
|
lower, upper, __, __ = rangeValue(fact.xValue, self.getDecimals(fact))
|
|
277
291
|
self._ranges[fact] = lower, upper
|
|
@@ -297,25 +311,25 @@ class DuplicateType(Flag):
|
|
|
297
311
|
|
|
298
312
|
@property
|
|
299
313
|
def description(self) -> str:
|
|
300
|
-
return
|
|
314
|
+
return "|".join([str(n.name) for n in self if n.name]).lower()
|
|
301
315
|
|
|
302
316
|
|
|
303
317
|
class DuplicateTypeArg(Enum):
|
|
304
|
-
NONE =
|
|
305
|
-
INCONSISTENT =
|
|
306
|
-
CONSISTENT =
|
|
307
|
-
INCOMPLETE =
|
|
308
|
-
COMPLETE =
|
|
309
|
-
ALL =
|
|
318
|
+
NONE = "none"
|
|
319
|
+
INCONSISTENT = "inconsistent"
|
|
320
|
+
CONSISTENT = "consistent"
|
|
321
|
+
INCOMPLETE = "incomplete"
|
|
322
|
+
COMPLETE = "complete"
|
|
323
|
+
ALL = "all"
|
|
310
324
|
|
|
311
325
|
def duplicateType(self) -> DuplicateType:
|
|
312
326
|
return DUPLICATE_TYPE_ARG_MAP.get(self, DuplicateType.NONE)
|
|
313
327
|
|
|
314
328
|
|
|
315
329
|
class DeduplicationType(Enum):
|
|
316
|
-
COMPLETE =
|
|
317
|
-
CONSISTENT_PAIRS =
|
|
318
|
-
CONSISTENT_SETS =
|
|
330
|
+
COMPLETE = "complete"
|
|
331
|
+
CONSISTENT_PAIRS = "consistent-pairs"
|
|
332
|
+
CONSISTENT_SETS = "consistent-sets"
|
|
319
333
|
|
|
320
334
|
|
|
321
335
|
DUPLICATE_TYPE_ARG_MAP = {
|
|
@@ -328,7 +342,9 @@ DUPLICATE_TYPE_ARG_MAP = {
|
|
|
328
342
|
}
|
|
329
343
|
|
|
330
344
|
|
|
331
|
-
def doesSetHaveDuplicateType(
|
|
345
|
+
def doesSetHaveDuplicateType(
|
|
346
|
+
duplicateFacts: DuplicateFactSet, duplicateType: DuplicateType
|
|
347
|
+
) -> bool:
|
|
332
348
|
"""
|
|
333
349
|
:param duplicateFacts:
|
|
334
350
|
:param duplicateType:
|
|
@@ -363,7 +379,9 @@ def areFactsValueEqual(factA: ModelFact, factB: ModelFact) -> bool:
|
|
|
363
379
|
return getFactValueEqualityKey(factA) == getFactValueEqualityKey(factB)
|
|
364
380
|
|
|
365
381
|
|
|
366
|
-
def getAspectEqualFacts(
|
|
382
|
+
def getAspectEqualFacts(
|
|
383
|
+
hashEquivalentFacts: list[ModelFact], includeSingles: bool
|
|
384
|
+
) -> Iterator[list[ModelFact]]:
|
|
367
385
|
"""
|
|
368
386
|
Given a list of concept/context/unit hash-equivalent facts,
|
|
369
387
|
yields sublists of aspect-equal facts from this list.
|
|
@@ -371,12 +389,24 @@ def getAspectEqualFacts(hashEquivalentFacts: list[ModelFact], includeSingles: bo
|
|
|
371
389
|
:param includeSingles: Whether to include lists of single facts (with no duplicates).
|
|
372
390
|
:return: Lists of aspect-equal facts.
|
|
373
391
|
"""
|
|
374
|
-
aspectEqualFacts: dict[
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
392
|
+
aspectEqualFacts: dict[
|
|
393
|
+
tuple[QName, str | None], dict[tuple[ModelContext, ModelUnit], list[ModelFact]]
|
|
394
|
+
] = defaultdict(dict)
|
|
395
|
+
for (
|
|
396
|
+
fact
|
|
397
|
+
) in (
|
|
398
|
+
hashEquivalentFacts
|
|
399
|
+
): # check for hash collision by value checks on context and unit
|
|
400
|
+
contextUnitDict = aspectEqualFacts[
|
|
401
|
+
(
|
|
402
|
+
fact.qname,
|
|
403
|
+
(
|
|
404
|
+
cast(str, fact.xmlLang or "").lower()
|
|
405
|
+
if fact.concept.type.isWgnStringFactType
|
|
406
|
+
else None
|
|
407
|
+
),
|
|
408
|
+
)
|
|
409
|
+
]
|
|
380
410
|
_matched = False
|
|
381
411
|
for (context, unit), contextUnitFacts in contextUnitDict.items():
|
|
382
412
|
if fact.context is None:
|
|
@@ -395,12 +425,16 @@ def getAspectEqualFacts(hashEquivalentFacts: list[ModelFact], includeSingles: bo
|
|
|
395
425
|
if not _matched:
|
|
396
426
|
contextUnitDict[(fact.context, fact.unit)] = [fact]
|
|
397
427
|
for contextUnitDict in aspectEqualFacts.values(): # dups by qname, lang
|
|
398
|
-
for
|
|
428
|
+
for (
|
|
429
|
+
duplicateFacts
|
|
430
|
+
) in contextUnitDict.values(): # dups by equal-context equal-unit
|
|
399
431
|
if includeSingles or len(duplicateFacts) > 1:
|
|
400
432
|
yield duplicateFacts
|
|
401
433
|
|
|
402
434
|
|
|
403
|
-
def getDeduplicatedFacts(
|
|
435
|
+
def getDeduplicatedFacts(
|
|
436
|
+
modelXbrl: ModelXbrl, deduplicationType: DeduplicationType
|
|
437
|
+
) -> list[ModelFact]:
|
|
404
438
|
results = []
|
|
405
439
|
for duplicateFactSet in getDuplicateFactSets(modelXbrl.facts, includeSingles=True):
|
|
406
440
|
message = None
|
|
@@ -418,12 +452,20 @@ def getDeduplicatedFacts(modelXbrl: ModelXbrl, deduplicationType: DeduplicationT
|
|
|
418
452
|
if message is not None:
|
|
419
453
|
modelXbrl.warning(
|
|
420
454
|
"info:deduplicationNotPossible",
|
|
421
|
-
_(
|
|
422
|
-
|
|
455
|
+
_(
|
|
456
|
+
"Deduplication of %(concept)s fact set not possible: %(message)s. concept=%(concept)s, context=%(context)s"
|
|
457
|
+
),
|
|
458
|
+
modelObject=facts[0],
|
|
459
|
+
concept=facts[0].concept.qname,
|
|
460
|
+
context=facts[0].contextID,
|
|
461
|
+
message=message,
|
|
462
|
+
)
|
|
423
463
|
return results
|
|
424
464
|
|
|
425
465
|
|
|
426
|
-
def getDuplicateFactSets(
|
|
466
|
+
def getDuplicateFactSets(
|
|
467
|
+
facts: list[ModelFact], includeSingles: bool
|
|
468
|
+
) -> Iterator[DuplicateFactSet]:
|
|
427
469
|
"""
|
|
428
470
|
:param facts: Facts to find duplicate sets from.
|
|
429
471
|
:param includeSingles: Whether to include lists of single facts (with no duplicates).
|
|
@@ -433,11 +475,15 @@ def getDuplicateFactSets(facts: list[ModelFact], includeSingles: bool) -> Iterat
|
|
|
433
475
|
for hashEquivalentFacts in hashEquivalentFactGroups:
|
|
434
476
|
if not includeSingles and len(hashEquivalentFacts) < 2:
|
|
435
477
|
continue
|
|
436
|
-
for duplicateFactList in getAspectEqualFacts(
|
|
478
|
+
for duplicateFactList in getAspectEqualFacts(
|
|
479
|
+
hashEquivalentFacts, includeSingles=includeSingles
|
|
480
|
+
): # dups by equal-context equal-unit
|
|
437
481
|
yield DuplicateFactSet(facts=duplicateFactList)
|
|
438
482
|
|
|
439
483
|
|
|
440
|
-
def getDuplicateFactSetsWithType(
|
|
484
|
+
def getDuplicateFactSetsWithType(
|
|
485
|
+
facts: list[ModelFact], duplicateType: DuplicateType
|
|
486
|
+
) -> Iterator[DuplicateFactSet]:
|
|
441
487
|
"""
|
|
442
488
|
:param facts: Facts to find duplicate sets from.
|
|
443
489
|
:param duplicateType: Type of duplicate to filter duplicate sets by.
|
|
@@ -451,9 +497,9 @@ def getDuplicateFactSetsWithType(facts: list[ModelFact], duplicateType: Duplicat
|
|
|
451
497
|
|
|
452
498
|
|
|
453
499
|
class FactValueEqualityType(Enum):
|
|
454
|
-
DEFAULT =
|
|
455
|
-
DATETIME =
|
|
456
|
-
LANGUAGE =
|
|
500
|
+
DEFAULT = "default"
|
|
501
|
+
DATETIME = "datetime"
|
|
502
|
+
LANGUAGE = "language"
|
|
457
503
|
|
|
458
504
|
|
|
459
505
|
TypeFactValueEqualityKey = tuple[FactValueEqualityType, tuple[Any, ...]]
|
|
@@ -471,7 +517,9 @@ def getFactValueEqualityKey(fact: ModelFact) -> TypeFactValueEqualityKey:
|
|
|
471
517
|
if isnan(cast(SupportsFloat, xValue)):
|
|
472
518
|
return FactValueEqualityType.DEFAULT, (float("nan"),)
|
|
473
519
|
if fact.concept.isLanguage:
|
|
474
|
-
return FactValueEqualityType.LANGUAGE, (
|
|
520
|
+
return FactValueEqualityType.LANGUAGE, (
|
|
521
|
+
cast(str, xValue).lower() if xValue is not None else None,
|
|
522
|
+
)
|
|
475
523
|
if isinstance(xValue, DateTime): # with/without time makes values unequal
|
|
476
524
|
return FactValueEqualityType.DATETIME, (xValue, xValue.dateOnly)
|
|
477
525
|
return FactValueEqualityType.DEFAULT, (fact.value,)
|
|
@@ -486,7 +534,16 @@ def getHashEquivalentFactGroups(facts: list[ModelFact]) -> list[list[ModelFact]]
|
|
|
486
534
|
"""
|
|
487
535
|
hashDict = defaultdict(list)
|
|
488
536
|
for f in facts:
|
|
489
|
-
if (
|
|
537
|
+
if (
|
|
538
|
+
(
|
|
539
|
+
f.isNil
|
|
540
|
+
or getattr(f, "xValid", XmlValidateConst.UNVALIDATED)
|
|
541
|
+
>= XmlValidateConst.VALID
|
|
542
|
+
)
|
|
543
|
+
and f.context is not None
|
|
544
|
+
and f.concept is not None
|
|
545
|
+
and f.concept.type is not None
|
|
546
|
+
):
|
|
490
547
|
hashDict[f.conceptContextUnitHash].append(f)
|
|
491
548
|
return list(hashDict.values())
|
|
492
549
|
|
|
@@ -494,7 +551,9 @@ def getHashEquivalentFactGroups(facts: list[ModelFact]) -> list[list[ModelFact]]
|
|
|
494
551
|
def logDeduplicatedFact(modelXbrl: ModelXbrl, fact: ModelFact) -> None:
|
|
495
552
|
modelXbrl.info(
|
|
496
553
|
"info:deduplicatedFact",
|
|
497
|
-
_(
|
|
554
|
+
_(
|
|
555
|
+
"Duplicate fact was excluded from deduplicated instance: %(fact)s, value=%(value)s, decimals=%(decimals)s"
|
|
556
|
+
),
|
|
498
557
|
modelObject=fact,
|
|
499
558
|
fact=fact.qname,
|
|
500
559
|
value=fact.xValue,
|
|
@@ -502,7 +561,9 @@ def logDeduplicatedFact(modelXbrl: ModelXbrl, fact: ModelFact) -> None:
|
|
|
502
561
|
)
|
|
503
562
|
|
|
504
563
|
|
|
505
|
-
def saveDeduplicatedInstance(
|
|
564
|
+
def saveDeduplicatedInstance(
|
|
565
|
+
modelXbrl: ModelXbrl, deduplicationType: DeduplicationType, outputFilepath: str
|
|
566
|
+
) -> None:
|
|
506
567
|
deduplicatedFacts = frozenset(getDeduplicatedFacts(modelXbrl, deduplicationType))
|
|
507
568
|
duplicateFacts = set(modelXbrl.facts) - deduplicatedFacts
|
|
508
569
|
for fact in duplicateFacts:
|
|
@@ -513,7 +574,9 @@ def saveDeduplicatedInstance(modelXbrl: ModelXbrl, deduplicationType: Deduplicat
|
|
|
513
574
|
modelXbrl.saveInstance(overrideFilepath=outputFilepath)
|
|
514
575
|
modelXbrl.info(
|
|
515
576
|
"info:deduplicatedInstance",
|
|
516
|
-
_(
|
|
577
|
+
_(
|
|
578
|
+
"Deduplicated instance was saved after removing %(count)s fact(s): %(filepath)s"
|
|
579
|
+
),
|
|
517
580
|
count=len(duplicateFacts),
|
|
518
581
|
filepath=outputFilepath,
|
|
519
582
|
)
|
arelle/_version.py
CHANGED
|
@@ -1,105 +1,222 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
2
|
Save HTML EBA Tables is an example of a plug-in to both GUI menu and command line/web service
|
|
3
3
|
that will save a directory containing HTML Tablesets with an EBA index page.
|
|
4
4
|
|
|
5
5
|
See COPYRIGHT.md for copyright information.
|
|
6
|
-
|
|
7
|
-
from arelle.Version import authorLabel, copyrightLabel
|
|
6
|
+
"""
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
import os
|
|
9
|
+
import threading
|
|
10
|
+
from operator import itemgetter
|
|
11
|
+
from optparse import OptionParser
|
|
12
|
+
from tkinter import Menu
|
|
13
|
+
from typing import Any
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
from lxml import etree
|
|
16
|
+
|
|
17
|
+
from arelle import Version, XbrlConst, XmlUtil
|
|
18
|
+
from arelle.CntlrCmdLine import CntlrCmdLine
|
|
19
|
+
from arelle.CntlrWinMain import CntlrWinMain
|
|
20
|
+
from arelle.ModelDocument import Type
|
|
21
|
+
from arelle.ModelObjectFactory import parser
|
|
22
|
+
from arelle.ModelRenderingObject import DefnMdlTable
|
|
23
|
+
from arelle.ModelXbrl import ModelXbrl
|
|
24
|
+
from arelle.rendering import RenderingEvaluator
|
|
25
|
+
from arelle.RuntimeOptions import RuntimeOptions
|
|
26
|
+
from arelle.typing import TypeGetText
|
|
27
|
+
from arelle.utils.PluginHooks import PluginHooks
|
|
28
|
+
from arelle.ViewFileRenderedGrid import viewRenderedGrid
|
|
29
|
+
|
|
30
|
+
_: TypeGetText
|
|
31
|
+
|
|
32
|
+
MENU_HTML = """<!DOCTYPE html>
|
|
33
|
+
<html>
|
|
34
|
+
<head>
|
|
35
|
+
<style>
|
|
36
|
+
body {
|
|
37
|
+
margin: 0;
|
|
38
|
+
padding: 10px;
|
|
39
|
+
font-family: Arial, sans-serif;
|
|
40
|
+
color: #243e5e;
|
|
41
|
+
}
|
|
42
|
+
.nav-list-menu-ul {
|
|
43
|
+
list-style-type: none;
|
|
44
|
+
padding: 0;
|
|
45
|
+
margin: 0;
|
|
46
|
+
}
|
|
47
|
+
.nav-list-menu-li {
|
|
48
|
+
margin: 5px 0;
|
|
49
|
+
padding: 5px;
|
|
50
|
+
border-bottom: 1px solid #eee;
|
|
51
|
+
}
|
|
52
|
+
.nav-list-menu-link {
|
|
53
|
+
text-decoration: none;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
display: block;
|
|
56
|
+
background: none;
|
|
57
|
+
border: none;
|
|
58
|
+
padding: 0;
|
|
59
|
+
}
|
|
60
|
+
.nav-list-menu-link:hover {
|
|
61
|
+
text-decoration: underline;
|
|
62
|
+
}
|
|
63
|
+
</style>
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<ul class="nav-list-menu-ul"/>
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|
|
69
|
+
"""
|
|
17
70
|
|
|
18
|
-
|
|
19
|
-
<html
|
|
20
|
-
<head
|
|
21
|
-
<
|
|
71
|
+
CENTER_LANDING_HTML = """<!DOCTYPE html>
|
|
72
|
+
<html>
|
|
73
|
+
<head>
|
|
74
|
+
<style>
|
|
75
|
+
body {
|
|
76
|
+
margin: 0;
|
|
77
|
+
padding: 20px;
|
|
78
|
+
font-family: Arial, sans-serif;
|
|
79
|
+
}
|
|
80
|
+
#page-title {
|
|
81
|
+
margin-bottom: 20px;
|
|
82
|
+
}
|
|
83
|
+
#page-title h1 {
|
|
84
|
+
color: #243e5e;
|
|
85
|
+
margin-top: 0;
|
|
86
|
+
}
|
|
87
|
+
#content-center {
|
|
88
|
+
margin-top: 20px;
|
|
89
|
+
line-height: 1.5;
|
|
90
|
+
}
|
|
91
|
+
</style>
|
|
22
92
|
</head>
|
|
23
|
-
<body
|
|
24
|
-
|
|
93
|
+
<body>
|
|
94
|
+
<div id="page-title">
|
|
95
|
+
<h1>Taxonomy Tables Viewer</h1>
|
|
96
|
+
</div>
|
|
97
|
+
<div id="content-center">
|
|
98
|
+
<p>Please select tables to view by clicking in the left column.</p>
|
|
99
|
+
</div>
|
|
100
|
+
</body>
|
|
101
|
+
</html>
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
TABLE_CSS_EXTRAS = """
|
|
105
|
+
table {background:#fff}
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def indexFileHTML(indexBaseName: str) -> str:
|
|
110
|
+
return f'''<!DOCTYPE html>
|
|
111
|
+
<html>
|
|
112
|
+
<head>
|
|
113
|
+
<meta charset="utf-8">
|
|
114
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
115
|
+
<title>EBA - Tablesets</title>
|
|
116
|
+
<style>
|
|
117
|
+
html, body {{
|
|
118
|
+
margin: 0;
|
|
119
|
+
padding: 0;
|
|
120
|
+
height: 100%;
|
|
121
|
+
width: 100%;
|
|
122
|
+
font-family: Arial, sans-serif;
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
}}
|
|
126
|
+
#header {{
|
|
127
|
+
background: rgb(36, 62, 94);
|
|
128
|
+
color: rgb(255, 255, 255);
|
|
129
|
+
height: 40px;
|
|
130
|
+
}}
|
|
131
|
+
#header h1 {{
|
|
132
|
+
font-size: 1.5em;
|
|
133
|
+
margin: 0.25em;
|
|
134
|
+
}}
|
|
135
|
+
#main-container {{
|
|
136
|
+
display: flex;
|
|
137
|
+
flex: 1;
|
|
138
|
+
height: calc(100vh - 40px);
|
|
139
|
+
}}
|
|
140
|
+
#menu-container {{
|
|
141
|
+
width: 360px;
|
|
142
|
+
border-right: 2px solid #243e5e;
|
|
143
|
+
overflow-y: auto;
|
|
144
|
+
box-sizing: border-box;
|
|
145
|
+
}}
|
|
146
|
+
#content-container {{
|
|
147
|
+
flex: 1;
|
|
148
|
+
overflow: auto;
|
|
149
|
+
box-sizing: border-box;
|
|
150
|
+
}}
|
|
151
|
+
iframe {{
|
|
152
|
+
border: none;
|
|
153
|
+
width: 100%;
|
|
154
|
+
height: 100%;
|
|
155
|
+
}}
|
|
156
|
+
</style>
|
|
157
|
+
<script>
|
|
158
|
+
function loadContent(url) {{
|
|
159
|
+
document.getElementById('content-frame').src = url;
|
|
160
|
+
}}
|
|
161
|
+
</script>
|
|
162
|
+
</head>
|
|
163
|
+
<body>
|
|
164
|
+
<div id="header"><h1>EBA - Tablesets</h1></div>
|
|
165
|
+
<div id="main-container">
|
|
166
|
+
<div id="menu-container">
|
|
167
|
+
<iframe src="{indexBaseName}FormsFrame.html" width="100%" height="100%" frameborder="0" id="menu-frame"></iframe>
|
|
168
|
+
</div>
|
|
169
|
+
<div id="content-container">
|
|
170
|
+
<iframe src="{indexBaseName}CenterLanding.html" width="100%" height="100%" frameborder="0" id="content-frame"></iframe>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
25
173
|
</body>
|
|
26
174
|
</html>
|
|
27
175
|
'''
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class nonTkBooleanVar():
|
|
39
|
-
def __init__(self, value=True):
|
|
40
|
-
self.value = value
|
|
41
|
-
def set(self, value):
|
|
42
|
-
self.value = value
|
|
43
|
-
def get(self):
|
|
44
|
-
return self.value
|
|
45
|
-
|
|
46
|
-
class View():
|
|
47
|
-
def __init__(self, tableOrELR, ignoreDimValidity, xAxisChildrenFirst, yAxisChildrenFirst):
|
|
48
|
-
self.tblELR = tableOrELR
|
|
49
|
-
# context menu boolean vars (non-tkinter boolean
|
|
50
|
-
self.ignoreDimValidity = nonTkBooleanVar(value=ignoreDimValidity)
|
|
51
|
-
self.xAxisChildrenFirst = nonTkBooleanVar(value=xAxisChildrenFirst)
|
|
52
|
-
self.yAxisChildrenFirst = nonTkBooleanVar(value=yAxisChildrenFirst)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def generateHtmlEbaTablesetFiles(dts: ModelXbrl, indexFile: str, lang: str = "en") -> None:
|
|
179
|
+
try:
|
|
180
|
+
numTableFiles = 0
|
|
181
|
+
_parser = parser(dts, None)[0]
|
|
182
|
+
menuFrameDocument = etree.fromstring(MENU_HTML, parser=_parser, base_url=indexFile)
|
|
183
|
+
listElt = menuFrameDocument.find(".//ul")
|
|
184
|
+
assert listElt is not None, "No list element in index document"
|
|
53
185
|
|
|
54
186
|
indexBase = indexFile.rpartition(".")[0]
|
|
55
187
|
groupTableRels = dts.modelXbrl.relationshipSet(XbrlConst.euGroupTable)
|
|
56
188
|
modelTables = []
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
table {background:#fff}
|
|
60
|
-
'''
|
|
61
|
-
# order number is missing
|
|
62
|
-
def viewTable(modelTable):
|
|
189
|
+
|
|
190
|
+
def viewTable(modelTable: DefnMdlTable) -> None:
|
|
63
191
|
if modelTable is None:
|
|
64
192
|
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
dts.modelManager.cntlr.addToLog("viewing: " +
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
tableName = modelTable.id
|
|
193
|
+
tableId = modelTable.id or ""
|
|
194
|
+
if isinstance(modelTable, DefnMdlTable):
|
|
195
|
+
dts.modelManager.cntlr.addToLog("viewing: " + tableId)
|
|
196
|
+
tblFile = os.path.join(os.path.dirname(indexFile), tableId + ".html")
|
|
197
|
+
tableName = tableId
|
|
71
198
|
if tableName.startswith("eba_t"):
|
|
72
199
|
tableName = tableName.removeprefix("eba_t")
|
|
73
|
-
|
|
200
|
+
elif tableName.startswith("srb_t"):
|
|
74
201
|
tableName = tableName.removeprefix("srb_t")
|
|
75
|
-
viewRenderedGrid(dts,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# generaate menu entry
|
|
82
|
-
elt = etree.SubElement(listElt, "{http://www.w3.org/1999/xhtml}li")
|
|
83
|
-
elt.set("class", "CMSListMenuLI")
|
|
84
|
-
elt.set("id", modelTable.id)
|
|
85
|
-
elt = etree.SubElement(elt, "{http://www.w3.org/1999/xhtml}a")
|
|
202
|
+
viewRenderedGrid(dts, tblFile, lang=lang, cssExtras=TABLE_CSS_EXTRAS, table=tableName) # type: ignore[no-untyped-call]
|
|
203
|
+
|
|
204
|
+
elt = etree.SubElement(listElt, "li")
|
|
205
|
+
elt.set("class", "nav-list-menu-li")
|
|
206
|
+
elt.set("id", tableId)
|
|
207
|
+
elt = etree.SubElement(elt, "button")
|
|
86
208
|
elt.text = modelTable.genLabel(lang=lang, strip=True)
|
|
87
|
-
elt.set("class", "
|
|
88
|
-
elt.set("
|
|
89
|
-
elt.set("onClick", "javascript:parent.body.location.href='{0}';".format(modelTable.id + ".html"))
|
|
209
|
+
elt.set("class", "nav-list-menu-link")
|
|
210
|
+
elt.set("onClick", f"javascript:parent.loadContent('{tableId}.html');")
|
|
90
211
|
elt.text = modelTable.genLabel(lang=lang, strip=True)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
elt = etree.SubElement(listElt, "{http://www.w3.org/1999/xhtml}li")
|
|
95
|
-
elt.set("class", "CMSListMenuLink")
|
|
96
|
-
elt.set("id", modelTable.id)
|
|
212
|
+
else:
|
|
213
|
+
elt = etree.SubElement(listElt, "li")
|
|
214
|
+
elt.set("id", tableId)
|
|
97
215
|
elt.text = modelTable.label(lang=lang, strip=True)
|
|
98
216
|
|
|
99
217
|
for rel in groupTableRels.fromModelObject(modelTable):
|
|
100
218
|
viewTable(rel.toModelObject)
|
|
101
219
|
|
|
102
|
-
|
|
103
220
|
for rootConcept in groupTableRels.rootConcepts:
|
|
104
221
|
sourceline = 0
|
|
105
222
|
for rel in dts.modelXbrl.relationshipSet(XbrlConst.euGroupTable).fromModelObject(rootConcept):
|
|
@@ -107,171 +224,129 @@ table {background:#fff}
|
|
|
107
224
|
break
|
|
108
225
|
modelTables.append((rootConcept, sourceline))
|
|
109
226
|
|
|
110
|
-
for modelTable,
|
|
227
|
+
for modelTable, _order in sorted(modelTables, key=itemgetter(1)):
|
|
111
228
|
viewTable(modelTable)
|
|
112
229
|
|
|
113
|
-
with open(indexBase + "FormsFrame.html", "
|
|
114
|
-
XmlUtil.writexml(fh,
|
|
230
|
+
with open(indexBase + "FormsFrame.html", "w", encoding="utf-8") as fh:
|
|
231
|
+
XmlUtil.writexml(fh, menuFrameDocument, encoding="utf-8")
|
|
115
232
|
|
|
116
|
-
with open(indexFile, "
|
|
117
|
-
fh.write(
|
|
118
|
-
'''
|
|
119
|
-
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
120
|
-
<head id="Head1">
|
|
121
|
-
<title>European Banking Authority - EBA - FINREP Taxonomy</title>
|
|
122
|
-
<meta name="generator" content="Arelle(r) {0}" />
|
|
123
|
-
<meta name="provider" content="Aguilonius(r)" />
|
|
124
|
-
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
|
125
|
-
<meta http-equiv="pragma" content="no-cache" />
|
|
126
|
-
<meta http-equiv="content-style-type" content="text/css" />
|
|
127
|
-
<meta http-equiv="content-script-type" content="text/javascript" />
|
|
128
|
-
<link type="text/css" rel="stylesheet" href="http://arelle.org/files/EBA/style20121210/eba.css" />
|
|
129
|
-
</head>
|
|
130
|
-
<frameset border="0" frameborder="0" rows="90,*">
|
|
131
|
-
<frame name="head" src="{1}" scrolling="no" marginwidth="0" marginheight="10"/>
|
|
132
|
-
<frameset bordercolor="#0000cc" border="10" frameborder="no" framespacing="0" cols="360, *">
|
|
133
|
-
<frame src="{2}" name="menu" bordercolor="#0000cc"/>
|
|
134
|
-
<frame src="{3}" name="body" bordercolor="#0000cc"/>
|
|
135
|
-
</frameset>
|
|
136
|
-
</frameset>
|
|
137
|
-
'''.format(Version.version,
|
|
138
|
-
os.path.basename(indexBase) + "TopFrame.html",
|
|
139
|
-
os.path.basename(indexBase) + "FormsFrame.html",
|
|
140
|
-
os.path.basename(indexBase) + "CenterLanding.html",
|
|
141
|
-
))
|
|
142
|
-
|
|
143
|
-
with open(indexBase + "TopFrame.html", "wt", encoding="utf-8") as fh:
|
|
144
|
-
fh.write(
|
|
145
|
-
'''
|
|
146
|
-
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
147
|
-
<head id="Top">
|
|
148
|
-
<link type="text/css" rel="stylesheet" href="http://arelle.org/files/EBA/style20121210/eba.css" />
|
|
149
|
-
</head>
|
|
150
|
-
<body class="LTR IE7 ENGB">
|
|
151
|
-
<div id="topsection">
|
|
152
|
-
<div id="topsectionLeft" style="cursor:pointer;" onclick="location.href='http://www.eba.europa.eu/home.aspx';"></div>
|
|
153
|
-
<div id="topsectionRight"></div>
|
|
154
|
-
<div id="topnavigation">
|
|
155
|
-
<ul id="menuElem">
|
|
156
|
-
<li><a href="http://www.eba.europa.eu/topnav/Contacts.aspx">Contacts</a></li>
|
|
157
|
-
<li><a href="http://www.eba.europa.eu/topnav/Links.aspx">Links</a></li>
|
|
158
|
-
<li><a href="http://www.eba.europa.eu/topnav/Sitemap.aspx">Sitemap</a></li>
|
|
159
|
-
<li><a href="http://www.eba.europa.eu/topnav/Legal-Notice.aspx">Legal Notice</a></li>
|
|
160
|
-
</ul>
|
|
161
|
-
</div>
|
|
162
|
-
</body>
|
|
163
|
-
</html>
|
|
164
|
-
''')
|
|
165
|
-
|
|
166
|
-
with open(indexBase + "CenterLanding.html", "wt", encoding="utf-8") as fh:
|
|
167
|
-
fh.write(
|
|
168
|
-
'''
|
|
169
|
-
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
170
|
-
<head id="Center">
|
|
171
|
-
<link type="text/css" rel="stylesheet" href="http://http://arelle.org/files/EBA/style20121210/eba.css" />
|
|
172
|
-
</head>
|
|
173
|
-
<body class="LTR IE7 ENGB">
|
|
174
|
-
<div id="plc_lt_zoneContent_usercontrol_userControlElem_ContentPanel">
|
|
175
|
-
<div id="plc_lt_zoneContent_usercontrol_userControlElem_PanelTitle">
|
|
176
|
-
<div id="pagetitle" style="float:left;width:500px;">
|
|
177
|
-
<h1>Taxonomy Tables Viewer</h1>
|
|
178
|
-
</div>
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
<div style="clear:both;"></div>
|
|
182
|
-
<div id="contentcenter">
|
|
183
|
-
<p style="text-align: justify; margin-top: 0pt; margin-bottom: 0pt">Please select tables to view by clicking in the left column.</p>
|
|
184
|
-
</div>
|
|
185
|
-
</body>
|
|
186
|
-
</html>
|
|
187
|
-
''')
|
|
233
|
+
with open(indexFile, "w", encoding="utf-8") as fh:
|
|
234
|
+
fh.write(indexFileHTML(os.path.basename(indexBase)))
|
|
188
235
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
#zf = zipfile.ZipFile(__file__.rpartition('.')[0] + "Files.zip", mode="r")
|
|
192
|
-
#zf.extractall(path=os.path.dirname(indexBase))
|
|
193
|
-
#zf.close()
|
|
236
|
+
with open(indexBase + "CenterLanding.html", "w", encoding="utf-8") as fh:
|
|
237
|
+
fh.write(CENTER_LANDING_HTML)
|
|
194
238
|
|
|
195
|
-
dts.info(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
239
|
+
dts.info(
|
|
240
|
+
"info:saveEBAtables",
|
|
241
|
+
_("Tables index file of %(entryFile)s has %(numberTableFiles)s table files with index file %(indexFile)s."),
|
|
242
|
+
modelObject=dts,
|
|
243
|
+
entryFile=dts.uri,
|
|
244
|
+
numberTableFiles=numTableFiles,
|
|
245
|
+
indexFile=indexFile,
|
|
246
|
+
)
|
|
199
247
|
|
|
200
248
|
dts.modelManager.showStatus(_("Saved EBA HTML Table Files"), 5000)
|
|
201
249
|
except Exception as ex:
|
|
202
|
-
dts.error(
|
|
203
|
-
|
|
250
|
+
dts.error(
|
|
251
|
+
"exception",
|
|
252
|
+
_("HTML EBA Tableset files generation exception: %(error)s"),
|
|
253
|
+
error=ex,
|
|
204
254
|
modelXbrl=dts,
|
|
205
|
-
exc_info=True
|
|
255
|
+
exc_info=True,
|
|
256
|
+
)
|
|
206
257
|
|
|
207
|
-
def saveHtmlEbaTablesMenuEntender(cntlr, menu, *args, **kwargs):
|
|
208
|
-
# Extend menu with an item for the save infoset plugin
|
|
209
|
-
menu.add_command(label="Save HTML EBA Tables",
|
|
210
|
-
underline=0,
|
|
211
|
-
command=lambda: saveHtmlEbaTablesMenuCommand(cntlr) )
|
|
212
258
|
|
|
213
|
-
def saveHtmlEbaTablesMenuCommand(cntlr):
|
|
214
|
-
# save Infoset menu item has been invoked
|
|
215
|
-
from arelle.ModelDocument import Type
|
|
259
|
+
def saveHtmlEbaTablesMenuCommand(cntlr: CntlrWinMain) -> None:
|
|
216
260
|
if cntlr.modelManager is None or cntlr.modelManager.modelXbrl is None:
|
|
217
|
-
cntlr.addToLog("No DTS loaded.")
|
|
261
|
+
cntlr.addToLog("No DTS loaded.") # type: ignore[no-untyped-call]
|
|
218
262
|
return
|
|
219
263
|
|
|
220
|
-
|
|
221
|
-
indexFile = cntlr.uiFileDialog(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
264
|
+
assert cntlr.config is not None
|
|
265
|
+
indexFile = cntlr.uiFileDialog( # type: ignore[no-untyped-call]
|
|
266
|
+
"save",
|
|
267
|
+
title=_("arelle - Save HTML EBA Tables Index file"),
|
|
268
|
+
initialdir=cntlr.config.setdefault("htmlEbaTablesFileDir", "."),
|
|
269
|
+
filetypes=[(_("HTML index file .html"), "*.html")],
|
|
270
|
+
defaultextension=".html",
|
|
271
|
+
)
|
|
272
|
+
if not isinstance(indexFile, str):
|
|
273
|
+
return
|
|
229
274
|
cntlr.config["htmlEbaTablesFileDir"] = os.path.dirname(indexFile)
|
|
230
275
|
cntlr.saveConfig()
|
|
231
276
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
277
|
+
thread = threading.Thread(
|
|
278
|
+
target=lambda _dts=cntlr.modelManager.modelXbrl, _indexFile=indexFile: generateHtmlEbaTablesetFiles(
|
|
279
|
+
_dts, _indexFile
|
|
280
|
+
)
|
|
281
|
+
)
|
|
237
282
|
thread.daemon = True
|
|
238
283
|
thread.start()
|
|
239
284
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
285
|
+
|
|
286
|
+
class SaveHtmlEbaTablesPlugin(PluginHooks):
|
|
287
|
+
@staticmethod
|
|
288
|
+
def cntlrWinMainMenuTools(
|
|
289
|
+
cntlr: CntlrWinMain,
|
|
290
|
+
menu: Menu,
|
|
291
|
+
*args: Any,
|
|
292
|
+
**kwargs: Any,
|
|
293
|
+
) -> None:
|
|
294
|
+
menu.add_command(label="Save HTML EBA Tables", underline=0, command=lambda: saveHtmlEbaTablesMenuCommand(cntlr))
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def cntlrCmdLineOptions(
|
|
298
|
+
parser: OptionParser,
|
|
299
|
+
*args: Any,
|
|
300
|
+
**kwargs: Any,
|
|
301
|
+
) -> None:
|
|
302
|
+
parser.add_option(
|
|
303
|
+
"--save-EBA-tablesets",
|
|
304
|
+
action="store",
|
|
305
|
+
dest="ebaTablesetIndexFile",
|
|
306
|
+
help=_("Save HTML EBA Tablesets index file with provided filename."),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
@staticmethod
|
|
310
|
+
def cntlrCmdLineXbrlLoaded(
|
|
311
|
+
cntlr: CntlrCmdLine,
|
|
312
|
+
options: RuntimeOptions,
|
|
313
|
+
modelXbrl: ModelXbrl,
|
|
314
|
+
*args: Any,
|
|
315
|
+
**kwargs: Any,
|
|
316
|
+
) -> None:
|
|
317
|
+
ebaTablesetIndexFile = getattr(options, "ebaTablesetIndexFile", None)
|
|
318
|
+
modelDocType = getattr(modelXbrl.modelDocument, "type", None)
|
|
319
|
+
if ebaTablesetIndexFile == "generateEBAFiles" and modelDocType in (Type.TESTCASESINDEX, Type.TESTCASE):
|
|
320
|
+
cntlr.modelManager.generateEBAFiles = True # type: ignore[attr-defined]
|
|
321
|
+
|
|
322
|
+
@staticmethod
|
|
323
|
+
def cntlrCmdLineXbrlRun(
|
|
324
|
+
cntlr: CntlrCmdLine,
|
|
325
|
+
options: RuntimeOptions,
|
|
326
|
+
modelXbrl: ModelXbrl,
|
|
327
|
+
*args: Any,
|
|
328
|
+
**kwargs: Any,
|
|
329
|
+
) -> None:
|
|
330
|
+
ebaTablesetIndexFile = getattr(options, "ebaTablesetIndexFile", None)
|
|
331
|
+
if ebaTablesetIndexFile is None or ebaTablesetIndexFile == "generateEBAFiles":
|
|
332
|
+
return
|
|
256
333
|
if cntlr.modelManager is None or cntlr.modelManager.modelXbrl is None:
|
|
257
334
|
cntlr.addToLog("No taxonomy loaded.")
|
|
258
335
|
return
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
RenderingEvaluator.init(modelXbrl)
|
|
262
|
-
generateHtmlEbaTablesetFiles(cntlr.modelManager.modelXbrl, options.ebaTablesetIndexFile)
|
|
336
|
+
RenderingEvaluator.init(modelXbrl) # type: ignore[no-untyped-call]
|
|
337
|
+
generateHtmlEbaTablesetFiles(cntlr.modelManager.modelXbrl, ebaTablesetIndexFile)
|
|
263
338
|
|
|
264
339
|
|
|
265
340
|
__pluginInfo__ = {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
341
|
+
"name": "Save HTML EBA Tables",
|
|
342
|
+
"version": "0.10",
|
|
343
|
+
"description": "This plug-in adds a feature to a directory containing HTML Tablesets with an EBA index page.",
|
|
344
|
+
"license": "Apache-2",
|
|
345
|
+
"author": Version.authorLabel,
|
|
346
|
+
"copyright": Version.copyrightLabel,
|
|
272
347
|
# classes of mount points (required)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
348
|
+
"CntlrWinMain.Menu.Tools": SaveHtmlEbaTablesPlugin.cntlrWinMainMenuTools,
|
|
349
|
+
"CntlrCmdLine.Options": SaveHtmlEbaTablesPlugin.cntlrCmdLineOptions,
|
|
350
|
+
"CntlrCmdLine.Xbrl.Loaded": SaveHtmlEbaTablesPlugin.cntlrCmdLineXbrlLoaded,
|
|
351
|
+
"CntlrCmdLine.Xbrl.Run": SaveHtmlEbaTablesPlugin.cntlrCmdLineXbrlRun,
|
|
277
352
|
}
|
arelle/utils/PluginHooks.py
CHANGED
|
@@ -94,6 +94,33 @@ class PluginHooks(ABC):
|
|
|
94
94
|
"""
|
|
95
95
|
raise NotImplementedError
|
|
96
96
|
|
|
97
|
+
@staticmethod
|
|
98
|
+
def cntlrCmdLineXbrlLoaded(
|
|
99
|
+
cntlr: CntlrCmdLine,
|
|
100
|
+
options: RuntimeOptions,
|
|
101
|
+
modelXbrl: ModelXbrl,
|
|
102
|
+
entrypoint: dict[str, str] | None = None,
|
|
103
|
+
responseZipStream: BinaryIO | None = None,
|
|
104
|
+
*args: Any,
|
|
105
|
+
**kwargs: Any,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Plugin hook: `CntlrCmdLine.Xbrl.Loaded`
|
|
109
|
+
|
|
110
|
+
This hook is triggered after loading, but prior to validation (if requested) and loading views.
|
|
111
|
+
It's useful if you need to perform operations with the XBRL model prior to rendering views.
|
|
112
|
+
|
|
113
|
+
:param cntlr: The [Cntlr](#arelle.Cntlr.Cntlr) being initialized.
|
|
114
|
+
:param options: Parsed options object.
|
|
115
|
+
:param modelXbrl: The loaded [ModelXbrl](#arelle.ModelXbrl.ModelXbrl).
|
|
116
|
+
:param entrypoint: The entrypoint that was parsed to load the model.
|
|
117
|
+
:param responseZipStream: The response zip stream if loaded from the webserver and the user requested a zip response.
|
|
118
|
+
:param args: Argument capture to ensure new parameters don't break plugin hook.
|
|
119
|
+
:param kwargs: Argument capture to ensure new named parameters don't break plugin hook.
|
|
120
|
+
:return: None
|
|
121
|
+
"""
|
|
122
|
+
raise NotImplementedError
|
|
123
|
+
|
|
97
124
|
@staticmethod
|
|
98
125
|
def cntlrCmdLineXbrlRun(
|
|
99
126
|
cntlr: CntlrCmdLine,
|
|
@@ -66,7 +66,7 @@ arelle/UiUtil.py,sha256=3G0xPclZI8xW_XQDbiFrmylB7Nd5muqi5n2x2oMkMZU,34218
|
|
|
66
66
|
arelle/Updater.py,sha256=ho8Z_9GOL39H1jHL3Gaw5uc6av7J8ZBB6dR_X-nF_e0,7124
|
|
67
67
|
arelle/UrlUtil.py,sha256=HrxZSG59EUMGMMGmWPuZkPi5-0BGqY3jAMkp7V4IdZo,32400
|
|
68
68
|
arelle/Validate.py,sha256=CBgvAqT_v3LwF0Z1b_QiiePf80x_sTqiSvA9RYBAwqI,55276
|
|
69
|
-
arelle/ValidateDuplicateFacts.py,sha256=
|
|
69
|
+
arelle/ValidateDuplicateFacts.py,sha256=074y-VWCOBHoi6iV6wDL_IXBtdz9oeI1uPvjEcC1oDs,21717
|
|
70
70
|
arelle/ValidateFilingText.py,sha256=XLlwKldK__191s6VDbgbhFeNhG5wp8hmZNr4kRCLu_E,53996
|
|
71
71
|
arelle/ValidateInfoset.py,sha256=Rz_XBi5Ha43KpxXYhjLolURcWVx5qmqyjLxw48Yt9Dg,20396
|
|
72
72
|
arelle/ValidateUtr.py,sha256=oxOPrOa1XEzBay4miXvx6eRLTnVFYUIJC9ueWUk4EkI,13633
|
|
@@ -123,7 +123,7 @@ arelle/XmlValidateConst.py,sha256=U_wN0Q-nWKwf6dKJtcu_83FXPn9c6P8JjzGA5b0w7P0,33
|
|
|
123
123
|
arelle/XmlValidateParticles.py,sha256=Mn6vhFl0ZKC_vag1mBwn1rH_x2jmlusJYqOOuxFPO2k,9231
|
|
124
124
|
arelle/XmlValidateSchema.py,sha256=6frtZOc1Yrx_5yYF6V6oHbScnglWrVbWr6xW4EHtLQI,7428
|
|
125
125
|
arelle/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
126
|
-
arelle/_version.py,sha256=
|
|
126
|
+
arelle/_version.py,sha256=sRa_qk_q3QBynjyOYjA-67YubqjzRKZedln2S9tzhN4,515
|
|
127
127
|
arelle/typing.py,sha256=Ct5lrNKRow_o9CraMEXNza8nFsJ_iGIKoUeGfPs2dxI,1084
|
|
128
128
|
arelle/api/Session.py,sha256=Vd09RAutWX7mxHSsrW7Bl8CsE1UzXpQpAJsZb55mqng,6188
|
|
129
129
|
arelle/archive/CustomLogger.py,sha256=v_JXOCQLDZcfaFWzxC9FRcEf9tQi4rCI4Sx7jCuAVQI,1231
|
|
@@ -321,7 +321,7 @@ arelle/plugin/profileCmdLine.py,sha256=uLL0fGshpiwtzyLKAtW0WuXAvcRtZgxQG6swM0e4B
|
|
|
321
321
|
arelle/plugin/profileFormula.py,sha256=PK273bQpYO-87QMFpBVQmnqGKfjDElIXPhAwc8Nyp04,5548
|
|
322
322
|
arelle/plugin/saveCHComponentFile.py,sha256=phW-n96P0BWONLqX3wBcuNcDa_IQ8QVfFWKL9NRrh7k,7242
|
|
323
323
|
arelle/plugin/saveDTS.py,sha256=D8hfFiM9Gc9LE7ZV0-pDvOHeU7Y5ebOzZx_nXzoZrl8,3216
|
|
324
|
-
arelle/plugin/saveHtmlEBAtables.py,sha256=
|
|
324
|
+
arelle/plugin/saveHtmlEBAtables.py,sha256=OITN4ftI_atUO5NvrDJDKUc0CAk67GY68Jl16xDBXiI,10961
|
|
325
325
|
arelle/plugin/saveLoadableExcel.py,sha256=ZPYB6injFabav0dRzgS7adCFzfHkwtftjl3PfBjijuU,20539
|
|
326
326
|
arelle/plugin/saveLoadableOIM.py,sha256=19R5mZzIDkPohEdabs22nOhIZoEPe8vR0HczqN3oSI0,36239
|
|
327
327
|
arelle/plugin/saveSKOS.py,sha256=7Z1Qedf83zMo9EbigKkxNpiMjzkTYQvLEnwWMObLc1Y,12465
|
|
@@ -697,7 +697,7 @@ arelle/scripts-macOS/startWebServer.command,sha256=KXLSwAwchDZBlL-k9PYXdf39RNBtt
|
|
|
697
697
|
arelle/scripts-unix/startWebServer.sh,sha256=_0puRzaGkdMZoFn3R7hDti9a3ryN6kTZAXwLweeZU1s,42
|
|
698
698
|
arelle/scripts-windows/startWebServer.bat,sha256=qmnF1yrjNo__bi4QodONWlN0qHShVLTKptJQYyZtgcY,122
|
|
699
699
|
arelle/utils/PluginData.py,sha256=GUnuZaApm1J4Xm9ZA1U2M1aask-AaNGviLtc0fgXbFg,265
|
|
700
|
-
arelle/utils/PluginHooks.py,sha256=
|
|
700
|
+
arelle/utils/PluginHooks.py,sha256=USi7zEZzwzTnKdKSyM3Xjacodo6YcU_Lb3EPQwWiVg4,30220
|
|
701
701
|
arelle/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
702
702
|
arelle/utils/validate/Decorator.py,sha256=8LGmA171HZgKrALtsMKyHqMNM-XCdwJOv6KpZz4pC2c,3161
|
|
703
703
|
arelle/utils/validate/DetectScriptsInXhtml.py,sha256=JOgsUP0_kvE6O3TuhzKxqKG1ZFP9LrUYZBgrar6JEm0,2178
|
|
@@ -716,6 +716,7 @@ tests/integration_tests/scripts/script_util.py,sha256=MuLV2X4NfaVcLXTSq1C4lxnInH
|
|
|
716
716
|
tests/integration_tests/scripts/test_scripts.py,sha256=6kSN385J3KG3k9rQN3aPP9bx70vzpCb4PbUGbGbJYU8,506
|
|
717
717
|
tests/integration_tests/scripts/tests/duplicate_facts_deduplication.py,sha256=qMWTwbIJmKMncEiDjoHIkQYJYKUmxj3JJ_S1hTlLAMs,4698
|
|
718
718
|
tests/integration_tests/scripts/tests/duplicate_facts_validate.py,sha256=hxguoZZnyaPdWfAeLwfbLpIRpTbB6O-nJ5E4j7XYLRE,3743
|
|
719
|
+
tests/integration_tests/scripts/tests/eba_tablesets.py,sha256=6uayfssQ7p3zyw6IUcIhraQMJiTsIVPxqYzQWdLx5q8,4232
|
|
719
720
|
tests/integration_tests/scripts/tests/entry_point_from_taxonomy_package.py,sha256=KCaK08pQ6N18lIUxhWbeyvNeYQdSHk0Rxi86On3W_n8,1525
|
|
720
721
|
tests/integration_tests/scripts/tests/ixbrl-viewer_cli.py,sha256=OdXj9p148MG9fycO3opyNIClYEDhyf8z4HKGFhrEAYU,2222
|
|
721
722
|
tests/integration_tests/scripts/tests/ixbrl-viewer_webserver.py,sha256=dDcftyfu9IA2qWih9CZUfoTmjeP_Zmng488N7Gp1wFU,2848
|
|
@@ -1552,9 +1553,9 @@ tests/unit_tests/arelle/oim/test_load.py,sha256=NxiUauQwJVfWAHbbpsMHGSU2d3Br8Pki
|
|
|
1552
1553
|
tests/unit_tests/arelle/plugin/test_plugin_imports.py,sha256=bdhIs9frAnFsdGU113yBk09_jis-z43dwUItMFYuSYM,1064
|
|
1553
1554
|
tests/unit_tests/arelle/plugin/validate/ESEF/ESEF_Current/test_validate_css_url.py,sha256=XHABmejQt7RlZ0udh7v42f2Xb2STGk_fSaIaJ9i2xo0,878
|
|
1554
1555
|
tests/unit_tests/arelle/utils/validate/test_decorator.py,sha256=ZS8FqIY1g-2FCbjF4UYm609dwViax6qBMRJSi0vfuhY,2482
|
|
1555
|
-
arelle_release-2.36.
|
|
1556
|
-
arelle_release-2.36.
|
|
1557
|
-
arelle_release-2.36.
|
|
1558
|
-
arelle_release-2.36.
|
|
1559
|
-
arelle_release-2.36.
|
|
1560
|
-
arelle_release-2.36.
|
|
1556
|
+
arelle_release-2.36.29.dist-info/LICENSE.md,sha256=rMbWwFLGzPgLoEjEu8LCmkpWDTqsvfOI-wzLSfeJsis,4107
|
|
1557
|
+
arelle_release-2.36.29.dist-info/METADATA,sha256=15UCnvMqfHvhaCle9jkkfDypnkx9UtyeUkHMfoDbGAk,9010
|
|
1558
|
+
arelle_release-2.36.29.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
|
|
1559
|
+
arelle_release-2.36.29.dist-info/entry_points.txt,sha256=Uj5niwfwVsx3vaQ3fYj8hrZ1xpfCJyTUA09tYKWbzpo,111
|
|
1560
|
+
arelle_release-2.36.29.dist-info/top_level.txt,sha256=ZYmYGmhW5Jvo3vJ4iXBZPUI29LvYhntom04w90esJvU,13
|
|
1561
|
+
arelle_release-2.36.29.dist-info/RECORD,,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import html.parser
|
|
4
|
+
import os
|
|
5
|
+
import urllib.request
|
|
6
|
+
import zipfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from shutil import rmtree
|
|
9
|
+
|
|
10
|
+
from tests.integration_tests.integration_test_util import get_s3_uri
|
|
11
|
+
from tests.integration_tests.scripts.script_util import (
|
|
12
|
+
assert_result,
|
|
13
|
+
parse_args,
|
|
14
|
+
prepare_logfile,
|
|
15
|
+
run_arelle,
|
|
16
|
+
validate_log_file,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
errors = []
|
|
20
|
+
this_file = Path(__file__)
|
|
21
|
+
args = parse_args(this_file.stem, "Confirm EBA Tablesets report runs successfully from the command line.")
|
|
22
|
+
arelle_command = args.arelle
|
|
23
|
+
arelle_offline = args.offline
|
|
24
|
+
working_directory = Path(args.working_directory)
|
|
25
|
+
test_directory = Path(args.test_directory)
|
|
26
|
+
arelle_log_file = prepare_logfile(test_directory, this_file)
|
|
27
|
+
samples_zip_path = test_directory / "eba_samples.zip"
|
|
28
|
+
samples_directory = test_directory / "eba_samples"
|
|
29
|
+
target_path = samples_directory / "DUMMYLEI123456789012_GB_COREP030000_COREPLECON_2021-06-30_20201218154732000.xbrl"
|
|
30
|
+
tablesets_report_path = test_directory / "index.html"
|
|
31
|
+
sample_report_zip_url = get_s3_uri("ci/packages/eba-samples.zip", version_id="iDJU3nFy6_rQ289k.mosenHjUFrXCmCM")
|
|
32
|
+
|
|
33
|
+
samples_url = get_s3_uri("ci/packages/eba_samples.zip", version_id="O7uYHbSYmxe_20nBhWWoXMfjGpquNMMj")
|
|
34
|
+
|
|
35
|
+
print(f"Downloading EBA sample files: {samples_zip_path}")
|
|
36
|
+
urllib.request.urlretrieve(samples_url, samples_zip_path)
|
|
37
|
+
|
|
38
|
+
print(f"Extracting EBA sample files: {samples_directory}")
|
|
39
|
+
with zipfile.ZipFile(samples_zip_path, "r") as zip_ref:
|
|
40
|
+
zip_ref.extractall(samples_directory)
|
|
41
|
+
|
|
42
|
+
print(f"Generating EBA Tablesets report: {tablesets_report_path}")
|
|
43
|
+
run_arelle(
|
|
44
|
+
arelle_command,
|
|
45
|
+
plugins=["saveHtmlEBAtables"],
|
|
46
|
+
additional_args=[
|
|
47
|
+
"-f",
|
|
48
|
+
str(target_path),
|
|
49
|
+
"--package",
|
|
50
|
+
str(samples_directory / "TP-Eurofiling2.1.zip"),
|
|
51
|
+
"--package",
|
|
52
|
+
str(samples_directory / "EBA_CRD_IV_XBRL_3.0_Dictionary_3.0.1.0.Errata3.zip"),
|
|
53
|
+
"--package",
|
|
54
|
+
str(samples_directory / "EBA_CRD_IV_XBRL_3.0_Reporting_COREP_FINREP_Frameworks_hotfix.Errata3.zip"),
|
|
55
|
+
"--save-EBA-tablesets",
|
|
56
|
+
str(tablesets_report_path),
|
|
57
|
+
],
|
|
58
|
+
offline=arelle_offline,
|
|
59
|
+
logFile=arelle_log_file,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
print(f"Checking for EBA Tablesets report: {tablesets_report_path}")
|
|
63
|
+
if not tablesets_report_path.exists():
|
|
64
|
+
errors.append(f'EBA Tablesets report not generated at "{tablesets_report_path}"')
|
|
65
|
+
|
|
66
|
+
eba_table_files = [
|
|
67
|
+
"eba_tC_00.01.html",
|
|
68
|
+
"eba_tC_26.00.html",
|
|
69
|
+
"eba_tC_27.00.html",
|
|
70
|
+
"eba_tC_28.00.html",
|
|
71
|
+
"eba_tC_29.00.html",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
eba_tablesets_report_files = [
|
|
75
|
+
test_directory / f for f in ("index.html", "indexCenterLanding.html", "indexFormsFrame.html", *eba_table_files)
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
for report_file in eba_tablesets_report_files:
|
|
79
|
+
if not report_file.exists():
|
|
80
|
+
errors.append(f'EBA Tablesets report file not generated at "{report_file}"')
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ButtonParser(html.parser.HTMLParser):
|
|
84
|
+
def __init__(self, table_files):
|
|
85
|
+
super().__init__()
|
|
86
|
+
self.table_files = table_files
|
|
87
|
+
self.found_buttons = {table_file: False for table_file in table_files}
|
|
88
|
+
|
|
89
|
+
def handle_starttag(self, tag, attrs):
|
|
90
|
+
if tag.lower() == "button":
|
|
91
|
+
attrs_dict = dict(attrs)
|
|
92
|
+
onclick = attrs_dict.get("onclick", "")
|
|
93
|
+
if onclick is not None:
|
|
94
|
+
for table_file in self.table_files:
|
|
95
|
+
expected_onclick = f"javascript:parent.loadContent('{table_file}');"
|
|
96
|
+
if expected_onclick in onclick:
|
|
97
|
+
self.found_buttons[table_file] = True
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
forms_frame_path = test_directory / "indexFormsFrame.html"
|
|
101
|
+
print("Checking for proper button elements in indexFormsFrame.html")
|
|
102
|
+
with open(forms_frame_path, encoding="utf-8") as fh:
|
|
103
|
+
html_content = fh.read()
|
|
104
|
+
parser = ButtonParser(eba_table_files)
|
|
105
|
+
parser.feed(html_content)
|
|
106
|
+
for table_file, found in parser.found_buttons.items():
|
|
107
|
+
if not found:
|
|
108
|
+
errors.append(f"Button for table {table_file} not found in indexFormsFrame.html")
|
|
109
|
+
|
|
110
|
+
print(f"Checking for log errors: {arelle_log_file}")
|
|
111
|
+
errors += validate_log_file(arelle_log_file)
|
|
112
|
+
|
|
113
|
+
assert_result(errors)
|
|
114
|
+
|
|
115
|
+
print("Cleaning up")
|
|
116
|
+
rmtree(samples_directory)
|
|
117
|
+
os.unlink(samples_zip_path)
|
|
118
|
+
for report_file in eba_tablesets_report_files:
|
|
119
|
+
os.unlink(report_file)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|