@diy-accounting-uk/diya-gl 1.2.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/data/filing/ct600-v3.toml +2 -0
- package/dist/app/data/hmrc/form-layouts/ltd.json +7 -9
- package/dist/app/data/hmrc/sa103-mtd-mapping.json +1 -1
- package/dist/app/data/releases.json +26 -0
- package/dist/app/lib/anchors/ltd.js +2 -0
- package/dist/app/lib/calculators/ltd.js +6 -2
- package/dist/app/lib/calculators/se-derivations.js +43 -7
- package/dist/app/lib/provenance-data.js +2 -2
- package/dist/app/lib/scenario-extractor.js +1 -1
- package/dist/app/lib/tax/corporation-tax.js +14 -6
- package/dist/app/products/ltd.js +42 -6
- package/package.json +1 -1
|
@@ -682,6 +682,7 @@ format = "integer"
|
|
|
682
682
|
notes = "Use when the AP straddles two financial years with different limits. Not for quarterly instalment payers"
|
|
683
683
|
element = "FirstYear"
|
|
684
684
|
xmlPath = "/IRenvelope/CompanyTaxReturn/CompanyTaxCalculation/CorporationTaxChargeable/AssociatedCompanies/AssociatedCompaniesFinancialYears/FirstYear"
|
|
685
|
+
sheetCell = "Financialaccounts.xlsx!CT600!Y118"
|
|
685
686
|
|
|
686
687
|
[[box]]
|
|
687
688
|
number = 328
|
|
@@ -691,6 +692,7 @@ format = "integer"
|
|
|
691
692
|
notes = "As box 327"
|
|
692
693
|
element = "SecondYear"
|
|
693
694
|
xmlPath = "/IRenvelope/CompanyTaxReturn/CompanyTaxCalculation/CorporationTaxChargeable/AssociatedCompanies/AssociatedCompaniesFinancialYears/SecondYear"
|
|
695
|
+
sheetCell = "Financialaccounts.xlsx!CT600!Y120"
|
|
694
696
|
|
|
695
697
|
[[box]]
|
|
696
698
|
number = 329
|
|
@@ -289,16 +289,14 @@
|
|
|
289
289
|
"rows": [
|
|
290
290
|
{
|
|
291
291
|
"box": 326,
|
|
292
|
-
"rule": {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
"box": 328,
|
|
300
|
-
"rule": { "op": "fixed", "value": 0, "why": "the working sheet's marginal relief assumes no associated companies" }
|
|
292
|
+
"rule": {
|
|
293
|
+
"op": "fixed",
|
|
294
|
+
"value": 0,
|
|
295
|
+
"why": "the sheet reports associated companies by financial year in boxes 327 and 328, not as a whole-period count"
|
|
296
|
+
}
|
|
301
297
|
},
|
|
298
|
+
{ "box": 327 },
|
|
299
|
+
{ "box": 328 },
|
|
302
300
|
{
|
|
303
301
|
"box": 329,
|
|
304
302
|
"rule": {
|
|
@@ -285,6 +285,32 @@
|
|
|
285
285
|
"scorecard": "101279 passed, 0 warnings, 0 failed"
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
"tag": "diya-gl-v1.2.2",
|
|
291
|
+
"date": "2026-09-10",
|
|
292
|
+
"formatVersion": "diya-gl/1",
|
|
293
|
+
"engineVersion": "1.2.2+f31df0f7451c",
|
|
294
|
+
"taxDataHash": "6a9c036b5f9c",
|
|
295
|
+
"reconciledCommit": "320b4b8a1585e16fa170c2d312c14df38d0bcf5b",
|
|
296
|
+
"templates": {
|
|
297
|
+
"bst": {
|
|
298
|
+
"hash": "0d003e10e00c",
|
|
299
|
+
"scorecard": "810 passed, 0 warnings, 0 failed"
|
|
300
|
+
},
|
|
301
|
+
"taxi": {
|
|
302
|
+
"hash": "1ff69654d64d",
|
|
303
|
+
"scorecard": "686 passed, 0 warnings, 0 failed"
|
|
304
|
+
},
|
|
305
|
+
"se": {
|
|
306
|
+
"hash": "c6a50059cc90",
|
|
307
|
+
"scorecard": "7679 passed, 0 warnings, 0 failed"
|
|
308
|
+
},
|
|
309
|
+
"ltd": {
|
|
310
|
+
"hash": "0c18ceeaced4",
|
|
311
|
+
"scorecard": "101279 passed, 0 warnings, 0 failed"
|
|
312
|
+
}
|
|
313
|
+
}
|
|
288
314
|
}
|
|
289
315
|
]
|
|
290
316
|
}
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
STRADDLING_COLUMNS,
|
|
32
32
|
STOCK_MATERIALS_PERCENT_CELL,
|
|
33
33
|
STOCK_FINAL_COUNT_CELL,
|
|
34
|
+
ADMIN_ASSOCIATED_COMPANIES_CELL,
|
|
34
35
|
SCHEDULE_ASSET_CLASSES,
|
|
35
36
|
SCHEDULE_NEW_ASSET_ROWS,
|
|
36
37
|
CHARGE_REGISTER_ROWS,
|
|
@@ -619,6 +620,7 @@ export function isLtdInputCell(file, sheet, cellRef, monthTabs = monthTabOrder(3
|
|
|
619
620
|
case "Financialaccounts.xlsx":
|
|
620
621
|
if (sheet === "OpenAccounts") return isOpenAccountsInputCell(cellRef);
|
|
621
622
|
if (sheet === "Stock") return cellRef === STOCK_MATERIALS_PERCENT_CELL || cellRef === STOCK_FINAL_COUNT_CELL;
|
|
623
|
+
if (sheet === "Admin") return cellRef === ADMIN_ASSOCIATED_COMPANIES_CELL;
|
|
622
624
|
return false;
|
|
623
625
|
default:
|
|
624
626
|
return false;
|
|
@@ -638,7 +638,7 @@ function computeLtd(book, lines, taxData, scenario) {
|
|
|
638
638
|
const tabs = period.tabs;
|
|
639
639
|
const results = {};
|
|
640
640
|
|
|
641
|
-
const admin = buildAdmin(taxData, period);
|
|
641
|
+
const admin = buildAdmin(taxData, period, scenario.business?.associated_companies ?? 0);
|
|
642
642
|
results.Admin = admin;
|
|
643
643
|
link("Admin", { ...linkOnlyCells(adminDateChain(period), admin), N11: admin.B32 });
|
|
644
644
|
|
|
@@ -872,7 +872,7 @@ export function calculateLtdCells(book, lines, taxData, scenario) {
|
|
|
872
872
|
// dates the whole book hangs off. The corporation tax rows split the period
|
|
873
873
|
// at 31 March: L6 is the period start, N6 the earlier of the first financial
|
|
874
874
|
// year's end and the period end, L7 the day after and N7 the period end.
|
|
875
|
-
function buildAdmin(taxData, period) {
|
|
875
|
+
function buildAdmin(taxData, period, associatedCompanies) {
|
|
876
876
|
const corporationTax = taxData.corporation_tax || {};
|
|
877
877
|
const capitalAllowances = taxData.capital_allowances || {};
|
|
878
878
|
const depreciation = taxData.depreciation || {};
|
|
@@ -893,6 +893,7 @@ function buildAdmin(taxData, period) {
|
|
|
893
893
|
P9: corporationTax.marginal_relief_fraction,
|
|
894
894
|
P12: corporationTax.small_profits_limit,
|
|
895
895
|
P13: corporationTax.main_rate_limit,
|
|
896
|
+
P14: associatedCompanies,
|
|
896
897
|
G5: Math.round(capitalAllowances.annual_investment_allowance * 100),
|
|
897
898
|
G6: Math.round(capitalAllowances.writing_down_allowance_main * 100),
|
|
898
899
|
G7: Math.round(capitalAllowances.annual_investment_allowance * 100),
|
|
@@ -1791,6 +1792,7 @@ function buildCorporationTax({ admin, trialBalance, blocks, publishedPl }) {
|
|
|
1791
1792
|
marginalReliefFraction: admin.P9,
|
|
1792
1793
|
lowerLimit: admin.P12,
|
|
1793
1794
|
upperLimit: admin.P13,
|
|
1795
|
+
associatedCompanies: admin.P14,
|
|
1794
1796
|
});
|
|
1795
1797
|
|
|
1796
1798
|
sheet.A33 = financialYears.years[0].days;
|
|
@@ -1833,6 +1835,7 @@ function buildCt600(corporationTax, pl, admin) {
|
|
|
1833
1835
|
AA126: corporationTax.G33,
|
|
1834
1836
|
AJ126: corporationTax.J33,
|
|
1835
1837
|
AJ128: corporationTax.J34,
|
|
1838
|
+
Y118: admin.P14,
|
|
1836
1839
|
};
|
|
1837
1840
|
// The second financial year's row is stated only when the period reaches
|
|
1838
1841
|
// into it.
|
|
@@ -1840,6 +1843,7 @@ function buildCt600(corporationTax, pl, admin) {
|
|
|
1840
1843
|
sheet.C128 = corporationTax.E34;
|
|
1841
1844
|
sheet.N128 = corporationTax.F34;
|
|
1842
1845
|
sheet.AA128 = corporationTax.G34;
|
|
1846
|
+
sheet.Y120 = admin.P14;
|
|
1843
1847
|
}
|
|
1844
1848
|
if (corporationTax.K22 > 0) sheet.Z70 = corporationTax.K22;
|
|
1845
1849
|
if (corporationTax.K26 > 0) sheet.Z72 = corporationTax.K26;
|
|
@@ -416,7 +416,8 @@ export function buildSelfEmploymentQuarterlyUpdates(book, lines, taxData, option
|
|
|
416
416
|
const box24 = boxEntry(boxes, "24");
|
|
417
417
|
warnings.push({
|
|
418
418
|
field: fieldsOf(box24)[1],
|
|
419
|
-
reason:
|
|
419
|
+
reason:
|
|
420
|
+
"SA103F box 24 combines advertising and business entertainment in one Profit & Loss Account row; the template cannot separate them.",
|
|
420
421
|
});
|
|
421
422
|
warnings.push({
|
|
422
423
|
field: "periodExpenses.consolidatedExpenses",
|
|
@@ -425,7 +426,7 @@ export function buildSelfEmploymentQuarterlyUpdates(book, lines, taxData, option
|
|
|
425
426
|
warnings.push({
|
|
426
427
|
field: disallowableField,
|
|
427
428
|
reason:
|
|
428
|
-
|
|
429
|
+
"SA103F box 44 is Profit & Loss Account row 34 alone; a loss on disposal (row 33) stays inside box 29's allowable total, " +
|
|
429
430
|
"which is not an allowable deduction, with nothing moved to a disallowable box.",
|
|
430
431
|
handComputed: round2(pl.B33 || 0),
|
|
431
432
|
});
|
|
@@ -452,22 +453,53 @@ export function buildSelfEmploymentQuarterlyUpdates(book, lines, taxData, option
|
|
|
452
453
|
// Every annual field this book can source, and a warning naming the box for
|
|
453
454
|
// every one it cannot -- section 8 of the design records why each is out of
|
|
454
455
|
// reach today: no cell in the template, a cell the customer fills by hand,
|
|
455
|
-
// or a box the schema has no field for.
|
|
456
|
+
// or a box the schema has no field for. Three of these boxes (52, 54, 69)
|
|
457
|
+
// carry a field that also moves in and out of HMRC's own schema by tax
|
|
458
|
+
// year -- annualFieldsUnavailableForYear() below is what keeps a year that
|
|
459
|
+
// no longer accepts a field from warning about it as if it still did.
|
|
456
460
|
const NO_SOURCE_ANNUAL_BOXES = [
|
|
457
461
|
{ box: "51", pick: 0 }, // allowances.capitalAllowanceSpecialRatePool
|
|
458
462
|
{ box: "51", pick: 1 }, // allowances.capitalAllowanceSingleAssetPool (shared with box 50)
|
|
463
|
+
{ box: "52", pick: 0 }, // allowances.zeroEmissionsGoodsVehicleAllowance -- gone from 2025-26
|
|
459
464
|
{ box: "52.1", pick: 0 }, // allowances.zeroEmissionsCarAllowance
|
|
460
465
|
{ box: "53", pick: 0 }, // allowances.structuredBuildingAllowance
|
|
461
466
|
{ box: "53.1", pick: 0 }, // allowances.enhancedStructuredBuildingAllowance
|
|
467
|
+
{ box: "54", pick: 1 }, // allowances.electricChargePointAllowance -- gone from 2025-26 (shared with box 55's enhancedCapitalAllowance)
|
|
462
468
|
{ box: "55", pick: 1 }, // allowances.businessPremisesRenovationAllowance (shared with enhancedCapitalAllowance)
|
|
463
469
|
{ box: "59", pick: 1 }, // adjustments.balancingChargeBpra (shared with balancingChargeOther)
|
|
464
470
|
{ box: "62", pick: 0 }, // adjustments.includedNonTaxableProfits
|
|
465
471
|
{ box: "68", pick: 0 }, // adjustments.basisAdjustment
|
|
472
|
+
{ box: "69", pick: 0 }, // adjustments.overlapReliefUsed -- gone from 2026-27
|
|
466
473
|
{ box: "71", pick: 0 }, // adjustments.accountingAdjustment
|
|
467
|
-
{ box: "73.3", pick: 0 }, // adjustments.transitionProfitAmount
|
|
468
|
-
{ box: "73.3", pick: 1 }, // adjustments.transitionProfitAccelerationAmount
|
|
474
|
+
{ box: "73.3", pick: 0 }, // adjustments.transitionProfitAmount -- added in 2024-25
|
|
475
|
+
{ box: "73.3", pick: 1 }, // adjustments.transitionProfitAccelerationAmount -- added in 2024-25
|
|
469
476
|
];
|
|
470
477
|
|
|
478
|
+
// api.years in sa103-mtd-mapping.json is the source of which annual fields
|
|
479
|
+
// HMRC's schema accepts in a given tax year: a field named in an earlier
|
|
480
|
+
// year's own fieldsAdded is not yet live before that year, and a field
|
|
481
|
+
// named in that year's or an earlier year's fieldsGone is no longer live
|
|
482
|
+
// from that year on. A fieldsAdded entry that carries a "status" (the two
|
|
483
|
+
// 2026-27 additions, gated behind HMRC test flags) is not treated as a
|
|
484
|
+
// live field here -- neither has a cell in this template regardless, so
|
|
485
|
+
// the payload does not change, only whether the gap is worth warning about.
|
|
486
|
+
function annualFieldsUnavailableForYear(taxYear) {
|
|
487
|
+
const years = loadMapping().api.years;
|
|
488
|
+
const order = Object.keys(years);
|
|
489
|
+
const index = order.indexOf(taxYear);
|
|
490
|
+
if (index === -1) throw new Error(`sa103-mtd-mapping.json's api.years carries no entry for tax year "${taxYear}"`);
|
|
491
|
+
const unavailable = new Set();
|
|
492
|
+
for (let i = index + 1; i < order.length; i++) {
|
|
493
|
+
for (const added of years[order[i]].fieldsAdded || []) {
|
|
494
|
+
if (typeof added === "string") unavailable.add(added);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
for (let i = 0; i <= index; i++) {
|
|
498
|
+
for (const gone of years[order[i]].fieldsGone || []) unavailable.add(gone);
|
|
499
|
+
}
|
|
500
|
+
return unavailable;
|
|
501
|
+
}
|
|
502
|
+
|
|
471
503
|
/**
|
|
472
504
|
* The annual submission that closes the year, built off the same engine the
|
|
473
505
|
* reconciliation runs.
|
|
@@ -485,6 +517,8 @@ export function buildSelfEmploymentAnnualSubmission(book, lines, taxData, option
|
|
|
485
517
|
const rawResults = calculateSeCells(book, lines, taxData, scenario);
|
|
486
518
|
const seFull = rawResults["SE Full"];
|
|
487
519
|
const boxes = sa103fBoxes();
|
|
520
|
+
const taxYear = taxYearLabel(taxData, book);
|
|
521
|
+
const unavailableFields = annualFieldsUnavailableForYear(taxYear);
|
|
488
522
|
|
|
489
523
|
// Each mapping field name already carries its own top-level branch
|
|
490
524
|
// ("allowances.annualInvestmentAllowance", "adjustments.balancingChargeOther",
|
|
@@ -518,13 +552,15 @@ export function buildSelfEmploymentAnnualSubmission(book, lines, taxData, option
|
|
|
518
552
|
];
|
|
519
553
|
for (const { box: boxNumber, pick } of NO_SOURCE_ANNUAL_BOXES) {
|
|
520
554
|
const entry = boxEntry(boxes, boxNumber);
|
|
555
|
+
const field = fieldsOf(entry)[pick];
|
|
556
|
+
if (unavailableFields.has(field)) continue;
|
|
521
557
|
warnings.push({
|
|
522
|
-
field
|
|
558
|
+
field,
|
|
523
559
|
reason: `SA103F box ${boxNumber} has no cell the template computes; the customer fills it in by hand.`,
|
|
524
560
|
});
|
|
525
561
|
}
|
|
526
562
|
|
|
527
|
-
const result = { taxYear
|
|
563
|
+
const result = { taxYear };
|
|
528
564
|
if (Object.keys(allowances).length > 0) result.allowances = allowances;
|
|
529
565
|
if (Object.keys(adjustments).length > 0) result.adjustments = adjustments;
|
|
530
566
|
result.warnings = warnings;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
export const PROVENANCE_DATA = {
|
|
14
14
|
"formatVersion": "diya-gl/1",
|
|
15
|
-
"engineVersion": "1.2.
|
|
15
|
+
"engineVersion": "1.2.3+894e8ba15123",
|
|
16
16
|
"taxDataHash": "6a9c036b5f9c",
|
|
17
17
|
"reconciledCommit": "320b4b8a1585e16fa170c2d312c14df38d0bcf5b",
|
|
18
18
|
"templates": {
|
|
@@ -29,7 +29,7 @@ export const PROVENANCE_DATA = {
|
|
|
29
29
|
"scorecard": "7679 passed, 0 warnings, 0 failed"
|
|
30
30
|
},
|
|
31
31
|
"ltd": {
|
|
32
|
-
"hash": "
|
|
32
|
+
"hash": "d95b3df1e439",
|
|
33
33
|
"scorecard": "101279 passed, 0 warnings, 0 failed"
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -1002,7 +1002,7 @@ export function formatScenarioToml(metadata, grouped, expected) {
|
|
|
1002
1002
|
if (metadata.business) {
|
|
1003
1003
|
parts.push("[business]");
|
|
1004
1004
|
for (const [k, v] of Object.entries(metadata.business)) {
|
|
1005
|
-
parts.push(`${k} = "${escapeTomlString(String(v))}"`);
|
|
1005
|
+
parts.push(typeof v === "number" ? `${k} = ${v}` : `${k} = "${escapeTomlString(String(v))}"`);
|
|
1006
1006
|
}
|
|
1007
1007
|
parts.push("");
|
|
1008
1008
|
}
|
|
@@ -13,15 +13,20 @@
|
|
|
13
13
|
* limit because there was one rate and no relief, and defaulting either of
|
|
14
14
|
* those away invents a relief those years never had.
|
|
15
15
|
*
|
|
16
|
+
* Both limits are divided by one plus the number of associated companies
|
|
17
|
+
* (`ctRates.associated_companies`, default 0 -- a company with no associates
|
|
18
|
+
* keeps the full limits).
|
|
19
|
+
*
|
|
16
20
|
* @param {number} profit - profit chargeable to CT
|
|
17
|
-
* @param {Object} ctRates - { small_profits_rate, main_rate, small_profits_limit, main_rate_limit, marginal_relief_fraction }
|
|
21
|
+
* @param {Object} ctRates - { small_profits_rate, main_rate, small_profits_limit, main_rate_limit, marginal_relief_fraction, associated_companies }
|
|
18
22
|
* @returns {{ profitChargeable, smallProfitsRate, mainRate, corporationTax, marginalRelief }}
|
|
19
23
|
*/
|
|
20
24
|
export function calculateCorporationTax(profit, ctRates) {
|
|
21
25
|
const spr = ctRates.small_profits_rate;
|
|
22
26
|
const mr = ctRates.main_rate;
|
|
23
|
-
const
|
|
24
|
-
const
|
|
27
|
+
const associatedDivisor = 1 + (ctRates.associated_companies ?? 0);
|
|
28
|
+
const spl = ctRates.small_profits_limit / associatedDivisor;
|
|
29
|
+
const splu = (ctRates.main_rate_limit ?? ctRates.small_profits_limit_upper ?? 250000) / associatedDivisor;
|
|
25
30
|
const mrf = ctRates.marginal_relief_fraction ?? 0.015;
|
|
26
31
|
|
|
27
32
|
if (profit <= 0) {
|
|
@@ -82,7 +87,8 @@ export function financialYearsInPeriod(periodStart, periodEnd) {
|
|
|
82
87
|
* working sheet charges it: the chargeable profit is split across the two
|
|
83
88
|
* years by day count, each share meets its own year's rate, and marginal
|
|
84
89
|
* relief applies to a share that sits between the two limits, with both
|
|
85
|
-
* limits apportioned by the same day count
|
|
90
|
+
* limits apportioned by the same day count and then divided by one plus the
|
|
91
|
+
* number of associated companies.
|
|
86
92
|
*
|
|
87
93
|
* @param {number} profitChargeable
|
|
88
94
|
* @param {Array<{ year: number, days: number }>} financialYears - two entries, the second possibly nil
|
|
@@ -93,14 +99,16 @@ export function financialYearsInPeriod(periodStart, periodEnd) {
|
|
|
93
99
|
* @param {number} rates.marginalReliefFraction
|
|
94
100
|
* @param {number} rates.lowerLimit
|
|
95
101
|
* @param {number} rates.upperLimit
|
|
102
|
+
* @param {number} [rates.associatedCompanies] - default 0
|
|
96
103
|
* @returns {{ rows: Array<{ year, days, profitShare, ratePercent, taxBeforeRelief, marginalRelief, tax }>, tax: number, marginalRelief: number, taxBeforeRelief: number }}
|
|
97
104
|
*/
|
|
98
105
|
export function apportionCorporationTax(profitChargeable, financialYears, totalDays, rates) {
|
|
106
|
+
const associatedDivisor = 1 + (rates.associatedCompanies ?? 0);
|
|
99
107
|
const rows = financialYears.map((financialYear, index) => {
|
|
100
108
|
const share = totalDays > 0 ? financialYear.days / totalDays : 0;
|
|
101
109
|
const profitShare = profitChargeable > 0 ? profitChargeable * share : 0;
|
|
102
|
-
const lowerLimit = rates.lowerLimit * share;
|
|
103
|
-
const upperLimit = rates.upperLimit * share;
|
|
110
|
+
const lowerLimit = (rates.lowerLimit * share) / associatedDivisor;
|
|
111
|
+
const upperLimit = (rates.upperLimit * share) / associatedDivisor;
|
|
104
112
|
const ratePercent = profitShare <= lowerLimit ? rates.smallProfitsRatePercent[index] : rates.mainRatePercent;
|
|
105
113
|
const taxBeforeRelief = (profitShare * ratePercent) / 100;
|
|
106
114
|
const marginalRelief =
|
package/dist/app/products/ltd.js
CHANGED
|
@@ -250,6 +250,11 @@ const STOCK_FINAL_ADJUSTMENT_CELL = "Z30";
|
|
|
250
250
|
// N4 and T4 are all zero.
|
|
251
251
|
export const STOCK_MATERIALS_PERCENT_CELL = "H4";
|
|
252
252
|
|
|
253
|
+
// The number of companies associated with this one, entered beside the
|
|
254
|
+
// corporation tax rates it divides. The only Admin cell cellWrites() fills;
|
|
255
|
+
// the rates beside it come from the tax data, not the book.
|
|
256
|
+
export const ADMIN_ASSOCIATED_COMPANIES_CELL = "P14";
|
|
257
|
+
|
|
253
258
|
// ── OpenAccounts layout ────────────────────────────────────────────────────
|
|
254
259
|
// Row 13 takes fixed assets as original cost (G:K) and accumulated
|
|
255
260
|
// depreciation (M:Q), one column per asset class, with net book value in E13.
|
|
@@ -561,6 +566,15 @@ export function cellWrites(scenario, targetStartYear, yearEndMonth) {
|
|
|
561
566
|
const directors = (scenario.employees || []).filter((e) => e.isDirector);
|
|
562
567
|
if (directors[0]?.name) bd.E5 = directors[0].name;
|
|
563
568
|
if (directors[1]?.name) bd.E6 = directors[1].name;
|
|
569
|
+
|
|
570
|
+
// The number of companies associated with this one, which divides both
|
|
571
|
+
// marginal relief limits. Written even when it is none: a blank cell and a
|
|
572
|
+
// stated zero mean the same to the sheet's arithmetic, but only the stated
|
|
573
|
+
// zero comes back when the package is read, and the JS engine states it
|
|
574
|
+
// either way. Leaving it blank makes the two halves of the roundtrip
|
|
575
|
+
// disagree about a cell they agree on the value of.
|
|
576
|
+
if (!hubWrites.Admin) hubWrites.Admin = {};
|
|
577
|
+
hubWrites.Admin.P14 = biz.associated_companies ?? 0;
|
|
564
578
|
}
|
|
565
579
|
|
|
566
580
|
// Opening balance sheet (OpenAccounts)
|
|
@@ -1185,6 +1199,8 @@ export const CELL_MAP = [
|
|
|
1185
1199
|
["CT600", "AJ131", "**Box 430: corporation tax**", "gl-cor:taxAmount (ct600.box430)", "CT600 as filed", 0],
|
|
1186
1200
|
["CT600", "Y133", "Box 435: marginal rate relief","gl-cor:taxAmount (ct600.box435)", "CT600 as filed", 1],
|
|
1187
1201
|
["CT600", "Y135", "**Box 440: corporation tax net of marginal rate relief**", "gl-cor:taxAmount (ct600.box440)", "CT600 as filed", 0],
|
|
1202
|
+
["CT600", "Y118", "Box 327: associated companies, first financial year", "gl-cor:count (ct600.box327)", "CT600 as filed", 1],
|
|
1203
|
+
["CT600", "Y120", "Box 328: associated companies, second financial year", "gl-cor:count (ct600.box328)", "CT600 as filed", 1],
|
|
1188
1204
|
// ── Published P&L (column B is last year, column F this year) ──
|
|
1189
1205
|
["PubP&L", "F7", "Sales Turnover", "gl-cor:amount (pubPL.salesTurnover)","Published P&L", 1],
|
|
1190
1206
|
["PubP&L", "F8", "Investment Grants", "gl-cor:amount (pubPL.grants)", "Published P&L", 1],
|
|
@@ -1606,6 +1622,7 @@ export function standardReads() {
|
|
|
1606
1622
|
for (const cell of CT600_CELLS) add("CT600", cell);
|
|
1607
1623
|
|
|
1608
1624
|
for (const [cell] of ADMIN_TAX_DATA_CELLS) add("Admin", cell);
|
|
1625
|
+
add("Admin", "P14");
|
|
1609
1626
|
add("Admin", "F21");
|
|
1610
1627
|
add("Admin", "B9");
|
|
1611
1628
|
add("Admin", "B32");
|
|
@@ -2043,9 +2060,9 @@ const RATE_CELLS = {
|
|
|
2043
2060
|
// Cells that hold a whole number of something: days, years, miles, shares, a
|
|
2044
2061
|
// week number or a flag.
|
|
2045
2062
|
const COUNT_CELLS = {
|
|
2046
|
-
Admin: ["K6", "K7", "N16", "N17"],
|
|
2063
|
+
Admin: ["K6", "K7", "N16", "N17", "P14"],
|
|
2047
2064
|
CorporationTax: ["A33", "A34", "A35", "E33", "E34"],
|
|
2048
|
-
CT600: ["C126"],
|
|
2065
|
+
CT600: ["C126", "Y118", "Y120"],
|
|
2049
2066
|
Report: ["I95", "F97", "F98"],
|
|
2050
2067
|
};
|
|
2051
2068
|
|
|
@@ -3957,6 +3974,11 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
|
|
|
3957
3974
|
check(`Admin ${cell}: ${label}`, num(admin[cell]), fromTaxData(taxData), 0.0001);
|
|
3958
3975
|
}
|
|
3959
3976
|
|
|
3977
|
+
// The associated companies count is a business fact, not a tax-year
|
|
3978
|
+
// one, so it is not in ADMIN_TAX_DATA_CELLS -- it comes from the
|
|
3979
|
+
// scenario's own business block, defaulting to none.
|
|
3980
|
+
check("Admin P14: number of associated companies", num(admin.P14), expected.business?.associated_companies ?? 0, 0);
|
|
3981
|
+
|
|
3960
3982
|
// F21 is the year-end seed every other date in the package cascades
|
|
3961
3983
|
// from. Its own anchor row and the three published documents that quote
|
|
3962
3984
|
// the year end all have to land on it. The seed itself is measured
|
|
@@ -4094,6 +4116,15 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
|
|
|
4094
4116
|
check("CT600: second financial year profit = second tax row profit", num(ct600.N128), num(corporationTax.F34));
|
|
4095
4117
|
check("CT600: second financial year rate = second tax row rate", num(ct600.AA128), num(corporationTax.G34), 0);
|
|
4096
4118
|
}
|
|
4119
|
+
// Boxes 327 and 328 carry the associated companies count for each
|
|
4120
|
+
// financial year row. The product tracks one count for the whole
|
|
4121
|
+
// period, so both boxes carry the same figure; box 328 follows box 385
|
|
4122
|
+
// and the rest of the second row blank when the period does not
|
|
4123
|
+
// straddle two financial years.
|
|
4124
|
+
check("CT600: associated companies, first financial year = Admin P14", num(ct600.Y118), num(admin.P14));
|
|
4125
|
+
if (num(corporationTax.A34) > 0) {
|
|
4126
|
+
check("CT600: associated companies, second financial year = Admin P14", num(ct600.Y120), num(admin.P14));
|
|
4127
|
+
}
|
|
4097
4128
|
check("CT600: tax payable = tax chargeable", num(ct600.AJ131), num(ct600.AJ126) + num(ct600.AJ128));
|
|
4098
4129
|
check("CT600: marginal rate relief = the working sheet's relief", num(ct600.Y133), num(corporationTax.L33) + num(corporationTax.L34));
|
|
4099
4130
|
check("CT600: tax net of marginal relief = the working sheet's charge", num(ct600.Y135), num(corporationTax.K35));
|
|
@@ -4142,11 +4173,13 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
|
|
|
4142
4173
|
// Each row's share of the profit is charged at the small profits rate
|
|
4143
4174
|
// up to its share of the lower limit and at the main rate above it,
|
|
4144
4175
|
// with marginal relief tapering the gap up to the upper limit. Both
|
|
4145
|
-
// limits are shared out over the period the same way the profit is
|
|
4176
|
+
// limits are shared out over the period the same way the profit is,
|
|
4177
|
+
// then divided by one plus the number of associated companies.
|
|
4146
4178
|
const admin = results.Admin;
|
|
4147
4179
|
if (admin && days > 0) {
|
|
4148
|
-
const
|
|
4149
|
-
const
|
|
4180
|
+
const associatedDivisor = 1 + num(admin.P14);
|
|
4181
|
+
const lowerLimit = (rowDays) => (num(admin.P12) * rowDays) / days / associatedDivisor;
|
|
4182
|
+
const upperLimit = (rowDays) => (num(admin.P13) * rowDays) / days / associatedDivisor;
|
|
4150
4183
|
const reliefFor = (rowProfit, rowDays) =>
|
|
4151
4184
|
rowProfit > lowerLimit(rowDays) && rowProfit < upperLimit(rowDays) ? (upperLimit(rowDays) - rowProfit) * num(admin.P9) : 0;
|
|
4152
4185
|
const rateFor = (rowProfit, rowDays, smallProfitsRate) => (rowProfit <= lowerLimit(rowDays) ? smallProfitsRate : num(admin.P8));
|
|
@@ -4186,7 +4219,10 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
|
|
|
4186
4219
|
|
|
4187
4220
|
// The charge the period's profit actually bears, against the statutory
|
|
4188
4221
|
// computation worked independently of the sheet.
|
|
4189
|
-
const statutory = calculateCorporationTax(profit,
|
|
4222
|
+
const statutory = calculateCorporationTax(profit, {
|
|
4223
|
+
...taxData.corporation_tax,
|
|
4224
|
+
associated_companies: expected.business?.associated_companies ?? 0,
|
|
4225
|
+
}).corporationTax;
|
|
4190
4226
|
check("CT: charge for the year = the statutory computation with marginal relief", num(ct.K35), statutory, 1);
|
|
4191
4227
|
}
|
|
4192
4228
|
}
|