kvitton-cli 0.4.4 → 0.4.5
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 +86 -51
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6475,16 +6475,16 @@ This is an accounting data repository for {{COMPANY_NAME}} (Swedish company), st
|
|
|
6475
6475
|
|
|
6476
6476
|
## CLI Commands
|
|
6477
6477
|
|
|
6478
|
-
Use the \`kvitton\` CLI to interact with this repository:
|
|
6478
|
+
Use the \`npx kvitton-cli\` CLI to interact with this repository:
|
|
6479
6479
|
|
|
6480
6480
|
\`\`\`bash
|
|
6481
6481
|
# Sync data from provider
|
|
6482
|
-
kvitton sync-journal # Sync journal entries from accounting provider
|
|
6483
|
-
kvitton sync-inbox # Download inbox files from accounting provider
|
|
6484
|
-
kvitton company-info # Display company information
|
|
6482
|
+
npx kvitton-cli sync-journal # Sync journal entries from accounting provider
|
|
6483
|
+
npx kvitton-cli sync-inbox # Download inbox files from accounting provider
|
|
6484
|
+
npx kvitton-cli company-info # Display company information
|
|
6485
6485
|
|
|
6486
6486
|
# Create journal entries
|
|
6487
|
-
kvitton create-entry --path <inbox-dir-or-file> \\
|
|
6487
|
+
npx kvitton-cli create-entry --path <inbox-dir-or-file> \\
|
|
6488
6488
|
--tax-code <code> --base-account <acc> --balancing-account <acc> \\
|
|
6489
6489
|
--series <A-K> --entry-number <num>
|
|
6490
6490
|
|
|
@@ -6492,27 +6492,27 @@ kvitton create-entry --path <inbox-dir-or-file> \\
|
|
|
6492
6492
|
# --description "Vendor name" --document-date 2025-01-15 --amount 1234.50
|
|
6493
6493
|
|
|
6494
6494
|
# Manage inbox
|
|
6495
|
-
kvitton discard <inbox-directory> # Remove item, mark for deletion from provider
|
|
6496
|
-
kvitton parse-pdf -f <file.pdf> # Extract text from PDF (for analysis)
|
|
6495
|
+
npx kvitton-cli discard <inbox-directory> # Remove item, mark for deletion from provider
|
|
6496
|
+
npx kvitton-cli parse-pdf -f <file.pdf> # Extract text from PDF (for analysis)
|
|
6497
6497
|
|
|
6498
6498
|
# Currency conversion
|
|
6499
|
-
kvitton convert-currency --amount <num> --currency <code> [--date YYYY-MM-DD]
|
|
6499
|
+
npx kvitton-cli convert-currency --amount <num> --currency <code> [--date YYYY-MM-DD]
|
|
6500
6500
|
# Converts foreign currency to SEK using Riksbank exchange rates
|
|
6501
6501
|
# Caches rates locally for efficiency
|
|
6502
6502
|
|
|
6503
6503
|
# Generate journal entry lines (alternative to create-entry for adding lines)
|
|
6504
|
-
kvitton generate-lines --inbox-dir <path> \\
|
|
6504
|
+
npx kvitton-cli generate-lines --inbox-dir <path> \\
|
|
6505
6505
|
--tax-code <code> --base-amount <num> --base-account <acc> \\
|
|
6506
|
-
--balancing-account <acc> [--memo "description"] [--
|
|
6506
|
+
--balancing-account <acc> [--memo "description"] [--currency USD]
|
|
6507
6507
|
|
|
6508
6508
|
# List available tax codes
|
|
6509
|
-
kvitton list-tax-codes
|
|
6509
|
+
npx kvitton-cli list-tax-codes
|
|
6510
6510
|
|
|
6511
6511
|
# Update instructions
|
|
6512
|
-
kvitton update # Update AGENTS.md to latest version
|
|
6512
|
+
npx kvitton-cli update # Update AGENTS.md to latest version
|
|
6513
6513
|
\`\`\`
|
|
6514
6514
|
|
|
6515
|
-
**Currency Conversion:** Both \`create-entry\` and \`generate-lines\` automatically convert to SEK when \`--
|
|
6515
|
+
**Currency Conversion:** Both \`create-entry\` and \`generate-lines\` automatically convert to SEK when \`--currency\` is different from SEK, using Riksbank exchange rates for the entry date.
|
|
6516
6516
|
|
|
6517
6517
|
## Workflow Overview
|
|
6518
6518
|
|
|
@@ -6526,17 +6526,21 @@ inbox/ → (create-entry) → drafts/ → (user confirms) → journal-entries/
|
|
|
6526
6526
|
|
|
6527
6527
|
To create a journal entry from an inbox document:
|
|
6528
6528
|
|
|
6529
|
-
1. **Read the document** - Use \`kvitton parse-pdf\` for PDFs to understand the content
|
|
6530
|
-
2. **
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6529
|
+
1. **Read the document** - Use \`npx kvitton-cli parse-pdf\` for PDFs to understand the content
|
|
6530
|
+
2. **Update documents.yaml** - After parsing, populate missing metadata in \`documents.yaml\`:
|
|
6531
|
+
- \`documentDate\`: Invoice/receipt date (YYYY-MM-DD)
|
|
6532
|
+
- \`description\`: Vendor or document name
|
|
6533
|
+
- \`totalAmount\`: With \`amount\` (string) and \`currency\` (e.g., EUR, SEK)
|
|
6534
|
+
3. **Look for similar entries** - Search \`journal-entries/\` and \`drafts/\` for entries from the same vendor or similar transaction types to learn which accounts and tax codes were used previously
|
|
6535
|
+
4. **Check accounts** - Review \`accounts.yaml\` for appropriate expense/revenue accounts
|
|
6536
|
+
5. **Determine tax code** - Based on vendor location and transaction type (see Tax Codes below). Explain your reasoning and describe the tax code meaning to the user
|
|
6537
|
+
6. **Ask for confirmation** - Before creating, confirm with the user: vendor, amount, accounts (with names), and tax code (with description)
|
|
6538
|
+
7. **Create entry** - Run \`npx kvitton-cli create-entry\` with the appropriate parameters
|
|
6539
|
+
8. **Files moved to drafts** - The inbox item is automatically moved to \`drafts/\` with \`entry.yaml\`
|
|
6536
6540
|
|
|
6537
6541
|
To discard unwanted inbox items:
|
|
6538
6542
|
\`\`\`bash
|
|
6539
|
-
kvitton discard inbox/2025-01-15-spam-document
|
|
6543
|
+
npx kvitton-cli discard inbox/2025-01-15-spam-document
|
|
6540
6544
|
\`\`\`
|
|
6541
6545
|
This removes the directory and marks the item for deletion from the provider on next sync.
|
|
6542
6546
|
|
|
@@ -6554,7 +6558,7 @@ When the user confirms draft entries are ready to post:
|
|
|
6554
6558
|
\`\`\`
|
|
6555
6559
|
Format: \`{SERIES}-{NUM}-{DATE}-{DESC}/\`
|
|
6556
6560
|
3. **Update entry.yaml** - Ensure \`entryNumber\` matches the directory and \`status\` is set appropriately
|
|
6557
|
-
4. **Sync to provider** - Run \`kvitton sync-journal\` to post the entries to {{PROVIDER}}
|
|
6561
|
+
4. **Sync to provider** - Run \`npx kvitton-cli sync-journal\` to post the entries to {{PROVIDER}}
|
|
6558
6562
|
|
|
6559
6563
|
## Tax Codes
|
|
6560
6564
|
|
|
@@ -9796,7 +9800,7 @@ async function syncBokioInbox(cwd) {
|
|
|
9796
9800
|
for (const upload of unprocessedUploads) {
|
|
9797
9801
|
const slug = upload.description ? slugify4(upload.description) : upload.id.slice(0, 8);
|
|
9798
9802
|
let dirName = `${today}-${slug}`;
|
|
9799
|
-
const alreadyDownloaded = await isAlreadyDownloaded2(
|
|
9803
|
+
const alreadyDownloaded = await isAlreadyDownloaded2(cwd, upload.id);
|
|
9800
9804
|
if (alreadyDownloaded) {
|
|
9801
9805
|
existingCount++;
|
|
9802
9806
|
continue;
|
|
@@ -9811,7 +9815,7 @@ async function syncBokioInbox(cwd) {
|
|
|
9811
9815
|
await fs12.mkdir(uploadDir, { recursive: true });
|
|
9812
9816
|
const result = await client2.downloadFile(upload.id);
|
|
9813
9817
|
const ext = getExtensionFromContentType(upload.contentType || result.contentType);
|
|
9814
|
-
const filename = upload.description ? `${slugify4(upload.description)}${ext}` :
|
|
9818
|
+
const filename = upload.description ? `${slugify4(upload.description)}${ext}` : `${upload.id.slice(0, 8)}${ext}`;
|
|
9815
9819
|
const filePath = path7.join(uploadDir, filename);
|
|
9816
9820
|
const buffer = Buffer.from(result.data);
|
|
9817
9821
|
await fs12.writeFile(filePath, buffer);
|
|
@@ -9861,22 +9865,28 @@ function getExtensionFromContentType(contentType) {
|
|
|
9861
9865
|
};
|
|
9862
9866
|
return map[contentType] || ".bin";
|
|
9863
9867
|
}
|
|
9864
|
-
async function isAlreadyDownloaded2(
|
|
9865
|
-
|
|
9866
|
-
|
|
9867
|
-
|
|
9868
|
-
|
|
9869
|
-
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
|
|
9873
|
-
|
|
9874
|
-
|
|
9875
|
-
|
|
9876
|
-
|
|
9877
|
-
|
|
9878
|
-
|
|
9879
|
-
|
|
9868
|
+
async function isAlreadyDownloaded2(cwd, sourceId) {
|
|
9869
|
+
const dirsToCheck = [
|
|
9870
|
+
path7.join(cwd, "inbox"),
|
|
9871
|
+
path7.join(cwd, "drafts")
|
|
9872
|
+
];
|
|
9873
|
+
for (const baseDir of dirsToCheck) {
|
|
9874
|
+
try {
|
|
9875
|
+
const dirs = await fs12.readdir(baseDir, { withFileTypes: true });
|
|
9876
|
+
for (const dir of dirs) {
|
|
9877
|
+
if (!dir.isDirectory())
|
|
9878
|
+
continue;
|
|
9879
|
+
const documentsPath = path7.join(baseDir, dir.name, "documents.yaml");
|
|
9880
|
+
try {
|
|
9881
|
+
const content = await fs12.readFile(documentsPath, "utf-8");
|
|
9882
|
+
const documents = parseYaml2(content);
|
|
9883
|
+
if (documents?.some((doc) => doc.sourceId === sourceId)) {
|
|
9884
|
+
return true;
|
|
9885
|
+
}
|
|
9886
|
+
} catch {}
|
|
9887
|
+
}
|
|
9888
|
+
} catch {}
|
|
9889
|
+
}
|
|
9880
9890
|
return false;
|
|
9881
9891
|
}
|
|
9882
9892
|
async function syncFortnoxInbox2(cwd) {
|
|
@@ -10095,14 +10105,15 @@ function getCachedRate(cwd, currency, date) {
|
|
|
10095
10105
|
const key = makeCacheKey(currency, date);
|
|
10096
10106
|
return cache2[key] ?? null;
|
|
10097
10107
|
}
|
|
10098
|
-
function
|
|
10108
|
+
function setCachedRates(cwd, currency, rates) {
|
|
10109
|
+
if (rates.length === 0)
|
|
10110
|
+
return;
|
|
10099
10111
|
const cache2 = readCache(cwd);
|
|
10100
|
-
const
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
rateDate,
|
|
10104
|
-
|
|
10105
|
-
};
|
|
10112
|
+
const cachedAt = new Date().toISOString();
|
|
10113
|
+
for (const { date, rate, rateDate } of rates) {
|
|
10114
|
+
const key = makeCacheKey(currency, date);
|
|
10115
|
+
cache2[key] = { rate, rateDate, cachedAt };
|
|
10116
|
+
}
|
|
10106
10117
|
writeCache(cwd, cache2);
|
|
10107
10118
|
}
|
|
10108
10119
|
|
|
@@ -10110,6 +10121,18 @@ function setCachedRate(cwd, currency, date, rate, rateDate) {
|
|
|
10110
10121
|
function getToday() {
|
|
10111
10122
|
return new Date().toISOString().slice(0, 10);
|
|
10112
10123
|
}
|
|
10124
|
+
function findClosestRate(observations, targetDate) {
|
|
10125
|
+
const sorted2 = [...observations].sort((a, b) => b.date.localeCompare(a.date));
|
|
10126
|
+
const match = sorted2.find((obs) => obs.date <= targetDate);
|
|
10127
|
+
if (match) {
|
|
10128
|
+
return { date: match.date, value: match.value };
|
|
10129
|
+
}
|
|
10130
|
+
const earliest = sorted2[sorted2.length - 1];
|
|
10131
|
+
if (!earliest) {
|
|
10132
|
+
throw new Error("No observations available");
|
|
10133
|
+
}
|
|
10134
|
+
return { date: earliest.date, value: earliest.value };
|
|
10135
|
+
}
|
|
10113
10136
|
async function convertCurrencyCommand(options) {
|
|
10114
10137
|
const cwd = process.cwd();
|
|
10115
10138
|
const amount = parseFloat(options.amount);
|
|
@@ -10154,11 +10177,23 @@ async function convertCurrencyCommand(options) {
|
|
|
10154
10177
|
rateDate = cached.rateDate;
|
|
10155
10178
|
source = "CACHE";
|
|
10156
10179
|
} else {
|
|
10157
|
-
const
|
|
10158
|
-
|
|
10159
|
-
|
|
10180
|
+
const startDate = new Date(date);
|
|
10181
|
+
startDate.setDate(startDate.getDate() - 7);
|
|
10182
|
+
const fromDate = startDate.toISOString().slice(0, 10);
|
|
10183
|
+
const observations = await riksbank.getObservations(currency, fromDate, date);
|
|
10184
|
+
if (observations.length === 0) {
|
|
10185
|
+
console.error(`Error: No rates found for ${currency} around ${date}`);
|
|
10186
|
+
process.exit(1);
|
|
10187
|
+
}
|
|
10188
|
+
const closest = findClosestRate(observations, date);
|
|
10189
|
+
rate = closest.value;
|
|
10190
|
+
rateDate = closest.date;
|
|
10160
10191
|
source = "RIKSBANK";
|
|
10161
|
-
|
|
10192
|
+
setCachedRates(cwd, currency, observations.map((obs) => ({
|
|
10193
|
+
date: obs.date,
|
|
10194
|
+
rate: obs.value,
|
|
10195
|
+
rateDate: obs.date
|
|
10196
|
+
})));
|
|
10162
10197
|
}
|
|
10163
10198
|
const convertedAmount = amount * rate;
|
|
10164
10199
|
console.log(`Exchange Rate: ${rate.toFixed(4)} SEK/${currency}`);
|