From 1be97d6610ff12674f2c373a76c1d6319f8e0021 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 31 Jan 2025 01:25:48 -0500 Subject: [PATCH] Enhance purchase order import with advanced receiving tracking and fulfillment logic - Implement FIFO-based receiving fulfillment tracking - Add detailed receiving history with excess and partial fulfillment support - Improve vendor name resolution and fallback handling - Optimize incremental update queries by removing redundant conditions - Enhance receiving status calculation with more granular tracking --- inventory-server/scripts/import/orders.js | 3 +- .../scripts/import/purchase-orders.js | 99 ++++++++++++------- 2 files changed, 65 insertions(+), 37 deletions(-) diff --git a/inventory-server/scripts/import/orders.js b/inventory-server/scripts/import/orders.js index b97e427..ce33da4 100644 --- a/inventory-server/scripts/import/orders.js +++ b/inventory-server/scripts/import/orders.js @@ -80,9 +80,10 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate = oi.qty_ordered as quantity, COALESCE(oi.prod_price_reg - oi.prod_price, 0) * oi.qty_ordered as base_discount FROM order_items oi + USE INDEX (PRIMARY) JOIN _order o ON oi.order_id = o.order_id WHERE o.order_status >= 15 - AND o.date_placed_onlydate >= DATE_SUB(CURRENT_DATE, INTERVAL 5 YEAR) + AND o.date_placed_onlydate >= DATE_SUB(CURRENT_DATE, INTERVAL ${incrementalUpdate ? '1' : '5'} YEAR) AND o.date_placed_onlydate IS NOT NULL ${incrementalUpdate ? ` AND ( diff --git a/inventory-server/scripts/import/purchase-orders.js b/inventory-server/scripts/import/purchase-orders.js index 3a3f8e0..54ce02a 100644 --- a/inventory-server/scripts/import/purchase-orders.js +++ b/inventory-server/scripts/import/purchase-orders.js @@ -30,7 +30,6 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental const incrementalWhereClause = incrementalUpdate ? `AND ( p.date_updated > ? - OR p.date_modified > ? OR p.date_ordered > ? OR p.date_estin > ? OR r.stamp > ? @@ -39,7 +38,7 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental )` : ""; const incrementalParams = incrementalUpdate - ? [lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime] + ? [lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime] : []; // First get all relevant PO IDs with basic info @@ -51,14 +50,14 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental USE INDEX (idx_date_created) JOIN po_products pop ON p.po_id = pop.po_id JOIN suppliers s ON p.supplier_id = s.supplierid - WHERE p.date_ordered >= DATE_SUB(CURRENT_DATE, INTERVAL 5 YEAR) + WHERE p.date_ordered >= DATE_SUB(CURRENT_DATE, INTERVAL ${incrementalUpdate ? '1' : '5'} YEAR) ${incrementalWhereClause} UNION SELECT DISTINCT r.receiving_id as po_id, rp.pid FROM receivings_products rp USE INDEX (received_date) LEFT JOIN receivings r ON r.receiving_id = rp.receiving_id - WHERE rp.received_date >= DATE_SUB(CURRENT_DATE, INTERVAL 5 YEAR) + WHERE rp.received_date >= DATE_SUB(CURRENT_DATE, INTERVAL ${incrementalUpdate ? '1' : '5'} YEAR) ${incrementalWhereClause} ) all_items `, [...incrementalParams, ...incrementalParams]); @@ -66,11 +65,11 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental const [poList] = await prodConnection.query(` SELECT DISTINCT COALESCE(p.po_id, r.receiving_id) as po_id, - CASE - WHEN p.po_id IS NOT NULL THEN s1.companyname - WHEN r.supplier_id IS NOT NULL THEN s2.companyname - ELSE 'No Supplier' - END as vendor, + COALESCE( + NULLIF(s1.companyname, ''), + NULLIF(s2.companyname, ''), + 'Unknown Vendor' + ) as vendor, CASE WHEN p.po_id IS NOT NULL THEN DATE(p.date_ordered) END as date, CASE WHEN p.po_id IS NOT NULL THEN DATE(p.date_estin) END as expected_date, COALESCE(p.status, 50) as status, @@ -79,15 +78,14 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental FROM ( SELECT po_id FROM po USE INDEX (idx_date_created) - WHERE date_ordered >= DATE_SUB(CURRENT_DATE, INTERVAL 5 YEAR) + WHERE date_ordered >= DATE_SUB(CURRENT_DATE, INTERVAL ${incrementalUpdate ? '1' : '5'} YEAR) AND (date_ordered > ? - OR date_updated > ? - OR date_modified > ?) + OR date_updated > ?) UNION SELECT DISTINCT r.receiving_id as po_id FROM receivings r JOIN receivings_products rp USE INDEX (received_date) ON r.receiving_id = rp.receiving_id - WHERE rp.received_date >= DATE_SUB(CURRENT_DATE, INTERVAL 5 YEAR) + WHERE rp.received_date >= DATE_SUB(CURRENT_DATE, INTERVAL ${incrementalUpdate ? '1' : '5'} YEAR) AND (rp.received_date > ? OR rp.stamp > ?) ) ids @@ -96,7 +94,7 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental LEFT JOIN receivings r ON ids.po_id = r.receiving_id LEFT JOIN suppliers s2 ON r.supplier_id = s2.supplierid ORDER BY po_id - `, [lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime]); + `, [lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime]); const totalItems = total; let processed = 0; @@ -215,24 +213,52 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental const altReceivingHistory = altReceivingMap.get(product.pid) || []; const noPOReceivingHistory = noPOReceivingMap.get(product.pid) || []; - const received = receivingHistory.reduce((sum, r) => sum + r.qty_each, 0); - const altReceived = altReceivingHistory.reduce((sum, r) => sum + r.qty_each, 0); - const noPOReceived = noPOReceivingHistory.reduce((sum, r) => sum + r.qty_each, 0); - const totalReceived = received + altReceived + noPOReceived; + // Combine all receivings and sort by date + const allReceivings = [ + ...receivingHistory.map(r => ({ ...r, type: 'original' })), + ...altReceivingHistory.map(r => ({ ...r, type: 'alternate' })), + ...noPOReceivingHistory.map(r => ({ ...r, type: 'no_po' })) + ].sort((a, b) => new Date(a.received_date) - new Date(b.received_date)); + + // Track FIFO fulfillment + let remainingToFulfill = product.ordered; + const fulfillmentTracking = []; + let totalReceived = 0; + + for (const receiving of allReceivings) { + const qtyToApply = Math.min(remainingToFulfill, receiving.qty_each); + if (qtyToApply > 0) { + fulfillmentTracking.push({ + receiving_id: receiving.receiving_id, + qty_applied: qtyToApply, + qty_total: receiving.qty_each, + cost: receiving.cost_each, + date: receiving.received_date, + received_by: receiving.received_by, + type: receiving.type, + remaining_qty: receiving.qty_each - qtyToApply + }); + remainingToFulfill -= qtyToApply; + } else { + // Track excess receivings + fulfillmentTracking.push({ + receiving_id: receiving.receiving_id, + qty_applied: 0, + qty_total: receiving.qty_each, + cost: receiving.cost_each, + date: receiving.received_date, + received_by: receiving.received_by, + type: receiving.type, + is_excess: true + }); + } + totalReceived += receiving.qty_each; + } const receiving_status = !totalReceived ? 1 : // created - totalReceived < product.ordered ? 30 : // partial + remainingToFulfill > 0 ? 30 : // partial 40; // full - const allReceivings = [...receivingHistory]; - if (altReceivingHistory.length > 0) { - allReceivings.push(...altReceivingHistory); - } - if (noPOReceivingHistory.length > 0) { - allReceivings.push(...noPOReceivingHistory); - } - allReceivings.sort((a, b) => new Date(a.received_date) - new Date(b.received_date)); - const firstReceiving = allReceivings[0] || {}; const lastReceiving = allReceivings[allReceivings.length - 1] || {}; @@ -250,18 +276,19 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental case 'long_note': return po.long_note; case 'ordered': return product.ordered; case 'received': return totalReceived; + case 'unfulfilled': return remainingToFulfill; + case 'excess_received': return Math.max(0, totalReceived - product.ordered); case 'received_date': return firstReceiving.received_date || null; case 'last_received_date': return lastReceiving.received_date || null; case 'received_by': return firstReceiving.received_by || null; case 'receiving_status': return receiving_status; - case 'receiving_history': return JSON.stringify(allReceivings.map(r => ({ - receiving_id: r.receiving_id, - qty: r.qty_each, - cost: r.cost_each, - date: r.received_date, - received_by: r.received_by, - alt_po: r.is_alt_po - }))); + case 'receiving_history': return JSON.stringify({ + fulfillment: fulfillmentTracking, + ordered_qty: product.ordered, + total_received: totalReceived, + remaining_unfulfilled: remainingToFulfill, + excess_received: Math.max(0, totalReceived - product.ordered) + }); default: return null; } }));