u
This commit is contained in:
@@ -223,6 +223,7 @@ import labeltemplateDelete from './store/labeltemplate/delete.js';
|
||||
import stockAdjust from './store/stock/adjust.js';
|
||||
import stockAccept from './store/stock/accept.js';
|
||||
import stockShip from './store/stock/ship.js';
|
||||
import stockRebuildSummaries from './store/stock/rebuildSummaries.js';
|
||||
import propertynameList from './store/propertyname/list.js';
|
||||
import propertynameCreate from './store/propertyname/create.js';
|
||||
import propertynameUpdate from './store/propertyname/update.js';
|
||||
@@ -624,6 +625,7 @@ export default class LibDashStore {
|
||||
adjust: stockAdjust(provider),
|
||||
accept: stockAccept(provider),
|
||||
ship: stockShip(provider),
|
||||
rebuildSummaries: stockRebuildSummaries(provider),
|
||||
};
|
||||
this.adjustment = this.stock;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** Drop order = reverse of schema create dependency order. */
|
||||
export const TABLES = [
|
||||
|
||||
'articlewarehousesummary',
|
||||
'deliverysummary',
|
||||
'shipmentsummary',
|
||||
'quotesummary',
|
||||
|
||||
@@ -161,3 +161,14 @@ WHERE e.id = @id
|
||||
AND cg.company_id = @companyId
|
||||
AND cg.name = @groupName
|
||||
`;
|
||||
|
||||
export const CHECK_SUPPLIER_IN_USE = `
|
||||
SELECT TOP 1 1 AS inUse
|
||||
FROM manifest m
|
||||
WHERE m.entity_id = @id AND m.company_id = @companyId
|
||||
UNION ALL
|
||||
SELECT TOP 1 1 AS inUse
|
||||
FROM articlesupplier asup
|
||||
INNER JOIN article a ON a.id = asup.article_id
|
||||
WHERE asup.entity_id = @id AND a.company_id = @companyId;
|
||||
`;
|
||||
|
||||
@@ -619,7 +619,7 @@ BEGIN
|
||||
pu FLOAT NOT NULL CONSTRAINT DF_articlesupplier_pu DEFAULT 1,
|
||||
sort INT NOT NULL CONSTRAINT DF_articlesupplier_sort DEFAULT 0,
|
||||
CONSTRAINT FK_articlesupplier_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
|
||||
CONSTRAINT FK_articlesupplier_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE CASCADE,
|
||||
CONSTRAINT FK_articlesupplier_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE NO ACTION,
|
||||
CONSTRAINT FK_articlesupplier_tax FOREIGN KEY (tax_id) REFERENCES tax(id) ON DELETE NO ACTION,
|
||||
CONSTRAINT UQ_articlesupplier_article_entity UNIQUE (article_id, entity_id),
|
||||
CONSTRAINT UQ_articlesupplier_article_sort UNIQUE (article_id, sort)
|
||||
@@ -643,7 +643,7 @@ BEGIN
|
||||
[date] DATETIME2 NOT NULL CONSTRAINT DF_manifest_date DEFAULT SYSUTCDATETIME(),
|
||||
created_at DATETIME2 NOT NULL CONSTRAINT DF_manifest_created_at DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT FK_manifest_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
|
||||
CONSTRAINT FK_manifest_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE SET NULL
|
||||
CONSTRAINT FK_manifest_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE NO ACTION
|
||||
);
|
||||
END
|
||||
`;
|
||||
@@ -1338,6 +1338,24 @@ BEGIN
|
||||
END
|
||||
`;
|
||||
|
||||
export const INIT_ARTICLEWAREHOUSESUMMARY = `
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articlewarehousesummary')
|
||||
BEGIN
|
||||
CREATE TABLE articlewarehousesummary (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
article_id INT NOT NULL,
|
||||
warehouse_id INT NOT NULL,
|
||||
stock FLOAT NOT NULL CONSTRAINT DF_articlewarehousesummary_stock DEFAULT 0,
|
||||
CONSTRAINT FK_articlewarehousesummary_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
|
||||
CONSTRAINT FK_articlewarehousesummary_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouse(id) ON DELETE CASCADE,
|
||||
CONSTRAINT UQ_articlewarehousesummary_article_warehouse UNIQUE (article_id, warehouse_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IX_articlewarehousesummary_warehouse
|
||||
ON articlewarehousesummary (warehouse_id);
|
||||
END
|
||||
`;
|
||||
|
||||
export const INIT_MCP_CLIENT = `
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'mcp_client')
|
||||
BEGIN
|
||||
@@ -1485,6 +1503,7 @@ export const SCHEMA_SCRIPTS = [
|
||||
INIT_QUOTESUMMARY,
|
||||
INIT_SHIPMENTSUMMARY,
|
||||
INIT_DELIVERYSUMMARY,
|
||||
INIT_ARTICLEWAREHOUSESUMMARY,
|
||||
INIT_MCP_CLIENT,
|
||||
INIT_MCP_AUTH_CODE,
|
||||
INIT_MCP_TOKEN,
|
||||
|
||||
@@ -23,4 +23,16 @@ WHERE id = @id
|
||||
AND customergroup_id IN (
|
||||
SELECT id FROM customergroup WHERE company_id = @companyId AND name = @groupName
|
||||
)
|
||||
`;
|
||||
|
||||
export const CHECK_SUPPLIER_IN_USE = `
|
||||
SELECT 1 AS inUse
|
||||
FROM manifest m
|
||||
WHERE m.entity_id = @id AND m.company_id = @companyId
|
||||
UNION ALL
|
||||
SELECT 1 AS inUse
|
||||
FROM articlesupplier asup
|
||||
INNER JOIN article a ON a.id = asup.article_id
|
||||
WHERE asup.entity_id = @id AND a.company_id = @companyId
|
||||
LIMIT 1;
|
||||
`;
|
||||
@@ -412,7 +412,7 @@ CREATE TABLE IF NOT EXISTS articlesupplier (
|
||||
UNIQUE (article_id, entity_id),
|
||||
UNIQUE (article_id, sort),
|
||||
FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE NO ACTION,
|
||||
FOREIGN KEY (tax_id) REFERENCES tax(id) ON DELETE NO ACTION
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS IX_articlesupplier_article_entity
|
||||
@@ -430,7 +430,7 @@ CREATE TABLE IF NOT EXISTS manifest (
|
||||
"date" TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||
created_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE NO ACTION,
|
||||
FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE SET NULL
|
||||
FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE NO ACTION
|
||||
);
|
||||
`;
|
||||
|
||||
@@ -981,6 +981,20 @@ CREATE INDEX IF NOT EXISTS IX_deliverysummary_company_supplier
|
||||
ON deliverysummary (company_id, supplier_id);
|
||||
`;
|
||||
|
||||
export const INIT_ARTICLEWAREHOUSESUMMARY = `
|
||||
CREATE TABLE IF NOT EXISTS articlewarehousesummary (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
article_id INTEGER NOT NULL,
|
||||
warehouse_id INTEGER NOT NULL,
|
||||
stock REAL NOT NULL DEFAULT 0,
|
||||
UNIQUE (article_id, warehouse_id),
|
||||
FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (warehouse_id) REFERENCES warehouse(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS IX_articlewarehousesummary_warehouse
|
||||
ON articlewarehousesummary (warehouse_id);
|
||||
`;
|
||||
|
||||
export const INIT_MCP_CLIENT = `
|
||||
CREATE TABLE IF NOT EXISTS mcp_client (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -1111,6 +1125,7 @@ export const SCHEMA_SCRIPTS = [
|
||||
INIT_QUOTESUMMARY,
|
||||
INIT_SHIPMENTSUMMARY,
|
||||
INIT_DELIVERYSUMMARY,
|
||||
INIT_ARTICLEWAREHOUSESUMMARY,
|
||||
INIT_MCP_CLIENT,
|
||||
INIT_MCP_AUTH_CODE,
|
||||
INIT_MCP_TOKEN,
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
recordChildAssignedToParentHistory,
|
||||
} from '../articlehistory/record.js';
|
||||
import attachChild from './attachChild.js';
|
||||
import { initArticleWarehouseSummary } from '../articlewarehousesummary/syncSummary.js';
|
||||
|
||||
export default function addChild(provider) {
|
||||
const resolve = resolveArticleBySku(provider);
|
||||
@@ -266,6 +267,10 @@ export default function addChild(provider) {
|
||||
childSku,
|
||||
});
|
||||
|
||||
if (finalTrackStock === 1) {
|
||||
await initArticleWarehouseSummary(runner, { companyId, articleId: childId });
|
||||
}
|
||||
|
||||
return load(googleAuthId, childSku, runner);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import nextSku from '../numbers/nextSku.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import resolveUniqueSlug from './resolveUniqueSlug.js';
|
||||
import { recordArticleCreatedHistory } from '../articlehistory/record.js';
|
||||
import { initArticleWarehouseSummary } from '../articlewarehousesummary/syncSummary.js';
|
||||
|
||||
export default function createUnderParent(provider) {
|
||||
const access = resolveCategoryAccess(provider);
|
||||
@@ -113,6 +114,10 @@ export default function createUnderParent(provider) {
|
||||
name,
|
||||
});
|
||||
|
||||
if (finalTrackStock === 1) {
|
||||
await initArticleWarehouseSummary(runner, { companyId, articleId: id });
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
parentId,
|
||||
|
||||
@@ -13,6 +13,10 @@ import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import { recordTrackStockChangeHistory } from '../articlehistory/record.js';
|
||||
import stockAdjust from '../stock/adjust.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import {
|
||||
initArticleWarehouseSummary,
|
||||
removeArticleWarehouseSummary,
|
||||
} from '../articlewarehousesummary/syncSummary.js';
|
||||
|
||||
export default function setTrackStockBySku(provider) {
|
||||
const resolve = resolveArticleBySku(provider);
|
||||
@@ -94,6 +98,12 @@ export default function setTrackStockBySku(provider) {
|
||||
|
||||
// Write history if track_stock changed
|
||||
if (oldTrack !== newTrack) {
|
||||
if (!newTrack) {
|
||||
await removeArticleWarehouseSummary(runner, { articleId });
|
||||
} else {
|
||||
await initArticleWarehouseSummary(runner, { companyId, articleId });
|
||||
}
|
||||
|
||||
await recordTrackStockChangeHistory(runner, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
recordParentDeactivatedHistory,
|
||||
} from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import { removeArticleWarehouseSummary } from '../articlewarehousesummary/syncSummary.js';
|
||||
|
||||
export default function toggleParent(provider) {
|
||||
const resolve = resolveArticleBySku(provider);
|
||||
@@ -96,6 +97,7 @@ export default function toggleParent(provider) {
|
||||
|
||||
await runner.query('DELETE FROM price WHERE article_id = @articleId', { articleId });
|
||||
await runner.query('UPDATE article SET track_stock = 0 WHERE id = @articleId', { articleId });
|
||||
await removeArticleWarehouseSummary(runner, { articleId });
|
||||
|
||||
// If article is a child, verify attribute is not used by parent or any parent up the chain
|
||||
let currentParentId = currentArt?.parentarticle_id;
|
||||
|
||||
266
src/store/articlewarehousesummary/syncSummary.js
Normal file
266
src/store/articlewarehousesummary/syncSummary.js
Normal 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 });
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,22 @@
|
||||
import { DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE } from '../../providers/mssql/queries/entity.js';
|
||||
import { DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE } from '../../providers/sqlite3/queries/entity.js';
|
||||
import {
|
||||
DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE,
|
||||
CHECK_SUPPLIER_IN_USE as MSSQL_CHECK_IN_USE,
|
||||
GET_BY_COMPANY_AND_ID as MSSQL_GET_BY_ID,
|
||||
} from '../../providers/mssql/queries/entity.js';
|
||||
import {
|
||||
DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE,
|
||||
CHECK_SUPPLIER_IN_USE as SQLITE_CHECK_IN_USE,
|
||||
GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_ID,
|
||||
} from '../../providers/sqlite3/queries/entity.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function del(provider) {
|
||||
const access = resolveSelectedCompanyAccess(provider);
|
||||
const sql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
const sql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
||||
const checkInUseSql = isSqlite ? SQLITE_CHECK_IN_USE : MSSQL_CHECK_IN_USE;
|
||||
const getByIdSql = isSqlite ? SQLITE_GET_BY_ID : MSSQL_GET_BY_ID;
|
||||
|
||||
/**
|
||||
* Deletes an entity by its id within the user's selected company.
|
||||
@@ -14,20 +25,38 @@ export default function del(provider) {
|
||||
* @param {number} id - The id of the entity to delete.
|
||||
* @param {object} [tx] - Optional transaction to run within.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the entity is not found.
|
||||
* @throws {Error} If the entity is not found or is in use.
|
||||
*
|
||||
* @example
|
||||
* await store.entity.delete(42, 5);
|
||||
*/
|
||||
return async (googleAuthId, id, tx) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
const numericId = Number(id);
|
||||
if (!Number.isInteger(numericId) || numericId <= 0) {
|
||||
throw new Error('Entity not found');
|
||||
}
|
||||
|
||||
const { companyId } = await access(googleAuthId, tx);
|
||||
const runner = makeRunner(provider, tx);
|
||||
const { companyId } = await access(googleAuthId, runner);
|
||||
|
||||
const result = await makeRunner(provider, tx).query(sql, {
|
||||
id,
|
||||
const existing = await runner.query(getByIdSql, {
|
||||
id: numericId,
|
||||
companyId,
|
||||
});
|
||||
if (!existing.rows || existing.rows.length === 0) {
|
||||
throw new Error('Entity not found');
|
||||
}
|
||||
|
||||
const usage = await runner.query(checkInUseSql, {
|
||||
id: numericId,
|
||||
companyId,
|
||||
});
|
||||
if (usage.rows && usage.rows.length > 0) {
|
||||
throw new Error('Entity in use');
|
||||
}
|
||||
|
||||
const result = await runner.query(sql, {
|
||||
id: numericId,
|
||||
companyId,
|
||||
});
|
||||
|
||||
@@ -36,3 +65,4 @@ export default function del(provider) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { syncDeliverySummary } from '../delivery/syncSummary.js';
|
||||
import { syncShipmentSummary } from '../shipment/syncSummary.js';
|
||||
import { syncArticleWarehouseSummaryForLedger } from '../articlewarehousesummary/syncSummary.js';
|
||||
import { createHash } from 'node:crypto';
|
||||
import * as mssqlQueries from '../../providers/mssql/queries/ledger.js';
|
||||
import * as sqliteQueries from '../../providers/sqlite3/queries/ledger.js';
|
||||
@@ -239,6 +240,12 @@ export default function add(provider) {
|
||||
memo: text,
|
||||
});
|
||||
|
||||
await syncArticleWarehouseSummaryForLedger(runner, {
|
||||
companyId,
|
||||
manifestarticleId,
|
||||
warehouseId: effectiveWhId,
|
||||
});
|
||||
|
||||
for (const match of articleMatches) {
|
||||
await recordWarehouseHistory(runner, {
|
||||
articleId: match.id,
|
||||
@@ -315,6 +322,12 @@ export default function add(provider) {
|
||||
memo: text,
|
||||
});
|
||||
|
||||
await syncArticleWarehouseSummaryForLedger(runner, {
|
||||
companyId,
|
||||
manifestarticleId,
|
||||
warehouseId: effectiveWhId,
|
||||
});
|
||||
|
||||
for (const match of articleMatches) {
|
||||
await recordWarehouseHistory(runner, {
|
||||
articleId: match.id,
|
||||
@@ -357,6 +370,12 @@ export default function add(provider) {
|
||||
memo: transactionText,
|
||||
});
|
||||
|
||||
await syncArticleWarehouseSummaryForLedger(runner, {
|
||||
companyId,
|
||||
manifestarticleId,
|
||||
warehouseId,
|
||||
});
|
||||
|
||||
return shape(row);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import nextNumber from '../numbers/nextNumber.js';
|
||||
import ledgerAdd from '../ledger/add.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import { parseDate } from '../manifestLines.js';
|
||||
import { syncArticleWarehouseSummaryForLedger } from '../articlewarehousesummary/syncSummary.js';
|
||||
|
||||
const ADJUSTMENT_TYPE = 'adjustment';
|
||||
|
||||
@@ -140,6 +141,12 @@ export default function adjust(provider) {
|
||||
});
|
||||
const ledgerRow = inserted.rows[0];
|
||||
|
||||
await syncArticleWarehouseSummaryForLedger(runner, {
|
||||
companyId,
|
||||
manifestarticleId,
|
||||
warehouseId,
|
||||
});
|
||||
|
||||
// Write warehouse history for tracked article
|
||||
if (manifestArticle.manifest_type === 'delivery') {
|
||||
await recordWarehouseHistory(runner, {
|
||||
|
||||
19
src/store/stock/rebuildSummaries.js
Normal file
19
src/store/stock/rebuildSummaries.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import { rebuildArticleWarehouseSummaries } from '../articlewarehousesummary/syncSummary.js';
|
||||
|
||||
/**
|
||||
* Rebuilds the `articlewarehousesummary` table for the user's selected company.
|
||||
*
|
||||
* @param {import('../../providers/provider.js').DatabaseProvider} provider
|
||||
*/
|
||||
export default function rebuildSummaries(provider) {
|
||||
const access = resolveSelectedCompanyAccess(provider);
|
||||
|
||||
return async (googleAuthId, tx) => {
|
||||
const runner = makeRunner(provider, tx);
|
||||
const { companyId } = await access(googleAuthId, runner);
|
||||
await rebuildArticleWarehouseSummaries(runner, companyId);
|
||||
return { ok: true, companyId };
|
||||
};
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
import {
|
||||
DELETE_BY_ID as MSSQL_DELETE_BY_ID,
|
||||
CHECK_SUPPLIER_IN_USE as MSSQL_CHECK_IN_USE,
|
||||
GET_SUPPLIER_BY_ID as MSSQL_GET_SUPPLIER,
|
||||
SUPPLIER_GROUP_NAME,
|
||||
} from '../../providers/mssql/queries/entity.js';
|
||||
import { DELETE_BY_ID as SQLITE_DELETE_BY_ID } from '../../providers/sqlite3/queries/entity.js';
|
||||
import {
|
||||
DELETE_BY_ID as SQLITE_DELETE_BY_ID,
|
||||
CHECK_SUPPLIER_IN_USE as SQLITE_CHECK_IN_USE,
|
||||
GET_SUPPLIER_BY_ID as SQLITE_GET_SUPPLIER,
|
||||
} from '../../providers/sqlite3/queries/entity.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function del(provider) {
|
||||
const access = resolveSelectedCompanyAccess(provider);
|
||||
const sql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE_BY_ID : MSSQL_DELETE_BY_ID;
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
const sql = isSqlite ? SQLITE_DELETE_BY_ID : MSSQL_DELETE_BY_ID;
|
||||
const checkInUseSql = isSqlite ? SQLITE_CHECK_IN_USE : MSSQL_CHECK_IN_USE;
|
||||
const getSupplierSql = isSqlite ? SQLITE_GET_SUPPLIER : MSSQL_GET_SUPPLIER;
|
||||
|
||||
/**
|
||||
* Deletes a supplier by its id within the user's selected company.
|
||||
@@ -17,21 +26,39 @@ export default function del(provider) {
|
||||
* @param {number} id - The id of the supplier to delete.
|
||||
* @param {object} [tx] - Optional transaction to run within.
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If the supplier is not found.
|
||||
* @throws {Error} If the supplier is not found or is in use.
|
||||
*
|
||||
* @example
|
||||
* await store.supplier.delete(42, 5);
|
||||
*/
|
||||
return async (googleAuthId, id, tx) => {
|
||||
if (!Number.isFinite(id)) {
|
||||
const numericId = Number(id);
|
||||
if (!Number.isInteger(numericId) || numericId <= 0) {
|
||||
throw new Error('Supplier not found');
|
||||
}
|
||||
|
||||
const runner = makeRunner(provider, tx);
|
||||
const { companyId } = await access(googleAuthId, tx);
|
||||
const { companyId } = await access(googleAuthId, runner);
|
||||
|
||||
const existing = await runner.query(getSupplierSql, {
|
||||
id: numericId,
|
||||
companyId,
|
||||
groupName: SUPPLIER_GROUP_NAME,
|
||||
});
|
||||
if (!existing.rows || existing.rows.length === 0) {
|
||||
throw new Error('Supplier not found');
|
||||
}
|
||||
|
||||
const usage = await runner.query(checkInUseSql, {
|
||||
id: numericId,
|
||||
companyId,
|
||||
});
|
||||
if (usage.rows && usage.rows.length > 0) {
|
||||
throw new Error('Supplier in use');
|
||||
}
|
||||
|
||||
const result = await runner.query(sql, {
|
||||
id,
|
||||
id: numericId,
|
||||
companyId,
|
||||
groupName: SUPPLIER_GROUP_NAME,
|
||||
});
|
||||
@@ -41,3 +68,4 @@ export default function del(provider) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { isUniqueConstraintError } from '../../providers/provider.js';
|
||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import { initWarehouseSummaryForNewWarehouse } from '../articlewarehousesummary/syncSummary.js';
|
||||
|
||||
function shape(row) {
|
||||
return { id: row.id, name: row.name, sort: row.sort };
|
||||
@@ -65,6 +66,9 @@ export default function create(provider) {
|
||||
name: cleaned,
|
||||
sort,
|
||||
};
|
||||
|
||||
await initWarehouseSummaryForNewWarehouse(runner, { companyId, warehouseId: row.id });
|
||||
|
||||
return shape(row);
|
||||
} catch (err) {
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user