@diy-accounting-uk/diya-gl 1.2.15 → 1.2.17

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
  },
@@ -623,6 +623,58 @@
623
623
  "scorecard": "101605 passed, 0 warnings, 0 failed"
624
624
  }
625
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
+ }
652
+ },
653
+ {
654
+ "tag": "diya-gl-v1.2.16",
655
+ "date": "2026-09-15",
656
+ "formatVersion": "diya-gl/1",
657
+ "engineVersion": "1.2.16+25abe637",
658
+ "taxDataHash": "3221a7c2f39c",
659
+ "reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
660
+ "templates": {
661
+ "bst": {
662
+ "hash": "0d003e10e00c",
663
+ "scorecard": "812 passed, 0 warnings, 0 failed"
664
+ },
665
+ "taxi": {
666
+ "hash": "1ff69654d64d",
667
+ "scorecard": "686 passed, 0 warnings, 0 failed"
668
+ },
669
+ "se": {
670
+ "hash": "107ff1b5c94a",
671
+ "scorecard": "7969 passed, 0 warnings, 0 failed"
672
+ },
673
+ "ltd": {
674
+ "hash": "caa14632083b",
675
+ "scorecard": "101605 passed, 0 warnings, 0 failed"
676
+ }
677
+ }
626
678
  }
627
679
  ]
628
680
  }
@@ -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";
@@ -632,6 +632,62 @@ function buildSchedule(scenario, taxData, rate) {
632
632
  return { existing, additions, totals: addScheduleTotals(existing, additions), motorRowCells };
633
633
  }
634
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
+
635
691
  // ── Hire purchase ──────────────────────────────────────────────────────────
636
692
 
637
693
  function buildHpFinance(scenario) {
@@ -750,6 +806,8 @@ function buildAdmin(taxData, dateSerials) {
750
806
  cells.G4 = ca.annual_investment_allowance;
751
807
  cells.G5 = ca.writing_down_allowance;
752
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;
753
811
  cells.G13 = dep.land_and_property;
754
812
  cells.G14 = dep.plant_and_machinery;
755
813
  cells.G15 = dep.fixtures_and_fittings;
@@ -824,6 +882,7 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
824
882
  const payroll = payrollMonths(scenario.payroll);
825
883
  const schedule = buildSchedule(scenario, taxData, rate);
826
884
  const scheduleNumber = (column) => sheetNumber(schedule.totals[column]);
885
+ const sba = buildStructuresAndBuildings(scenario, admin, monthOffset);
827
886
 
828
887
  // The twelve monthly stock cells telescope, so whatever is counted between
829
888
  // them the year's movement is the difference between its two ends. A count at
@@ -1020,7 +1079,15 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
1020
1079
  const boxes32to45Total = sheetSum(Object.values(seFullDisallowable)) + pl.B34;
1021
1080
 
1022
1081
  // ── The fixed asset workbook ──
1023
- 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
+ };
1024
1091
  for (const column of SCHEDULE_TOTAL_COLUMNS) scheduleCells[`${column}1`] = schedule.totals[column];
1025
1092
  const faReconciliation = {
1026
1093
  E11: schedule.additions.E,
@@ -1221,18 +1288,31 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
1221
1288
  seFull.D147 = carry([scheduleAC], () => scheduleAC);
1222
1289
  // Boxes with no formula behind them, carrying whatever figure the book
1223
1290
  // states and blank otherwise.
1224
- seFull.D152 = stated(annualAllowances.zeroEmissionsGoodsVehicleAllowance);
1225
- seFull.D156 = stated(annualAllowances.zeroEmissionsCarAllowance);
1226
- seFull.D160 = stated(annualAllowances.structuredBuildingAllowance);
1291
+ seFull.D150 = stated(annualAllowances.zeroEmissionsGoodsVehicleAllowance);
1292
+ seFull.D152 = stated(annualAllowances.zeroEmissionsCarAllowance);
1227
1293
  seFull.O139 = stated(annualAllowances.electricChargePointAllowance);
1228
1294
  seFull.D179 = stated(annualAdjustments.includedNonTaxableProfits);
1229
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;
1230
1299
  // Box 50 carries the whole writing down allowance the schedule claims.
1231
1300
  seFull.D144 = carry([scheduleR], () => scheduleR);
1232
1301
  seFull.O144 = smallPoolsWriteOff;
1233
1302
  seFull.O149 = scheduleY;
1234
1303
  seFull.O154 = carry([seFull.D139, seFull.D144, seFull.O144, seFull.O149], () =>
1235
- 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
+ ]),
1236
1316
  );
1237
1317
  seFull.O160 = scheduleZ;
1238
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
  }
@@ -457,7 +457,18 @@ export function diyaGlToScenario(book, lines, product) {
457
457
  // The annual SA103F figures the trader states by hand, keyed by HMRC's
458
458
  // own field names: the writer puts each on its SE Full box and the
459
459
  // derivation files it from there.
460
- if (allowances) scenario.annual_allowances = { ...allowances };
460
+ if (allowances) {
461
+ const { structuredBuildingAllowance, enhancedStructuredBuildingAllowance, ...restAllowances } = allowances;
462
+ if (Object.keys(restAllowances).length > 0) scenario.annual_allowances = { ...restAllowances };
463
+ // Structures and Buildings Allowance claims are inputs the Schedule's
464
+ // own SBA block computes the year's allowance from, not a stated
465
+ // figure, so they carry as claims rather than joining annual_allowances.
466
+ const sbaClaims = [
467
+ ...(structuredBuildingAllowance || []).map((claim) => ({ ...claim, enhanced: false })),
468
+ ...(enhancedStructuredBuildingAllowance || []).map((claim) => ({ ...claim, enhanced: true })),
469
+ ];
470
+ if (sbaClaims.length > 0) scenario.sba_claims = sbaClaims;
471
+ }
461
472
  if (adjustments) scenario.annual_adjustments = { ...adjustments };
462
473
  }
463
474
  if (book.debtors) {
@@ -825,7 +836,11 @@ export function extractTaxDataFromBook(book, product) {
825
836
  annual_investment_allowance: ca.annualInvestmentAllowance ? ca.annualInvestmentAllowance / 1000000 : 1.0,
826
837
  writing_down_allowance: ca.mainRateWDA || 0.18,
827
838
  };
828
- if (product === "se") baseTaxData.capital_allowances.writing_down_allowance_special = ca.specialRateWDA || 0.06;
839
+ if (product === "se") {
840
+ baseTaxData.capital_allowances.writing_down_allowance_special = ca.specialRateWDA || 0.06;
841
+ baseTaxData.capital_allowances.structures_and_buildings_allowance = ca.structuresAndBuildingsAllowance || 0.03;
842
+ baseTaxData.capital_allowances.structures_and_buildings_allowance_enhanced = ca.structuresAndBuildingsAllowanceEnhanced || 0.1;
843
+ }
829
844
  }
830
845
  const taxYearFile = taxYearFileForBook(book, product === "ltd" ? "ltd" : "se");
831
846
  baseTaxData.depreciation = taxYearFile.depreciation;
@@ -462,10 +462,14 @@ export function buildSeCellEdits(taxData, startYear) {
462
462
 
463
463
  // Capital allowances — G4 and G5 as BST; G6 is the special rate pool's
464
464
  // writing down rate, which only the Self Employed schedule keeps
465
- // (Fixedassets.xlsx Schedule!AC4 reads it).
465
+ // (Fixedassets.xlsx Schedule!AC4 reads it). G9 and G10 are the Structures
466
+ // and Buildings Allowance rates, which Fixedassets.xlsx Schedule's SBA
467
+ // block reads across the link ($G$9 and $G$10).
466
468
  numericEdits.G4 = ca.annual_investment_allowance;
467
469
  numericEdits.G5 = ca.writing_down_allowance;
468
470
  numericEdits.G6 = ca.writing_down_allowance_special;
471
+ numericEdits.G9 = ca.structures_and_buildings_allowance;
472
+ numericEdits.G10 = ca.structures_and_buildings_allowance_enhanced;
469
473
 
470
474
  // Depreciation — same as BST
471
475
  numericEdits.G13 = dep.land_and_property;
@@ -12,8 +12,8 @@
12
12
 
13
13
  export const PROVENANCE_DATA = {
14
14
  formatVersion: "diya-gl/1",
15
- engineVersion: "1.2.15+27b706de",
16
- taxDataHash: "4ecdbe985d22",
15
+ engineVersion: "1.2.17+67e3da11",
16
+ taxDataHash: "3221a7c2f39c",
17
17
  reconciledCommit: "54e8731a91279975e61b1a232fbb67fda89515f0",
18
18
  templates: {
19
19
  bst: {
@@ -25,8 +25,8 @@ export const PROVENANCE_DATA = {
25
25
  scorecard: "686 passed, 0 warnings, 0 failed",
26
26
  },
27
27
  se: {
28
- hash: "3c88a8ba129b",
29
- scorecard: "7966 passed, 0 warnings, 0 failed",
28
+ hash: "107ff1b5c94a",
29
+ scorecard: "7969 passed, 0 warnings, 0 failed",
30
30
  },
31
31
  ltd: {
32
32
  hash: "caa14632083b",
@@ -1159,6 +1159,26 @@ export function formatScenarioToml(metadata, grouped, expected) {
1159
1159
  parts.push("");
1160
1160
  }
1161
1161
 
1162
+ // Structures and Buildings Allowance claims (boxes 53 and 53.1), the
1163
+ // book's own two arrays merged with an enhanced flag the way
1164
+ // diyaGlToScenario merges them. The Schedule's SBA block computes the
1165
+ // year's allowance from these inputs; no amount is stated here.
1166
+ if (expected.sba_claims) {
1167
+ for (const claim of expected.sba_claims) {
1168
+ parts.push("[[sba_claims]]");
1169
+ parts.push(`qualifyingDate = ${claim.qualifyingDate}`);
1170
+ parts.push(`qualifyingAmountExpenditure = ${claim.qualifyingAmountExpenditure}`);
1171
+ if (claim.ceasedDate) parts.push(`ceasedDate = ${claim.ceasedDate}`);
1172
+ if (claim.enhanced) parts.push(`enhanced = true`);
1173
+ parts.push("");
1174
+ parts.push("[sba_claims.building]");
1175
+ if (claim.building?.name) parts.push(`name = "${escapeTomlString(claim.building.name)}"`);
1176
+ if (claim.building?.number) parts.push(`number = "${escapeTomlString(claim.building.number)}"`);
1177
+ parts.push(`postcode = "${escapeTomlString(claim.building.postcode)}"`);
1178
+ parts.push("");
1179
+ }
1180
+ }
1181
+
1162
1182
  // Charges and debentures registered over the company's assets (Ltd). Each
1163
1183
  // one secures a creditor falling due after more than one year.
1164
1184
  if (expected.charges) {
@@ -1703,15 +1703,22 @@ const SE_DISALLOWABLE_PERCENT_CELLS = {
1703
1703
  // se.js writes from ANNUAL_ALLOWANCE_CELLS and ANNUAL_ADJUSTMENT_CELLS, and
1704
1704
  // the Business Details cell both returns read box 60 from.
1705
1705
  const SE_ANNUAL_ALLOWANCE_CELLS = {
1706
- zeroEmissionsGoodsVehicleAllowance: "D152",
1707
- zeroEmissionsCarAllowance: "D156",
1708
- structuredBuildingAllowance: "D160",
1706
+ zeroEmissionsGoodsVehicleAllowance: "D150",
1707
+ zeroEmissionsCarAllowance: "D152",
1709
1708
  electricChargePointAllowance: "O139",
1710
1709
  };
1711
1710
  const SE_ANNUAL_ADJUSTMENT_CELLS = {
1712
1711
  includedNonTaxableProfits: "D179",
1713
1712
  accountingAdjustment: "D210",
1714
1713
  };
1714
+
1715
+ // Fixedassets.xlsx!Schedule rows 115 to 119, the Structures and Buildings
1716
+ // Allowance claims below the register (SA103F boxes 53 and 53.1); the same
1717
+ // rows and columns app/products/se.js writes from SBA_CLAIM_ROWS and
1718
+ // SBA_CLAIM_COLUMNS. E above nil is a claim; G = "F" puts it in the
1719
+ // enhanced (Freeport or Investment Zone) array, otherwise the standard one.
1720
+ const SCHEDULE_SBA_CLAIM_ROWS = [115, 116, 117, 118, 119];
1721
+ const SCHEDULE_SBA_ENHANCED_MARKER = "F";
1715
1722
  const SE_GOODS_FOR_OWN_USE_CELL = "O50";
1716
1723
 
1717
1724
  const ENTITY_CELLS = {
@@ -2568,6 +2575,31 @@ export async function extractBook(set, product, lines, cellMap, options = {}) {
2568
2575
  const allowances = readStated(seFull, SE_ANNUAL_ALLOWANCE_CELLS);
2569
2576
  const adjustments = readStated(seFull, SE_ANNUAL_ADJUSTMENT_CELLS);
2570
2577
  if (entitySheet) Object.assign(adjustments, readStated(entitySheet, { goodsAndServicesOwnUse: SE_GOODS_FOR_OWN_USE_CELL }));
2578
+ // The Structures and Buildings Allowance claims (boxes 53 and 53.1)
2579
+ // are not stated on SE Full: the Schedule's own claim rows carry them.
2580
+ const scheduleForSba = await scheduleSheet(set);
2581
+ if (scheduleForSba) {
2582
+ const { xml: sbaXml, sharedStrings: sbaStrings } = scheduleForSba;
2583
+ const structuredBuildingAllowance = [];
2584
+ const enhancedStructuredBuildingAllowance = [];
2585
+ for (const row of SCHEDULE_SBA_CLAIM_ROWS) {
2586
+ const qualifyingAmountExpenditure = numberAt(sbaXml, `E${row}`, sbaStrings);
2587
+ if (qualifyingAmountExpenditure === undefined || qualifyingAmountExpenditure === 0) continue;
2588
+ const claim = { qualifyingAmountExpenditure };
2589
+ assign(claim, "qualifyingDate", dateAt(sbaXml, `B${row}`, sbaStrings));
2590
+ assign(claim, "ceasedDate", dateAt(sbaXml, `I${row}`, sbaStrings));
2591
+ const building = {};
2592
+ assign(building, "name", textAt(sbaXml, `C${row}`, sbaStrings));
2593
+ assign(building, "number", textAt(sbaXml, `D${row}`, sbaStrings));
2594
+ assign(building, "postcode", textAt(sbaXml, `F${row}`, sbaStrings));
2595
+ claim.building = building;
2596
+ const enhanced = textAt(sbaXml, `G${row}`, sbaStrings) === SCHEDULE_SBA_ENHANCED_MARKER;
2597
+ (enhanced ? enhancedStructuredBuildingAllowance : structuredBuildingAllowance).push(claim);
2598
+ }
2599
+ if (structuredBuildingAllowance.length > 0) allowances.structuredBuildingAllowance = structuredBuildingAllowance;
2600
+ if (enhancedStructuredBuildingAllowance.length > 0)
2601
+ allowances.enhancedStructuredBuildingAllowance = enhancedStructuredBuildingAllowance;
2602
+ }
2571
2603
  if (Object.keys(allowances).length > 0) selfEmployment.allowances = allowances;
2572
2604
  if (Object.keys(adjustments).length > 0) selfEmployment.adjustments = adjustments;
2573
2605
  }
@@ -229,17 +229,18 @@ export const DISALLOWABLE_PERCENT_CELLS = {
229
229
 
230
230
  // The SA103F boxes the trader states by hand, keyed by HMRC's own API field
231
231
  // names. Verified against the template: SE Full!O154 (box 57) reads
232
- // D139+D144+D147+D152+D156+D160+O139+O144+O149, so every allowance cell here
233
- // is a term of the total; O169 (box 63) reads O154+D179; O194 (box 73) and
234
- // D219 (box 77) each floor the working sheet's one figure,
232
+ // D139+D144+D147+D150+D152+D156+D160+O139+O144+O149, so every allowance cell
233
+ // here is a term of the total; O169 (box 63) reads O154+D179; O194 (box 73)
234
+ // and D219 (box 77) each floor the working sheet's one figure,
235
235
  // O174-O179+N(D197)+D210+N(O190), at nil from its own side. Box 54's caption
236
236
  // at N136 is "Electric charge-point allowance" (L136 = 54); box 55's cell
237
237
  // O144 is the small pools formula, so the enhanced-allowance figure has no
238
- // input cell of its own.
238
+ // input cell of its own. Boxes 53 and 53.1 (D156, D160) are not stated here:
239
+ // the Schedule's SBA block computes them from scenario.sba_claims, so they
240
+ // carry no entry in this table.
239
241
  export const ANNUAL_ALLOWANCE_CELLS = {
240
- zeroEmissionsGoodsVehicleAllowance: "D152",
241
- zeroEmissionsCarAllowance: "D156",
242
- structuredBuildingAllowance: "D160",
242
+ zeroEmissionsGoodsVehicleAllowance: "D150",
243
+ zeroEmissionsCarAllowance: "D152",
243
244
  electricChargePointAllowance: "O139",
244
245
  };
245
246
  export const ANNUAL_ADJUSTMENT_CELLS = {
@@ -289,6 +290,28 @@ export const SINGLE_ASSET_POOL_MARKER_COLUMN = "AD";
289
290
  export const SINGLE_ASSET_POOL_MARKER = "P";
290
291
  export const PRIVATE_USE_COLUMN = "M";
291
292
 
293
+ // Structures and Buildings Allowance claims, Fixedassets.xlsx Schedule rows
294
+ // 115 to 119, below the register (SA103F boxes 53 and 53.1). Written left
295
+ // to right: B the date first in qualifying use, C the building name, D its
296
+ // number, E the qualifying expenditure, F the postcode, G "F" for a Freeport
297
+ // or Investment Zone tax site, I the date qualifying use ceased. H (the
298
+ // year's rate), J (days claimed) and K (the year's allowance) are the
299
+ // sheet's own formulas. Verified against the template: H115 =
300
+ // IF(E115>0,IF(G115="F",[1]Admin!$G$10,[1]Admin!$G$9)," "), K120 =
301
+ // SUMIF(G115:G119,"<>F",K115:K119) (box 53), K121 the same with "F"
302
+ // (box 53.1).
303
+ export const SBA_CLAIM_ROWS = [115, 116, 117, 118, 119];
304
+ export const SBA_CLAIM_COLUMNS = {
305
+ qualifyingDate: "B",
306
+ name: "C",
307
+ number: "D",
308
+ qualifyingAmountExpenditure: "E",
309
+ postcode: "F",
310
+ enhanced: "G",
311
+ ceasedDate: "I",
312
+ };
313
+ export const SBA_ENHANCED_MARKER = "F";
314
+
292
315
  // Hire purchase agreements (Fixedassets.xlsx HPfinance sheet). Only two
293
316
  // rows are available for scenario agreements before the sheet's own
294
317
  // layout runs out: row 8 (the "New" block's working master, whose
@@ -764,6 +787,45 @@ function composeWrites(scenario, targetStartYear) {
764
787
  });
765
788
  }
766
789
 
790
+ // Structures and Buildings Allowance claims (SA103F boxes 53 and 53.1).
791
+ if (scenario.sba_claims) {
792
+ if (!fixedAssetsWrites.Schedule) fixedAssetsWrites.Schedule = {};
793
+ const fa = fixedAssetsWrites.Schedule;
794
+ for (const claim of scenario.sba_claims.slice(SBA_CLAIM_ROWS.length)) {
795
+ skips.push(
796
+ skipped(
797
+ "structuresBuildingsAllowanceClaim",
798
+ { date: claim.qualifyingDate, code: claim.enhanced ? "enhanced" : "standard", amount: claim.qualifyingAmountExpenditure },
799
+ `the Schedule holds ${SBA_CLAIM_ROWS.length} Structures and Buildings Allowance claims`,
800
+ ),
801
+ );
802
+ }
803
+ scenario.sba_claims.slice(0, SBA_CLAIM_ROWS.length).forEach((claim, i) => {
804
+ const row = SBA_CLAIM_ROWS[i];
805
+ const qualifyingDate = shiftDate(parseDate(claim.qualifyingDate));
806
+ // Left to right: B, C, D, E, F, G, I -- see the opening asset writer
807
+ // above for why the order matters. H and J are the sheet's formulas.
808
+ fa[`${SBA_CLAIM_COLUMNS.qualifyingDate}${row}`] = toExcelSerial(
809
+ qualifyingDate.getUTCFullYear(),
810
+ qualifyingDate.getUTCMonth() + 1,
811
+ qualifyingDate.getUTCDate(),
812
+ );
813
+ if (claim.building?.name) fa[`${SBA_CLAIM_COLUMNS.name}${row}`] = claim.building.name;
814
+ if (claim.building?.number) fa[`${SBA_CLAIM_COLUMNS.number}${row}`] = claim.building.number;
815
+ fa[`${SBA_CLAIM_COLUMNS.qualifyingAmountExpenditure}${row}`] = claim.qualifyingAmountExpenditure;
816
+ fa[`${SBA_CLAIM_COLUMNS.postcode}${row}`] = claim.building.postcode;
817
+ if (claim.enhanced) fa[`${SBA_CLAIM_COLUMNS.enhanced}${row}`] = SBA_ENHANCED_MARKER;
818
+ if (claim.ceasedDate) {
819
+ const ceasedDate = shiftDate(parseDate(claim.ceasedDate));
820
+ fa[`${SBA_CLAIM_COLUMNS.ceasedDate}${row}`] = toExcelSerial(
821
+ ceasedDate.getUTCFullYear(),
822
+ ceasedDate.getUTCMonth() + 1,
823
+ ceasedDate.getUTCDate(),
824
+ );
825
+ }
826
+ });
827
+ }
828
+
767
829
  if (scenario.hp_agreements) {
768
830
  if (!fixedAssetsWrites.HPfinance) fixedAssetsWrites.HPfinance = {};
769
831
  const hp = fixedAssetsWrites.HPfinance;
@@ -1042,9 +1104,10 @@ export const CELL_MAP = [
1042
1104
  ["SE Full", "D139", "Annual investment allowance (box 49)", "tax.capitalAllowances.aia (sa103f)", "Self Assessment (SA103F)", 1],
1043
1105
  ["SE Full", "D144", "Capital allowances at 18% (box 50)", "tax.capitalAllowances.wda (sa103f)", "Self Assessment (SA103F)", 1],
1044
1106
  ["SE Full", "D147", "Capital allowances at 6% (box 51)", "tax.capitalAllowances.specialRateWDA (sa103f)", "Self Assessment (SA103F)", 1],
1045
- ["SE Full", "D152", "Zero-emission goods vehicle allowance (box 52)", "tax.selfEmployment.allowances.zeroEmissionsGoodsVehicleAllowance", "Self Assessment (SA103F)", 1],
1046
- ["SE Full", "D156", "Zero-emission car allowance (box 52.1)", "tax.selfEmployment.allowances.zeroEmissionsCarAllowance", "Self Assessment (SA103F)", 1],
1047
- ["SE Full", "D160", "Structures and Buildings Allowance (box 53)", "tax.selfEmployment.allowances.structuredBuildingAllowance", "Self Assessment (SA103F)", 1],
1107
+ ["SE Full", "D150", "Zero-emission goods vehicle allowance (box 52)", "tax.selfEmployment.allowances.zeroEmissionsGoodsVehicleAllowance", "Self Assessment (SA103F)", 1],
1108
+ ["SE Full", "D152", "Zero-emission car allowance (box 52.1)", "tax.selfEmployment.allowances.zeroEmissionsCarAllowance", "Self Assessment (SA103F)", 1],
1109
+ ["SE Full", "D156", "Structures and Buildings Allowance (box 53)", "tax.capitalAllowances.structuresAndBuildingsAllowance (sa103f)", "Self Assessment (SA103F)", 1],
1110
+ ["SE Full", "D160", "Freeport and Investment Zones Structures and Buildings Allowance (box 53.1)", "tax.capitalAllowances.structuresAndBuildingsAllowanceEnhanced (sa103f)", "Self Assessment (SA103F)", 1],
1048
1111
  ["SE Full", "O139", "Electric charge-point allowance (box 54)", "tax.selfEmployment.allowances.electricChargePointAllowance", "Self Assessment (SA103F)", 1],
1049
1112
  ["SE Full", "O144", "100% and other enhanced capital allowances (box 55)", "tax.capitalAllowances.enhanced (sa103f)", "Self Assessment (SA103F)", 1],
1050
1113
  ["SE Full", "O149", "Allowances on sale or cessation (box 56)", "tax.capitalAllowances.balancingAllowance (sa103f)", "Self Assessment (SA103F)", 1],
@@ -1118,6 +1181,8 @@ export const CELL_MAP = [
1118
1181
  ["Admin", "G4", "Annual Investment Allowance Rate", "", "Admin (Generator Injected)", 0],
1119
1182
  ["Admin", "G5", "Writing Down Allowance Rate", "tax.capitalAllowances.mainRateWDA", "Admin (Generator Injected)", 0],
1120
1183
  ["Admin", "G6", "Special Rate Writing Down Allowance Rate", "tax.capitalAllowances.specialRateWDA", "Admin (Generator Injected)", 0],
1184
+ ["Admin", "G9", "Structures and Buildings Allowance Rate", "tax.capitalAllowances.structuresAndBuildingsAllowance", "Admin (Generator Injected)", 0],
1185
+ ["Admin", "G10", "Freeport and Investment Zone SBA Rate", "tax.capitalAllowances.structuresAndBuildingsAllowanceEnhanced", "Admin (Generator Injected)", 0],
1121
1186
  ["Admin", "F21", "Mileage Higher Rate Limit", "", "Admin (Generator Injected)", 0],
1122
1187
  ["Admin", "G21", "Mileage Higher Rate Pence", "tax.mileage.carFirst10000", "Admin (Generator Injected)", 0],
1123
1188
  ["Admin", "F22", "Mileage Lower Rate Start", "", "Admin (Generator Injected)", 0],
@@ -1281,7 +1346,9 @@ export function multiFileOptions() {
1281
1346
  // pooled written-down value by rate pool, which the small pools test
1282
1347
  // now runs over per pool; M40, AD40, AE40 and AG40 are the fixture's
1283
1348
  // marked car row (van 38, estate car 39, hatchback 40), read so its
1284
- // marker and private use share are proved on the row itself.
1349
+ // marker and private use share are proved on the row itself. B115 to
1350
+ // K116 are the fixture's two SBA claim rows; K120 and K121 the SBA
1351
+ // block's box 53 and box 53.1 totals.
1285
1352
  Schedule: [
1286
1353
  "E1",
1287
1354
  "F1",
@@ -1310,6 +1377,23 @@ export function multiFileOptions() {
1310
1377
  "AD40",
1311
1378
  "AE40",
1312
1379
  "AG40",
1380
+ "B115",
1381
+ "C115",
1382
+ "E115",
1383
+ "F115",
1384
+ "H115",
1385
+ "J115",
1386
+ "K115",
1387
+ "B116",
1388
+ "D116",
1389
+ "E116",
1390
+ "F116",
1391
+ "G116",
1392
+ "H116",
1393
+ "J116",
1394
+ "K116",
1395
+ "K120",
1396
+ "K121",
1313
1397
  ],
1314
1398
  FAreconciliation: ["E11", "E13", "E15", "K11", "K13", "K15"],
1315
1399
  // E2 is the long-term-creditors total for the "New Hire Purchase
@@ -1714,6 +1798,23 @@ const FIXED_ASSET_CELL_LABELS = {
1714
1798
  AG40: "Single asset pool tax written down value on the third motor vehicle brought forward",
1715
1799
  E57: "Cost of the assets owned at the start of the year",
1716
1800
  E110: "Cost of the assets bought during the year",
1801
+ B115: "Date first in qualifying use, the fixture's standard-rate SBA claim",
1802
+ C115: "Building name, the fixture's standard-rate SBA claim",
1803
+ E115: "Qualifying expenditure, the fixture's standard-rate SBA claim",
1804
+ F115: "Postcode, the fixture's standard-rate SBA claim",
1805
+ H115: "Structures and Buildings Allowance rate applied to the standard-rate claim",
1806
+ J115: "Days claimed this year, the fixture's standard-rate SBA claim",
1807
+ K115: "Structures and Buildings Allowance claimed this year, the fixture's standard-rate claim",
1808
+ B116: "Date first in qualifying use, the fixture's Freeport SBA claim",
1809
+ D116: "Building number, the fixture's Freeport SBA claim",
1810
+ E116: "Qualifying expenditure, the fixture's Freeport SBA claim",
1811
+ F116: "Postcode, the fixture's Freeport SBA claim",
1812
+ G116: "Tax site marker, the fixture's Freeport SBA claim (F)",
1813
+ H116: "Structures and Buildings Allowance rate applied to the Freeport claim",
1814
+ J116: "Days claimed this year, the fixture's Freeport SBA claim",
1815
+ K116: "Structures and Buildings Allowance claimed this year, the fixture's Freeport claim",
1816
+ K120: "Structures and Buildings Allowance (SA103F box 53), the non-Freeport claims summed",
1817
+ K121: "Freeport and Investment Zone Structures and Buildings Allowance (SA103F box 53.1), the Freeport claims summed",
1717
1818
  },
1718
1819
  "Fixedassets.xlsx!FAreconciliation": {
1719
1820
  E11: "Additions the schedule lists, net of VAT",
@@ -1746,6 +1847,9 @@ const ADMIN_RATE_CELLS = new Set([
1746
1847
  "L23",
1747
1848
  "G4",
1748
1849
  "G5",
1850
+ "G6",
1851
+ "G9",
1852
+ "G10",
1749
1853
  "G13",
1750
1854
  "G14",
1751
1855
  "G15",
@@ -1898,9 +2002,14 @@ export function profitBridge(results) {
1898
2002
  { label: "Less other capital allowances (box 25)", cell: "SE Short!O80", value: -num(seShort.O80) },
1899
2003
  { label: "Add balancing charges (box 26)", cell: "SE Short!O85", value: num(seShort.O85) },
1900
2004
  { label: "Add goods and services for own use (box 27)", cell: "SE Short!D94", value: num(seShort.D94) },
1901
- { label: "Less the full return's own zero-emission goods vehicle allowance (box 52)", cell: "SE Full!D152", value: -num(seFull?.D152) },
1902
- { label: "Less the full return's own zero-emission car allowance (box 52.1)", cell: "SE Full!D156", value: -num(seFull?.D156) },
1903
- { label: "Less the full return's own Structures and Buildings Allowance (box 53)", cell: "SE Full!D160", value: -num(seFull?.D160) },
2005
+ { label: "Less the full return's own zero-emission goods vehicle allowance (box 52)", cell: "SE Full!D150", value: -num(seFull?.D150) },
2006
+ { label: "Less the full return's own zero-emission car allowance (box 52.1)", cell: "SE Full!D152", value: -num(seFull?.D152) },
2007
+ { label: "Less the full return's own Structures and Buildings Allowance (box 53)", cell: "SE Full!D156", value: -num(seFull?.D156) },
2008
+ {
2009
+ label: "Less the full return's own Freeport and Investment Zone Structures and Buildings Allowance (box 53.1)",
2010
+ cell: "SE Full!D160",
2011
+ value: -num(seFull?.D160),
2012
+ },
1904
2013
  { label: "Less the full return's own electric charge-point allowance (box 54)", cell: "SE Full!O139", value: -num(seFull?.O139) },
1905
2014
  { label: "Less the full return's own box 62 adjustment", cell: "SE Full!D179", value: -num(seFull?.D179) },
1906
2015
  {
@@ -1975,6 +2084,40 @@ function fixtureSmallPools(expected, taxData, rate) {
1975
2084
  return { ...pools, writeOff: writeOff(pools.main) + writeOff(pools.special) };
1976
2085
  }
1977
2086
 
2087
+ // SA103F boxes 53 and 53.1 the way Fixedassets.xlsx!Schedule!K120 and K121
2088
+ // state them (Structures and Buildings Allowance), from the fixture's own
2089
+ // claims rather than from the sheet: each claim's expenditure at its own
2090
+ // year's rate (standard or the enhanced Freeport/Investment Zone rate),
2091
+ // over the days of the chargeable period it covers -- the later of the
2092
+ // period's own start and the claim's qualifying date, to the earlier of the
2093
+ // period's end, the claim's ceased date and the 33 1/3 (or 10) year limit.
2094
+ const EXCEL_EPOCH_MS = Date.UTC(1899, 11, 30);
2095
+ function toExcelSerialDate(date) {
2096
+ return Math.round((date.getTime() - EXCEL_EPOCH_MS) / 86400000);
2097
+ }
2098
+ function fixtureStructuresAndBuildings(expected, taxData, periodStartSerial, periodEndSerial, shiftDate) {
2099
+ const rateStandard = taxData.capital_allowances.structures_and_buildings_allowance ?? 0;
2100
+ const rateEnhanced = taxData.capital_allowances.structures_and_buildings_allowance_enhanced ?? 0;
2101
+ const periodDays = periodEndSerial - periodStartSerial + 1;
2102
+ let box53 = 0;
2103
+ let box53_1 = 0;
2104
+ for (const claim of expected.sba_claims || []) {
2105
+ const enhanced = claim.enhanced === true;
2106
+ const qualifyingDate = shiftDate(parseDate(claim.qualifyingDate));
2107
+ const qualifyingSerial = toExcelSerialDate(qualifyingDate);
2108
+ const rate = enhanced ? rateEnhanced : rateStandard;
2109
+ const limitMonths = enhanced ? 120 : 400;
2110
+ let upperBound = Math.min(periodEndSerial, toExcelSerialDate(shiftMonths(qualifyingDate, limitMonths)) - 1);
2111
+ if (claim.ceasedDate) upperBound = Math.min(upperBound, toExcelSerialDate(shiftDate(parseDate(claim.ceasedDate))));
2112
+ const lowerBound = Math.max(periodStartSerial, qualifyingSerial);
2113
+ const days = Math.max(0, upperBound - lowerBound + 1);
2114
+ const allowance = (claim.qualifyingAmountExpenditure * rate * days) / periodDays;
2115
+ if (enhanced) box53_1 += allowance;
2116
+ else box53 += allowance;
2117
+ }
2118
+ return { box53, box53_1 };
2119
+ }
2120
+
1978
2121
  // ── Journal category VAT netting ───────────────────────────────────────────
1979
2122
 
1980
2123
  // A journal row takes its own VAT off its own gross and rounds nothing
@@ -2399,14 +2542,21 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
2399
2542
  );
2400
2543
  // Income Tax!E5 reads 'SE Full'!O210, so the short return's box 31
2401
2544
  // reaches it only through the full return: the two agree exactly when
2402
- // no box the trader states on SE Full alone (52, 52.1, 53, 54, 62 and
2403
- // 71) carries a figure, and stand apart by those figures otherwise.
2545
+ // no box the trader states or claims on SE Full alone (52, 52.1, 53,
2546
+ // 53.1, 54, 62 and 71) carries a figure, and stand apart by those
2547
+ // figures otherwise.
2404
2548
  const fullOnly = results["SE Full"] || {};
2405
2549
  const fullReturnOnlyBoxes =
2406
- num(fullOnly.D152) + num(fullOnly.D156) + num(fullOnly.D160) + num(fullOnly.O139) + num(fullOnly.D179) - num(fullOnly.D210);
2550
+ num(fullOnly.D150) +
2551
+ num(fullOnly.D152) +
2552
+ num(fullOnly.D156) +
2553
+ num(fullOnly.D160) +
2554
+ num(fullOnly.O139) +
2555
+ num(fullOnly.D179) -
2556
+ num(fullOnly.D210);
2407
2557
  if (seShort.D106) {
2408
2558
  check(
2409
- "SA103S: Profit for tax (D106) less the SE Full-only boxes 52, 52.1, 53, 54 and 62 plus box 71 = Income Tax E5",
2559
+ "SA103S: Profit for tax (D106) less the SE Full-only boxes 52, 52.1, 53, 53.1, 54 and 62 plus box 71 = Income Tax E5",
2410
2560
  Math.max(0, num(seShort.D106) - fullReturnOnlyBoxes),
2411
2561
  tax.E5,
2412
2562
  );
@@ -2602,6 +2752,7 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
2602
2752
  num(seFull.D139) +
2603
2753
  num(seFull.D144) +
2604
2754
  num(seFull.D147) +
2755
+ num(seFull.D150) +
2605
2756
  num(seFull.D152) +
2606
2757
  num(seFull.D156) +
2607
2758
  num(seFull.D160) +
@@ -2667,6 +2818,12 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
2667
2818
  );
2668
2819
  check("SA103F box 56 allowances on sale or cessation (O149) = Schedule Y1", num(seFull.O149), num(returnSchedule.Y1));
2669
2820
  check("SA103F box 59 balancing charge (O160) = Schedule Z1", num(seFull.O160), num(returnSchedule.Z1));
2821
+ check("SA103F box 53 Structures and Buildings Allowance (D156) = Schedule K120", num(seFull.D156), num(returnSchedule.K120));
2822
+ check(
2823
+ "SA103F box 53.1 Freeport and Investment Zone Structures and Buildings Allowance (D160) = Schedule K121",
2824
+ num(seFull.D160),
2825
+ num(returnSchedule.K121),
2826
+ );
2670
2827
  }
2671
2828
 
2672
2829
  // Box 50 against the scenario's own assets and the year's own rate,
@@ -2709,9 +2866,8 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
2709
2866
  const statedAllowances = expected.annual_allowances || {};
2710
2867
  const statedAdjustments = expected.annual_adjustments || {};
2711
2868
  const annualBoxes = [
2712
- ["52", "zero-emission goods vehicle allowance", "D152", statedAllowances.zeroEmissionsGoodsVehicleAllowance],
2713
- ["52.1", "zero-emission car allowance", "D156", statedAllowances.zeroEmissionsCarAllowance],
2714
- ["53", "Structures and Buildings Allowance", "D160", statedAllowances.structuredBuildingAllowance],
2869
+ ["52", "zero-emission goods vehicle allowance", "D150", statedAllowances.zeroEmissionsGoodsVehicleAllowance],
2870
+ ["52.1", "zero-emission car allowance", "D152", statedAllowances.zeroEmissionsCarAllowance],
2715
2871
  ["54", "electric charge-point allowance", "O139", statedAllowances.electricChargePointAllowance],
2716
2872
  ["62", "income included but not taxable as business profits", "D179", statedAdjustments.includedNonTaxableProfits],
2717
2873
  ["71", "adjustment for change of accounting practice", "D210", statedAdjustments.accountingAdjustment],
@@ -2719,6 +2875,24 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
2719
2875
  for (const [box, caption, cell, stated] of annualBoxes) {
2720
2876
  check(`SA103F box ${box} ${caption} (${cell}) = the figure the book states`, num(seFull[cell]), stated || 0);
2721
2877
  }
2878
+ // Boxes 53 and 53.1 are not stated: the Schedule's SBA block computes
2879
+ // them from the fixture's own claims, each priced at its own rate over
2880
+ // the days it covers within the package's period. Only when a rate
2881
+ // year's own tax data is in hand -- some callers check an isolated
2882
+ // cell with none, and the rate is what fixes the fixture's own figure.
2883
+ if (taxData?.capital_allowances) {
2884
+ const sba = fixtureStructuresAndBuildings(expected, taxData, num(seFull.Q2), num(seFull.V2), shiftDate);
2885
+ check(
2886
+ "SA103F box 53 Structures and Buildings Allowance (D156) = the fixture's own claims at the year's rate",
2887
+ num(seFull.D156),
2888
+ sba.box53,
2889
+ );
2890
+ check(
2891
+ "SA103F box 53.1 Freeport and Investment Zone Structures and Buildings Allowance (D160) = the fixture's own claims at the year's enhanced rate",
2892
+ num(seFull.D160),
2893
+ sba.box53_1,
2894
+ );
2895
+ }
2722
2896
  const ownUseStated = statedAdjustments.goodsAndServicesOwnUse || 0;
2723
2897
  check(
2724
2898
  "SA103F box 60 goods and services for own use (D169) = the figure the book states on Business Details!O50",
@@ -2736,8 +2910,15 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
2736
2910
  .filter(([box]) => box !== "62" && box !== "71")
2737
2911
  .reduce((total, [, , , stated]) => total + (stated || 0), 0);
2738
2912
  check(
2739
- "SA103F box 57 total capital allowances (O154) less the schedule-fed boxes 49, 50, 51, 55 and 56 = the allowances the book states (boxes 52, 52.1, 53 and 54)",
2740
- num(seFull.O154) - num(seFull.D139) - num(seFull.D144) - num(seFull.D147) - num(seFull.O144) - num(seFull.O149),
2913
+ "SA103F box 57 total capital allowances (O154) less the schedule-fed boxes 49, 50, 51, 53, 53.1, 55 and 56 = the allowances the book states (boxes 52, 52.1 and 54)",
2914
+ num(seFull.O154) -
2915
+ num(seFull.D139) -
2916
+ num(seFull.D144) -
2917
+ num(seFull.D147) -
2918
+ num(seFull.D156) -
2919
+ num(seFull.D160) -
2920
+ num(seFull.O144) -
2921
+ num(seFull.O149),
2741
2922
  statedAllowanceTotal,
2742
2923
  );
2743
2924
  check(
@@ -2781,19 +2962,20 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
2781
2962
  ["D231", "O124", "box 81 contractor deductions taken off"],
2782
2963
  ];
2783
2964
  // The short return reads the fixed asset schedule and Business Details
2784
- // directly, so the boxes the trader states on SE Full alone (52, 52.1,
2785
- // 53, 54 and 62) never reach it: its taxable profit stands higher than
2786
- // the full return's by exactly those figures.
2787
- const fullReturnOnlyDeductions = num(seFull.D152) + num(seFull.D156) + num(seFull.D160) + num(seFull.O139) + num(seFull.D179);
2965
+ // directly, so the boxes the trader states or claims on SE Full alone
2966
+ // (52, 52.1, 53, 53.1, 54 and 62) never reach it: its taxable profit
2967
+ // stands higher than the full return's by exactly those figures.
2968
+ const fullReturnOnlyDeductions =
2969
+ num(seFull.D150) + num(seFull.D152) + num(seFull.D156) + num(seFull.D160) + num(seFull.O139) + num(seFull.D179);
2788
2970
  check(
2789
- "SA103F box 64 net business profit for tax purposes: full return (O174) = short return (D99) less the SE Full-only boxes 52, 52.1, 53, 54 and 62",
2971
+ "SA103F box 64 net business profit for tax purposes: full return (O174) = short return (D99) less the SE Full-only boxes 52, 52.1, 53, 53.1, 54 and 62",
2790
2972
  num(seFull.O174),
2791
2973
  Math.max(0, num(sa103s.D99) - fullReturnOnlyDeductions),
2792
2974
  );
2793
2975
  // Box 71 is the one SE Full-only figure that adds to the taxable
2794
2976
  // profit; the short return has no box for it either.
2795
2977
  check(
2796
- "SA103F box 76 total taxable profits: full return (O210) = short return (D106) less the SE Full-only boxes 52, 52.1, 53, 54 and 62 plus box 71, with each return's own loss set-off",
2978
+ "SA103F box 76 total taxable profits: full return (O210) = short return (D106) less the SE Full-only boxes 52, 52.1, 53, 53.1, 54 and 62 plus box 71, with each return's own loss set-off",
2797
2979
  num(seFull.O210),
2798
2980
  Math.max(0, num(sa103s.D106) - fullReturnOnlyDeductions + num(seFull.D210) + num(sa103s.O94) - num(seFull.O199)),
2799
2981
  );
@@ -2851,12 +3033,19 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
2851
3033
  num(sa103s.D71) - num(seFull.O122),
2852
3034
  );
2853
3035
  check(
2854
- "SA103F box 57 total capital allowances (O154) = the short return's allowance boxes 23, 24 and 25 plus the SE Full-only boxes 52, 52.1, 53 and 54",
3036
+ "SA103F box 57 total capital allowances (O154) = the short return's allowance boxes 23, 24 and 25 plus the SE Full-only boxes 52, 52.1, 53, 53.1 and 54",
2855
3037
  num(seFull.O154),
2856
- num(sa103s.D80) + num(sa103s.D85) + num(sa103s.O80) + num(seFull.D152) + num(seFull.D156) + num(seFull.D160) + num(seFull.O139),
3038
+ num(sa103s.D80) +
3039
+ num(sa103s.D85) +
3040
+ num(sa103s.O80) +
3041
+ num(seFull.D150) +
3042
+ num(seFull.D152) +
3043
+ num(seFull.D156) +
3044
+ num(seFull.D160) +
3045
+ num(seFull.O139),
2857
3046
  );
2858
3047
  check(
2859
- "SA103S box 25 other capital allowances (O80) leaves out the allowances the trader states on SE Full alone (boxes 52, 52.1, 53 and 54)",
3048
+ "SA103S box 25 other capital allowances (O80) leaves out the allowances the trader states or claims on SE Full alone (boxes 52, 52.1, 53, 53.1 and 54)",
2860
3049
  num(sa103s.O80),
2861
3050
  num(seFull.O154) - num(sa103s.D80) - num(sa103s.D85),
2862
3051
  0.01,
@@ -854,7 +854,7 @@
854
854
  "properties": {
855
855
  "allowances": {
856
856
  "type": "object",
857
- "description": "Capital allowance claims the fixed asset schedule does not compute, written to the SA103F boxes the trader would otherwise fill by hand: box 52 (zeroEmissionsGoodsVehicleAllowance, SE Full!D152), box 52.1 (zeroEmissionsCarAllowance, D156), box 53 (structuredBuildingAllowance, D160) and box 54 (electricChargePointAllowance, O139). Box 57 totals them. HMRC's schema dropped the box 52 and box 54 fields from 2025-26; the sheet still totals a stated figure, and the derivation warns.",
857
+ "description": "Capital allowance claims the fixed asset schedule does not compute, written to the SA103F boxes the trader would otherwise fill by hand: box 52 (zeroEmissionsGoodsVehicleAllowance, SE Full!D150), box 52.1 (zeroEmissionsCarAllowance, D152), box 53 (structuredBuildingAllowance[], the Schedule's SBA block sums into D156) and box 53.1 (enhancedStructuredBuildingAllowance[], D160) and box 54 (electricChargePointAllowance, O139). Box 57 totals them. HMRC's schema dropped the box 52 and box 54 fields from 2025-26; the sheet still totals a stated figure, and the derivation warns.",
858
858
  "additionalProperties": false,
859
859
  "properties": {
860
860
  "zeroEmissionsGoodsVehicleAllowance": {
@@ -866,8 +866,12 @@
866
866
  "minimum": 0
867
867
  },
868
868
  "structuredBuildingAllowance": {
869
- "type": "number",
870
- "minimum": 0
869
+ "type": "array",
870
+ "items": { "$ref": "#/$defs/structuresBuildingsAllowanceClaim" }
871
+ },
872
+ "enhancedStructuredBuildingAllowance": {
873
+ "type": "array",
874
+ "items": { "$ref": "#/$defs/structuresBuildingsAllowanceClaim" }
871
875
  },
872
876
  "electricChargePointAllowance": {
873
877
  "type": "number",
@@ -979,6 +983,16 @@
979
983
  "minimum": 0,
980
984
  "maximum": 1
981
985
  },
986
+ "structuresAndBuildingsAllowance": {
987
+ "type": "number",
988
+ "minimum": 0,
989
+ "maximum": 1
990
+ },
991
+ "structuresAndBuildingsAllowanceEnhanced": {
992
+ "type": "number",
993
+ "minimum": 0,
994
+ "maximum": 1
995
+ },
982
996
  "firstYearAllowanceRate": {
983
997
  "type": "number",
984
998
  "minimum": 0,
@@ -1106,6 +1120,44 @@
1106
1120
  }
1107
1121
  }
1108
1122
  },
1123
+ "structuresBuildingsAllowanceClaim": {
1124
+ "type": "object",
1125
+ "description": "One Structures and Buildings Allowance claim (CAA 2001 s.270AA onwards), SA103F boxes 53 and 53.1. The Self Employed Schedule computes the year's allowance from these inputs at the year file's rate (3%, or 10% for a Freeport or Investment Zone tax site in the enhanced array) over the days of the chargeable period the claim covers -- a stated amount is never carried here.",
1126
+ "required": ["qualifyingAmountExpenditure", "qualifyingDate", "building"],
1127
+ "additionalProperties": false,
1128
+ "properties": {
1129
+ "qualifyingDate": {
1130
+ "type": "string",
1131
+ "format": "date",
1132
+ "description": "The later of the building's first qualifying use and the qualifying expenditure being incurred."
1133
+ },
1134
+ "qualifyingAmountExpenditure": {
1135
+ "type": "number",
1136
+ "minimum": 0
1137
+ },
1138
+ "ceasedDate": {
1139
+ "type": "string",
1140
+ "format": "date",
1141
+ "description": "The day qualifying use or the claimant's relevant interest ended, when it did so within the allowance's period."
1142
+ },
1143
+ "building": {
1144
+ "type": "object",
1145
+ "required": ["postcode"],
1146
+ "additionalProperties": false,
1147
+ "properties": {
1148
+ "name": {
1149
+ "type": "string"
1150
+ },
1151
+ "number": {
1152
+ "type": "string"
1153
+ },
1154
+ "postcode": {
1155
+ "type": "string"
1156
+ }
1157
+ }
1158
+ }
1159
+ }
1160
+ },
1109
1161
  "ledgerListing": {
1110
1162
  "type": "object",
1111
1163
  "description": "One named debtor or creditor balance.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diy-accounting-uk/diya-gl",
3
- "version": "1.2.15",
3
+ "version": "1.2.17",
4
4
  "type": "module",
5
5
  "description": "Recalculate, read and write DIYA-GL books from the command line",
6
6
  "license": "Apache-2.0",