diff --git a/src/store/article/toggleFather.js b/src/store/article/toggleFather.js index 32210bd..5b80a1f 100644 --- a/src/store/article/toggleFather.js +++ b/src/store/article/toggleFather.js @@ -54,8 +54,16 @@ export default function toggleFather(provider) { } const stockRes = await runner.query( - 'SELECT COALESCE(SUM(l.amount), 0) AS totalStock FROM ledger l WHERE l.article_id = @articleId', - { articleId }, + `SELECT COALESCE(SUM(l.amount), 0) AS totalStock + FROM ledger l + INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id + INNER JOIN manifest m ON m.id = ma.manifest_id + WHERE m.company_id = @companyId + AND ( + ma.sku = @sku + OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = @articleId) + )`, + { companyId, sku, articleId }, ); const totalStock = Number(stockRes.rows[0]?.totalStock ?? 0); if (totalStock !== 0) { diff --git a/src/store/articlelist/setItemsByArticleSku.js b/src/store/articlelist/setItemsByArticleSku.js index ec863f7..847382f 100644 --- a/src/store/articlelist/setItemsByArticleSku.js +++ b/src/store/articlelist/setItemsByArticleSku.js @@ -4,6 +4,39 @@ import resolveArticleBySku from '../article/resolveArticleBySku.js'; import getItemsByArticleSku from './getItemsByArticleSku.js'; import makeRunner from '../runner.js'; +async function hasArticlelistCycle(runner, rootArticleId, candidateComponentId) { + const visited = new Set(); + const queue = [candidateComponentId]; + + while (queue.length > 0) { + const currentId = queue.shift(); + if (currentId === rootArticleId) { + return true; + } + if (visited.has(currentId)) continue; + visited.add(currentId); + + const res = await runner.query( + `SELECT ala.article_id + FROM article a + INNER JOIN articlelistarticle ala ON ala.articlelist_id = a.articlelist_id + WHERE a.id = @currentId`, + { currentId }, + ); + + for (const row of res.rows) { + if (row.article_id === rootArticleId) { + return true; + } + if (!visited.has(row.article_id)) { + queue.push(row.article_id); + } + } + } + + return false; +} + export default function setItemsByArticleSku(provider) { const resolve = resolveArticleBySku(provider); const getItems = getItemsByArticleSku(provider); @@ -75,8 +108,8 @@ export default function setItemsByArticleSku(provider) { throw new Error(`Item at index ${i} must specify a valid articleId or sku`); } - if (itemArticleId === articleId) { - throw new Error('A Stückliste article cannot contain itself'); + if (itemArticleId === articleId || await hasArticlelistCycle(runner, articleId, itemArticleId)) { + throw new Error('A Stückliste article cannot contain itself or form a circular reference'); } if (seenArticleIds.has(itemArticleId)) { @@ -93,7 +126,16 @@ export default function setItemsByArticleSku(provider) { throw new Error(`Article not found for component: ${itemArticleId}`); } - const count = item?.count != null && Number(item.count) > 0 ? Number(item.count) : 1; + const rawCount = item?.count; + if ( + typeof rawCount !== 'number' || + !Number.isInteger(rawCount) || + rawCount <= 0 || + rawCount > 2147483647 + ) { + throw new Error(`Invalid count for Stückliste item at index ${i}: must be a positive integer`); + } + const count = rawCount; let sort = item?.sort != null && Number.isInteger(Number(item.sort)) ? Number(item.sort) : i; while (seenSorts.has(sort)) { diff --git a/src/store/optionslist/setOptionsByArticleSku.js b/src/store/optionslist/setOptionsByArticleSku.js index 8e5cb82..f4949f7 100644 --- a/src/store/optionslist/setOptionsByArticleSku.js +++ b/src/store/optionslist/setOptionsByArticleSku.js @@ -4,6 +4,39 @@ import resolveArticleBySku from '../article/resolveArticleBySku.js'; import getOptionsByArticleSku from './getOptionsByArticleSku.js'; import makeRunner from '../runner.js'; +async function hasOptionslistCycle(runner, rootArticleId, candidateOptionId) { + const visited = new Set(); + const queue = [candidateOptionId]; + + while (queue.length > 0) { + const currentId = queue.shift(); + if (currentId === rootArticleId) { + return true; + } + if (visited.has(currentId)) continue; + visited.add(currentId); + + const res = await runner.query( + `SELECT ola.article_id + FROM article a + INNER JOIN optionslistarticle ola ON ola.optionslist_id = a.optionslist_id + WHERE a.id = @currentId`, + { currentId }, + ); + + for (const row of res.rows) { + if (row.article_id === rootArticleId) { + return true; + } + if (!visited.has(row.article_id)) { + queue.push(row.article_id); + } + } + } + + return false; +} + export default function setOptionsByArticleSku(provider) { const resolve = resolveArticleBySku(provider); const getOptions = getOptionsByArticleSku(provider); @@ -75,8 +108,8 @@ export default function setOptionsByArticleSku(provider) { throw new Error(`Option at index ${i} must specify a valid articleId or sku`); } - if (itemArticleId === articleId) { - throw new Error('An article cannot have itself as an option'); + if (itemArticleId === articleId || await hasOptionslistCycle(runner, articleId, itemArticleId)) { + throw new Error('An article cannot have itself as an option or form a circular reference in options'); } if (seenArticleIds.has(itemArticleId)) { @@ -93,7 +126,16 @@ export default function setOptionsByArticleSku(provider) { throw new Error(`Article not found for option: ${itemArticleId}`); } - const count = item?.count != null && Number(item.count) > 0 ? Number(item.count) : 1; + const rawCount = item?.count; + if ( + typeof rawCount !== 'number' || + !Number.isInteger(rawCount) || + rawCount <= 0 || + rawCount > 2147483647 + ) { + throw new Error(`Invalid count for options item at index ${i}: must be a positive integer`); + } + const count = rawCount; let sort = item?.sort != null && Number.isInteger(Number(item.sort)) ? Number(item.sort) : i; while (seenSorts.has(sort)) {