@diy-accounting-uk/diya-gl 1.2.62 → 1.2.64

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.
@@ -1845,6 +1845,58 @@
1845
1845
  "scorecard": "101608 passed, 0 warnings, 0 failed"
1846
1846
  }
1847
1847
  }
1848
+ },
1849
+ {
1850
+ "tag": "diya-gl-v1.2.62",
1851
+ "date": "2026-10-28",
1852
+ "formatVersion": "diya-gl/1",
1853
+ "engineVersion": "1.2.62+2c4a0a8c",
1854
+ "taxDataHash": "51bdff60e993",
1855
+ "reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
1856
+ "templates": {
1857
+ "bst": {
1858
+ "hash": "0d003e10e00c",
1859
+ "scorecard": "812 passed, 0 warnings, 0 failed"
1860
+ },
1861
+ "taxi": {
1862
+ "hash": "1ff69654d64d",
1863
+ "scorecard": "700 passed, 0 warnings, 0 failed"
1864
+ },
1865
+ "se": {
1866
+ "hash": "064605ae76ed",
1867
+ "scorecard": "8015 passed, 0 warnings, 0 failed"
1868
+ },
1869
+ "ltd": {
1870
+ "hash": "b90d8b0f54a0",
1871
+ "scorecard": "101608 passed, 0 warnings, 0 failed"
1872
+ }
1873
+ }
1874
+ },
1875
+ {
1876
+ "tag": "diya-gl-v1.2.63",
1877
+ "date": "2026-10-28",
1878
+ "formatVersion": "diya-gl/1",
1879
+ "engineVersion": "1.2.63+2c4a0a8c",
1880
+ "taxDataHash": "51bdff60e993",
1881
+ "reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
1882
+ "templates": {
1883
+ "bst": {
1884
+ "hash": "0d003e10e00c",
1885
+ "scorecard": "812 passed, 0 warnings, 0 failed"
1886
+ },
1887
+ "taxi": {
1888
+ "hash": "1ff69654d64d",
1889
+ "scorecard": "700 passed, 0 warnings, 0 failed"
1890
+ },
1891
+ "se": {
1892
+ "hash": "064605ae76ed",
1893
+ "scorecard": "8015 passed, 0 warnings, 0 failed"
1894
+ },
1895
+ "ltd": {
1896
+ "hash": "b90d8b0f54a0",
1897
+ "scorecard": "101608 passed, 0 warnings, 0 failed"
1898
+ }
1899
+ }
1848
1900
  }
1849
1901
  ]
1850
1902
  }
@@ -2,6 +2,9 @@
2
2
  "cell/Cashaccount.xlsx!Mar!A1": "Bank account headers; bank view reads TrialBalance.",
3
3
  "cell/Cashaccount.xlsx!Mar!A2": "Bank account headers; bank view reads TrialBalance.",
4
4
  "cell/Companysecretary.xlsx!RegisterofMembers!F1": "RegisterofMembers headers; company view renders register.",
5
+ "cell/Companysecretary.xlsx!RegisterofMembers!F3": "RegisterofMembers nominal value column; company view renders the register.",
6
+ "cell/Companysecretary.xlsx!RegisterofMembers!F4": "RegisterofMembers nominal value column; company view renders the register.",
7
+ "cell/Companysecretary.xlsx!RegisterofMembers!F5": "RegisterofMembers nominal value column; company view renders the register.",
5
8
  "cell/Companysecretary.xlsx!RegisterofMembers!G1": "RegisterofMembers headers; company view renders register.",
6
9
  "cell/Creditcardaccount.xlsx!Mar!A1": "Bank account headers; bank view reads TrialBalance.",
7
10
  "cell/Creditcardaccount.xlsx!Mar!A2": "Bank account headers; bank view reads TrialBalance.",
@@ -56,6 +56,7 @@ import {
56
56
  } from "../payslips-layout.js";
57
57
  import { addMonths, endOfMonth, SHEET_BLANK } from "./shared.js";
58
58
  import { registerOfficers } from "../scenario-loader.js";
59
+ import { registerNominalValue } from "../share-nominal.js";
59
60
  import { attributionWriter, entriesOf, entryOf, fieldEntriesOf, unionOf } from "../entry-attribution.js";
60
61
 
61
62
  const SHORT_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
@@ -237,7 +238,8 @@ const REGISTER_MEMBER_ROWS = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 1
237
238
  // and the register of directors' interests one a row from row 2 as well.
238
239
  const DIRECTOR_SECRETARY_OFFICER_ROWS = [2, 3, 4, 5, 6, 7, 8];
239
240
  const DIRECTORS_INTERESTS_ROWS = [2, 3, 4, 5, 6];
240
- const SHARE_NOMINAL_VALUE = 1;
241
+ // The template's own fully-paid ordinary share placeholder row.
242
+ const PLACEHOLDER_SHARE_NOMINAL_VALUE = 1;
241
243
  const CHARGE_REGISTER_ROWS = [2, 3, 4, 5, 6];
242
244
  const HP_AGREEMENT_ROWS = [8, 10];
243
245
  const EXPENSES_FORM_MONTHS = Array.from({ length: 12 }, (_, index) => `Month ${String(index + 1).padStart(2, "0")}`);
@@ -1335,12 +1337,13 @@ function buildPayslipsCalendar(taxData, period, tabs) {
1335
1337
  // placeholder, which is where F1 takes the nominal value from whether or not
1336
1338
  // a scenario names any members.
1337
1339
  function buildCompanySecretary(scenario) {
1338
- const register = { F1: SHARE_NOMINAL_VALUE };
1339
1340
  const members = scenario.members || [];
1341
+ const register = { F1: registerNominalValue(members.map((member) => member.nominal_value)) ?? PLACEHOLDER_SHARE_NOMINAL_VALUE };
1340
1342
  members.forEach((member, index) => {
1341
1343
  const row = REGISTER_MEMBER_ROWS[index];
1342
1344
  if (row === undefined) return;
1343
1345
  register[`A${row}`] = member.name;
1346
+ register[`F${row}`] = member.nominal_value;
1344
1347
  register[`G${row}`] = member.shares;
1345
1348
  });
1346
1349
  // The directors' report prints the first two members a line each, so those
@@ -37,6 +37,7 @@ import {
37
37
  import { totalBusinessMiles, calculateMileageAllowance, FIXTURE_CAR_MILEAGE_RATES } from "./tax/mileage.js";
38
38
  import { compareLines } from "./diya-gl-canonical.js";
39
39
  import { tagEntry, entryOf } from "./entry-attribution.js";
40
+ import { memberNominalValue, registerNominalValue } from "./share-nominal.js";
40
41
 
41
42
  // The Taxi Driver masters keep their own chart of accounts (fuel at 5100,
42
43
  // fixed assets at 7000, ...), so filtering by BST_PURCHASE_CODE_MAP -- built
@@ -699,10 +700,11 @@ export function diyaGlToScenario(book, lines, product) {
699
700
  }
700
701
  if (product === "ltd" && book.members?.length > 0) {
701
702
  scenario.members = book.members.map((member) => {
702
- const entry = { name: member.name, shares: member.shares };
703
+ const entry = { name: member.name, shares: member.shares, nominal_value: memberNominalValue(member, book) };
703
704
  if (member.acquiredDate !== undefined) entry.acquired = member.acquiredDate;
704
705
  return entry;
705
706
  });
707
+ registerNominalValue(scenario.members.map((member) => member.nominal_value));
706
708
  }
707
709
  // The Board Minute sheet has one meeting date and one declared figure, so
708
710
  // a book with more than one dividend voucher still reaches the sheet as
@@ -11,8 +11,8 @@
11
11
  // stamped by any of the three surfaces carries the same values.
12
12
 
13
13
  export const PROVENANCE_DATA = {
14
- formatVersion: "diya-gl/1",
15
- engineVersion: "1.2.62+2c4a0a8c",
14
+ formatVersion: "diya-gl/2",
15
+ engineVersion: "1.2.64+abe8b184",
16
16
  taxDataHash: "51bdff60e993",
17
17
  reconciledCommit: "54e8731a91279975e61b1a232fbb67fda89515f0",
18
18
  templates: {
@@ -15,6 +15,7 @@
15
15
  // reads the exact same generated values.
16
16
 
17
17
  import { PROVENANCE_DATA } from "./provenance-data.js";
18
+ import { memberNominalValue } from "./share-nominal.js";
18
19
 
19
20
  // The same schema-name-to-product-id mapping xlsx-exporter.js's
20
21
  // productIdOf() carries, kept as a small map of its own rather than an
@@ -68,7 +69,11 @@ export function provenanceStamps(product) {
68
69
  export function stampBook(book) {
69
70
  const product = productOf(book);
70
71
  if (!product) return book;
71
- return { ...book, documentInfo: { ...book.documentInfo, ...provenanceStamps(product) } };
72
+ const stamped = { ...book, documentInfo: { ...book.documentInfo, ...provenanceStamps(product) } };
73
+ // A book that predates the format states no nominal value, and the format it
74
+ // is stamped with requires one on every member: state the £1 it was read as.
75
+ if (book.members) stamped.members = book.members.map((member) => ({ ...member, nominalValue: memberNominalValue(member, book) }));
76
+ return stamped;
72
77
  }
73
78
 
74
79
  /**
@@ -1094,6 +1094,7 @@ export function formatScenarioToml(metadata, grouped, expected) {
1094
1094
  parts.push("[[members]]");
1095
1095
  parts.push(`name = "${escapeTomlString(member.name)}"`);
1096
1096
  parts.push(`shares = ${member.shares}`);
1097
+ if (member.nominal_value !== undefined) parts.push(`nominal_value = ${member.nominal_value}`);
1097
1098
  if (member.acquired) parts.push(`acquired = ${member.acquired}`);
1098
1099
  parts.push("");
1099
1100
  }
@@ -86,6 +86,14 @@ export function registerOfficers(scenario) {
86
86
  .map((employee) => ({ name: employee.name, role: "Director" }));
87
87
  }
88
88
 
89
+ // A scenario file written before members carried a nominal value priced every
90
+ // share at £1.
91
+ const PRE_NOMINAL_VALUE_SHARE_PRICE = 1;
92
+
89
93
  export function loadScenario(path) {
90
- return parseTOML(readFileSync(path, "utf8"));
94
+ const scenario = parseTOML(readFileSync(path, "utf8"));
95
+ for (const member of scenario.members || []) {
96
+ if (member.nominal_value === undefined) member.nominal_value = PRE_NOMINAL_VALUE_SHARE_PRICE;
97
+ }
98
+ return scenario;
91
99
  }
@@ -0,0 +1,51 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2006-2026 DIY Accounting Limited
3
+ //
4
+ // share-nominal.js — the nominal value of one share on a book's register of
5
+ // members. A book written under diya-gl/2 or later states it on every member.
6
+ // A book that predates that format, or carries no format marker, priced every
7
+ // share at £1, so its members read as £1 here and nowhere else.
8
+
9
+ export const NOMINAL_VALUE_FORMAT = 2;
10
+
11
+ // The numeric part of a "diya-gl/2" or "diya-gl-books/1" marker, or undefined
12
+ // where the book carries none.
13
+ export function bookFormatNumber(book) {
14
+ const marker = book?.documentInfo?.["diya-gl:formatVersion"];
15
+ const match = typeof marker === "string" ? /\/(\d+)$/.exec(marker) : null;
16
+ return match ? Number(match[1]) : undefined;
17
+ }
18
+
19
+ export function statesNominalValue(book) {
20
+ const format = bookFormatNumber(book);
21
+ return format !== undefined && format >= NOMINAL_VALUE_FORMAT;
22
+ }
23
+
24
+ /**
25
+ * The nominal value per share for one member of the book's register.
26
+ * @param {Object} member - a members[] entry
27
+ * @param {Object} book - the parsed book, for its format marker
28
+ * @returns {number}
29
+ */
30
+ export function memberNominalValue(member, book) {
31
+ if (member.nominalValue !== undefined) return member.nominalValue;
32
+ if (!statesNominalValue(book)) return 1;
33
+ throw new Error(
34
+ `members[] "${member.name}" has no nominalValue, and the book's format ${book.documentInfo["diya-gl:formatVersion"]} states one on every member`,
35
+ );
36
+ }
37
+
38
+ /**
39
+ * The one nominal value the register prices every holding at. The members
40
+ * sheet takes its nominal value from a single cell, so members that differ
41
+ * cannot be carried.
42
+ * @param {number[]} nominalValues - one resolved value per member
43
+ * @returns {number|undefined} undefined for an empty register
44
+ */
45
+ export function registerNominalValue(nominalValues) {
46
+ const values = new Set(nominalValues);
47
+ if (values.size > 1) {
48
+ throw new Error(`The register of members prices shares at ${[...values].join(" and ")}; the members sheet carries one nominal value`);
49
+ }
50
+ return values.size === 1 ? [...values][0] : undefined;
51
+ }
@@ -2410,11 +2410,10 @@ async function registersFrom(companySecretaryZip) {
2410
2410
  const name = textAt(xml, `${MEMBER_COLUMNS.name}${row}`, sharedStrings);
2411
2411
  const shares = numberAt(xml, `${MEMBER_COLUMNS.shares}${row}`, sharedStrings);
2412
2412
  if (!name || !shares) continue;
2413
- const member = { memberID: `M${members.length + 1}`, name, shares };
2414
- // The register prices every holding at the same £1 nominal value
2415
- // (SHARE_NOMINAL_VALUE in ltd.js), a company-wide figure rather than a
2416
- // per-member one, so column F is not read back onto the member -- the
2417
- // schema's own note on this field says the same.
2413
+ const nominalValue = numberAt(xml, `${MEMBER_COLUMNS.nominalValue}${row}`, sharedStrings);
2414
+ if (nominalValue === undefined)
2415
+ throw new Error(`RegisterofMembers ${MEMBER_COLUMNS.nominalValue}${row}: member "${name}" holds shares with no nominal value`);
2416
+ const member = { memberID: `M${members.length + 1}`, name, shares, nominalValue };
2418
2417
  assign(member, "acquiredDate", dateAt(xml, `${MEMBER_COLUMNS.acquired}${row}`, sharedStrings));
2419
2418
  members.push(member);
2420
2419
  }
@@ -183,7 +183,6 @@ export const CHARGE_REGISTER_COLUMNS = { date: "A", asset: "B", valuation: "C",
183
183
  // creditor the bank's DV payments settle.
184
184
  export const REGISTER_MEMBER_ROWS = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
185
185
  export const REGISTER_MEMBER_COLUMNS = { name: "A", acquired: "C", nominalValue: "F", shares: "G" };
186
- const SHARE_NOMINAL_VALUE = 1;
187
186
  export const BOARD_MINUTE_CELLS = { date: "F2", dividendDeclared: "E4" };
188
187
 
189
188
  // ── Register of directors and directors' interests (Companysecretary.xlsx) ─
@@ -603,8 +602,8 @@ export function cellWrites(scenario, targetStartYear, yearEndMonth) {
603
602
  if (hubWrites.OpenAccounts) hubWrites.OpenAccounts = inSheetOrder(hubWrites.OpenAccounts);
604
603
 
605
604
  // Companysecretary.xlsx: the charges register, the share register and the
606
- // board minute. Ordinary shares are issued at their £1 nominal value,
607
- // matching the template's own "Fully paid Ordinary Shares" placeholder row.
605
+ // board minute. Each member row carries the nominal value
606
+ // of a share the scenario states for it.
608
607
  const companysecretaryWrites = {};
609
608
  if (scenario.charges) {
610
609
  companysecretaryWrites["Charges&Debentures"] = {};
@@ -633,7 +632,7 @@ export function cellWrites(scenario, targetStartYear, yearEndMonth) {
633
632
  const row = REGISTER_MEMBER_ROWS[index];
634
633
  if (row === undefined) return;
635
634
  register[`${REGISTER_MEMBER_COLUMNS.name}${row}`] = member.name;
636
- register[`${REGISTER_MEMBER_COLUMNS.nominalValue}${row}`] = SHARE_NOMINAL_VALUE;
635
+ register[`${REGISTER_MEMBER_COLUMNS.nominalValue}${row}`] = member.nominal_value;
637
636
  register[`${REGISTER_MEMBER_COLUMNS.shares}${row}`] = member.shares;
638
637
  if (member.acquired) {
639
638
  const acquired = parseDate(member.acquired);
@@ -1876,14 +1875,18 @@ export function multiFileOptions(yearEndMonth) {
1876
1875
  ...payslipsMonthTabReads(tabNames),
1877
1876
  },
1878
1877
  "Companysecretary.xlsx": {
1879
- // One member a row: A the name, G the holding. F1 is the sheet's own
1878
+ // One member a row: A the name, F the nominal value of a share, G the holding. F1 is the sheet's own
1880
1879
  // nominal-value formula (=F3) and G1 its shares-issued total
1881
1880
  // (=SUM(G3:G19)). The directors' report prints the first two members
1882
1881
  // a line each.
1883
1882
  "RegisterofMembers": [
1884
1883
  "F1",
1885
1884
  "G1",
1886
- ...REGISTER_MEMBER_ROWS.flatMap((row) => [`${REGISTER_MEMBER_COLUMNS.name}${row}`, `${REGISTER_MEMBER_COLUMNS.shares}${row}`]),
1885
+ ...REGISTER_MEMBER_ROWS.flatMap((row) => [
1886
+ `${REGISTER_MEMBER_COLUMNS.name}${row}`,
1887
+ `${REGISTER_MEMBER_COLUMNS.nominalValue}${row}`,
1888
+ `${REGISTER_MEMBER_COLUMNS.shares}${row}`,
1889
+ ]),
1887
1890
  ],
1888
1891
  // F2 is the date the board met and E4 the dividend it declared. The
1889
1892
  // directors' report and the trial balance both read E4 across the
@@ -3059,6 +3062,12 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
3059
3062
  member.shares,
3060
3063
  0,
3061
3064
  );
3065
+ check(
3066
+ `Register of members: row ${row} prices ${member.name}'s shares`,
3067
+ num(register[`${REGISTER_MEMBER_COLUMNS.nominalValue}${row}`]),
3068
+ member.nominal_value,
3069
+ 0,
3070
+ );
3062
3071
  });
3063
3072
  }
3064
3073
  if (report && expected.members) {
@@ -619,7 +619,7 @@
619
619
  "nominalValue": {
620
620
  "type": "number",
621
621
  "minimum": 0,
622
- "description": "Nominal value per share. The shipped register currently prices every holding at a single nominal value set for the whole company, so a per-member figure here is not yet read."
622
+ "description": "diya-gl: extension \u2014 Nominal value per share, in pounds. Required on every member of a book whose diya-gl:formatVersion is diya-gl/2 or later; a book that predates it, or carries no format marker, reads as 1. The members sheet carries one nominal value, so every member of a book states the same figure."
623
623
  },
624
624
  "acquiredDate": {
625
625
  "type": "string",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diy-accounting-uk/diya-gl",
3
- "version": "1.2.62",
3
+ "version": "1.2.64",
4
4
  "type": "module",
5
5
  "description": "Recalculate, read and write DIYA-GL books from the command line",
6
6
  "license": "Apache-2.0",