// Shared enrichment for M_Transaction rows (product history page + org transactions page). // Resolves locators (+ warehouse), products, shipment lines → shipment/receipt → partner + order, // inventory lines → physical inventory, movement lines → stock transfer. All lookups are batched // `$filter=X eq a or X eq b …` reads with `$select` — no child `$expand` (see rest-load-rules.md). import { string } from 'alga-js' import fetchHelper from './fetchHelper' export interface EnrichedTransaction { id: number | string movementDate: string movementQty: number movementType: string movementTypeId: string locator: string locatorId: number | null warehouse: string warehouseId: number | null productId: number | null productName: string productSku: string productValue: string orgId: number | null orgName: string inOutLineId: number | null inOutId: number | null inventoryId: number | null inventoryDocumentNo: string movementId: number | null movementDocumentNo: string bpartnerName: string bpartnerId: number | null externalOrderId: string orderId: number | null shipmentDocumentNo: string isSOTrx: boolean shipmentMovementType: string } export const extractId = (value: any): number | null => { if (typeof value === 'number') return value if (value?.id !== undefined && value?.id !== null) return Number(value.id) return null } const BATCH = 50 const batchedFetch = async ( event: any, token: string, model: string, idColumn: string, ids: Iterable, select: string, onRecord: (rec: any) => void ) => { const arr = Array.from(ids).filter(id => id > 0) for (let i = 0; i < arr.length; i += BATCH) { const batch = arr.slice(i, i + BATCH) const filter = batch.map(id => `${idColumn} eq ${id}`).join(' or ') try { const res: any = await fetchHelper( event, `models/${model}?$filter=${string.urlEncode(filter)}&$select=${string.urlEncode(select)}&$top=${batch.length}`, 'GET', token, null ) for (const rec of res?.records || []) onRecord(rec) } catch (err) { console.error(`Error fetching ${model} details:`, err) } } } /** * @param productInfo optional fixed product (product-history page: every row is the same product, * so the product lookup is skipped) */ export const enrichTransactions = async ( event: any, token: string, transactions: any[], productInfo: { name: string; sku: string; value?: string } | null = null ): Promise => { if (!transactions?.length) return [] const locatorIds = new Set() const productIds = new Set() const inOutLineIds = new Set() const inventoryLineIds = new Set() const movementLineIds = new Set() for (const t of transactions) { const l = extractId(t.M_Locator_ID); if (l) locatorIds.add(l) const p = extractId(t.M_Product_ID); if (p) productIds.add(p) const io = extractId(t.M_InOutLine_ID); if (io) inOutLineIds.add(io) const inv = extractId(t.M_InventoryLine_ID); if (inv) inventoryLineIds.add(inv) const mv = extractId(t.M_MovementLine_ID); if (mv) movementLineIds.add(mv) } // Locators (+ warehouse) const locators: Record = {} await batchedFetch(event, token, 'M_Locator', 'M_Locator_ID', locatorIds, 'M_Locator_ID,Value,M_Warehouse_ID', (rec) => { locators[rec.id] = { value: rec.Value || rec.identifier || '', warehouseId: extractId(rec.M_Warehouse_ID), warehouse: rec.M_Warehouse_ID?.identifier || '' } }) // Products const products: Record = {} if (!productInfo) { await batchedFetch(event, token, 'M_Product', 'M_Product_ID', productIds, 'M_Product_ID,Name,SKU,Value', (rec) => { products[rec.id] = { name: rec.Name || '', sku: rec.SKU || '', value: rec.Value || '' } }) } // Shipment lines → shipment ids const inOutLineToInOut: Record = {} const inOutIds = new Set() await batchedFetch(event, token, 'M_InOutLine', 'M_InOutLine_ID', inOutLineIds, 'M_InOutLine_ID,M_InOut_ID', (rec) => { const inOutId = extractId(rec.M_InOut_ID) inOutLineToInOut[rec.id] = inOutId if (inOutId) inOutIds.add(inOutId) }) // Shipments → partner / order const shipments: Record = {} const bpartnerIds = new Set() const orderIds = new Set() await batchedFetch(event, token, 'M_InOut', 'M_InOut_ID', inOutIds, 'M_InOut_ID,DocumentNo,IsSOTrx,MovementType,C_BPartner_ID,C_Order_ID', (rec) => { const bpartnerId = extractId(rec.C_BPartner_ID) const orderId = extractId(rec.C_Order_ID) if (bpartnerId) bpartnerIds.add(bpartnerId) if (orderId) orderIds.add(orderId) shipments[rec.id] = { documentNo: rec.DocumentNo || '', isSOTrx: rec.IsSOTrx === true || rec.IsSOTrx === 'Y', movementType: rec.MovementType?.id || rec.MovementType || '', bpartnerId, orderId, bpartnerName: '', externalOrderId: '' } }) const bpartnerNames: Record = {} await batchedFetch(event, token, 'C_BPartner', 'C_BPartner_ID', bpartnerIds, 'C_BPartner_ID,Name', (rec) => { bpartnerNames[rec.id] = rec.Name || '' }) const externalOrderIds: Record = {} await batchedFetch(event, token, 'C_Order', 'C_Order_ID', orderIds, 'C_Order_ID,ExternalOrderId', (rec) => { externalOrderIds[rec.id] = rec.ExternalOrderId || '' }) for (const s of Object.values(shipments)) { if (s.bpartnerId) s.bpartnerName = bpartnerNames[s.bpartnerId] || '' if (s.orderId) s.externalOrderId = externalOrderIds[s.orderId] || '' } // Inventory lines → physical inventory document const inventoryLineToInventory: Record = {} await batchedFetch(event, token, 'M_InventoryLine', 'M_InventoryLine_ID', inventoryLineIds, 'M_InventoryLine_ID,M_Inventory_ID', (rec) => { inventoryLineToInventory[rec.id] = { id: extractId(rec.M_Inventory_ID), documentNo: '' } }) const inventoryIds = new Set() for (const v of Object.values(inventoryLineToInventory)) if (v.id) inventoryIds.add(v.id) const inventoryDocNos: Record = {} await batchedFetch(event, token, 'M_Inventory', 'M_Inventory_ID', inventoryIds, 'M_Inventory_ID,DocumentNo', (rec) => { inventoryDocNos[rec.id] = rec.DocumentNo || '' }) // Movement lines → stock transfer document const movementLineToMovement: Record = {} await batchedFetch(event, token, 'M_MovementLine', 'M_MovementLine_ID', movementLineIds, 'M_MovementLine_ID,M_Movement_ID', (rec) => { movementLineToMovement[rec.id] = { id: extractId(rec.M_Movement_ID), documentNo: '' } }) const movementIds = new Set() for (const v of Object.values(movementLineToMovement)) if (v.id) movementIds.add(v.id) const movementDocNos: Record = {} await batchedFetch(event, token, 'M_Movement', 'M_Movement_ID', movementIds, 'M_Movement_ID,DocumentNo', (rec) => { movementDocNos[rec.id] = rec.DocumentNo || '' }) return transactions.map((t: any): EnrichedTransaction => { const locatorId = extractId(t.M_Locator_ID) const productId = extractId(t.M_Product_ID) const inOutLineId = extractId(t.M_InOutLine_ID) const inventoryLineId = extractId(t.M_InventoryLine_ID) const movementLineId = extractId(t.M_MovementLine_ID) const inOutId = inOutLineId ? inOutLineToInOut[inOutLineId] ?? null : null const shipment = inOutId ? shipments[inOutId] || {} : {} const loc = locatorId ? locators[locatorId] : undefined const prod = productInfo ? { name: productInfo.name, sku: productInfo.sku, value: productInfo.value || '' } : (productId ? products[productId] : undefined) const inv = inventoryLineId ? inventoryLineToInventory[inventoryLineId] : undefined const mv = movementLineId ? movementLineToMovement[movementLineId] : undefined return { id: t.id || t.uid, movementDate: t.MovementDate || '', movementQty: Number(t.MovementQty) || 0, movementType: t.MovementType?.identifier || t.MovementType || '', movementTypeId: t.MovementType?.id || '', locator: loc?.value || '', locatorId, warehouse: loc?.warehouse || '', warehouseId: loc?.warehouseId ?? null, productId, productName: prod?.name || t.M_Product_ID?.identifier || '', productSku: prod?.sku || '', productValue: prod?.value || '', orgId: extractId(t.AD_Org_ID), orgName: t.AD_Org_ID?.identifier || '', inOutLineId, inOutId, inventoryId: inv?.id ?? null, inventoryDocumentNo: inv?.id ? inventoryDocNos[inv.id] || '' : '', movementId: mv?.id ?? null, movementDocumentNo: mv?.id ? movementDocNos[mv.id] || '' : '', bpartnerName: shipment.bpartnerName || '', bpartnerId: shipment.bpartnerId || null, externalOrderId: shipment.externalOrderId || '', orderId: shipment.orderId || null, shipmentDocumentNo: shipment.documentNo || '', isSOTrx: shipment.isSOTrx || false, shipmentMovementType: shipment.movementType || '' } }) }