@diy-accounting-uk/diya-gl 1.2.58 → 1.2.60
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.
- package/dist/app/data/releases.json +52 -0
- package/dist/app/lib/diya-gl-interchange.js +73 -4
- package/dist/app/lib/ixbrl-accounts.js +170 -31
- package/dist/app/lib/ixbrl-reader.js +244 -84
- package/dist/app/lib/ixbrl-summary-book.js +16 -7
- package/dist/app/lib/ixbrl-uk-gaap.js +202 -0
- package/dist/app/lib/provenance-data.js +1 -1
- package/package.json +1 -1
|
@@ -1741,6 +1741,58 @@
|
|
|
1741
1741
|
"scorecard": "101608 passed, 0 warnings, 0 failed"
|
|
1742
1742
|
}
|
|
1743
1743
|
}
|
|
1744
|
+
},
|
|
1745
|
+
{
|
|
1746
|
+
"tag": "diya-gl-v1.2.58",
|
|
1747
|
+
"date": "2026-10-25",
|
|
1748
|
+
"formatVersion": "diya-gl/1",
|
|
1749
|
+
"engineVersion": "1.2.58+ad393516",
|
|
1750
|
+
"taxDataHash": "51bdff60e993",
|
|
1751
|
+
"reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
|
|
1752
|
+
"templates": {
|
|
1753
|
+
"bst": {
|
|
1754
|
+
"hash": "0d003e10e00c",
|
|
1755
|
+
"scorecard": "812 passed, 0 warnings, 0 failed"
|
|
1756
|
+
},
|
|
1757
|
+
"taxi": {
|
|
1758
|
+
"hash": "1ff69654d64d",
|
|
1759
|
+
"scorecard": "700 passed, 0 warnings, 0 failed"
|
|
1760
|
+
},
|
|
1761
|
+
"se": {
|
|
1762
|
+
"hash": "064605ae76ed",
|
|
1763
|
+
"scorecard": "8015 passed, 0 warnings, 0 failed"
|
|
1764
|
+
},
|
|
1765
|
+
"ltd": {
|
|
1766
|
+
"hash": "a65ab09cf35c",
|
|
1767
|
+
"scorecard": "101608 passed, 0 warnings, 0 failed"
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
},
|
|
1771
|
+
{
|
|
1772
|
+
"tag": "diya-gl-v1.2.59",
|
|
1773
|
+
"date": "2026-10-27",
|
|
1774
|
+
"formatVersion": "diya-gl/1",
|
|
1775
|
+
"engineVersion": "1.2.59+9d3b814a",
|
|
1776
|
+
"taxDataHash": "51bdff60e993",
|
|
1777
|
+
"reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
|
|
1778
|
+
"templates": {
|
|
1779
|
+
"bst": {
|
|
1780
|
+
"hash": "0d003e10e00c",
|
|
1781
|
+
"scorecard": "812 passed, 0 warnings, 0 failed"
|
|
1782
|
+
},
|
|
1783
|
+
"taxi": {
|
|
1784
|
+
"hash": "1ff69654d64d",
|
|
1785
|
+
"scorecard": "700 passed, 0 warnings, 0 failed"
|
|
1786
|
+
},
|
|
1787
|
+
"se": {
|
|
1788
|
+
"hash": "064605ae76ed",
|
|
1789
|
+
"scorecard": "8015 passed, 0 warnings, 0 failed"
|
|
1790
|
+
},
|
|
1791
|
+
"ltd": {
|
|
1792
|
+
"hash": "a65ab09cf35c",
|
|
1793
|
+
"scorecard": "101608 passed, 0 warnings, 0 failed"
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1744
1796
|
}
|
|
1745
1797
|
]
|
|
1746
1798
|
}
|
|
@@ -46,7 +46,7 @@ import { canonicalBookToml, canonicalLinesJsonl, compareLines, orderedBookTopLev
|
|
|
46
46
|
import { serializeReportDocument } from "./report-serializer.js";
|
|
47
47
|
import { parseDiyaGlData } from "./diya-gl-loader.js";
|
|
48
48
|
import { stampBook } from "./provenance.js";
|
|
49
|
-
import { sniffIxbrl } from "./ixbrl-reader.js";
|
|
49
|
+
import { sniffIxbrl, readIxbrlSet, checkSameReport, IxbrlError } from "./ixbrl-reader.js";
|
|
50
50
|
import { mapIxbrlAccounts } from "./ixbrl-accounts.js";
|
|
51
51
|
import { buildIxbrlSummaryBooks } from "./ixbrl-summary-book.js";
|
|
52
52
|
import * as bst from "../products/bst.js";
|
|
@@ -190,6 +190,12 @@ async function zipKind(bytes) {
|
|
|
190
190
|
const jsonEntries = entries.filter((name) => name.toLowerCase().endsWith(".json"));
|
|
191
191
|
if (jsonEntries.length === 1 && entries.length === 1) return "json-zip";
|
|
192
192
|
|
|
193
|
+
const documents = entries.filter((name) => !name.toUpperCase().startsWith("META-INF/"));
|
|
194
|
+
if (documents.length > 0) {
|
|
195
|
+
const sniffed = await Promise.all(documents.map(async (name) => sniffIxbrl(await zip.file(name).async("uint8array"))));
|
|
196
|
+
if (sniffed.every(Boolean)) return "ixbrl-zip";
|
|
197
|
+
}
|
|
198
|
+
|
|
193
199
|
return "unknown";
|
|
194
200
|
}
|
|
195
201
|
|
|
@@ -200,7 +206,7 @@ async function zipKind(bytes) {
|
|
|
200
206
|
* error messages only.
|
|
201
207
|
* @param {Uint8Array} bytes
|
|
202
208
|
* @param {string} [name] - a hint for messages, not for detection
|
|
203
|
-
* @returns {Promise<"workbook"|"package-zip"|"package-set"|"diya-gl-zip"|"json-zip"|"json"|"ixbrl"|"xbrl"|"xls"|"unknown">}
|
|
209
|
+
* @returns {Promise<"workbook"|"package-zip"|"package-set"|"diya-gl-zip"|"json-zip"|"json"|"ixbrl"|"xbrl"|"ixbrl-zip"|"xls"|"unknown">}
|
|
204
210
|
*/
|
|
205
211
|
export async function detectBookSource(bytes, name) {
|
|
206
212
|
void name; // content decides the kind; the name is a hint for a caller's own error messages, never for detection
|
|
@@ -463,8 +469,8 @@ async function readJsonZipSource(bytes) {
|
|
|
463
469
|
// An accounts file stands for two Company books, the file's own year and the
|
|
464
470
|
// year before, each a summary with one line per tagged line. The comparative
|
|
465
471
|
// pair, the accounts figures and the facts no figure uses ride beside them.
|
|
466
|
-
async function readIxbrlSource(kind,
|
|
467
|
-
const accounts = mapIxbrlAccounts(
|
|
472
|
+
async function readIxbrlSource(kind, source, deps) {
|
|
473
|
+
const accounts = mapIxbrlAccounts(source, deps.expect, { suiteConcepts: deps.suiteConcepts });
|
|
468
474
|
const summary = await buildIxbrlSummaryBooks(accounts, { resources: deps.resources });
|
|
469
475
|
const current = validated(kind, summary.book, summary.lines);
|
|
470
476
|
const prior = validated(kind, summary.comparative.book, summary.comparative.lines);
|
|
@@ -478,6 +484,67 @@ async function readIxbrlSource(kind, bytes, deps) {
|
|
|
478
484
|
};
|
|
479
485
|
}
|
|
480
486
|
|
|
487
|
+
// The directories of a Companies House package that hold the company's own
|
|
488
|
+
// accounts, in the order they are looked for.
|
|
489
|
+
const PACKAGE_ENTRY_DIRECTORIES = ["reports/english", "accounts", "subsidiary-accounts"];
|
|
490
|
+
const PACKAGE_WELSH_DIRECTORY = "reports/welsh";
|
|
491
|
+
|
|
492
|
+
const directoryOf = (entry) => entry.slice(0, Math.max(0, entry.lastIndexOf("/")));
|
|
493
|
+
|
|
494
|
+
// A zip of iXBRL documents is one document set when its files sit together,
|
|
495
|
+
// or a Companies House package when they sit in directories. A package is
|
|
496
|
+
// read through its entry directory; a Welsh package is also compared with its
|
|
497
|
+
// Welsh directory, as Companies House compares the two.
|
|
498
|
+
async function readIxbrlZipSource(bytes) {
|
|
499
|
+
const zip = await JSZip.loadAsync(bytes);
|
|
500
|
+
const entries = Object.keys(zip.files)
|
|
501
|
+
.filter((name) => !zip.files[name].dir && !name.toUpperCase().startsWith("META-INF/"))
|
|
502
|
+
.sort();
|
|
503
|
+
const documentOf = async (entry) => ({ name: entry, bytes: await zip.file(entry).async("uint8array") });
|
|
504
|
+
|
|
505
|
+
const directories = new Map();
|
|
506
|
+
for (const entry of entries) {
|
|
507
|
+
const directory = directoryOf(entry);
|
|
508
|
+
directories.set(directory, [...(directories.get(directory) ?? []), entry]);
|
|
509
|
+
}
|
|
510
|
+
if (directories.size === 1 && directories.has("")) return readIxbrlSet(await Promise.all(entries.map(documentOf)));
|
|
511
|
+
|
|
512
|
+
for (const [directory, members] of directories) {
|
|
513
|
+
if (members.length > 1)
|
|
514
|
+
throw new IxbrlError(`The zip holds more than one accounts file in "${directory}/"; a package folder holds one.`);
|
|
515
|
+
}
|
|
516
|
+
const entryDirectory = PACKAGE_ENTRY_DIRECTORIES.find((directory) => directories.has(directory));
|
|
517
|
+
if (!entryDirectory) {
|
|
518
|
+
throw new IxbrlError(
|
|
519
|
+
directories.size === 1
|
|
520
|
+
? "The zip holds accounts in a folder diya-gl does not recognise; drop the accounts file on its own."
|
|
521
|
+
: "The zip holds several folders and none is the company's own accounts; drop the accounts file on its own.",
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
const read = readIxbrlSet([await documentOf(directories.get(entryDirectory)[0])]);
|
|
525
|
+
if (entryDirectory === "reports/english" && directories.has(PACKAGE_WELSH_DIRECTORY)) {
|
|
526
|
+
const welsh = readIxbrlSet([await documentOf(directories.get(PACKAGE_WELSH_DIRECTORY)[0])]);
|
|
527
|
+
checkSameReport(read, welsh, "English", "Welsh");
|
|
528
|
+
}
|
|
529
|
+
return read;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Turn several files dropped together into D. Files that are all iXBRL
|
|
534
|
+
* documents are one document set. One file is read as readBookSource reads it.
|
|
535
|
+
* @param {Array<{name: string, bytes: Uint8Array}>} files
|
|
536
|
+
* @param {Object} [deps] - as readBookSource
|
|
537
|
+
* @returns {Promise<Object>} as readBookSource
|
|
538
|
+
*/
|
|
539
|
+
export async function readBookSources(files, deps = {}) {
|
|
540
|
+
if (files.length === 1) return readBookSource(files[0].bytes, files[0].name, deps);
|
|
541
|
+
const kinds = await Promise.all(files.map((file) => detectBookSource(file.bytes, file.name)));
|
|
542
|
+
if (kinds.every((kind) => kind === "ixbrl" || kind === "xbrl")) {
|
|
543
|
+
return readIxbrlSource("ixbrl", readIxbrlSet(files), deps);
|
|
544
|
+
}
|
|
545
|
+
throw new IxbrlError("Drop the accounts files on their own.");
|
|
546
|
+
}
|
|
547
|
+
|
|
481
548
|
/**
|
|
482
549
|
* Turn a byte array into D: the book and its lines, whichever of the kinds
|
|
483
550
|
* it sniffs as. The CLI's --file mode, the MCP server's extract_book
|
|
@@ -508,6 +575,8 @@ export async function readBookSource(bytes, name, deps = {}) {
|
|
|
508
575
|
case "ixbrl":
|
|
509
576
|
case "xbrl":
|
|
510
577
|
return readIxbrlSource(kind, bytes, deps);
|
|
578
|
+
case "ixbrl-zip":
|
|
579
|
+
return readIxbrlSource(kind, await readIxbrlZipSource(bytes), deps);
|
|
511
580
|
case "xls":
|
|
512
581
|
throw new XlsBookSourceError(name);
|
|
513
582
|
default:
|
|
@@ -41,6 +41,8 @@ export const FIXED_ASSET_ACCOUNTS = {
|
|
|
41
41
|
motorVehicles: "0040",
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
const NATURE_NOTE = "micro-entity and Format 2 profit and loss";
|
|
45
|
+
|
|
44
46
|
/**
|
|
45
47
|
* The mapping, one row per concept and member the reader maps. `figure` is
|
|
46
48
|
* the name the forms and builders use; `account` is where the summary book
|
|
@@ -89,6 +91,7 @@ export const MAPPING_TABLE = [
|
|
|
89
91
|
note: "WithinOneYear; posts as a trade creditor, which the balance sheet shows within one year",
|
|
90
92
|
},
|
|
91
93
|
{ concept: "AccruedLiabilitiesNotExpressedWithinCreditorsSubtotal", figure: "otherCreditors", account: "2100", note: "WithinOneYear" },
|
|
94
|
+
{ concept: "TaxationSocialSecurityPayable", figure: "otherCreditors", account: "2100", note: "WithinOneYear" },
|
|
92
95
|
{
|
|
93
96
|
concept: "Creditors",
|
|
94
97
|
dimension: MATURITY,
|
|
@@ -133,21 +136,77 @@ export const MAPPING_TABLE = [
|
|
|
133
136
|
account: "1200",
|
|
134
137
|
note: "a receipt coded K",
|
|
135
138
|
},
|
|
139
|
+
{
|
|
140
|
+
concept: "InterestPayableSimilarChargesFinanceCosts",
|
|
141
|
+
figure: "interestPayable",
|
|
142
|
+
account: "5803",
|
|
143
|
+
note: "the ltd book's loan interest account",
|
|
144
|
+
},
|
|
136
145
|
{ concept: "TaxTaxCreditOnProfitOrLossOnOrdinaryActivities", figure: "tax", account: "-", note: "not posted; diya-gl works the tax" },
|
|
137
146
|
{ concept: "GrossProfitLoss", figure: "grossProfit", account: "-", note: "a check" },
|
|
138
147
|
{ concept: "OperatingProfitLoss", figure: "operatingProfit", account: "-", note: "a check" },
|
|
139
148
|
{ concept: "ProfitLossOnOrdinaryActivitiesBeforeTax", figure: "profitBeforeTax", account: "-", note: "a check" },
|
|
140
149
|
{ concept: "ProfitLoss", figure: "profit", account: "-", note: "a check" },
|
|
141
|
-
{ concept: "OtherOperatingIncomeFormat2", figure: "otherOperatingIncome", account: "4003", note:
|
|
142
|
-
{
|
|
143
|
-
|
|
150
|
+
{ concept: "OtherOperatingIncomeFormat2", figure: "otherOperatingIncome", account: "4003", note: NATURE_NOTE },
|
|
151
|
+
{
|
|
152
|
+
concept: "OtherOperatingIncome",
|
|
153
|
+
figure: "otherOperatingIncome",
|
|
154
|
+
account: "4003",
|
|
155
|
+
note: `${NATURE_NOTE}; read when the Format 2 name is absent`,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
concept: "IncreaseDecreaseInStocksInventoriesFinishedGoodsWorkInProgress",
|
|
159
|
+
figure: "changeInStocks",
|
|
160
|
+
account: "-",
|
|
161
|
+
note: "Format 2 profit and loss; not posted, the stock movement of the balance sheets already moves 1100 and 5000, and a difference is listed",
|
|
162
|
+
},
|
|
163
|
+
{ concept: "OwnWorkCapitalised", figure: "ownWorkCapitalised", account: "4003", note: "Format 2 profit and loss" },
|
|
164
|
+
{ concept: "RawMaterialsConsumablesUsed", figure: "rawMaterialsConsumables", account: "5000", note: NATURE_NOTE },
|
|
165
|
+
{
|
|
166
|
+
concept: "OtherExternalCharges",
|
|
167
|
+
figure: "otherExternalCharges",
|
|
168
|
+
account: "5001",
|
|
169
|
+
note: "Format 2 profit and loss; the sub-contractors account",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
concept: "StaffCostsEmployeeBenefitsExpense",
|
|
173
|
+
figure: "staffCosts",
|
|
174
|
+
account: "5101",
|
|
175
|
+
note: `${NATURE_NOTE}; the sum of the three lines below when absent`,
|
|
176
|
+
},
|
|
177
|
+
{ concept: "WagesSalaries", figure: "wagesSalaries", account: "5101", note: "part of staffCosts" },
|
|
178
|
+
{ concept: "SocialSecurityCosts", figure: "socialSecurityCosts", account: "5101", note: "part of staffCosts" },
|
|
179
|
+
{
|
|
180
|
+
concept: "PensionOtherPost-employmentBenefitCostsOtherPensionCosts",
|
|
181
|
+
figure: "pensionCosts",
|
|
182
|
+
account: "5101",
|
|
183
|
+
note: "part of staffCosts",
|
|
184
|
+
},
|
|
144
185
|
{
|
|
145
186
|
concept: "DepreciationAmortisationImpairmentExpense",
|
|
146
187
|
figure: "depreciation",
|
|
147
188
|
account: "depreciationRate",
|
|
148
|
-
note:
|
|
189
|
+
note: NATURE_NOTE,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
concept: "DepreciationAmortisationExpense",
|
|
193
|
+
figure: "depreciation",
|
|
194
|
+
account: "depreciationRate",
|
|
195
|
+
note: `${NATURE_NOTE}; read when the impairment name is absent`,
|
|
196
|
+
},
|
|
197
|
+
{ concept: "OtherOperatingExpensesFormat2", figure: "otherOperatingExpenses", account: "5501", note: NATURE_NOTE },
|
|
198
|
+
{
|
|
199
|
+
concept: "OtherOperatingExpensesByNature",
|
|
200
|
+
figure: "otherOperatingExpenses",
|
|
201
|
+
account: "5501",
|
|
202
|
+
note: `${NATURE_NOTE}; read when the Format 2 name is absent`,
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
concept: "ProfitLossBeforeTax",
|
|
206
|
+
figure: "profitBeforeTax",
|
|
207
|
+
account: "-",
|
|
208
|
+
note: "a check; read when ProfitLossOnOrdinaryActivitiesBeforeTax is absent",
|
|
149
209
|
},
|
|
150
|
-
{ concept: "OtherOperatingExpensesFormat2", figure: "otherOperatingExpenses", account: "5501", note: "micro-entity profit and loss" },
|
|
151
210
|
{
|
|
152
211
|
concept: "GainLossOnDisposalsPropertyPlantEquipment",
|
|
153
212
|
figure: "gainOnDisposal",
|
|
@@ -295,6 +354,7 @@ function balanceSheetAt(set, endDay, notes, label) {
|
|
|
295
354
|
otherCreditors: sumDefined([
|
|
296
355
|
set.maturity("OtherCreditors", endDay, ["WithinOneYear", null]),
|
|
297
356
|
set.maturity("AccruedLiabilitiesNotExpressedWithinCreditorsSubtotal", endDay, ["WithinOneYear", null]),
|
|
357
|
+
set.maturity("TaxationSocialSecurityPayable", endDay, ["WithinOneYear", null]),
|
|
298
358
|
]),
|
|
299
359
|
creditorsWithinOneYear: set.instant("Creditors", endDay, { [MATURITY]: "WithinOneYear" }),
|
|
300
360
|
creditorsAfterOneYear: sumDefined([afterOneYear, provisions]),
|
|
@@ -308,22 +368,76 @@ function balanceSheetAt(set, endDay, notes, label) {
|
|
|
308
368
|
});
|
|
309
369
|
}
|
|
310
370
|
|
|
311
|
-
|
|
371
|
+
// The concepts that say which format the profit and loss account takes. Format 1 analyses costs by
|
|
372
|
+
// function; Format 2 by nature. A Format 2 concept in a Format 1 file belongs to a note.
|
|
373
|
+
const FORMAT_1_CONCEPTS = new Set([
|
|
374
|
+
"CostSales",
|
|
375
|
+
"GrossProfitLoss",
|
|
376
|
+
"DistributionCosts",
|
|
377
|
+
"AdministrativeExpenses",
|
|
378
|
+
"OtherOperatingIncomeFormat1",
|
|
379
|
+
"OtherOperatingExpensesFormat1",
|
|
380
|
+
]);
|
|
381
|
+
const FORMAT_2_CONCEPTS = new Set([
|
|
382
|
+
"IncreaseDecreaseInStocksInventoriesFinishedGoodsWorkInProgress",
|
|
383
|
+
"OwnWorkCapitalised",
|
|
384
|
+
"OtherOperatingIncomeFormat2",
|
|
385
|
+
"RawMaterialsConsumablesUsed",
|
|
386
|
+
"OtherExternalCharges",
|
|
387
|
+
"StaffCostsEmployeeBenefitsExpense",
|
|
388
|
+
"WagesSalaries",
|
|
389
|
+
"SocialSecurityCosts",
|
|
390
|
+
"PensionOtherPost-employmentBenefitCostsOtherPensionCosts",
|
|
391
|
+
"DepreciationAmortisationImpairmentExpense",
|
|
392
|
+
"DepreciationAmortisationExpense",
|
|
393
|
+
"OtherOperatingExpensesFormat2",
|
|
394
|
+
"OtherOperatingExpensesByNature",
|
|
395
|
+
]);
|
|
396
|
+
|
|
397
|
+
// 1 or 2: the format of the profit and loss account the facts ending on one of the days state.
|
|
398
|
+
function profitAndLossFormat(set, endDays) {
|
|
399
|
+
for (const endDay of endDays) {
|
|
400
|
+
const names = set.facts.filter((fact) => fact.period.endDate === endDay).map((fact) => fact.localName);
|
|
401
|
+
if (names.some((name) => FORMAT_1_CONCEPTS.has(name))) return 1;
|
|
402
|
+
if (names.some((name) => FORMAT_2_CONCEPTS.has(name))) return 2;
|
|
403
|
+
}
|
|
404
|
+
return 1;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function profitAndLossAt(set, endDay, byNature) {
|
|
408
|
+
const staffCosts = byNature ? (set.duration("StaffCostsEmployeeBenefitsExpense", endDay) ?? undefined) : undefined;
|
|
409
|
+
const wagesSalaries = byNature ? set.duration("WagesSalaries", endDay) : undefined;
|
|
410
|
+
const socialSecurityCosts = byNature ? set.duration("SocialSecurityCosts", endDay) : undefined;
|
|
411
|
+
const pensionCosts = byNature ? set.duration("PensionOtherPost-employmentBenefitCostsOtherPensionCosts", endDay) : undefined;
|
|
412
|
+
const nature = byNature
|
|
413
|
+
? {
|
|
414
|
+
changeInStocks: set.duration("IncreaseDecreaseInStocksInventoriesFinishedGoodsWorkInProgress", endDay),
|
|
415
|
+
ownWorkCapitalised: set.duration("OwnWorkCapitalised", endDay),
|
|
416
|
+
otherOperatingIncome: set.duration("OtherOperatingIncomeFormat2", endDay) ?? set.duration("OtherOperatingIncome", endDay),
|
|
417
|
+
rawMaterialsConsumables: set.duration("RawMaterialsConsumablesUsed", endDay),
|
|
418
|
+
otherExternalCharges: set.duration("OtherExternalCharges", endDay),
|
|
419
|
+
staffCosts: staffCosts ?? sumDefined([wagesSalaries, socialSecurityCosts, pensionCosts]),
|
|
420
|
+
wagesSalaries,
|
|
421
|
+
socialSecurityCosts,
|
|
422
|
+
pensionCosts,
|
|
423
|
+
depreciation:
|
|
424
|
+
set.duration("DepreciationAmortisationImpairmentExpense", endDay) ?? set.duration("DepreciationAmortisationExpense", endDay),
|
|
425
|
+
otherOperatingExpenses:
|
|
426
|
+
set.duration("OtherOperatingExpensesFormat2", endDay) ?? set.duration("OtherOperatingExpensesByNature", endDay),
|
|
427
|
+
}
|
|
428
|
+
: {};
|
|
312
429
|
return pick({
|
|
430
|
+
...nature,
|
|
313
431
|
turnover: set.duration("TurnoverRevenue", endDay),
|
|
314
432
|
costOfSales: set.duration("CostSales", endDay),
|
|
315
433
|
grossProfit: set.duration("GrossProfitLoss", endDay),
|
|
316
434
|
administrativeExpenses: set.duration("AdministrativeExpenses", endDay),
|
|
317
435
|
operatingProfit: set.duration("OperatingProfitLoss", endDay),
|
|
318
436
|
interestReceivable: set.duration("OtherInterestReceivableSimilarIncomeFinanceIncome", endDay),
|
|
319
|
-
|
|
437
|
+
interestPayable: set.duration("InterestPayableSimilarChargesFinanceCosts", endDay),
|
|
438
|
+
profitBeforeTax: set.duration("ProfitLossOnOrdinaryActivitiesBeforeTax", endDay) ?? set.duration("ProfitLossBeforeTax", endDay),
|
|
320
439
|
tax: set.duration("TaxTaxCreditOnProfitOrLossOnOrdinaryActivities", endDay),
|
|
321
440
|
profit: set.duration("ProfitLoss", endDay) ?? set.duration("ProfitLossOnOrdinaryActivitiesAfterTax", endDay),
|
|
322
|
-
otherOperatingIncome: set.duration("OtherOperatingIncomeFormat2", endDay),
|
|
323
|
-
rawMaterialsConsumables: set.duration("RawMaterialsConsumablesUsed", endDay),
|
|
324
|
-
staffCosts: set.duration("StaffCostsEmployeeBenefitsExpense", endDay),
|
|
325
|
-
depreciation: set.duration("DepreciationAmortisationImpairmentExpense", endDay),
|
|
326
|
-
otherOperatingExpenses: set.duration("OtherOperatingExpensesFormat2", endDay),
|
|
327
441
|
gainOnDisposal: set.duration("GainLossOnDisposalsPropertyPlantEquipment", endDay),
|
|
328
442
|
});
|
|
329
443
|
}
|
|
@@ -511,30 +625,49 @@ function checkProfitAndLoss(pl, label) {
|
|
|
511
625
|
if (
|
|
512
626
|
pl.profitBeforeTax !== undefined &&
|
|
513
627
|
pl.operatingProfit !== undefined &&
|
|
514
|
-
!close(pl.operatingProfit + n(pl.interestReceivable), pl.profitBeforeTax)
|
|
628
|
+
!close(pl.operatingProfit + n(pl.interestReceivable) - n(pl.interestPayable), pl.profitBeforeTax)
|
|
515
629
|
) {
|
|
516
|
-
mismatch("profit before tax", pl.operatingProfit + n(pl.interestReceivable), pl.profitBeforeTax);
|
|
630
|
+
mismatch("profit before tax", pl.operatingProfit + n(pl.interestReceivable) - n(pl.interestPayable), pl.profitBeforeTax);
|
|
517
631
|
}
|
|
518
632
|
if (pl.profit !== undefined && pl.profitBeforeTax !== undefined && !close(pl.profitBeforeTax - n(pl.tax), pl.profit)) {
|
|
519
633
|
mismatch("profit for the year", pl.profitBeforeTax - n(pl.tax), pl.profit);
|
|
520
634
|
}
|
|
521
|
-
const
|
|
522
|
-
if (
|
|
523
|
-
mismatch("profit
|
|
635
|
+
const byNature = natureProfitBeforeTax(pl);
|
|
636
|
+
if (byNature !== undefined && pl.operatingProfit !== undefined && !close(byNature, pl.operatingProfit)) {
|
|
637
|
+
mismatch("operating profit", byNature, pl.operatingProfit);
|
|
638
|
+
}
|
|
639
|
+
if (
|
|
640
|
+
byNature !== undefined &&
|
|
641
|
+
pl.profit !== undefined &&
|
|
642
|
+
!close(byNature + n(pl.interestReceivable) - n(pl.interestPayable) - n(pl.tax), pl.profit)
|
|
643
|
+
) {
|
|
644
|
+
mismatch("profit for the year", byNature + n(pl.interestReceivable) - n(pl.interestPayable) - n(pl.tax), pl.profit);
|
|
524
645
|
}
|
|
525
646
|
}
|
|
526
647
|
|
|
527
|
-
// The micro-entity
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
648
|
+
// The profit and loss account by nature (micro-entity lines and Format 2): turnover, the change in
|
|
649
|
+
// stocks, own work capitalised and other income, less raw materials, external charges, staff costs,
|
|
650
|
+
// depreciation and other expenses. Before interest, so a micro-entity's figure is its profit before tax.
|
|
651
|
+
export function natureProfitBeforeTax(pl) {
|
|
652
|
+
const hasNatureLine = [
|
|
653
|
+
"changeInStocks",
|
|
654
|
+
"ownWorkCapitalised",
|
|
655
|
+
"otherOperatingIncome",
|
|
656
|
+
"rawMaterialsConsumables",
|
|
657
|
+
"otherExternalCharges",
|
|
658
|
+
"staffCosts",
|
|
659
|
+
"depreciation",
|
|
660
|
+
"otherOperatingExpenses",
|
|
661
|
+
].some((key) => pl[key] !== undefined);
|
|
662
|
+
if (!hasNatureLine || pl.turnover === undefined) return undefined;
|
|
533
663
|
const n = (value) => value ?? 0;
|
|
534
664
|
return (
|
|
535
665
|
pl.turnover +
|
|
666
|
+
n(pl.changeInStocks) +
|
|
667
|
+
n(pl.ownWorkCapitalised) +
|
|
536
668
|
n(pl.otherOperatingIncome) -
|
|
537
669
|
n(pl.rawMaterialsConsumables) -
|
|
670
|
+
n(pl.otherExternalCharges) -
|
|
538
671
|
n(pl.staffCosts) -
|
|
539
672
|
n(pl.depreciation) -
|
|
540
673
|
n(pl.otherOperatingExpenses)
|
|
@@ -611,8 +744,11 @@ export function mapIxbrlAccounts(source, expect = {}, { suiteConcepts } = {}) {
|
|
|
611
744
|
const notes = [];
|
|
612
745
|
const currentYear = balanceSheetAt(set, period.end, notes, "Current year");
|
|
613
746
|
const priorYear = priorEnd ? balanceSheetAt(set, priorEnd, notes, "Prior year") : {};
|
|
614
|
-
const
|
|
615
|
-
|
|
747
|
+
const profitAndLossFormatNumber = profitAndLossFormat(set, priorEnd ? [period.end, priorEnd] : [period.end]);
|
|
748
|
+
// A micro-entity file's lines read as the micro lines; a Format 2 small company file reads the same lines and more.
|
|
749
|
+
const byNature = regime === "micro" || profitAndLossFormatNumber === 2;
|
|
750
|
+
const currentPl = profitAndLossAt(set, period.end, byNature);
|
|
751
|
+
const priorPl = priorEnd ? profitAndLossAt(set, priorEnd, byNature) : {};
|
|
616
752
|
const fixedAssetNote = priorEnd ? fixedAssetNoteAt(set, period.end, priorEnd) : null;
|
|
617
753
|
// The note's totals and net book values restate what its classes and the balance sheet already say.
|
|
618
754
|
for (const fact of set.facts) {
|
|
@@ -689,13 +825,15 @@ export function mapIxbrlAccounts(source, expect = {}, { suiteConcepts } = {}) {
|
|
|
689
825
|
for (const fact of [shares, nominal]) if (fact) set.consumed.add(fact);
|
|
690
826
|
const shareClassMember = (fact) => (fact && Object.keys(fact.dimensions).length === 1 ? local(Object.values(fact.dimensions)[0]) : null);
|
|
691
827
|
|
|
692
|
-
if (regime === "small" && hasProfitAndLoss && currentPl.costOfSales === undefined && currentPl.turnover === undefined) {
|
|
693
|
-
notes.push("This file's profit and loss account is Format 2, which the small company form does not take.");
|
|
694
|
-
}
|
|
695
|
-
|
|
696
828
|
const principalActivity = textFact(set, "DescriptionPrincipalActivities") ?? null;
|
|
697
829
|
const approvalDate = textFact(set, "DateAuthorisationFinancialStatementsForIssue") ?? null;
|
|
698
|
-
const definedBySuite = read.suite.date ? suiteConcepts?.(read.suite.date) : undefined;
|
|
830
|
+
const definedBySuite = read.suite.date && read.suite.family !== "UK-GAAP" ? suiteConcepts?.(read.suite.date) : undefined;
|
|
831
|
+
const sourceConcepts = {};
|
|
832
|
+
// A name stated both on its own and on a dimension (Equity) takes the UK GAAP name of the fact on its own.
|
|
833
|
+
const translated = set.facts.filter((fact) => fact.sourceConcept);
|
|
834
|
+
for (const fact of [...translated.filter((f) => Object.keys(f.dimensions).length === 0), ...translated]) {
|
|
835
|
+
sourceConcepts[fact.localName] ??= fact.sourceConcept;
|
|
836
|
+
}
|
|
699
837
|
const unmapped = set.facts
|
|
700
838
|
.filter((fact) => !set.consumed.has(fact) && fact.value !== "" && !isBoilerplate(fact))
|
|
701
839
|
.map((fact) => ({
|
|
@@ -717,7 +855,7 @@ export function mapIxbrlAccounts(source, expect = {}, { suiteConcepts } = {}) {
|
|
|
717
855
|
filleted,
|
|
718
856
|
hasProfitAndLoss,
|
|
719
857
|
balanceSheet: { currentYear, priorYear },
|
|
720
|
-
profitAndLoss: hasProfitAndLoss ? { currentYear: currentPl, priorYear: priorPl } : null,
|
|
858
|
+
profitAndLoss: hasProfitAndLoss ? { format: profitAndLossFormatNumber, currentYear: currentPl, priorYear: priorPl } : null,
|
|
721
859
|
fixedAssetNote,
|
|
722
860
|
averageEmployees: averageEmployees ?? null,
|
|
723
861
|
principalActivity,
|
|
@@ -734,6 +872,7 @@ export function mapIxbrlAccounts(source, expect = {}, { suiteConcepts } = {}) {
|
|
|
734
872
|
: null,
|
|
735
873
|
firstAccounts: Boolean(expect.firstAccounts),
|
|
736
874
|
notes,
|
|
875
|
+
sourceConcepts,
|
|
737
876
|
unmapped,
|
|
738
877
|
};
|
|
739
878
|
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// server run.
|
|
19
19
|
|
|
20
20
|
import { DOMParser } from "@xmldom/xmldom";
|
|
21
|
+
import { UK_GAAP_SUITE_DATE, translateUkGaapFacts } from "./ixbrl-uk-gaap.js";
|
|
21
22
|
|
|
22
23
|
const NS_IX_11 = "http://www.xbrl.org/2013/inlineXBRL";
|
|
23
24
|
const NS_IX_10 = "http://www.xbrl.org/2008/inlineXBRL";
|
|
@@ -41,7 +42,10 @@ const TRANSFORMATION_REGISTRIES = [
|
|
|
41
42
|
|
|
42
43
|
const FRC_NAMESPACE = /^http:\/\/xbrl\.frc\.org\.uk\/(fr|cd|reports)\/(\d{4}-\d{2}-\d{2})\/(.+)$/;
|
|
43
44
|
const FRC_ENTRY_POINT = /xbrl\.frc\.org\.uk\/([^/]+)\/(\d{4}-\d{2}-\d{2})\//;
|
|
44
|
-
const OLD_UK_GAAP_NAMESPACE = /^http:\/\/www\.xbrl\.org\/uk\/(gaap|cd|fr)\b/;
|
|
45
|
+
const OLD_UK_GAAP_NAMESPACE = /^http:\/\/www\.xbrl\.org\/uk\/(?:gaap|cd|fr|reports)\/[^/]+\/(\d{4}-\d{2}-\d{2})\b/;
|
|
46
|
+
const OLD_UK_GAAP_ENTRY_POINT = /xbrl\.org\/uk\/(?:gaap|cd|fr|reports)\/[^/]+\/(\d{4}-\d{2}-\d{2})\//;
|
|
47
|
+
|
|
48
|
+
const DROP_THE_SET = "Drop all the files of the set together, or the zip.";
|
|
45
49
|
|
|
46
50
|
// What a customer sees when a file reads as something other than FRS 102 or
|
|
47
51
|
// FRS 105 accounts.
|
|
@@ -361,7 +365,9 @@ function inlineParts(document) {
|
|
|
361
365
|
const root = document.documentElement;
|
|
362
366
|
const headers = descendants(root, (element) => isIx(element, "header"));
|
|
363
367
|
const resources = headers.flatMap((header) => descendants(header, (element) => isIx(element, "resources")));
|
|
364
|
-
const schemaRefs = headers.flatMap((header) =>
|
|
368
|
+
const schemaRefs = headers.flatMap((header) =>
|
|
369
|
+
descendants(header, (element) => is(element, NS_LINK, "schemaRef") && !element.parentNode.getAttribute?.("target")),
|
|
370
|
+
);
|
|
365
371
|
const hiddenElements = new Set(headers.flatMap((header) => descendants(header, (element) => isIx(element, "hidden"))));
|
|
366
372
|
const factElements = descendants(root, (element) => isIx(element, "nonFraction") || isIx(element, "nonNumeric"));
|
|
367
373
|
const continuations = new Map(
|
|
@@ -398,9 +404,7 @@ function nonNumericText(element, continuations) {
|
|
|
398
404
|
seen.add(next);
|
|
399
405
|
const continuation = continuations.get(next);
|
|
400
406
|
if (!continuation) {
|
|
401
|
-
throw new IxbrlError(
|
|
402
|
-
"This file is one part of a document set: a fact continues in another file. diya-gl reads single-file documents.",
|
|
403
|
-
);
|
|
407
|
+
throw new IxbrlError(`This file is one part of a document set: a fact continues in another file. ${DROP_THE_SET}`);
|
|
404
408
|
}
|
|
405
409
|
text += textOf(continuation);
|
|
406
410
|
next = continuation.getAttribute("continuedAt");
|
|
@@ -454,6 +458,7 @@ function suiteOf(namespaces, schemaRefs) {
|
|
|
454
458
|
suite.date = match[2];
|
|
455
459
|
} else if (/xbrl\.org\/uk\//.test(href)) {
|
|
456
460
|
suite.family = "UK-GAAP";
|
|
461
|
+
suite.date ??= href.match(OLD_UK_GAAP_ENTRY_POINT)?.[1] ?? null;
|
|
457
462
|
}
|
|
458
463
|
}
|
|
459
464
|
for (const namespace of namespaces) {
|
|
@@ -463,6 +468,7 @@ function suiteOf(namespaces, schemaRefs) {
|
|
|
463
468
|
suite.modules.add(match[3]);
|
|
464
469
|
} else if (OLD_UK_GAAP_NAMESPACE.test(namespace)) {
|
|
465
470
|
suite.family = "UK-GAAP";
|
|
471
|
+
suite.date ??= namespace.match(OLD_UK_GAAP_NAMESPACE)[1];
|
|
466
472
|
}
|
|
467
473
|
}
|
|
468
474
|
return suite;
|
|
@@ -473,111 +479,265 @@ const SUITE_LABELS = {
|
|
|
473
479
|
"IFRS": "IFRS",
|
|
474
480
|
"Charities": "Charities SORP",
|
|
475
481
|
"UKSEF": "UK Single Electronic Format",
|
|
476
|
-
"UK-GAAP": "old UK GAAP",
|
|
477
482
|
};
|
|
478
483
|
|
|
479
484
|
function checkSuite(suite) {
|
|
485
|
+
if (suite.family === "UK-GAAP") {
|
|
486
|
+
if (suite.date !== UK_GAAP_SUITE_DATE) {
|
|
487
|
+
throw new IxbrlError(
|
|
488
|
+
`This file uses the UK GAAP taxonomy of ${suite.date ?? "an unstated date"}; the form reads the ${UK_GAAP_SUITE_DATE} suite.`,
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
480
493
|
if (suite.family && !READ_SUITES.includes(suite.family)) {
|
|
481
494
|
const label = SUITE_LABELS[suite.family] ?? suite.family;
|
|
482
495
|
throw new IxbrlError(`This file uses the ${label} taxonomy; the form takes FRS 102 and FRS 105 accounts.`);
|
|
483
496
|
}
|
|
484
497
|
}
|
|
485
498
|
|
|
486
|
-
|
|
487
|
-
* Read an Inline XBRL document or an XBRL instance into facts.
|
|
488
|
-
* @param {Uint8Array|string} input - the document
|
|
489
|
-
* @returns {{kind: "ixbrl"|"xbrl", suite: {family: string|null, date: string|null, modules: string[]}, facts: Array<Object>, contexts: Object, units: Object}}
|
|
490
|
-
* @throws {IxbrlError} when the document cannot be read, with a message a customer reads
|
|
491
|
-
*/
|
|
492
|
-
export function readIxbrl(input) {
|
|
493
|
-
const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
494
|
-
const kind = sniffIxbrl(bytes);
|
|
495
|
-
if (!kind) throw new IxbrlError("This file is not an Inline XBRL document or an XBRL instance.");
|
|
499
|
+
const nameOf = (name) => (name ? `"${name}"` : "This file");
|
|
496
500
|
|
|
497
|
-
|
|
498
|
-
|
|
501
|
+
function describeSuite(suite) {
|
|
502
|
+
return [suite.family, suite.date].filter(Boolean).join(" ");
|
|
503
|
+
}
|
|
499
504
|
|
|
505
|
+
function sameJson(a, b) {
|
|
506
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function mergeResources(parsed) {
|
|
500
510
|
const contexts = {};
|
|
501
511
|
const units = {};
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
512
|
+
const identifiers = new Set();
|
|
513
|
+
for (const part of parsed) {
|
|
514
|
+
for (const resource of part.parts.resources) {
|
|
515
|
+
for (const child of elementChildren(resource)) {
|
|
516
|
+
if (is(child, NS_XBRLI, "context")) {
|
|
517
|
+
const context = readContext(child);
|
|
518
|
+
if (contexts[context.id] && !sameJson(contexts[context.id], context)) {
|
|
519
|
+
throw new IxbrlError(`The files define the context "${context.id}" differently.`);
|
|
520
|
+
}
|
|
521
|
+
contexts[context.id] = context;
|
|
522
|
+
identifiers.add(`${context.entity.scheme}|${context.entity.identifier}`);
|
|
523
|
+
} else if (is(child, NS_XBRLI, "unit")) {
|
|
524
|
+
const id = child.getAttribute("id");
|
|
525
|
+
const unit = readUnit(child);
|
|
526
|
+
if (units[id] !== undefined && units[id] !== unit) throw new IxbrlError(`The files define the unit "${id}" differently.`);
|
|
527
|
+
units[id] = unit;
|
|
528
|
+
}
|
|
509
529
|
}
|
|
510
530
|
}
|
|
511
531
|
}
|
|
512
|
-
|
|
513
|
-
|
|
532
|
+
return { contexts, units, identifiers };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function checkSetAgrees(parsed, identifiers) {
|
|
536
|
+
if (identifiers.size > 1) {
|
|
537
|
+
const names = [...identifiers].map((id) => id.split("|")[1]);
|
|
538
|
+
throw new IxbrlError(`The files are for different companies (${names.join(" and ")}).`);
|
|
539
|
+
}
|
|
540
|
+
const suites = parsed
|
|
541
|
+
.map((part) => suiteOf(new Set(), part.parts.schemaRefs))
|
|
542
|
+
.filter((suite) => suite.family || suite.date)
|
|
543
|
+
.map(describeSuite);
|
|
544
|
+
if (new Set(suites).size > 1) {
|
|
545
|
+
throw new IxbrlError(`The files name different taxonomy suites (${[...new Set(suites)].join(" and ")}).`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function readFactElement(element, part, shared) {
|
|
550
|
+
const { kind, parts } = part;
|
|
551
|
+
const { contexts, units, continuations } = shared;
|
|
552
|
+
const nameAttribute = kind === "ixbrl" ? element.getAttribute("name") : null;
|
|
553
|
+
if (kind === "ixbrl" && !nameAttribute) return null;
|
|
554
|
+
if (element.getAttributeNS(NS_XSI, "nil") === "true" || element.getAttribute("xsi:nil") === "true") return null;
|
|
555
|
+
|
|
556
|
+
const { namespace, localName } =
|
|
557
|
+
kind === "ixbrl"
|
|
558
|
+
? resolveQName(element, nameAttribute, "a concept name")
|
|
559
|
+
: { namespace: element.namespaceURI, localName: element.localName };
|
|
560
|
+
|
|
561
|
+
const contextRef = element.getAttribute("contextRef");
|
|
562
|
+
const context = contexts[contextRef];
|
|
563
|
+
if (!context) throw new IxbrlError(`The fact ${localName} refers to the context "${contextRef}", which the file does not define.`);
|
|
564
|
+
|
|
565
|
+
const numeric = kind === "ixbrl" ? isIx(element, "nonFraction") : element.hasAttribute("unitRef");
|
|
566
|
+
let value;
|
|
567
|
+
let unit = null;
|
|
568
|
+
let decimals;
|
|
569
|
+
if (numeric) {
|
|
570
|
+
const unitRef = element.getAttribute("unitRef");
|
|
571
|
+
unit = units[unitRef];
|
|
572
|
+
if (unit === undefined) throw new IxbrlError(`The fact ${localName} refers to the unit "${unitRef}", which the file does not define.`);
|
|
573
|
+
if (unit !== "GBP" && unit !== "pure" && unit !== "shares" && !unit.startsWith("divide:")) {
|
|
574
|
+
throw new IxbrlError(`The fact ${localName} is stated in ${unit}; diya-gl reads pounds sterling (GBP), shares and pure numbers.`);
|
|
575
|
+
}
|
|
576
|
+
const decimalsText = element.getAttribute("decimals");
|
|
577
|
+
decimals = decimalsText === null ? undefined : decimalsText === "INF" ? Infinity : Number(decimalsText);
|
|
578
|
+
value = kind === "ixbrl" ? nonFractionValue(element) : Number(textOf(element).trim());
|
|
579
|
+
} else {
|
|
580
|
+
value = kind === "ixbrl" ? nonNumericValue(element, continuations) : textOf(element).trim();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
namespace,
|
|
585
|
+
localName,
|
|
586
|
+
contextRef,
|
|
587
|
+
entity: context.entity,
|
|
588
|
+
period: context.period,
|
|
589
|
+
dimensions: context.dimensions,
|
|
590
|
+
unit,
|
|
591
|
+
decimals,
|
|
592
|
+
value,
|
|
593
|
+
hidden: insideHidden(element, parts.hiddenElements),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Read one or more Inline XBRL documents, or one XBRL instance, into facts.
|
|
599
|
+
* Several documents are one Inline XBRL Document Set: their contexts, units
|
|
600
|
+
* and continuations merge, facts with no target attribute are read, and the
|
|
601
|
+
* duplicate rule runs across the whole set.
|
|
602
|
+
* @param {Array<{name: string, bytes: Uint8Array|string}>} documents
|
|
603
|
+
* @returns {{kind: "ixbrl"|"xbrl", suite: {family: string|null, date: string|null, modules: string[]}, facts: Array<Object>, contexts: Object, units: Object, leftOutTargets: string[]}}
|
|
604
|
+
* @throws {IxbrlError} when the set cannot be read, with a message a customer reads
|
|
605
|
+
*/
|
|
606
|
+
export function readIxbrlSet(documents) {
|
|
607
|
+
if (!Array.isArray(documents) || documents.length === 0) throw new IxbrlError("There is no file to read.");
|
|
608
|
+
const many = documents.length > 1;
|
|
609
|
+
|
|
610
|
+
const parsed = documents.map(({ name, bytes: input }) => {
|
|
611
|
+
const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
612
|
+
const kind = sniffIxbrl(bytes);
|
|
613
|
+
if (!kind) throw new IxbrlError(`${nameOf(name)} is not an Inline XBRL document or an XBRL instance.`);
|
|
614
|
+
if (many && kind === "xbrl") throw new IxbrlError(`${nameOf(name)} is an XBRL instance; an instance is one file.`);
|
|
615
|
+
const document = parseDocument(decodeUtf8(bytes));
|
|
616
|
+
return { name, kind, parts: kind === "ixbrl" ? inlineParts(document) : instanceParts(document) };
|
|
617
|
+
});
|
|
618
|
+
const kind = parsed[0].kind;
|
|
619
|
+
|
|
620
|
+
const { contexts, units, identifiers } = mergeResources(parsed);
|
|
621
|
+
if (many) checkSetAgrees(parsed, identifiers);
|
|
622
|
+
|
|
623
|
+
const continuations = new Map();
|
|
624
|
+
for (const part of parsed) for (const [id, element] of part.parts.continuations) continuations.set(id, element);
|
|
625
|
+
|
|
626
|
+
if (Object.keys(contexts).length === 0 && parsed.some((part) => part.parts.factElements.length > 0)) {
|
|
627
|
+
throw new IxbrlError(`This file is one part of a document set: its contexts are in another file. ${DROP_THE_SET}`);
|
|
514
628
|
}
|
|
515
629
|
|
|
516
630
|
const facts = [];
|
|
517
631
|
const seen = new Map();
|
|
518
632
|
const factNamespaces = new Set();
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
factNamespaces.add(namespace);
|
|
529
|
-
|
|
530
|
-
const contextRef = element.getAttribute("contextRef");
|
|
531
|
-
const context = contexts[contextRef];
|
|
532
|
-
if (!context) throw new IxbrlError(`The fact ${localName} refers to the context "${contextRef}", which the file does not define.`);
|
|
533
|
-
|
|
534
|
-
const numeric = kind === "ixbrl" ? isIx(element, "nonFraction") : element.hasAttribute("unitRef");
|
|
535
|
-
let value;
|
|
536
|
-
let unit = null;
|
|
537
|
-
let decimals;
|
|
538
|
-
if (numeric) {
|
|
539
|
-
const unitRef = element.getAttribute("unitRef");
|
|
540
|
-
unit = units[unitRef];
|
|
541
|
-
if (unit === undefined)
|
|
542
|
-
throw new IxbrlError(`The fact ${localName} refers to the unit "${unitRef}", which the file does not define.`);
|
|
543
|
-
if (unit !== "GBP" && unit !== "pure" && unit !== "shares" && !unit.startsWith("divide:")) {
|
|
544
|
-
throw new IxbrlError(`The fact ${localName} is stated in ${unit}; diya-gl reads pounds sterling (GBP), shares and pure numbers.`);
|
|
633
|
+
const leftOut = new Set();
|
|
634
|
+
let defaultTargetElements = 0;
|
|
635
|
+
const shared = { contexts, units, continuations };
|
|
636
|
+
for (const part of parsed) {
|
|
637
|
+
for (const element of part.parts.factElements) {
|
|
638
|
+
const target = kind === "ixbrl" ? element.getAttribute("target") : null;
|
|
639
|
+
if (target) {
|
|
640
|
+
leftOut.add(target);
|
|
641
|
+
continue;
|
|
545
642
|
}
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
value,
|
|
563
|
-
hidden: insideHidden(element, parts.hiddenElements),
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
const key = `${namespace}#${localName}|${contextKey(context)}|${unit ?? ""}`;
|
|
567
|
-
if (seen.has(key)) {
|
|
568
|
-
const first = seen.get(key);
|
|
569
|
-
if (first.value !== value) {
|
|
570
|
-
throw new IxbrlError(`The file states ${localName} twice for the same period with different values (${first.value} and ${value}).`);
|
|
643
|
+
const fact = readFactElement(element, part, shared);
|
|
644
|
+
if (!fact) continue;
|
|
645
|
+
defaultTargetElements += 1;
|
|
646
|
+
factNamespaces.add(fact.namespace);
|
|
647
|
+
|
|
648
|
+
const context = contexts[fact.contextRef];
|
|
649
|
+
const key = `${fact.namespace}#${fact.localName}|${contextKey(context)}|${fact.unit ?? ""}`;
|
|
650
|
+
if (seen.has(key)) {
|
|
651
|
+
const first = seen.get(key);
|
|
652
|
+
if (first.value !== fact.value) {
|
|
653
|
+
throw new IxbrlError(
|
|
654
|
+
`${many ? "The files state" : "The file states"} ${fact.localName} twice for the same period with different values (${first.value} and ${fact.value}).`,
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
first.hidden = first.hidden && fact.hidden;
|
|
658
|
+
continue;
|
|
571
659
|
}
|
|
572
|
-
|
|
573
|
-
|
|
660
|
+
seen.set(key, fact);
|
|
661
|
+
facts.push(fact);
|
|
574
662
|
}
|
|
575
|
-
|
|
576
|
-
|
|
663
|
+
}
|
|
664
|
+
if (defaultTargetElements === 0 && leftOut.size > 0) {
|
|
665
|
+
throw new IxbrlError(`The set has no facts for the default target; its facts are for the targets ${[...leftOut].join(", ")}.`);
|
|
577
666
|
}
|
|
578
667
|
|
|
579
|
-
const suite = suiteOf(
|
|
668
|
+
const suite = suiteOf(
|
|
669
|
+
factNamespaces,
|
|
670
|
+
parsed.flatMap((part) => part.parts.schemaRefs),
|
|
671
|
+
);
|
|
580
672
|
checkSuite(suite);
|
|
581
673
|
|
|
582
|
-
return {
|
|
674
|
+
return {
|
|
675
|
+
kind,
|
|
676
|
+
suite: { family: suite.family, date: suite.date, modules: [...suite.modules] },
|
|
677
|
+
facts: suite.family === "UK-GAAP" ? translateUkGaapFacts(facts) : facts,
|
|
678
|
+
contexts,
|
|
679
|
+
units,
|
|
680
|
+
leftOutTargets: [...leftOut],
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Read an Inline XBRL document or an XBRL instance into facts.
|
|
686
|
+
* @param {Uint8Array|string} input - the document
|
|
687
|
+
* @returns {{kind: "ixbrl"|"xbrl", suite: {family: string|null, date: string|null, modules: string[]}, facts: Array<Object>, contexts: Object, units: Object, leftOutTargets: string[]}}
|
|
688
|
+
* @throws {IxbrlError} when the document cannot be read, with a message a customer reads
|
|
689
|
+
*/
|
|
690
|
+
export function readIxbrl(input) {
|
|
691
|
+
return readIxbrlSet([{ name: "", bytes: input }]);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const DATE_TEXT = /^\d{4}-\d{2}-\d{2}$/;
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Whether two reads of one report state the same entity, periods, numbers and
|
|
698
|
+
* dates. A Welsh package holds the report in two languages and Companies House
|
|
699
|
+
* rejects it when they differ.
|
|
700
|
+
* @param {Object} first - a read from readIxbrlSet
|
|
701
|
+
* @param {Object} second - a read from readIxbrlSet
|
|
702
|
+
* @param {string} firstLabel - what to call the first in the refusal
|
|
703
|
+
* @param {string} secondLabel
|
|
704
|
+
* @throws {IxbrlError} naming the first concept that differs
|
|
705
|
+
*/
|
|
706
|
+
export function checkSameReport(first, second, firstLabel, secondLabel) {
|
|
707
|
+
const entities = (read) => [...new Set(Object.values(read.contexts).map((context) => context.entity.identifier))].sort().join(",");
|
|
708
|
+
if (entities(first) !== entities(second)) {
|
|
709
|
+
throw new IxbrlError(`The ${firstLabel} and ${secondLabel} accounts are for different companies.`);
|
|
710
|
+
}
|
|
711
|
+
const periods = (read) =>
|
|
712
|
+
[
|
|
713
|
+
...new Set(
|
|
714
|
+
Object.values(read.contexts)
|
|
715
|
+
.filter((context) => !Object.keys(context.dimensions).length)
|
|
716
|
+
.map((context) => periodKey(context.period)),
|
|
717
|
+
),
|
|
718
|
+
]
|
|
719
|
+
.sort()
|
|
720
|
+
.join(",");
|
|
721
|
+
if (periods(first) !== periods(second)) {
|
|
722
|
+
throw new IxbrlError(`The ${firstLabel} and ${secondLabel} accounts are for different periods.`);
|
|
723
|
+
}
|
|
724
|
+
const figures = (read) =>
|
|
725
|
+
new Map(
|
|
726
|
+
read.facts
|
|
727
|
+
.filter((fact) => fact.unit !== null || (typeof fact.value === "string" && DATE_TEXT.test(fact.value)))
|
|
728
|
+
.map((fact) => [
|
|
729
|
+
`${fact.namespace}#${fact.localName}|${contextKey({ entity: fact.entity, period: fact.period, dimensions: fact.dimensions })}|${fact.unit ?? ""}`,
|
|
730
|
+
fact,
|
|
731
|
+
]),
|
|
732
|
+
);
|
|
733
|
+
const a = figures(first);
|
|
734
|
+
const b = figures(second);
|
|
735
|
+
for (const [key, fact] of a) {
|
|
736
|
+
if (!b.has(key) || b.get(key).value !== fact.value) {
|
|
737
|
+
throw new IxbrlError(`The ${firstLabel} and ${secondLabel} accounts differ at ${fact.localName}.`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
for (const [key, fact] of b) {
|
|
741
|
+
if (!a.has(key)) throw new IxbrlError(`The ${firstLabel} and ${secondLabel} accounts differ at ${fact.localName}.`);
|
|
742
|
+
}
|
|
583
743
|
}
|
|
@@ -18,7 +18,7 @@ import { buildNewBook } from "./diya-gl-new-book.js";
|
|
|
18
18
|
import { diyaGlToScenario } from "./diya-gl-loader.js";
|
|
19
19
|
import { calculateFromDiyaGl } from "./diya-gl-calculator.js";
|
|
20
20
|
import { buildOpeningBalance, toV2OpeningBalances } from "./scenario-extractor.js";
|
|
21
|
-
import { FIXED_ASSET_ACCOUNTS, FIXED_ASSET_CLASSES, IxbrlError,
|
|
21
|
+
import { FIXED_ASSET_ACCOUNTS, FIXED_ASSET_CLASSES, IxbrlError, natureProfitBeforeTax } from "./ixbrl-accounts.js";
|
|
22
22
|
|
|
23
23
|
// The register's class name for each class the accounts note uses.
|
|
24
24
|
const REGISTER_CLASS = {
|
|
@@ -284,8 +284,6 @@ class LineWriter {
|
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
const referenceFor = (concept) => `IXBRL-${concept}`;
|
|
288
|
-
|
|
289
287
|
// The opening journal: the balance sheet at the start of the year.
|
|
290
288
|
function openingJournal(writer, date, opening, assetClasses) {
|
|
291
289
|
const reference = "OB-001";
|
|
@@ -323,11 +321,14 @@ function profitAndLossLines(profitAndLoss, { stockMovement, depreciation, tagged
|
|
|
323
321
|
};
|
|
324
322
|
add("TurnoverRevenue", "sales", "4000", profitAndLoss.turnover, "Turnover");
|
|
325
323
|
add("OtherOperatingIncomeFormat2", "sales", "4003", profitAndLoss.otherOperatingIncome, "Other operating income");
|
|
324
|
+
add("OwnWorkCapitalised", "sales", "4003", profitAndLoss.ownWorkCapitalised, "Own work capitalised");
|
|
326
325
|
const costOfSales = profitAndLoss.costOfSales !== undefined ? profitAndLoss.costOfSales : profitAndLoss.rawMaterialsConsumables;
|
|
327
326
|
const costConcept = profitAndLoss.costOfSales !== undefined ? "CostSales" : "RawMaterialsConsumablesUsed";
|
|
328
327
|
if (costOfSales !== undefined || stockMovement !== 0)
|
|
329
328
|
add(costConcept, "purchases", "5000", n(costOfSales) + stockMovement, "Cost of sales");
|
|
329
|
+
add("OtherExternalCharges", "purchases", "5001", profitAndLoss.otherExternalCharges, "Other external charges");
|
|
330
330
|
add("StaffCostsEmployeeBenefitsExpense", "purchases", "5101", profitAndLoss.staffCosts, "Staff costs");
|
|
331
|
+
add("InterestPayableSimilarChargesFinanceCosts", "purchases", "5803", profitAndLoss.interestPayable, "Interest payable");
|
|
331
332
|
if (profitAndLoss.administrativeExpenses !== undefined) {
|
|
332
333
|
// The engine works the gain or loss on disposal from the sale, so it comes out of the expenses the file nets it into.
|
|
333
334
|
add(
|
|
@@ -354,9 +355,10 @@ function profitAndLossLines(profitAndLoss, { stockMovement, depreciation, tagged
|
|
|
354
355
|
|
|
355
356
|
function plProfitBeforeTax(profitAndLoss) {
|
|
356
357
|
if (profitAndLoss.profitBeforeTax !== undefined) return profitAndLoss.profitBeforeTax;
|
|
357
|
-
const micro =
|
|
358
|
-
|
|
359
|
-
if (
|
|
358
|
+
const micro = natureProfitBeforeTax(profitAndLoss);
|
|
359
|
+
const netInterest = n(profitAndLoss.interestReceivable) - n(profitAndLoss.interestPayable);
|
|
360
|
+
if (micro !== undefined) return micro + netInterest;
|
|
361
|
+
if (profitAndLoss.operatingProfit !== undefined) return profitAndLoss.operatingProfit + netInterest;
|
|
360
362
|
if (profitAndLoss.profit !== undefined) return profitAndLoss.profit + n(profitAndLoss.tax);
|
|
361
363
|
return undefined;
|
|
362
364
|
}
|
|
@@ -372,6 +374,7 @@ function bookShell(accounts, period, taxData, extras) {
|
|
|
372
374
|
book.documentInfo.entriesComment = extras.comment;
|
|
373
375
|
if (accounts.principalActivity) book.entityInformation.organizationDescription = accounts.principalActivity;
|
|
374
376
|
if (number) book.entityInformation["diya-gl:companyNumber"] = number;
|
|
377
|
+
book.accounts.purchases["5001"] = { "accountMainDescription": "Sub contractors", "diya-gl:column": "P" };
|
|
375
378
|
book.accounts.purchases["5101"] = { "accountMainDescription": "Employee wages (non-PAYE)", "diya-gl:column": "S" };
|
|
376
379
|
book.accounts.purchases["5501"] = { "accountMainDescription": "General admin", "diya-gl:column": "AA" };
|
|
377
380
|
book.accounts.sales["4006"] = { accountMainDescription: "Fixed asset sales" };
|
|
@@ -428,6 +431,7 @@ async function buildYear(accounts, which, deps) {
|
|
|
428
431
|
const period = current ? accounts.period : accounts.priorPeriod;
|
|
429
432
|
const closingSheet = sheetOf(accounts.balanceSheet[current ? "currentYear" : "priorYear"]);
|
|
430
433
|
const profitAndLoss = accounts.profitAndLoss?.[current ? "currentYear" : "priorYear"] ?? null;
|
|
434
|
+
const referenceFor = (concept, statedConcept = concept) => `IXBRL-${accounts.sourceConcepts?.[statedConcept] ?? concept}`;
|
|
431
435
|
const taxData = await loadTaxData(period, deps);
|
|
432
436
|
const additionsRate = taxData.depreciation?.[ADDITIONS_RATE_KEY] ?? 0;
|
|
433
437
|
|
|
@@ -496,6 +500,11 @@ async function buildYear(accounts, which, deps) {
|
|
|
496
500
|
};
|
|
497
501
|
};
|
|
498
502
|
let posted = postsFor(0);
|
|
503
|
+
if (current && profitAndLoss?.changeInStocks !== undefined && Math.abs(profitAndLoss.changeInStocks - stockMovement) > 1) {
|
|
504
|
+
differences.push(
|
|
505
|
+
`${label}: the file states a change in stocks of ${profitAndLoss.changeInStocks} and its balance sheets a stock movement of ${stockMovement}.`,
|
|
506
|
+
);
|
|
507
|
+
}
|
|
499
508
|
|
|
500
509
|
const build = (flow) => {
|
|
501
510
|
// The prior year's opening balance sheet: the closing one worked back.
|
|
@@ -513,7 +522,7 @@ async function buildYear(accounts, which, deps) {
|
|
|
513
522
|
writer.invoice(
|
|
514
523
|
"purchases",
|
|
515
524
|
period.end,
|
|
516
|
-
referenceFor("PropertyPlantEquipmentAdditions"),
|
|
525
|
+
referenceFor("PropertyPlantEquipmentAdditions", "AdditionsOtherThanThroughBusinessCombinationsPropertyPlantEquipment"),
|
|
517
526
|
"5900",
|
|
518
527
|
additionsAmount,
|
|
519
528
|
"Fixed asset additions",
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2006-2026 DIY Accounting Limited
|
|
3
|
+
//
|
|
4
|
+
// ixbrl-uk-gaap.js — the translation from the old UK GAAP taxonomy (the
|
|
5
|
+
// 2009-09-01 suite: uk-gaap, uk-bus and uk-direp) to the FRS 102 names that
|
|
6
|
+
// ixbrl-accounts.js reads. The reader applies it once, after it has found the
|
|
7
|
+
// suite, so the mapping, the checks and the summary book read FRS 102 names
|
|
8
|
+
// only. A translated fact keeps `sourceConcept`, the UK GAAP name it came
|
|
9
|
+
// from. A UK GAAP name this table does not hold keeps its own name and its
|
|
10
|
+
// own namespace, and the mapping lists it as unmapped.
|
|
11
|
+
|
|
12
|
+
export const UK_GAAP_SUITE_DATE = "2009-09-01";
|
|
13
|
+
|
|
14
|
+
const NS_UK_GAAP = `http://www.xbrl.org/uk/gaap/core/${UK_GAAP_SUITE_DATE}`;
|
|
15
|
+
const NS_UK_BUS = `http://www.xbrl.org/uk/cd/business/${UK_GAAP_SUITE_DATE}`;
|
|
16
|
+
const NS_UK_DIREP = `http://www.xbrl.org/uk/reports/direp/${UK_GAAP_SUITE_DATE}`;
|
|
17
|
+
|
|
18
|
+
// The namespaces a translated fact, dimension and member take. The mapping
|
|
19
|
+
// matches by local name, so these only say the name is an FRS 102 one.
|
|
20
|
+
const FRS_NAMESPACE = {
|
|
21
|
+
"uk-gaap": "http://xbrl.frc.org.uk/fr/2022-01-01/core",
|
|
22
|
+
"uk-bus": "http://xbrl.frc.org.uk/cd/2022-01-01/business",
|
|
23
|
+
"uk-direp": "http://xbrl.frc.org.uk/reports/2022-01-01/direp",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const MODULE_OF_NAMESPACE = { [NS_UK_GAAP]: "uk-gaap", [NS_UK_BUS]: "uk-bus", [NS_UK_DIREP]: "uk-direp" };
|
|
27
|
+
|
|
28
|
+
const MATURITY = "MaturitiesOrExpirationPeriodsDimension";
|
|
29
|
+
const EQUITY_CLASS = "EquityClassesDimension";
|
|
30
|
+
const WITHIN = "WithinOneYear";
|
|
31
|
+
const AFTER = "AfterOneYear";
|
|
32
|
+
|
|
33
|
+
const row = (module, concept, to = concept, dimension, member) => ({
|
|
34
|
+
module,
|
|
35
|
+
concept,
|
|
36
|
+
to,
|
|
37
|
+
...(dimension ? { dimension, member } : {}),
|
|
38
|
+
});
|
|
39
|
+
const gaap = (concept, to, dimension, member) => row("uk-gaap", concept, to, dimension, member);
|
|
40
|
+
const bus = (concept, to) => row("uk-bus", concept, to);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* One row per UK GAAP name: the FRS 102 local name, and the dimension and
|
|
44
|
+
* member the translated fact gains (replacing the fact's own value for that
|
|
45
|
+
* dimension). `OtherOperatingIncome` is the one row whose target depends on
|
|
46
|
+
* the file: `toWithGrossProfit` when the file states `CostSales` or
|
|
47
|
+
* `GrossProfitLoss`, `to` otherwise.
|
|
48
|
+
*/
|
|
49
|
+
export const UK_GAAP_TO_FRS_102 = [
|
|
50
|
+
gaap("FixedAssets"),
|
|
51
|
+
gaap("TangibleFixedAssets", "PropertyPlantEquipment"),
|
|
52
|
+
gaap("TangibleFixedAssetsCostOrValuation", "PropertyPlantEquipmentGrossCost"),
|
|
53
|
+
gaap("TangibleFixedAssetsAdditions", "AdditionsOtherThanThroughBusinessCombinationsPropertyPlantEquipment"),
|
|
54
|
+
gaap("TangibleFixedAssetsDisposals", "DisposalsPropertyPlantEquipment"),
|
|
55
|
+
gaap("TangibleFixedAssetsDepreciation", "AccumulatedDepreciationImpairmentPropertyPlantEquipment"),
|
|
56
|
+
gaap("TangibleFixedAssetsDepreciationChargedInPeriod", "IncreaseFromDepreciationChargeForYearPropertyPlantEquipment"),
|
|
57
|
+
gaap("TangibleFixedAssetsDepreciationDecreaseIncreaseOnDisposals", "DisposalsDecreaseInDepreciationImpairmentPropertyPlantEquipment"),
|
|
58
|
+
gaap("StocksInventory", "TotalInventories"),
|
|
59
|
+
gaap("Debtors"),
|
|
60
|
+
gaap("DebtorsDueWithinOneYear", "Debtors", MATURITY, WITHIN),
|
|
61
|
+
gaap("PrepaymentsAccruedIncomeNotExpressedWithinCurrentAssetSubtotal"),
|
|
62
|
+
gaap("CashBankInHand", "CashBankOnHand"),
|
|
63
|
+
gaap("CurrentAssets"),
|
|
64
|
+
gaap("NetCurrentAssetsLiabilities"),
|
|
65
|
+
gaap("TotalAssetsLessCurrentLiabilities"),
|
|
66
|
+
gaap("CreditorsDueWithinOneYear", "Creditors", MATURITY, WITHIN),
|
|
67
|
+
gaap("CreditorsDueAfterOneYear", "Creditors", MATURITY, AFTER),
|
|
68
|
+
gaap("TradeCreditors", "TradeCreditorsTradePayables", MATURITY, WITHIN),
|
|
69
|
+
gaap("TradeCreditorsWithinOneYear", "TradeCreditorsTradePayables", MATURITY, WITHIN),
|
|
70
|
+
gaap("CorporationTax", "CorporationTaxPayable"),
|
|
71
|
+
gaap("CorporationTaxDueWithinOneYear", "CorporationTaxPayable"),
|
|
72
|
+
gaap("OtherCreditors", "OtherCreditors", MATURITY, WITHIN),
|
|
73
|
+
gaap("OtherCreditorsDueWithinOneYear", "OtherCreditors", MATURITY, WITHIN),
|
|
74
|
+
gaap("TaxationSocialSecurityDueWithinOneYear", "TaxationSocialSecurityPayable", MATURITY, WITHIN),
|
|
75
|
+
gaap("AccrualsDeferredIncomeWithinOneYear", "AccruedLiabilitiesNotExpressedWithinCreditorsSubtotal", MATURITY, WITHIN),
|
|
76
|
+
gaap("ProvisionsForLiabilitiesCharges", "ProvisionsForLiabilitiesBalanceSheetSubtotal"),
|
|
77
|
+
gaap("NetAssetsLiabilitiesIncludingPensionAssetLiability", "NetAssetsLiabilities"),
|
|
78
|
+
gaap("CalledUpShareCapital", "Equity", EQUITY_CLASS, "ShareCapital"),
|
|
79
|
+
gaap("ProfitLossAccountReserve", "Equity", EQUITY_CLASS, "RetainedEarningsAccumulatedLosses"),
|
|
80
|
+
gaap("RevaluationReserve", "Equity", EQUITY_CLASS, "RevaluationReserve"),
|
|
81
|
+
gaap("ShareholderFunds", "Equity"),
|
|
82
|
+
gaap("TurnoverGrossOperatingRevenue", "TurnoverRevenue"),
|
|
83
|
+
gaap("CostSales"),
|
|
84
|
+
gaap("GrossProfitLoss"),
|
|
85
|
+
gaap("AdministrativeExpenses"),
|
|
86
|
+
gaap("OperatingProfitLoss"),
|
|
87
|
+
gaap("ProfitLossOnOrdinaryActivitiesBeforeTax"),
|
|
88
|
+
gaap("ProfitLossOnOrdinaryActivitiesAfterTax"),
|
|
89
|
+
gaap("OtherInterestReceivableSimilarIncome", "OtherInterestReceivableSimilarIncomeFinanceIncome"),
|
|
90
|
+
gaap("InterestPayableSimilarCharges", "InterestPayableSimilarChargesFinanceCosts"),
|
|
91
|
+
gaap("TaxOnProfitOrLossOnOrdinaryActivities", "TaxTaxCreditOnProfitOrLossOnOrdinaryActivities"),
|
|
92
|
+
gaap("ProfitLossForPeriod", "ProfitLoss"),
|
|
93
|
+
{ ...gaap("OtherOperatingIncome", "OtherOperatingIncomeFormat2"), toWithGrossProfit: "OtherOperatingIncomeFormat1" },
|
|
94
|
+
gaap("RawMaterialsConsumables", "RawMaterialsConsumablesUsed"),
|
|
95
|
+
gaap("OtherExternalCharges"),
|
|
96
|
+
gaap("OwnWorkCapitalised"),
|
|
97
|
+
gaap("WagesSalaries"),
|
|
98
|
+
gaap("SocialSecurityCosts"),
|
|
99
|
+
gaap("IncreaseDecreaseInStocksFinishedGoodsWorkInProgress", "IncreaseDecreaseInStocksInventoriesFinishedGoodsWorkInProgress"),
|
|
100
|
+
gaap("StaffCosts", "StaffCostsEmployeeBenefitsExpense"),
|
|
101
|
+
gaap("DepreciationOtherAmountsWrittenOffTangibleIntangibleFixedAssets", "DepreciationAmortisationImpairmentExpense"),
|
|
102
|
+
gaap("OtherOperatingChargesFormat2", "OtherOperatingExpensesFormat2"),
|
|
103
|
+
gaap("AverageNumberEmployeesDuringPeriod"),
|
|
104
|
+
gaap("ParValueShare"),
|
|
105
|
+
gaap("ShareCapitalAllottedCalledUpPaid", "ShareCapital"),
|
|
106
|
+
gaap("TradeDebtors", "TradeDebtorsTradeReceivables"),
|
|
107
|
+
gaap("OtherDebtors"),
|
|
108
|
+
gaap("IntangibleFixedAssets", "IntangibleAssets"),
|
|
109
|
+
gaap("BankBorrowingsOverdrafts"),
|
|
110
|
+
bus("UKCompaniesHouseRegisteredNumber"),
|
|
111
|
+
bus("EntityCurrentLegalOrRegisteredName"),
|
|
112
|
+
bus("BalanceSheetDate"),
|
|
113
|
+
bus("StartDateForPeriodCoveredByReport"),
|
|
114
|
+
bus("EndDateForPeriodCoveredByReport"),
|
|
115
|
+
bus("DescriptionPrincipalActivities"),
|
|
116
|
+
bus("NameEntityOfficer"),
|
|
117
|
+
bus("EntityDormant", "EntityDormantTruefalse"),
|
|
118
|
+
gaap("NameDirectorSigningAccounts", "DirectorSigningFinancialStatements"),
|
|
119
|
+
gaap("DateApprovalAccounts", "DateAuthorisationFinancialStatementsForIssue"),
|
|
120
|
+
gaap("NumberSharesAllotted", "NumberSharesIssuedFullyPaid"),
|
|
121
|
+
gaap("NominalValueSharesIssued", "NominalValueAllottedShareCapital"),
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
// The old suite names some dimensions and members the FRS 102 suite names differently. Members not listed keep their name.
|
|
125
|
+
export const UK_GAAP_DIMENSIONS_TO_FRS_102 = {
|
|
126
|
+
MaturitiesDimension: { to: MATURITY, members: {} },
|
|
127
|
+
TangibleFixedAssetClassesDimension: {
|
|
128
|
+
to: "PropertyPlantEquipmentClassesDimension",
|
|
129
|
+
members: { FixturesFittings: "FurnitureFittings" },
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const ROWS = new Map(UK_GAAP_TO_FRS_102.map((entry) => [`${entry.module}:${entry.concept}`, entry]));
|
|
134
|
+
|
|
135
|
+
const localOf = (key) => key.slice(key.indexOf("#") + 1);
|
|
136
|
+
|
|
137
|
+
function translatedDimensions(dimensions, entry, module) {
|
|
138
|
+
const out = {};
|
|
139
|
+
for (const [dimensionKey, memberKey] of Object.entries(dimensions)) {
|
|
140
|
+
const renamed = UK_GAAP_DIMENSIONS_TO_FRS_102[localOf(dimensionKey)];
|
|
141
|
+
if (renamed && MODULE_OF_NAMESPACE[dimensionKey.slice(0, dimensionKey.indexOf("#"))]) {
|
|
142
|
+
const member = localOf(memberKey);
|
|
143
|
+
out[`${FRS_NAMESPACE["uk-gaap"]}#${renamed.to}`] = `${FRS_NAMESPACE["uk-gaap"]}#${renamed.members[member] ?? member}`;
|
|
144
|
+
} else {
|
|
145
|
+
out[dimensionKey] = memberKey;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (entry.dimension) {
|
|
149
|
+
out[`${FRS_NAMESPACE[module]}#${entry.dimension}`] = `${FRS_NAMESPACE[module]}#${entry.member}`;
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function factKey(fact) {
|
|
155
|
+
const dimensions = Object.entries(fact.dimensions)
|
|
156
|
+
.sort(([a], [b]) => (a < b ? -1 : 1))
|
|
157
|
+
.map(([dimension, member]) => `${dimension}=${member}`)
|
|
158
|
+
.join(",");
|
|
159
|
+
const period = fact.period.instant ?? `${fact.period.startDate}/${fact.period.endDate}`;
|
|
160
|
+
return `${fact.namespace}#${fact.localName}|${fact.entity.scheme}|${fact.entity.identifier}|${period}|${dimensions}|${fact.unit ?? ""}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Translate the UK GAAP facts of a read into FRS 102 names.
|
|
165
|
+
* Two UK GAAP names that translate to the same fact (a total and the same
|
|
166
|
+
* total restated within one year) give one fact when they agree; when they
|
|
167
|
+
* differ the first is translated and the second keeps its UK GAAP name.
|
|
168
|
+
* @param {Array<Object>} facts - facts from the reader
|
|
169
|
+
* @returns {Array<Object>} the facts, translated where the table holds the name
|
|
170
|
+
*/
|
|
171
|
+
export function translateUkGaapFacts(facts) {
|
|
172
|
+
const grossProfitStated = facts.some(
|
|
173
|
+
(fact) => fact.namespace === NS_UK_GAAP && (fact.localName === "CostSales" || fact.localName === "GrossProfitLoss"),
|
|
174
|
+
);
|
|
175
|
+
const out = [];
|
|
176
|
+
const taken = new Map();
|
|
177
|
+
for (const fact of facts) {
|
|
178
|
+
const module = MODULE_OF_NAMESPACE[fact.namespace];
|
|
179
|
+
const entry = module ? ROWS.get(`${module}:${fact.localName}`) : undefined;
|
|
180
|
+
if (!entry) {
|
|
181
|
+
out.push(fact);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const localName = grossProfitStated && entry.toWithGrossProfit ? entry.toWithGrossProfit : entry.to;
|
|
185
|
+
const translated = {
|
|
186
|
+
...fact,
|
|
187
|
+
namespace: FRS_NAMESPACE[module],
|
|
188
|
+
localName,
|
|
189
|
+
dimensions: translatedDimensions(fact.dimensions, entry, module),
|
|
190
|
+
sourceConcept: fact.localName,
|
|
191
|
+
};
|
|
192
|
+
const key = factKey(translated);
|
|
193
|
+
const earlier = taken.get(key);
|
|
194
|
+
if (earlier === undefined) {
|
|
195
|
+
taken.set(key, translated);
|
|
196
|
+
out.push(translated);
|
|
197
|
+
} else if (earlier.value !== translated.value) {
|
|
198
|
+
out.push(fact);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|