需求:
公司需要先在OA上对准备生产的订单进行一次量产评审,所有相关人员评审通过后才可以进行生产,导致下工单的人员每次需要把OA上的信息复制到ERP进行审批。
为什么不直接在ERP上审批呢?
首先该节点涉及到很多不用ERP的用户,为了不增加用户数浪费资源。
其次泛微OA是以表单形式展现,相关人员有对该表单进行受控过,原先是需要线下进行签核表单信息,直接转为线上,支持手机电脑任意方式审批,加快效率。
总体设计思路:
1、开发一个泛微鉴权服务,保证获取的accesstoken是最新的,减少调用数量。
2、开发回调应用服务,对指定流程审核后进行触发,里面有一条判断审批节点的逻辑,原因是泛微的动作流需要收费,免费的只能获取所有回调,不支持指定审批节点回调,这样就导致任何一个节点都会触发一次回调,需要进行判断当前所属节点,如果是指定节点才进行下一步操作。
最终功能:
当OA流程到达指定审批节点后,自动把OA数据写入到云星空ERP的生产工单中。
开发语言采用python ,涉及到FastAPI、json、K3CloudApiSdk等常用库
开发代码,为方便查看和阅读,先硬编码写入JSON模板,以及采用替换方式完成模板替换,没有进行异常处理,后续考虑异常处理添加一个通知到企业微信给指定用户。
代码仅供参考:
#泛微 estams OA
#定时获取accesstoken服务,其他程序通过get获取OA的最新accesstoken
from fastapi import FastAPI, HTTPException
import httpx
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from contextlib import asynccontextmanager
app = FastAPI()
scheduler = AsyncIOScheduler()
# 全局变量来存储 accessToken
access_token = ""
# 配置OA信息
CORP_ID = "公司ID"
APP_KEY = "自建应用key"
APP_SECRET = "自建应用secret"
async def fetch_access_token():
global access_token
# Step 1: 获取code
authorize_url = "https://api.eteams.cn/oauth2/authorize"
params = {
"corpid": CORP_ID,
"response_type": "code",
"state": "lijinbin"
}
async with httpx.AsyncClient() as client:
response = await client.get(authorize_url, params=params)
if response.status_code != 200:
raise HTTPException(status_code=response.status_code, detail=response.text)
data = response.json()
if data["errcode"] != "0":
raise HTTPException(status_code=400, detail=data["errmsg"])
code = data["code"]
# Step 2: 使用code获取accessToken
token_url = "https://api.eteams.cn/oauth2/access_token"
payload = {
"app_key": APP_KEY,
"app_secret": APP_SECRET,
"grant_type": "authorization_code",
"code": code
}
async with httpx.AsyncClient() as client:
response = await client.post(token_url, data=payload)
if response.status_code != 200:
raise HTTPException(status_code=response.status_code, detail=response.text)
data = response.json()
if data["errcode"] != "0":
raise HTTPException(status_code=400, detail=data["errmsg"])
access_token = data["accessToken"]
@asynccontextmanager
async def lifespan(app: FastAPI):
# 在启动时获取一次 accessToken
await fetch_access_token()
# 定时任务每7100秒(略少于7200秒)更新一次 accessToken
scheduler.add_job(fetch_access_token, 'interval', seconds=7100)
scheduler.start()
yield
scheduler.shutdown()
app = FastAPI(lifespan=lifespan)
#通过get返回最新的accesstoken
@app.get("/get_access_token")
async def get_access_token():
return {"accessToken": access_token}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=5002)
#读取OA流程数据,然后写入信息到金蝶ERP
from fastapi import FastAPI, Request, HTTPException
import httpx
from k3cloud_webapi_sdk.main import K3CloudApiSdk
import json, uuid
from datetime import datetime
app = FastAPI()
access_token = ""
api_sdk = K3CloudApiSdk()
api_sdk.InitConfig('填写acct_id', '用户名', '填写app_id',
'填写app_secret')
# 订单量产评审的json模板
ddlcps_json = '''{
"NeedUpDateFields": [],
"NeedReturnFields": [],
"IsDeleteEntry": "true",
"SubSystemId": "",
"IsVerifyBaseDataField": "false",
"IsEntryBatchFill": "true",
"ValidateFlag": "true",
"NumberSearch": "true",
"IsAutoAdjustField": "false",
"InterationFlags": "",
"IgnoreInterationFlag": "",
"IsControlPrecision": "false",
"ValidateRepeatJson": "false",
"Model": {
"FID": 0,
"FBillType": {
"FNUMBER": "SCDD05_SYS"
},
"FDate": "file_单据日期",
"FPrdOrgId": {
"FNumber": "100"
},
"FOwnerTypeId": "BD_OwnerOrg",
"FIsRework": false,
"FBusinessType": "1",
"FTrustteed": false,
"FIsEntrust": false,
"FPPBOMType": "1",
"FIssueMtrl": false,
"FDescription": "file_备注",
"F_SLJH_Text_qtr": "file_OA流程",
"FIsQCMO": false,
"FTreeEntity": [
{
"FProductType": "1",
"FMaterialId": {
"FNumber": "file_物料编码"
},
"FWorkShopID": {
"FNumber": "file_生产车间"
},
"FUnitId": {
"FNumber": "Pcs"
},
"FQty": file_数量,
"FYieldQty": file_数量,
"FPlanStartDate": "file_计划开始时间",
"FPlanFinishDate": "file_计划完成时间",
"FRequestOrgId": {
"FNumber": "100"
},
"FISBACKFLUSH": true,
"FStockInOrgId": {
"FNumber": "100"
},
"FBaseYieldQty": file_数量,
"FReqType": "1",
"FPriority": 0,
"FSrcBillType": "",
"FSTOCKREADY": 0.0,
"FBaseStockReady": 0.0,
"FBaseRepairQty": 0.0,
"FRepairQty": 0.0,
"FBaseStockInScrapSelQty": 0.0,
"FStockInScrapSelQty": 0.0,
"FBaseStockInScrapQty": 0.0,
"FStockInScrapQty": 0.0,
"FBaseRptFinishQty": 0.0,
"FRptFinishQty": 0.0,
"FStockInFailSelAuxQty": 0.0,
"FStockInUlRatio": 0.0,
"FInStockOwnerTypeId": "BD_OwnerOrg",
"FBaseStockInLimitH": file_数量,
"FInStockOwnerId": {
"FNumber": "100"
},
"FSrcBillNo": "",
"FStockInLlRatio": 0.0,
"FCheckProduct": true,
"FBaseStockInLimitL": file_数量,
"FBaseUnitQty": file_数量,
"FRepQuaSelAuxQty": 0.0,
"FRepQuaAuxQty": 0.0,
"FRepFailSelAuxQty": 0.0,
"FRoutingId": {
"FNumber": "file_工艺路线"
},
"FRepFailAuxQty": 0.0,
"FStockInQuaAuxQty": 0.0,
"FStockInQuaSelAuxQty": 0.0,
"FStockInReMadeSelQty": 0.0,
"FStockInFailAuxQty": 0.0,
"FStockInQuaSelQty": 0.0,
"FStockInQuaQty": 0.0,
"FBaseUnitId": {
"FNumber": "Pcs"
},
"FStockInFailSelQty": 0.0,
"FStockInFailQty": 0.0,
"FRepQuaSelQty": 0.0,
"FRepQuaQty": 0.0,
"FStockInLimitH": file_数量,
"FRepFailSelQty": 0.0,
"FRepFailQty": 0.0,
"FStockInLimitL": file_数量,
"FOperId": 0,
"FCostRate": 100.0,
"FCreateType": "1",
"FYieldRate": 100.0,
"FGroup": 1,
"FNoStockInQty": file_数量,
"FRowExpandType": 0,
"FBaseNoStockInQty": file_数量,
"FRowId": "e3ec95fb-e4c0-94d4-11ef-635a7638364c",
"FScheduleSeq": 0.0,
"FSNQty": 0.0,
"FScheduleProcSplit": 0,
"FReStkQuaQty": 0.0,
"FBaseReStkQuaQty": 0.0,
"FReStkFailQty": 0.0,
"FBaseReStkFailQty": 0.0,
"FReStkScrapQty": 0.0,
"FBaseReStkScrapQty": 0.0,
"FReStkReMadeQty": 0.0,
"FStockInReMadeQty": 0.0,
"FBaseReStkReMadeQty": 0.0,
"FScheduleStatus": "1",
"FPickMtrlStatus": "1",
"FISNEWLC": 0,
"FSrcSplitSeq": 0,
"FSrcSplitEntryId": 0,
"FSrcSplitId": 0,
"FSRCBOMENTRYID": 0,
"FMOChangeFlag": false,
"FBaseStockInReMadeSelQty": 0.0,
"FIsFirstInspect": false,
"FFirstInspectStatus": "A",
"FBaseSampleDamageQty": 0.0,
"FSampleDamageQty": 0.0,
"FISENABLESCHEDULE": false,
"FPPBOMENTRYID": 0,
"FBOMENTRYID": 0,
"FISMRPCAL": false,
"F_ora_Text_re5": "file_客户订单号",
"FIsMRP": false,
"F_SLJH_Text_83g": "file_客户物料号",
"FFirstQCControlType": "0",
"FIsGenerateOrder": false,
"FBaseDirectStockInQuaQty": 0.0,
"FDirectStockInQuaQty": 0.0,
"FBaseDirectPickMtrlSelQty": 0.0,
"FDirectPickMtrlSelQty": 0.0
}
]
}
}'''
#泛微OA回调服务,只要流程节点审批后就回调信息
@app.get("/callback")
async def receive_callback(request: Request):
params = request.query_params
id_value = params.get("id")
if not id_value:
raise HTTPException(status_code=400, detail="Missing 'id' parameter")
print(id_value) # 流程ID
await get_acc_token()
await read_lc_info(id_value)
return {"message": "Callback received successfully", "params": dict(params)}
async def get_acc_token():
global access_token
print('准备读取token')
#这个地址就是我挂accesstoken服务的地址,使用get获取最新的accesstoken
url = "http://192.168.0.121:5002/get_access_token"
async with httpx.AsyncClient() as client:
response = await client.get(url)
if response.status_code != 200:
raise HTTPException(status_code=response.status_code, detail=response.text)
data = response.json()
access_token = data["accessToken"]
print('读取到token:' + access_token)
#读取流程信息json信息
async def read_lc_info(id):
url = "https://api.eteams.cn/workflow/v2/getInfoByID"
params = {
"access_token": access_token,
"userid": "OA的用户ID",
"id": id
}
async with httpx.AsyncClient() as client:
response = await client.get(url, params=params)
if response.status_code != 200:
raise HTTPException(status_code=response.status_code, detail=response.text)
data = response.json()
print(data)
# 读取当前节点
data_details = str(data['flowRequest']['currentNode'])
print(data_details)
# 如果当前节点等于供应链管理部,就执行插入工单到金蝶
if data_details == '供应链管理部':
print('节点正确,执行入到金蝶生产工单')
await read_lc_file(data)
#通过物料读取上一次的历史信息,方便判断这个物料的生产车间和工艺路线
async def select_FWorkShopID(FMaterial_Id):
# FBillNo 订单号
# FMaterialId.FNumber 物料编码
# FWorkShopID.FNumber 生产车间编码
# FRoutingId.FName 工艺路线
#FRoutingId.FNumber 工艺路线编码
# "FieldKeys": "FBillNo,FMaterialId.FNumber,FWorkShopID.FNumber,FRoutingId.FNumber",
para1 = {
"FormId": "PRD_MO",
"FieldKeys": "FBillNo,FMaterialId.FNumber,FWorkShopID.FNumber,FRoutingId.FNumber",
"FilterString": f"FMaterialId.FNumber='{FMaterial_Id}' and FDocumentStatus='C' O",
"OrderString": "FDate desc",
"TopRowCount": 0,
"StartRow": 0,
"Limit": 10,
"SubSystemId": ""
}
response = api_sdk.ExecuteBillQuery(para1)
#如果没有物料数据的情况下,默认生产车间和工艺路线
if not response:
#生产车间、工艺路线
return 'BM000013', 'RT000006'
data = json.loads(response)
gylx_id = data[0][2]
cj_id = data[0][3]
# 工艺路线、生产车间
return gylx_id,cj_id
# 读取流程里面的json字段信息
async def read_lc_file(lc_file):
# 读取流程明细字段
lc_mx = lc_file['formData']['dataDetails']
lc_name = str(lc_file['flowRequest']['name'])
# 读取插入金蝶的模板
template = ddlcps_json
current_date = datetime.now().strftime('%Y-%m-%d')
template = template.replace('file_单据日期', current_date)
erp_number=str(lc_mx.get('ERP编号', None))
#读取该物料上一次的工艺路线,车间和工艺路线
cj,gylx=await select_FWorkShopID(erp_number)
#一步步替换
template = template.replace('file_生产车间', str(cj)) # 默认半自动
template = template.replace('file_计划开始时间', current_date)
template = template.replace('file_计划完成时间', lc_mx.get('要求交货日期', None))
template = template.replace('file_物料编码', erp_number)
template = template.replace('file_客户订单号', lc_mx.get('客户订单编号', None))
template = template.replace('file_备注', lc_mx.get('客户', None) + "___" + lc_mx.get('软件型号', None))
template = template.replace('file_数量', lc_mx.get('订单数量', None))
template = template.replace('file_工艺路线', str(gylx))
template = template.replace('file_OA流程', lc_name)
rowid = uuid.uuid4()
template = template.replace('file_uuid', str(rowid))
# 判断客户的物料号是否存在,如果存在才进行插入,否者留空
if '客户物料编码' in lc_mx:
template = template.replace('file_客户物料号', lc_mx.get('客户物料编码', None))
else:
template = template.replace('file_客户物料号', '')
print('这是预备插入到金蝶的数据:' + template)
#插入到金蝶的生产订单中暂存
await insert_h_po_order(template)
# 创建金蝶生产工单并暂存
async def insert_h_po_order(gd_info):
formId = "PRD_MO"
# 调用接口
response = api_sdk.Draft(formId, gd_info)
print("接口返回结果:" + response)
# 对返回结果进行解析和校验
res = json.loads(response)
is_success = res["Result"]["ResponseStatus"]["IsSuccess"]
print(type(is_success)) # 打印出值和类型以便进一步分析
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="192.168.0.121", port=18809)