From 82ab27ef2b51e0786f45eb5968bfaeb3654ef96c Mon Sep 17 00:00:00 2001 From: seb Date: Fri, 23 Oct 2026 20:31:39 +0200 Subject: [PATCH] u --- API.md | 2 +- API.shipment.md | 2 +- API.stock.md | 2 +- scripts/test-ledger.js | 18 ++++++++--------- src/providers/mssql/queries/logistics.js | 2 +- src/store/shipment/create.js | 6 +++--- src/store/shipment/list.js | 4 ++-- src/store/shipment/syncSummary.js | 6 +++--- src/store/shipment/update.js | 4 ++-- src/store/stock/ship.js | 25 +++++++++++------------- test/ledger.test.js | 9 +++++---- 11 files changed, 39 insertions(+), 41 deletions(-) diff --git a/API.md b/API.md index 1c0da25..713b430 100644 --- a/API.md +++ b/API.md @@ -220,7 +220,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `create(provider: import('../../providers/provider.js').DatabaseProvider): void` - `delete(provider: import('../../providers/provider.js').DatabaseProvider): void` - `getById(provider: import('../../providers/provider.js').DatabaseProvider): void` - - `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, state?: 'new'|'delivered'|'returned'|'cancelled'|'all', includeCancelled?: boolean, seekMonths?: number, orderId?: number}, tx?: object): Promise<{shipments: Array, total: number, unfilteredTotal: number, offset: number, limit: number}|Array>` + - `list(googleAuthId: number, opts?: {offset?: number, limit?: number, search?: string, state?: 'new'|'delivered'|'cancelled'|'all', includeCancelled?: boolean, seekMonths?: number, orderId?: number}, tx?: object): Promise<{shipments: Array, total: number, unfilteredTotal: number, offset: number, limit: number}|Array>` - `listByOrderId(provider: import('../../providers/provider.js').DatabaseProvider): void` - `rebuildSummaries(googleAuthId: number, tx?: object): Promise` - `syncSummary(): void` diff --git a/API.shipment.md b/API.shipment.md index bc76444..555c08d 100644 --- a/API.shipment.md +++ b/API.shipment.md @@ -53,7 +53,7 @@ Lists the shipments of the user's selected company. | `[opts.offset=0]` | `number` | Number of shipments to skip. | | `[opts.limit=50]` | `number` | Number of shipments to return. | | `[opts.search]` | `string` | Free-text search matching shipment number, order number, external number, tracking code, or customer name. | -| `[opts.state]` | `'new'|'delivered'|'returned'|'cancelled'|'all'` | Filter by delivery state. | +| `[opts.state]` | `'new'|'delivered'|'cancelled'|'all'` | Filter by delivery state. | | `[opts.includeCancelled=false]` | `boolean` | Whether to include cancelled shipments when state is not set. | | `[opts.seekMonths]` | `number` | Negative integer for cutoff months ago (e.g. -1). | | `[opts.orderId]` | `number` | Optional filter for parent order manifest id. | diff --git a/API.stock.md b/API.stock.md index bf3b000..78436d3 100644 --- a/API.stock.md +++ b/API.stock.md @@ -59,7 +59,7 @@ Rebuilds the `articlewarehousesummary` table for the user's selected company. *Source: `src/store/stock/ship.js`* -Ships a shipment manifest item (or multiple shipment items). Requirements: - Every shipped shipment manifest item receives a document ledger entry: - If amount >= 0: transaction_type = 'SHIPMENT', amount = -amount, warehouse_id = null. - If amount < 0: transaction_type = 'RETOURE', amount = +|amount|, warehouse_id = null. - Always records article history with shipment number, customer, and quantity. - If the connected article has stock tracking (track_stock == 1): Calls adjust with: - If normal shipment (amount >= 0): -amount (outgoing stock from warehouse). - If retoure (amount < 0): +|amount| (incoming stock into warehouse). - If the connected article does not have stock tracking: Only document shipment is booked; no warehouse adjust is performed. - Double shipping of an already shipped line is strictly prevented. +Ships a shipment manifest item (or multiple shipment items). Requirements: - Every shipped shipment manifest item receives a document ledger entry: - transaction_type = 'SHIPMENT', amount = -amount, warehouse_id = null. - Line amount must be positive (> 0); negative or zero line amounts are rejected. - Always records article history with shipment number, customer, and quantity. - If the connected article has stock tracking (track_stock == 1): Calls adjust with: - -amount (outgoing stock from warehouse). - If the connected article does not have stock tracking: Only document shipment is booked; no warehouse adjust is performed. - Double shipping of an already shipped line is strictly prevented. **Parameters** diff --git a/scripts/test-ledger.js b/scripts/test-ledger.js index c0a43a2..5152dfd 100644 --- a/scripts/test-ledger.js +++ b/scripts/test-ledger.js @@ -350,7 +350,7 @@ async function run() { assert.equal(acceptNegResult.adjust_ledger.warehouse_id, wh1.id); // 9. Test store.stock.ship with shipmentmanifestitem - console.log('9. Testing store.stock.ship (positive shipment & negative retoure)...'); + console.log('9. Testing store.stock.ship (positive shipment & rejection of negative line)...'); const order3 = await store.order.create(googleAuthId, { number: 'O-3', entityId: customer.id, @@ -380,14 +380,14 @@ async function run() { assert.equal(shipPosResult.document_ledger.amount, -4); assert.equal(shipPosResult.adjust_ledger.amount, -4); - // Negative shipment line: retoure of a shipment (incoming) - const shipNegResult = await store.stock.ship(googleAuthId, { - manifestarticleId: negShipLineId, - warehouseId: wh1.id, - }); - assert.equal(shipNegResult.document_ledger.transaction_type, 'RETOURE'); - assert.equal(shipNegResult.document_ledger.amount, 1); - assert.equal(shipNegResult.adjust_ledger.amount, 1); + // Negative shipment line: shipping is rejected + await assert.rejects( + store.stock.ship(googleAuthId, { + manifestarticleId: negShipLineId, + warehouseId: wh1.id, + }), + /Amount must be positive/, + ); // 10. Test store.stock.adjust console.log('10. Testing store.stock.adjust failure on untracked article...'); diff --git a/src/providers/mssql/queries/logistics.js b/src/providers/mssql/queries/logistics.js index 0649598..b9c480f 100644 --- a/src/providers/mssql/queries/logistics.js +++ b/src/providers/mssql/queries/logistics.js @@ -108,7 +108,7 @@ WHERE m.id = @manifestId ) OR EXISTS ( SELECT 1 FROM shipmentsummary ss - WHERE ss.manifest_id = m.id AND LOWER(ss.[state]) IN ('delivered', 'returned') + WHERE ss.manifest_id = m.id AND LOWER(ss.[state]) = 'delivered' ) ); `; diff --git a/src/store/shipment/create.js b/src/store/shipment/create.js index e3931eb..404a879 100644 --- a/src/store/shipment/create.js +++ b/src/store/shipment/create.js @@ -37,7 +37,7 @@ export default function create(provider) { * @param {number} input.orderId - The parent order manifest id. * @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 {'new'|'delivered'|'returned'} [input.state='new'] - Optional delivery state (defaults to 'new'). + * @param {'new'|'delivered'} [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 {object} input.shipping - Required shipping (package) created atomically with the shipment, so a shipment never exists without a carrier. * @param {number} input.shipping.carrierId - The carrier id. @@ -290,12 +290,12 @@ export default function create(provider) { }); // 8b. Create the shipping (carrier) atomically with the shipment. Must run before the - // summary state ('delivered'/'returned') is written, as that would block shipping creation. + // summary state ('delivered') is written, as that would block shipping creation. await createShipping(googleAuthId, { ...input.shipping, manifestId }, innerTx); // 9. Synchronize order summary row and shipment summary row await syncOrderSummary(runner, { companyId, manifestId: orderId }); - const validState = input.state && ['new', 'delivered', 'returned'].includes(String(input.state).trim().toLowerCase()) + const validState = input.state && ['new', 'delivered'].includes(String(input.state).trim().toLowerCase()) ? String(input.state).trim().toLowerCase() : 'new'; diff --git a/src/store/shipment/list.js b/src/store/shipment/list.js index 7cf50fd..fd179bd 100644 --- a/src/store/shipment/list.js +++ b/src/store/shipment/list.js @@ -31,7 +31,7 @@ export default function list(provider) { * @param {number} [opts.offset=0] - Number of shipments to skip. * @param {number} [opts.limit=50] - Number of shipments to return. * @param {string} [opts.search] - Free-text search matching shipment number, order number, external number, tracking code, or customer name. - * @param {'new'|'delivered'|'returned'|'cancelled'|'all'} [opts.state] - Filter by delivery state. + * @param {'new'|'delivered'|'cancelled'|'all'} [opts.state] - Filter by delivery state. * @param {boolean} [opts.includeCancelled=false] - Whether to include cancelled shipments when state is not set. * @param {number} [opts.seekMonths] - Negative integer for cutoff months ago (e.g. -1). * @param {number} [opts.orderId] - Optional filter for parent order manifest id. @@ -110,7 +110,7 @@ export default function list(provider) { } } - // Delivery state filter ('new' | 'delivered' | 'returned' | 'cancelled' | 'all') + // Delivery state filter ('new' | 'delivered' | 'cancelled' | 'all') if (typeof opts?.state === 'string' && opts.state.trim() !== '') { const stateFilter = opts.state.trim().toLowerCase(); if (stateFilter !== 'all') { diff --git a/src/store/shipment/syncSummary.js b/src/store/shipment/syncSummary.js index e1df847..3fc5147 100644 --- a/src/store/shipment/syncSummary.js +++ b/src/store/shipment/syncSummary.js @@ -126,7 +126,7 @@ const SQL_UPSERT_SUMMARY_MSSQL = ` ); `; -const VALID_STATES = new Set(['new', 'delivered', 'returned', 'cancelled']); +const VALID_STATES = new Set(['new', 'delivered', 'cancelled']); /** * Recalculates and upserts a single shipment summary into `shipmentsummary`. @@ -137,7 +137,7 @@ const VALID_STATES = new Set(['new', 'delivered', 'returned', 'cancelled']); * @param {number} opts.manifestId - Shipment manifest id. * @param {number} [opts.orderId] - Optional parent order manifest id. * @param {string} [opts.orderNumber] - Optional parent order number. - * @param {string} [opts.state] - Optional new state ('new' | 'delivered' | 'returned'). + * @param {string} [opts.state] - Optional new state ('new' | 'delivered'). */ export async function syncShipmentSummary(runner, { companyId, manifestId, orderId, orderNumber, state }) { const isSqlite = runner.dialect === 'sqlite3'; @@ -203,7 +203,7 @@ export async function syncShipmentSummary(runner, { companyId, manifestId, order customerName = custRes.rows[0]?.customer_name || null; } - // 5. State ('new' | 'delivered' | 'returned' | 'cancelled') + // 5. State ('new' | 'delivered' | 'cancelled') let resolvedState = 'new'; if (state && VALID_STATES.has(state)) { resolvedState = state; diff --git a/src/store/shipment/update.js b/src/store/shipment/update.js index 856cdfe..4cdfd57 100644 --- a/src/store/shipment/update.js +++ b/src/store/shipment/update.js @@ -11,7 +11,7 @@ function requireId(val, name) { return n; } -const VALID_STATES = new Set(['new', 'delivered', 'returned', 'cancelled']); +const VALID_STATES = new Set(['new', 'delivered', 'cancelled']); /** * Updates a shipment's state or metadata. @@ -28,7 +28,7 @@ export default function update(provider) { * @param {number} googleAuthId - The id of the Google auth user. * @param {number} id - The shipment manifest ID. * @param {object} patch - Fields to update. - * @param {'new'|'delivered'|'returned'} [patch.state] - Delivery state. + * @param {'new'|'delivered'|'cancelled'} [patch.state] - Delivery state. * @param {object} [tx] - Optional transaction. * @returns {Promise} The updated shipment manifest. * @throws {Error} If shipment not found or invalid state. diff --git a/src/store/stock/ship.js b/src/store/stock/ship.js index f8cc05e..3f7e2e3 100644 --- a/src/store/stock/ship.js +++ b/src/store/stock/ship.js @@ -12,13 +12,12 @@ import makeRunner from '../runner.js'; * * Requirements: * - Every shipped shipment manifest item receives a document ledger entry: - * - If amount >= 0: transaction_type = 'SHIPMENT', amount = -amount, warehouse_id = null. - * - If amount < 0: transaction_type = 'RETOURE', amount = +|amount|, warehouse_id = null. + * - transaction_type = 'SHIPMENT', amount = -amount, warehouse_id = null. + * - Line amount must be positive (> 0); negative or zero line amounts are rejected. * - Always records article history with shipment number, customer, and quantity. * - If the connected article has stock tracking (track_stock == 1): * Calls adjust with: - * - If normal shipment (amount >= 0): -amount (outgoing stock from warehouse). - * - If retoure (amount < 0): +|amount| (incoming stock into warehouse). + * - -amount (outgoing stock from warehouse). * - If the connected article does not have stock tracking: * Only document shipment is booked; no warehouse adjust is performed. * - Double shipping of an already shipped line is strictly prevented. @@ -120,19 +119,17 @@ export default function ship(provider) { } const lineAmount = Number(manifestArticle.lineAmount || 0); + if (lineAmount <= 0) { + throw new Error('Amount must be positive'); + } const shipmentNumber = manifestArticle.manifestNumber; - const isRetoure = lineAmount < 0; - const docType = isRetoure ? 'RETOURE' : 'SHIPMENT'; - // Normal shipment: negative ledger amount (-Math.abs(lineAmount)). - // Retoure (customer return): positive ledger amount (+Math.abs(lineAmount)). - const docAmount = isRetoure ? Math.abs(lineAmount) : -Math.abs(lineAmount); - const defaultDocText = isRetoure - ? (shipmentNumber ? `Retoure Lieferschein ${shipmentNumber}` : 'Retoure Lieferschein') - : (shipmentNumber ? `Lieferschein ${shipmentNumber}` : 'Lieferschein'); + const docType = 'SHIPMENT'; + const docAmount = -lineAmount; + const defaultDocText = shipmentNumber ? `Lieferschein ${shipmentNumber}` : 'Lieferschein'; const docText = item.text?.trim?.() || defaultDocText; - // Insert document shipment/retoure entry (warehouse_id = null) + // Insert document shipment entry (warehouse_id = null) const latest = await runner.query(queries.GET_LATEST_ROW); const prevRow = latest.rows[0]; const prevHash = computePrevHash(prevRow); @@ -171,7 +168,7 @@ export default function ship(provider) { } } - // Write article history for shipment / retoure + // Write article history for shipment for (const match of articleMatches) { await recordShipmentHistory(runner, { articleId: match.id, diff --git a/test/ledger.test.js b/test/ledger.test.js index 5d2416c..c0f12fb 100644 --- a/test/ledger.test.js +++ b/test/ledger.test.js @@ -431,10 +431,11 @@ describe('store.ledger', () => { assert.equal(pos.document_ledger.transaction_type, 'SHIPMENT'); assert.equal(pos.document_ledger.amount, -4); - const neg = await store.stock.ship(gid, { manifestarticleId: shipment.lines[1].id, warehouseId: warehouse.id }); - assert.equal(neg.document_ledger.transaction_type, 'RETOURE'); - assert.equal(neg.document_ledger.amount, 1); - assert.equal(await stockOf(sku), 17); + await assert.rejects( + store.stock.ship(gid, { manifestarticleId: shipment.lines[1].id, warehouseId: warehouse.id }), + /Amount must be positive/, + ); + assert.equal(await stockOf(sku), 16); }); });