noxctl 0.4.1 → 0.5.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 +3 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +117 -2
- package/dist/cli.js.map +1 -1
- package/dist/fortnox-client.d.ts +29 -1
- package/dist/fortnox-client.d.ts.map +1 -1
- package/dist/fortnox-client.js +177 -59
- package/dist/fortnox-client.js.map +1 -1
- package/dist/operations/invoices.d.ts +38 -0
- package/dist/operations/invoices.d.ts.map +1 -1
- package/dist/operations/invoices.js +54 -3
- package/dist/operations/invoices.js.map +1 -1
- package/dist/tools/invoices.d.ts.map +1 -1
- package/dist/tools/invoices.js +136 -1
- package/dist/tools/invoices.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -261,6 +261,7 @@ Every operation is available both as a CLI command and as an MCP tool. The CLI i
|
|
|
261
261
|
| `noxctl invoices create --customer <number> --input <file>` | `fortnox_create_invoice` | Create an invoice (mutation) |
|
|
262
262
|
| `noxctl invoices update <docNumber> --input <file>` | `fortnox_update_invoice` | Update an invoice that has not been bookkeept (mutation) |
|
|
263
263
|
| `noxctl invoices send <docNumber> [--method email\|print\|einvoice] [--subject <s>] [--body <s>] [--bcc <email>]` | `fortnox_send_invoice` | Send via email (default), print, or e-invoice (mutation) |
|
|
264
|
+
| `noxctl invoices pdf <docNumber> [--file <path>\|-] [--mark-sent]` | `fortnox_invoice_pdf` | Download the invoice PDF (via `/preview`, no side effect). `--mark-sent` also flags it as sent afterwards (mutation) |
|
|
264
265
|
| `noxctl invoices bookkeep <docNumber>` | `fortnox_bookkeep_invoice` | Bookkeep an invoice (mutation) |
|
|
265
266
|
| `noxctl invoices credit <docNumber>` | `fortnox_credit_invoice` | Credit an invoice (mutation) |
|
|
266
267
|
|
|
@@ -480,6 +481,8 @@ CLI:
|
|
|
480
481
|
noxctl invoices send 1001 # prompts: "Send invoice 1001 via email. Continue? [y/N]"
|
|
481
482
|
noxctl invoices send 1001 --yes # skip prompt (scripting/AI)
|
|
482
483
|
noxctl invoices send 1001 --dry-run # preview without sending
|
|
484
|
+
noxctl invoices pdf 1001 # read-only, no prompt
|
|
485
|
+
noxctl invoices pdf 1001 --mark-sent # prompts: also flags the invoice as sent
|
|
483
486
|
noxctl customers update 42 --input customer.json # prompts for confirmation
|
|
484
487
|
noxctl vouchers create --input voucher.json --dry-run # preview payload
|
|
485
488
|
```
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAcA,OAAO,EAOL,KAAK,eAAe,EACrB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAcA,OAAO,EAOL,KAAK,eAAe,EACrB,MAAM,eAAe,CAAC;AAmIvB,wBAAgB,sBAAsB,IAAI,eAAe,CAExD"}
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command, Option } from 'commander';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
import { isJsonMode, outputList, outputDetail, outputConfirmation, formatTaxReport, formatFinancialReport, errorEnvelope, } from './formatter.js';
|
|
6
6
|
import { readActivePointer, readActivePointerOutcome, writeActivePointer, deleteActivePointer, readProfileIndex, resolveProfile, } from './profiles.js';
|
|
@@ -9,10 +9,20 @@ import { applyPeriod } from './date-periods.js';
|
|
|
9
9
|
import { DEFAULT_PROFILE, InvalidProfileNameError } from './profile-name.js';
|
|
10
10
|
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, financialYearListColumns, financialYearDetailColumns, lockedPeriodDetailColumns, contractListColumns, contractDetailColumns, topCustomerColumns, monthlyRevenueColumns, voucherAttachmentColumns, employeeListColumns, employeeDetailColumns, salaryTransactionListColumns, salaryTransactionDetailColumns, attendanceTransactionListColumns, attendanceTransactionDetailColumns, absenceTransactionListColumns, absenceTransactionDetailColumns, scheduleTimeDetailColumns, } from './views.js';
|
|
11
11
|
const program = new Command();
|
|
12
|
+
// A closed downstream pipe (`noxctl ... | head`, or a reader that exits early)
|
|
13
|
+
// makes stdout emit EPIPE. That is normal for a CLI, and without this listener
|
|
14
|
+
// Node reports it as an uncaught exception with a stack trace, bypassing the
|
|
15
|
+
// command's own error handling.
|
|
16
|
+
process.stdout.on('error', (err) => {
|
|
17
|
+
if (err.code === 'EPIPE') {
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
throw err;
|
|
21
|
+
});
|
|
12
22
|
program
|
|
13
23
|
.name('noxctl')
|
|
14
24
|
.description('CLI and MCP server for Fortnox accounting')
|
|
15
|
-
.version('0.
|
|
25
|
+
.version('0.5.0')
|
|
16
26
|
.addOption(new Option('-o, --output <format>', 'Output format (default: table on TTY, json when piped)')
|
|
17
27
|
.choices(['json', 'table'])
|
|
18
28
|
.default(undefined))
|
|
@@ -1114,6 +1124,111 @@ invoices
|
|
|
1114
1124
|
const data = await sendInvoice(documentNumber, opts.method, emailOptions);
|
|
1115
1125
|
outputConfirmation(`Invoice ${documentNumber} sent via ${opts.method}.`, json(), data, invoiceConfirmColumns, 'Invoice');
|
|
1116
1126
|
});
|
|
1127
|
+
invoices
|
|
1128
|
+
.command('pdf <documentNumber>')
|
|
1129
|
+
.description('Download an invoice as a PDF')
|
|
1130
|
+
// Note: -o/--output is already taken globally for the output *format*
|
|
1131
|
+
// (json|table), so the destination path is --file.
|
|
1132
|
+
.option('-f, --file <path>', 'Write the PDF here (- for stdout)')
|
|
1133
|
+
.option('--mark-sent', 'Also flag the invoice as sent in Fortnox (uses /print)')
|
|
1134
|
+
.option('-y, --yes', 'Skip confirmation prompt (only needed with --mark-sent)')
|
|
1135
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
1136
|
+
.addHelpText('after', `
|
|
1137
|
+
The PDF always comes from Fortnox's /preview endpoint, which does not change the
|
|
1138
|
+
invoice. --mark-sent additionally calls /print afterwards to set Sent=true — the
|
|
1139
|
+
file is written first, so a failed write never leaves an invoice flagged as sent
|
|
1140
|
+
with no PDF to show for it.
|
|
1141
|
+
|
|
1142
|
+
Without --file the PDF is written to invoice-<documentNumber>.pdf in the current
|
|
1143
|
+
directory.
|
|
1144
|
+
|
|
1145
|
+
When writing to a file, --mark-sent replaces it with the document /print itself
|
|
1146
|
+
returned, so the saved copy matches the version that was marked. Streaming to
|
|
1147
|
+
stdout cannot do that — bytes already written cannot be recalled — so with
|
|
1148
|
+
--file - the streamed document is the /preview render.
|
|
1149
|
+
|
|
1150
|
+
Examples:
|
|
1151
|
+
noxctl invoices pdf 28
|
|
1152
|
+
noxctl invoices pdf 28 --file ~/Desktop/faktura-28.pdf
|
|
1153
|
+
noxctl invoices pdf 28 --file - > faktura.pdf
|
|
1154
|
+
noxctl invoices pdf 28 --mark-sent --yes`)
|
|
1155
|
+
.action(async (documentNumber, opts) => {
|
|
1156
|
+
const { getInvoicePdf, markInvoicePrinted } = await import('./operations/invoices.js');
|
|
1157
|
+
const toStdout = opts.file === '-';
|
|
1158
|
+
// Only an *explicit* --output json conflicts here. json() alone is not the
|
|
1159
|
+
// test: it defaults to true whenever stdout is piped, which is exactly how
|
|
1160
|
+
// `--file -` is meant to be used.
|
|
1161
|
+
if (toStdout && program.opts().output === 'json') {
|
|
1162
|
+
throw new Error('--file - writes raw PDF bytes to stdout and cannot be combined with --output json.');
|
|
1163
|
+
}
|
|
1164
|
+
// Only --mark-sent mutates the invoice; a plain download needs no confirmation.
|
|
1165
|
+
if (opts.markSent || opts.dryRun) {
|
|
1166
|
+
const action = opts.markSent
|
|
1167
|
+
? `Download invoice ${documentNumber} as PDF and flag it as sent`
|
|
1168
|
+
: `Download invoice ${documentNumber} as PDF`;
|
|
1169
|
+
if (!(await confirmMutation(action, opts))) {
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
const pdf = await getInvoicePdf(documentNumber);
|
|
1174
|
+
if (toStdout) {
|
|
1175
|
+
// Wait for the bytes to be flushed before mutating anything: a closed or
|
|
1176
|
+
// broken pipe must not leave the invoice marked as sent.
|
|
1177
|
+
await new Promise((resolve, reject) => {
|
|
1178
|
+
process.stdout.write(pdf, (err) => (err ? reject(err) : resolve()));
|
|
1179
|
+
});
|
|
1180
|
+
if (opts.markSent)
|
|
1181
|
+
await markInvoicePrinted(documentNumber);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
const path = opts.file ?? `invoice-${documentNumber}.pdf`;
|
|
1185
|
+
writeFileSync(path, pdf);
|
|
1186
|
+
// Only now that the PDF is safely on disk do we change Fortnox. If that
|
|
1187
|
+
// fails, the download still succeeded — say so, or the user is left
|
|
1188
|
+
// thinking the whole command achieved nothing.
|
|
1189
|
+
let printed;
|
|
1190
|
+
if (opts.markSent) {
|
|
1191
|
+
try {
|
|
1192
|
+
printed = await markInvoicePrinted(documentNumber);
|
|
1193
|
+
}
|
|
1194
|
+
catch (err) {
|
|
1195
|
+
throw new Error(`Invoice ${documentNumber} saved to ${path} (${pdf.length} bytes), but marking it as sent failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
// Prefer the document /print actually produced, so the saved copy matches
|
|
1199
|
+
// the version that was marked as sent. Best-effort: the /preview copy is
|
|
1200
|
+
// already written and the invoice is already flagged, so a failure here is
|
|
1201
|
+
// reported as a note rather than raised as a failed operation.
|
|
1202
|
+
let bytes = pdf.length;
|
|
1203
|
+
let note = '';
|
|
1204
|
+
if (printed?.pdf) {
|
|
1205
|
+
try {
|
|
1206
|
+
writeFileSync(path, printed.pdf);
|
|
1207
|
+
bytes = printed.pdf.length;
|
|
1208
|
+
}
|
|
1209
|
+
catch (err) {
|
|
1210
|
+
note += ` Saved file is the /preview copy; rewriting it with the printed version failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
// Only claim the invoice is sent if Fortnox actually said so.
|
|
1214
|
+
if (printed && !printed.confirmed) {
|
|
1215
|
+
note += ` ${String(printed.invoice.Note)}`;
|
|
1216
|
+
}
|
|
1217
|
+
else if (printed && printed.invoice.Sent === true) {
|
|
1218
|
+
note += ' Marked as sent.';
|
|
1219
|
+
}
|
|
1220
|
+
else if (printed) {
|
|
1221
|
+
note += ' Warning: Fortnox still reports this invoice as not sent.';
|
|
1222
|
+
}
|
|
1223
|
+
outputConfirmation(`Invoice ${documentNumber} saved to ${path} (${bytes} bytes).${note}`, json(), {
|
|
1224
|
+
DocumentNumber: documentNumber,
|
|
1225
|
+
Path: path,
|
|
1226
|
+
Bytes: bytes,
|
|
1227
|
+
// Report what Fortnox says the invoice's state is, not what we asked
|
|
1228
|
+
// for; undefined means "not checked" or "could not be confirmed".
|
|
1229
|
+
Sent: printed?.confirmed ? printed.invoice.Sent : undefined,
|
|
1230
|
+
});
|
|
1231
|
+
});
|
|
1117
1232
|
invoices
|
|
1118
1233
|
.command('bookkeep <documentNumber>')
|
|
1119
1234
|
.description('Bookkeep an invoice')
|