@fuzzle/opencode-accountant 0.16.0 → 0.16.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.
Files changed (2) hide show
  1. package/dist/index.js +78 -19
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -23515,7 +23515,8 @@ function balancesMatch(balance1, balance2) {
23515
23515
  return false;
23516
23516
  }
23517
23517
  validateCurrencies(parsed1, parsed2);
23518
- return parsed1.amount === parsed2.amount;
23518
+ const round2 = (n) => Math.round(n * 100) / 100;
23519
+ return round2(parsed1.amount) === round2(parsed2.amount);
23519
23520
  }
23520
23521
 
23521
23522
  // src/utils/csvParser.ts
@@ -24035,7 +24036,7 @@ function buildRetryCommand(contextId, closingBalance, account) {
24035
24036
  }
24036
24037
  return parts.join(" ");
24037
24038
  }
24038
- function determineAccount(csvFile, rulesDir, importContext, manualAccount, relativeCsvPath, metadata) {
24039
+ function determineAccount(csvFile, rulesDir, directory, importContext, manualAccount, relativeCsvPath, metadata) {
24039
24040
  let account = manualAccount;
24040
24041
  const rulesMapping = loadRulesMapping(rulesDir);
24041
24042
  const rulesFile = findRulesForCsv(csvFile, rulesMapping);
@@ -24043,6 +24044,12 @@ function determineAccount(csvFile, rulesDir, importContext, manualAccount, relat
24043
24044
  if (rulesFile) {
24044
24045
  account = getAccountFromRulesFile(rulesFile) ?? undefined;
24045
24046
  }
24047
+ if (!account && importContext.rulesFile) {
24048
+ const contextRulesPath = path10.join(directory, importContext.rulesFile);
24049
+ if (fs11.existsSync(contextRulesPath)) {
24050
+ account = getAccountFromRulesFile(contextRulesPath) ?? undefined;
24051
+ }
24052
+ }
24046
24053
  }
24047
24054
  if (!account) {
24048
24055
  const rulesHint = rulesFile ? `Add 'account1 assets:bank:...' to ${rulesFile} or retry with: ${buildRetryCommand(importContext.id, undefined, "assets:bank:...")}` : `Create a rules file in ${rulesDir} with 'account1' directive or retry with: ${buildRetryCommand(importContext.id, undefined, "assets:bank:...")}`;
@@ -24160,7 +24167,7 @@ async function reconcileStatement(directory, agent, options, configLoader = load
24160
24167
  return balanceResult.error;
24161
24168
  }
24162
24169
  const { closingBalance, metadata, fromCSVAnalysis } = balanceResult;
24163
- const accountResult = determineAccount(csvFile, rulesDir, importContext, options.account, relativeCsvPath, metadata);
24170
+ const accountResult = determineAccount(csvFile, rulesDir, directory, importContext, options.account, relativeCsvPath, metadata);
24164
24171
  if ("error" in accountResult) {
24165
24172
  return accountResult.error;
24166
24173
  }
@@ -26962,6 +26969,34 @@ function generateIbkrAdjustmentEntry(adjustment, logger) {
26962
26969
  `;
26963
26970
  return entry;
26964
26971
  }
26972
+ function generateIbkrForexEntry(entry, logger) {
26973
+ const description = escapeDescription(entry.description);
26974
+ logger?.debug(`Generating IBKR Forex entry: ${entry.amount} ${entry.currency}`);
26975
+ const postings = [
26976
+ {
26977
+ account: `assets:broker:ibkr:${entry.currency.toLowerCase()}`,
26978
+ amount: formatAmount(entry.amount, entry.currency)
26979
+ }
26980
+ ];
26981
+ if (entry.amount < 0) {
26982
+ postings.push({
26983
+ account: "expenses:fees:forex:ibkr",
26984
+ amount: formatAmount(Math.abs(entry.amount), entry.currency)
26985
+ });
26986
+ } else {
26987
+ postings.push({
26988
+ account: "income:fx-gains:ibkr",
26989
+ amount: formatAmount(-entry.amount, entry.currency)
26990
+ });
26991
+ }
26992
+ let result = `${entry.date} ${description}
26993
+ `;
26994
+ result += ` ; ibkr:account:${entry.account}
26995
+ `;
26996
+ result += formatPostings(postings) + `
26997
+ `;
26998
+ return result;
26999
+ }
26965
27000
 
26966
27001
  // src/utils/ibkrCsvPreprocessor.ts
26967
27002
  var TRADE_TYPES2 = new Set(["Buy", "Sell"]);
@@ -27058,7 +27093,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27058
27093
  trades: 0,
27059
27094
  dividends: 0,
27060
27095
  adjustments: 0,
27061
- skippedForex: 0
27096
+ forex: 0
27062
27097
  },
27063
27098
  alreadyPreprocessed: true
27064
27099
  };
@@ -27070,7 +27105,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27070
27105
  const trades = [];
27071
27106
  const dividendTxns = [];
27072
27107
  const adjustments = [];
27073
- let skippedForex = 0;
27108
+ const forexTxns = [];
27074
27109
  for (const txn of transactions) {
27075
27110
  if (TRADE_TYPES2.has(txn.transactionType)) {
27076
27111
  trades.push(txn);
@@ -27081,7 +27116,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27081
27116
  } else if (txn.transactionType === DEPOSIT_TYPE) {
27082
27117
  deposits.push(txn);
27083
27118
  } else if (txn.transactionType === FOREX_TYPE) {
27084
- skippedForex++;
27119
+ forexTxns.push(txn);
27085
27120
  } else {
27086
27121
  logger?.warn(`Unknown IBKR transaction type: ${txn.transactionType}`);
27087
27122
  }
@@ -27167,10 +27202,41 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27167
27202
  usedAccounts.add("income:fx-gains:ibkr");
27168
27203
  }
27169
27204
  }
27205
+ if (forexTxns.length > 0) {
27206
+ const forexByDate = new Map;
27207
+ for (const txn of forexTxns) {
27208
+ const existing = forexByDate.get(txn.date);
27209
+ if (existing) {
27210
+ existing.total += txn.netAmount;
27211
+ } else {
27212
+ forexByDate.set(txn.date, { account: txn.account, total: txn.netAmount });
27213
+ }
27214
+ }
27215
+ const sortedDates = [...forexByDate.keys()].sort();
27216
+ for (const date5 of sortedDates) {
27217
+ const { account, total } = forexByDate.get(date5);
27218
+ const amount = Math.round(total * 100) / 100;
27219
+ if (amount === 0)
27220
+ continue;
27221
+ const forexEntry = {
27222
+ date: date5,
27223
+ account,
27224
+ description: "Forex conversion cost",
27225
+ amount,
27226
+ currency: currency.toUpperCase()
27227
+ };
27228
+ journalEntries.push(generateIbkrForexEntry(forexEntry, logger));
27229
+ usedAccounts.add(`assets:broker:ibkr:${currency}`);
27230
+ if (amount < 0) {
27231
+ usedAccounts.add("expenses:fees:forex:ibkr");
27232
+ } else {
27233
+ usedAccounts.add("income:fx-gains:ibkr");
27234
+ }
27235
+ }
27236
+ }
27170
27237
  saveLotInventory(directory, lotInventoryPath, inventory, logger);
27171
27238
  let journalFilePath = null;
27172
27239
  if (journalEntries.length > 0) {
27173
- const yearJournalPath = ensureYearJournalExists(directory, year);
27174
27240
  const header = `; IBKR ${currency.toUpperCase()} investment transactions for ${year}
27175
27241
  ; Generated by opencode-accountant
27176
27242
  ; This file is auto-generated - do not edit manually`;
@@ -27181,13 +27247,6 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27181
27247
  }
27182
27248
  fs22.writeFileSync(journalPath, journalContent);
27183
27249
  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
27250
  if (tradedSymbols.size > 0) {
27192
27251
  const commodityJournalPath = path17.join(directory, "ledger", "investments", "commodities.journal");
27193
27252
  ensureCommodityDeclarations(commodityJournalPath, tradedSymbols, logger);
@@ -27232,7 +27291,7 @@ async function preprocessIbkr(csvPath, directory, currency, year, lotInventoryPa
27232
27291
  trades: trades.length,
27233
27292
  dividends: dividendCount,
27234
27293
  adjustments: adjustments.length,
27235
- skippedForex
27294
+ forex: forexTxns.length
27236
27295
  },
27237
27296
  alreadyPreprocessed: false
27238
27297
  };
@@ -27611,7 +27670,7 @@ async function executeIbkrPreprocessStep(context, contextIds, logger) {
27611
27670
  trades: 0,
27612
27671
  dividends: 0,
27613
27672
  adjustments: 0,
27614
- skippedForex: 0
27673
+ forex: 0
27615
27674
  };
27616
27675
  let lastJournalFile = null;
27617
27676
  ibkrContexts.sort((a, b) => {
@@ -27627,7 +27686,7 @@ async function executeIbkrPreprocessStep(context, contextIds, logger) {
27627
27686
  totalStats.trades += result.stats.trades;
27628
27687
  totalStats.dividends += result.stats.dividends;
27629
27688
  totalStats.adjustments += result.stats.adjustments;
27630
- totalStats.skippedForex += result.stats.skippedForex;
27689
+ totalStats.forex += result.stats.forex;
27631
27690
  if (result.journalFile) {
27632
27691
  lastJournalFile = result.journalFile;
27633
27692
  }
@@ -27637,9 +27696,9 @@ async function executeIbkrPreprocessStep(context, contextIds, logger) {
27637
27696
  });
27638
27697
  logger?.info(`Updated context ${ibkrCtx.contextId} to use filtered CSV: ${path18.basename(result.simpleTransactionsCsv)}`);
27639
27698
  }
27640
- logger?.logStep("IBKR Preprocess", "success", `Processed: ${result.stats.trades} trades, ${result.stats.dividends} dividends, ${result.stats.adjustments} adjustments, ${result.stats.skippedForex} forex skipped`);
27699
+ logger?.logStep("IBKR Preprocess", "success", `Processed: ${result.stats.trades} trades, ${result.stats.dividends} dividends, ${result.stats.adjustments} adjustments, ${result.stats.forex} forex`);
27641
27700
  }
27642
- const message = `Preprocessed ${totalStats.totalRows} rows: ${totalStats.trades} trades, ${totalStats.dividends} dividends, ${totalStats.adjustments} adjustments, ${totalStats.deposits} deposits, ${totalStats.skippedForex} forex skipped`;
27701
+ const message = `Preprocessed ${totalStats.totalRows} rows: ${totalStats.trades} trades, ${totalStats.dividends} dividends, ${totalStats.adjustments} adjustments, ${totalStats.deposits} deposits, ${totalStats.forex} forex`;
27643
27702
  context.result.steps.ibkrPreprocess = buildStepResult(true, message, {
27644
27703
  ...totalStats,
27645
27704
  journalFile: lastJournalFile
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.16.0",
3
+ "version": "0.16.1-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",