@fuzzle/opencode-accountant 0.13.1 → 0.13.2

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