@diy-accounting-uk/diya-gl 1.2.31 → 1.2.33

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.
@@ -1039,6 +1039,58 @@
1039
1039
  "scorecard": "101605 passed, 0 warnings, 0 failed"
1040
1040
  }
1041
1041
  }
1042
+ },
1043
+ {
1044
+ "tag": "diya-gl-v1.2.31",
1045
+ "date": "2026-09-23",
1046
+ "formatVersion": "diya-gl/1",
1047
+ "engineVersion": "1.2.31+80457d57",
1048
+ "taxDataHash": "3221a7c2f39c",
1049
+ "reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
1050
+ "templates": {
1051
+ "bst": {
1052
+ "hash": "0d003e10e00c",
1053
+ "scorecard": "812 passed, 0 warnings, 0 failed"
1054
+ },
1055
+ "taxi": {
1056
+ "hash": "1ff69654d64d",
1057
+ "scorecard": "686 passed, 0 warnings, 0 failed"
1058
+ },
1059
+ "se": {
1060
+ "hash": "064605ae76ed",
1061
+ "scorecard": "7982 passed, 0 warnings, 0 failed"
1062
+ },
1063
+ "ltd": {
1064
+ "hash": "caa14632083b",
1065
+ "scorecard": "101605 passed, 0 warnings, 0 failed"
1066
+ }
1067
+ }
1068
+ },
1069
+ {
1070
+ "tag": "diya-gl-v1.2.32",
1071
+ "date": "2026-09-24",
1072
+ "formatVersion": "diya-gl/1",
1073
+ "engineVersion": "1.2.32+92a3639b",
1074
+ "taxDataHash": "3221a7c2f39c",
1075
+ "reconciledCommit": "54e8731a91279975e61b1a232fbb67fda89515f0",
1076
+ "templates": {
1077
+ "bst": {
1078
+ "hash": "0d003e10e00c",
1079
+ "scorecard": "812 passed, 0 warnings, 0 failed"
1080
+ },
1081
+ "taxi": {
1082
+ "hash": "1ff69654d64d",
1083
+ "scorecard": "686 passed, 0 warnings, 0 failed"
1084
+ },
1085
+ "se": {
1086
+ "hash": "064605ae76ed",
1087
+ "scorecard": "7982 passed, 0 warnings, 0 failed"
1088
+ },
1089
+ "ltd": {
1090
+ "hash": "caa14632083b",
1091
+ "scorecard": "101605 passed, 0 warnings, 0 failed"
1092
+ }
1093
+ }
1042
1094
  }
1043
1095
  ]
1044
1096
  }
@@ -17,7 +17,7 @@
17
17
  // up from the book's own entityInformation["diya-gl:product"].
18
18
 
19
19
  import { changeLineBankAccount } from "../diya-gl-edits.js";
20
- import { SE_PURCHASE_CODE_MAP, LTD_SALES_CODE_MAP, isStraddlingLine } from "../scenario-extractor.js";
20
+ import { SE_PURCHASE_CODE_MAP, LTD_SALES_CODE_MAP, isStraddlingLine, signedAmount } from "../scenario-extractor.js";
21
21
 
22
22
  // The workbook each bank account is kept in. A Self Employed package
23
23
  // carries two of them where a Company package carries four, so an account
@@ -558,7 +558,7 @@ function isVatRegistered(book) {
558
558
  // year and belongs to a different year's turnover.
559
559
  function netOfVatTurnover(lines, rate) {
560
560
  let total = 0;
561
- for (const line of lines) if (line.sourceJournalID === "sales" && !isStraddlingLine(line)) total += line.amount / (1 + rate);
561
+ for (const line of lines) if (line.sourceJournalID === "sales" && !isStraddlingLine(line)) total += signedAmount(line) / (1 + rate);
562
562
  return total;
563
563
  }
564
564
 
@@ -19,7 +19,7 @@ import { changeLinePostingDate, changeLineAccount, changeLineAmount, addSaleLine
19
19
  import { LTD_PRODUCT_RULES } from "./book-checks/ltd.js";
20
20
  import { TAXI_PRODUCT_RULES } from "./book-checks/taxi.js";
21
21
  import { SE_PRODUCT_RULES } from "./book-checks/se.js";
22
- import { isStraddlingLine } from "./scenario-extractor.js";
22
+ import { isStraddlingLine, signedAmount } from "./scenario-extractor.js";
23
23
 
24
24
  // ============================== shared helpers ==============================
25
25
 
@@ -323,7 +323,7 @@ function runChecks(ctx) {
323
323
  // year's turnover and not to this one.
324
324
  function salesTotal(lines) {
325
325
  let total = 0;
326
- for (const line of lines) if (line.sourceJournalID === "sales" && !isStraddlingLine(line)) total += line.amount;
326
+ for (const line of lines) if (line.sourceJournalID === "sales" && !isStraddlingLine(line)) total += signedAmount(line);
327
327
  return total;
328
328
  }
329
329
 
@@ -6,7 +6,7 @@
6
6
  // read returns, so app/products/bst.js's reportSections() and
7
7
  // checkCompliance() work unchanged on either source.
8
8
 
9
- import { BST_SALES_ACCOUNTS, CIS_DEDUCTION_FIELD, MONTH_ORDER, getMonthKey } from "../scenario-extractor.js";
9
+ import { BST_SALES_ACCOUNTS, CIS_DEDUCTION_FIELD, MONTH_ORDER, getMonthKey, signedAmount } from "../scenario-extractor.js";
10
10
  import { resolveBstPurchaseCodeMap } from "../diya-gl-loader.js";
11
11
  import { fixedAssetAdditions } from "../scenario-loader.js";
12
12
  import { calculateIncomeTax } from "../tax/income-tax.js";
@@ -91,14 +91,14 @@ export function calculateBstResults(book, lines, taxData, scenario) {
91
91
  const purchaseLines = lines.filter((l) => l.sourceJournalID === "purchases" && purchaseCodeMap[l.accountMainID] !== undefined);
92
92
 
93
93
  // Total sales (BST: gross, no VAT split)
94
- const totalSales = Math.round(salesLines.reduce((s, l) => s + l.amount, 0));
94
+ const totalSales = Math.round(salesLines.reduce((s, l) => s + signedAmount(l), 0));
95
95
 
96
96
  // Monthly sales
97
97
  const monthlySales = {};
98
98
  for (const month of MONTH_ORDER) monthlySales[month] = 0;
99
99
  for (const line of salesLines) {
100
100
  const month = getMonthKey(line.postingDate);
101
- monthlySales[month] += line.amount;
101
+ monthlySales[month] += signedAmount(line);
102
102
  }
103
103
 
104
104
  // Purchase expenses by code. A mileage-log entry is left out: it buys
@@ -3,7 +3,7 @@
3
3
  //
4
4
  // shared.js — Aggregation helpers used by more than one product calculator.
5
5
 
6
- import { getMonthKey } from "../scenario-extractor.js";
6
+ import { getMonthKey, signedAmount } from "../scenario-extractor.js";
7
7
 
8
8
  /**
9
9
  * Group lines by accountMainID and month, computing totals.
@@ -49,7 +49,7 @@ export function aggregateByCode(lines, codeMap) {
49
49
  const byCode = {};
50
50
  for (const line of lines) {
51
51
  const code = codeMap[line.accountMainID];
52
- if (code) byCode[code] = (byCode[code] || 0) + line.amount;
52
+ if (code) byCode[code] = (byCode[code] || 0) + signedAmount(line);
53
53
  }
54
54
  return byCode;
55
55
  }
@@ -10,6 +10,7 @@ import {
10
10
  MONTH_ORDER,
11
11
  getMonthKey,
12
12
  buildTaxMonthByDate,
13
+ signedAmount,
13
14
  } from "../scenario-extractor.js";
14
15
  import { fixedAssetAdditions } from "../scenario-loader.js";
15
16
  import { calculateIncomeTax } from "../tax/income-tax.js";
@@ -89,7 +90,7 @@ function aggregateByCodeAndMonth(lines, codeMap) {
89
90
  const code = codeMap[line.accountMainID];
90
91
  if (!code) continue;
91
92
  const month = getMonthKey(line.postingDate);
92
- byMonth[month][code] = (byMonth[month][code] || 0) + line.amount;
93
+ byMonth[month][code] = (byMonth[month][code] || 0) + signedAmount(line);
93
94
  }
94
95
  return byMonth;
95
96
  }
@@ -117,17 +118,17 @@ export function calculateTaxiResults(book, lines, taxData, scenario) {
117
118
  const byCode = aggregateByCode(cashPurchaseLines, TAXI_PURCHASE_CODE_MAP);
118
119
  const byCodeAndMonth = aggregateByCodeAndMonth(cashPurchaseLines, TAXI_PURCHASE_CODE_MAP);
119
120
 
120
- const totalSales = Math.floor(salesLines.reduce((s, l) => s + l.amount, 0));
121
+ const totalSales = Math.floor(salesLines.reduce((s, l) => s + signedAmount(l), 0));
121
122
  const monthlySales = {};
122
123
  for (const month of MONTH_ORDER) monthlySales[month] = 0;
123
- for (const line of salesLines) monthlySales[byDate.get(line.postingDate)] += line.amount;
124
+ for (const line of salesLines) monthlySales[byDate.get(line.postingDate)] += signedAmount(line);
124
125
 
125
126
  // Other business income, month by month on the same week-based tab a
126
127
  // fare's date falls in -- the Sales tab's rental and other-income rows
127
128
  // belong to a week exactly as a fare day does.
128
129
  const monthlyOther = {};
129
130
  for (const month of MONTH_ORDER) monthlyOther[month] = 0;
130
- for (const line of otherIncomeLines) monthlyOther[byDate.get(line.postingDate)] += line.amount;
131
+ for (const line of otherIncomeLines) monthlyOther[byDate.get(line.postingDate)] += signedAmount(line);
131
132
 
132
133
  // Capital allowances, from the fixed asset register cellWrites registers
133
134
  // (the same additions cellWrites itself derives via fixedAssetAdditions()).
@@ -31,6 +31,7 @@ import {
31
31
  computeSpreadsheetNetSales,
32
32
  splitStraddlingLines,
33
33
  deriveStraddlingEntries,
34
+ signedAmount,
34
35
  } from "./scenario-extractor.js";
35
36
  import { totalBusinessMiles, calculateMileageAllowance, HMRC_CAR_MILEAGE_RATES } from "./tax/mileage.js";
36
37
  import { compareLines } from "./diya-gl-canonical.js";
@@ -314,14 +315,14 @@ export function diyaGlToScenario(book, lines, product) {
314
315
  const turnoverLines = salesLines.filter((l) => TURNOVER_ACCOUNTS.has(l.accountMainID));
315
316
  totalSales = vatRegistered
316
317
  ? computeSpreadsheetNetSales(turnoverLines)
317
- : Math.round(turnoverLines.reduce((sum, l) => sum + l.amount, 0));
318
+ : Math.round(turnoverLines.reduce((sum, l) => sum + signedAmount(l), 0));
318
319
  }
319
320
 
320
321
  // Compute expense totals by code.
321
322
  const byCode = {};
322
323
  purchaseLines.forEach((l) => {
323
324
  const code = purchaseCodeMap[l.accountMainID];
324
- if (code) byCode[code] = (byCode[code] || 0) + l.amount;
325
+ if (code) byCode[code] = (byCode[code] || 0) + signedAmount(l);
325
326
  });
326
327
 
327
328
  // Build metadata from book.toml
@@ -382,7 +383,7 @@ export function diyaGlToScenario(book, lines, product) {
382
383
  .filter((l) => !(l.measurableUnitOfMeasure === "miles" && typeof l.measurableQuantity === "number"))
383
384
  .forEach((l) => {
384
385
  const code = purchaseCodeMap[l.accountMainID];
385
- if (code) bstByCode[code] = (bstByCode[code] || 0) + l.amount;
386
+ if (code) bstByCode[code] = (bstByCode[code] || 0) + signedAmount(l);
386
387
  });
387
388
  if (businessMiles) bstByCode.m = (bstByCode.m || 0) + calculateMileageAllowance(businessMiles, HMRC_CAR_MILEAGE_RATES);
388
389
 
@@ -415,7 +416,7 @@ export function diyaGlToScenario(book, lines, product) {
415
416
  const cashMotor = purchaseLines
416
417
  .filter((l) => !(product === "se" && l.measurableUnitOfMeasure === "miles" && typeof l.measurableQuantity === "number"))
417
418
  .filter((l) => purchaseCodeMap[l.accountMainID] === "v")
418
- .reduce((sum, l) => sum + l.amount, 0);
419
+ .reduce((sum, l) => sum + signedAmount(l), 0);
419
420
  const mileageClaim = product === "se" ? calculateMileageAllowance(businessMiles, HMRC_CAR_MILEAGE_RATES) : 0;
420
421
  expected.total_motor_net = Math.round(cashMotor / vatDivisor + mileageClaim);
421
422
  expected.total_legal_net = Math.round((byCode.l || 0) / vatDivisor);
@@ -12,7 +12,7 @@
12
12
 
13
13
  export const PROVENANCE_DATA = {
14
14
  formatVersion: "diya-gl/1",
15
- engineVersion: "1.2.31+80457d57",
15
+ engineVersion: "1.2.33+92a3639b",
16
16
  taxDataHash: "3221a7c2f39c",
17
17
  reconciledCommit: "54e8731a91279975e61b1a232fbb67fda89515f0",
18
18
  templates: {
@@ -514,11 +514,30 @@ export function tomlLocalDate(value) {
514
514
  return value instanceof Date ? value.toISOString().slice(0, 10) : value;
515
515
  }
516
516
 
517
+ // Bad debts written off (4005, sales code "o") and fixed-asset disposals
518
+ // (4006, code "fs") already carry their own sign in the sales code's own
519
+ // P&L formula (see LTD_SALES_CODE_MAP and the "o"/"fs" rows CONTEXT_SELF_
520
+ // EMPLOYED.md and CONTEXT_LIMITED_COMPANY.md describe), so negating a
521
+ // credit note against either account here would flip an already-correct
522
+ // figure a second time.
523
+ const SALES_ACCOUNTS_WITH_OWN_SIGN = new Set(["4005", "4006"]);
524
+
525
+ // A credit note reverses part of the sale or purchase it names. The schema
526
+ // fixes a line's amount at zero or more -- only documentType says which way
527
+ // it points -- so every total that sums a sales or purchases line's amount
528
+ // reads this instead of the raw field, or a refund inflates turnover (and a
529
+ // supplier's credit inflates spend) instead of reducing it.
530
+ export function signedAmount(line) {
531
+ if (line.documentType !== "credit-note") return line.amount;
532
+ if (line.sourceJournalID === "sales" && SALES_ACCOUNTS_WITH_OWN_SIGN.has(String(line.accountMainID))) return line.amount;
533
+ return -line.amount;
534
+ }
535
+
517
536
  export function computeNetSales(salesLines) {
518
537
  let netTotal = 0;
519
538
  for (const line of salesLines) {
520
539
  const rate = line.taxRate || 0;
521
- netTotal += line.amount / (1 + rate);
540
+ netTotal += signedAmount(line) / (1 + rate);
522
541
  }
523
542
  return Math.round(netTotal);
524
543
  }
@@ -527,14 +546,14 @@ export function computeNetSales(salesLines) {
527
546
  export function computeSpreadsheetNetSales(salesLines) {
528
547
  let netTotal = 0;
529
548
  for (const line of salesLines) {
530
- netTotal += line.amount / 1.2;
549
+ netTotal += signedAmount(line) / 1.2;
531
550
  }
532
551
  return Math.round(netTotal);
533
552
  }
534
553
 
535
554
  // BST: amounts are entered as-is (no VAT split), so total = sum of amounts
536
555
  export function computeGrossSales(salesLines) {
537
- return Math.round(salesLines.reduce((sum, line) => sum + line.amount, 0));
556
+ return Math.round(salesLines.reduce((sum, line) => sum + signedAmount(line), 0));
538
557
  }
539
558
 
540
559
  // ============================================================================
@@ -816,7 +835,7 @@ export function buildGrouped(
816
835
  date: line.postingDate,
817
836
  customer: line.detailComment,
818
837
  code,
819
- amount: line.amount,
838
+ amount: signedAmount(line),
820
839
  };
821
840
  if (carriesSourceFields) {
822
841
  sale.account = line.accountMainID;
@@ -836,7 +855,7 @@ export function buildGrouped(
836
855
  date: line.postingDate,
837
856
  supplier: line.detailComment,
838
857
  code,
839
- amount: line.amount,
858
+ amount: signedAmount(line),
840
859
  };
841
860
  if (carriesSourceFields) {
842
861
  purchase.account = line.accountMainID;
@@ -891,7 +910,7 @@ export function totalsByCode(lines, purchaseCodeMap) {
891
910
  if (line.sourceJournalID !== "purchases") continue;
892
911
  const code = purchaseCodeMap[line.accountMainID];
893
912
  if (!code) continue;
894
- totals[code] = Math.round(((totals[code] || 0) + line.amount) * 100) / 100;
913
+ totals[code] = Math.round(((totals[code] || 0) + signedAmount(line)) * 100) / 100;
895
914
  }
896
915
  return totals;
897
916
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diy-accounting-uk/diya-gl",
3
- "version": "1.2.31",
3
+ "version": "1.2.33",
4
4
  "type": "module",
5
5
  "description": "Recalculate, read and write DIYA-GL books from the command line",
6
6
  "license": "Apache-2.0",