@diy-accounting-uk/diya-gl 1.2.4 → 1.2.5

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.
@@ -485,7 +485,7 @@
485
485
  {
486
486
  "cell": "Financialaccounts.xlsx!CT600!AA126",
487
487
  "test": "lt",
488
- "against": "Financialaccounts.xlsx!Admin!P8"
488
+ "against": "Financialaccounts.xlsx!Admin!R6"
489
489
  }
490
490
  ],
491
491
  "why": "the form ticks 329 for marginal relief or the small profits rate; the sheet prints neither tick"
@@ -17,10 +17,15 @@ main_rate_limit = 0
17
17
  marginal_relief_fraction = 0
18
18
 
19
19
  # Financial year 2019, the year before this one. An accounting period that
20
- # reaches back over 1 April charges its earlier days at that year's small
21
- # profits rate, which the working sheet's first tax row carries in Admin P6.
20
+ # reaches back over 1 April charges its earlier days at that year's own rates,
21
+ # limits and relief fraction, which the working sheet's first tax row carries
22
+ # in Admin P6, R6, S6, T6 and U6.
22
23
  [corporation_tax_previous_financial_year]
24
+ main_rate = 0.19
23
25
  small_profits_rate = 0.19
26
+ small_profits_limit = 0
27
+ main_rate_limit = 0
28
+ marginal_relief_fraction = 0
24
29
 
25
30
  [capital_allowances]
26
31
  annual_investment_allowance = 1.00
@@ -17,10 +17,15 @@ main_rate_limit = 0
17
17
  marginal_relief_fraction = 0
18
18
 
19
19
  # Financial year 2020, the year before this one. An accounting period that
20
- # reaches back over 1 April charges its earlier days at that year's small
21
- # profits rate, which the working sheet's first tax row carries in Admin P6.
20
+ # reaches back over 1 April charges its earlier days at that year's own rates,
21
+ # limits and relief fraction, which the working sheet's first tax row carries
22
+ # in Admin P6, R6, S6, T6 and U6.
22
23
  [corporation_tax_previous_financial_year]
24
+ main_rate = 0.19
23
25
  small_profits_rate = 0.19
26
+ small_profits_limit = 0
27
+ main_rate_limit = 0
28
+ marginal_relief_fraction = 0
24
29
 
25
30
  [capital_allowances]
26
31
  annual_investment_allowance = 1.00
@@ -17,10 +17,15 @@ main_rate_limit = 0
17
17
  marginal_relief_fraction = 0
18
18
 
19
19
  # Financial year 2021, the year before this one. An accounting period that
20
- # reaches back over 1 April charges its earlier days at that year's small
21
- # profits rate, which the working sheet's first tax row carries in Admin P6.
20
+ # reaches back over 1 April charges its earlier days at that year's own rates,
21
+ # limits and relief fraction, which the working sheet's first tax row carries
22
+ # in Admin P6, R6, S6, T6 and U6.
22
23
  [corporation_tax_previous_financial_year]
24
+ main_rate = 0.19
23
25
  small_profits_rate = 0.19
26
+ small_profits_limit = 0
27
+ main_rate_limit = 0
28
+ marginal_relief_fraction = 0
24
29
 
25
30
  [capital_allowances]
26
31
  annual_investment_allowance = 1.00
@@ -17,10 +17,15 @@ main_rate_limit = 250000
17
17
  marginal_relief_fraction = 0.015
18
18
 
19
19
  # Financial year 2022, the year before this one. An accounting period that
20
- # reaches back over 1 April charges its earlier days at that year's small
21
- # profits rate, which the working sheet's first tax row carries in Admin P6.
20
+ # reaches back over 1 April charges its earlier days at that year's own rates,
21
+ # limits and relief fraction, which the working sheet's first tax row carries
22
+ # in Admin P6, R6, S6, T6 and U6.
22
23
  [corporation_tax_previous_financial_year]
24
+ main_rate = 0.19
23
25
  small_profits_rate = 0.19
26
+ small_profits_limit = 0
27
+ main_rate_limit = 0
28
+ marginal_relief_fraction = 0
24
29
 
25
30
  [capital_allowances]
26
31
  annual_investment_allowance = 1.00
@@ -17,10 +17,15 @@ main_rate_limit = 250000
17
17
  marginal_relief_fraction = 0.015
18
18
 
19
19
  # Financial year 2023, the year before this one. An accounting period that
20
- # reaches back over 1 April charges its earlier days at that year's small
21
- # profits rate, which the working sheet's first tax row carries in Admin P6.
20
+ # reaches back over 1 April charges its earlier days at that year's own rates,
21
+ # limits and relief fraction, which the working sheet's first tax row carries
22
+ # in Admin P6, R6, S6, T6 and U6.
22
23
  [corporation_tax_previous_financial_year]
24
+ main_rate = 0.25
23
25
  small_profits_rate = 0.19
26
+ small_profits_limit = 50000
27
+ main_rate_limit = 250000
28
+ marginal_relief_fraction = 0.015
24
29
 
25
30
  [capital_allowances]
26
31
  annual_investment_allowance = 1.00
@@ -17,10 +17,15 @@ main_rate_limit = 250000
17
17
  marginal_relief_fraction = 0.015
18
18
 
19
19
  # Financial year 2024, the year before this one. An accounting period that
20
- # reaches back over 1 April charges its earlier days at that year's small
21
- # profits rate, which the working sheet's first tax row carries in Admin P6.
20
+ # reaches back over 1 April charges its earlier days at that year's own rates,
21
+ # limits and relief fraction, which the working sheet's first tax row carries
22
+ # in Admin P6, R6, S6, T6 and U6.
22
23
  [corporation_tax_previous_financial_year]
24
+ main_rate = 0.25
23
25
  small_profits_rate = 0.19
26
+ small_profits_limit = 50000
27
+ main_rate_limit = 250000
28
+ marginal_relief_fraction = 0.015
24
29
 
25
30
  [capital_allowances]
26
31
  annual_investment_allowance = 1.00
@@ -21,10 +21,15 @@ main_rate_limit = 250000
21
21
  marginal_relief_fraction = 0.015 # 3/200
22
22
 
23
23
  # Financial year 2025, the year before this one. An accounting period that
24
- # reaches back over 1 April charges its earlier days at that year's small
25
- # profits rate, which the working sheet's first tax row carries in Admin P6.
24
+ # reaches back over 1 April charges its earlier days at that year's own rates,
25
+ # limits and relief fraction, which the working sheet's first tax row carries
26
+ # in Admin P6, R6, S6, T6 and U6.
26
27
  [corporation_tax_previous_financial_year]
28
+ main_rate = 0.25
27
29
  small_profits_rate = 0.19
30
+ small_profits_limit = 50000
31
+ main_rate_limit = 250000
32
+ marginal_relief_fraction = 0.015
28
33
 
29
34
  [capital_allowances]
30
35
  annual_investment_allowance = 1.00
@@ -19,10 +19,15 @@ main_rate_limit = 250000
19
19
  marginal_relief_fraction = 0.015 # 3/200
20
20
 
21
21
  # Financial year 2026, the year before this one. An accounting period that
22
- # reaches back over 1 April charges its earlier days at that year's small
23
- # profits rate, which the working sheet's first tax row carries in Admin P6.
22
+ # reaches back over 1 April charges its earlier days at that year's own rates,
23
+ # limits and relief fraction, which the working sheet's first tax row carries
24
+ # in Admin P6, R6, S6, T6 and U6.
24
25
  [corporation_tax_previous_financial_year]
26
+ main_rate = 0.25
25
27
  small_profits_rate = 0.19
28
+ small_profits_limit = 50000
29
+ main_rate_limit = 250000
30
+ marginal_relief_fraction = 0.015
26
31
 
27
32
  [capital_allowances]
28
33
  annual_investment_allowance = 1.00
@@ -337,6 +337,32 @@
337
337
  "scorecard": "101279 passed, 0 warnings, 0 failed"
338
338
  }
339
339
  }
340
+ },
341
+ {
342
+ "tag": "diya-gl-v1.2.4",
343
+ "date": "2026-09-11",
344
+ "formatVersion": "diya-gl/1",
345
+ "engineVersion": "1.2.4+f5483192",
346
+ "taxDataHash": "9b8294b452fb",
347
+ "reconciledCommit": "320b4b8a1585e16fa170c2d312c14df38d0bcf5b",
348
+ "templates": {
349
+ "bst": {
350
+ "hash": "0d003e10e00c",
351
+ "scorecard": "810 passed, 0 warnings, 0 failed"
352
+ },
353
+ "taxi": {
354
+ "hash": "1ff69654d64d",
355
+ "scorecard": "686 passed, 0 warnings, 0 failed"
356
+ },
357
+ "se": {
358
+ "hash": "e78c4aab0660",
359
+ "scorecard": "7679 passed, 0 warnings, 0 failed"
360
+ },
361
+ "ltd": {
362
+ "hash": "4dc8932fa566",
363
+ "scorecard": "101279 passed, 0 warnings, 0 failed"
364
+ }
365
+ }
340
366
  }
341
367
  ]
342
368
  }
@@ -25,12 +25,7 @@
25
25
 
26
26
  import { toExcelSerial } from "../spreadsheet-runner.js";
27
27
  import { BANK_ACCOUNT_FILES, BANK_LAYOUTS, OPENING_FIXED_ASSET_COLUMNS, isLtdOpeningBankLine } from "../ltd-layout.js";
28
- import {
29
- apportionCorporationTax,
30
- financialYearsInPeriod,
31
- financialYearNumber,
32
- smallProfitsRatePercentFor,
33
- } from "../tax/corporation-tax.js";
28
+ import { apportionCorporationTax, financialYearsInPeriod, financialYearNumber, financialYearRatesFor } from "../tax/corporation-tax.js";
34
29
  import { calculateCapitalAllowances } from "../tax/capital-allowances.js";
35
30
  import {
36
31
  monthlyPayrollBlockRow,
@@ -878,7 +873,6 @@ export function calculateLtdCells(book, lines, taxData, scenario) {
878
873
  // at 31 March: L6 is the period start, N6 the earlier of the first financial
879
874
  // year's end and the period end, L7 the day after and N7 the period end.
880
875
  function buildAdmin(taxData, period, associatedCompanies) {
881
- const corporationTax = taxData.corporation_tax || {};
882
876
  const capitalAllowances = taxData.capital_allowances || {};
883
877
  const depreciation = taxData.depreciation || {};
884
878
  const mileage = taxData.mileage || {};
@@ -889,19 +883,27 @@ function buildAdmin(taxData, period, associatedCompanies) {
889
883
  const financialYears = financialYearsInPeriod(period.start, period.yearEnd);
890
884
  const fileFinancialYear = financialYearNumber(period.yearEnd);
891
885
 
886
+ // What each tax row's own financial year charges by. The two rows differ
887
+ // whenever the period reaches back over 1 April into a year that charged
888
+ // differently.
889
+ const rowRates = financialYears.years.map((financialYear) =>
890
+ financialYearRatesFor(taxData, financialYear.year, fileFinancialYear, financialYear.days),
891
+ );
892
+
892
893
  return {
893
894
  B9: periodStartSerial,
894
895
  B32: yearEndSerial,
895
896
  F21: yearEndSerial,
896
- // Each tax row's own financial year's small profits rate. The two differ
897
- // whenever the period reaches back over 1 April into a year that charged
898
- // a different rate.
899
- P6: smallProfitsRatePercentFor(taxData, financialYears.years[0].year, fileFinancialYear, financialYears.years[0].days),
900
- P7: smallProfitsRatePercentFor(taxData, financialYears.years[1].year, fileFinancialYear, financialYears.years[1].days),
901
- P8: Math.round(corporationTax.main_rate * 100),
902
- P9: corporationTax.marginal_relief_fraction,
903
- P12: corporationTax.small_profits_limit,
904
- P13: corporationTax.main_rate_limit,
897
+ P6: rowRates[0].smallProfitsRatePercent,
898
+ R6: rowRates[0].mainRatePercent,
899
+ S6: rowRates[0].marginalReliefFraction,
900
+ T6: rowRates[0].lowerLimit,
901
+ U6: rowRates[0].upperLimit,
902
+ P7: rowRates[1].smallProfitsRatePercent,
903
+ R7: rowRates[1].mainRatePercent,
904
+ S7: rowRates[1].marginalReliefFraction,
905
+ T7: rowRates[1].lowerLimit,
906
+ U7: rowRates[1].upperLimit,
905
907
  P14: associatedCompanies,
906
908
  G5: Math.round(capitalAllowances.annual_investment_allowance * 100),
907
909
  G6: Math.round(capitalAllowances.writing_down_allowance_main * 100),
@@ -1804,11 +1806,13 @@ function buildCorporationTax({ admin, trialBalance, blocks, publishedPl, openAcc
1804
1806
 
1805
1807
  const financialYears = financialYearsInPeriod(fromSerial(admin.L6), fromSerial(admin.N7));
1806
1808
  const charge = apportionCorporationTax(sheet.K28, financialYears.years, financialYears.totalDays, {
1807
- smallProfitsRatePercent: [admin.P6, admin.P7],
1808
- mainRatePercent: admin.P8,
1809
- marginalReliefFraction: admin.P9,
1810
- lowerLimit: admin.P12,
1811
- upperLimit: admin.P13,
1809
+ perYear: [6, 7].map((adminRow) => ({
1810
+ smallProfitsRatePercent: admin[`P${adminRow}`],
1811
+ mainRatePercent: admin[`R${adminRow}`],
1812
+ marginalReliefFraction: admin[`S${adminRow}`],
1813
+ lowerLimit: admin[`T${adminRow}`],
1814
+ upperLimit: admin[`U${adminRow}`],
1815
+ })),
1812
1816
  associatedCompanies: admin.P14,
1813
1817
  frankedInvestmentIncome: sheet.K29,
1814
1818
  });
@@ -21,7 +21,7 @@ import {
21
21
  payrollYearStart,
22
22
  } from "./payslips-layout.js";
23
23
  import { rollPayslipsCachedDateChain } from "./payslips-date-chain.js";
24
- import { smallProfitsRatePercentFor, financialYearNumber } from "./tax/corporation-tax.js";
24
+ import { financialYearRatesFor, financialYearNumber } from "./tax/corporation-tax.js";
25
25
 
26
26
  // ── Deterministic zip output ───────────────────────────────────────────────
27
27
  //
@@ -499,37 +499,31 @@ export function buildLtdCellEdits(taxData, yearEndSerial) {
499
499
  const ca = taxData.capital_allowances;
500
500
  const dep = taxData.depreciation;
501
501
  const mil = taxData.mileage;
502
- const ct = taxData.corporation_tax;
503
502
 
504
503
  const numericEdits = {};
505
504
 
506
505
  // Year-end date — the ONE date cell. All others are formula-driven from F21.
507
506
  numericEdits.F21 = yearEndSerial;
508
507
 
509
- // Corporation Tax rates (stored as whole-number percentages in the
510
- // spreadsheet). P6 and P7 are the two tax rows' own financial years, which
511
- // carry different small profits rates whenever the period reaches back over
512
- // 1 April into a year that charged a different one.
508
+ // Corporation Tax rates. Rows 6 and 7 are the two tax rows' own financial
509
+ // years, and each carries every figure its year charges by: the small
510
+ // profits rate and the main rate as whole-number percentages, the marginal
511
+ // relief fraction, and the two limits the relief tapers between. The rows
512
+ // differ whenever the period reaches back over 1 April into a year that
513
+ // charged differently.
513
514
  const financialYearRows = ltdAdminFinancialYearRows(yearEndSerial);
514
515
  const fileFinancialYear = financialYearNumber(fromExcelSerial(yearEndSerial));
515
- numericEdits.P6 = smallProfitsRatePercentFor(
516
- taxData,
517
- financialYearRows.K6,
518
- fileFinancialYear,
519
- financialYearRows.N6 - financialYearRows.L6 + 1,
520
- );
521
- numericEdits.P7 = smallProfitsRatePercentFor(
522
- taxData,
523
- financialYearRows.K7,
524
- fileFinancialYear,
525
- financialYearRows.N7 - financialYearRows.L7 + 1,
526
- );
527
- numericEdits.P8 = Math.round(ct.main_rate * 100);
528
-
529
- // Marginal relief: the fraction and the two profit limits it tapers between
530
- numericEdits.P9 = ct.marginal_relief_fraction;
531
- numericEdits.P12 = ct.small_profits_limit;
532
- numericEdits.P13 = ct.main_rate_limit;
516
+ const rowRates = [
517
+ financialYearRatesFor(taxData, financialYearRows.K6, fileFinancialYear, financialYearRows.N6 - financialYearRows.L6 + 1),
518
+ financialYearRatesFor(taxData, financialYearRows.K7, fileFinancialYear, financialYearRows.N7 - financialYearRows.L7 + 1),
519
+ ];
520
+ for (const [index, row] of [6, 7].entries()) {
521
+ numericEdits[`P${row}`] = rowRates[index].smallProfitsRatePercent;
522
+ numericEdits[`R${row}`] = rowRates[index].mainRatePercent;
523
+ numericEdits[`S${row}`] = rowRates[index].marginalReliefFraction;
524
+ numericEdits[`T${row}`] = rowRates[index].lowerLimit;
525
+ numericEdits[`U${row}`] = rowRates[index].upperLimit;
526
+ }
533
527
 
534
528
  // Capital allowances (stored as whole-number percentages)
535
529
  numericEdits.G5 = Math.round(ca.annual_investment_allowance * 100);
@@ -12,8 +12,8 @@
12
12
 
13
13
  export const PROVENANCE_DATA = {
14
14
  formatVersion: "diya-gl/1",
15
- engineVersion: "1.2.4+f5483192",
16
- taxDataHash: "9b8294b452fb",
15
+ engineVersion: "1.2.5+adcbb86f",
16
+ taxDataHash: "84585062cad8",
17
17
  reconciledCommit: "320b4b8a1585e16fa170c2d312c14df38d0bcf5b",
18
18
  templates: {
19
19
  bst: {
@@ -29,7 +29,7 @@ export const PROVENANCE_DATA = {
29
29
  scorecard: "7679 passed, 0 warnings, 0 failed",
30
30
  },
31
31
  ltd: {
32
- hash: "4dc8932fa566",
32
+ hash: "81ffdc956e5a",
33
33
  scorecard: "101279 passed, 0 warnings, 0 failed",
34
34
  },
35
35
  },
@@ -92,56 +92,87 @@ export function financialYearsInPeriod(periodStart, periodEnd) {
92
92
  }
93
93
 
94
94
  /**
95
- * The small profits rate one of the working sheet's two tax rows charges, as
96
- * the whole-number percentage Admin P6 and P7 carry.
95
+ * The five figures a financial year charges corporation tax by, as the
96
+ * working sheet's rate rows carry them: whole-number percentages for the two
97
+ * rates, a fraction for the relief and pounds for the two limits.
98
+ *
99
+ * @param {Object} table - a corporation_tax table out of an ltd-<FY>.toml
100
+ * @returns {{ smallProfitsRatePercent: number, mainRatePercent: number, marginalReliefFraction: number, lowerLimit: number, upperLimit: number }}
101
+ */
102
+ function ratesFromTable(table) {
103
+ return {
104
+ smallProfitsRatePercent: Math.round(table.small_profits_rate * 100),
105
+ mainRatePercent: Math.round(table.main_rate * 100),
106
+ marginalReliefFraction: table.marginal_relief_fraction,
107
+ lowerLimit: table.small_profits_limit,
108
+ upperLimit: table.main_rate_limit,
109
+ };
110
+ }
111
+
112
+ // The fields a tax year file's previous financial year table has to state, so
113
+ // a row charging that year has every figure it needs from the one file.
114
+ const PREVIOUS_FINANCIAL_YEAR_FIELDS = [
115
+ "small_profits_rate",
116
+ "main_rate",
117
+ "marginal_relief_fraction",
118
+ "small_profits_limit",
119
+ "main_rate_limit",
120
+ ];
121
+
122
+ /**
123
+ * What one of the working sheet's two tax rows charges: its own financial
124
+ * year's small profits rate, main rate, relief fraction and two limits, the
125
+ * figures Admin rows 6 and 7 carry a cell each for.
97
126
  *
98
127
  * A package is generated from one app/data/ltd-<FY>.toml, named for the
99
128
  * financial year its own year end falls in. An accounting period that reaches
100
- * back over 1 April charges its earlier days at the year before's rate, and
101
- * that file states it as corporation_tax_previous_financial_year. A row with
102
- * no days charges nothing, so it takes the file's own rate rather than a rate
103
- * no file has.
104
- *
105
- * Only the small profits rate has a cell per financial year. The main rate,
106
- * the relief fraction and the two limits are one figure each for the whole
107
- * period, so a period straddling a change in those still charges both rows at
108
- * the year end's figures.
129
+ * back over 1 April charges its earlier days at the year before's figures,
130
+ * and that file states them as corporation_tax_previous_financial_year. A row
131
+ * with no days charges nothing, so it takes the file's own figures rather
132
+ * than figures no file has.
109
133
  *
110
134
  * @param {Object} taxData - the parsed ltd-<FY>.toml
111
135
  * @param {number} rowFinancialYear - the financial year this row charges
112
136
  * @param {number} fileFinancialYear - the financial year taxData is named for
113
137
  * @param {number} rowDays - days the row charges
114
- * @returns {number} whole-number percentage
138
+ * @returns {{ smallProfitsRatePercent: number, mainRatePercent: number, marginalReliefFraction: number, lowerLimit: number, upperLimit: number }}
115
139
  */
116
- export function smallProfitsRatePercentFor(taxData, rowFinancialYear, fileFinancialYear, rowDays) {
140
+ export function financialYearRatesFor(taxData, rowFinancialYear, fileFinancialYear, rowDays) {
117
141
  // Tax data with no corporation tax table at all reaches here from callers
118
142
  // that want the period dates off the Admin sheet and nothing else. Every
119
- // other rate cell comes out NaN for them, so this one does too rather than
120
- // demanding a rate the caller never had.
121
- if (!taxData.corporation_tax) return NaN;
122
- const own = Math.round(taxData.corporation_tax.small_profits_rate * 100);
123
- if (rowDays <= 0 || rowFinancialYear === fileFinancialYear) return own;
143
+ // other rate cell comes out NaN for them, so these do too rather than
144
+ // demanding rates the caller never had.
145
+ if (!taxData.corporation_tax) {
146
+ return { smallProfitsRatePercent: NaN, mainRatePercent: NaN, marginalReliefFraction: NaN, lowerLimit: NaN, upperLimit: NaN };
147
+ }
148
+ if (rowDays <= 0 || rowFinancialYear === fileFinancialYear) return ratesFromTable(taxData.corporation_tax);
124
149
  if (rowFinancialYear !== fileFinancialYear - 1) {
125
150
  throw new Error(
126
- `financial year ${rowFinancialYear} is neither ${fileFinancialYear} nor the year before it, so no tax year file states its small profits rate`,
151
+ `financial year ${rowFinancialYear} is neither ${fileFinancialYear} nor the year before it, so no tax year file states what it charged`,
127
152
  );
128
153
  }
129
154
  const previous = taxData.corporation_tax_previous_financial_year;
130
- if (previous?.small_profits_rate === undefined) {
155
+ const missing = PREVIOUS_FINANCIAL_YEAR_FIELDS.filter((field) => previous?.[field] === undefined);
156
+ if (missing.length > 0) {
131
157
  throw new Error(
132
- `the financial year ${fileFinancialYear} tax data states no corporation_tax_previous_financial_year.small_profits_rate, so the ${rowDays} days in financial year ${rowFinancialYear} have no rate of their own`,
158
+ `the financial year ${fileFinancialYear} tax data states no corporation_tax_previous_financial_year.${missing.join(", no corporation_tax_previous_financial_year.")}, so the ${rowDays} days in financial year ${rowFinancialYear} have no figures of their own`,
133
159
  );
134
160
  }
135
- return Math.round(previous.small_profits_rate * 100);
161
+ return ratesFromTable(previous);
136
162
  }
137
163
 
138
164
  /**
139
165
  * Corporation tax charged financial year by financial year, the way the
140
166
  * working sheet charges it: the chargeable profit is split across the two
141
- * years by day count, each share meets its own year's rate, and marginal
142
- * relief applies to a share that sits between the two limits, with both
143
- * limits apportioned by the same day count and then divided by one plus the
144
- * number of associated companies.
167
+ * years by day count, each share meets its own year's rates, and marginal
168
+ * relief applies to a share that sits between that year's two limits, with
169
+ * both limits apportioned by the same day count and then divided by one plus
170
+ * the number of associated companies.
171
+ *
172
+ * Every figure the charge turns on belongs to the row's own financial year,
173
+ * so a period straddling 1 April into a year that charged differently -- as
174
+ * FY2022 and FY2023 did, 19% flat against 25% with relief between 50,000 and
175
+ * 250,000 -- charges each part at what that part's year charged.
145
176
  *
146
177
  * Which band a row falls in is decided on its share of augmented profits --
147
178
  * the chargeable profit plus the exempt distributions received
@@ -154,11 +185,7 @@ export function smallProfitsRatePercentFor(taxData, rowFinancialYear, fileFinanc
154
185
  * @param {Array<{ year: number, days: number }>} financialYears - two entries, the second possibly nil
155
186
  * @param {number} totalDays
156
187
  * @param {Object} rates
157
- * @param {number[]} rates.smallProfitsRatePercent - one whole-number rate per financial year
158
- * @param {number} rates.mainRatePercent
159
- * @param {number} rates.marginalReliefFraction
160
- * @param {number} rates.lowerLimit
161
- * @param {number} rates.upperLimit
188
+ * @param {Array<{ smallProfitsRatePercent: number, mainRatePercent: number, marginalReliefFraction: number, lowerLimit: number, upperLimit: number }>} rates.perYear - what each financial year charges, in the same order as financialYears
162
189
  * @param {number} [rates.associatedCompanies] - default 0
163
190
  * @param {number} [rates.frankedInvestmentIncome] - default 0
164
191
  * @returns {{ rows: Array<{ year, days, profitShare, augmentedShare, ratePercent, taxBeforeRelief, marginalRelief, tax }>, tax: number, marginalRelief: number, taxBeforeRelief: number, augmentedProfits: number }}
@@ -167,16 +194,17 @@ export function apportionCorporationTax(profitChargeable, financialYears, totalD
167
194
  const associatedDivisor = 1 + (rates.associatedCompanies ?? 0);
168
195
  const augmentedProfits = profitChargeable + (rates.frankedInvestmentIncome ?? 0);
169
196
  const rows = financialYears.map((financialYear, index) => {
197
+ const yearRates = rates.perYear[index];
170
198
  const share = totalDays > 0 ? financialYear.days / totalDays : 0;
171
199
  const profitShare = profitChargeable > 0 ? profitChargeable * share : 0;
172
200
  const augmentedShare = augmentedProfits * share;
173
- const lowerLimit = (rates.lowerLimit * share) / associatedDivisor;
174
- const upperLimit = (rates.upperLimit * share) / associatedDivisor;
175
- const ratePercent = augmentedShare <= lowerLimit ? rates.smallProfitsRatePercent[index] : rates.mainRatePercent;
201
+ const lowerLimit = (yearRates.lowerLimit * share) / associatedDivisor;
202
+ const upperLimit = (yearRates.upperLimit * share) / associatedDivisor;
203
+ const ratePercent = augmentedShare <= lowerLimit ? yearRates.smallProfitsRatePercent : yearRates.mainRatePercent;
176
204
  const taxBeforeRelief = (profitShare * ratePercent) / 100;
177
205
  const marginalRelief =
178
206
  augmentedShare > lowerLimit && augmentedShare < upperLimit
179
- ? ((upperLimit - augmentedShare) * profitShare * rates.marginalReliefFraction) / augmentedShare
207
+ ? ((upperLimit - augmentedShare) * profitShare * yearRates.marginalReliefFraction) / augmentedShare
180
208
  : 0;
181
209
  return {
182
210
  year: financialYear.year,
@@ -35,7 +35,7 @@ import {
35
35
  payslipsWagesPaidCell,
36
36
  } from "../lib/payslips-layout.js";
37
37
  import { BANK_ACCOUNT_FILES, BANK_LAYOUTS, OPENING_FIXED_ASSET_COLUMNS, isLtdOpeningBankLine } from "../lib/ltd-layout.js";
38
- import { calculateCorporationTax, smallProfitsRatePercentFor } from "../lib/tax/corporation-tax.js";
38
+ import { apportionCorporationTax, financialYearRatesFor } from "../lib/tax/corporation-tax.js";
39
39
  import {
40
40
  buildCategoryNetting,
41
41
  buildProfitBridge,
@@ -1475,14 +1475,21 @@ const PL_ROW_CAPTIONS = {
1475
1475
  // Admin cells the generator injects from the tax-year TOML, and the TOML
1476
1476
  // path each one carries. Whole-number percentages where the sheet holds a
1477
1477
  // percentage, fractions where it holds a fraction.
1478
- // P6 and P7, the two tax rows' small profits rates, are not here: each row
1479
- // takes the rate of the financial year it names, so they are checked against
1480
- // their own year rather than against one figure from the file.
1478
+ // Rows 6 and 7 of the Admin rate table are not here: every corporation tax
1479
+ // figure on them belongs to the financial year that row names, so each is
1480
+ // checked against its own year rather than against one figure from the file.
1481
+ // Row 6 is the working sheet's first tax row and row 7 its second; each
1482
+ // column holds one of the five figures its financial year charges by.
1483
+ const ADMIN_FINANCIAL_YEAR_ROWS = [6, 7];
1484
+ const ADMIN_RATE_COLUMNS = {
1485
+ smallProfitsRatePercent: "P",
1486
+ mainRatePercent: "R",
1487
+ marginalReliefFraction: "S",
1488
+ lowerLimit: "T",
1489
+ upperLimit: "U",
1490
+ };
1491
+
1481
1492
  const ADMIN_TAX_DATA_CELLS = [
1482
- ["P8", "corporation tax main rate", (t) => Math.round(t.corporation_tax.main_rate * 100)],
1483
- ["P9", "marginal relief fraction", (t) => t.corporation_tax.marginal_relief_fraction],
1484
- ["P12", "marginal relief lower limit", (t) => t.corporation_tax.small_profits_limit],
1485
- ["P13", "marginal relief upper limit", (t) => t.corporation_tax.main_rate_limit],
1486
1493
  ["G5", "annual investment allowance", (t) => Math.round(t.capital_allowances.annual_investment_allowance * 100)],
1487
1494
  ["G7", "annual investment allowance (new assets)", (t) => Math.round(t.capital_allowances.annual_investment_allowance * 100)],
1488
1495
  ["G6", "writing down allowance", (t) => Math.round(t.capital_allowances.writing_down_allowance_main * 100)],
@@ -1638,10 +1645,11 @@ export function standardReads() {
1638
1645
  for (const cell of CT600_CELLS) add("CT600", cell);
1639
1646
 
1640
1647
  for (const [cell] of ADMIN_TAX_DATA_CELLS) add("Admin", cell);
1641
- // The two tax rows' small profits rates, which are not in that table
1642
- // because each row takes its own financial year's rate.
1643
- add("Admin", "P6");
1644
- add("Admin", "P7");
1648
+ // The two tax rows' own rates, limits and relief fraction, which are not in
1649
+ // that table because each row takes its own financial year's figures.
1650
+ for (const row of ADMIN_FINANCIAL_YEAR_ROWS) {
1651
+ for (const column of Object.values(ADMIN_RATE_COLUMNS)) add("Admin", `${column}${row}`);
1652
+ }
1645
1653
  add("Admin", "P14");
1646
1654
  add("Admin", "F21");
1647
1655
  add("Admin", "B9");
@@ -2070,7 +2078,7 @@ const DATE_CELLS = {
2070
2078
 
2071
2079
  // Cells that hold a rate or a percentage, whichever way the sheet writes it.
2072
2080
  const RATE_CELLS = {
2073
- Admin: ["P6", "P7", "P8", "P9", "M19", "M21", "G5", "G6", "G7", "G8", "G15", "G16", "G17", "G18", "G19", "O16", "O17"],
2081
+ Admin: ["P6", "P7", "R6", "R7", "S6", "S7", "M19", "M21", "G5", "G6", "G7", "G8", "G15", "G16", "G17", "G18", "G19", "O16", "O17"],
2074
2082
  CorporationTax: ["G33", "G34"],
2075
2083
  CT600: ["AA126", "W137"],
2076
2084
  PubNotes: ["B27", "B28", "B29", "B30", "B31"],
@@ -4206,83 +4214,106 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
4206
4214
  check("CT: charge for the year = the two tax rows", num(ct.K35), num(ct.I33) + num(ct.I34));
4207
4215
 
4208
4216
  // Which band a row falls in is decided on augmented profits: the
4209
- // chargeable profit plus the exempt distributions received. Both limits
4210
- // are shared out over the period the same way the profit is, then
4211
- // divided by one plus the number of associated companies. Relief
4212
- // tapers the gap up to the upper limit and is then scaled by the row's
4217
+ // chargeable profit plus the exempt distributions received. The row's
4218
+ // own financial year states both limits, which are shared out over the
4219
+ // period the same way the profit is and then divided by one plus the
4220
+ // number of associated companies. Relief tapers the gap up to the upper
4221
+ // limit at that year's own fraction and is then scaled by the row's
4213
4222
  // taxable share over its augmented share, so distributions take relief
4214
4223
  // away without ever being taxed.
4215
4224
  const admin = results.Admin;
4216
4225
  if (admin && days > 0) {
4217
4226
  const associatedDivisor = 1 + num(admin.P14);
4218
- const lowerLimit = (rowDays) => (num(admin.P12) * rowDays) / days / associatedDivisor;
4219
- const upperLimit = (rowDays) => (num(admin.P13) * rowDays) / days / associatedDivisor;
4227
+ const sheetRates = (adminRow) =>
4228
+ Object.fromEntries(Object.entries(ADMIN_RATE_COLUMNS).map(([figure, column]) => [figure, num(admin[`${column}${adminRow}`])]));
4229
+ const lowerLimit = (rates, rowDays) => (rates.lowerLimit * rowDays) / days / associatedDivisor;
4230
+ const upperLimit = (rates, rowDays) => (rates.upperLimit * rowDays) / days / associatedDivisor;
4220
4231
  const augmentedShare = (rowDays) => (num(ct.K30) * rowDays) / days;
4221
- const reliefFor = (rowProfit, rowDays) => {
4232
+ const reliefFor = (rates, rowProfit, rowDays) => {
4222
4233
  const augmented = augmentedShare(rowDays);
4223
- if (!(augmented > lowerLimit(rowDays) && augmented < upperLimit(rowDays))) return 0;
4224
- return ((upperLimit(rowDays) - augmented) * rowProfit * num(admin.P9)) / augmented;
4234
+ if (!(augmented > lowerLimit(rates, rowDays) && augmented < upperLimit(rates, rowDays))) return 0;
4235
+ return ((upperLimit(rates, rowDays) - augmented) * rowProfit * rates.marginalReliefFraction) / augmented;
4225
4236
  };
4226
- const rateFor = (rowDays, smallProfitsRate) => (augmentedShare(rowDays) <= lowerLimit(rowDays) ? smallProfitsRate : num(admin.P8));
4237
+ const rateFor = (rates, rowDays) =>
4238
+ augmentedShare(rowDays) <= lowerLimit(rates, rowDays) ? rates.smallProfitsRatePercent : rates.mainRatePercent;
4227
4239
 
4228
4240
  check(
4229
4241
  "CT: first tax row rate = the rate its share of the augmented profits falls in",
4230
4242
  num(ct.G33),
4231
- rateFor(num(ct.A33), num(admin.P6)),
4243
+ rateFor(sheetRates(6), num(ct.A33)),
4232
4244
  0,
4233
4245
  );
4234
4246
  check(
4235
4247
  "CT: second tax row rate = the rate its share of the augmented profits falls in",
4236
4248
  num(ct.G34),
4237
- rateFor(num(ct.A34), num(admin.P7)),
4249
+ rateFor(sheetRates(7), num(ct.A34)),
4238
4250
  0,
4239
4251
  );
4240
4252
  check(
4241
4253
  "CT: first tax row marginal relief = its share of the augmented profits against its share of the limits",
4242
4254
  num(ct.L33),
4243
- reliefFor(num(ct.F33), num(ct.A33)),
4255
+ reliefFor(sheetRates(6), num(ct.F33), num(ct.A33)),
4244
4256
  );
4245
4257
  check(
4246
4258
  "CT: second tax row marginal relief = its share of the augmented profits against its share of the limits",
4247
4259
  num(ct.L34),
4248
- reliefFor(num(ct.F34), num(ct.A34)),
4260
+ reliefFor(sheetRates(7), num(ct.F34), num(ct.A34)),
4249
4261
  );
4250
4262
 
4251
- // Each row's small profits rate is its own financial year's. The
4252
- // year the package was generated for supplies its own; a row whose
4253
- // year is the one before it takes that year's rate from the same
4254
- // file, and a row with no days charges nothing either way.
4263
+ // Every figure a row charges by is its own financial year's. The year
4264
+ // the package was generated for supplies its own; a row whose year is
4265
+ // the one before it takes that year's figures from the same file, and
4266
+ // a row with no days charges nothing either way.
4255
4267
  // The package is generated from the tax year file named for the
4256
4268
  // financial year its year end falls in: the second row's year when
4257
4269
  // the period straddles 1 April, and the first row's when the year
4258
4270
  // ends on 31 March and the period lies wholly inside one.
4259
4271
  const packageFinancialYear = num(ct.A34) > 0 ? num(admin.K7) : num(admin.K6);
4260
- check(
4261
- "CT: first tax row small profits rate = the rate its own financial year charged",
4262
- num(admin.P6),
4263
- smallProfitsRatePercentFor(taxData, num(admin.K6), packageFinancialYear, num(ct.A33)),
4264
- 0,
4265
- );
4266
- check(
4267
- "CT: second tax row small profits rate = the rate its own financial year charged",
4268
- num(admin.P7),
4269
- smallProfitsRatePercentFor(taxData, num(admin.K7), packageFinancialYear, num(ct.A34)),
4270
- 0,
4272
+ const ROW_NAMES = { 6: "first", 7: "second" };
4273
+ const FIGURE_NAMES = {
4274
+ smallProfitsRatePercent: "small profits rate",
4275
+ mainRatePercent: "main rate",
4276
+ marginalReliefFraction: "marginal relief fraction",
4277
+ lowerLimit: "marginal relief lower limit",
4278
+ upperLimit: "marginal relief upper limit",
4279
+ };
4280
+ const statedRates = ADMIN_FINANCIAL_YEAR_ROWS.map((adminRow, index) =>
4281
+ financialYearRatesFor(taxData, num(admin[`K${adminRow}`]), packageFinancialYear, num(index === 0 ? ct.A33 : ct.A34)),
4271
4282
  );
4283
+ for (const [index, adminRow] of ADMIN_FINANCIAL_YEAR_ROWS.entries()) {
4284
+ for (const [figure, column] of Object.entries(ADMIN_RATE_COLUMNS)) {
4285
+ check(
4286
+ `CT: ${ROW_NAMES[adminRow]} tax row ${FIGURE_NAMES[figure]} = what its own financial year charged`,
4287
+ num(admin[`${column}${adminRow}`]),
4288
+ statedRates[index][figure],
4289
+ 0,
4290
+ );
4291
+ }
4292
+ }
4293
+
4294
+ // The charge the period's profit actually bears, against the
4295
+ // statutory computation worked from the tax year file rather than
4296
+ // from the rate cells the sheet charged by: each financial year's
4297
+ // share of the profit at the figures that year's own table states.
4298
+ const statutory = apportionCorporationTax(
4299
+ profit,
4300
+ [
4301
+ { year: num(admin.K6), days: num(ct.A33) },
4302
+ { year: num(admin.K7), days: num(ct.A34) },
4303
+ ],
4304
+ days,
4305
+ {
4306
+ perYear: statedRates,
4307
+ associatedCompanies: expected.business?.associated_companies ?? 0,
4308
+ frankedInvestmentIncome: expected.business?.franked_investment_income ?? 0,
4309
+ },
4310
+ ).tax;
4311
+ check("CT: charge for the year = the statutory computation with marginal relief", num(ct.K35), statutory, 1);
4272
4312
  }
4273
4313
 
4274
4314
  // Tax outstanding is the charge less any income tax already deducted
4275
4315
  // at source from bank interest received.
4276
4316
  check("CT: Tax outstanding = CT less tax deducted at source", num(ct.K39), num(ct.K35) - num(ct.K37));
4277
-
4278
- // The charge the period's profit actually bears, against the statutory
4279
- // computation worked independently of the sheet.
4280
- const statutory = calculateCorporationTax(profit, {
4281
- ...taxData.corporation_tax,
4282
- associated_companies: expected.business?.associated_companies ?? 0,
4283
- franked_investment_income: expected.business?.franked_investment_income ?? 0,
4284
- }).corporationTax;
4285
- check("CT: charge for the year = the statutory computation with marginal relief", num(ct.K35), statutory, 1);
4286
4317
  }
4287
4318
  }
4288
4319
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diy-accounting-uk/diya-gl",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "type": "module",
5
5
  "description": "Recalculate, read and write DIYA-GL books from the command line",
6
6
  "license": "Apache-2.0",