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

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.
@@ -363,6 +363,58 @@
363
363
  "scorecard": "101279 passed, 0 warnings, 0 failed"
364
364
  }
365
365
  }
366
+ },
367
+ {
368
+ "tag": "diya-gl-v1.2.5",
369
+ "date": "2026-09-11",
370
+ "formatVersion": "diya-gl/1",
371
+ "engineVersion": "1.2.5+adcbb86f",
372
+ "taxDataHash": "84585062cad8",
373
+ "reconciledCommit": "320b4b8a1585e16fa170c2d312c14df38d0bcf5b",
374
+ "templates": {
375
+ "bst": {
376
+ "hash": "0d003e10e00c",
377
+ "scorecard": "810 passed, 0 warnings, 0 failed"
378
+ },
379
+ "taxi": {
380
+ "hash": "1ff69654d64d",
381
+ "scorecard": "686 passed, 0 warnings, 0 failed"
382
+ },
383
+ "se": {
384
+ "hash": "e78c4aab0660",
385
+ "scorecard": "7679 passed, 0 warnings, 0 failed"
386
+ },
387
+ "ltd": {
388
+ "hash": "81ffdc956e5a",
389
+ "scorecard": "101279 passed, 0 warnings, 0 failed"
390
+ }
391
+ }
392
+ },
393
+ {
394
+ "tag": "diya-gl-v1.2.6",
395
+ "date": "2026-09-12",
396
+ "formatVersion": "diya-gl/1",
397
+ "engineVersion": "1.2.6+8f36b1c6",
398
+ "taxDataHash": "84585062cad8",
399
+ "reconciledCommit": "1839cf9537c130410e2a180626e7729aca82ffc9",
400
+ "templates": {
401
+ "bst": {
402
+ "hash": "0d003e10e00c",
403
+ "scorecard": "812 passed, 0 warnings, 0 failed"
404
+ },
405
+ "taxi": {
406
+ "hash": "1ff69654d64d",
407
+ "scorecard": "686 passed, 0 warnings, 0 failed"
408
+ },
409
+ "se": {
410
+ "hash": "185e5ad6b61d",
411
+ "scorecard": "7692 passed, 0 warnings, 0 failed"
412
+ },
413
+ "ltd": {
414
+ "hash": "81ffdc956e5a",
415
+ "scorecard": "101537 passed, 0 warnings, 0 failed"
416
+ }
417
+ }
366
418
  }
367
419
  ]
368
420
  }
@@ -353,11 +353,11 @@
353
353
  "section/fixed-asset-schedule/net-book-value-at-the-year-end-schedule-k1": "The report's own Fixed Asset Schedule note. Its rows are the note's arithmetic -- cost carried forward with the disposals removed, the net book value of the assets sold -- not cells, and the Schedule totals they read are already on the Fixed assets view under their own cell keys.",
354
354
  "section/fixed-asset-schedule/net-book-value-of-the-assets-sold-at-the-date-of-sale": "The report's own Fixed Asset Schedule note. Its rows are the note's arithmetic -- cost carried forward with the disposals removed, the net book value of the assets sold -- not cells, and the Schedule totals they read are already on the Fixed assets view under their own cell keys.",
355
355
  "section/fixed-asset-schedule/sale-proceeds-of-the-assets-sold-net-of-vat-schedule-v1": "The report's own Fixed Asset Schedule note. Its rows are the note's arithmetic -- cost carried forward with the disposals removed, the net book value of the assets sold -- not cells, and the Schedule totals they read are already on the Fixed assets view under their own cell keys.",
356
- "section/journal-category-vat-netting/advertising-promotion-purchases-a/downstream": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
357
- "section/journal-category-vat-netting/advertising-promotion-purchases-a/gross": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
358
- "section/journal-category-vat-netting/advertising-promotion-purchases-a/net": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
359
- "section/journal-category-vat-netting/advertising-promotion-purchases-a/residue": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
360
- "section/journal-category-vat-netting/advertising-promotion-purchases-a/vat": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
356
+ "section/journal-category-vat-netting/advertising-promotion-entertainment-purchases-a/downstream": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
357
+ "section/journal-category-vat-netting/advertising-promotion-entertainment-purchases-a/gross": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
358
+ "section/journal-category-vat-netting/advertising-promotion-entertainment-purchases-a/net": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
359
+ "section/journal-category-vat-netting/advertising-promotion-entertainment-purchases-a/residue": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
360
+ "section/journal-category-vat-netting/advertising-promotion-entertainment-purchases-a/vat": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
361
361
  "section/journal-category-vat-netting/bad-debts-written-off-sales-o/downstream": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
362
362
  "section/journal-category-vat-netting/bad-debts-written-off-sales-o/gross": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
363
363
  "section/journal-category-vat-netting/bad-debts-written-off-sales-o/net": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
@@ -451,5 +451,24 @@
451
451
  "section/journal-category-vat-netting/travel-hotel-subsistence-purchases-h/vat": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
452
452
  "section/self-assessment-sa103s/accounting-date": "SE Short's own accounting-date caption (Q2 and V2, echoing Admin!B4 and B17); the form header prints it, the box table does not.",
453
453
  "section/self-assessment-sa103s/business-name": "SE Short's own business-name caption (C8); the Book details view names the business from the book itself, so no box on the SA103S view carries this row.",
454
- "section/self-assessment-sa103s/turnover-note": "SE Short's turnover-note caption (A33), which changes wording at the VAT threshold; a caption, not a numbered box."
454
+ "section/self-assessment-sa103s/turnover-note": "SE Short's turnover-note caption (A33), which changes wording at the VAT threshold; a caption, not a numbered box.",
455
+ "section/journal-category-vat-netting/business-entertainment-memo-purchases-e/downstream": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
456
+ "section/journal-category-vat-netting/business-entertainment-memo-purchases-e/gross": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
457
+ "section/journal-category-vat-netting/business-entertainment-memo-purchases-e/net": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
458
+ "section/journal-category-vat-netting/business-entertainment-memo-purchases-e/residue": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
459
+ "section/journal-category-vat-netting/business-entertainment-memo-purchases-e/vat": "The journal-category-vat-netting report is a reconciliation table (categoryNettingLines, products/se.js) explaining how a gross journal figure nets into a statement cell; no page view is this table.",
460
+ "section/accounting-profit-to-tax-profit-bridge/add-disallowable-expenses-added-back-box-46": "The page has no profit-bridge view; the bridge from accounting profit to tax profit is not printed anywhere on the books page.",
461
+ "cell/Financialaccounts.xlsx!Profit & Loss Account!B49": "The Profit & Loss view renders the statement itself, B5 to B39. Row 49 sits below it as the memo the sheet keeps for disallowable entertainment, so the add-back has a figure to read; it is not a statement line and no page view prints it.",
462
+ "cell/Financialaccounts.xlsx!Profit & Loss Account!C49": "The Profit & Loss view renders the statement itself, B5 to B39. Row 49 sits below it as the memo the sheet keeps for disallowable entertainment, so the add-back has a figure to read; it is not a statement line and no page view prints it.",
463
+ "cell/Financialaccounts.xlsx!Profit & Loss Account!D49": "The Profit & Loss view renders the statement itself, B5 to B39. Row 49 sits below it as the memo the sheet keeps for disallowable entertainment, so the add-back has a figure to read; it is not a statement line and no page view prints it.",
464
+ "cell/Financialaccounts.xlsx!Profit & Loss Account!E49": "The Profit & Loss view renders the statement itself, B5 to B39. Row 49 sits below it as the memo the sheet keeps for disallowable entertainment, so the add-back has a figure to read; it is not a statement line and no page view prints it.",
465
+ "cell/Financialaccounts.xlsx!Profit & Loss Account!F49": "The Profit & Loss view renders the statement itself, B5 to B39. Row 49 sits below it as the memo the sheet keeps for disallowable entertainment, so the add-back has a figure to read; it is not a statement line and no page view prints it.",
466
+ "cell/Financialaccounts.xlsx!Profit & Loss Account!G49": "The Profit & Loss view renders the statement itself, B5 to B39. Row 49 sits below it as the memo the sheet keeps for disallowable entertainment, so the add-back has a figure to read; it is not a statement line and no page view prints it.",
467
+ "cell/Financialaccounts.xlsx!Profit & Loss Account!H49": "The Profit & Loss view renders the statement itself, B5 to B39. Row 49 sits below it as the memo the sheet keeps for disallowable entertainment, so the add-back has a figure to read; it is not a statement line and no page view prints it.",
468
+ "cell/Financialaccounts.xlsx!Profit & Loss Account!I49": "The Profit & Loss view renders the statement itself, B5 to B39. Row 49 sits below it as the memo the sheet keeps for disallowable entertainment, so the add-back has a figure to read; it is not a statement line and no page view prints it.",
469
+ "cell/Financialaccounts.xlsx!Profit & Loss Account!J49": "The Profit & Loss view renders the statement itself, B5 to B39. Row 49 sits below it as the memo the sheet keeps for disallowable entertainment, so the add-back has a figure to read; it is not a statement line and no page view prints it.",
470
+ "cell/Financialaccounts.xlsx!Profit & Loss Account!K49": "The Profit & Loss view renders the statement itself, B5 to B39. Row 49 sits below it as the memo the sheet keeps for disallowable entertainment, so the add-back has a figure to read; it is not a statement line and no page view prints it.",
471
+ "cell/Financialaccounts.xlsx!Profit & Loss Account!L49": "The Profit & Loss view renders the statement itself, B5 to B39. Row 49 sits below it as the memo the sheet keeps for disallowable entertainment, so the add-back has a figure to read; it is not a statement line and no page view prints it.",
472
+ "cell/Financialaccounts.xlsx!Profit & Loss Account!M49": "The Profit & Loss view renders the statement itself, B5 to B39. Row 49 sits below it as the memo the sheet keeps for disallowable entertainment, so the add-back has a figure to read; it is not a statement line and no page view prints it.",
473
+ "cell/Financialaccounts.xlsx!Profit & Loss Account!N49": "The Profit & Loss view renders the statement itself, B5 to B39. Row 49 sits below it as the memo the sheet keeps for disallowable entertainment, so the add-back has a figure to read; it is not a statement line and no page view prints it."
455
474
  }
@@ -282,16 +282,6 @@
282
282
  "products": ["bst", "taxi", "se"],
283
283
  "reason": "The self-employment year files declare one writing_down_allowance rate. The special rate pool is a Company distinction their capital allowance block does not draw."
284
284
  },
285
- {
286
- "path": "tax.corporationTax.associatedCompanies",
287
- "products": ["ltd"],
288
- "reason": "No app/data year file carries a count of associated companies: it is a fact about the group a company sits in, not a rate the generator applied."
289
- },
290
- {
291
- "path": "tax.corporationTax.frankedInvestmentIncome",
292
- "products": ["ltd"],
293
- "reason": "No app/data year file carries the exempt distributions a company received: it is a figure from the company's own year, not a rate the generator applied."
294
- },
295
285
  {
296
286
  "path": "tax.incomeTax.additionalRate",
297
287
  "products": ["ltd"],
@@ -26,6 +26,7 @@ import {
26
26
  STOCK_OPENING_COUNT_CELL,
27
27
  STOCK_CLOSING_COUNT_CELL,
28
28
  BUSINESS_DESCRIPTION_CELL,
29
+ DISALLOWABLE_PERCENT_CELLS,
29
30
  SALESINVOICE_VAT_REG_CELL,
30
31
  SALESINVOICE_TELEPHONE_CELL,
31
32
  SALESINVOICE_PRODUCT_DETAILS_COLUMNS,
@@ -392,6 +393,7 @@ function isVatInputCell(sheet, cellRef) {
392
393
  function isHubInputCell(sheet, cellRef) {
393
394
  if (sheet === "Business Details") return cellRef === "C5" || cellRef === BUSINESS_DESCRIPTION_CELL;
394
395
  if (sheet === "StockControl") return cellRef === STOCK_OPENING_COUNT_CELL || cellRef === STOCK_CLOSING_COUNT_CELL;
396
+ if (sheet === "VitalTax") return Object.values(DISALLOWABLE_PERCENT_CELLS).includes(cellRef);
395
397
  return false;
396
398
  }
397
399
 
@@ -205,7 +205,9 @@ function quarterlyFromMonthlyGrid(pl) {
205
205
  // adjustment mixed in, read off app/lib/scenario-extractor.js's own account
206
206
  // maps by way of the code letters scenario transactions already carry.
207
207
  const SALES_QUARTERLY_ROW = { a: 5, b: 6, c: 7, d: 8 };
208
- const PURCHASES_QUARTERLY_ROW = { s: 14, c: 15, o: 16, w: 21, p: 22, m: 23, g: 24, v: 25, h: 26, a: 27, l: 28, y: 32 };
208
+ // Codes a (advertising) and e (business entertainment) both feed row 27 --
209
+ // box 24's own combined figure -- so both land on it here too.
210
+ const PURCHASES_QUARTERLY_ROW = { s: 14, c: 15, o: 16, w: 21, p: 22, m: 23, g: 24, v: 25, h: 26, a: 27, e: 27, l: 28, y: 32 };
209
211
 
210
212
  const MONTH_KEYS = ["apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec", "jan", "feb", "mar"];
211
213
  const MONTH_SHEET_NAMES = {
@@ -83,6 +83,7 @@ export const PURCHASES_ANALYSIS_COLUMNS = {
83
83
  l: "Z",
84
84
  y: "AA",
85
85
  fa: "AB",
86
+ e: "AC",
86
87
  };
87
88
 
88
89
  // Bank.xlsx and Cash.xlsx analyse a receipt and a payment under different code
@@ -803,7 +804,9 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
803
804
  pl[`${col}24`] = purchasesCode(index, "g");
804
805
  pl[`${col}25`] = purchasesCode(index, "v");
805
806
  pl[`${col}26`] = purchasesCode(index, "h");
806
- pl[`${col}27`] = purchasesCode(index, "a");
807
+ // Advertising and business entertainment share one Purchases analysis
808
+ // column each, but the same Purchases row: box 24 is the combined figure.
809
+ pl[`${col}27`] = purchasesCode(index, "a") + purchasesCode(index, "e");
807
810
  pl[`${col}28`] = purchasesCode(index, "l");
808
811
  pl[`${col}29`] = -salesCode(index, "o");
809
812
  pl[`${col}30`] = bankPayments(bank, index, "J");
@@ -815,9 +818,12 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
815
818
  pl[`${col}37`] = pl[`${col}19`] - pl[`${col}35`];
816
819
  pl[`${col}38`] = bankReceipts(bank, index, "K");
817
820
  pl[`${col}39`] = pl[`${col}37`] + pl[`${col}38`];
821
+ // Row 49: a memo of the entertainment share of row 27, outside the B35
822
+ // administrative-expenses total the way the template's own row 49 is.
823
+ pl[`${col}49`] = purchasesCode(index, "e");
818
824
  });
819
825
  const yearTotal = (row) => sheetSum(MONTH_COLS.map((col) => pl[`${col}${row}`]));
820
- for (const row of [5, 6, 7, 8, 11, 14, 15, 16, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 38]) {
826
+ for (const row of [5, 6, 7, 8, 11, 14, 15, 16, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 38, 49]) {
821
827
  pl[`B${row}`] = yearTotal(row);
822
828
  }
823
829
  pl.B9 = pl.B5 + pl.B6 + pl.B7 + pl.B8;
@@ -827,6 +833,119 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
827
833
  pl.B37 = pl.B19 - pl.B35;
828
834
  pl.B39 = pl.B37 + pl.B38;
829
835
 
836
+ // ── VitalTax ──
837
+ // The summary quotes the three product sales rows and the two direct cost
838
+ // rows, then a quarterly grid of fifteen expense categories: an allowable
839
+ // row read straight off the P&L beside a disallowable row that is a
840
+ // customer-set percentage of that same allowable row -- the source boxes
841
+ // 32 to 45 (SE Full's O66 to O118) read. Built ahead of the two self
842
+ // assessment returns because both read it.
843
+ const quarterSum = (row, quarter) => sheetSum(MONTH_COLS.slice(quarter * 3, quarter * 3 + 3).map((col) => pl[`${col}${row}`]));
844
+ const quarterSumRows = (rows, quarter) => sheetSum(rows.map((row) => quarterSum(row, quarter)));
845
+ const productSales = (quarter) => quarterSum(5, quarter) + quarterSum(6, quarter) + quarterSum(7, quarter);
846
+ const directCosts = (quarter) => quarterSum(14, quarter) + quarterSum(16, quarter);
847
+ const vitalTax = {
848
+ C5: productSales(0),
849
+ D5: productSales(1),
850
+ E5: productSales(2),
851
+ F5: productSales(3),
852
+ C7: directCosts(0),
853
+ D7: directCosts(1),
854
+ E7: directCosts(2),
855
+ F7: directCosts(3),
856
+ };
857
+ vitalTax.G5 = vitalTax.C5 + vitalTax.D5 + vitalTax.E5 + vitalTax.F5;
858
+ vitalTax.G7 = vitalTax.C7 + vitalTax.D7 + vitalTax.E7 + vitalTax.F7;
859
+
860
+ const QUARTER_COLS = [
861
+ ["C", 0],
862
+ ["D", 1],
863
+ ["E", 2],
864
+ ["F", 3],
865
+ ];
866
+
867
+ // Every allowable row the disallowable percentages multiply, keyed by its
868
+ // own row number, reading the P&L row (or rows) its caption names.
869
+ const ALLOWABLE_PL_ROWS = {
870
+ 12: [21], // Wages and staff costs
871
+ 13: [22], // Premises running costs
872
+ 14: [23], // Repairs and renewals
873
+ 15: [24], // General administrative expenses
874
+ 16: [31], // Finance charges (HP interest, lease, bank charges)
875
+ 17: [25, 26], // Car, van and travel expenses
876
+ 18: [27], // Advertising (now the combined advertising + entertainment row)
877
+ 19: [28], // Professional fees
878
+ 20: [29], // Bad debts
879
+ 21: [30], // Interest on bank and other loans
880
+ 22: [32], // Other expenses
881
+ 24: [15], // Payments to subcontractors
882
+ 25: [49], // Business entertainment (the memo row)
883
+ 26: [33, 34], // Loss on disposal + depreciation
884
+ };
885
+ for (const [row, plRows] of Object.entries(ALLOWABLE_PL_ROWS)) {
886
+ for (const [col, quarter] of QUARTER_COLS) vitalTax[`${col}${row}`] = quarterSumRows(plRows, quarter);
887
+ vitalTax[`G${row}`] = vitalTax[`C${row}`] + vitalTax[`D${row}`] + vitalTax[`E${row}`] + vitalTax[`F${row}`];
888
+ }
889
+
890
+ // The fifteen disallowable rows behind boxes 32 to 45. Thirteen are a
891
+ // customer-set percentage of the allowable row beside them; business
892
+ // entertainment (row 44) is wholly disallowable, so it echoes row 25
893
+ // outright, and depreciation (row 49) already has its own source on SE
894
+ // Full (O114 = P&L row 34), so it echoes that P&L row rather than taking a
895
+ // percentage of anything.
896
+ const DISALLOWABLE_ROWS = {
897
+ 36: { allowableRow: 7, key: "costOfGoods" },
898
+ 37: { allowableRow: 24, key: "paymentsToSubcontractors" },
899
+ 38: { allowableRow: 12, key: "wagesAndStaffCosts" },
900
+ 39: { allowableRow: 17, key: "carVanTravelExpenses" },
901
+ 40: { allowableRow: 13, key: "premisesRunningCosts" },
902
+ 41: { allowableRow: 14, key: "maintenanceCosts" },
903
+ 42: { allowableRow: 15, key: "adminCosts" },
904
+ 43: { allowableRow: 18, key: "advertisingCosts" },
905
+ 45: { allowableRow: 21, key: "interestOnBankOtherLoans" },
906
+ 46: { allowableRow: 16, key: "financeCharges" },
907
+ 47: { allowableRow: 20, key: "irrecoverableDebts" },
908
+ 48: { allowableRow: 19, key: "professionalFees" },
909
+ 50: { allowableRow: 22, key: "otherExpenses" },
910
+ };
911
+ const disallowablePercent = scenario.disallowable || {};
912
+ for (const [row, { allowableRow, key }] of Object.entries(DISALLOWABLE_ROWS)) {
913
+ const percent = disallowablePercent[key] || 0;
914
+ for (const [col] of QUARTER_COLS) vitalTax[`${col}${row}`] = vitalTax[`${col}${allowableRow}`] * percent;
915
+ vitalTax[`G${row}`] = vitalTax[`C${row}`] + vitalTax[`D${row}`] + vitalTax[`E${row}`] + vitalTax[`F${row}`];
916
+ }
917
+ for (const [col] of QUARTER_COLS) vitalTax[`${col}44`] = vitalTax[`${col}25`];
918
+ vitalTax.G44 = vitalTax.C44 + vitalTax.D44 + vitalTax.E44 + vitalTax.F44;
919
+ for (const [col, quarter] of QUARTER_COLS) vitalTax[`${col}49`] = quarterSum(34, quarter);
920
+ vitalTax.G49 = vitalTax.C49 + vitalTax.D49 + vitalTax.E49 + vitalTax.F49;
921
+
922
+ // Row 29: the grand total of allowable expenses. Entertainment (row 25) is
923
+ // left out -- it is already inside row 18's P&L!C27:E27 read, which now
924
+ // carries the combined advertising-and-entertainment figure.
925
+ const ROW29_ROWS = [7, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 26];
926
+ for (const [col] of QUARTER_COLS) vitalTax[`${col}29`] = sheetSum(ROW29_ROWS.map((row) => vitalTax[`${col}${row}`]));
927
+ vitalTax.G29 = sheetSum(ROW29_ROWS.map((row) => vitalTax[`G${row}`]));
928
+
929
+ // The thirteen SE Full disallowable boxes (32 to 45 minus 44, which shares
930
+ // box 39 with advertising) read straight off VitalTax's annual column.
931
+ const seFullDisallowable = {
932
+ O66: vitalTax.G36,
933
+ O70: vitalTax.G37,
934
+ O74: vitalTax.G38,
935
+ O78: vitalTax.G39,
936
+ O82: vitalTax.G40,
937
+ O86: vitalTax.G41,
938
+ O90: vitalTax.G42,
939
+ O94: vitalTax.G43 + vitalTax.G44,
940
+ O98: vitalTax.G45,
941
+ O102: vitalTax.G46,
942
+ O106: vitalTax.G47,
943
+ O110: vitalTax.G48,
944
+ O118: vitalTax.G50,
945
+ };
946
+ // Box 46, boxes 32 to 45 summed. seShort.O64 (box 20) reads the same total.
947
+ const boxes32to45Total = sheetSum(Object.values(seFullDisallowable)) + pl.B34;
948
+
830
949
  // ── The fixed asset workbook ──
831
950
  const scheduleCells = { E57: schedule.existing.E, E110: schedule.additions.E };
832
951
  for (const column of SCHEDULE_TOTAL_COLUMNS) scheduleCells[`${column}1`] = schedule.totals[column];
@@ -907,16 +1026,27 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
907
1026
  seShort.D38 > admin.F26
908
1027
  ? `SELF-EMPLOYMENT FULL RETURN REQUIRED AS TURNOVER EXCEEDS £${admin.F26} VAT threshold`
909
1028
  : `Business income - if your annual turnover was below £${admin.F26} VAT threshold`;
910
- seShort.D46 = analysed(pl.B17);
911
- seShort.O46 = analysed(pl.B28);
912
- seShort.D51 = analysed(pl.B25 + pl.B26);
913
- seShort.O51 = analysed(pl.B30 + pl.B31);
914
- seShort.D55 = analysed(pl.B21);
915
- seShort.O55 = analysed(pl.B24);
916
- seShort.D60 = analysed(pl.B22);
917
- seShort.O60 = analysed(pl.B27 + pl.B29 + pl.B32 + pl.B33);
918
- seShort.D64 = analysed(pl.B23);
919
- seShort.O64 = pl.B17 + pl.B35 - pl.B34;
1029
+ // Boxes 11 to 19 go net of their own share of boxes 32 to 45: SA103S has no
1030
+ // disallowable column of its own, so each allowable figure drops the
1031
+ // matching SE Full box before it reaches the short return. Box 33
1032
+ // (subcontractor payments) has no short-return box of its own -- SA103S
1033
+ // folds it into box 11 -- so D46 drops both box 32's and box 33's shares.
1034
+ seShort.D46 = analysed(pl.B17 - seFullDisallowable.O66 - seFullDisallowable.O70);
1035
+ seShort.O46 = analysed(pl.B28 - seFullDisallowable.O110);
1036
+ seShort.D51 = analysed(pl.B25 + pl.B26 - seFullDisallowable.O78);
1037
+ seShort.O51 = analysed(pl.B30 + pl.B31 - seFullDisallowable.O98 - seFullDisallowable.O102);
1038
+ seShort.D55 = analysed(pl.B21 - seFullDisallowable.O74);
1039
+ seShort.O55 = analysed(pl.B24 - seFullDisallowable.O90);
1040
+ seShort.D60 = analysed(pl.B22 - seFullDisallowable.O82);
1041
+ // Box 19's base (B27+B29+B32+B33) never includes depreciation (P&L row 34
1042
+ // sits outside it, in row 33's disposal loss instead), so nothing here
1043
+ // reads box 44's O114 -- box 20 (O64) is where depreciation's disallowable
1044
+ // share leaves the total. O94 already carries the whole of box 39's
1045
+ // entertainment share (VitalTax row 44 echoes row 25, the entertainment
1046
+ // memo's own annual total), so box 19 does not also subtract P&L!B49.
1047
+ seShort.O60 = analysed(pl.B27 + pl.B29 + pl.B32 + pl.B33 - seFullDisallowable.O94 - seFullDisallowable.O106 - seFullDisallowable.O118);
1048
+ seShort.D64 = analysed(pl.B23 - seFullDisallowable.O86);
1049
+ seShort.O64 = pl.B17 + pl.B35 - boxes32to45Total;
920
1050
  const shortNetProfit = seShort.D38 + seShort.O38 - seShort.O64;
921
1051
  seShort.D71 = shortNetProfit >= 0 ? shortNetProfit : 0;
922
1052
  seShort.O71 = shortNetProfit < 0 ? -shortNetProfit : 0;
@@ -969,43 +1099,23 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
969
1099
  seFull.D118 = pl.B32;
970
1100
  seFull.D122 = pl.B17 + pl.B35;
971
1101
  seFull.O114 = pl.B34;
972
- // Boxes 32 to 45, the disallowable side of each expense category. None has
973
- // a source yet -- a later change gives each one a book field -- so every
974
- // cell but box 44's depreciation stays the blank the sheet prints today.
975
- seFull.O66 = SHEET_BLANK;
976
- seFull.O70 = SHEET_BLANK;
977
- seFull.O74 = SHEET_BLANK;
978
- seFull.O78 = SHEET_BLANK;
979
- seFull.O82 = SHEET_BLANK;
980
- seFull.O86 = SHEET_BLANK;
981
- seFull.O90 = SHEET_BLANK;
982
- seFull.O94 = SHEET_BLANK;
983
- seFull.O98 = SHEET_BLANK;
984
- seFull.O102 = SHEET_BLANK;
985
- seFull.O106 = SHEET_BLANK;
986
- seFull.O110 = SHEET_BLANK;
987
- seFull.O118 = SHEET_BLANK;
988
- // Box 46's own caption calls it the total of boxes 32 to 45, not a second
989
- // read of the depreciation figure box 44 already carries. The sum stays
990
- // equal to box 44 alone until the boxes above gain a source.
991
- seFull.O122 = carry([seFull.O114], () =>
992
- sheetSum([
993
- seFull.O66,
994
- seFull.O70,
995
- seFull.O74,
996
- seFull.O78,
997
- seFull.O82,
998
- seFull.O86,
999
- seFull.O90,
1000
- seFull.O94,
1001
- seFull.O98,
1002
- seFull.O102,
1003
- seFull.O106,
1004
- seFull.O110,
1005
- seFull.O114,
1006
- seFull.O118,
1007
- ]),
1008
- );
1102
+ // Boxes 32 to 45, the disallowable side of each expense category, sourced
1103
+ // from the VitalTax percentages built earlier.
1104
+ seFull.O66 = seFullDisallowable.O66;
1105
+ seFull.O70 = seFullDisallowable.O70;
1106
+ seFull.O74 = seFullDisallowable.O74;
1107
+ seFull.O78 = seFullDisallowable.O78;
1108
+ seFull.O82 = seFullDisallowable.O82;
1109
+ seFull.O86 = seFullDisallowable.O86;
1110
+ seFull.O90 = seFullDisallowable.O90;
1111
+ seFull.O94 = seFullDisallowable.O94;
1112
+ seFull.O98 = seFullDisallowable.O98;
1113
+ seFull.O102 = seFullDisallowable.O102;
1114
+ seFull.O106 = seFullDisallowable.O106;
1115
+ seFull.O110 = seFullDisallowable.O110;
1116
+ seFull.O118 = seFullDisallowable.O118;
1117
+ // Box 46's own caption calls it the total of boxes 32 to 45.
1118
+ seFull.O122 = carry([seFull.O114], () => boxes32to45Total);
1009
1119
  const fullNetProfit = seFull.D55 + seFull.O55 - seFull.D122;
1010
1120
  seFull.D129 = fullNetProfit >= 0 ? fullNetProfit : 0;
1011
1121
  seFull.O129 = fullNetProfit < 0 ? -fullNetProfit : 0;
@@ -1127,25 +1237,6 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
1127
1237
  sheetSum([forecast.C42, forecast.C43, forecast.C44, forecast.C45]),
1128
1238
  );
1129
1239
 
1130
- // ── VitalTax ──
1131
- // The summary quotes the three product sales rows and the two direct cost
1132
- // rows, nothing else.
1133
- const quarterSum = (row, quarter) => sheetSum(MONTH_COLS.slice(quarter * 3, quarter * 3 + 3).map((col) => pl[`${col}${row}`]));
1134
- const productSales = (quarter) => quarterSum(5, quarter) + quarterSum(6, quarter) + quarterSum(7, quarter);
1135
- const directCosts = (quarter) => quarterSum(14, quarter) + quarterSum(16, quarter);
1136
- const vitalTax = {
1137
- C5: productSales(0),
1138
- D5: productSales(1),
1139
- E5: productSales(2),
1140
- F5: productSales(3),
1141
- C7: directCosts(0),
1142
- D7: directCosts(1),
1143
- E7: directCosts(2),
1144
- F7: directCosts(3),
1145
- };
1146
- vitalTax.G5 = vitalTax.C5 + vitalTax.D5 + vitalTax.E5 + vitalTax.F5;
1147
- vitalTax.G7 = vitalTax.C7 + vitalTax.D7 + vitalTax.E7 + vitalTax.F7;
1148
-
1149
1240
  // ── The results, keyed the way the reconciliation reads them ──
1150
1241
  const results = {
1151
1242
  "Business Details": { C5: businessName(scenario) },
@@ -448,6 +448,10 @@ export function diyaGlToScenario(book, lines, product) {
448
448
  scenario.stock = { opening: book.stock.openingValue, closing: book.stock.closingValue };
449
449
  if (book.stock.materialsPercent !== undefined) scenario.stock.materials_percent = book.stock.materialsPercent;
450
450
  }
451
+ // The disallowable proportions the SA103F boxes are computed from. Without
452
+ // them the calculator adds back only the wholly disallowable pair and
453
+ // charges tax on a larger profit than the package's own sheet does.
454
+ if (book.tax?.selfEmployment) scenario.disallowable = { ...book.tax.selfEmployment };
451
455
  if (book.debtors) {
452
456
  scenario.opening_debtors = ledgerListing(book.debtors, "opening", "customer");
453
457
  scenario.closing_debtors = ledgerListing(book.debtors, "closing", "customer");
@@ -12,25 +12,25 @@
12
12
 
13
13
  export const PROVENANCE_DATA = {
14
14
  formatVersion: "diya-gl/1",
15
- engineVersion: "1.2.5+adcbb86f",
15
+ engineVersion: "1.2.7+2c047a2a",
16
16
  taxDataHash: "84585062cad8",
17
- reconciledCommit: "320b4b8a1585e16fa170c2d312c14df38d0bcf5b",
17
+ reconciledCommit: "1839cf9537c130410e2a180626e7729aca82ffc9",
18
18
  templates: {
19
19
  bst: {
20
20
  hash: "0d003e10e00c",
21
- scorecard: "810 passed, 0 warnings, 0 failed",
21
+ scorecard: "812 passed, 0 warnings, 0 failed",
22
22
  },
23
23
  taxi: {
24
24
  hash: "1ff69654d64d",
25
25
  scorecard: "686 passed, 0 warnings, 0 failed",
26
26
  },
27
27
  se: {
28
- hash: "e78c4aab0660",
29
- scorecard: "7679 passed, 0 warnings, 0 failed",
28
+ hash: "185e5ad6b61d",
29
+ scorecard: "7692 passed, 0 warnings, 0 failed",
30
30
  },
31
31
  ltd: {
32
32
  hash: "81ffdc956e5a",
33
- scorecard: "101279 passed, 0 warnings, 0 failed",
33
+ scorecard: "101565 passed, 0 warnings, 0 failed",
34
34
  },
35
35
  },
36
36
  };
@@ -37,6 +37,7 @@ export const LTD_PURCHASE_CODE_MAP = {
37
37
  5401: "u",
38
38
  5500: "a",
39
39
  5501: "g",
40
+ 5502: "a", // business entertainment -> Advertising, the same category Ltd's own book.toml column folds it into
40
41
  5600: "h",
41
42
  5601: "v",
42
43
  5700: "n",
@@ -74,7 +75,7 @@ export const BST_PURCHASE_CODE_MAP = {
74
75
  5900: "f", // Fixed assets
75
76
  };
76
77
 
77
- // SE purchases: accountMainID -> SE code letter (21 codes)
78
+ // SE purchases: accountMainID -> SE code letter (22 codes)
78
79
  export const SE_PURCHASE_CODE_MAP = {
79
80
  5000: "s",
80
81
  5001: "c",
@@ -88,6 +89,7 @@ export const SE_PURCHASE_CODE_MAP = {
88
89
  5401: "y", // general shopping -> Other Expenses
89
90
  5500: "a",
90
91
  5501: "g",
92
+ 5502: "e", // business entertainment -> its own Purchases analysis column
91
93
  5600: "h",
92
94
  5601: "v",
93
95
  5700: "y", // insurance -> Other Expenses (no insurance column)
@@ -1135,6 +1137,15 @@ export function formatScenarioToml(metadata, grouped, expected) {
1135
1137
  parts.push("");
1136
1138
  }
1137
1139
 
1140
+ // The share of each SA103F expense category (32 to 45) that is private use
1141
+ // or otherwise disallowable, as a fraction (0.1 = 10%). A book setting, not
1142
+ // a transaction: no journal line carries it.
1143
+ if (expected.disallowable) {
1144
+ parts.push("[disallowable]");
1145
+ for (const [key, percent] of Object.entries(expected.disallowable)) parts.push(`${key} = ${percent}`);
1146
+ parts.push("");
1147
+ }
1148
+
1138
1149
  // Charges and debentures registered over the company's assets (Ltd). Each
1139
1150
  // one secures a creditor falling due after more than one year.
1140
1151
  if (expected.charges) {
@@ -1381,7 +1392,12 @@ export function buildSubsetBook(
1381
1392
  entityInformation: entity,
1382
1393
  accounts: accountFilter(book.accounts),
1383
1394
  tax: Object.fromEntries(
1384
- taxSections.filter((section) => book.tax[section]).map((section) => [section, { ...book.tax[section], ...taxOverrides[section] }]),
1395
+ // A section the master book has none of still belongs to the subset when
1396
+ // the override carries the whole of it: the trader's own disallowable
1397
+ // proportions are a fact about the subset, not a rate the master states.
1398
+ taxSections
1399
+ .filter((section) => book.tax[section] || taxOverrides[section])
1400
+ .map((section) => [section, { ...book.tax[section], ...taxOverrides[section] }]),
1385
1401
  ),
1386
1402
  };
1387
1403
  if (directors) subsetBook.directors = directors;