noxctl 0.1.0 → 0.2.0
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/README.md +186 -29
- package/dist/auth.d.ts +15 -7
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +123 -22
- package/dist/auth.js.map +1 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +948 -16
- package/dist/cli.js.map +1 -1
- package/dist/config-paths.d.ts +2 -0
- package/dist/config-paths.d.ts.map +1 -0
- package/dist/config-paths.js +7 -0
- package/dist/config-paths.js.map +1 -0
- package/dist/credentials-store.d.ts +13 -3
- package/dist/credentials-store.d.ts.map +1 -1
- package/dist/credentials-store.js +154 -48
- package/dist/credentials-store.js.map +1 -1
- package/dist/fortnox-client.d.ts.map +1 -1
- package/dist/fortnox-client.js +14 -2
- package/dist/fortnox-client.js.map +1 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +86 -2
- package/dist/index.js.map +1 -1
- package/dist/operations/costcenters.d.ts +20 -0
- package/dist/operations/costcenters.d.ts.map +1 -0
- package/dist/operations/costcenters.js +38 -0
- package/dist/operations/costcenters.js.map +1 -0
- package/dist/operations/invoice-payments.d.ts +21 -0
- package/dist/operations/invoice-payments.d.ts.map +1 -0
- package/dist/operations/invoice-payments.js +40 -0
- package/dist/operations/invoice-payments.js.map +1 -0
- package/dist/operations/offers.d.ts +25 -0
- package/dist/operations/offers.d.ts.map +1 -0
- package/dist/operations/offers.js +48 -0
- package/dist/operations/offers.js.map +1 -0
- package/dist/operations/orders.d.ts +24 -0
- package/dist/operations/orders.d.ts.map +1 -0
- package/dist/operations/orders.js +44 -0
- package/dist/operations/orders.js.map +1 -0
- package/dist/operations/pricelists.d.ts +36 -0
- package/dist/operations/pricelists.d.ts.map +1 -0
- package/dist/operations/pricelists.js +52 -0
- package/dist/operations/pricelists.js.map +1 -0
- package/dist/operations/projects.d.ts +19 -0
- package/dist/operations/projects.d.ts.map +1 -0
- package/dist/operations/projects.js +33 -0
- package/dist/operations/projects.js.map +1 -0
- package/dist/operations/supplier-invoice-payments.d.ts +20 -0
- package/dist/operations/supplier-invoice-payments.d.ts.map +1 -0
- package/dist/operations/supplier-invoice-payments.js +34 -0
- package/dist/operations/supplier-invoice-payments.js.map +1 -0
- package/dist/operations/taxreductions.d.ts +19 -0
- package/dist/operations/taxreductions.d.ts.map +1 -0
- package/dist/operations/taxreductions.js +28 -0
- package/dist/operations/taxreductions.js.map +1 -0
- package/dist/profile-name.d.ts +9 -0
- package/dist/profile-name.d.ts.map +1 -0
- package/dist/profile-name.js +58 -0
- package/dist/profile-name.js.map +1 -0
- package/dist/profiles.d.ts +55 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +210 -0
- package/dist/profiles.js.map +1 -0
- package/dist/tools/costcenters.d.ts +3 -0
- package/dist/tools/costcenters.d.ts.map +1 -0
- package/dist/tools/costcenters.js +79 -0
- package/dist/tools/costcenters.js.map +1 -0
- package/dist/tools/invoice-payments.d.ts +3 -0
- package/dist/tools/invoice-payments.d.ts.map +1 -0
- package/dist/tools/invoice-payments.js +60 -0
- package/dist/tools/invoice-payments.js.map +1 -0
- package/dist/tools/offers.d.ts +3 -0
- package/dist/tools/offers.d.ts.map +1 -0
- package/dist/tools/offers.js +130 -0
- package/dist/tools/offers.js.map +1 -0
- package/dist/tools/orders.d.ts +3 -0
- package/dist/tools/orders.d.ts.map +1 -0
- package/dist/tools/orders.js +114 -0
- package/dist/tools/orders.js.map +1 -0
- package/dist/tools/pricelists.d.ts +3 -0
- package/dist/tools/pricelists.d.ts.map +1 -0
- package/dist/tools/pricelists.js +104 -0
- package/dist/tools/pricelists.js.map +1 -0
- package/dist/tools/projects.d.ts +3 -0
- package/dist/tools/projects.d.ts.map +1 -0
- package/dist/tools/projects.js +80 -0
- package/dist/tools/projects.js.map +1 -0
- package/dist/tools/supplier-invoice-payments.d.ts +3 -0
- package/dist/tools/supplier-invoice-payments.d.ts.map +1 -0
- package/dist/tools/supplier-invoice-payments.js +60 -0
- package/dist/tools/supplier-invoice-payments.js.map +1 -0
- package/dist/tools/taxreductions.d.ts +3 -0
- package/dist/tools/taxreductions.d.ts.map +1 -0
- package/dist/tools/taxreductions.js +51 -0
- package/dist/tools/taxreductions.js.map +1 -0
- package/dist/views.d.ts +20 -0
- package/dist/views.d.ts.map +1 -1
- package/dist/views.js +177 -0
- package/dist/views.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,18 +3,95 @@ import { Command, Option } from 'commander';
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
import { isJsonMode, outputList, outputDetail, outputConfirmation, formatTaxReport, formatFinancialReport, } from './formatter.js';
|
|
6
|
-
import {
|
|
6
|
+
import { readActivePointer, readActivePointerOutcome, writeActivePointer, deleteActivePointer, readProfileIndex, resolveProfile, } from './profiles.js';
|
|
7
|
+
import { setResolvedProfile } from './auth.js';
|
|
8
|
+
import { DEFAULT_PROFILE, InvalidProfileNameError } from './profile-name.js';
|
|
9
|
+
import { invoiceListColumns, invoiceDetailColumns, invoiceConfirmColumns, customerListColumns, customerDetailColumns, voucherListColumns, voucherDetailColumns, voucherRowColumns, accountListColumns, companyDetailColumns, articleListColumns, articleDetailColumns, supplierListColumns, supplierDetailColumns, supplierInvoiceListColumns, supplierInvoiceDetailColumns, supplierInvoiceConfirmColumns, invoicePaymentListColumns, invoicePaymentDetailColumns, supplierInvoicePaymentListColumns, supplierInvoicePaymentDetailColumns, offerListColumns, offerDetailColumns, orderListColumns, orderDetailColumns, projectListColumns, projectDetailColumns, costCenterListColumns, costCenterDetailColumns, taxReductionListColumns, taxReductionDetailColumns, priceListListColumns, priceListDetailColumns, priceListColumns, priceDetailColumns, } from './views.js';
|
|
7
10
|
const program = new Command();
|
|
8
11
|
program
|
|
9
12
|
.name('noxctl')
|
|
10
13
|
.description('CLI and MCP server for Fortnox accounting')
|
|
11
|
-
.version('0.
|
|
14
|
+
.version('0.2.0')
|
|
12
15
|
.addOption(new Option('-o, --output <format>', 'Output format')
|
|
13
16
|
.choices(['json', 'table'])
|
|
14
|
-
.default(undefined))
|
|
17
|
+
.default(undefined))
|
|
18
|
+
.option('--profile <name>', 'Profile to operate on (overrides NOXCTL_PROFILE and active pointer)');
|
|
15
19
|
function json() {
|
|
16
20
|
return isJsonMode(program.opts());
|
|
17
21
|
}
|
|
22
|
+
let resolvedProfileInfo = { name: DEFAULT_PROFILE, source: 'default' };
|
|
23
|
+
export function getResolvedProfileInfo() {
|
|
24
|
+
return resolvedProfileInfo;
|
|
25
|
+
}
|
|
26
|
+
// Commands that don't need (and shouldn't require) a resolved profile.
|
|
27
|
+
const PROFILE_RESOLUTION_SKIP = new Set(['help']);
|
|
28
|
+
program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
29
|
+
const name = actionCommand.name();
|
|
30
|
+
if (PROFILE_RESOLUTION_SKIP.has(name))
|
|
31
|
+
return;
|
|
32
|
+
const flag = program.opts().profile ?? undefined;
|
|
33
|
+
const env = process.env['NOXCTL_PROFILE'] ?? undefined;
|
|
34
|
+
// Typed pointer resolution so we can distinguish "no pointer" (silent
|
|
35
|
+
// default) from "pointer unreadable/corrupt" (must surface). 2s bound
|
|
36
|
+
// catches wedged filesystems; the AbortSignal cancels the underlying read.
|
|
37
|
+
const outcome = await readActivePointerOutcome({ timeoutMs: 2000 });
|
|
38
|
+
let pointer = null;
|
|
39
|
+
if (outcome.kind === 'valid') {
|
|
40
|
+
pointer = outcome.name;
|
|
41
|
+
}
|
|
42
|
+
else if (outcome.kind !== 'missing') {
|
|
43
|
+
const desc = describePointerFault(outcome);
|
|
44
|
+
// Only fail closed when the pointer is the winning source (no flag/env)
|
|
45
|
+
// AND we're about to start the MCP server. For any other command, warn
|
|
46
|
+
// loudly but continue so `doctor` / `profile use` can repair the state.
|
|
47
|
+
const wouldRelyOnPointer = !flag && !env;
|
|
48
|
+
if (wouldRelyOnPointer && name === 'serve') {
|
|
49
|
+
console.error(`Active profile pointer ${desc}. Refusing to start MCP server with ambiguous profile. Run \`noxctl doctor\` or set NOXCTL_PROFILE explicitly.`);
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
process.stderr.write(`[warning: active-profile pointer ${desc}; ignoring]\n`);
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
resolvedProfileInfo = resolveProfile({ flag, env, pointer });
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (err instanceof InvalidProfileNameError) {
|
|
59
|
+
console.error(err.message);
|
|
60
|
+
process.exit(2);
|
|
61
|
+
}
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
setResolvedProfile(resolvedProfileInfo.name);
|
|
65
|
+
// Banner ownership: MCP `serve` prints its own banner in bindStartupProfile
|
|
66
|
+
// so host logs (Claude Desktop) always see it. Suppress here to avoid a
|
|
67
|
+
// duplicate on TTY invocations like `noxctl --profile X serve`.
|
|
68
|
+
if (resolvedProfileInfo.name.toLowerCase() !== DEFAULT_PROFILE &&
|
|
69
|
+
process.stderr.isTTY &&
|
|
70
|
+
name !== 'current' &&
|
|
71
|
+
name !== 'serve') {
|
|
72
|
+
process.stderr.write(`[profile: ${resolvedProfileInfo.name}]\n`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
function describePointerFault(outcome) {
|
|
76
|
+
switch (outcome.kind) {
|
|
77
|
+
case 'invalid-content':
|
|
78
|
+
return `contains an invalid profile name: "${outcome.raw}"`;
|
|
79
|
+
case 'read-error':
|
|
80
|
+
return `could not be read (${outcome.error.message})`;
|
|
81
|
+
case 'timeout':
|
|
82
|
+
return 'read timed out';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function fetchCompanyHint() {
|
|
86
|
+
try {
|
|
87
|
+
const { loadCredentials } = await import('./auth.js');
|
|
88
|
+
const creds = await loadCredentials();
|
|
89
|
+
return creds?.company_name;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
18
95
|
async function confirmMutation(action, opts, payload) {
|
|
19
96
|
if (opts.dryRun) {
|
|
20
97
|
console.log(`Dry run: ${action}. No request was sent to Fortnox.`);
|
|
@@ -29,12 +106,14 @@ async function confirmMutation(action, opts, payload) {
|
|
|
29
106
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
30
107
|
throw new Error(`Confirmation required to ${action}. Re-run with --yes, or --dry-run first.`);
|
|
31
108
|
}
|
|
109
|
+
const company = await fetchCompanyHint();
|
|
110
|
+
const suffix = company ? ` (${company})` : '';
|
|
32
111
|
const rl = createInterface({
|
|
33
112
|
input: process.stdin,
|
|
34
113
|
output: process.stdout,
|
|
35
114
|
});
|
|
36
115
|
try {
|
|
37
|
-
const answer = await rl.question(`${action}. Continue
|
|
116
|
+
const answer = await rl.question(`${action}. Continue?${suffix} [y/N] `);
|
|
38
117
|
return ['y', 'yes'].includes(answer.trim().toLowerCase());
|
|
39
118
|
}
|
|
40
119
|
finally {
|
|
@@ -45,10 +124,28 @@ async function confirmMutation(action, opts, payload) {
|
|
|
45
124
|
program
|
|
46
125
|
.command('init')
|
|
47
126
|
.description('Interactive setup wizard — recommended onboarding path')
|
|
48
|
-
.
|
|
127
|
+
.option('--profile <name>', 'Profile to create/re-auth (defaults to resolved profile or "default")')
|
|
128
|
+
.action(async (initOpts) => {
|
|
49
129
|
const { loadCredentials, runOAuthSetup } = await import('./auth.js');
|
|
130
|
+
const { validateProfileName } = await import('./profile-name.js');
|
|
131
|
+
let targetProfile;
|
|
132
|
+
try {
|
|
133
|
+
targetProfile = validateProfileName(initOpts.profile ?? resolvedProfileInfo.name);
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
137
|
+
process.exit(2);
|
|
138
|
+
}
|
|
139
|
+
// If `init --profile <name>` names a different profile than the preAction
|
|
140
|
+
// hook resolved, rebind the in-process resolved profile so downstream work
|
|
141
|
+
// (verification via getCompanyInfo, runOAuthSetup's internal saveCredentials
|
|
142
|
+
// call chain) targets the profile being initialized — not a stale pointer.
|
|
143
|
+
if (targetProfile.toLowerCase() !== resolvedProfileInfo.name.toLowerCase()) {
|
|
144
|
+
setResolvedProfile(targetProfile);
|
|
145
|
+
resolvedProfileInfo = { name: targetProfile, source: 'flag' };
|
|
146
|
+
}
|
|
50
147
|
// Step 1: Check if already configured
|
|
51
|
-
const existing = await loadCredentials();
|
|
148
|
+
const existing = await loadCredentials(targetProfile);
|
|
52
149
|
if (existing) {
|
|
53
150
|
console.log('Existing credentials found.');
|
|
54
151
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
@@ -181,7 +278,20 @@ program
|
|
|
181
278
|
}
|
|
182
279
|
}
|
|
183
280
|
// Step 6: Run OAuth flow
|
|
184
|
-
await runOAuthSetup({ clientId, clientSecret, serviceAccount });
|
|
281
|
+
await runOAuthSetup({ clientId, clientSecret, serviceAccount }, targetProfile);
|
|
282
|
+
// Step 6b: Set the active pointer if this is the first profile or no pointer exists.
|
|
283
|
+
try {
|
|
284
|
+
const idx = await readProfileIndex();
|
|
285
|
+
const existingPointer = await readActivePointer();
|
|
286
|
+
const firstProfile = idx.profiles.length <= 1;
|
|
287
|
+
if (firstProfile || !existingPointer) {
|
|
288
|
+
await writeActivePointer(targetProfile);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
293
|
+
console.warn(`Warning: could not update active profile pointer: ${msg}`);
|
|
294
|
+
}
|
|
185
295
|
// Step 7: Verify by fetching company info
|
|
186
296
|
try {
|
|
187
297
|
const { getCompanyInfo } = await import('./operations/company.js');
|
|
@@ -267,28 +377,139 @@ program
|
|
|
267
377
|
// --- logout ---
|
|
268
378
|
program
|
|
269
379
|
.command('logout')
|
|
270
|
-
.description('Remove stored Fortnox credentials')
|
|
380
|
+
.description('Remove stored Fortnox credentials for the resolved profile')
|
|
271
381
|
.option('-y, --yes', 'Skip confirmation prompt')
|
|
382
|
+
.option('--all', 'Remove credentials for every profile (and the legacy slot)')
|
|
272
383
|
.action(async (opts) => {
|
|
273
|
-
const { loadCredentials } = await import('./auth.js');
|
|
274
384
|
const { deleteCredentialBlob } = await import('./credentials-store.js');
|
|
275
|
-
const
|
|
385
|
+
const { removeProfile } = await import('./profiles.js');
|
|
386
|
+
if (opts.all) {
|
|
387
|
+
if (!(await confirmMutation('Remove stored Fortnox credentials for ALL profiles', opts))) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const idx = await readProfileIndex();
|
|
391
|
+
const names = new Set(idx.profiles.map((p) => p.name));
|
|
392
|
+
names.add(DEFAULT_PROFILE);
|
|
393
|
+
let removedAny = false;
|
|
394
|
+
for (const name of names) {
|
|
395
|
+
const deleted = await deleteCredentialBlob(name);
|
|
396
|
+
if (deleted)
|
|
397
|
+
removedAny = true;
|
|
398
|
+
try {
|
|
399
|
+
await removeProfile(name);
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
// best-effort — index cleanup must not abort logout
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
await deleteActivePointer();
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
// ignore
|
|
410
|
+
}
|
|
411
|
+
if (removedAny) {
|
|
412
|
+
console.log('Removed credentials for all profiles.');
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
console.log('No credentials found to remove.');
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const target = resolvedProfileInfo.name;
|
|
420
|
+
const { loadCredentials } = await import('./auth.js');
|
|
421
|
+
const existing = await loadCredentials(target);
|
|
276
422
|
if (!existing) {
|
|
277
|
-
console.log(
|
|
423
|
+
console.log(`No credentials found for profile "${target}". Nothing to remove.`);
|
|
278
424
|
return;
|
|
279
425
|
}
|
|
280
|
-
if (!(await confirmMutation(
|
|
426
|
+
if (!(await confirmMutation(`Remove stored credentials for profile "${target}"`, opts))) {
|
|
281
427
|
return;
|
|
282
428
|
}
|
|
283
|
-
const deleted = await deleteCredentialBlob();
|
|
429
|
+
const deleted = await deleteCredentialBlob(target);
|
|
430
|
+
try {
|
|
431
|
+
await removeProfile(target);
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
// best-effort
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
const pointer = await readActivePointer();
|
|
438
|
+
if (pointer && pointer.toLowerCase() === target.toLowerCase()) {
|
|
439
|
+
await deleteActivePointer();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
// ignore
|
|
444
|
+
}
|
|
284
445
|
if (deleted) {
|
|
285
|
-
console.log(
|
|
446
|
+
console.log(`Credentials for profile "${target}" removed.`);
|
|
286
447
|
}
|
|
287
448
|
else {
|
|
288
449
|
console.log('Could not remove credentials from the system keychain.');
|
|
289
450
|
console.log('They may have already been removed, or you may need to remove them manually.');
|
|
290
451
|
}
|
|
291
452
|
});
|
|
453
|
+
// --- profile ---
|
|
454
|
+
const profile = program.command('profile').description('Manage noxctl profiles');
|
|
455
|
+
profile
|
|
456
|
+
.command('use <name>')
|
|
457
|
+
.description('Set the active profile (writes ~/.fortnox-mcp/active-profile)')
|
|
458
|
+
.action(async (name) => {
|
|
459
|
+
const { validateProfileName } = await import('./profile-name.js');
|
|
460
|
+
let validated;
|
|
461
|
+
try {
|
|
462
|
+
validated = validateProfileName(name);
|
|
463
|
+
}
|
|
464
|
+
catch (err) {
|
|
465
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
466
|
+
process.exit(2);
|
|
467
|
+
}
|
|
468
|
+
const { loadCredentials } = await import('./auth.js');
|
|
469
|
+
const creds = await loadCredentials(validated);
|
|
470
|
+
if (!creds) {
|
|
471
|
+
console.error(`No credentials found for profile "${validated}". Run \`noxctl init --profile ${validated}\` first.`);
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
await writeActivePointer(validated);
|
|
475
|
+
if (json()) {
|
|
476
|
+
console.log(JSON.stringify({ name: validated, source: 'pointer' }));
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
console.log(`Active profile set to "${validated}".`);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
profile
|
|
483
|
+
.command('current')
|
|
484
|
+
.description('Show the currently resolved profile and where it came from')
|
|
485
|
+
.action(() => {
|
|
486
|
+
if (json()) {
|
|
487
|
+
console.log(JSON.stringify(resolvedProfileInfo));
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
console.log(`${resolvedProfileInfo.name} (source: ${resolvedProfileInfo.source})`);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
profile
|
|
494
|
+
.command('list')
|
|
495
|
+
.description('List known profiles from the index')
|
|
496
|
+
.action(async () => {
|
|
497
|
+
const idx = await readProfileIndex();
|
|
498
|
+
if (json()) {
|
|
499
|
+
console.log(JSON.stringify(idx.profiles, null, 2));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (idx.profiles.length === 0) {
|
|
503
|
+
console.log('No profiles registered. Run `noxctl init` to create one.');
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
for (const p of idx.profiles) {
|
|
507
|
+
const marker = p.name.toLowerCase() === resolvedProfileInfo.name.toLowerCase() ? '*' : ' ';
|
|
508
|
+
const company = p.company_name ? ` — ${p.company_name}` : '';
|
|
509
|
+
const tenant = p.tenant_id ? ` [tenant ${p.tenant_id}]` : '';
|
|
510
|
+
console.log(`${marker} ${p.name}${company}${tenant}`);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
292
513
|
// --- doctor ---
|
|
293
514
|
program
|
|
294
515
|
.command('doctor')
|
|
@@ -320,14 +541,51 @@ program
|
|
|
320
541
|
? 'Windows DPAPI'
|
|
321
542
|
: 'Linux Secret Service';
|
|
322
543
|
pass('Credential store', storeBackend);
|
|
544
|
+
// 2b. Profile resolution
|
|
545
|
+
pass('Profile', `${resolvedProfileInfo.name} (source: ${resolvedProfileInfo.source})`);
|
|
546
|
+
// 2c. Active pointer health — surface a corrupt pointer file even though
|
|
547
|
+
// readActivePointer() degrades silently.
|
|
548
|
+
try {
|
|
549
|
+
const { paths } = await import('./profiles.js');
|
|
550
|
+
const fsp = await import('node:fs/promises');
|
|
551
|
+
const raw = await fsp.readFile(paths.activePointerFile, 'utf-8').catch(() => null);
|
|
552
|
+
if (raw !== null) {
|
|
553
|
+
const trimmed = raw.trim();
|
|
554
|
+
if (trimmed.length > 0) {
|
|
555
|
+
const { validateProfileName } = await import('./profile-name.js');
|
|
556
|
+
try {
|
|
557
|
+
validateProfileName(trimmed);
|
|
558
|
+
const idx = await readProfileIndex();
|
|
559
|
+
const known = idx.profiles.some((p) => p.name.toLowerCase() === trimmed.toLowerCase());
|
|
560
|
+
if (known) {
|
|
561
|
+
pass('Active pointer', trimmed);
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
fail('Active pointer', `points to unknown profile "${trimmed}" — run \`noxctl profile use <name>\` to fix`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
fail('Active pointer', 'contains an invalid name — delete to reset');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
// best-effort diagnostic — don't block doctor on filesystem issues
|
|
575
|
+
}
|
|
323
576
|
// 3. Credentials exist
|
|
324
577
|
const creds = await loadCredentials();
|
|
325
578
|
if (!creds) {
|
|
326
|
-
fail('Credentials',
|
|
579
|
+
fail('Credentials', `not found for profile "${resolvedProfileInfo.name}" — run \`noxctl init${resolvedProfileInfo.name === DEFAULT_PROFILE
|
|
580
|
+
? ''
|
|
581
|
+
: ` --profile ${resolvedProfileInfo.name}`}\` to set up`);
|
|
327
582
|
console.log(`\n${ok ? 'All checks passed.' : 'Some checks failed.'}`);
|
|
328
583
|
return;
|
|
329
584
|
}
|
|
330
585
|
pass('Credentials', 'found');
|
|
586
|
+
if (creds.company_name) {
|
|
587
|
+
pass('Company (cached)', creds.company_name);
|
|
588
|
+
}
|
|
331
589
|
// 4. Client ID present
|
|
332
590
|
if (creds.client_id) {
|
|
333
591
|
pass('Client ID', `${creds.client_id.slice(0, 8)}...`);
|
|
@@ -372,6 +630,7 @@ program
|
|
|
372
630
|
article: 'articles?limit=1',
|
|
373
631
|
customer: 'customers?limit=1',
|
|
374
632
|
invoice: 'invoices?limit=1',
|
|
633
|
+
payment: 'invoicepayments?limit=1',
|
|
375
634
|
supplier: 'suppliers?limit=1',
|
|
376
635
|
supplierinvoice: 'supplierinvoices?limit=1',
|
|
377
636
|
bookkeeping: 'vouchers?limit=1',
|
|
@@ -408,7 +667,7 @@ program
|
|
|
408
667
|
.description('Start the MCP server (stdio transport)')
|
|
409
668
|
.action(async () => {
|
|
410
669
|
const { startMcpServer } = await import('./index.js');
|
|
411
|
-
await startMcpServer();
|
|
670
|
+
await startMcpServer({ profile: resolvedProfileInfo.name });
|
|
412
671
|
});
|
|
413
672
|
// --- invoices ---
|
|
414
673
|
const invoices = program.command('invoices').description('Invoice operations');
|
|
@@ -990,6 +1249,679 @@ Examples:
|
|
|
990
1249
|
const data = await createVoucher(params);
|
|
991
1250
|
outputDetail(data, voucherDetailColumns, json());
|
|
992
1251
|
});
|
|
1252
|
+
// --- invoice-payments ---
|
|
1253
|
+
const invoicePayments = program
|
|
1254
|
+
.command('invoice-payments')
|
|
1255
|
+
.alias('ip')
|
|
1256
|
+
.description('Invoice payment operations (inbetalningar)');
|
|
1257
|
+
invoicePayments
|
|
1258
|
+
.command('list')
|
|
1259
|
+
.description('List invoice payments')
|
|
1260
|
+
.option('--invoice <number>', 'Filter by invoice number')
|
|
1261
|
+
.option('--page <number>', 'Page number', parseInt)
|
|
1262
|
+
.option('--limit <number>', 'Results per page', parseInt)
|
|
1263
|
+
.option('-a, --all', 'Fetch all pages')
|
|
1264
|
+
.action(async (opts) => {
|
|
1265
|
+
const { listInvoicePayments } = await import('./operations/invoice-payments.js');
|
|
1266
|
+
const data = await listInvoicePayments({
|
|
1267
|
+
invoiceNumber: opts.invoice,
|
|
1268
|
+
page: opts.page,
|
|
1269
|
+
limit: opts.limit,
|
|
1270
|
+
all: opts.all,
|
|
1271
|
+
});
|
|
1272
|
+
const envelope = data;
|
|
1273
|
+
outputList(envelope.InvoicePayments ?? [], invoicePaymentListColumns, json(), data, envelope.MetaInformation);
|
|
1274
|
+
});
|
|
1275
|
+
invoicePayments
|
|
1276
|
+
.command('get <paymentNumber>')
|
|
1277
|
+
.description('Get a single invoice payment')
|
|
1278
|
+
.action(async (paymentNumber) => {
|
|
1279
|
+
const { getInvoicePayment } = await import('./operations/invoice-payments.js');
|
|
1280
|
+
const data = await getInvoicePayment(paymentNumber);
|
|
1281
|
+
outputDetail(data, invoicePaymentDetailColumns, json());
|
|
1282
|
+
});
|
|
1283
|
+
invoicePayments
|
|
1284
|
+
.command('create')
|
|
1285
|
+
.description('Register a payment against an invoice')
|
|
1286
|
+
.requiredOption('--invoice <number>', 'Invoice number')
|
|
1287
|
+
.requiredOption('--amount <amount>', 'Payment amount', parseFloat)
|
|
1288
|
+
.requiredOption('--date <date>', 'Payment date (YYYY-MM-DD)')
|
|
1289
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1290
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1291
|
+
.addHelpText('after', `
|
|
1292
|
+
Examples:
|
|
1293
|
+
noxctl invoice-payments create --invoice 1001 --amount 5000 --date 2026-03-20 --dry-run
|
|
1294
|
+
noxctl ip create --invoice 1001 --amount 5000 --date 2026-03-20 --yes`)
|
|
1295
|
+
.action(async (opts) => {
|
|
1296
|
+
const { createInvoicePayment } = await import('./operations/invoice-payments.js');
|
|
1297
|
+
const params = {
|
|
1298
|
+
InvoiceNumber: parseInt(opts.invoice, 10),
|
|
1299
|
+
Amount: opts.amount,
|
|
1300
|
+
PaymentDate: opts.date,
|
|
1301
|
+
};
|
|
1302
|
+
if (!(await confirmMutation(`Register payment of ${opts.amount} for invoice ${opts.invoice}`, opts, {
|
|
1303
|
+
InvoicePayment: params,
|
|
1304
|
+
}))) {
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
const data = await createInvoicePayment(params);
|
|
1308
|
+
outputDetail(data, invoicePaymentDetailColumns, json());
|
|
1309
|
+
});
|
|
1310
|
+
invoicePayments
|
|
1311
|
+
.command('delete <paymentNumber>')
|
|
1312
|
+
.description('Delete an invoice payment')
|
|
1313
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1314
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
1315
|
+
.action(async (paymentNumber, opts) => {
|
|
1316
|
+
const { deleteInvoicePayment } = await import('./operations/invoice-payments.js');
|
|
1317
|
+
if (!(await confirmMutation(`Delete invoice payment ${paymentNumber}`, opts))) {
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
await deleteInvoicePayment(paymentNumber);
|
|
1321
|
+
outputConfirmation(`Invoice payment ${paymentNumber} deleted.`, json(), {});
|
|
1322
|
+
});
|
|
1323
|
+
invoicePayments
|
|
1324
|
+
.command('bookkeep <paymentNumber>')
|
|
1325
|
+
.description('Bookkeep an invoice payment')
|
|
1326
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1327
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
1328
|
+
.action(async (paymentNumber, opts) => {
|
|
1329
|
+
const { bookkeepInvoicePayment } = await import('./operations/invoice-payments.js');
|
|
1330
|
+
if (!(await confirmMutation(`Bookkeep invoice payment ${paymentNumber}`, opts))) {
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
const result = await bookkeepInvoicePayment(paymentNumber);
|
|
1334
|
+
outputConfirmation(`Invoice payment ${paymentNumber} bookkeept.`, json(), result);
|
|
1335
|
+
});
|
|
1336
|
+
// --- supplier-invoice-payments ---
|
|
1337
|
+
const supplierInvoicePayments = program
|
|
1338
|
+
.command('supplier-invoice-payments')
|
|
1339
|
+
.alias('sip')
|
|
1340
|
+
.description('Supplier invoice payment operations (utbetalningar)');
|
|
1341
|
+
supplierInvoicePayments
|
|
1342
|
+
.command('list')
|
|
1343
|
+
.description('List supplier invoice payments')
|
|
1344
|
+
.option('--invoice <number>', 'Filter by invoice number')
|
|
1345
|
+
.option('--page <number>', 'Page number', parseInt)
|
|
1346
|
+
.option('--limit <number>', 'Results per page', parseInt)
|
|
1347
|
+
.option('-a, --all', 'Fetch all pages')
|
|
1348
|
+
.action(async (opts) => {
|
|
1349
|
+
const { listSupplierInvoicePayments } = await import('./operations/supplier-invoice-payments.js');
|
|
1350
|
+
const data = await listSupplierInvoicePayments({
|
|
1351
|
+
invoiceNumber: opts.invoice,
|
|
1352
|
+
page: opts.page,
|
|
1353
|
+
limit: opts.limit,
|
|
1354
|
+
all: opts.all,
|
|
1355
|
+
});
|
|
1356
|
+
const envelope = data;
|
|
1357
|
+
outputList(envelope.SupplierInvoicePayments ?? [], supplierInvoicePaymentListColumns, json(), data, envelope.MetaInformation);
|
|
1358
|
+
});
|
|
1359
|
+
supplierInvoicePayments
|
|
1360
|
+
.command('get <paymentNumber>')
|
|
1361
|
+
.description('Get a single supplier invoice payment')
|
|
1362
|
+
.action(async (paymentNumber) => {
|
|
1363
|
+
const { getSupplierInvoicePayment } = await import('./operations/supplier-invoice-payments.js');
|
|
1364
|
+
const data = await getSupplierInvoicePayment(paymentNumber);
|
|
1365
|
+
outputDetail(data, supplierInvoicePaymentDetailColumns, json());
|
|
1366
|
+
});
|
|
1367
|
+
supplierInvoicePayments
|
|
1368
|
+
.command('create')
|
|
1369
|
+
.description('Register a payment against a supplier invoice')
|
|
1370
|
+
.requiredOption('--invoice <number>', 'Supplier invoice number')
|
|
1371
|
+
.requiredOption('--amount <amount>', 'Payment amount', parseFloat)
|
|
1372
|
+
.requiredOption('--date <date>', 'Payment date (YYYY-MM-DD)')
|
|
1373
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1374
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1375
|
+
.addHelpText('after', `
|
|
1376
|
+
Examples:
|
|
1377
|
+
noxctl supplier-invoice-payments create --invoice 501 --amount 3000 --date 2026-03-20 --dry-run
|
|
1378
|
+
noxctl sip create --invoice 501 --amount 3000 --date 2026-03-20 --yes`)
|
|
1379
|
+
.action(async (opts) => {
|
|
1380
|
+
const { createSupplierInvoicePayment } = await import('./operations/supplier-invoice-payments.js');
|
|
1381
|
+
const params = {
|
|
1382
|
+
InvoiceNumber: opts.invoice,
|
|
1383
|
+
Amount: opts.amount,
|
|
1384
|
+
PaymentDate: opts.date,
|
|
1385
|
+
};
|
|
1386
|
+
if (!(await confirmMutation(`Register payment of ${opts.amount} for supplier invoice ${opts.invoice}`, opts, { SupplierInvoicePayment: params }))) {
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
const data = await createSupplierInvoicePayment(params);
|
|
1390
|
+
outputDetail(data, supplierInvoicePaymentDetailColumns, json());
|
|
1391
|
+
});
|
|
1392
|
+
supplierInvoicePayments
|
|
1393
|
+
.command('delete <paymentNumber>')
|
|
1394
|
+
.description('Delete a supplier invoice payment')
|
|
1395
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1396
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
1397
|
+
.action(async (paymentNumber, opts) => {
|
|
1398
|
+
const { deleteSupplierInvoicePayment } = await import('./operations/supplier-invoice-payments.js');
|
|
1399
|
+
if (!(await confirmMutation(`Delete supplier invoice payment ${paymentNumber}`, opts))) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
await deleteSupplierInvoicePayment(paymentNumber);
|
|
1403
|
+
outputConfirmation(`Supplier invoice payment ${paymentNumber} deleted.`, json(), {});
|
|
1404
|
+
});
|
|
1405
|
+
// --- offers ---
|
|
1406
|
+
const offers = program.command('offers').description('Offer/quote operations (offerter)');
|
|
1407
|
+
offers
|
|
1408
|
+
.command('list')
|
|
1409
|
+
.description('List/filter offers')
|
|
1410
|
+
.option('--filter <filter>', 'Filter: cancelled, expired, ordercreated, invoicecreated')
|
|
1411
|
+
.option('--customer <number>', 'Filter by customer number')
|
|
1412
|
+
.option('--from <date>', 'From date (YYYY-MM-DD)')
|
|
1413
|
+
.option('--to <date>', 'To date (YYYY-MM-DD)')
|
|
1414
|
+
.option('--page <number>', 'Page number', parseInt)
|
|
1415
|
+
.option('--limit <number>', 'Results per page', parseInt)
|
|
1416
|
+
.option('-a, --all', 'Fetch all pages')
|
|
1417
|
+
.action(async (opts) => {
|
|
1418
|
+
const { listOffers } = await import('./operations/offers.js');
|
|
1419
|
+
const data = await listOffers({
|
|
1420
|
+
filter: opts.filter,
|
|
1421
|
+
customerNumber: opts.customer,
|
|
1422
|
+
fromDate: opts.from,
|
|
1423
|
+
toDate: opts.to,
|
|
1424
|
+
page: opts.page,
|
|
1425
|
+
limit: opts.limit,
|
|
1426
|
+
all: opts.all,
|
|
1427
|
+
});
|
|
1428
|
+
const envelope = data;
|
|
1429
|
+
outputList(envelope.Offers ?? [], offerListColumns, json(), data, envelope.MetaInformation);
|
|
1430
|
+
});
|
|
1431
|
+
offers
|
|
1432
|
+
.command('get <documentNumber>')
|
|
1433
|
+
.description('Get a single offer')
|
|
1434
|
+
.action(async (documentNumber) => {
|
|
1435
|
+
const { getOffer } = await import('./operations/offers.js');
|
|
1436
|
+
const data = await getOffer(documentNumber);
|
|
1437
|
+
outputDetail(data, offerDetailColumns, json());
|
|
1438
|
+
});
|
|
1439
|
+
offers
|
|
1440
|
+
.command('create')
|
|
1441
|
+
.description('Create an offer')
|
|
1442
|
+
.requiredOption('--customer <number>', 'Customer number')
|
|
1443
|
+
.requiredOption('--input <file>', 'Offer data as JSON file (or - for stdin)')
|
|
1444
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1445
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1446
|
+
.addHelpText('after', `
|
|
1447
|
+
Examples:
|
|
1448
|
+
echo '{"OfferRows":[{"Description":"Consulting","DeliveredQuantity":10,"Price":1200}]}' | noxctl offers create --customer 25 --input - --dry-run`)
|
|
1449
|
+
.action(async (opts) => {
|
|
1450
|
+
const { createOffer } = await import('./operations/offers.js');
|
|
1451
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
1452
|
+
const input = JSON.parse(raw);
|
|
1453
|
+
const params = { CustomerNumber: opts.customer, ...input };
|
|
1454
|
+
if (!(await confirmMutation(`Create offer for customer ${opts.customer}`, opts, {
|
|
1455
|
+
Offer: params,
|
|
1456
|
+
}))) {
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
const data = await createOffer(params);
|
|
1460
|
+
outputDetail(data, offerDetailColumns, json());
|
|
1461
|
+
});
|
|
1462
|
+
offers
|
|
1463
|
+
.command('update <documentNumber>')
|
|
1464
|
+
.description('Update an offer')
|
|
1465
|
+
.requiredOption('--input <file>', 'Offer data as JSON file (or - for stdin)')
|
|
1466
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1467
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1468
|
+
.action(async (documentNumber, opts) => {
|
|
1469
|
+
const { updateOffer } = await import('./operations/offers.js');
|
|
1470
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
1471
|
+
const fields = JSON.parse(raw);
|
|
1472
|
+
if (!(await confirmMutation(`Update offer ${documentNumber}`, opts, { Offer: fields }))) {
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
const data = await updateOffer(documentNumber, fields);
|
|
1476
|
+
outputDetail(data, offerDetailColumns, json());
|
|
1477
|
+
});
|
|
1478
|
+
offers
|
|
1479
|
+
.command('create-invoice <documentNumber>')
|
|
1480
|
+
.description('Create an invoice from an offer')
|
|
1481
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1482
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
1483
|
+
.action(async (documentNumber, opts) => {
|
|
1484
|
+
const { createInvoiceFromOffer } = await import('./operations/offers.js');
|
|
1485
|
+
if (!(await confirmMutation(`Create invoice from offer ${documentNumber}`, opts))) {
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
const data = await createInvoiceFromOffer(documentNumber);
|
|
1489
|
+
outputConfirmation(`Invoice created from offer ${documentNumber}.`, json(), data, invoiceConfirmColumns);
|
|
1490
|
+
});
|
|
1491
|
+
offers
|
|
1492
|
+
.command('create-order <documentNumber>')
|
|
1493
|
+
.description('Create an order from an offer')
|
|
1494
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1495
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
1496
|
+
.action(async (documentNumber, opts) => {
|
|
1497
|
+
const { createOrderFromOffer } = await import('./operations/offers.js');
|
|
1498
|
+
if (!(await confirmMutation(`Create order from offer ${documentNumber}`, opts))) {
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
const data = await createOrderFromOffer(documentNumber);
|
|
1502
|
+
outputConfirmation(`Order created from offer ${documentNumber}.`, json(), data);
|
|
1503
|
+
});
|
|
1504
|
+
// --- orders ---
|
|
1505
|
+
const orders = program.command('orders').description('Order operations (ordrar)');
|
|
1506
|
+
orders
|
|
1507
|
+
.command('list')
|
|
1508
|
+
.description('List/filter orders')
|
|
1509
|
+
.option('--filter <filter>', 'Filter: cancelled, invoicecreated, invoicenotcreated')
|
|
1510
|
+
.option('--customer <number>', 'Filter by customer number')
|
|
1511
|
+
.option('--from <date>', 'From date (YYYY-MM-DD)')
|
|
1512
|
+
.option('--to <date>', 'To date (YYYY-MM-DD)')
|
|
1513
|
+
.option('--page <number>', 'Page number', parseInt)
|
|
1514
|
+
.option('--limit <number>', 'Results per page', parseInt)
|
|
1515
|
+
.option('-a, --all', 'Fetch all pages')
|
|
1516
|
+
.action(async (opts) => {
|
|
1517
|
+
const { listOrders } = await import('./operations/orders.js');
|
|
1518
|
+
const data = await listOrders({
|
|
1519
|
+
filter: opts.filter,
|
|
1520
|
+
customerNumber: opts.customer,
|
|
1521
|
+
fromDate: opts.from,
|
|
1522
|
+
toDate: opts.to,
|
|
1523
|
+
page: opts.page,
|
|
1524
|
+
limit: opts.limit,
|
|
1525
|
+
all: opts.all,
|
|
1526
|
+
});
|
|
1527
|
+
const envelope = data;
|
|
1528
|
+
outputList(envelope.Orders ?? [], orderListColumns, json(), data, envelope.MetaInformation);
|
|
1529
|
+
});
|
|
1530
|
+
orders
|
|
1531
|
+
.command('get <documentNumber>')
|
|
1532
|
+
.description('Get a single order')
|
|
1533
|
+
.action(async (documentNumber) => {
|
|
1534
|
+
const { getOrder } = await import('./operations/orders.js');
|
|
1535
|
+
const data = await getOrder(documentNumber);
|
|
1536
|
+
outputDetail(data, orderDetailColumns, json());
|
|
1537
|
+
});
|
|
1538
|
+
orders
|
|
1539
|
+
.command('create')
|
|
1540
|
+
.description('Create an order')
|
|
1541
|
+
.requiredOption('--customer <number>', 'Customer number')
|
|
1542
|
+
.requiredOption('--input <file>', 'Order data as JSON file (or - for stdin)')
|
|
1543
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1544
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1545
|
+
.addHelpText('after', `
|
|
1546
|
+
Examples:
|
|
1547
|
+
echo '{"OrderRows":[{"Description":"Consulting","DeliveredQuantity":10,"Price":1200}]}' | noxctl orders create --customer 25 --input - --dry-run`)
|
|
1548
|
+
.action(async (opts) => {
|
|
1549
|
+
const { createOrder } = await import('./operations/orders.js');
|
|
1550
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
1551
|
+
const input = JSON.parse(raw);
|
|
1552
|
+
const params = { CustomerNumber: opts.customer, ...input };
|
|
1553
|
+
if (!(await confirmMutation(`Create order for customer ${opts.customer}`, opts, {
|
|
1554
|
+
Order: params,
|
|
1555
|
+
}))) {
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
const data = await createOrder(params);
|
|
1559
|
+
outputDetail(data, orderDetailColumns, json());
|
|
1560
|
+
});
|
|
1561
|
+
orders
|
|
1562
|
+
.command('update <documentNumber>')
|
|
1563
|
+
.description('Update an order')
|
|
1564
|
+
.requiredOption('--input <file>', 'Order data as JSON file (or - for stdin)')
|
|
1565
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1566
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1567
|
+
.action(async (documentNumber, opts) => {
|
|
1568
|
+
const { updateOrder } = await import('./operations/orders.js');
|
|
1569
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
1570
|
+
const fields = JSON.parse(raw);
|
|
1571
|
+
if (!(await confirmMutation(`Update order ${documentNumber}`, opts, { Order: fields }))) {
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
const data = await updateOrder(documentNumber, fields);
|
|
1575
|
+
outputDetail(data, orderDetailColumns, json());
|
|
1576
|
+
});
|
|
1577
|
+
orders
|
|
1578
|
+
.command('create-invoice <documentNumber>')
|
|
1579
|
+
.description('Create an invoice from an order')
|
|
1580
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1581
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
1582
|
+
.action(async (documentNumber, opts) => {
|
|
1583
|
+
const { createInvoiceFromOrder } = await import('./operations/orders.js');
|
|
1584
|
+
if (!(await confirmMutation(`Create invoice from order ${documentNumber}`, opts))) {
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
const data = await createInvoiceFromOrder(documentNumber);
|
|
1588
|
+
outputConfirmation(`Invoice created from order ${documentNumber}.`, json(), data, invoiceConfirmColumns);
|
|
1589
|
+
});
|
|
1590
|
+
// --- projects ---
|
|
1591
|
+
const projects = program.command('projects').description('Project operations');
|
|
1592
|
+
projects
|
|
1593
|
+
.command('list')
|
|
1594
|
+
.description('List projects')
|
|
1595
|
+
.option('--page <number>', 'Page number', parseInt)
|
|
1596
|
+
.option('--limit <number>', 'Results per page', parseInt)
|
|
1597
|
+
.option('-a, --all', 'Fetch all pages')
|
|
1598
|
+
.action(async (opts) => {
|
|
1599
|
+
const { listProjects } = await import('./operations/projects.js');
|
|
1600
|
+
const data = await listProjects({
|
|
1601
|
+
page: opts.page,
|
|
1602
|
+
limit: opts.limit,
|
|
1603
|
+
all: opts.all,
|
|
1604
|
+
});
|
|
1605
|
+
const envelope = data;
|
|
1606
|
+
outputList(envelope.Projects ?? [], projectListColumns, json(), data, envelope.MetaInformation);
|
|
1607
|
+
});
|
|
1608
|
+
projects
|
|
1609
|
+
.command('get <projectNumber>')
|
|
1610
|
+
.description('Get a single project')
|
|
1611
|
+
.action(async (projectNumber) => {
|
|
1612
|
+
const { getProject } = await import('./operations/projects.js');
|
|
1613
|
+
const data = await getProject(projectNumber);
|
|
1614
|
+
outputDetail(data, projectDetailColumns, json());
|
|
1615
|
+
});
|
|
1616
|
+
projects
|
|
1617
|
+
.command('create')
|
|
1618
|
+
.description('Create a project')
|
|
1619
|
+
.requiredOption('--description <text>', 'Project description')
|
|
1620
|
+
.option('--project-number <number>', 'Project number (auto-generated if omitted)')
|
|
1621
|
+
.option('--status <status>', 'Status (ONGOING or COMPLETED)')
|
|
1622
|
+
.option('--input <file>', 'Project data as JSON file (or - for stdin)')
|
|
1623
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1624
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1625
|
+
.action(async (opts) => {
|
|
1626
|
+
const { createProject } = await import('./operations/projects.js');
|
|
1627
|
+
let input = {};
|
|
1628
|
+
if (opts.input) {
|
|
1629
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
1630
|
+
input = JSON.parse(raw);
|
|
1631
|
+
}
|
|
1632
|
+
const params = { ...input, Description: opts.description };
|
|
1633
|
+
if (opts.projectNumber)
|
|
1634
|
+
params.ProjectNumber = opts.projectNumber;
|
|
1635
|
+
if (opts.status)
|
|
1636
|
+
params.Status = opts.status;
|
|
1637
|
+
if (!(await confirmMutation(`Create project "${opts.description}"`, opts, { Project: params }))) {
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
const data = await createProject(params);
|
|
1641
|
+
outputDetail(data, projectDetailColumns, json());
|
|
1642
|
+
});
|
|
1643
|
+
projects
|
|
1644
|
+
.command('update <projectNumber>')
|
|
1645
|
+
.description('Update a project')
|
|
1646
|
+
.requiredOption('--input <file>', 'Project data as JSON file (or - for stdin)')
|
|
1647
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1648
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1649
|
+
.action(async (projectNumber, opts) => {
|
|
1650
|
+
const { updateProject } = await import('./operations/projects.js');
|
|
1651
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
1652
|
+
const fields = JSON.parse(raw);
|
|
1653
|
+
if (!(await confirmMutation(`Update project ${projectNumber}`, opts, { Project: fields }))) {
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
const data = await updateProject(projectNumber, fields);
|
|
1657
|
+
outputDetail(data, projectDetailColumns, json());
|
|
1658
|
+
});
|
|
1659
|
+
// --- cost centers ---
|
|
1660
|
+
const costcenters = program.command('costcenters').description('Cost center operations');
|
|
1661
|
+
costcenters
|
|
1662
|
+
.command('list')
|
|
1663
|
+
.description('List cost centers')
|
|
1664
|
+
.option('--page <number>', 'Page number', parseInt)
|
|
1665
|
+
.option('--limit <number>', 'Results per page', parseInt)
|
|
1666
|
+
.option('-a, --all', 'Fetch all pages')
|
|
1667
|
+
.action(async (opts) => {
|
|
1668
|
+
const { listCostCenters } = await import('./operations/costcenters.js');
|
|
1669
|
+
const data = await listCostCenters({
|
|
1670
|
+
page: opts.page,
|
|
1671
|
+
limit: opts.limit,
|
|
1672
|
+
all: opts.all,
|
|
1673
|
+
});
|
|
1674
|
+
const envelope = data;
|
|
1675
|
+
outputList(envelope.CostCenters ?? [], costCenterListColumns, json(), data, envelope.MetaInformation);
|
|
1676
|
+
});
|
|
1677
|
+
costcenters
|
|
1678
|
+
.command('get <code>')
|
|
1679
|
+
.description('Get a single cost center')
|
|
1680
|
+
.action(async (code) => {
|
|
1681
|
+
const { getCostCenter } = await import('./operations/costcenters.js');
|
|
1682
|
+
const data = await getCostCenter(code);
|
|
1683
|
+
outputDetail(data, costCenterDetailColumns, json());
|
|
1684
|
+
});
|
|
1685
|
+
costcenters
|
|
1686
|
+
.command('create')
|
|
1687
|
+
.description('Create a cost center')
|
|
1688
|
+
.requiredOption('--code <code>', 'Cost center code')
|
|
1689
|
+
.requiredOption('--description <text>', 'Description')
|
|
1690
|
+
.option('--input <file>', 'Cost center data as JSON file (or - for stdin)')
|
|
1691
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1692
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1693
|
+
.action(async (opts) => {
|
|
1694
|
+
const { createCostCenter } = await import('./operations/costcenters.js');
|
|
1695
|
+
let input = {};
|
|
1696
|
+
if (opts.input) {
|
|
1697
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
1698
|
+
input = JSON.parse(raw);
|
|
1699
|
+
}
|
|
1700
|
+
const params = {
|
|
1701
|
+
...input,
|
|
1702
|
+
Code: opts.code,
|
|
1703
|
+
Description: opts.description,
|
|
1704
|
+
};
|
|
1705
|
+
if (!(await confirmMutation(`Create cost center "${opts.code}"`, opts, { CostCenter: params }))) {
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
const data = await createCostCenter(params);
|
|
1709
|
+
outputDetail(data, costCenterDetailColumns, json());
|
|
1710
|
+
});
|
|
1711
|
+
costcenters
|
|
1712
|
+
.command('update <code>')
|
|
1713
|
+
.description('Update a cost center')
|
|
1714
|
+
.requiredOption('--input <file>', 'Cost center data as JSON file (or - for stdin)')
|
|
1715
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1716
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1717
|
+
.action(async (code, opts) => {
|
|
1718
|
+
const { updateCostCenter } = await import('./operations/costcenters.js');
|
|
1719
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
1720
|
+
const fields = JSON.parse(raw);
|
|
1721
|
+
if (!(await confirmMutation(`Update cost center ${code}`, opts, { CostCenter: fields }))) {
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
const data = await updateCostCenter(code, fields);
|
|
1725
|
+
outputDetail(data, costCenterDetailColumns, json());
|
|
1726
|
+
});
|
|
1727
|
+
costcenters
|
|
1728
|
+
.command('delete <code>')
|
|
1729
|
+
.description('Delete a cost center')
|
|
1730
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1731
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1732
|
+
.action(async (code, opts) => {
|
|
1733
|
+
const { deleteCostCenter } = await import('./operations/costcenters.js');
|
|
1734
|
+
if (!(await confirmMutation(`Delete cost center ${code}`, opts))) {
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
await deleteCostCenter(code);
|
|
1738
|
+
console.log(`Cost center ${code} deleted.`);
|
|
1739
|
+
});
|
|
1740
|
+
// --- tax reductions (ROT/RUT) ---
|
|
1741
|
+
const taxreductions = program
|
|
1742
|
+
.command('tax-reductions')
|
|
1743
|
+
.description('Tax reduction (ROT/RUT) operations');
|
|
1744
|
+
taxreductions
|
|
1745
|
+
.command('list')
|
|
1746
|
+
.description('List tax reductions')
|
|
1747
|
+
.option('--filter <type>', 'Filter by document type (invoices, offers, orders)')
|
|
1748
|
+
.option('--page <number>', 'Page number', parseInt)
|
|
1749
|
+
.option('--limit <number>', 'Results per page', parseInt)
|
|
1750
|
+
.option('-a, --all', 'Fetch all pages')
|
|
1751
|
+
.action(async (opts) => {
|
|
1752
|
+
const { listTaxReductions } = await import('./operations/taxreductions.js');
|
|
1753
|
+
const data = await listTaxReductions({
|
|
1754
|
+
filter: opts.filter,
|
|
1755
|
+
page: opts.page,
|
|
1756
|
+
limit: opts.limit,
|
|
1757
|
+
all: opts.all,
|
|
1758
|
+
});
|
|
1759
|
+
const envelope = data;
|
|
1760
|
+
outputList(envelope.TaxReductions ?? [], taxReductionListColumns, json(), data, envelope.MetaInformation);
|
|
1761
|
+
});
|
|
1762
|
+
taxreductions
|
|
1763
|
+
.command('get <id>')
|
|
1764
|
+
.description('Get a single tax reduction')
|
|
1765
|
+
.action(async (id) => {
|
|
1766
|
+
const { getTaxReduction } = await import('./operations/taxreductions.js');
|
|
1767
|
+
const data = await getTaxReduction(parseInt(id, 10));
|
|
1768
|
+
outputDetail(data, taxReductionDetailColumns, json());
|
|
1769
|
+
});
|
|
1770
|
+
taxreductions
|
|
1771
|
+
.command('create')
|
|
1772
|
+
.description('Create a tax reduction (ROT/RUT)')
|
|
1773
|
+
.requiredOption('--reference <number>', 'Reference number (e.g. invoice number)')
|
|
1774
|
+
.requiredOption('--type <type>', 'Type of reduction (rot or rut)')
|
|
1775
|
+
.requiredOption('--document-type <type>', 'Document type (INVOICE, OFFER, ORDER)')
|
|
1776
|
+
.requiredOption('--customer-name <name>', 'Customer name')
|
|
1777
|
+
.requiredOption('--amount <amount>', 'Asked amount in öre', parseInt)
|
|
1778
|
+
.option('--property <designation>', 'Property designation (required for ROT)')
|
|
1779
|
+
.option('--input <file>', 'Tax reduction data as JSON file (or - for stdin)')
|
|
1780
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1781
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1782
|
+
.action(async (opts) => {
|
|
1783
|
+
const { createTaxReduction } = await import('./operations/taxreductions.js');
|
|
1784
|
+
let input = {};
|
|
1785
|
+
if (opts.input) {
|
|
1786
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
1787
|
+
input = JSON.parse(raw);
|
|
1788
|
+
}
|
|
1789
|
+
const params = {
|
|
1790
|
+
...input,
|
|
1791
|
+
ReferenceNumber: opts.reference,
|
|
1792
|
+
ReferenceDocumentType: opts.documentType,
|
|
1793
|
+
TypeOfReduction: opts.type,
|
|
1794
|
+
CustomerName: opts.customerName,
|
|
1795
|
+
AskedAmount: opts.amount,
|
|
1796
|
+
};
|
|
1797
|
+
if (opts.property)
|
|
1798
|
+
params.PropertyDesignation = opts.property;
|
|
1799
|
+
if (!(await confirmMutation(`Create ${opts.type.toUpperCase()} tax reduction for ref ${opts.reference}`, opts, { TaxReduction: params }))) {
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
const data = await createTaxReduction(params);
|
|
1803
|
+
outputDetail(data, taxReductionDetailColumns, json());
|
|
1804
|
+
});
|
|
1805
|
+
// --- price lists ---
|
|
1806
|
+
const pricelists = program.command('pricelists').description('Price list operations');
|
|
1807
|
+
pricelists
|
|
1808
|
+
.command('list')
|
|
1809
|
+
.description('List price lists')
|
|
1810
|
+
.option('--page <number>', 'Page number', parseInt)
|
|
1811
|
+
.option('--limit <number>', 'Results per page', parseInt)
|
|
1812
|
+
.option('-a, --all', 'Fetch all pages')
|
|
1813
|
+
.action(async (opts) => {
|
|
1814
|
+
const { listPriceLists } = await import('./operations/pricelists.js');
|
|
1815
|
+
const data = await listPriceLists({
|
|
1816
|
+
page: opts.page,
|
|
1817
|
+
limit: opts.limit,
|
|
1818
|
+
all: opts.all,
|
|
1819
|
+
});
|
|
1820
|
+
const envelope = data;
|
|
1821
|
+
outputList(envelope.PriceLists ?? [], priceListListColumns, json(), data, envelope.MetaInformation);
|
|
1822
|
+
});
|
|
1823
|
+
pricelists
|
|
1824
|
+
.command('get <code>')
|
|
1825
|
+
.description('Get a single price list')
|
|
1826
|
+
.action(async (code) => {
|
|
1827
|
+
const { getPriceList } = await import('./operations/pricelists.js');
|
|
1828
|
+
const data = await getPriceList(code);
|
|
1829
|
+
outputDetail(data, priceListDetailColumns, json());
|
|
1830
|
+
});
|
|
1831
|
+
pricelists
|
|
1832
|
+
.command('create')
|
|
1833
|
+
.description('Create a price list')
|
|
1834
|
+
.requiredOption('--code <code>', 'Price list code')
|
|
1835
|
+
.requiredOption('--description <text>', 'Description')
|
|
1836
|
+
.option('--input <file>', 'Price list data as JSON file (or - for stdin)')
|
|
1837
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1838
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1839
|
+
.action(async (opts) => {
|
|
1840
|
+
const { createPriceList } = await import('./operations/pricelists.js');
|
|
1841
|
+
let input = {};
|
|
1842
|
+
if (opts.input) {
|
|
1843
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
1844
|
+
input = JSON.parse(raw);
|
|
1845
|
+
}
|
|
1846
|
+
const params = {
|
|
1847
|
+
...input,
|
|
1848
|
+
Code: opts.code,
|
|
1849
|
+
Description: opts.description,
|
|
1850
|
+
};
|
|
1851
|
+
if (!(await confirmMutation(`Create price list "${opts.code}"`, opts, { PriceList: params }))) {
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
const data = await createPriceList(params);
|
|
1855
|
+
outputDetail(data, priceListDetailColumns, json());
|
|
1856
|
+
});
|
|
1857
|
+
pricelists
|
|
1858
|
+
.command('update <code>')
|
|
1859
|
+
.description('Update a price list')
|
|
1860
|
+
.requiredOption('--input <file>', 'Price list data as JSON file (or - for stdin)')
|
|
1861
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1862
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1863
|
+
.action(async (code, opts) => {
|
|
1864
|
+
const { updatePriceList } = await import('./operations/pricelists.js');
|
|
1865
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
1866
|
+
const fields = JSON.parse(raw);
|
|
1867
|
+
if (!(await confirmMutation(`Update price list ${code}`, opts, { PriceList: fields }))) {
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
const data = await updatePriceList(code, fields);
|
|
1871
|
+
outputDetail(data, priceListDetailColumns, json());
|
|
1872
|
+
});
|
|
1873
|
+
// --- prices ---
|
|
1874
|
+
const prices = program.command('prices').description('Price operations within price lists');
|
|
1875
|
+
prices
|
|
1876
|
+
.command('list')
|
|
1877
|
+
.description('List prices in a price list')
|
|
1878
|
+
.requiredOption('--pricelist <code>', 'Price list code')
|
|
1879
|
+
.option('--article <number>', 'Filter by article number')
|
|
1880
|
+
.option('--page <number>', 'Page number', parseInt)
|
|
1881
|
+
.option('--limit <number>', 'Results per page', parseInt)
|
|
1882
|
+
.action(async (opts) => {
|
|
1883
|
+
const { listPrices } = await import('./operations/pricelists.js');
|
|
1884
|
+
const data = await listPrices({
|
|
1885
|
+
priceListCode: opts.pricelist,
|
|
1886
|
+
articleNumber: opts.article,
|
|
1887
|
+
page: opts.page,
|
|
1888
|
+
limit: opts.limit,
|
|
1889
|
+
});
|
|
1890
|
+
const envelope = data;
|
|
1891
|
+
outputList(envelope.Prices ?? [], priceListColumns, json(), data, envelope.MetaInformation);
|
|
1892
|
+
});
|
|
1893
|
+
prices
|
|
1894
|
+
.command('get')
|
|
1895
|
+
.description('Get a specific price')
|
|
1896
|
+
.requiredOption('--pricelist <code>', 'Price list code')
|
|
1897
|
+
.requiredOption('--article <number>', 'Article number')
|
|
1898
|
+
.option('--from-quantity <number>', 'From quantity (default 0)', parseInt)
|
|
1899
|
+
.action(async (opts) => {
|
|
1900
|
+
const { getPrice } = await import('./operations/pricelists.js');
|
|
1901
|
+
const data = await getPrice(opts.pricelist, opts.article, opts.fromQuantity);
|
|
1902
|
+
outputDetail(data, priceDetailColumns, json());
|
|
1903
|
+
});
|
|
1904
|
+
prices
|
|
1905
|
+
.command('update')
|
|
1906
|
+
.description('Update a price')
|
|
1907
|
+
.requiredOption('--pricelist <code>', 'Price list code')
|
|
1908
|
+
.requiredOption('--article <number>', 'Article number')
|
|
1909
|
+
.option('--from-quantity <number>', 'From quantity (default 0)', parseInt)
|
|
1910
|
+
.requiredOption('--input <file>', 'Price data as JSON file (or - for stdin)')
|
|
1911
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1912
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1913
|
+
.action(async (opts) => {
|
|
1914
|
+
const { updatePrice } = await import('./operations/pricelists.js');
|
|
1915
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
1916
|
+
const fields = JSON.parse(raw);
|
|
1917
|
+
if (!(await confirmMutation(`Update price ${opts.pricelist}/${opts.article}`, opts, {
|
|
1918
|
+
Price: fields,
|
|
1919
|
+
}))) {
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
const data = await updatePrice(opts.pricelist, opts.article, fields, opts.fromQuantity);
|
|
1923
|
+
outputDetail(data, priceDetailColumns, json());
|
|
1924
|
+
});
|
|
993
1925
|
// Error handling
|
|
994
1926
|
program.exitOverride();
|
|
995
1927
|
try {
|