PO-related fixes
This commit is contained in:
@@ -111,25 +111,35 @@ router.get('/purchase/metrics', async (req, res) => {
|
||||
SELECT
|
||||
COALESCE(COUNT(DISTINCT CASE
|
||||
WHEN po.receiving_status NOT IN ('partial_received', 'full_received', 'paid')
|
||||
AND po.date >= CURRENT_DATE - INTERVAL '6 months'
|
||||
AND NOT (po.date < CURRENT_DATE - INTERVAL '1 month' AND po.received >= po.ordered * 0.9)
|
||||
THEN po.po_id
|
||||
END), 0)::integer as active_pos,
|
||||
COALESCE(COUNT(DISTINCT CASE
|
||||
WHEN po.receiving_status NOT IN ('partial_received', 'full_received', 'paid')
|
||||
AND po.date >= CURRENT_DATE - INTERVAL '6 months'
|
||||
AND NOT (po.date < CURRENT_DATE - INTERVAL '1 month' AND po.received >= po.ordered * 0.9)
|
||||
AND po.expected_date < CURRENT_DATE
|
||||
THEN po.po_id
|
||||
END), 0)::integer as overdue_pos,
|
||||
COALESCE(SUM(CASE
|
||||
WHEN po.receiving_status NOT IN ('partial_received', 'full_received', 'paid')
|
||||
AND po.date >= CURRENT_DATE - INTERVAL '6 months'
|
||||
AND NOT (po.date < CURRENT_DATE - INTERVAL '1 month' AND po.received >= po.ordered * 0.9)
|
||||
THEN po.ordered
|
||||
ELSE 0
|
||||
END), 0)::integer as total_units,
|
||||
ROUND(COALESCE(SUM(CASE
|
||||
WHEN po.receiving_status NOT IN ('partial_received', 'full_received', 'paid')
|
||||
AND po.date >= CURRENT_DATE - INTERVAL '6 months'
|
||||
AND NOT (po.date < CURRENT_DATE - INTERVAL '1 month' AND po.received >= po.ordered * 0.9)
|
||||
THEN po.ordered * po.cost_price
|
||||
ELSE 0
|
||||
END), 0)::numeric, 3) as total_cost,
|
||||
ROUND(COALESCE(SUM(CASE
|
||||
WHEN po.receiving_status NOT IN ('partial_received', 'full_received', 'paid')
|
||||
AND po.date >= CURRENT_DATE - INTERVAL '6 months'
|
||||
AND NOT (po.date < CURRENT_DATE - INTERVAL '1 month' AND po.received >= po.ordered * 0.9)
|
||||
THEN po.ordered * pm.current_price
|
||||
ELSE 0
|
||||
END), 0)::numeric, 3) as total_retail
|
||||
@@ -147,6 +157,8 @@ router.get('/purchase/metrics', async (req, res) => {
|
||||
FROM purchase_orders po
|
||||
JOIN product_metrics pm ON po.pid = pm.pid
|
||||
WHERE po.receiving_status NOT IN ('partial_received', 'full_received', 'paid')
|
||||
AND po.date >= CURRENT_DATE - INTERVAL '6 months'
|
||||
AND NOT (po.date < CURRENT_DATE - INTERVAL '1 month' AND po.received >= po.ordered * 0.9)
|
||||
GROUP BY po.vendor
|
||||
HAVING ROUND(COALESCE(SUM(po.ordered * po.cost_price), 0)::numeric, 3) > 0
|
||||
ORDER BY cost DESC
|
||||
|
||||
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// Status code constants
|
||||
// Frontend uses these numeric codes but database uses strings
|
||||
const STATUS = {
|
||||
CANCELED: 0,
|
||||
CREATED: 1,
|
||||
@@ -13,6 +14,18 @@ const STATUS = {
|
||||
DONE: 50
|
||||
};
|
||||
|
||||
// Status mapping from database string values to frontend numeric codes
|
||||
const STATUS_MAPPING = {
|
||||
'canceled': STATUS.CANCELED,
|
||||
'created': STATUS.CREATED,
|
||||
'electronically_ready_send': STATUS.ELECTRONICALLY_READY_SEND,
|
||||
'ordered': STATUS.ORDERED,
|
||||
'preordered': STATUS.PREORDERED,
|
||||
'electronically_sent': STATUS.ELECTRONICALLY_SENT,
|
||||
'receiving_started': STATUS.RECEIVING_STARTED,
|
||||
'done': STATUS.DONE
|
||||
};
|
||||
|
||||
const RECEIVING_STATUS = {
|
||||
CANCELED: 0,
|
||||
CREATED: 1,
|
||||
@@ -21,6 +34,26 @@ const RECEIVING_STATUS = {
|
||||
PAID: 50
|
||||
};
|
||||
|
||||
// Receiving status mapping from database string values to frontend numeric codes
|
||||
const RECEIVING_STATUS_MAPPING = {
|
||||
'canceled': RECEIVING_STATUS.CANCELED,
|
||||
'created': RECEIVING_STATUS.CREATED,
|
||||
'partial_received': RECEIVING_STATUS.PARTIAL_RECEIVED,
|
||||
'full_received': RECEIVING_STATUS.FULL_RECEIVED,
|
||||
'paid': RECEIVING_STATUS.PAID
|
||||
};
|
||||
|
||||
// Helper for SQL status value comparison with string values in DB
|
||||
function getStatusWhereClause(statusNum) {
|
||||
const dbStatuses = Object.keys(STATUS_MAPPING).filter(key =>
|
||||
STATUS_MAPPING[key] === parseInt(statusNum));
|
||||
|
||||
if (dbStatuses.length > 0) {
|
||||
return `po.status = '${dbStatuses[0]}'`;
|
||||
}
|
||||
return `1=0`; // No match found, return false condition
|
||||
}
|
||||
|
||||
// Get all purchase orders with summary metrics
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
@@ -38,9 +71,7 @@ router.get('/', async (req, res) => {
|
||||
}
|
||||
|
||||
if (status && status !== 'all') {
|
||||
whereClause += ` AND po.status = $${paramCounter}`;
|
||||
params.push(Number(status));
|
||||
paramCounter++;
|
||||
whereClause += ` AND ${getStatusWhereClause(status)}`;
|
||||
}
|
||||
|
||||
if (vendor && vendor !== 'all') {
|
||||
@@ -139,17 +170,17 @@ router.get('/', async (req, res) => {
|
||||
GROUP BY po_id, vendor, date, status, receiving_status
|
||||
)
|
||||
SELECT
|
||||
po_id as id,
|
||||
vendor as vendor_name,
|
||||
to_char(date, 'YYYY-MM-DD') as order_date,
|
||||
status,
|
||||
receiving_status,
|
||||
total_items,
|
||||
total_quantity,
|
||||
total_cost,
|
||||
total_received,
|
||||
fulfillment_rate
|
||||
FROM po_totals
|
||||
pt.po_id as id,
|
||||
pt.vendor as vendor_name,
|
||||
to_char(pt.date, 'YYYY-MM-DD') as order_date,
|
||||
pt.status,
|
||||
pt.receiving_status,
|
||||
pt.total_items,
|
||||
pt.total_quantity,
|
||||
pt.total_cost,
|
||||
pt.total_received,
|
||||
pt.fulfillment_rate
|
||||
FROM po_totals pt
|
||||
ORDER BY ${orderByClause}
|
||||
LIMIT $${paramCounter} OFFSET $${paramCounter + 1}
|
||||
`, [...params, Number(limit), offset]);
|
||||
@@ -170,13 +201,36 @@ router.get('/', async (req, res) => {
|
||||
ORDER BY status
|
||||
`);
|
||||
|
||||
// Parse numeric values
|
||||
// Get product vendors for orders with Unknown Vendor
|
||||
const poIds = orders.filter(o => o.vendor_name === 'Unknown Vendor').map(o => o.id);
|
||||
let vendorMappings = {};
|
||||
|
||||
if (poIds.length > 0) {
|
||||
const { rows: productVendors } = await pool.query(`
|
||||
SELECT DISTINCT po.po_id, p.vendor
|
||||
FROM purchase_orders po
|
||||
JOIN products p ON po.pid = p.pid
|
||||
WHERE po.po_id = ANY($1)
|
||||
AND p.vendor IS NOT NULL AND p.vendor != ''
|
||||
GROUP BY po.po_id, p.vendor
|
||||
`, [poIds]);
|
||||
|
||||
// Create mapping of PO ID to actual vendor from products table
|
||||
vendorMappings = productVendors.reduce((acc, pv) => {
|
||||
if (!acc[pv.po_id]) {
|
||||
acc[pv.po_id] = pv.vendor;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Parse numeric values and map status strings to numeric codes
|
||||
const parsedOrders = orders.map(order => ({
|
||||
id: order.id,
|
||||
vendor_name: order.vendor_name,
|
||||
vendor_name: vendorMappings[order.id] || order.vendor_name,
|
||||
order_date: order.order_date,
|
||||
status: Number(order.status),
|
||||
receiving_status: Number(order.receiving_status),
|
||||
status: STATUS_MAPPING[order.status] || 0, // Map string status to numeric code
|
||||
receiving_status: RECEIVING_STATUS_MAPPING[order.receiving_status] || 0, // Map string receiving status to numeric code
|
||||
total_items: Number(order.total_items) || 0,
|
||||
total_quantity: Number(order.total_quantity) || 0,
|
||||
total_cost: Number(order.total_cost) || 0,
|
||||
@@ -205,7 +259,7 @@ router.get('/', async (req, res) => {
|
||||
},
|
||||
filters: {
|
||||
vendors: vendors.map(v => v.vendor),
|
||||
statuses: statuses.map(s => Number(s.status))
|
||||
statuses: statuses.map(s => STATUS_MAPPING[s.status] || 0) // Map string statuses to numeric codes for the frontend
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -228,14 +282,15 @@ router.get('/vendor-metrics', async (req, res) => {
|
||||
received,
|
||||
cost_price,
|
||||
CASE
|
||||
WHEN status >= ${STATUS.RECEIVING_STARTED} AND receiving_status >= ${RECEIVING_STATUS.PARTIAL_RECEIVED}
|
||||
WHEN status IN ('receiving_started', 'done')
|
||||
AND receiving_status IN ('partial_received', 'full_received', 'paid')
|
||||
AND received_date IS NOT NULL AND date IS NOT NULL
|
||||
THEN (received_date - date)::integer
|
||||
ELSE NULL
|
||||
END as delivery_days
|
||||
FROM purchase_orders
|
||||
WHERE vendor IS NOT NULL AND vendor != ''
|
||||
AND status != ${STATUS.CANCELED} -- Exclude canceled orders
|
||||
AND status != 'canceled' -- Exclude canceled orders
|
||||
)
|
||||
SELECT
|
||||
vendor as vendor_name,
|
||||
@@ -296,7 +351,7 @@ router.get('/cost-analysis', async (req, res) => {
|
||||
FROM purchase_orders po
|
||||
JOIN product_categories pc ON po.pid = pc.pid
|
||||
JOIN categories c ON pc.cat_id = c.cat_id
|
||||
WHERE po.status != ${STATUS.CANCELED} -- Exclude canceled orders
|
||||
WHERE po.status != 'canceled' -- Exclude canceled orders
|
||||
)
|
||||
SELECT
|
||||
category,
|
||||
@@ -311,7 +366,7 @@ router.get('/cost-analysis', async (req, res) => {
|
||||
ORDER BY total_spend DESC
|
||||
`);
|
||||
|
||||
// Parse numeric values
|
||||
// Parse numeric values and include ALL data for each category
|
||||
const parsedAnalysis = {
|
||||
unique_products: 0,
|
||||
avg_cost: 0,
|
||||
@@ -320,6 +375,11 @@ router.get('/cost-analysis', async (req, res) => {
|
||||
cost_variance: 0,
|
||||
total_spend_by_category: analysis.map(cat => ({
|
||||
category: cat.category,
|
||||
unique_products: Number(cat.unique_products) || 0,
|
||||
avg_cost: Number(cat.avg_cost) || 0,
|
||||
min_cost: Number(cat.min_cost) || 0,
|
||||
max_cost: Number(cat.max_cost) || 0,
|
||||
cost_variance: Number(cat.cost_variance) || 0,
|
||||
total_spend: Number(cat.total_spend) || 0
|
||||
}))
|
||||
};
|
||||
@@ -366,7 +426,7 @@ router.get('/receiving-status', async (req, res) => {
|
||||
SUM(received) as total_received,
|
||||
ROUND(SUM(ordered * cost_price)::numeric, 3) as total_cost
|
||||
FROM purchase_orders
|
||||
WHERE status != ${STATUS.CANCELED}
|
||||
WHERE status != 'canceled'
|
||||
GROUP BY po_id, status, receiving_status
|
||||
)
|
||||
SELECT
|
||||
@@ -379,16 +439,16 @@ router.get('/receiving-status', async (req, res) => {
|
||||
ROUND(SUM(total_cost)::numeric, 3) as total_value,
|
||||
ROUND(AVG(total_cost)::numeric, 3) as avg_cost,
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN receiving_status = ${RECEIVING_STATUS.CREATED} THEN po_id
|
||||
WHEN receiving_status = 'created' THEN po_id
|
||||
END) as pending_count,
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN receiving_status = ${RECEIVING_STATUS.PARTIAL_RECEIVED} THEN po_id
|
||||
WHEN receiving_status = 'partial_received' THEN po_id
|
||||
END) as partial_count,
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN receiving_status >= ${RECEIVING_STATUS.FULL_RECEIVED} THEN po_id
|
||||
WHEN receiving_status IN ('full_received', 'paid') THEN po_id
|
||||
END) as completed_count,
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN receiving_status = ${RECEIVING_STATUS.CANCELED} THEN po_id
|
||||
WHEN receiving_status = 'canceled' THEN po_id
|
||||
END) as canceled_count
|
||||
FROM po_totals
|
||||
`);
|
||||
@@ -423,7 +483,7 @@ router.get('/order-vs-received', async (req, res) => {
|
||||
|
||||
const { rows: quantities } = await pool.query(`
|
||||
SELECT
|
||||
p.product_id,
|
||||
p.pid as product_id,
|
||||
p.title as product,
|
||||
p.SKU as sku,
|
||||
SUM(po.ordered) as ordered_quantity,
|
||||
@@ -433,9 +493,9 @@ router.get('/order-vs-received', async (req, res) => {
|
||||
) as fulfillment_rate,
|
||||
COUNT(DISTINCT po.po_id) as order_count
|
||||
FROM products p
|
||||
JOIN purchase_orders po ON p.product_id = po.product_id
|
||||
JOIN purchase_orders po ON p.pid = po.pid
|
||||
WHERE po.date >= (CURRENT_DATE - INTERVAL '90 days')
|
||||
GROUP BY p.product_id, p.title, p.SKU
|
||||
GROUP BY p.pid, p.title, p.SKU
|
||||
HAVING COUNT(DISTINCT po.po_id) > 0
|
||||
ORDER BY SUM(po.ordered) DESC
|
||||
LIMIT 20
|
||||
@@ -445,10 +505,10 @@ router.get('/order-vs-received', async (req, res) => {
|
||||
const parsedQuantities = quantities.map(q => ({
|
||||
id: q.product_id,
|
||||
...q,
|
||||
ordered_quantity: Number(q.ordered_quantity),
|
||||
received_quantity: Number(q.received_quantity),
|
||||
fulfillment_rate: Number(q.fulfillment_rate),
|
||||
order_count: Number(q.order_count)
|
||||
ordered_quantity: Number(q.ordered_quantity) || 0,
|
||||
received_quantity: Number(q.received_quantity) || 0,
|
||||
fulfillment_rate: Number(q.fulfillment_rate) || 0,
|
||||
order_count: Number(q.order_count) || 0
|
||||
}));
|
||||
|
||||
res.json(parsedQuantities);
|
||||
|
||||
Reference in New Issue
Block a user