This commit is contained in:
seb
2026-10-16 22:09:12 +02:00
parent 796f6a3272
commit 7635821dad
38 changed files with 6191 additions and 3350 deletions

View File

@@ -0,0 +1,445 @@
/**
* Centralized sync and rebuild logic for the materialized `deliverysummary` table.
*
* Keeps `deliverysummary` in sync with manifest (delivery), manifestarticle,
* article (track_stock), and ledger (DELIVERY transactions).
*/
const SQL_GET_DELIVERY_BY_ID_SQLITE = `
SELECT m.id, m.company_id, m."number", m.external_number, m."date", m.created_at, m.entity_id
FROM manifest m
WHERE m.id = @manifestId AND m.company_id = @companyId AND m."type" = 'delivery'
`;
const SQL_GET_DELIVERY_BY_ID_MSSQL = `
SELECT m.id, m.company_id, m.[number], m.external_number, m.[date], m.created_at, m.entity_id
FROM manifest m
WHERE m.id = @manifestId AND m.company_id = @companyId AND m.[type] = 'delivery'
`;
const SQL_GET_DELIVERY_BY_NUMBER_SQLITE = `
SELECT m.id, m.company_id, m."number", m.external_number, m."date", m.created_at, m.entity_id
FROM manifest m
WHERE m."number" = @number AND m.company_id = @companyId AND m."type" = 'delivery'
`;
const SQL_GET_DELIVERY_BY_NUMBER_MSSQL = `
SELECT m.id, m.company_id, m.[number], m.external_number, m.[date], m.created_at, m.entity_id
FROM manifest m
WHERE m.[number] = @number AND m.company_id = @companyId AND m.[type] = 'delivery'
`;
const SQL_SUPPLIER_NAME_SQLITE = `
SELECT TRIM(COALESCE(first_name || ' ', '') || COALESCE(name, '')) AS supplier_name
FROM entity
WHERE id = @entityId
`;
const SQL_SUPPLIER_NAME_MSSQL = `
SELECT LTRIM(RTRIM(ISNULL(first_name + ' ', '') + ISNULL(name, ''))) AS supplier_name
FROM entity
WHERE id = @entityId
`;
const SQL_DELIVERY_TOTALS_SQLITE = `
WITH line_ledger AS (
SELECT
manifestarticle_id,
SUM(amount) AS booked_amount
FROM ledger
WHERE warehouse_id IS NULL
AND UPPER(transaction_type) IN ('ACCEPTANCE', 'DELIVERY', 'GOODS_RECEIPT')
GROUP BY manifestarticle_id
),
line_data AS (
SELECT
ma.id,
ma.amount,
ma.amount * (ma.net * (1.0 - ma.rebate / 100.0)) * (1.0 + ma.taxrate / 100.0) AS line_gross,
ma.amount * (ma.net * (1.0 - ma.rebate / 100.0)) AS line_net,
CASE WHEN COALESCE(a.track_stock, 1) = 0 THEN 0 ELSE 1 END AS track_stock,
COALESCE(ll.booked_amount, 0) AS booked_amount
FROM manifestarticle ma
LEFT JOIN article a ON a.company_id = @companyId AND (a.sku = ma.sku OR EXISTS (SELECT 1 FROM articlesupplier asup WHERE asup.article_id = a.id AND asup.sku = ma.sku))
LEFT JOIN line_ledger ll ON ll.manifestarticle_id = ma.id
WHERE ma.manifest_id = @manifestId
)
SELECT
COUNT(*) AS positions,
COALESCE(SUM(line_gross), 0) AS gross_total,
COALESCE(SUM(line_net), 0) AS net_total,
COALESCE(SUM(amount), 0) AS total_quantity,
COALESCE(SUM(CASE WHEN track_stock = 1 THEN 1 ELSE 0 END), 0) AS stock_positions,
COALESCE(SUM(CASE WHEN track_stock = 1 AND (CASE WHEN amount > 0 THEN booked_amount >= amount ELSE booked_amount > 0 END) THEN 1 ELSE 0 END), 0) AS booked_positions,
COALESCE(SUM(CASE WHEN track_stock = 1 THEN amount ELSE 0 END), 0) AS stock_quantity,
COALESCE(SUM(CASE WHEN track_stock = 1 THEN booked_amount ELSE 0 END), 0) AS booked_quantity
FROM line_data
`;
const SQL_DELIVERY_TOTALS_MSSQL = `
WITH line_ledger AS (
SELECT
manifestarticle_id,
SUM(amount) AS booked_amount
FROM ledger
WHERE warehouse_id IS NULL
AND UPPER(transaction_type) IN ('ACCEPTANCE', 'DELIVERY', 'GOODS_RECEIPT')
GROUP BY manifestarticle_id
),
line_data AS (
SELECT
ma.id,
ma.amount,
ma.amount * (ma.net * (1.0 - ma.rebate / 100.0)) * (1.0 + ma.taxrate / 100.0) AS line_gross,
ma.amount * (ma.net * (1.0 - ma.rebate / 100.0)) AS line_net,
CASE WHEN COALESCE(a.track_stock, 1) = 0 THEN 0 ELSE 1 END AS track_stock,
COALESCE(ll.booked_amount, 0) AS booked_amount
FROM manifestarticle ma
LEFT JOIN article a ON a.company_id = @companyId AND (a.sku = ma.sku OR EXISTS (SELECT 1 FROM articlesupplier asup WHERE asup.article_id = a.id AND asup.sku = ma.sku))
LEFT JOIN line_ledger ll ON ll.manifestarticle_id = ma.id
WHERE ma.manifest_id = @manifestId
)
SELECT
COUNT(*) AS positions,
COALESCE(SUM(line_gross), 0) AS gross_total,
COALESCE(SUM(line_net), 0) AS net_total,
COALESCE(SUM(amount), 0) AS total_quantity,
COALESCE(SUM(CASE WHEN track_stock = 1 THEN 1 ELSE 0 END), 0) AS stock_positions,
COALESCE(SUM(CASE WHEN track_stock = 1 AND (CASE WHEN amount > 0 THEN booked_amount >= amount ELSE booked_amount > 0 END) THEN 1 ELSE 0 END), 0) AS booked_positions,
COALESCE(SUM(CASE WHEN track_stock = 1 THEN amount ELSE 0 END), 0) AS stock_quantity,
COALESCE(SUM(CASE WHEN track_stock = 1 THEN booked_amount ELSE 0 END), 0) AS booked_quantity
FROM line_data
`;
const SQL_UPSERT_SUMMARY_SQLITE = `
INSERT INTO deliverysummary (
company_id, manifest_id, delivery_number, external_number, delivery_date, created_at,
supplier_id, supplier_name, positions, gross_total, net_total, total_quantity,
stock_positions, booked_positions, unbooked_positions,
stock_quantity, booked_quantity, unbooked_quantity, is_booked
) VALUES (
@companyId, @manifestId, @deliveryNumber, @externalNumber, @deliveryDate, @createdAt,
@supplierId, @supplierName, @positions, @grossTotal, @netTotal, @totalQuantity,
@stockPositions, @bookedPositions, @unbookedPositions,
@stockQuantity, @bookedQuantity, @unbookedQuantity, @isBooked
)
ON CONFLICT(manifest_id) DO UPDATE SET
delivery_number = excluded.delivery_number,
external_number = excluded.external_number,
delivery_date = excluded.delivery_date,
created_at = excluded.created_at,
supplier_id = excluded.supplier_id,
supplier_name = excluded.supplier_name,
positions = excluded.positions,
gross_total = excluded.gross_total,
net_total = excluded.net_total,
total_quantity = excluded.total_quantity,
stock_positions = excluded.stock_positions,
booked_positions = excluded.booked_positions,
unbooked_positions = excluded.unbooked_positions,
stock_quantity = excluded.stock_quantity,
booked_quantity = excluded.booked_quantity,
unbooked_quantity = excluded.unbooked_quantity,
is_booked = excluded.is_booked
`;
const SQL_UPSERT_SUMMARY_MSSQL = `
MERGE INTO deliverysummary AS target
USING (SELECT @manifestId AS manifest_id) AS source
ON (target.manifest_id = source.manifest_id)
WHEN MATCHED THEN
UPDATE SET
delivery_number = @deliveryNumber,
external_number = @externalNumber,
delivery_date = @deliveryDate,
created_at = @createdAt,
supplier_id = @supplierId,
supplier_name = @supplierName,
positions = @positions,
gross_total = @grossTotal,
net_total = @netTotal,
total_quantity = @totalQuantity,
stock_positions = @stockPositions,
booked_positions = @bookedPositions,
unbooked_positions = @unbookedPositions,
stock_quantity = @stockQuantity,
booked_quantity = @bookedQuantity,
unbooked_quantity = @unbookedQuantity,
is_booked = @isBooked
WHEN NOT MATCHED THEN
INSERT (
company_id, manifest_id, delivery_number, external_number, delivery_date, created_at,
supplier_id, supplier_name, positions, gross_total, net_total, total_quantity,
stock_positions, booked_positions, unbooked_positions,
stock_quantity, booked_quantity, unbooked_quantity, is_booked
) VALUES (
@companyId, @manifestId, @deliveryNumber, @externalNumber, @deliveryDate, @createdAt,
@supplierId, @supplierName, @positions, @grossTotal, @netTotal, @totalQuantity,
@stockPositions, @bookedPositions, @unbookedPositions,
@stockQuantity, @bookedQuantity, @unbookedQuantity, @isBooked
);
`;
/**
* Recalculates and upserts a single delivery summary into `deliverysummary`.
*
* @param {object} runner - Database runner.
* @param {object} opts
* @param {number} opts.companyId
* @param {number} [opts.manifestId] - Delivery manifest id.
* @param {string} [opts.deliveryNumber] - Delivery number (if manifestId not given).
*/
export async function syncDeliverySummary(runner, { companyId, manifestId, deliveryNumber }) {
const isSqlite = runner.dialect === 'sqlite3';
let deliveryRow;
if (manifestId) {
const res = await runner.query(
isSqlite ? SQL_GET_DELIVERY_BY_ID_SQLITE : SQL_GET_DELIVERY_BY_ID_MSSQL,
{ manifestId, companyId },
);
deliveryRow = res.rows[0];
} else if (deliveryNumber) {
const res = await runner.query(
isSqlite ? SQL_GET_DELIVERY_BY_NUMBER_SQLITE : SQL_GET_DELIVERY_BY_NUMBER_MSSQL,
{ number: deliveryNumber, companyId },
);
deliveryRow = res.rows[0];
}
if (!deliveryRow) {
if (manifestId) {
await runner.query('DELETE FROM deliverysummary WHERE manifest_id = @manifestId', { manifestId });
}
return;
}
const deliveryId = deliveryRow.id;
const num = deliveryRow.number;
const totalsRes = await runner.query(
isSqlite ? SQL_DELIVERY_TOTALS_SQLITE : SQL_DELIVERY_TOTALS_MSSQL,
{ manifestId: deliveryId, companyId },
);
const totals = totalsRes.rows[0] || {};
let supplierName = null;
if (deliveryRow.entity_id) {
const supRes = await runner.query(
isSqlite ? SQL_SUPPLIER_NAME_SQLITE : SQL_SUPPLIER_NAME_MSSQL,
{ entityId: deliveryRow.entity_id },
);
supplierName = supRes.rows[0]?.supplier_name || null;
}
const positions = Number(totals.positions || 0);
const grossTotal = Math.round(Number(totals.gross_total || 0) * 100) / 100;
const netTotal = Math.round(Number(totals.net_total || 0) * 100) / 100;
const totalQuantity = Math.round(Number(totals.total_quantity || 0) * 1000) / 1000;
const stockPositions = Number(totals.stock_positions || 0);
const bookedPositions = Number(totals.booked_positions || 0);
const unbookedPositions = Math.max(0, stockPositions - bookedPositions);
const stockQuantity = Math.round(Number(totals.stock_quantity || 0) * 1000) / 1000;
const bookedQuantity = Math.round(Number(totals.booked_quantity || 0) * 1000) / 1000;
const unbookedQuantity = Math.round(Math.max(0, stockQuantity - bookedQuantity) * 1000) / 1000;
const isBooked = stockPositions > 0 && bookedPositions >= stockPositions ? 1 : 0;
await runner.query(isSqlite ? SQL_UPSERT_SUMMARY_SQLITE : SQL_UPSERT_SUMMARY_MSSQL, {
companyId,
manifestId: deliveryId,
deliveryNumber: num,
externalNumber: deliveryRow.external_number ?? null,
deliveryDate: deliveryRow.date,
createdAt: deliveryRow.created_at,
supplierId: deliveryRow.entity_id ?? null,
supplierName,
positions,
grossTotal,
netTotal,
totalQuantity,
stockPositions,
bookedPositions,
unbookedPositions,
stockQuantity,
bookedQuantity,
unbookedQuantity,
isBooked,
});
}
/**
* Full rebuild of all deliveries in `deliverysummary` for a given company.
*
* @param {object} runner
* @param {number} companyId
*/
export async function rebuildDeliverySummaries(runner, companyId) {
const isSqlite = runner.dialect === 'sqlite3';
if (isSqlite) {
const sqlRebuildSqlite = `
WITH
line_ledger AS (
SELECT
manifestarticle_id,
SUM(amount) AS booked_amount
FROM ledger
WHERE warehouse_id IS NULL
AND UPPER(transaction_type) IN ('ACCEPTANCE', 'DELIVERY', 'GOODS_RECEIPT')
GROUP BY manifestarticle_id
),
line_data AS (
SELECT
ma.manifest_id,
ma.id AS line_id,
ma.amount,
ma.amount * (ma.net * (1.0 - ma.rebate / 100.0)) * (1.0 + ma.taxrate / 100.0) AS line_gross,
ma.amount * (ma.net * (1.0 - ma.rebate / 100.0)) AS line_net,
CASE WHEN COALESCE(a.track_stock, 1) = 0 THEN 0 ELSE 1 END AS track_stock,
COALESCE(ll.booked_amount, 0) AS booked_amount
FROM manifestarticle ma
JOIN manifest m ON m.id = ma.manifest_id
LEFT 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))
LEFT JOIN line_ledger ll ON ll.manifestarticle_id = ma.id
WHERE m.company_id = @companyId AND m."type" = 'delivery'
),
delivery_agg AS (
SELECT
manifest_id,
COUNT(*) AS positions,
SUM(line_gross) AS gross_total,
SUM(line_net) AS net_total,
SUM(amount) AS total_quantity,
SUM(CASE WHEN track_stock = 1 THEN 1 ELSE 0 END) AS stock_positions,
SUM(CASE WHEN track_stock = 1 AND (CASE WHEN amount > 0 THEN booked_amount >= amount ELSE booked_amount > 0 END) THEN 1 ELSE 0 END) AS booked_positions,
SUM(CASE WHEN track_stock = 1 THEN amount ELSE 0 END) AS stock_quantity,
SUM(CASE WHEN track_stock = 1 THEN booked_amount ELSE 0 END) AS booked_quantity
FROM line_data
GROUP BY manifest_id
)
INSERT INTO deliverysummary (
company_id, manifest_id, delivery_number, external_number, delivery_date, created_at,
supplier_id, supplier_name, positions, gross_total, net_total, total_quantity,
stock_positions, booked_positions, unbooked_positions,
stock_quantity, booked_quantity, unbooked_quantity, is_booked
)
SELECT
m.company_id,
m.id AS manifest_id,
m."number" AS delivery_number,
m.external_number,
m."date" AS delivery_date,
m.created_at,
m.entity_id AS supplier_id,
TRIM(COALESCE(e.first_name || ' ', '') || COALESCE(e.name, '')) AS supplier_name,
COALESCE(da.positions, 0) AS positions,
ROUND(COALESCE(da.gross_total, 0), 2) AS gross_total,
ROUND(COALESCE(da.net_total, 0), 2) AS net_total,
ROUND(COALESCE(da.total_quantity, 0), 3) AS total_quantity,
COALESCE(da.stock_positions, 0) AS stock_positions,
COALESCE(da.booked_positions, 0) AS booked_positions,
MAX(0, COALESCE(da.stock_positions, 0) - COALESCE(da.booked_positions, 0)) AS unbooked_positions,
ROUND(COALESCE(da.stock_quantity, 0), 3) AS stock_quantity,
ROUND(COALESCE(da.booked_quantity, 0), 3) AS booked_quantity,
ROUND(MAX(0, COALESCE(da.stock_quantity, 0) - COALESCE(da.booked_quantity, 0)), 3) AS unbooked_quantity,
CASE WHEN COALESCE(da.stock_positions, 0) > 0 AND COALESCE(da.booked_positions, 0) >= COALESCE(da.stock_positions, 0) THEN 1 ELSE 0 END AS is_booked
FROM manifest m
LEFT JOIN entity e ON e.id = m.entity_id
LEFT JOIN delivery_agg da ON da.manifest_id = m.id
WHERE m.company_id = @companyId AND m."type" = 'delivery'
ON CONFLICT(manifest_id) DO UPDATE SET
delivery_number = excluded.delivery_number,
external_number = excluded.external_number,
delivery_date = excluded.delivery_date,
created_at = excluded.created_at,
supplier_id = excluded.supplier_id,
supplier_name = excluded.supplier_name,
positions = excluded.positions,
gross_total = excluded.gross_total,
net_total = excluded.net_total,
total_quantity = excluded.total_quantity,
stock_positions = excluded.stock_positions,
booked_positions = excluded.booked_positions,
unbooked_positions = excluded.unbooked_positions,
stock_quantity = excluded.stock_quantity,
booked_quantity = excluded.booked_quantity,
unbooked_quantity = excluded.unbooked_quantity,
is_booked = excluded.is_booked;
`;
await runner.query(sqlRebuildSqlite, { companyId });
} else {
await runner.query('DELETE FROM deliverysummary WHERE company_id = @companyId', { companyId });
const sqlRebuildMssql = `
WITH
line_ledger AS (
SELECT
manifestarticle_id,
SUM(amount) AS booked_amount
FROM ledger
WHERE warehouse_id IS NULL
AND UPPER(transaction_type) IN ('ACCEPTANCE', 'DELIVERY', 'GOODS_RECEIPT')
GROUP BY manifestarticle_id
),
line_data AS (
SELECT
ma.manifest_id,
ma.id AS line_id,
ma.amount,
ma.amount * (ma.net * (1.0 - ma.rebate / 100.0)) * (1.0 + ma.taxrate / 100.0) AS line_gross,
ma.amount * (ma.net * (1.0 - ma.rebate / 100.0)) AS line_net,
CASE WHEN COALESCE(a.track_stock, 1) = 0 THEN 0 ELSE 1 END AS track_stock,
COALESCE(ll.booked_amount, 0) AS booked_amount
FROM manifestarticle ma
JOIN manifest m ON m.id = ma.manifest_id
LEFT 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))
LEFT JOIN line_ledger ll ON ll.manifestarticle_id = ma.id
WHERE m.company_id = @companyId AND m.[type] = 'delivery'
),
delivery_agg AS (
SELECT
manifest_id,
COUNT(*) AS positions,
SUM(line_gross) AS gross_total,
SUM(line_net) AS net_total,
SUM(amount) AS total_quantity,
SUM(CASE WHEN track_stock = 1 THEN 1 ELSE 0 END) AS stock_positions,
SUM(CASE WHEN track_stock = 1 AND (CASE WHEN amount > 0 THEN booked_amount >= amount ELSE booked_amount > 0 END) THEN 1 ELSE 0 END) AS booked_positions,
SUM(CASE WHEN track_stock = 1 THEN amount ELSE 0 END) AS stock_quantity,
SUM(CASE WHEN track_stock = 1 THEN booked_amount ELSE 0 END) AS booked_quantity
FROM line_data
GROUP BY manifest_id
)
INSERT INTO deliverysummary (
company_id, manifest_id, delivery_number, external_number, delivery_date, created_at,
supplier_id, supplier_name, positions, gross_total, net_total, total_quantity,
stock_positions, booked_positions, unbooked_positions,
stock_quantity, booked_quantity, unbooked_quantity, is_booked
)
SELECT
m.company_id,
m.id AS manifest_id,
m.[number] AS delivery_number,
m.external_number,
m.[date] AS delivery_date,
m.created_at,
m.entity_id AS supplier_id,
LTRIM(RTRIM(ISNULL(e.first_name + ' ', '') + ISNULL(e.name, ''))) AS supplier_name,
COALESCE(da.positions, 0) AS positions,
ROUND(COALESCE(da.gross_total, 0), 2) AS gross_total,
ROUND(COALESCE(da.net_total, 0), 2) AS net_total,
ROUND(COALESCE(da.total_quantity, 0), 3) AS total_quantity,
COALESCE(da.stock_positions, 0) AS stock_positions,
COALESCE(da.booked_positions, 0) AS booked_positions,
CASE WHEN COALESCE(da.stock_positions, 0) > COALESCE(da.booked_positions, 0) THEN COALESCE(da.stock_positions, 0) - COALESCE(da.booked_positions, 0) ELSE 0 END AS unbooked_positions,
ROUND(COALESCE(da.stock_quantity, 0), 3) AS stock_quantity,
ROUND(COALESCE(da.booked_quantity, 0), 3) AS booked_quantity,
ROUND(CASE WHEN COALESCE(da.stock_quantity, 0) > COALESCE(da.booked_quantity, 0) THEN COALESCE(da.stock_quantity, 0) - COALESCE(da.booked_quantity, 0) ELSE 0 END, 3) AS unbooked_quantity,
CASE WHEN COALESCE(da.stock_positions, 0) > 0 AND COALESCE(da.booked_positions, 0) >= COALESCE(da.stock_positions, 0) THEN 1 ELSE 0 END AS is_booked
FROM manifest m
LEFT JOIN entity e ON e.id = m.entity_id
LEFT JOIN delivery_agg da ON da.manifest_id = m.id
WHERE m.company_id = @companyId AND m.[type] = 'delivery';
`;
await runner.query(sqlRebuildMssql, { companyId });
}
}