From 50b86d6d8adfd2433fbdeac234088155eeab82b0 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 1 Feb 2025 10:51:47 -0500 Subject: [PATCH] Fix/add data to PO script --- inventory-server/db/schema.sql | 2 + inventory-server/scripts/import-from-prod.js | 6 +- .../scripts/import/purchase-orders.js | 114 +++++++++++++----- 3 files changed, 91 insertions(+), 31 deletions(-) diff --git a/inventory-server/db/schema.sql b/inventory-server/db/schema.sql index 38e2531..a202739 100644 --- a/inventory-server/db/schema.sql +++ b/inventory-server/db/schema.sql @@ -137,7 +137,9 @@ CREATE TABLE purchase_orders ( expected_date DATE, pid BIGINT NOT NULL, sku VARCHAR(50) NOT NULL, + name VARCHAR(100) NOT NULL COMMENT 'Product name from products.description', cost_price DECIMAL(10, 3) NOT NULL, + po_cost_price DECIMAL(10, 3) NOT NULL COMMENT 'Original cost from PO, before receiving adjustments', status TINYINT UNSIGNED DEFAULT 1 COMMENT '0=canceled,1=created,10=electronically_ready_send,11=ordered,12=preordered,13=electronically_sent,15=receiving_started,50=done', receiving_status TINYINT UNSIGNED DEFAULT 1 COMMENT '0=canceled,1=created,30=partial_received,40=full_received,50=paid', notes TEXT, diff --git a/inventory-server/scripts/import-from-prod.js b/inventory-server/scripts/import-from-prod.js index 67b6678..f5bc680 100644 --- a/inventory-server/scripts/import-from-prod.js +++ b/inventory-server/scripts/import-from-prod.js @@ -10,9 +10,9 @@ const importPurchaseOrders = require('./import/purchase-orders'); dotenv.config({ path: path.join(__dirname, "../.env") }); // Constants to control which imports run -const IMPORT_CATEGORIES = true; -const IMPORT_PRODUCTS = true; -const IMPORT_ORDERS = true; +const IMPORT_CATEGORIES = false; +const IMPORT_PRODUCTS = false; +const IMPORT_ORDERS = false; const IMPORT_PURCHASE_ORDERS = true; // Add flag for incremental updates diff --git a/inventory-server/scripts/import/purchase-orders.js b/inventory-server/scripts/import/purchase-orders.js index b492e9e..7dacde7 100644 --- a/inventory-server/scripts/import/purchase-orders.js +++ b/inventory-server/scripts/import/purchase-orders.js @@ -108,31 +108,62 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental 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, + CASE + WHEN p.po_id IS NOT NULL THEN + DATE(COALESCE( + NULLIF(p.date_ordered, '0000-00-00 00:00:00'), + p.date_created + )) + WHEN r.receiving_id IS NOT NULL THEN + DATE(r.date_created) + END as date, + NULLIF(p.date_estin, '0000-00-00') as expected_date, COALESCE(p.status, 50) as status, - COALESCE(p.short_note, '') as notes, - COALESCE(p.notes, '') as long_note + p.short_note as notes, + p.notes as long_note FROM ( SELECT po_id FROM po USE INDEX (idx_date_created) WHERE date_ordered >= DATE_SUB(CURRENT_DATE, INTERVAL ${incrementalUpdate ? '1' : '5'} YEAR) - AND (date_ordered > ? - OR date_updated > ?) + ${incrementalUpdate ? ` + AND ( + date_ordered > ? + OR date_updated > ? + OR date_estin > ? + ) + ` : ''} 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 ${incrementalUpdate ? '1' : '5'} YEAR) - AND (rp.received_date > ? - OR rp.stamp > ?) + ${incrementalUpdate ? ` + AND ( + r.date_created > ? + OR r.date_checked > ? + OR rp.stamp > ? + OR rp.received_date > ? + ) + ` : ''} ) ids LEFT JOIN po p ON ids.po_id = p.po_id LEFT JOIN suppliers s1 ON p.supplier_id = s1.supplierid 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]); + `, incrementalUpdate ? [ + lastSyncTime, lastSyncTime, lastSyncTime, // PO conditions + lastSyncTime, lastSyncTime, lastSyncTime, lastSyncTime // Receiving conditions + ] : []); + + console.log('Sample PO dates:', poList.slice(0, 5).map(po => ({ + po_id: po.po_id, + raw_date_ordered: po.raw_date_ordered, + raw_date_created: po.raw_date_created, + raw_date_estin: po.raw_date_estin, + computed_date: po.date, + expected_date: po.expected_date + }))); const totalItems = total; let processed = 0; @@ -156,7 +187,8 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental pop.po_id, pop.pid, pr.itemnumber as sku, - pop.cost_each as cost_price, + pr.description as name, + pop.cost_each, pop.qty_each as ordered FROM po_products pop USE INDEX (PRIMARY) @@ -171,7 +203,7 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental const productPids = [...new Set(productBatch.map(p => p.pid))]; const batchPoIds = [...new Set(productBatch.map(p => p.po_id))]; - // Get receivings for this batch + // Get receivings for this batch with employee names const [receivings] = await prodConnection.query(` SELECT r.po_id, @@ -179,8 +211,9 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental rp.receiving_id, rp.qty_each, rp.cost_each, - DATE(NULLIF(rp.received_date, '0000-00-00 00:00:00')) as received_date, + COALESCE(rp.received_date, r.date_created) as received_date, rp.received_by, + CONCAT(e.firstname, ' ', e.lastname) as received_by_name, CASE WHEN r.po_id IS NULL THEN 2 -- No PO WHEN r.po_id IN (?) THEN 0 -- Original PO @@ -189,8 +222,9 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental FROM receivings_products rp USE INDEX (received_date) LEFT JOIN receivings r ON r.receiving_id = rp.receiving_id + LEFT JOIN employees e ON rp.received_by = e.employeeid WHERE rp.pid IN (?) - AND rp.received_date >= DATE_SUB(CURRENT_DATE, INTERVAL 2 YEAR) + AND rp.received_date >= DATE_SUB(CURRENT_DATE, INTERVAL 5 YEAR) ORDER BY r.po_id, rp.pid, rp.received_date `, [batchPoIds, productPids]); @@ -235,10 +269,6 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental ); const validPids = new Set(existingPids.map(p => p.pid)); - // Prepare values for this sub-batch - const values = []; - let batchProcessed = 0; - // First check which PO lines already exist and get their current values const poLines = Array.from(poProductMap.values()) .filter(p => validPids.has(p.pid)) @@ -254,6 +284,7 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental // Split into inserts and updates const insertsAndUpdates = { inserts: [], updates: [] }; + let batchProcessed = 0; for (const po of batch) { const poProducts = Array.from(poProductMap.values()) @@ -270,16 +301,29 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental ...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)); + ].sort((a, b) => new Date(a.received_date || '9999-12-31') - new Date(b.received_date || '9999-12-31')); + + // Split receivings into original PO and others + const originalPOReceivings = allReceivings.filter(r => r.type === 'original'); + const otherReceivings = allReceivings.filter(r => r.type !== 'original'); // Track FIFO fulfillment let remainingToFulfill = product.ordered; const fulfillmentTracking = []; let totalReceived = 0; + let actualCost = null; // Will store the cost of the first receiving that fulfills this PO + let firstFulfillmentReceiving = null; + let lastFulfillmentReceiving = null; for (const receiving of allReceivings) { const qtyToApply = Math.min(remainingToFulfill, receiving.qty_each); if (qtyToApply > 0) { + // If this is the first receiving being applied, use its cost + if (actualCost === null) { + actualCost = receiving.cost_each; + firstFulfillmentReceiving = receiving; + } + lastFulfillmentReceiving = receiving; fulfillmentTracking.push({ receiving_id: receiving.receiving_id, qty_applied: qtyToApply, @@ -287,6 +331,7 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental cost: receiving.cost_each, date: receiving.received_date, received_by: receiving.received_by, + received_by_name: receiving.received_by_name || 'Unknown', type: receiving.type, remaining_qty: receiving.qty_each - qtyToApply }); @@ -300,29 +345,40 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental cost: receiving.cost_each, date: receiving.received_date, received_by: receiving.received_by, + received_by_name: receiving.received_by_name || 'Unknown', type: receiving.type, is_excess: true }); } totalReceived += receiving.qty_each; } - + const receiving_status = !totalReceived ? 1 : // created remainingToFulfill > 0 ? 30 : // partial 40; // full - const firstReceiving = allReceivings[0] || {}; - const lastReceiving = allReceivings[allReceivings.length - 1] || {}; + function formatDate(dateStr) { + if (!dateStr) return null; + try { + const date = new Date(dateStr); + if (isNaN(date.getTime())) return null; + return date.toISOString().split('T')[0]; + } catch (e) { + return null; + } + } const rowValues = columnNames.map(col => { switch (col) { case 'po_id': return po.po_id; case 'vendor': return po.vendor; - case 'date': return po.date; - case 'expected_date': return po.expected_date; + case 'date': return formatDate(po.date); + case 'expected_date': return formatDate(po.expected_date); case 'pid': return product.pid; case 'sku': return product.sku; - case 'cost_price': return product.cost_price; + case 'name': return product.name; + case 'cost_price': return actualCost || product.cost_each; + case 'po_cost_price': return product.cost_each; case 'status': return po.status; case 'notes': return po.notes; case 'long_note': return po.long_note; @@ -330,16 +386,18 @@ async function importPurchaseOrders(prodConnection, localConnection, incremental 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 'received_date': return formatDate(firstFulfillmentReceiving?.received_date); + case 'last_received_date': return formatDate(lastFulfillmentReceiving?.received_date); + case 'received_by': return firstFulfillmentReceiving?.received_by_name || null; case 'receiving_status': return receiving_status; 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) + excess_received: Math.max(0, totalReceived - product.ordered), + po_cost: product.cost_each, + actual_cost: actualCost || product.cost_each }); default: return null; }