mirror of
https://github.com/rnvm9wjdtj-bot/myaps_api.git
synced 2026-06-02 05:54:40 +00:00
feat(ZEST): 新增ZEST模块的测试配置与客户端实现
新增了test.json环境配置文件,包含ERP、MES、SRM等系统的连接参数与默认配置;同时创建了client.py实现APS相关的库存刷新、SAP交互、定时任务等核心功能逻辑
This commit is contained in:
@@ -0,0 +1,341 @@
|
|||||||
|
"""测试,基于海达"""
|
||||||
|
|
||||||
|
from re import A
|
||||||
|
import requests, uuid, asyncio, json#, logging#, os, atexit
|
||||||
|
import pandas as pd
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Dict, Union
|
||||||
|
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from fastapi import status
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from core.settings import MYAPS_DB_SET, MYAPS_MAIN_DB, THIS_BASE_URL, SCHEDULER_HOUR
|
||||||
|
from .._base import (
|
||||||
|
get_scheduler_minute, async_rate_limit, CacheItem,
|
||||||
|
ApsPayloadSponsor, EventResultPoster, CLIENT_LOGGER, standard_response, get_session, event_batch_handler,
|
||||||
|
cron_task, add_basic_auth_requests, db_delete, db_bupsert, db_query, PROJECT_JSON_FILE, pdv,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#################################################################################
|
||||||
|
# ⬇️对象及项目参数
|
||||||
|
#################################################################################
|
||||||
|
|
||||||
|
erp = PROJECT_JSON_FILE.get("erp", {})
|
||||||
|
sap_url1 = erp.get("base_url", "") + '/zrestful_test2?sap-client=800' # 库存
|
||||||
|
sap_url2 = erp.get("base_url", "") + '/zrestful_plan?sap-client=' + erp.get("sap-client") # 计划
|
||||||
|
werks = erp.get("werks", "")
|
||||||
|
sap_username = erp.get("username", "")
|
||||||
|
sap_password = erp.get("password", "")
|
||||||
|
# 创建requests会话
|
||||||
|
sap_session = get_session(allowed_methods=["GET", "POST"])
|
||||||
|
# 添加Basic认证
|
||||||
|
add_basic_auth_requests(sap_session, sap_username, sap_password)
|
||||||
|
|
||||||
|
# API 超时配置
|
||||||
|
API_TIMEOUT = 30.0 # API 调用超时(秒)
|
||||||
|
|
||||||
|
mes = PROJECT_JSON_FILE.get("mes", {})
|
||||||
|
mes_url = mes.get("base_url", "")
|
||||||
|
|
||||||
|
|
||||||
|
srm = PROJECT_JSON_FILE.get("srm", {})
|
||||||
|
srm_url = srm.get("base_url", "")
|
||||||
|
srm_headers = {
|
||||||
|
"Authorization": srm.get("Authorization", ""),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
srm_session = get_session()
|
||||||
|
srm_session.headers.update(srm_headers)
|
||||||
|
srm_field_map = {
|
||||||
|
"materialno": "material_no", "description": "description", "size": "size",
|
||||||
|
"type": "type", "abc": "abc", "planner": "planner", "datestr": "datestr",
|
||||||
|
"物料来源": "name", "首期库存": "stock_qty", "累计盈余": "cumulative_balance",
|
||||||
|
"期间合计需求": "total_demand", "期间合计供应": "total_supply", "期间盈余": "daily_balance",
|
||||||
|
"期间": "original_datestr", "期间要货数": "current_order_quantity",
|
||||||
|
"期初盈余": "initial_surplus", "期末盈余": "last_surplus", "要求交期": "datestr",
|
||||||
|
}
|
||||||
|
|
||||||
|
#################################################################################
|
||||||
|
# ⬇️项目可复用逻辑
|
||||||
|
#################################################################################
|
||||||
|
|
||||||
|
def sap_post(url: str, session: requests.Session, interface_id: str, data: dict):
|
||||||
|
"""
|
||||||
|
向SAP系统发送POST请求
|
||||||
|
url: 请求URL
|
||||||
|
session: requests会话
|
||||||
|
data: 请求数据
|
||||||
|
"""
|
||||||
|
headers = {
|
||||||
|
"INTF_ID": interface_id,
|
||||||
|
"SRC_SYSTEM": "APS",
|
||||||
|
"DEST_SYSTEM": "SAP",
|
||||||
|
"SRC_MSGID": str(uuid.uuid4()).replace("-", ""),
|
||||||
|
"BACKUP1": "",
|
||||||
|
"BACKUP2": ""
|
||||||
|
}
|
||||||
|
response: requests.Response = session.post(url, headers=headers, json={
|
||||||
|
"HEAD": headers,
|
||||||
|
"BODY": [data]
|
||||||
|
}, timeout=(15, 60))
|
||||||
|
|
||||||
|
response_json = {}
|
||||||
|
if response.status_code == status.HTTP_200_OK:
|
||||||
|
try:
|
||||||
|
response_json = response.json()
|
||||||
|
# CLIENT_LOGGER.success("POST请求", f"状态码{response.status_code}", f"响应{response_json}")
|
||||||
|
except Exception as e:
|
||||||
|
CLIENT_LOGGER.fail("POST请求", f"状态码{response.status_code}", f"解析JSON失败: {str(e)}")
|
||||||
|
CLIENT_LOGGER.fail("POST请求", f"状态码{response.status_code}", f"响应文本: {response.text}")
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
CLIENT_LOGGER.fail("POST请求", f"状态码{response.status_code}", f"响应{response.text}")
|
||||||
|
return {
|
||||||
|
'status_code': response.status_code,
|
||||||
|
'response_text': response.text,
|
||||||
|
'response_json': response_json
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def refresh_stock(dbs: str=MYAPS_DB_SET):
|
||||||
|
"""
|
||||||
|
刷新库存,先清空supply中类型为ST的数据,再从ERP同步1600厂全部库存数据
|
||||||
|
db: 对哪些账套生效,多个账套用逗号分隔
|
||||||
|
"""
|
||||||
|
def get_sap_stock_data():
|
||||||
|
"""
|
||||||
|
从SAP系统获取1600厂全部库存数据
|
||||||
|
"""
|
||||||
|
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
try:
|
||||||
|
sap_stock_response = sap_session.get(url=f"{sap_url1}", headers={'interface': 'stock', 'werks': werks}, timeout=(15, 60)).json()
|
||||||
|
sap_st_data = sap_stock_response.get('data', [])
|
||||||
|
df_sap_st = pd.DataFrame(sap_st_data)
|
||||||
|
df_sap_st = df_sap_st.astype({
|
||||||
|
'werks': 'str',
|
||||||
|
'matnr': 'str',
|
||||||
|
'lgort': 'str',
|
||||||
|
'labst': 'int32',
|
||||||
|
'labst2': 'int32',
|
||||||
|
'charg': 'str'
|
||||||
|
})
|
||||||
|
df_sap_st['avail_qty'] = df_sap_st['labst'] + df_sap_st['labst2']
|
||||||
|
df_sap_st['supplyno'] = df_sap_st['matnr'] + '-' + df_sap_st['werks'] # 注意不要用f string,否则supplyno会变成所有料号的超长字符串
|
||||||
|
df_sap_st['type'] = 'ST'
|
||||||
|
df_sap_st['priority'] = 0
|
||||||
|
df_sap_st['avail_date'] = now
|
||||||
|
df_sap_st['dt_req'] = now
|
||||||
|
df_sap_st['status'] = 'NEW'
|
||||||
|
df_sap_st['category'] = ''
|
||||||
|
df_sap_st['create_date'] = now
|
||||||
|
df_sap_st = (df_sap_st
|
||||||
|
.groupby(['supplyno'], as_index=False)
|
||||||
|
.agg({
|
||||||
|
'matnr': 'first',
|
||||||
|
'avail_qty': 'sum',
|
||||||
|
'type': 'first',
|
||||||
|
'avail_date': 'first',
|
||||||
|
'dt_req': 'first',
|
||||||
|
'priority': 'first',
|
||||||
|
'status': 'first',
|
||||||
|
'category': 'first',
|
||||||
|
'create_date': 'first',
|
||||||
|
}))
|
||||||
|
df_sap_st = df_sap_st.rename(columns={
|
||||||
|
'matnr': 'materialno',
|
||||||
|
})
|
||||||
|
df_sap_st['itemno'] = pdv.ITEMNO
|
||||||
|
except Exception as e:
|
||||||
|
CLIENT_LOGGER.fail("SAP库存获取", "", str(e))
|
||||||
|
raise
|
||||||
|
return df_sap_st
|
||||||
|
|
||||||
|
CLIENT_LOGGER.start("刷新库存任务")
|
||||||
|
mto_vir_st = await ApsPayloadSponsor.mto_workreport_to_virtual_stock()
|
||||||
|
df_sap_st = get_sap_stock_data()
|
||||||
|
|
||||||
|
if mto_vir_st is not None:
|
||||||
|
stock_data_total = pd.concat([df_sap_st, mto_vir_st], axis=0, ignore_index=True)
|
||||||
|
else:
|
||||||
|
stock_data_total = df_sap_st
|
||||||
|
|
||||||
|
# if stock_data_total is not None:
|
||||||
|
stock_data_total.fillna('', inplace=True)
|
||||||
|
await ApsPayloadSponsor.refresh_supply(stock_data_total.to_dict(orient='records'), dbs=dbs)
|
||||||
|
|
||||||
|
|
||||||
|
# async def push_pr(period: int = 30, groupdates: List[str] | str = None):
|
||||||
|
# if groupdates:
|
||||||
|
# if isinstance(groupdates, list):
|
||||||
|
# groupdates = ','.join(groupdates)
|
||||||
|
|
||||||
|
# pr_data = await ApsPayloadSponsor.get_dategrouped_pr(db_name=MYAPS_MAIN_DB, period=period, field_map=srm_field_map, groupdates=groupdates)
|
||||||
|
# timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
# for item in pr_data:
|
||||||
|
# item["plant"] = "1000"
|
||||||
|
# item["bu_code"] = werks
|
||||||
|
# item["version"] = timestamp
|
||||||
|
# CLIENT_LOGGER.start(f"推送要货计划到SRM:{pr_data}")
|
||||||
|
# response = srm_session.post(
|
||||||
|
# url=f"{srm_url}/jbl/service/execute/SRM_RECEIVE_PUSHED_DEMAND_PLAN_SERVICE",
|
||||||
|
# json={"demand_plan": pr_data})
|
||||||
|
# if response.json().get("body", {}).get("status", "").lower() == "success":
|
||||||
|
# CLIENT_LOGGER.success(f"推送要货计划到SRM")
|
||||||
|
# else:
|
||||||
|
# CLIENT_LOGGER.fail(f"推送要货计划到SRM", response.text)
|
||||||
|
|
||||||
|
|
||||||
|
# async def push_weekpr_to_srm():
|
||||||
|
# CLIENT_LOGGER.start("推送周要货计划到SRM任务")
|
||||||
|
# await push_pr(period=30)
|
||||||
|
# CLIENT_LOGGER.success("推送周要货计划到SRM任务", "", "执行完成")
|
||||||
|
|
||||||
|
|
||||||
|
# async def push_monthpr_to_srm():
|
||||||
|
# CLIENT_LOGGER.start("推送月度要货计划到SRM任务")
|
||||||
|
# date_list = [
|
||||||
|
# (datetime.now().replace(day=1) + relativedelta(months=i + 1) - relativedelta(days=1)).strftime('%Y-%m-%d')
|
||||||
|
# for i in range(3)
|
||||||
|
# ]
|
||||||
|
# await push_pr(period=90, groupdates=date_list)
|
||||||
|
# CLIENT_LOGGER.success("推送月度要货计划到SRM任务", "", "执行完成")
|
||||||
|
#################################################################################
|
||||||
|
# ⬇️定时任务设置
|
||||||
|
#################################################################################
|
||||||
|
|
||||||
|
@cron_task(hour=SCHEDULER_HOUR, minute=get_scheduler_minute(), description="刷新库存数据")
|
||||||
|
async def task_refresh_stock():
|
||||||
|
try:
|
||||||
|
await refresh_stock()
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# @cron_task(hour=SCHEDULER_HOUR, minute=get_scheduler_minute(2), description="确认报工")
|
||||||
|
# async def task_confirm_workreport():
|
||||||
|
# await ApsPayloadSponsor.confirm_workreport()
|
||||||
|
|
||||||
|
|
||||||
|
# @cron_task(hour=23, minute=59, description="推送周要货计划到SRM") # 每天23:59执行一次,需须在23:55拉取库存和确认报工之后
|
||||||
|
# # @cron_task(hour="8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23", minute="0,5,10,15,20,25,30,35,40,45,50,55")
|
||||||
|
# async def task_push_weekpr_to_srm():
|
||||||
|
# await push_weekpr_to_srm()
|
||||||
|
|
||||||
|
|
||||||
|
# @cron_task(day=1, hour=0, minute=5, description="推送月度要货计划到SRM")
|
||||||
|
# async def task_push_seasonpr_to_srm():
|
||||||
|
# await push_monthpr_to_srm()
|
||||||
|
|
||||||
|
|
||||||
|
#################################################################################
|
||||||
|
# ⬇️APS事件
|
||||||
|
#################################################################################
|
||||||
|
from .remind import ops_reminder, bus_reminder
|
||||||
|
|
||||||
|
|
||||||
|
# @event_batch_handler(reminder=bus_reminder)
|
||||||
|
# async def batch_handle_pl_status_a2e(event_data_list: List[Dict], _erp: EventResultPoster, description="PL 单据下达"):
|
||||||
|
# """
|
||||||
|
# Args:
|
||||||
|
# event_data_list: 事件数据,由数据库事件触发时注入
|
||||||
|
# _erp: EventResultPoster 实例,用于变更APS数据,由装饰器注入
|
||||||
|
# description: 事件描述,会被装饰器捕获,邮件头文字
|
||||||
|
# """
|
||||||
|
|
||||||
|
# @async_rate_limit()
|
||||||
|
# async def handle_pl_status_a2e(event_data: Dict, _aps: ApsPayloadSponsor):
|
||||||
|
# """
|
||||||
|
# 处理单个PL状态变为A2E事件
|
||||||
|
# Args:
|
||||||
|
# event_data: 事件数据,由主函数注入
|
||||||
|
# _aps: ApsPayloadStorage 实例,用于获取APS数据或缓存,由主函数注入
|
||||||
|
# """
|
||||||
|
|
||||||
|
# if isinstance(event_data, str):
|
||||||
|
# supplyno = event_data
|
||||||
|
# else:
|
||||||
|
# supplyno = event_data['supplyno']
|
||||||
|
|
||||||
|
# # 使用异步版本的函数,避免阻塞事件循环
|
||||||
|
# supplymo_detaildata = await _aps.get_supplymo_detaildata(supplyno=supplyno)
|
||||||
|
# try:
|
||||||
|
# start_datetime: str = supplymo_detaildata['dt_ordstart'].split(" ")[0]
|
||||||
|
# end_datetime: str = supplymo_detaildata['dt_ordend'].split(" ")[0]
|
||||||
|
# orderwc: list = supplymo_detaildata.get('orderwc', [])
|
||||||
|
|
||||||
|
# data = {
|
||||||
|
# "WERKS": werks, # 工厂
|
||||||
|
# "MATNR": supplymo_detaildata['materialno'],
|
||||||
|
# "AUART": "ZP01", # 订单类型
|
||||||
|
# "VERID": "SAP", # 生产版本
|
||||||
|
# "GSTRP": start_datetime, # 基本开始日期
|
||||||
|
# "GLTRP": end_datetime, # 基本完成日期
|
||||||
|
# "GAMNG": supplymo_detaildata['avail_qty'], # 总订单数量
|
||||||
|
# "WEMPF": "SAP", # 产线代码
|
||||||
|
# "BACKUP1": ','.join([i['workcenter'] for i in orderwc])
|
||||||
|
# }
|
||||||
|
|
||||||
|
# # 将同步的 sap_post 调用放在线程池中执行,避免阻塞事件循环
|
||||||
|
# loop = asyncio.get_event_loop()
|
||||||
|
# sap_post_future = loop.run_in_executor(
|
||||||
|
# None,
|
||||||
|
# sap_post,
|
||||||
|
# sap_url2,
|
||||||
|
# sap_session,
|
||||||
|
# "ZPP_PLAN_ORD_CREATE",
|
||||||
|
# data
|
||||||
|
# )
|
||||||
|
# try:
|
||||||
|
# sap_response = await asyncio.wait_for(sap_post_future, timeout=API_TIMEOUT)
|
||||||
|
# except asyncio.TimeoutError:
|
||||||
|
# await _erp.mo_release_failed(native_plno=supplyno, msg=f"SAP API 调用超时({API_TIMEOUT}秒)", push_data=data, msg_from='ERP')
|
||||||
|
# return
|
||||||
|
# sap_response_json = sap_response['response_json']
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# if 'BODY' in sap_response_json and len(sap_response_json['BODY']) > 0:
|
||||||
|
# sap_mo_data = sap_response_json['BODY'][0]
|
||||||
|
|
||||||
|
# if sap_mo_data.get('STATUS') == 'S':
|
||||||
|
# await _erp.mo_release_success(native_plno=supplyno, mono=sap_mo_data.get('AUFNR'), msg=sap_mo_data.get('MESSAGE'), msg_from='ERP')
|
||||||
|
# else:
|
||||||
|
# await _erp.mo_release_failed(native_plno=supplyno, msg=sap_mo_data.get('MESSAGE', '未知错误'), push_data=data, msg_from='ERP')
|
||||||
|
# else:
|
||||||
|
# # 处理响应格式不正确的情况
|
||||||
|
# await _erp.mo_release_failed(native_plno=supplyno, msg=f"响应格式不正确: {sap_response['response_text']}", push_data=data, msg_from='ERP')
|
||||||
|
# except Exception as e:
|
||||||
|
# await _erp.mo_release_failed(native_plno=supplyno, msg=f"处理响应时出错: {str(e)}", push_data=data, msg_from='ERP')
|
||||||
|
# except Exception as e:
|
||||||
|
# await _erp.mo_release_failed(native_plno=supplyno, msg=f"处理请求时出错: {str(e)}", push_data=data, msg_from='ERP')
|
||||||
|
|
||||||
|
|
||||||
|
# from apps.io_api.models import TSupply
|
||||||
|
|
||||||
|
# if not event_data_list:
|
||||||
|
# return
|
||||||
|
|
||||||
|
# supply_nos = [_['supplyno'] for _ in event_data_list]
|
||||||
|
# supply_list = await TSupply.filter(supplyno__in=supply_nos).update(memo=" 正在推送。。。")
|
||||||
|
# _aps = ApsPayloadSponsor(production_cache_items=[CacheItem.SUPPLY_MO, CacheItem.ORDER_WC])
|
||||||
|
# cache = await _aps.establish_production_cache(supplynos=supply_nos)
|
||||||
|
# tasks = [handle_pl_status_a2e(event_data=item, _aps=_aps) for item in event_data_list]
|
||||||
|
# await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
|
|
||||||
|
#################################################################################
|
||||||
|
# ⬇️一键通排批次日志
|
||||||
|
#################################################################################
|
||||||
|
|
||||||
|
# strategy -> handler function 映射表
|
||||||
|
_STRATEGY_HANDLERS: Dict[str, callable] = {
|
||||||
|
'库存': refresh_stock,
|
||||||
|
# 添加更多策略处理器...
|
||||||
|
# '采购': refresh_purchase,
|
||||||
|
# '生产': refresh_production,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def batch_handle_new_batchlog(event_data_list: List[Dict]):
|
||||||
|
|
||||||
|
await ApsPayloadSponsor.execute_batchlog(event_data_list[0], _STRATEGY_HANDLERS)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"THIS_DB_HOST": null,
|
||||||
|
"THIS_DB_PORT": null,
|
||||||
|
"THIS_DB_USER": null,
|
||||||
|
"THIS_DB_PASSWORD": null,
|
||||||
|
"THIS_DB_NAME": null,
|
||||||
|
"MYAPS_VERSION": "P",
|
||||||
|
"MYAPS_BASE_URL": "http://172.16.101.197:8092",
|
||||||
|
"MYAPS_DB_HOST": "172.16.101.197",
|
||||||
|
"MYAPS_DB_PORT": 3333,
|
||||||
|
"MYAPS_DB_USER": "root",
|
||||||
|
"MYAPS_DB_PASSWORD": "E9damw0o@#$",
|
||||||
|
"MYAPS_DB_SET": "hdtest",
|
||||||
|
"MYAPS_MAIN_DB": "hdtest"
|
||||||
|
},
|
||||||
|
"erp": {
|
||||||
|
"base_url": "http://192.168.201.2:8000",
|
||||||
|
"sap-client": "800",
|
||||||
|
"werks": "1600",
|
||||||
|
"username": "T058",
|
||||||
|
"password": "123456"
|
||||||
|
},
|
||||||
|
"mes": {
|
||||||
|
"base_url": "http://58.214.28.122:8851"
|
||||||
|
},
|
||||||
|
"srm": {
|
||||||
|
"base_url": "http://192.168.1.222:7776",
|
||||||
|
"Authorization": "Basic dHNpcGFkbWluOmFkbWlu"
|
||||||
|
},
|
||||||
|
"defaults": {
|
||||||
|
"!no_fill_defaults": ["lotmin", "lotmax"],
|
||||||
|
"auto_matver": true,
|
||||||
|
"matver_prefix": "V",
|
||||||
|
"matver": "V01",
|
||||||
|
"itemno_prefix": "A",
|
||||||
|
"itemno": "A01",
|
||||||
|
"plant": "1600",
|
||||||
|
"planner": "haida",
|
||||||
|
"leadday_e": 1,
|
||||||
|
"leadday_f": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user