@diy-accounting-uk/diya-gl 1.2.65 → 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 +26 -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
|
@@ -1923,6 +1923,32 @@
|
|
|
1923
1923
|
"scorecard": "101608 passed, 0 warnings, 0 failed"
|
|
1924
1924
|
}
|
|
1925
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
|
+
}
|
|
1926
1952
|
}
|
|
1927
1953
|
]
|
|
1928
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
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
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 { vatCycleRows, vatReturnCoverage } from "../report-generator.js";
|
|
7
|
+
import { fmt } from "./format.js";
|
|
8
|
+
import { formatSerialDate, periodEnding } from "./serial-dates.js";
|
|
9
|
+
import { netOfVat } from "./vat-rate.js";
|
|
10
|
+
|
|
11
|
+
// ── Vatinterface layout ────────────────────────────────────────────────────
|
|
12
|
+
// One row per VAT period, in date order. Rows 6-17 are the twelve accounting
|
|
13
|
+
// months in period order. Rows 4 and 5 are the two VAT periods before the
|
|
14
|
+
// accounting year, rows 18 to 20 the three after it; each is fed by its own
|
|
15
|
+
// S/P entry sheet rather than by a month tab. Column B is the period end date
|
|
16
|
+
// every VATQtr sheet looks up on, C the payment due date, D/F the period's
|
|
17
|
+
// sales net and output VAT, H/J its purchases net and input VAT, and E/G/I/K
|
|
18
|
+
// the rolling three-row sums the VAT boxes read. M carries the flat-rate flag
|
|
19
|
+
// box 6 switches on.
|
|
20
|
+
export const VATINTERFACE_ROWS = { first: 4, last: 20, firstMonth: 6 };
|
|
21
|
+
|
|
22
|
+
// Straddling VAT period name to the Vatinterface row it feeds, and to the
|
|
23
|
+
// pair of entry sheets it is entered on (S<period> and P<period>).
|
|
24
|
+
export const STRADDLING_PERIOD_ROWS = { "02Y1": 4, "03Y1": 5, "04Y2": 18, "05Y2": 19, "06Y2": 20 };
|
|
25
|
+
|
|
26
|
+
// A business that is not registered for VAT turns the rate off on the first
|
|
27
|
+
// month's Sales tab, and the rest of the book follows that cell.
|
|
28
|
+
export function writeVatRateCell(salesWrites, firstTab, rateCell, rate) {
|
|
29
|
+
if (!salesWrites[firstTab]) salesWrites[firstTab] = {};
|
|
30
|
+
salesWrites[firstTab][rateCell] = rate * 100;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// layout names what differs between the products' VAT workbooks:
|
|
34
|
+
// workbook the VAT workbook's file name
|
|
35
|
+
// monthTabs (results) => the month tab names, in accounting-year order
|
|
36
|
+
// salesNetCell, salesVatCell, purchasesNetCell, purchasesVatCell
|
|
37
|
+
// the cells of a month tab that hold its net and VAT totals
|
|
38
|
+
|
|
39
|
+
// Straddling VAT periods: entries on the S<period> and P<period> entry sheets.
|
|
40
|
+
export function writeStraddlingPeriod(vatReturnWrites, entries, sheetPrefix, nameField, columns, { workbook, shiftDate }) {
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (!STRADDLING_PERIOD_ROWS[entry.period]) {
|
|
43
|
+
throw new Error(`Straddling VAT entry names period "${entry.period}", which ${workbook} has no sheet for`);
|
|
44
|
+
}
|
|
45
|
+
const sheetName = `${sheetPrefix}${entry.period}`;
|
|
46
|
+
if (!vatReturnWrites[sheetName]) vatReturnWrites[sheetName] = {};
|
|
47
|
+
const sheet = vatReturnWrites[sheetName];
|
|
48
|
+
// Matching the whole reference rather than its first letter keeps a
|
|
49
|
+
// write further right (AD, say) out of the count of rows already there.
|
|
50
|
+
const amountColumnKey = new RegExp(`^${columns.amount}\\d+$`);
|
|
51
|
+
const entryRow = Object.keys(sheet).filter((k) => amountColumnKey.test(k)).length + 5;
|
|
52
|
+
const d = shiftDate(parseDate(entry.date));
|
|
53
|
+
sheet[`${columns.date}${entryRow}`] = toExcelSerial(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
|
|
54
|
+
if (entry[nameField]) sheet[`${columns.name}${entryRow}`] = entry[nameField];
|
|
55
|
+
if (entry.invoice) sheet[`${columns.invoice}${entryRow}`] = entry.invoice;
|
|
56
|
+
if (entry.description && columns.description) sheet[`${columns.description}${entryRow}`] = entry.description;
|
|
57
|
+
sheet[`${columns.amount}${entryRow}`] = entry.amount;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The VAT the books actually charged, taken from the month tabs and from the
|
|
62
|
+
// return itself. Every other statement in this report is stated net, so a
|
|
63
|
+
// registered trader and an unregistered one carrying the same trade read
|
|
64
|
+
// identically without it.
|
|
65
|
+
export function vatSection(results, layout) {
|
|
66
|
+
const { workbook, salesVatCell, salesNetCell, purchasesVatCell, purchasesNetCell } = layout;
|
|
67
|
+
const tabs = layout.monthTabs(results).filter((tab) => results[`Sales.xlsx!${tab}`] || results[`Purchases.xlsx!${tab}`]);
|
|
68
|
+
const months = tabs.map((tab) => [results[`Sales.xlsx!${tab}`], results[`Purchases.xlsx!${tab}`]]);
|
|
69
|
+
if (months.length === 0) return null;
|
|
70
|
+
const monthCells = (file, ...refs) => tabs.flatMap((tab) => refs.map((ref) => `${file}!${tab}!${ref}`));
|
|
71
|
+
|
|
72
|
+
const num = (v) => (typeof v === "number" ? v : 0);
|
|
73
|
+
const sum = (side, cell) => months.reduce((total, pair) => total + num(pair[side]?.[cell]), 0);
|
|
74
|
+
const salesVat = sum(0, salesVatCell);
|
|
75
|
+
const salesNet = sum(0, salesNetCell);
|
|
76
|
+
const purchasesVat = sum(1, purchasesVatCell);
|
|
77
|
+
const purchasesNet = sum(1, purchasesNetCell);
|
|
78
|
+
|
|
79
|
+
const rows = [
|
|
80
|
+
{
|
|
81
|
+
label: "Sales invoiced including VAT",
|
|
82
|
+
value: fmt(salesNet + salesVat),
|
|
83
|
+
indent: 1,
|
|
84
|
+
cells: monthCells("Sales.xlsx", salesVatCell, salesNetCell),
|
|
85
|
+
},
|
|
86
|
+
{ label: "VAT charged on sales", value: fmt(salesVat), indent: 1, cells: monthCells("Sales.xlsx", salesVatCell) },
|
|
87
|
+
{ label: "Sales net of VAT", value: fmt(salesNet), indent: 1, cells: monthCells("Sales.xlsx", salesNetCell) },
|
|
88
|
+
{
|
|
89
|
+
label: "Purchases invoiced including VAT",
|
|
90
|
+
value: fmt(purchasesNet + purchasesVat),
|
|
91
|
+
indent: 1,
|
|
92
|
+
cells: monthCells("Purchases.xlsx", purchasesVatCell, purchasesNetCell),
|
|
93
|
+
},
|
|
94
|
+
{ label: "VAT reclaimed on purchases", value: fmt(purchasesVat), indent: 1, cells: monthCells("Purchases.xlsx", purchasesVatCell) },
|
|
95
|
+
{ label: "Purchases net of VAT", value: fmt(purchasesNet), indent: 1, cells: monthCells("Purchases.xlsx", purchasesNetCell) },
|
|
96
|
+
{
|
|
97
|
+
label: "**VAT due for the year**",
|
|
98
|
+
value: fmt(salesVat - purchasesVat),
|
|
99
|
+
indent: 0,
|
|
100
|
+
cells: [...monthCells("Sales.xlsx", salesVatCell), ...monthCells("Purchases.xlsx", purchasesVatCell)],
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
// The package ships five return forms: four quarters from the VAT start
|
|
104
|
+
// month and one more, for a business whose quarter stagger does not line up
|
|
105
|
+
// with those four. Printing four left the fifth out of the report
|
|
106
|
+
// altogether. Each form carries the period it was filled in for, and the
|
|
107
|
+
// cycle rows above the boxes say which months each one reaches.
|
|
108
|
+
const forms = [];
|
|
109
|
+
const quarterRows = [];
|
|
110
|
+
for (let q = 1; q <= 5; q++) {
|
|
111
|
+
const boxes = results[`${workbook}!VATQtr${q}`];
|
|
112
|
+
if (!boxes) continue;
|
|
113
|
+
const end = num(boxes.G5);
|
|
114
|
+
forms.push({ name: `Q${q}`, end: vatinterfaceRowEnding(results, end, layout) });
|
|
115
|
+
const period = periodEnding(end);
|
|
116
|
+
const form = `${workbook}!VATQtr${q}`;
|
|
117
|
+
quarterRows.push({ label: `Q${q}${period} box 1: VAT due on sales`, value: fmt(num(boxes.G9)), indent: 1, cells: [`${form}!G9`] });
|
|
118
|
+
quarterRows.push({
|
|
119
|
+
label: `Q${q}${period} box 4: VAT reclaimed on purchases`,
|
|
120
|
+
value: fmt(num(boxes.G15)),
|
|
121
|
+
indent: 1,
|
|
122
|
+
cells: [`${form}!G15`],
|
|
123
|
+
});
|
|
124
|
+
quarterRows.push({ label: `Q${q}${period} box 5: net VAT due`, value: fmt(num(boxes.G17)), indent: 1, cells: [`${form}!G17`] });
|
|
125
|
+
}
|
|
126
|
+
if (quarterRows.length > 0) {
|
|
127
|
+
rows.push(...vatCycleRows(vatinterfacePeriods(results, layout), forms));
|
|
128
|
+
rows.push({ label: "**The return forms as the package fills them in**", value: "" });
|
|
129
|
+
rows.push(...quarterRows);
|
|
130
|
+
}
|
|
131
|
+
return { title: "VAT Returns", rows };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The VAT periods the interface carries, one per row, with the VAT on each
|
|
135
|
+
// side and whether the period is one of the twelve accounting months.
|
|
136
|
+
export function vatinterfacePeriods(results, layout) {
|
|
137
|
+
const vatinterface = results[`${layout.workbook}!Vatinterface`];
|
|
138
|
+
if (!vatinterface) return [];
|
|
139
|
+
const num = (v) => (typeof v === "number" ? v : 0);
|
|
140
|
+
const periods = [];
|
|
141
|
+
for (let row = VATINTERFACE_ROWS.first; row <= VATINTERFACE_ROWS.last; row++) {
|
|
142
|
+
const end = num(vatinterface[`B${row}`]);
|
|
143
|
+
if (!end) continue;
|
|
144
|
+
periods.push({
|
|
145
|
+
row,
|
|
146
|
+
endLabel: formatSerialDate(end),
|
|
147
|
+
outputVat: num(vatinterface[`F${row}`]),
|
|
148
|
+
inputVat: num(vatinterface[`J${row}`]),
|
|
149
|
+
inAccountingYear: row >= VATINTERFACE_ROWS.firstMonth && row < VATINTERFACE_ROWS.firstMonth + 12,
|
|
150
|
+
cells: {
|
|
151
|
+
end: `${layout.workbook}!Vatinterface!B${row}`,
|
|
152
|
+
output: `${layout.workbook}!Vatinterface!F${row}`,
|
|
153
|
+
input: `${layout.workbook}!Vatinterface!J${row}`,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return periods;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The interface row a return form's own period end date lands on, or null
|
|
161
|
+
// when the form names a date the interface does not carry.
|
|
162
|
+
export function vatinterfaceRowEnding(results, end, layout) {
|
|
163
|
+
const vatinterface = results[`${layout.workbook}!Vatinterface`];
|
|
164
|
+
if (!vatinterface || !end) return null;
|
|
165
|
+
for (let row = VATINTERFACE_ROWS.first; row <= VATINTERFACE_ROWS.last; row++) {
|
|
166
|
+
if (Math.round(typeof vatinterface[`B${row}`] === "number" ? vatinterface[`B${row}`] : 0) === Math.round(end)) return row;
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// The Vatinterface and the five return forms against the month tabs and
|
|
172
|
+
// straddling entry sheets that feed them.
|
|
173
|
+
export function checkVatChain({ check, num, rate }, results, expected, layout) {
|
|
174
|
+
// ── Vatinterface: where in the VAT chain a break happened ────────────────
|
|
175
|
+
//
|
|
176
|
+
// The box checks above catch a break; these say where it is. Each interface
|
|
177
|
+
// row is compared against the leaf workbook or the straddling entry sheet
|
|
178
|
+
// that feeds it, each quarter column against the three period rows it sums,
|
|
179
|
+
// and each VAT box against the interface row its LOOKUP lands on. A month
|
|
180
|
+
// link that stops carrying fails on that month and side alone.
|
|
181
|
+
const vatinterface = results[`${layout.workbook}!Vatinterface`];
|
|
182
|
+
if (vatinterface) {
|
|
183
|
+
layout.monthTabs(results).forEach((tab, i) => {
|
|
184
|
+
const row = VATINTERFACE_ROWS.firstMonth + i;
|
|
185
|
+
const salesMonth = results[`Sales.xlsx!${tab}`];
|
|
186
|
+
const purchasesMonth = results[`Purchases.xlsx!${tab}`];
|
|
187
|
+
if (salesMonth) {
|
|
188
|
+
check(
|
|
189
|
+
`Vatinterface D${row}: ${tab} sales net = Sales.xlsx ${tab}`,
|
|
190
|
+
num(vatinterface[`D${row}`]),
|
|
191
|
+
num(salesMonth[layout.salesNetCell]),
|
|
192
|
+
);
|
|
193
|
+
check(
|
|
194
|
+
`Vatinterface F${row}: ${tab} output VAT = Sales.xlsx ${tab}`,
|
|
195
|
+
num(vatinterface[`F${row}`]),
|
|
196
|
+
num(salesMonth[layout.salesVatCell]),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (purchasesMonth) {
|
|
200
|
+
check(
|
|
201
|
+
`Vatinterface H${row}: ${tab} purchases net = Purchases.xlsx ${tab}`,
|
|
202
|
+
num(vatinterface[`H${row}`]),
|
|
203
|
+
num(purchasesMonth[layout.purchasesNetCell]),
|
|
204
|
+
);
|
|
205
|
+
check(
|
|
206
|
+
`Vatinterface J${row}: ${tab} input VAT = Purchases.xlsx ${tab}`,
|
|
207
|
+
num(vatinterface[`J${row}`]),
|
|
208
|
+
num(purchasesMonth[layout.purchasesVatCell]),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// The straddling periods, anchored in the entries the scenario put on
|
|
214
|
+
// their own sheets. The sheets compute VAT from the gross figure at the
|
|
215
|
+
// standard rate, so the expectation splits the same gross the same way.
|
|
216
|
+
const straddlingGross = (entries) => {
|
|
217
|
+
const byPeriod = {};
|
|
218
|
+
for (const entry of entries || []) byPeriod[entry.period] = (byPeriod[entry.period] || 0) + entry.amount;
|
|
219
|
+
return byPeriod;
|
|
220
|
+
};
|
|
221
|
+
const straddlingSales = straddlingGross(expected.vat_straddling_sales);
|
|
222
|
+
const straddlingPurchases = straddlingGross(expected.vat_straddling_purchases);
|
|
223
|
+
if (expected.vat_straddling_sales || expected.vat_straddling_purchases) {
|
|
224
|
+
for (const [period, row] of Object.entries(STRADDLING_PERIOD_ROWS)) {
|
|
225
|
+
const salesGross = straddlingSales[period] || 0;
|
|
226
|
+
const purchasesGross = straddlingPurchases[period] || 0;
|
|
227
|
+
check(
|
|
228
|
+
`Vatinterface D${row}: ${period} sales net = the straddling sales entered for that period`,
|
|
229
|
+
num(vatinterface[`D${row}`]),
|
|
230
|
+
netOfVat(salesGross, rate),
|
|
231
|
+
);
|
|
232
|
+
check(
|
|
233
|
+
`Vatinterface F${row}: ${period} output VAT = the straddling sales entered for that period`,
|
|
234
|
+
num(vatinterface[`F${row}`]),
|
|
235
|
+
salesGross - netOfVat(salesGross, rate),
|
|
236
|
+
);
|
|
237
|
+
check(
|
|
238
|
+
`Vatinterface H${row}: ${period} purchases net = the straddling purchases entered for that period`,
|
|
239
|
+
num(vatinterface[`H${row}`]),
|
|
240
|
+
netOfVat(purchasesGross, rate),
|
|
241
|
+
);
|
|
242
|
+
check(
|
|
243
|
+
`Vatinterface J${row}: ${period} input VAT = the straddling purchases entered for that period`,
|
|
244
|
+
num(vatinterface[`J${row}`]),
|
|
245
|
+
purchasesGross - netOfVat(purchasesGross, rate),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const quarterColumns = [
|
|
251
|
+
["E", "D", "sales net"],
|
|
252
|
+
["G", "F", "output VAT"],
|
|
253
|
+
["I", "H", "purchases net"],
|
|
254
|
+
["K", "J", "input VAT"],
|
|
255
|
+
];
|
|
256
|
+
for (let q = 1; q <= 5; q++) {
|
|
257
|
+
const qtr = results[`${layout.workbook}!VATQtr${q}`];
|
|
258
|
+
if (!qtr || typeof qtr.G5 !== "number") continue;
|
|
259
|
+
let row = null;
|
|
260
|
+
for (let r = VATINTERFACE_ROWS.first; r <= VATINTERFACE_ROWS.last; r++) {
|
|
261
|
+
if (Math.round(num(vatinterface[`B${r}`])) === Math.round(qtr.G5)) row = r;
|
|
262
|
+
}
|
|
263
|
+
check(`VAT Q${q}: quarter end date is one of the Vatinterface periods`, row === null ? 0 : 1, 1, 0);
|
|
264
|
+
if (row === null) continue;
|
|
265
|
+
|
|
266
|
+
if (row - 2 >= VATINTERFACE_ROWS.first) {
|
|
267
|
+
for (const [total, period, label] of quarterColumns) {
|
|
268
|
+
check(
|
|
269
|
+
`Vatinterface ${total}${row}: quarter ${label} = its three period rows`,
|
|
270
|
+
num(vatinterface[`${total}${row}`]),
|
|
271
|
+
num(vatinterface[`${period}${row - 2}`]) + num(vatinterface[`${period}${row - 1}`]) + num(vatinterface[`${period}${row}`]),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
check(`VAT Q${q}: box 1 (G9) = Vatinterface quarter VAT due (G${row})`, num(qtr.G9), num(vatinterface[`G${row}`]));
|
|
277
|
+
check(`VAT Q${q}: box 4 (G15) = Vatinterface quarter VAT reclaimed (K${row})`, num(qtr.G15), num(vatinterface[`K${row}`]));
|
|
278
|
+
check(`VAT Q${q}: box 7 (G23) = Vatinterface quarter purchases net (I${row})`, num(qtr.G23), num(vatinterface[`I${row}`]));
|
|
279
|
+
// Box 6 is sales net of VAT, or sales including VAT when the flat rate
|
|
280
|
+
// scheme flag in column M is set.
|
|
281
|
+
const flatRate = num(vatinterface[`M${row}`]) > 0;
|
|
282
|
+
check(
|
|
283
|
+
`VAT Q${q}: box 6 (G21) = Vatinterface quarter sales ${flatRate ? "including" : "net of"} VAT`,
|
|
284
|
+
num(qtr.G21),
|
|
285
|
+
num(vatinterface[`E${row}`]) + (flatRate ? num(vatinterface[`G${row}`]) : 0),
|
|
286
|
+
);
|
|
287
|
+
check(
|
|
288
|
+
`VAT Q${q}: payment due date (G7) = Vatinterface final date for payment (C${row})`,
|
|
289
|
+
num(qtr.G7),
|
|
290
|
+
num(vatinterface[`C${row}`]),
|
|
291
|
+
0,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── The five return forms as one cycle ────────────────────────
|
|
296
|
+
//
|
|
297
|
+
// Each form's own date decides which three interface rows it declares, so
|
|
298
|
+
// the five together are checked as a cycle: distinct periods, each a
|
|
299
|
+
// quarter after the one before it, Q1 to Q4 covering the twelve accounting
|
|
300
|
+
// months once each, and the fifth on the last period the interface
|
|
301
|
+
// carries.
|
|
302
|
+
const periods = vatinterfacePeriods(results, layout);
|
|
303
|
+
const returnForms = [];
|
|
304
|
+
for (let q = 1; q <= 5; q++) {
|
|
305
|
+
const qtr = results[`${layout.workbook}!VATQtr${q}`];
|
|
306
|
+
if (qtr && typeof qtr.G5 === "number") returnForms.push({ name: `Q${q}`, end: vatinterfaceRowEnding(results, qtr.G5, layout) });
|
|
307
|
+
}
|
|
308
|
+
const coverage = vatReturnCoverage(periods, returnForms);
|
|
309
|
+
if (coverage.placed.length === 5) {
|
|
310
|
+
const [q1, q2, q3, q4, q5] = coverage.placed;
|
|
311
|
+
check("VAT: the five returns end on five different periods", new Set(coverage.placed.map((form) => form.row)).size, 5, 0);
|
|
312
|
+
for (const [earlier, later] of [
|
|
313
|
+
[q1, q2],
|
|
314
|
+
[q2, q3],
|
|
315
|
+
[q3, q4],
|
|
316
|
+
[q4, q5],
|
|
317
|
+
]) {
|
|
318
|
+
check(`VAT: ${later.name} ends a quarter after ${earlier.name}`, later.row - earlier.row, 3, 0);
|
|
319
|
+
}
|
|
320
|
+
const quarterlyRows = new Set([q1, q2, q3, q4].flatMap((form) => form.covers));
|
|
321
|
+
check(
|
|
322
|
+
"VAT: Q1-Q4 cover every month of the accounting year",
|
|
323
|
+
periods.filter((period) => period.inAccountingYear && quarterlyRows.has(period.row)).length,
|
|
324
|
+
periods.filter((period) => period.inAccountingYear).length,
|
|
325
|
+
0,
|
|
326
|
+
);
|
|
327
|
+
check("VAT: Q5 ends on the last period the Vatinterface carries", q5.row, VATINTERFACE_ROWS.last, 0);
|
|
328
|
+
|
|
329
|
+
// Five consecutive quarters need fifteen periods and the interface
|
|
330
|
+
// carries seventeen, so no two returns reach the same one. A period
|
|
331
|
+
// declared twice would be filed twice, so it fails rather than warns,
|
|
332
|
+
// and the output VAT on it says what the second filing would repeat.
|
|
333
|
+
check("VAT: periods more than one of the five returns declares", coverage.shared.length, 0, 0);
|
|
334
|
+
check(
|
|
335
|
+
"VAT: output VAT declared on more than one of the five returns",
|
|
336
|
+
coverage.shared.reduce((total, period) => total + period.outputVat, 0),
|
|
337
|
+
0,
|
|
338
|
+
0,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|