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.
Files changed (207) hide show
  1. package/README.md +70 -3
  2. package/dist/api-coverage.d.ts +48 -0
  3. package/dist/api-coverage.d.ts.map +1 -0
  4. package/dist/api-coverage.js +175 -0
  5. package/dist/api-coverage.js.map +1 -0
  6. package/dist/auth.d.ts +1 -0
  7. package/dist/auth.d.ts.map +1 -1
  8. package/dist/auth.js +7 -0
  9. package/dist/auth.js.map +1 -1
  10. package/dist/cli-validators.d.ts +3 -0
  11. package/dist/cli-validators.d.ts.map +1 -0
  12. package/dist/cli-validators.js +10 -0
  13. package/dist/cli-validators.js.map +1 -0
  14. package/dist/cli.d.ts.map +1 -1
  15. package/dist/cli.js +1114 -18
  16. package/dist/cli.js.map +1 -1
  17. package/dist/fortnox-client.d.ts +6 -0
  18. package/dist/fortnox-client.d.ts.map +1 -1
  19. package/dist/fortnox-client.js +36 -1
  20. package/dist/fortnox-client.js.map +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +8 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/operations/absencetransactions.d.ts +3 -1
  25. package/dist/operations/absencetransactions.d.ts.map +1 -1
  26. package/dist/operations/absencetransactions.js +11 -1
  27. package/dist/operations/absencetransactions.js.map +1 -1
  28. package/dist/operations/accounts.d.ts +5 -1
  29. package/dist/operations/accounts.d.ts.map +1 -1
  30. package/dist/operations/accounts.js +24 -2
  31. package/dist/operations/accounts.js.map +1 -1
  32. package/dist/operations/accruals.d.ts +65 -0
  33. package/dist/operations/accruals.d.ts.map +1 -0
  34. package/dist/operations/accruals.js +81 -0
  35. package/dist/operations/accruals.js.map +1 -0
  36. package/dist/operations/articles.d.ts +2 -1
  37. package/dist/operations/articles.d.ts.map +1 -1
  38. package/dist/operations/articles.js +7 -2
  39. package/dist/operations/articles.js.map +1 -1
  40. package/dist/operations/attendancetransactions.d.ts +3 -1
  41. package/dist/operations/attendancetransactions.d.ts.map +1 -1
  42. package/dist/operations/attendancetransactions.js +11 -1
  43. package/dist/operations/attendancetransactions.js.map +1 -1
  44. package/dist/operations/customers.d.ts +2 -1
  45. package/dist/operations/customers.d.ts.map +1 -1
  46. package/dist/operations/customers.js +7 -2
  47. package/dist/operations/customers.js.map +1 -1
  48. package/dist/operations/files.d.ts +85 -0
  49. package/dist/operations/files.d.ts.map +1 -0
  50. package/dist/operations/files.js +88 -0
  51. package/dist/operations/files.js.map +1 -0
  52. package/dist/operations/financial-years.d.ts +2 -1
  53. package/dist/operations/financial-years.d.ts.map +1 -1
  54. package/dist/operations/financial-years.js +9 -2
  55. package/dist/operations/financial-years.js.map +1 -1
  56. package/dist/operations/general-ledger.d.ts +24 -0
  57. package/dist/operations/general-ledger.d.ts.map +1 -0
  58. package/dist/operations/general-ledger.js +66 -0
  59. package/dist/operations/general-ledger.js.map +1 -0
  60. package/dist/operations/index.d.ts +386 -0
  61. package/dist/operations/index.d.ts.map +1 -1
  62. package/dist/operations/index.js +8 -0
  63. package/dist/operations/index.js.map +1 -1
  64. package/dist/operations/invoice-payments.d.ts +2 -1
  65. package/dist/operations/invoice-payments.d.ts.map +1 -1
  66. package/dist/operations/invoice-payments.js +6 -1
  67. package/dist/operations/invoice-payments.js.map +1 -1
  68. package/dist/operations/invoices.d.ts +32 -1
  69. package/dist/operations/invoices.d.ts.map +1 -1
  70. package/dist/operations/invoices.js +140 -1
  71. package/dist/operations/invoices.js.map +1 -1
  72. package/dist/operations/offers.d.ts +5 -1
  73. package/dist/operations/offers.d.ts.map +1 -1
  74. package/dist/operations/offers.js +19 -1
  75. package/dist/operations/offers.js.map +1 -1
  76. package/dist/operations/orders.d.ts +5 -1
  77. package/dist/operations/orders.d.ts.map +1 -1
  78. package/dist/operations/orders.js +26 -2
  79. package/dist/operations/orders.js.map +1 -1
  80. package/dist/operations/pricelists.d.ts +4 -2
  81. package/dist/operations/pricelists.d.ts.map +1 -1
  82. package/dist/operations/pricelists.js +23 -9
  83. package/dist/operations/pricelists.js.map +1 -1
  84. package/dist/operations/projects.d.ts +2 -1
  85. package/dist/operations/projects.d.ts.map +1 -1
  86. package/dist/operations/projects.js +5 -2
  87. package/dist/operations/projects.js.map +1 -1
  88. package/dist/operations/reference-data.d.ts +243 -0
  89. package/dist/operations/reference-data.d.ts.map +1 -0
  90. package/dist/operations/reference-data.js +113 -0
  91. package/dist/operations/reference-data.js.map +1 -0
  92. package/dist/operations/salarytransactions.d.ts +2 -1
  93. package/dist/operations/salarytransactions.d.ts.map +1 -1
  94. package/dist/operations/salarytransactions.js +6 -1
  95. package/dist/operations/salarytransactions.js.map +1 -1
  96. package/dist/operations/supplier-invoice-payments.d.ts +3 -1
  97. package/dist/operations/supplier-invoice-payments.d.ts.map +1 -1
  98. package/dist/operations/supplier-invoice-payments.js +11 -1
  99. package/dist/operations/supplier-invoice-payments.js.map +1 -1
  100. package/dist/operations/supplier-invoices.d.ts +21 -1
  101. package/dist/operations/supplier-invoices.d.ts.map +1 -1
  102. package/dist/operations/supplier-invoices.js +68 -1
  103. package/dist/operations/supplier-invoices.js.map +1 -1
  104. package/dist/operations/taxreductions.d.ts +3 -1
  105. package/dist/operations/taxreductions.d.ts.map +1 -1
  106. package/dist/operations/taxreductions.js +18 -2
  107. package/dist/operations/taxreductions.js.map +1 -1
  108. package/dist/operations/vouchers.d.ts +18 -1
  109. package/dist/operations/vouchers.d.ts.map +1 -1
  110. package/dist/operations/vouchers.js +49 -1
  111. package/dist/operations/vouchers.js.map +1 -1
  112. package/dist/safe-file-output.d.ts +4 -0
  113. package/dist/safe-file-output.d.ts.map +1 -0
  114. package/dist/safe-file-output.js +48 -0
  115. package/dist/safe-file-output.js.map +1 -0
  116. package/dist/schema-audit-mappings.d.ts +6 -5
  117. package/dist/schema-audit-mappings.d.ts.map +1 -1
  118. package/dist/schema-audit-mappings.js +159 -10
  119. package/dist/schema-audit-mappings.js.map +1 -1
  120. package/dist/schema-audit.d.ts +30 -1
  121. package/dist/schema-audit.d.ts.map +1 -1
  122. package/dist/schema-audit.js +288 -14
  123. package/dist/schema-audit.js.map +1 -1
  124. package/dist/scope-probes.d.ts.map +1 -1
  125. package/dist/scope-probes.js +1 -0
  126. package/dist/scope-probes.js.map +1 -1
  127. package/dist/sie.d.ts +51 -0
  128. package/dist/sie.d.ts.map +1 -0
  129. package/dist/sie.js +355 -0
  130. package/dist/sie.js.map +1 -0
  131. package/dist/tools/absencetransactions.d.ts.map +1 -1
  132. package/dist/tools/absencetransactions.js +30 -3
  133. package/dist/tools/absencetransactions.js.map +1 -1
  134. package/dist/tools/accruals.d.ts +18 -0
  135. package/dist/tools/accruals.d.ts.map +1 -0
  136. package/dist/tools/accruals.js +169 -0
  137. package/dist/tools/accruals.js.map +1 -0
  138. package/dist/tools/articles.d.ts.map +1 -1
  139. package/dist/tools/articles.js +17 -2
  140. package/dist/tools/articles.js.map +1 -1
  141. package/dist/tools/attendancetransactions.d.ts.map +1 -1
  142. package/dist/tools/attendancetransactions.js +30 -3
  143. package/dist/tools/attendancetransactions.js.map +1 -1
  144. package/dist/tools/bookkeeping.d.ts.map +1 -1
  145. package/dist/tools/bookkeeping.js +121 -2
  146. package/dist/tools/bookkeeping.js.map +1 -1
  147. package/dist/tools/contracts.js +1 -1
  148. package/dist/tools/contracts.js.map +1 -1
  149. package/dist/tools/customers.d.ts.map +1 -1
  150. package/dist/tools/customers.js +111 -20
  151. package/dist/tools/customers.js.map +1 -1
  152. package/dist/tools/files.d.ts +4 -0
  153. package/dist/tools/files.d.ts.map +1 -0
  154. package/dist/tools/files.js +120 -0
  155. package/dist/tools/files.js.map +1 -0
  156. package/dist/tools/financial-years.d.ts.map +1 -1
  157. package/dist/tools/financial-years.js +18 -2
  158. package/dist/tools/financial-years.js.map +1 -1
  159. package/dist/tools/general-ledger.d.ts +4 -0
  160. package/dist/tools/general-ledger.d.ts.map +1 -0
  161. package/dist/tools/general-ledger.js +61 -0
  162. package/dist/tools/general-ledger.js.map +1 -0
  163. package/dist/tools/invoice-payments.d.ts.map +1 -1
  164. package/dist/tools/invoice-payments.js +34 -1
  165. package/dist/tools/invoice-payments.js.map +1 -1
  166. package/dist/tools/invoices.d.ts.map +1 -1
  167. package/dist/tools/invoices.js +187 -57
  168. package/dist/tools/invoices.js.map +1 -1
  169. package/dist/tools/offers.d.ts.map +1 -1
  170. package/dist/tools/offers.js +68 -1
  171. package/dist/tools/offers.js.map +1 -1
  172. package/dist/tools/orders.d.ts.map +1 -1
  173. package/dist/tools/orders.js +68 -1
  174. package/dist/tools/orders.js.map +1 -1
  175. package/dist/tools/pricelists.d.ts.map +1 -1
  176. package/dist/tools/pricelists.js +35 -3
  177. package/dist/tools/pricelists.js.map +1 -1
  178. package/dist/tools/projects.d.ts.map +1 -1
  179. package/dist/tools/projects.js +14 -2
  180. package/dist/tools/projects.js.map +1 -1
  181. package/dist/tools/recurrings.js +1 -1
  182. package/dist/tools/recurrings.js.map +1 -1
  183. package/dist/tools/reference-data.d.ts +17 -0
  184. package/dist/tools/reference-data.d.ts.map +1 -0
  185. package/dist/tools/reference-data.js +124 -0
  186. package/dist/tools/reference-data.js.map +1 -0
  187. package/dist/tools/salarytransactions.d.ts.map +1 -1
  188. package/dist/tools/salarytransactions.js +27 -1
  189. package/dist/tools/salarytransactions.js.map +1 -1
  190. package/dist/tools/supplier-invoice-payments.d.ts.map +1 -1
  191. package/dist/tools/supplier-invoice-payments.js +34 -1
  192. package/dist/tools/supplier-invoice-payments.js.map +1 -1
  193. package/dist/tools/supplier-invoices.d.ts.map +1 -1
  194. package/dist/tools/supplier-invoices.js +147 -5
  195. package/dist/tools/supplier-invoices.js.map +1 -1
  196. package/dist/tools/taxreductions.d.ts.map +1 -1
  197. package/dist/tools/taxreductions.js +33 -2
  198. package/dist/tools/taxreductions.js.map +1 -1
  199. package/dist/version.d.ts +1 -1
  200. package/dist/version.d.ts.map +1 -1
  201. package/dist/version.js +1 -1
  202. package/dist/version.js.map +1 -1
  203. package/dist/views.d.ts +5 -0
  204. package/dist/views.d.ts.map +1 -1
  205. package/dist/views.js +40 -1
  206. package/dist/views.js.map +1 -1
  207. 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. Both are
206
- // licence-gated in Fortnox (Lön / Order), and asking for a scope the company
207
- // is not licensed for fails the whole authorization — hence not defaults.
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 scopes = [SCOPES, withOrders ? ORDER_SCOPES : '', withSalary ? SALARY_SCOPE : '']
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 (default command) ---
1051
+ // --- serve ---
1043
1052
  program
1044
- .command('serve', { isDefault: true })
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","Phone":"08-123456"}' | noxctl customers update 25 --input - --yes`)
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
- .action(async (id) => {
2454
- const { getAttendanceTransaction } = await import('./operations/attendancetransactions.js');
2455
- const data = await getAttendanceTransaction(id);
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
- .action(async (id) => {
2532
- const { getAbsenceTransaction } = await import('./operations/absencetransactions.js');
2533
- const data = await getAbsenceTransaction(id);
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
- .requiredOption('--pricelist <code>', 'Price list code')
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
- await program.parseAsync(process.argv);
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;