@diy-accounting-uk/diya-gl 1.2.46 → 1.2.47

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
@@ -38,12 +38,15 @@ diya-gl recalc --package bst --data my-book --years se-2025-2026 --output-dir ou
38
38
  diya-gl read-workbook --file my-workbook.xlsx --output-dir out
39
39
  diya-gl write-workbook --data my-book --output-dir out
40
40
  diya-gl link my-book
41
+ diya-gl new-book --product se --name "Lark Lane" --year-end 2027-04-05 --output-dir lark-lane
42
+ diya-gl roll-forward --data my-book --output-dir next-year
41
43
  diya-gl view my-book
42
44
  diya-gl mcp
43
45
  ```
44
46
 
45
47
  Each subcommand is also its own command, if you only want one on your `PATH`:
46
- `diya-gl-recalc`, `diya-gl-read-workbook`, `diya-gl-write-workbook`, `diya-gl-link`, `diya-gl-view`, `diya-gl-mcp`.
48
+ `diya-gl-recalc`, `diya-gl-read-workbook`, `diya-gl-write-workbook`, `diya-gl-link`, `diya-gl-new-book`,
49
+ `diya-gl-roll-forward`, `diya-gl-view`, `diya-gl-mcp`.
47
50
 
48
51
  - **recalc** takes a diya-gl book (`--data`) or a populated workbook (`--source-dir`,
49
52
  `--mode saved|recalculate`) and writes `report.json`: every figure, report section and
@@ -58,6 +61,22 @@ Each subcommand is also its own command, if you only want one on your `PATH`:
58
61
  opens it, the whole book carried in the fragment so nothing is uploaded. `--base <url>`
59
62
  replaces the origin. The fragment length goes to stderr, with a warning past 64 KB (about
60
63
  2,000 lines), where chat, email and QR codes start to cut links off.
64
+ - **new-book** starts an empty book for a new business: `--product` (`bst`, `se`, `taxi` or
65
+ `ltd`), `--name` and `--year-end` (YYYY-MM-DD), plus `--vat` for a VAT-registered Self
66
+ Employed or Company book. It writes `book.toml` (the product's starting chart of accounts,
67
+ the twelve months to the year end and that tax year's rates), an empty `lines.jsonl`,
68
+ `report.json` and `bookchecks.json` into `--output-dir`, the current directory by default.
69
+ It answers: how do I start books for a new business, what accounts does a new sole trader
70
+ or company book start with, and which tax rates apply to the first year?
71
+ - **roll-forward** takes a book (`--data`, the same sources as **link**) and writes the next
72
+ year's into `--output-dir`: the year end a year on with that year's tax rates, the closing
73
+ balances as opening ones, and lines holding only the opening entries (a Company's opening
74
+ journal and one brought-forward line per bank account; a Self Employed book's bank lines).
75
+ Beside `book.toml`, `lines.jsonl`, `report.json` and `bookchecks.json` it writes
76
+ `rollchecks.json`, each opening figure the new year prints against the closing figure the old
77
+ year printed, and exits 1 when one disagrees. It answers: how do I start next year's books,
78
+ what do I bring forward from last year, and does my opening balance sheet agree with last
79
+ year's closing one?
61
80
  - **view** takes the same book sources as **link**, serves the full diya-gl pages on
62
81
  `127.0.0.1` (`--port <number>`, default a free port) and opens the book in your default
63
82
  browser (`--no-open` prints the address only). The pages are not in this package: the local
@@ -67,8 +86,9 @@ Each subcommand is also its own command, if you only want one on your `PATH`:
67
86
  another site. The book stays in the browser (the part of the address after `#` is never sent
68
87
  anywhere). The server stays up until Ctrl-C. The workbook templates come from
69
88
  spreadsheets.diyaccounting.co.uk the first time a page action needs one.
70
- - **mcp** runs a stdio [MCP](https://modelcontextprotocol.io) server with eight tools:
71
- `extract_book`, `report`, `edit_lines`, `save_workbook`, and four that read every detail of
89
+ - **mcp** runs a stdio [MCP](https://modelcontextprotocol.io) server with ten tools:
90
+ `extract_book`, `new_book` (the empty book **new-book** writes), `roll_forward` (the next
91
+ year **roll-forward** writes), `report`, `edit_lines`, `save_workbook`, and four that read every detail of
72
92
  the loaded book: `lines` (filter and group every transaction, e.g. best customer or spend on
73
93
  fuel in June), `chart` (the accounts and the report rows each feeds), `book` (the business,
74
94
  period, tax settings and registers) and `checks` (every book check with the entryNumbers
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Copyright (C) 2006-2026 DIY Accounting Limited
4
+ //
5
+ // diya-gl-new-book.js — start an empty book for a new business.
6
+
7
+ import { runDistBin } from "./run-dist-bin.js";
8
+ runDistBin("app/bin/new-book.js", process.argv.slice(2));
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Copyright (C) 2006-2026 DIY Accounting Limited
4
+ //
5
+ // diya-gl-roll-forward.js — start the next year of a book.
6
+
7
+ import { runDistBin } from "./run-dist-bin.js";
8
+ runDistBin("app/bin/roll-forward.js", process.argv.slice(2));
package/bin/diya-gl.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  // Copyright (C) 2006-2026 DIY Accounting Limited
4
4
  //
5
- // diya-gl.js — the dispatcher: diya-gl <recalc|read-workbook|write-workbook|link|view|mcp> [args].
5
+ // diya-gl.js — the dispatcher: diya-gl <recalc|read-workbook|write-workbook|link|new-book|roll-forward|view|mcp> [args].
6
6
  // Each subcommand is also its own bin (diya-gl-recalc, and so on), for a
7
7
  // caller that wants one command on its PATH without the others.
8
8
 
@@ -17,6 +17,8 @@ const SUBCOMMANDS = {
17
17
  "read-workbook": "app/bin/export.js",
18
18
  "write-workbook": "app/bin/write-workbook.js",
19
19
  "link": "app/bin/link.js",
20
+ "new-book": "app/bin/new-book.js",
21
+ "roll-forward": "app/bin/roll-forward.js",
20
22
  "view": "app/bin/view.js",
21
23
  "mcp": "app/bin/diya-gl-mcp.js",
22
24
  };
@@ -253,6 +253,21 @@ function writeDiyaGlData(outputDir, book, lines) {
253
253
  console.log(` book.toml: ${Object.keys(stamped).length} tables`);
254
254
  }
255
255
 
256
+ /**
257
+ * A whole book directory from a (book, lines) pair already in hand: book.toml,
258
+ * lines.jsonl, report.json and bookchecks.json, written the way --file writes
259
+ * them. The new-book and roll-forward commands write their output through it.
260
+ * @param {string} outputDir
261
+ * @param {Object} book
262
+ * @param {Array} lines
263
+ */
264
+ export async function writeBookDirectory(outputDir, book, lines) {
265
+ const product = productOf(book);
266
+ writeDiyaGlData(outputDir, book, lines);
267
+ writeReportJson(outputDir, buildFileReportDocument(book, lines, product, PRODUCTS[product]));
268
+ await writeBookChecksJson(outputDir, book, lines);
269
+ }
270
+
256
271
  async function runFileMode(filePath, outputDirArg, packageName) {
257
272
  const resolvedFile = resolve(filePath);
258
273
  const resolvedOutput = resolve(outputDirArg || dirname(resolvedFile));
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Copyright (C) 2006-2026 DIY Accounting Limited
4
+ //
5
+ // new-book.js — start an empty book for a new business: the product's
6
+ // starting chart of accounts, the twelve months to the year end and that tax
7
+ // year's rates, with no lines yet.
8
+ //
9
+ // Usage:
10
+ // node app/bin/new-book.js --product se --name "Lark Lane" --year-end 2027-04-05
11
+ // node app/bin/new-book.js --product ltd --name "Lark Lane Ltd" --year-end 2027-03-31 --vat --output-dir lark-lane
12
+ //
13
+ // Writes book.toml, lines.jsonl, report.json and bookchecks.json into
14
+ // --output-dir, the current directory when none is given. It runs the MCP
15
+ // server's new_book tool, so the command and the tool build the same book.
16
+
17
+ import { resolve } from "path";
18
+ import { fileURLToPath } from "url";
19
+ import { TOOLS, createSession } from "../lib/mcp/diya-gl-tools.js";
20
+ import { writeBookDirectory } from "./export.js";
21
+
22
+ const USAGE = [
23
+ "Usage: diya-gl new-book --product <bst|se|taxi|ltd> --name <business> --year-end <YYYY-MM-DD> [--vat] [--output-dir <dir>]",
24
+ "Answers: How do I start books for a new business? What accounts does a new sole trader or company book start with? Which tax rates apply to my first year?",
25
+ ].join("\n");
26
+
27
+ function parseArgs(argv) {
28
+ const args = argv.slice(2);
29
+ const options = { vatRegistered: false, outputDir: "." };
30
+ const valueOf = (flag, index) => {
31
+ const value = args[index + 1];
32
+ if (value === undefined || value.startsWith("--")) throw new Error(`${flag} needs a value. ${USAGE}`);
33
+ return value;
34
+ };
35
+ for (let i = 0; i < args.length; i++) {
36
+ const flag = args[i];
37
+ if (flag === "--product") options.product = valueOf(flag, i++);
38
+ else if (flag === "--name") options.businessName = valueOf(flag, i++);
39
+ else if (flag === "--year-end") options.yearEnd = valueOf(flag, i++);
40
+ else if (flag === "--output-dir") options.outputDir = valueOf(flag, i++);
41
+ else if (flag === "--vat") options.vatRegistered = true;
42
+ else throw new Error(`Unknown argument ${flag}. ${USAGE}`);
43
+ }
44
+ if (!options.product || !options.businessName || !options.yearEnd) throw new Error(USAGE);
45
+ return options;
46
+ }
47
+
48
+ async function main() {
49
+ const { outputDir, ...params } = parseArgs(process.argv);
50
+ const { book, lines, bookChecks } = await TOOLS.new_book.handler(createSession(), params);
51
+ const resolvedOutput = resolve(outputDir);
52
+ await writeBookDirectory(resolvedOutput, book, lines);
53
+ const { pass, warn, fail } = bookChecks.summary;
54
+ console.log(
55
+ `New ${params.product} book for ${book.entityInformation.organizationIdentifier}, ${book.documentInfo.periodCoveredStart} to ${book.documentInfo.periodCoveredEnd}, in ${resolvedOutput}: ${pass} checks pass, ${warn} warn, ${fail} fail`,
56
+ );
57
+ }
58
+
59
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
60
+ main().catch((error) => {
61
+ console.error(error.message);
62
+ process.exit(1);
63
+ });
64
+ }
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ // Copyright (C) 2006-2026 DIY Accounting Limited
4
+ //
5
+ // roll-forward.js — start the next year of a book: the year end moves on a
6
+ // year, the closing balances become the opening ones, and the lines hold only
7
+ // the opening entries.
8
+ //
9
+ // Usage:
10
+ // node app/bin/roll-forward.js --data examples/brickwork-pro/ltd-nonvat --output-dir next-year
11
+ // node app/bin/roll-forward.js --data my-book-diya-gl.zip --output-dir next-year
12
+ //
13
+ // --data is a directory holding book.toml and lines.jsonl, or any diya-gl zip
14
+ // or JSON file diya-gl-interchange.js reads. Writes book.toml, lines.jsonl,
15
+ // report.json, bookchecks.json and rollchecks.json (each opening figure the
16
+ // new year prints against the closing figure the old year printed for it)
17
+ // into --output-dir. It runs the MCP server's roll_forward tool, so the
18
+ // command and the tool roll the same book. Exits 1 when a roll check fails.
19
+
20
+ import { existsSync, readFileSync, statSync, writeFileSync } from "fs";
21
+ import { join, resolve } from "path";
22
+ import { fileURLToPath } from "url";
23
+ import { parseDiyaGlData } from "../lib/diya-gl-loader.js";
24
+ import { readBookSource } from "../lib/diya-gl-interchange.js";
25
+ import { PRODUCTS } from "../lib/products.js";
26
+ import { TOOLS, createSession } from "../lib/mcp/diya-gl-tools.js";
27
+ import { writeBookDirectory } from "./export.js";
28
+
29
+ const USAGE = [
30
+ "Usage: diya-gl roll-forward --data <book directory | diya-gl zip | diya-gl JSON file> --output-dir <dir>",
31
+ "Answers: How do I start next year's books? What do I bring forward from last year? Does my opening balance sheet agree with last year's closing one?",
32
+ ].join("\n");
33
+
34
+ function parseArgs(argv) {
35
+ const args = argv.slice(2);
36
+ const options = {};
37
+ for (let i = 0; i < args.length; i++) {
38
+ const flag = args[i];
39
+ const value = args[i + 1];
40
+ if (flag !== "--data" && flag !== "--output-dir") throw new Error(`Unknown argument ${flag}. ${USAGE}`);
41
+ if (value === undefined || value.startsWith("--")) throw new Error(`${flag} needs a value. ${USAGE}`);
42
+ options[flag === "--data" ? "data" : "outputDir"] = value;
43
+ i++;
44
+ }
45
+ if (!options.data || !options.outputDir) throw new Error(USAGE);
46
+ return options;
47
+ }
48
+
49
+ async function readBook(source) {
50
+ const path = resolve(source);
51
+ if (!existsSync(path)) throw new Error(`${source} does not exist.`);
52
+ if (statSync(path).isDirectory()) {
53
+ return parseDiyaGlData(readFileSync(join(path, "book.toml"), "utf8"), readFileSync(join(path, "lines.jsonl"), "utf8"));
54
+ }
55
+ const { book, lines } = await readBookSource(readFileSync(path), source, { products: PRODUCTS });
56
+ return { book, lines };
57
+ }
58
+
59
+ async function main() {
60
+ const { data, outputDir } = parseArgs(process.argv);
61
+ const prior = await readBook(data);
62
+ const { book, lines, bookChecks, rollChecks } = await TOOLS.roll_forward.handler(createSession(), prior);
63
+ const resolvedOutput = resolve(outputDir);
64
+ await writeBookDirectory(resolvedOutput, book, lines);
65
+ writeFileSync(join(resolvedOutput, "rollchecks.json"), `${JSON.stringify(rollChecks, null, 2)}\n`);
66
+ const failing = rollChecks.filter((check) => check.result === "fail");
67
+ console.log(
68
+ `Rolled forward to ${book.documentInfo.periodCoveredStart} to ${book.documentInfo.periodCoveredEnd} in ${resolvedOutput}: ${lines.length} opening lines, ${bookChecks.summary.fail} book checks failing, ${failing.length} of ${rollChecks.length} roll checks failing`,
69
+ );
70
+ for (const check of failing) console.error(` ${check.key}: expected ${check.expected}, the new year opens on ${check.actual}`);
71
+ if (failing.length > 0) process.exitCode = 1;
72
+ }
73
+
74
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
75
+ main().catch((error) => {
76
+ console.error(error.message);
77
+ process.exit(1);
78
+ });
79
+ }
@@ -167,7 +167,7 @@ section = 1
167
167
  part = 2
168
168
  partName = "Section 1 Part 2: Accounting Non-Taxable Income Deducted"
169
169
  label = "Non-trading loan relationship credit"
170
- xbrl = "AdjustmentsNon-tradingLoanRelationshipsCreditsPerAccounts"
170
+ xbrl = "AdjustmentsNon-tradingLoanRelationshipCreditsPerAccounts"
171
171
  sheetCell = "Financialaccounts.xlsx!CorporationTax!K24"
172
172
  note = "Bank interest: the plan's Ltd view moves this to CT600 box 170 rather than adding it back in the trading computation."
173
173
 
@@ -432,7 +432,7 @@ section = 1
432
432
  part = 3
433
433
  partName = "Section 1 Part 3: Accounting Disallowable Expenditure Addback"
434
434
  label = "Non-trading loan relationship debit"
435
- xbrl = "AdjustmentsNon-tradingLoanRelationshipsDebitsPerAccounts"
435
+ xbrl = "AdjustmentsNon-tradingLoanRelationshipDebitsPerAccounts"
436
436
 
437
437
  [[line]]
438
438
  section = 1
@@ -1283,6 +1283,8 @@ label = "Tax already paid (and not already repaid)"
1283
1283
  section = "Tax reconciliation"
1284
1284
  format = "pounds-and-pence"
1285
1285
  element = "TaxAlreadyPaid"
1286
+ status = "entry-on-this-template"
1287
+ statusNote = "CT600!AJ163 is an entry cell with no formula: the company types what it has paid, and no book figure feeds it"
1286
1288
  xmlPath = "/IRenvelope/CompanyTaxReturn/TaxReconciliation/TaxAlreadyPaid"
1287
1289
  sheetCell = "Financialaccounts.xlsx!CT600!AJ163"
1288
1290
 
@@ -1429,6 +1429,32 @@
1429
1429
  "scorecard": "101605 passed, 0 warnings, 0 failed"
1430
1430
  }
1431
1431
  }
1432
+ },
1433
+ {
1434
+ "tag": "diya-gl-v1.2.46",
1435
+ "date": "2026-10-05",
1436
+ "formatVersion": "diya-gl/1",
1437
+ "engineVersion": "1.2.46+f38ebcb4",
1438
+ "taxDataHash": "89424bd36c0d",
1439
+ "reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
1440
+ "templates": {
1441
+ "bst": {
1442
+ "hash": "0d003e10e00c",
1443
+ "scorecard": "812 passed, 0 warnings, 0 failed"
1444
+ },
1445
+ "taxi": {
1446
+ "hash": "1ff69654d64d",
1447
+ "scorecard": "700 passed, 0 warnings, 0 failed"
1448
+ },
1449
+ "se": {
1450
+ "hash": "064605ae76ed",
1451
+ "scorecard": "8015 passed, 0 warnings, 0 failed"
1452
+ },
1453
+ "ltd": {
1454
+ "hash": "ab3dc062e15f",
1455
+ "scorecard": "101608 passed, 0 warnings, 0 failed"
1456
+ }
1457
+ }
1432
1458
  }
1433
1459
  ]
1434
1460
  }
@@ -0,0 +1,57 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Copyright (C) 2006-2026 DIY Accounting Limited
3
+ # UK self-employment tax rates and thresholds for the 2027-28 tax year
4
+ # Period: 6 April 2027 – 5 April 2028
5
+ # Source: HMRC. Class 2 weekly rate and small profits threshold carried forward from 2026-27 (not yet published)
6
+
7
+ [tax_year]
8
+ label = "2027-28"
9
+ next_label = "2028-29"
10
+ start = 2027-04-06
11
+ end = 2028-04-05
12
+
13
+ [income_tax]
14
+ personal_allowance = 12570
15
+ starting_rate = 0.00
16
+ basic_rate = 0.20
17
+ higher_rate = 0.40
18
+ starter_band_end = 0
19
+ basic_band_end = 37700
20
+ higher_band_start = 37701
21
+ higher_band_end = 125140
22
+ additional_rate = 0.45
23
+ personal_allowance_taper_threshold = 100000
24
+
25
+ [national_insurance]
26
+ class2_rate = 3.65
27
+ class4_lower_rate = 0.06
28
+ class4_lower_limit = 12570
29
+ class4_upper_rate = 0.02
30
+ class4_upper_limit = 50270
31
+ class2_weekly_rate = 3.65
32
+ class2_small_profits_threshold = 7105
33
+
34
+ [capital_allowances]
35
+ annual_investment_allowance = 1.00
36
+ writing_down_allowance = 0.14
37
+ writing_down_allowance_special = 0.06
38
+ structures_and_buildings_allowance = 0.03
39
+ structures_and_buildings_allowance_enhanced = 0.10
40
+
41
+ [depreciation]
42
+ land_and_property = 0.00
43
+ plant_and_machinery = 0.10
44
+ fixtures_and_fittings = 0.20
45
+ computer_equipment = 0.33
46
+ motor_vehicles = 0.25
47
+
48
+ # Approved mileage rates: gov.uk "Travel - mileage and fuel rates and allowances", cars and vans, tax year 2026 to 2027, unchanged for 2027-28: 55p first 10,000 miles, 25p after.
49
+ [mileage]
50
+ higher_rate_limit = 10000
51
+ higher_rate_pence = 0.55
52
+ lower_rate_start = 10001
53
+ lower_rate_pence = 0.25
54
+
55
+ [vat]
56
+ registration_threshold = 90000
57
+ standard_rate = 0.20
@@ -13,7 +13,7 @@ import { calculateIncomeTax } from "../tax/income-tax.js";
13
13
  import { calculateNIClass4 } from "../tax/national-insurance.js";
14
14
  import { calculateMileageAllowance, scenarioBusinessMiles } from "../tax/mileage.js";
15
15
  import { aggregateByCode } from "./shared.js";
16
- import { attributionWriter, entriesOf } from "../entry-attribution.js";
16
+ import { attributionWriter, entriesOf, entryOf } from "../entry-attribution.js";
17
17
 
18
18
  const BST_MONTH_COLS = {
19
19
  apr: "D",
@@ -87,6 +87,22 @@ function outstandingTotal(transactions) {
87
87
  * @param {Object} [attribution] - filled in place with the entryNumbers behind each cell (entry-attribution.js)
88
88
  * @returns {Object} { "SheetName": { "CellRef": value } }
89
89
  */
90
+ /**
91
+ * Each fixed asset bought in the year beside the tax written-down value the
92
+ * Fixed Assets sheet carries forward for it: the pool a book rolled forward
93
+ * brings into its next year.
94
+ * @param {Object} scenario - diyaGlToScenario's output
95
+ * @param {Object} taxData
96
+ * @returns {Array<{entryNumber: string, cost: number, writtenDownValue: number}>}
97
+ */
98
+ export function bstAdditionsWrittenDown(scenario, taxData) {
99
+ return fixedAssetAdditions(scenario, "f").map((asset) => ({
100
+ entryNumber: entryOf(asset),
101
+ cost: asset.cost,
102
+ writtenDownValue: calculateFixedAssetSchedule([asset], taxData).M1,
103
+ }));
104
+ }
105
+
90
106
  export function calculateBstResults(book, lines, taxData, scenario, attribution) {
91
107
  // A book's own declared chart can number its purchase accounts under a
92
108
  // scheme other than the Basic Sole Trader master's own (see
@@ -508,6 +508,7 @@ function buildSchedule(scenario, rate, depreciationRates, investmentAllowancePer
508
508
  assetClass: asset.category,
509
509
  row,
510
510
  acquiredInYear: false,
511
+ openingAsset: asset,
511
512
  cost: asset.cost,
512
513
  depreciationBroughtForward: asset.acc_dep || 0,
513
514
  taxWrittenDownValue: asset.tax_wdv || 0,
@@ -525,6 +526,7 @@ function buildSchedule(scenario, rate, depreciationRates, investmentAllowancePer
525
526
  assetClass: SCHEDULE_NEW_ASSET_CLASS,
526
527
  row,
527
528
  acquiredInYear: true,
529
+ entryNumber: entryOf(transaction),
528
530
  purchasedOn: serialOf(parseDate(transaction.date)),
529
531
  description: transaction.supplier,
530
532
  cost: writerNet(transaction.amount, rate),
@@ -767,13 +769,7 @@ function computeLtd(book, lines, taxData, scenario, attribution) {
767
769
  results[`${fileName}!${tabs[11]}`] = { A1: openingOfLastMonth, A2: balance };
768
770
  }
769
771
 
770
- const depreciationRates = {
771
- land: taxData.depreciation?.land_and_property ?? 0,
772
- plant: taxData.depreciation?.plant_and_machinery ?? 0,
773
- fixtures: taxData.depreciation?.fixtures_and_fittings ?? 0,
774
- computer: taxData.depreciation?.computer_equipment ?? 0,
775
- motor: taxData.depreciation?.motor_vehicles ?? 0,
776
- };
772
+ const depreciationRates = depreciationRatesOf(taxData);
777
773
  const openingBalance = scenario.opening_balance || {};
778
774
  const scheduleRowEntries = attribution ? new Map() : null;
779
775
  const scheduleRows = buildSchedule(scenario, rate, depreciationRates, admin.G5, admin.G6, scheduleRowEntries);
@@ -961,6 +957,33 @@ function computeLtd(book, lines, taxData, scenario, attribution) {
961
957
  * @param {Object} [attribution] - filled in place with the entryNumbers behind each cell (entry-attribution.js)
962
958
  * @returns {Object} { "SheetName": { "CellRef": value } }
963
959
  */
960
+ function depreciationRatesOf(taxData) {
961
+ return {
962
+ land: taxData.depreciation?.land_and_property ?? 0,
963
+ plant: taxData.depreciation?.plant_and_machinery ?? 0,
964
+ fixtures: taxData.depreciation?.fixtures_and_fittings ?? 0,
965
+ computer: taxData.depreciation?.computer_equipment ?? 0,
966
+ motor: taxData.depreciation?.motor_vehicles ?? 0,
967
+ };
968
+ }
969
+
970
+ /**
971
+ * Each Schedule row's own figures, beside the asset it holds: an asset
972
+ * brought forward (openingAsset, one of scenario.opening_fixed_assets) or the
973
+ * purchase line that bought it in the year (entryNumber). Rolling a book
974
+ * forward reads its carried forward cost, depreciation and tax written-down
975
+ * value from here.
976
+ * @param {Object} book
977
+ * @param {Object} taxData
978
+ * @param {Object} scenario - diyaGlToScenario's output
979
+ * @returns {Array<Object>} the rows buildSchedule computes
980
+ */
981
+ export function ltdScheduleRows(book, taxData, scenario) {
982
+ const rate = scenario?.metadata?.vat_registered === false ? 0 : VAT_RATE;
983
+ const admin = buildAdmin(taxData, periodFrom(book), scenario.business?.associated_companies ?? 0);
984
+ return buildSchedule(scenario, rate, depreciationRatesOf(taxData), admin.G5, admin.G6);
985
+ }
986
+
964
987
  export function calculateLtdResults(book, lines, taxData, scenario, attribution) {
965
988
  return computeLtd(book, lines, taxData, scenario, attribution).results;
966
989
  }
@@ -1996,8 +2019,11 @@ function buildCt600(corporationTax, pl, admin) {
1996
2019
  if (corporationTax.K26 > 0) sheet.Z72 = corporationTax.K26;
1997
2020
  if (corporationTax.K24 > 0) sheet.AJ76 = corporationTax.K24;
1998
2021
  sheet.AJ74 = (sheet.Z70 || 0) - (sheet.Z72 || 0);
1999
- sheet.AJ92 = sheet.AJ74 > 0 ? sheet.AJ74 + (sheet.AJ76 || 0) : 0;
2000
- sheet.AJ110 = sheet.AJ92;
2022
+ sheet.AJ92 = Math.max(sheet.AJ74, 0) + (sheet.AJ76 || 0);
2023
+ // A trading loss of the year meets the interest it can reach, as the working
2024
+ // sheet's chargeable profit nets them, so box 30 carries what was set off.
2025
+ if (corporationTax.K22 < 0 && corporationTax.K24 > 0) sheet.Z98 = Math.min(-corporationTax.K22, corporationTax.K24);
2026
+ sheet.AJ110 = sheet.AJ92 - (sheet.Z98 || 0);
2001
2027
  sheet.AJ131 = sheet.AJ126 + sheet.AJ128;
2002
2028
  sheet.Y133 = corporationTax.marginalRelief;
2003
2029
  sheet.Y135 = corporationTax.K35;
@@ -2511,7 +2537,8 @@ function attributeLtdResults(attribution, context) {
2511
2537
  a.set(CT600, "AJ76", a.get(CT, "K24"));
2512
2538
  a.set(CT600, "AJ74", a.cells(CT600, "Z70", "Z72"));
2513
2539
  a.set(CT600, "AJ92", a.cells(CT600, "AJ74", "AJ76"));
2514
- a.set(CT600, "AJ110", a.get(CT600, "AJ92"));
2540
+ a.set(CT600, "Z98", unionOf(a.get(CT, "K22"), a.get(CT, "K24")));
2541
+ a.set(CT600, "AJ110", a.cells(CT600, "AJ92", "Z98"));
2515
2542
  a.set(CT600, "AJ131", a.cells(CT600, "AJ126", "AJ128"));
2516
2543
  a.set(CT600, "Y133", charged);
2517
2544
  a.set(CT600, "Y135", a.get(CT, "K35"));
@@ -660,6 +660,7 @@ function buildSchedule(scenario, taxData, rate, entries) {
660
660
  });
661
661
 
662
662
  const existingRows = [];
663
+ const existingAssets = [];
663
664
  const existingRowEntries = [];
664
665
  // The motor rows' own single asset pool cells, keyed by cell reference:
665
666
  // the private use share the writer puts in M, the marker in AD, and the
@@ -681,6 +682,7 @@ function buildSchedule(scenario, taxData, rate, entries) {
681
682
  disposal: disposalByAsset.get(asset),
682
683
  });
683
684
  existingRows.push(row);
685
+ existingAssets.push(asset);
684
686
  existingRowEntries.push({ asset: new Set(), disposal: disposalEntries.get(disposalByAsset.get(asset)) ?? new Set() });
685
687
  if (category === "motor") {
686
688
  const rowNumber = block.rows[index];
@@ -706,7 +708,24 @@ function buildSchedule(scenario, taxData, rate, entries) {
706
708
  SCHEDULE_TOTAL_COLUMNS.map((column) => [column, unionOf(entries.existing[column], entries.additions[column])]),
707
709
  );
708
710
  }
709
- return { existing, additions, totals: addScheduleTotals(existing, additions), motorRowCells };
711
+ const register = [
712
+ ...existingRows.map((row, index) => ({ openingAsset: existingAssets[index], row })),
713
+ ...newRows.map((row, index) => ({ entryNumber: entryOf(capitalPurchases[index]), row })),
714
+ ];
715
+ return { existing, additions, totals: addScheduleTotals(existing, additions), motorRowCells, register };
716
+ }
717
+
718
+ /**
719
+ * Each Schedule row's own figures, beside the asset it holds: an asset
720
+ * brought forward (one of scenario.opening_fixed_assets) or the purchase line
721
+ * that bought it in the year. Rolling a book forward reads its carried
722
+ * forward cost, depreciation and tax written-down value from here.
723
+ * @param {Object} scenario - diyaGlToScenario's output
724
+ * @param {Object} taxData
725
+ * @returns {Array<{openingAsset?: Object, entryNumber?: string, row: Object}>}
726
+ */
727
+ export function seScheduleRegister(scenario, taxData) {
728
+ return buildSchedule(scenario, taxData, vatRateFor(scenario)).register;
710
729
  }
711
730
 
712
731
  // ── Structures and Buildings Allowance (SA103F boxes 53 and 53.1) ──────────
@@ -107,6 +107,22 @@ function aggregateByCodeAndMonth(lines, codeMap, entriesByMonth) {
107
107
  * @param {Object} [attribution] - filled in place with the entryNumbers behind each cell (entry-attribution.js)
108
108
  * @returns {Object} { "SheetName": { "CellRef": value } }
109
109
  */
110
+ /**
111
+ * Each fixed asset bought in the year beside the tax written-down value the
112
+ * Fixed Assets sheet carries forward for it: the pool a book rolled forward
113
+ * brings into its next year.
114
+ * @param {Object} scenario - diyaGlToScenario's output
115
+ * @param {Object} taxData
116
+ * @returns {Array<{entryNumber: string, cost: number, writtenDownValue: number}>}
117
+ */
118
+ export function taxiAdditionsWrittenDown(scenario, taxData) {
119
+ return fixedAssetAdditions(scenario, "f").map((asset) => ({
120
+ entryNumber: entryOf(asset),
121
+ cost: asset.cost,
122
+ writtenDownValue: calculateFixedAssetSchedule([asset], taxData).K1,
123
+ }));
124
+ }
125
+
110
126
  export function calculateTaxiResults(book, lines, taxData, scenario, attribution) {
111
127
  const salesLines = lines.filter((l) => l.sourceJournalID === "sales" && String(l.accountMainID) === TAXI_SALES_ACCOUNT);
112
128
  // A 4001 line is other business income, never a fare: the P&L keeps it off
@@ -98,6 +98,21 @@ function filterBstChart(lines, purchaseCodeMap) {
98
98
  });
99
99
  }
100
100
 
101
+ // Each scenario.opening_fixed_assets entry against the book.fixedAssets entry
102
+ // it was read from, kept beside the scenario rather than on it so the
103
+ // scenario's own shape stays what the writer and the fixtures expect.
104
+ const BOOK_ASSET_OF = new WeakMap();
105
+
106
+ /**
107
+ * The book.fixedAssets entry a scenario.opening_fixed_assets entry was read
108
+ * from, or undefined for one diyaGlToScenario did not build.
109
+ * @param {Object} opening - one of scenario.opening_fixed_assets
110
+ * @returns {Object|undefined}
111
+ */
112
+ export function bookAssetOf(opening) {
113
+ return BOOK_ASSET_OF.get(opening);
114
+ }
115
+
101
116
  // The Fixed Assets Schedule's asset-class blocks, keyed by the book's own
102
117
  // class enum. land and plant/fixtures only exist on the Ltd Schedule; SE's
103
118
  // Schedule carries motor and computer alone and throws on the other three,
@@ -617,6 +632,7 @@ export function diyaGlToScenario(book, lines, product) {
617
632
  opening.single_asset_pool = true;
618
633
  if (asset.privateUseProportion > 0) opening.private_use = asset.privateUseProportion;
619
634
  }
635
+ BOOK_ASSET_OF.set(opening, asset);
620
636
  return opening;
621
637
  });
622
638
  if (openingFixedAssets.length > 0) scenario.opening_fixed_assets = openingFixedAssets;