noxctl 0.9.0 → 0.10.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 +59 -1
- package/dist/api-coverage.d.ts +48 -0
- package/dist/api-coverage.d.ts.map +1 -0
- package/dist/api-coverage.js +175 -0
- package/dist/api-coverage.js.map +1 -0
- package/dist/cli-validators.d.ts +3 -0
- package/dist/cli-validators.d.ts.map +1 -0
- package/dist/cli-validators.js +10 -0
- package/dist/cli-validators.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1015 -13
- package/dist/cli.js.map +1 -1
- package/dist/fortnox-client.d.ts.map +1 -1
- package/dist/fortnox-client.js +1 -0
- package/dist/fortnox-client.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/operations/absencetransactions.d.ts +3 -1
- package/dist/operations/absencetransactions.d.ts.map +1 -1
- package/dist/operations/absencetransactions.js +11 -1
- package/dist/operations/absencetransactions.js.map +1 -1
- package/dist/operations/accounts.d.ts +5 -1
- package/dist/operations/accounts.d.ts.map +1 -1
- package/dist/operations/accounts.js +24 -2
- package/dist/operations/accounts.js.map +1 -1
- package/dist/operations/accruals.d.ts +65 -0
- package/dist/operations/accruals.d.ts.map +1 -0
- package/dist/operations/accruals.js +81 -0
- package/dist/operations/accruals.js.map +1 -0
- package/dist/operations/articles.d.ts +2 -1
- package/dist/operations/articles.d.ts.map +1 -1
- package/dist/operations/articles.js +7 -2
- package/dist/operations/articles.js.map +1 -1
- package/dist/operations/attendancetransactions.d.ts +3 -1
- package/dist/operations/attendancetransactions.d.ts.map +1 -1
- package/dist/operations/attendancetransactions.js +11 -1
- package/dist/operations/attendancetransactions.js.map +1 -1
- package/dist/operations/customers.d.ts +2 -1
- package/dist/operations/customers.d.ts.map +1 -1
- package/dist/operations/customers.js +7 -2
- package/dist/operations/customers.js.map +1 -1
- package/dist/operations/files.d.ts +85 -0
- package/dist/operations/files.d.ts.map +1 -0
- package/dist/operations/files.js +88 -0
- package/dist/operations/files.js.map +1 -0
- package/dist/operations/financial-years.d.ts +2 -1
- package/dist/operations/financial-years.d.ts.map +1 -1
- package/dist/operations/financial-years.js +9 -2
- package/dist/operations/financial-years.js.map +1 -1
- package/dist/operations/general-ledger.d.ts +24 -0
- package/dist/operations/general-ledger.d.ts.map +1 -0
- package/dist/operations/general-ledger.js +66 -0
- package/dist/operations/general-ledger.js.map +1 -0
- package/dist/operations/index.d.ts +376 -0
- package/dist/operations/index.d.ts.map +1 -1
- package/dist/operations/index.js +8 -0
- package/dist/operations/index.js.map +1 -1
- package/dist/operations/invoice-payments.d.ts +2 -1
- package/dist/operations/invoice-payments.d.ts.map +1 -1
- package/dist/operations/invoice-payments.js +6 -1
- package/dist/operations/invoice-payments.js.map +1 -1
- package/dist/operations/invoices.d.ts +12 -1
- package/dist/operations/invoices.d.ts.map +1 -1
- package/dist/operations/invoices.js +51 -6
- package/dist/operations/invoices.js.map +1 -1
- package/dist/operations/offers.d.ts +5 -1
- package/dist/operations/offers.d.ts.map +1 -1
- package/dist/operations/offers.js +19 -1
- package/dist/operations/offers.js.map +1 -1
- package/dist/operations/orders.d.ts +5 -1
- package/dist/operations/orders.d.ts.map +1 -1
- package/dist/operations/orders.js +26 -2
- package/dist/operations/orders.js.map +1 -1
- package/dist/operations/pricelists.d.ts +4 -2
- package/dist/operations/pricelists.d.ts.map +1 -1
- package/dist/operations/pricelists.js +23 -9
- package/dist/operations/pricelists.js.map +1 -1
- package/dist/operations/projects.d.ts +2 -1
- package/dist/operations/projects.d.ts.map +1 -1
- package/dist/operations/projects.js +5 -2
- package/dist/operations/projects.js.map +1 -1
- package/dist/operations/reference-data.d.ts +243 -0
- package/dist/operations/reference-data.d.ts.map +1 -0
- package/dist/operations/reference-data.js +113 -0
- package/dist/operations/reference-data.js.map +1 -0
- package/dist/operations/salarytransactions.d.ts +2 -1
- package/dist/operations/salarytransactions.d.ts.map +1 -1
- package/dist/operations/salarytransactions.js +6 -1
- package/dist/operations/salarytransactions.js.map +1 -1
- package/dist/operations/supplier-invoice-payments.d.ts +3 -1
- package/dist/operations/supplier-invoice-payments.d.ts.map +1 -1
- package/dist/operations/supplier-invoice-payments.js +11 -1
- package/dist/operations/supplier-invoice-payments.js.map +1 -1
- package/dist/operations/supplier-invoices.d.ts +21 -1
- package/dist/operations/supplier-invoices.d.ts.map +1 -1
- package/dist/operations/supplier-invoices.js +68 -1
- package/dist/operations/supplier-invoices.js.map +1 -1
- package/dist/operations/taxreductions.d.ts +3 -1
- package/dist/operations/taxreductions.d.ts.map +1 -1
- package/dist/operations/taxreductions.js +18 -2
- package/dist/operations/taxreductions.js.map +1 -1
- package/dist/operations/vouchers.d.ts +3 -1
- package/dist/operations/vouchers.d.ts.map +1 -1
- package/dist/operations/vouchers.js +12 -1
- package/dist/operations/vouchers.js.map +1 -1
- package/dist/safe-file-output.d.ts +4 -0
- package/dist/safe-file-output.d.ts.map +1 -0
- package/dist/safe-file-output.js +48 -0
- package/dist/safe-file-output.js.map +1 -0
- package/dist/schema-audit-mappings.d.ts +6 -5
- package/dist/schema-audit-mappings.d.ts.map +1 -1
- package/dist/schema-audit-mappings.js +159 -10
- package/dist/schema-audit-mappings.js.map +1 -1
- package/dist/schema-audit.d.ts +30 -1
- package/dist/schema-audit.d.ts.map +1 -1
- package/dist/schema-audit.js +288 -14
- package/dist/schema-audit.js.map +1 -1
- package/dist/sie.d.ts +51 -0
- package/dist/sie.d.ts.map +1 -0
- package/dist/sie.js +355 -0
- package/dist/sie.js.map +1 -0
- package/dist/tools/absencetransactions.d.ts.map +1 -1
- package/dist/tools/absencetransactions.js +30 -3
- package/dist/tools/absencetransactions.js.map +1 -1
- package/dist/tools/accruals.d.ts +18 -0
- package/dist/tools/accruals.d.ts.map +1 -0
- package/dist/tools/accruals.js +169 -0
- package/dist/tools/accruals.js.map +1 -0
- package/dist/tools/articles.d.ts.map +1 -1
- package/dist/tools/articles.js +17 -2
- package/dist/tools/articles.js.map +1 -1
- package/dist/tools/attendancetransactions.d.ts.map +1 -1
- package/dist/tools/attendancetransactions.js +30 -3
- package/dist/tools/attendancetransactions.js.map +1 -1
- package/dist/tools/bookkeeping.d.ts.map +1 -1
- package/dist/tools/bookkeeping.js +93 -52
- package/dist/tools/bookkeeping.js.map +1 -1
- package/dist/tools/contracts.js +1 -1
- package/dist/tools/contracts.js.map +1 -1
- package/dist/tools/customers.d.ts.map +1 -1
- package/dist/tools/customers.js +111 -20
- package/dist/tools/customers.js.map +1 -1
- package/dist/tools/files.d.ts +4 -0
- package/dist/tools/files.d.ts.map +1 -0
- package/dist/tools/files.js +120 -0
- package/dist/tools/files.js.map +1 -0
- package/dist/tools/financial-years.d.ts.map +1 -1
- package/dist/tools/financial-years.js +18 -2
- package/dist/tools/financial-years.js.map +1 -1
- package/dist/tools/general-ledger.d.ts +4 -0
- package/dist/tools/general-ledger.d.ts.map +1 -0
- package/dist/tools/general-ledger.js +61 -0
- package/dist/tools/general-ledger.js.map +1 -0
- package/dist/tools/invoice-payments.d.ts.map +1 -1
- package/dist/tools/invoice-payments.js +34 -1
- package/dist/tools/invoice-payments.js.map +1 -1
- package/dist/tools/invoices.d.ts.map +1 -1
- package/dist/tools/invoices.js +145 -55
- package/dist/tools/invoices.js.map +1 -1
- package/dist/tools/offers.d.ts.map +1 -1
- package/dist/tools/offers.js +68 -1
- package/dist/tools/offers.js.map +1 -1
- package/dist/tools/orders.d.ts.map +1 -1
- package/dist/tools/orders.js +68 -1
- package/dist/tools/orders.js.map +1 -1
- package/dist/tools/pricelists.d.ts.map +1 -1
- package/dist/tools/pricelists.js +35 -3
- package/dist/tools/pricelists.js.map +1 -1
- package/dist/tools/projects.d.ts.map +1 -1
- package/dist/tools/projects.js +14 -2
- package/dist/tools/projects.js.map +1 -1
- package/dist/tools/recurrings.js +1 -1
- package/dist/tools/recurrings.js.map +1 -1
- package/dist/tools/reference-data.d.ts +17 -0
- package/dist/tools/reference-data.d.ts.map +1 -0
- package/dist/tools/reference-data.js +124 -0
- package/dist/tools/reference-data.js.map +1 -0
- package/dist/tools/salarytransactions.d.ts.map +1 -1
- package/dist/tools/salarytransactions.js +27 -1
- package/dist/tools/salarytransactions.js.map +1 -1
- package/dist/tools/supplier-invoice-payments.d.ts.map +1 -1
- package/dist/tools/supplier-invoice-payments.js +34 -1
- package/dist/tools/supplier-invoice-payments.js.map +1 -1
- package/dist/tools/supplier-invoices.d.ts.map +1 -1
- package/dist/tools/supplier-invoices.js +147 -5
- package/dist/tools/supplier-invoices.js.map +1 -1
- package/dist/tools/taxreductions.d.ts.map +1 -1
- package/dist/tools/taxreductions.js +33 -2
- package/dist/tools/taxreductions.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/views.d.ts +3 -0
- package/dist/views.d.ts.map +1 -1
- package/dist/views.js +29 -1
- package/dist/views.js.map +1 -1
- package/package.json +8 -5
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command, Option } from 'commander';
|
|
3
|
+
import { parsePositiveInteger } from './cli-validators.js';
|
|
3
4
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
4
5
|
import { createInterface } from 'node:readline/promises';
|
|
5
6
|
import { isJsonMode, outputList, outputDetail, outputConfirmation, formatTaxReport, formatFinancialReport, errorEnvelope, } from './formatter.js';
|
|
@@ -8,7 +9,7 @@ import { setResolvedProfile } from './auth.js';
|
|
|
8
9
|
import { applyPeriod } from './date-periods.js';
|
|
9
10
|
import { DEFAULT_PROFILE, InvalidProfileNameError } from './profile-name.js';
|
|
10
11
|
import { VERSION } from './version.js';
|
|
11
|
-
import { invoiceListColumns, invoiceDetailColumns, invoiceConfirmColumns, invoiceAttachmentColumns, invoiceAttachmentListColumns, 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, recurringListColumns, recurringDetailColumns, invoiceRequestListColumns, invoiceRequestDetailColumns, topCustomerColumns, monthlyRevenueColumns, voucherAttachmentColumns, employeeListColumns, employeeDetailColumns, salaryTransactionListColumns, salaryTransactionDetailColumns, attendanceTransactionListColumns, attendanceTransactionDetailColumns, absenceTransactionListColumns, absenceTransactionDetailColumns, scheduleTimeDetailColumns, } from './views.js';
|
|
12
|
+
import { invoiceListColumns, invoiceDetailColumns, invoiceConfirmColumns, invoiceAttachmentColumns, invoiceAttachmentListColumns, customerListColumns, customerDetailColumns, voucherListColumns, voucherDetailColumns, voucherRowColumns, accountListColumns, companyDetailColumns, articleListColumns, articleDetailColumns, supplierListColumns, supplierDetailColumns, supplierInvoiceListColumns, supplierInvoiceDetailColumns, supplierInvoiceConfirmColumns, supplierInvoiceAttachmentColumns, invoicePaymentListColumns, invoicePaymentDetailColumns, supplierInvoicePaymentListColumns, supplierInvoicePaymentDetailColumns, offerListColumns, offerDetailColumns, offerConfirmColumns, orderListColumns, orderDetailColumns, orderConfirmColumns, projectListColumns, projectDetailColumns, costCenterListColumns, costCenterDetailColumns, taxReductionListColumns, taxReductionDetailColumns, priceListListColumns, priceListDetailColumns, priceListColumns, priceDetailColumns, financialYearListColumns, financialYearDetailColumns, lockedPeriodDetailColumns, contractListColumns, contractDetailColumns, recurringListColumns, recurringDetailColumns, invoiceRequestListColumns, invoiceRequestDetailColumns, topCustomerColumns, monthlyRevenueColumns, voucherAttachmentColumns, employeeListColumns, employeeDetailColumns, salaryTransactionListColumns, salaryTransactionDetailColumns, attendanceTransactionListColumns, attendanceTransactionDetailColumns, absenceTransactionListColumns, absenceTransactionDetailColumns, scheduleTimeDetailColumns, referenceDataColumns, generalLedgerColumns, } from './views.js';
|
|
12
13
|
const program = new Command();
|
|
13
14
|
// A closed downstream pipe (`noxctl ... | head`, or a reader that exits early)
|
|
14
15
|
// makes stdout emit EPIPE. That is normal for a CLI, and without this listener
|
|
@@ -1047,9 +1048,9 @@ keychain
|
|
|
1047
1048
|
console.log(`Deleted ${deleted} login-keychain entr${deleted === 1 ? 'y' : 'ies'}.`);
|
|
1048
1049
|
console.log('Credentials now live only in the YubiKey-locked keychain.');
|
|
1049
1050
|
});
|
|
1050
|
-
// --- serve
|
|
1051
|
+
// --- serve ---
|
|
1051
1052
|
program
|
|
1052
|
-
.command('serve'
|
|
1053
|
+
.command('serve')
|
|
1053
1054
|
.description('Start the MCP server (stdio transport)')
|
|
1054
1055
|
.action(async () => {
|
|
1055
1056
|
const { startMcpServer } = await import('./index.js');
|
|
@@ -1157,6 +1158,51 @@ invoices
|
|
|
1157
1158
|
const data = await sendInvoice(documentNumber, opts.method, emailOptions);
|
|
1158
1159
|
outputConfirmation(`Invoice ${documentNumber} sent via ${opts.method}.`, json(), data, invoiceConfirmColumns, 'Invoice');
|
|
1159
1160
|
});
|
|
1161
|
+
for (const action of [
|
|
1162
|
+
{ command: 'cancel', label: 'Cancel', exportName: 'cancelInvoice' },
|
|
1163
|
+
{ command: 'eprint', label: 'Send via e-print', exportName: 'eprintInvoice' },
|
|
1164
|
+
{
|
|
1165
|
+
command: 'external-print',
|
|
1166
|
+
label: 'Mark as externally printed',
|
|
1167
|
+
exportName: 'externalPrintInvoice',
|
|
1168
|
+
},
|
|
1169
|
+
]) {
|
|
1170
|
+
invoices
|
|
1171
|
+
.command(`${action.command} <documentNumber>`)
|
|
1172
|
+
.description(`${action.label} an invoice`)
|
|
1173
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1174
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
1175
|
+
.action(async (documentNumber, opts) => {
|
|
1176
|
+
const operations = await import('./operations/invoices.js');
|
|
1177
|
+
if (!(await confirmMutation(`${action.label} invoice ${documentNumber}`, opts)))
|
|
1178
|
+
return;
|
|
1179
|
+
const data = await operations[action.exportName](documentNumber);
|
|
1180
|
+
outputConfirmation(`Invoice ${documentNumber}: ${action.command} completed.`, json(), data, invoiceConfirmColumns, 'Invoice');
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
invoices
|
|
1184
|
+
.command('reminder-pdf <documentNumber>')
|
|
1185
|
+
.description('Print an invoice reminder and save the returned PDF')
|
|
1186
|
+
.option('-f, --file <path>', 'Write the PDF here')
|
|
1187
|
+
.option('--overwrite', 'Allow replacing an existing regular file')
|
|
1188
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1189
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
1190
|
+
.action(async (documentNumber, opts) => {
|
|
1191
|
+
const { getInvoiceReminderPdf } = await import('./operations/invoices.js');
|
|
1192
|
+
if (!(await confirmMutation(`Print reminder for invoice ${documentNumber}`, opts)))
|
|
1193
|
+
return;
|
|
1194
|
+
const pdf = await getInvoiceReminderPdf(documentNumber);
|
|
1195
|
+
if (!pdf) {
|
|
1196
|
+
outputConfirmation(`Invoice reminder ${documentNumber} printed without a PDF response.`, json(), {});
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const { writeBinaryFile } = await import('./safe-file-output.js');
|
|
1200
|
+
const path = writeBinaryFile(opts.file ?? `invoice-reminder-${documentNumber}.pdf`, pdf, opts.overwrite);
|
|
1201
|
+
outputConfirmation(`Invoice reminder saved to ${path}.`, json(), {
|
|
1202
|
+
Path: path,
|
|
1203
|
+
Bytes: pdf.length,
|
|
1204
|
+
});
|
|
1205
|
+
});
|
|
1160
1206
|
invoices
|
|
1161
1207
|
.command('pdf <documentNumber>')
|
|
1162
1208
|
.description('Download an invoice as a PDF')
|
|
@@ -1418,6 +1464,59 @@ accounts
|
|
|
1418
1464
|
});
|
|
1419
1465
|
outputList(data.Accounts ?? [], accountListColumns, json(), data, data.MetaInformation);
|
|
1420
1466
|
});
|
|
1467
|
+
accounts
|
|
1468
|
+
.command('get <number>')
|
|
1469
|
+
.description('Get one account')
|
|
1470
|
+
.action(async (number) => {
|
|
1471
|
+
const { getAccount } = await import('./operations/accounts.js');
|
|
1472
|
+
const data = await getAccount(Number(number));
|
|
1473
|
+
outputDetail(data, accountListColumns, json(), 'Account');
|
|
1474
|
+
});
|
|
1475
|
+
accounts
|
|
1476
|
+
.command('create')
|
|
1477
|
+
.description('Create an account')
|
|
1478
|
+
.requiredOption('--number <number>', 'Account number', parseInt)
|
|
1479
|
+
.requiredOption('--description <text>', 'Account description')
|
|
1480
|
+
.option('--input <file>', 'Additional account data as JSON file (or - for stdin)')
|
|
1481
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1482
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1483
|
+
.action(async (opts) => {
|
|
1484
|
+
const { createAccount } = await import('./operations/accounts.js');
|
|
1485
|
+
const extra = opts.input
|
|
1486
|
+
? JSON.parse(opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8'))
|
|
1487
|
+
: {};
|
|
1488
|
+
const fields = { ...extra, Number: opts.number, Description: opts.description };
|
|
1489
|
+
if (!(await confirmMutation(`Create account ${opts.number}`, opts, { Account: fields })))
|
|
1490
|
+
return;
|
|
1491
|
+
const data = await createAccount(fields);
|
|
1492
|
+
outputDetail(data, accountListColumns, json(), 'Account');
|
|
1493
|
+
});
|
|
1494
|
+
accounts
|
|
1495
|
+
.command('update <number>')
|
|
1496
|
+
.description('Update an account')
|
|
1497
|
+
.requiredOption('--input <file>', 'Account data as JSON file (or - for stdin)')
|
|
1498
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1499
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1500
|
+
.action(async (number, opts) => {
|
|
1501
|
+
const { updateAccount } = await import('./operations/accounts.js');
|
|
1502
|
+
const fields = JSON.parse(opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8'));
|
|
1503
|
+
if (!(await confirmMutation(`Update account ${number}`, opts, { Account: fields })))
|
|
1504
|
+
return;
|
|
1505
|
+
const data = await updateAccount(Number(number), fields);
|
|
1506
|
+
outputDetail(data, accountListColumns, json(), 'Account');
|
|
1507
|
+
});
|
|
1508
|
+
accounts
|
|
1509
|
+
.command('delete <number>')
|
|
1510
|
+
.description('Delete an account')
|
|
1511
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1512
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1513
|
+
.action(async (number, opts) => {
|
|
1514
|
+
const { deleteAccount } = await import('./operations/accounts.js');
|
|
1515
|
+
if (!(await confirmMutation(`Delete account ${number}`, opts)))
|
|
1516
|
+
return;
|
|
1517
|
+
await deleteAccount(Number(number));
|
|
1518
|
+
outputConfirmation(`Account ${number} deleted.`, json(), { Number: Number(number), deleted: true }, undefined, 'Account');
|
|
1519
|
+
});
|
|
1421
1520
|
// --- customers ---
|
|
1422
1521
|
const customers = program.command('customers').description('Customer operations');
|
|
1423
1522
|
customers
|
|
@@ -1479,7 +1578,7 @@ customers
|
|
|
1479
1578
|
.option('--dry-run', 'Preview the request without sending it')
|
|
1480
1579
|
.addHelpText('after', `
|
|
1481
1580
|
Examples:
|
|
1482
|
-
echo '{"Email":"new@acme.se","
|
|
1581
|
+
echo '{"Email":"new@acme.se","Phone1":"08-123456"}' | noxctl customers update 25 --input - --yes`)
|
|
1483
1582
|
.action(async (customerNumber, opts) => {
|
|
1484
1583
|
const { updateCustomer } = await import('./operations/customers.js');
|
|
1485
1584
|
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
@@ -1490,6 +1589,18 @@ Examples:
|
|
|
1490
1589
|
const data = await updateCustomer(customerNumber, fields);
|
|
1491
1590
|
outputDetail(data, customerDetailColumns, json(), 'Customer');
|
|
1492
1591
|
});
|
|
1592
|
+
customers
|
|
1593
|
+
.command('delete <customerNumber>')
|
|
1594
|
+
.description('Delete a customer')
|
|
1595
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1596
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1597
|
+
.action(async (customerNumber, opts) => {
|
|
1598
|
+
const { deleteCustomer } = await import('./operations/customers.js');
|
|
1599
|
+
if (!(await confirmMutation(`Delete customer ${customerNumber}`, opts)))
|
|
1600
|
+
return;
|
|
1601
|
+
await deleteCustomer(customerNumber);
|
|
1602
|
+
outputConfirmation(`Customer ${customerNumber} deleted.`, json(), { CustomerNumber: customerNumber, deleted: true }, undefined, 'Customer');
|
|
1603
|
+
});
|
|
1493
1604
|
// --- articles ---
|
|
1494
1605
|
const articles = program.command('articles').description('Article operations');
|
|
1495
1606
|
articles
|
|
@@ -1565,6 +1676,18 @@ Examples:
|
|
|
1565
1676
|
const data = await updateArticle(articleNumber, fields);
|
|
1566
1677
|
outputDetail(data, articleDetailColumns, json(), 'Article');
|
|
1567
1678
|
});
|
|
1679
|
+
articles
|
|
1680
|
+
.command('delete <articleNumber>')
|
|
1681
|
+
.description('Delete an article')
|
|
1682
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1683
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1684
|
+
.action(async (articleNumber, opts) => {
|
|
1685
|
+
const { deleteArticle } = await import('./operations/articles.js');
|
|
1686
|
+
if (!(await confirmMutation(`Delete article ${articleNumber}`, opts)))
|
|
1687
|
+
return;
|
|
1688
|
+
await deleteArticle(articleNumber);
|
|
1689
|
+
outputConfirmation(`Article ${articleNumber} deleted.`, json(), { ArticleNumber: articleNumber, deleted: true }, undefined, 'Article');
|
|
1690
|
+
});
|
|
1568
1691
|
// --- suppliers ---
|
|
1569
1692
|
const suppliers = program.command('suppliers').description('Supplier operations');
|
|
1570
1693
|
suppliers
|
|
@@ -1645,7 +1768,7 @@ const supplierInvoices = program
|
|
|
1645
1768
|
supplierInvoices
|
|
1646
1769
|
.command('list')
|
|
1647
1770
|
.description('List/filter supplier invoices')
|
|
1648
|
-
.option('--filter <filter>', 'Filter: cancelled, fullypaid, unpaid, unpaidoverdue, unbooked, pendingpayment')
|
|
1771
|
+
.option('--filter <filter>', 'Filter: cancelled, fullypaid, unpaid, unpaidoverdue, unbooked, pendingpayment, authorizepending')
|
|
1649
1772
|
.option('--supplier <number>', 'Filter by supplier number')
|
|
1650
1773
|
.option('--from <date>', 'From date (YYYY-MM-DD)')
|
|
1651
1774
|
.option('--to <date>', 'To date (YYYY-MM-DD)')
|
|
@@ -1710,6 +1833,98 @@ supplierInvoices
|
|
|
1710
1833
|
const data = await bookkeepSupplierInvoice(givenNumber);
|
|
1711
1834
|
outputConfirmation(`Supplier invoice ${givenNumber} bookkeept.`, json(), data, supplierInvoiceConfirmColumns, 'SupplierInvoice');
|
|
1712
1835
|
});
|
|
1836
|
+
supplierInvoices
|
|
1837
|
+
.command('update <givenNumber>')
|
|
1838
|
+
.description('Update a supplier invoice')
|
|
1839
|
+
.requiredOption('--input <file>', 'Invoice data as JSON file (or - for stdin)')
|
|
1840
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1841
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
1842
|
+
.action(async (givenNumber, opts) => {
|
|
1843
|
+
const { updateSupplierInvoice } = await import('./operations/supplier-invoices.js');
|
|
1844
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
1845
|
+
const fields = JSON.parse(raw);
|
|
1846
|
+
if (!(await confirmMutation(`Update supplier invoice ${givenNumber}`, opts, {
|
|
1847
|
+
SupplierInvoice: fields,
|
|
1848
|
+
})))
|
|
1849
|
+
return;
|
|
1850
|
+
const data = await updateSupplierInvoice(givenNumber, fields);
|
|
1851
|
+
outputDetail(data, supplierInvoiceDetailColumns, json(), 'SupplierInvoice');
|
|
1852
|
+
});
|
|
1853
|
+
for (const action of [
|
|
1854
|
+
{
|
|
1855
|
+
command: 'approval-bookkeep',
|
|
1856
|
+
label: 'Approval-bookkeep',
|
|
1857
|
+
exportName: 'approvalBookkeepSupplierInvoice',
|
|
1858
|
+
},
|
|
1859
|
+
{
|
|
1860
|
+
command: 'approval-payment',
|
|
1861
|
+
label: 'Payment-approve',
|
|
1862
|
+
exportName: 'approvalPaymentSupplierInvoice',
|
|
1863
|
+
},
|
|
1864
|
+
{ command: 'cancel', label: 'Cancel', exportName: 'cancelSupplierInvoice' },
|
|
1865
|
+
{ command: 'credit', label: 'Credit', exportName: 'creditSupplierInvoice' },
|
|
1866
|
+
]) {
|
|
1867
|
+
supplierInvoices
|
|
1868
|
+
.command(`${action.command} <givenNumber>`)
|
|
1869
|
+
.description(`${action.label} a supplier invoice`)
|
|
1870
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1871
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
1872
|
+
.action(async (givenNumber, opts) => {
|
|
1873
|
+
const operations = await import('./operations/supplier-invoices.js');
|
|
1874
|
+
if (!(await confirmMutation(`${action.label} supplier invoice ${givenNumber}`, opts)))
|
|
1875
|
+
return;
|
|
1876
|
+
const data = await operations[action.exportName](givenNumber);
|
|
1877
|
+
outputConfirmation(`Supplier invoice ${givenNumber}: ${action.command} completed.`, json(), data, supplierInvoiceConfirmColumns, 'SupplierInvoice');
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
supplierInvoices
|
|
1881
|
+
.command('attachments <givenNumber>')
|
|
1882
|
+
.description('List files (e.g. the scanned/received invoice) attached to a supplier invoice — works for unbooked/authorizepending invoices too')
|
|
1883
|
+
.action(async (givenNumber) => {
|
|
1884
|
+
const { listSupplierInvoiceAttachments } = await import('./operations/supplier-invoices.js');
|
|
1885
|
+
const attachments = await listSupplierInvoiceAttachments(givenNumber);
|
|
1886
|
+
if (json()) {
|
|
1887
|
+
console.log(JSON.stringify({ Attachments: attachments }, null, 2));
|
|
1888
|
+
}
|
|
1889
|
+
else {
|
|
1890
|
+
outputList(attachments, supplierInvoiceAttachmentColumns, false, attachments);
|
|
1891
|
+
}
|
|
1892
|
+
});
|
|
1893
|
+
supplierInvoices
|
|
1894
|
+
.command('file <fileId>')
|
|
1895
|
+
.description('Download a file attached to a supplier invoice (get the fileId from "supplier-invoices attachments")')
|
|
1896
|
+
.option('-f, --file <path>', 'Write the file here (- for stdout; default: supplier-invoice-file-<fileId><ext>)')
|
|
1897
|
+
.option('--overwrite', 'Allow replacing an existing regular file')
|
|
1898
|
+
.addHelpText('after', `
|
|
1899
|
+
Examples:
|
|
1900
|
+
noxctl supplier-invoices attachments 1
|
|
1901
|
+
noxctl supplier-invoices file c7e578d8-59a7-4e00-b29b-b99e4d9ede63
|
|
1902
|
+
noxctl supplier-invoices file c7e578d8-59a7-4e00-b29b-b99e4d9ede63 --file invoice-scan.pdf`)
|
|
1903
|
+
.action(async (fileId, opts) => {
|
|
1904
|
+
const { getSupplierInvoiceFile } = await import('./operations/supplier-invoices.js');
|
|
1905
|
+
const { extensionForMime } = await import('./operations/vouchers.js');
|
|
1906
|
+
const { safeLocalOutputPath, writeBinaryFile } = await import('./safe-file-output.js');
|
|
1907
|
+
const toStdout = opts.file === '-';
|
|
1908
|
+
if (toStdout && program.opts().output === 'json') {
|
|
1909
|
+
throw new Error('--file - writes raw file bytes to stdout and cannot be combined with --output json.');
|
|
1910
|
+
}
|
|
1911
|
+
const file = await getSupplierInvoiceFile(fileId);
|
|
1912
|
+
if (toStdout) {
|
|
1913
|
+
await new Promise((resolve, reject) => {
|
|
1914
|
+
process.stdout.write(file.buffer, (err) => (err ? reject(err) : resolve()));
|
|
1915
|
+
});
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
const path = opts.file ??
|
|
1919
|
+
safeLocalOutputPath('supplier-invoice-file-', fileId, extensionForMime(file.contentType));
|
|
1920
|
+
const savedPath = writeBinaryFile(path, file.buffer, opts.overwrite);
|
|
1921
|
+
outputConfirmation(`File saved to ${savedPath} (${file.contentType}, ${file.buffer.length} bytes).`, json(), {
|
|
1922
|
+
FileId: fileId,
|
|
1923
|
+
Path: savedPath,
|
|
1924
|
+
ContentType: file.contentType,
|
|
1925
|
+
Bytes: file.buffer.length,
|
|
1926
|
+
});
|
|
1927
|
+
});
|
|
1713
1928
|
// --- company ---
|
|
1714
1929
|
const company = program.command('company').description('Company operations');
|
|
1715
1930
|
company
|
|
@@ -1859,6 +2074,93 @@ Examples:
|
|
|
1859
2074
|
Bytes: file.buffer.length,
|
|
1860
2075
|
});
|
|
1861
2076
|
});
|
|
2077
|
+
vouchers
|
|
2078
|
+
.command('connection <fileId>')
|
|
2079
|
+
.description('Get voucher file-connection metadata')
|
|
2080
|
+
.action(async (fileId) => {
|
|
2081
|
+
const { getVoucherFileConnection } = await import('./operations/vouchers.js');
|
|
2082
|
+
const result = await getVoucherFileConnection(fileId);
|
|
2083
|
+
outputDetail(result, voucherAttachmentColumns, json(), 'VoucherFileConnection');
|
|
2084
|
+
});
|
|
2085
|
+
vouchers
|
|
2086
|
+
.command('detach <fileId>')
|
|
2087
|
+
.description('Detach a file from its voucher')
|
|
2088
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2089
|
+
.option('--dry-run', 'Preview without detaching')
|
|
2090
|
+
.action(async (fileId, opts) => {
|
|
2091
|
+
const { deleteVoucherFileConnection } = await import('./operations/vouchers.js');
|
|
2092
|
+
if (!(await confirmMutation(`Delete voucher file connection ${fileId}`, opts)))
|
|
2093
|
+
return;
|
|
2094
|
+
await deleteVoucherFileConnection(fileId);
|
|
2095
|
+
outputConfirmation(`Voucher file connection ${fileId} deleted.`, json(), {
|
|
2096
|
+
FileId: fileId,
|
|
2097
|
+
detached: true,
|
|
2098
|
+
});
|
|
2099
|
+
});
|
|
2100
|
+
supplierInvoices
|
|
2101
|
+
.command('connection <fileId>')
|
|
2102
|
+
.description('Get supplier-invoice file-connection metadata')
|
|
2103
|
+
.action(async (fileId) => {
|
|
2104
|
+
const { getSupplierInvoiceFileConnection } = await import('./operations/supplier-invoices.js');
|
|
2105
|
+
const result = await getSupplierInvoiceFileConnection(fileId);
|
|
2106
|
+
outputDetail(result, supplierInvoiceAttachmentColumns, json(), 'SupplierInvoiceFileConnection');
|
|
2107
|
+
});
|
|
2108
|
+
supplierInvoices
|
|
2109
|
+
.command('connect-file <givenNumber> <fileId>')
|
|
2110
|
+
.description('Connect an existing inbox file to a supplier invoice')
|
|
2111
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2112
|
+
.option('--dry-run', 'Preview exact payload')
|
|
2113
|
+
.action(async (givenNumber, fileId, opts) => {
|
|
2114
|
+
const payload = { SupplierInvoiceNumber: givenNumber, FileId: fileId };
|
|
2115
|
+
if (!(await confirmMutation(`Connect file ${fileId} to supplier invoice ${givenNumber}`, opts, {
|
|
2116
|
+
SupplierInvoiceFileConnection: payload,
|
|
2117
|
+
})))
|
|
2118
|
+
return;
|
|
2119
|
+
const { createSupplierInvoiceFileConnection } = await import('./operations/supplier-invoices.js');
|
|
2120
|
+
const result = await createSupplierInvoiceFileConnection(givenNumber, fileId);
|
|
2121
|
+
outputDetail(result, supplierInvoiceAttachmentColumns, json(), 'SupplierInvoiceFileConnection');
|
|
2122
|
+
});
|
|
2123
|
+
supplierInvoices
|
|
2124
|
+
.command('detach-file <fileId>')
|
|
2125
|
+
.description('Detach a file from a supplier invoice')
|
|
2126
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2127
|
+
.option('--dry-run', 'Preview without detaching')
|
|
2128
|
+
.action(async (fileId, opts) => {
|
|
2129
|
+
const { deleteSupplierInvoiceFileConnection } = await import('./operations/supplier-invoices.js');
|
|
2130
|
+
if (!(await confirmMutation(`Delete supplier invoice file connection ${fileId}`, opts)))
|
|
2131
|
+
return;
|
|
2132
|
+
await deleteSupplierInvoiceFileConnection(fileId);
|
|
2133
|
+
outputConfirmation(`Supplier invoice file connection ${fileId} deleted.`, json(), {
|
|
2134
|
+
FileId: fileId,
|
|
2135
|
+
detached: true,
|
|
2136
|
+
});
|
|
2137
|
+
});
|
|
2138
|
+
// --- general-ledger ---
|
|
2139
|
+
const generalLedger = program
|
|
2140
|
+
.command('general-ledger')
|
|
2141
|
+
.alias('ledger')
|
|
2142
|
+
.description('Bookkeeping transaction list with amounts, via Fortnox’s SIE export — fast even for a full year, unlike walking vouchers one by one');
|
|
2143
|
+
generalLedger
|
|
2144
|
+
.command('list')
|
|
2145
|
+
.description('List bookkeeping transactions for a date range, one row per posting')
|
|
2146
|
+
.requiredOption('--from <date>', 'From date (YYYY-MM-DD)')
|
|
2147
|
+
.requiredOption('--to <date>', 'To date (YYYY-MM-DD)')
|
|
2148
|
+
.option('--year <number>', 'Financial year (optional; resolved automatically from --from otherwise)', parsePositiveInteger)
|
|
2149
|
+
.option('--account <number>', 'Filter to one account number')
|
|
2150
|
+
.option('--series <code>', 'Filter to one voucher series (e.g. "A")')
|
|
2151
|
+
.action(async (opts) => {
|
|
2152
|
+
const { getGeneralLedger } = await import('./operations/general-ledger.js');
|
|
2153
|
+
let rows = await getGeneralLedger({
|
|
2154
|
+
fromDate: opts.from,
|
|
2155
|
+
toDate: opts.to,
|
|
2156
|
+
financialYear: opts.year,
|
|
2157
|
+
});
|
|
2158
|
+
if (opts.account)
|
|
2159
|
+
rows = rows.filter((r) => r.account === opts.account);
|
|
2160
|
+
if (opts.series)
|
|
2161
|
+
rows = rows.filter((r) => r.series === opts.series);
|
|
2162
|
+
outputList(rows, generalLedgerColumns, json(), rows);
|
|
2163
|
+
});
|
|
1862
2164
|
// --- invoice-payments ---
|
|
1863
2165
|
const invoicePayments = program
|
|
1864
2166
|
.command('invoice-payments')
|
|
@@ -1943,6 +2245,23 @@ invoicePayments
|
|
|
1943
2245
|
const result = await bookkeepInvoicePayment(paymentNumber);
|
|
1944
2246
|
outputConfirmation(`Invoice payment ${paymentNumber} bookkeept.`, json(), result, undefined, 'InvoicePayment');
|
|
1945
2247
|
});
|
|
2248
|
+
invoicePayments
|
|
2249
|
+
.command('update <paymentNumber>')
|
|
2250
|
+
.description('Update an invoice payment')
|
|
2251
|
+
.requiredOption('--input <file>', 'Payment data as JSON file (or - for stdin)')
|
|
2252
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2253
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
2254
|
+
.action(async (paymentNumber, opts) => {
|
|
2255
|
+
const { updateInvoicePayment } = await import('./operations/invoice-payments.js');
|
|
2256
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
2257
|
+
const fields = JSON.parse(raw);
|
|
2258
|
+
if (!(await confirmMutation(`Update invoice payment ${paymentNumber}`, opts, {
|
|
2259
|
+
InvoicePayment: fields,
|
|
2260
|
+
})))
|
|
2261
|
+
return;
|
|
2262
|
+
const data = await updateInvoicePayment(paymentNumber, fields);
|
|
2263
|
+
outputDetail(data, invoicePaymentDetailColumns, json(), 'InvoicePayment');
|
|
2264
|
+
});
|
|
1946
2265
|
// --- supplier-invoice-payments ---
|
|
1947
2266
|
const supplierInvoicePayments = program
|
|
1948
2267
|
.command('supplier-invoice-payments')
|
|
@@ -2012,6 +2331,35 @@ supplierInvoicePayments
|
|
|
2012
2331
|
await deleteSupplierInvoicePayment(paymentNumber);
|
|
2013
2332
|
outputConfirmation(`Supplier invoice payment ${paymentNumber} deleted.`, json(), { Number: paymentNumber, deleted: true }, undefined, 'SupplierInvoicePayment');
|
|
2014
2333
|
});
|
|
2334
|
+
supplierInvoicePayments
|
|
2335
|
+
.command('update <paymentNumber>')
|
|
2336
|
+
.description('Update a supplier invoice payment')
|
|
2337
|
+
.requiredOption('--input <file>', 'Payment data as JSON file (or - for stdin)')
|
|
2338
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2339
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
2340
|
+
.action(async (paymentNumber, opts) => {
|
|
2341
|
+
const { updateSupplierInvoicePayment } = await import('./operations/supplier-invoice-payments.js');
|
|
2342
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
2343
|
+
const fields = JSON.parse(raw);
|
|
2344
|
+
if (!(await confirmMutation(`Update supplier invoice payment ${paymentNumber}`, opts, {
|
|
2345
|
+
SupplierInvoicePayment: fields,
|
|
2346
|
+
})))
|
|
2347
|
+
return;
|
|
2348
|
+
const data = await updateSupplierInvoicePayment(paymentNumber, fields);
|
|
2349
|
+
outputDetail(data, supplierInvoicePaymentDetailColumns, json(), 'SupplierInvoicePayment');
|
|
2350
|
+
});
|
|
2351
|
+
supplierInvoicePayments
|
|
2352
|
+
.command('bookkeep <paymentNumber>')
|
|
2353
|
+
.description('Bookkeep a supplier invoice payment')
|
|
2354
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2355
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
2356
|
+
.action(async (paymentNumber, opts) => {
|
|
2357
|
+
const { bookkeepSupplierInvoicePayment } = await import('./operations/supplier-invoice-payments.js');
|
|
2358
|
+
if (!(await confirmMutation(`Bookkeep supplier invoice payment ${paymentNumber}`, opts)))
|
|
2359
|
+
return;
|
|
2360
|
+
const data = await bookkeepSupplierInvoicePayment(paymentNumber);
|
|
2361
|
+
outputConfirmation(`Supplier invoice payment ${paymentNumber} bookkeept.`, json(), data, undefined, 'SupplierInvoicePayment');
|
|
2362
|
+
});
|
|
2015
2363
|
// --- offers ---
|
|
2016
2364
|
const offers = program.command('offers').description('Offer/quote operations (offerter)');
|
|
2017
2365
|
offers
|
|
@@ -2111,6 +2459,56 @@ offers
|
|
|
2111
2459
|
const data = await createOrderFromOffer(documentNumber);
|
|
2112
2460
|
outputConfirmation(`Order created from offer ${documentNumber}.`, json(), data, undefined, 'Order');
|
|
2113
2461
|
});
|
|
2462
|
+
for (const action of [
|
|
2463
|
+
{ command: 'cancel', label: 'Cancel', exportName: 'cancelOffer' },
|
|
2464
|
+
{ command: 'email', label: 'Email', exportName: 'emailOffer' },
|
|
2465
|
+
{
|
|
2466
|
+
command: 'external-print',
|
|
2467
|
+
label: 'Mark as externally printed',
|
|
2468
|
+
exportName: 'externalPrintOffer',
|
|
2469
|
+
},
|
|
2470
|
+
]) {
|
|
2471
|
+
offers
|
|
2472
|
+
.command(`${action.command} <documentNumber>`)
|
|
2473
|
+
.description(`${action.label} an offer`)
|
|
2474
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2475
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
2476
|
+
.action(async (documentNumber, opts) => {
|
|
2477
|
+
const operations = await import('./operations/offers.js');
|
|
2478
|
+
if (!(await confirmMutation(`${action.label} offer ${documentNumber}`, opts)))
|
|
2479
|
+
return;
|
|
2480
|
+
const data = await operations[action.exportName](documentNumber);
|
|
2481
|
+
outputConfirmation(`Offer ${documentNumber}: ${action.command} completed.`, json(), data, offerConfirmColumns, 'Offer');
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
offers
|
|
2485
|
+
.command('pdf <documentNumber>')
|
|
2486
|
+
.description('Download or print an offer as PDF')
|
|
2487
|
+
.option('-f, --file <path>', 'Write the PDF here')
|
|
2488
|
+
.option('--print', 'Use the state-changing Fortnox print action')
|
|
2489
|
+
.option('--overwrite', 'Allow replacing an existing regular file')
|
|
2490
|
+
.option('-y, --yes', 'Skip confirmation prompt (required with --print)')
|
|
2491
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
2492
|
+
.action(async (documentNumber, opts) => {
|
|
2493
|
+
const mode = opts.print ? 'print' : 'preview';
|
|
2494
|
+
if (opts.print || opts.dryRun) {
|
|
2495
|
+
if (!(await confirmMutation(`${mode} offer ${documentNumber} as PDF`, opts)))
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
const { getOfferPdf } = await import('./operations/offers.js');
|
|
2499
|
+
const pdf = await getOfferPdf(documentNumber, mode);
|
|
2500
|
+
if (!pdf) {
|
|
2501
|
+
outputConfirmation(`Offer ${documentNumber} printed without a PDF response.`, json(), {});
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
const { writeBinaryFile } = await import('./safe-file-output.js');
|
|
2505
|
+
const path = writeBinaryFile(opts.file ?? `offer-${documentNumber}.pdf`, pdf, opts.overwrite);
|
|
2506
|
+
outputConfirmation(`Offer PDF saved to ${path}.`, json(), {
|
|
2507
|
+
Path: path,
|
|
2508
|
+
Bytes: pdf.length,
|
|
2509
|
+
Mode: mode,
|
|
2510
|
+
});
|
|
2511
|
+
});
|
|
2114
2512
|
// --- orders ---
|
|
2115
2513
|
const orders = program.command('orders').description('Order operations (ordrar)');
|
|
2116
2514
|
orders
|
|
@@ -2197,6 +2595,56 @@ orders
|
|
|
2197
2595
|
const data = await createInvoiceFromOrder(documentNumber);
|
|
2198
2596
|
outputConfirmation(`Invoice created from order ${documentNumber}.`, json(), data, invoiceConfirmColumns, 'Invoice');
|
|
2199
2597
|
});
|
|
2598
|
+
for (const action of [
|
|
2599
|
+
{ command: 'cancel', label: 'Cancel', exportName: 'cancelOrder' },
|
|
2600
|
+
{ command: 'email', label: 'Email', exportName: 'emailOrder' },
|
|
2601
|
+
{
|
|
2602
|
+
command: 'external-print',
|
|
2603
|
+
label: 'Mark as externally printed',
|
|
2604
|
+
exportName: 'externalPrintOrder',
|
|
2605
|
+
},
|
|
2606
|
+
]) {
|
|
2607
|
+
orders
|
|
2608
|
+
.command(`${action.command} <documentNumber>`)
|
|
2609
|
+
.description(`${action.label} an order`)
|
|
2610
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2611
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
2612
|
+
.action(async (documentNumber, opts) => {
|
|
2613
|
+
const operations = await import('./operations/orders.js');
|
|
2614
|
+
if (!(await confirmMutation(`${action.label} order ${documentNumber}`, opts)))
|
|
2615
|
+
return;
|
|
2616
|
+
const data = await operations[action.exportName](documentNumber);
|
|
2617
|
+
outputConfirmation(`Order ${documentNumber}: ${action.command} completed.`, json(), data, orderConfirmColumns, 'Order');
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
orders
|
|
2621
|
+
.command('pdf <documentNumber>')
|
|
2622
|
+
.description('Download or print an order as PDF')
|
|
2623
|
+
.option('-f, --file <path>', 'Write the PDF here')
|
|
2624
|
+
.option('--print', 'Use the state-changing Fortnox print action')
|
|
2625
|
+
.option('--overwrite', 'Allow replacing an existing regular file')
|
|
2626
|
+
.option('-y, --yes', 'Skip confirmation prompt (required with --print)')
|
|
2627
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
2628
|
+
.action(async (documentNumber, opts) => {
|
|
2629
|
+
const mode = opts.print ? 'print' : 'preview';
|
|
2630
|
+
if (opts.print || opts.dryRun) {
|
|
2631
|
+
if (!(await confirmMutation(`${mode} order ${documentNumber} as PDF`, opts)))
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
const { getOrderPdf } = await import('./operations/orders.js');
|
|
2635
|
+
const pdf = await getOrderPdf(documentNumber, mode);
|
|
2636
|
+
if (!pdf) {
|
|
2637
|
+
outputConfirmation(`Order ${documentNumber} printed without a PDF response.`, json(), {});
|
|
2638
|
+
return;
|
|
2639
|
+
}
|
|
2640
|
+
const { writeBinaryFile } = await import('./safe-file-output.js');
|
|
2641
|
+
const path = writeBinaryFile(opts.file ?? `order-${documentNumber}.pdf`, pdf, opts.overwrite);
|
|
2642
|
+
outputConfirmation(`Order PDF saved to ${path}.`, json(), {
|
|
2643
|
+
Path: path,
|
|
2644
|
+
Bytes: pdf.length,
|
|
2645
|
+
Mode: mode,
|
|
2646
|
+
});
|
|
2647
|
+
});
|
|
2200
2648
|
// --- projects ---
|
|
2201
2649
|
const projects = program.command('projects').description('Project operations');
|
|
2202
2650
|
projects
|
|
@@ -2266,6 +2714,21 @@ projects
|
|
|
2266
2714
|
const data = await updateProject(projectNumber, fields);
|
|
2267
2715
|
outputDetail(data, projectDetailColumns, json(), 'Project');
|
|
2268
2716
|
});
|
|
2717
|
+
projects
|
|
2718
|
+
.command('delete <projectNumber>')
|
|
2719
|
+
.description('Delete a project')
|
|
2720
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2721
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
2722
|
+
.action(async (projectNumber, opts) => {
|
|
2723
|
+
const { deleteProject } = await import('./operations/projects.js');
|
|
2724
|
+
if (!(await confirmMutation(`Delete project ${projectNumber}`, opts)))
|
|
2725
|
+
return;
|
|
2726
|
+
await deleteProject(projectNumber);
|
|
2727
|
+
outputConfirmation(`Project ${projectNumber} deleted.`, json(), {
|
|
2728
|
+
ProjectNumber: projectNumber,
|
|
2729
|
+
deleted: true,
|
|
2730
|
+
});
|
|
2731
|
+
});
|
|
2269
2732
|
// --- cost centers ---
|
|
2270
2733
|
const costcenters = program.command('costcenters').description('Cost center operations');
|
|
2271
2734
|
costcenters
|
|
@@ -2517,6 +2980,23 @@ salarytransactions
|
|
|
2517
2980
|
await deleteSalaryTransaction(salaryRow);
|
|
2518
2981
|
outputConfirmation(`Salary transaction ${salaryRow} deleted.`, json(), { SalaryRow: salaryRow, deleted: true }, undefined, 'SalaryTransaction');
|
|
2519
2982
|
});
|
|
2983
|
+
salarytransactions
|
|
2984
|
+
.command('update <salaryRow>')
|
|
2985
|
+
.description('Update a salary transaction')
|
|
2986
|
+
.requiredOption('--input <file>', 'Transaction data as JSON file (or - for stdin)')
|
|
2987
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
2988
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
2989
|
+
.action(async (salaryRow, opts) => {
|
|
2990
|
+
const { updateSalaryTransaction } = await import('./operations/salarytransactions.js');
|
|
2991
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
2992
|
+
const fields = JSON.parse(raw);
|
|
2993
|
+
if (!(await confirmMutation(`Update salary transaction ${salaryRow}`, opts, {
|
|
2994
|
+
SalaryTransaction: fields,
|
|
2995
|
+
})))
|
|
2996
|
+
return;
|
|
2997
|
+
const data = await updateSalaryTransaction(salaryRow, fields);
|
|
2998
|
+
outputDetail(data, salaryTransactionDetailColumns, json(), 'SalaryTransaction');
|
|
2999
|
+
});
|
|
2520
3000
|
// --- attendance transactions (närvaro / Lön) ---
|
|
2521
3001
|
const attendancetransactions = program
|
|
2522
3002
|
.command('attendance-transactions')
|
|
@@ -2544,9 +3024,15 @@ attendancetransactions
|
|
|
2544
3024
|
attendancetransactions
|
|
2545
3025
|
.command('get <id>')
|
|
2546
3026
|
.description('Get a single attendance transaction')
|
|
2547
|
-
.
|
|
2548
|
-
|
|
2549
|
-
|
|
3027
|
+
.option('--date <date>', 'Date for the composite Fortnox key (YYYY-MM-DD)')
|
|
3028
|
+
.option('--code <code>', 'Cause code for the composite Fortnox key')
|
|
3029
|
+
.action(async (id, opts) => {
|
|
3030
|
+
const { getAttendanceTransaction, getAttendanceTransactionByDateCode } = await import('./operations/attendancetransactions.js');
|
|
3031
|
+
if ((opts.date && !opts.code) || (opts.code && !opts.date))
|
|
3032
|
+
throw new Error('--date and --code must be supplied together');
|
|
3033
|
+
const data = opts.date && opts.code
|
|
3034
|
+
? await getAttendanceTransactionByDateCode(id, opts.date, opts.code)
|
|
3035
|
+
: await getAttendanceTransaction(id);
|
|
2550
3036
|
outputDetail(data, attendanceTransactionDetailColumns, json(), 'AttendanceTransaction');
|
|
2551
3037
|
});
|
|
2552
3038
|
attendancetransactions
|
|
@@ -2595,6 +3081,23 @@ attendancetransactions
|
|
|
2595
3081
|
await deleteAttendanceTransaction(id);
|
|
2596
3082
|
outputConfirmation(`Attendance transaction ${id} deleted.`, json(), { id, deleted: true }, undefined, 'AttendanceTransaction');
|
|
2597
3083
|
});
|
|
3084
|
+
attendancetransactions
|
|
3085
|
+
.command('update <id>')
|
|
3086
|
+
.description('Update an attendance transaction')
|
|
3087
|
+
.requiredOption('--input <file>', 'Transaction data as JSON file (or - for stdin)')
|
|
3088
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
3089
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
3090
|
+
.action(async (id, opts) => {
|
|
3091
|
+
const { updateAttendanceTransaction } = await import('./operations/attendancetransactions.js');
|
|
3092
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
3093
|
+
const fields = JSON.parse(raw);
|
|
3094
|
+
if (!(await confirmMutation(`Update attendance transaction ${id}`, opts, {
|
|
3095
|
+
AttendanceTransaction: fields,
|
|
3096
|
+
})))
|
|
3097
|
+
return;
|
|
3098
|
+
const data = await updateAttendanceTransaction(id, fields);
|
|
3099
|
+
outputDetail(data, attendanceTransactionDetailColumns, json(), 'AttendanceTransaction');
|
|
3100
|
+
});
|
|
2598
3101
|
// --- absence transactions (frånvaro / Lön) ---
|
|
2599
3102
|
const absencetransactions = program
|
|
2600
3103
|
.command('absence-transactions')
|
|
@@ -2622,9 +3125,15 @@ absencetransactions
|
|
|
2622
3125
|
absencetransactions
|
|
2623
3126
|
.command('get <id>')
|
|
2624
3127
|
.description('Get a single absence transaction')
|
|
2625
|
-
.
|
|
2626
|
-
|
|
2627
|
-
|
|
3128
|
+
.option('--date <date>', 'Date for the composite Fortnox key (YYYY-MM-DD)')
|
|
3129
|
+
.option('--code <code>', 'Cause code for the composite Fortnox key')
|
|
3130
|
+
.action(async (id, opts) => {
|
|
3131
|
+
const { getAbsenceTransaction, getAbsenceTransactionByDateCode } = await import('./operations/absencetransactions.js');
|
|
3132
|
+
if ((opts.date && !opts.code) || (opts.code && !opts.date))
|
|
3133
|
+
throw new Error('--date and --code must be supplied together');
|
|
3134
|
+
const data = opts.date && opts.code
|
|
3135
|
+
? await getAbsenceTransactionByDateCode(id, opts.date, opts.code)
|
|
3136
|
+
: await getAbsenceTransaction(id);
|
|
2628
3137
|
outputDetail(data, absenceTransactionDetailColumns, json(), 'AbsenceTransaction');
|
|
2629
3138
|
});
|
|
2630
3139
|
absencetransactions
|
|
@@ -2676,6 +3185,23 @@ absencetransactions
|
|
|
2676
3185
|
await deleteAbsenceTransaction(id);
|
|
2677
3186
|
outputConfirmation(`Absence transaction ${id} deleted.`, json(), { id, deleted: true }, undefined, 'AbsenceTransaction');
|
|
2678
3187
|
});
|
|
3188
|
+
absencetransactions
|
|
3189
|
+
.command('update <id>')
|
|
3190
|
+
.description('Update an absence transaction')
|
|
3191
|
+
.requiredOption('--input <file>', 'Transaction data as JSON file (or - for stdin)')
|
|
3192
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
3193
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
3194
|
+
.action(async (id, opts) => {
|
|
3195
|
+
const { updateAbsenceTransaction } = await import('./operations/absencetransactions.js');
|
|
3196
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
3197
|
+
const fields = JSON.parse(raw);
|
|
3198
|
+
if (!(await confirmMutation(`Update absence transaction ${id}`, opts, {
|
|
3199
|
+
AbsenceTransaction: fields,
|
|
3200
|
+
})))
|
|
3201
|
+
return;
|
|
3202
|
+
const data = await updateAbsenceTransaction(id, fields);
|
|
3203
|
+
outputDetail(data, absenceTransactionDetailColumns, json(), 'AbsenceTransaction');
|
|
3204
|
+
});
|
|
2679
3205
|
// --- schedule times (schematider / Lön) ---
|
|
2680
3206
|
const scheduletimes = program
|
|
2681
3207
|
.command('schedule-times')
|
|
@@ -2789,6 +3315,34 @@ taxreductions
|
|
|
2789
3315
|
const data = await createTaxReduction(params);
|
|
2790
3316
|
outputDetail(data, taxReductionDetailColumns, json(), 'TaxReduction');
|
|
2791
3317
|
});
|
|
3318
|
+
taxreductions
|
|
3319
|
+
.command('update <id>')
|
|
3320
|
+
.description('Update a tax reduction')
|
|
3321
|
+
.requiredOption('--input <file>', 'Tax reduction data as JSON file (or - for stdin)')
|
|
3322
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
3323
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
3324
|
+
.action(async (id, opts) => {
|
|
3325
|
+
const { updateTaxReduction } = await import('./operations/taxreductions.js');
|
|
3326
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
3327
|
+
const fields = JSON.parse(raw);
|
|
3328
|
+
const numericId = parseInt(id, 10);
|
|
3329
|
+
if (!(await confirmMutation(`Update tax reduction ${id}`, opts, { TaxReduction: fields })))
|
|
3330
|
+
return;
|
|
3331
|
+
const data = await updateTaxReduction(numericId, fields);
|
|
3332
|
+
outputDetail(data, taxReductionDetailColumns, json(), 'TaxReduction');
|
|
3333
|
+
});
|
|
3334
|
+
taxreductions
|
|
3335
|
+
.command('delete <id>')
|
|
3336
|
+
.description('Delete a tax reduction')
|
|
3337
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
3338
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
3339
|
+
.action(async (id, opts) => {
|
|
3340
|
+
const { deleteTaxReduction } = await import('./operations/taxreductions.js');
|
|
3341
|
+
if (!(await confirmMutation(`Delete tax reduction ${id}`, opts)))
|
|
3342
|
+
return;
|
|
3343
|
+
await deleteTaxReduction(parseInt(id, 10));
|
|
3344
|
+
outputConfirmation(`Tax reduction ${id} deleted.`, json(), { Id: Number(id), deleted: true });
|
|
3345
|
+
});
|
|
2792
3346
|
// --- price lists ---
|
|
2793
3347
|
const pricelists = program.command('pricelists').description('Price list operations');
|
|
2794
3348
|
pricelists
|
|
@@ -2862,7 +3416,7 @@ const prices = program.command('prices').description('Price operations within pr
|
|
|
2862
3416
|
prices
|
|
2863
3417
|
.command('list')
|
|
2864
3418
|
.description('List prices in a price list')
|
|
2865
|
-
.
|
|
3419
|
+
.option('--pricelist <code>', 'Price list code (omit to list all prices)')
|
|
2866
3420
|
.option('--article <number>', 'Filter by article number')
|
|
2867
3421
|
.option('--page <number>', 'Page number', parseInt)
|
|
2868
3422
|
.option('--limit <number>', 'Results per page', parseInt)
|
|
@@ -2888,6 +3442,26 @@ prices
|
|
|
2888
3442
|
const data = await getPrice(opts.pricelist, opts.article, opts.fromQuantity);
|
|
2889
3443
|
outputDetail(data, priceDetailColumns, json(), 'Price');
|
|
2890
3444
|
});
|
|
3445
|
+
prices
|
|
3446
|
+
.command('create')
|
|
3447
|
+
.description('Create a price')
|
|
3448
|
+
.requiredOption('--pricelist <code>', 'Price list code')
|
|
3449
|
+
.requiredOption('--article <number>', 'Article number')
|
|
3450
|
+
.requiredOption('--input <file>', 'Price data as JSON file (or - for stdin)')
|
|
3451
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
3452
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
3453
|
+
.action(async (opts) => {
|
|
3454
|
+
const { createPrice } = await import('./operations/pricelists.js');
|
|
3455
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
3456
|
+
const input = JSON.parse(raw);
|
|
3457
|
+
const fields = { ...input, PriceList: opts.pricelist, ArticleNumber: opts.article };
|
|
3458
|
+
if (!(await confirmMutation(`Create price ${opts.pricelist}/${opts.article}`, opts, {
|
|
3459
|
+
Price: fields,
|
|
3460
|
+
})))
|
|
3461
|
+
return;
|
|
3462
|
+
const data = await createPrice(fields);
|
|
3463
|
+
outputDetail(data, priceDetailColumns, json(), 'Price');
|
|
3464
|
+
});
|
|
2891
3465
|
prices
|
|
2892
3466
|
.command('update')
|
|
2893
3467
|
.description('Update a price')
|
|
@@ -2909,6 +3483,22 @@ prices
|
|
|
2909
3483
|
const data = await updatePrice(opts.pricelist, opts.article, fields, opts.fromQuantity);
|
|
2910
3484
|
outputDetail(data, priceDetailColumns, json(), 'Price');
|
|
2911
3485
|
});
|
|
3486
|
+
prices
|
|
3487
|
+
.command('delete')
|
|
3488
|
+
.description('Delete a quantity-aware price')
|
|
3489
|
+
.requiredOption('--pricelist <code>', 'Price list code')
|
|
3490
|
+
.requiredOption('--article <number>', 'Article number')
|
|
3491
|
+
.requiredOption('--from-quantity <number>', 'From quantity', parseInt)
|
|
3492
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
3493
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
3494
|
+
.action(async (opts) => {
|
|
3495
|
+
const { deletePrice } = await import('./operations/pricelists.js');
|
|
3496
|
+
const target = `${opts.pricelist}/${opts.article}/${opts.fromQuantity}`;
|
|
3497
|
+
if (!(await confirmMutation(`Delete price ${target}`, opts)))
|
|
3498
|
+
return;
|
|
3499
|
+
await deletePrice(opts.pricelist, opts.article, opts.fromQuantity);
|
|
3500
|
+
outputConfirmation(`Price ${target} deleted.`, json(), { deleted: true });
|
|
3501
|
+
});
|
|
2912
3502
|
// --- contracts ---
|
|
2913
3503
|
const contracts = program
|
|
2914
3504
|
.command('contracts')
|
|
@@ -3186,6 +3776,27 @@ financialYears
|
|
|
3186
3776
|
const data = await getFinancialYear(parseInt(id, 10));
|
|
3187
3777
|
outputDetail(data, financialYearDetailColumns, json(), 'FinancialYear');
|
|
3188
3778
|
});
|
|
3779
|
+
financialYears
|
|
3780
|
+
.command('create')
|
|
3781
|
+
.description('Create a financial year')
|
|
3782
|
+
.requiredOption('--from <date>', 'Start date (YYYY-MM-DD)')
|
|
3783
|
+
.requiredOption('--to <date>', 'End date (YYYY-MM-DD)')
|
|
3784
|
+
.option('--accounting-method <method>', 'Accounting method (ACCRUAL or CASH)')
|
|
3785
|
+
.option('--account-chart <type>', 'Account chart type')
|
|
3786
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
3787
|
+
.option('--dry-run', 'Preview the request without sending it')
|
|
3788
|
+
.action(async (opts) => {
|
|
3789
|
+
const { createFinancialYear } = await import('./operations/financial-years.js');
|
|
3790
|
+
const fields = { FromDate: opts.from, ToDate: opts.to };
|
|
3791
|
+
if (opts.accountingMethod)
|
|
3792
|
+
fields.AccountingMethod = opts.accountingMethod;
|
|
3793
|
+
if (opts.accountChart)
|
|
3794
|
+
fields.AccountChartType = opts.accountChart;
|
|
3795
|
+
if (!(await confirmMutation('Create financial year', opts, { FinancialYear: fields })))
|
|
3796
|
+
return;
|
|
3797
|
+
const data = await createFinancialYear(fields);
|
|
3798
|
+
outputDetail(data, financialYearDetailColumns, json(), 'FinancialYear');
|
|
3799
|
+
});
|
|
3189
3800
|
financialYears
|
|
3190
3801
|
.command('locked-period')
|
|
3191
3802
|
.description('Show the locked period (bokföring låst t.o.m.)')
|
|
@@ -3202,6 +3813,396 @@ financialYears
|
|
|
3202
3813
|
}
|
|
3203
3814
|
outputDetail(data, lockedPeriodDetailColumns, false);
|
|
3204
3815
|
});
|
|
3816
|
+
const archive = program.command('archive').description('Fortnox archive operations');
|
|
3817
|
+
archive
|
|
3818
|
+
.command('list')
|
|
3819
|
+
.option('--path <path>', 'Archive path')
|
|
3820
|
+
.option('--file-id <id>', 'File ID')
|
|
3821
|
+
.action(async (opts) => {
|
|
3822
|
+
const { listArchive } = await import('./operations/files.js');
|
|
3823
|
+
const raw = await listArchive({ path: opts.path, fileId: opts.fileId });
|
|
3824
|
+
const folder = (raw.Folder ?? {});
|
|
3825
|
+
const items = [
|
|
3826
|
+
...(folder.Folders ?? []),
|
|
3827
|
+
...(folder.Files ?? []),
|
|
3828
|
+
];
|
|
3829
|
+
outputList(items, referenceDataColumns, json(), raw);
|
|
3830
|
+
});
|
|
3831
|
+
archive
|
|
3832
|
+
.command('get <id>')
|
|
3833
|
+
.option('--path <path>', 'Archive path')
|
|
3834
|
+
.option('--file-id <fileId>', 'File ID')
|
|
3835
|
+
.option('-o, --output <file>', 'Output file (default: private temporary directory)')
|
|
3836
|
+
.option('--overwrite', 'Overwrite an existing regular file')
|
|
3837
|
+
.action(async (id, opts) => {
|
|
3838
|
+
const { getArchiveEntry } = await import('./operations/files.js');
|
|
3839
|
+
const { privateOutputPath, writeBinaryFile } = await import('./safe-file-output.js');
|
|
3840
|
+
const file = await getArchiveEntry(id, { path: opts.path, fileId: opts.fileId });
|
|
3841
|
+
const target = writeBinaryFile(opts.output ?? privateOutputPath('noxctl-', `archive-${id}`), file.buffer, opts.overwrite);
|
|
3842
|
+
outputConfirmation(`Archive file ${id} saved to ${target}.`, json(), {
|
|
3843
|
+
Id: id,
|
|
3844
|
+
File: target,
|
|
3845
|
+
Bytes: file.buffer.length,
|
|
3846
|
+
ContentType: file.contentType,
|
|
3847
|
+
});
|
|
3848
|
+
});
|
|
3849
|
+
archive
|
|
3850
|
+
.command('upload <file>')
|
|
3851
|
+
.option('--folder-id <id>', 'Target folder ID')
|
|
3852
|
+
.option('--path <path>', 'Target archive path')
|
|
3853
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
3854
|
+
.option('--dry-run', 'Preview without reading or uploading the file')
|
|
3855
|
+
.action(async (file, opts) => {
|
|
3856
|
+
const { uploadArchiveFile } = await import('./operations/files.js');
|
|
3857
|
+
if (!(await confirmMutation(`Upload ${file} to archive`, opts, {
|
|
3858
|
+
folderId: opts.folderId,
|
|
3859
|
+
path: opts.path,
|
|
3860
|
+
})))
|
|
3861
|
+
return;
|
|
3862
|
+
const raw = await uploadArchiveFile(file, { folderId: opts.folderId, path: opts.path });
|
|
3863
|
+
outputDetail((raw.File ?? raw), referenceDataColumns, json(), 'File');
|
|
3864
|
+
});
|
|
3865
|
+
archive
|
|
3866
|
+
.command('delete-path <path>')
|
|
3867
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
3868
|
+
.option('--dry-run', 'Preview without deleting')
|
|
3869
|
+
.action(async (path, opts) => {
|
|
3870
|
+
const { deleteArchivePath } = await import('./operations/files.js');
|
|
3871
|
+
if (!(await confirmMutation(`Delete archive path ${JSON.stringify(path)}`, opts)))
|
|
3872
|
+
return;
|
|
3873
|
+
await deleteArchivePath(path);
|
|
3874
|
+
outputConfirmation(`Archive path ${JSON.stringify(path)} deleted.`, json(), {
|
|
3875
|
+
path,
|
|
3876
|
+
deleted: true,
|
|
3877
|
+
});
|
|
3878
|
+
});
|
|
3879
|
+
archive
|
|
3880
|
+
.command('delete <id>')
|
|
3881
|
+
.option('--path <path>', 'Archive path')
|
|
3882
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
3883
|
+
.option('--dry-run', 'Preview without deleting')
|
|
3884
|
+
.action(async (id, opts) => {
|
|
3885
|
+
const { deleteArchiveEntry } = await import('./operations/files.js');
|
|
3886
|
+
if (!(await confirmMutation(`Delete archive entry ${id}`, opts, { path: opts.path })))
|
|
3887
|
+
return;
|
|
3888
|
+
await deleteArchiveEntry(id, opts.path);
|
|
3889
|
+
outputConfirmation(`Archive entry ${id} deleted.`, json(), { Id: id, deleted: true });
|
|
3890
|
+
});
|
|
3891
|
+
const inbox = program.command('inbox').description('Fortnox inbox operations');
|
|
3892
|
+
inbox.command('list').action(async () => {
|
|
3893
|
+
const { listInbox } = await import('./operations/files.js');
|
|
3894
|
+
const raw = await listInbox();
|
|
3895
|
+
const folder = (raw.Folder ?? {});
|
|
3896
|
+
const items = [
|
|
3897
|
+
...(folder.Folders ?? []),
|
|
3898
|
+
...(folder.Files ?? []),
|
|
3899
|
+
];
|
|
3900
|
+
outputList(items, referenceDataColumns, json(), raw);
|
|
3901
|
+
});
|
|
3902
|
+
inbox
|
|
3903
|
+
.command('upload <file>')
|
|
3904
|
+
.option('--folder-id <id>', 'Target folder ID')
|
|
3905
|
+
.option('--path <path>', 'Target inbox path')
|
|
3906
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
3907
|
+
.option('--dry-run', 'Preview without reading or uploading the file')
|
|
3908
|
+
.action(async (file, opts) => {
|
|
3909
|
+
const { uploadInboxEntry } = await import('./operations/files.js');
|
|
3910
|
+
if (!(await confirmMutation(`Upload ${file} to inbox`, opts, {
|
|
3911
|
+
folderId: opts.folderId,
|
|
3912
|
+
path: opts.path,
|
|
3913
|
+
})))
|
|
3914
|
+
return;
|
|
3915
|
+
const raw = await uploadInboxEntry(file, { folderId: opts.folderId, path: opts.path });
|
|
3916
|
+
outputDetail((raw.File ?? raw), referenceDataColumns, json(), 'File');
|
|
3917
|
+
});
|
|
3918
|
+
inbox
|
|
3919
|
+
.command('file <id>')
|
|
3920
|
+
.option('-f, --file <path>', 'Write the file here')
|
|
3921
|
+
.option('--overwrite', 'Allow replacing an existing regular file')
|
|
3922
|
+
.action(async (id, opts) => {
|
|
3923
|
+
const { getInboxFile } = await import('./operations/files.js');
|
|
3924
|
+
const { privateOutputPath, writeBinaryFile } = await import('./safe-file-output.js');
|
|
3925
|
+
const file = await getInboxFile(id);
|
|
3926
|
+
const path = writeBinaryFile(opts.file ?? privateOutputPath('noxctl-', `inbox-${id}`), file.buffer, opts.overwrite);
|
|
3927
|
+
outputConfirmation(`Inbox file ${id} saved to ${path}.`, json(), {
|
|
3928
|
+
Path: path,
|
|
3929
|
+
Bytes: file.buffer.length,
|
|
3930
|
+
});
|
|
3931
|
+
});
|
|
3932
|
+
inbox
|
|
3933
|
+
.command('delete <id>')
|
|
3934
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
3935
|
+
.option('--dry-run', 'Preview without deleting')
|
|
3936
|
+
.action(async (id, opts) => {
|
|
3937
|
+
const { deleteInboxEntry } = await import('./operations/files.js');
|
|
3938
|
+
if (!(await confirmMutation(`Delete inbox entry ${id}`, opts)))
|
|
3939
|
+
return;
|
|
3940
|
+
await deleteInboxEntry(id);
|
|
3941
|
+
outputConfirmation(`Inbox entry ${id} deleted.`, json(), { Id: id, deleted: true });
|
|
3942
|
+
});
|
|
3943
|
+
const attachments = program
|
|
3944
|
+
.command('attachments')
|
|
3945
|
+
.description('Cross-document attachment operations');
|
|
3946
|
+
attachments
|
|
3947
|
+
.command('attach <entityType> <documentNumber> <fileId>')
|
|
3948
|
+
.option('--exclude-on-send', 'Do not include the attachment when the document is sent')
|
|
3949
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
3950
|
+
.option('--dry-run', 'Preview without attaching')
|
|
3951
|
+
.action(async (entityType, documentNumber, fileId, opts) => {
|
|
3952
|
+
if (!['F', 'OF', 'O', 'C'].includes(entityType))
|
|
3953
|
+
throw new Error('entityType must be F, OF, O, or C');
|
|
3954
|
+
const body = {
|
|
3955
|
+
documentNumber,
|
|
3956
|
+
entityType,
|
|
3957
|
+
fileId,
|
|
3958
|
+
includeOnSend: !opts.excludeOnSend,
|
|
3959
|
+
};
|
|
3960
|
+
if (!(await confirmMutation(`Attach archive file ${fileId} to ${entityType} ${documentNumber}`, opts, body)))
|
|
3961
|
+
return;
|
|
3962
|
+
const { createDocumentAttachment } = await import('./operations/invoices.js');
|
|
3963
|
+
const result = await createDocumentAttachment(documentNumber, entityType, fileId, !opts.excludeOnSend);
|
|
3964
|
+
outputDetail(result, invoiceAttachmentListColumns, json(), 'Attachment');
|
|
3965
|
+
});
|
|
3966
|
+
attachments
|
|
3967
|
+
.command('list <entityType> <documentNumber>')
|
|
3968
|
+
.description('List attachments for OF (offer), O (order), or C (contract)')
|
|
3969
|
+
.action(async (entityType, documentNumber) => {
|
|
3970
|
+
if (!['OF', 'O', 'C'].includes(entityType))
|
|
3971
|
+
throw new Error('entityType must be OF, O, or C');
|
|
3972
|
+
const { listDocumentAttachments } = await import('./operations/invoices.js');
|
|
3973
|
+
const rows = await listDocumentAttachments(documentNumber, entityType);
|
|
3974
|
+
outputList(rows, invoiceAttachmentListColumns, json(), rows);
|
|
3975
|
+
});
|
|
3976
|
+
attachments
|
|
3977
|
+
.command('counts <entityType> <entityIds...>')
|
|
3978
|
+
.action(async (entityType, entityIds) => {
|
|
3979
|
+
if (!['F', 'OF', 'O', 'C'].includes(entityType))
|
|
3980
|
+
throw new Error('entityType must be F, OF, O, or C');
|
|
3981
|
+
const { getAttachmentCounts } = await import('./operations/invoices.js');
|
|
3982
|
+
const result = await getAttachmentCounts(entityIds.map(Number), entityType);
|
|
3983
|
+
outputDetail(result, invoiceAttachmentListColumns, json(), 'AttachmentCounts');
|
|
3984
|
+
});
|
|
3985
|
+
attachments
|
|
3986
|
+
.command('validate')
|
|
3987
|
+
.requiredOption('--input <file>', 'Attachment array as JSON (or - for stdin)')
|
|
3988
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
3989
|
+
.option('--dry-run', 'Preview exact payload')
|
|
3990
|
+
.action(async (opts) => {
|
|
3991
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
3992
|
+
const rows = JSON.parse(raw);
|
|
3993
|
+
if (!Array.isArray(rows))
|
|
3994
|
+
throw new Error('Attachment input must be an array');
|
|
3995
|
+
if (!(await confirmMutation('Validate attachments included on send', opts, rows)))
|
|
3996
|
+
return;
|
|
3997
|
+
const { validateAttachmentsOnSend } = await import('./operations/invoices.js');
|
|
3998
|
+
await validateAttachmentsOnSend(rows);
|
|
3999
|
+
outputConfirmation('Attachment validation accepted.', json(), {});
|
|
4000
|
+
});
|
|
4001
|
+
attachments
|
|
4002
|
+
.command('update <attachmentId>')
|
|
4003
|
+
.requiredOption('--input <file>', 'Attachment fields as JSON (or - for stdin)')
|
|
4004
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
4005
|
+
.option('--dry-run', 'Preview exact payload')
|
|
4006
|
+
.action(async (attachmentId, opts) => {
|
|
4007
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
4008
|
+
const fields = JSON.parse(raw);
|
|
4009
|
+
if (!(await confirmMutation(`Update attachment ${attachmentId}`, opts, fields)))
|
|
4010
|
+
return;
|
|
4011
|
+
const { updateDocumentAttachment } = await import('./operations/invoices.js');
|
|
4012
|
+
const result = await updateDocumentAttachment(attachmentId, fields);
|
|
4013
|
+
outputDetail(result, invoiceAttachmentListColumns, json(), 'Attachment');
|
|
4014
|
+
});
|
|
4015
|
+
attachments
|
|
4016
|
+
.command('detach <attachmentId>')
|
|
4017
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
4018
|
+
.option('--dry-run', 'Preview without detaching')
|
|
4019
|
+
.action(async (attachmentId, opts) => {
|
|
4020
|
+
if (!(await confirmMutation(`Detach attachment ${attachmentId}`, opts)))
|
|
4021
|
+
return;
|
|
4022
|
+
const { detachDocumentAttachment } = await import('./operations/invoices.js');
|
|
4023
|
+
await detachDocumentAttachment(attachmentId);
|
|
4024
|
+
outputConfirmation(`Attachment ${attachmentId} detached.`, json(), {
|
|
4025
|
+
AttachmentId: attachmentId,
|
|
4026
|
+
detached: true,
|
|
4027
|
+
});
|
|
4028
|
+
});
|
|
4029
|
+
const accrualResources = [
|
|
4030
|
+
{
|
|
4031
|
+
command: 'invoice-accruals',
|
|
4032
|
+
envelope: 'InvoiceAccrual',
|
|
4033
|
+
list: 'listInvoiceAccruals',
|
|
4034
|
+
get: 'getInvoiceAccrual',
|
|
4035
|
+
create: 'createInvoiceAccrual',
|
|
4036
|
+
update: 'updateInvoiceAccrual',
|
|
4037
|
+
delete: 'deleteInvoiceAccrual',
|
|
4038
|
+
},
|
|
4039
|
+
{
|
|
4040
|
+
command: 'supplier-invoice-accruals',
|
|
4041
|
+
envelope: 'SupplierInvoiceAccrual',
|
|
4042
|
+
list: 'listSupplierInvoiceAccruals',
|
|
4043
|
+
get: 'getSupplierInvoiceAccrual',
|
|
4044
|
+
create: 'createSupplierInvoiceAccrual',
|
|
4045
|
+
update: 'updateSupplierInvoiceAccrual',
|
|
4046
|
+
delete: 'deleteSupplierInvoiceAccrual',
|
|
4047
|
+
},
|
|
4048
|
+
{
|
|
4049
|
+
command: 'contract-accruals',
|
|
4050
|
+
envelope: 'ContractAccrual',
|
|
4051
|
+
list: 'listContractAccruals',
|
|
4052
|
+
get: 'getContractAccrual',
|
|
4053
|
+
create: 'createContractAccrual',
|
|
4054
|
+
update: 'updateContractAccrual',
|
|
4055
|
+
delete: 'deleteContractAccrual',
|
|
4056
|
+
},
|
|
4057
|
+
];
|
|
4058
|
+
for (const definition of accrualResources) {
|
|
4059
|
+
const resource = program
|
|
4060
|
+
.command(definition.command)
|
|
4061
|
+
.description('Fortnox-native accrual operations');
|
|
4062
|
+
resource.command('list').action(async () => {
|
|
4063
|
+
const operations = await import('./operations/accruals.js');
|
|
4064
|
+
const result = await operations[definition.list]();
|
|
4065
|
+
outputList(result.items, referenceDataColumns, json(), result.raw, result.raw.MetaInformation);
|
|
4066
|
+
});
|
|
4067
|
+
resource.command('get <documentNumber>').action(async (documentNumber) => {
|
|
4068
|
+
const operations = await import('./operations/accruals.js');
|
|
4069
|
+
const result = await operations[definition.get](documentNumber);
|
|
4070
|
+
outputDetail(result, referenceDataColumns, json(), definition.envelope);
|
|
4071
|
+
});
|
|
4072
|
+
resource
|
|
4073
|
+
.command('create')
|
|
4074
|
+
.requiredOption('--input <file>', 'Complete accrual JSON data (or - for stdin)')
|
|
4075
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
4076
|
+
.option('--dry-run', 'Preview the exact payload without sending it')
|
|
4077
|
+
.action(async (opts) => {
|
|
4078
|
+
const operations = await import('./operations/accruals.js');
|
|
4079
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
4080
|
+
const fields = operations.prepareAccrualFields(definition.envelope, JSON.parse(raw));
|
|
4081
|
+
if (!(await confirmMutation(`Create ${definition.command}`, opts, {
|
|
4082
|
+
[definition.envelope]: fields,
|
|
4083
|
+
})))
|
|
4084
|
+
return;
|
|
4085
|
+
const result = await operations[definition.create](fields);
|
|
4086
|
+
outputDetail(result, referenceDataColumns, json(), definition.envelope);
|
|
4087
|
+
});
|
|
4088
|
+
resource
|
|
4089
|
+
.command('update <documentNumber>')
|
|
4090
|
+
.requiredOption('--input <file>', 'Complete accrual JSON data (or - for stdin)')
|
|
4091
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
4092
|
+
.option('--dry-run', 'Preview the exact payload without sending it')
|
|
4093
|
+
.action(async (documentNumber, opts) => {
|
|
4094
|
+
const operations = await import('./operations/accruals.js');
|
|
4095
|
+
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
4096
|
+
const fields = operations.prepareAccrualFields(definition.envelope, JSON.parse(raw));
|
|
4097
|
+
if (!(await confirmMutation(`Update ${definition.command} ${documentNumber}`, opts, {
|
|
4098
|
+
[definition.envelope]: fields,
|
|
4099
|
+
})))
|
|
4100
|
+
return;
|
|
4101
|
+
const result = await operations[definition.update](documentNumber, fields);
|
|
4102
|
+
outputDetail(result, referenceDataColumns, json(), definition.envelope);
|
|
4103
|
+
});
|
|
4104
|
+
resource
|
|
4105
|
+
.command('delete <documentNumber>')
|
|
4106
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
4107
|
+
.option('--dry-run', 'Preview the action without sending it')
|
|
4108
|
+
.action(async (documentNumber, opts) => {
|
|
4109
|
+
const operations = await import('./operations/accruals.js');
|
|
4110
|
+
if (!(await confirmMutation(`Delete ${definition.command} ${documentNumber}`, opts)))
|
|
4111
|
+
return;
|
|
4112
|
+
await operations[definition.delete](documentNumber);
|
|
4113
|
+
outputConfirmation(`${definition.envelope} ${documentNumber} deleted.`, json(), {
|
|
4114
|
+
DocumentNumber: documentNumber,
|
|
4115
|
+
deleted: true,
|
|
4116
|
+
});
|
|
4117
|
+
});
|
|
4118
|
+
}
|
|
4119
|
+
const referenceResources = [
|
|
4120
|
+
{
|
|
4121
|
+
command: 'currencies',
|
|
4122
|
+
description: 'Currency reference data',
|
|
4123
|
+
list: 'listCurrencies',
|
|
4124
|
+
get: 'getCurrency',
|
|
4125
|
+
},
|
|
4126
|
+
{ command: 'units', description: 'Unit reference data', list: 'listUnits', get: 'getUnit' },
|
|
4127
|
+
{
|
|
4128
|
+
command: 'modes-of-payments',
|
|
4129
|
+
description: 'Payment mode reference data',
|
|
4130
|
+
list: 'listModesOfPayments',
|
|
4131
|
+
get: 'getModeOfPayment',
|
|
4132
|
+
},
|
|
4133
|
+
{
|
|
4134
|
+
command: 'terms-of-deliveries',
|
|
4135
|
+
description: 'Delivery term reference data',
|
|
4136
|
+
list: 'listTermsOfDeliveries',
|
|
4137
|
+
get: 'getTermOfDelivery',
|
|
4138
|
+
},
|
|
4139
|
+
{
|
|
4140
|
+
command: 'terms-of-payments',
|
|
4141
|
+
description: 'Payment term reference data',
|
|
4142
|
+
list: 'listTermsOfPayments',
|
|
4143
|
+
get: 'getTermOfPayment',
|
|
4144
|
+
},
|
|
4145
|
+
{
|
|
4146
|
+
command: 'ways-of-delivery',
|
|
4147
|
+
description: 'Delivery way reference data',
|
|
4148
|
+
list: 'listWaysOfDelivery',
|
|
4149
|
+
get: 'getWayOfDelivery',
|
|
4150
|
+
},
|
|
4151
|
+
{
|
|
4152
|
+
command: 'voucher-series',
|
|
4153
|
+
description: 'Voucher series setup data',
|
|
4154
|
+
list: 'listVoucherSeries',
|
|
4155
|
+
get: 'getVoucherSeries',
|
|
4156
|
+
},
|
|
4157
|
+
{
|
|
4158
|
+
command: 'predefined-voucher-series',
|
|
4159
|
+
description: 'Predefined voucher series',
|
|
4160
|
+
list: 'listPredefinedVoucherSeries',
|
|
4161
|
+
get: 'getPredefinedVoucherSeries',
|
|
4162
|
+
},
|
|
4163
|
+
{
|
|
4164
|
+
command: 'account-charts',
|
|
4165
|
+
description: 'Available account chart types',
|
|
4166
|
+
list: 'listAccountCharts',
|
|
4167
|
+
},
|
|
4168
|
+
{
|
|
4169
|
+
command: 'predefined-accounts',
|
|
4170
|
+
description: 'Predefined account setup',
|
|
4171
|
+
list: 'listPredefinedAccounts',
|
|
4172
|
+
get: 'getPredefinedAccount',
|
|
4173
|
+
},
|
|
4174
|
+
{
|
|
4175
|
+
command: 'customer-references',
|
|
4176
|
+
description: 'Customer reference data',
|
|
4177
|
+
list: 'listCustomerReferences',
|
|
4178
|
+
get: 'getCustomerReference',
|
|
4179
|
+
},
|
|
4180
|
+
];
|
|
4181
|
+
for (const definition of referenceResources) {
|
|
4182
|
+
const resource = program.command(definition.command).description(definition.description);
|
|
4183
|
+
resource
|
|
4184
|
+
.command('list')
|
|
4185
|
+
.option('--page <number>', 'Page number', parseInt)
|
|
4186
|
+
.option('--limit <number>', 'Results per page', parseInt)
|
|
4187
|
+
.option('-a, --all', 'Fetch all pages')
|
|
4188
|
+
.action(async (opts) => {
|
|
4189
|
+
const operations = await import('./operations/reference-data.js');
|
|
4190
|
+
const list = operations[definition.list];
|
|
4191
|
+
const result = await list({ page: opts.page, limit: opts.limit, all: opts.all });
|
|
4192
|
+
outputList(result.items, referenceDataColumns, json(), result.raw, result.raw.MetaInformation);
|
|
4193
|
+
});
|
|
4194
|
+
if ('get' in definition) {
|
|
4195
|
+
resource
|
|
4196
|
+
.command('get <identifier>')
|
|
4197
|
+
.description(`Get one ${definition.command} entry`)
|
|
4198
|
+
.action(async (identifier) => {
|
|
4199
|
+
const operations = await import('./operations/reference-data.js');
|
|
4200
|
+
const get = operations[definition.get];
|
|
4201
|
+
const result = await get(identifier);
|
|
4202
|
+
outputDetail(result.item, referenceDataColumns, json(), definition.command);
|
|
4203
|
+
});
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
3205
4206
|
// --- analytics ---
|
|
3206
4207
|
const analytics = program
|
|
3207
4208
|
.command('analytics')
|
|
@@ -3323,7 +4324,8 @@ Examples:
|
|
|
3323
4324
|
// Error handling (configureOutput + exitOverride set above, before the command
|
|
3324
4325
|
// tree, so subcommands inherit them).
|
|
3325
4326
|
try {
|
|
3326
|
-
|
|
4327
|
+
const args = process.argv.length === 2 ? [...process.argv, '--help'] : process.argv;
|
|
4328
|
+
await program.parseAsync(args);
|
|
3327
4329
|
}
|
|
3328
4330
|
catch (err) {
|
|
3329
4331
|
const code = err instanceof Error && 'code' in err ? err.code : undefined;
|