@diy-accounting-uk/diya-gl 1.2.55 → 1.2.57

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/README.md CHANGED
@@ -96,8 +96,9 @@ Each subcommand is also its own command, if you only want one on your `PATH`:
96
96
 
97
97
  ## API
98
98
 
99
- The package root exports three functions that turn a book into the figures HMRC's MTD APIs
100
- take. Each takes the parsed book, its lines and a parameters object, and returns a promise.
99
+ The package root exports five functions that turn a book into the figures HMRC's MTD APIs
100
+ take. Each takes the parsed book, its lines and a parameters object. The VAT and self-employment
101
+ functions return a promise; the two UK property functions return their answer directly.
101
102
  Nothing is sent anywhere: the answer is the request body to send, plus what fed it.
102
103
 
103
104
  Load a book first:
@@ -166,6 +167,36 @@ const answer = await deriveItsaAnnualSubmission(book, lines, { taxYear: "2025-26
166
167
  console.log(answer.allowances.annualInvestmentAllowance);
167
168
  ```
168
169
 
170
+ ### `deriveUkPropertyPeriod(book, lines, { periodStartDate, periodEndDate })`
171
+
172
+ One period of HMRC's Property Business API (UK property) from a sole trader book. The book's
173
+ chart names the HMRC field each account feeds with `diya-gl:ukPropertyField` on the account
174
+ (for example `"income.periodAmount"` on the rent account, `"expenses.repairsAndMaintenance"` on
175
+ the repairs account). The function sums the sales and purchases postings on those accounts
176
+ between the two dates inclusive, net of VAT, with a credit note taking from its field. The
177
+ answer carries `income`, `expenses`, `omitted` (the fields no posting fed; they are left out of
178
+ the body, never sent as zero), `lines` and `warnings`. A book that names
179
+ `expenses.consolidatedExpenses` and an itemised expense in the same period is refused, because
180
+ HMRC takes one or the other.
181
+
182
+ ```js
183
+ import { deriveUkPropertyPeriod } from "@diy-accounting-uk/diya-gl";
184
+
185
+ const { book, lines } = loadBook("my-landlord-book");
186
+ const answer = deriveUkPropertyPeriod(book, lines, { periodStartDate: "2025-04-06", periodEndDate: "2025-07-05" });
187
+ console.log(answer.income.periodAmount, answer.expenses.repairsAndMaintenance);
188
+ ```
189
+
190
+ ### `deriveUkPropertyAnnual(book, lines, { taxYear? })`
191
+
192
+ The year's adjustments (`balancingCharge`, `privateUseAdjustment`,
193
+ `businessPremisesRenovationAllowanceBalancingCharges`) and allowances (`annualInvestmentAllowance`,
194
+ `businessPremisesRenovationAllowance`, `otherCapitalAllowance`, `costOfReplacingDomesticItems`,
195
+ `zeroEmissionsCarAllowance`) from the postings on the accounts the chart names, for the tax year
196
+ `taxYear` (as `2025-26`; the default is the year the book's period end falls in). The property
197
+ income allowance, `nonResidentLandlord` and `rentARoom.jointlyLet` are choices and facts about the
198
+ landlord, not ledger figures; the answer lists them under `leftToPage`.
199
+
169
200
  ## The format
170
201
 
171
202
  A diya-gl book's fields are drawn from the XBRL Global Ledger Taxonomy Framework 2015 and
@@ -1663,6 +1663,58 @@
1663
1663
  "scorecard": "101608 passed, 0 warnings, 0 failed"
1664
1664
  }
1665
1665
  }
1666
+ },
1667
+ {
1668
+ "tag": "diya-gl-v1.2.55",
1669
+ "date": "2026-10-24",
1670
+ "formatVersion": "diya-gl/1",
1671
+ "engineVersion": "1.2.55+bb0dbecd",
1672
+ "taxDataHash": "51bdff60e993",
1673
+ "reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
1674
+ "templates": {
1675
+ "bst": {
1676
+ "hash": "0d003e10e00c",
1677
+ "scorecard": "812 passed, 0 warnings, 0 failed"
1678
+ },
1679
+ "taxi": {
1680
+ "hash": "1ff69654d64d",
1681
+ "scorecard": "700 passed, 0 warnings, 0 failed"
1682
+ },
1683
+ "se": {
1684
+ "hash": "064605ae76ed",
1685
+ "scorecard": "8015 passed, 0 warnings, 0 failed"
1686
+ },
1687
+ "ltd": {
1688
+ "hash": "a65ab09cf35c",
1689
+ "scorecard": "101608 passed, 0 warnings, 0 failed"
1690
+ }
1691
+ }
1692
+ },
1693
+ {
1694
+ "tag": "diya-gl-v1.2.56",
1695
+ "date": "2026-10-25",
1696
+ "formatVersion": "diya-gl/1",
1697
+ "engineVersion": "1.2.56+4d61d48a",
1698
+ "taxDataHash": "51bdff60e993",
1699
+ "reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
1700
+ "templates": {
1701
+ "bst": {
1702
+ "hash": "0d003e10e00c",
1703
+ "scorecard": "812 passed, 0 warnings, 0 failed"
1704
+ },
1705
+ "taxi": {
1706
+ "hash": "1ff69654d64d",
1707
+ "scorecard": "700 passed, 0 warnings, 0 failed"
1708
+ },
1709
+ "se": {
1710
+ "hash": "064605ae76ed",
1711
+ "scorecard": "8015 passed, 0 warnings, 0 failed"
1712
+ },
1713
+ "ltd": {
1714
+ "hash": "a65ab09cf35c",
1715
+ "scorecard": "101608 passed, 0 warnings, 0 failed"
1716
+ }
1717
+ }
1666
1718
  }
1667
1719
  ]
1668
1720
  }
@@ -554,7 +554,8 @@ function buildSchedule(scenario, rate, depreciationRates, investmentAllowancePer
554
554
  }
555
555
 
556
556
  for (const row of rows) {
557
- const ratePercent = depreciationRates[row.assetClass];
557
+ // An asset that states its own rate depreciates at it; any other takes its class's rate.
558
+ const ratePercent = row.openingAsset?.depreciation_rate ?? depreciationRates[row.assetClass];
558
559
  const netBookValue = row.cost - row.depreciationBroughtForward;
559
560
  row.depreciationCharge = row.cost > 0 ? Math.min(row.cost * ratePercent, netBookValue) : 0;
560
561
  row.depreciationCarriedForward = row.cost > 0 ? row.depreciationBroughtForward + row.depreciationCharge : 0;
@@ -6,3 +6,4 @@
6
6
 
7
7
  export { deriveVatReturn } from "./vat-return.js";
8
8
  export { deriveItsaQuarterlyUpdate, deriveItsaAnnualSubmission } from "./itsa.js";
9
+ export { deriveUkPropertyPeriod, deriveUkPropertyAnnual } from "./uk-property.js";
@@ -0,0 +1,218 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2006-2026 DIY Accounting Limited
3
+ //
4
+ // uk-property.js — the two UK property answers HMRC's Property Business API
5
+ // takes from a sole trader's book. deriveUkPropertyPeriod answers one
6
+ // period's income and expenses; deriveUkPropertyAnnual answers the year's
7
+ // adjustments and allowances the book can show.
8
+ //
9
+ // A book names the HMRC field an account feeds with the account's
10
+ // diya-gl:ukPropertyField, in HMRC's own dotted names (income.periodAmount,
11
+ // expenses.repairsAndMaintenance, allowances.costOfReplacingDomesticItems).
12
+ // Lines posted through the sales and purchases journals to a named account
13
+ // are summed net of VAT. A field with no posting is omitted, never sent as a
14
+ // zero. Elections and facts about the landlord (the property income
15
+ // allowance, non-resident landlord, rent a room jointly let) are not in a
16
+ // ledger; they are listed under leftToPage.
17
+
18
+ import { productOf } from "../product-workbook.js";
19
+ import { signedAmount } from "../scenario-extractor.js";
20
+
21
+ export const UK_PROPERTY_ACCOUNT_FIELD = "diya-gl:ukPropertyField";
22
+
23
+ export const PERIOD_INCOME_FIELDS = ["periodAmount", "premiumsOfLeaseGrant", "reversePremiums", "otherIncome", "taxDeducted"].map(
24
+ (name) => `income.${name}`,
25
+ );
26
+
27
+ export const PERIOD_EXPENSE_FIELDS = [
28
+ "premisesRunningCosts",
29
+ "repairsAndMaintenance",
30
+ "financialCosts",
31
+ "professionalFees",
32
+ "costOfServices",
33
+ "other",
34
+ "residentialFinancialCost",
35
+ "travelCosts",
36
+ "consolidatedExpenses",
37
+ ].map((name) => `expenses.${name}`);
38
+
39
+ export const ANNUAL_ADJUSTMENT_FIELDS = [
40
+ "balancingCharge",
41
+ "privateUseAdjustment",
42
+ "businessPremisesRenovationAllowanceBalancingCharges",
43
+ ].map((name) => `adjustments.${name}`);
44
+
45
+ export const ANNUAL_ALLOWANCE_FIELDS = [
46
+ "annualInvestmentAllowance",
47
+ "businessPremisesRenovationAllowance",
48
+ "otherCapitalAllowance",
49
+ "costOfReplacingDomesticItems",
50
+ "zeroEmissionsCarAllowance",
51
+ ].map((name) => `allowances.${name}`);
52
+
53
+ export const PERIOD_FIELDS = [...PERIOD_INCOME_FIELDS, ...PERIOD_EXPENSE_FIELDS];
54
+ export const ANNUAL_FIELDS = [...ANNUAL_ADJUSTMENT_FIELDS, ...ANNUAL_ALLOWANCE_FIELDS];
55
+ export const UK_PROPERTY_FIELDS = [...PERIOD_FIELDS, ...ANNUAL_FIELDS];
56
+
57
+ // What the annual submission asks that no posting can answer.
58
+ export const ANNUAL_LEFT_TO_PAGE = [
59
+ {
60
+ field: "allowances.propertyIncomeAllowance",
61
+ reason: "an election between the property income allowance and itemised expenses, made by the landlord.",
62
+ },
63
+ { field: "adjustments.nonResidentLandlord", reason: "a fact about the landlord's residence, not a ledger figure." },
64
+ { field: "adjustments.rentARoom.jointlyLet", reason: "a fact about how the room is let, not a ledger figure." },
65
+ ];
66
+
67
+ const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
68
+ const TAX_YEAR_LABEL = /^(\d{4})-(\d{2})$/;
69
+ const OPENING_BALANCE_PREFIX = "OB-";
70
+
71
+ function round2(value) {
72
+ return Math.round(value * 100) / 100;
73
+ }
74
+
75
+ function setPath(target, path, value) {
76
+ const parts = path.split(".");
77
+ let node = target;
78
+ for (const part of parts.slice(0, -1)) node = node[part] ??= {};
79
+ node[parts.at(-1)] = value;
80
+ }
81
+
82
+ function requirePropertyBook(book, lines, caller) {
83
+ if (!book || !lines) throw new Error(`${caller} requires a book and its lines`);
84
+ const product = productOf(book);
85
+ if (product === "ltd") {
86
+ throw new Error(`The book is a "ltd" book; UK property income is an individual's, so ${caller} reads a sole trader book`);
87
+ }
88
+ }
89
+
90
+ // Account id -> HMRC field, from the book's own chart.
91
+ function accountFields(book) {
92
+ const fields = new Map();
93
+ for (const journal of ["sales", "purchases"]) {
94
+ for (const [id, account] of Object.entries(book.accounts?.[journal] || {})) {
95
+ const field = account[UK_PROPERTY_ACCOUNT_FIELD];
96
+ if (field === undefined) continue;
97
+ if (!UK_PROPERTY_FIELDS.includes(field)) {
98
+ throw new Error(`account ${id} names ${UK_PROPERTY_ACCOUNT_FIELD} "${field}", which is not a UK property field`);
99
+ }
100
+ fields.set(id, field);
101
+ }
102
+ }
103
+ return fields;
104
+ }
105
+
106
+ function netAmount(line) {
107
+ return signedAmount(line) / (1 + (line.taxRate || 0));
108
+ }
109
+
110
+ // The postings inside [start, end] on a mapped account, summed by field.
111
+ function figuresBetween(book, lines, fieldsWanted, start, end) {
112
+ const fields = accountFields(book);
113
+ const totals = new Map();
114
+ const contributions = [];
115
+ for (const line of lines) {
116
+ if (line.sourceJournalID !== "sales" && line.sourceJournalID !== "purchases") continue;
117
+ if (String(line.documentReference || "").startsWith(OPENING_BALANCE_PREFIX)) continue;
118
+ const field = fields.get(String(line.accountMainID));
119
+ if (!field || !fieldsWanted.includes(field)) continue;
120
+ if (line.postingDate < start || line.postingDate > end) continue;
121
+ const amount = round2(netAmount(line));
122
+ totals.set(field, (totals.get(field) ?? 0) + amount);
123
+ contributions.push({
124
+ entryNumber: line.entryNumber,
125
+ date: line.postingDate,
126
+ account: String(line.accountMainID),
127
+ field,
128
+ reference: line.documentReference ?? null,
129
+ counterparty: line.detailComment ?? null,
130
+ amount,
131
+ });
132
+ }
133
+ const figures = {};
134
+ for (const field of fieldsWanted) {
135
+ if (totals.has(field)) setPath(figures, field, round2(totals.get(field)));
136
+ }
137
+ return { figures, contributions, present: fieldsWanted.filter((field) => totals.has(field)) };
138
+ }
139
+
140
+ /**
141
+ * One period of HMRC's UK property income and expenses from a sole trader
142
+ * book whose chart names the property accounts. The answer is the period's
143
+ * own figures; a cumulative year's running total is the same call with the
144
+ * period starting on 6 April.
145
+ * @param {Object} book - parsed book.toml
146
+ * @param {Array} lines - parsed lines.jsonl entries
147
+ * @param {{periodStartDate: string, periodEndDate: string}} params - the period, both ends inclusive, as YYYY-MM-DD
148
+ * @returns {Object} { periodDates, income, expenses, fieldSlots, omitted, lines, warnings }
149
+ */
150
+ export function deriveUkPropertyPeriod(book, lines, { periodStartDate, periodEndDate } = {}) {
151
+ requirePropertyBook(book, lines, "deriveUkPropertyPeriod");
152
+ if (!ISO_DAY.test(periodStartDate ?? "") || !ISO_DAY.test(periodEndDate ?? "")) {
153
+ throw new Error("periodStartDate and periodEndDate must be YYYY-MM-DD days");
154
+ }
155
+ if (periodStartDate > periodEndDate) throw new Error(`the period starts ${periodStartDate}, after it ends ${periodEndDate}`);
156
+
157
+ const { figures, contributions, present } = figuresBetween(book, lines, PERIOD_FIELDS, periodStartDate, periodEndDate);
158
+ const warnings = [];
159
+ const itemised = present.filter((field) => field.startsWith("expenses.") && field !== "expenses.consolidatedExpenses");
160
+ if (present.includes("expenses.consolidatedExpenses") && itemised.length > 0) {
161
+ throw new Error(`the period carries consolidated expenses and itemised expenses (${itemised.join(", ")}); HMRC takes one or the other`);
162
+ }
163
+ if (contributions.length === 0) warnings.push("no posting on a UK property account falls in the period");
164
+
165
+ return {
166
+ periodDates: { periodStartDate, periodEndDate },
167
+ income: figures.income ?? {},
168
+ expenses: figures.expenses ?? {},
169
+ fieldSlots: PERIOD_FIELDS,
170
+ omitted: PERIOD_FIELDS.filter((field) => !present.includes(field)),
171
+ lines: contributions,
172
+ warnings,
173
+ };
174
+ }
175
+
176
+ function taxYearBounds(label) {
177
+ const match = TAX_YEAR_LABEL.exec(label ?? "");
178
+ if (!match) throw new Error(`taxYear must look like 2025-26, not "${label}"`);
179
+ const start = Number(match[1]);
180
+ if (String(start + 1).slice(-2) !== match[2]) throw new Error(`taxYear "${label}" does not name two consecutive years`);
181
+ return { start: `${start}-04-06`, end: `${start + 1}-04-05` };
182
+ }
183
+
184
+ // The tax year (6 April to 5 April) a day falls in.
185
+ function taxYearLabelOf(isoDay) {
186
+ const [year, month, day] = isoDay.split("-").map(Number);
187
+ const start = month > 4 || (month === 4 && day >= 6) ? year : year - 1;
188
+ return `${start}-${String(start + 1).slice(-2)}`;
189
+ }
190
+
191
+ /**
192
+ * The year's adjustments and allowances for HMRC's UK property annual
193
+ * submission, from a sole trader book whose chart names the accounts. The
194
+ * year defaults to the one the book's period end falls in.
195
+ * @param {Object} book - parsed book.toml
196
+ * @param {Array} lines - parsed lines.jsonl entries
197
+ * @param {{taxYear?: string}} [params] - taxYear as 2025-26
198
+ * @returns {Object} { taxYear, adjustments, allowances, fieldSlots, omitted, leftToPage, lines, warnings }
199
+ */
200
+ export function deriveUkPropertyAnnual(book, lines, { taxYear } = {}) {
201
+ requirePropertyBook(book, lines, "deriveUkPropertyAnnual");
202
+ const label =
203
+ taxYear ?? (book.documentInfo?.periodCoveredEnd ? taxYearLabelOf(String(book.documentInfo.periodCoveredEnd).slice(0, 10)) : undefined);
204
+ if (!label) throw new Error("the book declares no period end, so taxYear is required");
205
+ const { start, end } = taxYearBounds(label);
206
+
207
+ const { figures, contributions, present } = figuresBetween(book, lines, ANNUAL_FIELDS, start, end);
208
+ return {
209
+ taxYear: label,
210
+ adjustments: figures.adjustments ?? {},
211
+ allowances: figures.allowances ?? {},
212
+ fieldSlots: ANNUAL_FIELDS,
213
+ omitted: ANNUAL_FIELDS.filter((field) => !present.includes(field)),
214
+ leftToPage: ANNUAL_LEFT_TO_PAGE,
215
+ lines: contributions,
216
+ warnings: contributions.length === 0 ? ["no posting on a UK property account falls in the tax year"] : [],
217
+ };
218
+ }
@@ -9,13 +9,14 @@
9
9
  // writeDiyaGlZip/writeBookJson, so a download from one matches an export
10
10
  // from another byte-for-byte.
11
11
  //
12
- // Seven byte kinds, told apart by content, never by file extension: a
12
+ // Nine byte kinds, told apart by content, never by file extension: a
13
13
  // workbook, the zip a single-workbook package ships as, the zip a
14
14
  // multi-file package ships as, this module's own zip (book.toml +
15
- // lines.jsonl), that pair as one JSON file, the same JSON zipped, and the
16
- // legacy .xls this pipeline cannot read directly. A customer's upload can
17
- // be any of the first six under any name at all -- a renamed .xlsx is still
18
- // a workbook -- so the kind is decided by what the bytes actually contain.
15
+ // lines.jsonl), that pair as one JSON file, the same JSON zipped, an Inline
16
+ // XBRL accounts document, a plain XBRL instance, and the legacy .xls this
17
+ // pipeline cannot read directly. A customer's upload can be any of the first
18
+ // six under any name at all -- a renamed .xlsx is still a workbook -- so the
19
+ // kind is decided by what the bytes actually contain.
19
20
  //
20
21
  // Reading a workbook or its package zip runs the pipeline export.js has
21
22
  // always run: the anchor guard, then the extractors and book builder
@@ -45,6 +46,9 @@ import { canonicalBookToml, canonicalLinesJsonl, compareLines, orderedBookTopLev
45
46
  import { serializeReportDocument } from "./report-serializer.js";
46
47
  import { parseDiyaGlData } from "./diya-gl-loader.js";
47
48
  import { stampBook } from "./provenance.js";
49
+ import { sniffIxbrl } from "./ixbrl-reader.js";
50
+ import { mapIxbrlAccounts } from "./ixbrl-accounts.js";
51
+ import { buildIxbrlSummaryBooks } from "./ixbrl-summary-book.js";
48
52
  import * as bst from "../products/bst.js";
49
53
 
50
54
  export { AnchorError };
@@ -70,7 +74,7 @@ export class UnknownBookSourceError extends Error {
70
74
  constructor(name) {
71
75
  super(
72
76
  `${name ? `"${name}"` : "This file"} is not one of the kinds diya-gl reads: a Basic Sole Trader workbook (.xlsx), ` +
73
- `its package zip, a diya-gl zip, a diya-gl JSON file, or that JSON zipped.`,
77
+ `its package zip, a diya-gl zip, a diya-gl JSON file, that JSON zipped, or an iXBRL accounts file.`,
74
78
  );
75
79
  this.name = "UnknownBookSourceError";
76
80
  }
@@ -196,14 +200,14 @@ async function zipKind(bytes) {
196
200
  * error messages only.
197
201
  * @param {Uint8Array} bytes
198
202
  * @param {string} [name] - a hint for messages, not for detection
199
- * @returns {Promise<"workbook"|"package-zip"|"package-set"|"diya-gl-zip"|"json-zip"|"json"|"xls"|"unknown">}
203
+ * @returns {Promise<"workbook"|"package-zip"|"package-set"|"diya-gl-zip"|"json-zip"|"json"|"ixbrl"|"xbrl"|"xls"|"unknown">}
200
204
  */
201
205
  export async function detectBookSource(bytes, name) {
202
206
  void name; // content decides the kind; the name is a hint for a caller's own error messages, never for detection
203
207
  if (isZipContainer(bytes)) return zipKind(bytes);
204
208
  if (startsWithBytes(bytes, OLE_MAGIC)) return "xls";
205
209
  if (looksLikeJsonObject(bytes)) return "json";
206
- return "unknown";
210
+ return sniffIxbrl(bytes) ?? "unknown";
207
211
  }
208
212
 
209
213
  // ============================================================================
@@ -456,17 +460,37 @@ async function readJsonZipSource(bytes) {
456
460
  return { ...validated("json-zip", document.book, document.lines), product: document.product };
457
461
  }
458
462
 
463
+ // An accounts file stands for two Company books, the file's own year and the
464
+ // year before, each a summary with one line per tagged line. The comparative
465
+ // pair, the accounts figures and the facts no figure uses ride beside them.
466
+ async function readIxbrlSource(kind, bytes, deps) {
467
+ const accounts = mapIxbrlAccounts(bytes, deps.expect);
468
+ const summary = await buildIxbrlSummaryBooks(accounts, { resources: deps.resources });
469
+ const current = validated(kind, summary.book, summary.lines);
470
+ const prior = validated(kind, summary.comparative.book, summary.comparative.lines);
471
+ return {
472
+ ...current,
473
+ product: "ltd",
474
+ comparative: { book: prior.book, lines: prior.lines },
475
+ accounts,
476
+ unmapped: accounts.unmapped,
477
+ differences: summary.differences,
478
+ };
479
+ }
480
+
459
481
  /**
460
- * Turn a byte array into D: the book and its lines, whichever of the six
461
- * kinds it sniffs as. The CLI's --file mode, the MCP server's extract_book
482
+ * Turn a byte array into D: the book and its lines, whichever of the kinds
483
+ * it sniffs as. The CLI's --file mode, the MCP server's extract_book
462
484
  * tool and the books page all call this, so a file that loads in one loads
463
485
  * in all.
464
486
  * @param {Uint8Array} bytes
465
487
  * @param {string} [name] - a hint for error messages
466
488
  * @param {Object} [deps] - {products}: product id to product module
467
489
  * (CELL_MAP, cellLabels()) for the products this caller can read;
468
- * defaults to app/products/bst.js alone
469
- * @returns {Promise<{kind: string, product?: string, book: Object, lines: Array, overtyped?: Object, workbookSet?: Object}>}
490
+ * defaults to app/products/bst.js alone. {expect, resources} serve an
491
+ * iXBRL accounts file: what the page expects of the file (company number,
492
+ * period, regime), and the loader of the tax year's data.
493
+ * @returns {Promise<{kind: string, product?: string, book: Object, lines: Array, overtyped?: Object, workbookSet?: Object, comparative?: {book: Object, lines: Array}, accounts?: Object, unmapped?: Array, differences?: string[]}>}
470
494
  */
471
495
  export async function readBookSource(bytes, name, deps = {}) {
472
496
  const kind = await detectBookSource(bytes, name);
@@ -481,6 +505,9 @@ export async function readBookSource(bytes, name, deps = {}) {
481
505
  return readJsonSource(bytes);
482
506
  case "json-zip":
483
507
  return readJsonZipSource(bytes);
508
+ case "ixbrl":
509
+ case "xbrl":
510
+ return readIxbrlSource(kind, bytes, deps);
484
511
  case "xls":
485
512
  throw new XlsBookSourceError(name);
486
513
  default:
@@ -607,6 +607,7 @@ export function diyaGlToScenario(book, lines, product) {
607
607
  }
608
608
  const opening = { category, description: asset.description, cost: asset.cost, acc_dep: asset.accumulatedDepreciation };
609
609
  if (asset.taxWrittenDownValue !== undefined) opening.tax_wdv = asset.taxWrittenDownValue;
610
+ if (asset.depreciationRate !== undefined) opening.depreciation_rate = asset.depreciationRate;
610
611
  if (asset.capitalAllowancePool === "special") opening.pool = "special";
611
612
  // A single asset pool is a fact the book states, never one the loader
612
613
  // infers: private use puts an asset in its own pool (CAA 2001 s.206),