@fuzzle/opencode-accountant 0.0.13 → 0.0.14-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 +106 -7
- package/docs/tools/import-statements.md +54 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -17689,6 +17689,18 @@ function countTransactions(hledgerOutput) {
|
|
|
17689
17689
|
}
|
|
17690
17690
|
return count;
|
|
17691
17691
|
}
|
|
17692
|
+
function extractTransactionYears(hledgerOutput) {
|
|
17693
|
+
const years = new Set;
|
|
17694
|
+
const lines = hledgerOutput.split(`
|
|
17695
|
+
`);
|
|
17696
|
+
for (const line of lines) {
|
|
17697
|
+
const match = line.match(/^(\d{4})-\d{2}-\d{2}\s+/);
|
|
17698
|
+
if (match) {
|
|
17699
|
+
years.add(parseInt(match[1], 10));
|
|
17700
|
+
}
|
|
17701
|
+
}
|
|
17702
|
+
return years;
|
|
17703
|
+
}
|
|
17692
17704
|
|
|
17693
17705
|
// src/utils/rulesParser.ts
|
|
17694
17706
|
function parseSkipRows(rulesContent) {
|
|
@@ -17903,6 +17915,36 @@ function findMatchingCsvRow(posting, csvRows, config2) {
|
|
|
17903
17915
|
}
|
|
17904
17916
|
|
|
17905
17917
|
// src/tools/import-statements.ts
|
|
17918
|
+
function ensureYearJournalExists(directory, year) {
|
|
17919
|
+
const ledgerDir = path6.join(directory, "ledger");
|
|
17920
|
+
const yearJournalPath = path6.join(ledgerDir, `${year}.journal`);
|
|
17921
|
+
const mainJournalPath = path6.join(directory, ".hledger.journal");
|
|
17922
|
+
if (!fs7.existsSync(ledgerDir)) {
|
|
17923
|
+
fs7.mkdirSync(ledgerDir, { recursive: true });
|
|
17924
|
+
}
|
|
17925
|
+
if (!fs7.existsSync(yearJournalPath)) {
|
|
17926
|
+
fs7.writeFileSync(yearJournalPath, `; ${year} transactions
|
|
17927
|
+
`);
|
|
17928
|
+
}
|
|
17929
|
+
if (!fs7.existsSync(mainJournalPath)) {
|
|
17930
|
+
throw new Error(`.hledger.journal not found at ${mainJournalPath}. Create it first with appropriate includes.`);
|
|
17931
|
+
}
|
|
17932
|
+
const mainJournalContent = fs7.readFileSync(mainJournalPath, "utf-8");
|
|
17933
|
+
const includeDirective = `include ledger/${year}.journal`;
|
|
17934
|
+
const lines = mainJournalContent.split(`
|
|
17935
|
+
`);
|
|
17936
|
+
const includeExists = lines.some((line) => {
|
|
17937
|
+
const trimmed = line.trim();
|
|
17938
|
+
return trimmed === includeDirective || trimmed.startsWith(includeDirective + " ");
|
|
17939
|
+
});
|
|
17940
|
+
if (!includeExists) {
|
|
17941
|
+
const newContent = mainJournalContent.trimEnd() + `
|
|
17942
|
+
` + includeDirective + `
|
|
17943
|
+
`;
|
|
17944
|
+
fs7.writeFileSync(mainJournalPath, newContent);
|
|
17945
|
+
}
|
|
17946
|
+
return yearJournalPath;
|
|
17947
|
+
}
|
|
17906
17948
|
function findPendingCsvFiles(pendingDir, provider, currency) {
|
|
17907
17949
|
const csvFiles = [];
|
|
17908
17950
|
if (!fs7.existsSync(pendingDir)) {
|
|
@@ -18006,18 +18048,32 @@ async function importStatementsCore(directory, agent, options, configLoader = lo
|
|
|
18006
18048
|
const unknownPostings = parseUnknownPostings(result.stdout);
|
|
18007
18049
|
const transactionCount = countTransactions(result.stdout);
|
|
18008
18050
|
const matchedCount = transactionCount - unknownPostings.length;
|
|
18051
|
+
const years = extractTransactionYears(result.stdout);
|
|
18052
|
+
if (years.size > 1) {
|
|
18053
|
+
const yearList = Array.from(years).sort().join(", ");
|
|
18054
|
+
filesWithErrors++;
|
|
18055
|
+
fileResults.push({
|
|
18056
|
+
csv: path6.relative(directory, csvFile),
|
|
18057
|
+
rulesFile: path6.relative(directory, rulesFile),
|
|
18058
|
+
totalTransactions: transactionCount,
|
|
18059
|
+
matchedTransactions: matchedCount,
|
|
18060
|
+
unknownPostings: [],
|
|
18061
|
+
error: `CSV contains transactions from multiple years (${yearList}). Split the CSV by year before importing.`
|
|
18062
|
+
});
|
|
18063
|
+
continue;
|
|
18064
|
+
}
|
|
18065
|
+
const transactionYear = years.size === 1 ? Array.from(years)[0] : undefined;
|
|
18009
18066
|
if (unknownPostings.length > 0) {
|
|
18010
18067
|
try {
|
|
18011
18068
|
const rulesContent = fs7.readFileSync(rulesFile, "utf-8");
|
|
18012
18069
|
const rulesConfig = parseRulesFile(rulesContent);
|
|
18013
18070
|
const csvRows = parseCsvFile(csvFile, rulesConfig);
|
|
18014
18071
|
for (const posting of unknownPostings) {
|
|
18015
|
-
|
|
18072
|
+
posting.csvRow = findMatchingCsvRow({
|
|
18016
18073
|
date: posting.date,
|
|
18017
18074
|
description: posting.description,
|
|
18018
18075
|
amount: posting.amount
|
|
18019
18076
|
}, csvRows, rulesConfig);
|
|
18020
|
-
posting.csvRow = csvRow;
|
|
18021
18077
|
}
|
|
18022
18078
|
} catch {
|
|
18023
18079
|
for (const posting of unknownPostings) {
|
|
@@ -18033,7 +18089,8 @@ async function importStatementsCore(directory, agent, options, configLoader = lo
|
|
|
18033
18089
|
rulesFile: path6.relative(directory, rulesFile),
|
|
18034
18090
|
totalTransactions: transactionCount,
|
|
18035
18091
|
matchedTransactions: matchedCount,
|
|
18036
|
-
unknownPostings
|
|
18092
|
+
unknownPostings,
|
|
18093
|
+
transactionYear
|
|
18037
18094
|
});
|
|
18038
18095
|
}
|
|
18039
18096
|
const hasUnknowns = totalUnknown > 0;
|
|
@@ -18077,11 +18134,53 @@ async function importStatementsCore(directory, agent, options, configLoader = lo
|
|
|
18077
18134
|
});
|
|
18078
18135
|
}
|
|
18079
18136
|
const importedFiles = [];
|
|
18080
|
-
for (const
|
|
18081
|
-
const
|
|
18137
|
+
for (const fileResult of fileResults) {
|
|
18138
|
+
const csvFile = path6.join(directory, fileResult.csv);
|
|
18139
|
+
const rulesFile = fileResult.rulesFile ? path6.join(directory, fileResult.rulesFile) : null;
|
|
18082
18140
|
if (!rulesFile)
|
|
18083
18141
|
continue;
|
|
18084
|
-
const
|
|
18142
|
+
const year = fileResult.transactionYear;
|
|
18143
|
+
if (!year) {
|
|
18144
|
+
return JSON.stringify({
|
|
18145
|
+
success: false,
|
|
18146
|
+
files: fileResults,
|
|
18147
|
+
summary: {
|
|
18148
|
+
filesProcessed: csvFiles.length,
|
|
18149
|
+
filesWithErrors: 1,
|
|
18150
|
+
filesWithoutRules,
|
|
18151
|
+
totalTransactions,
|
|
18152
|
+
matched: totalMatched,
|
|
18153
|
+
unknown: totalUnknown
|
|
18154
|
+
},
|
|
18155
|
+
error: `No transactions found in ${fileResult.csv}`
|
|
18156
|
+
});
|
|
18157
|
+
}
|
|
18158
|
+
let yearJournalPath;
|
|
18159
|
+
try {
|
|
18160
|
+
yearJournalPath = ensureYearJournalExists(directory, year);
|
|
18161
|
+
} catch (error45) {
|
|
18162
|
+
return JSON.stringify({
|
|
18163
|
+
success: false,
|
|
18164
|
+
files: fileResults,
|
|
18165
|
+
summary: {
|
|
18166
|
+
filesProcessed: csvFiles.length,
|
|
18167
|
+
filesWithErrors: 1,
|
|
18168
|
+
filesWithoutRules,
|
|
18169
|
+
totalTransactions,
|
|
18170
|
+
matched: totalMatched,
|
|
18171
|
+
unknown: totalUnknown
|
|
18172
|
+
},
|
|
18173
|
+
error: error45 instanceof Error ? error45.message : String(error45)
|
|
18174
|
+
});
|
|
18175
|
+
}
|
|
18176
|
+
const result = await hledgerExecutor([
|
|
18177
|
+
"import",
|
|
18178
|
+
"-f",
|
|
18179
|
+
yearJournalPath,
|
|
18180
|
+
csvFile,
|
|
18181
|
+
"--rules-file",
|
|
18182
|
+
rulesFile
|
|
18183
|
+
]);
|
|
18085
18184
|
if (result.exitCode !== 0) {
|
|
18086
18185
|
return JSON.stringify({
|
|
18087
18186
|
success: false,
|
|
@@ -18094,7 +18193,7 @@ async function importStatementsCore(directory, agent, options, configLoader = lo
|
|
|
18094
18193
|
matched: totalMatched,
|
|
18095
18194
|
unknown: totalUnknown
|
|
18096
18195
|
},
|
|
18097
|
-
error: `Import failed for ${
|
|
18196
|
+
error: `Import failed for ${fileResult.csv}: ${result.stderr.trim()}`
|
|
18098
18197
|
});
|
|
18099
18198
|
}
|
|
18100
18199
|
importedFiles.push(csvFile);
|
|
@@ -5,6 +5,20 @@ The `import-statements` tool imports classified CSV bank statements into hledger
|
|
|
5
5
|
- **Check mode** (`checkOnly: true`, default): Validates transactions and reports any that cannot be categorized
|
|
6
6
|
- **Import mode** (`checkOnly: false`): Imports validated transactions and moves processed files to the done directory
|
|
7
7
|
|
|
8
|
+
## Year-Based Journal Routing
|
|
9
|
+
|
|
10
|
+
Transactions are automatically routed to year-specific journal files based on transaction dates:
|
|
11
|
+
|
|
12
|
+
- Transactions from 2025 → `ledger/2025.journal`
|
|
13
|
+
- Transactions from 2026 → `ledger/2026.journal`
|
|
14
|
+
|
|
15
|
+
**Automatic setup:**
|
|
16
|
+
|
|
17
|
+
- If the year journal doesn't exist, it is created automatically
|
|
18
|
+
- The include directive (`include ledger/YYYY.journal`) is added to `.hledger.journal` if not already present
|
|
19
|
+
|
|
20
|
+
**Constraint:** Each CSV file must contain transactions from a single year. CSVs with transactions spanning multiple years are rejected during check mode with an error message listing the years found.
|
|
21
|
+
|
|
8
22
|
## Arguments
|
|
9
23
|
|
|
10
24
|
| Argument | Type | Default | Description |
|
|
@@ -33,7 +47,8 @@ When all transactions have matching rules:
|
|
|
33
47
|
"csv": "{paths.pending}/ubs/chf/transactions-ubs-0235-90250546.0.csv",
|
|
34
48
|
"rulesFile": "{paths.rules}/ubs-0235-90250546.0.rules",
|
|
35
49
|
"transactions": 25,
|
|
36
|
-
"unknownPostings": []
|
|
50
|
+
"unknownPostings": [],
|
|
51
|
+
"transactionYear": 2026
|
|
37
52
|
}
|
|
38
53
|
],
|
|
39
54
|
"summary": {
|
|
@@ -250,3 +265,41 @@ If the config file is missing or invalid:
|
|
|
250
265
|
"hint": "Ensure config/import/providers.yaml exists with a 'rules' path configured"
|
|
251
266
|
}
|
|
252
267
|
```
|
|
268
|
+
|
|
269
|
+
### Multi-Year CSV Error
|
|
270
|
+
|
|
271
|
+
If a CSV contains transactions from multiple years:
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
{
|
|
275
|
+
"success": false,
|
|
276
|
+
"files": [
|
|
277
|
+
{
|
|
278
|
+
"csv": "{paths.pending}/ubs/chf/transactions.csv",
|
|
279
|
+
"rulesFile": "{paths.rules}/ubs.rules",
|
|
280
|
+
"totalTransactions": 10,
|
|
281
|
+
"matchedTransactions": 10,
|
|
282
|
+
"unknownPostings": [],
|
|
283
|
+
"error": "CSV contains transactions from multiple years (2025, 2026). Split the CSV by year before importing."
|
|
284
|
+
}
|
|
285
|
+
],
|
|
286
|
+
"summary": {
|
|
287
|
+
"filesProcessed": 1,
|
|
288
|
+
"filesWithErrors": 1,
|
|
289
|
+
"totalTransactions": 0,
|
|
290
|
+
"matched": 0,
|
|
291
|
+
"unknown": 0
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Missing Main Journal
|
|
297
|
+
|
|
298
|
+
If `.hledger.journal` doesn't exist when attempting import:
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"success": false,
|
|
303
|
+
"error": ".hledger.journal not found at /path/to/.hledger.journal. Create it first with appropriate includes."
|
|
304
|
+
}
|
|
305
|
+
```
|