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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -196,9 +196,10 @@ format text is CC BY 4.0, so you can implement the format in anything with attri
196
196
  The workbook templates are not part of this package. `write-workbook` and the MCP `save_workbook`
197
197
  tool fetch the template they need from
198
198
  [spreadsheets.diyaccounting.co.uk](https://spreadsheets.diyaccounting.co.uk) the first time they run,
199
- print the terms, and cache it for later runs. The templates are the company's own work, under the
200
- PolyForm Internal Use License 1.0.0 with an additional grant: use them for your own accounts, or for
201
- your clients' accounts if you are an accountant or a bookkeeper, and do not redistribute them.
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.
202
203
 
203
204
  This repository does not accept contributions.
204
205
 
@@ -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 { dirname, join, resolve } from "path";
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);
@@ -1455,6 +1455,58 @@
1455
1455
  "scorecard": "101608 passed, 0 warnings, 0 failed"
1456
1456
  }
1457
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
+ }
1484
+ },
1485
+ {
1486
+ "tag": "diya-gl-v1.2.48",
1487
+ "date": "2026-10-05",
1488
+ "formatVersion": "diya-gl/1",
1489
+ "engineVersion": "1.2.48+bf25fb49",
1490
+ "taxDataHash": "51bdff60e993",
1491
+ "reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
1492
+ "templates": {
1493
+ "bst": {
1494
+ "hash": "0d003e10e00c",
1495
+ "scorecard": "812 passed, 0 warnings, 0 failed"
1496
+ },
1497
+ "taxi": {
1498
+ "hash": "1ff69654d64d",
1499
+ "scorecard": "700 passed, 0 warnings, 0 failed"
1500
+ },
1501
+ "se": {
1502
+ "hash": "064605ae76ed",
1503
+ "scorecard": "8015 passed, 0 warnings, 0 failed"
1504
+ },
1505
+ "ltd": {
1506
+ "hash": "a65ab09cf35c",
1507
+ "scorecard": "101608 passed, 0 warnings, 0 failed"
1508
+ }
1509
+ }
1458
1510
  }
1459
1511
  ]
1460
1512
  }
@@ -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 cached = resolve(cacheRoot, "diya-gl", path);
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}`;
@@ -12,9 +12,7 @@
12
12
  // this module never restates a box's API field name in code, so a mapping
13
13
  // correction there reaches both derivations with no code change.
14
14
 
15
- import { readFileSync } from "fs";
16
- import { resolve, dirname } from "path";
17
- import { fileURLToPath } from "url";
15
+ import mapping from "../../data/hmrc/sa103-mtd-mapping.json" with { type: "json" };
18
16
  import { diyaGlToScenario } from "../diya-gl-loader.js";
19
17
  import { calculateSeCells } from "./se.js";
20
18
  import { SE_YEAR_END_MONTH, SBA_CLAIM_ROWS } from "../../products/se.js";
@@ -22,17 +20,8 @@ import { extractTaxYearStart, parseDate } from "../scenario-loader.js";
22
20
  import { shiftMonths, periodShiftMonths } from "../period-shift.js";
23
21
  import { splitVat } from "../tax/vat.js";
24
22
 
25
- let cachedMapping = null;
26
-
27
- // Resolved on first use, not at import: the engine bundle loads in the browser,
28
- // where import.meta.url is an http URL and fileURLToPath throws.
29
23
  export function loadMapping() {
30
- if (!cachedMapping) {
31
- const here = dirname(fileURLToPath(import.meta.url));
32
- const mappingPath = resolve(here, "..", "..", "data", "hmrc", "sa103-mtd-mapping.json");
33
- cachedMapping = JSON.parse(readFileSync(mappingPath, "utf8"));
34
- }
35
- return cachedMapping;
24
+ return mapping;
36
25
  }
37
26
 
38
27
  function sa103fBoxes() {
@@ -49,8 +49,8 @@ function taxYearFileNameFor(label) {
49
49
  return `se-${start}-${end}`;
50
50
  }
51
51
 
52
- async function taxDataFor(book, taxYear) {
53
- const options = taxYear ? { taxYearName: taxYearFileNameFor(taxYear) } : {};
52
+ async function taxDataFor(book, taxYear, resources) {
53
+ const options = taxYear ? { taxYearName: taxYearFileNameFor(taxYear), resources } : { resources };
54
54
  const taxData = await loadTaxDataForBook(book, options);
55
55
  const label = taxData?.tax_year?.label;
56
56
  if (!label) throw new Error("the tax year file declares no tax_year.label");
@@ -153,13 +153,13 @@ function periodLabel(period) {
153
153
  * @param {Object} book - parsed book.toml
154
154
  * @param {Array} lines - parsed lines.jsonl entries
155
155
  * @param {{periodEndDate?: string, quarterlyPeriodType?: "standard"|"calendar", taxYear?: string}} [params] -
156
- * quarterlyPeriodType defaults to standard (6 April quarters); taxYear (as 2025-26) overrides the year the
156
+ * resources is the loader the tax data reads through (defaults to reading app/); quarterlyPeriodType defaults to standard (6 April quarters); taxYear (as 2025-26) overrides the year the
157
157
  * book's dates imply
158
158
  * @returns {Promise<Object>} { taxYear, shape, quarterlyPeriodType, fieldSlots, periods, warnings }
159
159
  */
160
- export async function deriveItsaQuarterlyUpdate(book, lines, { periodEndDate, quarterlyPeriodType, taxYear } = {}) {
160
+ export async function deriveItsaQuarterlyUpdate(book, lines, { periodEndDate, quarterlyPeriodType, taxYear, resources } = {}) {
161
161
  requireSelfEmployedBook(book, lines, "deriveItsaQuarterlyUpdate");
162
- const taxData = await taxDataFor(book, taxYear);
162
+ const taxData = await taxDataFor(book, taxYear, resources);
163
163
  const label = taxData.tax_year.label;
164
164
  const shape = loadMapping().api.years[label].quarterly;
165
165
  const periodType = quarterlyPeriodType || "standard";
@@ -217,9 +217,9 @@ export async function deriveItsaQuarterlyUpdate(book, lines, { periodEndDate, qu
217
217
  * @param {{taxYear?: string}} [params] - taxYear (as 2025-26) overrides the year the book's dates imply
218
218
  * @returns {Promise<Object>} { taxYear, fieldSlots, allowances, adjustments, omitted, warnings }
219
219
  */
220
- export async function deriveItsaAnnualSubmission(book, lines, { taxYear } = {}) {
220
+ export async function deriveItsaAnnualSubmission(book, lines, { taxYear, resources } = {}) {
221
221
  requireSelfEmployedBook(book, lines, "deriveItsaAnnualSubmission");
222
- const taxData = await taxDataFor(book, taxYear);
222
+ const taxData = await taxDataFor(book, taxYear, resources);
223
223
  const label = taxData.tax_year.label;
224
224
  const slots = annualFieldSlotsForTaxYear(label);
225
225
 
@@ -141,12 +141,12 @@ function reconcile(label, attributed, engine) {
141
141
  * @param {Object} book - parsed book.toml
142
142
  * @param {Array} lines - parsed lines.jsonl entries
143
143
  * @param {{periodEnd: string, periodStart?: string, periodKey?: string}} params - periodEnd is the
144
- * obligation's period end (YYYY-MM-DD); periodStart, when given, must open that quarter; periodKey is
144
+ * obligation's period end (YYYY-MM-DD); resources is the loader the tax data reads through (defaults to reading app/); periodStart, when given, must open that quarter; periodKey is
145
145
  * echoed back untouched
146
146
  * @returns {Promise<Object>} { vatRegistrationNumber, periodKey, periodStart, periodEnd, dueDate, scheme,
147
147
  * months, boxes, hmrc, lines, notes }
148
148
  */
149
- export async function deriveVatReturn(book, lines, { periodEnd, periodStart, periodKey } = {}) {
149
+ export async function deriveVatReturn(book, lines, { periodEnd, periodStart, periodKey, resources } = {}) {
150
150
  if (!book || !lines) throw new Error("deriveVatReturn requires a book and its lines");
151
151
  if (!periodEnd) throw new Error("deriveVatReturn requires periodEnd (the obligation's period end, YYYY-MM-DD)");
152
152
  const entity = book.entityInformation ?? {};
@@ -157,7 +157,7 @@ export async function deriveVatReturn(book, lines, { periodEnd, periodStart, per
157
157
  }
158
158
 
159
159
  const product = productOf(book);
160
- const taxData = await loadTaxDataForBook(book);
160
+ const taxData = await loadTaxDataForBook(book, { resources });
161
161
  const results = calculateFromDiyaGl(book, lines, product, taxData, diyaGlToScenario(book, lines, product));
162
162
  const rows = interfaceRows(results);
163
163
  const wantedEnd = isoDate(periodEnd);
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2006-2026 DIY Accounting Limited
3
+ //
4
+ // package-version.js — the version of the engine that is running, read from
5
+ // the nearest package.json above this file: the package's own in an install,
6
+ // the repository's in a checkout. Both carry the same version, and the caches
7
+ // of files fetched from the site are keyed on it.
8
+ //
9
+ // Node's own modules load on the call rather than with this file, so a bundle
10
+ // that never asks for the version never pulls them in.
11
+
12
+ /**
13
+ * @returns {Promise<string>}
14
+ */
15
+ export async function packageVersion() {
16
+ const { existsSync, readFileSync } = await import("fs");
17
+ const { dirname, join } = await import("path");
18
+ const { fileURLToPath } = await import("url");
19
+
20
+ let directory = dirname(fileURLToPath(import.meta.url));
21
+ for (;;) {
22
+ const candidate = join(directory, "package.json");
23
+ if (existsSync(candidate)) return JSON.parse(readFileSync(candidate, "utf8")).version;
24
+ const parent = dirname(directory);
25
+ if (parent === directory) throw new Error("No package.json found above " + import.meta.url);
26
+ directory = parent;
27
+ }
28
+ }
@@ -12,7 +12,7 @@
12
12
 
13
13
  export const PROVENANCE_DATA = {
14
14
  formatVersion: "diya-gl/1",
15
- engineVersion: "1.2.47+26b9e6ec",
15
+ engineVersion: "1.2.49+f18282c3",
16
16
  taxDataHash: "51bdff60e993",
17
17
  reconciledCommit: "54e8731a91279975e61b1a232fbb67fda89515f0",
18
18
  templates: {
@@ -2258,6 +2258,27 @@ async function seAccountingPeriodFromSheet(set) {
2258
2258
  return { start, end };
2259
2259
  }
2260
2260
 
2261
+ // The Taxi package's own accounting period. Its end is the box "Date your
2262
+ // books or accounts are made up to" (Business Details!N25, a formula on
2263
+ // Admin!B17, the 5 April the writer sets for the package's tax year); the
2264
+ // period opens the day after the same date a year earlier. periodCovered()'s
2265
+ // guess would run the period 1 April to 31 March instead.
2266
+ const TAXI_PERIOD_END_CELL = { sheet: "Business Details", cell: "N25" };
2267
+
2268
+ async function taxiAccountingPeriodFromSheet(set) {
2269
+ const workbookName = singleWorkbookName(set);
2270
+ const sheet = await openSheet(await openWorkbook(set, workbookName), TAXI_PERIOD_END_CELL.sheet);
2271
+ const end = sheet ? dateAt(sheet.xml, TAXI_PERIOD_END_CELL.cell, sheet.sharedStrings) : undefined;
2272
+ if (!end) {
2273
+ throw new Error(
2274
+ `${workbookName}: no date in ${TAXI_PERIOD_END_CELL.sheet}!${TAXI_PERIOD_END_CELL.cell}, the date the books are made up to`,
2275
+ );
2276
+ }
2277
+ const [year, month, day] = end.split("-").map(Number);
2278
+ const start = new Date(Date.UTC(year - 1, month - 1, day + 1)).toISOString().slice(0, 10);
2279
+ return { start, end };
2280
+ }
2281
+
2261
2282
  function assign(target, key, value) {
2262
2283
  if (value !== undefined && value !== "" && value !== 0) target[key] = value;
2263
2284
  }
@@ -2685,11 +2706,13 @@ export async function extractBook(set, product, lines, cellMap, options = {}) {
2685
2706
  }
2686
2707
  }
2687
2708
 
2688
- // SE reads its true accounting period off the printed boxes, since a book
2689
- // whose dates do not run April to March still has every posting written
2690
- // onto the package's own April-March tab grid.
2709
+ // SE and Taxi read their true accounting period off the printed boxes,
2710
+ // since a book whose dates do not run April to March still has every
2711
+ // posting written onto the package's own April-March tab grid.
2691
2712
  const period =
2692
- (product === "se" && (await seAccountingPeriodFromSheet(set))) || periodCovered(await extractPeriodStartMonth(set, product), lines);
2713
+ product === "taxi"
2714
+ ? await taxiAccountingPeriodFromSheet(set)
2715
+ : (product === "se" && (await seAccountingPeriodFromSheet(set))) || periodCovered(await extractPeriodStartMonth(set, product), lines);
2693
2716
  const book = {
2694
2717
  documentInfo: {
2695
2718
  entriesType: "journal",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diy-accounting-uk/diya-gl",
3
- "version": "1.2.47",
3
+ "version": "1.2.49",
4
4
  "type": "module",
5
5
  "description": "Recalculate, read and write DIYA-GL books from the command line",
6
6
  "license": "Apache-2.0",