kvitton-cli 0.4.2 → 0.4.4
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 +10005 -5112
- package/package.json +8 -3
- package/dist/commands/company-info.js +0 -48
- package/dist/commands/convert-currency.js +0 -76
- package/dist/commands/create-entry.js +0 -198
- package/dist/commands/generate-lines.js +0 -140
- package/dist/commands/parse-pdf.js +0 -20
- package/dist/commands/sync-inbox.js +0 -186
- package/dist/commands/sync-journal.js +0 -490
- package/dist/lib/client.js +0 -10
- package/dist/lib/currency-cache.js +0 -49
- package/dist/lib/env.js +0 -37
package/package.json
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kvitton-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "CLI for kvitton bookkeeping repositories",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"kvitton": "./dist/index.js"
|
|
8
8
|
},
|
|
9
|
-
"files": [
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
10
12
|
"scripts": {
|
|
11
13
|
"build": "bun scripts/build.ts",
|
|
12
14
|
"dev": "tsc --watch",
|
|
13
15
|
"type-check": "tsc --noEmit"
|
|
14
16
|
},
|
|
15
17
|
"dependencies": {
|
|
18
|
+
"@inquirer/prompts": "^8.1.0",
|
|
16
19
|
"@supabase/supabase-js": "^2.49.1",
|
|
17
20
|
"commander": "^12.1.0",
|
|
18
21
|
"dotenv": "^16.0.0",
|
|
@@ -27,9 +30,11 @@
|
|
|
27
30
|
"@types/pdf-parse": "^1.1.5",
|
|
28
31
|
"typescript": "^5.0.0",
|
|
29
32
|
"integrations-bokio": "workspace:*",
|
|
33
|
+
"integrations-core": "workspace:*",
|
|
34
|
+
"integrations-fortnox": "workspace:*",
|
|
30
35
|
"integrations-riksbank": "workspace:*",
|
|
31
36
|
"shared": "workspace:*",
|
|
32
|
-
"
|
|
37
|
+
"accounting": "workspace:*"
|
|
33
38
|
},
|
|
34
39
|
"engines": {
|
|
35
40
|
"node": ">=18"
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import ora from "ora";
|
|
2
|
-
import { loadConfig } from "../lib/env";
|
|
3
|
-
import { createBokioClient } from "../lib/client";
|
|
4
|
-
export async function companyInfoCommand() {
|
|
5
|
-
const cwd = process.cwd();
|
|
6
|
-
// Load config
|
|
7
|
-
const spinner = ora("Loading configuration...").start();
|
|
8
|
-
let config;
|
|
9
|
-
try {
|
|
10
|
-
config = loadConfig(cwd);
|
|
11
|
-
spinner.succeed(`Provider: ${config.provider}`);
|
|
12
|
-
}
|
|
13
|
-
catch (error) {
|
|
14
|
-
spinner.fail(error instanceof Error ? error.message : "Unknown error");
|
|
15
|
-
process.exit(1);
|
|
16
|
-
}
|
|
17
|
-
// Create client
|
|
18
|
-
const client = createBokioClient(config);
|
|
19
|
-
// Fetch company info
|
|
20
|
-
const infoSpinner = ora("Fetching company information...").start();
|
|
21
|
-
try {
|
|
22
|
-
const companyInfo = await client.getCompanyInformation();
|
|
23
|
-
infoSpinner.stop();
|
|
24
|
-
console.log(`
|
|
25
|
-
Company Information
|
|
26
|
-
-------------------
|
|
27
|
-
Name: ${companyInfo.name}
|
|
28
|
-
Org Number: ${companyInfo.organizationNumber ?? "N/A"}
|
|
29
|
-
VAT Number: ${companyInfo.vatNumber ?? "N/A"}
|
|
30
|
-
Address: ${formatAddress(companyInfo.address)}
|
|
31
|
-
`);
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
infoSpinner.fail("Failed to fetch company information");
|
|
35
|
-
console.error(error instanceof Error ? error.message : "Unknown error");
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function formatAddress(address) {
|
|
40
|
-
if (!address)
|
|
41
|
-
return "N/A";
|
|
42
|
-
const parts = [
|
|
43
|
-
address.street,
|
|
44
|
-
[address.zipCode, address.city].filter(Boolean).join(" "),
|
|
45
|
-
address.country,
|
|
46
|
-
].filter(Boolean);
|
|
47
|
-
return parts.length > 0 ? parts.join(", ") : "N/A";
|
|
48
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { RiksbankClient } from "integrations-riksbank";
|
|
2
|
-
import { getCachedRate, setCachedRate } from "../lib/currency-cache";
|
|
3
|
-
function getToday() {
|
|
4
|
-
return new Date().toISOString().slice(0, 10);
|
|
5
|
-
}
|
|
6
|
-
export async function convertCurrencyCommand(options) {
|
|
7
|
-
const cwd = process.cwd();
|
|
8
|
-
const amount = parseFloat(options.amount);
|
|
9
|
-
const currency = options.currency.toUpperCase();
|
|
10
|
-
const date = options.date ?? getToday();
|
|
11
|
-
// Validate amount
|
|
12
|
-
if (Number.isNaN(amount) || amount <= 0) {
|
|
13
|
-
console.error("Error: --amount must be a positive number");
|
|
14
|
-
process.exit(1);
|
|
15
|
-
}
|
|
16
|
-
// Validate date format
|
|
17
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
18
|
-
console.error("Error: --date must be in YYYY-MM-DD format");
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
// Handle SEK (no conversion needed)
|
|
22
|
-
if (currency === "SEK") {
|
|
23
|
-
console.log("Currency Conversion");
|
|
24
|
-
console.log("===================");
|
|
25
|
-
console.log(`Amount: ${amount.toFixed(2)} SEK`);
|
|
26
|
-
console.log(`Date: ${date}`);
|
|
27
|
-
console.log("");
|
|
28
|
-
console.log("No conversion needed (already SEK)");
|
|
29
|
-
console.log("");
|
|
30
|
-
console.log(`Result: ${amount.toFixed(2)} SEK`);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
// Initialize Riksbank client
|
|
34
|
-
const riksbank = new RiksbankClient();
|
|
35
|
-
// Validate currency is supported
|
|
36
|
-
if (!riksbank.isSupportedCurrency(currency)) {
|
|
37
|
-
console.error(`Error: Unsupported currency: ${currency}`);
|
|
38
|
-
console.error(`Supported currencies: ${riksbank.getSupportedCurrencies().join(", ")}`);
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
console.log("Currency Conversion");
|
|
42
|
-
console.log("===================");
|
|
43
|
-
console.log(`Amount: ${amount.toFixed(2)} ${currency}`);
|
|
44
|
-
console.log(`Date: ${date}`);
|
|
45
|
-
console.log("");
|
|
46
|
-
// Check cache first
|
|
47
|
-
const cached = getCachedRate(cwd, currency, date);
|
|
48
|
-
let rate;
|
|
49
|
-
let rateDate;
|
|
50
|
-
let source;
|
|
51
|
-
if (cached) {
|
|
52
|
-
rate = cached.rate;
|
|
53
|
-
rateDate = cached.rateDate;
|
|
54
|
-
source = "CACHE";
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
// Fetch from Riksbank
|
|
58
|
-
const result = await riksbank.getRate(currency, date);
|
|
59
|
-
rate = result.value;
|
|
60
|
-
rateDate = result.date;
|
|
61
|
-
source = "RIKSBANK";
|
|
62
|
-
// Save to cache
|
|
63
|
-
setCachedRate(cwd, currency, date, rate, rateDate);
|
|
64
|
-
}
|
|
65
|
-
const convertedAmount = amount * rate;
|
|
66
|
-
console.log(`Exchange Rate: ${rate.toFixed(4)} SEK/${currency}`);
|
|
67
|
-
if (rateDate !== date) {
|
|
68
|
-
console.log(`Rate Date: ${rateDate} (closest available)`);
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
console.log(`Rate Date: ${rateDate}`);
|
|
72
|
-
}
|
|
73
|
-
console.log(`Source: ${source}`);
|
|
74
|
-
console.log("");
|
|
75
|
-
console.log(`Converted Amount: ${convertedAmount.toFixed(2)} SEK`);
|
|
76
|
-
}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Create a complete journal entry from an inbox document
|
|
3
|
-
* Reads document.yaml, generates lines, and creates entry.yaml
|
|
4
|
-
*/
|
|
5
|
-
import { RiksbankClient } from "integrations-riksbank";
|
|
6
|
-
import { resolveTaxCode, createJournalLinesFromTaxCode, getAllTaxCodes, } from "shared/dist/accounting";
|
|
7
|
-
import { readAccountsYaml, getAccountName, toYaml } from "shared/dist/yaml";
|
|
8
|
-
import * as path from "node:path";
|
|
9
|
-
import * as fs from "node:fs/promises";
|
|
10
|
-
import * as yaml from "yaml";
|
|
11
|
-
export async function createEntryCommand(options) {
|
|
12
|
-
// Handle --list-tax-codes flag
|
|
13
|
-
if (options.listTaxCodes) {
|
|
14
|
-
console.log("Available Tax Codes");
|
|
15
|
-
console.log("===================");
|
|
16
|
-
for (const tc of getAllTaxCodes()) {
|
|
17
|
-
console.log(` ${tc.code}`);
|
|
18
|
-
console.log(` ${tc.description}`);
|
|
19
|
-
console.log(` Direction: ${tc.direction} | Territory: ${tc.territory} | Rate: ${tc.ratePercent}%`);
|
|
20
|
-
console.log("");
|
|
21
|
-
}
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
const { inboxDir, taxCode: taxCodeStr, baseAccount: baseAccountCode, balancingAccount: balancingAccountCode, series: seriesInput, entryNumber: entryNumberStr, memo, dryRun = false, } = options;
|
|
25
|
-
const series = seriesInput.toUpperCase();
|
|
26
|
-
const entryNumber = Number.parseInt(entryNumberStr, 10);
|
|
27
|
-
if (Number.isNaN(entryNumber) || entryNumber < 1) {
|
|
28
|
-
console.error("Error: --entry-number must be a positive integer");
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
// File paths
|
|
32
|
-
const documentPath = path.join(inboxDir, "document.yaml");
|
|
33
|
-
const accountsPath = path.join(path.dirname(inboxDir), "accounts.yaml");
|
|
34
|
-
// Read document.yaml
|
|
35
|
-
let documentContent;
|
|
36
|
-
try {
|
|
37
|
-
documentContent = await fs.readFile(documentPath, "utf-8");
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
console.error(`Error: document.yaml not found at ${documentPath}`);
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
const document = yaml.parse(documentContent);
|
|
44
|
-
// Extract data from document
|
|
45
|
-
const description = document.description || "Unknown vendor";
|
|
46
|
-
const documentDate = document.documentDate;
|
|
47
|
-
const totalAmount = document.totalAmount;
|
|
48
|
-
const subTotal = document.subTotal;
|
|
49
|
-
if (!documentDate) {
|
|
50
|
-
console.error("Error: documentDate not found in document.yaml");
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
// Determine base amount - use subTotal (net) if available, otherwise totalAmount
|
|
54
|
-
const amountSource = subTotal || totalAmount;
|
|
55
|
-
if (!amountSource) {
|
|
56
|
-
console.error("Error: No amount found in document.yaml (need totalAmount or subTotal)");
|
|
57
|
-
process.exit(1);
|
|
58
|
-
}
|
|
59
|
-
const baseAmount = Number.parseFloat(amountSource.amount);
|
|
60
|
-
const currency = amountSource.currency || "SEK";
|
|
61
|
-
if (Number.isNaN(baseAmount) || baseAmount <= 0) {
|
|
62
|
-
console.error("Error: Invalid amount in document.yaml");
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
console.log("Creating Journal Entry");
|
|
66
|
-
console.log("======================");
|
|
67
|
-
console.log(`Document: ${document.fileName}`);
|
|
68
|
-
console.log(`Vendor: ${description}`);
|
|
69
|
-
console.log(`Date: ${documentDate}`);
|
|
70
|
-
console.log(`Amount: ${baseAmount.toFixed(2)} ${currency}`);
|
|
71
|
-
console.log("");
|
|
72
|
-
// Resolve tax code
|
|
73
|
-
const taxCode = resolveTaxCode(taxCodeStr);
|
|
74
|
-
if (!taxCode) {
|
|
75
|
-
console.error(`Error: Invalid tax code: ${taxCodeStr}`);
|
|
76
|
-
console.error("\nRun with --list-tax-codes to see available codes");
|
|
77
|
-
process.exit(1);
|
|
78
|
-
}
|
|
79
|
-
console.log(`Tax Code: ${taxCode.code}`);
|
|
80
|
-
console.log(` ${taxCode.description}`);
|
|
81
|
-
console.log(` VAT Rate: ${taxCode.ratePercent}%`);
|
|
82
|
-
console.log("");
|
|
83
|
-
// Currency conversion if needed
|
|
84
|
-
let amountInSEK = baseAmount;
|
|
85
|
-
if (currency !== "SEK") {
|
|
86
|
-
console.log(`Converting ${baseAmount.toFixed(2)} ${currency} to SEK...`);
|
|
87
|
-
const riksbank = new RiksbankClient();
|
|
88
|
-
if (!riksbank.isSupportedCurrency(currency)) {
|
|
89
|
-
console.error(`Error: Unsupported currency: ${currency}`);
|
|
90
|
-
process.exit(1);
|
|
91
|
-
}
|
|
92
|
-
const rate = await riksbank.getRate(currency, documentDate);
|
|
93
|
-
amountInSEK = baseAmount * rate.value;
|
|
94
|
-
console.log(` Rate: ${rate.value.toFixed(4)} SEK/${currency} (${rate.date})`);
|
|
95
|
-
console.log(` Converted: ${amountInSEK.toFixed(2)} SEK`);
|
|
96
|
-
console.log("");
|
|
97
|
-
}
|
|
98
|
-
// Load accounts for memo lookup
|
|
99
|
-
let accounts = [];
|
|
100
|
-
try {
|
|
101
|
-
accounts = await readAccountsYaml(accountsPath);
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
// accounts.yaml not found, continue without account names
|
|
105
|
-
}
|
|
106
|
-
// Generate journal lines
|
|
107
|
-
const result = createJournalLinesFromTaxCode({
|
|
108
|
-
taxCode,
|
|
109
|
-
baseAmount: amountInSEK,
|
|
110
|
-
baseAccountCode,
|
|
111
|
-
balancingAccountCode,
|
|
112
|
-
memo,
|
|
113
|
-
startingLineNumber: 1,
|
|
114
|
-
});
|
|
115
|
-
// Build entry.yaml content
|
|
116
|
-
const entryLines = result.lines.map((line) => ({
|
|
117
|
-
lineNumber: line.line_number,
|
|
118
|
-
account: line.ledger_account_code,
|
|
119
|
-
debit: {
|
|
120
|
-
amount: line.debit ?? 0,
|
|
121
|
-
currency: "SEK",
|
|
122
|
-
},
|
|
123
|
-
credit: {
|
|
124
|
-
amount: line.credit ?? 0,
|
|
125
|
-
currency: "SEK",
|
|
126
|
-
},
|
|
127
|
-
memo: line.memo ||
|
|
128
|
-
getAccountName(line.ledger_account_code, accounts) ||
|
|
129
|
-
undefined,
|
|
130
|
-
}));
|
|
131
|
-
const totalDebit = result.lines.reduce((sum, l) => sum + (l.debit ?? 0), 0);
|
|
132
|
-
const totalCredit = result.lines.reduce((sum, l) => sum + (l.credit ?? 0), 0);
|
|
133
|
-
const entry = {
|
|
134
|
-
series,
|
|
135
|
-
entryNumber,
|
|
136
|
-
entryDate: documentDate,
|
|
137
|
-
description,
|
|
138
|
-
status: "DRAFT",
|
|
139
|
-
currency: "SEK",
|
|
140
|
-
totalDebit: {
|
|
141
|
-
amount: Math.round(totalDebit * 100) / 100,
|
|
142
|
-
currency: "SEK",
|
|
143
|
-
},
|
|
144
|
-
totalCredit: {
|
|
145
|
-
amount: Math.round(totalCredit * 100) / 100,
|
|
146
|
-
currency: "SEK",
|
|
147
|
-
},
|
|
148
|
-
lines: entryLines,
|
|
149
|
-
};
|
|
150
|
-
// Output the entry
|
|
151
|
-
console.log("Generated Entry:");
|
|
152
|
-
console.log("-".repeat(40));
|
|
153
|
-
const entryYaml = toYaml(entry);
|
|
154
|
-
console.log(entryYaml);
|
|
155
|
-
console.log("-".repeat(40));
|
|
156
|
-
console.log(`Total Debit: ${totalDebit.toFixed(2)} SEK`);
|
|
157
|
-
console.log(`Total Credit: ${totalCredit.toFixed(2)} SEK`);
|
|
158
|
-
console.log(`Balanced: ${Math.abs(totalDebit - totalCredit) < 0.01 ? "✓" : "✗"}`);
|
|
159
|
-
// Calculate drafts directory name: {date} - {title}
|
|
160
|
-
const titleSlug = slugify(description, 30);
|
|
161
|
-
const draftsDirName = `${documentDate} - ${titleSlug}`;
|
|
162
|
-
const draftsDir = path.join(path.dirname(inboxDir), "drafts", draftsDirName);
|
|
163
|
-
// Write entry.yaml and move to drafts unless dry-run
|
|
164
|
-
if (dryRun) {
|
|
165
|
-
console.log("\n[Dry run - files not moved]");
|
|
166
|
-
console.log(`Would move to: ${draftsDir}`);
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
// Create drafts directory
|
|
170
|
-
await fs.mkdir(draftsDir, { recursive: true });
|
|
171
|
-
// Copy all files from inbox to drafts
|
|
172
|
-
const inboxFiles = await fs.readdir(inboxDir);
|
|
173
|
-
for (const file of inboxFiles) {
|
|
174
|
-
const srcPath = path.join(inboxDir, file);
|
|
175
|
-
const destPath = path.join(draftsDir, file);
|
|
176
|
-
await fs.copyFile(srcPath, destPath);
|
|
177
|
-
}
|
|
178
|
-
// Write entry.yaml to drafts
|
|
179
|
-
const entryPath = path.join(draftsDir, "entry.yaml");
|
|
180
|
-
await fs.writeFile(entryPath, entryYaml, "utf-8");
|
|
181
|
-
// Delete inbox directory
|
|
182
|
-
await fs.rm(inboxDir, { recursive: true });
|
|
183
|
-
console.log(`\n✓ Moved to ${draftsDir}`);
|
|
184
|
-
console.log(`✓ Entry written to ${entryPath}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Convert text to a URL-friendly slug
|
|
189
|
-
*/
|
|
190
|
-
function slugify(text, maxLength = 30) {
|
|
191
|
-
return text
|
|
192
|
-
.toLowerCase()
|
|
193
|
-
.slice(0, maxLength)
|
|
194
|
-
.replace(/[^a-z0-9\s-]/g, "")
|
|
195
|
-
.replace(/\s+/g, "-")
|
|
196
|
-
.replace(/-+/g, "-")
|
|
197
|
-
.replace(/^-|-$/g, "");
|
|
198
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generate journal entry lines from tax codes
|
|
3
|
-
* Appends lines to an existing entry.yaml
|
|
4
|
-
*/
|
|
5
|
-
import { createClient } from "@supabase/supabase-js";
|
|
6
|
-
import { resolveTaxCode, createJournalLinesFromTaxCode, convertToSEK, calculateLineTotals, getAllTaxCodes, } from "shared/dist/accounting";
|
|
7
|
-
import { readEntryYaml, readDocumentYaml, appendLinesToEntry, readAccountsYaml, validateAccountExists, getAccountName, } from "shared/dist/yaml";
|
|
8
|
-
import * as path from "node:path";
|
|
9
|
-
import * as fs from "node:fs/promises";
|
|
10
|
-
export async function generateLinesCommand(options) {
|
|
11
|
-
const { inboxDir, taxCode: taxCodeStr, baseAmount: baseAmountStr, baseAccount: baseAccountCode, balancingAccount: balancingAccountCode, memo, amountCurrency = "SEK", } = options;
|
|
12
|
-
const baseAmount = Number.parseFloat(baseAmountStr);
|
|
13
|
-
// Validate base amount
|
|
14
|
-
if (Number.isNaN(baseAmount) || baseAmount <= 0) {
|
|
15
|
-
console.error("Error: --base-amount must be a positive number");
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
// File paths
|
|
19
|
-
const entryPath = path.join(inboxDir, "entry.yaml");
|
|
20
|
-
const documentPath = path.join(inboxDir, "document.yaml");
|
|
21
|
-
const accountsPath = path.join(path.dirname(path.dirname(inboxDir)), "accounts.yaml");
|
|
22
|
-
// Check if files exist
|
|
23
|
-
try {
|
|
24
|
-
await fs.access(entryPath);
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
console.error(`Error: entry.yaml not found at ${entryPath}`);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
try {
|
|
31
|
-
await fs.access(documentPath);
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
console.error(`Error: document.yaml not found at ${documentPath}`);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
// Read entry and document
|
|
38
|
-
console.log("Reading entry and document files...");
|
|
39
|
-
const entry = await readEntryYaml(entryPath);
|
|
40
|
-
// Document is read to validate it exists but not directly used
|
|
41
|
-
await readDocumentYaml(documentPath);
|
|
42
|
-
console.log(`Entry date: ${entry.entryDate}`);
|
|
43
|
-
console.log(`Description: ${entry.description}`);
|
|
44
|
-
// Resolve tax code
|
|
45
|
-
console.log(`\nResolving tax code: ${taxCodeStr}`);
|
|
46
|
-
const taxCode = resolveTaxCode(taxCodeStr);
|
|
47
|
-
if (!taxCode) {
|
|
48
|
-
console.error(`Error: Invalid tax code: ${taxCodeStr}`);
|
|
49
|
-
console.error("\nAvailable tax codes:");
|
|
50
|
-
for (const tc of getAllTaxCodes()) {
|
|
51
|
-
console.error(` ${tc.code} - ${tc.description}`);
|
|
52
|
-
}
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
console.log(`✓ Tax code resolved: ${taxCode.description}`);
|
|
56
|
-
console.log(` Direction: ${taxCode.direction}`);
|
|
57
|
-
console.log(` Territory: ${taxCode.territory}`);
|
|
58
|
-
console.log(` VAT Rate: ${taxCode.ratePercent}%`);
|
|
59
|
-
// Currency conversion if needed
|
|
60
|
-
let amountInSEK = baseAmount;
|
|
61
|
-
if (amountCurrency !== "SEK") {
|
|
62
|
-
console.log(`\nCurrency conversion: ${baseAmount} ${amountCurrency} → SEK`);
|
|
63
|
-
// Initialize Supabase client
|
|
64
|
-
const supabaseUrl = process.env.SUPABASE_URL;
|
|
65
|
-
const supabaseKey = process.env.SUPABASE_SECRET_KEY;
|
|
66
|
-
if (!supabaseUrl || !supabaseKey) {
|
|
67
|
-
console.error("Error: Missing Supabase credentials");
|
|
68
|
-
console.error("Ensure SUPABASE_URL and SUPABASE_SECRET_KEY are set in environment");
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
const supabase = createClient(supabaseUrl, supabaseKey);
|
|
72
|
-
const converted = await convertToSEK(supabase, baseAmount, amountCurrency, entry.entryDate);
|
|
73
|
-
if (converted === null) {
|
|
74
|
-
console.error(`Error: No currency rate found for ${amountCurrency} on ${entry.entryDate}`);
|
|
75
|
-
console.error("Please sync currency rates from Riksbank or provide amount already in SEK");
|
|
76
|
-
process.exit(1);
|
|
77
|
-
}
|
|
78
|
-
amountInSEK = converted;
|
|
79
|
-
console.log(`✓ Converted to SEK: ${amountInSEK.toFixed(2)}`);
|
|
80
|
-
}
|
|
81
|
-
// Validate accounts exist (if accounts.yaml is available)
|
|
82
|
-
let accounts = [];
|
|
83
|
-
try {
|
|
84
|
-
await fs.access(accountsPath);
|
|
85
|
-
accounts = await readAccountsYaml(accountsPath);
|
|
86
|
-
console.log(`\nValidating accounts against ${accountsPath}`);
|
|
87
|
-
if (!validateAccountExists(baseAccountCode, accounts)) {
|
|
88
|
-
console.error(`Error: Base account ${baseAccountCode} not found`);
|
|
89
|
-
console.error("Run export-accounts first to create accounts.yaml");
|
|
90
|
-
process.exit(1);
|
|
91
|
-
}
|
|
92
|
-
if (!validateAccountExists(balancingAccountCode, accounts)) {
|
|
93
|
-
console.error(`Error: Balancing account ${balancingAccountCode} not found`);
|
|
94
|
-
console.error("Run export-accounts first to create accounts.yaml");
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
console.log(`✓ Base account: ${baseAccountCode} - ${getAccountName(baseAccountCode, accounts)}`);
|
|
98
|
-
console.log(`✓ Balancing account: ${balancingAccountCode} - ${getAccountName(balancingAccountCode, accounts)}`);
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
console.warn(`\nWarning: accounts.yaml not found at ${accountsPath}, skipping account validation`);
|
|
102
|
-
}
|
|
103
|
-
// Generate lines
|
|
104
|
-
console.log("\nGenerating journal lines...");
|
|
105
|
-
const result = createJournalLinesFromTaxCode({
|
|
106
|
-
taxCode,
|
|
107
|
-
baseAmount: amountInSEK,
|
|
108
|
-
baseAccountCode,
|
|
109
|
-
balancingAccountCode,
|
|
110
|
-
memo,
|
|
111
|
-
startingLineNumber: (entry.lines?.length || 0) + 1,
|
|
112
|
-
});
|
|
113
|
-
console.log(`✓ Generated ${result.lines.length} lines:`);
|
|
114
|
-
console.log(` Net amount: ${result.totals.net.toFixed(2)} SEK`);
|
|
115
|
-
console.log(` VAT amount: ${result.totals.vat.toFixed(2)} SEK`);
|
|
116
|
-
console.log(` Gross amount: ${result.totals.gross.toFixed(2)} SEK`);
|
|
117
|
-
for (const line of result.lines) {
|
|
118
|
-
const accountName = getAccountName(line.ledger_account_code, accounts);
|
|
119
|
-
const debit = line.debit ? `${line.debit.toFixed(2)} SEK` : "-";
|
|
120
|
-
const credit = line.credit ? `${line.credit.toFixed(2)} SEK` : "-";
|
|
121
|
-
console.log(` Line ${line.line_number}: ${line.ledger_account_code} ${accountName ? `(${accountName})` : ""} | Debit: ${debit} | Credit: ${credit}`);
|
|
122
|
-
}
|
|
123
|
-
// Calculate total debits and credits across ALL lines (existing + new)
|
|
124
|
-
const allLinesForTotal = [
|
|
125
|
-
...(entry.lines || []).map((l) => ({
|
|
126
|
-
line_number: l.lineNumber,
|
|
127
|
-
ledger_account_code: l.account,
|
|
128
|
-
debit: l.debit.amount,
|
|
129
|
-
credit: l.credit.amount,
|
|
130
|
-
})),
|
|
131
|
-
...result.lines,
|
|
132
|
-
];
|
|
133
|
-
const totals = calculateLineTotals(allLinesForTotal);
|
|
134
|
-
console.log("\nUpdating entry.yaml...");
|
|
135
|
-
await appendLinesToEntry(entryPath, result.lines, totals);
|
|
136
|
-
console.log(`✓ Successfully appended lines to ${entryPath} and updated totals`);
|
|
137
|
-
console.log(` Total Debit: ${totals.totalDebit.toFixed(2)} SEK`);
|
|
138
|
-
console.log(` Total Credit: ${totals.totalCredit.toFixed(2)} SEK`);
|
|
139
|
-
console.log(` Balanced: ${totals.totalDebit === totals.totalCredit ? "✓" : "✗"}`);
|
|
140
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extract text from a PDF file
|
|
3
|
-
*/
|
|
4
|
-
import pdf from "pdf-parse";
|
|
5
|
-
import { readFile } from "node:fs/promises";
|
|
6
|
-
export async function parsePdfCommand(options) {
|
|
7
|
-
const { file: filePath } = options;
|
|
8
|
-
try {
|
|
9
|
-
const buffer = await readFile(filePath);
|
|
10
|
-
const data = await pdf(buffer);
|
|
11
|
-
console.log(data.text);
|
|
12
|
-
}
|
|
13
|
-
catch (err) {
|
|
14
|
-
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
15
|
-
console.error(`Error: File not found: ${filePath}`);
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
throw err;
|
|
19
|
-
}
|
|
20
|
-
}
|