1. 食品MES基础数据建模与物料主数据配置

为什么食品MES的物料主数据不能照搬离散制造

离散制造的物料主数据通常只关心“是什么、有多少、在哪道工序用”。食品行业还必须回答四个额外问题:这批原料从哪来(批次/供应商/产地)、什么时候必须用完(保质期/复检期)、在什么条件下存放(温湿度/冷藏冷冻)、能否与其它物料混放或替代(致敏原/清洁状态)。因此本例的建模重点不是把字段堆满,而是把“编码规则 + 批次属性 + 保质期 + 存储条件”四类约束设计成可校验、可被后续配方、工单、质检、追溯直接引用的结构。

物料分类与编码规则设计

先按“物料在食品链中的形态”划分三大类,再在编码中嵌入类别位,使编码本身可读、可排序、可被系统按前缀检索:

类别编码前缀编码结构(示例)示例关键属性
原料 RMRMRM-大类-流水号RM-DRY-0001 小麦粉供应商批次、产地、复检期
半成品 SFSFSF-产线-流水号SF-L1-0007 面团产出批次、允许存放时长
成品 FGFGFG-品类-规格-流水号FG-BRD-500-0003 吐司500g成品保质期、储运温度

编码只承载“稳定身份”,可变信息(供应商、批次、库位)一律放到批次层,避免因换供应商就新建物料导致BOM与追溯断裂。

批次属性、保质期与存储条件字段

食品物料主数据必须区分物料级默认值与批次级实际值。物料级定义“这类物料通常的保质期与存储要求”,批次级记录“这一批的实际生产日期、到期日、当前存储条件”。关键字段建议如下:

  • 保质期天数 / 复检期天数:原料常用复检期(如面粉90天复检),成品用保质期(如吐司7天)。
  • 存储条件代码:常温 DRY、冷藏 CHL(0~4℃)、冷冻 FRZ(≤-18℃),并附温度上下限。
  • 致敏原标识:含麸质、含乳、含坚果等,供清线与混放校验使用。
  • 批次状态:待检、合格、冻结、过期,决定能否被工单领用。
物料级主数据(稳定身份) 物料编码 RM-DRY-0001 物料名称 / 规格 / 计量单位 默认保质期 / 复检期(天) 存储条件代码 + 温度上下限 致敏原标识 / 清洁状态要求 定义“这类物料通常怎样” 批次级属性(每批实际值) 批次号 / 供应商 / 产地 生产日期 / 到期日 实际存储库位与温度记录 批次状态:待检/合格/冻结/过期 检验结果 / 复检记录 记录“这一批实际怎样” 默认值继承 批次可覆盖,但不可为空 数据完整性约束(保存/启用时校验) C1 编码唯一 前缀与类别一致 不可重复 C2 保质期必填 保质期 > 0 复检期 ≤ 保质期 C3 存储条件 代码在枚举内 温度下限 < 上限 C4 批次一致性 到期日 = 生产日 + 保质期 状态合法迁移 任一约束不通过 → 禁止启用

在MES中落地:字段配置与校验实现

下面给出一段可直接运行的 Python 校验脚本,模拟MES保存物料主数据与批次数据时的完整性检查。它把上面的 C1~C4 约束写成函数,输入是教学示例数据,输出是“通过/拒绝 + 原因”,便于在实施前先验证规则本身是否合理。

from datetime import date, timedelta

STORAGE = {
    "DRY": (10, 25),   # 常温 10~25℃
    "CHL": (0, 4),     # 冷藏 0~4℃
    "FRZ": (-25, -18), # 冷冻 -25~-18℃
}

def check_material(m):
    errs = []
    # C1 编码唯一性与前缀
    prefix_map = {"原料": "RM", "半成品": "SF", "成品": "FG"}
    if not m["code"].startswith(prefix_map[m["category"]]):
        errs.append("C1 编码前缀与类别不符")
    # C2 保质期
    if m["shelf_life_days"] <= 0:
        errs.append("C2 保质期必须大于0")
    if m["recheck_days"] > m["shelf_life_days"]:
        errs.append("C2 复检期不得超过保质期")
    # C3 存储条件
    if m["storage_code"] not in STORAGE:
        errs.append("C3 存储条件代码非法")
    else:
        lo, hi = STORAGE[m["storage_code"]]
        if not (m["temp_min"] < m["temp_max"]):
            errs.append("C3 温度下限必须小于上限")
        if m["temp_min"] < lo or m["temp_max"] > hi:
            errs.append("C3 温度范围超出该存储条件允许区间")
    return errs

def check_batch(b, m):
    errs = []
    # C4 批次一致性
    expect = b["prod_date"] + timedelta(days=m["shelf_life_days"])
    if b["expire_date"] != expect:
        errs.append(f"C4 到期日应为 {expect}")
    if b["status"] not in {"待检", "合格", "冻结", "过期"}:
        errs.append("C4 批次状态非法")
    return errs

# 教学示例数据
mat = {
    "code": "RM-DRY-0001", "category": "原料", "name": "小麦粉",
    "shelf_life_days": 180, "recheck_days": 90,
    "storage_code": "DRY", "temp_min": 10, "temp_max": 25,
}
bat = {
    "batch_no": "B20240601-01", "prod_date": date(2024, 6, 1),
    "expire_date": date(2024, 11, 28), "status": "合格",
}

print("物料校验:", check_material(mat) or "通过")
print("批次校验:", check_batch(bat, mat) or "通过")

运行后预期输出:物料校验通过;批次校验通过(2024-06-01 加 180 天为 2024-11-28)。若把 recheck_days 改为 200,则会返回“C2 复检期不得超过保质期”,说明规则生效。

实施时把 C1~C4 配置为MES的“保存前校验 + 启用前校验”双层规则:保存前只拦明显错误(编码重复、必填为空),启用前再校验跨字段逻辑(复检期与保质期、温度区间、批次到期日),避免录入过程中频繁打断操作。

验证数据完整性:用一组反例确认约束真的生效

只测“正确数据能保存”不足以证明约束有效,必须构造反例。建议至少覆盖以下四类,并记录系统返回的提示是否指向具体字段:

用例构造方式预期结果
编码冲突新建 RM-DRY-0001 重复编码拒绝,提示编码已存在
复检期越界保质期180、复检期200拒绝,提示复检期不得超过保质期
存储温度越界CHL 条件却填 -20~-18℃拒绝,提示超出冷藏允许区间
批次到期日错算生产日6/1、保质期180天,到期日填11/27拒绝,提示应为11/28

四类反例全部被正确拦截、且提示能定位到字段,才可判定本节的数据完整性约束配置完成;随后配方、工单、质检模块即可安全引用这套物料主数据。