@aderis-erp/cli 1.0.17 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/access-groups.js +49 -17
- package/dist/commands/account-transaction-allocations.js +46 -14
- package/dist/commands/account-transactions.js +75 -35
- package/dist/commands/accounts.js +54 -22
- package/dist/commands/agent-platform.js +53 -21
- package/dist/commands/{nango-integrations.d.ts → agent-runs.d.ts} +1 -1
- package/dist/commands/agent-runs.js +1155 -0
- package/dist/commands/analytics.js +47 -15
- package/dist/commands/api-keys.js +49 -16
- package/dist/commands/{text-templates.d.ts → async-jobs.d.ts} +1 -1
- package/dist/commands/async-jobs.js +1158 -0
- package/dist/commands/audit-log.js +46 -14
- package/dist/commands/bank-accounts.js +63 -30
- package/dist/commands/bank-integrations.d.ts +1 -0
- package/dist/commands/bank-integrations.js +66 -31
- package/dist/commands/business-partners.js +77 -44
- package/dist/commands/chat.js +52 -19
- package/dist/commands/credit-card-accounts.d.ts +1 -0
- package/dist/commands/credit-card-accounts.js +67 -29
- package/dist/commands/delivery-notes.js +78 -44
- package/dist/commands/dunning-configs.js +49 -17
- package/dist/commands/dunning-reminders.js +52 -20
- package/dist/commands/dunning-runs.js +45 -13
- package/dist/commands/email-transactions.js +47 -15
- package/dist/commands/employees.js +76 -44
- package/dist/commands/entity-configurations.js +47 -15
- package/dist/commands/event-processor-mappings.js +49 -17
- package/dist/commands/files.js +56 -24
- package/dist/commands/health.js +49 -17
- package/dist/commands/{tag-configurations.d.ts → imports.d.ts} +1 -1
- package/dist/commands/{tag-configurations.js → imports.js} +59 -31
- package/dist/commands/inbox.js +52 -19
- package/dist/commands/integration-identities.d.ts +40 -0
- package/dist/commands/integration-identities.js +1151 -0
- package/dist/commands/integrations.js +47 -15
- package/dist/commands/item-categories.js +49 -17
- package/dist/commands/items.js +49 -17
- package/dist/commands/journal-lines.js +45 -13
- package/dist/commands/journals.js +68 -36
- package/dist/commands/ledgers.d.ts +3 -1
- package/dist/commands/ledgers.js +96 -34
- package/dist/commands/my-account.d.ts +39 -0
- package/dist/commands/my-account.js +1160 -0
- package/dist/commands/office-locations.js +49 -17
- package/dist/commands/payment-methods.js +54 -22
- package/dist/commands/payment-runs.js +61 -29
- package/dist/commands/payment-terms.js +50 -18
- package/dist/commands/payments.js +51 -19
- package/dist/commands/payroll-runs.d.ts +43 -0
- package/dist/commands/payroll-runs.js +1188 -0
- package/dist/commands/permissions.d.ts +39 -0
- package/dist/commands/{text-templates.js → permissions.js} +54 -30
- package/dist/commands/price-lists.js +54 -22
- package/dist/commands/processor-mappings.js +48 -16
- package/dist/commands/processors.js +55 -24
- package/dist/commands/purchase-order-types.js +49 -17
- package/dist/commands/purchase-orders.js +69 -36
- package/dist/commands/reports.js +49 -17
- package/dist/commands/roles.d.ts +39 -0
- package/dist/commands/{nango-integrations.js → roles.js} +63 -26
- package/dist/commands/salary-types.js +49 -17
- package/dist/commands/sales-invoices.js +84 -51
- package/dist/commands/sales-orders.js +78 -44
- package/dist/commands/saved-views.js +47 -15
- package/dist/commands/scheduled-processor-mappings.js +49 -17
- package/dist/commands/schedules.js +53 -21
- package/dist/commands/support/end.d.ts +9 -0
- package/dist/commands/support/end.js +29 -0
- package/dist/commands/support/start.d.ts +14 -0
- package/dist/commands/support/start.js +50 -0
- package/dist/commands/support/status.d.ts +10 -0
- package/dist/commands/support/status.js +45 -0
- package/dist/commands/support/tenants.d.ts +10 -0
- package/dist/commands/support/tenants.js +39 -0
- package/dist/commands/tags.js +51 -18
- package/dist/commands/tax-codes.js +52 -20
- package/dist/commands/tax-matrix.js +49 -17
- package/dist/commands/templates.d.ts +39 -0
- package/dist/commands/templates.js +1157 -0
- package/dist/commands/tenants.js +52 -20
- package/dist/commands/tools.js +46 -14
- package/dist/commands/users.d.ts +42 -0
- package/dist/commands/users.js +1174 -0
- package/dist/commands/vat-declarations.d.ts +2 -0
- package/dist/commands/vat-declarations.js +72 -36
- package/dist/commands/vendor-invoices.js +81 -49
- package/dist/commands/webhooks.js +51 -19
- package/dist/commands/workflows.js +47 -15
- package/dist/lib/api-client.d.ts +6 -0
- package/dist/lib/api-client.js +13 -0
- package/dist/lib/auth/constants.d.ts +12 -0
- package/dist/lib/auth/constants.js +12 -0
- package/dist/lib/auth/index.d.ts +11 -1
- package/dist/lib/auth/index.js +105 -6
- package/dist/lib/auth/jwks.d.ts +19 -0
- package/dist/lib/auth/jwks.js +112 -0
- package/dist/lib/auth/keycloak-provider.d.ts +45 -7
- package/dist/lib/auth/keycloak-provider.js +161 -27
- package/dist/lib/auth/tokens.d.ts +8 -3
- package/dist/lib/auth/tokens.js +8 -7
- package/dist/lib/auth/types.d.ts +5 -1
- package/dist/lib/config.d.ts +18 -0
- package/dist/lib/config.js +10 -0
- package/oclif.manifest.json +2182 -744
- package/package.json +7 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command, Args, Flags } from '@oclif/core';
|
|
2
|
-
import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
|
|
2
|
+
import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
|
|
3
3
|
import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
|
|
4
4
|
// Add 'import' action if both create and update exist
|
|
5
5
|
const HAS_IMPORT = true;
|
|
@@ -8,11 +8,11 @@ const HAS_CAMT053 = false;
|
|
|
8
8
|
const NESTED_IMPORTS = [];
|
|
9
9
|
const SUBCOMMANDS = [...["list", "create", "get", "update", "delete"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
|
|
10
10
|
const ENDPOINTS = {
|
|
11
|
-
'list': { method: 'GET', path: '/v1/salary-types', summary: 'Get all salary types with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
12
|
-
'create': { method: 'POST', path: '/v1/salary-types', summary: 'Create a new salary type', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
13
|
-
'get': { method: 'GET', path: '/v1/salary-types/{id}', summary: 'Get a salary type by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
14
|
-
'update': { method: 'PATCH', path: '/v1/salary-types/{id}', summary: 'Update a salary type', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
15
|
-
'delete': { method: 'DELETE', path: '/v1/salary-types/{id}', summary: 'Delete a salary type', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
11
|
+
'list': { method: 'GET', path: '/v1/salary-types', summary: 'Get all salary types with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
12
|
+
'create': { method: 'POST', path: '/v1/salary-types', summary: 'Create a new salary type', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
13
|
+
'get': { method: 'GET', path: '/v1/salary-types/{id}', summary: 'Get a salary type by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
14
|
+
'update': { method: 'PATCH', path: '/v1/salary-types/{id}', summary: 'Update a salary type', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
15
|
+
'delete': { method: 'DELETE', path: '/v1/salary-types/{id}', summary: 'Delete a salary type', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
16
16
|
};
|
|
17
17
|
export default class SalaryTypes extends Command {
|
|
18
18
|
static description = 'Manage salary types';
|
|
@@ -264,10 +264,11 @@ export default class SalaryTypes extends Command {
|
|
|
264
264
|
process.stderr.write('\n');
|
|
265
265
|
}
|
|
266
266
|
try {
|
|
267
|
-
// Binary response:
|
|
267
|
+
// Binary response: to -f / --file, or to stdout when it is redirected.
|
|
268
268
|
if (endpoint.isBinaryResponse) {
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
const toStdout = !flags.file;
|
|
270
|
+
if (toStdout && process.stdout.isTTY) {
|
|
271
|
+
this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
|
|
271
272
|
}
|
|
272
273
|
const binResponse = await apiRequest(endpoint.method, apiPath, {
|
|
273
274
|
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
@@ -275,24 +276,55 @@ export default class SalaryTypes extends Command {
|
|
|
275
276
|
contextName: flags.context,
|
|
276
277
|
binary: true,
|
|
277
278
|
});
|
|
278
|
-
const fsModule = await import('node:fs');
|
|
279
279
|
const binData = binResponse.data;
|
|
280
280
|
const buffer = Buffer.isBuffer(binData)
|
|
281
281
|
? binData
|
|
282
282
|
: Buffer.from(binData);
|
|
283
|
+
if (toStdout) {
|
|
284
|
+
// Nothing else may reach stdout here — it is the file.
|
|
285
|
+
process.stdout.write(buffer);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const fsModule = await import('node:fs');
|
|
283
289
|
fsModule.writeFileSync(flags.file, buffer);
|
|
284
290
|
this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
|
|
285
291
|
return;
|
|
286
292
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
+
let response;
|
|
294
|
+
if (endpoint.hasBinaryVariant) {
|
|
295
|
+
// This route answers JSON or a file depending on the request body, so the
|
|
296
|
+
// bytes are read first and the response's own content-type decides. Asking
|
|
297
|
+
// for text here would corrupt an xlsx/pdf before anything could inspect it.
|
|
298
|
+
const raw = await apiRequest(endpoint.method, apiPath, {
|
|
299
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
300
|
+
data,
|
|
301
|
+
contextName: flags.context,
|
|
302
|
+
binary: true,
|
|
303
|
+
});
|
|
304
|
+
const rawType = String(raw.headers['content-type'] || '').toLowerCase();
|
|
305
|
+
const rawData = raw.data;
|
|
306
|
+
const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
|
|
307
|
+
if (!isTextualMediaType(rawType)) {
|
|
308
|
+
if (process.stdout.isTTY) {
|
|
309
|
+
this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
|
|
310
|
+
}
|
|
311
|
+
// Nothing else may reach stdout here — it is the file.
|
|
312
|
+
process.stdout.write(buffer);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
response = await apiRequest(endpoint.method, apiPath, {
|
|
319
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
320
|
+
data,
|
|
321
|
+
contextName: flags.context,
|
|
322
|
+
acceptAny: true,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
293
325
|
// Check content-type for non-JSON responses (XML, text, etc.)
|
|
294
326
|
const contentType = (response.headers['content-type'] || '').toLowerCase();
|
|
295
|
-
if (contentType
|
|
327
|
+
if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
|
|
296
328
|
// Output raw content for XML/text responses (CSV, plain, HTML, etc.)
|
|
297
329
|
this.log(String(response.data));
|
|
298
330
|
return;
|
|
@@ -1,51 +1,52 @@
|
|
|
1
1
|
import { Command, Args, Flags } from '@oclif/core';
|
|
2
|
-
import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
|
|
2
|
+
import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
|
|
3
3
|
import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
|
|
4
4
|
// Add 'import' action if both create and update exist
|
|
5
5
|
const HAS_IMPORT = true;
|
|
6
6
|
const HAS_EXPORT = true;
|
|
7
7
|
const HAS_CAMT053 = false;
|
|
8
8
|
const NESTED_IMPORTS = ["migrated-invoice", "line-items", "notes", "files"];
|
|
9
|
-
const SUBCOMMANDS = [...["list", "create", "create-migrated-invoice", "pdf", "get", "update", "delete", "payment-derivation", "update-migrated-invoice", "approve", "match
|
|
9
|
+
const SUBCOMMANDS = [...["list", "create", "create-migrated-invoice", "get-pdf", "create-pdf", "get", "update", "delete", "payment-derivation", "update-migrated-invoice", "approve", "match", "validate", "request-approval", "send", "send-resend", "cancel", "copy", "replace", "rebuild-journal", "recalculate-payment-status", "merge", "create-line-items", "line-items-from-sales-order", "line-items-from-delivery-note", "update-line-items-reorder", "update-line-items", "delete-line-items", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
|
|
10
10
|
const ENDPOINTS = {
|
|
11
|
-
'list': { method: 'GET', path: '/v1/sales-invoices', summary: 'Get all sales invoices with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
12
|
-
'create': { method: 'POST', path: '/v1/sales-invoices', summary: 'Create a new sales invoice', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['earlyPaymentDiscounts', 'lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc'] },
|
|
13
|
-
'create-migrated-invoice': { method: 'POST', path: '/v1/sales-invoices/create-migrated-invoice', summary: 'Create a sales invoice with explicit tax codes on line items (for data migration)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['lineItems', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc'] },
|
|
14
|
-
'pdf': { method: 'GET', path: '/v1/sales-invoices/{id}/pdf', summary: '
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'send
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'line-items
|
|
34
|
-
'line-items-from-
|
|
35
|
-
'
|
|
36
|
-
'update-line-items': { method: 'PATCH', path: '/v1/sales-invoices/{invoiceId}/line-items/
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
'
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'files
|
|
11
|
+
'list': { method: 'GET', path: '/v1/sales-invoices', summary: 'Get all sales invoices with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
12
|
+
'create': { method: 'POST', path: '/v1/sales-invoices', summary: 'Create a new sales invoice', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts', 'lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc'] },
|
|
13
|
+
'create-migrated-invoice': { method: 'POST', path: '/v1/sales-invoices/create-migrated-invoice', summary: 'Create a sales invoice with explicit tax codes on line items (for data migration)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lineItems', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc'] },
|
|
14
|
+
'get-pdf': { method: 'GET', path: '/v1/sales-invoices/{id}/pdf', summary: 'Render a sales invoice PDF as the mapped event would, or preview a stored template', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
15
|
+
'create-pdf': { method: 'POST', path: '/v1/sales-invoices/{id}/pdf', summary: 'Render a sales invoice PDF from unsaved HTML posted in the body', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
16
|
+
'get': { method: 'GET', path: '/v1/sales-invoices/{id}', summary: 'Get a sales invoice by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
17
|
+
'update': { method: 'PATCH', path: '/v1/sales-invoices/{id}', summary: 'Update a sales invoice', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts', 'lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc', 'removeFileIds'] },
|
|
18
|
+
'delete': { method: 'DELETE', path: '/v1/sales-invoices/{id}', summary: 'Delete a sales invoice', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
19
|
+
'payment-derivation': { method: 'GET', path: '/v1/sales-invoices/{id}/payment-derivation', summary: 'Explain how a sales invoice\'s paid amount is derived', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
20
|
+
'update-migrated-invoice': { method: 'PATCH', path: '/v1/sales-invoices/{id}/update-migrated-invoice', summary: 'Update a migrated sales invoice during migration (before go-live)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lineItems', 'files', 'notes', 'tagIds', 'tagCodes', 'invoiceEmailCc', 'totalAmountByTaxRate'] },
|
|
21
|
+
'approve': { method: 'POST', path: '/v1/sales-invoices/{id}/approve', summary: 'Approve a sales invoice from DRAFT or PENDING_APPROVAL status (generates invoice number and finalizes)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts', 'lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc', 'removeFileIds'] },
|
|
22
|
+
'match': { method: 'POST', path: '/v1/sales-invoices/{id}/match', summary: 'Match this invoice\'s lines to sales-order lines and to delivery-note lines', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
23
|
+
'validate': { method: 'POST', path: '/v1/sales-invoices/{id}/validate', summary: 'Validate a sales invoice update without persisting changes', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts', 'lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc', 'removeFileIds'] },
|
|
24
|
+
'request-approval': { method: 'POST', path: '/v1/sales-invoices/{id}/request-approval', summary: 'Request approval for sales invoice (DRAFT → PENDING_APPROVAL)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
25
|
+
'send': { method: 'POST', path: '/v1/sales-invoices/{id}/send', summary: 'Mark invoice as sent to customer (APPROVED → SENT)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
26
|
+
'send-resend': { method: 'POST', path: '/v1/sales-invoices/{id}/resend', summary: 'Resend a sales invoice to the original delivery recipient or an alternative one', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['emailCc', 'emailBcc'] },
|
|
27
|
+
'cancel': { method: 'POST', path: '/v1/sales-invoices/{id}/cancel', summary: 'Cancel a sales invoice', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
28
|
+
'copy': { method: 'POST', path: '/v1/sales-invoices/{id}/copy', summary: 'Create a copy of an invoice as a new draft', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
29
|
+
'replace': { method: 'POST', path: '/v1/sales-invoices/{id}/replace', summary: 'Replace invoice (creates a new CORRECTED_INVOICE; a SELF_BILLED_INVOICE is replaced by a new SELF_BILLED_INVOICE)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
30
|
+
'rebuild-journal': { method: 'POST', path: '/v1/sales-invoices/{id}/rebuild-journal', summary: 'Re-create the booking journal of a sales invoice', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
31
|
+
'recalculate-payment-status': { method: 'POST', path: '/v1/sales-invoices/{id}/recalculate-payment-status', summary: 'Recalculate a sales invoice\'s open amount and payment status', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
32
|
+
'merge': { method: 'POST', path: '/v1/sales-invoices/{id}/merge', summary: 'Merge draft invoices into this master', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['sourceIds'] },
|
|
33
|
+
'create-line-items': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/line-items', summary: 'Add a line item to a sales invoice', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['discounts', 'salesOrderLineItems', 'deliveryNoteLineItems', 'tagIds', 'tagCodes'] },
|
|
34
|
+
'line-items-from-sales-order': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/line-items/from-sales-order', summary: 'Add line items to a sales invoice from a sales order', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lines'] },
|
|
35
|
+
'line-items-from-delivery-note': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/line-items/from-delivery-note', summary: 'Add line items to a sales invoice from a delivery note', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lines'] },
|
|
36
|
+
'update-line-items-reorder': { method: 'PATCH', path: '/v1/sales-invoices/{invoiceId}/line-items/reorder', summary: 'Bulk reorder line items (drag-and-drop)', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['items'] },
|
|
37
|
+
'update-line-items': { method: 'PATCH', path: '/v1/sales-invoices/{invoiceId}/line-items/{lineItemId}', summary: 'Update a line item on a sales invoice', pathParams: ['invoiceId', 'lineItemId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['discounts', 'salesOrderLineItems', 'deliveryNoteLineItems', 'tagIds', 'tagCodes'] },
|
|
38
|
+
'delete-line-items': { method: 'DELETE', path: '/v1/sales-invoices/{invoiceId}/line-items/{lineItemId}', summary: 'Remove a line item from a sales invoice', pathParams: ['invoiceId', 'lineItemId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
39
|
+
'list-notes': { method: 'GET', path: '/v1/sales-invoices/{salesInvoiceId}/notes', summary: 'Get all notes for a sales invoice', pathParams: ['salesInvoiceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
40
|
+
'create-notes': { method: 'POST', path: '/v1/sales-invoices/{salesInvoiceId}/notes', summary: 'Create a note for a sales invoice', pathParams: ['salesInvoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
41
|
+
'get-notes': { method: 'GET', path: '/v1/sales-invoices/{salesInvoiceId}/notes/{noteId}', summary: 'Get a specific note for a sales invoice', pathParams: ['salesInvoiceId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
42
|
+
'update-notes': { method: 'PATCH', path: '/v1/sales-invoices/{salesInvoiceId}/notes/{noteId}', summary: 'Update a note for a sales invoice', pathParams: ['salesInvoiceId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
43
|
+
'delete-notes': { method: 'DELETE', path: '/v1/sales-invoices/{salesInvoiceId}/notes/{noteId}', summary: 'Delete a note from a sales invoice', pathParams: ['salesInvoiceId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
44
|
+
'list-files': { method: 'GET', path: '/v1/sales-invoices/{parentId}/files', summary: 'List file attachments for a sales invoice', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
45
|
+
'create-files': { method: 'POST', path: '/v1/sales-invoices/{parentId}/files', summary: 'Upload a file attachment to a sales invoice', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
46
|
+
'get-files': { method: 'GET', path: '/v1/sales-invoices/{parentId}/files/{fileId}', summary: 'Get a file attachment for a sales invoice', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
47
|
+
'update-files': { method: 'PATCH', path: '/v1/sales-invoices/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a sales invoice', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
48
|
+
'delete-files': { method: 'DELETE', path: '/v1/sales-invoices/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a sales invoice', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
49
|
+
'files-download': { method: 'GET', path: '/v1/sales-invoices/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a sales invoice', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
49
50
|
};
|
|
50
51
|
export default class SalesInvoices extends Command {
|
|
51
52
|
static description = 'Manage sales invoices';
|
|
@@ -300,10 +301,11 @@ export default class SalesInvoices extends Command {
|
|
|
300
301
|
process.stderr.write('\n');
|
|
301
302
|
}
|
|
302
303
|
try {
|
|
303
|
-
// Binary response:
|
|
304
|
+
// Binary response: to -f / --file, or to stdout when it is redirected.
|
|
304
305
|
if (endpoint.isBinaryResponse) {
|
|
305
|
-
|
|
306
|
-
|
|
306
|
+
const toStdout = !flags.file;
|
|
307
|
+
if (toStdout && process.stdout.isTTY) {
|
|
308
|
+
this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
|
|
307
309
|
}
|
|
308
310
|
const binResponse = await apiRequest(endpoint.method, apiPath, {
|
|
309
311
|
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
@@ -311,24 +313,55 @@ export default class SalesInvoices extends Command {
|
|
|
311
313
|
contextName: flags.context,
|
|
312
314
|
binary: true,
|
|
313
315
|
});
|
|
314
|
-
const fsModule = await import('node:fs');
|
|
315
316
|
const binData = binResponse.data;
|
|
316
317
|
const buffer = Buffer.isBuffer(binData)
|
|
317
318
|
? binData
|
|
318
319
|
: Buffer.from(binData);
|
|
320
|
+
if (toStdout) {
|
|
321
|
+
// Nothing else may reach stdout here — it is the file.
|
|
322
|
+
process.stdout.write(buffer);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const fsModule = await import('node:fs');
|
|
319
326
|
fsModule.writeFileSync(flags.file, buffer);
|
|
320
327
|
this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
|
|
321
328
|
return;
|
|
322
329
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
330
|
+
let response;
|
|
331
|
+
if (endpoint.hasBinaryVariant) {
|
|
332
|
+
// This route answers JSON or a file depending on the request body, so the
|
|
333
|
+
// bytes are read first and the response's own content-type decides. Asking
|
|
334
|
+
// for text here would corrupt an xlsx/pdf before anything could inspect it.
|
|
335
|
+
const raw = await apiRequest(endpoint.method, apiPath, {
|
|
336
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
337
|
+
data,
|
|
338
|
+
contextName: flags.context,
|
|
339
|
+
binary: true,
|
|
340
|
+
});
|
|
341
|
+
const rawType = String(raw.headers['content-type'] || '').toLowerCase();
|
|
342
|
+
const rawData = raw.data;
|
|
343
|
+
const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
|
|
344
|
+
if (!isTextualMediaType(rawType)) {
|
|
345
|
+
if (process.stdout.isTTY) {
|
|
346
|
+
this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
|
|
347
|
+
}
|
|
348
|
+
// Nothing else may reach stdout here — it is the file.
|
|
349
|
+
process.stdout.write(buffer);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
response = await apiRequest(endpoint.method, apiPath, {
|
|
356
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
357
|
+
data,
|
|
358
|
+
contextName: flags.context,
|
|
359
|
+
acceptAny: true,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
329
362
|
// Check content-type for non-JSON responses (XML, text, etc.)
|
|
330
363
|
const contentType = (response.headers['content-type'] || '').toLowerCase();
|
|
331
|
-
if (contentType
|
|
364
|
+
if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
|
|
332
365
|
// Output raw content for XML/text responses (CSV, plain, HTML, etc.)
|
|
333
366
|
this.log(String(response.data));
|
|
334
367
|
return;
|
|
@@ -1,44 +1,46 @@
|
|
|
1
1
|
import { Command, Args, Flags } from '@oclif/core';
|
|
2
|
-
import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
|
|
2
|
+
import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
|
|
3
3
|
import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
|
|
4
4
|
// Add 'import' action if both create and update exist
|
|
5
5
|
const HAS_IMPORT = true;
|
|
6
6
|
const HAS_EXPORT = true;
|
|
7
7
|
const HAS_CAMT053 = false;
|
|
8
8
|
const NESTED_IMPORTS = ["line-items", "notes", "files"];
|
|
9
|
-
const SUBCOMMANDS = [...["list", "create", "create-invoices", "get", "update", "delete", "pdf", "submit", "approve", "accept", "terminate", "cancel", "replace", "merge", "create-invoice", "send", "create-line-items", "update-line-items-reorder", "update-line-items", "delete-line-items", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
|
|
9
|
+
const SUBCOMMANDS = [...["list", "create", "create-invoices", "get", "update", "delete", "get-pdf", "create-pdf", "submit", "approve", "accept", "terminate", "cancel", "replace", "copy", "merge", "create-invoice", "send", "create-line-items", "update-line-items-reorder", "update-line-items", "delete-line-items", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
|
|
10
10
|
const ENDPOINTS = {
|
|
11
|
-
'list': { method: 'GET', path: '/v1/sales-orders', summary: 'Get all sales orders with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
12
|
-
'create': { method: 'POST', path: '/v1/sales-orders', summary: 'Create a new sales order', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['files', 'notes', 'tagIds', 'tagCodes', 'discounts', 'lineItems'] },
|
|
13
|
-
'create-invoices': { method: 'POST', path: '/v1/sales-orders/create-invoices', summary: 'Create ONE draft invoice from several sales orders (same customer)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['salesOrderIds'] },
|
|
14
|
-
'get': { method: 'GET', path: '/v1/sales-orders/{id}', summary: 'Get a sales order by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
15
|
-
'update': { method: 'PATCH', path: '/v1/sales-orders/{id}', summary: 'Update a sales order', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['files', 'notes', 'tagIds', 'tagCodes', 'discounts', 'lineItems', 'removeFileIds'] },
|
|
16
|
-
'delete': { method: 'DELETE', path: '/v1/sales-orders/{id}', summary: 'Delete a sales order', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
17
|
-
'pdf': { method: 'GET', path: '/v1/sales-orders/{id}/pdf', summary: 'Render a sales order PDF
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'create-
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'files
|
|
11
|
+
'list': { method: 'GET', path: '/v1/sales-orders', summary: 'Get all sales orders with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
12
|
+
'create': { method: 'POST', path: '/v1/sales-orders', summary: 'Create a new sales order', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['files', 'notes', 'tagIds', 'tagCodes', 'discounts', 'lineItems'] },
|
|
13
|
+
'create-invoices': { method: 'POST', path: '/v1/sales-orders/create-invoices', summary: 'Create ONE draft invoice from several sales orders (same customer)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['salesOrderIds'] },
|
|
14
|
+
'get': { method: 'GET', path: '/v1/sales-orders/{id}', summary: 'Get a sales order by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
15
|
+
'update': { method: 'PATCH', path: '/v1/sales-orders/{id}', summary: 'Update a sales order', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['files', 'notes', 'tagIds', 'tagCodes', 'discounts', 'lineItems', 'removeFileIds'] },
|
|
16
|
+
'delete': { method: 'DELETE', path: '/v1/sales-orders/{id}', summary: 'Delete a sales order', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
17
|
+
'get-pdf': { method: 'GET', path: '/v1/sales-orders/{id}/pdf', summary: 'Render a sales order PDF as the mapped event would, or preview a stored template', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
18
|
+
'create-pdf': { method: 'POST', path: '/v1/sales-orders/{id}/pdf', summary: 'Render a sales order PDF from unsaved HTML posted in the body', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
19
|
+
'submit': { method: 'POST', path: '/v1/sales-orders/{id}/submit', summary: 'Submit order for approval (DRAFT or CANCELLED → PENDING_APPROVAL)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
20
|
+
'approve': { method: 'POST', path: '/v1/sales-orders/{id}/approve', summary: 'Approve order (DRAFT, PENDING_APPROVAL or CANCELLED → APPROVED). Generates order number if missing.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
21
|
+
'accept': { method: 'POST', path: '/v1/sales-orders/{id}/accept', summary: 'Accept order (APPROVED → ACCEPTED). Adjusts billing start date.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
22
|
+
'terminate': { method: 'POST', path: '/v1/sales-orders/{id}/terminate', summary: 'Terminate order (ACCEPTED → TERMINATED). Requires billingEndDate.', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
23
|
+
'cancel': { method: 'POST', path: '/v1/sales-orders/{id}/cancel', summary: 'Cancel order (any pre-active status → CANCELLED)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
24
|
+
'replace': { method: 'POST', path: '/v1/sales-orders/{id}/replace', summary: 'Replace order with corrected version (APPROVED → creates new DRAFT)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
25
|
+
'copy': { method: 'POST', path: '/v1/sales-orders/{id}/copy', summary: 'Create a copy of a sales order as a new draft', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
26
|
+
'merge': { method: 'POST', path: '/v1/sales-orders/{id}/merge', summary: 'Merge draft orders into this master', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['sourceIds'] },
|
|
27
|
+
'create-invoice': { method: 'POST', path: '/v1/sales-orders/{id}/create-invoice', summary: 'Create invoice from sales order (ACCEPTED → creates DRAFT invoice)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
28
|
+
'send': { method: 'POST', path: '/v1/sales-orders/{id}/send', summary: 'Send a sales order to the customer by email', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['emailCc', 'emailBcc', 'files'] },
|
|
29
|
+
'create-line-items': { method: 'POST', path: '/v1/sales-orders/{orderId}/line-items', summary: 'Add a line item to a sales order', pathParams: ['orderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['discounts', 'tagIds', 'tagCodes'] },
|
|
30
|
+
'update-line-items-reorder': { method: 'PATCH', path: '/v1/sales-orders/{orderId}/line-items/reorder', summary: 'Bulk reorder line items (drag-and-drop)', pathParams: ['orderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['items'] },
|
|
31
|
+
'update-line-items': { method: 'PATCH', path: '/v1/sales-orders/{orderId}/line-items/{lineItemId}', summary: 'Update a line item on a sales order', pathParams: ['orderId', 'lineItemId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['discounts', 'tagIds', 'tagCodes'] },
|
|
32
|
+
'delete-line-items': { method: 'DELETE', path: '/v1/sales-orders/{orderId}/line-items/{lineItemId}', summary: 'Remove a line item from a sales order', pathParams: ['orderId', 'lineItemId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
33
|
+
'list-notes': { method: 'GET', path: '/v1/sales-orders/{salesOrderId}/notes', summary: 'Get all notes for a sales order', pathParams: ['salesOrderId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
34
|
+
'create-notes': { method: 'POST', path: '/v1/sales-orders/{salesOrderId}/notes', summary: 'Create a note for a sales order', pathParams: ['salesOrderId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
35
|
+
'get-notes': { method: 'GET', path: '/v1/sales-orders/{salesOrderId}/notes/{noteId}', summary: 'Get a specific note for a sales order', pathParams: ['salesOrderId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
36
|
+
'update-notes': { method: 'PATCH', path: '/v1/sales-orders/{salesOrderId}/notes/{noteId}', summary: 'Update a note for a sales order', pathParams: ['salesOrderId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
37
|
+
'delete-notes': { method: 'DELETE', path: '/v1/sales-orders/{salesOrderId}/notes/{noteId}', summary: 'Delete a note from a sales order', pathParams: ['salesOrderId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
38
|
+
'list-files': { method: 'GET', path: '/v1/sales-orders/{parentId}/files', summary: 'List file attachments for a sales order', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
39
|
+
'create-files': { method: 'POST', path: '/v1/sales-orders/{parentId}/files', summary: 'Upload a file attachment to a sales order', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
40
|
+
'get-files': { method: 'GET', path: '/v1/sales-orders/{parentId}/files/{fileId}', summary: 'Get a file attachment for a sales order', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
41
|
+
'update-files': { method: 'PATCH', path: '/v1/sales-orders/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a sales order', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
42
|
+
'delete-files': { method: 'DELETE', path: '/v1/sales-orders/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a sales order', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
43
|
+
'files-download': { method: 'GET', path: '/v1/sales-orders/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a sales order', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
|
|
42
44
|
};
|
|
43
45
|
export default class SalesOrders extends Command {
|
|
44
46
|
static description = 'Manage sales orders';
|
|
@@ -293,10 +295,11 @@ export default class SalesOrders extends Command {
|
|
|
293
295
|
process.stderr.write('\n');
|
|
294
296
|
}
|
|
295
297
|
try {
|
|
296
|
-
// Binary response:
|
|
298
|
+
// Binary response: to -f / --file, or to stdout when it is redirected.
|
|
297
299
|
if (endpoint.isBinaryResponse) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
+
const toStdout = !flags.file;
|
|
301
|
+
if (toStdout && process.stdout.isTTY) {
|
|
302
|
+
this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
|
|
300
303
|
}
|
|
301
304
|
const binResponse = await apiRequest(endpoint.method, apiPath, {
|
|
302
305
|
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
@@ -304,24 +307,55 @@ export default class SalesOrders extends Command {
|
|
|
304
307
|
contextName: flags.context,
|
|
305
308
|
binary: true,
|
|
306
309
|
});
|
|
307
|
-
const fsModule = await import('node:fs');
|
|
308
310
|
const binData = binResponse.data;
|
|
309
311
|
const buffer = Buffer.isBuffer(binData)
|
|
310
312
|
? binData
|
|
311
313
|
: Buffer.from(binData);
|
|
314
|
+
if (toStdout) {
|
|
315
|
+
// Nothing else may reach stdout here — it is the file.
|
|
316
|
+
process.stdout.write(buffer);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const fsModule = await import('node:fs');
|
|
312
320
|
fsModule.writeFileSync(flags.file, buffer);
|
|
313
321
|
this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
|
|
314
322
|
return;
|
|
315
323
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
324
|
+
let response;
|
|
325
|
+
if (endpoint.hasBinaryVariant) {
|
|
326
|
+
// This route answers JSON or a file depending on the request body, so the
|
|
327
|
+
// bytes are read first and the response's own content-type decides. Asking
|
|
328
|
+
// for text here would corrupt an xlsx/pdf before anything could inspect it.
|
|
329
|
+
const raw = await apiRequest(endpoint.method, apiPath, {
|
|
330
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
331
|
+
data,
|
|
332
|
+
contextName: flags.context,
|
|
333
|
+
binary: true,
|
|
334
|
+
});
|
|
335
|
+
const rawType = String(raw.headers['content-type'] || '').toLowerCase();
|
|
336
|
+
const rawData = raw.data;
|
|
337
|
+
const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
|
|
338
|
+
if (!isTextualMediaType(rawType)) {
|
|
339
|
+
if (process.stdout.isTTY) {
|
|
340
|
+
this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
|
|
341
|
+
}
|
|
342
|
+
// Nothing else may reach stdout here — it is the file.
|
|
343
|
+
process.stdout.write(buffer);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
response = await apiRequest(endpoint.method, apiPath, {
|
|
350
|
+
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
351
|
+
data,
|
|
352
|
+
contextName: flags.context,
|
|
353
|
+
acceptAny: true,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
322
356
|
// Check content-type for non-JSON responses (XML, text, etc.)
|
|
323
357
|
const contentType = (response.headers['content-type'] || '').toLowerCase();
|
|
324
|
-
if (contentType
|
|
358
|
+
if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
|
|
325
359
|
// Output raw content for XML/text responses (CSV, plain, HTML, etc.)
|
|
326
360
|
this.log(String(response.data));
|
|
327
361
|
return;
|