This commit is contained in:
seb
2026-10-17 23:31:37 +02:00
parent ab42a3ffb4
commit 688fa4f3fd
19 changed files with 475 additions and 18 deletions

View File

@@ -0,0 +1,266 @@
/**
* Centralized sync and rebuild logic for the materialized `articlewarehousesummary` table.
*
* Keeps `articlewarehousesummary` in sync with ledger movements, track_stock status,
* and warehouses.
*/
const SQL_FIND_ARTICLES_BY_MANIFESTARTICLE = `
SELECT a.id AS articleId, a.company_id AS companyId, a.track_stock AS trackStock
FROM manifestarticle ma
INNER JOIN manifest m ON m.id = ma.manifest_id
INNER JOIN article a ON a.company_id = m.company_id
AND (
a.sku = ma.sku
OR EXISTS (SELECT 1 FROM articlesupplier asup WHERE asup.article_id = a.id AND asup.sku = ma.sku)
)
WHERE ma.id = @manifestarticleId AND m.company_id = @companyId;
`;
const SQL_GET_STOCK_FROM_LEDGER = `
SELECT COALESCE(SUM(l.amount), 0) AS stock
FROM ledger l
INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id
INNER JOIN manifest m ON m.id = ma.manifest_id
WHERE l.warehouse_id = @warehouseId
AND m.company_id = @companyId
AND (
ma.sku = (SELECT sku FROM article WHERE id = @articleId)
OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = @articleId)
);
`;
const SQL_UPSERT_SUMMARY_SQLITE = `
INSERT INTO articlewarehousesummary (article_id, warehouse_id, stock)
VALUES (@articleId, @warehouseId, @stock)
ON CONFLICT (article_id, warehouse_id) DO UPDATE SET stock = excluded.stock;
`;
const SQL_UPSERT_SUMMARY_MSSQL = `
MERGE INTO articlewarehousesummary AS target
USING (SELECT @articleId AS article_id, @warehouseId AS warehouse_id) AS source
ON (target.article_id = source.article_id AND target.warehouse_id = source.warehouse_id)
WHEN MATCHED THEN
UPDATE SET stock = @stock
WHEN NOT MATCHED THEN
INSERT (article_id, warehouse_id, stock)
VALUES (@articleId, @warehouseId, @stock);
`;
const SQL_INIT_FOR_ARTICLE_SQLITE = `
INSERT INTO articlewarehousesummary (article_id, warehouse_id, stock)
SELECT @articleId, w.id, 0
FROM warehouse w
WHERE w.company_id = @companyId
ON CONFLICT (article_id, warehouse_id) DO UPDATE SET stock = 0;
`;
const SQL_INIT_FOR_ARTICLE_MSSQL = `
MERGE INTO articlewarehousesummary AS target
USING (
SELECT @articleId AS article_id, w.id AS warehouse_id
FROM warehouse w
WHERE w.company_id = @companyId
) AS source
ON (target.article_id = source.article_id AND target.warehouse_id = source.warehouse_id)
WHEN MATCHED THEN
UPDATE SET stock = 0
WHEN NOT MATCHED THEN
INSERT (article_id, warehouse_id, stock)
VALUES (source.article_id, source.warehouse_id, 0);
`;
const SQL_INIT_FOR_NEW_WAREHOUSE_SQLITE = `
INSERT INTO articlewarehousesummary (article_id, warehouse_id, stock)
SELECT a.id, @warehouseId, 0
FROM article a
WHERE a.company_id = @companyId AND a.track_stock = 1
ON CONFLICT (article_id, warehouse_id) DO UPDATE SET stock = 0;
`;
const SQL_INIT_FOR_NEW_WAREHOUSE_MSSQL = `
MERGE INTO articlewarehousesummary AS target
USING (
SELECT a.id AS article_id, @warehouseId AS warehouse_id
FROM article a
WHERE a.company_id = @companyId AND a.track_stock = 1
) AS source
ON (target.article_id = source.article_id AND target.warehouse_id = source.warehouse_id)
WHEN MATCHED THEN
UPDATE SET stock = 0
WHEN NOT MATCHED THEN
INSERT (article_id, warehouse_id, stock)
VALUES (source.article_id, source.warehouse_id, 0);
`;
const SQL_DELETE_BY_ARTICLE = `
DELETE FROM articlewarehousesummary WHERE article_id = @articleId;
`;
/**
* Updates `articlewarehousesummary` after a ledger entry is appended with a warehouseId.
*
* @param {object} runner - Database runner.
* @param {object} opts
* @param {number} opts.companyId
* @param {number} opts.manifestarticleId
* @param {number|null} opts.warehouseId
*/
export async function syncArticleWarehouseSummaryForLedger(runner, { companyId, manifestarticleId, warehouseId }) {
if (warehouseId == null) {
return;
}
const isSqlite = runner.dialect === 'sqlite3';
const upsertSql = isSqlite ? SQL_UPSERT_SUMMARY_SQLITE : SQL_UPSERT_SUMMARY_MSSQL;
const res = await runner.query(SQL_FIND_ARTICLES_BY_MANIFESTARTICLE, {
manifestarticleId,
companyId,
});
const rows = res.rows ?? [];
for (const row of rows) {
const articleId = row.articleId;
const isTracked = Boolean(row.trackStock ?? 0);
if (!isTracked) {
await runner.query(SQL_DELETE_BY_ARTICLE, { articleId });
continue;
}
const stockRes = await runner.query(SQL_GET_STOCK_FROM_LEDGER, {
articleId,
warehouseId,
companyId,
});
const rawStock = stockRes.rows[0]?.stock ?? 0;
const stock = Math.round(Number(rawStock) * 1000) / 1000;
await runner.query(upsertSql, {
articleId,
warehouseId,
stock,
});
}
}
/**
* Initializes `articlewarehousesummary` with stock = 0 for all warehouses of the company
* when track_stock is enabled for an article.
*
* @param {object} runner - Database runner.
* @param {object} opts
* @param {number} opts.companyId
* @param {number} opts.articleId
*/
export async function initArticleWarehouseSummary(runner, { companyId, articleId }) {
const isSqlite = runner.dialect === 'sqlite3';
const sql = isSqlite ? SQL_INIT_FOR_ARTICLE_SQLITE : SQL_INIT_FOR_ARTICLE_MSSQL;
await runner.query(sql, { companyId, articleId });
}
/**
* Removes all rows for an article from `articlewarehousesummary` when track_stock is disabled.
*
* @param {object} runner - Database runner.
* @param {object} opts
* @param {number} opts.articleId
*/
export async function removeArticleWarehouseSummary(runner, { articleId }) {
await runner.query(SQL_DELETE_BY_ARTICLE, { articleId });
}
/**
* Initializes entries in `articlewarehousesummary` for a newly created warehouse
* for all articles with stock tracking enabled.
*
* @param {object} runner - Database runner.
* @param {object} opts
* @param {number} opts.companyId
* @param {number} opts.warehouseId
*/
export async function initWarehouseSummaryForNewWarehouse(runner, { companyId, warehouseId }) {
const isSqlite = runner.dialect === 'sqlite3';
const sql = isSqlite ? SQL_INIT_FOR_NEW_WAREHOUSE_SQLITE : SQL_INIT_FOR_NEW_WAREHOUSE_MSSQL;
await runner.query(sql, { companyId, warehouseId });
}
/**
* Rebuilds the entire `articlewarehousesummary` for a given company.
*
* @param {object} runner - Database runner.
* @param {number} companyId - The company id.
*/
export async function rebuildArticleWarehouseSummaries(runner, companyId) {
const isSqlite = runner.dialect === 'sqlite3';
if (isSqlite) {
const sqlRebuildSqlite = `
DELETE FROM articlewarehousesummary
WHERE article_id IN (SELECT id FROM article WHERE company_id = @companyId);
INSERT INTO articlewarehousesummary (article_id, warehouse_id, stock)
SELECT
a.id AS article_id,
w.id AS warehouse_id,
COALESCE(s.stock, 0) AS stock
FROM article a
CROSS JOIN warehouse w ON w.company_id = a.company_id
LEFT JOIN (
SELECT
ca.id AS article_id,
l.warehouse_id,
SUM(l.amount) AS stock
FROM ledger l
INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id
INNER JOIN manifest m ON m.id = ma.manifest_id
INNER JOIN article ca ON ca.company_id = m.company_id
AND (
ca.sku = ma.sku
OR EXISTS (SELECT 1 FROM articlesupplier asup WHERE asup.article_id = ca.id AND asup.sku = ma.sku)
)
WHERE m.company_id = @companyId
AND l.warehouse_id IS NOT NULL
GROUP BY ca.id, l.warehouse_id
) s ON s.article_id = a.id AND s.warehouse_id = w.id
WHERE a.company_id = @companyId
AND a.track_stock = 1;
`;
await runner.query(sqlRebuildSqlite, { companyId });
} else {
const sqlRebuildMssql = `
DELETE FROM articlewarehousesummary
WHERE article_id IN (SELECT id FROM article WHERE company_id = @companyId);
INSERT INTO articlewarehousesummary (article_id, warehouse_id, stock)
SELECT
a.id AS article_id,
w.id AS warehouse_id,
COALESCE(s.stock, 0) AS stock
FROM article a
CROSS JOIN warehouse w
LEFT JOIN (
SELECT
ca.id AS article_id,
l.warehouse_id,
SUM(l.amount) AS stock
FROM ledger l
INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id
INNER JOIN manifest m ON m.id = ma.manifest_id
INNER JOIN article ca ON ca.company_id = m.company_id
AND (
ca.sku = ma.sku
OR EXISTS (SELECT 1 FROM articlesupplier asup WHERE asup.article_id = ca.id AND asup.sku = ma.sku)
)
WHERE m.company_id = @companyId
AND l.warehouse_id IS NOT NULL
GROUP BY ca.id, l.warehouse_id
) s ON s.article_id = a.id AND s.warehouse_id = w.id
WHERE a.company_id = @companyId
AND w.company_id = @companyId
AND a.track_stock = 1;
`;
await runner.query(sqlRebuildMssql, { companyId });
}
}