@diy-accounting-uk/diya-gl 1.2.56 → 1.2.57
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 +33 -2
- package/dist/app/data/releases.json +26 -0
- package/dist/app/lib/derivations/index.js +1 -0
- package/dist/app/lib/derivations/uk-property.js +218 -0
- package/dist/app/lib/provenance-data.js +1 -1
- package/dist/web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-book-v2.schema.json +28 -0
- package/dist/web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-docs.md +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,8 +96,9 @@ Each subcommand is also its own command, if you only want one on your `PATH`:
|
|
|
96
96
|
|
|
97
97
|
## API
|
|
98
98
|
|
|
99
|
-
The package root exports
|
|
100
|
-
take. Each takes the parsed book, its lines and a parameters object
|
|
99
|
+
The package root exports five functions that turn a book into the figures HMRC's MTD APIs
|
|
100
|
+
take. Each takes the parsed book, its lines and a parameters object. The VAT and self-employment
|
|
101
|
+
functions return a promise; the two UK property functions return their answer directly.
|
|
101
102
|
Nothing is sent anywhere: the answer is the request body to send, plus what fed it.
|
|
102
103
|
|
|
103
104
|
Load a book first:
|
|
@@ -166,6 +167,36 @@ const answer = await deriveItsaAnnualSubmission(book, lines, { taxYear: "2025-26
|
|
|
166
167
|
console.log(answer.allowances.annualInvestmentAllowance);
|
|
167
168
|
```
|
|
168
169
|
|
|
170
|
+
### `deriveUkPropertyPeriod(book, lines, { periodStartDate, periodEndDate })`
|
|
171
|
+
|
|
172
|
+
One period of HMRC's Property Business API (UK property) from a sole trader book. The book's
|
|
173
|
+
chart names the HMRC field each account feeds with `diya-gl:ukPropertyField` on the account
|
|
174
|
+
(for example `"income.periodAmount"` on the rent account, `"expenses.repairsAndMaintenance"` on
|
|
175
|
+
the repairs account). The function sums the sales and purchases postings on those accounts
|
|
176
|
+
between the two dates inclusive, net of VAT, with a credit note taking from its field. The
|
|
177
|
+
answer carries `income`, `expenses`, `omitted` (the fields no posting fed; they are left out of
|
|
178
|
+
the body, never sent as zero), `lines` and `warnings`. A book that names
|
|
179
|
+
`expenses.consolidatedExpenses` and an itemised expense in the same period is refused, because
|
|
180
|
+
HMRC takes one or the other.
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
import { deriveUkPropertyPeriod } from "@diy-accounting-uk/diya-gl";
|
|
184
|
+
|
|
185
|
+
const { book, lines } = loadBook("my-landlord-book");
|
|
186
|
+
const answer = deriveUkPropertyPeriod(book, lines, { periodStartDate: "2025-04-06", periodEndDate: "2025-07-05" });
|
|
187
|
+
console.log(answer.income.periodAmount, answer.expenses.repairsAndMaintenance);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### `deriveUkPropertyAnnual(book, lines, { taxYear? })`
|
|
191
|
+
|
|
192
|
+
The year's adjustments (`balancingCharge`, `privateUseAdjustment`,
|
|
193
|
+
`businessPremisesRenovationAllowanceBalancingCharges`) and allowances (`annualInvestmentAllowance`,
|
|
194
|
+
`businessPremisesRenovationAllowance`, `otherCapitalAllowance`, `costOfReplacingDomesticItems`,
|
|
195
|
+
`zeroEmissionsCarAllowance`) from the postings on the accounts the chart names, for the tax year
|
|
196
|
+
`taxYear` (as `2025-26`; the default is the year the book's period end falls in). The property
|
|
197
|
+
income allowance, `nonResidentLandlord` and `rentARoom.jointlyLet` are choices and facts about the
|
|
198
|
+
landlord, not ledger figures; the answer lists them under `leftToPage`.
|
|
199
|
+
|
|
169
200
|
## The format
|
|
170
201
|
|
|
171
202
|
A diya-gl book's fields are drawn from the XBRL Global Ledger Taxonomy Framework 2015 and
|
|
@@ -1689,6 +1689,32 @@
|
|
|
1689
1689
|
"scorecard": "101608 passed, 0 warnings, 0 failed"
|
|
1690
1690
|
}
|
|
1691
1691
|
}
|
|
1692
|
+
},
|
|
1693
|
+
{
|
|
1694
|
+
"tag": "diya-gl-v1.2.56",
|
|
1695
|
+
"date": "2026-10-25",
|
|
1696
|
+
"formatVersion": "diya-gl/1",
|
|
1697
|
+
"engineVersion": "1.2.56+4d61d48a",
|
|
1698
|
+
"taxDataHash": "51bdff60e993",
|
|
1699
|
+
"reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
|
|
1700
|
+
"templates": {
|
|
1701
|
+
"bst": {
|
|
1702
|
+
"hash": "0d003e10e00c",
|
|
1703
|
+
"scorecard": "812 passed, 0 warnings, 0 failed"
|
|
1704
|
+
},
|
|
1705
|
+
"taxi": {
|
|
1706
|
+
"hash": "1ff69654d64d",
|
|
1707
|
+
"scorecard": "700 passed, 0 warnings, 0 failed"
|
|
1708
|
+
},
|
|
1709
|
+
"se": {
|
|
1710
|
+
"hash": "064605ae76ed",
|
|
1711
|
+
"scorecard": "8015 passed, 0 warnings, 0 failed"
|
|
1712
|
+
},
|
|
1713
|
+
"ltd": {
|
|
1714
|
+
"hash": "a65ab09cf35c",
|
|
1715
|
+
"scorecard": "101608 passed, 0 warnings, 0 failed"
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1692
1718
|
}
|
|
1693
1719
|
]
|
|
1694
1720
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2006-2026 DIY Accounting Limited
|
|
3
|
+
//
|
|
4
|
+
// uk-property.js — the two UK property answers HMRC's Property Business API
|
|
5
|
+
// takes from a sole trader's book. deriveUkPropertyPeriod answers one
|
|
6
|
+
// period's income and expenses; deriveUkPropertyAnnual answers the year's
|
|
7
|
+
// adjustments and allowances the book can show.
|
|
8
|
+
//
|
|
9
|
+
// A book names the HMRC field an account feeds with the account's
|
|
10
|
+
// diya-gl:ukPropertyField, in HMRC's own dotted names (income.periodAmount,
|
|
11
|
+
// expenses.repairsAndMaintenance, allowances.costOfReplacingDomesticItems).
|
|
12
|
+
// Lines posted through the sales and purchases journals to a named account
|
|
13
|
+
// are summed net of VAT. A field with no posting is omitted, never sent as a
|
|
14
|
+
// zero. Elections and facts about the landlord (the property income
|
|
15
|
+
// allowance, non-resident landlord, rent a room jointly let) are not in a
|
|
16
|
+
// ledger; they are listed under leftToPage.
|
|
17
|
+
|
|
18
|
+
import { productOf } from "../product-workbook.js";
|
|
19
|
+
import { signedAmount } from "../scenario-extractor.js";
|
|
20
|
+
|
|
21
|
+
export const UK_PROPERTY_ACCOUNT_FIELD = "diya-gl:ukPropertyField";
|
|
22
|
+
|
|
23
|
+
export const PERIOD_INCOME_FIELDS = ["periodAmount", "premiumsOfLeaseGrant", "reversePremiums", "otherIncome", "taxDeducted"].map(
|
|
24
|
+
(name) => `income.${name}`,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
export const PERIOD_EXPENSE_FIELDS = [
|
|
28
|
+
"premisesRunningCosts",
|
|
29
|
+
"repairsAndMaintenance",
|
|
30
|
+
"financialCosts",
|
|
31
|
+
"professionalFees",
|
|
32
|
+
"costOfServices",
|
|
33
|
+
"other",
|
|
34
|
+
"residentialFinancialCost",
|
|
35
|
+
"travelCosts",
|
|
36
|
+
"consolidatedExpenses",
|
|
37
|
+
].map((name) => `expenses.${name}`);
|
|
38
|
+
|
|
39
|
+
export const ANNUAL_ADJUSTMENT_FIELDS = [
|
|
40
|
+
"balancingCharge",
|
|
41
|
+
"privateUseAdjustment",
|
|
42
|
+
"businessPremisesRenovationAllowanceBalancingCharges",
|
|
43
|
+
].map((name) => `adjustments.${name}`);
|
|
44
|
+
|
|
45
|
+
export const ANNUAL_ALLOWANCE_FIELDS = [
|
|
46
|
+
"annualInvestmentAllowance",
|
|
47
|
+
"businessPremisesRenovationAllowance",
|
|
48
|
+
"otherCapitalAllowance",
|
|
49
|
+
"costOfReplacingDomesticItems",
|
|
50
|
+
"zeroEmissionsCarAllowance",
|
|
51
|
+
].map((name) => `allowances.${name}`);
|
|
52
|
+
|
|
53
|
+
export const PERIOD_FIELDS = [...PERIOD_INCOME_FIELDS, ...PERIOD_EXPENSE_FIELDS];
|
|
54
|
+
export const ANNUAL_FIELDS = [...ANNUAL_ADJUSTMENT_FIELDS, ...ANNUAL_ALLOWANCE_FIELDS];
|
|
55
|
+
export const UK_PROPERTY_FIELDS = [...PERIOD_FIELDS, ...ANNUAL_FIELDS];
|
|
56
|
+
|
|
57
|
+
// What the annual submission asks that no posting can answer.
|
|
58
|
+
export const ANNUAL_LEFT_TO_PAGE = [
|
|
59
|
+
{
|
|
60
|
+
field: "allowances.propertyIncomeAllowance",
|
|
61
|
+
reason: "an election between the property income allowance and itemised expenses, made by the landlord.",
|
|
62
|
+
},
|
|
63
|
+
{ field: "adjustments.nonResidentLandlord", reason: "a fact about the landlord's residence, not a ledger figure." },
|
|
64
|
+
{ field: "adjustments.rentARoom.jointlyLet", reason: "a fact about how the room is let, not a ledger figure." },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
68
|
+
const TAX_YEAR_LABEL = /^(\d{4})-(\d{2})$/;
|
|
69
|
+
const OPENING_BALANCE_PREFIX = "OB-";
|
|
70
|
+
|
|
71
|
+
function round2(value) {
|
|
72
|
+
return Math.round(value * 100) / 100;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function setPath(target, path, value) {
|
|
76
|
+
const parts = path.split(".");
|
|
77
|
+
let node = target;
|
|
78
|
+
for (const part of parts.slice(0, -1)) node = node[part] ??= {};
|
|
79
|
+
node[parts.at(-1)] = value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function requirePropertyBook(book, lines, caller) {
|
|
83
|
+
if (!book || !lines) throw new Error(`${caller} requires a book and its lines`);
|
|
84
|
+
const product = productOf(book);
|
|
85
|
+
if (product === "ltd") {
|
|
86
|
+
throw new Error(`The book is a "ltd" book; UK property income is an individual's, so ${caller} reads a sole trader book`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Account id -> HMRC field, from the book's own chart.
|
|
91
|
+
function accountFields(book) {
|
|
92
|
+
const fields = new Map();
|
|
93
|
+
for (const journal of ["sales", "purchases"]) {
|
|
94
|
+
for (const [id, account] of Object.entries(book.accounts?.[journal] || {})) {
|
|
95
|
+
const field = account[UK_PROPERTY_ACCOUNT_FIELD];
|
|
96
|
+
if (field === undefined) continue;
|
|
97
|
+
if (!UK_PROPERTY_FIELDS.includes(field)) {
|
|
98
|
+
throw new Error(`account ${id} names ${UK_PROPERTY_ACCOUNT_FIELD} "${field}", which is not a UK property field`);
|
|
99
|
+
}
|
|
100
|
+
fields.set(id, field);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return fields;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function netAmount(line) {
|
|
107
|
+
return signedAmount(line) / (1 + (line.taxRate || 0));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The postings inside [start, end] on a mapped account, summed by field.
|
|
111
|
+
function figuresBetween(book, lines, fieldsWanted, start, end) {
|
|
112
|
+
const fields = accountFields(book);
|
|
113
|
+
const totals = new Map();
|
|
114
|
+
const contributions = [];
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
if (line.sourceJournalID !== "sales" && line.sourceJournalID !== "purchases") continue;
|
|
117
|
+
if (String(line.documentReference || "").startsWith(OPENING_BALANCE_PREFIX)) continue;
|
|
118
|
+
const field = fields.get(String(line.accountMainID));
|
|
119
|
+
if (!field || !fieldsWanted.includes(field)) continue;
|
|
120
|
+
if (line.postingDate < start || line.postingDate > end) continue;
|
|
121
|
+
const amount = round2(netAmount(line));
|
|
122
|
+
totals.set(field, (totals.get(field) ?? 0) + amount);
|
|
123
|
+
contributions.push({
|
|
124
|
+
entryNumber: line.entryNumber,
|
|
125
|
+
date: line.postingDate,
|
|
126
|
+
account: String(line.accountMainID),
|
|
127
|
+
field,
|
|
128
|
+
reference: line.documentReference ?? null,
|
|
129
|
+
counterparty: line.detailComment ?? null,
|
|
130
|
+
amount,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const figures = {};
|
|
134
|
+
for (const field of fieldsWanted) {
|
|
135
|
+
if (totals.has(field)) setPath(figures, field, round2(totals.get(field)));
|
|
136
|
+
}
|
|
137
|
+
return { figures, contributions, present: fieldsWanted.filter((field) => totals.has(field)) };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* One period of HMRC's UK property income and expenses from a sole trader
|
|
142
|
+
* book whose chart names the property accounts. The answer is the period's
|
|
143
|
+
* own figures; a cumulative year's running total is the same call with the
|
|
144
|
+
* period starting on 6 April.
|
|
145
|
+
* @param {Object} book - parsed book.toml
|
|
146
|
+
* @param {Array} lines - parsed lines.jsonl entries
|
|
147
|
+
* @param {{periodStartDate: string, periodEndDate: string}} params - the period, both ends inclusive, as YYYY-MM-DD
|
|
148
|
+
* @returns {Object} { periodDates, income, expenses, fieldSlots, omitted, lines, warnings }
|
|
149
|
+
*/
|
|
150
|
+
export function deriveUkPropertyPeriod(book, lines, { periodStartDate, periodEndDate } = {}) {
|
|
151
|
+
requirePropertyBook(book, lines, "deriveUkPropertyPeriod");
|
|
152
|
+
if (!ISO_DAY.test(periodStartDate ?? "") || !ISO_DAY.test(periodEndDate ?? "")) {
|
|
153
|
+
throw new Error("periodStartDate and periodEndDate must be YYYY-MM-DD days");
|
|
154
|
+
}
|
|
155
|
+
if (periodStartDate > periodEndDate) throw new Error(`the period starts ${periodStartDate}, after it ends ${periodEndDate}`);
|
|
156
|
+
|
|
157
|
+
const { figures, contributions, present } = figuresBetween(book, lines, PERIOD_FIELDS, periodStartDate, periodEndDate);
|
|
158
|
+
const warnings = [];
|
|
159
|
+
const itemised = present.filter((field) => field.startsWith("expenses.") && field !== "expenses.consolidatedExpenses");
|
|
160
|
+
if (present.includes("expenses.consolidatedExpenses") && itemised.length > 0) {
|
|
161
|
+
throw new Error(`the period carries consolidated expenses and itemised expenses (${itemised.join(", ")}); HMRC takes one or the other`);
|
|
162
|
+
}
|
|
163
|
+
if (contributions.length === 0) warnings.push("no posting on a UK property account falls in the period");
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
periodDates: { periodStartDate, periodEndDate },
|
|
167
|
+
income: figures.income ?? {},
|
|
168
|
+
expenses: figures.expenses ?? {},
|
|
169
|
+
fieldSlots: PERIOD_FIELDS,
|
|
170
|
+
omitted: PERIOD_FIELDS.filter((field) => !present.includes(field)),
|
|
171
|
+
lines: contributions,
|
|
172
|
+
warnings,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function taxYearBounds(label) {
|
|
177
|
+
const match = TAX_YEAR_LABEL.exec(label ?? "");
|
|
178
|
+
if (!match) throw new Error(`taxYear must look like 2025-26, not "${label}"`);
|
|
179
|
+
const start = Number(match[1]);
|
|
180
|
+
if (String(start + 1).slice(-2) !== match[2]) throw new Error(`taxYear "${label}" does not name two consecutive years`);
|
|
181
|
+
return { start: `${start}-04-06`, end: `${start + 1}-04-05` };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// The tax year (6 April to 5 April) a day falls in.
|
|
185
|
+
function taxYearLabelOf(isoDay) {
|
|
186
|
+
const [year, month, day] = isoDay.split("-").map(Number);
|
|
187
|
+
const start = month > 4 || (month === 4 && day >= 6) ? year : year - 1;
|
|
188
|
+
return `${start}-${String(start + 1).slice(-2)}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The year's adjustments and allowances for HMRC's UK property annual
|
|
193
|
+
* submission, from a sole trader book whose chart names the accounts. The
|
|
194
|
+
* year defaults to the one the book's period end falls in.
|
|
195
|
+
* @param {Object} book - parsed book.toml
|
|
196
|
+
* @param {Array} lines - parsed lines.jsonl entries
|
|
197
|
+
* @param {{taxYear?: string}} [params] - taxYear as 2025-26
|
|
198
|
+
* @returns {Object} { taxYear, adjustments, allowances, fieldSlots, omitted, leftToPage, lines, warnings }
|
|
199
|
+
*/
|
|
200
|
+
export function deriveUkPropertyAnnual(book, lines, { taxYear } = {}) {
|
|
201
|
+
requirePropertyBook(book, lines, "deriveUkPropertyAnnual");
|
|
202
|
+
const label =
|
|
203
|
+
taxYear ?? (book.documentInfo?.periodCoveredEnd ? taxYearLabelOf(String(book.documentInfo.periodCoveredEnd).slice(0, 10)) : undefined);
|
|
204
|
+
if (!label) throw new Error("the book declares no period end, so taxYear is required");
|
|
205
|
+
const { start, end } = taxYearBounds(label);
|
|
206
|
+
|
|
207
|
+
const { figures, contributions, present } = figuresBetween(book, lines, ANNUAL_FIELDS, start, end);
|
|
208
|
+
return {
|
|
209
|
+
taxYear: label,
|
|
210
|
+
adjustments: figures.adjustments ?? {},
|
|
211
|
+
allowances: figures.allowances ?? {},
|
|
212
|
+
fieldSlots: ANNUAL_FIELDS,
|
|
213
|
+
omitted: ANNUAL_FIELDS.filter((field) => !present.includes(field)),
|
|
214
|
+
leftToPage: ANNUAL_LEFT_TO_PAGE,
|
|
215
|
+
lines: contributions,
|
|
216
|
+
warnings: contributions.length === 0 ? ["no posting on a UK property account falls in the tax year"] : [],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -1112,6 +1112,34 @@
|
|
|
1112
1112
|
"minimum": 1,
|
|
1113
1113
|
"maximum": 9,
|
|
1114
1114
|
"description": "diya-gl: extension \u2014 VAT return box number (1-9) this account contributes to"
|
|
1115
|
+
},
|
|
1116
|
+
"diya-gl:ukPropertyField": {
|
|
1117
|
+
"type": "string",
|
|
1118
|
+
"enum": [
|
|
1119
|
+
"income.periodAmount",
|
|
1120
|
+
"income.premiumsOfLeaseGrant",
|
|
1121
|
+
"income.reversePremiums",
|
|
1122
|
+
"income.otherIncome",
|
|
1123
|
+
"income.taxDeducted",
|
|
1124
|
+
"expenses.premisesRunningCosts",
|
|
1125
|
+
"expenses.repairsAndMaintenance",
|
|
1126
|
+
"expenses.financialCosts",
|
|
1127
|
+
"expenses.professionalFees",
|
|
1128
|
+
"expenses.costOfServices",
|
|
1129
|
+
"expenses.other",
|
|
1130
|
+
"expenses.residentialFinancialCost",
|
|
1131
|
+
"expenses.travelCosts",
|
|
1132
|
+
"expenses.consolidatedExpenses",
|
|
1133
|
+
"adjustments.balancingCharge",
|
|
1134
|
+
"adjustments.privateUseAdjustment",
|
|
1135
|
+
"adjustments.businessPremisesRenovationAllowanceBalancingCharges",
|
|
1136
|
+
"allowances.annualInvestmentAllowance",
|
|
1137
|
+
"allowances.businessPremisesRenovationAllowance",
|
|
1138
|
+
"allowances.otherCapitalAllowance",
|
|
1139
|
+
"allowances.costOfReplacingDomesticItems",
|
|
1140
|
+
"allowances.zeroEmissionsCarAllowance"
|
|
1141
|
+
],
|
|
1142
|
+
"description": "diya-gl: extension \u2014 the HMRC UK property field (Property Business API names) this sales or purchases account feeds, so deriveUkPropertyPeriod and deriveUkPropertyAnnual can sum its postings"
|
|
1115
1143
|
}
|
|
1116
1144
|
}
|
|
1117
1145
|
},
|
|
@@ -572,6 +572,7 @@ metadata needed for the spreadsheet mapping:
|
|
|
572
572
|
| `diya-gl:sa103sBox` | SA103S box reference (e.g. 'box10') | book.toml |
|
|
573
573
|
| `diya-gl:ct600Box` | CT600 box reference | book.toml |
|
|
574
574
|
| `diya-gl:vatBox` | VAT return box number (1-9) | book.toml |
|
|
575
|
+
| `diya-gl:ukPropertyField` | HMRC UK property field this account feeds | book.toml |
|
|
575
576
|
| `diya-gl:bankCode` | Bank receipt/payment code (DR/CR/W/RP/RV...) | .jsonl |
|
|
576
577
|
| `diya-gl:bankAccountID` | Bank account code from chart of accounts | .jsonl |
|
|
577
578
|
| `diya-gl:employeeID` | Employee identifier (matches employees[]) | .jsonl |
|