@diy-accounting-uk/diya-gl 1.2.14 → 1.2.16

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.
@@ -251,10 +251,14 @@
251
251
  "label": "Capital allowances at 6% on equipment, including cars with higher CO2 emissions",
252
252
  "cell": "Financialaccounts.xlsx!SE Full!D147"
253
253
  },
254
- { "box": "52", "label": "Zero-emission goods vehicle allowance", "cell": "Financialaccounts.xlsx!SE Full!D152" },
255
- { "box": "52.1", "label": "Zero-emission car allowance", "cell": "Financialaccounts.xlsx!SE Full!D156" },
256
- { "box": "53", "label": "The Structures and Buildings Allowance", "cell": "Financialaccounts.xlsx!SE Full!D160" },
257
- { "box": "53.1", "label": "Freeport and Investment Zones Structures and Buildings Allowance", "cell": null },
254
+ { "box": "52", "label": "Zero-emission goods vehicle allowance", "cell": "Financialaccounts.xlsx!SE Full!D150" },
255
+ { "box": "52.1", "label": "Zero-emission car allowance", "cell": "Financialaccounts.xlsx!SE Full!D152" },
256
+ { "box": "53", "label": "The Structures and Buildings Allowance", "cell": "Financialaccounts.xlsx!SE Full!D156" },
257
+ {
258
+ "box": "53.1",
259
+ "label": "Freeport and Investment Zones Structures and Buildings Allowance",
260
+ "cell": "Financialaccounts.xlsx!SE Full!D160"
261
+ },
258
262
  { "box": "54", "label": "Electric charge-point allowance", "cell": "Financialaccounts.xlsx!SE Full!O139" },
259
263
  { "box": "55", "label": "100% and other enhanced capital allowances", "cell": "Financialaccounts.xlsx!SE Full!O144" },
260
264
  { "box": "56", "label": "Allowances on sale or cessation of business use", "cell": "Financialaccounts.xlsx!SE Full!O149" },
@@ -452,7 +452,7 @@
452
452
  "form": "SA103F",
453
453
  "box": "52",
454
454
  "label": "Zero-emission goods vehicle allowance",
455
- "cell": "SE Full!D152",
455
+ "cell": "SE Full!D150",
456
456
  "field": "allowances.zeroEmissionsGoodsVehicleAllowance",
457
457
  "route": "annual"
458
458
  },
@@ -460,7 +460,7 @@
460
460
  "form": "SA103F",
461
461
  "box": "52.1",
462
462
  "label": "Zero-emission car allowance",
463
- "cell": "SE Full!D156",
463
+ "cell": "SE Full!D152",
464
464
  "field": "allowances.zeroEmissionsCarAllowance",
465
465
  "route": "annual"
466
466
  },
@@ -468,7 +468,7 @@
468
468
  "form": "SA103F",
469
469
  "box": "53",
470
470
  "label": "The Structures and Buildings Allowance",
471
- "cell": "SE Full!D160",
471
+ "cell": "SE Full!D156",
472
472
  "field": "allowances.structuredBuildingAllowance",
473
473
  "route": "annual"
474
474
  },
@@ -476,7 +476,7 @@
476
476
  "form": "SA103F",
477
477
  "box": "53.1",
478
478
  "label": "Freeport Structures and Buildings Allowance",
479
- "cell": null,
479
+ "cell": "SE Full!D160",
480
480
  "field": "allowances.enhancedStructuredBuildingAllowance",
481
481
  "route": "annual"
482
482
  },
@@ -597,6 +597,58 @@
597
597
  "scorecard": "101605 passed, 0 warnings, 0 failed"
598
598
  }
599
599
  }
600
+ },
601
+ {
602
+ "tag": "diya-gl-v1.2.14",
603
+ "date": "2026-09-14",
604
+ "formatVersion": "diya-gl/1",
605
+ "engineVersion": "1.2.14+959e4a1d",
606
+ "taxDataHash": "4ecdbe985d22",
607
+ "reconciledCommit": "c910fdeefbe4f2f71ae9ec5f5d4a713783a1c0c7",
608
+ "templates": {
609
+ "bst": {
610
+ "hash": "0d003e10e00c",
611
+ "scorecard": "812 passed, 0 warnings, 0 failed"
612
+ },
613
+ "taxi": {
614
+ "hash": "1ff69654d64d",
615
+ "scorecard": "686 passed, 0 warnings, 0 failed"
616
+ },
617
+ "se": {
618
+ "hash": "94754ec2c44b",
619
+ "scorecard": "7962 passed, 0 warnings, 0 failed"
620
+ },
621
+ "ltd": {
622
+ "hash": "caa14632083b",
623
+ "scorecard": "101605 passed, 0 warnings, 0 failed"
624
+ }
625
+ }
626
+ },
627
+ {
628
+ "tag": "diya-gl-v1.2.15",
629
+ "date": "2026-09-15",
630
+ "formatVersion": "diya-gl/1",
631
+ "engineVersion": "1.2.15+27b706de",
632
+ "taxDataHash": "4ecdbe985d22",
633
+ "reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
634
+ "templates": {
635
+ "bst": {
636
+ "hash": "0d003e10e00c",
637
+ "scorecard": "812 passed, 0 warnings, 0 failed"
638
+ },
639
+ "taxi": {
640
+ "hash": "1ff69654d64d",
641
+ "scorecard": "686 passed, 0 warnings, 0 failed"
642
+ },
643
+ "se": {
644
+ "hash": "3c88a8ba129b",
645
+ "scorecard": "7966 passed, 0 warnings, 0 failed"
646
+ },
647
+ "ltd": {
648
+ "hash": "caa14632083b",
649
+ "scorecard": "101605 passed, 0 warnings, 0 failed"
650
+ }
651
+ }
600
652
  }
601
653
  ]
602
654
  }
@@ -339,6 +339,7 @@
339
339
  "section/accounting-profit-to-tax-profit-bridge/less-small-balance-allowance-box-24": "The page has no profit-bridge view; the bridge from accounting profit to tax profit is not printed anywhere on the books page.",
340
340
  "section/accounting-profit-to-tax-profit-bridge/less-the-full-return-s-own-box-62-adjustment": "The page has no profit-bridge view; the bridge from accounting profit to tax profit is not printed anywhere on the books page.",
341
341
  "section/accounting-profit-to-tax-profit-bridge/less-the-full-return-s-own-electric-charge-point-allowance-box-54": "The page has no profit-bridge view; the bridge from accounting profit to tax profit is not printed anywhere on the books page.",
342
+ "section/accounting-profit-to-tax-profit-bridge/less-the-full-return-s-own-freeport-and-investment-zone-structures-and-buildings-allowance-box-53-1": "The page has no profit-bridge view; the bridge from accounting profit to tax profit is not printed anywhere on the books page.",
342
343
  "section/accounting-profit-to-tax-profit-bridge/less-the-full-return-s-own-structures-and-buildings-allowance-box-53": "The page has no profit-bridge view; the bridge from accounting profit to tax profit is not printed anywhere on the books page.",
343
344
  "section/accounting-profit-to-tax-profit-bridge/less-the-full-return-s-own-zero-emission-car-allowance-box-52-1": "The page has no profit-bridge view; the bridge from accounting profit to tax profit is not printed anywhere on the books page.",
344
345
  "section/accounting-profit-to-tax-profit-bridge/less-the-full-return-s-own-zero-emission-goods-vehicle-allowance-box-52": "The page has no profit-bridge view; the bridge from accounting profit to tax profit is not printed anywhere on the books page.",
@@ -466,5 +467,20 @@
466
467
  "cell/Fixedassets.xlsx!Schedule!AD40": "The single asset pool car's own Schedule row (the third motor asset brought forward): its private use share, its P marker, its allowance and its written-down value. The Fixed assets view lists the brought-forward register by asset, without a cell key a row, and the AE1, AF1 and AG1 totals those rows sum into are on the same view under their own cell keys.",
467
468
  "cell/Fixedassets.xlsx!Schedule!AE40": "The single asset pool car's own Schedule row (the third motor asset brought forward): its private use share, its P marker, its allowance and its written-down value. The Fixed assets view lists the brought-forward register by asset, without a cell key a row, and the AE1, AF1 and AG1 totals those rows sum into are on the same view under their own cell keys.",
468
469
  "cell/Fixedassets.xlsx!Schedule!AG40": "The single asset pool car's own Schedule row (the third motor asset brought forward): its private use share, its P marker, its allowance and its written-down value. The Fixed assets view lists the brought-forward register by asset, without a cell key a row, and the AE1, AF1 and AG1 totals those rows sum into are on the same view under their own cell keys.",
469
- "section/accounting-profit-to-tax-profit-bridge/add-the-full-return-s-own-box-71-adjustment-for-change-of-accounting-practice": "The page has no profit-bridge view; the bridge from accounting profit to tax profit is not printed anywhere on the books page."
470
+ "section/accounting-profit-to-tax-profit-bridge/add-the-full-return-s-own-box-71-adjustment-for-change-of-accounting-practice": "The page has no profit-bridge view; the bridge from accounting profit to tax profit is not printed anywhere on the books page.",
471
+ "cell/Fixedassets.xlsx!Schedule!B115": "The fixture's own Structures and Buildings Allowance claim row (standard rate): its date, building, expenditure and postcode. The Fixed assets view has no per-claim table, and the K120 total those rows sum into is on the same view under its own cell key.",
472
+ "cell/Fixedassets.xlsx!Schedule!C115": "The fixture's own Structures and Buildings Allowance claim row (standard rate): its date, building, expenditure and postcode. The Fixed assets view has no per-claim table, and the K120 total those rows sum into is on the same view under its own cell key.",
473
+ "cell/Fixedassets.xlsx!Schedule!E115": "The fixture's own Structures and Buildings Allowance claim row (standard rate): its date, building, expenditure and postcode. The Fixed assets view has no per-claim table, and the K120 total those rows sum into is on the same view under its own cell key.",
474
+ "cell/Fixedassets.xlsx!Schedule!F115": "The fixture's own Structures and Buildings Allowance claim row (standard rate): its date, building, expenditure and postcode. The Fixed assets view has no per-claim table, and the K120 total those rows sum into is on the same view under its own cell key.",
475
+ "cell/Fixedassets.xlsx!Schedule!H115": "The fixture's own Structures and Buildings Allowance claim row (standard rate): its rate, its days claimed and its allowance. The Fixed assets view has no per-claim table, and the K120 total those rows sum into is on the same view under its own cell key.",
476
+ "cell/Fixedassets.xlsx!Schedule!J115": "The fixture's own Structures and Buildings Allowance claim row (standard rate): its rate, its days claimed and its allowance. The Fixed assets view has no per-claim table, and the K120 total those rows sum into is on the same view under its own cell key.",
477
+ "cell/Fixedassets.xlsx!Schedule!K115": "The fixture's own Structures and Buildings Allowance claim row (standard rate): its rate, its days claimed and its allowance. The Fixed assets view has no per-claim table, and the K120 total those rows sum into is on the same view under its own cell key.",
478
+ "cell/Fixedassets.xlsx!Schedule!B116": "The fixture's own Structures and Buildings Allowance claim row (the Freeport claim): its date, building, expenditure, postcode and tax site marker. The Fixed assets view has no per-claim table, and the K121 total those rows sum into is on the same view under its own cell key.",
479
+ "cell/Fixedassets.xlsx!Schedule!D116": "The fixture's own Structures and Buildings Allowance claim row (the Freeport claim): its date, building, expenditure, postcode and tax site marker. The Fixed assets view has no per-claim table, and the K121 total those rows sum into is on the same view under its own cell key.",
480
+ "cell/Fixedassets.xlsx!Schedule!E116": "The fixture's own Structures and Buildings Allowance claim row (the Freeport claim): its date, building, expenditure, postcode and tax site marker. The Fixed assets view has no per-claim table, and the K121 total those rows sum into is on the same view under its own cell key.",
481
+ "cell/Fixedassets.xlsx!Schedule!F116": "The fixture's own Structures and Buildings Allowance claim row (the Freeport claim): its date, building, expenditure, postcode and tax site marker. The Fixed assets view has no per-claim table, and the K121 total those rows sum into is on the same view under its own cell key.",
482
+ "cell/Fixedassets.xlsx!Schedule!G116": "The fixture's own Structures and Buildings Allowance claim row (the Freeport claim): its date, building, expenditure, postcode and tax site marker. The Fixed assets view has no per-claim table, and the K121 total those rows sum into is on the same view under its own cell key.",
483
+ "cell/Fixedassets.xlsx!Schedule!H116": "The fixture's own Structures and Buildings Allowance claim row (the Freeport claim): its rate, its days claimed and its allowance. The Fixed assets view has no per-claim table, and the K121 total those rows sum into is on the same view under its own cell key.",
484
+ "cell/Fixedassets.xlsx!Schedule!J116": "The fixture's own Structures and Buildings Allowance claim row (the Freeport claim): its rate, its days claimed and its allowance. The Fixed assets view has no per-claim table, and the K121 total those rows sum into is on the same view under its own cell key.",
485
+ "cell/Fixedassets.xlsx!Schedule!K116": "The fixture's own Structures and Buildings Allowance claim row (the Freeport claim): its rate, its days claimed and its allowance. The Fixed assets view has no per-claim table, and the K121 total those rows sum into is on the same view under its own cell key."
470
486
  }
@@ -36,6 +36,8 @@ class2_small_profits_threshold = 6475
36
36
  annual_investment_allowance = 1.00
37
37
  writing_down_allowance = 0.18
38
38
  writing_down_allowance_special = 0.06
39
+ structures_and_buildings_allowance = 0.03
40
+ structures_and_buildings_allowance_enhanced = 0.10
39
41
 
40
42
  [depreciation]
41
43
  land_and_property = 0.00
@@ -36,6 +36,8 @@ class2_small_profits_threshold = 6515
36
36
  annual_investment_allowance = 1.00
37
37
  writing_down_allowance = 0.18
38
38
  writing_down_allowance_special = 0.06
39
+ structures_and_buildings_allowance = 0.03
40
+ structures_and_buildings_allowance_enhanced = 0.10
39
41
 
40
42
  [depreciation]
41
43
  land_and_property = 0.00
@@ -39,6 +39,8 @@ class2_small_profits_threshold = 6725
39
39
  annual_investment_allowance = 1.00
40
40
  writing_down_allowance = 0.18
41
41
  writing_down_allowance_special = 0.06
42
+ structures_and_buildings_allowance = 0.03
43
+ structures_and_buildings_allowance_enhanced = 0.10
42
44
 
43
45
  [depreciation]
44
46
  land_and_property = 0.00
@@ -38,6 +38,8 @@ class2_small_profits_threshold = 6725
38
38
  annual_investment_allowance = 1.00
39
39
  writing_down_allowance = 0.18
40
40
  writing_down_allowance_special = 0.06
41
+ structures_and_buildings_allowance = 0.03
42
+ structures_and_buildings_allowance_enhanced = 0.10
41
43
 
42
44
  [depreciation]
43
45
  land_and_property = 0.00
@@ -35,6 +35,8 @@ class2_small_profits_threshold = 6725
35
35
  annual_investment_allowance = 1.00
36
36
  writing_down_allowance = 0.18
37
37
  writing_down_allowance_special = 0.06
38
+ structures_and_buildings_allowance = 0.03
39
+ structures_and_buildings_allowance_enhanced = 0.10
38
40
 
39
41
  [depreciation]
40
42
  land_and_property = 0.00
@@ -35,6 +35,8 @@ class2_small_profits_threshold = 6845
35
35
  annual_investment_allowance = 1.00
36
36
  writing_down_allowance = 0.18
37
37
  writing_down_allowance_special = 0.06
38
+ structures_and_buildings_allowance = 0.03
39
+ structures_and_buildings_allowance_enhanced = 0.10
38
40
 
39
41
  [depreciation]
40
42
  land_and_property = 0.00
@@ -35,6 +35,8 @@ class2_small_profits_threshold = 7105
35
35
  annual_investment_allowance = 1.00
36
36
  writing_down_allowance = 0.14
37
37
  writing_down_allowance_special = 0.06
38
+ structures_and_buildings_allowance = 0.03
39
+ structures_and_buildings_allowance_enhanced = 0.10
38
40
 
39
41
  [depreciation]
40
42
  land_and_property = 0.00
@@ -41,6 +41,8 @@ import {
41
41
  EXISTING_ASSET_ROWS,
42
42
  NEW_PLANT_ROWS,
43
43
  HP_AGREEMENT_ROWS,
44
+ SBA_CLAIM_ROWS,
45
+ SBA_CLAIM_COLUMNS,
44
46
  } from "../../products/se.js";
45
47
  import { SALES_ANALYSIS_COLUMNS, PURCHASES_ANALYSIS_COLUMNS } from "../calculators/se.js";
46
48
  import {
@@ -230,6 +232,7 @@ export const SE_ANCHORS = {
230
232
  { sheet: "Schedule", cell: "C1", label: "FIXED ASSETS" },
231
233
  { sheet: "Schedule", cell: "B59", label: "NEW FIXED ASSETS Bought AFTER" }, // template carries a trailing space; textAt() trims it
232
234
  { sheet: "Schedule", cell: "B64", label: "New Land & Property" },
235
+ { sheet: "Schedule", cell: "B113", label: "STRUCTURES AND BUILDINGS ALLOWANCE (SA103F boxes 53 and 53.1)" },
233
236
  { sheet: "HPfinance", cell: "B5", label: "Agreement Date" },
234
237
  { sheet: "HPfinance", cell: "C5", label: "Finance Company" },
235
238
  { sheet: "HPfinance", cell: "E5", label: "Total Amount Financed excluding Admin & Interest" },
@@ -361,7 +364,9 @@ function isPayslipsInputCell(sheet, cellRef) {
361
364
  // its private use share (M), its special rate pool marker (AB) and its single
362
365
  // asset pool marker (AD) on its existing-asset row, or an
363
366
  // in-year disposal's date/proceeds (U, V) on that same row; a new purchase's
364
- // date/supplier/cost (B, C, E) on a New Plant & Machinery row. See
367
+ // date/supplier/cost (B, C, E) on a New Plant & Machinery row; a Structures
368
+ // and Buildings Allowance claim's date/building/expenditure/postcode/tax
369
+ // site/ceased date (B, C, D, E, F, G, I) on an SBA claim row. See
365
370
  // app/products/se.js's cellWrites() Schedule writer.
366
371
  const EXISTING_SCHEDULE_ROWS = new Set([...EXISTING_ASSET_ROWS.motor, ...EXISTING_ASSET_ROWS.computer]);
367
372
  const NEW_SCHEDULE_ROWS = new Set(NEW_PLANT_ROWS);
@@ -377,6 +382,8 @@ const EXISTING_SCHEDULE_COLUMNS = [
377
382
  SINGLE_ASSET_POOL_MARKER_COLUMN,
378
383
  ];
379
384
  const NEW_SCHEDULE_COLUMNS = ["B", "C", "E"];
385
+ const SBA_SCHEDULE_ROWS = new Set(SBA_CLAIM_ROWS);
386
+ const SBA_SCHEDULE_COLUMNS = Object.values(SBA_CLAIM_COLUMNS);
380
387
  const HP_ROWS = new Set(HP_AGREEMENT_ROWS);
381
388
  const HP_COLUMNS = ["B", "C", "D", "E", "F", "G", "H", "L"];
382
389
 
@@ -386,6 +393,7 @@ function isFixedAssetsInputCell(sheet, cellRef) {
386
393
  if (sheet === "Schedule") {
387
394
  if (EXISTING_SCHEDULE_ROWS.has(ref.row)) return EXISTING_SCHEDULE_COLUMNS.includes(ref.col);
388
395
  if (NEW_SCHEDULE_ROWS.has(ref.row)) return NEW_SCHEDULE_COLUMNS.includes(ref.col);
396
+ if (SBA_SCHEDULE_ROWS.has(ref.row)) return SBA_SCHEDULE_COLUMNS.includes(ref.col);
389
397
  return false;
390
398
  }
391
399
  if (sheet === "HPfinance") return HP_ROWS.has(ref.row) && HP_COLUMNS.includes(ref.col);
@@ -17,7 +17,7 @@ import { resolve, dirname } from "path";
17
17
  import { fileURLToPath } from "url";
18
18
  import { diyaGlToScenario } from "../diya-gl-loader.js";
19
19
  import { calculateSeCells } from "./se.js";
20
- import { SE_YEAR_END_MONTH } from "../../products/se.js";
20
+ import { SE_YEAR_END_MONTH, SBA_CLAIM_ROWS } from "../../products/se.js";
21
21
  import { extractTaxYearStart, parseDate } from "../scenario-loader.js";
22
22
  import { shiftMonths, periodShiftMonths } from "../period-shift.js";
23
23
  import { splitVat } from "../tax/vat.js";
@@ -465,9 +465,8 @@ export function buildSelfEmploymentQuarterlyUpdates(book, lines, taxData, option
465
465
  // reads it back from there, so the payload carries what the return prints.
466
466
  // A box the book does not state stays blank and warns.
467
467
  const BOOK_STATED_ANNUAL_BOXES = [
468
- { box: "52", cell: "D152", table: "allowances" }, // zeroEmissionsGoodsVehicleAllowance -- gone from 2025-26
469
- { box: "52.1", cell: "D156", table: "allowances" }, // zeroEmissionsCarAllowance
470
- { box: "53", cell: "D160", table: "allowances" }, // structuredBuildingAllowance
468
+ { box: "52", cell: "D150", table: "allowances" }, // zeroEmissionsGoodsVehicleAllowance -- gone from 2025-26
469
+ { box: "52.1", cell: "D152", table: "allowances" }, // zeroEmissionsCarAllowance
471
470
  { box: "62", cell: "D179", table: "adjustments" }, // includedNonTaxableProfits
472
471
  { box: "71", cell: "D210", table: "adjustments" }, // accountingAdjustment
473
472
  ];
@@ -477,9 +476,10 @@ const BOOK_STATED_ANNUAL_BOXES = [
477
476
  // out of reach today: no cell in the template, or a figure the book has no
478
477
  // record for. Box 69's field also moves out of HMRC's own schema by tax
479
478
  // year -- annualFieldsUnavailableForYear() below is what keeps a year that
480
- // no longer accepts a field from warning about it as if it still did.
479
+ // no longer accepts a field from warning about it as if it still did. Boxes
480
+ // 53 and 53.1 are not here: the Schedule's SBA block computes them, and
481
+ // they are filed as arrays below, one item per claim.
481
482
  const NO_SOURCE_ANNUAL_BOXES = [
482
- { box: "53.1", pick: 0 }, // allowances.enhancedStructuredBuildingAllowance
483
483
  { box: "55", pick: 1 }, // allowances.businessPremisesRenovationAllowance (shared with enhancedCapitalAllowance)
484
484
  { box: "59", pick: 1 }, // adjustments.balancingChargeBpra (shared with balancingChargeOther)
485
485
  { box: "68", pick: 0 }, // adjustments.basisAdjustment
@@ -624,6 +624,44 @@ export function buildSelfEmploymentAnnualSubmission(book, lines, taxData, option
624
624
  });
625
625
  }
626
626
 
627
+ // Structures and Buildings Allowance (boxes 53 and 53.1): the Schedule's
628
+ // SBA block computes each claim's year allowance from the book's own
629
+ // inputs, so the claim files as one item per row, not a stated scalar. A
630
+ // claim whose allowance is nil this year (ceased before the period, or
631
+ // past the 33 1/3 year limit) carries nothing to file. firstYear is the
632
+ // API's first-year details, filed once, on the claim's own qualifying
633
+ // date -- a claim already running before this package's period does not
634
+ // repeat them.
635
+ const admin = rawResults["Admin"] || {};
636
+ const structuredBuildingAllowance = [];
637
+ const enhancedStructuredBuildingAllowance = [];
638
+ SBA_CLAIM_ROWS.forEach((row, index) => {
639
+ const claim = (scenario.sba_claims || [])[index];
640
+ if (!claim) return;
641
+ const amount = schedule?.[`K${row}`];
642
+ if (typeof amount !== "number" || amount === 0) return;
643
+ const item = { amount: round2(amount), building: { ...claim.building } };
644
+ const qualifyingSerial = schedule[`B${row}`];
645
+ if (typeof qualifyingSerial === "number" && typeof admin.B4 === "number" && qualifyingSerial >= admin.B4) {
646
+ // A book's own TOML date parses as a Date instance; the API payload
647
+ // states a date the same way every time, regardless of how the
648
+ // caller's scenario happened to carry it.
649
+ item.firstYear = {
650
+ qualifyingDate: parseDate(claim.qualifyingDate).toISOString().slice(0, 10),
651
+ qualifyingAmountExpenditure: claim.qualifyingAmountExpenditure,
652
+ };
653
+ }
654
+ (claim.enhanced ? enhancedStructuredBuildingAllowance : structuredBuildingAllowance).push(item);
655
+ });
656
+ const structuredBuildingField = primaryField(boxes, "53");
657
+ const enhancedStructuredBuildingField = primaryField(boxes, "53.1");
658
+ if (structuredBuildingAllowance.length > 0 && !unavailableFields.has(structuredBuildingField)) {
659
+ setPath(root, structuredBuildingField, structuredBuildingAllowance);
660
+ }
661
+ if (enhancedStructuredBuildingAllowance.length > 0 && !unavailableFields.has(enhancedStructuredBuildingField)) {
662
+ setPath(root, enhancedStructuredBuildingField, enhancedStructuredBuildingAllowance);
663
+ }
664
+
627
665
  const allowances = root.allowances || {};
628
666
  const adjustments = root.adjustments || {};
629
667
 
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { MONTH_SHEETS, extractTaxYearStart } from "../scenario-loader.js";
18
18
  import { shiftMonths, periodShiftMonths } from "../period-shift.js";
19
- import { standardReads, multiFileOptions, SE_YEAR_END_MONTH } from "../../products/se.js";
19
+ import { standardReads, multiFileOptions, SE_YEAR_END_MONTH, SBA_CLAIM_ROWS } from "../../products/se.js";
20
20
  import { generateAdminDates, seVatPaymentDueDate, generatePayslipsCalendar } from "../generator.js";
21
21
  import { calculateIncomeTax } from "../tax/income-tax.js";
22
22
  import { calculateMileageAllowance } from "../tax/mileage.js";
@@ -491,6 +491,10 @@ function scheduleRow({
491
491
  row.AE = singleAssetPool && !specialPool ? sheetNumber(row.R) : SHEET_BLANK;
492
492
  row.AF = singleAssetPool && specialPool ? sheetNumber(row.AC) : SHEET_BLANK;
493
493
  row.AG = singleAssetPool ? sheetNumber(row.S) : SHEET_BLANK;
494
+ // AH and AI split the pooled written-down value by rate pool, single asset
495
+ // pool rows left out, so the small pools test can run on each pool alone.
496
+ row.AH = !singleAssetPool && !specialPool ? sheetNumber(row.S) : SHEET_BLANK;
497
+ row.AI = !singleAssetPool && specialPool ? sheetNumber(row.S) : SHEET_BLANK;
494
498
 
495
499
  row.V = disposal ? disposal.proceeds : SHEET_BLANK;
496
500
  row.W = disposal ? cost : SHEET_BLANK;
@@ -521,7 +525,29 @@ function carry(inputs, compute) {
521
525
  return inputs.some((value) => value === SHEET_ERROR) ? SHEET_ERROR : compute();
522
526
  }
523
527
 
524
- const SCHEDULE_TOTAL_COLUMNS = ["E", "F", "G", "I", "J", "K", "O", "Q", "R", "S", "V", "W", "X", "Y", "Z", "AC", "AE", "AF", "AG"];
528
+ const SCHEDULE_TOTAL_COLUMNS = [
529
+ "E",
530
+ "F",
531
+ "G",
532
+ "I",
533
+ "J",
534
+ "K",
535
+ "O",
536
+ "Q",
537
+ "R",
538
+ "S",
539
+ "V",
540
+ "W",
541
+ "X",
542
+ "Y",
543
+ "Z",
544
+ "AC",
545
+ "AE",
546
+ "AF",
547
+ "AG",
548
+ "AH",
549
+ "AI",
550
+ ];
525
551
 
526
552
  function scheduleTotals(rows) {
527
553
  const totals = {};
@@ -606,6 +632,62 @@ function buildSchedule(scenario, taxData, rate) {
606
632
  return { existing, additions, totals: addScheduleTotals(existing, additions), motorRowCells };
607
633
  }
608
634
 
635
+ // ── Structures and Buildings Allowance (SA103F boxes 53 and 53.1) ──────────
636
+ //
637
+ // Fixedassets.xlsx!Schedule rows 115 to 119, below the fixed asset register.
638
+ // The book states each claim's inputs; the sheet computes the year's
639
+ // allowance from the year file's rate over the days of the chargeable
640
+ // period the claim covers. Verified against the template:
641
+ // H115 = IF(E115>0,IF(G115="F",[1]Admin!$G$10,[1]Admin!$G$9)," ")
642
+ // J115 = IF(E115>0,MAX(0,MIN($S$4,IF(N(I115)>0,I115,$S$4),
643
+ // EDATE(B115,IF(G115="F",120,400))-1)-MAX($D$6,B115)+1)," ")
644
+ // K115 = IF(E115>0,E115*H115*J115/($S$4-$D$6+1)," ")
645
+ // $D$6 = [1]Admin!$B$4 (period start) and $S$4 = [1]Admin!$B$17 (period
646
+ // end); 400 and 120 months are the 33 1/3 and 10 year limits. K120 =
647
+ // SUMIF(G115:G119,"<>F",K115:K119) (box 53), K121 the same with "F"
648
+ // (box 53.1). A claim's dates shift by the same whole-year offset as every
649
+ // other scenario date, so every package year sees the same day counts.
650
+ function buildStructuresAndBuildings(scenario, admin, monthOffset) {
651
+ const periodStart = admin.B4;
652
+ const periodEnd = admin.B17;
653
+ const claims = scenario.sba_claims || [];
654
+ const cells = {};
655
+ let standardTotal = 0;
656
+ let enhancedTotal = 0;
657
+
658
+ SBA_CLAIM_ROWS.forEach((row, index) => {
659
+ const claim = claims[index];
660
+ if (!claim) return;
661
+ const enhanced = claim.enhanced === true;
662
+ const qualifyingDate = excelSerial(shiftMonths(new Date(claim.qualifyingDate), monthOffset));
663
+ cells[`B${row}`] = qualifyingDate;
664
+ if (claim.building?.name) cells[`C${row}`] = claim.building.name;
665
+ if (claim.building?.number) cells[`D${row}`] = claim.building.number;
666
+ cells[`E${row}`] = claim.qualifyingAmountExpenditure;
667
+ cells[`F${row}`] = claim.building.postcode;
668
+ if (enhanced) cells[`G${row}`] = "F";
669
+ let ceasedDate;
670
+ if (claim.ceasedDate) {
671
+ ceasedDate = excelSerial(shiftMonths(new Date(claim.ceasedDate), monthOffset));
672
+ cells[`I${row}`] = ceasedDate;
673
+ }
674
+ const rate = enhanced ? admin.G10 : admin.G9;
675
+ cells[`H${row}`] = rate;
676
+ const limitMonths = enhanced ? 120 : 400;
677
+ const limitEndSerial = excelSerial(shiftMonths(dateFromExcelSerial(qualifyingDate), limitMonths)) - 1;
678
+ const upperBound = Math.min(periodEnd, ceasedDate !== undefined ? ceasedDate : periodEnd, limitEndSerial);
679
+ const lowerBound = Math.max(periodStart, qualifyingDate);
680
+ const days = Math.max(0, upperBound - lowerBound + 1);
681
+ cells[`J${row}`] = days;
682
+ const allowance = (claim.qualifyingAmountExpenditure * rate * days) / (periodEnd - periodStart + 1);
683
+ cells[`K${row}`] = allowance;
684
+ if (enhanced) enhancedTotal += allowance;
685
+ else standardTotal += allowance;
686
+ });
687
+
688
+ return { cells, K120: standardTotal, K121: enhancedTotal };
689
+ }
690
+
609
691
  // ── Hire purchase ──────────────────────────────────────────────────────────
610
692
 
611
693
  function buildHpFinance(scenario) {
@@ -724,6 +806,8 @@ function buildAdmin(taxData, dateSerials) {
724
806
  cells.G4 = ca.annual_investment_allowance;
725
807
  cells.G5 = ca.writing_down_allowance;
726
808
  cells.G6 = ca.writing_down_allowance_special;
809
+ cells.G9 = ca.structures_and_buildings_allowance;
810
+ cells.G10 = ca.structures_and_buildings_allowance_enhanced;
727
811
  cells.G13 = dep.land_and_property;
728
812
  cells.G14 = dep.plant_and_machinery;
729
813
  cells.G15 = dep.fixtures_and_fittings;
@@ -798,6 +882,7 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
798
882
  const payroll = payrollMonths(scenario.payroll);
799
883
  const schedule = buildSchedule(scenario, taxData, rate);
800
884
  const scheduleNumber = (column) => sheetNumber(schedule.totals[column]);
885
+ const sba = buildStructuresAndBuildings(scenario, admin, monthOffset);
801
886
 
802
887
  // The twelve monthly stock cells telescope, so whatever is counted between
803
888
  // them the year's movement is the difference between its two ends. A count at
@@ -994,7 +1079,15 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
994
1079
  const boxes32to45Total = sheetSum(Object.values(seFullDisallowable)) + pl.B34;
995
1080
 
996
1081
  // ── The fixed asset workbook ──
997
- const scheduleCells = { E57: schedule.existing.E, E110: schedule.additions.E, AC4: admin.G6, ...schedule.motorRowCells };
1082
+ const scheduleCells = {
1083
+ E57: schedule.existing.E,
1084
+ E110: schedule.additions.E,
1085
+ AC4: admin.G6,
1086
+ ...schedule.motorRowCells,
1087
+ ...sba.cells,
1088
+ K120: sba.K120,
1089
+ K121: sba.K121,
1090
+ };
998
1091
  for (const column of SCHEDULE_TOTAL_COLUMNS) scheduleCells[`${column}1`] = schedule.totals[column];
999
1092
  const faReconciliation = {
1000
1093
  E11: schedule.additions.E,
@@ -1057,16 +1150,20 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
1057
1150
  const scheduleQ = schedule.totals.Q;
1058
1151
  const scheduleR = schedule.totals.R;
1059
1152
  const scheduleAC = schedule.totals.AC;
1060
- const scheduleS = schedule.totals.S;
1061
- // The small pools write-off runs over the pooled balance alone: the
1062
- // single asset pool rows (AE, AF, AG) come out of both the £1,000 test
1063
- // and the figure written off. The sheet reads
1064
- // IF((R1+S1-AE1-AF1-AG1)<1000,S1-AG1,0) on SE Full!O144 and SE Short!D85.
1153
+ // The small pools write-off runs per rate pool over the pooled balance
1154
+ // before this year's allowance, the single asset pool rows (AE, AF) left
1155
+ // out: the main pool's is R1-AE1+AH1 and the special rate pool's
1156
+ // AC1-AF1+AI1, and each pool under £1,000 writes off its own written-down
1157
+ // value. The sheet reads
1158
+ // IF((R1-AE1+AH1)<1000,AH1,0)+IF((AC1-AF1+AI1)<1000,AI1,0) on
1159
+ // SE Full!O144 and SE Short!D85.
1065
1160
  const scheduleAE = schedule.totals.AE;
1066
1161
  const scheduleAF = schedule.totals.AF;
1067
- const scheduleAG = schedule.totals.AG;
1068
- const smallPoolsWriteOff = carry([scheduleR, scheduleS, scheduleAE, scheduleAF, scheduleAG], () =>
1069
- scheduleR + scheduleS - scheduleAE - scheduleAF - scheduleAG < 1000 ? scheduleS - scheduleAG : 0,
1162
+ const scheduleAH = schedule.totals.AH;
1163
+ const scheduleAI = schedule.totals.AI;
1164
+ const smallPoolsWriteOff = carry(
1165
+ [scheduleR, scheduleAC, scheduleAE, scheduleAF, scheduleAH, scheduleAI],
1166
+ () => (scheduleR - scheduleAE + scheduleAH < 1000 ? scheduleAH : 0) + (scheduleAC - scheduleAF + scheduleAI < 1000 ? scheduleAI : 0),
1070
1167
  );
1071
1168
  const scheduleY = schedule.totals.Y;
1072
1169
  const scheduleZ = schedule.totals.Z;
@@ -1191,18 +1288,31 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
1191
1288
  seFull.D147 = carry([scheduleAC], () => scheduleAC);
1192
1289
  // Boxes with no formula behind them, carrying whatever figure the book
1193
1290
  // states and blank otherwise.
1194
- seFull.D152 = stated(annualAllowances.zeroEmissionsGoodsVehicleAllowance);
1195
- seFull.D156 = stated(annualAllowances.zeroEmissionsCarAllowance);
1196
- seFull.D160 = stated(annualAllowances.structuredBuildingAllowance);
1291
+ seFull.D150 = stated(annualAllowances.zeroEmissionsGoodsVehicleAllowance);
1292
+ seFull.D152 = stated(annualAllowances.zeroEmissionsCarAllowance);
1197
1293
  seFull.O139 = stated(annualAllowances.electricChargePointAllowance);
1198
1294
  seFull.D179 = stated(annualAdjustments.includedNonTaxableProfits);
1199
1295
  seFull.D210 = stated(annualAdjustments.accountingAdjustment);
1296
+ // Boxes 53 and 53.1: the Schedule's SBA block computes them, not the book.
1297
+ seFull.D156 = sba.K120;
1298
+ seFull.D160 = sba.K121;
1200
1299
  // Box 50 carries the whole writing down allowance the schedule claims.
1201
1300
  seFull.D144 = carry([scheduleR], () => scheduleR);
1202
1301
  seFull.O144 = smallPoolsWriteOff;
1203
1302
  seFull.O149 = scheduleY;
1204
1303
  seFull.O154 = carry([seFull.D139, seFull.D144, seFull.O144, seFull.O149], () =>
1205
- sheetSum([seFull.D139, seFull.D144, seFull.D147, seFull.D152, seFull.D156, seFull.D160, seFull.O139, seFull.O144, seFull.O149]),
1304
+ sheetSum([
1305
+ seFull.D139,
1306
+ seFull.D144,
1307
+ seFull.D147,
1308
+ seFull.D150,
1309
+ seFull.D152,
1310
+ seFull.D156,
1311
+ seFull.D160,
1312
+ seFull.O139,
1313
+ seFull.O144,
1314
+ seFull.O149,
1315
+ ]),
1206
1316
  );
1207
1317
  seFull.O160 = scheduleZ;
1208
1318
  seFull.D169 = goodsForOwnUse;
@@ -265,16 +265,20 @@ function renderAccounts(bookSchema, accounts, lines) {
265
265
  // A plain object table (documentInfo, entityInformation, stock, tax's own
266
266
  // sub-sections, openingBalances' assetClassAmounts): every property in
267
267
  // schema order, scalars as `key = value`, a nested object recursed as
268
- // `[header.key]`, and a patternProperties dict (only openingBalances.
269
- // bankAccounts is one) as a table of bare account-code keys.
268
+ // `[header.key]`, an array as `[[header.key]]` blocks (structuresBuildings
269
+ // AllowanceClaim, tax.selfEmployment.allowances' own claim arrays), and a
270
+ // patternProperties dict (only openingBalances.bankAccounts is one) as a
271
+ // table of bare account-code keys.
270
272
  function renderObjectTable(bookSchema, header, value, objectSchema, lines) {
271
273
  const props = objectSchema?.properties || {};
272
274
  const scalarKeys = [];
273
275
  const nestedKeys = [];
276
+ const arrayKeys = [];
274
277
  for (const key of Object.keys(props)) {
275
278
  if (value[key] === undefined) continue;
276
279
  const propSchema = resolveRef(bookSchema, props[key]);
277
- if (propSchema.type === "object") nestedKeys.push(key);
280
+ if (propSchema.type === "array") arrayKeys.push(key);
281
+ else if (propSchema.type === "object") nestedKeys.push(key);
278
282
  else scalarKeys.push(key);
279
283
  }
280
284
  if (scalarKeys.length > 0) {
@@ -292,16 +296,34 @@ function renderObjectTable(bookSchema, header, value, objectSchema, lines) {
292
296
  renderObjectTable(bookSchema, `${header}.${key}`, value[key], propSchema, lines);
293
297
  }
294
298
  }
299
+ for (const key of arrayKeys) {
300
+ const propSchema = resolveRef(bookSchema, props[key]);
301
+ renderArrayOfTables(bookSchema, `${header}.${key}`, value[key], resolveRef(bookSchema, propSchema.items), lines);
302
+ }
295
303
  }
296
304
 
297
- function renderArrayOfTables(key, items, itemSchema, lines) {
305
+ // items is an array-of-tables ([[key]] blocks); an item field whose own
306
+ // schema is an object (structuresBuildingsAllowanceClaim's building) prints
307
+ // as a [key.field] table straight after that item's own [[key]] block,
308
+ // rather than as a scalar -- the one shape "[[key]]" plus a following
309
+ // object header can express without a nesting level of its own.
310
+ function renderArrayOfTables(bookSchema, key, items, itemSchema, lines) {
298
311
  for (const item of sortById(items, itemSchema)) {
299
312
  lines.push(`[[${key}]]`);
313
+ const nestedFields = [];
300
314
  for (const field of Object.keys(itemSchema.properties)) {
301
315
  if (item[field] === undefined) continue;
302
- lines.push(`${tomlKey(field)} = ${tomlScalar(item[field], itemSchema.properties[field])}`);
316
+ const fieldSchema = resolveRef(bookSchema, itemSchema.properties[field]);
317
+ if (fieldSchema.type === "object") {
318
+ nestedFields.push(field);
319
+ continue;
320
+ }
321
+ lines.push(`${tomlKey(field)} = ${tomlScalar(item[field], fieldSchema)}`);
303
322
  }
304
323
  lines.push("");
324
+ for (const field of nestedFields) {
325
+ renderObjectTable(bookSchema, `${key}.${field}`, item[field], resolveRef(bookSchema, itemSchema.properties[field]), lines);
326
+ }
305
327
  }
306
328
  }
307
329
 
@@ -326,7 +348,7 @@ export function canonicalBookToml(book) {
326
348
  }
327
349
  const propSchema = resolveRef(bookSchema, bookSchema.properties[key]);
328
350
  if (propSchema.type === "array") {
329
- renderArrayOfTables(key, book[key], resolveRef(bookSchema, propSchema.items), lines);
351
+ renderArrayOfTables(bookSchema, key, book[key], resolveRef(bookSchema, propSchema.items), lines);
330
352
  } else {
331
353
  renderObjectTable(bookSchema, key, book[key], propSchema, lines);
332
354
  }