noxctl 0.9.0 → 0.10.0

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