import { TOUCH_UPDATED_AT as MSSQL_TOUCH, TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as MSSQL_TOUCH_STUECKLISTE, } from '../../providers/mssql/queries/article.js'; import { TOUCH_UPDATED_AT as SQLITE_TOUCH, TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as SQLITE_TOUCH_STUECKLISTE, } from '../../providers/sqlite3/queries/article.js'; /** * 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, }); const touchSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; const touchStuecklisteSql = isSqlite ? SQLITE_TOUCH_STUECKLISTE : MSSQL_TOUCH_STUECKLISTE; await runner.query(touchSql, { articleId }); await runner.query(touchStuecklisteSql, { articleId }); } } /** * 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 }); } }