@diy-accounting-uk/diya-gl 1.2.40 → 1.2.42
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 +7 -1
- package/bin/diya-gl-link.js +9 -0
- package/bin/diya-gl.js +2 -1
- package/dist/app/bin/export.js +9 -2
- package/dist/app/bin/link.js +81 -0
- package/dist/app/bin/report.js +8 -3
- package/dist/app/data/releases.json +52 -0
- package/dist/app/lib/book-checks/taxi.js +7 -1
- package/dist/app/lib/book-checks.js +32 -6
- package/dist/app/lib/calculators/bst.js +114 -2
- package/dist/app/lib/calculators/ltd.js +624 -21
- package/dist/app/lib/calculators/se.js +505 -18
- package/dist/app/lib/calculators/shared.js +12 -2
- package/dist/app/lib/calculators/taxi.js +144 -4
- package/dist/app/lib/diya-gl-calculator.js +8 -5
- package/dist/app/lib/diya-gl-link.js +51 -0
- package/dist/app/lib/diya-gl-loader.js +31 -20
- package/dist/app/lib/entry-attribution.js +124 -0
- package/dist/app/lib/mcp/diya-gl-tools.js +72 -44
- package/dist/app/lib/report-generator.js +16 -4
- package/dist/app/lib/report-serializer.js +105 -12
- package/dist/app/lib/scenario-extractor.js +48 -26
- package/dist/app/lib/scenario-loader.js +2 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -37,11 +37,12 @@ docker run --rm -v "$PWD":/data ghcr.io/diy-accounting-uk/diya-gl:latest \
|
|
|
37
37
|
diya-gl recalc --package bst --data my-book --years se-2025-2026 --output-dir out
|
|
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
|
+
diya-gl link my-book
|
|
40
41
|
diya-gl mcp
|
|
41
42
|
```
|
|
42
43
|
|
|
43
44
|
Each subcommand is also its own command, if you only want one on your `PATH`:
|
|
44
|
-
`diya-gl-recalc`, `diya-gl-read-workbook`, `diya-gl-write-workbook`, `diya-gl-mcp`.
|
|
45
|
+
`diya-gl-recalc`, `diya-gl-read-workbook`, `diya-gl-write-workbook`, `diya-gl-link`, `diya-gl-mcp`.
|
|
45
46
|
|
|
46
47
|
- **recalc** takes a diya-gl book (`--data`) or a populated workbook (`--source-dir`,
|
|
47
48
|
`--mode saved|recalculate`) and writes `report.json`: every figure, report section and
|
|
@@ -51,6 +52,11 @@ Each subcommand is also its own command, if you only want one on your `PATH`:
|
|
|
51
52
|
- **write-workbook** takes a diya-gl book (`--data` or `--file`) and writes the Excel
|
|
52
53
|
package its product composes onto its template — the same client-side path the DIYA-GL
|
|
53
54
|
pages use, so it never needs LibreOffice. Add `--zip` for the package as one zip.
|
|
55
|
+
- **link** takes a book (a directory with `book.toml` and `lines.jsonl`, a diya-gl zip or a
|
|
56
|
+
diya-gl JSON file) and prints the `https://diya-gl.co.uk/<product>.html#book=<data>` link that
|
|
57
|
+
opens it, the whole book carried in the fragment so nothing is uploaded. `--base <url>`
|
|
58
|
+
replaces the origin. The fragment length goes to stderr, with a warning past 64 KB (about
|
|
59
|
+
2,000 lines), where chat, email and QR codes start to cut links off.
|
|
54
60
|
- **mcp** runs a stdio [MCP](https://modelcontextprotocol.io) server with four tools:
|
|
55
61
|
`extract_book`, `report`, `edit_lines`, `save_workbook`. Point an MCP client at
|
|
56
62
|
`diya-gl-mcp` (or `diya-gl mcp`) with no arguments.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// Copyright (C) 2006-2026 DIY Accounting Limited
|
|
4
|
+
//
|
|
5
|
+
// diya-gl-link.js — print the diya-gl.co.uk link that opens a whole book,
|
|
6
|
+
// the book carried in the URL fragment.
|
|
7
|
+
|
|
8
|
+
import { runDistBin } from "./run-dist-bin.js";
|
|
9
|
+
runDistBin("app/bin/link.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|mcp> [args].
|
|
5
|
+
// diya-gl.js — the dispatcher: diya-gl <recalc|read-workbook|write-workbook|link|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
|
|
|
@@ -16,6 +16,7 @@ const SUBCOMMANDS = {
|
|
|
16
16
|
"recalc": "app/bin/report.js",
|
|
17
17
|
"read-workbook": "app/bin/export.js",
|
|
18
18
|
"write-workbook": "app/bin/write-workbook.js",
|
|
19
|
+
"link": "app/bin/link.js",
|
|
19
20
|
"mcp": "app/bin/diya-gl-mcp.js",
|
|
20
21
|
};
|
|
21
22
|
|
package/dist/app/bin/export.js
CHANGED
|
@@ -132,10 +132,16 @@ function parseArgs(argv) {
|
|
|
132
132
|
// buildReportDocument -- the same loop report.js's --data mode runs, so the
|
|
133
133
|
// CLI's --file mode, report.js and the MCP server's tools never diverge on
|
|
134
134
|
// how R is built from the same D. Pure: no disk access, no console output.
|
|
135
|
-
|
|
135
|
+
//
|
|
136
|
+
// attribute asks for each attributed figure's entryNumbers. A year's book
|
|
137
|
+
// lists every line under each total it reaches, which multiplies R's size
|
|
138
|
+
// many times over, so the written report.json leaves them out and a caller
|
|
139
|
+
// that drills from a figure to its lines asks for them.
|
|
140
|
+
export function buildFileReportDocument(book, lines, packageName, productMod, { attribute = false } = {}) {
|
|
136
141
|
const taxData = extractTaxDataFromBook(book, packageName);
|
|
137
142
|
const scenario = diyaGlToScenario(book, lines, packageName);
|
|
138
|
-
const
|
|
143
|
+
const attribution = attribute ? {} : undefined;
|
|
144
|
+
const results = calculateFromDiyaGl(book, lines, packageName, taxData, scenario, { attribution });
|
|
139
145
|
const mergedScenario = { ...scenario, ...scenario.expected };
|
|
140
146
|
const periodEnd = book.documentInfo?.periodCoveredEnd;
|
|
141
147
|
const yearEnd = periodEnd ? new Date(periodEnd).toISOString().slice(0, 10) : null;
|
|
@@ -147,6 +153,7 @@ export function buildFileReportDocument(book, lines, packageName, productMod) {
|
|
|
147
153
|
packageName,
|
|
148
154
|
engine: "js",
|
|
149
155
|
results,
|
|
156
|
+
attribution,
|
|
150
157
|
productMod,
|
|
151
158
|
scenario: mergedScenario,
|
|
152
159
|
taxData,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// Copyright (C) 2006-2026 DIY Accounting Limited
|
|
4
|
+
//
|
|
5
|
+
// link.js — print the diya-gl.co.uk link that opens a whole book, the book
|
|
6
|
+
// carried in the URL fragment.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// node app/bin/link.js examples/basic-taxi-driver
|
|
10
|
+
// node app/bin/link.js my-book-diya-gl.zip
|
|
11
|
+
// node app/bin/link.js my-book.json --base http://localhost:8080
|
|
12
|
+
//
|
|
13
|
+
// The book is a directory holding book.toml and lines.jsonl, or any diya-gl
|
|
14
|
+
// zip or JSON file diya-gl-interchange.js reads. The link goes to stdout;
|
|
15
|
+
// its length and any warning go to stderr.
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
18
|
+
import { join, resolve } from "path";
|
|
19
|
+
import { fileURLToPath } from "url";
|
|
20
|
+
import { parseDiyaGlData } from "../lib/diya-gl-loader.js";
|
|
21
|
+
import { readBookSource } from "../lib/diya-gl-interchange.js";
|
|
22
|
+
import { canonicalBookToml, canonicalLinesJsonl } from "../lib/diya-gl-canonical.js";
|
|
23
|
+
import { productIdOf } from "../lib/xlsx-exporter.js";
|
|
24
|
+
import { PRODUCTS } from "../lib/products.js";
|
|
25
|
+
import { encodeBookFragment, FRAGMENT_KEY } from "../lib/diya-gl-link.js";
|
|
26
|
+
|
|
27
|
+
const DEFAULT_BASE = "https://diya-gl.co.uk";
|
|
28
|
+
const USAGE = "Usage: node app/bin/link.js <book directory | diya-gl zip | diya-gl JSON file> [--base <url>]";
|
|
29
|
+
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const args = argv.slice(2);
|
|
32
|
+
let base = DEFAULT_BASE;
|
|
33
|
+
const positional = [];
|
|
34
|
+
for (let i = 0; i < args.length; i++) {
|
|
35
|
+
if (args[i] === "--base") {
|
|
36
|
+
base = args[++i];
|
|
37
|
+
if (!base) throw new Error(`--base needs a URL. ${USAGE}`);
|
|
38
|
+
} else {
|
|
39
|
+
positional.push(args[i]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (positional.length !== 1) throw new Error(USAGE);
|
|
43
|
+
return { source: positional[0], base: base.replace(/\/+$/, "") };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function readBookText(source) {
|
|
47
|
+
const path = resolve(source);
|
|
48
|
+
if (!existsSync(path)) throw new Error(`${source} does not exist.`);
|
|
49
|
+
if (statSync(path).isDirectory()) {
|
|
50
|
+
const toml = readFileSync(join(path, "book.toml"), "utf8");
|
|
51
|
+
const lines = readFileSync(join(path, "lines.jsonl"), "utf8");
|
|
52
|
+
return { toml, lines, book: parseDiyaGlData(toml, lines).book };
|
|
53
|
+
}
|
|
54
|
+
const { book, lines } = await readBookSource(readFileSync(path), source, { products: PRODUCTS });
|
|
55
|
+
return { toml: canonicalBookToml(book), lines: canonicalLinesJsonl(lines), book };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function bookLink(source, base = DEFAULT_BASE) {
|
|
59
|
+
const { toml, lines, book } = await readBookText(source);
|
|
60
|
+
const declared = book.entityInformation && book.entityInformation["diya-gl:product"];
|
|
61
|
+
const product = productIdOf(declared);
|
|
62
|
+
if (!product)
|
|
63
|
+
throw new Error(`The book declares no product with a page: entityInformation."diya-gl:product" is ${JSON.stringify(declared)}.`);
|
|
64
|
+
const { fragment, length, warning } = encodeBookFragment({ toml, lines });
|
|
65
|
+
return { url: `${base}/${product}.html#${FRAGMENT_KEY}=${fragment}`, length, warning };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function main() {
|
|
69
|
+
const { source, base } = parseArgs(process.argv);
|
|
70
|
+
const { url, length, warning } = await bookLink(source, base);
|
|
71
|
+
console.log(url);
|
|
72
|
+
console.error(`Fragment length: ${length} characters`);
|
|
73
|
+
if (warning) console.error(`Warning: ${warning}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
77
|
+
main().catch((error) => {
|
|
78
|
+
console.error(error.message);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
});
|
|
81
|
+
}
|
package/dist/app/bin/report.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
// --mode saved (default) Read xlsx cell values as-is from XML. No LibreOffice needed.
|
|
14
14
|
// --mode recalculate Run xls roundtrip first, then read. Requires LibreOffice.
|
|
15
15
|
// --data <dir> Compute reports from diya-gl data via JS engine. No Excel needed.
|
|
16
|
+
// --entry-numbers With --data, give each attributed figure in report.json the
|
|
17
|
+
// entryNumbers of the ledger lines behind it.
|
|
16
18
|
//
|
|
17
19
|
// Either mode writes report.json beside the markdown: R, the canonical
|
|
18
20
|
// document verify-roundtrip.js scores. --data also writes its own input to
|
|
@@ -65,6 +67,7 @@ function parseArgs(argv) {
|
|
|
65
67
|
const offset = getArg("--offset");
|
|
66
68
|
const years = getArg("--years");
|
|
67
69
|
const yearEndArg = getArg("--year-end");
|
|
70
|
+
const entryNumbers = args.includes("--entry-numbers");
|
|
68
71
|
|
|
69
72
|
if (!packageName) {
|
|
70
73
|
console.error("Error: --package is required (bst, taxi, se, ltd)");
|
|
@@ -79,7 +82,7 @@ function parseArgs(argv) {
|
|
|
79
82
|
process.exit(1);
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
return { packageName, sourceDir, outputDir, mode, dataDir, offset, years, yearEndArg };
|
|
85
|
+
return { packageName, sourceDir, outputDir, mode, dataDir, offset, years, yearEndArg, entryNumbers };
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
// R, the canonical report document, beside the markdown. Both engines write
|
|
@@ -92,7 +95,7 @@ function writeReportJson(outputDir, options) {
|
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
async function main() {
|
|
95
|
-
const { packageName, sourceDir, outputDir, mode, dataDir, offset, years, yearEndArg } = parseArgs(process.argv);
|
|
98
|
+
const { packageName, sourceDir, outputDir, mode, dataDir, offset, years, yearEndArg, entryNumbers } = parseArgs(process.argv);
|
|
96
99
|
|
|
97
100
|
const productMod = PRODUCTS[packageName];
|
|
98
101
|
if (!productMod) {
|
|
@@ -129,7 +132,8 @@ async function main() {
|
|
|
129
132
|
// on the Excel side: opening balances, stock, debtors, creditors and
|
|
130
133
|
// business details. Passing it through lights those up on the JS side too.
|
|
131
134
|
const scenario = diyaGlToScenario(book, lines, packageName);
|
|
132
|
-
const
|
|
135
|
+
const attribution = entryNumbers ? {} : undefined;
|
|
136
|
+
const results = calculateFromDiyaGl(book, lines, packageName, taxData, scenario, { attribution });
|
|
133
137
|
|
|
134
138
|
// Fixture anchors (opening_debtors, closing_creditors, ...) are top-level
|
|
135
139
|
// scenario tables, not [expected] keys, so checks that anchor against
|
|
@@ -156,6 +160,7 @@ async function main() {
|
|
|
156
160
|
packageName,
|
|
157
161
|
engine: "js",
|
|
158
162
|
results,
|
|
163
|
+
attribution,
|
|
159
164
|
productMod,
|
|
160
165
|
scenario: mergedScenario,
|
|
161
166
|
taxData,
|
|
@@ -1273,6 +1273,58 @@
|
|
|
1273
1273
|
"scorecard": "101605 passed, 0 warnings, 0 failed"
|
|
1274
1274
|
}
|
|
1275
1275
|
}
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
"tag": "diya-gl-v1.2.40",
|
|
1279
|
+
"date": "2026-09-30",
|
|
1280
|
+
"formatVersion": "diya-gl/1",
|
|
1281
|
+
"engineVersion": "1.2.40+57de8f55b",
|
|
1282
|
+
"taxDataHash": "89424bd36c0d",
|
|
1283
|
+
"reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
|
|
1284
|
+
"templates": {
|
|
1285
|
+
"bst": {
|
|
1286
|
+
"hash": "0d003e10e00c",
|
|
1287
|
+
"scorecard": "812 passed, 0 warnings, 0 failed"
|
|
1288
|
+
},
|
|
1289
|
+
"taxi": {
|
|
1290
|
+
"hash": "1ff69654d64d",
|
|
1291
|
+
"scorecard": "700 passed, 0 warnings, 0 failed"
|
|
1292
|
+
},
|
|
1293
|
+
"se": {
|
|
1294
|
+
"hash": "064605ae76ed",
|
|
1295
|
+
"scorecard": "8014 passed, 0 warnings, 0 failed"
|
|
1296
|
+
},
|
|
1297
|
+
"ltd": {
|
|
1298
|
+
"hash": "5bb7be9b6d97",
|
|
1299
|
+
"scorecard": "101605 passed, 0 warnings, 0 failed"
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
"tag": "diya-gl-v1.2.41",
|
|
1305
|
+
"date": "2026-10-01",
|
|
1306
|
+
"formatVersion": "diya-gl/1",
|
|
1307
|
+
"engineVersion": "1.2.40+57de8f55b",
|
|
1308
|
+
"taxDataHash": "89424bd36c0d",
|
|
1309
|
+
"reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
|
|
1310
|
+
"templates": {
|
|
1311
|
+
"bst": {
|
|
1312
|
+
"hash": "0d003e10e00c",
|
|
1313
|
+
"scorecard": "812 passed, 0 warnings, 0 failed"
|
|
1314
|
+
},
|
|
1315
|
+
"taxi": {
|
|
1316
|
+
"hash": "1ff69654d64d",
|
|
1317
|
+
"scorecard": "700 passed, 0 warnings, 0 failed"
|
|
1318
|
+
},
|
|
1319
|
+
"se": {
|
|
1320
|
+
"hash": "064605ae76ed",
|
|
1321
|
+
"scorecard": "8014 passed, 0 warnings, 0 failed"
|
|
1322
|
+
},
|
|
1323
|
+
"ltd": {
|
|
1324
|
+
"hash": "5bb7be9b6d97",
|
|
1325
|
+
"scorecard": "101605 passed, 0 warnings, 0 failed"
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1276
1328
|
}
|
|
1277
1329
|
]
|
|
1278
1330
|
}
|
|
@@ -212,4 +212,10 @@ export const TAXI_BOOK_HELPERS = {
|
|
|
212
212
|
},
|
|
213
213
|
};
|
|
214
214
|
|
|
215
|
-
export const TAXI_PRODUCT_RULES = {
|
|
215
|
+
export const TAXI_PRODUCT_RULES = {
|
|
216
|
+
checks: [],
|
|
217
|
+
warnings: TAXI_WARNINGS,
|
|
218
|
+
sharedOffenders: {},
|
|
219
|
+
omittedSharedWarnings: ["book-missing-document-reference"],
|
|
220
|
+
bookHelpers: TAXI_BOOK_HELPERS,
|
|
221
|
+
};
|
|
@@ -407,6 +407,26 @@ function emptyDetailWarning(ctx) {
|
|
|
407
407
|
};
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
function missingDocumentReferenceWarning(ctx) {
|
|
411
|
+
const offenders = ctx.lines
|
|
412
|
+
.filter(function (line) {
|
|
413
|
+
return line.sourceJournalID === "sales" && (!line.documentReference || !String(line.documentReference).trim());
|
|
414
|
+
})
|
|
415
|
+
.sort(byEntryNumber);
|
|
416
|
+
const warn = offenders.length > 0;
|
|
417
|
+
return {
|
|
418
|
+
id: "book-missing-document-reference",
|
|
419
|
+
tier: "warning",
|
|
420
|
+
label: "Every sale names its invoice or receipt",
|
|
421
|
+
result: warn ? "warn" : "pass",
|
|
422
|
+
actual: offenders.length,
|
|
423
|
+
consequence: warn
|
|
424
|
+
? "A sale with no invoice or receipt reference cannot be matched to its document when the books are reviewed later."
|
|
425
|
+
: null,
|
|
426
|
+
offenders: offenders.map(offenderOf),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
410
430
|
function negativeAmountWarning(ctx) {
|
|
411
431
|
const offenders = ctx.lines
|
|
412
432
|
.filter(function (line) {
|
|
@@ -469,14 +489,20 @@ function runWarnings(ctx, taxData, results) {
|
|
|
469
489
|
vatWarning(ctx, taxData),
|
|
470
490
|
duplicateEntriesWarning(ctx),
|
|
471
491
|
emptyDetailWarning(ctx),
|
|
492
|
+
missingDocumentReferenceWarning(ctx),
|
|
472
493
|
negativeAmountWarning(ctx),
|
|
473
494
|
emptyMonthWarning(ctx),
|
|
474
|
-
]
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
495
|
+
]
|
|
496
|
+
.filter(function (warning) {
|
|
497
|
+
// A product whose sales carry no document leaves that shared warning out.
|
|
498
|
+
return !(ctx.productRules.omittedSharedWarnings || []).includes(warning.id);
|
|
499
|
+
})
|
|
500
|
+
.map(function (warning) {
|
|
501
|
+
// A product whose sheets measure a shared warning differently replaces
|
|
502
|
+
// the whole result, keeping its id; every other product keeps this one.
|
|
503
|
+
const readDifferently = (ctx.productRules.sharedWarnings || {})[warning.id];
|
|
504
|
+
return readDifferently ? readDifferently(ctx, taxData, warning) : warning;
|
|
505
|
+
});
|
|
480
506
|
return shared.concat(
|
|
481
507
|
ctx.productRules.warnings.map(function (warning) {
|
|
482
508
|
return warning(ctx, taxData, results);
|
|
@@ -13,6 +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
17
|
|
|
17
18
|
const BST_MONTH_COLS = {
|
|
18
19
|
apr: "D",
|
|
@@ -78,7 +79,15 @@ function outstandingTotal(transactions) {
|
|
|
78
79
|
return (transactions || []).reduce((sum, tx) => (tx.payment || tx.mileage ? sum : sum + (tx.amount || 0)), 0);
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
/**
|
|
83
|
+
* @param {Object} book - parsed book.toml
|
|
84
|
+
* @param {Array} lines - parsed lines.jsonl entries
|
|
85
|
+
* @param {Object} taxData
|
|
86
|
+
* @param {Object} scenario
|
|
87
|
+
* @param {Object} [attribution] - filled in place with the entryNumbers behind each cell (entry-attribution.js)
|
|
88
|
+
* @returns {Object} { "SheetName": { "CellRef": value } }
|
|
89
|
+
*/
|
|
90
|
+
export function calculateBstResults(book, lines, taxData, scenario, attribution) {
|
|
82
91
|
// A book's own declared chart can number its purchase accounts under a
|
|
83
92
|
// scheme other than the Basic Sole Trader master's own (see
|
|
84
93
|
// resolveBstPurchaseCodeMap in diya-gl-loader.js); this must resolve to
|
|
@@ -105,7 +114,8 @@ export function calculateBstResults(book, lines, taxData, scenario) {
|
|
|
105
114
|
// nothing, and cellWrites gives the sheet its miles rather than its amount
|
|
106
115
|
// so the sheet can price the claim at its own Admin rates.
|
|
107
116
|
const cashPurchaseLines = purchaseLines.filter((l) => !carriesBusinessMiles(l));
|
|
108
|
-
const
|
|
117
|
+
const purchaseEntries = {};
|
|
118
|
+
const byCode = aggregateByCode(cashPurchaseLines, purchaseCodeMap, purchaseEntries);
|
|
109
119
|
|
|
110
120
|
// The mileage claim the sheet makes of those miles. It reaches Motor
|
|
111
121
|
// Expenses (verified against the template: PurchasesApr!G4 bands the running
|
|
@@ -377,5 +387,107 @@ export function calculateBstResults(book, lines, taxData, scenario) {
|
|
|
377
387
|
results.PurchasesMar.C1 = businessMiles;
|
|
378
388
|
results.PurchasesMar.A1 = Math.round(mileageAllowance * 100) / 100;
|
|
379
389
|
|
|
390
|
+
if (attribution) attributeBstResults(attribution, { results, scenario, salesLines, purchaseEntries, assetAdditions });
|
|
380
391
|
return results;
|
|
381
392
|
}
|
|
393
|
+
|
|
394
|
+
// The scenario transactions a mileage figure is read from: every one that
|
|
395
|
+
// records business miles, the same set scenarioBusinessMiles() sums.
|
|
396
|
+
function mileageTransactions(scenario) {
|
|
397
|
+
const transactions = [];
|
|
398
|
+
for (const table of [scenario.sales, scenario.purchases]) {
|
|
399
|
+
for (const monthTransactions of Object.values(table || {})) {
|
|
400
|
+
for (const tx of monthTransactions) if (typeof tx.mileage === "number") transactions.push(tx);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return transactions;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// The entries behind each cell calculateBstResults() writes, cell for cell
|
|
407
|
+
// beside the arithmetic above: a line-fed cell takes its own lines, and a
|
|
408
|
+
// derived cell the union of the cells it is computed from.
|
|
409
|
+
function attributeBstResults(attribution, { results, scenario, salesLines, purchaseEntries, assetAdditions }) {
|
|
410
|
+
const a = attributionWriter(attribution);
|
|
411
|
+
const PL = "Profit & Loss Acc";
|
|
412
|
+
const code = (letter) => purchaseEntries[letter];
|
|
413
|
+
const mileage = entriesOf(mileageTransactions(scenario));
|
|
414
|
+
const assets = entriesOf(assetAdditions);
|
|
415
|
+
|
|
416
|
+
for (const sheet of ["Admin", "Business Details", "PurchasesStock"]) {
|
|
417
|
+
for (const cell of Object.keys(results[sheet])) a.set(sheet, cell);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
a.set(PL, "C4", salesLines);
|
|
421
|
+
for (const month of MONTH_ORDER) {
|
|
422
|
+
a.set(
|
|
423
|
+
PL,
|
|
424
|
+
`${BST_MONTH_COLS[month]}4`,
|
|
425
|
+
salesLines.filter((line) => getMonthKey(line.postingDate) === month),
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
a.set(PL, "C6", code("s"));
|
|
429
|
+
a.set(PL, "C7", code("d"));
|
|
430
|
+
a.set(PL, "C9", a.cells(PL, "C4", "C6", "C7"));
|
|
431
|
+
const expenseCells = { C11: "e", C12: "p", C13: "r", C14: "g", C16: "t", C17: "a", C18: "l", C19: "b", C20: "i", C21: "o" };
|
|
432
|
+
for (const [cell, letter] of Object.entries(expenseCells)) a.set(PL, cell, code(letter));
|
|
433
|
+
a.set(PL, "C15", code("m"), mileage);
|
|
434
|
+
a.set(PL, "C22", a.cells(PL, "C11", "C12", "C13", "C14", "C15", "C16", "C17", "C18", "C19", "C20", "C21"));
|
|
435
|
+
a.set(PL, "C24", a.cells(PL, "C9", "C22"));
|
|
436
|
+
|
|
437
|
+
for (const cell of Object.keys(results["Fixed Assets"])) a.set("Fixed Assets", cell, assets);
|
|
438
|
+
a.set("PurchasesMar", "X1", assets);
|
|
439
|
+
a.set("PurchasesMar", "C1", mileage);
|
|
440
|
+
a.set("PurchasesMar", "A1", mileage);
|
|
441
|
+
|
|
442
|
+
const SE = "SE Short";
|
|
443
|
+
for (const cell of ["D80", "O80", "D85", "O85"]) a.set(SE, cell, assets);
|
|
444
|
+
a.set(PL, "C26", assets);
|
|
445
|
+
a.set(PL, "C28", a.cells(PL, "C24", "C26"));
|
|
446
|
+
a.set(PL, "C30");
|
|
447
|
+
|
|
448
|
+
a.set(SE, "D38", a.get(PL, "C4"));
|
|
449
|
+
if (results[SE].D46 !== undefined) {
|
|
450
|
+
a.set(SE, "D46", a.cells(PL, "C6", "C7"));
|
|
451
|
+
a.set(SE, "D51", a.cells(PL, "C15", "C16"));
|
|
452
|
+
a.set(SE, "D55", a.get(PL, "C11"));
|
|
453
|
+
a.set(SE, "D60", a.get(PL, "C12"));
|
|
454
|
+
a.set(SE, "D64", a.get(PL, "C13"));
|
|
455
|
+
}
|
|
456
|
+
a.set(SE, "D71", a.cells(PL, "C4", "C6", "C7", "C22"));
|
|
457
|
+
a.set(SE, "O71", a.get(SE, "D71"));
|
|
458
|
+
a.set(SE, "D94");
|
|
459
|
+
a.set(SE, "O94");
|
|
460
|
+
a.set(SE, "D99", a.cells(SE, "D71", "O71", "D80", "D85", "O80", "O85"));
|
|
461
|
+
a.set(SE, "O99", a.get(PL, "C30"));
|
|
462
|
+
a.set(SE, "D106", a.cells(SE, "D99", "O99"));
|
|
463
|
+
|
|
464
|
+
const IT = "Income Tax";
|
|
465
|
+
const profit = a.get(SE, "D106");
|
|
466
|
+
for (const cell of ["E5", "E6", "E7", "E8", "E9", "E10", "E11", "E15", "E16"]) a.set(IT, cell, profit);
|
|
467
|
+
for (const cell of ["D8", "C9", "D9", "C10", "D10"]) a.set(IT, cell);
|
|
468
|
+
a.set(
|
|
469
|
+
IT,
|
|
470
|
+
"E12",
|
|
471
|
+
salesLines.filter((line) => line[CIS_DEDUCTION_FIELD]),
|
|
472
|
+
);
|
|
473
|
+
a.set(IT, "E18", a.cells(IT, "E11", "E12", "E15", "E16"));
|
|
474
|
+
a.set(PL, "C32", a.cells(IT, "E11", "E12"));
|
|
475
|
+
a.set(PL, "C33", a.cells(IT, "E15", "E16"));
|
|
476
|
+
a.set(PL, "C35", a.cells(PL, "C28", "C30", "C32", "C33"));
|
|
477
|
+
|
|
478
|
+
// Debtors & Creditors: each month row is the month's transactions with
|
|
479
|
+
// nothing recorded in the payment column, the same rows outstandingTotal()
|
|
480
|
+
// sums; the column totals add the opening figure the book states.
|
|
481
|
+
const outstanding = (transactions) => (transactions || []).filter((tx) => !(tx.payment || tx.mileage));
|
|
482
|
+
const DC = "Debtors & Creditors";
|
|
483
|
+
a.set(DC, "C3");
|
|
484
|
+
a.set(DC, "F3");
|
|
485
|
+
MONTH_ORDER.forEach((month, index) => {
|
|
486
|
+
const row = 5 + index * 2;
|
|
487
|
+
a.set(DC, `C${row}`, entriesOf(outstanding(scenario.sales?.[month])));
|
|
488
|
+
a.set(DC, `F${row}`, entriesOf(outstanding(scenario.purchases?.[month])));
|
|
489
|
+
});
|
|
490
|
+
const rows = MONTH_ORDER.map((_, index) => 5 + index * 2);
|
|
491
|
+
a.set(DC, "C29", a.cells(DC, "C3", ...rows.map((row) => `C${row}`)));
|
|
492
|
+
a.set(DC, "F29", a.cells(DC, "F3", ...rows.map((row) => `F${row}`)));
|
|
493
|
+
}
|