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