From 5cb38874ca10ef1c99cc9e2bae4cb9a757c29fc5 Mon Sep 17 00:00:00 2001 From: seb Date: Tue, 20 Oct 2026 02:22:58 +0200 Subject: [PATCH] u --- API.article.md | 4 +- API.md | 2 +- src/providers/mssql/queries/article.js | 38 ++++++++++++- src/providers/sqlite3/queries/article.js | 38 ++++++++++++- src/store/article/getBySku.js | 11 ++-- test/article.test.js | 68 +++++++++++++++++++++++- 6 files changed, 151 insertions(+), 10 deletions(-) diff --git a/API.article.md b/API.article.md index 847f804..006ee51 100644 --- a/API.article.md +++ b/API.article.md @@ -181,12 +181,10 @@ Returns the full detail of an article by its SKU, including unit, brand, categor | Name | Type | Description | | --- | --- | --- | | `googleAuthId` | `number` | The id of the Google auth user. | -| `sku` | `string` | The article SKU. | -| `[tx]` | `object` | Optional transaction. | **Returns** -`Promise<{article: {id: number, sku: string, name: string, slug: string, desc: string|null, seo: string|null, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null, incoming: Array<{date: string, amount: number}>}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>` — The full article detail. +`Promise<{article: {id: number, sku: string, name: string, slug: string, desc: string|null, seo: string|null, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null, incoming: Array<{date: string, amount: number}>}>, warehouses: Array<{id: number, name: string, sort: number, stock: number}>, undeliveredOrders: Array<{orderId: number, orderNumber: string, date: string, customerName: string|null, amount: number}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>` — The full article detail. **Throws** diff --git a/API.md b/API.md index 7eeee06..9751835 100644 --- a/API.md +++ b/API.md @@ -111,7 +111,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `createUnderParent(input: {googleAuthId: number, parentId: number, name: string, desc?: string, seo?: string, track_stock?: boolean|number}, tx?: object): Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>` - `delete(googleAuthId: number, sku: string, optionsOrTx?: object|*, maybeTx?: object): Promise` - `deleteImage(googleAuthId: number, sku: string, fullHash: string, tx?: object): Promise` - - `getBySku(googleAuthId: number, sku: string, tx?: object): Promise<{article: {id: number, sku: string, name: string, slug: string, desc: string|null, seo: string|null, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null, incoming: Array<{date: string, amount: number}>}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>` + - `getBySku(googleAuthId: number): Promise<{article: {id: number, sku: string, name: string, slug: string, desc: string|null, seo: string|null, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null, incoming: Array<{date: string, amount: number}>}>, warehouses: Array<{id: number, name: string, sort: number, stock: number}>, undeliveredOrders: Array<{orderId: number, orderNumber: string, date: string, customerName: string|null, amount: number}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>` - `getHistoryBySku(googleAuthId: number, sku: string, opts?: {limit?: number}, tx?: object): Promise>` - `importRows(input: {googleAuthId: number, rows: Array}, tx?: object): Promise<{imported: Array, skipped: Array, errors: Array}>` - `listChildren(googleAuthId: number, parentSku: string, tx?: object): Promise>` diff --git a/src/providers/mssql/queries/article.js b/src/providers/mssql/queries/article.js index 33c3a67..83fe149 100644 --- a/src/providers/mssql/queries/article.js +++ b/src/providers/mssql/queries/article.js @@ -653,7 +653,43 @@ SELECT WHERE w.company_id = a.company_id ORDER BY w.sort ASC, w.name ASC FOR JSON PATH - ) AS warehousesJson + ) AS warehousesJson, + ( + SELECT + m.id AS orderId, + m.[number] AS orderNumber, + m.[date] AS [date], + COALESCE(os.customer_name, LTRIM(RTRIM(ISNULL(e.first_name + ' ', '') + ISNULL(e.name, '')))) AS customerName, + ROUND(SUM(ma.amount) - COALESCE(( + SELECT SUM(ma_s.amount) + FROM manifest m_s + INNER JOIN manifestarticle ma_s ON ma_s.manifest_id = m_s.id + WHERE m_s.company_id = m.company_id + AND m_s.[type] = 'shipment' + AND ma_s.sku = a.sku + AND m_s.[number] LIKE m.[number] + COALESCE((SELECT prefix FROM numbers WHERE company_id = m.company_id AND [type] = 'shipment'), '-L-') + '%' + ), 0), 3) AS amount + FROM manifest m + INNER JOIN manifestarticle ma ON ma.manifest_id = m.id + LEFT JOIN ordersummary os ON os.manifest_id = m.id + LEFT JOIN entity e ON e.id = m.entity_id + WHERE m.company_id = a.company_id + AND m.[type] = 'order' + AND ma.sku = a.sku + AND COALESCE(os.is_shipped, 0) = 0 + GROUP BY m.id, m.[number], m.[date], os.customer_name, e.first_name, e.name + HAVING (SUM(ma.amount) - COALESCE(( + SELECT SUM(ma_s.amount) + FROM manifest m_s + INNER JOIN manifestarticle ma_s ON ma_s.manifest_id = m_s.id + WHERE m_s.company_id = m.company_id + AND m_s.[type] = 'shipment' + AND ma_s.sku = a.sku + AND m_s.[number] LIKE m.[number] + COALESCE((SELECT prefix FROM numbers WHERE company_id = m.company_id AND [type] = 'shipment'), '-L-') + '%' + ), 0)) > 0 + ORDER BY m.[date] ASC, m.id ASC + FOR JSON PATH + ) AS undeliveredOrdersJson FROM article a LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = auth.languageId diff --git a/src/providers/sqlite3/queries/article.js b/src/providers/sqlite3/queries/article.js index 8a9d4a9..dca6527 100644 --- a/src/providers/sqlite3/queries/article.js +++ b/src/providers/sqlite3/queries/article.js @@ -288,7 +288,43 @@ SELECT auth.*, GROUP BY l.warehouse_id ) stock_calc ON stock_calc.warehouse_id = w.id WHERE w.company_id = a.company_id - ORDER BY w.sort ASC, w.name ASC)) + ORDER BY w.sort ASC, w.name ASC)), + 'undeliveredOrdersJson', json(( + SELECT COALESCE(json_group_array(json_object( + 'orderId', uo.orderId, + 'orderNumber', uo.orderNumber, + 'date', uo.date, + 'customerName', uo.customerName, + 'amount', uo.amount + )), '[]') + FROM ( + SELECT + m.id AS orderId, + m."number" AS orderNumber, + m."date" AS date, + COALESCE(os.customer_name, TRIM(COALESCE(e.first_name || ' ', '') || COALESCE(e.name, ''))) AS customerName, + ROUND(SUM(ma.amount) - COALESCE(( + SELECT SUM(ma_s.amount) + FROM manifest m_s + INNER JOIN manifestarticle ma_s ON ma_s.manifest_id = m_s.id + WHERE m_s.company_id = m.company_id + AND m_s."type" = 'shipment' + AND ma_s.sku = a.sku + AND m_s."number" LIKE m."number" || COALESCE((SELECT prefix FROM numbers WHERE company_id = m.company_id AND "type" = 'shipment'), '-L-') || '%' + ), 0), 3) AS amount + FROM manifest m + INNER JOIN manifestarticle ma ON ma.manifest_id = m.id + LEFT JOIN ordersummary os ON os.manifest_id = m.id + LEFT JOIN entity e ON e.id = m.entity_id + WHERE m.company_id = a.company_id + AND m."type" = 'order' + AND ma.sku = a.sku + AND COALESCE(os.is_shipped, 0) = 0 + GROUP BY m.id, m."number", m."date", os.customer_name, e.first_name, e.name + HAVING amount > 0 + ORDER BY m."date" ASC, m.id ASC + ) uo + )) ) AS articleJson FROM auth INNER JOIN article a ON a.id = auth.articleId diff --git a/src/store/article/getBySku.js b/src/store/article/getBySku.js index 8474881..99a1f86 100644 --- a/src/store/article/getBySku.js +++ b/src/store/article/getBySku.js @@ -16,9 +16,7 @@ export default function getBySku(provider) { * categories, photos, EANs and prices. * * @param {number} googleAuthId - The id of the Google auth user. - * @param {string} sku - The article SKU. - * @param {object} [tx] - Optional transaction. - * @returns {Promise<{article: {id: number, sku: string, name: string, slug: string, desc: string|null, seo: string|null, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null, incoming: Array<{date: string, amount: number}>}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>} The full article detail. + * @returns {Promise<{article: {id: number, sku: string, name: string, slug: string, desc: string|null, seo: string|null, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null, incoming: Array<{date: string, amount: number}>}>, warehouses: Array<{id: number, name: string, sort: number, stock: number}>, undeliveredOrders: Array<{orderId: number, orderNumber: string, date: string, customerName: string|null, amount: number}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>} The full article detail. * @throws {Error} If the article is not found or the user is not authorized. * * @example @@ -98,6 +96,13 @@ export default function getBySku(provider) { sort: Number(w.sort ?? 0), stock: Number(w.stock ?? 0), })), + undeliveredOrders: parseJsonArray(a.undeliveredOrdersJson).map((uo) => ({ + orderId: uo.orderId, + orderNumber: uo.orderNumber, + date: uo.date, + customerName: uo.customerName ?? null, + amount: Math.round(Number(uo.amount ?? 0) * 1000) / 1000, + })), optionsCount: a.optionsCount, optionslistId: a.optionslistId ?? null, options: parseJsonArray(a.optionslistItemsJson).map((o) => ({ diff --git a/test/article.test.js b/test/article.test.js index bbbbfbf..63b66c6 100644 --- a/test/article.test.js +++ b/test/article.test.js @@ -1,6 +1,6 @@ import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; -import { createTestStore, createTenant, createArticle, makePng } from './helpers.js'; +import { createTestStore, createTenant, createArticle, createCustomer, makePng } from './helpers.js'; describe('store.article', () => { let ctx; @@ -694,4 +694,70 @@ describe('store.article', () => { assert.equal(res.article.suppliers[0].incoming[0].date, '2026-11-15T00:00:00.000Z'); }); }); + + describe('undelivered customer orders', () => { + it('returns total remaining unshipped quantity across open orders', async () => { + const art = await createArticle(store, tenant, { name: 'Order Article', sku: 'ORD-TEST-1' }); + const cust = await createCustomer(store, tenant, 'Order Customer'); + + let res = await store.article.getBySku(gid, art.sku); + assert.deepEqual(res.article.undeliveredOrders, []); + + const o1 = await store.order.create(gid, { + number: 'ORD-1001', + date: '2026-10-20', + entityId: cust.id, + lines: [{ sku: art.sku, name: 'Item', amount: 10, net: 5, taxrate: 19 }], + }); + + res = await store.article.getBySku(gid, art.sku); + assert.equal(res.article.undeliveredOrders.length, 1); + assert.equal(res.article.undeliveredOrders[0].orderId, o1.id); + assert.equal(res.article.undeliveredOrders[0].orderNumber, 'ORD-1001'); + assert.equal(res.article.undeliveredOrders[0].customerName, 'Order Customer'); + assert.equal(res.article.undeliveredOrders[0].amount, 10); + + const o2 = await store.order.create(gid, { + number: 'ORD-1002', + date: '2026-10-21', + entityId: cust.id, + lines: [{ sku: art.sku, name: 'Item', amount: 5, net: 5, taxrate: 19 }], + }); + + res = await store.article.getBySku(gid, art.sku); + assert.equal(res.article.undeliveredOrders.length, 2); + assert.equal(res.article.undeliveredOrders[0].orderNumber, 'ORD-1001'); + assert.equal(res.article.undeliveredOrders[0].amount, 10); + assert.equal(res.article.undeliveredOrders[1].orderNumber, 'ORD-1002'); + assert.equal(res.article.undeliveredOrders[1].amount, 5); + + await store.shipment.create(gid, { + orderId: o1.id, + lines: [{ sku: art.sku, amount: 4 }], + }); + + res = await store.article.getBySku(gid, art.sku); + assert.equal(res.article.undeliveredOrders.length, 2); + assert.equal(res.article.undeliveredOrders[0].amount, 6); + assert.equal(res.article.undeliveredOrders[1].amount, 5); + + await store.shipment.create(gid, { + orderId: o1.id, + lines: [{ sku: art.sku, amount: 6 }], + }); + + res = await store.article.getBySku(gid, art.sku); + assert.equal(res.article.undeliveredOrders.length, 1); + assert.equal(res.article.undeliveredOrders[0].orderNumber, 'ORD-1002'); + assert.equal(res.article.undeliveredOrders[0].amount, 5); + + await store.shipment.create(gid, { + orderId: o2.id, + }); + + res = await store.article.getBySku(gid, art.sku); + assert.deepEqual(res.article.undeliveredOrders, []); + }); + }); }); +