@fuzzle/opencode-accountant 0.13.0 → 0.13.1-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.
- package/dist/index.js +212 -87
- 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
|
|
24682
|
-
const
|
|
24683
|
-
|
|
24684
|
-
|
|
24685
|
-
|
|
24686
|
-
|
|
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
|
-
|
|
24712
|
+
postings.push({ account: "expenses:fees:btc", amount: `${feeAmount} ${feeCurrency}` });
|
|
24693
24713
|
}
|
|
24694
|
-
|
|
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);
|
|
@@ -24745,8 +24766,18 @@ function generateBtcPurchaseJournal(fiatCsvPaths, btcCsvPath, yearJournalPath, l
|
|
|
24745
24766
|
}
|
|
24746
24767
|
const newEntries = [];
|
|
24747
24768
|
let skippedDuplicates = 0;
|
|
24769
|
+
const accountsUsed = new Set;
|
|
24748
24770
|
const sortedMatches = [...matches].sort((a, b) => a.fiatRow.date.getTime() - b.fiatRow.date.getTime());
|
|
24749
24771
|
for (const match2 of sortedMatches) {
|
|
24772
|
+
const fiat = match2.fiatRow.currency.toLowerCase();
|
|
24773
|
+
const pair = `${fiat}-btc`;
|
|
24774
|
+
accountsUsed.add(`assets:bank:revolut:${fiat}`);
|
|
24775
|
+
accountsUsed.add(`equity:conversion:${pair}:${fiat}`);
|
|
24776
|
+
accountsUsed.add(`equity:conversion:${pair}:btc`);
|
|
24777
|
+
accountsUsed.add("assets:bank:revolut:btc");
|
|
24778
|
+
if (match2.btcRow.fees.amount > 0) {
|
|
24779
|
+
accountsUsed.add("expenses:fees:btc");
|
|
24780
|
+
}
|
|
24750
24781
|
if (isDuplicate(match2, journalContent)) {
|
|
24751
24782
|
skippedDuplicates++;
|
|
24752
24783
|
logger?.debug(`Skipping duplicate: ${match2.fiatRow.dateStr} ${formatAmount(match2.fiatRow.amount)} ${match2.fiatRow.currency}`);
|
|
@@ -24770,7 +24801,8 @@ function generateBtcPurchaseJournal(fiatCsvPaths, btcCsvPath, yearJournalPath, l
|
|
|
24770
24801
|
entriesAdded: newEntries.length,
|
|
24771
24802
|
skippedDuplicates,
|
|
24772
24803
|
unmatchedFiat,
|
|
24773
|
-
unmatchedBtc
|
|
24804
|
+
unmatchedBtc,
|
|
24805
|
+
accountsUsed
|
|
24774
24806
|
};
|
|
24775
24807
|
}
|
|
24776
24808
|
|
|
@@ -25119,17 +25151,27 @@ function generateBuyEntry(trade, logger) {
|
|
|
25119
25151
|
const cashOut = totalCost + fees;
|
|
25120
25152
|
logger?.debug(`Generating Buy entry: ${qty} ${trade.symbol} @ ${price} ${trade.currency}`);
|
|
25121
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
|
+
});
|
|
25122
25170
|
let entry = `${date5} ${description}
|
|
25123
25171
|
`;
|
|
25124
25172
|
entry += ` ; swissquote:order:${trade.orderNum} isin:${trade.isin}
|
|
25125
25173
|
`;
|
|
25126
|
-
entry +=
|
|
25127
|
-
`;
|
|
25128
|
-
if (fees > 0) {
|
|
25129
|
-
entry += ` expenses:fees:trading:swissquote ${formatAmount2(fees, trade.currency)}
|
|
25130
|
-
`;
|
|
25131
|
-
}
|
|
25132
|
-
entry += ` assets:broker:swissquote:${trade.currency.toLowerCase()} ${formatAmount2(-cashOut, trade.currency)}
|
|
25174
|
+
entry += formatPostings(postings) + `
|
|
25133
25175
|
`;
|
|
25134
25176
|
return entry;
|
|
25135
25177
|
}
|
|
@@ -25144,49 +25186,72 @@ function generateSellEntry(trade, consumed, logger) {
|
|
|
25144
25186
|
const gain = calculateCapitalGain(consumed, salePrice, trade.quantity);
|
|
25145
25187
|
logger?.debug(`Generating Sell entry: ${qty} ${trade.symbol} @ ${salePrice} ${trade.currency}, gain: ${gain.toFixed(2)}`);
|
|
25146
25188
|
const commodity = formatCommodity(trade.symbol);
|
|
25147
|
-
let entry = `${date5} ${description}
|
|
25148
|
-
`;
|
|
25149
|
-
entry += ` ; swissquote:order:${trade.orderNum} isin:${trade.isin}
|
|
25150
|
-
`;
|
|
25151
25189
|
const lotDetails = consumed.map((c) => `${c.lot.date}: ${formatQuantity(c.quantity)}@${formatPrice(c.lot.costBasis)}`).join(", ");
|
|
25152
|
-
|
|
25153
|
-
`;
|
|
25190
|
+
const postings = [];
|
|
25154
25191
|
for (const c of consumed) {
|
|
25155
25192
|
const lotQty = formatQuantity(c.quantity);
|
|
25156
25193
|
const lotPrice = formatPrice(c.lot.costBasis);
|
|
25157
|
-
|
|
25158
|
-
|
|
25194
|
+
postings.push({
|
|
25195
|
+
account: `assets:investments:stocks:${trade.symbol}`,
|
|
25196
|
+
amount: `-${lotQty} ${commodity} @ ${lotPrice} ${trade.currency}`
|
|
25197
|
+
});
|
|
25159
25198
|
}
|
|
25160
|
-
|
|
25161
|
-
|
|
25199
|
+
postings.push({
|
|
25200
|
+
account: `assets:broker:swissquote:${trade.currency.toLowerCase()}`,
|
|
25201
|
+
amount: formatAmount2(cashIn, trade.currency)
|
|
25202
|
+
});
|
|
25162
25203
|
if (fees > 0) {
|
|
25163
|
-
|
|
25164
|
-
|
|
25204
|
+
postings.push({
|
|
25205
|
+
account: "expenses:fees:trading:swissquote",
|
|
25206
|
+
amount: formatAmount2(fees, trade.currency)
|
|
25207
|
+
});
|
|
25165
25208
|
}
|
|
25166
25209
|
if (gain >= 0) {
|
|
25167
|
-
|
|
25168
|
-
|
|
25210
|
+
postings.push({
|
|
25211
|
+
account: "income:capital-gains:realized",
|
|
25212
|
+
amount: formatAmount2(-gain, trade.currency)
|
|
25213
|
+
});
|
|
25169
25214
|
} else {
|
|
25170
|
-
|
|
25171
|
-
|
|
25215
|
+
postings.push({
|
|
25216
|
+
account: "expenses:losses:capital",
|
|
25217
|
+
amount: formatAmount2(-gain, trade.currency)
|
|
25218
|
+
});
|
|
25172
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
|
+
`;
|
|
25173
25228
|
return entry;
|
|
25174
25229
|
}
|
|
25175
25230
|
function generateDividendEntry(dividend, logger) {
|
|
25176
25231
|
const date5 = formatDate(dividend.date);
|
|
25177
25232
|
const description = escapeDescription(`Dividend ${dividend.symbol} - ${dividend.name}`);
|
|
25178
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
|
+
});
|
|
25179
25250
|
let entry = `${date5} ${description}
|
|
25180
25251
|
`;
|
|
25181
25252
|
entry += ` ; swissquote:order:${dividend.orderNum} isin:${dividend.isin}
|
|
25182
25253
|
`;
|
|
25183
|
-
entry +=
|
|
25184
|
-
`;
|
|
25185
|
-
if (dividend.withholdingTax > 0) {
|
|
25186
|
-
entry += ` expenses:taxes:withholding ${formatAmount2(dividend.withholdingTax, dividend.currency)}
|
|
25187
|
-
`;
|
|
25188
|
-
}
|
|
25189
|
-
entry += ` income:dividends:${dividend.symbol} ${formatAmount2(-dividend.grossAmount, dividend.currency)}
|
|
25254
|
+
entry += formatPostings(postings) + `
|
|
25190
25255
|
`;
|
|
25191
25256
|
return entry;
|
|
25192
25257
|
}
|
|
@@ -25196,20 +25261,26 @@ function generateSplitEntry(action, oldQuantity, newQuantity, logger) {
|
|
|
25196
25261
|
const splitType = ratio > 1 ? "Split" : "Reverse Split";
|
|
25197
25262
|
const description = escapeDescription(`${splitType}: ${action.symbol} (${action.name})`);
|
|
25198
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
|
+
];
|
|
25199
25277
|
let entry = `${date5} ${description}
|
|
25200
25278
|
`;
|
|
25201
25279
|
entry += ` ; swissquote:order:${action.orderNum} isin:${action.isin}
|
|
25202
25280
|
`;
|
|
25203
25281
|
entry += ` ; Ratio: ${ratio.toFixed(4)} (${oldQuantity} -> ${newQuantity})
|
|
25204
25282
|
`;
|
|
25205
|
-
|
|
25206
|
-
entry += ` assets:investments:stocks:${action.symbol} -${formatQuantity(oldQuantity)} ${commodity}
|
|
25207
|
-
`;
|
|
25208
|
-
entry += ` equity:conversion ${formatQuantity(oldQuantity)} ${commodity}
|
|
25209
|
-
`;
|
|
25210
|
-
entry += ` equity:conversion -${formatQuantity(newQuantity)} ${commodity}
|
|
25211
|
-
`;
|
|
25212
|
-
entry += ` assets:investments:stocks:${action.symbol} ${formatQuantity(newQuantity)} ${commodity}
|
|
25283
|
+
entry += formatPostings(postings) + `
|
|
25213
25284
|
`;
|
|
25214
25285
|
return entry;
|
|
25215
25286
|
}
|
|
@@ -25220,20 +25291,24 @@ function generateWorthlessEntry(action, removedLots, logger) {
|
|
|
25220
25291
|
const totalCost = removedLots.reduce((sum, lot) => sum + lot.quantity * lot.costBasis, 0);
|
|
25221
25292
|
const currency = removedLots[0]?.currency || "USD";
|
|
25222
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) });
|
|
25223
25305
|
let entry = `${date5} ${description}
|
|
25224
25306
|
`;
|
|
25225
25307
|
entry += ` ; swissquote:order:${action.orderNum} isin:${action.isin}
|
|
25226
25308
|
`;
|
|
25227
25309
|
entry += ` ; Total loss: ${totalCost.toFixed(2)} ${currency}
|
|
25228
25310
|
`;
|
|
25229
|
-
|
|
25230
|
-
for (const lot of removedLots) {
|
|
25231
|
-
const qty = formatQuantity(lot.quantity);
|
|
25232
|
-
const price = formatPrice(lot.costBasis);
|
|
25233
|
-
entry += ` assets:investments:stocks:${action.symbol} -${qty} ${commodity} @ ${price} ${currency}
|
|
25234
|
-
`;
|
|
25235
|
-
}
|
|
25236
|
-
entry += ` expenses:losses:capital ${formatAmount2(totalCost, currency)}
|
|
25311
|
+
entry += formatPostings(postings) + `
|
|
25237
25312
|
`;
|
|
25238
25313
|
return entry;
|
|
25239
25314
|
}
|
|
@@ -25244,32 +25319,37 @@ function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, crossC
|
|
|
25244
25319
|
const descriptionParts = outSymbols.join(" + ");
|
|
25245
25320
|
const description = escapeDescription(inSymbols.length > 0 ? `Merger: ${descriptionParts} -> ${inSymbols.join(" + ")}` : `Merger: ${descriptionParts}`);
|
|
25246
25321
|
logger?.debug(`Generating multi-way merger entry: ${outSymbols.join(", ")} -> ${inSymbols.join(", ")}`);
|
|
25247
|
-
let entry = `${date5} ${description}
|
|
25248
|
-
`;
|
|
25249
|
-
entry += ` ; swissquote:order:${group.orderNum}
|
|
25250
|
-
`;
|
|
25251
25322
|
const oldIsins = (crossCurrencyOutgoingIsins ?? group.outgoing.map((a) => a.isin)).filter(Boolean);
|
|
25252
25323
|
const newIsins = group.incoming.map((a) => a.isin).filter(Boolean);
|
|
25253
|
-
|
|
25254
|
-
entry += ` ; Old ISIN: ${oldIsins.join(", ") || "n/a"}, New ISINs: ${newIsins.join(", ") || "n/a"}
|
|
25255
|
-
`;
|
|
25256
|
-
}
|
|
25324
|
+
const postings = [];
|
|
25257
25325
|
for (const out of group.outgoing) {
|
|
25258
25326
|
const qty = formatQuantity(Math.abs(out.quantity));
|
|
25259
25327
|
const commodity = formatCommodity(out.symbol);
|
|
25260
|
-
|
|
25261
|
-
|
|
25262
|
-
|
|
25263
|
-
|
|
25328
|
+
postings.push({
|
|
25329
|
+
account: `assets:investments:stocks:${out.symbol}`,
|
|
25330
|
+
amount: `-${qty} ${commodity}`
|
|
25331
|
+
});
|
|
25332
|
+
postings.push({ account: "equity:conversion", amount: `${qty} ${commodity}` });
|
|
25264
25333
|
}
|
|
25265
25334
|
for (const inc of group.incoming) {
|
|
25266
25335
|
const qty = formatQuantity(Math.abs(inc.quantity));
|
|
25267
25336
|
const commodity = formatCommodity(inc.symbol);
|
|
25268
|
-
|
|
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}
|
|
25344
|
+
`;
|
|
25345
|
+
entry += ` ; swissquote:order:${group.orderNum}
|
|
25269
25346
|
`;
|
|
25270
|
-
|
|
25347
|
+
if (oldIsins.length > 0 || newIsins.length > 0) {
|
|
25348
|
+
entry += ` ; Old ISIN: ${oldIsins.join(", ") || "n/a"}, New ISINs: ${newIsins.join(", ") || "n/a"}
|
|
25271
25349
|
`;
|
|
25272
25350
|
}
|
|
25351
|
+
entry += formatPostings(postings) + `
|
|
25352
|
+
`;
|
|
25273
25353
|
return entry;
|
|
25274
25354
|
}
|
|
25275
25355
|
function generateRightsDistributionEntry(action, logger) {
|
|
@@ -25278,13 +25358,18 @@ function generateRightsDistributionEntry(action, logger) {
|
|
|
25278
25358
|
const description = escapeDescription(`Rights Distribution: ${action.symbol} - ${action.name}`);
|
|
25279
25359
|
logger?.debug(`Generating Rights Distribution entry: ${qty} ${action.symbol}`);
|
|
25280
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
|
+
];
|
|
25281
25368
|
let entry = `${date5} ${description}
|
|
25282
25369
|
`;
|
|
25283
25370
|
entry += ` ; swissquote:order:${action.orderNum} isin:${action.isin}
|
|
25284
25371
|
`;
|
|
25285
|
-
entry +=
|
|
25286
|
-
`;
|
|
25287
|
-
entry += ` income:capital-gains:rights-distribution 0.00 CAD
|
|
25372
|
+
entry += formatPostings(postings) + `
|
|
25288
25373
|
`;
|
|
25289
25374
|
return entry;
|
|
25290
25375
|
}
|
|
@@ -25985,14 +26070,20 @@ function formatExchangeEntry(match2) {
|
|
|
25985
26070
|
const pair = `${sourceCurrency.toLowerCase()}-${targetCurrency.toLowerCase()}`;
|
|
25986
26071
|
const equitySource = `equity:conversion:${pair}:${sourceCurrency.toLowerCase()}`;
|
|
25987
26072
|
const equityTarget = `equity:conversion:${pair}:${targetCurrency.toLowerCase()}`;
|
|
25988
|
-
|
|
25989
|
-
|
|
25990
|
-
|
|
25991
|
-
|
|
25992
|
-
|
|
25993
|
-
|
|
25994
|
-
|
|
25995
|
-
|
|
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)}`;
|
|
25996
26087
|
}
|
|
25997
26088
|
function formatAmount3(amount) {
|
|
25998
26089
|
return amount.toFixed(2);
|
|
@@ -26054,7 +26145,13 @@ function generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger)
|
|
|
26054
26145
|
}
|
|
26055
26146
|
if (rowsByCsv.size < 2) {
|
|
26056
26147
|
logger?.info("Need at least 2 CSVs with EXCHANGE rows to match pairs, skipping");
|
|
26057
|
-
return {
|
|
26148
|
+
return {
|
|
26149
|
+
matchCount: 0,
|
|
26150
|
+
entriesAdded: 0,
|
|
26151
|
+
skippedDuplicates: 0,
|
|
26152
|
+
matchedRowIndices: new Map,
|
|
26153
|
+
accountsUsed: new Set
|
|
26154
|
+
};
|
|
26058
26155
|
}
|
|
26059
26156
|
logger?.info("Matching exchange pairs across CSVs...");
|
|
26060
26157
|
const matches = matchExchangePairs(rowsByCsv);
|
|
@@ -26065,8 +26162,16 @@ function generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger)
|
|
|
26065
26162
|
}
|
|
26066
26163
|
const newEntries = [];
|
|
26067
26164
|
let skippedDuplicates = 0;
|
|
26165
|
+
const accountsUsed = new Set;
|
|
26068
26166
|
const sortedMatches = [...matches].sort((a, b) => a.source.date.getTime() - b.source.date.getTime());
|
|
26069
26167
|
for (const match2 of sortedMatches) {
|
|
26168
|
+
const source = match2.source.currency.toLowerCase();
|
|
26169
|
+
const target = match2.target.currency.toLowerCase();
|
|
26170
|
+
const pair = `${source}-${target}`;
|
|
26171
|
+
accountsUsed.add(`assets:bank:revolut:${source}`);
|
|
26172
|
+
accountsUsed.add(`equity:conversion:${pair}:${source}`);
|
|
26173
|
+
accountsUsed.add(`equity:conversion:${pair}:${target}`);
|
|
26174
|
+
accountsUsed.add(`assets:bank:revolut:${target}`);
|
|
26070
26175
|
if (isDuplicate2(match2, journalContent)) {
|
|
26071
26176
|
skippedDuplicates++;
|
|
26072
26177
|
logger?.debug(`Skipping duplicate: ${match2.source.dateStr} ${formatAmount3(match2.source.amount)} ${match2.source.currency} \u2192 ${formatAmount3(match2.target.amount)} ${match2.target.currency}`);
|
|
@@ -26098,7 +26203,8 @@ function generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger)
|
|
|
26098
26203
|
matchCount: matches.length,
|
|
26099
26204
|
entriesAdded: newEntries.length,
|
|
26100
26205
|
skippedDuplicates,
|
|
26101
|
-
matchedRowIndices
|
|
26206
|
+
matchedRowIndices,
|
|
26207
|
+
accountsUsed
|
|
26102
26208
|
};
|
|
26103
26209
|
}
|
|
26104
26210
|
|
|
@@ -26270,6 +26376,9 @@ async function executeBtcPurchaseStep(context, contextIds, logger) {
|
|
|
26270
26376
|
const year = yearMatch ? parseInt(yearMatch[1], 10) : new Date().getFullYear();
|
|
26271
26377
|
const yearJournalPath = ensureYearJournalExists(context.directory, year);
|
|
26272
26378
|
const result = generateBtcPurchaseJournal(fiatCsvPaths, btcCsvPath, yearJournalPath, logger);
|
|
26379
|
+
for (const account of result.accountsUsed) {
|
|
26380
|
+
context.generatedAccounts.add(account);
|
|
26381
|
+
}
|
|
26273
26382
|
const message = result.entriesAdded > 0 ? `Generated ${result.entriesAdded} BTC purchase entries (${result.matchCount} matched, ${result.skippedDuplicates} skipped)` : `No new BTC purchase entries (${result.matchCount} matched, ${result.skippedDuplicates} duplicates)`;
|
|
26274
26383
|
logger?.logStep("BTC Purchases", "success", message);
|
|
26275
26384
|
logger?.endSection();
|
|
@@ -26304,6 +26413,9 @@ async function executeCurrencyExchangeStep(context, contextIds, logger) {
|
|
|
26304
26413
|
const yearJournalPath = ensureYearJournalExists(context.directory, year);
|
|
26305
26414
|
const fiatCsvPaths = fiatContexts.map((c) => c.csvPath);
|
|
26306
26415
|
const result = generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger);
|
|
26416
|
+
for (const account of result.accountsUsed) {
|
|
26417
|
+
context.generatedAccounts.add(account);
|
|
26418
|
+
}
|
|
26307
26419
|
if (result.matchedRowIndices.size > 0) {
|
|
26308
26420
|
for (const { contextId, csvPath } of fiatContexts) {
|
|
26309
26421
|
const indices = result.matchedRowIndices.get(csvPath);
|
|
@@ -26706,7 +26818,8 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
26706
26818
|
options,
|
|
26707
26819
|
configLoader,
|
|
26708
26820
|
hledgerExecutor,
|
|
26709
|
-
result
|
|
26821
|
+
result,
|
|
26822
|
+
generatedAccounts: new Set
|
|
26710
26823
|
};
|
|
26711
26824
|
try {
|
|
26712
26825
|
const contextIds = await executeClassifyStep(context, logger);
|
|
@@ -26719,6 +26832,18 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
26719
26832
|
await executeBtcPurchaseStep(context, contextIds, logger);
|
|
26720
26833
|
await executeCurrencyExchangeStep(context, contextIds, logger);
|
|
26721
26834
|
await executeSwissquotePreprocessStep(context, contextIds, logger);
|
|
26835
|
+
if (context.generatedAccounts.size > 0) {
|
|
26836
|
+
const firstCtx = contextIds.map((id) => loadContext(context.directory, id)).find((c) => c.provider === "revolut");
|
|
26837
|
+
if (firstCtx) {
|
|
26838
|
+
const yearMatch = firstCtx.filePath.match(/(\d{4})-\d{2}-\d{2}/);
|
|
26839
|
+
const year = yearMatch ? parseInt(yearMatch[1], 10) : new Date().getFullYear();
|
|
26840
|
+
const yearJournalPath = ensureYearJournalExists(context.directory, year);
|
|
26841
|
+
const declResult = ensureAccountDeclarations(yearJournalPath, context.generatedAccounts);
|
|
26842
|
+
if (declResult.added.length > 0) {
|
|
26843
|
+
logger.info(`Declared ${declResult.added.length} account(s) from generated entries: ${declResult.added.join(", ")}`);
|
|
26844
|
+
}
|
|
26845
|
+
}
|
|
26846
|
+
}
|
|
26722
26847
|
const importConfig = loadImportConfig(context.directory);
|
|
26723
26848
|
const orderedContextIds = [...contextIds].sort((a, b) => {
|
|
26724
26849
|
const ctxA = loadContext(context.directory, a);
|