@diy-accounting-uk/diya-gl 1.2.64 → 1.2.66
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/dist/app/data/releases.json +52 -0
- package/dist/app/lib/product-sheets/format.js +15 -0
- package/dist/app/lib/product-sheets/payslips.js +115 -0
- package/dist/app/lib/product-sheets/salesinvoice.js +130 -0
- package/dist/app/lib/product-sheets/serial-dates.js +18 -0
- package/dist/app/lib/product-sheets/vat-chain.js +342 -0
- package/dist/app/lib/product-sheets/vat-rate.js +27 -0
- package/dist/app/lib/provenance-data.js +1 -1
- package/dist/app/products/ltd/cell-map.js +294 -0
- package/dist/app/products/ltd/checks/admin-filing.js +364 -0
- package/dist/app/products/ltd/checks/company-registers.js +301 -0
- package/dist/app/products/ltd/checks/fixed-assets.js +123 -0
- package/dist/app/products/ltd/checks/index.js +260 -0
- package/dist/app/products/ltd/checks/ledgers.js +420 -0
- package/dist/app/products/ltd/checks/payroll.js +462 -0
- package/dist/app/products/ltd/checks/vat.js +62 -0
- package/dist/app/products/ltd/layout.js +211 -0
- package/dist/app/products/ltd/reads.js +581 -0
- package/dist/app/products/ltd/report.js +358 -0
- package/dist/app/products/ltd/writes.js +728 -0
- package/dist/app/products/ltd.js +51 -4487
- package/dist/app/products/se/cell-map.js +314 -0
- package/dist/app/products/se/checks/admin.js +120 -0
- package/dist/app/products/se/checks/fixed-assets.js +219 -0
- package/dist/app/products/se/checks/index.js +444 -0
- package/dist/app/products/se/checks/ledgers.js +152 -0
- package/dist/app/products/se/checks/payroll.js +245 -0
- package/dist/app/products/se/checks/sa103f.js +574 -0
- package/dist/app/products/se/checks/vat.js +126 -0
- package/dist/app/products/se/layout.js +248 -0
- package/dist/app/products/se/reads.js +387 -0
- package/dist/app/products/se/report.js +634 -0
- package/dist/app/products/se/writes.js +640 -0
- package/dist/app/products/se.js +62 -4432
- package/package.json +1 -1
|
@@ -1897,6 +1897,58 @@
|
|
|
1897
1897
|
"scorecard": "101608 passed, 0 warnings, 0 failed"
|
|
1898
1898
|
}
|
|
1899
1899
|
}
|
|
1900
|
+
},
|
|
1901
|
+
{
|
|
1902
|
+
"tag": "diya-gl-v1.2.64",
|
|
1903
|
+
"date": "2026-10-29",
|
|
1904
|
+
"formatVersion": "diya-gl/2",
|
|
1905
|
+
"engineVersion": "1.2.64+abe8b184",
|
|
1906
|
+
"taxDataHash": "51bdff60e993",
|
|
1907
|
+
"reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
|
|
1908
|
+
"templates": {
|
|
1909
|
+
"bst": {
|
|
1910
|
+
"hash": "0d003e10e00c",
|
|
1911
|
+
"scorecard": "812 passed, 0 warnings, 0 failed"
|
|
1912
|
+
},
|
|
1913
|
+
"taxi": {
|
|
1914
|
+
"hash": "1ff69654d64d",
|
|
1915
|
+
"scorecard": "700 passed, 0 warnings, 0 failed"
|
|
1916
|
+
},
|
|
1917
|
+
"se": {
|
|
1918
|
+
"hash": "064605ae76ed",
|
|
1919
|
+
"scorecard": "8015 passed, 0 warnings, 0 failed"
|
|
1920
|
+
},
|
|
1921
|
+
"ltd": {
|
|
1922
|
+
"hash": "b90d8b0f54a0",
|
|
1923
|
+
"scorecard": "101608 passed, 0 warnings, 0 failed"
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
},
|
|
1927
|
+
{
|
|
1928
|
+
"tag": "diya-gl-v1.2.65",
|
|
1929
|
+
"date": "2026-10-31",
|
|
1930
|
+
"formatVersion": "diya-gl/2",
|
|
1931
|
+
"engineVersion": "1.2.65+abe8b1846",
|
|
1932
|
+
"taxDataHash": "51bdff60e993",
|
|
1933
|
+
"reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
|
|
1934
|
+
"templates": {
|
|
1935
|
+
"bst": {
|
|
1936
|
+
"hash": "0d003e10e00c",
|
|
1937
|
+
"scorecard": "812 passed, 0 warnings, 0 failed"
|
|
1938
|
+
},
|
|
1939
|
+
"taxi": {
|
|
1940
|
+
"hash": "1ff69654d64d",
|
|
1941
|
+
"scorecard": "700 passed, 0 warnings, 0 failed"
|
|
1942
|
+
},
|
|
1943
|
+
"se": {
|
|
1944
|
+
"hash": "064605ae76ed",
|
|
1945
|
+
"scorecard": "8015 passed, 0 warnings, 0 failed"
|
|
1946
|
+
},
|
|
1947
|
+
"ltd": {
|
|
1948
|
+
"hash": "b90d8b0f54a0",
|
|
1949
|
+
"scorecard": "101608 passed, 0 warnings, 0 failed"
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1900
1952
|
}
|
|
1901
1953
|
]
|
|
1902
1954
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2006-2026 DIY Accounting Limited
|
|
3
|
+
|
|
4
|
+
import { canonicalForUnit } from "../canonical-report-value.js";
|
|
5
|
+
|
|
6
|
+
export function fmt(v, unit = "money") {
|
|
7
|
+
if (v === null || v === undefined || v === "" || v === " ") return "—";
|
|
8
|
+
if (typeof v === "number") {
|
|
9
|
+
const canonical = Number(canonicalForUnit(v, unit));
|
|
10
|
+
// A nil that arrived by negation carries a sign bit and prints as "-0",
|
|
11
|
+
// which reads as a defect in a statement.
|
|
12
|
+
return (canonical === 0 ? 0 : canonical).toLocaleString("en-GB", { minimumFractionDigits: 0, maximumFractionDigits: 2 });
|
|
13
|
+
}
|
|
14
|
+
return String(v);
|
|
15
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2006-2026 DIY Accounting Limited
|
|
3
|
+
|
|
4
|
+
import { toExcelSerial } from "../spreadsheet-runner.js";
|
|
5
|
+
import { parseDate } from "../scenario-loader.js";
|
|
6
|
+
import {
|
|
7
|
+
PAYE_DUE_DAY,
|
|
8
|
+
PAYE_SCHEDULE_FIRST_ROW,
|
|
9
|
+
PAYE_SCHEDULE_MONTH_TABS,
|
|
10
|
+
PAYSLIPS_EMPLOYEE_BASE_ROWS,
|
|
11
|
+
PAYSLIPS_EMPLOYEE_START_DATE_OFFSET,
|
|
12
|
+
PAYSLIPS_ENTRY_COLUMNS,
|
|
13
|
+
payeTaxMonthDates,
|
|
14
|
+
payslipsMonthEntryRows,
|
|
15
|
+
payslipsStartDate,
|
|
16
|
+
payslipsWagesPaidCell,
|
|
17
|
+
} from "../payslips-layout.js";
|
|
18
|
+
|
|
19
|
+
// Payslips.xlsx Jul (sheet5.xml) and Aug (sheet6.xml) shipped with 35 dead
|
|
20
|
+
// #REF! cells: Jul!F11:F15 lost the weekly-block pay-date reference
|
|
21
|
+
// (Employee!F$24/F$26 compared against E$9), Jul!T41 carried a stray
|
|
22
|
+
// formula where every other month has a literal 0, and Aug!H11:M15 (K only
|
|
23
|
+
// on rows 12-15) lost the previous month's row-41 brought-forward
|
|
24
|
+
// reference. additionalReads never read either tab directly -- only the
|
|
25
|
+
// Payment/Admin aggregates -- so none of it ever surfaced. Every fixture's
|
|
26
|
+
// employees pay monthly, so the weekly-block gate (Employee!D$28 etc = "m")
|
|
27
|
+
// and the carry-forward gate (Aug!T$9 = "Y") are never true: both blocks
|
|
28
|
+
// always resolve to their blank/nil branch regardless of what the broken
|
|
29
|
+
// formula pointed at, which is exactly why the #REF!s shipped unnoticed.
|
|
30
|
+
export const PAYSLIPS_WEEKLY_ROWS = [11, 12, 13, 14, 15];
|
|
31
|
+
export const PAYSLIPS_JUL_DEAD_CELLS = [...PAYSLIPS_WEEKLY_ROWS.map((row) => `F${row}`), "T41"];
|
|
32
|
+
export const PAYSLIPS_AUG_BROUGHT_FORWARD_CELLS = PAYSLIPS_WEEKLY_ROWS.flatMap((row) => {
|
|
33
|
+
const cells = ["H", "I", "J", "L", "M"].map((col) => `${col}${row}`);
|
|
34
|
+
if (row >= 12) cells.push(`K${row}`);
|
|
35
|
+
return cells;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// The cells Payslips.xlsx cellWrites populates for a month's payroll, one
|
|
39
|
+
// employee a row down the month's own monthly block, plus the wages-paid date
|
|
40
|
+
// the block is dated from.
|
|
41
|
+
export const payslipsMonthEntryCells = (monthIndex) => [
|
|
42
|
+
payslipsWagesPaidCell(monthIndex),
|
|
43
|
+
...payslipsMonthEntryRows(monthIndex).flatMap((row) => Object.values(PAYSLIPS_ENTRY_COLUMNS).map((col) => `${col}${row}`)),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// The Employee sheet of Payslips.xlsx: the business details and up to five
|
|
47
|
+
// employee blocks. payrollStart is the payroll year the package's calendar runs
|
|
48
|
+
// on, payrollOpened the date the payroll record opened.
|
|
49
|
+
export function writePayslipsEmployee(scenario, { payrollStart, payrollOpened, shiftDate }) {
|
|
50
|
+
// Employee blocks start at rows 13, 39, 65, 91, 117 (26-row intervals)
|
|
51
|
+
const EMP_BASE_ROWS = PAYSLIPS_EMPLOYEE_BASE_ROWS;
|
|
52
|
+
const emp = {};
|
|
53
|
+
|
|
54
|
+
// Business details in Payslips Employee sheet
|
|
55
|
+
const biz = scenario.business || {};
|
|
56
|
+
if (biz.name) emp.D5 = biz.name;
|
|
57
|
+
if (biz.address) emp.D6 = biz.address;
|
|
58
|
+
if (biz.town) emp.D7 = biz.town;
|
|
59
|
+
if (biz.postcode) emp.D9 = biz.postcode;
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < Math.min(scenario.employees.length, 5); i++) {
|
|
62
|
+
const e = scenario.employees[i];
|
|
63
|
+
const base = EMP_BASE_ROWS[i];
|
|
64
|
+
if (e.name) {
|
|
65
|
+
const parts = e.name.split(" ");
|
|
66
|
+
emp[`D${base + 2}`] = parts.slice(-1)[0]; // surname
|
|
67
|
+
emp[`D${base + 3}`] = parts.slice(0, -1).join(" "); // forename(s)
|
|
68
|
+
}
|
|
69
|
+
if (e.niNumber) emp[`M${base + 2}`] = e.niNumber;
|
|
70
|
+
// The date the employee joined, read against the payroll year this
|
|
71
|
+
// package's calendar runs on. Without it the employee's line on every
|
|
72
|
+
// month tab stays blank and the printed payslip prints no figures.
|
|
73
|
+
if (e.startDate && payrollStart) {
|
|
74
|
+
const joined = parseDate(e.startDate);
|
|
75
|
+
const onSheet = payslipsStartDate(joined, payrollOpened, shiftDate(joined), payrollStart);
|
|
76
|
+
emp[`D${base + PAYSLIPS_EMPLOYEE_START_DATE_OFFSET}`] = toExcelSerial(
|
|
77
|
+
onSheet.getUTCFullYear(),
|
|
78
|
+
onSheet.getUTCMonth() + 1,
|
|
79
|
+
onSheet.getUTCDate(),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
emp[`D${base + 15}`] = e.payFrequency === "weekly" ? "W" : "M";
|
|
83
|
+
// The payroll number, not the scenario's own employee id. The printed
|
|
84
|
+
// payslip adds this cell to its block's start row to find the
|
|
85
|
+
// employee's line on the month tab, so a name in it takes the whole
|
|
86
|
+
// page's arithmetic with it.
|
|
87
|
+
emp[`D${base + 16}`] = i + 1;
|
|
88
|
+
emp[`D${base + 17}`] = e.isDirector ? "D" : e.niCategory || "A";
|
|
89
|
+
}
|
|
90
|
+
return emp;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The PAYE remittance schedule: one row per tax month, each with the month it
|
|
94
|
+
// ends and the day the payment falls due.
|
|
95
|
+
export function checkPayeSchedule({ check, num }, paymentSchedule, payrollYearOpens) {
|
|
96
|
+
if (paymentSchedule && payrollYearOpens) {
|
|
97
|
+
const asSerial = (day) => toExcelSerial(day.getUTCFullYear(), day.getUTCMonth() + 1, day.getUTCDate());
|
|
98
|
+
PAYE_SCHEDULE_MONTH_TABS.forEach((tab, taxMonth) => {
|
|
99
|
+
const row = PAYE_SCHEDULE_FIRST_ROW + taxMonth;
|
|
100
|
+
const { ends, due } = payeTaxMonthDates(payrollYearOpens, taxMonth);
|
|
101
|
+
check(
|
|
102
|
+
`Payslips!Payment B${row} tax month ${taxMonth + 1} ends on the last day of ${tab}`,
|
|
103
|
+
num(paymentSchedule[`B${row}`]),
|
|
104
|
+
asSerial(ends),
|
|
105
|
+
0,
|
|
106
|
+
);
|
|
107
|
+
check(
|
|
108
|
+
`Payslips!Payment C${row} tax month ${taxMonth + 1} is due on the ${PAYE_DUE_DAY}th after it`,
|
|
109
|
+
num(paymentSchedule[`C${row}`]),
|
|
110
|
+
asSerial(due),
|
|
111
|
+
0,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2006-2026 DIY Accounting Limited
|
|
3
|
+
|
|
4
|
+
// ── Sales invoice sample line (Salesinvoice.xlsx) ───────────────────────────
|
|
5
|
+
// The customer-facing invoice template has no external link into the rest of
|
|
6
|
+
// the book, so its own VAT rate is proved directly: the generator now writes
|
|
7
|
+
// the tax year's standard rate into Product Details!D2:D99 (generator.js),
|
|
8
|
+
// and this checks one sample invoice line, anchored to the fixture's own
|
|
9
|
+
// first sale, computes the right net, VAT and gross from it. Row 2 of
|
|
10
|
+
// Product Details already ships the template's own placeholder product code
|
|
11
|
+
// (A2 = 1001); this only sets its selling price. Business Details!B11 is the
|
|
12
|
+
// VAT registration number the invoice template itself gates its per-row VAT
|
|
13
|
+
// lookup on (verified against the XML: Invoice Template!N38 reads
|
|
14
|
+
// 'Product Details'!D:D only when O8, which reads 'Business Details'!B11, is
|
|
15
|
+
// not blank) -- an unregistered business is left untouched, the same as the
|
|
16
|
+
// template's own guidance for it. The invoice also carries a carriage charge
|
|
17
|
+
// (Invoice Database!E, the "Carriage Charge" column), which the template
|
|
18
|
+
// taxes separately from the product lines in Invoice Template!P62. That term
|
|
19
|
+
// used to read a literal 0.2; it now reads 'Product Details'!$D$2, the same
|
|
20
|
+
// cell every row of D2:D99 carries the tax year's rate into, so carriage is
|
|
21
|
+
// taxed at the written rate like every other line.
|
|
22
|
+
export const SALESINVOICE_VAT_REG_CELL = "B11";
|
|
23
|
+
// The same sheet's "Telephone" box, the entry cell beside its A8 label.
|
|
24
|
+
export const SALESINVOICE_TELEPHONE_CELL = "B8";
|
|
25
|
+
export const SALESINVOICE_SAMPLE_PRODUCT_CODE = 1001;
|
|
26
|
+
export const SALESINVOICE_SAMPLE_PRODUCT_ROW = 2;
|
|
27
|
+
export const SALESINVOICE_SAMPLE_CARRIAGE_CHARGE = 37.5;
|
|
28
|
+
export const SALESINVOICE_PRODUCT_DETAILS_COLUMNS = { code: "A", price: "C", vatRate: "D" };
|
|
29
|
+
export const SALESINVOICE_INVOICE_DATABASE_COLUMNS = {
|
|
30
|
+
activate: "A",
|
|
31
|
+
invoiceNumber: "B",
|
|
32
|
+
carriage: "E",
|
|
33
|
+
productCode1: "F",
|
|
34
|
+
quantity1: "G",
|
|
35
|
+
};
|
|
36
|
+
export const SALESINVOICE_INVOICE_TEMPLATE_CELLS = { netTotal: "P58", carriageNet: "P60", vatTotal: "P62", grossTotal: "P64" };
|
|
37
|
+
export const SALESINVOICE_LINE1_CELLS = { productCode: "C38", unitPrice: "J38", quantity: "L38", lineNet: "P38", lineVat: "V38" };
|
|
38
|
+
|
|
39
|
+
// The Salesinvoice.xlsx writes for a scenario: the telephone number, and one
|
|
40
|
+
// sample invoice line for a VAT-registered business.
|
|
41
|
+
export function writeSalesinvoice(scenario, rate) {
|
|
42
|
+
// Anchored to the fixture's own first sale so the customer-facing invoice
|
|
43
|
+
// total and VAT can be checked against a real figure (see checkSalesinvoice).
|
|
44
|
+
const salesinvoiceWrites = {};
|
|
45
|
+
// The invoice's own letterhead carries the business telephone number,
|
|
46
|
+
// which no other sheet in these workbooks has a box for.
|
|
47
|
+
if (scenario.business?.phone) salesinvoiceWrites["Business Details"] = { [SALESINVOICE_TELEPHONE_CELL]: scenario.business.phone };
|
|
48
|
+
const firstInvoiceSale = Object.values(scenario.sales || {}).flat()[0];
|
|
49
|
+
if (rate > 0 && scenario.business?.vat_number && firstInvoiceSale) {
|
|
50
|
+
salesinvoiceWrites["Business Details"] = {
|
|
51
|
+
...salesinvoiceWrites["Business Details"],
|
|
52
|
+
[SALESINVOICE_VAT_REG_CELL]: scenario.business.vat_number,
|
|
53
|
+
};
|
|
54
|
+
salesinvoiceWrites["Invoice Database"] = {
|
|
55
|
+
[`${SALESINVOICE_INVOICE_DATABASE_COLUMNS.activate}2`]: 1,
|
|
56
|
+
[`${SALESINVOICE_INVOICE_DATABASE_COLUMNS.invoiceNumber}2`]: 1,
|
|
57
|
+
[`${SALESINVOICE_INVOICE_DATABASE_COLUMNS.carriage}2`]: SALESINVOICE_SAMPLE_CARRIAGE_CHARGE,
|
|
58
|
+
[`${SALESINVOICE_INVOICE_DATABASE_COLUMNS.productCode1}2`]: SALESINVOICE_SAMPLE_PRODUCT_CODE,
|
|
59
|
+
[`${SALESINVOICE_INVOICE_DATABASE_COLUMNS.quantity1}2`]: 1,
|
|
60
|
+
};
|
|
61
|
+
salesinvoiceWrites["Product Details"] = {
|
|
62
|
+
[`${SALESINVOICE_PRODUCT_DETAILS_COLUMNS.price}${SALESINVOICE_SAMPLE_PRODUCT_ROW}`]: firstInvoiceSale.amount,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return salesinvoiceWrites;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The customer-facing invoice against the tax year's own VAT rate.
|
|
69
|
+
export function checkSalesinvoice({ check, num }, results, expected, taxData, rate) {
|
|
70
|
+
// Salesinvoice.xlsx carries no external link to the rest of the book, so a
|
|
71
|
+
// wrong figure here never reaches the accounts -- it reaches the
|
|
72
|
+
// customer's customer. The generator writes the tax year's standard rate
|
|
73
|
+
// into Product Details!D2:D99 (generator.js); this checks the rate landed
|
|
74
|
+
// and that the one sample invoice line, plus the invoice's carriage
|
|
75
|
+
// charge, both compute their VAT correctly from it, hand-computed from the
|
|
76
|
+
// fixture's own first sale and the sample carriage charge.
|
|
77
|
+
const invoiceProductDetails = results["Salesinvoice.xlsx!Product Details"];
|
|
78
|
+
const invoiceTemplate = results["Salesinvoice.xlsx!Invoice Template"];
|
|
79
|
+
if (invoiceProductDetails && invoiceTemplate && taxData) {
|
|
80
|
+
const standardRatePercent = Math.round(taxData.vat.standard_rate * 100);
|
|
81
|
+
check(
|
|
82
|
+
"Salesinvoice Product Details: VAT Rate = the tax year's standard rate",
|
|
83
|
+
num(invoiceProductDetails[`${SALESINVOICE_PRODUCT_DETAILS_COLUMNS.vatRate}${SALESINVOICE_SAMPLE_PRODUCT_ROW}`]),
|
|
84
|
+
standardRatePercent,
|
|
85
|
+
0,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// cellWrites only puts the sample invoice line and carriage charge on the
|
|
89
|
+
// sheet for a VAT-registered business (rate > 0 and a VAT number) --
|
|
90
|
+
// otherwise Salesinvoice.xlsx keeps its blank template state, and these
|
|
91
|
+
// five figures would have nothing to compare against.
|
|
92
|
+
const firstInvoiceSale = Object.values(expected.sales || {}).flat()[0];
|
|
93
|
+
if (rate > 0 && expected.business?.vat_number && firstInvoiceSale) {
|
|
94
|
+
const expectedNet = firstInvoiceSale.amount;
|
|
95
|
+
const expectedLineVat = (expectedNet * standardRatePercent) / 100;
|
|
96
|
+
const expectedCarriageVat = (SALESINVOICE_SAMPLE_CARRIAGE_CHARGE * standardRatePercent) / 100;
|
|
97
|
+
const expectedVatTotal = expectedLineVat + expectedCarriageVat;
|
|
98
|
+
check(
|
|
99
|
+
"Salesinvoice: line VAT = price x quantity x the tax year's standard rate",
|
|
100
|
+
num(invoiceTemplate[SALESINVOICE_LINE1_CELLS.lineVat]),
|
|
101
|
+
expectedLineVat,
|
|
102
|
+
0.01,
|
|
103
|
+
);
|
|
104
|
+
check(
|
|
105
|
+
"Salesinvoice: net total = the invoice's one line",
|
|
106
|
+
num(invoiceTemplate[SALESINVOICE_INVOICE_TEMPLATE_CELLS.netTotal]),
|
|
107
|
+
expectedNet,
|
|
108
|
+
0.01,
|
|
109
|
+
);
|
|
110
|
+
check(
|
|
111
|
+
"Salesinvoice: carriage charge lands on the invoice",
|
|
112
|
+
num(invoiceTemplate[SALESINVOICE_INVOICE_TEMPLATE_CELLS.carriageNet]),
|
|
113
|
+
SALESINVOICE_SAMPLE_CARRIAGE_CHARGE,
|
|
114
|
+
0.01,
|
|
115
|
+
);
|
|
116
|
+
check(
|
|
117
|
+
"Salesinvoice: VAT total = line VAT plus carriage VAT at the tax year's standard rate",
|
|
118
|
+
num(invoiceTemplate[SALESINVOICE_INVOICE_TEMPLATE_CELLS.vatTotal]),
|
|
119
|
+
expectedVatTotal,
|
|
120
|
+
0.01,
|
|
121
|
+
);
|
|
122
|
+
check(
|
|
123
|
+
"Salesinvoice: amount payable = net plus carriage plus VAT",
|
|
124
|
+
num(invoiceTemplate[SALESINVOICE_INVOICE_TEMPLATE_CELLS.grossTotal]),
|
|
125
|
+
expectedNet + SALESINVOICE_SAMPLE_CARRIAGE_CHARGE + expectedVatTotal,
|
|
126
|
+
0.01,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2006-2026 DIY Accounting Limited
|
|
3
|
+
|
|
4
|
+
// The date an Excel serial stands for.
|
|
5
|
+
export function excelSerialToUtcDate(serial) {
|
|
6
|
+
const epoch = Date.UTC(1899, 11, 30);
|
|
7
|
+
return new Date(epoch + Math.round(serial) * 24 * 60 * 60 * 1000);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatSerialDate(serial) {
|
|
11
|
+
return excelSerialToUtcDate(serial).toLocaleDateString("en-GB", { timeZone: "UTC", day: "numeric", month: "long", year: "numeric" });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// The " (period ending d Month yyyy)" a VAT return line carries, or nothing
|
|
15
|
+
// at all when the form has no date on it.
|
|
16
|
+
export function periodEnding(serial) {
|
|
17
|
+
return serial ? ` (period ending ${formatSerialDate(serial)})` : "";
|
|
18
|
+
}
|