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
This commit is contained in:
2025-01-31 01:25:48 -05:00
parent b506f89dd7
commit 1be97d6610
2 changed files with 65 additions and 37 deletions

View File

@@ -80,9 +80,10 @@ async function importOrders(prodConnection, localConnection, incrementalUpdate =
oi.qty_ordered as quantity, oi.qty_ordered as quantity,
COALESCE(oi.prod_price_reg - oi.prod_price, 0) * oi.qty_ordered as base_discount COALESCE(oi.prod_price_reg - oi.prod_price, 0) * oi.qty_ordered as base_discount
FROM order_items oi FROM order_items oi
USE INDEX (PRIMARY)
JOIN _order o ON oi.order_id = o.order_id JOIN _order o ON oi.order_id = o.order_id
WHERE o.order_status >= 15 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 AND o.date_placed_onlydate IS NOT NULL
${incrementalUpdate ? ` ${incrementalUpdate ? `
AND ( AND (

View File

@@ -30,7 +30,6 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
const incrementalWhereClause = incrementalUpdate const incrementalWhereClause = incrementalUpdate
? `AND ( ? `AND (
p.date_updated > ? p.date_updated > ?
OR p.date_modified > ?
OR p.date_ordered > ? OR p.date_ordered > ?
OR p.date_estin > ? OR p.date_estin > ?
OR r.stamp > ? OR r.stamp > ?
@@ -39,7 +38,7 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
)` )`
: ""; : "";
const incrementalParams = incrementalUpdate 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 // First get all relevant PO IDs with basic info
@@ -51,14 +50,14 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
USE INDEX (idx_date_created) USE INDEX (idx_date_created)
JOIN po_products pop ON p.po_id = pop.po_id JOIN po_products pop ON p.po_id = pop.po_id
JOIN suppliers s ON p.supplier_id = s.supplierid 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} ${incrementalWhereClause}
UNION UNION
SELECT DISTINCT r.receiving_id as po_id, rp.pid SELECT DISTINCT r.receiving_id as po_id, rp.pid
FROM receivings_products rp FROM receivings_products rp
USE INDEX (received_date) USE INDEX (received_date)
LEFT JOIN receivings r ON r.receiving_id = rp.receiving_id 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} ${incrementalWhereClause}
) all_items ) all_items
`, [...incrementalParams, ...incrementalParams]); `, [...incrementalParams, ...incrementalParams]);
@@ -66,11 +65,11 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
const [poList] = await prodConnection.query(` const [poList] = await prodConnection.query(`
SELECT DISTINCT SELECT DISTINCT
COALESCE(p.po_id, r.receiving_id) as po_id, COALESCE(p.po_id, r.receiving_id) as po_id,
CASE COALESCE(
WHEN p.po_id IS NOT NULL THEN s1.companyname NULLIF(s1.companyname, ''),
WHEN r.supplier_id IS NOT NULL THEN s2.companyname NULLIF(s2.companyname, ''),
ELSE 'No Supplier' 'Unknown Vendor'
END as 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_ordered) END as date,
CASE WHEN p.po_id IS NOT NULL THEN DATE(p.date_estin) END as expected_date, CASE WHEN p.po_id IS NOT NULL THEN DATE(p.date_estin) END as expected_date,
COALESCE(p.status, 50) as status, COALESCE(p.status, 50) as status,
@@ -79,15 +78,14 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
FROM ( FROM (
SELECT po_id FROM po SELECT po_id FROM po
USE INDEX (idx_date_created) 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 > ? AND (date_ordered > ?
OR date_updated > ? OR date_updated > ?)
OR date_modified > ?)
UNION UNION
SELECT DISTINCT r.receiving_id as po_id SELECT DISTINCT r.receiving_id as po_id
FROM receivings r FROM receivings r
JOIN receivings_products rp USE INDEX (received_date) ON r.receiving_id = rp.receiving_id 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 > ? AND (rp.received_date > ?
OR rp.stamp > ?) OR rp.stamp > ?)
) ids ) ids
@@ -96,7 +94,7 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
LEFT JOIN receivings r ON ids.po_id = r.receiving_id LEFT JOIN receivings r ON ids.po_id = r.receiving_id
LEFT JOIN suppliers s2 ON r.supplier_id = s2.supplierid LEFT JOIN suppliers s2 ON r.supplier_id = s2.supplierid
ORDER BY po_id ORDER BY po_id
`, [lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime]); `, [lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime]);
const totalItems = total; const totalItems = total;
let processed = 0; let processed = 0;
@@ -215,24 +213,52 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
const altReceivingHistory = altReceivingMap.get(product.pid) || []; const altReceivingHistory = altReceivingMap.get(product.pid) || [];
const noPOReceivingHistory = noPOReceivingMap.get(product.pid) || []; const noPOReceivingHistory = noPOReceivingMap.get(product.pid) || [];
const received = receivingHistory.reduce((sum, r) => sum + r.qty_each, 0); // Combine all receivings and sort by date
const altReceived = altReceivingHistory.reduce((sum, r) => sum + r.qty_each, 0); const allReceivings = [
const noPOReceived = noPOReceivingHistory.reduce((sum, r) => sum + r.qty_each, 0); ...receivingHistory.map(r => ({ ...r, type: 'original' })),
const totalReceived = received + altReceived + noPOReceived; ...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 const receiving_status = !totalReceived ? 1 : // created
totalReceived < product.ordered ? 30 : // partial remainingToFulfill > 0 ? 30 : // partial
40; // full 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 firstReceiving = allReceivings[0] || {};
const lastReceiving = allReceivings[allReceivings.length - 1] || {}; const lastReceiving = allReceivings[allReceivings.length - 1] || {};
@@ -250,18 +276,19 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental
case 'long_note': return po.long_note; case 'long_note': return po.long_note;
case 'ordered': return product.ordered; case 'ordered': return product.ordered;
case 'received': return totalReceived; 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 'received_date': return firstReceiving.received_date || null;
case 'last_received_date': return lastReceiving.received_date || null; case 'last_received_date': return lastReceiving.received_date || null;
case 'received_by': return firstReceiving.received_by || null; case 'received_by': return firstReceiving.received_by || null;
case 'receiving_status': return receiving_status; case 'receiving_status': return receiving_status;
case 'receiving_history': return JSON.stringify(allReceivings.map(r => ({ case 'receiving_history': return JSON.stringify({
receiving_id: r.receiving_id, fulfillment: fulfillmentTracking,
qty: r.qty_each, ordered_qty: product.ordered,
cost: r.cost_each, total_received: totalReceived,
date: r.received_date, remaining_unfulfilled: remainingToFulfill,
received_by: r.received_by, excess_received: Math.max(0, totalReceived - product.ordered)
alt_po: r.is_alt_po });
})));
default: return null; default: return null;
} }
})); }));