From bd5bcdd5485aaf03f3412ed884a9707436e41e4d Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 1 Feb 2025 23:38:13 -0500 Subject: [PATCH] Fix calculate errors --- inventory-server/scripts/calculate-metrics.js | 14 ++-- .../scripts/metrics/category-metrics.js | 49 ++++++++---- .../scripts/metrics/financial-metrics.js | 3 +- .../scripts/metrics/product-metrics.js | 18 +++-- .../scripts/metrics/time-aggregates.js | 80 +++++-------------- .../scripts/metrics/vendor-metrics.js | 20 ++++- 6 files changed, 92 insertions(+), 92 deletions(-) diff --git a/inventory-server/scripts/calculate-metrics.js b/inventory-server/scripts/calculate-metrics.js index 20e37e9..4ba8317 100644 --- a/inventory-server/scripts/calculate-metrics.js +++ b/inventory-server/scripts/calculate-metrics.js @@ -7,13 +7,13 @@ require('dotenv').config({ path: path.resolve(__dirname, '..', '.env') }); // Configuration flags for controlling which metrics to calculate // Set to 1 to skip the corresponding calculation, 0 to run it -const SKIP_PRODUCT_METRICS = 1; // Skip all product metrics -const SKIP_TIME_AGGREGATES = 1; // Skip time aggregates -const SKIP_FINANCIAL_METRICS = 1; // Skip financial metrics -const SKIP_VENDOR_METRICS = 1; // Skip vendor metrics -const SKIP_CATEGORY_METRICS = 1; // Skip category metrics -const SKIP_BRAND_METRICS = 1; // Skip brand metrics -const SKIP_SALES_FORECASTS = 1; // Skip sales forecasts +const SKIP_PRODUCT_METRICS = 1; +const SKIP_TIME_AGGREGATES = 1; +const SKIP_FINANCIAL_METRICS = 0; +const SKIP_VENDOR_METRICS = 0; +const SKIP_CATEGORY_METRICS = 0; +const SKIP_BRAND_METRICS = 0; +const SKIP_SALES_FORECASTS = 0; // Add error handler for uncaught exceptions process.on('uncaughtException', (error) => { diff --git a/inventory-server/scripts/metrics/category-metrics.js b/inventory-server/scripts/metrics/category-metrics.js index 912d77a..1e2393b 100644 --- a/inventory-server/scripts/metrics/category-metrics.js +++ b/inventory-server/scripts/metrics/category-metrics.js @@ -151,7 +151,7 @@ async function calculateCategoryMetrics(startTime, totalProducts, processedCount WITH current_period AS ( SELECT pc.cat_id, - SUM(o.quantity * o.price) / (1 + COALESCE(ss.seasonality_factor, 0)) as revenue + SUM(o.quantity * o.price / (1 + COALESCE(ss.seasonality_factor, 0))) as revenue FROM product_categories pc JOIN products p ON pc.pid = p.pid JOIN orders o ON p.pid = o.pid @@ -163,7 +163,7 @@ async function calculateCategoryMetrics(startTime, totalProducts, processedCount previous_period AS ( SELECT pc.cat_id, - SUM(o.quantity * o.price) / (1 + COALESCE(ss.seasonality_factor, 0)) as revenue + SUM(o.quantity * o.price / (1 + COALESCE(ss.seasonality_factor, 0))) as revenue FROM product_categories pc JOIN products p ON pc.pid = p.pid JOIN orders o ON p.pid = o.pid @@ -177,7 +177,7 @@ async function calculateCategoryMetrics(startTime, totalProducts, processedCount SELECT pc.cat_id, MONTH(o.date) as month, - SUM(o.quantity * o.price) / (1 + COALESCE(ss.seasonality_factor, 0)) as revenue, + SUM(o.quantity * o.price / (1 + COALESCE(ss.seasonality_factor, 0))) as revenue, COUNT(DISTINCT DATE(o.date)) as days_in_month FROM product_categories pc JOIN products p ON pc.pid = p.pid @@ -187,14 +187,24 @@ async function calculateCategoryMetrics(startTime, totalProducts, processedCount AND o.date >= DATE_SUB(CURRENT_DATE, INTERVAL 15 MONTH) GROUP BY pc.cat_id, MONTH(o.date) ), - trend_analysis AS ( + trend_stats AS ( SELECT cat_id, - REGR_SLOPE(revenue / days_in_month, MONTH) as trend_slope, - AVG(revenue / days_in_month) as avg_daily_revenue + COUNT(*) as n, + AVG(month) as avg_x, + AVG(revenue / days_in_month) as avg_y, + SUM(month * (revenue / days_in_month)) as sum_xy, + SUM(month * month) as sum_xx FROM trend_data GROUP BY cat_id HAVING COUNT(*) >= 6 + ), + trend_analysis AS ( + SELECT + cat_id, + ((n * sum_xy) - (avg_x * n * avg_y)) / ((n * sum_xx) - (n * avg_x * avg_x)) as trend_slope, + avg_y as avg_daily_revenue + FROM trend_stats ) UPDATE category_metrics cm LEFT JOIN current_period cp ON cm.category_id = cp.cat_id @@ -265,13 +275,23 @@ async function calculateCategoryMetrics(startTime, totalProducts, processedCount COUNT(DISTINCT CASE WHEN p.visible = true THEN p.pid END) as active_products, SUM(p.stock_quantity * p.cost_price) as total_value, SUM(o.quantity * o.price) as total_revenue, + CASE + WHEN SUM(o.quantity * o.price) > 0 THEN + LEAST( + GREATEST( + SUM(o.quantity * (o.price - GREATEST(p.cost_price, 0))) * 100.0 / + SUM(o.quantity * o.price), + -100 + ), + 100 + ) + ELSE 0 + END as avg_margin, COALESCE( - SUM(o.quantity * (o.price - p.cost_price)) * 100.0 / - NULLIF(SUM(o.quantity * o.price), 0), - 0 - ) as avg_margin, - COALESCE( - SUM(o.quantity) / NULLIF(AVG(GREATEST(p.stock_quantity, 0)), 0), + LEAST( + SUM(o.quantity) / NULLIF(AVG(GREATEST(p.stock_quantity, 0)), 0), + 999.99 + ), 0 ) as turnover_rate FROM product_categories pc @@ -286,8 +306,7 @@ async function calculateCategoryMetrics(startTime, totalProducts, processedCount total_value = VALUES(total_value), total_revenue = VALUES(total_revenue), avg_margin = VALUES(avg_margin), - turnover_rate = VALUES(turnover_rate), - last_calculated_at = CURRENT_TIMESTAMP + turnover_rate = VALUES(turnover_rate) `); processedCount = Math.floor(totalProducts * 0.99); @@ -342,7 +361,7 @@ async function calculateCategoryMetrics(startTime, totalProducts, processedCount sales_data AS ( SELECT pc.cat_id, - p.brand, + COALESCE(p.brand, 'Unknown') as brand, dr.period_start, dr.period_end, COUNT(DISTINCT p.pid) as num_products, diff --git a/inventory-server/scripts/metrics/financial-metrics.js b/inventory-server/scripts/metrics/financial-metrics.js index acb03a0..79faac2 100644 --- a/inventory-server/scripts/metrics/financial-metrics.js +++ b/inventory-server/scripts/metrics/financial-metrics.js @@ -119,8 +119,7 @@ async function calculateFinancialMetrics(startTime, totalProducts, processedCoun WHEN COALESCE(mf.inventory_value, 0) > 0 AND mf.active_days > 0 THEN (COALESCE(mf.gross_profit, 0) * (365.0 / mf.active_days)) / COALESCE(mf.inventory_value, 0) ELSE 0 - END, - pta.last_calculated_at = CURRENT_TIMESTAMP + END `); processedCount = Math.floor(totalProducts * 0.70); diff --git a/inventory-server/scripts/metrics/product-metrics.js b/inventory-server/scripts/metrics/product-metrics.js index 2b174f6..9502c73 100644 --- a/inventory-server/scripts/metrics/product-metrics.js +++ b/inventory-server/scripts/metrics/product-metrics.js @@ -35,6 +35,13 @@ async function calculateProductMetrics(startTime, totalProducts, processedCount return processedCount; } + // First ensure all products have a metrics record + await connection.query(` + INSERT IGNORE INTO product_metrics (pid, last_calculated_at) + SELECT pid, NOW() + FROM products + `); + // Calculate base product metrics if (!SKIP_PRODUCT_BASE_METRICS) { outputProgress({ @@ -59,6 +66,8 @@ async function calculateProductMetrics(startTime, totalProducts, processedCount JOIN ( SELECT p.pid, + p.stock_quantity, + p.cost_price, p.cost_price * p.stock_quantity as inventory_value, SUM(o.quantity) as total_quantity, COUNT(DISTINCT o.order_number) as number_of_orders, @@ -71,7 +80,7 @@ async function calculateProductMetrics(startTime, totalProducts, processedCount COUNT(DISTINCT DATE(o.date)) as active_days FROM products p LEFT JOIN orders o ON p.pid = o.pid AND o.canceled = false - GROUP BY p.pid + GROUP BY p.pid, p.stock_quantity, p.cost_price ) stats ON pm.pid = stats.pid SET pm.inventory_value = COALESCE(stats.inventory_value, 0), @@ -89,12 +98,12 @@ async function calculateProductMetrics(startTime, totalProducts, processedCount pm.last_sale_date = stats.last_sale_date, pm.days_of_inventory = CASE WHEN COALESCE(stats.total_quantity / NULLIF(stats.active_days, 0), 0) > 0 - THEN FLOOR(p.stock_quantity / (stats.total_quantity / stats.active_days)) + THEN FLOOR(stats.stock_quantity / (stats.total_quantity / stats.active_days)) ELSE NULL END, pm.weeks_of_inventory = CASE WHEN COALESCE(stats.total_quantity / NULLIF(stats.active_days, 0), 0) > 0 - THEN FLOOR(p.stock_quantity / (stats.total_quantity / stats.active_days) / 7) + THEN FLOOR(stats.stock_quantity / (stats.total_quantity / stats.active_days) / 7) ELSE NULL END, pm.gmroi = CASE @@ -239,8 +248,7 @@ async function calculateProductMetrics(startTime, totalProducts, processedCount avg_price = VALUES(avg_price), profit_margin = VALUES(profit_margin), inventory_value = VALUES(inventory_value), - gmroi = VALUES(gmroi), - last_calculated_at = CURRENT_TIMESTAMP + gmroi = VALUES(gmroi) `); processedCount = Math.floor(totalProducts * 0.6); diff --git a/inventory-server/scripts/metrics/time-aggregates.js b/inventory-server/scripts/metrics/time-aggregates.js index 26806e9..6f3675e 100644 --- a/inventory-server/scripts/metrics/time-aggregates.js +++ b/inventory-server/scripts/metrics/time-aggregates.js @@ -52,7 +52,9 @@ async function calculateTimeAggregates(startTime, totalProducts, processedCount, stock_received, stock_ordered, avg_price, - profit_margin + profit_margin, + inventory_value, + gmroi ) WITH sales_data AS ( SELECT @@ -69,11 +71,12 @@ async function calculateTimeAggregates(startTime, totalProducts, processedCount, ELSE ((SUM((o.price - COALESCE(o.discount, 0)) * o.quantity) - SUM(COALESCE(p.cost_price, 0) * o.quantity)) / SUM((o.price - COALESCE(o.discount, 0)) * o.quantity)) * 100 - END as profit_margin + END as profit_margin, + p.cost_price * p.stock_quantity as inventory_value FROM orders o JOIN products p ON o.pid = p.pid WHERE o.canceled = 0 - GROUP BY o.pid, YEAR(o.date), MONTH(o.date) + GROUP BY o.pid, YEAR(o.date), MONTH(o.date), p.cost_price, p.stock_quantity ), purchase_data AS ( SELECT @@ -81,40 +84,9 @@ async function calculateTimeAggregates(startTime, totalProducts, processedCount, YEAR(date) as year, MONTH(date) as month, SUM(received) as stock_received, - SUM(ordered) as stock_ordered, - COUNT(DISTINCT CASE WHEN receiving_status = 40 THEN id END) as fulfilled_orders, - COUNT(DISTINCT id) as total_orders, - AVG(CASE - WHEN receiving_status = 40 - THEN DATEDIFF(received_date, date) - END) as avg_lead_time, - SUM(CASE - WHEN receiving_status = 40 AND received_date > expected_date - THEN 1 ELSE 0 - END) as late_deliveries + SUM(ordered) as stock_ordered FROM purchase_orders GROUP BY pid, YEAR(date), MONTH(date) - ), - stock_trends AS ( - SELECT - p.pid, - YEAR(po.date) as year, - MONTH(po.date) as month, - AVG(p.stock_quantity) as avg_stock_level, - STDDEV(p.stock_quantity) as stock_volatility, - SUM(CASE - WHEN p.stock_quantity <= COALESCE(pm.reorder_point, 5) - THEN 1 ELSE 0 - END) as days_below_reorder, - COUNT(*) as total_days - FROM products p - CROSS JOIN ( - SELECT DISTINCT DATE(date) as date - FROM purchase_orders - WHERE date >= DATE_SUB(CURRENT_DATE, INTERVAL 12 MONTH) - ) po - LEFT JOIN product_metrics pm ON p.pid = pm.pid - GROUP BY p.pid, YEAR(po.date), MONTH(po.date) ) SELECT s.pid, @@ -128,23 +100,17 @@ async function calculateTimeAggregates(startTime, totalProducts, processedCount, COALESCE(p.stock_ordered, 0) as stock_ordered, s.avg_price, s.profit_margin, - COALESCE(p.fulfilled_orders, 0) as fulfilled_orders, - COALESCE(p.total_orders, 0) as total_orders, - COALESCE(p.avg_lead_time, 0) as avg_lead_time, - COALESCE(p.late_deliveries, 0) as late_deliveries, - COALESCE(st.avg_stock_level, 0) as avg_stock_level, - COALESCE(st.stock_volatility, 0) as stock_volatility, - COALESCE(st.days_below_reorder, 0) as days_below_reorder, - COALESCE(st.total_days, 0) as total_days + s.inventory_value, + CASE + WHEN s.inventory_value > 0 THEN + (s.total_revenue - s.total_cost) / s.inventory_value + ELSE 0 + END as gmroi FROM sales_data s LEFT JOIN purchase_data p ON s.pid = p.pid AND s.year = p.year AND s.month = p.month - LEFT JOIN stock_trends st - ON s.pid = st.pid - AND s.year = st.year - AND s.month = st.month UNION SELECT p.pid, @@ -158,23 +124,13 @@ async function calculateTimeAggregates(startTime, totalProducts, processedCount, p.stock_ordered, 0 as avg_price, 0 as profit_margin, - p.fulfilled_orders, - p.total_orders, - p.avg_lead_time, - p.late_deliveries, - st.avg_stock_level, - st.stock_volatility, - st.days_below_reorder, - st.total_days + (SELECT cost_price * stock_quantity FROM products WHERE pid = p.pid) as inventory_value, + 0 as gmroi FROM purchase_data p LEFT JOIN sales_data s ON p.pid = s.pid AND p.year = s.year AND p.month = s.month - LEFT JOIN stock_trends st - ON p.pid = st.pid - AND p.year = st.year - AND p.month = st.month WHERE s.pid IS NULL ON DUPLICATE KEY UPDATE total_quantity_sold = VALUES(total_quantity_sold), @@ -185,7 +141,8 @@ async function calculateTimeAggregates(startTime, totalProducts, processedCount, stock_ordered = VALUES(stock_ordered), avg_price = VALUES(avg_price), profit_margin = VALUES(profit_margin), - last_calculated_at = CURRENT_TIMESTAMP + inventory_value = VALUES(inventory_value), + gmroi = VALUES(gmroi) `); processedCount = Math.floor(totalProducts * 0.60); @@ -231,8 +188,7 @@ async function calculateTimeAggregates(startTime, totalProducts, processedCount, WHEN COALESCE(fin.inventory_value, 0) > 0 AND fin.days_in_period > 0 THEN (COALESCE(fin.gross_profit, 0) * (365.0 / fin.days_in_period)) / COALESCE(fin.inventory_value, 0) ELSE 0 - END, - pta.last_calculated_at = CURRENT_TIMESTAMP + END `); processedCount = Math.floor(totalProducts * 0.65); diff --git a/inventory-server/scripts/metrics/vendor-metrics.js b/inventory-server/scripts/metrics/vendor-metrics.js index 396021e..a988581 100644 --- a/inventory-server/scripts/metrics/vendor-metrics.js +++ b/inventory-server/scripts/metrics/vendor-metrics.js @@ -218,6 +218,7 @@ async function calculateVendorMetrics(startTime, totalProducts, processedCount, JOIN orders o ON p.pid = o.pid WHERE o.canceled = false AND o.date >= DATE_SUB(CURRENT_DATE, INTERVAL 12 MONTH) + AND p.vendor IS NOT NULL GROUP BY p.vendor, YEAR(o.date), MONTH(o.date) ), monthly_po AS ( @@ -238,6 +239,7 @@ async function calculateVendorMetrics(startTime, totalProducts, processedCount, FROM products p JOIN purchase_orders po ON p.pid = po.pid WHERE po.date >= DATE_SUB(CURRENT_DATE, INTERVAL 12 MONTH) + AND p.vendor IS NOT NULL GROUP BY p.vendor, YEAR(po.date), MONTH(po.date) ) SELECT @@ -248,7 +250,7 @@ async function calculateVendorMetrics(startTime, totalProducts, processedCount, COALESCE(mp.late_orders, 0) as late_orders, COALESCE(mp.avg_lead_time_days, 0) as avg_lead_time_days, COALESCE(mp.total_purchase_value, 0) as total_purchase_value, - COALESCE(mo.total_revenue, 0) as total_revenue, + mo.total_revenue, CASE WHEN mo.total_revenue > 0 THEN (mo.total_margin / mo.total_revenue) * 100 @@ -258,6 +260,22 @@ async function calculateVendorMetrics(startTime, totalProducts, processedCount, LEFT JOIN monthly_po mp ON mo.vendor = mp.vendor AND mo.year = mp.year AND mo.month = mp.month + UNION + SELECT + mp.vendor, + mp.year, + mp.month, + mp.total_po as total_orders, + mp.late_orders, + mp.avg_lead_time_days, + mp.total_purchase_value, + 0 as total_revenue, + 0 as avg_margin_percent + FROM monthly_po mp + LEFT JOIN monthly_orders mo ON mp.vendor = mo.vendor + AND mp.year = mo.year + AND mp.month = mo.month + WHERE mo.vendor IS NULL ON DUPLICATE KEY UPDATE total_orders = VALUES(total_orders), late_orders = VALUES(late_orders),