@fuzzle/opencode-accountant 0.13.1 → 0.13.2-next.1

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.
Files changed (2) hide show
  1. package/dist/index.js +165 -83
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -24456,6 +24456,26 @@ function createImportLogger(directory, worktreeId, provider) {
24456
24456
 
24457
24457
  // src/utils/btcPurchaseGenerator.ts
24458
24458
  import * as fs14 from "fs";
24459
+
24460
+ // src/utils/journalFormatting.ts
24461
+ var MIN_GAP = 5;
24462
+ var INDENT = " ";
24463
+ function formatPostings(postings) {
24464
+ const maxAccountLen = postings.reduce((max, p) => Math.max(max, p.account.length), 0);
24465
+ return postings.map((p) => {
24466
+ const isNegative = p.amount.startsWith("-");
24467
+ const signOffset = isNegative ? 1 : 0;
24468
+ const gap = Math.max(MIN_GAP - signOffset, maxAccountLen - p.account.length + MIN_GAP - signOffset);
24469
+ const line = `${INDENT}${p.account}${" ".repeat(gap)}${p.amount}`;
24470
+ if (p.comment) {
24471
+ return `${line} ${p.comment}`;
24472
+ }
24473
+ return line;
24474
+ }).join(`
24475
+ `);
24476
+ }
24477
+
24478
+ // src/utils/btcPurchaseGenerator.ts
24459
24479
  function parseRevolutFiatDatetime(dateStr) {
24460
24480
  const [datePart, timePart] = dateStr.split(" ");
24461
24481
  if (!datePart || !timePart) {
@@ -24669,30 +24689,33 @@ function formatJournalEntry(match2) {
24669
24689
  const { fiatRow, btcRow } = match2;
24670
24690
  const date5 = fiatRow.dateStr;
24671
24691
  const fiatCurrency = fiatRow.currency;
24672
- const fiatAmount = formatAmount(fiatRow.amount);
24673
24692
  const btcQuantity = formatBtcQuantity(btcRow.quantity);
24674
- const btcPrice = formatAmount(btcRow.price.amount);
24675
24693
  const priceCurrency = btcRow.price.currency;
24676
24694
  const hasFees = btcRow.fees.amount > 0;
24677
24695
  const isBaseCurrency = fiatCurrency === "CHF";
24696
+ const netFiatAmount = hasFees ? fiatRow.amount - btcRow.fees.amount : fiatRow.amount;
24697
+ const effectivePrice = hasFees ? netFiatAmount / btcRow.quantity : btcRow.price.amount;
24698
+ const fiatAmount = formatAmount(fiatRow.amount);
24699
+ const netFiat = formatAmount(netFiatAmount);
24700
+ const btcPrice = formatAmount(effectivePrice);
24678
24701
  const pair = `${fiatCurrency.toLowerCase()}-btc`;
24679
24702
  const equityFiat = `equity:conversion:${pair}:${fiatCurrency.toLowerCase()}`;
24680
24703
  const equityBtc = `equity:conversion:${pair}:btc`;
24681
- const assetCostAnnotation = isBaseCurrency ? ` @ ${btcPrice} ${priceCurrency}` : "";
24682
- const lines = [
24683
- `${date5} Bitcoin purchase`,
24684
- ` assets:bank:revolut:${fiatCurrency.toLowerCase()} -${fiatAmount} ${fiatCurrency}`,
24685
- ` ${equityFiat} ${fiatAmount} ${fiatCurrency}`,
24686
- ` ${equityBtc} -${btcQuantity} BTC`,
24687
- ` assets:bank:revolut:btc ${btcQuantity} BTC${assetCostAnnotation}`
24704
+ const costAnnotation = isBaseCurrency ? ` @ ${btcPrice} ${priceCurrency}` : "";
24705
+ const postings = [
24706
+ {
24707
+ account: `assets:bank:revolut:${fiatCurrency.toLowerCase()}`,
24708
+ amount: `-${fiatAmount} ${fiatCurrency}`
24709
+ }
24688
24710
  ];
24689
24711
  if (hasFees) {
24690
24712
  const feeAmount = formatAmount(btcRow.fees.amount);
24691
24713
  const feeCurrency = btcRow.fees.currency;
24692
- lines.push(` expenses:fees:btc ${feeAmount} ${feeCurrency}`, ` ${equityFiat} -${feeAmount} ${feeCurrency}`);
24714
+ postings.push({ account: "expenses:fees:btc", amount: `${feeAmount} ${feeCurrency}` });
24693
24715
  }
24694
- return lines.join(`
24695
- `);
24716
+ postings.push({ account: equityFiat, amount: `${netFiat} ${fiatCurrency}` }, { account: equityBtc, amount: `-${btcQuantity} BTC` }, { account: "assets:bank:revolut:btc", amount: `${btcQuantity} BTC${costAnnotation}` });
24717
+ return `${date5} Bitcoin purchase
24718
+ ${formatPostings(postings)}`;
24696
24719
  }
24697
24720
  function formatAmount(amount) {
24698
24721
  return amount.toFixed(2);
@@ -25130,17 +25153,27 @@ function generateBuyEntry(trade, logger) {
25130
25153
  const cashOut = totalCost + fees;
25131
25154
  logger?.debug(`Generating Buy entry: ${qty} ${trade.symbol} @ ${price} ${trade.currency}`);
25132
25155
  const commodity = formatCommodity(trade.symbol);
25156
+ const postings = [
25157
+ {
25158
+ account: `assets:investments:stocks:${trade.symbol}`,
25159
+ amount: `${qty} ${commodity} @ ${price} ${trade.currency}`
25160
+ }
25161
+ ];
25162
+ if (fees > 0) {
25163
+ postings.push({
25164
+ account: "expenses:fees:trading:swissquote",
25165
+ amount: formatAmount2(fees, trade.currency)
25166
+ });
25167
+ }
25168
+ postings.push({
25169
+ account: `assets:broker:swissquote:${trade.currency.toLowerCase()}`,
25170
+ amount: formatAmount2(-cashOut, trade.currency)
25171
+ });
25133
25172
  let entry = `${date5} ${description}
25134
25173
  `;
25135
25174
  entry += ` ; swissquote:order:${trade.orderNum} isin:${trade.isin}
25136
25175
  `;
25137
- entry += ` assets:investments:stocks:${trade.symbol} ${qty} ${commodity} @ ${price} ${trade.currency}
25138
- `;
25139
- if (fees > 0) {
25140
- entry += ` expenses:fees:trading:swissquote ${formatAmount2(fees, trade.currency)}
25141
- `;
25142
- }
25143
- entry += ` assets:broker:swissquote:${trade.currency.toLowerCase()} ${formatAmount2(-cashOut, trade.currency)}
25176
+ entry += formatPostings(postings) + `
25144
25177
  `;
25145
25178
  return entry;
25146
25179
  }
@@ -25155,49 +25188,72 @@ function generateSellEntry(trade, consumed, logger) {
25155
25188
  const gain = calculateCapitalGain(consumed, salePrice, trade.quantity);
25156
25189
  logger?.debug(`Generating Sell entry: ${qty} ${trade.symbol} @ ${salePrice} ${trade.currency}, gain: ${gain.toFixed(2)}`);
25157
25190
  const commodity = formatCommodity(trade.symbol);
25158
- let entry = `${date5} ${description}
25159
- `;
25160
- entry += ` ; swissquote:order:${trade.orderNum} isin:${trade.isin}
25161
- `;
25162
25191
  const lotDetails = consumed.map((c) => `${c.lot.date}: ${formatQuantity(c.quantity)}@${formatPrice(c.lot.costBasis)}`).join(", ");
25163
- entry += ` ; FIFO lots: ${lotDetails}
25164
- `;
25192
+ const postings = [];
25165
25193
  for (const c of consumed) {
25166
25194
  const lotQty = formatQuantity(c.quantity);
25167
25195
  const lotPrice = formatPrice(c.lot.costBasis);
25168
- entry += ` assets:investments:stocks:${trade.symbol} -${lotQty} ${commodity} @ ${lotPrice} ${trade.currency}
25169
- `;
25196
+ postings.push({
25197
+ account: `assets:investments:stocks:${trade.symbol}`,
25198
+ amount: `-${lotQty} ${commodity} @ ${lotPrice} ${trade.currency}`
25199
+ });
25170
25200
  }
25171
- entry += ` assets:broker:swissquote:${trade.currency.toLowerCase()} ${formatAmount2(cashIn, trade.currency)}
25172
- `;
25201
+ postings.push({
25202
+ account: `assets:broker:swissquote:${trade.currency.toLowerCase()}`,
25203
+ amount: formatAmount2(cashIn, trade.currency)
25204
+ });
25173
25205
  if (fees > 0) {
25174
- entry += ` expenses:fees:trading:swissquote ${formatAmount2(fees, trade.currency)}
25175
- `;
25206
+ postings.push({
25207
+ account: "expenses:fees:trading:swissquote",
25208
+ amount: formatAmount2(fees, trade.currency)
25209
+ });
25176
25210
  }
25177
25211
  if (gain >= 0) {
25178
- entry += ` income:capital-gains:realized ${formatAmount2(-gain, trade.currency)}
25179
- `;
25212
+ postings.push({
25213
+ account: "income:capital-gains:realized",
25214
+ amount: formatAmount2(-gain, trade.currency)
25215
+ });
25180
25216
  } else {
25181
- entry += ` expenses:losses:capital ${formatAmount2(-gain, trade.currency)}
25182
- `;
25217
+ postings.push({
25218
+ account: "expenses:losses:capital",
25219
+ amount: formatAmount2(-gain, trade.currency)
25220
+ });
25183
25221
  }
25222
+ let entry = `${date5} ${description}
25223
+ `;
25224
+ entry += ` ; swissquote:order:${trade.orderNum} isin:${trade.isin}
25225
+ `;
25226
+ entry += ` ; FIFO lots: ${lotDetails}
25227
+ `;
25228
+ entry += formatPostings(postings) + `
25229
+ `;
25184
25230
  return entry;
25185
25231
  }
25186
25232
  function generateDividendEntry(dividend, logger) {
25187
25233
  const date5 = formatDate(dividend.date);
25188
25234
  const description = escapeDescription(`Dividend ${dividend.symbol} - ${dividend.name}`);
25189
25235
  logger?.debug(`Generating Dividend entry: ${dividend.symbol}, net: ${dividend.netAmount} ${dividend.currency}`);
25236
+ const postings = [
25237
+ {
25238
+ account: `assets:broker:swissquote:${dividend.currency.toLowerCase()}`,
25239
+ amount: formatAmount2(dividend.netAmount, dividend.currency)
25240
+ }
25241
+ ];
25242
+ if (dividend.withholdingTax > 0) {
25243
+ postings.push({
25244
+ account: "expenses:taxes:withholding",
25245
+ amount: formatAmount2(dividend.withholdingTax, dividend.currency)
25246
+ });
25247
+ }
25248
+ postings.push({
25249
+ account: `income:dividends:${dividend.symbol}`,
25250
+ amount: formatAmount2(-dividend.grossAmount, dividend.currency)
25251
+ });
25190
25252
  let entry = `${date5} ${description}
25191
25253
  `;
25192
25254
  entry += ` ; swissquote:order:${dividend.orderNum} isin:${dividend.isin}
25193
25255
  `;
25194
- entry += ` assets:broker:swissquote:${dividend.currency.toLowerCase()} ${formatAmount2(dividend.netAmount, dividend.currency)}
25195
- `;
25196
- if (dividend.withholdingTax > 0) {
25197
- entry += ` expenses:taxes:withholding ${formatAmount2(dividend.withholdingTax, dividend.currency)}
25198
- `;
25199
- }
25200
- entry += ` income:dividends:${dividend.symbol} ${formatAmount2(-dividend.grossAmount, dividend.currency)}
25256
+ entry += formatPostings(postings) + `
25201
25257
  `;
25202
25258
  return entry;
25203
25259
  }
@@ -25207,20 +25263,26 @@ function generateSplitEntry(action, oldQuantity, newQuantity, logger) {
25207
25263
  const splitType = ratio > 1 ? "Split" : "Reverse Split";
25208
25264
  const description = escapeDescription(`${splitType}: ${action.symbol} (${action.name})`);
25209
25265
  logger?.debug(`Generating ${splitType} entry: ${oldQuantity} -> ${newQuantity} ${action.symbol}`);
25266
+ const commodity = formatCommodity(action.symbol);
25267
+ const postings = [
25268
+ {
25269
+ account: `assets:investments:stocks:${action.symbol}`,
25270
+ amount: `-${formatQuantity(oldQuantity)} ${commodity}`
25271
+ },
25272
+ { account: "equity:conversion", amount: `${formatQuantity(oldQuantity)} ${commodity}` },
25273
+ { account: "equity:conversion", amount: `-${formatQuantity(newQuantity)} ${commodity}` },
25274
+ {
25275
+ account: `assets:investments:stocks:${action.symbol}`,
25276
+ amount: `${formatQuantity(newQuantity)} ${commodity}`
25277
+ }
25278
+ ];
25210
25279
  let entry = `${date5} ${description}
25211
25280
  `;
25212
25281
  entry += ` ; swissquote:order:${action.orderNum} isin:${action.isin}
25213
25282
  `;
25214
25283
  entry += ` ; Ratio: ${ratio.toFixed(4)} (${oldQuantity} -> ${newQuantity})
25215
25284
  `;
25216
- const commodity = formatCommodity(action.symbol);
25217
- entry += ` assets:investments:stocks:${action.symbol} -${formatQuantity(oldQuantity)} ${commodity}
25218
- `;
25219
- entry += ` equity:conversion ${formatQuantity(oldQuantity)} ${commodity}
25220
- `;
25221
- entry += ` equity:conversion -${formatQuantity(newQuantity)} ${commodity}
25222
- `;
25223
- entry += ` assets:investments:stocks:${action.symbol} ${formatQuantity(newQuantity)} ${commodity}
25285
+ entry += formatPostings(postings) + `
25224
25286
  `;
25225
25287
  return entry;
25226
25288
  }
@@ -25231,20 +25293,24 @@ function generateWorthlessEntry(action, removedLots, logger) {
25231
25293
  const totalCost = removedLots.reduce((sum, lot) => sum + lot.quantity * lot.costBasis, 0);
25232
25294
  const currency = removedLots[0]?.currency || "USD";
25233
25295
  logger?.debug(`Generating Worthless entry: ${totalQuantity} ${action.symbol}, loss: ${totalCost} ${currency}`);
25296
+ const commodity = formatCommodity(action.symbol);
25297
+ const postings = [];
25298
+ for (const lot of removedLots) {
25299
+ const qty = formatQuantity(lot.quantity);
25300
+ const price = formatPrice(lot.costBasis);
25301
+ postings.push({
25302
+ account: `assets:investments:stocks:${action.symbol}`,
25303
+ amount: `-${qty} ${commodity} @ ${price} ${currency}`
25304
+ });
25305
+ }
25306
+ postings.push({ account: "expenses:losses:capital", amount: formatAmount2(totalCost, currency) });
25234
25307
  let entry = `${date5} ${description}
25235
25308
  `;
25236
25309
  entry += ` ; swissquote:order:${action.orderNum} isin:${action.isin}
25237
25310
  `;
25238
25311
  entry += ` ; Total loss: ${totalCost.toFixed(2)} ${currency}
25239
25312
  `;
25240
- const commodity = formatCommodity(action.symbol);
25241
- for (const lot of removedLots) {
25242
- const qty = formatQuantity(lot.quantity);
25243
- const price = formatPrice(lot.costBasis);
25244
- entry += ` assets:investments:stocks:${action.symbol} -${qty} ${commodity} @ ${price} ${currency}
25245
- `;
25246
- }
25247
- entry += ` expenses:losses:capital ${formatAmount2(totalCost, currency)}
25313
+ entry += formatPostings(postings) + `
25248
25314
  `;
25249
25315
  return entry;
25250
25316
  }
@@ -25255,32 +25321,37 @@ function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, crossC
25255
25321
  const descriptionParts = outSymbols.join(" + ");
25256
25322
  const description = escapeDescription(inSymbols.length > 0 ? `Merger: ${descriptionParts} -> ${inSymbols.join(" + ")}` : `Merger: ${descriptionParts}`);
25257
25323
  logger?.debug(`Generating multi-way merger entry: ${outSymbols.join(", ")} -> ${inSymbols.join(", ")}`);
25258
- let entry = `${date5} ${description}
25259
- `;
25260
- entry += ` ; swissquote:order:${group.orderNum}
25261
- `;
25262
25324
  const oldIsins = (crossCurrencyOutgoingIsins ?? group.outgoing.map((a) => a.isin)).filter(Boolean);
25263
25325
  const newIsins = group.incoming.map((a) => a.isin).filter(Boolean);
25264
- if (oldIsins.length > 0 || newIsins.length > 0) {
25265
- entry += ` ; Old ISIN: ${oldIsins.join(", ") || "n/a"}, New ISINs: ${newIsins.join(", ") || "n/a"}
25266
- `;
25267
- }
25326
+ const postings = [];
25268
25327
  for (const out of group.outgoing) {
25269
25328
  const qty = formatQuantity(Math.abs(out.quantity));
25270
25329
  const commodity = formatCommodity(out.symbol);
25271
- entry += ` assets:investments:stocks:${out.symbol} -${qty} ${commodity}
25272
- `;
25273
- entry += ` equity:conversion ${qty} ${commodity}
25274
- `;
25330
+ postings.push({
25331
+ account: `assets:investments:stocks:${out.symbol}`,
25332
+ amount: `-${qty} ${commodity}`
25333
+ });
25334
+ postings.push({ account: "equity:conversion", amount: `${qty} ${commodity}` });
25275
25335
  }
25276
25336
  for (const inc of group.incoming) {
25277
25337
  const qty = formatQuantity(Math.abs(inc.quantity));
25278
25338
  const commodity = formatCommodity(inc.symbol);
25279
- entry += ` equity:conversion -${qty} ${commodity}
25339
+ postings.push({ account: "equity:conversion", amount: `-${qty} ${commodity}` });
25340
+ postings.push({
25341
+ account: `assets:investments:stocks:${inc.symbol}`,
25342
+ amount: `${qty} ${commodity}`
25343
+ });
25344
+ }
25345
+ let entry = `${date5} ${description}
25280
25346
  `;
25281
- entry += ` assets:investments:stocks:${inc.symbol} ${qty} ${commodity}
25347
+ entry += ` ; swissquote:order:${group.orderNum}
25348
+ `;
25349
+ if (oldIsins.length > 0 || newIsins.length > 0) {
25350
+ entry += ` ; Old ISIN: ${oldIsins.join(", ") || "n/a"}, New ISINs: ${newIsins.join(", ") || "n/a"}
25282
25351
  `;
25283
25352
  }
25353
+ entry += formatPostings(postings) + `
25354
+ `;
25284
25355
  return entry;
25285
25356
  }
25286
25357
  function generateRightsDistributionEntry(action, logger) {
@@ -25289,13 +25360,18 @@ function generateRightsDistributionEntry(action, logger) {
25289
25360
  const description = escapeDescription(`Rights Distribution: ${action.symbol} - ${action.name}`);
25290
25361
  logger?.debug(`Generating Rights Distribution entry: ${qty} ${action.symbol}`);
25291
25362
  const commodity = formatCommodity(action.symbol);
25363
+ const postings = [
25364
+ {
25365
+ account: `assets:investments:stocks:${action.symbol}`,
25366
+ amount: `${qty} ${commodity} @ 0.00 CAD`
25367
+ },
25368
+ { account: "income:capital-gains:rights-distribution", amount: "0.00 CAD" }
25369
+ ];
25292
25370
  let entry = `${date5} ${description}
25293
25371
  `;
25294
25372
  entry += ` ; swissquote:order:${action.orderNum} isin:${action.isin}
25295
25373
  `;
25296
- entry += ` assets:investments:stocks:${action.symbol} ${qty} ${commodity} @ 0.00 CAD
25297
- `;
25298
- entry += ` income:capital-gains:rights-distribution 0.00 CAD
25374
+ entry += formatPostings(postings) + `
25299
25375
  `;
25300
25376
  return entry;
25301
25377
  }
@@ -25996,14 +26072,20 @@ function formatExchangeEntry(match2) {
25996
26072
  const pair = `${sourceCurrency.toLowerCase()}-${targetCurrency.toLowerCase()}`;
25997
26073
  const equitySource = `equity:conversion:${pair}:${sourceCurrency.toLowerCase()}`;
25998
26074
  const equityTarget = `equity:conversion:${pair}:${targetCurrency.toLowerCase()}`;
25999
- return [
26000
- `${date5} ${description}`,
26001
- ` assets:bank:revolut:${sourceCurrency.toLowerCase()} -${sourceAmount} ${sourceCurrency}`,
26002
- ` ${equitySource} ${sourceAmount} ${sourceCurrency}`,
26003
- ` ${equityTarget} -${targetAmount} ${targetCurrency}`,
26004
- ` assets:bank:revolut:${targetCurrency.toLowerCase()} ${targetAmount} ${targetCurrency}`
26005
- ].join(`
26006
- `);
26075
+ const postings = [
26076
+ {
26077
+ account: `assets:bank:revolut:${sourceCurrency.toLowerCase()}`,
26078
+ amount: `-${sourceAmount} ${sourceCurrency}`
26079
+ },
26080
+ { account: equitySource, amount: `${sourceAmount} ${sourceCurrency}` },
26081
+ { account: equityTarget, amount: `-${targetAmount} ${targetCurrency}` },
26082
+ {
26083
+ account: `assets:bank:revolut:${targetCurrency.toLowerCase()}`,
26084
+ amount: `${targetAmount} ${targetCurrency}`
26085
+ }
26086
+ ];
26087
+ return `${date5} ${description}
26088
+ ${formatPostings(postings)}`;
26007
26089
  }
26008
26090
  function formatAmount3(amount) {
26009
26091
  return amount.toFixed(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.13.1",
3
+ "version": "0.13.2-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",