@diy-accounting-uk/diya-gl 1.2.59 → 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.
@@ -1767,6 +1767,32 @@
1767
1767
  "scorecard": "101608 passed, 0 warnings, 0 failed"
1768
1768
  }
1769
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
+ }
1770
1796
  }
1771
1797
  ]
1772
1798
  }
@@ -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: "micro-entity profit and loss" },
142
- { concept: "RawMaterialsConsumablesUsed", figure: "rawMaterialsConsumables", account: "5000", note: "micro-entity profit and loss" },
143
- { concept: "StaffCostsEmployeeBenefitsExpense", figure: "staffCosts", account: "5101", note: "micro-entity profit and loss" },
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: "micro-entity profit and loss",
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
- function profitAndLossAt(set, endDay) {
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
- profitBeforeTax: set.duration("ProfitLossOnOrdinaryActivitiesBeforeTax", endDay),
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 micro = microProfitBeforeTax(pl);
522
- if (micro !== undefined && pl.profit !== undefined && !close(micro - n(pl.tax), pl.profit)) {
523
- mismatch("profit for the year", micro - n(pl.tax), pl.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 P&L: turnover plus other income less the four expense lines.
528
- export function microProfitBeforeTax(pl) {
529
- const hasMicroLine = ["otherOperatingIncome", "rawMaterialsConsumables", "staffCosts", "depreciation", "otherOperatingExpenses"].some(
530
- (key) => pl[key] !== undefined,
531
- );
532
- if (!hasMicroLine || pl.turnover === undefined) return undefined;
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 currentPl = profitAndLossAt(set, period.end);
615
- const priorPl = priorEnd ? profitAndLossAt(set, priorEnd) : {};
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,8 @@ 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})\//;
45
47
 
46
48
  const DROP_THE_SET = "Drop all the files of the set together, or the zip.";
47
49
 
@@ -456,6 +458,7 @@ function suiteOf(namespaces, schemaRefs) {
456
458
  suite.date = match[2];
457
459
  } else if (/xbrl\.org\/uk\//.test(href)) {
458
460
  suite.family = "UK-GAAP";
461
+ suite.date ??= href.match(OLD_UK_GAAP_ENTRY_POINT)?.[1] ?? null;
459
462
  }
460
463
  }
461
464
  for (const namespace of namespaces) {
@@ -465,6 +468,7 @@ function suiteOf(namespaces, schemaRefs) {
465
468
  suite.modules.add(match[3]);
466
469
  } else if (OLD_UK_GAAP_NAMESPACE.test(namespace)) {
467
470
  suite.family = "UK-GAAP";
471
+ suite.date ??= namespace.match(OLD_UK_GAAP_NAMESPACE)[1];
468
472
  }
469
473
  }
470
474
  return suite;
@@ -475,10 +479,17 @@ const SUITE_LABELS = {
475
479
  "IFRS": "IFRS",
476
480
  "Charities": "Charities SORP",
477
481
  "UKSEF": "UK Single Electronic Format",
478
- "UK-GAAP": "old UK GAAP",
479
482
  };
480
483
 
481
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
+ }
482
493
  if (suite.family && !READ_SUITES.includes(suite.family)) {
483
494
  const label = SUITE_LABELS[suite.family] ?? suite.family;
484
495
  throw new IxbrlError(`This file uses the ${label} taxonomy; the form takes FRS 102 and FRS 105 accounts.`);
@@ -663,7 +674,7 @@ export function readIxbrlSet(documents) {
663
674
  return {
664
675
  kind,
665
676
  suite: { family: suite.family, date: suite.date, modules: [...suite.modules] },
666
- facts,
677
+ facts: suite.family === "UK-GAAP" ? translateUkGaapFacts(facts) : facts,
667
678
  contexts,
668
679
  units,
669
680
  leftOutTargets: [...leftOut],
@@ -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, microProfitBeforeTax } from "./ixbrl-accounts.js";
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 = microProfitBeforeTax(profitAndLoss);
358
- if (micro !== undefined) return micro + n(profitAndLoss.interestReceivable);
359
- if (profitAndLoss.operatingProfit !== undefined) return profitAndLoss.operatingProfit + n(profitAndLoss.interestReceivable);
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
+ }
@@ -12,7 +12,7 @@
12
12
 
13
13
  export const PROVENANCE_DATA = {
14
14
  formatVersion: "diya-gl/1",
15
- engineVersion: "1.2.59+9d3b814a",
15
+ engineVersion: "1.2.60+602620de",
16
16
  taxDataHash: "51bdff60e993",
17
17
  reconciledCommit: "54e8731a91279975e61b1a232fbb67fda89515f0",
18
18
  templates: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diy-accounting-uk/diya-gl",
3
- "version": "1.2.59",
3
+ "version": "1.2.60",
4
4
  "type": "module",
5
5
  "description": "Recalculate, read and write DIYA-GL books from the command line",
6
6
  "license": "Apache-2.0",