arelle-release 2.37.12__py3-none-any.whl → 2.37.14__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/CntlrCmdLine.py +1 -66
- arelle/ModelDtsObject.py +1 -1
- arelle/ModelInstanceObject.py +9 -0
- arelle/ModelTestcaseObject.py +8 -1
- arelle/Validate.py +58 -23
- arelle/ValidateDuplicateFacts.py +4 -10
- arelle/_version.py +2 -2
- arelle/formula/XPathParser.py +9 -3
- arelle/packages/report/ReportPackage.py +7 -3
- arelle/packages/report/ReportPackageValidator.py +4 -3
- arelle/plugin/inlineXbrlDocumentSet.py +1 -1
- arelle/plugin/validate/DBA/PluginValidationDataExtension.py +9 -1
- arelle/plugin/validate/DBA/ValidationPluginExtension.py +4 -2
- arelle/plugin/validate/DBA/rules/fr.py +30 -32
- arelle/plugin/validate/DBA/rules/tm.py +22 -12
- arelle/plugin/validate/ESEF/ESEF_2021/ValidateXbrlFinally.py +3 -3
- arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +4 -5
- arelle/plugin/validate/NL/PluginValidationDataExtension.py +163 -32
- arelle/plugin/validate/NL/__init__.py +0 -11
- arelle/plugin/validate/NL/rules/nl_kvk.py +322 -12
- arelle/plugin/validate/ROS/rules/ros.py +1 -1
- arelle/typing.py +8 -1
- arelle/utils/EntryPointDetection.py +73 -0
- {arelle_release-2.37.12.dist-info → arelle_release-2.37.14.dist-info}/METADATA +1 -1
- {arelle_release-2.37.12.dist-info → arelle_release-2.37.14.dist-info}/RECORD +40 -38
- {arelle_release-2.37.12.dist-info → arelle_release-2.37.14.dist-info}/WHEEL +1 -1
- tests/integration_tests/ui_tests/ArelleGUITest/ArelleGUITest/ArelleGUITest.csproj +1 -1
- tests/integration_tests/validation/conformance_suite_configurations/nl_inline_2024.py +14 -21
- tests/integration_tests/validation/conformance_suite_configurations/xbrl_report_packages_1_0.py +3 -1
- tests/resources/conformance_suites/dba/fr/fr7-invalid.xhtml +1 -1
- tests/resources/conformance_suites/dba/fr/fr83-invalid.xbrl +1 -1
- tests/resources/conformance_suites/dba/fr/fr89-testcase.xml +10 -0
- tests/resources/conformance_suites/dba/fr/fr89-valid.xhtml +6816 -0
- tests/resources/conformance_suites/dba/fr/fr91-invalid.xhtml +1 -2
- tests/resources/conformance_suites/dba/tm/tm29-invalid.xhtml +0 -1
- tests/resources/conformance_suites/dba/tm/tm31-invalid.xhtml +10 -1
- tests/resources/conformance_suites/dba/tr/tr06-invalid.xhtml +1 -1
- {arelle_release-2.37.12.dist-info → arelle_release-2.37.14.dist-info}/entry_points.txt +0 -0
- {arelle_release-2.37.12.dist-info → arelle_release-2.37.14.dist-info}/licenses/LICENSE.md +0 -0
- {arelle_release-2.37.12.dist-info → arelle_release-2.37.14.dist-info}/top_level.txt +0 -0
|
@@ -228,12 +228,12 @@ def validateXbrlFinally(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None:
|
|
|
228
228
|
_baseName, _baseExt = os.path.splitext(doc.basename)
|
|
229
229
|
if _baseExt not in (".xhtml",".html"):
|
|
230
230
|
if val.consolidated:
|
|
231
|
-
|
|
231
|
+
errorCode = "ESEF.2.6.1.incorrectFileExtension"
|
|
232
232
|
reportType = _("Inline XBRL document included within a ESEF report package")
|
|
233
233
|
else:
|
|
234
|
-
|
|
234
|
+
errorCode = "ESEF.4.1.1.incorrectFileExtension"
|
|
235
235
|
reportType = _("Stand-alone XHTML document")
|
|
236
|
-
modelXbrl.error(
|
|
236
|
+
modelXbrl.error(errorCode,
|
|
237
237
|
_("%(reportType)s MUST have a .html or .xhtml extension: %(fileName)s"),
|
|
238
238
|
modelObject=doc, fileName=doc.basename, reportType=reportType)
|
|
239
239
|
docinfo = doc.xmlRootElement.getroottree().docinfo
|
|
@@ -664,8 +664,7 @@ def validateXbrlFinally(val: ValidateXbrl, *args: Any, **kwargs: Any) -> None:
|
|
|
664
664
|
if esefDisclosureSystemYear >= 2024:
|
|
665
665
|
if not f.id:
|
|
666
666
|
factsMissingId.append(f)
|
|
667
|
-
|
|
668
|
-
if f.concept is not None and escaped != f.concept.isTextBlock:
|
|
667
|
+
if isinstance(f, ModelInlineFact) and f.concept is not None and f.isEscaped != f.concept.isTextBlock:
|
|
669
668
|
modelXbrl.error("ESEF.2.2.7.improperApplicationOfEscapeAttribute",
|
|
670
669
|
_("Facts with datatype 'dtr-types:textBlockItemType' MUST use the 'escape' attribute set to 'true'. Facts with any other datatype MUST use the 'escape' attribute set to 'false' - fact %(conceptName)s"),
|
|
671
670
|
modelObject=f, conceptName=f.concept.qname)
|
|
@@ -6,18 +6,23 @@ from __future__ import annotations
|
|
|
6
6
|
from collections import defaultdict
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from functools import lru_cache
|
|
9
|
-
from
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, TYPE_CHECKING, cast
|
|
10
11
|
|
|
11
12
|
import regex as re
|
|
12
13
|
from lxml.etree import _Comment, _ElementTree, _Entity, _ProcessingInstruction
|
|
13
14
|
|
|
14
15
|
from arelle.FunctionIxt import ixtNamespaces
|
|
15
|
-
from arelle.ModelInstanceObject import ModelContext, ModelFact, ModelInlineFootnote, ModelUnit
|
|
16
|
+
from arelle.ModelInstanceObject import ModelContext, ModelFact, ModelInlineFootnote, ModelUnit, ModelInlineFact
|
|
17
|
+
from arelle.ModelObject import ModelObject
|
|
16
18
|
from arelle.ModelValue import QName
|
|
17
19
|
from arelle.ModelXbrl import ModelXbrl
|
|
20
|
+
from arelle.typing import assert_type
|
|
18
21
|
from arelle.utils.PluginData import PluginData
|
|
19
22
|
from arelle.utils.validate.ValidationUtil import etreeIterWithDepth
|
|
23
|
+
from arelle.XbrlConst import ixbrl11
|
|
20
24
|
from arelle.XmlValidate import lexicalPatterns
|
|
25
|
+
from arelle.XmlValidateConst import VALID
|
|
21
26
|
|
|
22
27
|
XBRLI_IDENTIFIER_PATTERN = re.compile(r"^(?!00)\d{8}$")
|
|
23
28
|
XBRLI_IDENTIFIER_SCHEMA = 'http://www.kvk.nl/kvk-id'
|
|
@@ -27,6 +32,25 @@ DISALLOWED_IXT_NAMESPACES = frozenset((
|
|
|
27
32
|
ixtNamespaces["ixt v2"],
|
|
28
33
|
ixtNamespaces["ixt v3"],
|
|
29
34
|
))
|
|
35
|
+
UNTRANSFORMABLE_TYPES = frozenset((
|
|
36
|
+
"anyURI",
|
|
37
|
+
"base64Binary",
|
|
38
|
+
"duration",
|
|
39
|
+
"hexBinary",
|
|
40
|
+
"NOTATION",
|
|
41
|
+
"QName",
|
|
42
|
+
"time",
|
|
43
|
+
"token",
|
|
44
|
+
"language",
|
|
45
|
+
))
|
|
46
|
+
STYLE_IX_HIDDEN_PATTERN = re.compile(r"(.*[^\w]|^)ix-hidden\s*:\s*([\w.-]+).*")
|
|
47
|
+
|
|
48
|
+
ALLOWABLE_LANGUAGES = frozenset((
|
|
49
|
+
'nl',
|
|
50
|
+
'en',
|
|
51
|
+
'de',
|
|
52
|
+
'fr'
|
|
53
|
+
))
|
|
30
54
|
|
|
31
55
|
@dataclass(frozen=True)
|
|
32
56
|
class ContextData:
|
|
@@ -36,10 +60,18 @@ class ContextData:
|
|
|
36
60
|
contextsWithSegments: list[ModelContext | None]
|
|
37
61
|
|
|
38
62
|
@dataclass(frozen=True)
|
|
39
|
-
class
|
|
63
|
+
class HiddenElementsData:
|
|
64
|
+
eligibleForTransformHiddenFacts: set[ModelInlineFact]
|
|
65
|
+
hiddenFactsOutsideHiddenSection: set[ModelInlineFact]
|
|
66
|
+
requiredToDisplayFacts: set[ModelInlineFact]
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class InlineHTMLData:
|
|
40
70
|
noMatchLangFootnotes: set[ModelInlineFootnote]
|
|
41
71
|
orphanedFootnotes: set[ModelInlineFootnote]
|
|
72
|
+
tupleElements: set[tuple[Any]]
|
|
42
73
|
factLangFootnotes: dict[ModelInlineFootnote, set[str]]
|
|
74
|
+
fractionElements: set[Any]
|
|
43
75
|
|
|
44
76
|
@dataclass
|
|
45
77
|
class PluginValidationDataExtension(PluginData):
|
|
@@ -101,29 +133,83 @@ class PluginValidationDataExtension(PluginData):
|
|
|
101
133
|
)
|
|
102
134
|
|
|
103
135
|
@lru_cache(1)
|
|
104
|
-
def
|
|
136
|
+
def checkHiddenElements(self, modelXbrl: ModelXbrl) -> HiddenElementsData:
|
|
137
|
+
eligibleForTransformHiddenFacts = set()
|
|
138
|
+
hiddenEltIds = {}
|
|
139
|
+
hiddenFactsOutsideHiddenSection = set()
|
|
140
|
+
presentedHiddenEltIds = defaultdict(list)
|
|
141
|
+
requiredToDisplayFacts = set()
|
|
142
|
+
for ixdsHtmlRootElt in modelXbrl.ixdsHtmlElements:
|
|
143
|
+
ixNStag = getattr(ixdsHtmlRootElt.modelDocument, "ixNStag", ixbrl11)
|
|
144
|
+
for ixHiddenElt in ixdsHtmlRootElt.iterdescendants(tag=ixNStag + "hidden"):
|
|
145
|
+
for tag in (ixNStag + "nonNumeric", ixNStag+"nonFraction"):
|
|
146
|
+
for ixElt in ixHiddenElt.iterdescendants(tag=tag):
|
|
147
|
+
if getattr(ixElt, "xValid", 0) >= VALID:
|
|
148
|
+
if ixElt.concept.baseXsdType not in UNTRANSFORMABLE_TYPES and not ixElt.isNil:
|
|
149
|
+
eligibleForTransformHiddenFacts.add(ixElt)
|
|
150
|
+
if ixElt.id:
|
|
151
|
+
hiddenEltIds[ixElt.id] = ixElt
|
|
152
|
+
for ixdsHtmlRootElt in modelXbrl.ixdsHtmlElements:
|
|
153
|
+
for ixElt in ixdsHtmlRootElt.getroottree().iterfind(".//{http://www.w3.org/1999/xhtml}*[@style]"):
|
|
154
|
+
styleValue = ixElt.get("style","")
|
|
155
|
+
hiddenFactRefMatch = STYLE_IX_HIDDEN_PATTERN.match(styleValue)
|
|
156
|
+
if hiddenFactRefMatch:
|
|
157
|
+
hiddenFactRef = hiddenFactRefMatch.group(2)
|
|
158
|
+
if hiddenFactRef not in hiddenEltIds:
|
|
159
|
+
hiddenFactsOutsideHiddenSection.add(ixElt)
|
|
160
|
+
else:
|
|
161
|
+
presentedHiddenEltIds[hiddenFactRef].append(ixElt)
|
|
162
|
+
for hiddenEltId, ixElt in hiddenEltIds.items():
|
|
163
|
+
if (hiddenEltId not in presentedHiddenEltIds and
|
|
164
|
+
getattr(ixElt, "xValid", 0) >= VALID and # may not be validated
|
|
165
|
+
(ixElt.concept.baseXsdType in UNTRANSFORMABLE_TYPES or ixElt.isNil)):
|
|
166
|
+
requiredToDisplayFacts.add(ixElt)
|
|
167
|
+
return HiddenElementsData(
|
|
168
|
+
eligibleForTransformHiddenFacts=eligibleForTransformHiddenFacts,
|
|
169
|
+
hiddenFactsOutsideHiddenSection=hiddenFactsOutsideHiddenSection,
|
|
170
|
+
requiredToDisplayFacts=requiredToDisplayFacts,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
@lru_cache(1)
|
|
174
|
+
def checkInlineHTMLElements(self, modelXbrl: ModelXbrl) -> InlineHTMLData:
|
|
105
175
|
factLangs = self.factLangs(modelXbrl)
|
|
106
176
|
footnotesRelationshipSet = modelXbrl.relationshipSet("XBRL-footnotes")
|
|
107
|
-
orphanedFootnotes = set()
|
|
108
|
-
noMatchLangFootnotes = set()
|
|
109
177
|
factLangFootnotes = defaultdict(set)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
178
|
+
fractionElements = set()
|
|
179
|
+
noMatchLangFootnotes = set()
|
|
180
|
+
tupleElements = set()
|
|
181
|
+
orphanedFootnotes = set()
|
|
182
|
+
for ixdsHtmlRootElt in modelXbrl.ixdsHtmlElements:
|
|
183
|
+
ixNStag = getattr(ixdsHtmlRootElt.modelDocument, "ixNStag", ixbrl11)
|
|
184
|
+
ixTupleTag = ixNStag + "tuple"
|
|
185
|
+
ixFractionTag = ixNStag + "fraction"
|
|
186
|
+
for elts in modelXbrl.ixdsEltById.values(): # type: ignore[attr-defined]
|
|
187
|
+
for elt in elts:
|
|
188
|
+
if isinstance(elt, ModelInlineFootnote):
|
|
189
|
+
if elt.textValue is not None:
|
|
190
|
+
if not any(isinstance(rel.fromModelObject, ModelFact)
|
|
191
|
+
for rel in footnotesRelationshipSet.toModelObject(elt)):
|
|
192
|
+
orphanedFootnotes.add(elt)
|
|
193
|
+
if elt.xmlLang not in factLangs:
|
|
194
|
+
noMatchLangFootnotes.add(elt)
|
|
195
|
+
if elt.xmlLang is not None:
|
|
196
|
+
for rel in footnotesRelationshipSet.toModelObject(elt):
|
|
197
|
+
if rel.fromModelObject is not None:
|
|
198
|
+
fromObj = cast(ModelObject, rel.fromModelObject)
|
|
199
|
+
lang = cast(str, elt.xmlLang)
|
|
200
|
+
factLangFootnotes[fromObj].add(lang)
|
|
201
|
+
if elt.tag == ixTupleTag:
|
|
202
|
+
tupleElements.add(elt)
|
|
203
|
+
if elt.tag == ixFractionTag:
|
|
204
|
+
fractionElements.add(elt)
|
|
122
205
|
factLangFootnotes.default_factory = None
|
|
123
|
-
|
|
206
|
+
assert_type(factLangFootnotes, defaultdict[ModelObject, set[str]])
|
|
207
|
+
return InlineHTMLData(
|
|
208
|
+
factLangFootnotes=cast(dict[ModelInlineFootnote, set[str]], factLangFootnotes),
|
|
209
|
+
fractionElements=fractionElements,
|
|
124
210
|
noMatchLangFootnotes=noMatchLangFootnotes,
|
|
125
211
|
orphanedFootnotes=orphanedFootnotes,
|
|
126
|
-
|
|
212
|
+
tupleElements=tupleElements,
|
|
127
213
|
)
|
|
128
214
|
|
|
129
215
|
@lru_cache(1)
|
|
@@ -158,33 +244,78 @@ class PluginValidationDataExtension(PluginData):
|
|
|
158
244
|
def getContextsWithSegments(self, modelXbrl: ModelXbrl) -> list[ModelContext | None]:
|
|
159
245
|
return self.checkContexts(modelXbrl).contextsWithSegments
|
|
160
246
|
|
|
247
|
+
def getEligibleForTransformHiddenFacts(self, modelXbrl: ModelXbrl) -> set[ModelInlineFact]:
|
|
248
|
+
return self.checkHiddenElements(modelXbrl).eligibleForTransformHiddenFacts
|
|
249
|
+
|
|
250
|
+
def getFactLangFootnotes(self, modelXbrl: ModelXbrl) -> dict[ModelInlineFootnote, set[str]]:
|
|
251
|
+
return self.checkInlineHTMLElements(modelXbrl).factLangFootnotes
|
|
252
|
+
|
|
253
|
+
def getFractionElements(self, modelXbrl: ModelXbrl) -> set[Any]:
|
|
254
|
+
return self.checkInlineHTMLElements(modelXbrl).fractionElements
|
|
255
|
+
|
|
256
|
+
def getHiddenFactsOutsideHiddenSection(self, modelXbrl: ModelXbrl) -> set[ModelInlineFact]:
|
|
257
|
+
return self.checkHiddenElements(modelXbrl).hiddenFactsOutsideHiddenSection
|
|
258
|
+
|
|
259
|
+
@lru_cache(1)
|
|
260
|
+
def getFilenameAllowedCharactersPattern(self) -> re.Pattern[str]:
|
|
261
|
+
return re.compile(
|
|
262
|
+
r"^[\w\.-]*$",
|
|
263
|
+
flags=re.ASCII
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
@lru_cache(1)
|
|
267
|
+
def getFilenameFormatPattern(self) -> re.Pattern[str]:
|
|
268
|
+
return re.compile(
|
|
269
|
+
r"^(?<base>[^-]*)"
|
|
270
|
+
r"-(?<year>\d{4})-(?<month>0[1-9]|1[012])-(?<day>0?[1-9]|[12][0-9]|3[01])"
|
|
271
|
+
r"-(?<lang>[^-]*)"
|
|
272
|
+
r"\.(?<extension>html|htm|xhtml)$",
|
|
273
|
+
flags=re.ASCII
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
@lru_cache(1)
|
|
277
|
+
def getFilenameParts(self, filename: str) -> dict[str, Any] | None:
|
|
278
|
+
match = self.getFilenameFormatPattern().match(filename)
|
|
279
|
+
if match:
|
|
280
|
+
return match.groupdict()
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
@lru_cache(1)
|
|
284
|
+
def getIxdsDocBasenames(self, modelXbrl: ModelXbrl) -> set[str]:
|
|
285
|
+
return set(Path(url).name for url in modelXbrl.ixdsDocUrls)
|
|
286
|
+
|
|
161
287
|
def getNoMatchLangFootnotes(self, modelXbrl: ModelXbrl) -> set[ModelInlineFootnote]:
|
|
162
|
-
return self.
|
|
288
|
+
return self.checkInlineHTMLElements(modelXbrl).noMatchLangFootnotes
|
|
163
289
|
|
|
164
290
|
def getOrphanedFootnotes(self, modelXbrl: ModelXbrl) -> set[ModelInlineFootnote]:
|
|
165
|
-
return self.
|
|
291
|
+
return self.checkInlineHTMLElements(modelXbrl).orphanedFootnotes
|
|
166
292
|
|
|
167
|
-
def
|
|
168
|
-
return self.
|
|
293
|
+
def getRequiredToDisplayFacts(self, modelXbrl: ModelXbrl) -> set[ModelInlineFact]:
|
|
294
|
+
return self.checkHiddenElements(modelXbrl).requiredToDisplayFacts
|
|
295
|
+
|
|
296
|
+
def getTupleElements(self, modelXbrl: ModelXbrl) -> set[tuple[Any]]:
|
|
297
|
+
return self.checkInlineHTMLElements(modelXbrl).tupleElements
|
|
169
298
|
|
|
170
299
|
@lru_cache(1)
|
|
171
300
|
def getReportXmlLang(self, modelXbrl: ModelXbrl) -> str | None:
|
|
172
|
-
firstIxdsDoc = True
|
|
173
301
|
reportXmlLang = None
|
|
174
302
|
firstRootmostXmlLangDepth = 9999999
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
303
|
+
if modelXbrl.ixdsHtmlElements:
|
|
304
|
+
ixdsHtmlRootElt = modelXbrl.ixdsHtmlElements[0]
|
|
305
|
+
for elt, depth in etreeIterWithDepth(ixdsHtmlRootElt):
|
|
178
306
|
if isinstance(elt, (_Comment, _ElementTree, _Entity, _ProcessingInstruction)):
|
|
179
307
|
continue
|
|
180
|
-
if
|
|
181
|
-
xmlLang
|
|
182
|
-
if xmlLang:
|
|
308
|
+
if not reportXmlLang or depth < firstRootmostXmlLangDepth:
|
|
309
|
+
if xmlLang := elt.get("{http://www.w3.org/XML/1998/namespace}lang"):
|
|
183
310
|
reportXmlLang = xmlLang
|
|
184
311
|
firstRootmostXmlLangDepth = depth
|
|
185
|
-
firstIxdsDoc = False
|
|
186
312
|
return reportXmlLang
|
|
187
313
|
|
|
314
|
+
@lru_cache(1)
|
|
315
|
+
def isFilenameValidCharacters(self, filename: str) -> bool:
|
|
316
|
+
match = self.getFilenameAllowedCharactersPattern().match(filename)
|
|
317
|
+
return match is not None
|
|
318
|
+
|
|
188
319
|
@lru_cache(1)
|
|
189
320
|
def unitsByDocument(self, modelXbrl: ModelXbrl) -> dict[str, list[ModelUnit]]:
|
|
190
321
|
unitsByDocument = defaultdict(list)
|
|
@@ -48,16 +48,6 @@ def validateXbrlFinally(*args: Any, **kwargs: Any) -> None:
|
|
|
48
48
|
return validationPlugin.validateXbrlFinally(*args, **kwargs)
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
def modelTestcaseVariationReportPackageIxdsOptions(
|
|
52
|
-
val: ValidateXbrl,
|
|
53
|
-
rptPkgIxdsOptions: dict[str, bool],
|
|
54
|
-
*args: Any,
|
|
55
|
-
**kwargs: Any,
|
|
56
|
-
) -> None:
|
|
57
|
-
rptPkgIxdsOptions["lookOutsideReportsDirectory"] = True
|
|
58
|
-
rptPkgIxdsOptions["combineIntoSingleIxds"] = True
|
|
59
|
-
|
|
60
|
-
|
|
61
51
|
__pluginInfo__ = {
|
|
62
52
|
"name": PLUGIN_NAME,
|
|
63
53
|
"version": "0.0.1",
|
|
@@ -70,5 +60,4 @@ __pluginInfo__ = {
|
|
|
70
60
|
"DisclosureSystem.ConfigURL": disclosureSystemConfigURL,
|
|
71
61
|
"ModelXbrl.LoadComplete": modelXbrlLoadComplete,
|
|
72
62
|
"Validate.XBRL.Finally": validateXbrlFinally,
|
|
73
|
-
"ModelTestcaseVariation.ReportPackageIxdsOptions": modelTestcaseVariationReportPackageIxdsOptions,
|
|
74
63
|
}
|