diff --git a/API.article.md b/API.article.md index aad1b36..847f804 100644 --- a/API.article.md +++ b/API.article.md @@ -186,7 +186,7 @@ Returns the full detail of an article by its SKU, including unit, brand, categor **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}>, 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}>}>, 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 3607991..36b3018 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}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>` + - `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}}>` - `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 498981f..33c3a67 100644 --- a/src/providers/mssql/queries/article.js +++ b/src/providers/mssql/queries/article.js @@ -574,7 +574,30 @@ SELECT asup.sort, asup.net AS [price.net], asup.tax_id AS [price.taxId], - t.rate AS [price.taxRate] + t.rate AS [price.taxRate], + ( + SELECT + m.[date] AS [date], + SUM(ma.amount - COALESCE(ma_ledger.booked_amount, 0)) AS amount + FROM manifest m + INNER JOIN manifestarticle ma ON ma.manifest_id = m.id + LEFT JOIN ( + SELECT manifestarticle_id, SUM(amount) AS booked_amount + FROM ledger + WHERE warehouse_id IS NULL + GROUP BY manifestarticle_id + ) ma_ledger ON ma_ledger.manifestarticle_id = ma.id + WHERE m.company_id = a.company_id + AND m.[type] = 'delivery' + AND m.entity_id = asup.entity_id + AND asup.sku IS NOT NULL + AND asup.sku <> '' + AND ma.sku = asup.sku + AND (ma.amount - COALESCE(ma_ledger.booked_amount, 0)) > 0 + GROUP BY m.id, m.[date] + ORDER BY m.[date] ASC, m.id ASC + FOR JSON PATH + ) AS incomingJson FROM articlesupplier asup INNER JOIN entity e ON e.id = asup.entity_id LEFT JOIN tax t ON t.id = asup.tax_id diff --git a/src/providers/sqlite3/queries/article.js b/src/providers/sqlite3/queries/article.js index 0f4cf9e..8a9d4a9 100644 --- a/src/providers/sqlite3/queries/article.js +++ b/src/providers/sqlite3/queries/article.js @@ -207,11 +207,40 @@ SELECT auth.*, 'stock', stock, 'sort', sort, 'price', CASE WHEN net IS NULL THEN NULL ELSE json_object( 'net', net, 'taxId', tax_id, 'taxRate', tax_rate - ) END + ) END, + 'incoming', json(incoming_json) )), '[]') FROM ( SELECT s.id, s.article_id, s.entity_id, e.name AS supplier_name, s.sku, s.pu, s.stock, s.sort, - s.net, s.tax_id, t.rate AS tax_rate + s.net, s.tax_id, t.rate AS tax_rate, + COALESCE(( + SELECT json_group_array(json_object( + 'date', inc.date, + 'amount', inc.amount + )) + FROM ( + SELECT + m.date AS date, + SUM(ma.amount - COALESCE(ma_ledger.booked_amount, 0)) AS amount + FROM manifest m + INNER JOIN manifestarticle ma ON ma.manifest_id = m.id + LEFT JOIN ( + SELECT manifestarticle_id, SUM(amount) AS booked_amount + FROM ledger + WHERE warehouse_id IS NULL + GROUP BY manifestarticle_id + ) ma_ledger ON ma_ledger.manifestarticle_id = ma.id + WHERE m.company_id = a.company_id + AND m.type = 'delivery' + AND m.entity_id = s.entity_id + AND s.sku IS NOT NULL + AND s.sku <> '' + AND ma.sku = s.sku + AND (ma.amount - COALESCE(ma_ledger.booked_amount, 0)) > 0 + GROUP BY m.id, m.date + ORDER BY m.date ASC, m.id ASC + ) inc + ), '[]') AS incoming_json FROM articlesupplier s INNER JOIN entity e ON e.id = s.entity_id LEFT JOIN tax t ON t.id = s.tax_id WHERE s.article_id = a.id ORDER BY s.sort, s.id diff --git a/src/store/article/getBySku.js b/src/store/article/getBySku.js index 6f9dc35..8474881 100644 --- a/src/store/article/getBySku.js +++ b/src/store/article/getBySku.js @@ -18,7 +18,7 @@ export default function getBySku(provider) { * @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}>, 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}>}>, 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 @@ -87,6 +87,10 @@ export default function getBySku(provider) { stock: s.stock ?? null, sort: s.sort, price: s.price?.net != null ? s.price : null, + incoming: parseJsonArray(s.incomingJson ?? s.incoming).map((inc) => ({ + date: inc.date, + amount: Number(inc.amount ?? 0), + })), })), warehouses: parseJsonArray(a.warehousesJson).map((w) => ({ id: w.id, diff --git a/test/article.test.js b/test/article.test.js index c646d2c..bbbbfbf 100644 --- a/test/article.test.js +++ b/test/article.test.js @@ -651,4 +651,47 @@ describe('store.article', () => { assert.deepEqual(untouched.eans, []); }); }); + + describe('supplier incoming deliveries', () => { + it('returns incoming dates and unbooked amounts for each supplier', async () => { + const art = await createArticle(store, tenant, { name: 'Incoming Article', sku: 'INC-ART-1' }); + const supp = await store.supplier.create(gid, { name: 'Supplier Incoming' }); + await store.articlesupplier.create(gid, art.sku, { + entityId: supp.id, + sku: 'SUP-SKU-1', + pu: 10, + }); + + let res = await store.article.getBySku(gid, art.sku); + assert.equal(res.article.suppliers.length, 1); + assert.deepEqual(res.article.suppliers[0].incoming, []); + + const d1 = await store.delivery.create(gid, { + supplierId: supp.id, + date: '2026-11-01T00:00:00.000Z', + lines: [{ sku: 'SUP-SKU-1', name: 'Item', pu: 10, amount: 25, net: 5, taxrate: 19 }], + }); + + const d2 = await store.delivery.create(gid, { + supplierId: supp.id, + date: '2026-11-15T00:00:00.000Z', + lines: [{ sku: 'SUP-SKU-1', name: 'Item', pu: 10, amount: 15, net: 5, taxrate: 19 }], + }); + + res = await store.article.getBySku(gid, art.sku); + assert.equal(res.article.suppliers[0].incoming.length, 2); + assert.equal(res.article.suppliers[0].incoming[0].amount, 25); + assert.equal(res.article.suppliers[0].incoming[0].date, '2026-11-01T00:00:00.000Z'); + assert.equal(res.article.suppliers[0].incoming[1].amount, 15); + assert.equal(res.article.suppliers[0].incoming[1].date, '2026-11-15T00:00:00.000Z'); + + const wh = await store.warehouse.create(gid, { name: 'Inc WH' }); + await store.stock.accept(gid, { deliveryId: d1.id, warehouseId: wh.id }); + + res = await store.article.getBySku(gid, art.sku); + assert.equal(res.article.suppliers[0].incoming.length, 1); + assert.equal(res.article.suppliers[0].incoming[0].amount, 15); + assert.equal(res.article.suppliers[0].incoming[0].date, '2026-11-15T00:00:00.000Z'); + }); + }); });