This commit is contained in:
seb
2026-10-21 03:31:52 +02:00
parent e3761bc48b
commit e9e98d041c
14 changed files with 72 additions and 35 deletions

6
API.md
View File

@@ -207,15 +207,15 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `syncSummary(): void` - `syncSummary(): void`
- `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` - `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
- [order](API.order.md) - [order](API.order.md)
- `create(googleAuthId: number, body: {number: string, date: string, entityId: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` - `create(googleAuthId: number, body: {number: string, date: string, entityId: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
- `createFromQuote(googleAuthId: number, quoteId: number, body?: {number?: string, date?: string, createdAt?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` - `createFromQuote(googleAuthId: number, quoteId: number, body?: {number?: string, date?: string, createdAt?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
- `createShipment(googleAuthId: number, orderId: number, input: {lines: Array<{sku: string, amount?: number, name?: string, pu?: number, net?: number, rebate?: number, taxrate?: number, comment?: string}>, date?: string|Date, externalNumber?: string}, tx?: object): Promise<object>` - `createShipment(googleAuthId: number, orderId: number, input: {lines: Array<{sku: string, amount?: number, name?: string, pu?: number, net?: number, rebate?: number, taxrate?: number, comment?: string}>, date?: string|Date, externalNumber?: string}, tx?: object): Promise<object>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>` - `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` - `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
- `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, onlyUnpaid?: boolean, onlyUndelivered?: boolean, seekMonths?: number}, tx?: object): Promise<{orders: Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, createdAt: string|null, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>` - `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, onlyUnpaid?: boolean, onlyUndelivered?: boolean, seekMonths?: number}, tx?: object): Promise<{orders: Array<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, createdAt: string|null, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>, total: number, unfilteredTotal: number, offset: number, limit: number}|Array<object>>`
- `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void` - `rebuildSummaries(provider: import('../../providers/provider.js').DatabaseProvider): void`
- `syncSummary(): void` - `syncSummary(): void`
- `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>` - `update(googleAuthId: number, id: number, patch: {number?: string, date?: string, entityId?: number, externalNumber?: string, lines?: Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>}, tx?: object): Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>`
- [shipment](API.shipment.md) - [shipment](API.shipment.md)
- `create(provider: import('../../providers/provider.js').DatabaseProvider): void` - `create(provider: import('../../providers/provider.js').DatabaseProvider): void`
- `delete(provider: import('../../providers/provider.js').DatabaseProvider): void` - `delete(provider: import('../../providers/provider.js').DatabaseProvider): void`

View File

@@ -18,7 +18,7 @@ Creates a new order for the user's selected company with its lines and records a
| `body.date` | `string` | The order date. | | `body.date` | `string` | The order date. |
| `body.entityId` | `number` | The id of the customer entity. | | `body.entityId` | `number` | The id of the customer entity. |
| `[body.externalNumber]` | `string` | An external (customer) reference number. | | `[body.externalNumber]` | `string` | An external (customer) reference number. |
| `[body.lines]` | `Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>` | The order lines. | | `[body.lines]` | `Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>` | The order lines. `shippable` (default true) false marks non-shippable lines (e.g. shipping cost, rounding) that are excluded from shipment and order completeness. |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
@@ -54,7 +54,7 @@ Creates a new sales order from an existing quote for the user's selected company
| `[body.createdAt]` | `string` | Creation timestamp; defaults to current time if omitted. | | `[body.createdAt]` | `string` | Creation timestamp; defaults to current time if omitted. |
| `[body.entityId]` | `number` | Customer entity id override; defaults to quote's entityId. | | `[body.entityId]` | `number` | Customer entity id override; defaults to quote's entityId. |
| `[body.externalNumber]` | `string` | Customer reference override; defaults to quote's number. | | `[body.externalNumber]` | `string` | Customer reference override; defaults to quote's number. |
| `[body.lines]` | `Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>` | Lines override; defaults to quote's lines. | | `[body.lines]` | `Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>` | Lines override; defaults to quote's lines. `shippable` (default true) false excludes a line from shipment and order completeness. |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
@@ -226,7 +226,7 @@ Updates an order's header and/or lines.
| `[patch.date]` | `string` | The new order date. | | `[patch.date]` | `string` | The new order date. |
| `[patch.entityId]` | `number` | The new customer entity id. | | `[patch.entityId]` | `number` | The new customer entity id. |
| `[patch.externalNumber]` | `string` | The new external (customer) reference number. | | `[patch.externalNumber]` | `string` | The new external (customer) reference number. |
| `[patch.lines]` | `Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>` | The new order lines. | | `[patch.lines]` | `Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>` | The new order lines. `shippable` (default true) false marks non-shippable lines excluded from shipment and order completeness. |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**

View File

@@ -55,6 +55,7 @@ SELECT
ma.net, ma.net,
ma.rebate, ma.rebate,
ma.taxrate, ma.taxrate,
ma.shippable,
ma.comment ma.comment
FROM manifestarticle ma FROM manifestarticle ma
WHERE ma.manifest_id IN (SELECT CAST(value AS INT) FROM OPENJSON(@manifestIdsJson)) WHERE ma.manifest_id IN (SELECT CAST(value AS INT) FROM OPENJSON(@manifestIdsJson))
@@ -86,6 +87,7 @@ SELECT
ma.net, ma.net,
ma.rebate, ma.rebate,
ma.taxrate, ma.taxrate,
ma.shippable,
ma.comment ma.comment
FROM manifestarticle ma FROM manifestarticle ma
WHERE ma.manifest_id = @manifestId WHERE ma.manifest_id = @manifestId
@@ -133,7 +135,8 @@ INSERT INTO manifestarticle (
net, net,
comment, comment,
rebate, rebate,
taxrate taxrate,
shippable
) )
OUTPUT INSERTED.id OUTPUT INSERTED.id
VALUES ( VALUES (
@@ -145,7 +148,8 @@ VALUES (
@net, @net,
@comment, @comment,
@rebate, @rebate,
@taxrate @taxrate,
@shippable
) )
`; `;

View File

@@ -7,7 +7,7 @@ WHERE id = @orderId AND company_id = @companyId AND [type] = 'order';
`; `;
export const GET_ORDER_LINES = ` export const GET_ORDER_LINES = `
SELECT id, sku, name, pu, amount, net, rebate, taxrate, comment SELECT id, sku, name, pu, amount, net, rebate, taxrate, shippable, comment
FROM manifestarticle FROM manifestarticle
WHERE manifest_id = @orderId WHERE manifest_id = @orderId
ORDER BY id ASC; ORDER BY id ASC;
@@ -31,7 +31,7 @@ ORDER BY m.id ASC;
`; `;
export const GET_SHIPPED_AMOUNTS_BY_ORDER_PATTERN = ` export const GET_SHIPPED_AMOUNTS_BY_ORDER_PATTERN = `
SELECT ma.sku, SUM(ma.amount) AS shippedAmount SELECT ma.sku, ma.name, SUM(ma.amount) AS shippedAmount
FROM manifest m FROM manifest m
INNER JOIN manifestarticle ma ON ma.manifest_id = m.id INNER JOIN manifestarticle ma ON ma.manifest_id = m.id
LEFT JOIN shipmentsummary ss ON ss.manifest_id = m.id LEFT JOIN shipmentsummary ss ON ss.manifest_id = m.id
@@ -40,7 +40,7 @@ WHERE m.company_id = @companyId
AND m.[number] LIKE @pattern AND m.[number] LIKE @pattern
AND (@excludeShipmentId IS NULL OR m.id != @excludeShipmentId) AND (@excludeShipmentId IS NULL OR m.id != @excludeShipmentId)
AND (ss.state IS NULL OR ss.state != 'cancelled') AND (ss.state IS NULL OR ss.state != 'cancelled')
GROUP BY ma.sku; GROUP BY ma.sku, ma.name;
`; `;
export const GET_SHIPMENT_HEADER = ` export const GET_SHIPMENT_HEADER = `

View File

@@ -667,6 +667,7 @@ BEGIN
comment NVARCHAR(MAX) NULL, comment NVARCHAR(MAX) NULL,
rebate FLOAT NOT NULL CONSTRAINT DF_manifestarticle_rebate DEFAULT 0, rebate FLOAT NOT NULL CONSTRAINT DF_manifestarticle_rebate DEFAULT 0,
taxrate FLOAT NOT NULL CONSTRAINT DF_manifestarticle_taxrate DEFAULT 0, taxrate FLOAT NOT NULL CONSTRAINT DF_manifestarticle_taxrate DEFAULT 0,
shippable TINYINT NOT NULL CONSTRAINT DF_manifestarticle_shippable DEFAULT 1,
CONSTRAINT FK_manifestarticle_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE CONSTRAINT FK_manifestarticle_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE
); );
END END

View File

@@ -35,6 +35,7 @@ SELECT
ma.net, ma.net,
ma.rebate, ma.rebate,
ma.taxrate, ma.taxrate,
ma.shippable,
ma.comment ma.comment
FROM manifestarticle ma FROM manifestarticle ma
WHERE ma.manifest_id IN (SELECT value FROM json_each(@manifestIdsJson)) WHERE ma.manifest_id IN (SELECT value FROM json_each(@manifestIdsJson))

View File

@@ -448,6 +448,7 @@ CREATE TABLE IF NOT EXISTS manifestarticle (
comment TEXT, comment TEXT,
rebate REAL NOT NULL DEFAULT 0, rebate REAL NOT NULL DEFAULT 0,
taxrate REAL NOT NULL DEFAULT 0, taxrate REAL NOT NULL DEFAULT 0,
shippable INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE
); );
`; `;

View File

@@ -12,7 +12,7 @@ import makeRunner from './runner.js';
* Normalizes and validates an array of manifest line inputs. * Normalizes and validates an array of manifest line inputs.
* *
* @param {Array<object>} lines - The raw line inputs. * @param {Array<object>} lines - The raw line inputs.
* @returns {Array<{sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>} The normalized lines. * @returns {Array<{sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null, shippable: boolean}>} The normalized lines.
* @throws {Error} If the lines are invalid. * @throws {Error} If the lines are invalid.
*/ */
export function normalizeLines(lines) { export function normalizeLines(lines) {
@@ -36,6 +36,8 @@ export function normalizeLines(lines) {
const rebate = const rebate =
entry?.rebate === undefined || entry?.rebate === null ? 0 : entry.rebate; entry?.rebate === undefined || entry?.rebate === null ? 0 : entry.rebate;
const taxrate = entry?.taxrate; const taxrate = entry?.taxrate;
const shippable =
entry?.shippable === undefined || entry?.shippable === null ? true : entry.shippable;
const comment = const comment =
entry?.comment === undefined || entry?.comment === null entry?.comment === undefined || entry?.comment === null
? null ? null
@@ -46,12 +48,13 @@ export function normalizeLines(lines) {
typeof amount !== 'number' || !Number.isFinite(amount) || typeof amount !== 'number' || !Number.isFinite(amount) ||
typeof net !== 'number' || !Number.isFinite(net) || typeof net !== 'number' || !Number.isFinite(net) ||
typeof rebate !== 'number' || !Number.isFinite(rebate) || typeof rebate !== 'number' || !Number.isFinite(rebate) ||
typeof taxrate !== 'number' || !Number.isFinite(taxrate) || taxrate < 0 typeof taxrate !== 'number' || !Number.isFinite(taxrate) || taxrate < 0 ||
typeof shippable !== 'boolean'
) { ) {
throw new Error('Invalid lines'); throw new Error('Invalid lines');
} }
return { sku, name, pu, amount, net, rebate, taxrate, comment }; return { sku, name, pu, amount, net, rebate, taxrate, comment, shippable };
}); });
} }
@@ -92,6 +95,7 @@ export function shapeLine(row) {
rebate: row.rebate, rebate: row.rebate,
taxrate, taxrate,
comment: row.comment ?? null, comment: row.comment ?? null,
shippable: Number(row.shippable) !== 0,
}; };
} }
@@ -162,6 +166,7 @@ export async function insertLines(
comment: line.comment, comment: line.comment,
rebate: line.rebate, rebate: line.rebate,
taxrate: line.taxrate, taxrate: line.taxrate,
shippable: line.shippable ? 1 : 0,
}); });
} }
} }

View File

@@ -38,7 +38,7 @@ export default function create(provider) {
* @param {string} body.date - The order date. * @param {string} body.date - The order date.
* @param {number} body.entityId - The id of the customer entity. * @param {number} body.entityId - The id of the customer entity.
* @param {string} [body.externalNumber] - An external (customer) reference number. * @param {string} [body.externalNumber] - An external (customer) reference number.
* @param {Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>} [body.lines] - The order lines. * @param {Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>} [body.lines] - The order lines. `shippable` (default true) false marks non-shippable lines (e.g. shipping cost, rounding) that are excluded from shipment and order completeness.
* @param {object} [tx] - Optional transaction to run within. * @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>} The created order. * @returns {Promise<{id: number, number: string, type: string, entityId: number, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>} The created order.
* @throws {Error} If the order is invalid, no entity is selected, or entity not found. * @throws {Error} If the order is invalid, no entity is selected, or entity not found.

View File

@@ -48,7 +48,7 @@ export default function createFromQuote(provider) {
* @param {string} [body.createdAt] - Creation timestamp; defaults to current time if omitted. * @param {string} [body.createdAt] - Creation timestamp; defaults to current time if omitted.
* @param {number} [body.entityId] - Customer entity id override; defaults to quote's entityId. * @param {number} [body.entityId] - Customer entity id override; defaults to quote's entityId.
* @param {string} [body.externalNumber] - Customer reference override; defaults to quote's number. * @param {string} [body.externalNumber] - Customer reference override; defaults to quote's number.
* @param {Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>} [body.lines] - Lines override; defaults to quote's lines. * @param {Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>} [body.lines] - Lines override; defaults to quote's lines. `shippable` (default true) false excludes a line from shipment and order completeness.
* @param {object} [tx] - Optional transaction to run within. * @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>} The created order. * @returns {Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>} The created order.
* @throws {Error} If the quote is not found, no entity is selected, or the order is invalid. * @throws {Error} If the quote is not found, no entity is selected, or the order is invalid.

View File

@@ -33,7 +33,7 @@ const SQL_GET_ORDER_BY_NUMBER_MSSQL = `
const SQL_LINE_TOTALS = ` const SQL_LINE_TOTALS = `
SELECT SELECT
COUNT(*) AS positions, COUNT(*) AS positions,
COALESCE(SUM(amount), 0) AS ordered_quantity, COALESCE(SUM(CASE WHEN shippable = 1 THEN amount ELSE 0 END), 0) AS ordered_quantity,
COALESCE(SUM(amount * (net * (1.0 - rebate / 100.0)) * (1.0 + taxrate / 100.0)), 0) AS gross_total COALESCE(SUM(amount * (net * (1.0 - rebate / 100.0)) * (1.0 + taxrate / 100.0)), 0) AS gross_total
FROM manifestarticle FROM manifestarticle
WHERE manifest_id = @manifestId WHERE manifest_id = @manifestId
@@ -241,7 +241,7 @@ export async function rebuildOrderSummaries(runner, companyId) {
SELECT SELECT
manifest_id, manifest_id,
COUNT(*) AS positions, COUNT(*) AS positions,
SUM(amount) AS ordered_quantity, SUM(CASE WHEN shippable = 1 THEN amount ELSE 0 END) AS ordered_quantity,
SUM(amount * (net * (1.0 - rebate / 100.0)) * (1.0 + taxrate / 100.0)) AS gross_total SUM(amount * (net * (1.0 - rebate / 100.0)) * (1.0 + taxrate / 100.0)) AS gross_total
FROM manifestarticle FROM manifestarticle
GROUP BY manifest_id GROUP BY manifest_id
@@ -315,7 +315,7 @@ export async function rebuildOrderSummaries(runner, companyId) {
SELECT SELECT
manifest_id, manifest_id,
COUNT(*) AS positions, COUNT(*) AS positions,
SUM(amount) AS ordered_quantity, SUM(CASE WHEN shippable = 1 THEN amount ELSE 0 END) AS ordered_quantity,
SUM(amount * (net * (1.0 - rebate / 100.0)) * (1.0 + taxrate / 100.0)) AS gross_total SUM(amount * (net * (1.0 - rebate / 100.0)) * (1.0 + taxrate / 100.0)) AS gross_total
FROM manifestarticle FROM manifestarticle
GROUP BY manifest_id GROUP BY manifest_id

View File

@@ -37,7 +37,7 @@ export default function update(provider) {
* @param {string} [patch.date] - The new order date. * @param {string} [patch.date] - The new order date.
* @param {number} [patch.entityId] - The new customer entity id. * @param {number} [patch.entityId] - The new customer entity id.
* @param {string} [patch.externalNumber] - The new external (customer) reference number. * @param {string} [patch.externalNumber] - The new external (customer) reference number.
* @param {Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>} [patch.lines] - The new order lines. * @param {Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null, shippable?: boolean}>} [patch.lines] - The new order lines. `shippable` (default true) false marks non-shippable lines excluded from shipment and order completeness.
* @param {object} [tx] - Optional transaction to run within. * @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>} The updated order. * @returns {Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>} The updated order.
* @throws {Error} If the order is not found. * @throws {Error} If the order is not found.

View File

@@ -33,7 +33,7 @@ export default function create(provider) {
* @param {number} googleAuthId - The id of the Google auth user. * @param {number} googleAuthId - The id of the Google auth user.
* @param {object} input - The shipment input. * @param {object} input - The shipment input.
* @param {number} input.orderId - The parent order manifest id. * @param {number} input.orderId - The parent order manifest id.
* @param {Array<{sku: string, amount?: number, name?: string, pu?: number, net?: number, rebate?: number, taxrate?: number, comment?: string}>} input.lines - The shipment positions. * @param {Array<{sku?: string, orderLineId?: number, amount?: number, name?: string, pu?: number, net?: number, rebate?: number, taxrate?: number, comment?: string}>} input.lines - The shipment positions. Order lines with a SKU are referenced by `sku`; SKU-less order lines by `orderLineId` (matched across shipments by name).
* @param {string|Date} [input.date] - Optional shipment date (defaults to current date). * @param {string|Date} [input.date] - Optional shipment date (defaults to current date).
* @param {'new'|'delivered'|'returned'} [input.state='new'] - Optional delivery state (defaults to 'new'). * @param {'new'|'delivered'|'returned'} [input.state='new'] - Optional delivery state (defaults to 'new').
* @param {string} [input.externalNumber] - Optional external delivery note number (defaults to order.externalNumber or order.number). * @param {string} [input.externalNumber] - Optional external delivery note number (defaults to order.externalNumber or order.number).
@@ -82,15 +82,21 @@ export default function create(provider) {
throw new Error('Order has no lines'); throw new Error('Order has no lines');
} }
// Lines are keyed by SKU; SKU-less lines are keyed by name (shipment lines copy the name).
const lineKey = (sku, name) => {
const s = String(sku ?? '').trim();
return s || `\u0000name:${String(name ?? '').trim()}`;
};
const orderAmountBySku = new Map(); const orderAmountBySku = new Map();
const orderLineBySku = new Map(); const orderLineBySku = new Map();
const orderLineById = new Map();
for (const ol of orderLines) { for (const ol of orderLines) {
const sku = String(ol.sku ?? '').trim(); const key = lineKey(ol.sku, ol.name);
if (!sku) continue; orderLineById.set(Number(ol.id), ol);
orderAmountBySku.set(sku, (orderAmountBySku.get(sku) || 0) + Number(ol.amount || 0)); orderAmountBySku.set(key, (orderAmountBySku.get(key) || 0) + Number(ol.amount || 0));
if (!orderLineBySku.has(sku)) { if (!orderLineBySku.has(key)) {
orderLineBySku.set(sku, ol); orderLineBySku.set(key, ol);
} }
} }
@@ -107,8 +113,8 @@ export default function create(provider) {
}); });
const shippedAmountBySku = new Map(); const shippedAmountBySku = new Map();
for (const row of shippedRes.rows ?? []) { for (const row of shippedRes.rows ?? []) {
const sku = String(row.sku ?? '').trim(); const key = lineKey(row.sku, row.name);
shippedAmountBySku.set(sku, Number(row.shippedAmount || 0)); shippedAmountBySku.set(key, (shippedAmountBySku.get(key) || 0) + Number(row.shippedAmount || 0));
} }
// 5. Determine warehouse and load stock for tracked articles // 5. Determine warehouse and load stock for tracked articles
@@ -124,6 +130,7 @@ export default function create(provider) {
const remainingStockBySku = new Map(); const remainingStockBySku = new Map();
for (const sku of orderAmountBySku.keys()) { for (const sku of orderAmountBySku.keys()) {
if (sku.startsWith('\u0000name:')) continue; // SKU-less lines carry no stock
const artRes = await runner.query(q.GET_ARTICLE_TRACK_STOCK, { companyId, sku }); const artRes = await runner.query(q.GET_ARTICLE_TRACK_STOCK, { companyId, sku });
const art = artRes.rows[0]; const art = artRes.rows[0];
const isTracked = Number(art?.track_stock ?? 0) === 1; const isTracked = Number(art?.track_stock ?? 0) === 1;
@@ -152,9 +159,25 @@ export default function create(provider) {
const proposedBySku = new Map(); const proposedBySku = new Map();
for (const line of input.lines) { for (const line of input.lines) {
const sku = String(line?.sku ?? '').trim(); const rawSku = String(line?.sku ?? '').trim();
if (!sku || !orderAmountBySku.has(sku)) { let sku = rawSku;
throw new Error(`Article (${sku}) is not part of order (${order.number})`); let shipSku = rawSku;
let label = rawSku;
if (!rawSku) {
// SKU-less line: referenced by order line id
const target = orderLineById.get(Number(line?.orderLineId));
if (!target || String(target.sku ?? '').trim()) {
throw new Error(`Article (${line?.orderLineId ?? ''}) is not part of order (${order.number})`);
}
sku = lineKey(null, target.name);
shipSku = null;
label = String(target.name ?? '').trim();
}
if (!orderAmountBySku.has(sku)) {
throw new Error(`Article (${label}) is not part of order (${order.number})`);
}
if (Number(orderLineBySku.get(sku)?.shippable) === 0) {
throw new Error(`Article (${label}) is not shippable`);
} }
const ordered = orderAmountBySku.get(sku) || 0; const ordered = orderAmountBySku.get(sku) || 0;
@@ -170,14 +193,14 @@ export default function create(provider) {
} else { } else {
// Default to remaining quantity // Default to remaining quantity
if (remaining <= 0) { if (remaining <= 0) {
throw new Error(`Shipment amount exceeds ordered amount for article (${sku})`); throw new Error(`Shipment amount exceeds ordered amount for article (${label})`);
} }
amount = remaining; amount = remaining;
} }
const currentProposed = proposedBySku.get(sku) || 0; const currentProposed = proposedBySku.get(sku) || 0;
if (amount > 0 && alreadyShipped + currentProposed + amount > ordered) { if (amount > 0 && alreadyShipped + currentProposed + amount > ordered) {
throw new Error(`Shipment amount exceeds ordered amount for article (${sku})`); throw new Error(`Shipment amount exceeds ordered amount for article (${label})`);
} }
const info = articleInfoBySku.get(sku); const info = articleInfoBySku.get(sku);
@@ -193,14 +216,15 @@ export default function create(provider) {
const ol = orderLineBySku.get(sku); const ol = orderLineBySku.get(sku);
linesToInsert.push({ linesToInsert.push({
sku, sku: shipSku || null,
name: line.name ?? ol?.name ?? sku, name: shipSku ? (line.name ?? ol?.name ?? shipSku) : ol.name,
pu: line.pu !== undefined ? line.pu : (ol?.pu ?? 1), pu: line.pu !== undefined ? line.pu : (ol?.pu ?? 1),
amount, amount,
net: line.net !== undefined ? Number(line.net) : Number(ol?.net ?? 0), net: line.net !== undefined ? Number(line.net) : Number(ol?.net ?? 0),
rebate: line.rebate !== undefined ? Number(line.rebate) : Number(ol?.rebate ?? 0), rebate: line.rebate !== undefined ? Number(line.rebate) : Number(ol?.rebate ?? 0),
taxrate: line.taxrate !== undefined ? Number(line.taxrate) : Number(ol?.taxrate ?? 0), taxrate: line.taxrate !== undefined ? Number(line.taxrate) : Number(ol?.taxrate ?? 0),
comment: line.comment !== undefined ? (line.comment ? String(line.comment) : null) : (ol?.comment ?? null), comment: line.comment !== undefined ? (line.comment ? String(line.comment) : null) : (ol?.comment ?? null),
shippable: true,
}); });
} }

View File

@@ -338,6 +338,7 @@ export default function adjust(provider) {
comment: item.text, comment: item.text,
rebate: 0, rebate: 0,
taxrate: 0, taxrate: 0,
shippable: 1,
}); });
const manifestarticleId = lineRes.rows[0]?.id ?? lineRes.insertId; const manifestarticleId = lineRes.rows[0]?.id ?? lineRes.insertId;