noxctl 0.8.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 +70 -3
- 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/auth.d.ts +1 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +7 -0
- package/dist/auth.js.map +1 -1
- 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 +1114 -18
- package/dist/cli.js.map +1 -1
- package/dist/fortnox-client.d.ts +6 -0
- package/dist/fortnox-client.d.ts.map +1 -1
- package/dist/fortnox-client.js +36 -1
- 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 +386 -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 +32 -1
- package/dist/operations/invoices.d.ts.map +1 -1
- package/dist/operations/invoices.js +140 -1
- 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 +18 -1
- package/dist/operations/vouchers.d.ts.map +1 -1
- package/dist/operations/vouchers.js +49 -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/scope-probes.d.ts.map +1 -1
- package/dist/scope-probes.js +1 -0
- package/dist/scope-probes.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 +121 -2
- 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 +187 -57
- 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 +5 -0
- package/dist/views.d.ts.map +1 -1
- package/dist/views.js +40 -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, 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
|
|
@@ -199,15 +200,23 @@ program
|
|
|
199
200
|
.option('--profile <name>', 'Profile to create/re-auth (defaults to resolved profile or "default")')
|
|
200
201
|
.option('--with-salary', 'Also request the Lön (salary/payroll) scope — requires the Lön permission enabled on your Fortnox app')
|
|
201
202
|
.option('--with-orders', 'Also request the offer/order scopes — requires the Order licence on the Fortnox company')
|
|
203
|
+
.option('--with-archive', 'Also request the archive (Arkivplats) scope — lets noxctl read files from the Fortnox file archive, e.g. attachments made via the Fortnox web UI')
|
|
202
204
|
.action(async (initOpts) => {
|
|
203
|
-
const { inspectCredentials, runOAuthSetup, SCOPES, SALARY_SCOPE, ORDER_SCOPES } = await import('./auth.js');
|
|
205
|
+
const { inspectCredentials, runOAuthSetup, SCOPES, SALARY_SCOPE, ORDER_SCOPES, ARCHIVE_SCOPE, } = await import('./auth.js');
|
|
204
206
|
const { validateProfileName } = await import('./profile-name.js');
|
|
205
|
-
// Opt-in scopes: flag for TTY, env var for non-interactive/CI runs.
|
|
206
|
-
// licence-gated in Fortnox (Lön / Order), and asking for a scope
|
|
207
|
-
// is not licensed for fails the whole authorization — hence not
|
|
207
|
+
// Opt-in scopes: flag for TTY, env var for non-interactive/CI runs. All are
|
|
208
|
+
// licence-gated in Fortnox (Lön / Order / Arkivplats), and asking for a scope
|
|
209
|
+
// the company is not licensed for fails the whole authorization — hence not
|
|
210
|
+
// defaults.
|
|
208
211
|
const withSalary = Boolean(initOpts.withSalary) || process.env.FORTNOX_WITH_SALARY === '1';
|
|
209
212
|
const withOrders = Boolean(initOpts.withOrders) || process.env.FORTNOX_WITH_ORDERS === '1';
|
|
210
|
-
const
|
|
213
|
+
const withArchive = Boolean(initOpts.withArchive) || process.env.FORTNOX_WITH_ARCHIVE === '1';
|
|
214
|
+
const scopes = [
|
|
215
|
+
SCOPES,
|
|
216
|
+
withOrders ? ORDER_SCOPES : '',
|
|
217
|
+
withSalary ? SALARY_SCOPE : '',
|
|
218
|
+
withArchive ? ARCHIVE_SCOPE : '',
|
|
219
|
+
]
|
|
211
220
|
.filter(Boolean)
|
|
212
221
|
.join(' ');
|
|
213
222
|
let targetProfile;
|
|
@@ -1039,9 +1048,9 @@ keychain
|
|
|
1039
1048
|
console.log(`Deleted ${deleted} login-keychain entr${deleted === 1 ? 'y' : 'ies'}.`);
|
|
1040
1049
|
console.log('Credentials now live only in the YubiKey-locked keychain.');
|
|
1041
1050
|
});
|
|
1042
|
-
// --- serve
|
|
1051
|
+
// --- serve ---
|
|
1043
1052
|
program
|
|
1044
|
-
.command('serve'
|
|
1053
|
+
.command('serve')
|
|
1045
1054
|
.description('Start the MCP server (stdio transport)')
|
|
1046
1055
|
.action(async () => {
|
|
1047
1056
|
const { startMcpServer } = await import('./index.js');
|
|
@@ -1149,6 +1158,51 @@ invoices
|
|
|
1149
1158
|
const data = await sendInvoice(documentNumber, opts.method, emailOptions);
|
|
1150
1159
|
outputConfirmation(`Invoice ${documentNumber} sent via ${opts.method}.`, json(), data, invoiceConfirmColumns, 'Invoice');
|
|
1151
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
|
+
});
|
|
1152
1206
|
invoices
|
|
1153
1207
|
.command('pdf <documentNumber>')
|
|
1154
1208
|
.description('Download an invoice as a PDF')
|
|
@@ -1280,6 +1334,47 @@ invoices
|
|
|
1280
1334
|
const data = await creditInvoice(documentNumber);
|
|
1281
1335
|
outputConfirmation(`Invoice ${documentNumber} credited.`, json(), data, invoiceConfirmColumns, 'Invoice');
|
|
1282
1336
|
});
|
|
1337
|
+
invoices
|
|
1338
|
+
.command('attach <documentNumber> <files...>')
|
|
1339
|
+
.description('Upload receipt/underlag files and attach them to a customer invoice')
|
|
1340
|
+
.option('--no-include-on-send', 'Do not bundle the file when the invoice is sent (default: bundled)')
|
|
1341
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
1342
|
+
.option('--dry-run', 'Preview without uploading')
|
|
1343
|
+
.addHelpText('after', `
|
|
1344
|
+
Requires the "archive" scope — run \`noxctl init --with-archive\` first if
|
|
1345
|
+
\`noxctl doctor\` reports it missing.
|
|
1346
|
+
|
|
1347
|
+
Examples:
|
|
1348
|
+
noxctl invoices attach 91110646 underlag.pdf
|
|
1349
|
+
noxctl invoices attach 91110646 underlag.pdf --no-include-on-send`)
|
|
1350
|
+
.action(async (documentNumber, files, opts) => {
|
|
1351
|
+
const { attachInvoiceFiles } = await import('./operations/invoices.js');
|
|
1352
|
+
if (!(await confirmMutation(`Attach ${files.length} file(s) to invoice ${documentNumber}`, opts, {
|
|
1353
|
+
files,
|
|
1354
|
+
includeOnSend: opts.includeOnSend ?? true,
|
|
1355
|
+
}))) {
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
const results = await attachInvoiceFiles({
|
|
1359
|
+
documentNumber,
|
|
1360
|
+
filePaths: files,
|
|
1361
|
+
includeOnSend: opts.includeOnSend,
|
|
1362
|
+
});
|
|
1363
|
+
if (json()) {
|
|
1364
|
+
console.log(JSON.stringify({ Attachments: results }, null, 2));
|
|
1365
|
+
}
|
|
1366
|
+
else {
|
|
1367
|
+
outputList(results, invoiceAttachmentColumns, false, results);
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
invoices
|
|
1371
|
+
.command('attachments <documentNumber>')
|
|
1372
|
+
.description('List files attached to a customer invoice')
|
|
1373
|
+
.action(async (documentNumber) => {
|
|
1374
|
+
const { listInvoiceAttachments } = await import('./operations/invoices.js');
|
|
1375
|
+
const results = await listInvoiceAttachments(documentNumber);
|
|
1376
|
+
outputList(results, invoiceAttachmentListColumns, json(), { Attachments: results });
|
|
1377
|
+
});
|
|
1283
1378
|
// --- tax ---
|
|
1284
1379
|
const tax = program.command('tax').description('Tax operations');
|
|
1285
1380
|
tax
|
|
@@ -1369,6 +1464,59 @@ accounts
|
|
|
1369
1464
|
});
|
|
1370
1465
|
outputList(data.Accounts ?? [], accountListColumns, json(), data, data.MetaInformation);
|
|
1371
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
|
+
});
|
|
1372
1520
|
// --- customers ---
|
|
1373
1521
|
const customers = program.command('customers').description('Customer operations');
|
|
1374
1522
|
customers
|
|
@@ -1430,7 +1578,7 @@ customers
|
|
|
1430
1578
|
.option('--dry-run', 'Preview the request without sending it')
|
|
1431
1579
|
.addHelpText('after', `
|
|
1432
1580
|
Examples:
|
|
1433
|
-
echo '{"Email":"new@acme.se","
|
|
1581
|
+
echo '{"Email":"new@acme.se","Phone1":"08-123456"}' | noxctl customers update 25 --input - --yes`)
|
|
1434
1582
|
.action(async (customerNumber, opts) => {
|
|
1435
1583
|
const { updateCustomer } = await import('./operations/customers.js');
|
|
1436
1584
|
const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
|
|
@@ -1441,6 +1589,18 @@ Examples:
|
|
|
1441
1589
|
const data = await updateCustomer(customerNumber, fields);
|
|
1442
1590
|
outputDetail(data, customerDetailColumns, json(), 'Customer');
|
|
1443
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
|
+
});
|
|
1444
1604
|
// --- articles ---
|
|
1445
1605
|
const articles = program.command('articles').description('Article operations');
|
|
1446
1606
|
articles
|
|
@@ -1516,6 +1676,18 @@ Examples:
|
|
|
1516
1676
|
const data = await updateArticle(articleNumber, fields);
|
|
1517
1677
|
outputDetail(data, articleDetailColumns, json(), 'Article');
|
|
1518
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
|
+
});
|
|
1519
1691
|
// --- suppliers ---
|
|
1520
1692
|
const suppliers = program.command('suppliers').description('Supplier operations');
|
|
1521
1693
|
suppliers
|
|
@@ -1596,7 +1768,7 @@ const supplierInvoices = program
|
|
|
1596
1768
|
supplierInvoices
|
|
1597
1769
|
.command('list')
|
|
1598
1770
|
.description('List/filter supplier invoices')
|
|
1599
|
-
.option('--filter <filter>', 'Filter: cancelled, fullypaid, unpaid, unpaidoverdue, unbooked, pendingpayment')
|
|
1771
|
+
.option('--filter <filter>', 'Filter: cancelled, fullypaid, unpaid, unpaidoverdue, unbooked, pendingpayment, authorizepending')
|
|
1600
1772
|
.option('--supplier <number>', 'Filter by supplier number')
|
|
1601
1773
|
.option('--from <date>', 'From date (YYYY-MM-DD)')
|
|
1602
1774
|
.option('--to <date>', 'To date (YYYY-MM-DD)')
|
|
@@ -1661,6 +1833,98 @@ supplierInvoices
|
|
|
1661
1833
|
const data = await bookkeepSupplierInvoice(givenNumber);
|
|
1662
1834
|
outputConfirmation(`Supplier invoice ${givenNumber} bookkeept.`, json(), data, supplierInvoiceConfirmColumns, 'SupplierInvoice');
|
|
1663
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
|
+
});
|
|
1664
1928
|
// --- company ---
|
|
1665
1929
|
const company = program.command('company').description('Company operations');
|
|
1666
1930
|
company
|
|
@@ -1765,6 +2029,138 @@ Examples:
|
|
|
1765
2029
|
outputList(results, voucherAttachmentColumns, false, results);
|
|
1766
2030
|
}
|
|
1767
2031
|
});
|
|
2032
|
+
vouchers
|
|
2033
|
+
.command('attachments <series> <voucherNumber>')
|
|
2034
|
+
.description('List files (receipts/underlag) attached to a voucher')
|
|
2035
|
+
.option('--year <number>', 'Financial year (recommended — voucher numbers reset between financial years)', parseInt)
|
|
2036
|
+
.action(async (series, voucherNumber, opts) => {
|
|
2037
|
+
const { listVoucherAttachments } = await import('./operations/vouchers.js');
|
|
2038
|
+
const attachments = await listVoucherAttachments(series, voucherNumber, opts.year);
|
|
2039
|
+
if (json()) {
|
|
2040
|
+
console.log(JSON.stringify({ Attachments: attachments }, null, 2));
|
|
2041
|
+
}
|
|
2042
|
+
else {
|
|
2043
|
+
outputList(attachments, voucherAttachmentColumns, false, attachments);
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
2046
|
+
vouchers
|
|
2047
|
+
.command('file <fileId>')
|
|
2048
|
+
.description('Download a file attached to a voucher (get the fileId from "vouchers attachments")')
|
|
2049
|
+
.option('-f, --file <path>', 'Write the file here (- for stdout; default: voucher-file-<fileId><ext>)')
|
|
2050
|
+
.addHelpText('after', `
|
|
2051
|
+
Examples:
|
|
2052
|
+
noxctl vouchers attachments A 60 --year 4
|
|
2053
|
+
noxctl vouchers file c7e578d8-59a7-4e00-b29b-b99e4d9ede63
|
|
2054
|
+
noxctl vouchers file c7e578d8-59a7-4e00-b29b-b99e4d9ede63 --file receipt.pdf`)
|
|
2055
|
+
.action(async (fileId, opts) => {
|
|
2056
|
+
const { getVoucherFile, extensionForMime } = await import('./operations/vouchers.js');
|
|
2057
|
+
const toStdout = opts.file === '-';
|
|
2058
|
+
if (toStdout && program.opts().output === 'json') {
|
|
2059
|
+
throw new Error('--file - writes raw file bytes to stdout and cannot be combined with --output json.');
|
|
2060
|
+
}
|
|
2061
|
+
const file = await getVoucherFile(fileId);
|
|
2062
|
+
if (toStdout) {
|
|
2063
|
+
await new Promise((resolve, reject) => {
|
|
2064
|
+
process.stdout.write(file.buffer, (err) => (err ? reject(err) : resolve()));
|
|
2065
|
+
});
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
const path = opts.file ?? `voucher-file-${fileId}${extensionForMime(file.contentType)}`;
|
|
2069
|
+
writeFileSync(path, file.buffer);
|
|
2070
|
+
outputConfirmation(`File saved to ${path} (${file.contentType}, ${file.buffer.length} bytes).`, json(), {
|
|
2071
|
+
FileId: fileId,
|
|
2072
|
+
Path: path,
|
|
2073
|
+
ContentType: file.contentType,
|
|
2074
|
+
Bytes: file.buffer.length,
|
|
2075
|
+
});
|
|
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
|
+
});
|
|
1768
2164
|
// --- invoice-payments ---
|
|
1769
2165
|
const invoicePayments = program
|
|
1770
2166
|
.command('invoice-payments')
|
|
@@ -1849,6 +2245,23 @@ invoicePayments
|
|
|
1849
2245
|
const result = await bookkeepInvoicePayment(paymentNumber);
|
|
1850
2246
|
outputConfirmation(`Invoice payment ${paymentNumber} bookkeept.`, json(), result, undefined, 'InvoicePayment');
|
|
1851
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
|
+
});
|
|
1852
2265
|
// --- supplier-invoice-payments ---
|
|
1853
2266
|
const supplierInvoicePayments = program
|
|
1854
2267
|
.command('supplier-invoice-payments')
|
|
@@ -1918,6 +2331,35 @@ supplierInvoicePayments
|
|
|
1918
2331
|
await deleteSupplierInvoicePayment(paymentNumber);
|
|
1919
2332
|
outputConfirmation(`Supplier invoice payment ${paymentNumber} deleted.`, json(), { Number: paymentNumber, deleted: true }, undefined, 'SupplierInvoicePayment');
|
|
1920
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
|
+
});
|
|
1921
2363
|
// --- offers ---
|
|
1922
2364
|
const offers = program.command('offers').description('Offer/quote operations (offerter)');
|
|
1923
2365
|
offers
|
|
@@ -2017,6 +2459,56 @@ offers
|
|
|
2017
2459
|
const data = await createOrderFromOffer(documentNumber);
|
|
2018
2460
|
outputConfirmation(`Order created from offer ${documentNumber}.`, json(), data, undefined, 'Order');
|
|
2019
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
|
+
});
|
|
2020
2512
|
// --- orders ---
|
|
2021
2513
|
const orders = program.command('orders').description('Order operations (ordrar)');
|
|
2022
2514
|
orders
|
|
@@ -2103,6 +2595,56 @@ orders
|
|
|
2103
2595
|
const data = await createInvoiceFromOrder(documentNumber);
|
|
2104
2596
|
outputConfirmation(`Invoice created from order ${documentNumber}.`, json(), data, invoiceConfirmColumns, 'Invoice');
|
|
2105
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
|
+
});
|
|
2106
2648
|
// --- projects ---
|
|
2107
2649
|
const projects = program.command('projects').description('Project operations');
|
|
2108
2650
|
projects
|
|
@@ -2172,6 +2714,21 @@ projects
|
|
|
2172
2714
|
const data = await updateProject(projectNumber, fields);
|
|
2173
2715
|
outputDetail(data, projectDetailColumns, json(), 'Project');
|
|
2174
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
|
+
});
|
|
2175
2732
|
// --- cost centers ---
|
|
2176
2733
|
const costcenters = program.command('costcenters').description('Cost center operations');
|
|
2177
2734
|
costcenters
|
|
@@ -2423,6 +2980,23 @@ salarytransactions
|
|
|
2423
2980
|
await deleteSalaryTransaction(salaryRow);
|
|
2424
2981
|
outputConfirmation(`Salary transaction ${salaryRow} deleted.`, json(), { SalaryRow: salaryRow, deleted: true }, undefined, 'SalaryTransaction');
|
|
2425
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
|
+
});
|
|
2426
3000
|
// --- attendance transactions (närvaro / Lön) ---
|
|
2427
3001
|
const attendancetransactions = program
|
|
2428
3002
|
.command('attendance-transactions')
|
|
@@ -2450,9 +3024,15 @@ attendancetransactions
|
|
|
2450
3024
|
attendancetransactions
|
|
2451
3025
|
.command('get <id>')
|
|
2452
3026
|
.description('Get a single attendance transaction')
|
|
2453
|
-
.
|
|
2454
|
-
|
|
2455
|
-
|
|
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);
|
|
2456
3036
|
outputDetail(data, attendanceTransactionDetailColumns, json(), 'AttendanceTransaction');
|
|
2457
3037
|
});
|
|
2458
3038
|
attendancetransactions
|
|
@@ -2501,6 +3081,23 @@ attendancetransactions
|
|
|
2501
3081
|
await deleteAttendanceTransaction(id);
|
|
2502
3082
|
outputConfirmation(`Attendance transaction ${id} deleted.`, json(), { id, deleted: true }, undefined, 'AttendanceTransaction');
|
|
2503
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
|
+
});
|
|
2504
3101
|
// --- absence transactions (frånvaro / Lön) ---
|
|
2505
3102
|
const absencetransactions = program
|
|
2506
3103
|
.command('absence-transactions')
|
|
@@ -2528,9 +3125,15 @@ absencetransactions
|
|
|
2528
3125
|
absencetransactions
|
|
2529
3126
|
.command('get <id>')
|
|
2530
3127
|
.description('Get a single absence transaction')
|
|
2531
|
-
.
|
|
2532
|
-
|
|
2533
|
-
|
|
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);
|
|
2534
3137
|
outputDetail(data, absenceTransactionDetailColumns, json(), 'AbsenceTransaction');
|
|
2535
3138
|
});
|
|
2536
3139
|
absencetransactions
|
|
@@ -2582,6 +3185,23 @@ absencetransactions
|
|
|
2582
3185
|
await deleteAbsenceTransaction(id);
|
|
2583
3186
|
outputConfirmation(`Absence transaction ${id} deleted.`, json(), { id, deleted: true }, undefined, 'AbsenceTransaction');
|
|
2584
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
|
+
});
|
|
2585
3205
|
// --- schedule times (schematider / Lön) ---
|
|
2586
3206
|
const scheduletimes = program
|
|
2587
3207
|
.command('schedule-times')
|
|
@@ -2695,6 +3315,34 @@ taxreductions
|
|
|
2695
3315
|
const data = await createTaxReduction(params);
|
|
2696
3316
|
outputDetail(data, taxReductionDetailColumns, json(), 'TaxReduction');
|
|
2697
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
|
+
});
|
|
2698
3346
|
// --- price lists ---
|
|
2699
3347
|
const pricelists = program.command('pricelists').description('Price list operations');
|
|
2700
3348
|
pricelists
|
|
@@ -2768,7 +3416,7 @@ const prices = program.command('prices').description('Price operations within pr
|
|
|
2768
3416
|
prices
|
|
2769
3417
|
.command('list')
|
|
2770
3418
|
.description('List prices in a price list')
|
|
2771
|
-
.
|
|
3419
|
+
.option('--pricelist <code>', 'Price list code (omit to list all prices)')
|
|
2772
3420
|
.option('--article <number>', 'Filter by article number')
|
|
2773
3421
|
.option('--page <number>', 'Page number', parseInt)
|
|
2774
3422
|
.option('--limit <number>', 'Results per page', parseInt)
|
|
@@ -2794,6 +3442,26 @@ prices
|
|
|
2794
3442
|
const data = await getPrice(opts.pricelist, opts.article, opts.fromQuantity);
|
|
2795
3443
|
outputDetail(data, priceDetailColumns, json(), 'Price');
|
|
2796
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
|
+
});
|
|
2797
3465
|
prices
|
|
2798
3466
|
.command('update')
|
|
2799
3467
|
.description('Update a price')
|
|
@@ -2815,6 +3483,22 @@ prices
|
|
|
2815
3483
|
const data = await updatePrice(opts.pricelist, opts.article, fields, opts.fromQuantity);
|
|
2816
3484
|
outputDetail(data, priceDetailColumns, json(), 'Price');
|
|
2817
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
|
+
});
|
|
2818
3502
|
// --- contracts ---
|
|
2819
3503
|
const contracts = program
|
|
2820
3504
|
.command('contracts')
|
|
@@ -3092,6 +3776,27 @@ financialYears
|
|
|
3092
3776
|
const data = await getFinancialYear(parseInt(id, 10));
|
|
3093
3777
|
outputDetail(data, financialYearDetailColumns, json(), 'FinancialYear');
|
|
3094
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
|
+
});
|
|
3095
3800
|
financialYears
|
|
3096
3801
|
.command('locked-period')
|
|
3097
3802
|
.description('Show the locked period (bokföring låst t.o.m.)')
|
|
@@ -3108,6 +3813,396 @@ financialYears
|
|
|
3108
3813
|
}
|
|
3109
3814
|
outputDetail(data, lockedPeriodDetailColumns, false);
|
|
3110
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
|
+
}
|
|
3111
4206
|
// --- analytics ---
|
|
3112
4207
|
const analytics = program
|
|
3113
4208
|
.command('analytics')
|
|
@@ -3229,7 +4324,8 @@ Examples:
|
|
|
3229
4324
|
// Error handling (configureOutput + exitOverride set above, before the command
|
|
3230
4325
|
// tree, so subcommands inherit them).
|
|
3231
4326
|
try {
|
|
3232
|
-
|
|
4327
|
+
const args = process.argv.length === 2 ? [...process.argv, '--help'] : process.argv;
|
|
4328
|
+
await program.parseAsync(args);
|
|
3233
4329
|
}
|
|
3234
4330
|
catch (err) {
|
|
3235
4331
|
const code = err instanceof Error && 'code' in err ? err.code : undefined;
|