import { string } from 'alga-js' import refreshTokenHelper from "../../utils/refreshTokenHelper" import forceLogoutHelper from "../../utils/forceLogoutHelper" import errorHandlingHelper from "../../utils/errorHandlingHelper" import fetchHelper from "../../utils/fetchHelper" import sharedLang from "~/assets/langs/shared.js" /** * Validates paket-to-shipment 1:1 relationship * Parses M_InOut_IDs from parcel line Description fields and cross-references with shipment records */ const performPaketShipmentValidation = async ( event: any, token: string, organizationId: number, parcelLines: any, shipmentLines: any[] ) => { const validation: any = { totalParcels: 0, totalShipments: 0, matchedParcels: 0, matchedShipments: 0, unmatchedParcels: [], unmatchedShipments: [], parcelToShipmentMap: new Map(), shipmentToPaketMap: new Map() } try { // Step 1: Extract M_InOut_IDs from parcel Description fields const parcelInoutIds = new Set() console.log('Processing parcel lines...') console.log('Parcel lines keys:', Object.keys(parcelLines)) console.log('Total parcel products:', Object.keys(parcelLines).length) for (const productId in parcelLines) { const lineData = parcelLines[productId] console.log(`Processing parcel product ${productId}, details count:`, lineData.details?.length || 0) // Each detail represents a parcel fee line for (const detail of lineData.details) { if (detail.description) { console.log(` Parcel fee line ${detail.id}: Description="${detail.description}"`) // Parse comma-separated M_InOut_IDs from Description const inoutIdsStr = detail.description.split(',').map((s: string) => s.trim()) console.log(` Parsed IDs:`, inoutIdsStr) for (const idStr of inoutIdsStr) { const inoutId = parseInt(idStr) if (!isNaN(inoutId) && inoutId > 0) { console.log(` Adding parcel InOut ID: ${inoutId}`) parcelInoutIds.add(inoutId) // Track mapping from shipment to parcel if (!validation.shipmentToPaketMap.has(inoutId)) { validation.shipmentToPaketMap.set(inoutId, []) } validation.shipmentToPaketMap.get(inoutId).push({ feeLineId: detail.id, productId: parseInt(productId), qty: detail.qty }) } } } else { console.log(` Parcel fee line ${detail.id}: No Description field`) } validation.totalParcels++ } } // Step 2: Collect M_InOut_IDs from individual fulfillment sub-items // These are the detail lines with FulfillTypeAccounting === 'fulfillment' that get grouped into the final product const shipmentInoutIds = new Set() for (const line of shipmentLines) { const inoutId = line.M_InOut_ID?.id const fulfillType = line.FulfillTypeAccounting // Only include fulfillment sub-items (the individual lines that get counted together) if (inoutId && fulfillType === 'fulfillment') { console.log(` Fulfillment line ${line.id}: M_InOut_ID=${inoutId}, DocumentNo=${line.M_InOut_ID?.DocumentNo}`) shipmentInoutIds.add(inoutId) // Track mapping from shipment to parcel (only add unique shipment IDs) if (!validation.parcelToShipmentMap.has(inoutId)) { validation.parcelToShipmentMap.set(inoutId, { inoutId, documentNo: line.M_InOut_ID?.DocumentNo || inoutId.toString(), feeLineId: line.id, type: fulfillType, productId: line.M_Product_ID?.id }) } } } // Count unique shipments validation.totalShipments = shipmentInoutIds.size console.log('═══════════════════════════════════════════════════════════') console.log('PAKET-SHIPMENT VALIDATION DEBUG') console.log('═══════════════════════════════════════════════════════════') console.log('Total fee lines checked:', shipmentLines.length) console.log('Fulfillment type lines found:', shipmentLines.filter(l => l.FulfillTypeAccounting === 'fulfillment').length) console.log('Parcel InOut IDs from Description:', Array.from(parcelInoutIds).sort((a, b) => a - b)) console.log('Fulfillment InOut IDs (FulfillTypeAccounting=fulfillment):', Array.from(shipmentInoutIds).sort((a, b) => a - b)) console.log('Total unique parcel references:', parcelInoutIds.size) console.log('Total unique fulfillment shipments:', shipmentInoutIds.size) console.log('═══════════════════════════════════════════════════════════') // Store detailed comparison info for frontend display validation.comparisonDebug = { parcelInoutIds: Array.from(parcelInoutIds).sort((a, b) => a - b), fulfillmentInoutIds: Array.from(shipmentInoutIds).sort((a, b) => a - b), totalFeeLines: shipmentLines.length, fulfillmentTypeLines: shipmentLines.filter(l => l.FulfillTypeAccounting === 'fulfillment').length } // Step 3: Find matches and mismatches // Check which parcel inouts have corresponding shipments for (const inoutId of parcelInoutIds) { if (shipmentInoutIds.has(inoutId)) { validation.matchedParcels++ } else { validation.unmatchedParcels.push({ inoutId, paketLines: validation.shipmentToPaketMap.get(inoutId) || [], reason: 'Parcel references this shipment but no fulfillment detail line found' }) } } // Check which shipment inouts have corresponding parcels for (const inoutId of shipmentInoutIds) { if (parcelInoutIds.has(inoutId)) { validation.matchedShipments++ } else { const shipmentInfo = validation.parcelToShipmentMap.get(inoutId) validation.unmatchedShipments.push({ inoutId, documentNo: shipmentInfo?.documentNo || inoutId.toString(), feeLineId: shipmentInfo?.feeLineId, type: shipmentInfo?.type, reason: 'Fulfillment detail line exists but no parcel references this shipment' }) } } console.log('Validation Results:', { totalParcels: validation.totalParcels, totalShipments: validation.totalShipments, matchedParcels: validation.matchedParcels, matchedShipments: validation.matchedShipments, unmatchedParcelCount: validation.unmatchedParcels.length, unmatchedShipmentCount: validation.unmatchedShipments.length }) } catch (err: any) { console.error('Error performing paket-shipment validation:', err) validation.error = err.message || 'Validation failed' } return validation } const handleFunc = async (event: any, authToken: any = null) => { let data: any = { success: [], errors: [], preview: false } const token = authToken ?? await getTokenHelper(event) const organizationId = parseInt(getCookie(event, 'organizationId') || '1000000') const body = await readBody(event) // Get language preference from cookie (default to 'en-US') const lang = getCookie(event, 'language') || 'en-US' const t = sharedLang(lang) const isPreview = body.preview === true const shouldFetchCountryData = body.fetchCountryData === true const shouldCreateReport = body.createReport === true && !isPreview const shouldValidatePaketShipment = body.validatePaketShipment === true && isPreview const debugStorageUsage = body.debugStorageUsage === true && isPreview const selectedOrders = body.selectedOrders || null // Array of {bpartnerId, monthYear} console.log('Organization ID from cookie:', organizationId) console.log('Create Report:', shouldCreateReport) console.log('Selected Orders:', selectedOrders) // Set preview flag in response data.preview = isPreview // Helper function to round to 2 decimal places const roundToTwo = (num: number): number => { return Math.round((num + Number.EPSILON) * 100) / 100 } // Cache for country data const countryCache: any = {} // Helper function to fetch country data from shipment or order const fetchCountryData = async (orderId?: number, inoutId?: number) => { // Only fetch country data if explicitly requested if (!shouldFetchCountryData) { return { countryCode: null, countryName: null } } // Try order first if available if (orderId) { const cacheKey = `order_${orderId}` if (countryCache[cacheKey]) { return countryCache[cacheKey] } if (countryCache[cacheKey] === false) { return { countryCode: null, countryName: null } } try { const order: any = await event.context.fetch( `models/c_order/${orderId}`, 'GET', token, null ) if (order?.C_BPartner_Location_ID?.id) { const bpLocation: any = await event.context.fetch( `models/c_bpartner_location/${order.C_BPartner_Location_ID.id}`, 'GET', token, null ) if (bpLocation?.C_Location_ID?.id) { const location: any = await event.context.fetch( `models/c_location/${bpLocation.C_Location_ID.id}`, 'GET', token, null ) const result = { countryCode: location?.C_Country_ID?.CountryCode, countryName: location?.C_Country_ID?.Name } countryCache[cacheKey] = result return result } } countryCache[cacheKey] = false } catch (err: any) { if (!err?.statusCode || err.statusCode !== 404) { console.error(`Failed to fetch country from order ${orderId}:`, err) } countryCache[cacheKey] = false } } // Try shipment if order failed or not available if (inoutId) { const cacheKey = `inout_${inoutId}` if (countryCache[cacheKey]) { return countryCache[cacheKey] } if (countryCache[cacheKey] === false) { return { countryCode: null, countryName: null } } try { // Optimized query to minimize traffic - only get location data const inout: any = await event.context.fetch( `models/m_inout/${inoutId}?$select=C_BPartner_Location_ID&$expand=C_BPartner_Location_ID($select=C_Location_ID)`, 'GET', token, null ) // The C_Location_ID is nested inside C_BPartner_Location_ID and contains C_Country_ID const locationData = inout?.C_BPartner_Location_ID?.C_Location_ID const countryId = locationData?.C_Country_ID?.id if (countryId) { // Fetch only the country code and name const country: any = await event.context.fetch( `models/c_country/${countryId}?$select=CountryCode,Name`, 'GET', token, null ) const result = { countryCode: country?.CountryCode || null, countryName: country?.Name || null } countryCache[cacheKey] = result return result } countryCache[cacheKey] = false } catch (err: any) { if (!err?.statusCode || err.statusCode !== 404) { console.error(`Failed to fetch country from shipment ${inoutId}:`, err) } countryCache[cacheKey] = false } } return { countryCode: null, countryName: null } } // Build filter for fulfillment fee lines let filter = 'A_Processed eq false' if (body.partnerId) { filter += ` and C_BPartner_ID eq ${body.partnerId}` } if (body.month && body.year) { // Filter by shipping_date month/year const startDate = new Date(Date.UTC(body.year, body.month - 1, 1)).toISOString().split('T')[0] const endDate = new Date(Date.UTC(body.year, body.month, 0, 23, 59, 59)).toISOString() filter += ` and shipping_date ge '${startDate}' and shipping_date lt '${endDate}T23:59:59'` } // Fetch unprocessed fulfillment fee lines with expanded relationships (only first level) // Description field should be included by default const feeLines: any = await event.context.fetch( `models/cust_fulfillmentfeeline?$filter=${string.urlEncode(filter)}&$expand=C_BPartner_ID,M_Product_ID,M_InOut_ID,C_Order_ID`, 'GET', token, null ) if (!feeLines?.records || feeLines.records.length === 0) { return { success: [], errors: [], message: 'No unprocessed fulfillment fee lines found' } } // Debug: Log fulfill types // const fulfillTypes: any = {} // feeLines.records.forEach((line: any) => { // const type = line.FulfillTypeAccounting // fulfillTypes[type] = (fulfillTypes[type] || 0) + 1 // }) // data.debug.fulfillTypesBreakdown = fulfillTypes // data.debug.totalRecordsFetched = feeLines.records.length // data.debug.filter = filter // console.log('╔════════════════════════════════════════════════════════╗') // console.log('║ FULFILL TYPES BREAKDOWN - INITIAL FETCH ║') // console.log('╠════════════════════════════════════════════════════════╣') // console.log(' Total records fetched:', feeLines.records.length) // console.log(' Filter used:', filter) // console.log(' Types breakdown:') // Object.entries(fulfillTypes).forEach(([type, count]) => { // console.log(` - ${type}: ${count}`) // }) // console.log(' ') // console.log(' Sample records (first 5):') // feeLines.records.slice(0, 5).forEach((record: any) => { // console.log(` ID: ${record.id}, Type: ${record.FulfillTypeAccounting}, Partner: ${record.C_BPartner_ID?.id || 'NONE'}, Product: ${record.M_Product_ID?.id || 'NONE'}`) // }) // console.log('╚════════════════════════════════════════════════════════╝') // Group records by C_BPartner_ID and shipping_date (month/year) const grouped: any = {} for (const line of feeLines.records) { // Get C_BPartner_ID directly from the fee line const bpartnerId = line.C_BPartner_ID?.id if (!bpartnerId) { data.errors.push({ lineId: line.id, error: 'No business partner found for line' }) continue } const shippingDate = new Date(line.shipping_date) const monthYear = `${shippingDate.getFullYear()}-${String(shippingDate.getMonth() + 1).padStart(2, '0')}` const groupKey = `${bpartnerId}_${monthYear}` if (!grouped[groupKey]) { grouped[groupKey] = { bpartnerId, monthYear, month: shippingDate.getMonth() + 1, year: shippingDate.getFullYear(), lines: [] } } grouped[groupKey].lines.push(line) } // Process groups in smaller batches to avoid connection pool exhaustion let groupKeys = Object.keys(grouped) // Filter groups if selectedOrders is provided if (selectedOrders && selectedOrders.length > 0) { const selectedSet = new Set( selectedOrders.map((sel: any) => `${sel.bpartnerId}_${sel.monthYear}`) ) groupKeys = groupKeys.filter(key => selectedSet.has(key)) console.log(`Filtered to ${groupKeys.length} selected orders from ${Object.keys(grouped).length} total`) } const batchSize = 3 // Process 3 business partners at a time to avoid pool exhaustion let processedCount = 0 console.log(`Processing ${groupKeys.length} business partner groups in batches of ${batchSize}`) for (let batchStart = 0; batchStart < groupKeys.length; batchStart += batchSize) { const batchKeys = groupKeys.slice(batchStart, batchStart + batchSize) console.log(`Processing batch ${Math.floor(batchStart / batchSize) + 1}/${Math.ceil(groupKeys.length / batchSize)} (${batchKeys.length} groups)`) // Process batch sequentially with small delay between groups for (const groupKey of batchKeys) { const group = grouped[groupKey] let orderPayload: any = null // Declare outside try block so it's accessible in catch try { // Fetch business partner details with expanded relations const bpartner: any = await event.context.fetch( `models/c_bpartner/${group.bpartnerId}?$expand=C_BPartner_Location,AD_User&$filter=${string.urlEncode('isFulfillmentCustomer eq true and IsActive eq true')}`, 'GET', token, null ) if (!bpartner || !bpartner.isFulfillmentCustomer || !bpartner.IsActive) { data.errors.push({ bpartnerId: group.bpartnerId, error: 'Partner is not an active fulfillment customer' }) continue } // Get IDs from partner or use defaults const warehouseId = bpartner.M_Warehouse_ID?.id || 1000000 const priceListId = bpartner.M_PriceList_ID?.id || 1000002 const paymentTermId = bpartner.C_PaymentTerm_ID?.id || 1000000 // Use tax exempt rate if partner is tax exempt const taxId = bpartner.IsTaxExempt === true ? 1000001 : 1000000 // Group lines by FulfillTypeAccounting and M_Product_ID const fulfillmentLines: any = {} const returnLines: any = {} const spaceRentLines: any = {} const spaceRentFlatLines: any = {} const parcelLines: any = {} const subscriptionLines: any = {} const shippingFeeLines: any = {} for (const line of group.lines) { const productId = line.M_Product_ID?.id const productName = line.M_Product_ID?.Name || line.M_Product_ID?.Value || `Product ${productId}` if (!productId) { // console.log('⚠️ Skipping line without product ID:', line.id, 'Type:', line.FulfillTypeAccounting) continue } // console.log(`Processing line ${line.id}: Type="${line.FulfillTypeAccounting}", Product=${productId}`) if (line.FulfillTypeAccounting === 'fulfillment') { if (!fulfillmentLines[productId]) { fulfillmentLines[productId] = { productId, productName, totalAmt: 0, inoutIds: new Set(), details: [] } } fulfillmentLines[productId].totalAmt += parseFloat(line.LineTotalAmt) || 0 if (line.M_InOut_ID?.id) { fulfillmentLines[productId].inoutIds.add(line.M_InOut_ID.id) } let country = { countryCode: null, countryName: null } try { country = await fetchCountryData(line.C_Order_ID?.id, line.M_InOut_ID?.id) } catch (countryErr) { console.error(`Failed to fetch country for line ${line.id}:`, countryErr) } fulfillmentLines[productId].details.push({ id: line.id, inoutId: line.M_InOut_ID?.id, inoutDocumentNo: line.M_InOut_ID?.DocumentNo, orderId: line.C_Order_ID?.id, orderDocumentNo: line.C_Order_ID?.DocumentNo, countryCode: country.countryCode, countryName: country.countryName, shippingDate: line.shipping_date, qty: parseFloat(line.QtyInvoiced) || 0, price: parseFloat(line.PriceEntered) || 0, amount: parseFloat(line.LineTotalAmt) || 0, sourceDescription: line.Description || '' }) } else if (line.FulfillTypeAccounting === 'return') { if (!returnLines[productId]) { returnLines[productId] = { productId, productName, totalAmt: 0, inoutIds: new Set(), details: [] } } returnLines[productId].totalAmt += parseFloat(line.LineTotalAmt) || 0 if (line.M_InOut_ID?.id) { returnLines[productId].inoutIds.add(line.M_InOut_ID.id) } let country = { countryCode: null, countryName: null } try { country = await fetchCountryData(line.C_Order_ID?.id, line.M_InOut_ID?.id) } catch (countryErr) { console.error(`Failed to fetch country for line ${line.id}:`, countryErr) } returnLines[productId].details.push({ id: line.id, inoutId: line.M_InOut_ID?.id, inoutDocumentNo: line.M_InOut_ID?.DocumentNo, orderId: line.C_Order_ID?.id, orderDocumentNo: line.C_Order_ID?.DocumentNo, countryCode: country.countryCode, countryName: country.countryName, shippingDate: line.shipping_date, qty: parseFloat(line.QtyInvoiced) || 0, price: parseFloat(line.PriceEntered) || 0, amount: parseFloat(line.LineTotalAmt) || 0 }) } else if (line.FulfillTypeAccounting === 'spacerentqm3') { if (!spaceRentLines[productId]) { spaceRentLines[productId] = { productId, productName, totalAmt: 0, qtyInvoiced: 0, details: [] } } spaceRentLines[productId].totalAmt += parseFloat(line.LineTotalAmt) || 0 spaceRentLines[productId].qtyInvoiced += parseFloat(line.QtyInvoiced) || 0 spaceRentLines[productId].details.push({ id: line.id, shippingDate: line.shipping_date, qty: parseFloat(line.QtyInvoiced) || 0, price: parseFloat(line.PriceEntered) || 0, amount: parseFloat(line.LineTotalAmt) || 0 }) } else if (line.FulfillTypeAccounting === 'spacerentflat') { if (!spaceRentFlatLines[productId]) { spaceRentFlatLines[productId] = { productId, productName, totalAmt: 0, qtyInvoiced: 0, priceEntered: 0, details: [] } } spaceRentFlatLines[productId].totalAmt += parseFloat(line.LineTotalAmt) || 0 spaceRentFlatLines[productId].qtyInvoiced += parseFloat(line.QtyInvoiced) || 0 spaceRentFlatLines[productId].details.push({ id: line.id, shippingDate: line.shipping_date, qty: parseFloat(line.QtyInvoiced) || 0, price: parseFloat(line.PriceEntered) || 0, amount: parseFloat(line.LineTotalAmt) || 0 }) } else if (line.FulfillTypeAccounting === 'parcel') { if (!parcelLines[productId]) { parcelLines[productId] = { productId, productName, totalAmt: 0, qtyInvoiced: 0, priceEntered: 0, details: [] } } parcelLines[productId].totalAmt += parseFloat(line.LineTotalAmt) || 0 parcelLines[productId].qtyInvoiced += parseFloat(line.QtyInvoiced) || 0 parcelLines[productId].details.push({ id: line.id, shippingDate: line.shipping_date, qty: parseFloat(line.QtyInvoiced) || 0, price: parseFloat(line.PriceEntered) || 0, amount: parseFloat(line.LineTotalAmt) || 0, description: line.Description || null // Include Description for validation }) } else if (line.FulfillTypeAccounting === 'subscription') { if (!subscriptionLines[productId]) { subscriptionLines[productId] = { productId, productName, totalAmt: 0, qtyInvoiced: 0, details: [] } } subscriptionLines[productId].totalAmt += parseFloat(line.LineTotalAmt) || 0 subscriptionLines[productId].qtyInvoiced += parseFloat(line.QtyInvoiced) || 0 subscriptionLines[productId].details.push({ id: line.id, shippingDate: line.shipping_date, qty: parseFloat(line.QtyInvoiced) || 0, price: parseFloat(line.PriceEntered) || 0, amount: parseFloat(line.LineTotalAmt) || 0 }) } else if (line.FulfillTypeAccounting === 'shippingfee') { if (!shippingFeeLines[productId]) { shippingFeeLines[productId] = { productId, productName, totalAmt: 0, qtyInvoiced: 0, details: [] } } shippingFeeLines[productId].totalAmt += parseFloat(line.LineTotalAmt) || 0 shippingFeeLines[productId].qtyInvoiced += parseFloat(line.QtyInvoiced) || 0 // Only fetch country and shipping fee details in preview mode let country = { countryCode: null, countryName: null } let shippingFees = null if (isPreview && shouldFetchCountryData) { try { country = await fetchCountryData(line.C_Order_ID?.id, line.M_InOut_ID?.id) } catch (countryErr) { console.error(`Failed to fetch country for line ${line.id}:`, countryErr) } // Get shipping fee details from M_InOut if available if (line.M_InOut_ID?.id) { try { const inout: any = await event.context.fetch( `models/m_inout/${line.M_InOut_ID.id}?$select=IsCommissioned,IsCommissionedConfirmed,ack_commissioned_laravel,shipping_date,M_Paket_Type_ID,ext_freight_co2,ext_freight_energy,ext_freight_cost,ext_freight_weight,shipping_service_name,isDHLKleinPaket,isplentyinprogress,isAccountingExported,isExportedPaketAccounting,ext_freight_total,int_freight_total,int_freight_co2,int_freight_energy,int_freight_cost,dhl_product_id,dhl_product_description,isExternalFeeAccounted,isShippingFeeAccounted&$expand=M_Paket_Type_ID($select=Name)`, 'GET', token, null ) shippingFees = { // External fees extTotal: inout?.ext_freight_total, extCo2: inout?.ext_freight_co2, extEnergy: inout?.ext_freight_energy, extCost: inout?.ext_freight_cost, extWeight: inout?.ext_freight_weight, // Internal fees intTotal: inout?.int_freight_total, intCo2: inout?.int_freight_co2, intEnergy: inout?.int_freight_energy, intCost: inout?.int_freight_cost, // Shipment details serviceName: inout?.shipping_service_name, productDescription: inout?.dhl_product_description, dhlProductId: inout?.dhl_product_id, paketType: inout?.M_Paket_Type_ID?.Name, shippingDate: inout?.shipping_date, // Flags isCommissioned: inout?.IsCommissioned, isCommissionedConfirmed: inout?.IsCommissionedConfirmed, ackCommissionedLaravel: inout?.ack_commissioned_laravel, isDHLKleinPaket: inout?.isDHLKleinPaket, isPlentyInProgress: inout?.isplentyinprogress, isAccountingExported: inout?.isAccountingExported, isExportedPaketAccounting: inout?.isExportedPaketAccounting, isExternalFeeAccounted: inout?.isExternalFeeAccounted, isShippingFeeAccounted: inout?.isShippingFeeAccounted } } catch (err: any) { if (!err?.statusCode || err.statusCode !== 404) { console.error(`Failed to fetch shipping fees from shipment ${line.M_InOut_ID.id}:`, err) } } } } shippingFeeLines[productId].details.push({ id: line.id, orderId: line.C_Order_ID?.id, orderDocumentNo: line.C_Order_ID?.DocumentNo, inoutId: line.M_InOut_ID?.id, inoutDocumentNo: line.M_InOut_ID?.DocumentNo, countryCode: country.countryCode, countryName: country.countryName, productName: line.M_Product_ID?.Name || productName, shippingDate: line.shipping_date, qty: parseFloat(line.QtyInvoiced) || 0, price: parseFloat(line.PriceEntered) || 0, amount: parseFloat(line.LineTotalAmt) || 0, shippingFees: shippingFees }) } } // Debug: Log what we collected // const groupedSummary = { // fulfillmentProducts: Object.keys(fulfillmentLines).length, // returnProducts: Object.keys(returnLines).length, // spaceRentProducts: Object.keys(spaceRentLines).length, // spaceRentFlatProducts: Object.keys(spaceRentFlatLines).length, // parcelProducts: Object.keys(parcelLines).length, // subscriptionProducts: Object.keys(subscriptionLines).length, // shippingFeeProducts: Object.keys(shippingFeeLines).length, // shippingFeeDetails: shippingFeeLines // } // if (!data.debug.groupedByPartner) data.debug.groupedByPartner = {} // data.debug.groupedByPartner[`${group.bpartnerId}_${group.monthYear}`] = groupedSummary // console.log('=== GROUPED LINES SUMMARY ===') // console.log(JSON.stringify(groupedSummary, null, 2)) // console.log('=============================') // Build order lines array (store product IDs as integers for the payload) const orderLines: any[] = [] let lineNo = 10 // Add fulfillment positions for (const productId in fulfillmentLines) { const lineData = fulfillmentLines[productId] const orderCount = lineData.inoutIds.size const positionCount = lineData.details.length const qty = positionCount const unitPrice = roundToTwo(qty > 0 ? lineData.totalAmt / qty : lineData.totalAmt) orderLines.push({ productId: parseInt(productId), QtyOrdered: 1, QtyEntered: 1, PriceEntered: roundToTwo(lineData.totalAmt), PriceActual: roundToTwo(lineData.totalAmt), LineNetAmt: roundToTwo(lineData.totalAmt), Description: `${t.fulfillment}: ${orderCount} ${orderCount > 1 ? t.orders : t.order} ${t.and} ${positionCount} ${positionCount > 1 ? t.positions : t.position} (${qty} x ${unitPrice})`, Line: lineNo }) lineNo += 10 } // Add return positions for (const productId in returnLines) { const lineData = returnLines[productId] const orderCount = lineData.inoutIds.size const positionCount = lineData.details.length const qty = positionCount const unitPrice = roundToTwo(qty > 0 ? lineData.totalAmt / qty : lineData.totalAmt) orderLines.push({ productId: parseInt(productId), QtyOrdered: 1, QtyEntered: 1, PriceEntered: roundToTwo(lineData.totalAmt), PriceActual: roundToTwo(lineData.totalAmt), LineNetAmt: roundToTwo(lineData.totalAmt), Description: `${t.return}: ${orderCount} ${orderCount > 1 ? t.orders : t.order} ${t.and} ${positionCount} ${positionCount > 1 ? t.positions : t.position} (${qty} x ${unitPrice})`, Line: lineNo }) lineNo += 10 } // Add space rent positions for (const productId in spaceRentLines) { const lineData = spaceRentLines[productId] const qty = lineData.qtyInvoiced const unitPrice = roundToTwo(qty > 0 ? lineData.totalAmt / qty : lineData.totalAmt) orderLines.push({ productId: parseInt(productId), QtyOrdered: 1, QtyEntered: 1, PriceEntered: roundToTwo(lineData.totalAmt), PriceActual: roundToTwo(lineData.totalAmt), LineNetAmt: roundToTwo(lineData.totalAmt), Description: `${t.warehouse_rent}: ${qty.toFixed(2)} qm³ (${qty.toFixed(2)} x ${unitPrice})`, Line: lineNo }) lineNo += 10 } // Add flat space rent positions for (const productId in spaceRentFlatLines) { const lineData = spaceRentFlatLines[productId] const qty = lineData.qtyInvoiced const unitPrice = roundToTwo(qty > 0 ? lineData.totalAmt / qty : lineData.totalAmt) orderLines.push({ productId: parseInt(productId), QtyOrdered: 1, QtyEntered: 1, PriceEntered: roundToTwo(lineData.totalAmt), PriceActual: roundToTwo(lineData.totalAmt), LineNetAmt: roundToTwo(lineData.totalAmt), Description: `${t.warehouse_rent_flat}: ${qty.toFixed(2)} ${qty !== 1 ? t.units : t.unit} (${qty.toFixed(2)} x ${unitPrice})`, Line: lineNo }) lineNo += 10 } // Add parcel positions for (const productId in parcelLines) { const lineData = parcelLines[productId] const qty = lineData.qtyInvoiced const unitPrice = roundToTwo(qty > 0 ? lineData.totalAmt / qty : lineData.totalAmt) orderLines.push({ productId: parseInt(productId), QtyOrdered: 1, QtyEntered: 1, PriceEntered: roundToTwo(lineData.totalAmt), PriceActual: roundToTwo(lineData.totalAmt), LineNetAmt: roundToTwo(lineData.totalAmt), Description: `${t.parcel}: ${qty.toFixed(0)} ${qty !== 1 ? t.parcels : t.parcel} (${qty.toFixed(0)} x ${unitPrice})`, Line: lineNo }) lineNo += 10 } // Add subscription positions for (const productId in subscriptionLines) { const lineData = subscriptionLines[productId] const qty = lineData.qtyInvoiced const unitPrice = roundToTwo(qty > 0 ? lineData.totalAmt / qty : lineData.totalAmt) orderLines.push({ productId: parseInt(productId), QtyOrdered: 1, QtyEntered: 1, PriceEntered: roundToTwo(lineData.totalAmt), PriceActual: roundToTwo(lineData.totalAmt), LineNetAmt: roundToTwo(lineData.totalAmt), Description: `${t.subscription}: ${qty.toFixed(2)} ${qty !== 1 ? t.units : t.unit} (${qty.toFixed(2)} x ${unitPrice})`, Line: lineNo }) lineNo += 10 } // Add shipping fee positions for (const productId in shippingFeeLines) { const lineData = shippingFeeLines[productId] const qty = lineData.qtyInvoiced const unitPrice = roundToTwo(qty > 0 ? lineData.totalAmt / qty : lineData.totalAmt) orderLines.push({ productId: parseInt(productId), QtyOrdered: 1, QtyEntered: 1, PriceEntered: roundToTwo(lineData.totalAmt), PriceActual: roundToTwo(lineData.totalAmt), LineNetAmt: roundToTwo(lineData.totalAmt), Description: `${t.shipping_fee}: ${qty.toFixed(2)} ${qty !== 1 ? t.units : t.unit} (${qty.toFixed(2)} x ${unitPrice})`, Line: lineNo }) lineNo += 10 } if (orderLines.length === 0) { data.errors.push({ bpartnerId: group.bpartnerId, error: 'No valid order lines generated' }) continue } // Calculate totals const totalLines = orderLines.reduce((sum, line) => sum + (line.LineNetAmt || 0), 0) // Get month name using translations const monthKeys = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'] const monthName = t[monthKeys[group.month - 1]] // Calculate last day of the month for PeriodPerformanceDate const lastDayOfMonth = new Date(group.year, group.month, 0).toISOString().split('T')[0] // Perform paket-shipment validation if requested let validationResults: any = null if (shouldValidatePaketShipment && isPreview) { validationResults = await performPaketShipmentValidation( event, token, organizationId, parcelLines, group.lines ) } // In preview mode, skip order creation if (isPreview) { // Build preview order lines with product names const previewOrderLines: any[] = [] for (const productId in fulfillmentLines) { const lineData = fulfillmentLines[productId] const orderCount = lineData.inoutIds.size const positionCount = lineData.details.length previewOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Fulfillment: ${orderCount} Order${orderCount > 1 ? 's' : ''} and ${positionCount} position${positionCount > 1 ? 's' : ''}`, qtyOrdered: 1, priceActual: roundToTwo(lineData.totalAmt), lineNetAmt: roundToTwo(lineData.totalAmt), type: 'fulfillment', details: lineData.details }) } for (const productId in returnLines) { const lineData = returnLines[productId] const orderCount = lineData.inoutIds.size const positionCount = lineData.details.length previewOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Return: ${orderCount} Order${orderCount > 1 ? 's' : ''} and ${positionCount} position${positionCount > 1 ? 's' : ''}`, qtyOrdered: 1, priceActual: roundToTwo(lineData.totalAmt), lineNetAmt: roundToTwo(lineData.totalAmt), type: 'return', details: lineData.details }) } for (const productId in spaceRentLines) { const lineData = spaceRentLines[productId] previewOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Warehouse Rent: ${lineData.qtyInvoiced.toFixed(2)} qm³`, qtyOrdered: 1, priceActual: roundToTwo(lineData.totalAmt), lineNetAmt: roundToTwo(lineData.totalAmt), type: 'spacerentqm3', details: lineData.details }) } for (const productId in spaceRentFlatLines) { const lineData = spaceRentFlatLines[productId] const flatQty = lineData.qtyInvoiced const avgPrice = roundToTwo(flatQty > 0 ? lineData.totalAmt / flatQty : 0) previewOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Warehouse Rent (Flat): ${flatQty.toFixed(2)} units`, qtyOrdered: flatQty, priceActual: avgPrice, lineNetAmt: roundToTwo(lineData.totalAmt), type: 'spacerentflat', details: lineData.details }) } for (const productId in parcelLines) { const lineData = parcelLines[productId] const parcelQty = lineData.qtyInvoiced const avgPrice = roundToTwo(parcelQty > 0 ? lineData.totalAmt / parcelQty : 0) previewOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Parcel: ${parcelQty.toFixed(0)} parcel${parcelQty !== 1 ? 's' : ''}`, qtyOrdered: parcelQty, priceActual: avgPrice, lineNetAmt: roundToTwo(lineData.totalAmt), type: 'parcel', details: lineData.details }) } for (const productId in subscriptionLines) { const lineData = subscriptionLines[productId] const subscriptionQty = lineData.qtyInvoiced const avgPrice = roundToTwo(subscriptionQty > 0 ? lineData.totalAmt / subscriptionQty : 0) previewOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Subscription: ${subscriptionQty.toFixed(2)} unit${subscriptionQty !== 1 ? 's' : ''}`, qtyOrdered: subscriptionQty, priceActual: avgPrice, lineNetAmt: roundToTwo(lineData.totalAmt), type: 'subscription', details: lineData.details }) } for (const productId in shippingFeeLines) { const lineData = shippingFeeLines[productId] const shippingFeeQty = lineData.qtyInvoiced const avgPrice = roundToTwo(shippingFeeQty > 0 ? lineData.totalAmt / shippingFeeQty : 0) previewOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Shipping Fee: ${shippingFeeQty.toFixed(2)} unit${shippingFeeQty !== 1 ? 's' : ''}`, qtyOrdered: shippingFeeQty, priceActual: avgPrice, lineNetAmt: roundToTwo(lineData.totalAmt), type: 'shippingfee', details: lineData.details }) } data.success.push({ bpartnerId: group.bpartnerId, bpartnerName: bpartner.Name, monthYear: group.monthYear, linesProcessed: group.lines.length, totalAmount: totalLines, orderLines: previewOrderLines, validationResults: validationResults }) continue } // Get first location for billing and shipping const bpLocation = bpartner.C_BPartner_Location?.[0] const bpUser = bpartner.AD_User?.[0] // Validate that partner has a location if (!bpLocation?.id) { data.errors.push({ bpartnerId: group.bpartnerId, error: `Business Partner has no Ship To Address - ${bpartner.Name || group.bpartnerId}`, details: { partnerId: group.bpartnerId, partnerName: bpartner.Name } }) continue } // Create the sales order with all integer IDs (no object notation) orderPayload = { AD_Org_ID: organizationId, IsSOTrx: true, C_DocTypeTarget_ID: 1000032, // Standard Order DateOrdered: new Date().toISOString().split('T')[0], DatePromised: new Date().toISOString().split('T')[0], DateAcct: new Date().toISOString().split('T')[0], PeriodPerformanceDate: lastDayOfMonth, // Set to last day of the month Description: `${t.fulfillment_invoice} ${monthName} ${group.year}`, SalesRep_ID: 1000004, // Default sales rep C_BPartner_ID: group.bpartnerId, C_PaymentTerm_ID: paymentTermId, // From partner or default "Immediate" C_Currency_ID: 102, // EUR M_PriceList_ID: priceListId, // From partner or default M_Warehouse_ID: warehouseId, // From partner or default PaymentRule: 'P', // On Credit DeliveryRule: 'F', // Force DeliveryViaRule: 'D', // Delivery FreightCostRule: 'I', // Freight included InvoiceRule: 'D', // After Delivery PriorityRule: '5', // Medium IsTaxIncluded: false, c_orderline: orderLines.map(line => ({ Line: line.Line, AD_Org_ID: organizationId, M_Product_ID: line.productId, C_UOM_ID: 100, // Each QtyEntered: line.QtyEntered, QtyOrdered: line.QtyOrdered, PriceEntered: line.PriceEntered, PriceActual: line.PriceActual, C_Tax_ID: taxId, // Use tax exempt rate (1000001) if partner is tax exempt, otherwise 19% Mwst. (1000000) M_Warehouse_ID: warehouseId, ...(line.Description ? { Description: line.Description } : {}) })) } // Add business partner location if available if (bpLocation?.id) { orderPayload.C_BPartner_Location_ID = bpLocation.id orderPayload.Bill_Location_ID = bpLocation.id } // Add business partner user if available if (bpUser?.id) { orderPayload.AD_User_ID = bpUser.id orderPayload.Bill_User_ID = bpUser.id } // Add bill partner (same as main partner) orderPayload.Bill_BPartner_ID = group.bpartnerId // Log the full request payload for debugging console.log('═══════════════════════════════════════════════════') console.log('CREATE ORDER REQUEST PAYLOAD') console.log('═══════════════════════════════════════════════════') console.log('Partner ID:', group.bpartnerId) console.log('Month/Year:', group.monthYear) console.log('Full Payload:', JSON.stringify(orderPayload, null, 2)) console.log('═══════════════════════════════════════════════════') const order: any = await fetchHelper(event, 'models/c_order', 'POST', token, orderPayload) console.log('ORDER CREATION RESPONSE:', JSON.stringify(order, null, 2)) if (order && order.id) { // Mark all lines as processed using SQL batch update (only in non-preview mode) if (!isPreview) { try { const lineIds = group.lines.map((line: any) => line.id) // Use batch SQL update endpoint - set a_processed='Y' and c_order_refinv_id await $fetch('/api/fulfillment/batch-process-lines', { method: 'POST', body: { lineIds, orderId: order.id } }) console.log(`Batch updated ${group.lines.length} fee lines for order ${order.id}, linked to c_order_refinv_id`) } catch (updateError: any) { console.error('Batch update error:', updateError) data.errors.push({ bpartnerId: group.bpartnerId, error: `Order created (ID: ${order.id}) but failed to mark lines as processed`, details: { orderId: order.id, orderDocumentNo: order.DocumentNo, totalLines: group.lines.length, error: updateError.message || String(updateError) } }) } } // Build order lines with product names for response const responseOrderLines: any[] = [] for (const productId in fulfillmentLines) { const lineData = fulfillmentLines[productId] const orderCount = lineData.inoutIds.size const positionCount = lineData.details.length responseOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Fulfillment: ${orderCount} Order${orderCount > 1 ? 's' : ''} and ${positionCount} position${positionCount > 1 ? 's' : ''}`, qtyOrdered: 1, priceActual: lineData.totalAmt, lineNetAmt: lineData.totalAmt }) } for (const productId in returnLines) { const lineData = returnLines[productId] const orderCount = lineData.inoutIds.size const positionCount = lineData.details.length responseOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Return: ${orderCount} Order${orderCount > 1 ? 's' : ''} and ${positionCount} position${positionCount > 1 ? 's' : ''}`, qtyOrdered: 1, priceActual: lineData.totalAmt, lineNetAmt: lineData.totalAmt }) } for (const productId in spaceRentLines) { const lineData = spaceRentLines[productId] responseOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Warehouse Rent: ${lineData.qtyInvoiced.toFixed(2)} qm³`, qtyOrdered: 1, priceActual: lineData.totalAmt, lineNetAmt: lineData.totalAmt }) } for (const productId in spaceRentFlatLines) { const lineData = spaceRentFlatLines[productId] const flatQty = lineData.qtyInvoiced const avgPrice = flatQty > 0 ? lineData.totalAmt / flatQty : 0 responseOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Warehouse Rent (Flat): ${flatQty.toFixed(2)} units`, qtyOrdered: flatQty, priceActual: avgPrice, lineNetAmt: lineData.totalAmt }) } for (const productId in parcelLines) { const lineData = parcelLines[productId] const parcelQty = lineData.qtyInvoiced const avgPrice = parcelQty > 0 ? lineData.totalAmt / parcelQty : 0 responseOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Parcel: ${parcelQty.toFixed(0)} parcel${parcelQty !== 1 ? 's' : ''}`, qtyOrdered: parcelQty, priceActual: avgPrice, lineNetAmt: lineData.totalAmt }) } for (const productId in subscriptionLines) { const lineData = subscriptionLines[productId] const subscriptionQty = lineData.qtyInvoiced const avgPrice = subscriptionQty > 0 ? lineData.totalAmt / subscriptionQty : 0 responseOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Subscription: ${subscriptionQty.toFixed(2)} unit${subscriptionQty !== 1 ? 's' : ''}`, qtyOrdered: subscriptionQty, priceActual: avgPrice, lineNetAmt: lineData.totalAmt }) } for (const productId in shippingFeeLines) { const lineData = shippingFeeLines[productId] const shippingFeeQty = lineData.qtyInvoiced const avgPrice = shippingFeeQty > 0 ? lineData.totalAmt / shippingFeeQty : 0 responseOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Shipping Fee: ${shippingFeeQty.toFixed(2)} unit${shippingFeeQty !== 1 ? 's' : ''}`, qtyOrdered: shippingFeeQty, priceActual: avgPrice, lineNetAmt: lineData.totalAmt }) } // Generate PDF report if requested if (shouldCreateReport && order.id && order.DocumentNo) { try { console.log(`Generating PDF report for order ${order.DocumentNo}...`) // Prepare report data with full details const reportOrderLines: any[] = [] for (const productId in fulfillmentLines) { const lineData = fulfillmentLines[productId] reportOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Fulfillment: ${lineData.inoutIds.size} shipment${lineData.inoutIds.size > 1 ? 's' : ''}`, qtyOrdered: 1, priceActual: lineData.totalAmt, lineNetAmt: lineData.totalAmt, type: 'fulfillment', details: lineData.details }) } for (const productId in returnLines) { const lineData = returnLines[productId] reportOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Return: ${lineData.inoutIds.size} return${lineData.inoutIds.size > 1 ? 's' : ''}`, qtyOrdered: 1, priceActual: lineData.totalAmt, lineNetAmt: lineData.totalAmt, type: 'return', details: lineData.details }) } for (const productId in spaceRentLines) { const lineData = spaceRentLines[productId] reportOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Warehouse Rent: ${lineData.qtyInvoiced.toFixed(2)} qm³`, qtyOrdered: 1, priceActual: lineData.totalAmt, lineNetAmt: lineData.totalAmt, type: 'spacerentqm3', details: lineData.details }) } for (const productId in spaceRentFlatLines) { const lineData = spaceRentFlatLines[productId] const flatQty = lineData.qtyInvoiced const avgPrice = flatQty > 0 ? lineData.totalAmt / flatQty : 0 reportOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Warehouse Rent (Flat): ${flatQty.toFixed(2)} units`, qtyOrdered: flatQty, priceActual: avgPrice, lineNetAmt: lineData.totalAmt, type: 'spacerentflat', details: lineData.details }) } for (const productId in parcelLines) { const lineData = parcelLines[productId] const parcelQty = lineData.qtyInvoiced const avgPrice = parcelQty > 0 ? lineData.totalAmt / parcelQty : 0 reportOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Parcel: ${parcelQty.toFixed(0)} parcel${parcelQty !== 1 ? 's' : ''}`, qtyOrdered: parcelQty, priceActual: avgPrice, lineNetAmt: lineData.totalAmt, type: 'parcel', details: lineData.details }) } for (const productId in subscriptionLines) { const lineData = subscriptionLines[productId] const subscriptionQty = lineData.qtyInvoiced const avgPrice = subscriptionQty > 0 ? lineData.totalAmt / subscriptionQty : 0 reportOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Subscription: ${subscriptionQty.toFixed(2)} unit${subscriptionQty !== 1 ? 's' : ''}`, qtyOrdered: subscriptionQty, priceActual: avgPrice, lineNetAmt: lineData.totalAmt, type: 'subscription', details: lineData.details }) } for (const productId in shippingFeeLines) { const lineData = shippingFeeLines[productId] const shippingFeeQty = lineData.qtyInvoiced const avgPrice = shippingFeeQty > 0 ? lineData.totalAmt / shippingFeeQty : 0 reportOrderLines.push({ productId: parseInt(productId), productName: lineData.productName, description: `Shipping Fee: ${shippingFeeQty.toFixed(2)} unit${shippingFeeQty !== 1 ? 's' : ''}`, qtyOrdered: shippingFeeQty, priceActual: avgPrice, lineNetAmt: lineData.totalAmt, type: 'shippingfee', details: lineData.details }) } // Query storage usage via API if customer has isAccountingWarehouseVolume = true let storageUsageData: any[] = [] const hasSpaceRentQm3 = reportOrderLines.some(line => line.type === 'spacerentqm3') let storageUsageDebug: any = null if (hasSpaceRentQm3 && bpartner.isAccountingWarehouseVolume === true) { try { const [year, month] = group.monthYear.split('-') const startDate = `${year}-${month.padStart(2, '0')}-01` const endDate = new Date(parseInt(year), parseInt(month), 0).toISOString().split('T')[0] // IMPORTANT: Resolve BPartner strictly from the created Sales Order (no fallback) // - Non-preview (real order): use only order.C_BPartner_ID.id; if missing, SKIP fetching storage usage // - Preview: no real order exists, so use the group's BPartner ONLY for preview/debug purposes let usedBPartnerId: number | null = null let usedBpSource: 'order_bpartner' | 'preview_group_bpartner' | 'unknown' = 'unknown' if (!isPreview) { usedBPartnerId = order?.C_BPartner_ID?.id ?? null usedBpSource = 'order_bpartner' } else { usedBPartnerId = group.bpartnerId ?? null usedBpSource = 'preview_group_bpartner' } let customerOrgId: number | null = null try { if (usedBPartnerId) { // Query ad_org endpoint to get the organization ID for this business partner const orgFilter = encodeURIComponent(`C_BPartner_ID eq ${usedBPartnerId}`) const orgSelect = encodeURIComponent('ad_org_id') const orgUrl = `models/ad_org?$filter=${orgFilter}&$select=${orgSelect}` const orgResult: any = await event.context.fetch( orgUrl, 'GET', token, null ) const orgRecords: any[] = Array.isArray(orgResult?.records) ? orgResult.records : Array.isArray(orgResult) ? orgResult : [] if (orgRecords.length > 0) { customerOrgId = orgRecords[0].id ?? orgRecords[0].ad_org_id ?? orgRecords[0].AD_Org_ID ?? null } } } catch (bpErr) { console.error(`Failed to resolve ad_org_id for BPartner ${usedBPartnerId}:`, bpErr) } if (!customerOrgId) { console.warn(`Skipping storage usage fetch: ${usedBpSource === 'order_bpartner' ? 'Order' : 'Preview group'} BPartner ${usedBPartnerId} has no AD_Org_ID`) if (debugStorageUsage) { storageUsageDebug = { enabled: true, gatedBy: 'missing_customer_org', hasSpaceRentQm3, isAccountingWarehouseVolume: bpartner.isAccountingWarehouseVolume === true, usedBPartnerId, customerOrgId: null, customerOrgIdUsedSource: usedBpSource, startDate, endDate } } } else { // Build OData query to match requested behavior // Example: /models/cust_storage_usage?$filter=ad_org_id eq 1000005 AND DateAcct ge 2025-10-01 AND DateAcct le 2025-10-31 const filter = encodeURIComponent(`ad_org_id eq ${customerOrgId} AND DateAcct ge ${startDate} AND DateAcct le ${endDate}`) const select = encodeURIComponent('CUST_Storage_Usage_ID,DateAcct,Volume,Qty,ProductValue,Product_Name,Volume_Single,Volume_Single_Price,Volume_Price') const orderby = encodeURIComponent('DateAcct,Product_Name') const url = `models/cust_storage_usage?$select=${select}&$filter=${filter}&$orderby=${orderby}` const apiResult: any = await event.context.fetch( url, 'GET', token, null ) const records: any[] = Array.isArray(apiResult?.records) ? apiResult.records : Array.isArray(apiResult) ? apiResult : [] // Normalize field names expected by the PDF generator (lowercase, underscores) storageUsageData = records.map((r: any) => ({ cust_storage_usage_id: r.CUST_Storage_Usage_ID ?? r.cust_storage_usage_id ?? r.id, dateacct: r.DateAcct ?? r.dateacct, volume: r.Volume ?? r.volume ?? 0, qty: r.Qty ?? r.qty ?? 0, productvalue: r.ProductValue ?? r.productvalue ?? '', product_name: r.Product_Name ?? r.product_name ?? '', volume_single: r.Volume_Single ?? r.volume_single ?? 0, volume_single_price: r.Volume_Single_Price ?? r.volume_single_price ?? 0, volume_price: r.Volume_Price ?? r.volume_price ?? 0 })) console.log(`Fetched ${storageUsageData.length} storage usage records (API) for org ${customerOrgId} (used BP ${usedBPartnerId})`) if (debugStorageUsage) { storageUsageDebug = { enabled: true, hasSpaceRentQm3, isAccountingWarehouseVolume: bpartner.isAccountingWarehouseVolume === true, usedBPartnerId, customerOrgId, customerOrgIdUsedSource: usedBpSource, startDate, endDate, requestUrl: url, fetchedCount: storageUsageData.length, sample: storageUsageData.slice(0, 3) } } } } catch (err) { console.error('Error fetching storage usage data via API:', err) if (debugStorageUsage) { storageUsageDebug = { enabled: true, hasSpaceRentQm3, isAccountingWarehouseVolume: bpartner.isAccountingWarehouseVolume === true, // Keep any previously set info if available error: err?.message || String(err) } } } } else if (debugStorageUsage) { // Explain why we didn't fetch storageUsageDebug = { enabled: true, hasSpaceRentQm3, isAccountingWarehouseVolume: bpartner.isAccountingWarehouseVolume === true, skippedReason: !hasSpaceRentQm3 ? 'no_spacerentqm3_lines' : 'customer_flag_isAccountingWarehouseVolume_is_false' } } const reportData = { orderId: order.id, documentNo: order.DocumentNo, bpartnerId: group.bpartnerId, bpartnerName: bpartner.Name, monthYear: group.monthYear, totalAmount: totalLines, orderLines: reportOrderLines, storageUsage: storageUsageData } // Call the report generation API const reportResult = await $fetch('/api/fulfillment/generate-fee-report', { method: 'POST', body: { reportData } }) console.log('Report result:', reportResult) if (reportResult.success && reportResult.fileUrl) { console.log(`Report generated successfully: ${reportResult.fileUrl}`) // Update order's Report_Strapi_Reference with the Strapi file reference try { const updateResult = await event.context.fetch( `models/c_order/${order.id}`, 'PUT', token, { Report_Strapi_Reference: reportResult.fileUrl } ) console.log(`Updated order ${order.DocumentNo} Report_Strapi_Reference with report file: ${reportResult.fileUrl}`) console.log('Update result:', updateResult) } catch (updateErr: any) { console.error(`Failed to update Report_Strapi_Reference for order ${order.DocumentNo}:`, updateErr) data.errors.push({ bpartnerId: group.bpartnerId, error: `Order created but failed to update Report_Strapi_Reference`, details: { orderId: order.id, orderDocumentNo: order.DocumentNo, reportUrl: reportResult.fileUrl, updateError: updateErr.message || String(updateErr) } }) } } else { console.error(`Failed to generate report for order ${order.DocumentNo}`) data.errors.push({ bpartnerId: group.bpartnerId, error: `Order created but failed to generate report`, details: { orderId: order.id, orderDocumentNo: order.DocumentNo, reportError: reportResult.error || 'Unknown error' } }) } } catch (reportErr: any) { console.error(`Error generating report for order ${order.DocumentNo}:`, reportErr) data.errors.push({ bpartnerId: group.bpartnerId, error: `Order created but failed to generate report`, details: { orderId: order.id, orderDocumentNo: order.DocumentNo, reportError: reportErr.message || String(reportErr) } }) } } const successPayload: any = { orderId: order.id, documentNo: order.DocumentNo, bpartnerId: group.bpartnerId, bpartnerName: bpartner.Name, monthYear: group.monthYear, linesProcessed: group.lines.length, totalAmount: totalLines, orderLines: responseOrderLines } if (debugStorageUsage) { successPayload.debug = successPayload.debug || {} successPayload.debug.storageUsage = storageUsageDebug } data.success.push(successPayload) } else { data.errors.push({ bpartnerId: group.bpartnerId, error: 'Failed to create order', details: order, payload: orderPayload // Include payload in error response }) } } catch (err: any) { console.error(`ORDER CREATION ERROR for partner ${group.bpartnerId}:`, err) data.errors.push({ bpartnerId: group.bpartnerId, monthYear: group.monthYear, error: err.message || 'Unknown error occurred', errorType: err.name || 'Error', linesCount: group.lines.length, details: { message: err.message, statusCode: err.statusCode, stack: err.stack?.split('\n').slice(0, 3).join('\n') // First 3 lines of stack }, payload: orderPayload // Include payload in error response }) // Continue to next group instead of stopping continue } processedCount++ // Add small delay between groups to prevent connection pool exhaustion if (processedCount < groupKeys.length) { await new Promise(resolve => setTimeout(resolve, 100)) } } // Longer delay between batches if (batchStart + batchSize < groupKeys.length) { console.log('Pausing 500ms between batches...') await new Promise(resolve => setTimeout(resolve, 500)) } } console.log(`Completed processing. Success: ${data.success.length}, Errors: ${data.errors.length}`) return data } export default defineEventHandler(async (event) => { let data: any = { success: [], errors: [], preview: false } try { data = await handleFunc(event) } catch(err: any) { try { let authToken: any = await refreshTokenHelper(event) data = await handleFunc(event, authToken) } catch(error: any) { console.error('Fatal error in generate-orders:', error) // Check if this is a local dev environment error (missing tables) const errorMsg = error.message || String(error) if (errorMsg.includes('cust_fulfillmentfeeline') || errorMsg.includes('No match found for table')) { console.log('Detected local dev environment - returning dummy data') const body = await readBody(event) return { success: [ { bpartnerId: body.partnerId || 1014225, bpartnerName: 'Main Branch', monthYear: `${body.month || 11}/${body.year || 2025}`, orderPreview: { totalAmount: 1500.00, shippingFees: 150.00, parcelFees: 50.00, storageFees: 25.00, lineCount: 5, lines: [ { productName: 'Shipping Fee - November', quantity: 1, price: 150.00, lineTotal: 150.00 }, { productName: 'Parcel Fee', quantity: 5, price: 10.00, lineTotal: 50.00 }, { productName: 'Storage Fee', quantity: 1, price: 25.00, lineTotal: 25.00 } ] } } ], errors: [], preview: body.preview === true, localDevMode: true } } // Return partial results if any were generated if (data.success && data.success.length > 0) { data.errors.push({ error: 'Request partially completed with errors', details: error.message || String(error) }) return data } // Otherwise return error but don't throw data = { success: [], errors: [{ error: 'Failed to generate orders', details: errorMsg }], preview: false } } } return data })