@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.
@@ -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": { "op": "fixed", "value": 0, "why": "the working sheet's marginal relief assumes no associated companies" }
293
- },
294
- {
295
- "box": 327,
296
- "rule": { "op": "fixed", "value": 0, "why": "the working sheet's marginal relief assumes no associated companies" }
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": {
@@ -848,7 +848,7 @@
848
848
  "box": "102",
849
849
  "label": "Adjustment to profits chargeable to Class 4 NICs",
850
850
  "cell": null,
851
- "field": "adjustmentToProfitsForClass4",
851
+ "field": "adjustments.adjustmentToProfitsForClass4",
852
852
  "route": "annual"
853
853
  },
854
854
  {
@@ -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: "SA103F box 24 combines advertising and business entertainment in one Profit & Loss Account row; the template cannot separate them.",
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
- 'SA103F box 44 is Profit & Loss Account row 34 alone; a loss on disposal (row 33) stays inside box 29\'s allowable total, ' +
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: fieldsOf(entry)[pick],
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: taxYearLabel(taxData, book) };
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.2+f31df0f7451c",
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": "0c18ceeaced4",
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 spl = ctRates.small_profits_limit;
24
- const splu = ctRates.main_rate_limit ?? ctRates.small_profits_limit_upper ?? 250000;
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 =
@@ -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 lowerLimit = (rowDays) => (num(admin.P12) * rowDays) / days;
4149
- const upperLimit = (rowDays) => (num(admin.P13) * rowDays) / days;
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, taxData.corporation_tax).corporationTax;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diy-accounting-uk/diya-gl",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "type": "module",
5
5
  "description": "Recalculate, read and write DIYA-GL books from the command line",
6
6
  "license": "Apache-2.0",