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

@@ -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;

View File

@@ -1,6 +1,7 @@
/** Drop order = reverse of schema create dependency order. */
export const TABLES = [
'articlewarehousesummary',
'deliverysummary',
'shipmentsummary',
'quotesummary',

View File

@@ -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;
`;

View File

@@ -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,

View File

@@ -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;
`;

View File

@@ -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,

View File

@@ -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);
};

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;

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 });
}
}

View File

@@ -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) {
}
};
}

View File

@@ -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);
};

View File

@@ -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, {

View 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 };
};
}

View File

@@ -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) {
}
};
}

View File

@@ -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 (