@diy-accounting-uk/diya-gl 1.2.46 → 1.2.48
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 +27 -6
- package/bin/diya-gl-new-book.js +8 -0
- package/bin/diya-gl-roll-forward.js +8 -0
- package/bin/diya-gl.js +3 -1
- package/dist/app/bin/export.js +15 -0
- package/dist/app/bin/new-book.js +64 -0
- package/dist/app/bin/roll-forward.js +79 -0
- package/dist/app/bin/view.js +3 -17
- package/dist/app/data/filing/ct-computation-v1.1.toml +2 -2
- package/dist/app/data/filing/ct600-v3.toml +2 -0
- package/dist/app/data/releases.json +52 -0
- package/dist/app/data/se-2027-2028.toml +57 -0
- package/dist/app/lib/app-resources.js +9 -3
- package/dist/app/lib/calculators/bst.js +17 -1
- package/dist/app/lib/calculators/ltd.js +37 -10
- package/dist/app/lib/calculators/se.js +20 -1
- package/dist/app/lib/calculators/taxi.js +16 -0
- package/dist/app/lib/diya-gl-loader.js +16 -0
- package/dist/app/lib/diya-gl-new-book.js +194 -0
- package/dist/app/lib/diya-gl-roll-forward.js +687 -0
- package/dist/app/lib/mcp/diya-gl-tools.js +92 -2
- package/dist/app/lib/mcp/server.js +4 -2
- package/dist/app/lib/package-version.js +28 -0
- package/dist/app/lib/provenance-data.js +3 -3
- package/dist/app/lib/scenario-loader.js +7 -1
- package/dist/app/lib/xlsx-exporter.js +39 -5
- package/dist/app/products/ltd.js +4 -1
- package/dist/web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-docs.md +71 -0
- package/package.json +3 -1
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-
|
|
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
|
|
71
|
-
`extract_book`, `
|
|
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
|
|
@@ -176,9 +196,10 @@ format text is CC BY 4.0, so you can implement the format in anything with attri
|
|
|
176
196
|
The workbook templates are not part of this package. `write-workbook` and the MCP `save_workbook`
|
|
177
197
|
tool fetch the template they need from
|
|
178
198
|
[spreadsheets.diyaccounting.co.uk](https://spreadsheets.diyaccounting.co.uk) the first time they run,
|
|
179
|
-
print the terms, and cache it for later runs. The templates are the company's
|
|
180
|
-
PolyForm Internal Use License 1.0.0 with an additional grant: use them for your
|
|
181
|
-
your clients' accounts if you are an accountant or a bookkeeper, and do not
|
|
199
|
+
print the terms, and cache it for later runs of the same version. The templates are the company's
|
|
200
|
+
own work, under the PolyForm Internal Use License 1.0.0 with an additional grant: use them for your
|
|
201
|
+
own accounts, or for your clients' accounts if you are an accountant or a bookkeeper, and do not
|
|
202
|
+
redistribute them.
|
|
182
203
|
|
|
183
204
|
This repository does not accept contributions.
|
|
184
205
|
|
|
@@ -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
|
};
|
package/dist/app/bin/export.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/app/bin/view.js
CHANGED
|
@@ -15,30 +15,16 @@
|
|
|
15
15
|
// one directory per package version and origin host.
|
|
16
16
|
|
|
17
17
|
import { spawn } from "child_process";
|
|
18
|
-
import { existsSync, readFileSync } from "fs";
|
|
19
18
|
import { tmpdir } from "os";
|
|
20
|
-
import {
|
|
21
|
-
import { fileURLToPath } from "url";
|
|
19
|
+
import { join, resolve } from "path";
|
|
22
20
|
import { readBookFragment } from "./link.js";
|
|
23
21
|
import { FRAGMENT_KEY } from "../lib/diya-gl-link.js";
|
|
24
22
|
import { DEFAULT_ORIGIN, startViewServer } from "../lib/diya-gl-view-server.js";
|
|
23
|
+
import { packageVersion } from "../lib/package-version.js";
|
|
25
24
|
|
|
26
25
|
const USAGE =
|
|
27
26
|
"Usage: diya-gl view <book directory | diya-gl zip | diya-gl JSON file | workbook> [--port <number>] [--origin <url>] [--no-open]";
|
|
28
27
|
|
|
29
|
-
// The nearest package.json above this file: the package's own in dist/, the
|
|
30
|
-
// repository's in a checkout. Both carry the version the cache is keyed on.
|
|
31
|
-
function packageVersion() {
|
|
32
|
-
let directory = dirname(fileURLToPath(import.meta.url));
|
|
33
|
-
for (;;) {
|
|
34
|
-
const candidate = join(directory, "package.json");
|
|
35
|
-
if (existsSync(candidate)) return JSON.parse(readFileSync(candidate, "utf8")).version;
|
|
36
|
-
const parent = dirname(directory);
|
|
37
|
-
if (parent === directory) throw new Error("No package.json found above " + import.meta.url);
|
|
38
|
-
directory = parent;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
28
|
function parseArgs(argv) {
|
|
43
29
|
const args = argv.slice(2);
|
|
44
30
|
let port = 0;
|
|
@@ -86,7 +72,7 @@ function openInBrowser(url) {
|
|
|
86
72
|
async function main() {
|
|
87
73
|
const { source, port, origin, open } = parseArgs(process.argv);
|
|
88
74
|
const { product, fragment, length, warning } = await readBookFragment(source);
|
|
89
|
-
const cacheDir = join(tmpdir(), "diya-gl-view", packageVersion(), new URL(origin).host.replace(/[^A-Za-z0-9.-]/g, "_"));
|
|
75
|
+
const cacheDir = join(tmpdir(), "diya-gl-view", await packageVersion(), new URL(origin).host.replace(/[^A-Za-z0-9.-]/g, "_"));
|
|
90
76
|
const running = await startViewServer({ cacheDir, origin, port });
|
|
91
77
|
const url = `http://${running.host}:${running.port}/${product}.html#${FRAGMENT_KEY}=${fragment}`;
|
|
92
78
|
console.log(url);
|
|
@@ -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-
|
|
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-
|
|
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,58 @@
|
|
|
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
|
+
}
|
|
1458
|
+
},
|
|
1459
|
+
{
|
|
1460
|
+
"tag": "diya-gl-v1.2.47",
|
|
1461
|
+
"date": "2026-10-05",
|
|
1462
|
+
"formatVersion": "diya-gl/1",
|
|
1463
|
+
"engineVersion": "1.2.47+26b9e6ec",
|
|
1464
|
+
"taxDataHash": "51bdff60e993",
|
|
1465
|
+
"reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
|
|
1466
|
+
"templates": {
|
|
1467
|
+
"bst": {
|
|
1468
|
+
"hash": "0d003e10e00c",
|
|
1469
|
+
"scorecard": "812 passed, 0 warnings, 0 failed"
|
|
1470
|
+
},
|
|
1471
|
+
"taxi": {
|
|
1472
|
+
"hash": "1ff69654d64d",
|
|
1473
|
+
"scorecard": "700 passed, 0 warnings, 0 failed"
|
|
1474
|
+
},
|
|
1475
|
+
"se": {
|
|
1476
|
+
"hash": "064605ae76ed",
|
|
1477
|
+
"scorecard": "8015 passed, 0 warnings, 0 failed"
|
|
1478
|
+
},
|
|
1479
|
+
"ltd": {
|
|
1480
|
+
"hash": "a65ab09cf35c",
|
|
1481
|
+
"scorecard": "101608 passed, 0 warnings, 0 failed"
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1432
1484
|
}
|
|
1433
1485
|
]
|
|
1434
1486
|
}
|
|
@@ -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
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
// Node's own modules load on the first read rather than with this file, so a
|
|
20
20
|
// bundle whose caller supplies a loader never pulls them in.
|
|
21
21
|
|
|
22
|
+
import { packageVersion } from "./package-version.js";
|
|
23
|
+
|
|
22
24
|
// The resource space is app/ with one named exception. The two v2 schemas are
|
|
23
25
|
// published by the site, not carried in app/, and the site serves them at
|
|
24
26
|
// /schema/ — so "schema/" is a root of its own rather than a directory under
|
|
@@ -31,7 +33,10 @@ const SCHEMA_ROOT_FROM_APP = ["..", "web", "spreadsheets.diyaccounting.co.uk", "
|
|
|
31
33
|
// checkout has app/templates and reads it straight off disk. A packaged install
|
|
32
34
|
// has not, so the loader takes the same file from the site, which already
|
|
33
35
|
// serves the templates the DIYA-GL pages use, and keeps it in a user cache so
|
|
34
|
-
// every later run works with no network.
|
|
36
|
+
// every later run of the same version works with no network. The cache is keyed
|
|
37
|
+
// on the package version and the source host, because the anchors the reader
|
|
38
|
+
// checks a package against move with the version: a template cached by an
|
|
39
|
+
// older version writes a package the newer reader refuses.
|
|
35
40
|
//
|
|
36
41
|
// Both the current /diya-gl/ and legacy /books/ paths resolve — the old one
|
|
37
42
|
// through a redirect — so published versions built before the path moved keep
|
|
@@ -71,11 +76,12 @@ async function fetchTemplate(path) {
|
|
|
71
76
|
const { dirname, resolve } = await import("path");
|
|
72
77
|
const { homedir } = await import("os");
|
|
73
78
|
|
|
79
|
+
const source = templateSource();
|
|
74
80
|
const cacheRoot = process.env.XDG_CACHE_HOME || resolve(homedir(), ".cache");
|
|
75
|
-
const
|
|
81
|
+
const sourceHost = new URL(source).host.replace(/[^A-Za-z0-9.-]/g, "_");
|
|
82
|
+
const cached = resolve(cacheRoot, "diya-gl", await packageVersion(), sourceHost, path);
|
|
76
83
|
if (existsSync(cached)) return readFileSync(cached);
|
|
77
84
|
|
|
78
|
-
const source = templateSource();
|
|
79
85
|
announceTemplateTerms(source);
|
|
80
86
|
|
|
81
87
|
const url = `${source}/${path}`;
|
|
@@ -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
|
|
2000
|
-
|
|
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, "
|
|
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
|
-
|
|
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) ──────────
|