@diy-accounting-uk/diya-gl 1.0.0
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 +63 -0
- package/bin/diya-gl-mcp.js +10 -0
- package/bin/diya-gl-read-workbook.js +11 -0
- package/bin/diya-gl-recalc.js +10 -0
- package/bin/diya-gl-write-workbook.js +12 -0
- package/bin/diya-gl.js +24 -0
- package/bin/run-dist-bin.js +23 -0
- package/dist/app/bin/build-diya-gl-spec.js +680 -0
- package/dist/app/bin/build-packages.js +149 -0
- package/dist/app/bin/build-reconciliation-pages.js +1052 -0
- package/dist/app/bin/build-sitemaps.js +51 -0
- package/dist/app/bin/compliance-report.js +125 -0
- package/dist/app/bin/cross-package-reconciliation.js +92 -0
- package/dist/app/bin/diya-gl-mcp.js +27 -0
- package/dist/app/bin/export.js +318 -0
- package/dist/app/bin/extract-scenarios.js +1348 -0
- package/dist/app/bin/generate.js +353 -0
- package/dist/app/bin/judge-reconciliation.js +649 -0
- package/dist/app/bin/reconcile.js +247 -0
- package/dist/app/bin/report.js +291 -0
- package/dist/app/bin/verify-roundtrip.js +941 -0
- package/dist/app/bin/verify-stability.js +429 -0
- package/dist/app/bin/write-workbook.js +106 -0
- package/dist/app/data/filing/ct-computation-v1.1.toml +1033 -0
- package/dist/app/data/filing/ct600-v3.toml +2329 -0
- package/dist/app/data/filing/frs105-formats.toml +244 -0
- package/dist/app/data/fixture-master-gaps.json +247 -0
- package/dist/app/data/hmrc/SOURCE.md +14 -0
- package/dist/app/data/hmrc/form-layouts/bst.json +116 -0
- package/dist/app/data/hmrc/form-layouts/ltd.json +452 -0
- package/dist/app/data/hmrc/form-layouts/se.json +485 -0
- package/dist/app/data/hmrc/form-layouts/taxi.json +117 -0
- package/dist/app/data/hmrc/sa103-mtd-mapping.json +1078 -0
- package/dist/app/data/hmrc/sa103f_mapping_v3.csv +111 -0
- package/dist/app/data/judge-rubric.md +52 -0
- package/dist/app/data/ltd-2020.toml +49 -0
- package/dist/app/data/ltd-2021.toml +49 -0
- package/dist/app/data/ltd-2022.toml +49 -0
- package/dist/app/data/ltd-2023.toml +49 -0
- package/dist/app/data/ltd-2024.toml +49 -0
- package/dist/app/data/ltd-2025.toml +49 -0
- package/dist/app/data/ltd-2026.toml +53 -0
- package/dist/app/data/ltd-2027.toml +51 -0
- package/dist/app/data/render-unrepresentable/bst.json +24 -0
- package/dist/app/data/render-unrepresentable/ltd.json +863 -0
- package/dist/app/data/render-unrepresentable/se.json +455 -0
- package/dist/app/data/render-unrepresentable/taxi.json +16 -0
- package/dist/app/data/roundtrip-budget.json +38 -0
- package/dist/app/data/roundtrip-matrix-budget.json +26 -0
- package/dist/app/data/roundtrip-unrepresentable.json +401 -0
- package/dist/app/data/se-2020-2021.toml +52 -0
- package/dist/app/data/se-2021-2022.toml +52 -0
- package/dist/app/data/se-2022-2023.toml +55 -0
- package/dist/app/data/se-2023-2024.toml +54 -0
- package/dist/app/data/se-2024-2025.toml +51 -0
- package/dist/app/data/se-2025-2026.toml +51 -0
- package/dist/app/data/se-2026-2027.toml +51 -0
- package/dist/app/data/volatile-cells.json +16 -0
- package/dist/app/lib/anchors/bst.js +79 -0
- package/dist/app/lib/anchors/ltd.js +626 -0
- package/dist/app/lib/anchors/run.js +100 -0
- package/dist/app/lib/anchors/se.js +456 -0
- package/dist/app/lib/anchors/taxi.js +164 -0
- package/dist/app/lib/app-resources.js +80 -0
- package/dist/app/lib/book-checks/ltd.js +408 -0
- package/dist/app/lib/book-checks/se.js +687 -0
- package/dist/app/lib/book-checks/taxi.js +215 -0
- package/dist/app/lib/book-checks.js +872 -0
- package/dist/app/lib/books-engine.js +197 -0
- package/dist/app/lib/books-interchange.js +533 -0
- package/dist/app/lib/calculators/bst.js +381 -0
- package/dist/app/lib/calculators/ltd.js +1938 -0
- package/dist/app/lib/calculators/se.js +1239 -0
- package/dist/app/lib/calculators/shared.js +131 -0
- package/dist/app/lib/calculators/taxi.js +461 -0
- package/dist/app/lib/canonical-report-value.js +77 -0
- package/dist/app/lib/compliance-report.js +341 -0
- package/dist/app/lib/diya-gl-calculator.js +52 -0
- package/dist/app/lib/diya-gl-canonical.js +355 -0
- package/dist/app/lib/diya-gl-edits-ltd.js +144 -0
- package/dist/app/lib/diya-gl-edits.js +361 -0
- package/dist/app/lib/diya-gl-loader.js +774 -0
- package/dist/app/lib/diya-gl-schema.js +212 -0
- package/dist/app/lib/generator.js +1912 -0
- package/dist/app/lib/guide.js +33 -0
- package/dist/app/lib/headlines.js +293 -0
- package/dist/app/lib/link-caches.js +421 -0
- package/dist/app/lib/ltd-layout.js +139 -0
- package/dist/app/lib/mcp/diya-gl-tools.js +330 -0
- package/dist/app/lib/mcp/jsonrpc-stdio.js +66 -0
- package/dist/app/lib/mcp/server.js +65 -0
- package/dist/app/lib/overtype-sidecar.js +180 -0
- package/dist/app/lib/package-builder.js +202 -0
- package/dist/app/lib/payslips-date-chain.js +481 -0
- package/dist/app/lib/payslips-layout.js +235 -0
- package/dist/app/lib/period-shift.js +111 -0
- package/dist/app/lib/product-workbook.js +317 -0
- package/dist/app/lib/products.js +27 -0
- package/dist/app/lib/provenance-data.js +36 -0
- package/dist/app/lib/provenance.js +92 -0
- package/dist/app/lib/report-generator.js +374 -0
- package/dist/app/lib/report-indicators.js +468 -0
- package/dist/app/lib/report-serializer.js +392 -0
- package/dist/app/lib/scenario-extractor.js +1403 -0
- package/dist/app/lib/scenario-loader.js +84 -0
- package/dist/app/lib/sitemap-builder.js +59 -0
- package/dist/app/lib/spreadsheet-runner.js +566 -0
- package/dist/app/lib/tax/capital-allowances.js +77 -0
- package/dist/app/lib/tax/corporation-tax.js +144 -0
- package/dist/app/lib/tax/income-tax.js +122 -0
- package/dist/app/lib/tax/mileage.js +69 -0
- package/dist/app/lib/tax/national-insurance.js +31 -0
- package/dist/app/lib/tax/vat.js +188 -0
- package/dist/app/lib/tax-year.js +31 -0
- package/dist/app/lib/template-formula-map.js +253 -0
- package/dist/app/lib/workbook-set.js +153 -0
- package/dist/app/lib/xlsx-exporter.js +2815 -0
- package/dist/app/lib/xlsx-parts.js +111 -0
- package/dist/app/lib/xlsx-reader.js +84 -0
- package/dist/app/products/bst.js +727 -0
- package/dist/app/products/ltd.js +4270 -0
- package/dist/app/products/se.js +3403 -0
- package/dist/app/products/taxi.js +912 -0
- package/dist/app/templates/bst/bst-excel.xlsx +0 -0
- package/dist/app/templates/bst/meta.toml +27 -0
- package/dist/app/templates/ltd/Cashaccount.xlsx +0 -0
- package/dist/app/templates/ltd/Companysecretary.xlsx +0 -0
- package/dist/app/templates/ltd/Creditcardaccount.xlsx +0 -0
- package/dist/app/templates/ltd/Currentaccount.xlsx +0 -0
- package/dist/app/templates/ltd/Dividend Voucher.docx +0 -0
- package/dist/app/templates/ltd/Financialaccounts.xlsx +0 -0
- package/dist/app/templates/ltd/Fixedassets.xlsx +0 -0
- package/dist/app/templates/ltd/Payslips.xlsx +0 -0
- package/dist/app/templates/ltd/Purchases.xlsx +0 -0
- package/dist/app/templates/ltd/Sales.xlsx +0 -0
- package/dist/app/templates/ltd/Salesinvoice.xlsx +0 -0
- package/dist/app/templates/ltd/Savingaccount.xlsx +0 -0
- package/dist/app/templates/ltd/Vatreturns.xlsx +0 -0
- package/dist/app/templates/ltd/expensesform.xlsx +0 -0
- package/dist/app/templates/ltd/meta.toml +54 -0
- package/dist/app/templates/meta.toml +12 -0
- package/dist/app/templates/se/Bank.xlsx +0 -0
- package/dist/app/templates/se/Cash.xlsx +0 -0
- package/dist/app/templates/se/Financialaccounts.xlsx +0 -0
- package/dist/app/templates/se/Fixedassets.xlsx +0 -0
- package/dist/app/templates/se/Payslips.xlsx +0 -0
- package/dist/app/templates/se/Purchases.xlsx +0 -0
- package/dist/app/templates/se/Sales.xlsx +0 -0
- package/dist/app/templates/se/Salesinvoice.xlsx +0 -0
- package/dist/app/templates/se/Vat.xlsx +0 -0
- package/dist/app/templates/se/meta.toml +43 -0
- package/dist/app/templates/taxi/meta.toml +43 -0
- package/dist/app/templates/taxi/taxi-excel.xlsx +0 -0
- package/dist/web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-book-v2.schema.json +713 -0
- package/dist/web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-docs.md +930 -0
- package/dist/web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-lines-v2.schema.json +220 -0
- package/package.json +37 -0
|
@@ -0,0 +1,1938 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (C) 2026 DIY Accounting Ltd
|
|
3
|
+
//
|
|
4
|
+
// ltd.js — JS calculation engine for the Limited Company product.
|
|
5
|
+
//
|
|
6
|
+
// This module answers the question the Excel package answers: given the same
|
|
7
|
+
// book, journal and tax year, what does every cell the reconciliation reads
|
|
8
|
+
// hold? So it follows the workbook's own arithmetic rather than a tidier
|
|
9
|
+
// route to the same statement. Two consequences run through the file.
|
|
10
|
+
//
|
|
11
|
+
// A journal row takes its own VAT off its own gross and rounds nothing
|
|
12
|
+
// (Sales and Purchases column G = F * rate / (100 + rate), column H = F - G),
|
|
13
|
+
// and every analysis total is a sum of those row figures. Netting a month's
|
|
14
|
+
// gross instead leaves pennies behind, so sheetNet() is used wherever a
|
|
15
|
+
// statement line comes off a journal. The writer, by contrast, rounds a
|
|
16
|
+
// figure it puts on the Fixed Assets schedule to the penny, so a capital
|
|
17
|
+
// purchase or a disposal goes through writerNet().
|
|
18
|
+
//
|
|
19
|
+
// The workbook only holds what the writer put in it. cellWrites() fills the
|
|
20
|
+
// Fixed Assets schedule, the share register, the board minute and the stock
|
|
21
|
+
// count from the scenario, so where the scenario carries none of those the
|
|
22
|
+
// sheet keeps its own empty layout and every figure downstream follows. This
|
|
23
|
+
// engine reads the same scenario and lands in the same place, which is what
|
|
24
|
+
// makes the two comparable.
|
|
25
|
+
|
|
26
|
+
import { toExcelSerial } from "../spreadsheet-runner.js";
|
|
27
|
+
import { BANK_ACCOUNT_FILES, BANK_LAYOUTS, OPENING_FIXED_ASSET_COLUMNS, isLtdOpeningBankLine } from "../ltd-layout.js";
|
|
28
|
+
import { apportionCorporationTax, financialYearsInPeriod } from "../tax/corporation-tax.js";
|
|
29
|
+
import { calculateCapitalAllowances } from "../tax/capital-allowances.js";
|
|
30
|
+
import {
|
|
31
|
+
monthlyPayrollBlockRow,
|
|
32
|
+
PAYE_SCHEDULE_FIRST_ROW,
|
|
33
|
+
PAYE_SCHEDULE_MONTH_TAB_CELLS,
|
|
34
|
+
PAYE_SCHEDULE_MONTH_TABS,
|
|
35
|
+
payeTaxMonthDates,
|
|
36
|
+
PAYROLL_WEEKS_PER_MONTH,
|
|
37
|
+
PAYSLIP_PRINT_CELLS,
|
|
38
|
+
PAYSLIP_PRINT_FIRST_PAYROLL_NUMBER,
|
|
39
|
+
PAYSLIP_PRINT_MONTHLY_HEADING,
|
|
40
|
+
PAYSLIP_PRINT_PERIOD_CELLS,
|
|
41
|
+
PAYSLIP_PRINT_SHEET,
|
|
42
|
+
PAYSLIP_PRINT_PERIOD,
|
|
43
|
+
PAYSLIP_PRINT_TO_DATE_CELLS,
|
|
44
|
+
PAYSLIPS_DIRECTLY_READ_MONTH_INDEXES,
|
|
45
|
+
PAYSLIPS_ENTRY_COLUMNS,
|
|
46
|
+
PAYSLIPS_ZERO_FILLED_COLUMNS,
|
|
47
|
+
payslipsMonthEntryRows,
|
|
48
|
+
payslipsMonthPeriod,
|
|
49
|
+
payslipsPeriodStartCell,
|
|
50
|
+
payslipsWagesPaidCell,
|
|
51
|
+
payrollYearStart,
|
|
52
|
+
} from "../payslips-layout.js";
|
|
53
|
+
import { addMonths, endOfMonth, SHEET_BLANK } from "./shared.js";
|
|
54
|
+
import { registerOfficers } from "../scenario-loader.js";
|
|
55
|
+
|
|
56
|
+
const SHORT_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
57
|
+
const MONTH_COLS = ["C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N"];
|
|
58
|
+
const VAT_RATE = 0.2;
|
|
59
|
+
|
|
60
|
+
// Sales and Purchases analysis columns, code letter by column letter, in the
|
|
61
|
+
// order row 5 of each month tab tests them. Column AK of a Purchases month
|
|
62
|
+
// tab is the CIS certificates column, which the writer fills from a
|
|
63
|
+
// sub-contractor purchase rather than from a code letter.
|
|
64
|
+
export const SALES_ANALYSIS_COLUMNS = { a: "O", b: "P", c: "Q", d: "R", g: "S", o: "T", fs: "U" };
|
|
65
|
+
export const PURCHASE_ANALYSIS_COLUMNS = {
|
|
66
|
+
s: "O",
|
|
67
|
+
c: "P",
|
|
68
|
+
o: "Q",
|
|
69
|
+
d: "R",
|
|
70
|
+
w: "S",
|
|
71
|
+
r: "T",
|
|
72
|
+
p: "U",
|
|
73
|
+
t: "V",
|
|
74
|
+
q: "W",
|
|
75
|
+
m: "X",
|
|
76
|
+
u: "Y",
|
|
77
|
+
a: "Z",
|
|
78
|
+
g: "AA",
|
|
79
|
+
h: "AB",
|
|
80
|
+
v: "AC",
|
|
81
|
+
n: "AD",
|
|
82
|
+
f: "AE",
|
|
83
|
+
l: "AF",
|
|
84
|
+
y: "AG",
|
|
85
|
+
z: "AH",
|
|
86
|
+
fa: "AI",
|
|
87
|
+
};
|
|
88
|
+
const SALES_CIS_COLUMN = "V";
|
|
89
|
+
const PURCHASES_CIS_COLUMN = "AK";
|
|
90
|
+
|
|
91
|
+
// The two ledgers each journal workbook keeps beside its twelve month tabs,
|
|
92
|
+
// for the amounts owed at the year start and the year end.
|
|
93
|
+
const SALES_LEDGER_SHEETS = ["OpeningDebtors", "ClosingDebtors"];
|
|
94
|
+
const PURCHASES_LEDGER_SHEETS = ["OpeningCreditors", "ClosingCreditors"];
|
|
95
|
+
|
|
96
|
+
// The Admin B column is a chain of dates anchored at B32, the year end. It
|
|
97
|
+
// runs from thirteen months before the year end to four months after it.
|
|
98
|
+
const ADMIN_DATE_CHAIN_FIRST_ROW = 6;
|
|
99
|
+
const ADMIN_DATE_CHAIN_LAST_ROW = 40;
|
|
100
|
+
|
|
101
|
+
// The management P&L's five turnover rows, and the gap between each and the
|
|
102
|
+
// trial balance income row it reads. The trial balance holds income as a
|
|
103
|
+
// credit, so the P&L negates it back to a positive turnover.
|
|
104
|
+
const SALES_PL_ROWS = [4, 5, 6, 7, 8];
|
|
105
|
+
const SALES_ROW_OFFSET = 49;
|
|
106
|
+
const SALES_BAD_DEBT_ROW = 34;
|
|
107
|
+
|
|
108
|
+
// The management P&L's expense rows and the trial balance row each reads.
|
|
109
|
+
const EXPENSE_PL_ROWS = { 21: 68, 22: 69, 23: 70, 24: 71, 25: 72, 26: 73, 27: 74, 28: 75, 29: 76, 30: 77, 31: 78, 32: 79, 33: 80 };
|
|
110
|
+
|
|
111
|
+
// WagesInterface holds a month a row in two blocks, employees above
|
|
112
|
+
// directors, twelve rows each.
|
|
113
|
+
const WAGES_INTERFACE_EMPLOYEE_FIRST_ROW = 4;
|
|
114
|
+
const WAGES_INTERFACE_DIRECTOR_FIRST_ROW = 17;
|
|
115
|
+
|
|
116
|
+
// Fixed Assets schedule blocks. Each class has a block of rows for assets
|
|
117
|
+
// already owned with a totals row, and a second block for assets bought in
|
|
118
|
+
// the year with a totals row of its own. The published note reads one column
|
|
119
|
+
// per class.
|
|
120
|
+
const SCHEDULE_CLASSES = {
|
|
121
|
+
land: {
|
|
122
|
+
existingRows: [8, 9, 10],
|
|
123
|
+
existingTotalRow: 11,
|
|
124
|
+
newRows: [60, 61, 62, 63],
|
|
125
|
+
newTotalRow: 64,
|
|
126
|
+
noteColumn: "B",
|
|
127
|
+
rateCell: "H7",
|
|
128
|
+
openingKey: "land_buildings",
|
|
129
|
+
label: "Land & Property",
|
|
130
|
+
},
|
|
131
|
+
plant: {
|
|
132
|
+
existingRows: [14, 15, 16, 17, 18, 19, 20, 21],
|
|
133
|
+
existingTotalRow: 22,
|
|
134
|
+
newRows: [67, 68, 69, 70, 71, 72, 73, 74],
|
|
135
|
+
newTotalRow: 75,
|
|
136
|
+
noteColumn: "C",
|
|
137
|
+
rateCell: "H13",
|
|
138
|
+
openingKey: "plant_machinery",
|
|
139
|
+
label: "Plant & Machinery",
|
|
140
|
+
},
|
|
141
|
+
fixtures: {
|
|
142
|
+
existingRows: [25, 26, 27, 28, 29],
|
|
143
|
+
existingTotalRow: 30,
|
|
144
|
+
newRows: [78, 79, 80, 81, 82],
|
|
145
|
+
newTotalRow: 83,
|
|
146
|
+
noteColumn: "D",
|
|
147
|
+
rateCell: "H24",
|
|
148
|
+
openingKey: "fixtures_fittings",
|
|
149
|
+
label: "Fixtures & Fittings",
|
|
150
|
+
},
|
|
151
|
+
computer: {
|
|
152
|
+
existingRows: [33, 34, 35, 36, 37, 38, 39, 40],
|
|
153
|
+
existingTotalRow: 41,
|
|
154
|
+
newRows: [86, 87, 88, 89, 90, 91, 92, 93],
|
|
155
|
+
newTotalRow: 94,
|
|
156
|
+
noteColumn: "E",
|
|
157
|
+
rateCell: "H32",
|
|
158
|
+
openingKey: "computer_technology",
|
|
159
|
+
label: "Computers",
|
|
160
|
+
},
|
|
161
|
+
motor: {
|
|
162
|
+
existingRows: [50, 51, 52, 53, 54],
|
|
163
|
+
existingTotalRow: 55,
|
|
164
|
+
newRows: [103, 104, 105, 106, 107],
|
|
165
|
+
newTotalRow: 108,
|
|
166
|
+
noteColumn: "F",
|
|
167
|
+
rateCell: "H43",
|
|
168
|
+
openingKey: "motor_vehicles",
|
|
169
|
+
label: "Motor Vehicles",
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// The trial balance's own fixed asset rows, class by class.
|
|
174
|
+
const SCHEDULE_COST_ROWS = { land: 6, plant: 7, fixtures: 8, computer: 9, motor: 10 };
|
|
175
|
+
const SCHEDULE_DEPRECIATION_ROWS = { land: 11, plant: 12, fixtures: 13, computer: 14, motor: 15 };
|
|
176
|
+
|
|
177
|
+
// Assets bought in the year all land on the New Plant & Machinery rows,
|
|
178
|
+
// matching the writer.
|
|
179
|
+
const SCHEDULE_NEW_ASSET_CLASS = "plant";
|
|
180
|
+
|
|
181
|
+
// The classes whose new-asset rows carry a capital allowance formula, and
|
|
182
|
+
// their existing-block writing down allowance totals. Land claims none.
|
|
183
|
+
const SCHEDULE_ALLOWANCE_CLASSES = ["plant", "fixtures", "computer", "motor"];
|
|
184
|
+
|
|
185
|
+
// The cars sub-block of the motor class. Its rows carry a writing down
|
|
186
|
+
// allowance formula rather than an investment allowance one, and the
|
|
187
|
+
// calculator never places an asset on them, so each stays on the sheet's own
|
|
188
|
+
// blank branch.
|
|
189
|
+
const SCHEDULE_CAR_ROWS = [97, 98, 99, 100, 101];
|
|
190
|
+
|
|
191
|
+
// A class totals row states whether the schedule and the opening balance
|
|
192
|
+
// sheet agree about that class. The computer block's warning carries a
|
|
193
|
+
// spelling slip the template has always had, and the reconciliation reads the
|
|
194
|
+
// cell as it stands.
|
|
195
|
+
const SCHEDULE_DISAGREEMENT_TEXT = "Check Opening Balance Sheet figures agree";
|
|
196
|
+
const SCHEDULE_COMPUTER_DISAGREEMENT_TEXT = "Check Opening Balkance Sheet figures agree";
|
|
197
|
+
|
|
198
|
+
// Vatinterface rows: two VAT periods before the accounting year, its own
|
|
199
|
+
// twelve months, then three after. Each row carries one period's figures, and
|
|
200
|
+
// columns E, G, I and K carry the rolling three-row sums the return boxes
|
|
201
|
+
// read. Row 4 reads the Admin month end two rows above the period start, and
|
|
202
|
+
// every row after it is two Admin rows further on.
|
|
203
|
+
const VATINTERFACE_FIRST_ROW = 4;
|
|
204
|
+
const VATINTERFACE_LAST_ROW = 20;
|
|
205
|
+
const VATINTERFACE_FIRST_MONTH_ROW = 6;
|
|
206
|
+
const VATINTERFACE_FIRST_ADMIN_ROW = 6;
|
|
207
|
+
|
|
208
|
+
// A VAT period either side of the accounting year is entered on its own pair
|
|
209
|
+
// of sheets rather than reached through a month tab.
|
|
210
|
+
const STRADDLING_PERIOD_ROWS = { "02Y1": 4, "03Y1": 5, "04Y2": 18, "05Y2": 19, "06Y2": 20 };
|
|
211
|
+
|
|
212
|
+
// The payroll calendar: tax week 1 is the five days from 6 April, every week
|
|
213
|
+
// after it is seven days, and the payroll months take the weeks the shared
|
|
214
|
+
// layout names.
|
|
215
|
+
const PAYROLL_FIRST_WEEK_DAYS = 5;
|
|
216
|
+
const PAYSLIPS_CALENDAR_FIRST_ROW = 2;
|
|
217
|
+
|
|
218
|
+
const REGISTER_MEMBER_ROWS = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
|
|
219
|
+
// The register of directors and secretary runs one officer a row from row 2,
|
|
220
|
+
// and the register of directors' interests one a row from row 2 as well.
|
|
221
|
+
const DIRECTOR_SECRETARY_OFFICER_ROWS = [2, 3, 4, 5, 6, 7, 8];
|
|
222
|
+
const DIRECTORS_INTERESTS_ROWS = [2, 3, 4, 5, 6];
|
|
223
|
+
const SHARE_NOMINAL_VALUE = 1;
|
|
224
|
+
const CHARGE_REGISTER_ROWS = [2, 3, 4, 5, 6];
|
|
225
|
+
const HP_AGREEMENT_ROWS = [8, 10];
|
|
226
|
+
const EXPENSES_FORM_MONTHS = Array.from({ length: 12 }, (_, index) => `Month ${String(index + 1).padStart(2, "0")}`);
|
|
227
|
+
|
|
228
|
+
// The trial balance cells the reconciliation reads. The sheet carries a row
|
|
229
|
+
// for every account in the chart and the statements read it whole, so the
|
|
230
|
+
// engine builds the whole column and publishes this much of it.
|
|
231
|
+
const TRIAL_BALANCE_READS = [
|
|
232
|
+
"D6",
|
|
233
|
+
"D7",
|
|
234
|
+
"D8",
|
|
235
|
+
"D9",
|
|
236
|
+
"D10",
|
|
237
|
+
"D11",
|
|
238
|
+
"D12",
|
|
239
|
+
"D13",
|
|
240
|
+
"D14",
|
|
241
|
+
"D15",
|
|
242
|
+
"D19",
|
|
243
|
+
"D20",
|
|
244
|
+
"D22",
|
|
245
|
+
"D23",
|
|
246
|
+
"D24",
|
|
247
|
+
"D25",
|
|
248
|
+
"D28",
|
|
249
|
+
"D29",
|
|
250
|
+
"D30",
|
|
251
|
+
"D31",
|
|
252
|
+
"D33",
|
|
253
|
+
"D35",
|
|
254
|
+
"D39",
|
|
255
|
+
"D40",
|
|
256
|
+
"D42",
|
|
257
|
+
"D43",
|
|
258
|
+
"D91",
|
|
259
|
+
"EH35",
|
|
260
|
+
"EJ20",
|
|
261
|
+
"EJ22",
|
|
262
|
+
"EJ23",
|
|
263
|
+
"EJ24",
|
|
264
|
+
"EJ25",
|
|
265
|
+
"EJ26",
|
|
266
|
+
"EJ28",
|
|
267
|
+
"EJ29",
|
|
268
|
+
"EJ30",
|
|
269
|
+
"EJ31",
|
|
270
|
+
"EJ32",
|
|
271
|
+
"EJ33",
|
|
272
|
+
"EJ34",
|
|
273
|
+
"EJ35",
|
|
274
|
+
"EJ39",
|
|
275
|
+
"EJ40",
|
|
276
|
+
"EJ48",
|
|
277
|
+
"EJ66",
|
|
278
|
+
"EJ91",
|
|
279
|
+
"L34",
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
// Every trial balance row EJ91 adds up: the balance sheet down to the profit
|
|
283
|
+
// distribution, then the income and expense rows.
|
|
284
|
+
const AUDIT_ROWS = [
|
|
285
|
+
6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 20, 22, 23, 24, 25, 26, 28, 29, 30, 31, 32, 33, 34, 35, 37, 39, 40, 42, 43, 44, 47, 48,
|
|
286
|
+
49, 53, 54, 55, 56, 57, 58, 60, 61, 62, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87,
|
|
287
|
+
88, 89,
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
// ── Small helpers ──────────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
function sheetVat(gross, rate) {
|
|
293
|
+
return (gross * rate) / (1 + rate);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function sheetNet(gross, rate) {
|
|
297
|
+
return gross - sheetVat(gross, rate);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// The net figure the writer puts on a schedule row, rounded to the penny.
|
|
301
|
+
function writerNet(gross, rate) {
|
|
302
|
+
return Math.round((gross / (1 + rate)) * 100) / 100;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function sum(values) {
|
|
306
|
+
return values.reduce((total, value) => total + value, 0);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function sumValuesOf(object) {
|
|
310
|
+
return object ? sum(Object.values(object)) : 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function parseDate(value) {
|
|
314
|
+
if (value instanceof Date) return new Date(Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()));
|
|
315
|
+
const [year, month, day] = String(value).slice(0, 10).split("-").map(Number);
|
|
316
|
+
return new Date(Date.UTC(year, month - 1, day));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function serialOf(date) {
|
|
320
|
+
return toExcelSerial(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate());
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function fromSerial(serial) {
|
|
324
|
+
return new Date(Date.UTC(1899, 11, 30) + Math.round(serial) * 24 * 60 * 60 * 1000);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function zeroMonths() {
|
|
328
|
+
return Array.from({ length: 12 }, () => 0);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// The cells of a month's totals the report does not already carry. Those are
|
|
332
|
+
// the ones a sibling workbook's link reads and nothing else does.
|
|
333
|
+
function linkOnlyCells(all, reported) {
|
|
334
|
+
return Object.fromEntries(Object.entries(all).filter(([cell]) => !(cell in reported)));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ── The accounting period ──────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
// The twelve month tabs in accounting-period order, and the Admin sheet's own
|
|
340
|
+
// B-column month ends. B32 is the year end and every other B entry is two
|
|
341
|
+
// rows per month away from it, so a row number alone names a month end either
|
|
342
|
+
// side of the year.
|
|
343
|
+
function periodFrom(book) {
|
|
344
|
+
const yearEnd = parseDate(book.documentInfo?.periodCoveredEnd);
|
|
345
|
+
const start = addMonths(new Date(Date.UTC(yearEnd.getUTCFullYear(), yearEnd.getUTCMonth(), 1)), -11);
|
|
346
|
+
const tabs = Array.from({ length: 12 }, (_, index) => SHORT_MONTHS[(start.getUTCMonth() + index) % 12]);
|
|
347
|
+
const adminMonthEnd = (row) => endOfMonth(yearEnd, (row - 32) / 2);
|
|
348
|
+
return { yearEnd, start, tabs, adminMonthEnd };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Every journal entry bucketed onto the month tab it lands on. The scenario's
|
|
352
|
+
// month keys already sit in the book's own period, and the writer moves each
|
|
353
|
+
// entry by the gap between that period and the package's, which is nil when
|
|
354
|
+
// both are this book's.
|
|
355
|
+
function bucketByTab(journal, tabs) {
|
|
356
|
+
const buckets = Object.fromEntries(tabs.map((tab) => [tab, []]));
|
|
357
|
+
for (const transactions of Object.values(journal || {})) {
|
|
358
|
+
for (const transaction of transactions) {
|
|
359
|
+
const tab = SHORT_MONTHS[parseDate(transaction.date).getUTCMonth()];
|
|
360
|
+
if (buckets[tab]) buckets[tab].push(transaction);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return buckets;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── Sales and Purchases month tabs ─────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
// Row 1 of a month tab totals every analysis column, and an analysis column
|
|
369
|
+
// takes a row's net figure when the row's code letter matches.
|
|
370
|
+
function journalMonthTotals(transactions, rate, analysisColumns, defaultCode, cisColumn) {
|
|
371
|
+
const totals = { F1: 0, G1: 0, H1: 0, G2: rate * 100, [`${cisColumn}1`]: 0 };
|
|
372
|
+
for (const column of Object.values(analysisColumns)) totals[`${column}1`] = 0;
|
|
373
|
+
for (const transaction of transactions) {
|
|
374
|
+
const vat = sheetVat(transaction.amount, rate);
|
|
375
|
+
const net = transaction.amount - vat;
|
|
376
|
+
totals.F1 += transaction.amount;
|
|
377
|
+
totals.G1 += vat;
|
|
378
|
+
totals.H1 += net;
|
|
379
|
+
const column = analysisColumns[(transaction.code || defaultCode).toLowerCase()];
|
|
380
|
+
if (column) totals[`${column}1`] += net;
|
|
381
|
+
if (transaction.cis_deduction) totals[`${cisColumn}1`] += transaction.cis_deduction;
|
|
382
|
+
}
|
|
383
|
+
return totals;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ── Bank workbooks ─────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
// Each workbook's month tabs: the receipts and payments totals and each code
|
|
389
|
+
// letter's own total. A "BC"-coded entry dated the period's own opening day
|
|
390
|
+
// is the account's opening balance, which the workbook takes in A1 rather
|
|
391
|
+
// than as a statement line; a "BC"-coded entry any other day is an ordinary
|
|
392
|
+
// transfer statement line, the same as any other code (BC also names the
|
|
393
|
+
// sibling a Cashaccount.xlsx transfer points at, on every other workbook's
|
|
394
|
+
// own tabs -- see BANK_TRANSFER_CODES).
|
|
395
|
+
function bankMonthTotals(scenario, tabs, periodStart) {
|
|
396
|
+
const files = {};
|
|
397
|
+
for (const fileName of Object.values(BANK_ACCOUNT_FILES)) {
|
|
398
|
+
files[fileName] = {
|
|
399
|
+
opening: 0,
|
|
400
|
+
months: Object.fromEntries(tabs.map((tab) => [tab, { receipts: 0, payments: 0, receiptCodes: {}, paymentCodes: {} }])),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
for (const transactions of Object.values(scenario.bank || {})) {
|
|
404
|
+
for (const transaction of transactions) {
|
|
405
|
+
const fileName = BANK_ACCOUNT_FILES[transaction.account || "1200"];
|
|
406
|
+
if (!fileName) continue;
|
|
407
|
+
const file = files[fileName];
|
|
408
|
+
if (isLtdOpeningBankLine(transaction.code, parseDate(transaction.date), periodStart)) {
|
|
409
|
+
file.opening = transaction.amount;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const month = file.months[SHORT_MONTHS[parseDate(transaction.date).getUTCMonth()]];
|
|
413
|
+
if (!month) continue;
|
|
414
|
+
const receipt = transaction.direction === "in";
|
|
415
|
+
month[receipt ? "receipts" : "payments"] += transaction.amount;
|
|
416
|
+
const codes = receipt ? month.receiptCodes : month.paymentCodes;
|
|
417
|
+
codes[transaction.code] = (codes[transaction.code] || 0) + transaction.amount;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return files;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// One bank code's own total for each month of the year, across all four
|
|
424
|
+
// workbooks.
|
|
425
|
+
function bankCodeMonths(banks, code, side, tabs) {
|
|
426
|
+
return tabs.map((tab) =>
|
|
427
|
+
sum(
|
|
428
|
+
Object.values(banks).map((file) => {
|
|
429
|
+
const month = file.months[tab];
|
|
430
|
+
const codes = side === "receipt" ? month.receiptCodes : month.paymentCodes;
|
|
431
|
+
return codes[code] || 0;
|
|
432
|
+
}),
|
|
433
|
+
),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── Fixed Assets schedule ──────────────────────────────────────────────────
|
|
438
|
+
|
|
439
|
+
// One schedule row per asset, computed the way the sheet's own formulas
|
|
440
|
+
// compute it. An asset's depreciation charge is its cost at the class rate,
|
|
441
|
+
// capped at the net book value it carries; a disposal drops its net book
|
|
442
|
+
// value to nil and pulls its cost and accumulated depreciation into the
|
|
443
|
+
// disposal columns.
|
|
444
|
+
function buildSchedule(scenario, rate, depreciationRates, investmentAllowancePercent, writingDownPercent) {
|
|
445
|
+
const rows = [];
|
|
446
|
+
const usedByClass = {};
|
|
447
|
+
for (const asset of scenario.opening_fixed_assets || []) {
|
|
448
|
+
const layout = SCHEDULE_CLASSES[asset.category];
|
|
449
|
+
if (!layout) continue;
|
|
450
|
+
const index = (usedByClass[asset.category] = (usedByClass[asset.category] || 0) + 1) - 1;
|
|
451
|
+
const row = layout.existingRows[index];
|
|
452
|
+
if (row === undefined) continue;
|
|
453
|
+
rows.push({
|
|
454
|
+
assetClass: asset.category,
|
|
455
|
+
row,
|
|
456
|
+
acquiredInYear: false,
|
|
457
|
+
cost: asset.cost,
|
|
458
|
+
depreciationBroughtForward: asset.acc_dep || 0,
|
|
459
|
+
taxWrittenDownValue: asset.tax_wdv || 0,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const newLayout = SCHEDULE_CLASSES[SCHEDULE_NEW_ASSET_CLASS];
|
|
464
|
+
let newIndex = 0;
|
|
465
|
+
for (const transactions of Object.values(scenario.purchases || {})) {
|
|
466
|
+
for (const transaction of transactions) {
|
|
467
|
+
if (transaction.code !== "fa") continue;
|
|
468
|
+
const row = newLayout.newRows[newIndex++];
|
|
469
|
+
if (row === undefined) continue;
|
|
470
|
+
rows.push({
|
|
471
|
+
assetClass: SCHEDULE_NEW_ASSET_CLASS,
|
|
472
|
+
row,
|
|
473
|
+
acquiredInYear: true,
|
|
474
|
+
purchasedOn: serialOf(parseDate(transaction.date)),
|
|
475
|
+
description: transaction.supplier,
|
|
476
|
+
cost: writerNet(transaction.amount, rate),
|
|
477
|
+
depreciationBroughtForward: 0,
|
|
478
|
+
taxWrittenDownValue: 0,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// A disposal attaches to an asset already on the schedule, assets brought
|
|
484
|
+
// forward first, so the sheet's own disposal formulas resolve that row's
|
|
485
|
+
// cost and accumulated depreciation.
|
|
486
|
+
const disposalOrder = rows.filter((row) => !row.acquiredInYear).concat(rows.filter((row) => row.acquiredInYear));
|
|
487
|
+
let disposalIndex = 0;
|
|
488
|
+
for (const transactions of Object.values(scenario.sales || {})) {
|
|
489
|
+
for (const transaction of transactions) {
|
|
490
|
+
if (transaction.code !== "fs") continue;
|
|
491
|
+
const target = disposalOrder[disposalIndex++];
|
|
492
|
+
if (!target) continue;
|
|
493
|
+
target.disposalProceeds = writerNet(transaction.amount, rate);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
for (const row of rows) {
|
|
498
|
+
const ratePercent = depreciationRates[row.assetClass];
|
|
499
|
+
const netBookValue = row.cost - row.depreciationBroughtForward;
|
|
500
|
+
row.depreciationCharge = row.cost > 0 ? Math.min(row.cost * ratePercent, netBookValue) : 0;
|
|
501
|
+
row.depreciationCarriedForward = row.cost > 0 ? row.depreciationBroughtForward + row.depreciationCharge : 0;
|
|
502
|
+
row.disposed = (row.disposalProceeds || 0) > 0;
|
|
503
|
+
row.netBookValueCarriedForward = row.cost > 0 && !row.disposed ? row.cost - row.depreciationCarriedForward : 0;
|
|
504
|
+
row.disposalCost = row.disposed ? row.cost : 0;
|
|
505
|
+
row.disposalDepreciation = row.disposed ? row.depreciationCarriedForward : 0;
|
|
506
|
+
|
|
507
|
+
const claimRates = {
|
|
508
|
+
investmentAllowancePercent: row.acquiredInYear ? investmentAllowancePercent : 0,
|
|
509
|
+
writingDownPercent,
|
|
510
|
+
};
|
|
511
|
+
const claim = calculateCapitalAllowances(
|
|
512
|
+
[
|
|
513
|
+
{
|
|
514
|
+
acquiredInYear: row.acquiredInYear,
|
|
515
|
+
cost: row.cost,
|
|
516
|
+
taxWrittenDownValue: row.taxWrittenDownValue,
|
|
517
|
+
disposalProceeds: row.disposalProceeds || 0,
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
claimRates,
|
|
521
|
+
);
|
|
522
|
+
row.investmentAllowance = claim.investmentAllowance;
|
|
523
|
+
row.writingDownAllowance = claim.writingDownAllowance;
|
|
524
|
+
row.poolCarriedForward = row.acquiredInYear
|
|
525
|
+
? row.cost > 0
|
|
526
|
+
? row.cost - claim.investmentAllowance
|
|
527
|
+
: 0
|
|
528
|
+
: row.taxWrittenDownValue > 0
|
|
529
|
+
? row.taxWrittenDownValue - claim.writingDownAllowance
|
|
530
|
+
: 0;
|
|
531
|
+
row.balancingAllowance = claim.balancingAllowance;
|
|
532
|
+
row.balancingCharge = claim.balancingCharge;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return rows;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// A block's own totals row: E cost, F depreciation brought forward, G net
|
|
539
|
+
// book value brought forward, I the charge for the year, J depreciation
|
|
540
|
+
// carried forward, K net book value carried forward, O the tax written-down
|
|
541
|
+
// value brought forward, Q the investment allowance, R the writing down
|
|
542
|
+
// allowance, S the pool carried forward, V disposal proceeds, W disposal
|
|
543
|
+
// cost, X disposal depreciation, Y the balancing allowance, Z the balancing
|
|
544
|
+
// charge.
|
|
545
|
+
function scheduleTotals(rows) {
|
|
546
|
+
return {
|
|
547
|
+
E: sum(rows.map((row) => row.cost)),
|
|
548
|
+
F: sum(rows.map((row) => row.depreciationBroughtForward)),
|
|
549
|
+
G: sum(rows.map((row) => row.cost - row.depreciationBroughtForward)),
|
|
550
|
+
I: sum(rows.map((row) => row.depreciationCharge)),
|
|
551
|
+
J: sum(rows.map((row) => row.depreciationCarriedForward)),
|
|
552
|
+
K: sum(rows.map((row) => row.netBookValueCarriedForward)),
|
|
553
|
+
O: sum(rows.map((row) => row.taxWrittenDownValue)),
|
|
554
|
+
Q: sum(rows.map((row) => row.investmentAllowance)),
|
|
555
|
+
R: sum(rows.map((row) => row.writingDownAllowance)),
|
|
556
|
+
S: sum(rows.map((row) => row.poolCarriedForward)),
|
|
557
|
+
V: sum(rows.map((row) => row.disposalProceeds || 0)),
|
|
558
|
+
W: sum(rows.map((row) => row.disposalCost)),
|
|
559
|
+
X: sum(rows.map((row) => row.disposalDepreciation)),
|
|
560
|
+
Y: sum(rows.map((row) => row.balancingAllowance)),
|
|
561
|
+
Z: sum(rows.map((row) => row.balancingCharge)),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function scheduleBlocks(rows) {
|
|
566
|
+
const blocks = {};
|
|
567
|
+
for (const [className, layout] of Object.entries(SCHEDULE_CLASSES)) {
|
|
568
|
+
blocks[className] = {
|
|
569
|
+
layout,
|
|
570
|
+
existing: scheduleTotals(rows.filter((row) => row.assetClass === className && !row.acquiredInYear)),
|
|
571
|
+
newAssets: scheduleTotals(rows.filter((row) => row.assetClass === className && row.acquiredInYear)),
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
blocks.allExisting = scheduleTotals(rows.filter((row) => !row.acquiredInYear));
|
|
575
|
+
blocks.allNew = scheduleTotals(rows.filter((row) => row.acquiredInYear));
|
|
576
|
+
blocks.whole = scheduleTotals(rows);
|
|
577
|
+
return blocks;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ── Payroll ────────────────────────────────────────────────────────────────
|
|
581
|
+
|
|
582
|
+
// One month tab's payslip entries, in the order the writer fills the block.
|
|
583
|
+
function payrollEntriesByTab(scenario, tabs) {
|
|
584
|
+
const buckets = Object.fromEntries(tabs.map((tab) => [tab, []]));
|
|
585
|
+
for (const [monthKey, entries] of Object.entries(scenario.payroll || {})) {
|
|
586
|
+
const tab = SHORT_MONTHS.find((month) => month.toLowerCase() === monthKey);
|
|
587
|
+
if (tab === undefined || buckets[tab] === undefined) continue;
|
|
588
|
+
buckets[tab].push(...entries);
|
|
589
|
+
}
|
|
590
|
+
return buckets;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// A month tab gives an employee their line by position, so the first entry in
|
|
594
|
+
// a month's block belongs to the first employee on the Employee sheet. `keep`
|
|
595
|
+
// picks the positions a bucket counts, which is how the employees' and
|
|
596
|
+
// directors' halves of the month are told apart.
|
|
597
|
+
function payrollByTab(entriesByTab, keep = () => true) {
|
|
598
|
+
const buckets = {};
|
|
599
|
+
for (const [tab, allEntries] of Object.entries(entriesByTab)) {
|
|
600
|
+
const entries = allEntries.filter((entry, index) => keep(index));
|
|
601
|
+
buckets[tab] = entries.reduce(
|
|
602
|
+
(sums, entry) => ({
|
|
603
|
+
grossPay: sums.grossPay + (entry.grossPay || 0),
|
|
604
|
+
incomeTax: sums.incomeTax + (entry.incomeTax || 0),
|
|
605
|
+
employeeNI: sums.employeeNI + (entry.employeeNI || 0),
|
|
606
|
+
employerNI: sums.employerNI + (entry.employerNI || 0),
|
|
607
|
+
}),
|
|
608
|
+
{ grossPay: 0, incomeTax: 0, employeeNI: 0, employerNI: 0 },
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
return buckets;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// The row each payroll month opens on, with the tax week and the days from
|
|
615
|
+
// 6 April that put it there.
|
|
616
|
+
function payrollMonthStarts() {
|
|
617
|
+
const starts = [];
|
|
618
|
+
let weeksBefore = 0;
|
|
619
|
+
for (let month = 1; month <= 12; month++) {
|
|
620
|
+
const daysBefore = weeksBefore === 0 ? 0 : PAYROLL_FIRST_WEEK_DAYS + (weeksBefore - 1) * 7;
|
|
621
|
+
starts.push({ month, row: PAYSLIPS_CALENDAR_FIRST_ROW + daysBefore, daysBefore, week: weeksBefore + 1 });
|
|
622
|
+
weeksBefore += PAYROLL_WEEKS_PER_MONTH[month - 1];
|
|
623
|
+
}
|
|
624
|
+
return starts;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ── The engine ─────────────────────────────────────────────────────────────
|
|
628
|
+
|
|
629
|
+
// Every figure the package holds, in two parts: `results`, the cells the
|
|
630
|
+
// reconciliation reads, and `linkCells`, the leaf cells a sibling workbook's
|
|
631
|
+
// external link addresses and nothing else does. Both come off the same run,
|
|
632
|
+
// so a cell in each carries the same value.
|
|
633
|
+
function computeLtd(book, lines, taxData, scenario) {
|
|
634
|
+
const rate = scenario?.metadata?.vat_registered === false ? 0 : VAT_RATE;
|
|
635
|
+
const linkCells = {};
|
|
636
|
+
const link = (key, cells) => Object.assign((linkCells[key] ||= {}), cells);
|
|
637
|
+
const period = periodFrom(book);
|
|
638
|
+
const tabs = period.tabs;
|
|
639
|
+
const results = {};
|
|
640
|
+
|
|
641
|
+
const admin = buildAdmin(taxData, period);
|
|
642
|
+
results.Admin = admin;
|
|
643
|
+
link("Admin", { ...linkOnlyCells(adminDateChain(period), admin), N11: admin.B32 });
|
|
644
|
+
|
|
645
|
+
const salesByTab = bucketByTab(scenario.sales, tabs);
|
|
646
|
+
const purchasesByTab = bucketByTab(scenario.purchases, tabs);
|
|
647
|
+
const salesMonths = {};
|
|
648
|
+
const purchaseMonths = {};
|
|
649
|
+
for (const tab of tabs) {
|
|
650
|
+
salesMonths[tab] = journalMonthTotals(salesByTab[tab], rate, SALES_ANALYSIS_COLUMNS, "a", SALES_CIS_COLUMN);
|
|
651
|
+
purchaseMonths[tab] = journalMonthTotals(purchasesByTab[tab], rate, PURCHASE_ANALYSIS_COLUMNS, "g", PURCHASES_CIS_COLUMN);
|
|
652
|
+
results[`Sales.xlsx!${tab}`] = {
|
|
653
|
+
G1: salesMonths[tab].G1,
|
|
654
|
+
G2: salesMonths[tab].G2,
|
|
655
|
+
H1: salesMonths[tab].H1,
|
|
656
|
+
T1: salesMonths[tab].T1,
|
|
657
|
+
U1: salesMonths[tab].U1,
|
|
658
|
+
};
|
|
659
|
+
results[`Purchases.xlsx!${tab}`] = {
|
|
660
|
+
G1: purchaseMonths[tab].G1,
|
|
661
|
+
G2: purchaseMonths[tab].G2,
|
|
662
|
+
H1: purchaseMonths[tab].H1,
|
|
663
|
+
O1: purchaseMonths[tab].O1,
|
|
664
|
+
R1: purchaseMonths[tab].R1,
|
|
665
|
+
S1: purchaseMonths[tab].S1,
|
|
666
|
+
AI1: purchaseMonths[tab].AI1,
|
|
667
|
+
};
|
|
668
|
+
link(`Sales.xlsx!${tab}`, linkOnlyCells(salesMonths[tab], results[`Sales.xlsx!${tab}`]));
|
|
669
|
+
link(`Purchases.xlsx!${tab}`, linkOnlyCells(purchaseMonths[tab], results[`Purchases.xlsx!${tab}`]));
|
|
670
|
+
}
|
|
671
|
+
// A Sales tab's flat-rate cell is empty on the first tab and reads the tab
|
|
672
|
+
// before it after that, so every tab but the first, and both ledgers, hold
|
|
673
|
+
// nil. The two ledgers echo the first tab's VAT rate.
|
|
674
|
+
for (const tab of tabs.slice(1)) link(`Sales.xlsx!${tab}`, { G4: 0 });
|
|
675
|
+
for (const ledger of SALES_LEDGER_SHEETS) link(`Sales.xlsx!${ledger}`, { G2: salesMonths[tabs[0]].G2, G4: 0 });
|
|
676
|
+
for (const ledger of PURCHASES_LEDGER_SHEETS) link(`Purchases.xlsx!${ledger}`, { G2: purchaseMonths[tabs[0]].G2 });
|
|
677
|
+
const salesMonthly = (column) => tabs.map((tab) => salesMonths[tab][`${column}1`] || 0);
|
|
678
|
+
const purchasesMonthly = (column) => tabs.map((tab) => purchaseMonths[tab][`${column}1`] || 0);
|
|
679
|
+
|
|
680
|
+
const banks = bankMonthTotals(scenario, tabs, period.start);
|
|
681
|
+
for (const [fileName, file] of Object.entries(banks)) {
|
|
682
|
+
let balance = file.opening;
|
|
683
|
+
let openingOfLastMonth = balance;
|
|
684
|
+
for (const tab of tabs) {
|
|
685
|
+
openingOfLastMonth = balance;
|
|
686
|
+
const month = file.months[tab];
|
|
687
|
+
balance = balance + month.receipts - month.payments;
|
|
688
|
+
const layout = BANK_LAYOUTS[fileName];
|
|
689
|
+
link(`${fileName}!${tab}`, {
|
|
690
|
+
[`${layout.receipt.amount}1`]: month.receipts,
|
|
691
|
+
[`${layout.payment.amount}1`]: month.payments,
|
|
692
|
+
...Object.fromEntries(layout.receiptColumns.map(([code, column]) => [`${column}1`, month.receiptCodes[code] || 0])),
|
|
693
|
+
...Object.fromEntries(layout.paymentColumns.map(([code, column]) => [`${column}1`, month.paymentCodes[code] || 0])),
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
results[`${fileName}!${tabs[11]}`] = { A1: openingOfLastMonth, A2: balance };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const depreciationRates = {
|
|
700
|
+
land: taxData.depreciation?.land_and_property ?? 0,
|
|
701
|
+
plant: taxData.depreciation?.plant_and_machinery ?? 0,
|
|
702
|
+
fixtures: taxData.depreciation?.fixtures_and_fittings ?? 0,
|
|
703
|
+
computer: taxData.depreciation?.computer_equipment ?? 0,
|
|
704
|
+
motor: taxData.depreciation?.motor_vehicles ?? 0,
|
|
705
|
+
};
|
|
706
|
+
const openingBalance = scenario.opening_balance || {};
|
|
707
|
+
const scheduleRows = buildSchedule(scenario, rate, depreciationRates, admin.G5, admin.G6);
|
|
708
|
+
const blocks = scheduleBlocks(scheduleRows);
|
|
709
|
+
const scheduleSheet = buildScheduleSheet(blocks, openingBalance, depreciationRates);
|
|
710
|
+
// The reconciliation checks pin a disposal's WDA and balancing allowance
|
|
711
|
+
// split against the first existing motor row directly, not just the class
|
|
712
|
+
// total, so that row's own O/R/S/V/Y cells need a value here too.
|
|
713
|
+
const firstMotorRow = SCHEDULE_CLASSES.motor.existingRows[0];
|
|
714
|
+
const motorRow = scheduleRows.find((row) => row.row === firstMotorRow && !row.acquiredInYear);
|
|
715
|
+
if (motorRow) {
|
|
716
|
+
scheduleSheet[`O${firstMotorRow}`] = motorRow.taxWrittenDownValue;
|
|
717
|
+
scheduleSheet[`R${firstMotorRow}`] = motorRow.writingDownAllowance;
|
|
718
|
+
scheduleSheet[`S${firstMotorRow}`] = motorRow.poolCarriedForward;
|
|
719
|
+
if (motorRow.disposalProceeds !== undefined) scheduleSheet[`V${firstMotorRow}`] = motorRow.disposalProceeds;
|
|
720
|
+
if (motorRow.disposed) scheduleSheet[`Y${firstMotorRow}`] = motorRow.balancingAllowance;
|
|
721
|
+
}
|
|
722
|
+
results["Fixedassets.xlsx!Schedule"] = scheduleSheet;
|
|
723
|
+
link("Fixedassets.xlsx!Schedule", scheduleLinkCells(blocks, scheduleRows));
|
|
724
|
+
// The workbook's own tie-out between the schedule and the two ledgers. E11
|
|
725
|
+
// re-sums the schedule's New-asset cost totals (E6:E10 = Schedule E64, E75,
|
|
726
|
+
// E83, E94, E108) and K11 the disposal proceeds on both halves of each
|
|
727
|
+
// class (K6:K10 = Schedule V11+V64 and so on down). E13 and K13 read the
|
|
728
|
+
// ledgers' own annual fixed asset totals across a leaf-to-leaf link
|
|
729
|
+
// ([2]Mar!AI2 and [3]Mar!U2), and E15/K15 are the differences the sheet
|
|
730
|
+
// prints. Each side has to come off its own source: deriving both from the
|
|
731
|
+
// ledger leaves the difference nil whatever the schedule holds.
|
|
732
|
+
const purchasesFixedAssetTotal = sum(purchasesMonthly("AI"));
|
|
733
|
+
const salesFixedAssetTotal = sum(salesMonthly("U"));
|
|
734
|
+
link(`Purchases.xlsx!${tabs[11]}`, { AI2: purchasesFixedAssetTotal });
|
|
735
|
+
link(`Sales.xlsx!${tabs[11]}`, { U2: salesFixedAssetTotal });
|
|
736
|
+
results["Fixedassets.xlsx!FAreconciliation"] = {
|
|
737
|
+
E11: blocks.allNew.E,
|
|
738
|
+
E13: purchasesFixedAssetTotal,
|
|
739
|
+
E15: purchasesFixedAssetTotal - blocks.allNew.E,
|
|
740
|
+
K11: blocks.whole.V,
|
|
741
|
+
K13: salesFixedAssetTotal,
|
|
742
|
+
K15: salesFixedAssetTotal - blocks.whole.V,
|
|
743
|
+
};
|
|
744
|
+
const hp = buildHirePurchase(scenario);
|
|
745
|
+
results["Fixedassets.xlsx!HPfinance"] = hp.sheet;
|
|
746
|
+
|
|
747
|
+
const payrollEntries = payrollEntriesByTab(scenario, tabs);
|
|
748
|
+
const isDirectorsLine = (index) => Boolean((scenario.employees || [])[index]?.isDirector);
|
|
749
|
+
const payroll = payrollByTab(payrollEntries);
|
|
750
|
+
const employeePayroll = payrollByTab(payrollEntries, (index) => !isDirectorsLine(index));
|
|
751
|
+
const directorPayroll = payrollByTab(payrollEntries, isDirectorsLine);
|
|
752
|
+
results.WagesInterface = buildWagesInterface(employeePayroll, directorPayroll, tabs);
|
|
753
|
+
const yearStart = payrollYearStart(payrollYearOf(taxData, period));
|
|
754
|
+
results["Payslips.xlsx!Payment"] = buildPayslipsPayment(payroll, yearStart);
|
|
755
|
+
results["Payslips.xlsx!Admin"] = buildPayslipsCalendar(taxData, period, tabs);
|
|
756
|
+
for (const monthIndex of PAYSLIPS_DIRECTLY_READ_MONTH_INDEXES) {
|
|
757
|
+
const monthTab = buildPayslipsMonthTab(monthIndex, payrollEntries[tabs[monthIndex]] || []);
|
|
758
|
+
addPayslipsWeeklyRemnants(monthTab, monthIndex);
|
|
759
|
+
results[`Payslips.xlsx!${tabs[monthIndex]}`] = monthTab;
|
|
760
|
+
}
|
|
761
|
+
// Every month tab opens its monthly payroll block with the day the month it
|
|
762
|
+
// is named for begins, and carries its own whole-month totals on row 1 for
|
|
763
|
+
// the PAYE schedule to read, so all twelve carry those cells whether the
|
|
764
|
+
// reconciliation reads the rest of the tab or not.
|
|
765
|
+
for (let monthIndex = 0; monthIndex < 12; monthIndex++) {
|
|
766
|
+
const tab = tabs[monthIndex];
|
|
767
|
+
const key = `Payslips.xlsx!${tab}`;
|
|
768
|
+
if (!results[key]) results[key] = {};
|
|
769
|
+
results[key][payslipsPeriodStartCell(monthIndex)] = serialOf(payslipsMonthPeriod(period.start, monthIndex, yearStart).first);
|
|
770
|
+
results[key][PAYE_SCHEDULE_MONTH_TAB_CELLS.employerNI] = payroll[tab].employerNI;
|
|
771
|
+
results[key][PAYE_SCHEDULE_MONTH_TAB_CELLS.employeeNI] = payroll[tab].employeeNI;
|
|
772
|
+
results[key][PAYE_SCHEDULE_MONTH_TAB_CELLS.incomeTax] = payroll[tab].incomeTax;
|
|
773
|
+
results[key][PAYE_SCHEDULE_MONTH_TAB_CELLS.studentLoan] = 0;
|
|
774
|
+
// Row 1 also totals the month's gross pay, its statutory pay and its
|
|
775
|
+
// other deductions, and row 2 is the directors' block below it.
|
|
776
|
+
link(key, {
|
|
777
|
+
G1: 0,
|
|
778
|
+
M1: payroll[tab].grossPay,
|
|
779
|
+
Q1: 0,
|
|
780
|
+
M2: directorPayroll[tab].grossPay,
|
|
781
|
+
N2: directorPayroll[tab].incomeTax,
|
|
782
|
+
O2: directorPayroll[tab].employeeNI,
|
|
783
|
+
P2: 0,
|
|
784
|
+
Q2: 0,
|
|
785
|
+
T2: directorPayroll[tab].employerNI,
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
results[`Payslips.xlsx!${PAYSLIP_PRINT_SHEET}`] = buildPayslipsPrintPage(PAYSLIP_PRINT_PERIOD, tabs, payrollEntries);
|
|
789
|
+
|
|
790
|
+
const companySecretary = buildCompanySecretary(scenario);
|
|
791
|
+
Object.assign(results, companySecretary);
|
|
792
|
+
const boardMeeting = companySecretary["Companysecretary.xlsx!Boardmeeting"] || {};
|
|
793
|
+
const dividendDeclared = boardMeeting.E4 || 0;
|
|
794
|
+
const shareIssue = boardMeeting.E6 || 0;
|
|
795
|
+
|
|
796
|
+
const stock = buildStock(scenario, openingBalance, rate, salesMonthly("O"), purchasesMonthly("O"));
|
|
797
|
+
results.Stock = stock.sheet;
|
|
798
|
+
results.OpenAccounts = buildOpenAccounts(book, scenario, openingBalance);
|
|
799
|
+
link("OpenAccounts", openingFixedAssetCells(openingBalance));
|
|
800
|
+
|
|
801
|
+
const trialBalance = buildTrialBalance({
|
|
802
|
+
openingBalance,
|
|
803
|
+
salesMonthly,
|
|
804
|
+
purchasesMonthly,
|
|
805
|
+
banks,
|
|
806
|
+
payroll,
|
|
807
|
+
employeePayroll,
|
|
808
|
+
directorPayroll,
|
|
809
|
+
blocks,
|
|
810
|
+
stock,
|
|
811
|
+
tabs,
|
|
812
|
+
hp,
|
|
813
|
+
dividendDeclared,
|
|
814
|
+
shareIssue,
|
|
815
|
+
smallProfitsRatePercent: admin.P7,
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
const monthlyPl = buildMonthlyProfitAndLoss(trialBalance, tabs);
|
|
819
|
+
const publishedPl = buildPublishedProfitAndLoss(trialBalance, monthlyPl, admin);
|
|
820
|
+
const corporationTax = buildCorporationTax({ admin, trialBalance, blocks, publishedPl });
|
|
821
|
+
|
|
822
|
+
// The tax charge closes the books: the trial balance's corporation tax rows
|
|
823
|
+
// read it, the published P&L reads those rows back, and the retained profit
|
|
824
|
+
// that leaves is the reserve movement. That is the order the workbook's own
|
|
825
|
+
// recalculation takes.
|
|
826
|
+
trialBalance.EJ35 = trialBalance.corporationTaxCreditorBeforeCharge - corporationTax.K35 + trialBalance.EH35;
|
|
827
|
+
trialBalance.EJ47 = corporationTax.K35;
|
|
828
|
+
publishedPl.F50 = trialBalance.EJ47;
|
|
829
|
+
publishedPl.F51 = publishedPl.F49 - publishedPl.F50;
|
|
830
|
+
publishedPl.F52 = trialBalance.EJ48;
|
|
831
|
+
publishedPl.F54 = publishedPl.F51 - publishedPl.F52;
|
|
832
|
+
trialBalance.EJ43 = trialBalance.retainedEarningsBroughtForward - publishedPl.F54;
|
|
833
|
+
trialBalance.EJ49 = publishedPl.F54;
|
|
834
|
+
trialBalance.EJ91 = sum(AUDIT_ROWS.map((row) => trialBalance[`EJ${row}`] || 0));
|
|
835
|
+
|
|
836
|
+
results["MnthP&L"] = monthlyPl;
|
|
837
|
+
results.TrialBalance = trialBalanceReads(trialBalance);
|
|
838
|
+
results.CorporationTax = corporationTaxReads(corporationTax);
|
|
839
|
+
results.CT600 = buildCt600(corporationTax, monthlyPl, admin);
|
|
840
|
+
results["PubP&L"] = publishedPl;
|
|
841
|
+
results.PubBalSht = buildPublishedBalanceSheet(trialBalance, admin);
|
|
842
|
+
results.PubNotes = buildPublishedNotes(blocks, trialBalance, corporationTax, admin, depreciationRates);
|
|
843
|
+
results.Report = buildDirectorsReport(publishedPl, results.PubBalSht, companySecretary);
|
|
844
|
+
|
|
845
|
+
Object.assign(results, buildVatReturns(salesMonths, purchaseMonths, period, scenario, rate));
|
|
846
|
+
|
|
847
|
+
const mileageRate = taxData.mileage?.higher_rate_pence ?? 0;
|
|
848
|
+
for (const sheet of EXPENSES_FORM_MONTHS) results[`expensesform.xlsx!${sheet}`] = { C30: mileageRate };
|
|
849
|
+
|
|
850
|
+
Object.assign(results, buildSalesInvoice(scenario, rate, taxData));
|
|
851
|
+
|
|
852
|
+
return { results, linkCells };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// The cells the reconciliation reads.
|
|
856
|
+
export function calculateLtdResults(book, lines, taxData, scenario) {
|
|
857
|
+
return computeLtd(book, lines, taxData, scenario).results;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Every cell a sibling workbook's link addresses, on top of the report's
|
|
861
|
+
// cells. The link-cache refresh reads its figures from here.
|
|
862
|
+
export function calculateLtdCells(book, lines, taxData, scenario) {
|
|
863
|
+
const { results, linkCells } = computeLtd(book, lines, taxData, scenario);
|
|
864
|
+
const cells = Object.fromEntries(Object.entries(results).map(([key, sheet]) => [key, { ...sheet }]));
|
|
865
|
+
for (const [key, sheet] of Object.entries(linkCells)) Object.assign((cells[key] ||= {}), sheet);
|
|
866
|
+
return cells;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// ── Admin ──────────────────────────────────────────────────────────────────
|
|
870
|
+
|
|
871
|
+
// Everything the generator injects from the tax-year data, plus the period
|
|
872
|
+
// dates the whole book hangs off. The corporation tax rows split the period
|
|
873
|
+
// at 31 March: L6 is the period start, N6 the earlier of the first financial
|
|
874
|
+
// year's end and the period end, L7 the day after and N7 the period end.
|
|
875
|
+
function buildAdmin(taxData, period) {
|
|
876
|
+
const corporationTax = taxData.corporation_tax || {};
|
|
877
|
+
const capitalAllowances = taxData.capital_allowances || {};
|
|
878
|
+
const depreciation = taxData.depreciation || {};
|
|
879
|
+
const mileage = taxData.mileage || {};
|
|
880
|
+
const vat = taxData.vat || {};
|
|
881
|
+
|
|
882
|
+
const yearEndSerial = serialOf(period.yearEnd);
|
|
883
|
+
const periodStartSerial = serialOf(period.start);
|
|
884
|
+
const financialYears = financialYearsInPeriod(period.start, period.yearEnd);
|
|
885
|
+
|
|
886
|
+
return {
|
|
887
|
+
B9: periodStartSerial,
|
|
888
|
+
B32: yearEndSerial,
|
|
889
|
+
F21: yearEndSerial,
|
|
890
|
+
P6: Math.round(corporationTax.small_profits_rate * 100),
|
|
891
|
+
P7: Math.round(corporationTax.small_profits_rate * 100),
|
|
892
|
+
P8: Math.round(corporationTax.main_rate * 100),
|
|
893
|
+
P9: corporationTax.marginal_relief_fraction,
|
|
894
|
+
P12: corporationTax.small_profits_limit,
|
|
895
|
+
P13: corporationTax.main_rate_limit,
|
|
896
|
+
G5: Math.round(capitalAllowances.annual_investment_allowance * 100),
|
|
897
|
+
G6: Math.round(capitalAllowances.writing_down_allowance_main * 100),
|
|
898
|
+
G7: Math.round(capitalAllowances.annual_investment_allowance * 100),
|
|
899
|
+
G8: Math.round(capitalAllowances.writing_down_allowance_main * 100),
|
|
900
|
+
G15: depreciation.land_and_property,
|
|
901
|
+
G16: depreciation.plant_and_machinery,
|
|
902
|
+
G17: depreciation.fixtures_and_fittings,
|
|
903
|
+
G18: depreciation.computer_equipment,
|
|
904
|
+
G19: depreciation.motor_vehicles,
|
|
905
|
+
N16: mileage.higher_rate_limit,
|
|
906
|
+
O16: mileage.higher_rate_pence,
|
|
907
|
+
N17: mileage.lower_rate_start,
|
|
908
|
+
O17: mileage.lower_rate_pence,
|
|
909
|
+
M19: Math.round(vat.standard_rate * 100),
|
|
910
|
+
M21: Math.round(vat.standard_rate * 100),
|
|
911
|
+
K6: financialYears.years[0].year,
|
|
912
|
+
K7: financialYears.years[1].year,
|
|
913
|
+
L6: periodStartSerial,
|
|
914
|
+
L7: serialOf(financialYears.years[1].start),
|
|
915
|
+
N6: serialOf(financialYears.years[0].end),
|
|
916
|
+
N7: yearEndSerial,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Every row of the chain: an even row is a month end and the odd row under
|
|
921
|
+
// it the day the next month opens.
|
|
922
|
+
function adminDateChain(period) {
|
|
923
|
+
const chain = {};
|
|
924
|
+
for (let row = ADMIN_DATE_CHAIN_FIRST_ROW; row <= ADMIN_DATE_CHAIN_LAST_ROW; row++) {
|
|
925
|
+
chain[`B${row}`] = row % 2 === 0 ? serialOf(period.adminMonthEnd(row)) : chain[`B${row - 1}`] + 1;
|
|
926
|
+
}
|
|
927
|
+
return chain;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// The opening cost and depreciation the opening balance sheet splits across
|
|
931
|
+
// row 13, one column a class, for the classes the book carries a figure for.
|
|
932
|
+
function openingFixedAssetCells(openingBalance) {
|
|
933
|
+
const cells = {};
|
|
934
|
+
const cost = openingBalance.fixed_asset_cost || {};
|
|
935
|
+
const depreciation = openingBalance.fixed_asset_depreciation || {};
|
|
936
|
+
for (const [assetClass, columns] of Object.entries(OPENING_FIXED_ASSET_COLUMNS)) {
|
|
937
|
+
if (cost[assetClass] !== undefined) cells[`${columns.cost}13`] = cost[assetClass];
|
|
938
|
+
if (depreciation[assetClass] !== undefined) cells[`${columns.depreciation}13`] = depreciation[assetClass];
|
|
939
|
+
}
|
|
940
|
+
return cells;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// ── Fixed Assets schedule sheet ────────────────────────────────────────────
|
|
944
|
+
|
|
945
|
+
// The schedule cells only the Financialaccounts corporation tax page reads:
|
|
946
|
+
// each class's writing down allowance on the existing block, the investment
|
|
947
|
+
// allowance an asset bought in the year claims, and the date, description and
|
|
948
|
+
// cost the writer put on that asset's row. A new-asset row with no asset on
|
|
949
|
+
// it stays on the sheet's own blank branch.
|
|
950
|
+
function scheduleLinkCells(blocks, rows) {
|
|
951
|
+
const cells = {};
|
|
952
|
+
for (const className of SCHEDULE_ALLOWANCE_CLASSES) {
|
|
953
|
+
const layout = SCHEDULE_CLASSES[className];
|
|
954
|
+
cells[`R${layout.existingTotalRow}`] = blocks[className].existing.R;
|
|
955
|
+
for (const row of layout.newRows) {
|
|
956
|
+
cells[`Q${row}`] = rows.find((asset) => asset.row === row)?.investmentAllowance ?? SHEET_BLANK;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
for (const row of SCHEDULE_CAR_ROWS) cells[`R${row}`] = SHEET_BLANK;
|
|
960
|
+
for (const asset of rows) {
|
|
961
|
+
if (!asset.acquiredInYear) continue;
|
|
962
|
+
cells[`B${asset.row}`] = asset.purchasedOn;
|
|
963
|
+
cells[`E${asset.row}`] = asset.cost;
|
|
964
|
+
if (asset.description !== undefined) cells[`C${asset.row}`] = asset.description;
|
|
965
|
+
}
|
|
966
|
+
return cells;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function buildScheduleSheet(blocks, openingBalance, depreciationRates) {
|
|
970
|
+
const sheet = {};
|
|
971
|
+
for (const [className, layout] of Object.entries(SCHEDULE_CLASSES)) {
|
|
972
|
+
const block = blocks[className];
|
|
973
|
+
for (const column of ["E", "F", "I", "W", "X"]) {
|
|
974
|
+
sheet[`${column}${layout.existingTotalRow}`] = block.existing[column];
|
|
975
|
+
sheet[`${column}${layout.newTotalRow}`] = block.newAssets[column];
|
|
976
|
+
}
|
|
977
|
+
sheet[layout.rateCell] = depreciationRates[className];
|
|
978
|
+
|
|
979
|
+
const openingCost = openingBalance.fixed_asset_cost?.[layout.openingKey] || 0;
|
|
980
|
+
const openingDepreciation = openingBalance.fixed_asset_depreciation?.[layout.openingKey] || 0;
|
|
981
|
+
const agrees = block.existing.E - block.existing.F === openingCost - openingDepreciation;
|
|
982
|
+
sheet[`B${layout.existingTotalRow}`] = agrees
|
|
983
|
+
? `Existing ${layout.label}`
|
|
984
|
+
: className === "computer"
|
|
985
|
+
? SCHEDULE_COMPUTER_DISAGREEMENT_TEXT
|
|
986
|
+
: SCHEDULE_DISAGREEMENT_TEXT;
|
|
987
|
+
}
|
|
988
|
+
sheet.E57 = blocks.allExisting.E;
|
|
989
|
+
sheet.E110 = blocks.allNew.E;
|
|
990
|
+
for (const column of ["E", "F", "I", "J", "K", "Q", "R", "V", "W", "X", "Y", "Z"]) {
|
|
991
|
+
sheet[`${column}1`] = blocks.whole[column];
|
|
992
|
+
}
|
|
993
|
+
// Net book value brought forward is stated for an asset already owned and
|
|
994
|
+
// left blank for one bought in the year, so the whole-schedule total is the
|
|
995
|
+
// existing blocks alone.
|
|
996
|
+
sheet.G1 = blocks.allExisting.G;
|
|
997
|
+
return sheet;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// ── Hire purchase ──────────────────────────────────────────────────────────
|
|
1001
|
+
|
|
1002
|
+
// The "New Hire Purchase Agreements" block totals what the agreements
|
|
1003
|
+
// financed in E2, and each agreement's row splits its monthly payment into
|
|
1004
|
+
// capital and interest.
|
|
1005
|
+
function buildHirePurchase(scenario) {
|
|
1006
|
+
const agreements = (scenario.hp_agreements || []).slice(0, HP_AGREEMENT_ROWS.length);
|
|
1007
|
+
const sheet = { E2: sum(agreements.map((agreement) => agreement.amount_financed || 0)) };
|
|
1008
|
+
agreements.forEach((agreement, index) => {
|
|
1009
|
+
const row = HP_AGREEMENT_ROWS[index];
|
|
1010
|
+
const months = agreement.months || 0;
|
|
1011
|
+
const financed = agreement.amount_financed || 0;
|
|
1012
|
+
if (months <= 0 || financed <= 0) return;
|
|
1013
|
+
const interest = agreement.total_interest || 0;
|
|
1014
|
+
const adminCharges = agreement.admin_charges || 0;
|
|
1015
|
+
// The whole monthly payment, the interest inside it, and the capital that
|
|
1016
|
+
// is left: I = (E + F + G) / H, K = G / H, J = I - K.
|
|
1017
|
+
sheet[`I${row}`] = (financed + adminCharges + interest) / months;
|
|
1018
|
+
sheet[`K${row}`] = interest / months;
|
|
1019
|
+
sheet[`J${row}`] = sheet[`I${row}`] - sheet[`K${row}`];
|
|
1020
|
+
});
|
|
1021
|
+
return { sheet, longTermCreditor: sheet.E2 };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// ── Payroll sheets ─────────────────────────────────────────────────────────
|
|
1025
|
+
|
|
1026
|
+
// One month a row in each of two blocks, employees from row 4 and directors
|
|
1027
|
+
// from row 17. The sheet works the employees' side out as the month's whole
|
|
1028
|
+
// payroll less the month tab's own directors sub-total, so what reaches the
|
|
1029
|
+
// P&L's two wages lines depends on which lines belong to a director.
|
|
1030
|
+
function buildWagesInterface(employeePayroll, directorPayroll, tabs) {
|
|
1031
|
+
const sheet = {};
|
|
1032
|
+
const block = (firstRow, payroll) =>
|
|
1033
|
+
tabs.forEach((tab, index) => {
|
|
1034
|
+
const row = firstRow + index;
|
|
1035
|
+
sheet[`C${row}`] = payroll[tab].grossPay;
|
|
1036
|
+
sheet[`D${row}`] = payroll[tab].incomeTax;
|
|
1037
|
+
sheet[`E${row}`] = payroll[tab].employeeNI;
|
|
1038
|
+
sheet[`H${row}`] = payroll[tab].employerNI;
|
|
1039
|
+
});
|
|
1040
|
+
block(WAGES_INTERFACE_EMPLOYEE_FIRST_ROW, employeePayroll);
|
|
1041
|
+
block(WAGES_INTERFACE_DIRECTOR_FIRST_ROW, directorPayroll);
|
|
1042
|
+
return sheet;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// The PAYE remittance schedule, one row per tax month from row 4. B is the
|
|
1046
|
+
// month end and C the day the payment falls due, both counted off the payroll
|
|
1047
|
+
// year's first day. D is the National Insurance due, employer and employee, E
|
|
1048
|
+
// the income tax and I the whole amount payable; the statutory pay and student
|
|
1049
|
+
// loan columns the total also carries stay nil. A row takes the month tab
|
|
1050
|
+
// named for the calendar month its tax month ends in, so row 4 is April
|
|
1051
|
+
// whatever the package's year end.
|
|
1052
|
+
function buildPayslipsPayment(payroll, payrollYearOpens) {
|
|
1053
|
+
const sheet = {};
|
|
1054
|
+
PAYE_SCHEDULE_MONTH_TABS.forEach((tab, taxMonth) => {
|
|
1055
|
+
const row = PAYE_SCHEDULE_FIRST_ROW + taxMonth;
|
|
1056
|
+
const { ends, due } = payeTaxMonthDates(payrollYearOpens, taxMonth);
|
|
1057
|
+
sheet[`B${row}`] = serialOf(ends);
|
|
1058
|
+
sheet[`C${row}`] = serialOf(due);
|
|
1059
|
+
const nationalInsurance = payroll[tab].employerNI + payroll[tab].employeeNI;
|
|
1060
|
+
sheet[`D${row}`] = nationalInsurance;
|
|
1061
|
+
sheet[`E${row}`] = payroll[tab].incomeTax;
|
|
1062
|
+
sheet[`I${row}`] = nationalInsurance + payroll[tab].incomeTax;
|
|
1063
|
+
});
|
|
1064
|
+
return sheet;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// The rows a month tab's weekly blocks keep for an employee paid weekly, and
|
|
1068
|
+
// the row the last of them totals into. Every fixture pays monthly, so the
|
|
1069
|
+
// weekly gate never reads true and these resolve to the sheet's own
|
|
1070
|
+
// not-carried-forward branch: nil where the template holds a figure, blank
|
|
1071
|
+
// where it holds text.
|
|
1072
|
+
const PAYSLIPS_WEEKLY_ROWS = [11, 12, 13, 14, 15];
|
|
1073
|
+
const PAYSLIPS_WEEKLY_PERIOD_TOTAL_CELL = "T41";
|
|
1074
|
+
// The columns the following month's block brings a part-finished weekly cycle
|
|
1075
|
+
// forward in. K starts a row lower than the rest, and M -- the payslip total
|
|
1076
|
+
// -- brings its blank forward rather than a nil, so neither engine carries it.
|
|
1077
|
+
const PAYSLIPS_BROUGHT_FORWARD_COLUMNS = ["H", "I", "J", "L"];
|
|
1078
|
+
const PAYSLIPS_BROUGHT_FORWARD_LATE_COLUMN = { column: "K", firstRow: 12 };
|
|
1079
|
+
|
|
1080
|
+
// One month tab's monthly payroll block: an employee a row from block row +
|
|
1081
|
+
// 3, with the wages-paid date above them. A row the scenario has no employee
|
|
1082
|
+
// for keeps the three columns the template ships as a literal zero and stays
|
|
1083
|
+
// blank in the other five, which is what the workbook itself carries there.
|
|
1084
|
+
function buildPayslipsMonthTab(monthIndex, entries) {
|
|
1085
|
+
const sheet = {};
|
|
1086
|
+
const columns = PAYSLIPS_ENTRY_COLUMNS;
|
|
1087
|
+
payslipsMonthEntryRows(monthIndex).forEach((row, index) => {
|
|
1088
|
+
const entry = entries[index];
|
|
1089
|
+
if (!entry) {
|
|
1090
|
+
for (const column of PAYSLIPS_ZERO_FILLED_COLUMNS) sheet[`${column}${row}`] = 0;
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (entry.name) sheet[`${columns.name}${row}`] = entry.name;
|
|
1094
|
+
if (entry.taxCode) sheet[`${columns.taxCode}${row}`] = entry.taxCode;
|
|
1095
|
+
sheet[`${columns.grossPay}${row}`] = entry.grossPay || 0;
|
|
1096
|
+
sheet[`${columns.incomeTax}${row}`] = entry.incomeTax || 0;
|
|
1097
|
+
sheet[`${columns.employeeNI}${row}`] = entry.employeeNI || 0;
|
|
1098
|
+
sheet[`${columns.netPay}${row}`] = entry.netPay || 0;
|
|
1099
|
+
sheet[`${columns.employerNI}${row}`] = entry.employerNI || 0;
|
|
1100
|
+
if (entry.reference) sheet[`${columns.reference}${row}`] = entry.reference;
|
|
1101
|
+
});
|
|
1102
|
+
if (entries.length > 0) sheet[payslipsWagesPaidCell(monthIndex)] = serialOf(parseDate(entries[0].date));
|
|
1103
|
+
return sheet;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Template position 3 keeps its weekly employee lines and its period total;
|
|
1107
|
+
// position 4 keeps the cells that would bring an unfinished weekly cycle in
|
|
1108
|
+
// from the month before it.
|
|
1109
|
+
function addPayslipsWeeklyRemnants(sheet, monthIndex) {
|
|
1110
|
+
if (monthIndex === PAYSLIPS_DIRECTLY_READ_MONTH_INDEXES[0]) {
|
|
1111
|
+
sheet[PAYSLIPS_WEEKLY_PERIOD_TOTAL_CELL] = 0;
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
if (monthIndex !== PAYSLIPS_DIRECTLY_READ_MONTH_INDEXES[1]) return;
|
|
1115
|
+
for (const row of PAYSLIPS_WEEKLY_ROWS) {
|
|
1116
|
+
for (const column of PAYSLIPS_BROUGHT_FORWARD_COLUMNS) sheet[`${column}${row}`] = 0;
|
|
1117
|
+
if (row >= PAYSLIPS_BROUGHT_FORWARD_LATE_COLUMN.firstRow) sheet[`${PAYSLIPS_BROUGHT_FORWARD_LATE_COLUMN.column}${row}`] = 0;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// The page the employer prints. H3 and H4 are the join -- the tab the chosen
|
|
1122
|
+
// period lands on and the row its block starts at -- and I9, I10 and L7 the
|
|
1123
|
+
// heading it prints above the figures. Everything below the heading is the
|
|
1124
|
+
// first employee's own line on that month tab, gated on M8, the payroll
|
|
1125
|
+
// number their Employee-sheet block gives them once their starting date has
|
|
1126
|
+
// arrived. The year-to-date row runs from the payroll year's first month, so
|
|
1127
|
+
// it adds that employee's line over every month up to the one printed.
|
|
1128
|
+
function buildPayslipsPrintPage(period, tabs, entriesByTab) {
|
|
1129
|
+
const monthIndex = period - 1;
|
|
1130
|
+
const sheet = {
|
|
1131
|
+
[PAYSLIP_PRINT_CELLS.tab]: tabs[monthIndex],
|
|
1132
|
+
[PAYSLIP_PRINT_CELLS.blockRow]: monthlyPayrollBlockRow(monthIndex),
|
|
1133
|
+
[PAYSLIP_PRINT_CELLS.heading]: PAYSLIP_PRINT_MONTHLY_HEADING,
|
|
1134
|
+
[PAYSLIP_PRINT_CELLS.periodNumber]: period,
|
|
1135
|
+
};
|
|
1136
|
+
const entries = entriesByTab[tabs[monthIndex]] || [];
|
|
1137
|
+
if (entries.length === 0) return sheet;
|
|
1138
|
+
|
|
1139
|
+
const entry = entries[0];
|
|
1140
|
+
sheet[PAYSLIP_PRINT_CELLS.periodEnd] = serialOf(parseDate(entry.date));
|
|
1141
|
+
sheet.M8 = PAYSLIP_PRINT_FIRST_PAYROLL_NUMBER;
|
|
1142
|
+
for (const [cell, field] of Object.entries(PAYSLIP_PRINT_PERIOD_CELLS)) sheet[cell] = entry[field] || 0;
|
|
1143
|
+
|
|
1144
|
+
const toDate = tabs.slice(0, period).flatMap((tab) => (entriesByTab[tab] || []).slice(0, 1));
|
|
1145
|
+
for (const [cell, field] of Object.entries(PAYSLIP_PRINT_TO_DATE_CELLS)) {
|
|
1146
|
+
sheet[cell] = toDate.reduce((total, line) => total + (line[field] || 0), 0);
|
|
1147
|
+
}
|
|
1148
|
+
// M18's ADDRESS now points at the same cell I9 does -- the wages-paid date.
|
|
1149
|
+
sheet.M18 = sheet[PAYSLIP_PRINT_CELLS.periodEnd];
|
|
1150
|
+
return sheet;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// The year the package's payroll runs in, which is the year its tax data
|
|
1154
|
+
// opens in. A book that carries no tax data derives it the way cellWrites
|
|
1155
|
+
// does: the year before the year end's own calendar year, unless the year
|
|
1156
|
+
// end falls in January to March, when the payroll year already opened the
|
|
1157
|
+
// April before that year.
|
|
1158
|
+
function payrollYearOf(taxData, period) {
|
|
1159
|
+
const financialYearStart = taxData.financial_year?.start;
|
|
1160
|
+
if (financialYearStart) return new Date(financialYearStart).getUTCFullYear();
|
|
1161
|
+
const targetStartYear = period.yearEnd.getUTCFullYear() - 1;
|
|
1162
|
+
const yearEndMonth = period.yearEnd.getUTCMonth() + 1;
|
|
1163
|
+
return yearEndMonth <= 3 ? targetStartYear : targetStartYear + 1;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// The calendar every payslip dates from. B2 carries the payroll year's first
|
|
1167
|
+
// day and every date under it is the row above plus one, so naming the row a
|
|
1168
|
+
// month opens on names its date, its tax week and its week within the month.
|
|
1169
|
+
// The payroll year is the tax year the package was generated for, not the
|
|
1170
|
+
// accounting period, so a company with a June year end still runs its payroll
|
|
1171
|
+
// from the 6 April the rates start on. Column A is headed "Month Sheet" and
|
|
1172
|
+
// is the printed payslip's join, so it names the package's own month tabs in
|
|
1173
|
+
// order rather than the calendar months the dates beside it fall in.
|
|
1174
|
+
function buildPayslipsCalendar(taxData, period, tabs) {
|
|
1175
|
+
const anchor = serialOf(payrollYearStart(payrollYearOf(taxData, period)));
|
|
1176
|
+
const sheet = { B2: anchor };
|
|
1177
|
+
for (const { month, row, daysBefore, week } of payrollMonthStarts()) {
|
|
1178
|
+
sheet[`A${row}`] = tabs[month - 1];
|
|
1179
|
+
sheet[`B${row}`] = anchor + daysBefore;
|
|
1180
|
+
sheet[`C${row}`] = week;
|
|
1181
|
+
sheet[`D${row}`] = month;
|
|
1182
|
+
sheet[`F${row}`] = 1;
|
|
1183
|
+
}
|
|
1184
|
+
return sheet;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// ── Company secretary ──────────────────────────────────────────────────────
|
|
1188
|
+
|
|
1189
|
+
// The share register, the board minute and the register of charges. Row 3 of
|
|
1190
|
+
// the members register carries the template's own fully-paid ordinary share
|
|
1191
|
+
// placeholder, which is where F1 takes the nominal value from whether or not
|
|
1192
|
+
// a scenario names any members.
|
|
1193
|
+
function buildCompanySecretary(scenario) {
|
|
1194
|
+
const register = { F1: SHARE_NOMINAL_VALUE };
|
|
1195
|
+
const members = scenario.members || [];
|
|
1196
|
+
members.forEach((member, index) => {
|
|
1197
|
+
const row = REGISTER_MEMBER_ROWS[index];
|
|
1198
|
+
if (row === undefined) return;
|
|
1199
|
+
register[`A${row}`] = member.name;
|
|
1200
|
+
register[`G${row}`] = member.shares;
|
|
1201
|
+
});
|
|
1202
|
+
// The directors' report prints the first two members a line each, so those
|
|
1203
|
+
// two share cells read back as nil rather than as nothing even when the
|
|
1204
|
+
// register is empty.
|
|
1205
|
+
for (const row of REGISTER_MEMBER_ROWS.slice(0, 2)) if (register[`G${row}`] === undefined) register[`G${row}`] = 0;
|
|
1206
|
+
register.G1 = sum(REGISTER_MEMBER_ROWS.map((row) => register[`G${row}`] || 0));
|
|
1207
|
+
|
|
1208
|
+
// The minute and the charges register are read whether or not a scenario
|
|
1209
|
+
// fills them: a company that minuted no dividend still has a sheet, and the
|
|
1210
|
+
// checks that read it are the ones that would catch a dividend appearing
|
|
1211
|
+
// from nowhere.
|
|
1212
|
+
const boardMeeting = {};
|
|
1213
|
+
if (scenario.dividend) {
|
|
1214
|
+
boardMeeting.E4 = scenario.dividend.declared || 0;
|
|
1215
|
+
if (scenario.dividend.board_meeting) boardMeeting.F2 = serialOf(parseDate(scenario.dividend.board_meeting));
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const charges = {};
|
|
1219
|
+
(scenario.charges || []).forEach((charge, index) => {
|
|
1220
|
+
const row = CHARGE_REGISTER_ROWS[index];
|
|
1221
|
+
if (row === undefined) return;
|
|
1222
|
+
charges[`C${row}`] = charge.valuation;
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
// The register of directors and secretary, and the register of directors'
|
|
1226
|
+
// interests: every officer the book declares, in the order it declares
|
|
1227
|
+
// them, with the business's own address and the capacity each was
|
|
1228
|
+
// appointed in. A director's interest is dated from the day the register of
|
|
1229
|
+
// members says they acquired their shares.
|
|
1230
|
+
const officers = {};
|
|
1231
|
+
const interests = {};
|
|
1232
|
+
const directors = registerOfficers(scenario);
|
|
1233
|
+
const business = scenario.business || {};
|
|
1234
|
+
const officerAddress = [business.address, business.town, business.postcode].filter(Boolean).join(", ") || undefined;
|
|
1235
|
+
directors.forEach((director, index) => {
|
|
1236
|
+
const officerRow = DIRECTOR_SECRETARY_OFFICER_ROWS[index];
|
|
1237
|
+
if (officerRow !== undefined) {
|
|
1238
|
+
officers[`A${officerRow}`] = director.name;
|
|
1239
|
+
if (officerAddress) officers[`B${officerRow}`] = officerAddress;
|
|
1240
|
+
if (director.appointed) officers[`C${officerRow}`] = serialOf(parseDate(director.appointed));
|
|
1241
|
+
officers[`D${officerRow}`] = director.role || "Director";
|
|
1242
|
+
}
|
|
1243
|
+
const interestRow = DIRECTORS_INTERESTS_ROWS[index];
|
|
1244
|
+
if (interestRow === undefined) return;
|
|
1245
|
+
interests[`A${interestRow}`] = director.name;
|
|
1246
|
+
if (officerAddress) interests[`B${interestRow}`] = officerAddress;
|
|
1247
|
+
const holding = members.find((member) => member.name === director.name);
|
|
1248
|
+
if (holding?.acquired) interests[`C${interestRow}`] = serialOf(parseDate(holding.acquired));
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
return {
|
|
1252
|
+
"Companysecretary.xlsx!RegisterofMembers": register,
|
|
1253
|
+
"Companysecretary.xlsx!Boardmeeting": boardMeeting,
|
|
1254
|
+
"Companysecretary.xlsx!Charges&Debentures": charges,
|
|
1255
|
+
"Companysecretary.xlsx!Directors&Secretary": officers,
|
|
1256
|
+
"Companysecretary.xlsx!DirectorsInterests": interests,
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// ── The customer-facing invoice ────────────────────────────────────────────
|
|
1261
|
+
|
|
1262
|
+
// The sample carriage charge cellWrites puts on the invoice's Invoice
|
|
1263
|
+
// Database!E2 for a VAT-registered scenario (app/products/ltd.js).
|
|
1264
|
+
const SALESINVOICE_SAMPLE_CARRIAGE_CHARGE = 37.5;
|
|
1265
|
+
|
|
1266
|
+
// Salesinvoice.xlsx links to nothing else in the book. The generator writes
|
|
1267
|
+
// the tax year's standard rate down Product Details column D, and the invoice
|
|
1268
|
+
// page looks its one sample line up from there.
|
|
1269
|
+
//
|
|
1270
|
+
// cellWrites only raises the sample invoice line for a VAT-registered
|
|
1271
|
+
// business with a first sale to anchor it on (rate > 0, a VAT number, and a
|
|
1272
|
+
// sale) -- the same condition checkCompliance's carriage checks gate on
|
|
1273
|
+
// (app/products/ltd.js). A recalculated package proves the two states: with
|
|
1274
|
+
// the line raised, Invoice Template!N27 through P64 resolve to real figures
|
|
1275
|
+
// off the sample line and its carriage charge; without it, N27 stays blank
|
|
1276
|
+
// and every cell downstream of it follows -- P58, P62 and P64 land on their
|
|
1277
|
+
// formulas' own zero branch, while C38, J38, L38, P38 and the carriage cell
|
|
1278
|
+
// P60 land on their formulas' own blank-string branch. This mirrors both
|
|
1279
|
+
// states rather than assuming one.
|
|
1280
|
+
function buildSalesInvoice(scenario, rate, taxData) {
|
|
1281
|
+
const standardRatePercent = Math.round((taxData?.vat?.standard_rate ?? 0) * 100);
|
|
1282
|
+
const productDetails = { D2: standardRatePercent };
|
|
1283
|
+
const firstInvoiceSale = Object.values(scenario.sales || {}).flat()[0];
|
|
1284
|
+
if (!(rate > 0 && scenario.business?.vat_number && firstInvoiceSale)) {
|
|
1285
|
+
return {
|
|
1286
|
+
"Salesinvoice.xlsx!Product Details": productDetails,
|
|
1287
|
+
"Salesinvoice.xlsx!Invoice Template": {
|
|
1288
|
+
J38: SHEET_BLANK,
|
|
1289
|
+
L38: SHEET_BLANK,
|
|
1290
|
+
P38: SHEET_BLANK,
|
|
1291
|
+
V38: 0,
|
|
1292
|
+
P58: 0,
|
|
1293
|
+
P60: SHEET_BLANK,
|
|
1294
|
+
P62: 0,
|
|
1295
|
+
P64: 0,
|
|
1296
|
+
},
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
const lineNet = firstInvoiceSale.amount;
|
|
1300
|
+
const lineVat = (lineNet * standardRatePercent) / 100;
|
|
1301
|
+
const carriageVat = (SALESINVOICE_SAMPLE_CARRIAGE_CHARGE * standardRatePercent) / 100;
|
|
1302
|
+
const vatTotal = lineVat + carriageVat;
|
|
1303
|
+
return {
|
|
1304
|
+
"Salesinvoice.xlsx!Product Details": productDetails,
|
|
1305
|
+
"Salesinvoice.xlsx!Invoice Template": {
|
|
1306
|
+
J38: lineNet,
|
|
1307
|
+
L38: 1,
|
|
1308
|
+
P38: lineNet,
|
|
1309
|
+
V38: lineVat,
|
|
1310
|
+
P58: lineNet,
|
|
1311
|
+
P60: SALESINVOICE_SAMPLE_CARRIAGE_CHARGE,
|
|
1312
|
+
P62: vatTotal,
|
|
1313
|
+
P64: lineNet + SALESINVOICE_SAMPLE_CARRIAGE_CHARGE + vatTotal,
|
|
1314
|
+
},
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// ── Stock ──────────────────────────────────────────────────────────────────
|
|
1319
|
+
|
|
1320
|
+
// The sheet runs a row per month end, carrying the opening figure forward,
|
|
1321
|
+
// adding the materials bought and taking out the materials its own percentage
|
|
1322
|
+
// reckons went into the product sales. With no percentage the materials
|
|
1323
|
+
// column stays switched off and the calculated value is the opening figure
|
|
1324
|
+
// all the way down. Only the final row takes a physical count, and the
|
|
1325
|
+
// difference between count and calculation is the year's stock adjustment.
|
|
1326
|
+
function buildStock(scenario, openingBalance, rate, productASalesMonthly, materialsBoughtMonthly) {
|
|
1327
|
+
const opening = openingBalance.stock || 0;
|
|
1328
|
+
const materialsPercent = scenario.stock?.materials_percent;
|
|
1329
|
+
const active = materialsPercent !== undefined;
|
|
1330
|
+
|
|
1331
|
+
const movements = materialsBoughtMonthly.map((bought, index) => (active ? bought - materialsPercent * productASalesMonthly[index] : 0));
|
|
1332
|
+
const calculated = opening + sum(movements);
|
|
1333
|
+
const count = scenario.stock?.closing ?? calculated;
|
|
1334
|
+
const adjustment = count === calculated ? 0 : count - calculated;
|
|
1335
|
+
movements[movements.length - 1] += adjustment;
|
|
1336
|
+
|
|
1337
|
+
return {
|
|
1338
|
+
sheet: { D6: opening, D30: calculated, AB30: count, Z30: adjustment },
|
|
1339
|
+
opening,
|
|
1340
|
+
closing: opening + sum(movements),
|
|
1341
|
+
movements,
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// ── Opening balance sheet ──────────────────────────────────────────────────
|
|
1346
|
+
|
|
1347
|
+
function buildOpenAccounts(book, scenario, openingBalance) {
|
|
1348
|
+
const entity = book.entityInformation || {};
|
|
1349
|
+
const business = scenario.business || {};
|
|
1350
|
+
const directors = (scenario.employees || []).filter((employee) => employee.isDirector);
|
|
1351
|
+
|
|
1352
|
+
const cost = sumValuesOf(openingBalance.fixed_asset_cost);
|
|
1353
|
+
const depreciation = sumValuesOf(openingBalance.fixed_asset_depreciation);
|
|
1354
|
+
const bank =
|
|
1355
|
+
(openingBalance.current_account || 0) +
|
|
1356
|
+
(openingBalance.savings_account || 0) +
|
|
1357
|
+
(openingBalance.credit_card || 0) +
|
|
1358
|
+
(openingBalance.cash || 0);
|
|
1359
|
+
const taxAndSocial = (openingBalance.paye_due || 0) + (openingBalance.vat_due || 0) + (openingBalance.cis_due || 0);
|
|
1360
|
+
|
|
1361
|
+
const assets =
|
|
1362
|
+
cost -
|
|
1363
|
+
depreciation +
|
|
1364
|
+
(openingBalance.stock || 0) +
|
|
1365
|
+
(openingBalance.trade_debtors || 0) +
|
|
1366
|
+
bank +
|
|
1367
|
+
(openingBalance.long_term_debtors || 0);
|
|
1368
|
+
const liabilities =
|
|
1369
|
+
(openingBalance.trade_creditors || 0) +
|
|
1370
|
+
(openingBalance.net_wages_due || 0) +
|
|
1371
|
+
(openingBalance.wage_deductions_due || 0) +
|
|
1372
|
+
(openingBalance.dividends_due || 0) +
|
|
1373
|
+
(openingBalance.corporation_tax || 0) +
|
|
1374
|
+
taxAndSocial +
|
|
1375
|
+
(openingBalance.directors_loan || 0) +
|
|
1376
|
+
(openingBalance.long_term_creditors || 0) +
|
|
1377
|
+
(openingBalance.share_capital || 0) +
|
|
1378
|
+
(openingBalance.retained_earnings || 0) +
|
|
1379
|
+
(openingBalance.capital_reserves || 0);
|
|
1380
|
+
|
|
1381
|
+
const sheet = {
|
|
1382
|
+
E2: business.name || entity.organizationIdentifier || "",
|
|
1383
|
+
E13: cost - depreciation,
|
|
1384
|
+
E15: openingBalance.stock || 0,
|
|
1385
|
+
E16: openingBalance.trade_debtors || 0,
|
|
1386
|
+
E18: bank,
|
|
1387
|
+
E20: openingBalance.trade_creditors || 0,
|
|
1388
|
+
E24: openingBalance.corporation_tax || 0,
|
|
1389
|
+
E26: taxAndSocial,
|
|
1390
|
+
E30: openingBalance.directors_loan || 0,
|
|
1391
|
+
E33: openingBalance.share_capital || 0,
|
|
1392
|
+
E34: openingBalance.retained_earnings || 0,
|
|
1393
|
+
E37: assets - liabilities,
|
|
1394
|
+
E48: 0,
|
|
1395
|
+
};
|
|
1396
|
+
if (business.company_number) sheet.E3 = business.company_number;
|
|
1397
|
+
if (business.phone) sheet.E4 = business.phone;
|
|
1398
|
+
const description = business.description || entity.organizationDescription;
|
|
1399
|
+
if (description) sheet.E8 = description;
|
|
1400
|
+
if (business.address) sheet.J3 = business.address;
|
|
1401
|
+
if (business.town) sheet.J4 = business.town;
|
|
1402
|
+
if (business.postcode) sheet.N6 = business.postcode;
|
|
1403
|
+
if (business.utr) sheet.O3 = business.utr;
|
|
1404
|
+
if (directors[0]?.name) sheet.E5 = directors[0].name;
|
|
1405
|
+
return sheet;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// ── Trial balance ──────────────────────────────────────────────────────────
|
|
1409
|
+
|
|
1410
|
+
// Every closing balance the statements read. The workbook builds each one as
|
|
1411
|
+
// the opening figure plus twelve months of movement plus a year-end
|
|
1412
|
+
// adjustment, so that is the order here: the opening column first, then the
|
|
1413
|
+
// journals, the banks and the payroll, then the schedule and the board
|
|
1414
|
+
// minute. Each income and expense row keeps its twelve monthly figures beside
|
|
1415
|
+
// its total, because the management P&L reads the months and the published
|
|
1416
|
+
// statements read the year.
|
|
1417
|
+
function buildTrialBalance(input) {
|
|
1418
|
+
const {
|
|
1419
|
+
openingBalance,
|
|
1420
|
+
salesMonthly,
|
|
1421
|
+
purchasesMonthly,
|
|
1422
|
+
banks,
|
|
1423
|
+
payroll,
|
|
1424
|
+
employeePayroll,
|
|
1425
|
+
directorPayroll,
|
|
1426
|
+
blocks,
|
|
1427
|
+
stock,
|
|
1428
|
+
tabs,
|
|
1429
|
+
hp,
|
|
1430
|
+
dividendDeclared,
|
|
1431
|
+
shareIssue,
|
|
1432
|
+
} = input;
|
|
1433
|
+
const openingCost = openingBalance.fixed_asset_cost || {};
|
|
1434
|
+
const openingDepreciation = openingBalance.fixed_asset_depreciation || {};
|
|
1435
|
+
const receipts = (code) => bankCodeMonths(banks, code, "receipt", tabs);
|
|
1436
|
+
const payments = (code) => bankCodeMonths(banks, code, "payment", tabs);
|
|
1437
|
+
const negated = (values) => values.map((value) => -value);
|
|
1438
|
+
const added = (...series) => series[0].map((_, index) => sum(series.map((values) => values[index])));
|
|
1439
|
+
|
|
1440
|
+
const tb = { monthly: {} };
|
|
1441
|
+
|
|
1442
|
+
// The opening column, cell by cell from the opening balance sheet.
|
|
1443
|
+
tb.D6 = openingCost.land_buildings || 0;
|
|
1444
|
+
tb.D7 = openingCost.plant_machinery || 0;
|
|
1445
|
+
tb.D8 = openingCost.fixtures_fittings || 0;
|
|
1446
|
+
tb.D9 = openingCost.computer_technology || 0;
|
|
1447
|
+
tb.D10 = openingCost.motor_vehicles || 0;
|
|
1448
|
+
tb.D11 = -(openingDepreciation.land_buildings || 0);
|
|
1449
|
+
tb.D12 = -(openingDepreciation.plant_machinery || 0);
|
|
1450
|
+
tb.D13 = -(openingDepreciation.fixtures_fittings || 0);
|
|
1451
|
+
tb.D14 = -(openingDepreciation.computer_technology || 0);
|
|
1452
|
+
tb.D15 = -(openingDepreciation.motor_vehicles || 0);
|
|
1453
|
+
tb.D19 = openingBalance.stock || 0;
|
|
1454
|
+
tb.D20 = openingBalance.trade_debtors || 0;
|
|
1455
|
+
tb.D22 = openingBalance.current_account || 0;
|
|
1456
|
+
tb.D23 = openingBalance.savings_account || 0;
|
|
1457
|
+
tb.D24 = openingBalance.credit_card || 0;
|
|
1458
|
+
tb.D25 = openingBalance.cash || 0;
|
|
1459
|
+
tb.D28 = -(openingBalance.trade_creditors || 0);
|
|
1460
|
+
tb.D29 = -(openingBalance.net_wages_due || 0);
|
|
1461
|
+
tb.D30 = -(openingBalance.wage_deductions_due || 0);
|
|
1462
|
+
tb.D31 = -(openingBalance.dividends_due || 0);
|
|
1463
|
+
tb.D32 = -(openingBalance.cis_due || 0);
|
|
1464
|
+
tb.D33 = -(openingBalance.vat_due || 0);
|
|
1465
|
+
tb.D34 = -(openingBalance.paye_due || 0);
|
|
1466
|
+
tb.D35 = -(openingBalance.corporation_tax || 0);
|
|
1467
|
+
tb.D37 = openingBalance.long_term_debtors || 0;
|
|
1468
|
+
tb.D39 = -(openingBalance.directors_loan || 0);
|
|
1469
|
+
tb.D40 = -(openingBalance.long_term_creditors || 0);
|
|
1470
|
+
tb.D42 = -(openingBalance.share_capital || 0);
|
|
1471
|
+
tb.D43 = -(openingBalance.retained_earnings || 0);
|
|
1472
|
+
tb.D44 = -(openingBalance.capital_reserves || 0);
|
|
1473
|
+
tb.D91 = sum(
|
|
1474
|
+
Object.entries(tb)
|
|
1475
|
+
.filter(([key]) => /^D\d+$/.test(key))
|
|
1476
|
+
.map(([, value]) => value),
|
|
1477
|
+
);
|
|
1478
|
+
|
|
1479
|
+
// Income and expense rows, month by month. The sales analysis columns are
|
|
1480
|
+
// held as credits, which is why the management P&L negates them back.
|
|
1481
|
+
const monthly = tb.monthly;
|
|
1482
|
+
monthly[53] = negated(salesMonthly("O"));
|
|
1483
|
+
monthly[54] = negated(salesMonthly("P"));
|
|
1484
|
+
monthly[55] = negated(salesMonthly("Q"));
|
|
1485
|
+
monthly[56] = negated(salesMonthly("R"));
|
|
1486
|
+
monthly[57] = negated(salesMonthly("S"));
|
|
1487
|
+
monthly[58] = negated(receipts("K"));
|
|
1488
|
+
monthly[60] = added(purchasesMonthly("O"), negated(stock.movements));
|
|
1489
|
+
monthly[61] = purchasesMonthly("P");
|
|
1490
|
+
monthly[62] = purchasesMonthly("Q");
|
|
1491
|
+
monthly[64] = tabs.map((tab) => employeePayroll[tab].grossPay);
|
|
1492
|
+
monthly[65] = purchasesMonthly("S");
|
|
1493
|
+
monthly[66] = added(
|
|
1494
|
+
purchasesMonthly("R"),
|
|
1495
|
+
tabs.map((tab) => directorPayroll[tab].grossPay),
|
|
1496
|
+
);
|
|
1497
|
+
monthly[67] = tabs.map((tab) => payroll[tab].employerNI);
|
|
1498
|
+
for (const [row, column] of Object.entries({
|
|
1499
|
+
68: "T",
|
|
1500
|
+
69: "U",
|
|
1501
|
+
70: "V",
|
|
1502
|
+
71: "W",
|
|
1503
|
+
72: "X",
|
|
1504
|
+
73: "Y",
|
|
1505
|
+
74: "Z",
|
|
1506
|
+
75: "AA",
|
|
1507
|
+
76: "AB",
|
|
1508
|
+
77: "AC",
|
|
1509
|
+
78: "AD",
|
|
1510
|
+
79: "AE",
|
|
1511
|
+
80: "AF",
|
|
1512
|
+
})) {
|
|
1513
|
+
monthly[row] = purchasesMonthly(column);
|
|
1514
|
+
}
|
|
1515
|
+
monthly[81] = negated(salesMonthly("T"));
|
|
1516
|
+
monthly[82] = payments("J");
|
|
1517
|
+
monthly[83] = payments("B");
|
|
1518
|
+
monthly[84] = purchasesMonthly("AG");
|
|
1519
|
+
monthly[85] = purchasesMonthly("AH");
|
|
1520
|
+
// Depreciation and the loss on disposal reach the books a twelfth a month.
|
|
1521
|
+
const lossOnDisposal = blocks.whole.W - blocks.whole.X - blocks.whole.V;
|
|
1522
|
+
monthly[86] = zeroMonths().map(() => lossOnDisposal / 12);
|
|
1523
|
+
monthly[87] = zeroMonths().map(() => blocks.whole.I / 12);
|
|
1524
|
+
monthly[88] = negated(receipts("X"));
|
|
1525
|
+
monthly[89] = payments("X");
|
|
1526
|
+
monthly[19] = stock.movements;
|
|
1527
|
+
for (const [row, values] of Object.entries(monthly)) tb[`EJ${row}`] = sum(values) + (tb[`D${row}`] || 0);
|
|
1528
|
+
|
|
1529
|
+
// Interest received arrives net of the tax deducted at source, and the
|
|
1530
|
+
// computation charges the gross figure. EH58 is the grossing-up and EH35
|
|
1531
|
+
// gives the same tax back as a credit against the charge.
|
|
1532
|
+
tb.EH58 = tb.EJ58 / ((100 - input.smallProfitsRatePercent) / 100) - tb.EJ58;
|
|
1533
|
+
tb.EJ58 += tb.EH58;
|
|
1534
|
+
tb.EH35 = -tb.EH58;
|
|
1535
|
+
|
|
1536
|
+
// The fixed asset rows. Cost carries the additions and drops the disposals,
|
|
1537
|
+
// accumulated depreciation carries the year's charge and drops the
|
|
1538
|
+
// disposals' own, and the two accrual rows take the additions and the
|
|
1539
|
+
// disposal proceeds back out so the year's movement lands once.
|
|
1540
|
+
for (const [className, row] of Object.entries(SCHEDULE_COST_ROWS)) {
|
|
1541
|
+
const block = blocks[className];
|
|
1542
|
+
tb[`EJ${row}`] = (tb[`D${row}`] || 0) + block.newAssets.E - (block.existing.W + block.newAssets.W);
|
|
1543
|
+
}
|
|
1544
|
+
for (const [className, row] of Object.entries(SCHEDULE_DEPRECIATION_ROWS)) {
|
|
1545
|
+
const block = blocks[className];
|
|
1546
|
+
tb[`EJ${row}`] = (tb[`D${row}`] || 0) - (block.existing.I + block.newAssets.I) + (block.existing.X + block.newAssets.X);
|
|
1547
|
+
}
|
|
1548
|
+
tb.EJ16 = sum(purchasesMonthly("AI")) - blocks.allNew.E;
|
|
1549
|
+
tb.EJ17 = -sum(salesMonthly("U")) - lossOnDisposal + (blocks.whole.W - blocks.whole.X);
|
|
1550
|
+
|
|
1551
|
+
// Debtors, creditors and the bank.
|
|
1552
|
+
tb.EJ20 = tb.D20 + sum(salesMonthly("F")) - sum(salesMonthly(SALES_CIS_COLUMN)) - sum(receipts("DR"));
|
|
1553
|
+
tb.EJ22 = tb.D22 + bankNet(banks["Currentaccount.xlsx"]);
|
|
1554
|
+
tb.EJ23 = tb.D23 + bankNet(banks["Savingaccount.xlsx"]);
|
|
1555
|
+
tb.EJ24 = tb.D24 + bankNet(banks["Creditcardaccount.xlsx"]);
|
|
1556
|
+
tb.EJ25 = tb.D25 + bankNet(banks["Cashaccount.xlsx"]) + shareIssue;
|
|
1557
|
+
tb.EJ26 = intraTransfers(banks);
|
|
1558
|
+
tb.EJ28 = tb.D28 - sum(purchasesMonthly("F")) + sum(purchasesMonthly(PURCHASES_CIS_COLUMN)) + sum(payments("CR")) + hp.longTermCreditor;
|
|
1559
|
+
tb.EJ29 = tb.D29;
|
|
1560
|
+
tb.EJ30 = tb.D30;
|
|
1561
|
+
tb.EJ31 = tb.D31 + sum(payments("DV")) - dividendDeclared;
|
|
1562
|
+
tb.EJ32 =
|
|
1563
|
+
tb.D32 + sum(salesMonthly(SALES_CIS_COLUMN)) - sum(purchasesMonthly(PURCHASES_CIS_COLUMN)) - sum(receipts("RC")) + sum(payments("RC"));
|
|
1564
|
+
tb.EJ33 = tb.D33 - sum(salesMonthly("G")) + sum(purchasesMonthly("G")) - sum(receipts("RV")) + sum(payments("RV"));
|
|
1565
|
+
tb.L34 = -sum(tabs.slice(0, 1).map((tab) => payroll[tab].incomeTax + payroll[tab].employeeNI + payroll[tab].employerNI));
|
|
1566
|
+
tb.EJ34 =
|
|
1567
|
+
tb.D34 + sum(payments("RP")) - sum(tabs.map((tab) => payroll[tab].incomeTax + payroll[tab].employeeNI + payroll[tab].employerNI));
|
|
1568
|
+
tb.EJ37 = tb.D37 - sum(receipts("LDR")) + sum(payments("LDR"));
|
|
1569
|
+
tb.EJ39 = tb.D39 - sum(receipts("DL")) + sum(payments("DL"));
|
|
1570
|
+
tb.EJ40 = tb.D40 - sum(receipts("LCR")) + sum(payments("LCR")) - hp.longTermCreditor;
|
|
1571
|
+
tb.EJ42 = tb.D42 - shareIssue;
|
|
1572
|
+
tb.EJ44 = tb.D44;
|
|
1573
|
+
tb.EJ48 = dividendDeclared;
|
|
1574
|
+
|
|
1575
|
+
tb.corporationTaxCreditorBeforeCharge = tb.D35 + sum(payments("RT"));
|
|
1576
|
+
tb.retainedEarningsBroughtForward = tb.D43;
|
|
1577
|
+
return tb;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function bankNet(file) {
|
|
1581
|
+
if (!file) return 0;
|
|
1582
|
+
return sum(Object.values(file.months).map((month) => month.receipts - month.payments));
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// The transfer legs each workbook carries, so a movement between two accounts
|
|
1586
|
+
// nets to nil across the pair.
|
|
1587
|
+
function intraTransfers(banks) {
|
|
1588
|
+
let total = 0;
|
|
1589
|
+
for (const [fileName, file] of Object.entries(banks)) {
|
|
1590
|
+
for (const month of Object.values(file.months)) {
|
|
1591
|
+
for (const code of BANK_LAYOUTS[fileName].transfers) {
|
|
1592
|
+
total += -(month.receiptCodes[code] || 0) + (month.paymentCodes[code] || 0);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return total;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// The cells the reconciliation reads off the trial balance.
|
|
1600
|
+
function trialBalanceReads(tb) {
|
|
1601
|
+
return Object.fromEntries(TRIAL_BALANCE_READS.map((cell) => [cell, tb[cell] || 0]));
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// ── Management profit and loss ─────────────────────────────────────────────
|
|
1605
|
+
|
|
1606
|
+
// Column B is the year and columns C to N the twelve months. A month column
|
|
1607
|
+
// is that month's own movement on the trial balance row the line reads.
|
|
1608
|
+
function buildMonthlyProfitAndLoss(tb, tabs) {
|
|
1609
|
+
const pl = {};
|
|
1610
|
+
const setRow = (row, values) => {
|
|
1611
|
+
tabs.forEach((tab, index) => {
|
|
1612
|
+
pl[`${MONTH_COLS[index]}${row}`] = values[index];
|
|
1613
|
+
});
|
|
1614
|
+
pl[`B${row}`] = sum(values);
|
|
1615
|
+
};
|
|
1616
|
+
const negated = (values) => values.map((value) => -value);
|
|
1617
|
+
|
|
1618
|
+
// The five turnover lines sit 49 rows above their trial balance rows, and
|
|
1619
|
+
// the trial balance holds income as a credit, which is why they come back
|
|
1620
|
+
// negated.
|
|
1621
|
+
for (const row of SALES_PL_ROWS) setRow(row, negated(tb.monthly[row + SALES_ROW_OFFSET]));
|
|
1622
|
+
setRow(
|
|
1623
|
+
9,
|
|
1624
|
+
tabs.map((_, index) => sum(SALES_PL_ROWS.map((row) => pl[`${MONTH_COLS[index]}${row}`]))),
|
|
1625
|
+
);
|
|
1626
|
+
|
|
1627
|
+
pl.B11 = tb.EJ60;
|
|
1628
|
+
setRow(12, tb.monthly[61]);
|
|
1629
|
+
setRow(13, tb.monthly[62]);
|
|
1630
|
+
pl.B14 = pl.B11 + pl.B12 + pl.B13;
|
|
1631
|
+
pl.B16 = pl.B9 - pl.B14;
|
|
1632
|
+
|
|
1633
|
+
pl.B18 = tb.EJ64 + tb.EJ65;
|
|
1634
|
+
pl.B19 = tb.EJ66;
|
|
1635
|
+
pl.B20 = tb.EJ67;
|
|
1636
|
+
for (const [row, source] of Object.entries(EXPENSE_PL_ROWS)) setRow(row, tb.monthly[source]);
|
|
1637
|
+
setRow(SALES_BAD_DEBT_ROW, tb.monthly[81]);
|
|
1638
|
+
pl.B35 = tb.EJ82;
|
|
1639
|
+
pl.B36 = tb.EJ83 + tb.EJ88 + tb.EJ89;
|
|
1640
|
+
setRow(37, tb.monthly[84]);
|
|
1641
|
+
setRow(38, tb.monthly[85]);
|
|
1642
|
+
setRow(39, tb.monthly[86]);
|
|
1643
|
+
setRow(40, tb.monthly[87]);
|
|
1644
|
+
|
|
1645
|
+
pl.B41 = sum(Array.from({ length: 23 }, (_, index) => pl[`B${18 + index}`] || 0));
|
|
1646
|
+
pl.B43 = pl.B16 - pl.B41;
|
|
1647
|
+
// The month columns carry the interest as the accounts received it. The
|
|
1648
|
+
// grossing-up for tax deducted at source is a year-end adjustment the
|
|
1649
|
+
// working sheet reads, not a movement any month saw.
|
|
1650
|
+
pl.B44 = -sum(tb.monthly[58]);
|
|
1651
|
+
pl.B45 = pl.B43 + pl.B44;
|
|
1652
|
+
return pl;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// ── Published statements ───────────────────────────────────────────────────
|
|
1656
|
+
|
|
1657
|
+
function buildPublishedProfitAndLoss(tb, pl, admin) {
|
|
1658
|
+
const turnover = -(tb.EJ53 + tb.EJ54 + tb.EJ55 + tb.EJ56);
|
|
1659
|
+
const grants = -tb.EJ57;
|
|
1660
|
+
const sheet = {
|
|
1661
|
+
D3: admin.B32,
|
|
1662
|
+
E5: admin.B32,
|
|
1663
|
+
B9: 0,
|
|
1664
|
+
B14: 0,
|
|
1665
|
+
B18: 0,
|
|
1666
|
+
B54: 0,
|
|
1667
|
+
F7: turnover,
|
|
1668
|
+
F8: grants,
|
|
1669
|
+
F9: turnover + grants,
|
|
1670
|
+
F16: pl.B14,
|
|
1671
|
+
F44: pl.B41,
|
|
1672
|
+
};
|
|
1673
|
+
sheet.F18 = sheet.F9 - sheet.F16;
|
|
1674
|
+
sheet.F46 = sheet.F18 - sheet.F44;
|
|
1675
|
+
sheet.F49 = sheet.F46 - tb.EJ58;
|
|
1676
|
+
return sheet;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
function buildPublishedBalanceSheet(tb, admin) {
|
|
1680
|
+
const fixedAssets = sum([6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17].map((row) => tb[`EJ${row}`] || 0));
|
|
1681
|
+
const statementAccounts = tb.EJ22 + tb.EJ23 + tb.EJ24;
|
|
1682
|
+
const sheet = {
|
|
1683
|
+
D2: admin.B32,
|
|
1684
|
+
F6: fixedAssets,
|
|
1685
|
+
E10: tb.EJ19,
|
|
1686
|
+
E11: tb.EJ20,
|
|
1687
|
+
E12: statementAccounts > 0 ? statementAccounts + tb.EJ25 + tb.EJ26 : tb.EJ25,
|
|
1688
|
+
E16: -(tb.EJ28 + tb.EJ29 + tb.EJ30 + tb.EJ31),
|
|
1689
|
+
E17: -tb.EJ35,
|
|
1690
|
+
E18: -(tb.EJ32 + tb.EJ33 + tb.EJ34),
|
|
1691
|
+
E29: -tb.EJ39,
|
|
1692
|
+
E30: -tb.EJ40,
|
|
1693
|
+
F36: -tb.EJ42,
|
|
1694
|
+
};
|
|
1695
|
+
// The reserves rows sit under the share capital and roll into the
|
|
1696
|
+
// shareholders' funds total, which is the only one of the three the
|
|
1697
|
+
// statements quote.
|
|
1698
|
+
const revenueReserve = -tb.EJ43;
|
|
1699
|
+
const capitalReserve = -tb.EJ44;
|
|
1700
|
+
sheet.E13 = sheet.E10 + sheet.E11 + sheet.E12;
|
|
1701
|
+
sheet.E20 = sheet.E16 + sheet.E17 + sheet.E18;
|
|
1702
|
+
sheet.F22 = sheet.E13 - sheet.E20;
|
|
1703
|
+
sheet.F26 = sheet.F6 + sheet.F22;
|
|
1704
|
+
sheet.F31 = sheet.E29 + sheet.E30;
|
|
1705
|
+
sheet.F33 = sheet.F26 - sheet.F31;
|
|
1706
|
+
sheet.F39 = sheet.F36 + revenueReserve + capitalReserve;
|
|
1707
|
+
return sheet;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// The fixed asset note reads the schedule class by class, and the emoluments
|
|
1711
|
+
// and tax notes read the trial balance and the working sheet.
|
|
1712
|
+
function buildPublishedNotes(blocks, tb, corporationTax, admin, depreciationRates) {
|
|
1713
|
+
const sheet = { A11: admin.B32 };
|
|
1714
|
+
const columns = [];
|
|
1715
|
+
for (const [className, layout] of Object.entries(SCHEDULE_CLASSES)) {
|
|
1716
|
+
const { existing, newAssets } = blocks[className];
|
|
1717
|
+
const column = layout.noteColumn;
|
|
1718
|
+
columns.push(column);
|
|
1719
|
+
sheet[`${column}8`] = Math.max(existing.E, 0);
|
|
1720
|
+
sheet[`${column}9`] = Math.max(newAssets.E, 0);
|
|
1721
|
+
sheet[`${column}10`] = Math.max(existing.W + newAssets.W, 0);
|
|
1722
|
+
sheet[`${column}11`] = sheet[`${column}8`] + sheet[`${column}9`] - sheet[`${column}10`];
|
|
1723
|
+
sheet[`${column}14`] = Math.max(existing.F, 0);
|
|
1724
|
+
sheet[`${column}15`] = Math.max(existing.I + newAssets.I, 0);
|
|
1725
|
+
sheet[`${column}16`] = Math.max(existing.X + newAssets.X, 0);
|
|
1726
|
+
sheet[`${column}17`] = sheet[`${column}14`] - sheet[`${column}16`] + sheet[`${column}15`];
|
|
1727
|
+
sheet[`${column}20`] = sheet[`${column}11`] - sheet[`${column}17`];
|
|
1728
|
+
}
|
|
1729
|
+
for (const row of [8, 9, 10, 11, 14, 15, 16, 17, 20]) {
|
|
1730
|
+
sheet[`G${row}`] = sum(columns.map((column) => sheet[`${column}${row}`]));
|
|
1731
|
+
}
|
|
1732
|
+
sheet.B27 = depreciationRates.land;
|
|
1733
|
+
sheet.B28 = depreciationRates.plant;
|
|
1734
|
+
sheet.B29 = depreciationRates.fixtures;
|
|
1735
|
+
sheet.B30 = depreciationRates.computer;
|
|
1736
|
+
sheet.B31 = depreciationRates.motor;
|
|
1737
|
+
sheet.D35 = tb.EJ66;
|
|
1738
|
+
sheet.D41 = corporationTax.K35;
|
|
1739
|
+
return sheet;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function buildDirectorsReport(publishedPl, publishedBalanceSheet, companySecretary) {
|
|
1743
|
+
const register = companySecretary["Companysecretary.xlsx!RegisterofMembers"];
|
|
1744
|
+
const boardMeeting = companySecretary["Companysecretary.xlsx!Boardmeeting"] || {};
|
|
1745
|
+
const sheet = {
|
|
1746
|
+
F22: publishedBalanceSheet.D2,
|
|
1747
|
+
E87: publishedPl.F9,
|
|
1748
|
+
H87: publishedPl.B9,
|
|
1749
|
+
D94: boardMeeting.E4 || 0,
|
|
1750
|
+
I95: register.G1,
|
|
1751
|
+
F97: register.G3 ?? 0,
|
|
1752
|
+
F98: register.G4 ?? 0,
|
|
1753
|
+
};
|
|
1754
|
+
if (publishedPl.F9 > 0) sheet.D89 = publishedPl.F18 / publishedPl.F9;
|
|
1755
|
+
if (publishedPl.B9 > 0) sheet.I89 = publishedPl.B18 / publishedPl.B9;
|
|
1756
|
+
if (register.A3) sheet.A97 = register.A3;
|
|
1757
|
+
if (register.A4) sheet.A98 = register.A4;
|
|
1758
|
+
return sheet;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// ── Corporation tax working sheet and CT600 ────────────────────────────────
|
|
1762
|
+
|
|
1763
|
+
function buildCorporationTax({ admin, trialBalance, blocks, publishedPl }) {
|
|
1764
|
+
const goodwill = trialBalance.EJ85 > 0 ? trialBalance.EJ85 : 0;
|
|
1765
|
+
const depreciation = trialBalance.EJ87 > 0 ? trialBalance.EJ87 : 0;
|
|
1766
|
+
const netBalancingCharge = blocks.whole.W > 0 ? blocks.whole.Z - blocks.whole.Y : 0;
|
|
1767
|
+
|
|
1768
|
+
const sheet = {
|
|
1769
|
+
E5: admin.L6,
|
|
1770
|
+
H5: admin.N7,
|
|
1771
|
+
K5: publishedPl.F46,
|
|
1772
|
+
I15: Math.max(blocks.allNew.Q, 0),
|
|
1773
|
+
I16: Math.max(blocks.allNew.R, 0),
|
|
1774
|
+
I17: Math.max(blocks.allExisting.R, 0),
|
|
1775
|
+
I18: netBalancingCharge !== 0 ? -netBalancingCharge : 0,
|
|
1776
|
+
};
|
|
1777
|
+
if (goodwill > 0) sheet.I7 = goodwill;
|
|
1778
|
+
if (depreciation > 0) sheet.I8 = depreciation;
|
|
1779
|
+
sheet.K10 = goodwill + depreciation;
|
|
1780
|
+
sheet.K12 = sheet.K5 + sheet.K10;
|
|
1781
|
+
sheet.K20 = sheet.I15 + sheet.I16 + sheet.I17 + sheet.I18;
|
|
1782
|
+
sheet.K22 = sheet.K12 - sheet.K20;
|
|
1783
|
+
sheet.K24 = -trialBalance.EJ58;
|
|
1784
|
+
sheet.K26 = 0;
|
|
1785
|
+
sheet.K28 = sheet.K22 + sheet.K24 - sheet.K26;
|
|
1786
|
+
|
|
1787
|
+
const financialYears = financialYearsInPeriod(fromSerial(admin.L6), fromSerial(admin.N7));
|
|
1788
|
+
const charge = apportionCorporationTax(sheet.K28, financialYears.years, financialYears.totalDays, {
|
|
1789
|
+
smallProfitsRatePercent: [admin.P6, admin.P7],
|
|
1790
|
+
mainRatePercent: admin.P8,
|
|
1791
|
+
marginalReliefFraction: admin.P9,
|
|
1792
|
+
lowerLimit: admin.P12,
|
|
1793
|
+
upperLimit: admin.P13,
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
sheet.A33 = financialYears.years[0].days;
|
|
1797
|
+
sheet.A34 = financialYears.years[1].days;
|
|
1798
|
+
sheet.A35 = financialYears.totalDays;
|
|
1799
|
+
sheet.E33 = admin.K6;
|
|
1800
|
+
sheet.E34 = admin.K7;
|
|
1801
|
+
charge.rows.forEach((row, index) => {
|
|
1802
|
+
const sheetRow = 33 + index;
|
|
1803
|
+
sheet[`F${sheetRow}`] = row.profitShare;
|
|
1804
|
+
sheet[`G${sheetRow}`] = row.ratePercent;
|
|
1805
|
+
sheet[`J${sheetRow}`] = row.taxBeforeRelief;
|
|
1806
|
+
sheet[`L${sheetRow}`] = row.marginalRelief;
|
|
1807
|
+
sheet[`I${sheetRow}`] = row.tax;
|
|
1808
|
+
});
|
|
1809
|
+
sheet.K35 = charge.tax;
|
|
1810
|
+
sheet.K37 = trialBalance.EH35;
|
|
1811
|
+
sheet.K39 = sheet.K35 - sheet.K37;
|
|
1812
|
+
sheet.marginalRelief = charge.marginalRelief;
|
|
1813
|
+
return sheet;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
function corporationTaxReads(sheet) {
|
|
1817
|
+
const reads = {};
|
|
1818
|
+
for (const [key, value] of Object.entries(sheet)) if (/^[A-Z]+\d+$/.test(key)) reads[key] = value;
|
|
1819
|
+
return reads;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// The filed form, box by box, from the working sheet beside it. A form states
|
|
1823
|
+
// a figure only when there is one to state: a box the sheet leaves blank
|
|
1824
|
+
// rather than nil is left out here too, and a year that made a loss files
|
|
1825
|
+
// nothing in the trading profit and chargeable profit boxes.
|
|
1826
|
+
function buildCt600(corporationTax, pl, admin) {
|
|
1827
|
+
const sheet = {
|
|
1828
|
+
B33: admin.L6,
|
|
1829
|
+
M33: admin.N7,
|
|
1830
|
+
AK66: pl.B9,
|
|
1831
|
+
C126: corporationTax.E33,
|
|
1832
|
+
N126: corporationTax.F33,
|
|
1833
|
+
AA126: corporationTax.G33,
|
|
1834
|
+
AJ126: corporationTax.J33,
|
|
1835
|
+
AJ128: corporationTax.J34,
|
|
1836
|
+
};
|
|
1837
|
+
// The second financial year's row is stated only when the period reaches
|
|
1838
|
+
// into it.
|
|
1839
|
+
if (corporationTax.A34 > 0) {
|
|
1840
|
+
sheet.C128 = corporationTax.E34;
|
|
1841
|
+
sheet.N128 = corporationTax.F34;
|
|
1842
|
+
sheet.AA128 = corporationTax.G34;
|
|
1843
|
+
}
|
|
1844
|
+
if (corporationTax.K22 > 0) sheet.Z70 = corporationTax.K22;
|
|
1845
|
+
if (corporationTax.K26 > 0) sheet.Z72 = corporationTax.K26;
|
|
1846
|
+
if (corporationTax.K24 > 0) sheet.AJ76 = corporationTax.K24;
|
|
1847
|
+
sheet.AJ74 = (sheet.Z70 || 0) - (sheet.Z72 || 0);
|
|
1848
|
+
sheet.AJ92 = sheet.AJ74 > 0 ? sheet.AJ74 + (sheet.AJ76 || 0) : 0;
|
|
1849
|
+
sheet.AJ110 = sheet.AJ92;
|
|
1850
|
+
sheet.AJ131 = sheet.AJ126 + sheet.AJ128;
|
|
1851
|
+
sheet.Y133 = corporationTax.marginalRelief;
|
|
1852
|
+
sheet.Y135 = sheet.AJ131 - sheet.Y133;
|
|
1853
|
+
sheet.AJ145 = sheet.Y135;
|
|
1854
|
+
sheet.AJ154 = corporationTax.K37 > 0 ? corporationTax.K37 : 0;
|
|
1855
|
+
sheet.AJ159 = sheet.AJ145 > 0 ? sheet.AJ145 - sheet.AJ154 : 0;
|
|
1856
|
+
sheet.AJ166 = sheet.AJ159 > 0 ? sheet.AJ159 : 0;
|
|
1857
|
+
if (sheet.Y135 > 0 && sheet.AJ110 !== 0) sheet.W137 = (sheet.Y135 * 100) / sheet.AJ110;
|
|
1858
|
+
return sheet;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// ── VAT interface and returns ──────────────────────────────────────────────
|
|
1862
|
+
|
|
1863
|
+
// One interface row per VAT period, in date order: the two periods before the
|
|
1864
|
+
// accounting year, its own twelve months, then the three after. Columns E, G,
|
|
1865
|
+
// I and K carry the rolling three-row sums a quarterly return reads, and each
|
|
1866
|
+
// return looks its own quarter end up in column B.
|
|
1867
|
+
function buildVatReturns(salesMonths, purchaseMonths, period, scenario, rate) {
|
|
1868
|
+
const rows = [];
|
|
1869
|
+
for (let row = VATINTERFACE_FIRST_ROW; row <= VATINTERFACE_LAST_ROW; row++) {
|
|
1870
|
+
const adminRow = VATINTERFACE_FIRST_ADMIN_ROW + (row - VATINTERFACE_FIRST_ROW) * 2;
|
|
1871
|
+
const monthIndex = row - VATINTERFACE_FIRST_MONTH_ROW;
|
|
1872
|
+
const tab = monthIndex >= 0 && monthIndex < 12 ? period.tabs[monthIndex] : null;
|
|
1873
|
+
const straddling = straddlingTotals(scenario, row, rate);
|
|
1874
|
+
rows.push({
|
|
1875
|
+
row,
|
|
1876
|
+
end: serialOf(period.adminMonthEnd(adminRow)),
|
|
1877
|
+
due: serialOf(period.adminMonthEnd(adminRow + 2)),
|
|
1878
|
+
salesNet: tab ? salesMonths[tab].H1 : straddling.salesNet,
|
|
1879
|
+
salesVat: tab ? salesMonths[tab].G1 : straddling.salesVat,
|
|
1880
|
+
purchasesNet: tab ? purchaseMonths[tab].H1 : straddling.purchasesNet,
|
|
1881
|
+
purchasesVat: tab ? purchaseMonths[tab].G1 : straddling.purchasesVat,
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
const byRow = Object.fromEntries(rows.map((entry) => [entry.row, entry]));
|
|
1886
|
+
const rolling = (row, field) => sum([row - 2, row - 1, row].map((index) => (byRow[index] ? byRow[index][field] : 0)));
|
|
1887
|
+
|
|
1888
|
+
const sheet = {};
|
|
1889
|
+
for (const entry of rows) {
|
|
1890
|
+
sheet[`B${entry.row}`] = entry.end;
|
|
1891
|
+
sheet[`C${entry.row}`] = entry.due;
|
|
1892
|
+
sheet[`D${entry.row}`] = entry.salesNet;
|
|
1893
|
+
sheet[`F${entry.row}`] = entry.salesVat;
|
|
1894
|
+
sheet[`H${entry.row}`] = entry.purchasesNet;
|
|
1895
|
+
sheet[`J${entry.row}`] = entry.purchasesVat;
|
|
1896
|
+
sheet[`M${entry.row}`] = 0;
|
|
1897
|
+
if (entry.row < VATINTERFACE_FIRST_MONTH_ROW) continue;
|
|
1898
|
+
sheet[`E${entry.row}`] = rolling(entry.row, "salesNet");
|
|
1899
|
+
sheet[`G${entry.row}`] = rolling(entry.row, "salesVat");
|
|
1900
|
+
sheet[`I${entry.row}`] = rolling(entry.row, "purchasesNet");
|
|
1901
|
+
sheet[`K${entry.row}`] = rolling(entry.row, "purchasesVat");
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
const results = { "Vatreturns.xlsx!Vatinterface": sheet };
|
|
1905
|
+
for (let quarter = 1; quarter <= 5; quarter++) {
|
|
1906
|
+
const row = VATINTERFACE_FIRST_MONTH_ROW - 1 + quarter * 3;
|
|
1907
|
+
const outputVat = sheet[`G${row}`] || 0;
|
|
1908
|
+
const inputVat = sheet[`K${row}`] || 0;
|
|
1909
|
+
results[`Vatreturns.xlsx!VATQtr${quarter}`] = {
|
|
1910
|
+
G5: byRow[row].end,
|
|
1911
|
+
G7: byRow[row].due,
|
|
1912
|
+
G9: outputVat,
|
|
1913
|
+
G13: outputVat,
|
|
1914
|
+
G15: inputVat,
|
|
1915
|
+
G17: outputVat - inputVat,
|
|
1916
|
+
G21: sheet[`E${row}`] || 0,
|
|
1917
|
+
G23: sheet[`I${row}`] || 0,
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
return results;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
function straddlingTotals(scenario, row, rate) {
|
|
1924
|
+
const totals = { salesNet: 0, salesVat: 0, purchasesNet: 0, purchasesVat: 0 };
|
|
1925
|
+
const period = Object.entries(STRADDLING_PERIOD_ROWS).find(([, entryRow]) => entryRow === row)?.[0];
|
|
1926
|
+
if (!period) return totals;
|
|
1927
|
+
for (const entry of scenario.vat_straddling_sales || []) {
|
|
1928
|
+
if (entry.period !== period) continue;
|
|
1929
|
+
totals.salesVat += sheetVat(entry.amount, rate);
|
|
1930
|
+
totals.salesNet += sheetNet(entry.amount, rate);
|
|
1931
|
+
}
|
|
1932
|
+
for (const entry of scenario.vat_straddling_purchases || []) {
|
|
1933
|
+
if (entry.period !== period) continue;
|
|
1934
|
+
totals.purchasesVat += sheetVat(entry.amount, rate);
|
|
1935
|
+
totals.purchasesNet += sheetNet(entry.amount, rate);
|
|
1936
|
+
}
|
|
1937
|
+
return totals;
|
|
1938
|
+
}
|