kvitton-cli 0.4.4 → 0.4.6

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 +115 -56
  2. 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"] [--amount-currency USD]
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 \`--amount-currency\` is different from SEK, using Riksbank exchange rates for the entry date.
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. **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
6531
- 3. **Check accounts** - Review \`accounts.yaml\` for appropriate expense/revenue accounts
6532
- 4. **Determine tax code** - Based on vendor location and transaction type (see Tax Codes below)
6533
- 5. **Ask for confirmation** - Before creating, confirm with the user: vendor, amount, accounts, and tax code
6534
- 6. **Create entry** - Run \`kvitton create-entry\` with the appropriate parameters
6535
- 7. **Files moved to drafts** - The inbox item is automatically moved to \`drafts/\` with \`entry.yaml\`
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
 
@@ -9564,13 +9568,13 @@ async function uploadDocumentsForEntry(client2, storage2, absoluteDirPath, relat
9564
9568
  const { content } = await storage2.readFile(documentsYamlPath);
9565
9569
  existingDocs = parseYaml2(content) || [];
9566
9570
  } catch {}
9567
- const uploadedFiles = new Set(existingDocs.filter((d) => d.sourceIntegration === "bokio" && d.sourceId).map((d) => d.fileName));
9571
+ const linkedFiles = new Set(existingDocs.filter((d) => d.sourceIntegration === "bokio" && d.linkedToJournalEntry).map((d) => d.fileName));
9568
9572
  for (const filename of files) {
9569
9573
  const ext = path6.extname(filename).toLowerCase();
9570
9574
  if (!UPLOADABLE_EXTENSIONS.includes(ext)) {
9571
9575
  continue;
9572
9576
  }
9573
- if (uploadedFiles.has(filename)) {
9577
+ if (linkedFiles.has(filename)) {
9574
9578
  continue;
9575
9579
  }
9576
9580
  try {
@@ -9588,12 +9592,27 @@ async function uploadDocumentsForEntry(client2, storage2, absoluteDirPath, relat
9588
9592
  filename,
9589
9593
  journalEntryId
9590
9594
  });
9591
- existingDocs.push({
9592
- fileName: filename,
9593
- mimeType,
9594
- sourceIntegration: "bokio",
9595
- sourceId: upload.id
9596
- });
9595
+ const existingDoc = existingDocs.find((d) => d.fileName === filename);
9596
+ if (existingDoc) {
9597
+ const previousSourceIds = [
9598
+ ...existingDoc.previousSourceIds || [],
9599
+ ...existingDoc.sourceId ? [existingDoc.sourceId] : []
9600
+ ];
9601
+ existingDoc.sourceId = upload.id;
9602
+ existingDoc.sourceIntegration = "bokio";
9603
+ existingDoc.linkedToJournalEntry = true;
9604
+ if (previousSourceIds.length > 0) {
9605
+ existingDoc.previousSourceIds = previousSourceIds;
9606
+ }
9607
+ } else {
9608
+ existingDocs.push({
9609
+ fileName: filename,
9610
+ mimeType,
9611
+ sourceIntegration: "bokio",
9612
+ sourceId: upload.id,
9613
+ linkedToJournalEntry: true
9614
+ });
9615
+ }
9597
9616
  await storage2.writeFile(documentsYamlPath, toYaml2(existingDocs));
9598
9617
  console.log(` Uploaded file: ${filename}`);
9599
9618
  } catch (fileError) {
@@ -9796,7 +9815,7 @@ async function syncBokioInbox(cwd) {
9796
9815
  for (const upload of unprocessedUploads) {
9797
9816
  const slug = upload.description ? slugify4(upload.description) : upload.id.slice(0, 8);
9798
9817
  let dirName = `${today}-${slug}`;
9799
- const alreadyDownloaded = await isAlreadyDownloaded2(inboxDir, upload.id);
9818
+ const alreadyDownloaded = await isAlreadyDownloaded2(cwd, upload.id);
9800
9819
  if (alreadyDownloaded) {
9801
9820
  existingCount++;
9802
9821
  continue;
@@ -9811,7 +9830,7 @@ async function syncBokioInbox(cwd) {
9811
9830
  await fs12.mkdir(uploadDir, { recursive: true });
9812
9831
  const result = await client2.downloadFile(upload.id);
9813
9832
  const ext = getExtensionFromContentType(upload.contentType || result.contentType);
9814
- const filename = upload.description ? `${slugify4(upload.description)}${ext}` : `document${ext}`;
9833
+ const filename = upload.description ? `${slugify4(upload.description)}${ext}` : `${upload.id.slice(0, 8)}${ext}`;
9815
9834
  const filePath = path7.join(uploadDir, filename);
9816
9835
  const buffer = Buffer.from(result.data);
9817
9836
  await fs12.writeFile(filePath, buffer);
@@ -9861,22 +9880,37 @@ function getExtensionFromContentType(contentType) {
9861
9880
  };
9862
9881
  return map[contentType] || ".bin";
9863
9882
  }
9864
- async function isAlreadyDownloaded2(inboxDir, sourceId) {
9883
+ async function isAlreadyDownloaded2(cwd, sourceId) {
9884
+ const dirsToCheck = [
9885
+ path7.join(cwd, "inbox"),
9886
+ path7.join(cwd, "drafts")
9887
+ ];
9888
+ const journalEntriesDir = path7.join(cwd, "journal-entries");
9865
9889
  try {
9866
- const dirs = await fs12.readdir(inboxDir, { withFileTypes: true });
9867
- for (const dir of dirs) {
9868
- if (!dir.isDirectory())
9869
- continue;
9870
- const documentsPath = path7.join(inboxDir, dir.name, "documents.yaml");
9871
- try {
9872
- const content = await fs12.readFile(documentsPath, "utf-8");
9873
- const documents = parseYaml2(content);
9874
- if (documents?.some((doc) => doc.sourceId === sourceId)) {
9875
- return true;
9876
- }
9877
- } catch {}
9890
+ const fyDirs = await fs12.readdir(journalEntriesDir, { withFileTypes: true });
9891
+ for (const fyDir of fyDirs) {
9892
+ if (fyDir.isDirectory() && fyDir.name.startsWith("FY-")) {
9893
+ dirsToCheck.push(path7.join(journalEntriesDir, fyDir.name));
9894
+ }
9878
9895
  }
9879
9896
  } catch {}
9897
+ for (const baseDir of dirsToCheck) {
9898
+ try {
9899
+ const dirs = await fs12.readdir(baseDir, { withFileTypes: true });
9900
+ for (const dir of dirs) {
9901
+ if (!dir.isDirectory())
9902
+ continue;
9903
+ const documentsPath = path7.join(baseDir, dir.name, "documents.yaml");
9904
+ try {
9905
+ const content = await fs12.readFile(documentsPath, "utf-8");
9906
+ const documents = parseYaml2(content);
9907
+ if (documents?.some((doc) => doc.sourceId === sourceId || doc.previousSourceIds?.includes(sourceId))) {
9908
+ return true;
9909
+ }
9910
+ } catch {}
9911
+ }
9912
+ } catch {}
9913
+ }
9880
9914
  return false;
9881
9915
  }
9882
9916
  async function syncFortnoxInbox2(cwd) {
@@ -10095,14 +10129,15 @@ function getCachedRate(cwd, currency, date) {
10095
10129
  const key = makeCacheKey(currency, date);
10096
10130
  return cache2[key] ?? null;
10097
10131
  }
10098
- function setCachedRate(cwd, currency, date, rate, rateDate) {
10132
+ function setCachedRates(cwd, currency, rates) {
10133
+ if (rates.length === 0)
10134
+ return;
10099
10135
  const cache2 = readCache(cwd);
10100
- const key = makeCacheKey(currency, date);
10101
- cache2[key] = {
10102
- rate,
10103
- rateDate,
10104
- cachedAt: new Date().toISOString()
10105
- };
10136
+ const cachedAt = new Date().toISOString();
10137
+ for (const { date, rate, rateDate } of rates) {
10138
+ const key = makeCacheKey(currency, date);
10139
+ cache2[key] = { rate, rateDate, cachedAt };
10140
+ }
10106
10141
  writeCache(cwd, cache2);
10107
10142
  }
10108
10143
 
@@ -10110,6 +10145,18 @@ function setCachedRate(cwd, currency, date, rate, rateDate) {
10110
10145
  function getToday() {
10111
10146
  return new Date().toISOString().slice(0, 10);
10112
10147
  }
10148
+ function findClosestRate(observations, targetDate) {
10149
+ const sorted2 = [...observations].sort((a, b) => b.date.localeCompare(a.date));
10150
+ const match = sorted2.find((obs) => obs.date <= targetDate);
10151
+ if (match) {
10152
+ return { date: match.date, value: match.value };
10153
+ }
10154
+ const earliest = sorted2[sorted2.length - 1];
10155
+ if (!earliest) {
10156
+ throw new Error("No observations available");
10157
+ }
10158
+ return { date: earliest.date, value: earliest.value };
10159
+ }
10113
10160
  async function convertCurrencyCommand(options) {
10114
10161
  const cwd = process.cwd();
10115
10162
  const amount = parseFloat(options.amount);
@@ -10154,11 +10201,23 @@ async function convertCurrencyCommand(options) {
10154
10201
  rateDate = cached.rateDate;
10155
10202
  source = "CACHE";
10156
10203
  } else {
10157
- const result = await riksbank.getRate(currency, date);
10158
- rate = result.value;
10159
- rateDate = result.date;
10204
+ const startDate = new Date(date);
10205
+ startDate.setDate(startDate.getDate() - 7);
10206
+ const fromDate = startDate.toISOString().slice(0, 10);
10207
+ const observations = await riksbank.getObservations(currency, fromDate, date);
10208
+ if (observations.length === 0) {
10209
+ console.error(`Error: No rates found for ${currency} around ${date}`);
10210
+ process.exit(1);
10211
+ }
10212
+ const closest = findClosestRate(observations, date);
10213
+ rate = closest.value;
10214
+ rateDate = closest.date;
10160
10215
  source = "RIKSBANK";
10161
- setCachedRate(cwd, currency, date, rate, rateDate);
10216
+ setCachedRates(cwd, currency, observations.map((obs) => ({
10217
+ date: obs.date,
10218
+ rate: obs.value,
10219
+ rateDate: obs.date
10220
+ })));
10162
10221
  }
10163
10222
  const convertedAmount = amount * rate;
10164
10223
  console.log(`Exchange Rate: ${rate.toFixed(4)} SEK/${currency}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kvitton-cli",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "CLI for kvitton bookkeeping repositories",
5
5
  "type": "module",
6
6
  "bin": {