@diy-accounting-uk/diya-gl 1.0.0
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 +63 -0
- package/bin/diya-gl-mcp.js +10 -0
- package/bin/diya-gl-read-workbook.js +11 -0
- package/bin/diya-gl-recalc.js +10 -0
- package/bin/diya-gl-write-workbook.js +12 -0
- package/bin/diya-gl.js +24 -0
- package/bin/run-dist-bin.js +23 -0
- package/dist/app/bin/build-diya-gl-spec.js +680 -0
- package/dist/app/bin/build-packages.js +149 -0
- package/dist/app/bin/build-reconciliation-pages.js +1052 -0
- package/dist/app/bin/build-sitemaps.js +51 -0
- package/dist/app/bin/compliance-report.js +125 -0
- package/dist/app/bin/cross-package-reconciliation.js +92 -0
- package/dist/app/bin/diya-gl-mcp.js +27 -0
- package/dist/app/bin/export.js +318 -0
- package/dist/app/bin/extract-scenarios.js +1348 -0
- package/dist/app/bin/generate.js +353 -0
- package/dist/app/bin/judge-reconciliation.js +649 -0
- package/dist/app/bin/reconcile.js +247 -0
- package/dist/app/bin/report.js +291 -0
- package/dist/app/bin/verify-roundtrip.js +941 -0
- package/dist/app/bin/verify-stability.js +429 -0
- package/dist/app/bin/write-workbook.js +106 -0
- package/dist/app/data/filing/ct-computation-v1.1.toml +1033 -0
- package/dist/app/data/filing/ct600-v3.toml +2329 -0
- package/dist/app/data/filing/frs105-formats.toml +244 -0
- package/dist/app/data/fixture-master-gaps.json +247 -0
- package/dist/app/data/hmrc/SOURCE.md +14 -0
- package/dist/app/data/hmrc/form-layouts/bst.json +116 -0
- package/dist/app/data/hmrc/form-layouts/ltd.json +452 -0
- package/dist/app/data/hmrc/form-layouts/se.json +485 -0
- package/dist/app/data/hmrc/form-layouts/taxi.json +117 -0
- package/dist/app/data/hmrc/sa103-mtd-mapping.json +1078 -0
- package/dist/app/data/hmrc/sa103f_mapping_v3.csv +111 -0
- package/dist/app/data/judge-rubric.md +52 -0
- package/dist/app/data/ltd-2020.toml +49 -0
- package/dist/app/data/ltd-2021.toml +49 -0
- package/dist/app/data/ltd-2022.toml +49 -0
- package/dist/app/data/ltd-2023.toml +49 -0
- package/dist/app/data/ltd-2024.toml +49 -0
- package/dist/app/data/ltd-2025.toml +49 -0
- package/dist/app/data/ltd-2026.toml +53 -0
- package/dist/app/data/ltd-2027.toml +51 -0
- package/dist/app/data/render-unrepresentable/bst.json +24 -0
- package/dist/app/data/render-unrepresentable/ltd.json +863 -0
- package/dist/app/data/render-unrepresentable/se.json +455 -0
- package/dist/app/data/render-unrepresentable/taxi.json +16 -0
- package/dist/app/data/roundtrip-budget.json +38 -0
- package/dist/app/data/roundtrip-matrix-budget.json +26 -0
- package/dist/app/data/roundtrip-unrepresentable.json +401 -0
- package/dist/app/data/se-2020-2021.toml +52 -0
- package/dist/app/data/se-2021-2022.toml +52 -0
- package/dist/app/data/se-2022-2023.toml +55 -0
- package/dist/app/data/se-2023-2024.toml +54 -0
- package/dist/app/data/se-2024-2025.toml +51 -0
- package/dist/app/data/se-2025-2026.toml +51 -0
- package/dist/app/data/se-2026-2027.toml +51 -0
- package/dist/app/data/volatile-cells.json +16 -0
- package/dist/app/lib/anchors/bst.js +79 -0
- package/dist/app/lib/anchors/ltd.js +626 -0
- package/dist/app/lib/anchors/run.js +100 -0
- package/dist/app/lib/anchors/se.js +456 -0
- package/dist/app/lib/anchors/taxi.js +164 -0
- package/dist/app/lib/app-resources.js +80 -0
- package/dist/app/lib/book-checks/ltd.js +408 -0
- package/dist/app/lib/book-checks/se.js +687 -0
- package/dist/app/lib/book-checks/taxi.js +215 -0
- package/dist/app/lib/book-checks.js +872 -0
- package/dist/app/lib/books-engine.js +197 -0
- package/dist/app/lib/books-interchange.js +533 -0
- package/dist/app/lib/calculators/bst.js +381 -0
- package/dist/app/lib/calculators/ltd.js +1938 -0
- package/dist/app/lib/calculators/se.js +1239 -0
- package/dist/app/lib/calculators/shared.js +131 -0
- package/dist/app/lib/calculators/taxi.js +461 -0
- package/dist/app/lib/canonical-report-value.js +77 -0
- package/dist/app/lib/compliance-report.js +341 -0
- package/dist/app/lib/diya-gl-calculator.js +52 -0
- package/dist/app/lib/diya-gl-canonical.js +355 -0
- package/dist/app/lib/diya-gl-edits-ltd.js +144 -0
- package/dist/app/lib/diya-gl-edits.js +361 -0
- package/dist/app/lib/diya-gl-loader.js +774 -0
- package/dist/app/lib/diya-gl-schema.js +212 -0
- package/dist/app/lib/generator.js +1912 -0
- package/dist/app/lib/guide.js +33 -0
- package/dist/app/lib/headlines.js +293 -0
- package/dist/app/lib/link-caches.js +421 -0
- package/dist/app/lib/ltd-layout.js +139 -0
- package/dist/app/lib/mcp/diya-gl-tools.js +330 -0
- package/dist/app/lib/mcp/jsonrpc-stdio.js +66 -0
- package/dist/app/lib/mcp/server.js +65 -0
- package/dist/app/lib/overtype-sidecar.js +180 -0
- package/dist/app/lib/package-builder.js +202 -0
- package/dist/app/lib/payslips-date-chain.js +481 -0
- package/dist/app/lib/payslips-layout.js +235 -0
- package/dist/app/lib/period-shift.js +111 -0
- package/dist/app/lib/product-workbook.js +317 -0
- package/dist/app/lib/products.js +27 -0
- package/dist/app/lib/provenance-data.js +36 -0
- package/dist/app/lib/provenance.js +92 -0
- package/dist/app/lib/report-generator.js +374 -0
- package/dist/app/lib/report-indicators.js +468 -0
- package/dist/app/lib/report-serializer.js +392 -0
- package/dist/app/lib/scenario-extractor.js +1403 -0
- package/dist/app/lib/scenario-loader.js +84 -0
- package/dist/app/lib/sitemap-builder.js +59 -0
- package/dist/app/lib/spreadsheet-runner.js +566 -0
- package/dist/app/lib/tax/capital-allowances.js +77 -0
- package/dist/app/lib/tax/corporation-tax.js +144 -0
- package/dist/app/lib/tax/income-tax.js +122 -0
- package/dist/app/lib/tax/mileage.js +69 -0
- package/dist/app/lib/tax/national-insurance.js +31 -0
- package/dist/app/lib/tax/vat.js +188 -0
- package/dist/app/lib/tax-year.js +31 -0
- package/dist/app/lib/template-formula-map.js +253 -0
- package/dist/app/lib/workbook-set.js +153 -0
- package/dist/app/lib/xlsx-exporter.js +2815 -0
- package/dist/app/lib/xlsx-parts.js +111 -0
- package/dist/app/lib/xlsx-reader.js +84 -0
- package/dist/app/products/bst.js +727 -0
- package/dist/app/products/ltd.js +4270 -0
- package/dist/app/products/se.js +3403 -0
- package/dist/app/products/taxi.js +912 -0
- package/dist/app/templates/bst/bst-excel.xlsx +0 -0
- package/dist/app/templates/bst/meta.toml +27 -0
- package/dist/app/templates/ltd/Cashaccount.xlsx +0 -0
- package/dist/app/templates/ltd/Companysecretary.xlsx +0 -0
- package/dist/app/templates/ltd/Creditcardaccount.xlsx +0 -0
- package/dist/app/templates/ltd/Currentaccount.xlsx +0 -0
- package/dist/app/templates/ltd/Dividend Voucher.docx +0 -0
- package/dist/app/templates/ltd/Financialaccounts.xlsx +0 -0
- package/dist/app/templates/ltd/Fixedassets.xlsx +0 -0
- package/dist/app/templates/ltd/Payslips.xlsx +0 -0
- package/dist/app/templates/ltd/Purchases.xlsx +0 -0
- package/dist/app/templates/ltd/Sales.xlsx +0 -0
- package/dist/app/templates/ltd/Salesinvoice.xlsx +0 -0
- package/dist/app/templates/ltd/Savingaccount.xlsx +0 -0
- package/dist/app/templates/ltd/Vatreturns.xlsx +0 -0
- package/dist/app/templates/ltd/expensesform.xlsx +0 -0
- package/dist/app/templates/ltd/meta.toml +54 -0
- package/dist/app/templates/meta.toml +12 -0
- package/dist/app/templates/se/Bank.xlsx +0 -0
- package/dist/app/templates/se/Cash.xlsx +0 -0
- package/dist/app/templates/se/Financialaccounts.xlsx +0 -0
- package/dist/app/templates/se/Fixedassets.xlsx +0 -0
- package/dist/app/templates/se/Payslips.xlsx +0 -0
- package/dist/app/templates/se/Purchases.xlsx +0 -0
- package/dist/app/templates/se/Sales.xlsx +0 -0
- package/dist/app/templates/se/Salesinvoice.xlsx +0 -0
- package/dist/app/templates/se/Vat.xlsx +0 -0
- package/dist/app/templates/se/meta.toml +43 -0
- package/dist/app/templates/taxi/meta.toml +43 -0
- package/dist/app/templates/taxi/taxi-excel.xlsx +0 -0
- package/dist/web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-book-v2.schema.json +713 -0
- package/dist/web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-docs.md +930 -0
- package/dist/web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-lines-v2.schema.json +220 -0
- package/package.json +37 -0
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright (C) 2026 DIY Accounting Ltd
|
|
4
|
+
//
|
|
5
|
+
// build-diya-gl-spec.js — Generate the diya-gl format specification page.
|
|
6
|
+
//
|
|
7
|
+
// Usage:
|
|
8
|
+
// node app/bin/build-diya-gl-spec.js
|
|
9
|
+
//
|
|
10
|
+
// Reads: web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-book-v2.schema.json
|
|
11
|
+
// web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-lines-v2.schema.json
|
|
12
|
+
// app/data/hmrc/form-layouts/{bst,taxi,se}.json
|
|
13
|
+
// app/data/hmrc/sa103-mtd-mapping.json
|
|
14
|
+
// app/lib/books-interchange.js (the zip entries and the format version)
|
|
15
|
+
// app/lib/book-checks.js, run over one example book per product
|
|
16
|
+
// web/spreadsheets.diyaccounting.co.uk/public/reconciliation/*.json
|
|
17
|
+
// Writes: web/spreadsheets.diyaccounting.co.uk/public/diya-gl.html
|
|
18
|
+
//
|
|
19
|
+
// Every table on the page is read from the artefact it describes, so the
|
|
20
|
+
// page cannot claim a field, a box or a check the code does not carry. The
|
|
21
|
+
// prose sits in this file; the numbers in it are counted from the tables.
|
|
22
|
+
|
|
23
|
+
import { readFileSync, readdirSync, writeFileSync, existsSync, statSync } from "fs";
|
|
24
|
+
import { resolve, dirname, join } from "path";
|
|
25
|
+
import { fileURLToPath } from "url";
|
|
26
|
+
import { loadDiyaGlData, diyaGlToScenario, extractTaxDataFromBook } from "../lib/diya-gl-loader.js";
|
|
27
|
+
import { calculateFromDiyaGl } from "../lib/diya-gl-calculator.js";
|
|
28
|
+
import { runBookChecks } from "../lib/book-checks.js";
|
|
29
|
+
|
|
30
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const ROOT = resolve(__dirname, "..", "..");
|
|
32
|
+
const PUBLIC_DIR = resolve(ROOT, "web", "spreadsheets.diyaccounting.co.uk", "public");
|
|
33
|
+
const SCHEMA_DIR = resolve(PUBLIC_DIR, "schema");
|
|
34
|
+
const RECONCILIATION_DIR = resolve(PUBLIC_DIR, "reconciliation");
|
|
35
|
+
const HMRC_DIR = resolve(ROOT, "app", "data", "hmrc");
|
|
36
|
+
const EXAMPLES_DIR = resolve(ROOT, "examples");
|
|
37
|
+
const OUT_PATH = resolve(PUBLIC_DIR, "diya-gl.html");
|
|
38
|
+
|
|
39
|
+
const CANONICAL_URL = "https://spreadsheets.diyaccounting.co.uk/diya-gl.html";
|
|
40
|
+
const PAGE_TITLE = "The DIYA-GL format - DIY Accounting Spreadsheets";
|
|
41
|
+
const PAGE_DESCRIPTION =
|
|
42
|
+
"The diya-gl accounting file format: the fields it declares and the XBRL GL 2015 element each one comes from, the SA103S box every computed figure lands in, the check catalogue, the zip layout and the reconciliation evidence.";
|
|
43
|
+
|
|
44
|
+
// The package name each declared product is calculated under.
|
|
45
|
+
const PACKAGE_OF_PRODUCT = { BasicSoleTrader: "bst", TaxiDriver: "taxi", SelfEmployed: "se", Company: "ltd" };
|
|
46
|
+
const PRODUCT_NAMES = { bst: "Basic Sole Trader", taxi: "Taxi Driver", se: "Self Employed", ltd: "Limited Company" };
|
|
47
|
+
const PRODUCT_ORDER = ["bst", "taxi", "se", "ltd"];
|
|
48
|
+
|
|
49
|
+
// ── Reading the schemas ─────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
// The marker a schema description opens with: the GL element the field is
|
|
52
|
+
// taken from, or the diya-gl: extension label for a field the framework has
|
|
53
|
+
// no element for.
|
|
54
|
+
const ELEMENT_MARKER =
|
|
55
|
+
/^(?:Adapted from\s+)?((?:gl-[a-z]{3}(?::[A-Za-z]+)?)(?:\s*\+\s*gl-[a-z]{3}(?::[A-Za-z]+)?)*|diya-gl: extension)\s*(?:—|--)\s*([\s\S]*)$/;
|
|
56
|
+
const ELEMENT_TOKEN = /gl-[a-z]{3}:[A-Za-z]+/;
|
|
57
|
+
const EXTENSION = "diya-gl: extension";
|
|
58
|
+
|
|
59
|
+
function resolveRef(schema, node) {
|
|
60
|
+
if (!node || !node.$ref) return node;
|
|
61
|
+
const name = node.$ref.replace("#/$defs/", "");
|
|
62
|
+
return schema.$defs[name];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A field's element and its meaning, read from the description: the leading
|
|
66
|
+
// marker where there is one, otherwise the first GL element the description
|
|
67
|
+
// names, otherwise the extension label inherited from the table above it.
|
|
68
|
+
function elementOf(description, inherited) {
|
|
69
|
+
const marked = ELEMENT_MARKER.exec(description);
|
|
70
|
+
if (marked) return { element: marked[1], meaning: marked[2].trim() };
|
|
71
|
+
const named = ELEMENT_TOKEN.exec(description);
|
|
72
|
+
if (named) return { element: named[0], meaning: description.trim() };
|
|
73
|
+
return { element: inherited || "", meaning: description.trim() };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function typeLabel(node) {
|
|
77
|
+
if (node.type === "array") return "array of tables";
|
|
78
|
+
if (node.type === "object") return "table";
|
|
79
|
+
if (node.enum) return `${node.type}, one of ${node.enum.length}`;
|
|
80
|
+
return node.type || "";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Every field the schema declares, depth first, in declaration order.
|
|
84
|
+
function fieldsOf(schema, node, path, inherited, rows) {
|
|
85
|
+
const resolved = resolveRef(schema, node);
|
|
86
|
+
const required = resolved.required || [];
|
|
87
|
+
for (const [key, rawChild] of Object.entries(resolved.properties || {})) {
|
|
88
|
+
const child = resolveRef(schema, rawChild);
|
|
89
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
90
|
+
const { element, meaning } = elementOf(child.description || "", inherited);
|
|
91
|
+
rows.push({
|
|
92
|
+
path: childPath,
|
|
93
|
+
type: typeLabel(child),
|
|
94
|
+
required: required.includes(key),
|
|
95
|
+
element,
|
|
96
|
+
meaning,
|
|
97
|
+
});
|
|
98
|
+
const passDown = element === EXTENSION ? EXTENSION : inherited;
|
|
99
|
+
// A section keyed by account code holds one account definition per key;
|
|
100
|
+
// that definition is a table of its own rather than 4,000 repeated rows.
|
|
101
|
+
if (child.type === "object" && child.properties) fieldsOf(schema, child, childPath, passDown, rows);
|
|
102
|
+
if (child.type === "array") {
|
|
103
|
+
const items = resolveRef(schema, child.items);
|
|
104
|
+
if (items && items.properties) fieldsOf(schema, items, `${childPath}[]`, passDown, rows);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return rows;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function loadSchemas() {
|
|
111
|
+
const book = JSON.parse(readFileSync(resolve(SCHEMA_DIR, "diya-gl-book-v2.schema.json"), "utf8"));
|
|
112
|
+
const lines = JSON.parse(readFileSync(resolve(SCHEMA_DIR, "diya-gl-lines-v2.schema.json"), "utf8"));
|
|
113
|
+
return { book, lines };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// The three groups the field tables are shown in: the book's document info,
|
|
117
|
+
// the book's chart of accounts and its registers, and the transaction lines.
|
|
118
|
+
function fieldGroups({ book, lines }) {
|
|
119
|
+
const bookRows = fieldsOf(book, book, "", "", []);
|
|
120
|
+
const documentInfo = bookRows.filter((row) => row.path.startsWith("documentInfo") || row.path.startsWith("entityInformation"));
|
|
121
|
+
const registers = bookRows.filter((row) => !documentInfo.includes(row));
|
|
122
|
+
const accountDefinition = fieldsOf(book, book.$defs.accountDefinition, "", "", []);
|
|
123
|
+
const lineRows = fieldsOf(lines, lines, "", "", []);
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
id: "document-info",
|
|
127
|
+
title: "Document info",
|
|
128
|
+
tables: [
|
|
129
|
+
{
|
|
130
|
+
note: "The two tables at the head of book.toml: what period the file covers, and whose business it is.",
|
|
131
|
+
rows: documentInfo,
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "accounts",
|
|
137
|
+
title: "Accounts and registers",
|
|
138
|
+
tables: [
|
|
139
|
+
{
|
|
140
|
+
note:
|
|
141
|
+
"The chart of accounts, the registers a year-end needs (assets, hire purchase, members, charges) and the tax rates the period " +
|
|
142
|
+
"was calculated at. The rate tables hold HMRC's published figures for the year; the framework has no element for a rate table, " +
|
|
143
|
+
"so those rows name none.",
|
|
144
|
+
rows: registers,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
heading: "One account",
|
|
148
|
+
note: "Each four-digit account code in a section holds one of these.",
|
|
149
|
+
rows: accountDefinition,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "lines",
|
|
155
|
+
title: "Transaction lines",
|
|
156
|
+
tables: [
|
|
157
|
+
{
|
|
158
|
+
note:
|
|
159
|
+
"One JSON object per line of lines.jsonl. Four fields are required on every line, and a journal line must also say which side " +
|
|
160
|
+
"it posts.",
|
|
161
|
+
rows: lineRows,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── The SA103S boxes ────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
// What a product does with a box: read a workbook cell, work it out from the
|
|
171
|
+
// profit and loss account, or leave it blank.
|
|
172
|
+
function boxTreatment(box) {
|
|
173
|
+
if (box.cell) return "figure";
|
|
174
|
+
if (box.derived || box.rule) return "derived";
|
|
175
|
+
return "blank";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function sa103sBoxes() {
|
|
179
|
+
const layouts = {};
|
|
180
|
+
for (const product of ["bst", "taxi", "se"]) {
|
|
181
|
+
layouts[product] = JSON.parse(readFileSync(resolve(HMRC_DIR, "form-layouts", `${product}.json`), "utf8"));
|
|
182
|
+
}
|
|
183
|
+
const mapping = JSON.parse(readFileSync(resolve(HMRC_DIR, "sa103-mtd-mapping.json"), "utf8"));
|
|
184
|
+
const apiField = new Map();
|
|
185
|
+
for (const entry of mapping.boxes) if (entry.form === "SA103S" && entry.field) apiField.set(entry.box, entry.field);
|
|
186
|
+
|
|
187
|
+
const rows = new Map();
|
|
188
|
+
for (const [product, layout] of Object.entries(layouts)) {
|
|
189
|
+
const form = layout.forms.sa103s;
|
|
190
|
+
for (const section of form.sections) {
|
|
191
|
+
for (const box of section.boxes) {
|
|
192
|
+
if (!rows.has(box.box)) {
|
|
193
|
+
rows.set(box.box, { box: box.box, heading: section.heading, label: box.label, products: {}, field: apiField.get(box.box) || "" });
|
|
194
|
+
}
|
|
195
|
+
rows.get(box.box).products[product] = boxTreatment(box);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const year = layouts.bst.forms.sa103s.year;
|
|
200
|
+
return { rows: [...rows.values()], year, source: mapping.source };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── The check catalogue ─────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
// What each check reads, in the page's own words. A check the code runs and
|
|
206
|
+
// this table has no line for stops the build, so the catalogue cannot fall
|
|
207
|
+
// behind the engine.
|
|
208
|
+
const CHECK_DESCRIPTIONS = {
|
|
209
|
+
"book-dates-in-period": "Every entry is dated inside the accounting period the book declares.",
|
|
210
|
+
"book-accounts-in-chart": "Every entry reaches an account the book's chart of accounts declares.",
|
|
211
|
+
"book-amounts-whole-pence": "Every amount is a whole number of pence.",
|
|
212
|
+
"book-bank-account-has-workbook": "Every bank entry is on an account the package keeps a workbook for.",
|
|
213
|
+
"book-bank-code-analysed": "Every bank entry is coded to a column its workbook analyses.",
|
|
214
|
+
"book-bank-line-has-side": "Every bank entry says whether it is a receipt or a payment.",
|
|
215
|
+
"book-payslip-names-employee": "Every payslip names someone the book employs.",
|
|
216
|
+
"book-fixed-asset-rows-fit": "Every asset, disposal and hire purchase agreement has a row on the fixed asset schedule.",
|
|
217
|
+
"book-ltd-bank-line-has-side": "Every bank entry says whether it is a receipt or a payment.",
|
|
218
|
+
"book-ltd-bank-code-analysed": "Every bank entry is coded to a column its workbook analyses.",
|
|
219
|
+
"book-ltd-straddling-line-has-vat-period": "Every sale and purchase dated outside the period names the VAT return period it belongs to.",
|
|
220
|
+
"book-ltd-payroll-line-names-employee": "Every payroll entry names someone on the payroll.",
|
|
221
|
+
"book-ltd-fixed-asset-rows-fit-schedule": "Every asset, disposal and hire purchase agreement has a row on the fixed asset schedule.",
|
|
222
|
+
"book-vat-threshold": "Turnover for the year against the VAT registration threshold for that year.",
|
|
223
|
+
"book-duplicate-entries": "No two entries share the same journal, date, amount and detail without being each other's two sides.",
|
|
224
|
+
"book-empty-detail": "Every entry names who or what it was with.",
|
|
225
|
+
"book-negative-amount": "Every sale and purchase amount is zero or more.",
|
|
226
|
+
"book-empty-month": "Every month between the first and last entry has at least one entry.",
|
|
227
|
+
"book-cash-never-overdrawn": "The cash book closes every month at zero or more.",
|
|
228
|
+
"book-bank-overdrawn": "The bank book closes every month at zero or more.",
|
|
229
|
+
"book-employee-paid-every-month": "Every employee is paid in each month between their first and last payslip.",
|
|
230
|
+
"book-taxi-fare-miles": "Every fare day that carries miles elsewhere carries its own.",
|
|
231
|
+
"book-taxi-vehicle-register": "Every vehicle bought is on the fixed asset register.",
|
|
232
|
+
"book-taxi-miles-band": "Business miles stay inside the higher-rate mileage band.",
|
|
233
|
+
"book-ltd-transfer-has-counter-leg": "Every transfer between the company's own accounts appears on both of them.",
|
|
234
|
+
"book-ltd-dividend-within-distributable-profits": "Dividends declared against the retained profits available to pay them.",
|
|
235
|
+
"book-ltd-cis-on-subcontractor-line": "Every CIS deduction sits on a sub-contractor purchase.",
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// One example book per product, taken in path order so the same book is read
|
|
239
|
+
// on every run.
|
|
240
|
+
function exampleBooksByProduct() {
|
|
241
|
+
const found = {};
|
|
242
|
+
const walk = (dir) => {
|
|
243
|
+
for (const entry of readdirSync(dir).sort()) {
|
|
244
|
+
const path = join(dir, entry);
|
|
245
|
+
if (!statSync(path).isDirectory()) continue;
|
|
246
|
+
if (existsSync(join(path, "book.toml")) && existsSync(join(path, "lines.jsonl"))) {
|
|
247
|
+
const book = readFileSync(join(path, "book.toml"), "utf8");
|
|
248
|
+
const declared = /"diya-gl:product"\s*=\s*"([A-Za-z]+)"/.exec(book);
|
|
249
|
+
const packageName = declared && PACKAGE_OF_PRODUCT[declared[1]];
|
|
250
|
+
if (packageName && !found[packageName]) found[packageName] = path;
|
|
251
|
+
}
|
|
252
|
+
walk(path);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
walk(EXAMPLES_DIR);
|
|
256
|
+
const missing = PRODUCT_ORDER.filter((product) => !found[product]);
|
|
257
|
+
if (missing.length > 0) throw new Error(`No example book found for: ${missing.join(", ")}`);
|
|
258
|
+
return found;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function checkCatalogue() {
|
|
262
|
+
const byProduct = exampleBooksByProduct();
|
|
263
|
+
const catalogue = new Map();
|
|
264
|
+
for (const packageName of PRODUCT_ORDER) {
|
|
265
|
+
const { book, lines } = loadDiyaGlData(byProduct[packageName]);
|
|
266
|
+
const taxData = extractTaxDataFromBook(book, packageName);
|
|
267
|
+
const scenario = diyaGlToScenario(book, lines, packageName);
|
|
268
|
+
const results = calculateFromDiyaGl(book, lines, packageName, taxData, scenario);
|
|
269
|
+
for (const result of runBookChecks({ book, lines, taxData, results }).results) {
|
|
270
|
+
if (!catalogue.has(result.id)) catalogue.set(result.id, { id: result.id, tier: result.tier, products: [] });
|
|
271
|
+
catalogue.get(result.id).products.push(packageName);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const rows = [...catalogue.values()];
|
|
275
|
+
const undescribed = rows.filter((row) => !CHECK_DESCRIPTIONS[row.id]).map((row) => row.id);
|
|
276
|
+
if (undescribed.length > 0) {
|
|
277
|
+
throw new Error(`The check catalogue has no description for: ${undescribed.join(", ")}. Add one to CHECK_DESCRIPTIONS.`);
|
|
278
|
+
}
|
|
279
|
+
const stale = Object.keys(CHECK_DESCRIPTIONS).filter((id) => !catalogue.has(id));
|
|
280
|
+
if (stale.length > 0) {
|
|
281
|
+
throw new Error(`CHECK_DESCRIPTIONS describes checks the engine no longer runs: ${stale.join(", ")}. Remove them.`);
|
|
282
|
+
}
|
|
283
|
+
// Shared rules first, then each product's own, and alphabetically inside
|
|
284
|
+
// each group so a new check lands in a stable place.
|
|
285
|
+
const shared = rows.filter((row) => row.products.length === PRODUCT_ORDER.length);
|
|
286
|
+
const own = rows.filter((row) => row.products.length < PRODUCT_ORDER.length);
|
|
287
|
+
const byId = (a, b) => (a.id < b.id ? -1 : 1);
|
|
288
|
+
const byTier = (a, b) => (a.tier === b.tier ? byId(a, b) : a.tier === "check" ? -1 : 1);
|
|
289
|
+
return shared.sort(byTier).concat(own.sort(byTier));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── The zip layout and the format version ───────────────────────────────────
|
|
293
|
+
|
|
294
|
+
const ZIP_ENTRY_NOTES = {
|
|
295
|
+
"book.toml": "The business, the period, the chart of accounts and the registers. Always written.",
|
|
296
|
+
"lines.jsonl": "One transaction per line, in canonical order. Always written.",
|
|
297
|
+
"report.json": "Every figure the engine computed from the two files above, keyed by its cell reference. Always written.",
|
|
298
|
+
"bookchecks.json": "The result of every check and warning in the catalogue above, sorted by id. Written when the writer has run them.",
|
|
299
|
+
"overtyped.json": "Any cell a person typed over in the workbook, so the next generation keeps it. Written when there is one.",
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// The zip's entries, the format name and the format version, read out of the
|
|
303
|
+
// module that writes them.
|
|
304
|
+
function interchangeFacts() {
|
|
305
|
+
const source = readFileSync(resolve(ROOT, "app", "lib", "books-interchange.js"), "utf8");
|
|
306
|
+
const writer = source.slice(source.indexOf("export async function writeDiyaGlZip"));
|
|
307
|
+
const entries = [...writer.matchAll(/zip\.file\("([^"]+)"/g)].map((match) => match[1]);
|
|
308
|
+
if (entries.length === 0) throw new Error("No zip entries found in writeDiyaGlZip.");
|
|
309
|
+
const undescribed = entries.filter((entry) => !ZIP_ENTRY_NOTES[entry]);
|
|
310
|
+
if (undescribed.length > 0) throw new Error(`The zip layout has no note for: ${undescribed.join(", ")}. Add one to ZIP_ENTRY_NOTES.`);
|
|
311
|
+
const format = /const JSON_FORMAT = "([^"]+)"/.exec(source);
|
|
312
|
+
const version = /const JSON_VERSION = (\d+)/.exec(source);
|
|
313
|
+
if (!format || !version) throw new Error("The format name and version are no longer literals in books-interchange.js.");
|
|
314
|
+
return { entries, format: format[1], version: version[1] };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── The reconciliation evidence ─────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
function reconciliationScorecards() {
|
|
320
|
+
if (!existsSync(RECONCILIATION_DIR)) return [];
|
|
321
|
+
return readdirSync(RECONCILIATION_DIR)
|
|
322
|
+
.filter((file) => file.endsWith(".json"))
|
|
323
|
+
.map((file) => JSON.parse(readFileSync(resolve(RECONCILIATION_DIR, file), "utf8")))
|
|
324
|
+
.filter((card) => PRODUCT_ORDER.includes(card.product))
|
|
325
|
+
.sort((a, b) => PRODUCT_ORDER.indexOf(a.product) - PRODUCT_ORDER.indexOf(b.product));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Rendering ───────────────────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
function escapeHtml(value) {
|
|
331
|
+
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function table(headings, rows, className = "spec-table") {
|
|
335
|
+
const head = headings.map((heading) => `<th>${escapeHtml(heading)}</th>`).join("");
|
|
336
|
+
const body = rows.map((cells) => `<tr>${cells.map((cell) => `<td>${cell}</td>`).join("")}</tr>`).join("\n ");
|
|
337
|
+
return [
|
|
338
|
+
' <div class="spec-scroll">',
|
|
339
|
+
` <table class="${className}">`,
|
|
340
|
+
` <thead><tr>${head}</tr></thead>`,
|
|
341
|
+
` <tbody>`,
|
|
342
|
+
` ${body}`,
|
|
343
|
+
" </tbody>",
|
|
344
|
+
" </table>",
|
|
345
|
+
" </div>",
|
|
346
|
+
].join("\n");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function code(value) {
|
|
350
|
+
return `<code>${escapeHtml(value)}</code>`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function fieldRow(row) {
|
|
354
|
+
return [
|
|
355
|
+
code(row.path),
|
|
356
|
+
escapeHtml(row.type),
|
|
357
|
+
row.element === EXTENSION ? "<em>diya-gl extension</em>" : row.element ? code(row.element) : "—",
|
|
358
|
+
escapeHtml(row.meaning) || "—",
|
|
359
|
+
row.required ? "required" : "optional",
|
|
360
|
+
];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function buildPage() {
|
|
364
|
+
const schemas = loadSchemas();
|
|
365
|
+
const groups = fieldGroups(schemas);
|
|
366
|
+
const boxes = sa103sBoxes();
|
|
367
|
+
const checks = checkCatalogue();
|
|
368
|
+
const zip = interchangeFacts();
|
|
369
|
+
const scorecards = reconciliationScorecards();
|
|
370
|
+
|
|
371
|
+
const allFields = groups.flatMap((group) => group.tables.flatMap((part) => part.rows));
|
|
372
|
+
const glElements = new Set();
|
|
373
|
+
for (const row of allFields) {
|
|
374
|
+
for (const token of row.element.split("+")) {
|
|
375
|
+
const trimmed = token.trim();
|
|
376
|
+
if (trimmed && trimmed !== EXTENSION) glElements.add(trimmed);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
const extensionCount = allFields.filter((row) => row.element === EXTENSION).length;
|
|
380
|
+
const checkCount = checks.filter((row) => row.tier === "check").length;
|
|
381
|
+
const warningCount = checks.filter((row) => row.tier === "warning").length;
|
|
382
|
+
const boxesWithField = boxes.rows.filter((row) => row.field).length;
|
|
383
|
+
|
|
384
|
+
const sections = [];
|
|
385
|
+
|
|
386
|
+
sections.push(` <h3 id="what-it-is">What DIYA-GL is</h3>
|
|
387
|
+
<p>
|
|
388
|
+
DIYA-GL is the file format behind DIY Accounting's DIYA-GL pages, its command line tools and the spreadsheets it generates. A whole
|
|
389
|
+
year of a small business's accounts is two text files in a zip. <code>book.toml</code> says who the business is, what period the
|
|
390
|
+
books cover and what the chart of accounts holds. <code>lines.jsonl</code> holds one transaction per line. A sole trader's year
|
|
391
|
+
runs to about 15 KB.
|
|
392
|
+
</p>
|
|
393
|
+
<p>
|
|
394
|
+
It is for people who want to own their accounting data outright. You can read it in a text editor, keep it in git and see what
|
|
395
|
+
changed, and recalculate it in a browser or on a command line without Excel and without a server. This page is the specification:
|
|
396
|
+
the fields, the figures they compute, the checks they have to pass, the zip they travel in, and the evidence that the figures agree
|
|
397
|
+
with the spreadsheets we have shipped for twenty years.
|
|
398
|
+
</p>`);
|
|
399
|
+
|
|
400
|
+
sections.push(` <h3 id="declared-subset">The declared subset</h3>
|
|
401
|
+
<p>
|
|
402
|
+
Every field name in the two schemas is taken from the XBRL Global Ledger Taxonomy Framework 2015, the XBRL Standards Board's
|
|
403
|
+
recommendation of 25 March 2015. The tables below name the element each field comes from.
|
|
404
|
+
</p>
|
|
405
|
+
<p>
|
|
406
|
+
That is a lineage and a published mapping, and this page claims nothing past what the tables show. XBRL GL runs no conformance
|
|
407
|
+
programme and issues no certificate for a data format. A diya-gl file is TOML and JSON Lines; it is not an XBRL instance document,
|
|
408
|
+
and no XBRL processor reads it.
|
|
409
|
+
</p>
|
|
410
|
+
<p>
|
|
411
|
+
The format names ${glElements.size} elements, from four of the framework's modules: <code>gl-cor</code> (core),
|
|
412
|
+
<code>gl-bus</code> (business), <code>gl-muc</code> (multicurrency) and <code>gl-taf</code> (tax audit file). No other element of
|
|
413
|
+
the framework is named in either schema. The framework's nested tuples are flattened: an entry header and its detail are one line,
|
|
414
|
+
and <code>gl-cor:account</code> becomes the pair <code>accountMainID</code> and <code>accountMainDescription</code>. Where a UK
|
|
415
|
+
small business needs something the framework has no element for, the schema declares a diya-gl extension: a
|
|
416
|
+
<code>diya-gl:</code> prefix on a field, or a whole table marked as one. There are ${extensionCount} extension fields, and the tables
|
|
417
|
+
below mark every one.
|
|
418
|
+
</p>
|
|
419
|
+
<p>
|
|
420
|
+
The schemas themselves are published beside this page:
|
|
421
|
+
<a href="schema/diya-gl-book-v2.schema.json">diya-gl-book-v2.schema.json</a> and
|
|
422
|
+
<a href="schema/diya-gl-lines-v2.schema.json">diya-gl-lines-v2.schema.json</a>, both JSON Schema draft 2020-12. A longer commentary
|
|
423
|
+
on the mapping is in <a href="schema/diya-gl-docs.md">diya-gl-docs.md</a>.
|
|
424
|
+
</p>`);
|
|
425
|
+
|
|
426
|
+
for (const group of groups) {
|
|
427
|
+
const body = group.tables
|
|
428
|
+
.map(
|
|
429
|
+
(part) =>
|
|
430
|
+
(part.heading ? ` <h5>${escapeHtml(part.heading)}</h5>\n` : "") +
|
|
431
|
+
` <p>${escapeHtml(part.note)}</p>\n` +
|
|
432
|
+
table(["Field", "Type", "XBRL GL 2015 element", "Meaning", "Required"], part.rows.map(fieldRow)),
|
|
433
|
+
)
|
|
434
|
+
.join("\n");
|
|
435
|
+
sections.push(` <h4 id="${group.id}">${escapeHtml(group.title)}</h4>\n${body}`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const boxRow = (row) => [
|
|
439
|
+
code(row.box),
|
|
440
|
+
escapeHtml(row.label),
|
|
441
|
+
...["bst", "taxi", "se"].map((product) => escapeHtml(row.products[product] || "—")),
|
|
442
|
+
row.field ? code(row.field) : "—",
|
|
443
|
+
];
|
|
444
|
+
sections.push(` <h3 id="sa103s">Computed figures and their SA103S boxes</h3>
|
|
445
|
+
<p>
|
|
446
|
+
The three sole trader products fill the short self-employment pages, SA103S. The table gives every box on the ${boxes.year} form.
|
|
447
|
+
<em>figure</em> means the product computes the box. <em>derived</em> means the page works it out from the profit and loss account.
|
|
448
|
+
<em>blank</em> means the format carries nothing for that box, so it prints empty. Box numbers follow the ${boxes.year} form; nothing
|
|
449
|
+
here is the HMRC document itself.
|
|
450
|
+
</p>
|
|
451
|
+
<p>
|
|
452
|
+
The last column is HMRC's own field name for the box in the Making Tax Digital Self Employment Business API, from HMRC's published
|
|
453
|
+
SA103 mapping (<a href="${escapeHtml(boxes.source.url)}">${escapeHtml(boxes.source.csv)}</a>, read ${escapeHtml(boxes.source.read)}).
|
|
454
|
+
${boxesWithField} of the ${boxes.rows.length} boxes have one.
|
|
455
|
+
</p>
|
|
456
|
+
<p>
|
|
457
|
+
Self Employed books fill the full pages, SA103F, as well, and both they and Limited Company books fill a VAT return. Limited Company
|
|
458
|
+
books file a CT600 and micro-entity accounts rather than SA103S. Every one of those views is on the product's own DIYA-GL page, and
|
|
459
|
+
the figures behind them are on its <a href="reconciliation/index.html">reconciliation scorecard</a>.
|
|
460
|
+
</p>
|
|
461
|
+
${table(["Box", "Label", "Basic Sole Trader", "Taxi Driver", "Self Employed", "MTD API field"], boxes.rows.map(boxRow))}`);
|
|
462
|
+
|
|
463
|
+
const checkRow = (row) => [
|
|
464
|
+
code(row.id),
|
|
465
|
+
escapeHtml(CHECK_DESCRIPTIONS[row.id]),
|
|
466
|
+
row.tier === "check" ? "check" : "warning",
|
|
467
|
+
row.products.length === PRODUCT_ORDER.length ? "all" : row.products.map((product) => PRODUCT_NAMES[product]).join(", "),
|
|
468
|
+
];
|
|
469
|
+
sections.push(` <h3 id="checks">The check catalogue</h3>
|
|
470
|
+
<p>
|
|
471
|
+
A book is read in three stages. The two JSON Schemas validate <code>book.toml</code> and every line of <code>lines.jsonl</code>. The
|
|
472
|
+
validator then refuses a file whose lines reach an account the book does not declare, or name a hire purchase agreement, an asset or
|
|
473
|
+
a member the registers do not hold. Last, the ${checks.length} book checks below run over the data itself.
|
|
474
|
+
</p>
|
|
475
|
+
<p>
|
|
476
|
+
A <strong>check</strong> passes or fails, and a failure names the entries that caused it; ${checkCount} of the rules are checks. A
|
|
477
|
+
<strong>warning</strong> passes or warns and never blocks a save; ${warningCount} of them are warnings. Which rules a book runs
|
|
478
|
+
depends on the product it declares. Every result is written to <code>bookchecks.json</code> in the zip.
|
|
479
|
+
</p>
|
|
480
|
+
<p>
|
|
481
|
+
Beside these, the engine runs its reconciliation checks: one for every figure the workbook and the engine both compute, compared to
|
|
482
|
+
the penny. Those depend on the books in hand rather than on the format, and every one of them is published on the scorecards below.
|
|
483
|
+
</p>
|
|
484
|
+
${table(["Id", "What it reads", "Severity", "Products"], checks.map(checkRow))}`);
|
|
485
|
+
|
|
486
|
+
const zipRow = (entry) => [code(entry), escapeHtml(ZIP_ENTRY_NOTES[entry])];
|
|
487
|
+
sections.push(` <h3 id="zip">The zip layout</h3>
|
|
488
|
+
<p>The zip has no directories. Its entries are:</p>
|
|
489
|
+
${table(["Entry", "What it holds"], zip.entries.map(zipRow))}
|
|
490
|
+
<p>
|
|
491
|
+
Two writes of the same book produce the same bytes, on any machine and in any of the three surfaces (the browser page, the command
|
|
492
|
+
line tool and the MCP server). The rules that make that hold:
|
|
493
|
+
</p>
|
|
494
|
+
<ul>
|
|
495
|
+
<li>Lines sort by posting date, then source journal, account, entry number, line number, document reference and amount.</li>
|
|
496
|
+
<li>Fields within a line follow the order the lines schema declares them in, and a field with no value is left out.</li>
|
|
497
|
+
<li><code>book.toml</code> follows the book schema's table and key order, and every array of tables sorts by its own id field.</li>
|
|
498
|
+
<li>Money carries exactly two decimal places and a rate exactly four, trailing zeroes included.</li>
|
|
499
|
+
<li>Dates are ISO 8601, <code>YYYY-MM-DD</code>, with no time.</li>
|
|
500
|
+
<li>Every zip entry is stamped 1 January 1980, so the timestamp never varies between two writes.</li>
|
|
501
|
+
</ul>
|
|
502
|
+
<p>
|
|
503
|
+
The same book and lines also travel as one JSON file, with a
|
|
504
|
+
<code>{ "format": "${escapeHtml(zip.format)}", "version": ${escapeHtml(zip.version)}, "product": ..., "book": ..., "lines": [...] }</code>
|
|
505
|
+
envelope, and as that JSON zipped. The DIYA-GL pages read all three, plus a workbook and a package zip.
|
|
506
|
+
</p>`);
|
|
507
|
+
|
|
508
|
+
const stampRows = [
|
|
509
|
+
[
|
|
510
|
+
"format version",
|
|
511
|
+
`<code>${escapeHtml(zip.format)}</code> ${escapeHtml(zip.version)}`,
|
|
512
|
+
"Can this tool read this file",
|
|
513
|
+
"written today",
|
|
514
|
+
],
|
|
515
|
+
["engine version", "the package version, and the commit it was built from", "Which code produced these figures", "written today"],
|
|
516
|
+
["tax-data version", "a hash over the year's rate files", "Which rates were applied", "written today"],
|
|
517
|
+
["template version", "a hash over the product's workbook templates", "Which workbook this reproduces", "written today"],
|
|
518
|
+
[
|
|
519
|
+
"reconciled commit",
|
|
520
|
+
"the commit whose reconciliation run passed",
|
|
521
|
+
"The proof this release rests on",
|
|
522
|
+
"written by the reconciliation workflows' commit job, empty until the first one runs",
|
|
523
|
+
],
|
|
524
|
+
];
|
|
525
|
+
sections.push(` <h3 id="versioning">Versioning and provenance</h3>
|
|
526
|
+
<p>
|
|
527
|
+
The format is <code>${escapeHtml(zip.format)}</code> version ${escapeHtml(zip.version)}. The two schemas are at v2, and the version is
|
|
528
|
+
part of each schema's <code>$id</code> URL. A v1 existed but nothing outside this repository ever read it, so v2 replaced it in place.
|
|
529
|
+
</p>
|
|
530
|
+
<p>
|
|
531
|
+
Five stamps sit in the book's document info and at the head of <code>report.json</code>. They answer the question a file raises
|
|
532
|
+
years later. Recalculate a 2026 book in 2030 and it either reproduces its <code>report.json</code> byte for byte, or names the
|
|
533
|
+
stamp that differs.
|
|
534
|
+
</p>
|
|
535
|
+
${table(
|
|
536
|
+
["Stamp", "Value", "What it answers", "Status"],
|
|
537
|
+
stampRows.map((row) => [escapeHtml(row[0]), row[1], escapeHtml(row[2]), escapeHtml(row[3])]),
|
|
538
|
+
)}`);
|
|
539
|
+
|
|
540
|
+
// A scorecard's status, run count and date move with every generate run, so
|
|
541
|
+
// they stay on the scorecard; this table carries what does not move.
|
|
542
|
+
const scorecardRow = (card) => [
|
|
543
|
+
`<a href="reconciliation/${escapeHtml(card.page)}">${escapeHtml(card.name)}</a>`,
|
|
544
|
+
escapeHtml(card.featuredScenario),
|
|
545
|
+
];
|
|
546
|
+
sections.push(` <h3 id="evidence">Reconciliation evidence</h3>
|
|
547
|
+
<p>
|
|
548
|
+
Every figure this format computes is checked against the same figure in the spreadsheet. For each product, CI generates the
|
|
549
|
+
package from a set of books, recalculates the workbook in LibreOffice, computes the same figures from the diya-gl data alone, and
|
|
550
|
+
compares the two to the penny. Each scorecard carries the whole run: its status, the checks, the input transactions, screenshots
|
|
551
|
+
of the recalculated sheets, the accounting statements and the tax review.
|
|
552
|
+
</p>
|
|
553
|
+
${table(["Product", "Featured scenario"], scorecards.map(scorecardRow))}
|
|
554
|
+
<p><a href="reconciliation/index.html">All reconciliation reports</a></p>`);
|
|
555
|
+
|
|
556
|
+
sections.push(` <h3 id="tools">Getting the tools</h3>
|
|
557
|
+
<p>
|
|
558
|
+
The DIYA-GL pages read and write the format in your browser. Nothing you load leaves the machine.
|
|
559
|
+
</p>
|
|
560
|
+
<ul>
|
|
561
|
+
<li><a href="books/bst.html">Basic Sole Trader books</a></li>
|
|
562
|
+
<li><a href="books/taxi.html">Taxi Driver books</a></li>
|
|
563
|
+
<li><a href="books/se.html">Self Employed books</a></li>
|
|
564
|
+
<li><a href="books/ltd.html">Limited Company books</a></li>
|
|
565
|
+
</ul>
|
|
566
|
+
<p>
|
|
567
|
+
The same engine runs on the command line and as an MCP server, published as the npm package
|
|
568
|
+
<code>@diy-accounting-uk/diya-gl</code>. It is not on npm yet; this page will carry the install line when it is.
|
|
569
|
+
</p>
|
|
570
|
+
<p>
|
|
571
|
+
The spreadsheets themselves stay free to download on a donation basis. <a href="download.html">Download a package</a>, or
|
|
572
|
+
<a href="donate.html">chip in</a> if the tools earn it.
|
|
573
|
+
</p>`);
|
|
574
|
+
|
|
575
|
+
const toc = [
|
|
576
|
+
["what-it-is", "What DIYA-GL is"],
|
|
577
|
+
["declared-subset", "The declared subset"],
|
|
578
|
+
["sa103s", "Computed figures and their SA103S boxes"],
|
|
579
|
+
["checks", "The check catalogue"],
|
|
580
|
+
["zip", "The zip layout"],
|
|
581
|
+
["versioning", "Versioning and provenance"],
|
|
582
|
+
["evidence", "Reconciliation evidence"],
|
|
583
|
+
["tools", "Getting the tools"],
|
|
584
|
+
];
|
|
585
|
+
|
|
586
|
+
return `<!doctype html>
|
|
587
|
+
<html lang="en">
|
|
588
|
+
<head>
|
|
589
|
+
<meta charset="UTF-8" />
|
|
590
|
+
<title>${escapeHtml(PAGE_TITLE)}</title>
|
|
591
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
592
|
+
<meta name="description" content="${escapeHtml(PAGE_DESCRIPTION)}" />
|
|
593
|
+
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
|
594
|
+
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
|
595
|
+
<meta property="og:title" content="${escapeHtml(PAGE_TITLE)}" />
|
|
596
|
+
<meta property="og:description" content="${escapeHtml(PAGE_DESCRIPTION)}" />
|
|
597
|
+
<meta property="og:url" content="${CANONICAL_URL}" />
|
|
598
|
+
<meta property="og:site_name" content="DIY Accounting" />
|
|
599
|
+
<meta property="og:type" content="website" />
|
|
600
|
+
<link rel="canonical" href="${CANONICAL_URL}" />
|
|
601
|
+
<link rel="stylesheet" href="spreadsheets.css" />
|
|
602
|
+
<style>
|
|
603
|
+
.spec-scroll { overflow-x: auto; }
|
|
604
|
+
.spec-table { border-collapse: collapse; width: 100%; margin: 0.8em 0; font-size: 0.9em; }
|
|
605
|
+
.spec-table th, .spec-table td { border: 1px solid #ddd; padding: 0.35em 0.6em; text-align: left; vertical-align: top; }
|
|
606
|
+
.spec-table th { background: #f5f5f5; font-weight: 600; }
|
|
607
|
+
.spec-table code { white-space: nowrap; }
|
|
608
|
+
.spec-toc { margin: 1em 0 2em; padding: 0.8em 1em; background: #f7f7f7; border-radius: 4px; }
|
|
609
|
+
.spec-toc ol { margin: 0; padding-left: 1.4em; }
|
|
610
|
+
</style>
|
|
611
|
+
<script src="lib/analytics.js"></script>
|
|
612
|
+
<script src="lib/consent-banner.js"></script>
|
|
613
|
+
</head>
|
|
614
|
+
<body>
|
|
615
|
+
<a href="#mainContent" class="skip-link">Skip to main content</a>
|
|
616
|
+
|
|
617
|
+
<nav class="top-nav" aria-label="Main navigation">
|
|
618
|
+
<a href="index.html">Products</a>
|
|
619
|
+
<a href="download.html">Download</a>
|
|
620
|
+
<a href="knowledge-base.html">Knowledge Base</a>
|
|
621
|
+
<a href="community.html">Community</a>
|
|
622
|
+
<a href="https://submit.diyaccounting.co.uk">Submit VAT MTD</a>
|
|
623
|
+
<a href="donate.html">Donate</a>
|
|
624
|
+
</nav>
|
|
625
|
+
|
|
626
|
+
<header>
|
|
627
|
+
<h1>DIY Accounting Spreadsheets</h1>
|
|
628
|
+
<p class="subtitle">Excel bookkeeping and accounting software for UK small businesses</p>
|
|
629
|
+
</header>
|
|
630
|
+
|
|
631
|
+
<main id="mainContent">
|
|
632
|
+
<nav class="nav-back" aria-label="Breadcrumb"><a href="index.html">← Products</a></nav>
|
|
633
|
+
|
|
634
|
+
<h2 class="kb-page-title">The DIYA-GL format</h2>
|
|
635
|
+
<p class="kb-page-description">A year of accounts in two text files, and the mapping, the checks and the evidence behind them.</p>
|
|
636
|
+
|
|
637
|
+
<nav class="spec-toc" aria-label="On this page">
|
|
638
|
+
<ol>
|
|
639
|
+
${toc.map(([id, title]) => ` <li><a href="#${id}">${escapeHtml(title)}</a></li>`).join("\n")}
|
|
640
|
+
</ol>
|
|
641
|
+
</nav>
|
|
642
|
+
|
|
643
|
+
${sections.join("\n\n")}
|
|
644
|
+
</main>
|
|
645
|
+
|
|
646
|
+
<footer>
|
|
647
|
+
<div class="footer-content">
|
|
648
|
+
<div class="footer-left">
|
|
649
|
+
<a href="https://diyaccounting.co.uk">diyaccounting.co.uk</a>
|
|
650
|
+
<a href="knowledge-base.html">knowledge base</a>
|
|
651
|
+
<a href="reconciliation/index.html">reconciliation</a>
|
|
652
|
+
<a href="https://submit.diyaccounting.co.uk/privacy.html">privacy</a>
|
|
653
|
+
<a href="https://submit.diyaccounting.co.uk/terms.html">terms</a>
|
|
654
|
+
<a href="https://submit.diyaccounting.co.uk/accessibility.html">accessibility</a>
|
|
655
|
+
</div>
|
|
656
|
+
<div class="footer-center">
|
|
657
|
+
<p>© 2025-2026 DIY Accounting Limited</p>
|
|
658
|
+
</div>
|
|
659
|
+
</div>
|
|
660
|
+
</footer>
|
|
661
|
+
</body>
|
|
662
|
+
</html>
|
|
663
|
+
`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* The diya-gl specification page, built from the schemas, the HMRC form
|
|
668
|
+
* layouts, the check catalogue, the interchange writer and the published
|
|
669
|
+
* reconciliation scorecards.
|
|
670
|
+
* @returns {string} the whole HTML document
|
|
671
|
+
*/
|
|
672
|
+
export function buildDiyaGlSpecHtml() {
|
|
673
|
+
return buildPage();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
677
|
+
const html = buildDiyaGlSpecHtml();
|
|
678
|
+
writeFileSync(OUT_PATH, html, "utf8");
|
|
679
|
+
console.log(`diya-gl spec page: ${OUT_PATH} (${html.length} bytes)`);
|
|
680
|
+
}
|