@fuzzle/opencode-accountant 0.15.0-next.1 → 0.15.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 +639 -124
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4280,7 +4280,7 @@ __export(exports_accountSuggester, {
|
|
|
4280
4280
|
extractRulePatternsFromFile: () => extractRulePatternsFromFile,
|
|
4281
4281
|
clearSuggestionCache: () => clearSuggestionCache
|
|
4282
4282
|
});
|
|
4283
|
-
import * as
|
|
4283
|
+
import * as fs23 from "fs";
|
|
4284
4284
|
import * as crypto from "crypto";
|
|
4285
4285
|
function clearSuggestionCache() {
|
|
4286
4286
|
Object.keys(suggestionCache).forEach((key) => delete suggestionCache[key]);
|
|
@@ -4290,10 +4290,10 @@ function hashTransaction(posting) {
|
|
|
4290
4290
|
return crypto.createHash("md5").update(data).digest("hex");
|
|
4291
4291
|
}
|
|
4292
4292
|
function loadExistingAccounts(yearJournalPath) {
|
|
4293
|
-
if (!
|
|
4293
|
+
if (!fs23.existsSync(yearJournalPath)) {
|
|
4294
4294
|
return [];
|
|
4295
4295
|
}
|
|
4296
|
-
const content =
|
|
4296
|
+
const content = fs23.readFileSync(yearJournalPath, "utf-8");
|
|
4297
4297
|
const lines = content.split(`
|
|
4298
4298
|
`);
|
|
4299
4299
|
const accounts = [];
|
|
@@ -4309,10 +4309,10 @@ function loadExistingAccounts(yearJournalPath) {
|
|
|
4309
4309
|
return accounts.sort();
|
|
4310
4310
|
}
|
|
4311
4311
|
function extractRulePatternsFromFile(rulesPath) {
|
|
4312
|
-
if (!
|
|
4312
|
+
if (!fs23.existsSync(rulesPath)) {
|
|
4313
4313
|
return [];
|
|
4314
4314
|
}
|
|
4315
|
-
const content =
|
|
4315
|
+
const content = fs23.readFileSync(rulesPath, "utf-8");
|
|
4316
4316
|
const lines = content.split(`
|
|
4317
4317
|
`);
|
|
4318
4318
|
const patterns = [];
|
|
@@ -4547,7 +4547,7 @@ var init_accountSuggester = __esm(() => {
|
|
|
4547
4547
|
|
|
4548
4548
|
// src/index.ts
|
|
4549
4549
|
init_agentLoader();
|
|
4550
|
-
import { dirname as
|
|
4550
|
+
import { dirname as dirname10, join as join18 } from "path";
|
|
4551
4551
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4552
4552
|
|
|
4553
4553
|
// node_modules/zod/v4/classic/external.js
|
|
@@ -17370,7 +17370,7 @@ var REQUIRED_PATH_FIELDS = [
|
|
|
17370
17370
|
"unrecognized",
|
|
17371
17371
|
"rules"
|
|
17372
17372
|
];
|
|
17373
|
-
var REQUIRED_DETECTION_FIELDS = ["header"
|
|
17373
|
+
var REQUIRED_DETECTION_FIELDS = ["header"];
|
|
17374
17374
|
function validatePaths(paths) {
|
|
17375
17375
|
if (typeof paths !== "object" || paths === null) {
|
|
17376
17376
|
throw new Error("Invalid config: 'paths' must be an object");
|
|
@@ -17445,6 +17445,11 @@ function validateDetectionRule(providerName, index, rule) {
|
|
|
17445
17445
|
}
|
|
17446
17446
|
}
|
|
17447
17447
|
}
|
|
17448
|
+
const hasCurrencyField = typeof ruleObj.currencyField === "string" && ruleObj.currencyField !== "";
|
|
17449
|
+
const hasCurrencyMetadata = Array.isArray(ruleObj.metadata) && ruleObj.metadata.some((m) => m.field === "currency");
|
|
17450
|
+
if (!hasCurrencyField && !hasCurrencyMetadata) {
|
|
17451
|
+
throw new Error(`Invalid config: provider '${providerName}' detect[${index}] must have either currencyField or a metadata entry with field: "currency"`);
|
|
17452
|
+
}
|
|
17448
17453
|
if (ruleObj.closingBalanceField !== undefined) {
|
|
17449
17454
|
if (typeof ruleObj.closingBalanceField !== "string" || ruleObj.closingBalanceField === "") {
|
|
17450
17455
|
throw new Error(`Invalid config: provider '${providerName}' detect[${index}].closingBalanceField must be a non-empty string`);
|
|
@@ -17646,12 +17651,18 @@ function detectProvider(filename, content, config2) {
|
|
|
17646
17651
|
if (!firstRow) {
|
|
17647
17652
|
continue;
|
|
17648
17653
|
}
|
|
17649
|
-
const
|
|
17654
|
+
const headerMetadata = extractMetadata(content, skipRows, delimiter, rule.metadata);
|
|
17655
|
+
const metadata = { ...filenameMetadata, ...headerMetadata };
|
|
17656
|
+
let rawCurrency;
|
|
17657
|
+
if (rule.currencyField) {
|
|
17658
|
+
rawCurrency = firstRow[rule.currencyField];
|
|
17659
|
+
}
|
|
17660
|
+
if (!rawCurrency && metadata["currency"]) {
|
|
17661
|
+
rawCurrency = metadata["currency"];
|
|
17662
|
+
}
|
|
17650
17663
|
if (!rawCurrency) {
|
|
17651
17664
|
continue;
|
|
17652
17665
|
}
|
|
17653
|
-
const headerMetadata = extractMetadata(content, skipRows, delimiter, rule.metadata);
|
|
17654
|
-
const metadata = { ...filenameMetadata, ...headerMetadata };
|
|
17655
17666
|
if (rule.closingBalanceField) {
|
|
17656
17667
|
const closingBalance = extractRowField(content, skipRows, delimiter, rule.closingBalanceField, rule.closingBalanceRow ?? "last");
|
|
17657
17668
|
if (closingBalance) {
|
|
@@ -24266,8 +24277,8 @@ Note: This tool requires a contextId from a prior classify/import step.`,
|
|
|
24266
24277
|
}
|
|
24267
24278
|
});
|
|
24268
24279
|
// src/tools/import-pipeline.ts
|
|
24269
|
-
import * as
|
|
24270
|
-
import * as
|
|
24280
|
+
import * as fs24 from "fs";
|
|
24281
|
+
import * as path18 from "path";
|
|
24271
24282
|
|
|
24272
24283
|
// src/utils/accountDeclarations.ts
|
|
24273
24284
|
init_journalMatchers();
|
|
@@ -24552,6 +24563,32 @@ function formatPostings(postings) {
|
|
|
24552
24563
|
}).join(`
|
|
24553
24564
|
`);
|
|
24554
24565
|
}
|
|
24566
|
+
function formatAmount(amount, currency) {
|
|
24567
|
+
const formatted = Math.abs(amount).toFixed(2);
|
|
24568
|
+
const sign = amount < 0 ? "-" : "";
|
|
24569
|
+
return `${sign}${formatted} ${currency}`;
|
|
24570
|
+
}
|
|
24571
|
+
function escapeDescription(desc) {
|
|
24572
|
+
return desc.replace(/[;|]/g, "-").trim();
|
|
24573
|
+
}
|
|
24574
|
+
function formatCommodity(symbol2) {
|
|
24575
|
+
return `"${symbol2.toUpperCase()}"`;
|
|
24576
|
+
}
|
|
24577
|
+
function formatQuantity(qty) {
|
|
24578
|
+
return qty.toFixed(6).replace(/\.?0+$/, "");
|
|
24579
|
+
}
|
|
24580
|
+
function formatPrice(price) {
|
|
24581
|
+
const full = price.toFixed(6).replace(/0+$/, "");
|
|
24582
|
+
const [integer2, decimal = ""] = full.split(".");
|
|
24583
|
+
return `${integer2}.${decimal.padEnd(2, "0")}`;
|
|
24584
|
+
}
|
|
24585
|
+
function formatJournalFile(entries, header) {
|
|
24586
|
+
const headerBlock = header ? `${header}
|
|
24587
|
+
|
|
24588
|
+
` : "";
|
|
24589
|
+
return headerBlock + entries.join(`
|
|
24590
|
+
`);
|
|
24591
|
+
}
|
|
24555
24592
|
|
|
24556
24593
|
// src/utils/btcPurchaseGenerator.ts
|
|
24557
24594
|
function parseRevolutFiatDatetime(dateStr) {
|
|
@@ -24772,9 +24809,9 @@ function formatJournalEntry(match2) {
|
|
|
24772
24809
|
const hasFees = btcRow.fees.amount > 0;
|
|
24773
24810
|
const netFiatAmount = hasFees ? fiatRow.amount - btcRow.fees.amount : fiatRow.amount;
|
|
24774
24811
|
const effectivePrice = hasFees ? netFiatAmount / btcRow.quantity : btcRow.price.amount;
|
|
24775
|
-
const fiatAmount =
|
|
24776
|
-
const netFiat =
|
|
24777
|
-
const btcPrice =
|
|
24812
|
+
const fiatAmount = formatAmount2(fiatRow.amount);
|
|
24813
|
+
const netFiat = formatAmount2(netFiatAmount);
|
|
24814
|
+
const btcPrice = formatAmount2(effectivePrice);
|
|
24778
24815
|
const pair = `${fiatCurrency.toLowerCase()}-btc`;
|
|
24779
24816
|
const equityFiat = `equity:conversion:${pair}:${fiatCurrency.toLowerCase()}`;
|
|
24780
24817
|
const equityBtc = `equity:conversion:${pair}:btc`;
|
|
@@ -24786,7 +24823,7 @@ function formatJournalEntry(match2) {
|
|
|
24786
24823
|
}
|
|
24787
24824
|
];
|
|
24788
24825
|
if (hasFees) {
|
|
24789
|
-
const feeAmount =
|
|
24826
|
+
const feeAmount = formatAmount2(btcRow.fees.amount);
|
|
24790
24827
|
const feeCurrency = btcRow.fees.currency;
|
|
24791
24828
|
postings.push({
|
|
24792
24829
|
account: `expenses:fees:revolut:${feeCurrency.toLowerCase()}`,
|
|
@@ -24797,7 +24834,7 @@ function formatJournalEntry(match2) {
|
|
|
24797
24834
|
return `${date5} Bitcoin purchase
|
|
24798
24835
|
${formatPostings(postings)}`;
|
|
24799
24836
|
}
|
|
24800
|
-
function
|
|
24837
|
+
function formatAmount2(amount) {
|
|
24801
24838
|
return amount.toFixed(2);
|
|
24802
24839
|
}
|
|
24803
24840
|
function formatBtcQuantity(quantity) {
|
|
@@ -24812,7 +24849,7 @@ function formatDateIso(date5) {
|
|
|
24812
24849
|
}
|
|
24813
24850
|
function isDuplicate(match2, journalContent) {
|
|
24814
24851
|
const date5 = match2.fiatRow.dateStr;
|
|
24815
|
-
const amount =
|
|
24852
|
+
const amount = formatAmount2(match2.fiatRow.amount);
|
|
24816
24853
|
const currency = match2.fiatRow.currency;
|
|
24817
24854
|
const pattern = `${date5} Bitcoin purchase`;
|
|
24818
24855
|
if (!journalContent.includes(pattern))
|
|
@@ -24862,7 +24899,7 @@ function generateBtcPurchaseJournal(fiatCsvPaths, btcCsvPath, yearJournalPath, l
|
|
|
24862
24899
|
}
|
|
24863
24900
|
if (isDuplicate(match2, journalContent)) {
|
|
24864
24901
|
skippedDuplicates++;
|
|
24865
|
-
logger?.debug(`Skipping duplicate: ${match2.fiatRow.dateStr} ${
|
|
24902
|
+
logger?.debug(`Skipping duplicate: ${match2.fiatRow.dateStr} ${formatAmount2(match2.fiatRow.amount)} ${match2.fiatRow.currency}`);
|
|
24866
24903
|
continue;
|
|
24867
24904
|
}
|
|
24868
24905
|
newEntries.push(formatJournalEntry(match2));
|
|
@@ -25019,7 +25056,7 @@ function normalizeSymbol(symbol2) {
|
|
|
25019
25056
|
return symbol2.toLowerCase();
|
|
25020
25057
|
}
|
|
25021
25058
|
|
|
25022
|
-
// src/utils/
|
|
25059
|
+
// src/utils/lotTracker.ts
|
|
25023
25060
|
import * as fs17 from "fs";
|
|
25024
25061
|
import * as path12 from "path";
|
|
25025
25062
|
var DEFAULT_LOT_INVENTORY_PATH = "ledger/investments/lot-inventory/{symbol}-lot.json";
|
|
@@ -25196,11 +25233,6 @@ function getHeldQuantity(inventory, symbol2) {
|
|
|
25196
25233
|
}
|
|
25197
25234
|
|
|
25198
25235
|
// src/utils/swissquoteJournalGenerator.ts
|
|
25199
|
-
function formatAmount2(amount, currency) {
|
|
25200
|
-
const formatted = Math.abs(amount).toFixed(2);
|
|
25201
|
-
const sign = amount < 0 ? "-" : "";
|
|
25202
|
-
return `${sign}${formatted} ${currency}`;
|
|
25203
|
-
}
|
|
25204
25236
|
function formatDate(dateStr) {
|
|
25205
25237
|
const match2 = dateStr.match(/^(\d{2})-(\d{2})-(\d{4})/);
|
|
25206
25238
|
if (!match2) {
|
|
@@ -25209,20 +25241,6 @@ function formatDate(dateStr) {
|
|
|
25209
25241
|
const [, day, month, year] = match2;
|
|
25210
25242
|
return `${year}-${month}-${day}`;
|
|
25211
25243
|
}
|
|
25212
|
-
function escapeDescription(desc) {
|
|
25213
|
-
return desc.replace(/[;|]/g, "-").trim();
|
|
25214
|
-
}
|
|
25215
|
-
function formatCommodity(symbol2) {
|
|
25216
|
-
return `"${symbol2.toUpperCase()}"`;
|
|
25217
|
-
}
|
|
25218
|
-
function formatQuantity(qty) {
|
|
25219
|
-
return qty.toFixed(6).replace(/\.?0+$/, "");
|
|
25220
|
-
}
|
|
25221
|
-
function formatPrice(price) {
|
|
25222
|
-
const full = price.toFixed(6).replace(/0+$/, "");
|
|
25223
|
-
const [integer2, decimal = ""] = full.split(".");
|
|
25224
|
-
return `${integer2}.${decimal.padEnd(2, "0")}`;
|
|
25225
|
-
}
|
|
25226
25244
|
function generateBuyEntry(trade, logger) {
|
|
25227
25245
|
const date5 = formatDate(trade.date);
|
|
25228
25246
|
const description = escapeDescription(`Buy ${trade.symbol} - ${trade.name}`);
|
|
@@ -25241,12 +25259,12 @@ function generateBuyEntry(trade, logger) {
|
|
|
25241
25259
|
if (fees > 0) {
|
|
25242
25260
|
postings.push({
|
|
25243
25261
|
account: "expenses:fees:trading:swissquote",
|
|
25244
|
-
amount:
|
|
25262
|
+
amount: formatAmount(fees, trade.currency)
|
|
25245
25263
|
});
|
|
25246
25264
|
}
|
|
25247
25265
|
postings.push({
|
|
25248
25266
|
account: `assets:broker:swissquote:${trade.currency.toLowerCase()}`,
|
|
25249
|
-
amount:
|
|
25267
|
+
amount: formatAmount(-cashOut, trade.currency)
|
|
25250
25268
|
});
|
|
25251
25269
|
const computedNet = Math.round((trade.quantity * trade.unitPrice + fees) * 100);
|
|
25252
25270
|
const brokerNet = Math.round(cashOut * 100);
|
|
@@ -25254,7 +25272,7 @@ function generateBuyEntry(trade, logger) {
|
|
|
25254
25272
|
if (roundingCents !== 0) {
|
|
25255
25273
|
postings.push({
|
|
25256
25274
|
account: "equity:rounding",
|
|
25257
|
-
amount:
|
|
25275
|
+
amount: formatAmount(roundingCents / 100, trade.currency)
|
|
25258
25276
|
});
|
|
25259
25277
|
logger?.debug(`Rounding adjustment: ${roundingCents / 100} ${trade.currency}`);
|
|
25260
25278
|
}
|
|
@@ -25288,23 +25306,23 @@ function generateSellEntry(trade, consumed, logger) {
|
|
|
25288
25306
|
}
|
|
25289
25307
|
postings.push({
|
|
25290
25308
|
account: `assets:broker:swissquote:${trade.currency.toLowerCase()}`,
|
|
25291
|
-
amount:
|
|
25309
|
+
amount: formatAmount(cashIn, trade.currency)
|
|
25292
25310
|
});
|
|
25293
25311
|
if (fees > 0) {
|
|
25294
25312
|
postings.push({
|
|
25295
25313
|
account: "expenses:fees:trading:swissquote",
|
|
25296
|
-
amount:
|
|
25314
|
+
amount: formatAmount(fees, trade.currency)
|
|
25297
25315
|
});
|
|
25298
25316
|
}
|
|
25299
25317
|
if (gain >= 0) {
|
|
25300
25318
|
postings.push({
|
|
25301
25319
|
account: "income:capital-gains:realized",
|
|
25302
|
-
amount:
|
|
25320
|
+
amount: formatAmount(-gain, trade.currency)
|
|
25303
25321
|
});
|
|
25304
25322
|
} else {
|
|
25305
25323
|
postings.push({
|
|
25306
25324
|
account: "expenses:losses:capital",
|
|
25307
|
-
amount:
|
|
25325
|
+
amount: formatAmount(-gain, trade.currency)
|
|
25308
25326
|
});
|
|
25309
25327
|
}
|
|
25310
25328
|
const computedNet = Math.round((trade.quantity * trade.unitPrice - fees) * 100);
|
|
@@ -25313,7 +25331,7 @@ function generateSellEntry(trade, consumed, logger) {
|
|
|
25313
25331
|
if (roundingCents !== 0) {
|
|
25314
25332
|
postings.push({
|
|
25315
25333
|
account: "equity:rounding",
|
|
25316
|
-
amount:
|
|
25334
|
+
amount: formatAmount(roundingCents / 100, trade.currency)
|
|
25317
25335
|
});
|
|
25318
25336
|
logger?.debug(`Rounding adjustment: ${roundingCents / 100} ${trade.currency}`);
|
|
25319
25337
|
}
|
|
@@ -25335,18 +25353,18 @@ function generateDividendEntry(dividend, logger) {
|
|
|
25335
25353
|
const postings = [
|
|
25336
25354
|
{
|
|
25337
25355
|
account: `assets:broker:swissquote:${dividend.currency.toLowerCase()}`,
|
|
25338
|
-
amount:
|
|
25356
|
+
amount: formatAmount(dividend.netAmount, dividend.currency)
|
|
25339
25357
|
}
|
|
25340
25358
|
];
|
|
25341
25359
|
if (dividend.withholdingTax > 0) {
|
|
25342
25360
|
postings.push({
|
|
25343
25361
|
account: "expenses:taxes:withholding",
|
|
25344
|
-
amount:
|
|
25362
|
+
amount: formatAmount(dividend.withholdingTax, dividend.currency)
|
|
25345
25363
|
});
|
|
25346
25364
|
}
|
|
25347
25365
|
postings.push({
|
|
25348
25366
|
account: `income:dividends:${dividend.symbol}`,
|
|
25349
|
-
amount:
|
|
25367
|
+
amount: formatAmount(-dividend.grossAmount, dividend.currency)
|
|
25350
25368
|
});
|
|
25351
25369
|
let entry = `${date5} ${description}
|
|
25352
25370
|
`;
|
|
@@ -25363,7 +25381,7 @@ function generateSplitEntry(action, oldQuantity, newQuantity, totalCostBasis, cu
|
|
|
25363
25381
|
const description = escapeDescription(`${splitType}: ${action.symbol} (${action.name})`);
|
|
25364
25382
|
logger?.debug(`Generating ${splitType} entry: ${oldQuantity} -> ${newQuantity} ${action.symbol}`);
|
|
25365
25383
|
const commodity = formatCommodity(action.symbol);
|
|
25366
|
-
const totalCost =
|
|
25384
|
+
const totalCost = formatAmount(totalCostBasis, currency);
|
|
25367
25385
|
const conversionAccount = `equity:conversion:${currency.toLowerCase()}`;
|
|
25368
25386
|
const outgoingPostings = [
|
|
25369
25387
|
{
|
|
@@ -25378,7 +25396,7 @@ function generateSplitEntry(action, oldQuantity, newQuantity, totalCostBasis, cu
|
|
|
25378
25396
|
const incomingPostings = [
|
|
25379
25397
|
{
|
|
25380
25398
|
account: conversionAccount,
|
|
25381
|
-
amount:
|
|
25399
|
+
amount: formatAmount(-totalCostBasis, currency)
|
|
25382
25400
|
},
|
|
25383
25401
|
{
|
|
25384
25402
|
account: `assets:investments:stocks:${action.symbol}`,
|
|
@@ -25419,7 +25437,7 @@ function generateWorthlessEntry(action, removedLots, logger) {
|
|
|
25419
25437
|
amount: `-${qty} ${commodity} @ ${price} ${currency}`
|
|
25420
25438
|
});
|
|
25421
25439
|
}
|
|
25422
|
-
postings.push({ account: "expenses:losses:capital", amount:
|
|
25440
|
+
postings.push({ account: "expenses:losses:capital", amount: formatAmount(totalCost, currency) });
|
|
25423
25441
|
let entry = `${date5} ${description}
|
|
25424
25442
|
`;
|
|
25425
25443
|
entry += ` ; swissquote:order:${action.orderNum} isin:${action.isin}
|
|
@@ -25451,7 +25469,7 @@ function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, crossC
|
|
|
25451
25469
|
const qty = formatQuantity(Math.abs(out.quantity));
|
|
25452
25470
|
const commodity = formatCommodity(out.symbol);
|
|
25453
25471
|
if (costInfo && costInfo.outgoingTotalCosts[i2] != null) {
|
|
25454
|
-
const costAmount =
|
|
25472
|
+
const costAmount = formatAmount(costInfo.outgoingTotalCosts[i2], costInfo.currency);
|
|
25455
25473
|
const postings = [
|
|
25456
25474
|
{
|
|
25457
25475
|
account: `assets:investments:stocks:${out.symbol}`,
|
|
@@ -25488,11 +25506,11 @@ function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, crossC
|
|
|
25488
25506
|
const qty = formatQuantity(Math.abs(inc.quantity));
|
|
25489
25507
|
const commodity = formatCommodity(inc.symbol);
|
|
25490
25508
|
if (costInfo && costInfo.incomingTotalCosts[i2] != null) {
|
|
25491
|
-
const costAmount =
|
|
25509
|
+
const costAmount = formatAmount(costInfo.incomingTotalCosts[i2], costInfo.currency);
|
|
25492
25510
|
const postings = [
|
|
25493
25511
|
{
|
|
25494
25512
|
account: `equity:conversion:${costInfo.currency.toLowerCase()}`,
|
|
25495
|
-
amount:
|
|
25513
|
+
amount: formatAmount(-costInfo.incomingTotalCosts[i2], costInfo.currency)
|
|
25496
25514
|
},
|
|
25497
25515
|
{
|
|
25498
25516
|
account: `assets:investments:stocks:${inc.symbol}`,
|
|
@@ -25522,7 +25540,7 @@ function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, crossC
|
|
|
25522
25540
|
}
|
|
25523
25541
|
if (group.cashSettlement) {
|
|
25524
25542
|
const cashCurrency = group.cashSettlement.currency.toLowerCase();
|
|
25525
|
-
const cashAmount =
|
|
25543
|
+
const cashAmount = formatAmount(group.cashSettlement.amount, group.cashSettlement.currency);
|
|
25526
25544
|
let sub = `${date5} ${description}
|
|
25527
25545
|
`;
|
|
25528
25546
|
sub += metadata;
|
|
@@ -25571,12 +25589,12 @@ function generateDirectWorthlessEntry(date5, orderNum, outgoingSymbols, outgoing
|
|
|
25571
25589
|
const commodity = formatCommodity(symbol2);
|
|
25572
25590
|
postings.push({
|
|
25573
25591
|
account: `assets:investments:stocks:${symbol2}`,
|
|
25574
|
-
amount: `-${formatQuantity(quantity)} ${commodity} @@ ${
|
|
25592
|
+
amount: `-${formatQuantity(quantity)} ${commodity} @@ ${formatAmount(cost, currency)}`
|
|
25575
25593
|
});
|
|
25576
25594
|
}
|
|
25577
25595
|
postings.push({
|
|
25578
25596
|
account: "expenses:losses:capital",
|
|
25579
|
-
amount:
|
|
25597
|
+
amount: formatAmount(totalCostBasis, currency)
|
|
25580
25598
|
});
|
|
25581
25599
|
let entry = `${formattedDate} ${description}
|
|
25582
25600
|
`;
|
|
@@ -25592,14 +25610,11 @@ function generateDirectWorthlessEntry(date5, orderNum, outgoingSymbols, outgoing
|
|
|
25592
25610
|
`;
|
|
25593
25611
|
return entry;
|
|
25594
25612
|
}
|
|
25595
|
-
function
|
|
25613
|
+
function formatJournalFile2(entries, year, currency) {
|
|
25596
25614
|
const header = `; Swissquote ${currency.toUpperCase()} investment transactions for ${year}
|
|
25597
25615
|
; Generated by opencode-accountant
|
|
25598
|
-
; This file is auto-generated - do not edit manually
|
|
25599
|
-
|
|
25600
|
-
`;
|
|
25601
|
-
return header + entries.join(`
|
|
25602
|
-
`);
|
|
25616
|
+
; This file is auto-generated - do not edit manually`;
|
|
25617
|
+
return formatJournalFile(entries, header);
|
|
25603
25618
|
}
|
|
25604
25619
|
|
|
25605
25620
|
// src/utils/mergerFmvConfig.ts
|
|
@@ -26090,7 +26105,7 @@ function resolveRemainingPendingMergers(projectDir, lotInventoryPath, logger) {
|
|
|
26090
26105
|
const parts = basename5.split("-");
|
|
26091
26106
|
const yearStr = parts[0];
|
|
26092
26107
|
const currency = parts.slice(1).join("-");
|
|
26093
|
-
const header =
|
|
26108
|
+
const header = formatJournalFile2(journalEntries, parseInt(yearStr, 10), currency);
|
|
26094
26109
|
fs19.writeFileSync(journalFile, header);
|
|
26095
26110
|
}
|
|
26096
26111
|
journalFilesUpdated.push(journalFile);
|
|
@@ -26364,7 +26379,7 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
|
|
|
26364
26379
|
fs19.mkdirSync(investmentsDir, { recursive: true });
|
|
26365
26380
|
}
|
|
26366
26381
|
journalFile = path14.join(investmentsDir, `${year}-${currency}.journal`);
|
|
26367
|
-
const journalContent =
|
|
26382
|
+
const journalContent = formatJournalFile2(journalEntries, year, currency);
|
|
26368
26383
|
if (fs19.existsSync(journalFile)) {
|
|
26369
26384
|
fs19.appendFileSync(journalFile, `
|
|
26370
26385
|
` + journalEntries.join(`
|
|
@@ -26803,6 +26818,426 @@ function generateSwissquoteForexJournal(allForexRows, yearJournalPath, logger) {
|
|
|
26803
26818
|
};
|
|
26804
26819
|
}
|
|
26805
26820
|
|
|
26821
|
+
// src/utils/ibkrCsvPreprocessor.ts
|
|
26822
|
+
var import_papaparse3 = __toESM(require_papaparse(), 1);
|
|
26823
|
+
import * as fs22 from "fs";
|
|
26824
|
+
import * as path17 from "path";
|
|
26825
|
+
|
|
26826
|
+
// src/utils/ibkrJournalGenerator.ts
|
|
26827
|
+
function generateIbkrBuyEntry(trade, logger) {
|
|
26828
|
+
const description = escapeDescription(`Buy ${trade.symbol} - ${trade.description}`);
|
|
26829
|
+
const qty = formatQuantity(trade.quantity);
|
|
26830
|
+
const totalCost = Math.abs(trade.grossAmount);
|
|
26831
|
+
const fees = Math.abs(trade.commission);
|
|
26832
|
+
const cashOut = Math.abs(trade.netAmount);
|
|
26833
|
+
logger?.debug(`Generating IBKR Buy entry: ${qty} ${trade.symbol} @@ ${totalCost} ${trade.currency}`);
|
|
26834
|
+
const commodity = formatCommodity(trade.symbol);
|
|
26835
|
+
const postings = [
|
|
26836
|
+
{
|
|
26837
|
+
account: `assets:investments:stocks:${trade.symbol}`,
|
|
26838
|
+
amount: `${qty} ${commodity} @@ ${formatAmount(totalCost, trade.currency)}`
|
|
26839
|
+
}
|
|
26840
|
+
];
|
|
26841
|
+
if (fees > 0) {
|
|
26842
|
+
postings.push({
|
|
26843
|
+
account: "expenses:fees:trading:ibkr",
|
|
26844
|
+
amount: formatAmount(fees, trade.currency)
|
|
26845
|
+
});
|
|
26846
|
+
}
|
|
26847
|
+
postings.push({
|
|
26848
|
+
account: `assets:broker:ibkr:${trade.currency.toLowerCase()}`,
|
|
26849
|
+
amount: formatAmount(-cashOut, trade.currency)
|
|
26850
|
+
});
|
|
26851
|
+
let entry = `${trade.date} ${description}
|
|
26852
|
+
`;
|
|
26853
|
+
entry += ` ; ibkr:account:${trade.account}
|
|
26854
|
+
`;
|
|
26855
|
+
entry += formatPostings(postings) + `
|
|
26856
|
+
`;
|
|
26857
|
+
return entry;
|
|
26858
|
+
}
|
|
26859
|
+
function generateIbkrSellEntry(trade, consumed, logger) {
|
|
26860
|
+
const description = escapeDescription(`Sell ${trade.symbol} - ${trade.description}`);
|
|
26861
|
+
const qty = formatQuantity(trade.quantity);
|
|
26862
|
+
const fees = Math.abs(trade.commission);
|
|
26863
|
+
const cashIn = trade.netAmount;
|
|
26864
|
+
const totalCostBasis = consumed.reduce((sum, c) => sum + c.totalCost, 0);
|
|
26865
|
+
const saleProceeds = Math.abs(trade.grossAmount);
|
|
26866
|
+
const capitalGain = saleProceeds - totalCostBasis;
|
|
26867
|
+
logger?.debug(`Generating IBKR Sell entry: ${qty} ${trade.symbol}, gain: ${capitalGain.toFixed(2)}`);
|
|
26868
|
+
const commodity = formatCommodity(trade.symbol);
|
|
26869
|
+
const lotDetails = consumed.map((c) => `${c.lot.date}: ${formatQuantity(c.quantity)}@${formatPrice(c.lot.costBasis)}`).join(", ");
|
|
26870
|
+
const postings = [];
|
|
26871
|
+
for (const c of consumed) {
|
|
26872
|
+
const lotQty = formatQuantity(c.quantity);
|
|
26873
|
+
const lotPrice = formatPrice(c.lot.costBasis);
|
|
26874
|
+
postings.push({
|
|
26875
|
+
account: `assets:investments:stocks:${trade.symbol}`,
|
|
26876
|
+
amount: `-${lotQty} ${commodity} @ ${lotPrice} ${trade.currency}`
|
|
26877
|
+
});
|
|
26878
|
+
}
|
|
26879
|
+
postings.push({
|
|
26880
|
+
account: `assets:broker:ibkr:${trade.currency.toLowerCase()}`,
|
|
26881
|
+
amount: formatAmount(cashIn, trade.currency)
|
|
26882
|
+
});
|
|
26883
|
+
if (fees > 0) {
|
|
26884
|
+
postings.push({
|
|
26885
|
+
account: "expenses:fees:trading:ibkr",
|
|
26886
|
+
amount: formatAmount(fees, trade.currency)
|
|
26887
|
+
});
|
|
26888
|
+
}
|
|
26889
|
+
if (capitalGain >= 0) {
|
|
26890
|
+
postings.push({
|
|
26891
|
+
account: "income:capital-gains:realized",
|
|
26892
|
+
amount: formatAmount(-capitalGain, trade.currency)
|
|
26893
|
+
});
|
|
26894
|
+
} else {
|
|
26895
|
+
postings.push({
|
|
26896
|
+
account: "expenses:losses:capital",
|
|
26897
|
+
amount: formatAmount(-capitalGain, trade.currency)
|
|
26898
|
+
});
|
|
26899
|
+
}
|
|
26900
|
+
let entry = `${trade.date} ${description}
|
|
26901
|
+
`;
|
|
26902
|
+
entry += ` ; ibkr:account:${trade.account}
|
|
26903
|
+
`;
|
|
26904
|
+
entry += ` ; FIFO lots: ${lotDetails}
|
|
26905
|
+
`;
|
|
26906
|
+
entry += formatPostings(postings) + `
|
|
26907
|
+
`;
|
|
26908
|
+
return entry;
|
|
26909
|
+
}
|
|
26910
|
+
function generateIbkrDividendEntry(dividend, logger) {
|
|
26911
|
+
const description = escapeDescription(`Dividend ${dividend.symbol}`);
|
|
26912
|
+
logger?.debug(`Generating IBKR Dividend entry: ${dividend.symbol}, net: ${dividend.netAmount} ${dividend.currency}`);
|
|
26913
|
+
const postings = [
|
|
26914
|
+
{
|
|
26915
|
+
account: `assets:broker:ibkr:${dividend.currency.toLowerCase()}`,
|
|
26916
|
+
amount: formatAmount(dividend.netAmount, dividend.currency)
|
|
26917
|
+
}
|
|
26918
|
+
];
|
|
26919
|
+
if (dividend.withholdingTax > 0) {
|
|
26920
|
+
postings.push({
|
|
26921
|
+
account: "expenses:taxes:withholding",
|
|
26922
|
+
amount: formatAmount(dividend.withholdingTax, dividend.currency)
|
|
26923
|
+
});
|
|
26924
|
+
}
|
|
26925
|
+
postings.push({
|
|
26926
|
+
account: `income:dividends:${dividend.symbol}`,
|
|
26927
|
+
amount: formatAmount(-dividend.grossAmount, dividend.currency)
|
|
26928
|
+
});
|
|
26929
|
+
let entry = `${dividend.date} ${description}
|
|
26930
|
+
`;
|
|
26931
|
+
entry += ` ; ibkr:account:${dividend.account}
|
|
26932
|
+
`;
|
|
26933
|
+
entry += formatPostings(postings) + `
|
|
26934
|
+
`;
|
|
26935
|
+
return entry;
|
|
26936
|
+
}
|
|
26937
|
+
function generateIbkrAdjustmentEntry(adjustment, logger) {
|
|
26938
|
+
const description = escapeDescription(adjustment.description);
|
|
26939
|
+
logger?.debug(`Generating IBKR Adjustment entry: ${adjustment.amount} ${adjustment.currency}`);
|
|
26940
|
+
const postings = [
|
|
26941
|
+
{
|
|
26942
|
+
account: `assets:broker:ibkr:${adjustment.currency.toLowerCase()}`,
|
|
26943
|
+
amount: formatAmount(adjustment.amount, adjustment.currency)
|
|
26944
|
+
}
|
|
26945
|
+
];
|
|
26946
|
+
if (adjustment.amount < 0) {
|
|
26947
|
+
postings.push({
|
|
26948
|
+
account: "expenses:fx-losses:ibkr",
|
|
26949
|
+
amount: formatAmount(Math.abs(adjustment.amount), adjustment.currency)
|
|
26950
|
+
});
|
|
26951
|
+
} else {
|
|
26952
|
+
postings.push({
|
|
26953
|
+
account: "income:fx-gains:ibkr",
|
|
26954
|
+
amount: formatAmount(-adjustment.amount, adjustment.currency)
|
|
26955
|
+
});
|
|
26956
|
+
}
|
|
26957
|
+
let entry = `${adjustment.date} ${description}
|
|
26958
|
+
`;
|
|
26959
|
+
entry += ` ; ibkr:account:${adjustment.account}
|
|
26960
|
+
`;
|
|
26961
|
+
entry += formatPostings(postings) + `
|
|
26962
|
+
`;
|
|
26963
|
+
return entry;
|
|
26964
|
+
}
|
|
26965
|
+
|
|
26966
|
+
// src/utils/ibkrCsvPreprocessor.ts
|
|
26967
|
+
var TRADE_TYPES2 = new Set(["Buy", "Sell"]);
|
|
26968
|
+
var DIVIDEND_TYPE = "Dividend";
|
|
26969
|
+
var TAX_TYPE = "Foreign Tax Withholding";
|
|
26970
|
+
var ADJUSTMENT_TYPE = "Adjustment";
|
|
26971
|
+
var FOREX_TYPE = "Forex Trade Component";
|
|
26972
|
+
var DEPOSIT_TYPE = "Deposit";
|
|
26973
|
+
function parseIbkrCsv(content) {
|
|
26974
|
+
const transactions = [];
|
|
26975
|
+
const result = import_papaparse3.default.parse(content, {
|
|
26976
|
+
header: false,
|
|
26977
|
+
skipEmptyLines: true
|
|
26978
|
+
});
|
|
26979
|
+
for (const row of result.data) {
|
|
26980
|
+
const values = Object.values(row);
|
|
26981
|
+
if (values[0] !== "Transaction History" || values[1] !== "Data") {
|
|
26982
|
+
continue;
|
|
26983
|
+
}
|
|
26984
|
+
const date5 = values[2]?.trim() || "";
|
|
26985
|
+
const account = values[3]?.trim() || "";
|
|
26986
|
+
const description = values[4]?.trim() || "";
|
|
26987
|
+
const transactionType = values[5]?.trim() || "";
|
|
26988
|
+
const symbol2 = values[6]?.trim() || "";
|
|
26989
|
+
const quantity = parseFloat(values[7] || "0") || 0;
|
|
26990
|
+
const price = parseFloat(values[8] || "0") || 0;
|
|
26991
|
+
const priceCurrency = values[9]?.trim() || "";
|
|
26992
|
+
const grossAmount = parseFloat(values[10] || "0") || 0;
|
|
26993
|
+
const commission = parseFloat(values[11] || "0") || 0;
|
|
26994
|
+
const netAmount = parseFloat(values[12] || "0") || 0;
|
|
26995
|
+
if (!date5 || !transactionType)
|
|
26996
|
+
continue;
|
|
26997
|
+
transactions.push({
|
|
26998
|
+
date: date5,
|
|
26999
|
+
account,
|
|
27000
|
+
description,
|
|
27001
|
+
transactionType,
|
|
27002
|
+
symbol: symbol2,
|
|
27003
|
+
quantity,
|
|
27004
|
+
price,
|
|
27005
|
+
priceCurrency,
|
|
27006
|
+
grossAmount,
|
|
27007
|
+
commission,
|
|
27008
|
+
netAmount
|
|
27009
|
+
});
|
|
27010
|
+
}
|
|
27011
|
+
return transactions;
|
|
27012
|
+
}
|
|
27013
|
+
function groupDividends(transactions) {
|
|
27014
|
+
const dividendMap = new Map;
|
|
27015
|
+
for (const txn of transactions) {
|
|
27016
|
+
if (txn.transactionType !== DIVIDEND_TYPE && txn.transactionType !== TAX_TYPE)
|
|
27017
|
+
continue;
|
|
27018
|
+
const key = `${txn.date}:${txn.symbol}`;
|
|
27019
|
+
if (!dividendMap.has(key)) {
|
|
27020
|
+
dividendMap.set(key, {});
|
|
27021
|
+
}
|
|
27022
|
+
const group = dividendMap.get(key);
|
|
27023
|
+
if (txn.transactionType === DIVIDEND_TYPE) {
|
|
27024
|
+
group.dividend = txn;
|
|
27025
|
+
} else {
|
|
27026
|
+
group.tax = txn;
|
|
27027
|
+
}
|
|
27028
|
+
}
|
|
27029
|
+
const groups = [];
|
|
27030
|
+
for (const [, group] of dividendMap) {
|
|
27031
|
+
if (!group.dividend)
|
|
27032
|
+
continue;
|
|
27033
|
+
const grossAmount = Math.abs(group.dividend.netAmount);
|
|
27034
|
+
const withholdingTax = group.tax ? Math.abs(group.tax.netAmount) : 0;
|
|
27035
|
+
groups.push({
|
|
27036
|
+
date: group.dividend.date,
|
|
27037
|
+
symbol: group.dividend.symbol,
|
|
27038
|
+
account: group.dividend.account,
|
|
27039
|
+
grossAmount,
|
|
27040
|
+
withholdingTax
|
|
27041
|
+
});
|
|
27042
|
+
}
|
|
27043
|
+
groups.sort((a, b) => a.date.localeCompare(b.date));
|
|
27044
|
+
return groups;
|
|
27045
|
+
}
|
|
27046
|
+
async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPath, logger) {
|
|
27047
|
+
const csvDir = path17.dirname(csvPath);
|
|
27048
|
+
const csvBasename = path17.basename(csvPath, ".csv");
|
|
27049
|
+
const journalPath = path17.join(directory, "ledger", "investments", `${year}-ibkr-${currency}.journal`);
|
|
27050
|
+
if (fs22.existsSync(journalPath)) {
|
|
27051
|
+
logger?.info(`IBKR journal already exists: ${journalPath}, skipping preprocessing`);
|
|
27052
|
+
return {
|
|
27053
|
+
simpleTransactionsCsv: null,
|
|
27054
|
+
journalFile: journalPath,
|
|
27055
|
+
stats: {
|
|
27056
|
+
totalRows: 0,
|
|
27057
|
+
deposits: 0,
|
|
27058
|
+
trades: 0,
|
|
27059
|
+
dividends: 0,
|
|
27060
|
+
adjustments: 0,
|
|
27061
|
+
skippedForex: 0
|
|
27062
|
+
},
|
|
27063
|
+
alreadyPreprocessed: true
|
|
27064
|
+
};
|
|
27065
|
+
}
|
|
27066
|
+
const content = fs22.readFileSync(csvPath, "utf-8");
|
|
27067
|
+
const transactions = parseIbkrCsv(content);
|
|
27068
|
+
logger?.info(`Parsed ${transactions.length} IBKR transactions from ${path17.basename(csvPath)}`);
|
|
27069
|
+
const deposits = [];
|
|
27070
|
+
const trades = [];
|
|
27071
|
+
const dividendTxns = [];
|
|
27072
|
+
const adjustments = [];
|
|
27073
|
+
let skippedForex = 0;
|
|
27074
|
+
for (const txn of transactions) {
|
|
27075
|
+
if (TRADE_TYPES2.has(txn.transactionType)) {
|
|
27076
|
+
trades.push(txn);
|
|
27077
|
+
} else if (txn.transactionType === DIVIDEND_TYPE || txn.transactionType === TAX_TYPE) {
|
|
27078
|
+
dividendTxns.push(txn);
|
|
27079
|
+
} else if (txn.transactionType === ADJUSTMENT_TYPE) {
|
|
27080
|
+
adjustments.push(txn);
|
|
27081
|
+
} else if (txn.transactionType === DEPOSIT_TYPE) {
|
|
27082
|
+
deposits.push(txn);
|
|
27083
|
+
} else if (txn.transactionType === FOREX_TYPE) {
|
|
27084
|
+
skippedForex++;
|
|
27085
|
+
} else {
|
|
27086
|
+
logger?.warn(`Unknown IBKR transaction type: ${txn.transactionType}`);
|
|
27087
|
+
}
|
|
27088
|
+
}
|
|
27089
|
+
trades.sort((a, b) => a.date.localeCompare(b.date));
|
|
27090
|
+
const inventory = loadLotInventory(directory, lotInventoryPath, logger);
|
|
27091
|
+
const tradedSymbols = new Set;
|
|
27092
|
+
const usedAccounts = new Set;
|
|
27093
|
+
const journalEntries = [];
|
|
27094
|
+
for (const trade of trades) {
|
|
27095
|
+
tradedSymbols.add(trade.symbol);
|
|
27096
|
+
const totalCost = Math.abs(trade.grossAmount);
|
|
27097
|
+
const unitPrice = trade.quantity !== 0 ? totalCost / Math.abs(trade.quantity) : 0;
|
|
27098
|
+
const tradeEntry = {
|
|
27099
|
+
date: trade.date,
|
|
27100
|
+
account: trade.account,
|
|
27101
|
+
description: trade.description,
|
|
27102
|
+
type: trade.transactionType,
|
|
27103
|
+
symbol: trade.symbol,
|
|
27104
|
+
quantity: Math.abs(trade.quantity),
|
|
27105
|
+
grossAmount: trade.grossAmount,
|
|
27106
|
+
commission: trade.commission,
|
|
27107
|
+
netAmount: trade.netAmount,
|
|
27108
|
+
currency: currency.toUpperCase()
|
|
27109
|
+
};
|
|
27110
|
+
if (trade.transactionType === "Buy") {
|
|
27111
|
+
const tradeInfo = {
|
|
27112
|
+
date: trade.date,
|
|
27113
|
+
orderNum: "",
|
|
27114
|
+
symbol: trade.symbol,
|
|
27115
|
+
isin: "",
|
|
27116
|
+
quantity: Math.abs(trade.quantity),
|
|
27117
|
+
unitPrice,
|
|
27118
|
+
currency: currency.toUpperCase()
|
|
27119
|
+
};
|
|
27120
|
+
addLot(inventory, tradeInfo, logger);
|
|
27121
|
+
journalEntries.push(generateIbkrBuyEntry(tradeEntry, logger));
|
|
27122
|
+
usedAccounts.add(`assets:investments:stocks:${trade.symbol}`);
|
|
27123
|
+
usedAccounts.add(`assets:broker:ibkr:${currency}`);
|
|
27124
|
+
usedAccounts.add("expenses:fees:trading:ibkr");
|
|
27125
|
+
} else {
|
|
27126
|
+
const consumed = consumeLotsFIFO(inventory, trade.symbol, Math.abs(trade.quantity), logger);
|
|
27127
|
+
journalEntries.push(generateIbkrSellEntry(tradeEntry, consumed, logger));
|
|
27128
|
+
usedAccounts.add(`assets:investments:stocks:${trade.symbol}`);
|
|
27129
|
+
usedAccounts.add(`assets:broker:ibkr:${currency}`);
|
|
27130
|
+
usedAccounts.add("expenses:fees:trading:ibkr");
|
|
27131
|
+
usedAccounts.add("income:capital-gains:realized");
|
|
27132
|
+
}
|
|
27133
|
+
}
|
|
27134
|
+
const dividendGroups = groupDividends(dividendTxns);
|
|
27135
|
+
for (const group of dividendGroups) {
|
|
27136
|
+
const netAmount = group.grossAmount - group.withholdingTax;
|
|
27137
|
+
const dividendEntry = {
|
|
27138
|
+
date: group.date,
|
|
27139
|
+
account: group.account,
|
|
27140
|
+
symbol: group.symbol,
|
|
27141
|
+
grossAmount: group.grossAmount,
|
|
27142
|
+
withholdingTax: group.withholdingTax,
|
|
27143
|
+
netAmount,
|
|
27144
|
+
currency: currency.toUpperCase()
|
|
27145
|
+
};
|
|
27146
|
+
journalEntries.push(generateIbkrDividendEntry(dividendEntry, logger));
|
|
27147
|
+
usedAccounts.add(`assets:broker:ibkr:${currency}`);
|
|
27148
|
+
usedAccounts.add(`income:dividends:${group.symbol}`);
|
|
27149
|
+
if (group.withholdingTax > 0) {
|
|
27150
|
+
usedAccounts.add("expenses:taxes:withholding");
|
|
27151
|
+
}
|
|
27152
|
+
}
|
|
27153
|
+
adjustments.sort((a, b) => a.date.localeCompare(b.date));
|
|
27154
|
+
for (const adj of adjustments) {
|
|
27155
|
+
const adjustmentEntry = {
|
|
27156
|
+
date: adj.date,
|
|
27157
|
+
account: adj.account,
|
|
27158
|
+
description: adj.description || "FX Translations P&L",
|
|
27159
|
+
amount: adj.netAmount,
|
|
27160
|
+
currency: currency.toUpperCase()
|
|
27161
|
+
};
|
|
27162
|
+
journalEntries.push(generateIbkrAdjustmentEntry(adjustmentEntry, logger));
|
|
27163
|
+
usedAccounts.add(`assets:broker:ibkr:${currency}`);
|
|
27164
|
+
if (adj.netAmount < 0) {
|
|
27165
|
+
usedAccounts.add("expenses:fx-losses:ibkr");
|
|
27166
|
+
} else {
|
|
27167
|
+
usedAccounts.add("income:fx-gains:ibkr");
|
|
27168
|
+
}
|
|
27169
|
+
}
|
|
27170
|
+
saveLotInventory(directory, lotInventoryPath, inventory, logger);
|
|
27171
|
+
let journalFilePath = null;
|
|
27172
|
+
if (journalEntries.length > 0) {
|
|
27173
|
+
const yearJournalPath = ensureYearJournalExists(directory, year);
|
|
27174
|
+
const header = `; IBKR ${currency.toUpperCase()} investment transactions for ${year}
|
|
27175
|
+
; Generated by opencode-accountant
|
|
27176
|
+
; This file is auto-generated - do not edit manually`;
|
|
27177
|
+
const journalContent = formatJournalFile(journalEntries, header);
|
|
27178
|
+
const investmentsDir = path17.join(directory, "ledger", "investments");
|
|
27179
|
+
if (!fs22.existsSync(investmentsDir)) {
|
|
27180
|
+
fs22.mkdirSync(investmentsDir, { recursive: true });
|
|
27181
|
+
}
|
|
27182
|
+
fs22.writeFileSync(journalPath, journalContent);
|
|
27183
|
+
journalFilePath = journalPath;
|
|
27184
|
+
const yearJournalContent = fs22.readFileSync(yearJournalPath, "utf-8");
|
|
27185
|
+
const includeDirective = `include investments/${path17.basename(journalPath)}`;
|
|
27186
|
+
if (!yearJournalContent.includes(includeDirective)) {
|
|
27187
|
+
fs22.writeFileSync(yearJournalPath, yearJournalContent.trimEnd() + `
|
|
27188
|
+
` + includeDirective + `
|
|
27189
|
+
`);
|
|
27190
|
+
}
|
|
27191
|
+
if (tradedSymbols.size > 0) {
|
|
27192
|
+
const commodityJournalPath = path17.join(directory, "ledger", "investments", "commodities.journal");
|
|
27193
|
+
ensureCommodityDeclarations(commodityJournalPath, tradedSymbols, logger);
|
|
27194
|
+
}
|
|
27195
|
+
if (usedAccounts.size > 0) {
|
|
27196
|
+
const accountJournalPath = path17.join(directory, "ledger", "investments", "accounts.journal");
|
|
27197
|
+
ensureInvestmentAccountDeclarations(accountJournalPath, usedAccounts, logger);
|
|
27198
|
+
}
|
|
27199
|
+
logger?.info(`Generated IBKR journal: ${journalPath} with ${journalEntries.length} entries`);
|
|
27200
|
+
}
|
|
27201
|
+
let simpleTransactionsCsvPath = null;
|
|
27202
|
+
if (deposits.length > 0) {
|
|
27203
|
+
const filteredCsvPath = path17.join(csvDir, `${csvBasename}-filtered.csv`);
|
|
27204
|
+
const csvHeader = "Date,Account,Description,Transaction Type,Symbol,Quantity,Price,Price Currency,Gross Amount,Commission,Net Amount";
|
|
27205
|
+
const csvRows = deposits.map((d) => [
|
|
27206
|
+
d.date,
|
|
27207
|
+
d.account,
|
|
27208
|
+
`"${d.description.replace(/"/g, '""')}"`,
|
|
27209
|
+
d.transactionType,
|
|
27210
|
+
d.symbol,
|
|
27211
|
+
d.quantity || "",
|
|
27212
|
+
d.price || "",
|
|
27213
|
+
d.priceCurrency,
|
|
27214
|
+
d.grossAmount || "",
|
|
27215
|
+
d.commission || "",
|
|
27216
|
+
d.netAmount
|
|
27217
|
+
].join(","));
|
|
27218
|
+
fs22.writeFileSync(filteredCsvPath, csvHeader + `
|
|
27219
|
+
` + csvRows.join(`
|
|
27220
|
+
`) + `
|
|
27221
|
+
`);
|
|
27222
|
+
simpleTransactionsCsvPath = filteredCsvPath;
|
|
27223
|
+
logger?.info(`Generated filtered CSV for deposits: ${filteredCsvPath} (${deposits.length} rows)`);
|
|
27224
|
+
}
|
|
27225
|
+
const dividendCount = dividendGroups.length;
|
|
27226
|
+
return {
|
|
27227
|
+
simpleTransactionsCsv: simpleTransactionsCsvPath,
|
|
27228
|
+
journalFile: journalFilePath,
|
|
27229
|
+
stats: {
|
|
27230
|
+
totalRows: transactions.length,
|
|
27231
|
+
deposits: deposits.length,
|
|
27232
|
+
trades: trades.length,
|
|
27233
|
+
dividends: dividendCount,
|
|
27234
|
+
adjustments: adjustments.length,
|
|
27235
|
+
skippedForex
|
|
27236
|
+
},
|
|
27237
|
+
alreadyPreprocessed: false
|
|
27238
|
+
};
|
|
27239
|
+
}
|
|
27240
|
+
|
|
26806
27241
|
// src/tools/import-pipeline.ts
|
|
26807
27242
|
class NoTransactionsError extends Error {
|
|
26808
27243
|
constructor() {
|
|
@@ -26875,7 +27310,7 @@ function executePreprocessFiatStep(context, contextIds, logger) {
|
|
|
26875
27310
|
for (const contextId of contextIds) {
|
|
26876
27311
|
const importCtx = loadContext(context.directory, contextId);
|
|
26877
27312
|
if (importCtx.provider === "revolut" && importCtx.currency !== "btc") {
|
|
26878
|
-
fiatCsvPaths.push(
|
|
27313
|
+
fiatCsvPaths.push(path18.join(context.directory, importCtx.filePath));
|
|
26879
27314
|
}
|
|
26880
27315
|
}
|
|
26881
27316
|
if (fiatCsvPaths.length === 0) {
|
|
@@ -26919,7 +27354,7 @@ function executePreprocessBtcStep(context, contextIds, logger) {
|
|
|
26919
27354
|
for (const contextId of contextIds) {
|
|
26920
27355
|
const importCtx = loadContext(context.directory, contextId);
|
|
26921
27356
|
if (importCtx.provider === "revolut" && importCtx.currency === "btc") {
|
|
26922
|
-
btcCsvPath =
|
|
27357
|
+
btcCsvPath = path18.join(context.directory, importCtx.filePath);
|
|
26923
27358
|
break;
|
|
26924
27359
|
}
|
|
26925
27360
|
}
|
|
@@ -26953,7 +27388,7 @@ async function executeBtcPurchaseStep(context, contextIds, logger) {
|
|
|
26953
27388
|
const importCtx = loadContext(context.directory, contextId);
|
|
26954
27389
|
if (importCtx.provider !== "revolut")
|
|
26955
27390
|
continue;
|
|
26956
|
-
const csvPath =
|
|
27391
|
+
const csvPath = path18.join(context.directory, importCtx.filePath);
|
|
26957
27392
|
if (importCtx.currency === "btc") {
|
|
26958
27393
|
btcCsvPath = csvPath;
|
|
26959
27394
|
} else {
|
|
@@ -26966,7 +27401,7 @@ async function executeBtcPurchaseStep(context, contextIds, logger) {
|
|
|
26966
27401
|
}
|
|
26967
27402
|
logger?.startSection("Step 1b: Generate BTC Purchase Entries");
|
|
26968
27403
|
logger?.logStep("BTC Purchases", "start");
|
|
26969
|
-
const btcFilename =
|
|
27404
|
+
const btcFilename = path18.basename(btcCsvPath);
|
|
26970
27405
|
const yearMatch = btcFilename.match(/(\d{4})-\d{2}-\d{2}/);
|
|
26971
27406
|
const year = yearMatch ? parseInt(yearMatch[1], 10) : new Date().getFullYear();
|
|
26972
27407
|
const yearJournalPath = ensureYearJournalExists(context.directory, year);
|
|
@@ -26993,7 +27428,7 @@ async function executeCurrencyExchangeStep(context, contextIds, logger) {
|
|
|
26993
27428
|
continue;
|
|
26994
27429
|
if (importCtx.currency === "btc")
|
|
26995
27430
|
continue;
|
|
26996
|
-
const csvPath =
|
|
27431
|
+
const csvPath = path18.join(context.directory, importCtx.filePath);
|
|
26997
27432
|
fiatContexts.push({ contextId, csvPath });
|
|
26998
27433
|
}
|
|
26999
27434
|
if (fiatContexts.length < 2) {
|
|
@@ -27002,7 +27437,7 @@ async function executeCurrencyExchangeStep(context, contextIds, logger) {
|
|
|
27002
27437
|
}
|
|
27003
27438
|
logger?.startSection("Step 1e: Generate Currency Exchange Entries");
|
|
27004
27439
|
logger?.logStep("Currency Exchanges", "start");
|
|
27005
|
-
const firstFilename =
|
|
27440
|
+
const firstFilename = path18.basename(fiatContexts[0].csvPath);
|
|
27006
27441
|
const yearMatch = firstFilename.match(/(\d{4})-\d{2}-\d{2}/);
|
|
27007
27442
|
const year = yearMatch ? parseInt(yearMatch[1], 10) : new Date().getFullYear();
|
|
27008
27443
|
const yearJournalPath = ensureYearJournalExists(context.directory, year);
|
|
@@ -27018,9 +27453,9 @@ async function executeCurrencyExchangeStep(context, contextIds, logger) {
|
|
|
27018
27453
|
continue;
|
|
27019
27454
|
const filteredCsvPath = filterExchangeRows(csvPath, indices, logger);
|
|
27020
27455
|
updateContext(context.directory, contextId, {
|
|
27021
|
-
filePath:
|
|
27456
|
+
filePath: path18.relative(context.directory, filteredCsvPath)
|
|
27022
27457
|
});
|
|
27023
|
-
logger?.info(`Updated context ${contextId} to use filtered CSV: ${
|
|
27458
|
+
logger?.info(`Updated context ${contextId} to use filtered CSV: ${path18.basename(filteredCsvPath)}`);
|
|
27024
27459
|
}
|
|
27025
27460
|
}
|
|
27026
27461
|
const message = result.entriesAdded > 0 ? `Generated ${result.entriesAdded} currency exchange entries (${result.matchCount} matched, ${result.skippedDuplicates} skipped)` : `No new currency exchange entries (${result.matchCount} matched, ${result.skippedDuplicates} duplicates)`;
|
|
@@ -27037,7 +27472,7 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
27037
27472
|
for (const contextId of contextIds) {
|
|
27038
27473
|
const importCtx = loadContext(context.directory, contextId);
|
|
27039
27474
|
if (importCtx.provider === "swissquote") {
|
|
27040
|
-
const csvPath =
|
|
27475
|
+
const csvPath = path18.join(context.directory, importCtx.filePath);
|
|
27041
27476
|
const toDateMatch = importCtx.filename.match(/to-\d{4}(\d{4})\./);
|
|
27042
27477
|
let year = new Date().getFullYear();
|
|
27043
27478
|
if (toDateMatch) {
|
|
@@ -27061,11 +27496,11 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
27061
27496
|
const lotInventoryPath = swissquoteProvider?.lotInventoryPath ?? DEFAULT_LOT_INVENTORY_PATH;
|
|
27062
27497
|
const symbolMapPath = swissquoteProvider?.symbolMapPath ?? "config/import/symbolMap.yaml";
|
|
27063
27498
|
let symbolMap = {};
|
|
27064
|
-
const symbolMapFullPath =
|
|
27065
|
-
if (
|
|
27499
|
+
const symbolMapFullPath = path18.join(context.directory, symbolMapPath);
|
|
27500
|
+
if (fs24.existsSync(symbolMapFullPath)) {
|
|
27066
27501
|
try {
|
|
27067
27502
|
const yaml = await Promise.resolve().then(() => (init_js_yaml(), exports_js_yaml));
|
|
27068
|
-
const content =
|
|
27503
|
+
const content = fs24.readFileSync(symbolMapFullPath, "utf-8");
|
|
27069
27504
|
const parsed = yaml.load(content);
|
|
27070
27505
|
if (parsed && typeof parsed === "object") {
|
|
27071
27506
|
symbolMap = parsed;
|
|
@@ -27089,7 +27524,7 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
27089
27524
|
let lastJournalFile = null;
|
|
27090
27525
|
const allForexRows = [];
|
|
27091
27526
|
for (const sqCtx of swissquoteContexts) {
|
|
27092
|
-
logger?.logStep("Swissquote Preprocess", "start", `Processing ${
|
|
27527
|
+
logger?.logStep("Swissquote Preprocess", "start", `Processing ${path18.basename(sqCtx.csvPath)}`);
|
|
27093
27528
|
const result = await preprocessSwissquote(sqCtx.csvPath, context.directory, sqCtx.currency, sqCtx.year, lotInventoryPath, symbolMap, logger);
|
|
27094
27529
|
totalStats.totalRows += result.stats.totalRows;
|
|
27095
27530
|
totalStats.simpleTransactions += result.stats.simpleTransactions;
|
|
@@ -27104,9 +27539,9 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
27104
27539
|
}
|
|
27105
27540
|
if (result.simpleTransactionsCsv) {
|
|
27106
27541
|
updateContext(context.directory, sqCtx.contextId, {
|
|
27107
|
-
filePath:
|
|
27542
|
+
filePath: path18.relative(context.directory, result.simpleTransactionsCsv)
|
|
27108
27543
|
});
|
|
27109
|
-
logger?.info(`Updated context ${sqCtx.contextId} to use filtered CSV: ${
|
|
27544
|
+
logger?.info(`Updated context ${sqCtx.contextId} to use filtered CSV: ${path18.basename(result.simpleTransactionsCsv)}`);
|
|
27110
27545
|
}
|
|
27111
27546
|
logger?.logStep("Swissquote Preprocess", "success", `Processed: ${result.stats.trades} trades, ${result.stats.dividends} dividends, ${result.stats.corporateActions} corporate actions, ${result.stats.forexTransactions} forex`);
|
|
27112
27547
|
}
|
|
@@ -27116,11 +27551,11 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
27116
27551
|
}
|
|
27117
27552
|
if (allForexRows.length > 0) {
|
|
27118
27553
|
const firstCtx = swissquoteContexts[0];
|
|
27119
|
-
const yearJournalPath =
|
|
27554
|
+
const yearJournalPath = path18.join(context.directory, "ledger", `${firstCtx.year}.journal`);
|
|
27120
27555
|
logger?.info(`Generating forex journal entries for ${allForexRows.length} forex rows...`);
|
|
27121
27556
|
const forexResult = generateSwissquoteForexJournal(allForexRows, yearJournalPath, logger);
|
|
27122
27557
|
if (forexResult.accountsUsed.size > 0) {
|
|
27123
|
-
const accountJournalPath =
|
|
27558
|
+
const accountJournalPath = path18.join(context.directory, "ledger", "investments", "accounts.journal");
|
|
27124
27559
|
ensureInvestmentAccountDeclarations(accountJournalPath, forexResult.accountsUsed, logger);
|
|
27125
27560
|
}
|
|
27126
27561
|
logger?.logStep("Swissquote Forex", "success", `Matched ${forexResult.matchCount} forex pairs, added ${forexResult.entriesAdded} entries` + (forexResult.skippedDuplicates > 0 ? `, skipped ${forexResult.skippedDuplicates} duplicates` : "") + (forexResult.unmatchedRows > 0 ? `, ${forexResult.unmatchedRows} unmatched rows` : ""));
|
|
@@ -27142,13 +27577,92 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
|
|
|
27142
27577
|
logger?.endSection();
|
|
27143
27578
|
}
|
|
27144
27579
|
}
|
|
27580
|
+
async function executeIbkrPreprocessStep(context, contextIds, logger) {
|
|
27581
|
+
const ibkrContexts = [];
|
|
27582
|
+
for (const contextId of contextIds) {
|
|
27583
|
+
const importCtx = loadContext(context.directory, contextId);
|
|
27584
|
+
if (importCtx.provider === "ibkr") {
|
|
27585
|
+
const csvPath = path18.join(context.directory, importCtx.filePath);
|
|
27586
|
+
const dateMatch = importCtx.filename.match(/(\d{4})\d{4}\./);
|
|
27587
|
+
let year = new Date().getFullYear();
|
|
27588
|
+
if (dateMatch) {
|
|
27589
|
+
year = parseInt(dateMatch[1], 10);
|
|
27590
|
+
}
|
|
27591
|
+
ibkrContexts.push({
|
|
27592
|
+
contextId,
|
|
27593
|
+
csvPath,
|
|
27594
|
+
currency: importCtx.currency,
|
|
27595
|
+
year
|
|
27596
|
+
});
|
|
27597
|
+
}
|
|
27598
|
+
}
|
|
27599
|
+
if (ibkrContexts.length === 0) {
|
|
27600
|
+
logger?.info("No IBKR CSV found, skipping preprocessing");
|
|
27601
|
+
return;
|
|
27602
|
+
}
|
|
27603
|
+
logger?.startSection("Step 1f: Preprocess IBKR CSV");
|
|
27604
|
+
const config2 = context.configLoader(context.directory);
|
|
27605
|
+
const ibkrProvider = config2.providers?.["ibkr"];
|
|
27606
|
+
const lotInventoryPath = ibkrProvider?.lotInventoryPath ?? DEFAULT_LOT_INVENTORY_PATH;
|
|
27607
|
+
try {
|
|
27608
|
+
let totalStats = {
|
|
27609
|
+
totalRows: 0,
|
|
27610
|
+
deposits: 0,
|
|
27611
|
+
trades: 0,
|
|
27612
|
+
dividends: 0,
|
|
27613
|
+
adjustments: 0,
|
|
27614
|
+
skippedForex: 0
|
|
27615
|
+
};
|
|
27616
|
+
let lastJournalFile = null;
|
|
27617
|
+
ibkrContexts.sort((a, b) => {
|
|
27618
|
+
const dateA = path18.basename(a.csvPath).match(/(\d{8})\./)?.[1] || "";
|
|
27619
|
+
const dateB = path18.basename(b.csvPath).match(/(\d{8})\./)?.[1] || "";
|
|
27620
|
+
return dateA.localeCompare(dateB);
|
|
27621
|
+
});
|
|
27622
|
+
for (const ibkrCtx of ibkrContexts) {
|
|
27623
|
+
logger?.logStep("IBKR Preprocess", "start", `Processing ${path18.basename(ibkrCtx.csvPath)}`);
|
|
27624
|
+
const result = await preprocessIbkr(ibkrCtx.csvPath, context.directory, ibkrCtx.currency, ibkrCtx.year, lotInventoryPath, logger);
|
|
27625
|
+
totalStats.totalRows += result.stats.totalRows;
|
|
27626
|
+
totalStats.deposits += result.stats.deposits;
|
|
27627
|
+
totalStats.trades += result.stats.trades;
|
|
27628
|
+
totalStats.dividends += result.stats.dividends;
|
|
27629
|
+
totalStats.adjustments += result.stats.adjustments;
|
|
27630
|
+
totalStats.skippedForex += result.stats.skippedForex;
|
|
27631
|
+
if (result.journalFile) {
|
|
27632
|
+
lastJournalFile = result.journalFile;
|
|
27633
|
+
}
|
|
27634
|
+
if (result.simpleTransactionsCsv) {
|
|
27635
|
+
updateContext(context.directory, ibkrCtx.contextId, {
|
|
27636
|
+
filePath: path18.relative(context.directory, result.simpleTransactionsCsv)
|
|
27637
|
+
});
|
|
27638
|
+
logger?.info(`Updated context ${ibkrCtx.contextId} to use filtered CSV: ${path18.basename(result.simpleTransactionsCsv)}`);
|
|
27639
|
+
}
|
|
27640
|
+
logger?.logStep("IBKR Preprocess", "success", `Processed: ${result.stats.trades} trades, ${result.stats.dividends} dividends, ${result.stats.adjustments} adjustments, ${result.stats.skippedForex} forex skipped`);
|
|
27641
|
+
}
|
|
27642
|
+
const message = `Preprocessed ${totalStats.totalRows} rows: ${totalStats.trades} trades, ${totalStats.dividends} dividends, ${totalStats.adjustments} adjustments, ${totalStats.deposits} deposits, ${totalStats.skippedForex} forex skipped`;
|
|
27643
|
+
context.result.steps.ibkrPreprocess = buildStepResult(true, message, {
|
|
27644
|
+
...totalStats,
|
|
27645
|
+
journalFile: lastJournalFile
|
|
27646
|
+
});
|
|
27647
|
+
} catch (error45) {
|
|
27648
|
+
const errorMessage = `IBKR preprocessing failed: ${error45 instanceof Error ? error45.message : String(error45)}`;
|
|
27649
|
+
logger?.error(errorMessage);
|
|
27650
|
+
logger?.logStep("IBKR Preprocess", "error", errorMessage);
|
|
27651
|
+
context.result.steps.ibkrPreprocess = buildStepResult(false, errorMessage);
|
|
27652
|
+
context.result.error = errorMessage;
|
|
27653
|
+
context.result.hint = "Check the IBKR CSV format and lot inventory state";
|
|
27654
|
+
throw new Error(errorMessage);
|
|
27655
|
+
} finally {
|
|
27656
|
+
logger?.endSection();
|
|
27657
|
+
}
|
|
27658
|
+
}
|
|
27145
27659
|
async function executeAccountDeclarationsStep(context, contextId, logger) {
|
|
27146
27660
|
logger?.startSection("Step 2: Check Account Declarations");
|
|
27147
27661
|
logger?.logStep("Check Accounts", "start");
|
|
27148
27662
|
const config2 = context.configLoader(context.directory);
|
|
27149
|
-
const rulesDir =
|
|
27663
|
+
const rulesDir = path18.join(context.directory, config2.paths.rules);
|
|
27150
27664
|
const importCtx = loadContext(context.directory, contextId);
|
|
27151
|
-
const csvPath =
|
|
27665
|
+
const csvPath = path18.join(context.directory, importCtx.filePath);
|
|
27152
27666
|
const csvFiles = [csvPath];
|
|
27153
27667
|
const rulesMapping = loadRulesMapping(rulesDir);
|
|
27154
27668
|
const matchedRulesFiles = new Set;
|
|
@@ -27171,7 +27685,7 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
|
|
|
27171
27685
|
context.result.steps.accountDeclarations = buildStepResult(true, "No accounts found in rules files", {
|
|
27172
27686
|
accountsAdded: [],
|
|
27173
27687
|
journalUpdated: "",
|
|
27174
|
-
rulesScanned: Array.from(matchedRulesFiles).map((f) =>
|
|
27688
|
+
rulesScanned: Array.from(matchedRulesFiles).map((f) => path18.relative(context.directory, f))
|
|
27175
27689
|
});
|
|
27176
27690
|
return;
|
|
27177
27691
|
}
|
|
@@ -27195,7 +27709,7 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
|
|
|
27195
27709
|
context.result.steps.accountDeclarations = buildStepResult(true, "No transactions found, skipping account declarations", {
|
|
27196
27710
|
accountsAdded: [],
|
|
27197
27711
|
journalUpdated: "",
|
|
27198
|
-
rulesScanned: Array.from(matchedRulesFiles).map((f) =>
|
|
27712
|
+
rulesScanned: Array.from(matchedRulesFiles).map((f) => path18.relative(context.directory, f))
|
|
27199
27713
|
});
|
|
27200
27714
|
return;
|
|
27201
27715
|
}
|
|
@@ -27206,12 +27720,12 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
|
|
|
27206
27720
|
context.result.steps.accountDeclarations = buildStepResult(false, `Failed to create year journal: ${error45 instanceof Error ? error45.message : String(error45)}`, {
|
|
27207
27721
|
accountsAdded: [],
|
|
27208
27722
|
journalUpdated: "",
|
|
27209
|
-
rulesScanned: Array.from(matchedRulesFiles).map((f) =>
|
|
27723
|
+
rulesScanned: Array.from(matchedRulesFiles).map((f) => path18.relative(context.directory, f))
|
|
27210
27724
|
});
|
|
27211
27725
|
return;
|
|
27212
27726
|
}
|
|
27213
27727
|
const result = ensureAccountDeclarations(yearJournalPath, allAccounts);
|
|
27214
|
-
const message = result.added.length > 0 ? `Added ${result.added.length} account declaration(s) to ${
|
|
27728
|
+
const message = result.added.length > 0 ? `Added ${result.added.length} account declaration(s) to ${path18.relative(context.directory, yearJournalPath)}` : "All required accounts already declared";
|
|
27215
27729
|
logger?.logStep("Check Accounts", "success", message);
|
|
27216
27730
|
if (result.added.length > 0) {
|
|
27217
27731
|
for (const account of result.added) {
|
|
@@ -27220,17 +27734,17 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
|
|
|
27220
27734
|
}
|
|
27221
27735
|
context.result.steps.accountDeclarations = buildStepResult(true, message, {
|
|
27222
27736
|
accountsAdded: result.added,
|
|
27223
|
-
journalUpdated:
|
|
27224
|
-
rulesScanned: Array.from(matchedRulesFiles).map((f) =>
|
|
27737
|
+
journalUpdated: path18.relative(context.directory, yearJournalPath),
|
|
27738
|
+
rulesScanned: Array.from(matchedRulesFiles).map((f) => path18.relative(context.directory, f))
|
|
27225
27739
|
});
|
|
27226
27740
|
logger?.endSection();
|
|
27227
27741
|
}
|
|
27228
27742
|
async function buildSuggestionContext(context, contextId, logger) {
|
|
27229
27743
|
const { loadExistingAccounts: loadExistingAccounts2, extractRulePatternsFromFile: extractRulePatternsFromFile2 } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
|
|
27230
27744
|
const config2 = context.configLoader(context.directory);
|
|
27231
|
-
const rulesDir =
|
|
27745
|
+
const rulesDir = path18.join(context.directory, config2.paths.rules);
|
|
27232
27746
|
const importCtx = loadContext(context.directory, contextId);
|
|
27233
|
-
const csvPath =
|
|
27747
|
+
const csvPath = path18.join(context.directory, importCtx.filePath);
|
|
27234
27748
|
const rulesMapping = loadRulesMapping(rulesDir);
|
|
27235
27749
|
const rulesFile = findRulesForCsv(csvPath, rulesMapping);
|
|
27236
27750
|
if (!rulesFile) {
|
|
@@ -27446,6 +27960,7 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
27446
27960
|
await executeBtcPurchaseStep(context, contextIds, logger);
|
|
27447
27961
|
await executeCurrencyExchangeStep(context, contextIds, logger);
|
|
27448
27962
|
await executeSwissquotePreprocessStep(context, contextIds, logger);
|
|
27963
|
+
await executeIbkrPreprocessStep(context, contextIds, logger);
|
|
27449
27964
|
if (context.generatedAccounts.size > 0) {
|
|
27450
27965
|
const firstCtx = contextIds.map((id) => loadContext(context.directory, id)).find((c) => c.provider === "revolut");
|
|
27451
27966
|
if (firstCtx) {
|
|
@@ -27459,8 +27974,8 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
27459
27974
|
}
|
|
27460
27975
|
}
|
|
27461
27976
|
const importConfig = loadImportConfig(context.directory);
|
|
27462
|
-
const pendingDir =
|
|
27463
|
-
const doneDir =
|
|
27977
|
+
const pendingDir = path18.join(context.directory, importConfig.paths.pending);
|
|
27978
|
+
const doneDir = path18.join(context.directory, importConfig.paths.done);
|
|
27464
27979
|
const orderedContextIds = [...contextIds].sort((a, b) => {
|
|
27465
27980
|
const ctxA = loadContext(context.directory, a);
|
|
27466
27981
|
const ctxB = loadContext(context.directory, b);
|
|
@@ -27483,8 +27998,8 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
27483
27998
|
totalTransactions += context.result.steps.import?.details?.summary?.totalTransactions || 0;
|
|
27484
27999
|
} catch (error45) {
|
|
27485
28000
|
if (error45 instanceof NoTransactionsError) {
|
|
27486
|
-
const csvPath =
|
|
27487
|
-
if (
|
|
28001
|
+
const csvPath = path18.join(context.directory, importContext.filePath);
|
|
28002
|
+
if (fs24.existsSync(csvPath)) {
|
|
27488
28003
|
moveCsvToDone(csvPath, pendingDir, doneDir);
|
|
27489
28004
|
logger.info(`No transactions to import for ${importContext.filename}, moved to done`);
|
|
27490
28005
|
} else {
|
|
@@ -27565,16 +28080,16 @@ This tool orchestrates the full import workflow:
|
|
|
27565
28080
|
}
|
|
27566
28081
|
});
|
|
27567
28082
|
// src/tools/init-directories.ts
|
|
27568
|
-
import * as
|
|
27569
|
-
import * as
|
|
28083
|
+
import * as fs25 from "fs";
|
|
28084
|
+
import * as path19 from "path";
|
|
27570
28085
|
async function initDirectories(directory) {
|
|
27571
28086
|
try {
|
|
27572
28087
|
const config2 = loadImportConfig(directory);
|
|
27573
28088
|
const directoriesCreated = [];
|
|
27574
28089
|
const gitkeepFiles = [];
|
|
27575
|
-
const importBase =
|
|
27576
|
-
if (!
|
|
27577
|
-
|
|
28090
|
+
const importBase = path19.join(directory, "import");
|
|
28091
|
+
if (!fs25.existsSync(importBase)) {
|
|
28092
|
+
fs25.mkdirSync(importBase, { recursive: true });
|
|
27578
28093
|
directoriesCreated.push("import");
|
|
27579
28094
|
}
|
|
27580
28095
|
const pathsToCreate = [
|
|
@@ -27584,20 +28099,20 @@ async function initDirectories(directory) {
|
|
|
27584
28099
|
{ key: "unrecognized", path: config2.paths.unrecognized }
|
|
27585
28100
|
];
|
|
27586
28101
|
for (const { path: dirPath } of pathsToCreate) {
|
|
27587
|
-
const fullPath =
|
|
27588
|
-
if (!
|
|
27589
|
-
|
|
28102
|
+
const fullPath = path19.join(directory, dirPath);
|
|
28103
|
+
if (!fs25.existsSync(fullPath)) {
|
|
28104
|
+
fs25.mkdirSync(fullPath, { recursive: true });
|
|
27590
28105
|
directoriesCreated.push(dirPath);
|
|
27591
28106
|
}
|
|
27592
|
-
const gitkeepPath =
|
|
27593
|
-
if (!
|
|
27594
|
-
|
|
27595
|
-
gitkeepFiles.push(
|
|
28107
|
+
const gitkeepPath = path19.join(fullPath, ".gitkeep");
|
|
28108
|
+
if (!fs25.existsSync(gitkeepPath)) {
|
|
28109
|
+
fs25.writeFileSync(gitkeepPath, "");
|
|
28110
|
+
gitkeepFiles.push(path19.join(dirPath, ".gitkeep"));
|
|
27596
28111
|
}
|
|
27597
28112
|
}
|
|
27598
|
-
const gitignorePath =
|
|
28113
|
+
const gitignorePath = path19.join(importBase, ".gitignore");
|
|
27599
28114
|
let gitignoreCreated = false;
|
|
27600
|
-
if (!
|
|
28115
|
+
if (!fs25.existsSync(gitignorePath)) {
|
|
27601
28116
|
const gitignoreContent = `# Ignore CSV/PDF files in temporary directories
|
|
27602
28117
|
/incoming/*.csv
|
|
27603
28118
|
/incoming/*.pdf
|
|
@@ -27615,7 +28130,7 @@ async function initDirectories(directory) {
|
|
|
27615
28130
|
.DS_Store
|
|
27616
28131
|
Thumbs.db
|
|
27617
28132
|
`;
|
|
27618
|
-
|
|
28133
|
+
fs25.writeFileSync(gitignorePath, gitignoreContent);
|
|
27619
28134
|
gitignoreCreated = true;
|
|
27620
28135
|
}
|
|
27621
28136
|
const parts = [];
|
|
@@ -27691,32 +28206,32 @@ You can now drop CSV files into import/incoming/ and run import-pipeline.`);
|
|
|
27691
28206
|
}
|
|
27692
28207
|
});
|
|
27693
28208
|
// src/tools/generate-btc-purchases.ts
|
|
27694
|
-
import * as
|
|
27695
|
-
import * as
|
|
28209
|
+
import * as path20 from "path";
|
|
28210
|
+
import * as fs26 from "fs";
|
|
27696
28211
|
function findFiatCsvPaths(directory, pendingDir, provider) {
|
|
27697
|
-
const providerDir =
|
|
27698
|
-
if (!
|
|
28212
|
+
const providerDir = path20.join(directory, pendingDir, provider);
|
|
28213
|
+
if (!fs26.existsSync(providerDir))
|
|
27699
28214
|
return [];
|
|
27700
28215
|
const csvPaths = [];
|
|
27701
|
-
const entries =
|
|
28216
|
+
const entries = fs26.readdirSync(providerDir, { withFileTypes: true });
|
|
27702
28217
|
for (const entry of entries) {
|
|
27703
28218
|
if (!entry.isDirectory())
|
|
27704
28219
|
continue;
|
|
27705
28220
|
if (entry.name === "btc")
|
|
27706
28221
|
continue;
|
|
27707
|
-
const csvFiles = findCsvFiles(
|
|
28222
|
+
const csvFiles = findCsvFiles(path20.join(providerDir, entry.name), { fullPaths: true });
|
|
27708
28223
|
csvPaths.push(...csvFiles);
|
|
27709
28224
|
}
|
|
27710
28225
|
return csvPaths;
|
|
27711
28226
|
}
|
|
27712
28227
|
function findBtcCsvPath(directory, pendingDir, provider) {
|
|
27713
|
-
const btcDir =
|
|
28228
|
+
const btcDir = path20.join(directory, pendingDir, provider, "btc");
|
|
27714
28229
|
const csvFiles = findCsvFiles(btcDir, { fullPaths: true });
|
|
27715
28230
|
return csvFiles[0];
|
|
27716
28231
|
}
|
|
27717
28232
|
function determineYear(csvPaths) {
|
|
27718
28233
|
for (const csvPath of csvPaths) {
|
|
27719
|
-
const match2 =
|
|
28234
|
+
const match2 = path20.basename(csvPath).match(/(\d{4})-\d{2}-\d{2}/);
|
|
27720
28235
|
if (match2)
|
|
27721
28236
|
return parseInt(match2[1], 10);
|
|
27722
28237
|
}
|
|
@@ -27769,7 +28284,7 @@ async function generateBtcPurchases(directory, agent, options = {}, configLoader
|
|
|
27769
28284
|
skippedDuplicates: result.skippedDuplicates,
|
|
27770
28285
|
unmatchedFiat: result.unmatchedFiat.length,
|
|
27771
28286
|
unmatchedBtc: result.unmatchedBtc.length,
|
|
27772
|
-
yearJournal:
|
|
28287
|
+
yearJournal: path20.relative(directory, yearJournalPath)
|
|
27773
28288
|
});
|
|
27774
28289
|
} catch (err) {
|
|
27775
28290
|
logger.error("Failed to generate BTC purchases", err);
|
|
@@ -27811,8 +28326,8 @@ to produce equity conversion entries for Bitcoin purchases.
|
|
|
27811
28326
|
}
|
|
27812
28327
|
});
|
|
27813
28328
|
// src/index.ts
|
|
27814
|
-
var __dirname2 =
|
|
27815
|
-
var AGENT_FILE =
|
|
28329
|
+
var __dirname2 = dirname10(fileURLToPath3(import.meta.url));
|
|
28330
|
+
var AGENT_FILE = join18(__dirname2, "..", "agent", "accountant.md");
|
|
27816
28331
|
var AccountantPlugin = async () => {
|
|
27817
28332
|
const agent = loadAgent(AGENT_FILE);
|
|
27818
28333
|
return {
|
package/package.json
CHANGED