@diy-accounting-uk/diya-gl 1.2.12 → 1.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/data/releases.json +52 -0
- package/dist/app/data/render-unrepresentable/se.json +6 -1
- package/dist/app/lib/anchors/se.js +16 -3
- package/dist/app/lib/calculators/se-derivations.js +12 -11
- package/dist/app/lib/calculators/se.js +77 -30
- package/dist/app/lib/diya-gl-loader.js +24 -0
- package/dist/app/lib/provenance-data.js +3 -3
- package/dist/app/lib/scenario-extractor.js +9 -0
- package/dist/app/lib/xlsx-exporter.js +12 -0
- package/dist/app/products/se.js +218 -41
- package/dist/web/spreadsheets.diyaccounting.co.uk/public/schema/diya-gl-book-v2.schema.json +11 -0
- package/package.json +1 -1
|
@@ -545,6 +545,58 @@
|
|
|
545
545
|
"scorecard": "101605 passed, 0 warnings, 0 failed"
|
|
546
546
|
}
|
|
547
547
|
}
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
"tag": "diya-gl-v1.2.12",
|
|
551
|
+
"date": "2026-09-14",
|
|
552
|
+
"formatVersion": "diya-gl/1",
|
|
553
|
+
"engineVersion": "1.2.12+8ad4ddf1",
|
|
554
|
+
"taxDataHash": "4ecdbe985d22",
|
|
555
|
+
"reconciledCommit": "c910fdeefbe4f2f71ae9ec5f5d4a713783a1c0c7",
|
|
556
|
+
"templates": {
|
|
557
|
+
"bst": {
|
|
558
|
+
"hash": "0d003e10e00c",
|
|
559
|
+
"scorecard": "812 passed, 0 warnings, 0 failed"
|
|
560
|
+
},
|
|
561
|
+
"taxi": {
|
|
562
|
+
"hash": "1ff69654d64d",
|
|
563
|
+
"scorecard": "686 passed, 0 warnings, 0 failed"
|
|
564
|
+
},
|
|
565
|
+
"se": {
|
|
566
|
+
"hash": "4decdcb12664",
|
|
567
|
+
"scorecard": "7952 passed, 0 warnings, 0 failed"
|
|
568
|
+
},
|
|
569
|
+
"ltd": {
|
|
570
|
+
"hash": "caa14632083b",
|
|
571
|
+
"scorecard": "101605 passed, 0 warnings, 0 failed"
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
"tag": "diya-gl-v1.2.13",
|
|
577
|
+
"date": "2026-09-14",
|
|
578
|
+
"formatVersion": "diya-gl/1",
|
|
579
|
+
"engineVersion": "1.2.13+f5b44cdf",
|
|
580
|
+
"taxDataHash": "4ecdbe985d22",
|
|
581
|
+
"reconciledCommit": "c910fdeefbe4f2f71ae9ec5f5d4a713783a1c0c7",
|
|
582
|
+
"templates": {
|
|
583
|
+
"bst": {
|
|
584
|
+
"hash": "0d003e10e00c",
|
|
585
|
+
"scorecard": "812 passed, 0 warnings, 0 failed"
|
|
586
|
+
},
|
|
587
|
+
"taxi": {
|
|
588
|
+
"hash": "1ff69654d64d",
|
|
589
|
+
"scorecard": "686 passed, 0 warnings, 0 failed"
|
|
590
|
+
},
|
|
591
|
+
"se": {
|
|
592
|
+
"hash": "15d3b753f7a4",
|
|
593
|
+
"scorecard": "7956 passed, 0 warnings, 0 failed"
|
|
594
|
+
},
|
|
595
|
+
"ltd": {
|
|
596
|
+
"hash": "caa14632083b",
|
|
597
|
+
"scorecard": "101605 passed, 0 warnings, 0 failed"
|
|
598
|
+
}
|
|
599
|
+
}
|
|
548
600
|
}
|
|
549
601
|
]
|
|
550
602
|
}
|
|
@@ -461,5 +461,10 @@
|
|
|
461
461
|
"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.",
|
|
462
462
|
"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.",
|
|
463
463
|
"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.",
|
|
464
|
-
"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."
|
|
464
|
+
"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.",
|
|
465
|
+
"cell/Fixedassets.xlsx!Schedule!M40": "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.",
|
|
466
|
+
"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
|
+
"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
|
+
"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."
|
|
465
470
|
}
|
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
ANNUAL_ADJUSTMENT_CELLS,
|
|
32
32
|
GOODS_FOR_OWN_USE_CELL,
|
|
33
33
|
SPECIAL_RATE_POOL_MARKER_COLUMN,
|
|
34
|
+
SINGLE_ASSET_POOL_MARKER_COLUMN,
|
|
35
|
+
PRIVATE_USE_COLUMN,
|
|
34
36
|
SALESINVOICE_VAT_REG_CELL,
|
|
35
37
|
SALESINVOICE_TELEPHONE_CELL,
|
|
36
38
|
SALESINVOICE_PRODUCT_DETAILS_COLUMNS,
|
|
@@ -355,14 +357,25 @@ function isPayslipsInputCell(sheet, cellRef) {
|
|
|
355
357
|
return isPayslipsMonthInputCell(sheet, cellRef);
|
|
356
358
|
}
|
|
357
359
|
|
|
358
|
-
// Schedule: an opening asset's description/cost/acc-dep/tax-wdv (C, E, F, O)
|
|
359
|
-
//
|
|
360
|
+
// Schedule: an opening asset's description/cost/acc-dep/tax-wdv (C, E, F, O),
|
|
361
|
+
// its private use share (M), its special rate pool marker (AB) and its single
|
|
362
|
+
// asset pool marker (AD) on its existing-asset row, or an
|
|
360
363
|
// in-year disposal's date/proceeds (U, V) on that same row; a new purchase's
|
|
361
364
|
// date/supplier/cost (B, C, E) on a New Plant & Machinery row. See
|
|
362
365
|
// app/products/se.js's cellWrites() Schedule writer.
|
|
363
366
|
const EXISTING_SCHEDULE_ROWS = new Set([...EXISTING_ASSET_ROWS.motor, ...EXISTING_ASSET_ROWS.computer]);
|
|
364
367
|
const NEW_SCHEDULE_ROWS = new Set(NEW_PLANT_ROWS);
|
|
365
|
-
const EXISTING_SCHEDULE_COLUMNS = [
|
|
368
|
+
const EXISTING_SCHEDULE_COLUMNS = [
|
|
369
|
+
"C",
|
|
370
|
+
"E",
|
|
371
|
+
"F",
|
|
372
|
+
PRIVATE_USE_COLUMN,
|
|
373
|
+
"O",
|
|
374
|
+
"U",
|
|
375
|
+
"V",
|
|
376
|
+
SPECIAL_RATE_POOL_MARKER_COLUMN,
|
|
377
|
+
SINGLE_ASSET_POOL_MARKER_COLUMN,
|
|
378
|
+
];
|
|
366
379
|
const NEW_SCHEDULE_COLUMNS = ["B", "C", "E"];
|
|
367
380
|
const HP_ROWS = new Set(HP_AGREEMENT_ROWS);
|
|
368
381
|
const HP_COLUMNS = ["B", "C", "D", "E", "F", "G", "H", "L"];
|
|
@@ -479,7 +479,6 @@ const BOOK_STATED_ANNUAL_BOXES = [
|
|
|
479
479
|
// year -- annualFieldsUnavailableForYear() below is what keeps a year that
|
|
480
480
|
// no longer accepts a field from warning about it as if it still did.
|
|
481
481
|
const NO_SOURCE_ANNUAL_BOXES = [
|
|
482
|
-
{ box: "51", pick: 1 }, // allowances.capitalAllowanceSingleAssetPool (shared with box 50)
|
|
483
482
|
{ box: "53.1", pick: 0 }, // allowances.enhancedStructuredBuildingAllowance
|
|
484
483
|
{ box: "55", pick: 1 }, // allowances.businessPremisesRenovationAllowance (shared with enhancedCapitalAllowance)
|
|
485
484
|
{ box: "59", pick: 1 }, // adjustments.balancingChargeBpra (shared with balancingChargeOther)
|
|
@@ -541,9 +540,19 @@ export function buildSelfEmploymentAnnualSubmission(book, lines, taxData, option
|
|
|
541
540
|
const root = {};
|
|
542
541
|
const cellNumber = (cell) => (typeof seFull[cell] === "number" ? seFull[cell] : 0);
|
|
543
542
|
setPath(root, primaryField(boxes, "49"), round2(seFull.D139));
|
|
544
|
-
|
|
543
|
+
// The printed boxes 50 and 51 hold every writing down allowance by rate,
|
|
544
|
+
// single asset pools included; the API files the single asset pools
|
|
545
|
+
// (Schedule AE and AF, the rows marked P) under their own field and the
|
|
546
|
+
// pooled balance under each rate's field.
|
|
547
|
+
const schedule = rawResults["Fixedassets.xlsx!Schedule"];
|
|
548
|
+
const singleAssetMain = typeof schedule?.AE1 === "number" ? schedule.AE1 : 0;
|
|
549
|
+
const singleAssetSpecial = typeof schedule?.AF1 === "number" ? schedule.AF1 : 0;
|
|
550
|
+
setPath(root, primaryField(boxes, "50"), round2(seFull.D144 - singleAssetMain));
|
|
545
551
|
// Box 51 is the schedule's special rate pool (column AC, the rows marked S).
|
|
546
|
-
setPath(root, primaryField(boxes, "51"), round2(seFull.D147));
|
|
552
|
+
setPath(root, primaryField(boxes, "51"), round2(seFull.D147 - singleAssetSpecial));
|
|
553
|
+
if (singleAssetMain + singleAssetSpecial !== 0) {
|
|
554
|
+
setPath(root, fieldsOf(boxEntry(boxes, "50"))[1], round2(singleAssetMain + singleAssetSpecial));
|
|
555
|
+
}
|
|
547
556
|
// HMRC's mapping files box 54 and box 55 under one field: the small pools
|
|
548
557
|
// write-off the sheet computes at O144 and the charge-point figure the book
|
|
549
558
|
// states at O139 both reach it.
|
|
@@ -605,14 +614,6 @@ export function buildSelfEmploymentAnnualSubmission(book, lines, taxData, option
|
|
|
605
614
|
"SA103F box 54 has no formula behind it and the book states no tax.selfEmployment.allowances.electricChargePointAllowance; the box stays blank.",
|
|
606
615
|
});
|
|
607
616
|
}
|
|
608
|
-
if (cellNumber("D210") !== 0) {
|
|
609
|
-
warnings.push({
|
|
610
|
-
field: primaryField(boxes, "71"),
|
|
611
|
-
reason:
|
|
612
|
-
"SE Full carries box 71 into box 77 (D219 = O179+E197+D210+P190) and not into box 73 (O194 = O174); HMRC computes the adjusted profit from the filed field, so the sheet's printed box 73 and its Income Tax sheet leave it out.",
|
|
613
|
-
handComputed: round2(seFull.O174 + cellNumber("D210")),
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
617
|
for (const { box: boxNumber, pick } of NO_SOURCE_ANNUAL_BOXES) {
|
|
617
618
|
const entry = boxEntry(boxes, boxNumber);
|
|
618
619
|
const field = fieldsOf(entry)[pick];
|
|
@@ -440,9 +440,24 @@ function payrollMonths(payroll) {
|
|
|
440
440
|
* That is what the sheet reports for a disposal with no tax value entered, so
|
|
441
441
|
* it is what this reports too.
|
|
442
442
|
*/
|
|
443
|
-
function scheduleRow({
|
|
443
|
+
function scheduleRow({
|
|
444
|
+
cost,
|
|
445
|
+
accDep = 0,
|
|
446
|
+
taxWdv,
|
|
447
|
+
depRate,
|
|
448
|
+
aiaRate,
|
|
449
|
+
wdaRate,
|
|
450
|
+
specialRate = 0,
|
|
451
|
+
pool,
|
|
452
|
+
singleAssetPool = false,
|
|
453
|
+
privateUse = 0,
|
|
454
|
+
disposal,
|
|
455
|
+
}) {
|
|
444
456
|
const written = cost > 0;
|
|
445
457
|
const isNewAsset = aiaRate !== undefined;
|
|
458
|
+
// The car rows multiply the allowances by (1-M), the business share; M is
|
|
459
|
+
// nil on every other row, so the factor is 1 there.
|
|
460
|
+
const businessShare = 1 - privateUse;
|
|
446
461
|
const row = { E: cost, F: accDep, H: depRate };
|
|
447
462
|
// A New asset block has no opening net book value column at all, so the row
|
|
448
463
|
// shows nothing there and the depreciation charge below is not capped.
|
|
@@ -459,8 +474,8 @@ function scheduleRow({ cost, accDep = 0, taxWdv, depRate, aiaRate, wdaRate, spec
|
|
|
459
474
|
row.O = taxWdv === undefined ? SHEET_BLANK : taxWdv;
|
|
460
475
|
row.Q = SHEET_BLANK;
|
|
461
476
|
const claimsWritingDown = typeof row.O === "number" && row.O > 0;
|
|
462
|
-
row.R = claimsWritingDown && !specialPool ? row.O * wdaRate : SHEET_BLANK;
|
|
463
|
-
row.AC = claimsWritingDown && specialPool ? row.O * specialRate : SHEET_BLANK;
|
|
477
|
+
row.R = claimsWritingDown && !specialPool ? row.O * wdaRate * businessShare : SHEET_BLANK;
|
|
478
|
+
row.AC = claimsWritingDown && specialPool ? row.O * specialRate * businessShare : SHEET_BLANK;
|
|
464
479
|
row.S = claimsWritingDown ? row.O - sheetNumber(row.R) - sheetNumber(row.AC) : SHEET_BLANK;
|
|
465
480
|
} else {
|
|
466
481
|
row.O = SHEET_BLANK;
|
|
@@ -470,6 +485,12 @@ function scheduleRow({ cost, accDep = 0, taxWdv, depRate, aiaRate, wdaRate, spec
|
|
|
470
485
|
row.AC = SHEET_BLANK;
|
|
471
486
|
row.S = written ? cost - sheetNumber(row.Q) : SHEET_BLANK;
|
|
472
487
|
}
|
|
488
|
+
// Column AD marks a row "P" for a single asset pool: AE and AF repeat its
|
|
489
|
+
// allowance by rate and AG its written-down value, so the small pools
|
|
490
|
+
// test can leave the row out. The row's own R, AC and S do not change.
|
|
491
|
+
row.AE = singleAssetPool && !specialPool ? sheetNumber(row.R) : SHEET_BLANK;
|
|
492
|
+
row.AF = singleAssetPool && specialPool ? sheetNumber(row.AC) : SHEET_BLANK;
|
|
493
|
+
row.AG = singleAssetPool ? sheetNumber(row.S) : SHEET_BLANK;
|
|
473
494
|
|
|
474
495
|
row.V = disposal ? disposal.proceeds : SHEET_BLANK;
|
|
475
496
|
row.W = disposal ? cost : SHEET_BLANK;
|
|
@@ -481,8 +502,8 @@ function scheduleRow({ cost, accDep = 0, taxWdv, depRate, aiaRate, wdaRate, spec
|
|
|
481
502
|
row.Y = SHEET_ERROR;
|
|
482
503
|
row.Z = SHEET_BLANK;
|
|
483
504
|
} else {
|
|
484
|
-
row.Y = disposal.proceeds < row.S ? row.S - disposal.proceeds : SHEET_BLANK;
|
|
485
|
-
row.Z = disposal.proceeds > row.S ? disposal.proceeds - row.S : SHEET_BLANK;
|
|
505
|
+
row.Y = disposal.proceeds < row.S ? (row.S - disposal.proceeds) * businessShare : SHEET_BLANK;
|
|
506
|
+
row.Z = disposal.proceeds > row.S ? (disposal.proceeds - row.S) * businessShare : SHEET_BLANK;
|
|
486
507
|
}
|
|
487
508
|
return row;
|
|
488
509
|
}
|
|
@@ -500,7 +521,7 @@ function carry(inputs, compute) {
|
|
|
500
521
|
return inputs.some((value) => value === SHEET_ERROR) ? SHEET_ERROR : compute();
|
|
501
522
|
}
|
|
502
523
|
|
|
503
|
-
const SCHEDULE_TOTAL_COLUMNS = ["E", "F", "G", "I", "J", "K", "O", "Q", "R", "S", "V", "W", "X", "Y", "Z", "AC"];
|
|
524
|
+
const SCHEDULE_TOTAL_COLUMNS = ["E", "F", "G", "I", "J", "K", "O", "Q", "R", "S", "V", "W", "X", "Y", "Z", "AC", "AE", "AF", "AG"];
|
|
504
525
|
|
|
505
526
|
function scheduleTotals(rows) {
|
|
506
527
|
const totals = {};
|
|
@@ -546,21 +567,34 @@ function buildSchedule(scenario, taxData, rate) {
|
|
|
546
567
|
});
|
|
547
568
|
|
|
548
569
|
const existingRows = [];
|
|
570
|
+
// The motor rows' own single asset pool cells, keyed by cell reference:
|
|
571
|
+
// the private use share the writer puts in M, the marker in AD, and the
|
|
572
|
+
// row's AE and AG, so a reconciliation that reads the marked row finds
|
|
573
|
+
// the same figures here.
|
|
574
|
+
const motorRowCells = {};
|
|
549
575
|
for (const [category, block] of Object.entries(EXISTING_ASSET_BLOCKS)) {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
);
|
|
563
|
-
|
|
576
|
+
assetsByCategory[category].slice(0, block.rows.length).forEach((asset, index) => {
|
|
577
|
+
const row = scheduleRow({
|
|
578
|
+
cost: asset.cost,
|
|
579
|
+
accDep: asset.acc_dep || 0,
|
|
580
|
+
taxWdv: asset.tax_wdv,
|
|
581
|
+
depRate: depreciation[block.rateKey] ?? 0,
|
|
582
|
+
wdaRate,
|
|
583
|
+
specialRate,
|
|
584
|
+
pool: asset.pool,
|
|
585
|
+
singleAssetPool: asset.single_asset_pool === true,
|
|
586
|
+
privateUse: category === "motor" ? asset.private_use || 0 : 0,
|
|
587
|
+
disposal: disposalByAsset.get(asset),
|
|
588
|
+
});
|
|
589
|
+
existingRows.push(row);
|
|
590
|
+
if (category === "motor") {
|
|
591
|
+
const rowNumber = block.rows[index];
|
|
592
|
+
motorRowCells[`M${rowNumber}`] = asset.private_use || 0;
|
|
593
|
+
motorRowCells[`AD${rowNumber}`] = asset.single_asset_pool ? "P" : SHEET_BLANK;
|
|
594
|
+
motorRowCells[`AE${rowNumber}`] = row.AE;
|
|
595
|
+
motorRowCells[`AG${rowNumber}`] = row.AG;
|
|
596
|
+
}
|
|
597
|
+
});
|
|
564
598
|
}
|
|
565
599
|
|
|
566
600
|
const newRows = capitalPurchases
|
|
@@ -569,7 +603,7 @@ function buildSchedule(scenario, taxData, rate) {
|
|
|
569
603
|
|
|
570
604
|
const existing = scheduleTotals(existingRows);
|
|
571
605
|
const additions = scheduleTotals(newRows);
|
|
572
|
-
return { existing, additions, totals: addScheduleTotals(existing, additions) };
|
|
606
|
+
return { existing, additions, totals: addScheduleTotals(existing, additions), motorRowCells };
|
|
573
607
|
}
|
|
574
608
|
|
|
575
609
|
// ── Hire purchase ──────────────────────────────────────────────────────────
|
|
@@ -960,7 +994,7 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
|
|
|
960
994
|
const boxes32to45Total = sheetSum(Object.values(seFullDisallowable)) + pl.B34;
|
|
961
995
|
|
|
962
996
|
// ── The fixed asset workbook ──
|
|
963
|
-
const scheduleCells = { E57: schedule.existing.E, E110: schedule.additions.E, AC4: admin.G6 };
|
|
997
|
+
const scheduleCells = { E57: schedule.existing.E, E110: schedule.additions.E, AC4: admin.G6, ...schedule.motorRowCells };
|
|
964
998
|
for (const column of SCHEDULE_TOTAL_COLUMNS) scheduleCells[`${column}1`] = schedule.totals[column];
|
|
965
999
|
const faReconciliation = {
|
|
966
1000
|
E11: schedule.additions.E,
|
|
@@ -1024,6 +1058,16 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
|
|
|
1024
1058
|
const scheduleR = schedule.totals.R;
|
|
1025
1059
|
const scheduleAC = schedule.totals.AC;
|
|
1026
1060
|
const scheduleS = schedule.totals.S;
|
|
1061
|
+
// The small pools write-off runs over the pooled balance alone: the
|
|
1062
|
+
// single asset pool rows (AE, AF, AG) come out of both the £1,000 test
|
|
1063
|
+
// and the figure written off. The sheet reads
|
|
1064
|
+
// IF((R1+S1-AE1-AF1-AG1)<1000,S1-AG1,0) on SE Full!O144 and SE Short!D85.
|
|
1065
|
+
const scheduleAE = schedule.totals.AE;
|
|
1066
|
+
const scheduleAF = schedule.totals.AF;
|
|
1067
|
+
const scheduleAG = schedule.totals.AG;
|
|
1068
|
+
const smallPoolsWriteOff = carry([scheduleR, scheduleS, scheduleAE, scheduleAF, scheduleAG], () =>
|
|
1069
|
+
scheduleR + scheduleS - scheduleAE - scheduleAF - scheduleAG < 1000 ? scheduleS - scheduleAG : 0,
|
|
1070
|
+
);
|
|
1027
1071
|
const scheduleY = schedule.totals.Y;
|
|
1028
1072
|
const scheduleZ = schedule.totals.Z;
|
|
1029
1073
|
|
|
@@ -1073,7 +1117,7 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
|
|
|
1073
1117
|
seShort.O80 = carry([scheduleR, scheduleY, scheduleAC], () =>
|
|
1074
1118
|
scheduleR + scheduleY + scheduleAC > 0 ? scheduleR + scheduleY + scheduleAC : 0,
|
|
1075
1119
|
);
|
|
1076
|
-
seShort.D85 =
|
|
1120
|
+
seShort.D85 = smallPoolsWriteOff;
|
|
1077
1121
|
seShort.O85 = carry([scheduleZ], () => (scheduleZ > 0 ? scheduleZ : 0));
|
|
1078
1122
|
seShort.D94 = goodsForOwnUse;
|
|
1079
1123
|
const shortAllowances = [seShort.D71, seShort.O85, seShort.D94, seShort.O71, seShort.D80, seShort.D85, seShort.O80];
|
|
@@ -1155,7 +1199,7 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
|
|
|
1155
1199
|
seFull.D210 = stated(annualAdjustments.accountingAdjustment);
|
|
1156
1200
|
// Box 50 carries the whole writing down allowance the schedule claims.
|
|
1157
1201
|
seFull.D144 = carry([scheduleR], () => scheduleR);
|
|
1158
|
-
seFull.O144 =
|
|
1202
|
+
seFull.O144 = smallPoolsWriteOff;
|
|
1159
1203
|
seFull.O149 = scheduleY;
|
|
1160
1204
|
seFull.O154 = carry([seFull.D139, seFull.D144, seFull.O144, seFull.O149], () =>
|
|
1161
1205
|
sheetSum([seFull.D139, seFull.D144, seFull.D147, seFull.D152, seFull.D156, seFull.D160, seFull.O139, seFull.O144, seFull.O149]),
|
|
@@ -1175,15 +1219,18 @@ export function calculateSeCells(book, lines, taxData, scenario = {}) {
|
|
|
1175
1219
|
const fromNetLoss = -seFull.O129 + seFull.D174 - seFull.O169;
|
|
1176
1220
|
return fromNetLoss < 0 ? -fromNetLoss : 0;
|
|
1177
1221
|
});
|
|
1178
|
-
|
|
1222
|
+
// HMRC's SA103F working sheet for boxes 73 and 77 takes one figure, box
|
|
1223
|
+
// 64 less box 65 plus boxes 68, 71 and 72: box 73 is that figure when it
|
|
1224
|
+
// is positive and box 77 is its negation when it is not. The sheet reads
|
|
1225
|
+
// O174-O179+N(D197)+D210+N(O190); boxes 68 and 72 have no input cell on
|
|
1226
|
+
// this template, so their printed dash cells (D197, O190) contribute nil
|
|
1227
|
+
// and box 71 (D210) is the only adjustment that moves either figure.
|
|
1228
|
+
const adjustedProfitBeforeFloor = carry([seFull.O174, seFull.O179], () => seFull.O174 - seFull.O179 + sheetNumber(seFull.D210));
|
|
1229
|
+
seFull.O194 = carry([adjustedProfitBeforeFloor], () => Math.max(0, adjustedProfitBeforeFloor));
|
|
1179
1230
|
seFull.O204 = pl.B11;
|
|
1180
1231
|
seFull.O199 = carry([seFull.O194], () => (sheetNumber(seFull.D179) > 0 ? 0 : Math.min(seFull.O194 + seFull.O204, lossesBroughtForward)));
|
|
1181
1232
|
seFull.O210 = carry([seFull.O194, seFull.O199], () => seFull.O194 - seFull.O199 + seFull.O204);
|
|
1182
|
-
|
|
1183
|
-
// 72. Boxes 68 and 72 have no input cell on this template, so their
|
|
1184
|
-
// printed dash cells (D197, O190) always contribute nil; box 71 (D210)
|
|
1185
|
-
// is the only one that moves the figure.
|
|
1186
|
-
seFull.D219 = carry([seFull.O179], () => seFull.O179 - sheetNumber(seFull.D210));
|
|
1233
|
+
seFull.D219 = carry([adjustedProfitBeforeFloor], () => Math.max(0, -adjustedProfitBeforeFloor));
|
|
1187
1234
|
seFull.O224 = seFull.D219;
|
|
1188
1235
|
seFull.D231 = contractorDeductions;
|
|
1189
1236
|
seFull.J280 = admin.N20;
|
|
@@ -563,6 +563,30 @@ export function diyaGlToScenario(book, lines, product) {
|
|
|
563
563
|
const opening = { category, description: asset.description, cost: asset.cost, acc_dep: asset.accumulatedDepreciation };
|
|
564
564
|
if (asset.taxWrittenDownValue !== undefined) opening.tax_wdv = asset.taxWrittenDownValue;
|
|
565
565
|
if (asset.capitalAllowancePool === "special") opening.pool = "special";
|
|
566
|
+
// A single asset pool is a fact the book states, never one the loader
|
|
567
|
+
// infers: private use puts an asset in its own pool (CAA 2001 s.206),
|
|
568
|
+
// so a private use share without the marker is a contradiction. The
|
|
569
|
+
// Schedule applies the private use share on the motor rows only, and
|
|
570
|
+
// the Company Schedule has neither column.
|
|
571
|
+
if (asset.singleAssetPool || asset.privateUseProportion > 0) {
|
|
572
|
+
if (product !== "se") {
|
|
573
|
+
throw new Error(
|
|
574
|
+
`Fixed asset ${asset.assetID} states a single asset pool or private use, which the Company Schedule has no column for`,
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
if (category !== "motor") {
|
|
578
|
+
throw new Error(
|
|
579
|
+
`Fixed asset ${asset.assetID} states a single asset pool or private use on class "${asset.class}", which the Schedule applies to motor vehicles only`,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
if (!asset.singleAssetPool) {
|
|
583
|
+
throw new Error(
|
|
584
|
+
`Fixed asset ${asset.assetID} states privateUseProportion without singleAssetPool; a privately used asset is its own pool`,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
opening.single_asset_pool = true;
|
|
588
|
+
if (asset.privateUseProportion > 0) opening.private_use = asset.privateUseProportion;
|
|
589
|
+
}
|
|
566
590
|
return opening;
|
|
567
591
|
});
|
|
568
592
|
if (openingFixedAssets.length > 0) scenario.opening_fixed_assets = openingFixedAssets;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
export const PROVENANCE_DATA = {
|
|
14
14
|
formatVersion: "diya-gl/1",
|
|
15
|
-
engineVersion: "1.2.
|
|
15
|
+
engineVersion: "1.2.14+959e4a1d",
|
|
16
16
|
taxDataHash: "4ecdbe985d22",
|
|
17
17
|
reconciledCommit: "c910fdeefbe4f2f71ae9ec5f5d4a713783a1c0c7",
|
|
18
18
|
templates: {
|
|
@@ -25,8 +25,8 @@ export const PROVENANCE_DATA = {
|
|
|
25
25
|
scorecard: "686 passed, 0 warnings, 0 failed",
|
|
26
26
|
},
|
|
27
27
|
se: {
|
|
28
|
-
hash: "
|
|
29
|
-
scorecard: "
|
|
28
|
+
hash: "94754ec2c44b",
|
|
29
|
+
scorecard: "7962 passed, 0 warnings, 0 failed",
|
|
30
30
|
},
|
|
31
31
|
ltd: {
|
|
32
32
|
hash: "caa14632083b",
|
|
@@ -1282,6 +1282,15 @@ export function formatScenarioToml(metadata, grouped, expected) {
|
|
|
1282
1282
|
parts.push("# The special rate (6%) pool, SA103F box 51; the Schedule marks the row S in column AB.");
|
|
1283
1283
|
parts.push(`pool = "special"`);
|
|
1284
1284
|
}
|
|
1285
|
+
if (asset.single_asset_pool) {
|
|
1286
|
+
parts.push("# A single asset pool (private use or a short-life asset election): the Schedule marks the row P in");
|
|
1287
|
+
parts.push("# column AD, and the API files its allowance as capitalAllowanceSingleAssetPool.");
|
|
1288
|
+
parts.push("single_asset_pool = true");
|
|
1289
|
+
}
|
|
1290
|
+
if (asset.private_use !== undefined) {
|
|
1291
|
+
parts.push("# The share of use that is private, which reduces the allowances (Schedule column M on the car rows).");
|
|
1292
|
+
parts.push(`private_use = ${asset.private_use}`);
|
|
1293
|
+
}
|
|
1285
1294
|
parts.push("");
|
|
1286
1295
|
}
|
|
1287
1296
|
}
|
|
@@ -1167,6 +1167,11 @@ const SCHEDULE_DISPOSAL_COLUMNS = { disposedDate: "U", disposalProceeds: "V" };
|
|
|
1167
1167
|
// Column AB, "S" on a row whose tax written-down value sits in the special
|
|
1168
1168
|
// rate pool (the Self Employed Schedule's AC column, SA103F box 51).
|
|
1169
1169
|
const SCHEDULE_SPECIAL_RATE_POOL_MARKER = { column: "AB", value: "S" };
|
|
1170
|
+
// The single asset pool marker beside it, and the private use share the car
|
|
1171
|
+
// rows carry in column M. The Company Schedule has neither column, so its
|
|
1172
|
+
// register never sets either field.
|
|
1173
|
+
const SCHEDULE_SINGLE_ASSET_POOL_MARKER = { column: "AD", value: "P" };
|
|
1174
|
+
const SCHEDULE_PRIVATE_USE_COLUMN = "M";
|
|
1170
1175
|
|
|
1171
1176
|
// The single-file products keep one Fixed Assets sheet inside the workbook
|
|
1172
1177
|
// and their writers fill only its in-year addition block: BST the Plant &
|
|
@@ -1262,6 +1267,13 @@ async function fixedAssetRegisterFrom(set, product) {
|
|
|
1262
1267
|
if (textAt(xml, `${SCHEDULE_SPECIAL_RATE_POOL_MARKER.column}${row}`, sharedStrings) === SCHEDULE_SPECIAL_RATE_POOL_MARKER.value) {
|
|
1263
1268
|
asset.capitalAllowancePool = "special";
|
|
1264
1269
|
}
|
|
1270
|
+
if (textAt(xml, `${SCHEDULE_SINGLE_ASSET_POOL_MARKER.column}${row}`, sharedStrings) === SCHEDULE_SINGLE_ASSET_POOL_MARKER.value) {
|
|
1271
|
+
asset.singleAssetPool = true;
|
|
1272
|
+
}
|
|
1273
|
+
if (assetClass === "motorVehicles") {
|
|
1274
|
+
const privateUse = numberAt(xml, `${SCHEDULE_PRIVATE_USE_COLUMN}${row}`, sharedStrings);
|
|
1275
|
+
if (privateUse > 0) asset.privateUseProportion = privateUse;
|
|
1276
|
+
}
|
|
1265
1277
|
assign(asset, "description", textAt(xml, `${SCHEDULE_ASSET_COLUMNS.description}${row}`, sharedStrings));
|
|
1266
1278
|
assign(asset, "accumulatedDepreciation", numberAt(xml, `${SCHEDULE_ASSET_COLUMNS.accumulatedDepreciation}${row}`, sharedStrings));
|
|
1267
1279
|
assign(asset, "taxWrittenDownValue", numberAt(xml, `${SCHEDULE_ASSET_COLUMNS.taxWrittenDownValue}${row}`, sharedStrings));
|
package/dist/app/products/se.js
CHANGED
|
@@ -230,10 +230,12 @@ export const DISALLOWABLE_PERCENT_CELLS = {
|
|
|
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
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;
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
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,
|
|
235
|
+
// O174-O179+N(D197)+D210+N(O190), at nil from its own side. Box 54's caption
|
|
236
|
+
// at N136 is "Electric charge-point allowance" (L136 = 54); box 55's cell
|
|
237
|
+
// O144 is the small pools formula, so the enhanced-allowance figure has no
|
|
238
|
+
// input cell of its own.
|
|
237
239
|
export const ANNUAL_ALLOWANCE_CELLS = {
|
|
238
240
|
zeroEmissionsGoodsVehicleAllowance: "D152",
|
|
239
241
|
zeroEmissionsCarAllowance: "D156",
|
|
@@ -275,6 +277,17 @@ export const NEW_PLANT_ROWS = [67, 68, 69, 70, 71];
|
|
|
275
277
|
// AB38="S" and AC$4, S38 = IF(O38>0,O38-N(R38)-N(AC38)," "), AC1 = AC57+AC110.
|
|
276
278
|
export const SPECIAL_RATE_POOL_MARKER_COLUMN = "AB";
|
|
277
279
|
export const SPECIAL_RATE_POOL_MARKER = "S";
|
|
280
|
+
// Column AD on an asset row: "P" keeps the row's tax written-down value in a
|
|
281
|
+
// single asset pool. R, AC and S stay as they are; AE and AF repeat the
|
|
282
|
+
// row's writing down allowance by rate and AG its written-down value, so the
|
|
283
|
+
// small pools test on SE Full!O144 can leave the marked rows out. Column M
|
|
284
|
+
// on the car rows (38 to 48, 91 to 101) is the private use share the R, AC,
|
|
285
|
+
// Y and Z formulas multiply by (1-M). Verified against the template:
|
|
286
|
+
// AE38 = IF(AND(AD38="P",AB38<>"S"),N(R38)," "), AF38 the same with AB38="S"
|
|
287
|
+
// and N(AC38), AG38 = IF(AD38="P",N(S38)," "), AE1 = AE57+AE110.
|
|
288
|
+
export const SINGLE_ASSET_POOL_MARKER_COLUMN = "AD";
|
|
289
|
+
export const SINGLE_ASSET_POOL_MARKER = "P";
|
|
290
|
+
export const PRIVATE_USE_COLUMN = "M";
|
|
278
291
|
|
|
279
292
|
// Hire purchase agreements (Fixedassets.xlsx HPfinance sheet). Only two
|
|
280
293
|
// rows are available for scenario agreements before the sheet's own
|
|
@@ -668,7 +681,7 @@ function composeWrites(scenario, targetStartYear) {
|
|
|
668
681
|
skips.push(skipped("openingFixedAsset", assetEntry, why));
|
|
669
682
|
continue;
|
|
670
683
|
}
|
|
671
|
-
// Written left-to-right (C,
|
|
684
|
+
// Written left-to-right (C, E, F, M, O, AB, AD). setCellValue/setCellString
|
|
672
685
|
// in spreadsheet-runner.js replaces a matched cell together with every
|
|
673
686
|
// self-closing sibling up to the row's next already-closed cell -- an
|
|
674
687
|
// earlier write onto a later (rightward) column silently deletes any
|
|
@@ -680,6 +693,9 @@ function composeWrites(scenario, targetStartYear) {
|
|
|
680
693
|
if (asset.description) fa[`C${row}`] = asset.description;
|
|
681
694
|
fa[`E${row}`] = asset.cost;
|
|
682
695
|
if (asset.acc_dep) fa[`F${row}`] = asset.acc_dep;
|
|
696
|
+
// The private use share reads on the car rows alone; the loader has
|
|
697
|
+
// already refused it on any other class.
|
|
698
|
+
if (asset.private_use !== undefined && asset.category === "motor") fa[`${PRIVATE_USE_COLUMN}${row}`] = asset.private_use;
|
|
683
699
|
// Column O is the written down TAX value brought forward, the figure
|
|
684
700
|
// the capital allowance columns work from. The schedule computes a
|
|
685
701
|
// disposal's balancing allowance as that value less the sale proceeds,
|
|
@@ -687,6 +703,7 @@ function composeWrites(scenario, targetStartYear) {
|
|
|
687
703
|
// allowance block, and every figure downstream of it, in error.
|
|
688
704
|
if (asset.tax_wdv) fa[`O${row}`] = asset.tax_wdv;
|
|
689
705
|
if (asset.pool === "special") fa[`${SPECIAL_RATE_POOL_MARKER_COLUMN}${row}`] = SPECIAL_RATE_POOL_MARKER;
|
|
706
|
+
if (asset.single_asset_pool) fa[`${SINGLE_ASSET_POOL_MARKER_COLUMN}${row}`] = SINGLE_ASSET_POOL_MARKER;
|
|
690
707
|
existingAssetRowsUsed[asset.category].push(row);
|
|
691
708
|
}
|
|
692
709
|
}
|
|
@@ -1260,7 +1277,37 @@ export function multiFileOptions() {
|
|
|
1260
1277
|
// E57 and E110 are the schedule's own existing-asset and new-asset
|
|
1261
1278
|
// cost subtotals; row 1 adds the two. Reading both lets the report
|
|
1262
1279
|
// state the year's asset movement rather than one closing total.
|
|
1263
|
-
|
|
1280
|
+
// AE1, AF1 and AG1 are the single asset pool totals; M40, AD40, AE40
|
|
1281
|
+
// and AG40 are the fixture's marked car row (van 38, estate car 39,
|
|
1282
|
+
// hatchback 40), read so its marker and private use share are proved
|
|
1283
|
+
// on the row itself.
|
|
1284
|
+
Schedule: [
|
|
1285
|
+
"E1",
|
|
1286
|
+
"F1",
|
|
1287
|
+
"G1",
|
|
1288
|
+
"I1",
|
|
1289
|
+
"J1",
|
|
1290
|
+
"K1",
|
|
1291
|
+
"Q1",
|
|
1292
|
+
"R1",
|
|
1293
|
+
"S1",
|
|
1294
|
+
"V1",
|
|
1295
|
+
"W1",
|
|
1296
|
+
"X1",
|
|
1297
|
+
"Y1",
|
|
1298
|
+
"Z1",
|
|
1299
|
+
"AC1",
|
|
1300
|
+
"AC4",
|
|
1301
|
+
"AE1",
|
|
1302
|
+
"AF1",
|
|
1303
|
+
"AG1",
|
|
1304
|
+
"E57",
|
|
1305
|
+
"E110",
|
|
1306
|
+
"M40",
|
|
1307
|
+
"AD40",
|
|
1308
|
+
"AE40",
|
|
1309
|
+
"AG40",
|
|
1310
|
+
],
|
|
1264
1311
|
FAreconciliation: ["E11", "E13", "E15", "K11", "K13", "K15"],
|
|
1265
1312
|
// E2 is the long-term-creditors total for the "New Hire Purchase
|
|
1266
1313
|
// Agreements" block (SUM(E8:E14)); I/J/K on rows 8 and 10 are the
|
|
@@ -1651,6 +1698,15 @@ const FIXED_ASSET_CELL_LABELS = {
|
|
|
1651
1698
|
X1: "Accumulated depreciation on the assets sold in the year",
|
|
1652
1699
|
Y1: "Balancing allowance on the disposals",
|
|
1653
1700
|
Z1: "Balancing charge on the disposals",
|
|
1701
|
+
AC1: "Total special rate writing down allowance claimed (the rows marked S in column AB)",
|
|
1702
|
+
AC4: "Special rate writing down allowance rate, read from Admin",
|
|
1703
|
+
AE1: "Single asset pool writing down allowance at the main rate (the rows marked P in column AD)",
|
|
1704
|
+
AF1: "Single asset pool writing down allowance at the special rate (the rows marked P and S)",
|
|
1705
|
+
AG1: "Single asset pool tax written down value carried forward",
|
|
1706
|
+
M40: "Private use share of the third motor vehicle brought forward",
|
|
1707
|
+
AD40: "Single asset pool marker on the third motor vehicle brought forward (P)",
|
|
1708
|
+
AE40: "Single asset pool main rate allowance on the third motor vehicle brought forward",
|
|
1709
|
+
AG40: "Single asset pool tax written down value on the third motor vehicle brought forward",
|
|
1654
1710
|
E57: "Cost of the assets owned at the start of the year",
|
|
1655
1711
|
E110: "Cost of the assets bought during the year",
|
|
1656
1712
|
},
|
|
@@ -1842,6 +1898,11 @@ export function profitBridge(results) {
|
|
|
1842
1898
|
{ label: "Less the full return's own Structures and Buildings Allowance (box 53)", cell: "SE Full!D160", value: -num(seFull?.D160) },
|
|
1843
1899
|
{ label: "Less the full return's own electric charge-point allowance (box 54)", cell: "SE Full!O139", value: -num(seFull?.O139) },
|
|
1844
1900
|
{ label: "Less the full return's own box 62 adjustment", cell: "SE Full!D179", value: -num(seFull?.D179) },
|
|
1901
|
+
{
|
|
1902
|
+
label: "Add the full return's own box 71 adjustment for change of accounting practice",
|
|
1903
|
+
cell: "SE Full!D210",
|
|
1904
|
+
value: num(seFull?.D210),
|
|
1905
|
+
},
|
|
1845
1906
|
{
|
|
1846
1907
|
label: "Add back the year's loss, carried forward rather than reducing tax below nil",
|
|
1847
1908
|
cell: "SE Full!O179",
|
|
@@ -1854,6 +1915,56 @@ export function profitBridge(results) {
|
|
|
1854
1915
|
return buildProfitBridge(rows, `${TAX_SHEET}!E5`, num(tax.E5));
|
|
1855
1916
|
}
|
|
1856
1917
|
|
|
1918
|
+
// ── The Schedule's capital allowance pools, from the fixture ───────────────
|
|
1919
|
+
|
|
1920
|
+
// The opening tax written-down values that claim a writing down allowance in
|
|
1921
|
+
// one rate pool, each reduced to its business share: a privately used car
|
|
1922
|
+
// claims on (1 - M), which the Schedule applies on the car rows.
|
|
1923
|
+
function openingPoolAllowance(expected, pool) {
|
|
1924
|
+
return (expected.opening_fixed_assets || [])
|
|
1925
|
+
.filter((asset) => (asset.pool === "special") === (pool === "special"))
|
|
1926
|
+
.reduce((total, asset) => total + (asset.tax_wdv || 0) * (1 - (asset.private_use || 0)), 0);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// The small pools write-off the way SE Full!O144 and SE Short!D85 state it:
|
|
1930
|
+
// IF((R1+S1-AE1-AF1-AG1)<1000,S1-AG1,0). The single asset pool rows leave
|
|
1931
|
+
// both the £1,000 test and the figure written off.
|
|
1932
|
+
function smallPoolsWriteOff(schedule) {
|
|
1933
|
+
const num = (v) => (typeof v === "number" ? v : 0);
|
|
1934
|
+
const pooledBalance = num(schedule.R1) + num(schedule.S1) - num(schedule.AE1) - num(schedule.AF1) - num(schedule.AG1);
|
|
1935
|
+
return pooledBalance < 1000 ? num(schedule.S1) - num(schedule.AG1) : 0;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// The same two figures from the fixture's own assets. A pooled opening asset
|
|
1939
|
+
// puts its whole tax written-down value into the test (R + S = O on a main
|
|
1940
|
+
// rate row, O less the special rate allowance on a special rate row) and its
|
|
1941
|
+
// written-down balance after this year's allowance into the write-off; a
|
|
1942
|
+
// single asset pool row puts nothing into either; an asset bought in the year
|
|
1943
|
+
// puts its cost less the annual investment allowance into both.
|
|
1944
|
+
function fixtureSmallPools(expected, taxData, rate) {
|
|
1945
|
+
const wda = taxData.capital_allowances.writing_down_allowance;
|
|
1946
|
+
const specialWda = taxData.capital_allowances.writing_down_allowance_special || 0;
|
|
1947
|
+
const aia = taxData.capital_allowances.annual_investment_allowance ?? 0;
|
|
1948
|
+
let pooledBalance = 0;
|
|
1949
|
+
let pooledWrittenDown = 0;
|
|
1950
|
+
for (const asset of expected.opening_fixed_assets || []) {
|
|
1951
|
+
if (asset.single_asset_pool || !(asset.tax_wdv > 0)) continue;
|
|
1952
|
+
const share = 1 - (asset.private_use || 0);
|
|
1953
|
+
const allowance = asset.tax_wdv * (asset.pool === "special" ? specialWda : wda) * share;
|
|
1954
|
+
pooledBalance += asset.pool === "special" ? asset.tax_wdv - allowance : asset.tax_wdv;
|
|
1955
|
+
pooledWrittenDown += asset.tax_wdv - allowance;
|
|
1956
|
+
}
|
|
1957
|
+
for (const transactions of Object.values(expected.purchases || {})) {
|
|
1958
|
+
for (const tx of transactions) {
|
|
1959
|
+
if (tx.code !== "fa") continue;
|
|
1960
|
+
const cost = netOfVat(tx.amount, rate);
|
|
1961
|
+
pooledBalance += cost - cost * aia;
|
|
1962
|
+
pooledWrittenDown += cost - cost * aia;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
return { pooledBalance, pooledWrittenDown };
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1857
1968
|
// ── Journal category VAT netting ───────────────────────────────────────────
|
|
1858
1969
|
|
|
1859
1970
|
// A journal row takes its own VAT off its own gross and rounds nothing
|
|
@@ -2278,13 +2389,14 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
|
|
|
2278
2389
|
);
|
|
2279
2390
|
// Income Tax!E5 reads 'SE Full'!O210, so the short return's box 31
|
|
2280
2391
|
// reaches it only through the full return: the two agree exactly when
|
|
2281
|
-
// no box the trader states on SE Full alone (52, 52.1, 53, 54, 62
|
|
2282
|
-
// carries a figure, and stand apart by those figures otherwise.
|
|
2392
|
+
// no box the trader states on SE Full alone (52, 52.1, 53, 54, 62 and
|
|
2393
|
+
// 71) carries a figure, and stand apart by those figures otherwise.
|
|
2283
2394
|
const fullOnly = results["SE Full"] || {};
|
|
2284
|
-
const fullReturnOnlyBoxes =
|
|
2395
|
+
const fullReturnOnlyBoxes =
|
|
2396
|
+
num(fullOnly.D152) + num(fullOnly.D156) + num(fullOnly.D160) + num(fullOnly.O139) + num(fullOnly.D179) - num(fullOnly.D210);
|
|
2285
2397
|
if (seShort.D106) {
|
|
2286
2398
|
check(
|
|
2287
|
-
"SA103S: Profit for tax (D106) less the SE Full-only boxes 52, 52.1, 53, 54 and 62 = Income Tax E5",
|
|
2399
|
+
"SA103S: Profit for tax (D106) less the SE Full-only boxes 52, 52.1, 53, 54 and 62 plus box 71 = Income Tax E5",
|
|
2288
2400
|
Math.max(0, num(seShort.D106) - fullReturnOnlyBoxes),
|
|
2289
2401
|
tax.E5,
|
|
2290
2402
|
);
|
|
@@ -2507,7 +2619,21 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
|
|
|
2507
2619
|
num(seFull.O174),
|
|
2508
2620
|
taxProfitFromNetProfit > 0 ? taxProfitFromNetProfit : Math.max(0, taxProfitFromNetLoss),
|
|
2509
2621
|
);
|
|
2510
|
-
|
|
2622
|
+
// HMRC's working sheet for boxes 73 and 77 takes one figure, box 64 less
|
|
2623
|
+
// box 65 plus boxes 68, 71 and 72; box 73 is it when positive, box 77 its
|
|
2624
|
+
// negation when not. Boxes 68 (D197) and 72 (O190) print a dash on this
|
|
2625
|
+
// template and contribute nil, so box 71 is the only adjustment here.
|
|
2626
|
+
const adjustedProfitBeforeFloor = num(seFull.O174) - num(seFull.O179) + num(seFull.D210);
|
|
2627
|
+
check(
|
|
2628
|
+
"SA103F box 73 adjusted profit (O194) = box 64 less box 65 plus box 71, floored at nil",
|
|
2629
|
+
num(seFull.O194),
|
|
2630
|
+
Math.max(0, adjustedProfitBeforeFloor),
|
|
2631
|
+
);
|
|
2632
|
+
check(
|
|
2633
|
+
"SA103F box 77 adjusted loss (D219) = box 65 less box 64 and box 71, floored at nil",
|
|
2634
|
+
num(seFull.D219),
|
|
2635
|
+
Math.max(0, -adjustedProfitBeforeFloor),
|
|
2636
|
+
);
|
|
2511
2637
|
check(
|
|
2512
2638
|
"SA103F box 76 total taxable profits (O210) = box 73 less box 74 plus box 75",
|
|
2513
2639
|
num(seFull.O210),
|
|
@@ -2521,10 +2647,13 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
|
|
|
2521
2647
|
check("SA103F box 49 annual investment allowance (D139) = Schedule Q1", num(seFull.D139), Math.max(0, num(returnSchedule.Q1)));
|
|
2522
2648
|
check("SA103F box 50 capital allowances at 18% (D144) = Schedule R1", num(seFull.D144), num(returnSchedule.R1));
|
|
2523
2649
|
check("SA103F box 51 capital allowances at 6% (D147) = Schedule AC1", num(seFull.D147), num(returnSchedule.AC1));
|
|
2650
|
+
// The small pools write-off runs over the pooled balance alone: the
|
|
2651
|
+
// single asset pool rows (AE1, AF1, AG1) leave both the £1,000 test
|
|
2652
|
+
// and the figure written off (O144 = IF((R1+S1-AE1-AF1-AG1)<1000,S1-AG1,0)).
|
|
2524
2653
|
check(
|
|
2525
|
-
"SA103F box 55 100% and other enhanced capital allowances (O144) = Schedule S1 while the
|
|
2654
|
+
"SA103F box 55 100% and other enhanced capital allowances (O144) = Schedule S1 less the single asset pools while the pooled balance is under £1,000",
|
|
2526
2655
|
num(seFull.O144),
|
|
2527
|
-
|
|
2656
|
+
smallPoolsWriteOff(returnSchedule),
|
|
2528
2657
|
);
|
|
2529
2658
|
check("SA103F box 56 allowances on sale or cessation (O149) = Schedule Y1", num(seFull.O149), num(returnSchedule.Y1));
|
|
2530
2659
|
check("SA103F box 59 balancing charge (O160) = Schedule Z1", num(seFull.O160), num(returnSchedule.Z1));
|
|
@@ -2539,20 +2668,28 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
|
|
|
2539
2668
|
// Each pool against the scenario's own assets: the main pool's boxes 50
|
|
2540
2669
|
// and the special rate pool's box 51, each at its own year's rate, so an
|
|
2541
2670
|
// asset marked for one pool that lands in the other moves both boxes.
|
|
2671
|
+
// A privately used car claims its allowance on the business share alone
|
|
2672
|
+
// (Schedule column M), single asset pool or not, so the anchor carries
|
|
2673
|
+
// that factor row by row.
|
|
2542
2674
|
if (taxData?.capital_allowances && expected.opening_fixed_assets) {
|
|
2543
|
-
const poolTaxWdv = (pool) =>
|
|
2544
|
-
expected.opening_fixed_assets
|
|
2545
|
-
.filter((asset) => (asset.pool === "special") === (pool === "special"))
|
|
2546
|
-
.reduce((total, asset) => total + (asset.tax_wdv || 0), 0);
|
|
2547
2675
|
check(
|
|
2548
|
-
"SA103F box 50 capital allowances at 18% (D144) = the scenario's opening tax written-down values at the year's writing down rate",
|
|
2676
|
+
"SA103F box 50 capital allowances at 18% (D144) = the scenario's opening tax written-down values at the year's writing down rate, less private use",
|
|
2549
2677
|
num(seFull.D144),
|
|
2550
|
-
|
|
2678
|
+
openingPoolAllowance(expected, "main") * taxData.capital_allowances.writing_down_allowance,
|
|
2551
2679
|
);
|
|
2552
2680
|
check(
|
|
2553
|
-
"SA103F box 51 capital allowances at 6% (D147) = the scenario's special rate pool tax written-down values at the year's special rate",
|
|
2681
|
+
"SA103F box 51 capital allowances at 6% (D147) = the scenario's special rate pool tax written-down values at the year's special rate, less private use",
|
|
2554
2682
|
num(seFull.D147),
|
|
2555
|
-
|
|
2683
|
+
openingPoolAllowance(expected, "special") * (taxData.capital_allowances.writing_down_allowance_special || 0),
|
|
2684
|
+
);
|
|
2685
|
+
// The same rule the sheet applies, run over the fixture's own pools:
|
|
2686
|
+
// a sheet whose R1, S1 and AE1 to AG1 agree with each other but not
|
|
2687
|
+
// with the assets fails here.
|
|
2688
|
+
const pools = fixtureSmallPools(expected, taxData, rate);
|
|
2689
|
+
check(
|
|
2690
|
+
"SA103F box 55 100% and other enhanced capital allowances (O144) = the small pools write-off computed from the scenario's own pooled assets",
|
|
2691
|
+
num(seFull.O144),
|
|
2692
|
+
pools.pooledBalance < 1000 ? pools.pooledWrittenDown : 0,
|
|
2556
2693
|
);
|
|
2557
2694
|
}
|
|
2558
2695
|
|
|
@@ -2604,22 +2741,19 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
|
|
|
2604
2741
|
num(seFull.D174) - num(seFull.O122) - num(seFull.O160),
|
|
2605
2742
|
ownUseStated,
|
|
2606
2743
|
);
|
|
2607
|
-
//
|
|
2608
|
-
//
|
|
2609
|
-
//
|
|
2744
|
+
// The same two boxes anchored on the figure the book states, so a sheet
|
|
2745
|
+
// whose box 71 cell and adjusted figures agree with each other but not
|
|
2746
|
+
// with the book fails here.
|
|
2747
|
+
const statedAccountingAdjustment = statedAdjustments.accountingAdjustment || 0;
|
|
2610
2748
|
check(
|
|
2611
|
-
"SA103F box
|
|
2612
|
-
num(seFull.
|
|
2613
|
-
num(seFull.
|
|
2749
|
+
"SA103F box 73 adjusted profit (O194) = box 64 less box 65 plus the box 71 figure the book states, floored at nil",
|
|
2750
|
+
num(seFull.O194),
|
|
2751
|
+
Math.max(0, num(seFull.O174) - num(seFull.O179) + statedAccountingAdjustment),
|
|
2614
2752
|
);
|
|
2615
|
-
// Box 73 reads box 64 alone (O194 = O174). HMRC's working sheet adds box
|
|
2616
|
-
// 71 to the adjusted profit; the sheet carries box 71 into box 77 only.
|
|
2617
2753
|
check(
|
|
2618
|
-
"SA103F box
|
|
2619
|
-
num(seFull.
|
|
2620
|
-
num(seFull.
|
|
2621
|
-
0.01,
|
|
2622
|
-
"warning",
|
|
2754
|
+
"SA103F box 77 adjusted loss (D219) = box 65 less box 64 and the box 71 figure the book states, floored at nil",
|
|
2755
|
+
num(seFull.D219),
|
|
2756
|
+
Math.max(0, num(seFull.O179) - num(seFull.O174) - statedAccountingAdjustment),
|
|
2623
2757
|
);
|
|
2624
2758
|
|
|
2625
2759
|
if (sa103s) {
|
|
@@ -2647,10 +2781,12 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
|
|
|
2647
2781
|
num(seFull.O174),
|
|
2648
2782
|
Math.max(0, num(sa103s.D99) - fullReturnOnlyDeductions),
|
|
2649
2783
|
);
|
|
2784
|
+
// Box 71 is the one SE Full-only figure that adds to the taxable
|
|
2785
|
+
// profit; the short return has no box for it either.
|
|
2650
2786
|
check(
|
|
2651
|
-
"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, with each return's own loss set-off",
|
|
2787
|
+
"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",
|
|
2652
2788
|
num(seFull.O210),
|
|
2653
|
-
Math.max(0, num(sa103s.D106) - fullReturnOnlyDeductions + num(sa103s.O94) - num(seFull.O199)),
|
|
2789
|
+
Math.max(0, num(sa103s.D106) - fullReturnOnlyDeductions + num(seFull.D210) + num(sa103s.O94) - num(seFull.O199)),
|
|
2654
2790
|
);
|
|
2655
2791
|
check(
|
|
2656
2792
|
"SA103S box 28 net business profit for tax purposes (D99) leaves out the allowances and box 62 adjustment the trader states on SE Full alone",
|
|
@@ -2822,14 +2958,55 @@ export function checkCompliance(results, expected, taxData, calculateExpectedTax
|
|
|
2822
2958
|
taxData.capital_allowances.writing_down_allowance_special || 0,
|
|
2823
2959
|
0.0001,
|
|
2824
2960
|
);
|
|
2825
|
-
const specialPoolWdv = expected.opening_fixed_assets
|
|
2826
|
-
.filter((asset) => asset.pool === "special")
|
|
2827
|
-
.reduce((t, asset) => t + (asset.tax_wdv || 0), 0);
|
|
2828
2961
|
check(
|
|
2829
|
-
"Fixed assets: Schedule special rate pool allowance (AC1) = the scenario's special rate assets at the year's special rate",
|
|
2962
|
+
"Fixed assets: Schedule special rate pool allowance (AC1) = the scenario's special rate assets at the year's special rate, less private use",
|
|
2830
2963
|
sched.AC1 || 0,
|
|
2831
|
-
|
|
2964
|
+
openingPoolAllowance(expected, "special") * (taxData.capital_allowances.writing_down_allowance_special || 0),
|
|
2832
2965
|
);
|
|
2966
|
+
|
|
2967
|
+
// The single asset pools (column AD marked P): each marked asset's
|
|
2968
|
+
// allowance at its own rate on its business share, and its written-down
|
|
2969
|
+
// value less that allowance, anchored on the fixture's assets.
|
|
2970
|
+
const wda = taxData.capital_allowances.writing_down_allowance;
|
|
2971
|
+
const specialWda = taxData.capital_allowances.writing_down_allowance_special || 0;
|
|
2972
|
+
const singleAssets = expected.opening_fixed_assets.filter((asset) => asset.single_asset_pool);
|
|
2973
|
+
const singleAllowance = (asset) =>
|
|
2974
|
+
(asset.tax_wdv || 0) * (asset.pool === "special" ? specialWda : wda) * (1 - (asset.private_use || 0));
|
|
2975
|
+
check(
|
|
2976
|
+
"Fixed assets: Schedule single asset pool main rate allowance (AE1) = the scenario's single asset pool main rate assets at the year's rate, less private use",
|
|
2977
|
+
sched.AE1 || 0,
|
|
2978
|
+
singleAssets.filter((asset) => asset.pool !== "special").reduce((t, asset) => t + singleAllowance(asset), 0),
|
|
2979
|
+
);
|
|
2980
|
+
check(
|
|
2981
|
+
"Fixed assets: Schedule single asset pool special rate allowance (AF1) = the scenario's single asset pool special rate assets at the year's special rate, less private use",
|
|
2982
|
+
sched.AF1 || 0,
|
|
2983
|
+
singleAssets.filter((asset) => asset.pool === "special").reduce((t, asset) => t + singleAllowance(asset), 0),
|
|
2984
|
+
);
|
|
2985
|
+
check(
|
|
2986
|
+
"Fixed assets: Schedule single asset pool written down value (AG1) = the scenario's single asset pool assets' tax written-down values less this year's allowance",
|
|
2987
|
+
sched.AG1 || 0,
|
|
2988
|
+
singleAssets.reduce((t, asset) => t + (asset.tax_wdv || 0) - singleAllowance(asset), 0),
|
|
2989
|
+
);
|
|
2990
|
+
// The marker and the private use share on the row itself, for each
|
|
2991
|
+
// motor asset the fixture declares, in the order the writer fills the
|
|
2992
|
+
// block.
|
|
2993
|
+
expected.opening_fixed_assets
|
|
2994
|
+
.filter((asset) => asset.category === "motor")
|
|
2995
|
+
.forEach((asset, index) => {
|
|
2996
|
+
const row = EXISTING_ASSET_ROWS.motor[index];
|
|
2997
|
+
if (row === undefined || !(`AD${row}` in sched)) return;
|
|
2998
|
+
checkText(
|
|
2999
|
+
`Fixed assets: Schedule single asset pool marker (AD${row}) = ${asset.single_asset_pool ? "P" : "blank"} for the scenario's motor asset ${index + 1}`,
|
|
3000
|
+
typeof sched[`AD${row}`] === "string" ? sched[`AD${row}`].trim() : "",
|
|
3001
|
+
asset.single_asset_pool ? SINGLE_ASSET_POOL_MARKER : "",
|
|
3002
|
+
);
|
|
3003
|
+
check(
|
|
3004
|
+
`Fixed assets: Schedule private use share (M${row}) = the scenario's motor asset ${index + 1} private use`,
|
|
3005
|
+
num(sched[`M${row}`]),
|
|
3006
|
+
asset.private_use || 0,
|
|
3007
|
+
0.0001,
|
|
3008
|
+
);
|
|
3009
|
+
});
|
|
2833
3010
|
}
|
|
2834
3011
|
|
|
2835
3012
|
// 3. P&L depreciation and disposal lines carry the Schedule's own
|
|
@@ -483,6 +483,17 @@
|
|
|
483
483
|
"default": "main",
|
|
484
484
|
"description": "Which writing down pool the asset's tax written-down value sits in. special is the 6% pool (cars with higher CO2 emissions, integral features, long-life assets), SA103F box 51; the Self Employed Schedule marks the row S in column AB and claims the allowance in column AC. Absent means the main pool, box 50."
|
|
485
485
|
},
|
|
486
|
+
"singleAssetPool": {
|
|
487
|
+
"type": "boolean",
|
|
488
|
+
"default": false,
|
|
489
|
+
"description": "The asset's tax written-down value is its own pool: an asset with private use (CAA 2001 s.206) or under a short-life asset election (ss.83-86). Its writing down allowance still prints in SA103F box 50 or 51 by rate, but the API files it as capitalAllowanceSingleAssetPool and the small pools allowance never applies to it. The Self Employed Schedule marks the row P in column AD and carries the allowance and the written-down value in columns AE to AG. Self Employed only."
|
|
490
|
+
},
|
|
491
|
+
"privateUseProportion": {
|
|
492
|
+
"type": "number",
|
|
493
|
+
"minimum": 0,
|
|
494
|
+
"maximum": 1,
|
|
495
|
+
"description": "The share of the asset's use that is not business use, which reduces its writing down and balancing allowances (the Self Employed Schedule's column M on the car rows). An asset with private use is a single asset pool, so singleAssetPool must be true whenever this is above nil. Self Employed motor vehicles only."
|
|
496
|
+
},
|
|
486
497
|
"description": {
|
|
487
498
|
"type": "string"
|
|
488
499
|
},
|