import refreshTokenHelper from "../../../utils/refreshTokenHelper" import forceLogoutHelper from "../../../utils/forceLogoutHelper" import errorHandlingHelper from "../../../utils/errorHandlingHelper" import fetchHelper from "../../../utils/fetchHelper" import { createProductionTicket } from "../../../utils/productionTicket" // Creates one m_production DRAFT header per selected order line (org + warehouse // taken from the order, default locator of that warehouse, BOM & doctype resolved // server-side), then runs the iDempiere "Create Production" process // (m_production_create) to generate the production lines. The document is // intentionally NOT completed — assembly is confirmed later. const handleFunc = async (event: any, authToken: any = null) => { let data: any = {} const token = authToken ?? await getTokenHelper(event) const body = await readBody(event) const items: any[] = body?.items || [] if(items.length === 0) { return { status: 422, message: 'No items to create productions for', results: [] } } // Pre-flight BEFORE any write: resolves the doctype AND surfaces auth errors so // the refresh-token retry can never re-run after headers were already created. const doctypeRes: any = await fetchHelper(event, `models/c_doctype?$filter=${encodeURIComponent("DocBaseType eq 'MMP' and IsActive eq true")}&$top=10`, 'GET', token, null) const doctypes: any[] = doctypeRes?.records || [] if(doctypes.length === 0) { return { status: 422, message: 'No document type with DocBaseType MMP (Material Production) found', results: [] } } const today = new Date().toISOString().slice(0, 10) const locatorCache: any = {} const ticketCache: any = {} const results: any[] = [] for(const item of items) { const result: any = { orderLineId: item.orderLineId } try { // Order line + full order → org, warehouse, partner, open qty const line: any = await fetchHelper(event, `models/c_orderline/${item.orderLineId}?$expand=C_Order_ID`, 'GET', token, null) const order = line?.C_Order_ID if(!order) throw new Error(`Order line ${item.orderLineId} has no order`) const organizationId = order.AD_Org_ID?.id const warehouseId = order.M_Warehouse_ID?.id const productId = line.M_Product_ID?.id const openQty = Number(line.QtyOrdered ?? 0) - Number(line.QtyDelivered ?? 0) const qty = Number(item.qty || openQty) result.orderId = order.id result.orderDocumentNo = order.DocumentNo result.productId = productId result.productName = line.M_Product_ID?.identifier || '' result.qty = qty if(!(qty > 0)) throw new Error('Quantity to produce must be greater than 0') // Default locator of the order's warehouse (mostly 0,0,0), fallback: first active let locator = locatorCache[warehouseId] if(!locator) { const locRes: any = await fetchHelper(event, `models/m_locator?$filter=${encodeURIComponent(`M_Warehouse_ID eq ${warehouseId} and IsDefault eq true and IsActive eq true`)}&$top=1`, 'GET', token, null) locator = locRes?.records?.[0] if(!locator) { const anyLocRes: any = await fetchHelper(event, `models/m_locator?$filter=${encodeURIComponent(`M_Warehouse_ID eq ${warehouseId} and IsActive eq true`)}&$top=1`, 'GET', token, null) locator = anyLocRes?.records?.[0] } if(!locator) throw new Error(`No active locator found for warehouse ${order.M_Warehouse_ID?.identifier || warehouseId}`) locatorCache[warehouseId] = locator } // Current BOM & Formula of the product (same resolution as the process default: // BOMType 'A', BOMUse 'A', prefer the record of the order's org over org 0) const bomRes: any = await fetchHelper(event, `models/pp_product_bom?$filter=${encodeURIComponent(`M_Product_ID eq ${productId} and BOMType eq 'A' and BOMUse eq 'A' and IsActive eq true`)}&$top=10`, 'GET', token, null) const boms: any[] = bomRes?.records || [] const bom = boms.find((b: any) => Number(b.AD_Org_ID?.id) === Number(organizationId)) ?? boms.find((b: any) => Number(b.AD_Org_ID?.id) === 0) ?? boms[0] if(!bom) throw new Error(`No active BOM & Formula found for product ${line.M_Product_ID?.identifier || productId}`) const doctype = doctypes.find((d: any) => Number(d.AD_Org_ID?.id) === Number(organizationId)) ?? doctypes.find((d: any) => Number(d.AD_Org_ID?.id) === 0) ?? doctypes[0] // 1. Production header (draft) let newObjValue: any = {} if(order.C_BPartner_ID?.id) { newObjValue = {...newObjValue, C_BPartner_ID: { id: order.C_BPartner_ID.id, tableName: 'C_BPartner' } } } if(order.DatePromised) { newObjValue = {...newObjValue, datePromised: String(order.DatePromised).slice(0, 10)} } const descriptionParts = [`Order ${order.DocumentNo} / Line ${line.Line}`] if(item.description) descriptionParts.push(item.description) const production: any = await fetchHelper(event, 'models/m_production', 'POST', token, { AD_Org_ID: { id: organizationId, tableName: 'AD_Org' }, M_Product_ID: { id: productId, tableName: 'M_Product' }, M_Locator_ID: { id: locator.id, tableName: 'M_Locator' }, PP_Product_BOM_ID: { id: bom.id, tableName: 'PP_Product_BOM' }, C_DocType_ID: { id: doctype.id, tableName: 'C_DocType' }, C_OrderLine_ID: { id: line.id, tableName: 'C_OrderLine' }, movementDate: item.movementDate || today, productionQty: qty, description: descriptionParts.join(' — '), ...newObjValue, tableName: 'm_production' }) if(!production?.id) throw new Error('Production header could not be created') result.productionId = production.id result.productionDocumentNo = production.DocumentNo // 2. "Create Production" process — generates the production lines from the BOM. // The document stays in DRAFT (no completion here). const processRes: any = await fetchHelper(event, 'processes/m_production_create', 'POST', token, { 'record-id': production.id, 'model-name': 'm_production', Recreate: 'Y', ProductionQty: qty, PP_Product_BOM_ID: bom.id }) result.processSummary = processRes?.summary || '' if(processRes?.isError) { throw new Error(processRes?.summary || 'Create Production process failed') } // 3. Created production lines (end product + components) for the response const linesRes: any = await fetchHelper(event, `models/m_productionline?$filter=${encodeURIComponent(`M_Production_ID eq ${production.id}`)}&$orderby=Line&$top=500`, 'GET', token, null) result.lines = (linesRes?.records || []).map((l: any) => ({ id: l.id, line: l.Line, productId: l.M_Product_ID?.id || '', product: l.M_Product_ID?.identifier || '', locator: l.M_Locator_ID?.identifier || '', movementQty: Number(l.MovementQty ?? 0), isEndProduct: l.IsEndProduct === true })) result.linesCreated = result.lines.filter((l: any) => !l.isEndProduct).length // 4. Tracking ticket (r_request) — links the production, the sales order // and the merchant org; C_BPartner is the org-linked partner. Fail-soft: // a ticket error must never fail the already-created production. try { const ticket: any = await createProductionTicket(event, token, { organizationId, productionId: production.id, productionDocumentNo: production.DocumentNo, productId, productName: line.M_Product_ID?.identifier || '', qty, orderId: order.id, orderDocumentNo: order.DocumentNo }, ticketCache) result.requestId = ticket.id result.requestDocumentNo = ticket.DocumentNo } catch(ticketErr: any) { result.requestError = ticketErr?.data?.detail || ticketErr?.data?.message || ticketErr?.message || 'Ticket creation failed' } result.status = 200 } catch(err: any) { result.status = 422 result.message = err?.data?.detail || err?.data?.message || err?.message || 'Unknown error' } results.push(result) } const failed = results.filter((r: any) => Number(r.status) !== 200).length data = { status: 200, message: failed > 0 ? `${failed} of ${results.length} production(s) failed` : '', results } return data } export default defineEventHandler(async (event) => { let data: any = {} try { data = await handleFunc(event) } catch(err: any) { try { let authToken: any = await refreshTokenHelper(event) data = await handleFunc(event, authToken) } catch(error) { data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) forceLogoutHelper(event, data) } } return data })