Create Web Service
In this section you'll build the sendMO endpoint: a WebDev doPost resource that receives a payload from the ERP simulator and passes the request body to a shared script function, trainingERP.sendMO(body), which validates the order, creates or updates it, and schedules it. In the next section you'll test the flow end to end.
If you haven't already installed WebDev on your gateway, you'll need to do that first. Otherwise, you can skip this step.
- Navigate to Ignition Downloads
- Select the version of Ignition you are running
- Scroll down to the Ignition Modules section and download the WebDev Module
- Install the module
sendMO WebDev endpoint-
In the Ignition Designer, open the KANOA_SCRIPTS project. This is where the WebDev resource will be created.
-
Navigate to the WebDev section.
-
Create a new folder named
api. -
Inside that folder, create a new Python resource named
sendMO. -
Configure the resource:
- HTTP method:
doPost - Enabled: Yes
- HTTP method:
-
Copy the code block below into the
doPostresource:
doPost code block
'''
This function....
1. Reads the request headers.
2. Reads the raw request body.
3. Passes the body to `trainingERP.sendMO`.
4. Catches unexpected errors.
5. Returns a JSON response to the caller.
'''
headers = request.get("headers", {}) # Get headers safely
content_type = (headers.get("Content-Type") or headers.get("content-type") or "").lower()
body = request.get("data", b"") # Get raw request body
if body is None: body = b""
try:
status, msg = trainingERP.sendMO(body)
except:
return {
"status": 400,
"contentType": "application/json",
"response": system.util.jsonEncode({"ok": False, "msg": "Unknown error"})
}
return {
"status": 200,
"contentType": "application/json",
"response": system.util.jsonEncode({"ok": status, "msg": msg})
}
This creates the endpoint that the ERP simulator calls. WebDev resources are served at http://<gateway>:8088/system/webdev/<project>/<path>, and because kanoaMES inherits from KANOA_SCRIPTS, the endpoint is also available through it. The ERP simulator calls http://localhost:8088/system/webdev/kanoaMES/api/sendMO.
The endpoint should handle only HTTP-specific concerns: the request, the response, the status code, the content type, and basic exception handling. The actual business logic will live in a project script library, which makes it easier to test, reuse, maintain, version, and call from other workflows if needed.
- In the KANOA_SCRIPTS project, create a script library called
trainingERP. - Copy the code block below into the
trainingERPscript library.
trainingERP code block
import java.lang
#################################################################################
def updateWOTag(message, update):
"""
Demo-only helper for the ERP simulator screen.
"""
tagPaths = ['[Kanoa]erpSim/message', '[Kanoa]erpSim/woUpdate']
values = [message, update]
system.tag.writeBlocking(tagPaths, values)
return
#################################################################################
def validateRequiredFields(orderInfo):
"""
Validate the minimum fields needed to create a production order.
"""
requiredFields = ['manufacturingOrder', 'orderQty', 'dueDate', 'status', 'itemName']
missingFields = [field for field in requiredFields if orderInfo.get(field) in [None, '']]
if missingFields:
return False, 'Missing required order fields: %s' % ', '.join(missingFields)
return True, None
#################################################################################
def parseDate(value):
"""
ERP sends dates as text, e.g. '2026-05-31' or '2026-05-14 19:30'. Returns None if unreadable.
"""
for fmt in ['yyyy-MM-dd HH:mm', 'yyyy-MM-dd']:
try:
return system.date.parse(value, fmt)
except:
pass
return None
#################################################################################
def removeFutureBlocks(workOrderId, userId):
"""
Delete this order's schedule blocks that haven't started yet.
Returns the asset ids whose schedule changed, so their screens can be refreshed.
"""
assetIds = set()
scheduleBlocks = system.kanoa.utilities.convertDatasetToJSON(system.kanoa.schedule.getScheduleBlocks({'workOrderId': workOrderId}))
for block in scheduleBlocks:
if block['startDate'] > system.date.now():
system.kanoa.schedule.deleteScheduleBlock(block['scheduleBlockId'], userId)
assetIds.add(block['assetId'])
return assetIds
#################################################################################
def sendMO(body):
"""
ERP-to-Kanoa production order import with scheduling.
Safe to resend: an order that already exists is updated, not duplicated.
- Released orders: create anything missing (unit, order source, item, rate),
add or update the order, and schedule it on the asset whose erpName matches
- Scheduled orders are set to the 'Scheduled' status, if you've created one
- Closed / Completed orders: set the status and remove future schedule blocks
- Other statuses (e.g. Hold): update an existing order's status
"""
log = system.util.getLogger('kanoa_ERP_IF')
utils = system.kanoa.utilities
woName = None
try:
# WebDev passes a dict for JSON requests. Accept a JSON string too.
if isinstance(body, basestring):
body = system.util.jsonDecode(body)
log.info('Received sendMO() POST with %s' % body)
orderInfo = body.get('order') if body else None
if not orderInfo: return False, 'No order information found'
isValid, validationMsg = validateRequiredFields(orderInfo)
if not isValid:
log.warn(validationMsg)
return False, validationMsg
woName = orderInfo['manufacturingOrder'].strip()
woStatus = orderInfo['status']
userId = system.kanoa.security.getIDPUserId({'userName': 'SYSTEM'})
# Look up IDs by name. Database IDs can differ between systems, so never hardcode them.
woStates = utils.convertDatasetToDict(system.kanoa.order.getProductionOrderStates({}), 'workOrderStatusName', 'workOrderStatusId')
woInfo = utils.convertDatasetRowToJSON(system.kanoa.order.getProductionOrders({'workOrderName': woName}), 0)
# 1. Any status other than Released only changes an order we already have.
if woStatus != 'Released':
if not woInfo['workOrderId'] or woStatus not in woStates:
msg = 'Order %s ignored. Status: %s' % (woName, woStatus)
log.info(msg)
return True, msg
system.kanoa.order.updateProductionOrderField(woInfo['workOrderId'], 'workOrderStatusId', woStates[woStatus], userId)
msg = 'Order %s set to %s' % (woName, woStatus)
if woStatus in ['Closed', 'Completed']:
for assetId in removeFutureBlocks(woInfo['workOrderId'], userId):
system.kanoa.schedule.updateClientsScheduleChange(utils.getFieldValue('assetPath', system.kanoa.asset.getAssets({'assetId': assetId})))
msg += ' and future schedule blocks removed'
log.info(msg)
updateWOTag(msg, True)
return True, msg
# 2. Released: create anything the order needs that doesn't exist yet.
reqdQty = float(orderInfo['orderQty'])
uom = (orderInfo.get('uom') or 'units').lower()
itemUnitId = utils.getFieldValue('itemUnitId', system.kanoa.item.getItemUnits({'itemUnitName': uom}))
if itemUnitId is None:
itemUnitId, unitMsg = system.kanoa.item.addItemUnit({'itemUnitName': uom, 'enabled': True}, userId)
workOrderSourceId = utils.getFieldValue('workOrderSourceId', system.kanoa.order.getProductionOrderSource({'workOrderSourceName': 'ERP'}))
if workOrderSourceId is None:
workOrderSourceId, sourceMsg = system.kanoa.order.addProductionOrderSource({'workOrderSourceName': 'ERP', 'enabled': True}, userId)
# orderType arrives as e.g. 'PRODUCTION'. Fall back to the Production mode.
modeId = utils.getFieldValue('modeId', system.kanoa.asset.getModes({'modeName': (orderInfo.get('orderType') or '').title()}))
if modeId is None:
modeId = utils.getFieldValue('modeId', system.kanoa.asset.getModes({'modeName': 'Production'}))
itemName = orderInfo['itemName'].strip()
itemDescription = orderInfo.get('itemDescription')
itemClassPath = orderInfo.get('itemClassPath')
itemClassId = system.kanoa.item.checkItemClassPath(itemClassPath, userId) if itemClassPath else None
itemInfo = utils.convertDatasetRowToJSON(system.kanoa.item.getItems({'itemName': itemName}), 0)
if itemInfo['itemId'] is None:
log.info('Adding new item %s to %s' % (itemName, itemClassPath))
itemInfo['itemId'] = system.kanoa.item.addItem({'itemName': itemName, 'itemDescription': itemDescription, 'itemClassId': itemClassId, 'itemColor': None, 'enabled': True}, userId)
elif itemDescription and itemDescription != itemInfo['itemDescription']:
system.kanoa.item.updateItemField(itemInfo['itemId'], 'itemDescription', itemDescription, userId)
# 3. Find the asset to schedule on: the one whose erpName attribute matches scheduleInfo.assetName.
scheduleInfo = orderInfo.get('scheduleInfo') or {}
assetId = None
if scheduleInfo.get('enabled') and scheduleInfo.get('assetName'):
assetId = utils.getFieldValue('assetId', system.kanoa.asset.getAssetAttributes({'assetAttrName': 'erpName', 'assetAttrValue': scheduleInfo['assetName'].strip()}))
if assetId is None:
log.warn('No asset has erpName %s. Order %s will not be scheduled' % (scheduleInfo['assetName'], woName))
# 4. Add or update the production order.
newInfo = {
'workOrderName': woName, 'modeId': modeId, 'itemId': itemInfo['itemId'], 'itemUnitId': itemUnitId,
'assetId': assetId, 'workOrderSourceId': workOrderSourceId, 'reqdQty': reqdQty,
'dueDate': parseDate(orderInfo['dueDate']), 'workOrderStatusId': woStates['Released'], 'note': orderInfo.get('notes')
}
if assetId and woStates.get('Scheduled'):
newInfo['workOrderStatusId'] = woStates['Scheduled'] # optional: only if a 'Scheduled' status exists
if woInfo['workOrderId']:
newInfo['workOrderId'] = woInfo['workOrderId']
if woInfo.get('workOrderStatusName') == 'In Progress':
newInfo['workOrderStatusId'] = woStates['In Progress'] # don't send a running order back to Released
system.kanoa.order.updateProductionOrder(newInfo, userId)
msg = 'Order %s update received from ERP' % woName
else:
newInfo['workOrderId'] = system.kanoa.order.addProductionOrder(newInfo, userId)
msg = 'New order %s has been received from ERP' % woName
woId = newInfo['workOrderId']
# 5. Schedule it. Replace blocks that haven't started, so a resend moves the order.
changedAssets = removeFutureBlocks(woId, userId)
if assetId:
rateInfo = scheduleInfo.get('rateInfo') or {}
period = (rateInfo.get('period') or 'hour').title()
rate = utils.convertDatasetRowToJSON(system.kanoa.item.getAssetItems({'assetId': assetId, 'itemId': itemInfo['itemId']}), 0)
if rate.get('itemId') is None and rateInfo.get('equipmentRate'):
# No rate for this item on this asset yet: add one from what ERP sent.
rate = {
'itemId': itemInfo['itemId'], 'assetId': assetId,
'standardRate': float(rateInfo['equipmentRate']), 'scheduleRate': float(rateInfo.get('budgetRate') or rateInfo['equipmentRate']),
'itemPeriodId': utils.getFieldValue('itemPeriodId', system.kanoa.item.getItemPeriod({'itemPeriodName': period})),
'itemSourceId': utils.getFieldValue('itemSourceId', system.kanoa.item.getItemSource({'itemSourceName': 'output'})),
'infeedUnitId': itemUnitId, 'outfeedUnitId': itemUnitId, 'wasteUnitId': itemUnitId, 'packageCount': 1.0, 'enabled': True
}
system.kanoa.item.addItemAssetRate(rate, userId)
rate['itemPeriodName'] = period
# Block length = quantity / scheduled rate. Without a rate, schedule 6 hours.
start = parseDate(scheduleInfo.get('startDate')) or system.date.now()
ratePerMinute = rate.get('scheduleRate') or rate.get('standardRate') or 0
if rate.get('itemPeriodName') == 'Hour':
ratePerMinute = ratePerMinute / 60.0
end = system.date.addMinutes(start, int(reqdQty / ratePerMinute)) if ratePerMinute else system.date.addHours(start, 6)
scheduleBlockInfo = {'scheduleBlockName': 'Manufacturing Order', 'assetId': assetId, 'itemId': itemInfo['itemId'], 'workOrderId': woId,
'scheduledQty': reqdQty, 'modeId': modeId, 'startDate': start, 'endDate': end, 'notes': None, 'rruleStr': None, 'color': None}
system.kanoa.schedule.addScheduleBlock(scheduleBlockInfo, userId)
changedAssets.add(assetId)
msg += ' and scheduled'
for changedAssetId in changedAssets:
system.kanoa.schedule.updateClientsScheduleChange(utils.getFieldValue('assetPath', system.kanoa.asset.getAssets({'assetId': changedAssetId})))
# 6. Save what ERP sent as order metadata: the order as apiBody, plus each metaData entry (e.g. machineSetpoints).
metaData = dict(body.get('metaData') or {})
metaData['apiBody'] = orderInfo
existingMeta = utils.convertDatasetToDict(system.kanoa.order.getProductionOrderMetaData({'workOrderId': woId}), 'metaDataName', 'workOrderMetaDataId')
for name, value in metaData.items():
metaInfo = {'workOrderId': woId, 'metaDataName': name, 'metaDataValue': system.util.jsonEncode(value)}
if existingMeta.get(name):
metaInfo['workOrderMetaDataId'] = existingMeta[name]
system.kanoa.order.updateProductionOrderMetaData(metaInfo, userId)
else:
system.kanoa.order.addProductionOrderMetaData(metaInfo, userId)
log.info('%s (workOrderId %s)' % (msg, woId))
updateWOTag(msg, True) # demo only: ERP simulator feedback
return True, msg
except java.lang.Exception, e:
msg = 'Java Exception: Failed to add ERP order %s. %s' % (woName or '<unknown>', e)
log.error(msg)
return False, msg
except Exception, e:
msg = 'Exception: Failed to add ERP order %s. %s' % (woName or '<unknown>', e)
log.error(msg)
return False, msg