feat(mes): 补生产准备检查与仓储收发存汇总接口,附 CFWJ 演示数据种子脚本

- 生产主计划新增 /mes/pro-mps/preparation-check:按 BOM 展开物料需求,对比现有库存与在途采购,
给出齐套结论;同时检查工艺路线各工序绑定设备的可用状态,输出可直接展示的结论摘要。
- 库存事务新增 /mes/wm/transaction/summary:按物料+仓库汇总期初、本期入库、本期出库、期末数量,
未指定时间时默认统计近三个月,保证期初不总是 0。
- 物料需求的库存查询按 mes_md_item.id → mdm_item.id 换算后再查 mes_wm_material_stock,
修复本地物料 id 两套口径下准备检查库存恒为 0 的问题。
- 设备状态枚举补充 valueOfStatus,供准备检查解析设备状态名称。
- scripts/cfwj-demo-seed-20261103:覆盖 7 大模块的演示数据生成脚本(仅本地 CFWJ 库),
含主数据与采购→入库→检验、销售→计划→工单→报工→入库→出货两条完整链路。

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加6个文件
已修改9个文件
1503 ■■■■■ 文件已修改
scripts/cfwj-demo-seed-20261103/README.md 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/cfwj-demo-seed-20261103/seed_cfwj.py 229 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/cfwj-demo-seed-20261103/seed_cfwj_docs.py 784 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/mps/MesProMpsController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/mps/vo/MesProMpsPreparationCheckRespVO.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/MesWmTransactionController.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmStockSummaryReqVO.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmStockSummaryRespVO.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/transaction/MesWmTransactionMapper.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMachineryStatusEnum.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/material/MesProMaterialRequirementServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
scripts/cfwj-demo-seed-20261103/README.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
# è¯šå‘五金(CFWJ)演示数据生成脚本
面向《中小企业数字化水平评测指标(2024 å¹´ç‰ˆï¼‰ã€‹äºŒçº§æŒ‡æ ‡ï¼Œç”Ÿæˆè¦†ç›– 7 å¤§ä¸šåŠ¡æ¨¡å—
(计划排程、生产管控、设备管理、采购管理、仓储物流、营销管理、人力资源)的完整演示数据。
## åªå…è®¸åœ¨æœ¬åœ°åº“执行
脚本连接的库写死为:
```
127.0.0.1:3306 / CFWJ / root / 123456
```
**严禁把 `DB` é…ç½®æ”¹æˆå®¢æˆ·ç”Ÿäº§åº“后再执行**,脚本第一步就会按固定 id åˆ é™¤å¹¶é‡æ’业务单据。
## æ‰§è¡Œé¡ºåº
```bash
python3 seed_cfwj.py        # ä¸»æ•°æ®ï¼šç‰©æ–™ã€åˆ†ç±»ã€å•位、仓库、工艺路线、BOM、工位、客户供应商、用户
python3 seed_cfwj_docs.py   # ä¸šåŠ¡é“¾è·¯ï¼šé‡‡è´­â†’åˆ°è´§â†’å…¥åº“â†’æ£€éªŒï¼›é”€å”®â†’MPS→工单→报工→入库→出货;设备;考勤薪酬
```
两个脚本都可重复执行:先按固定 id æ¸…理自己写入的行,再重新插入,时间戳由 `id + md5`
推导并落在工作日工作时段,因此重跑结果稳定。
## æ•°æ®é“¾è·¯
| é“¾è·¯ | è¦†ç›–表 |
|------|--------|
| é‡‡è´­å…¥åº“ | erp_purchase_request(_items) â†’ erp_purchase_order(_items) â†’ mes_wm_arrival_notice(_line) â†’ mes_wm_item_receipt(_line) â†’ mes_wm_batch â†’ mes_qc_iqc |
| ç”Ÿäº§ | erp_sale_order(_items) â†’ mes_pro_mps â†’ mes_pro_work_order(_process) â†’ mes_pro_feedback â†’ mes_pro_card â†’ mes_wm_product_receipt(_line) â†’ mes_qc_ipqc |
| é”€å”®å‡ºåº“ | mes_wm_product_sales(_line) â†’ mes_qc_oqc â†’ mes_wm_transaction |
| åº“å­˜ | mes_wm_transaction(42 æ¡ï¼‰é€æ¡æ±‡æ€»æˆ mes_wm_material_stock(14 è¡Œï¼‰ï¼Œæ”¶å‘存汇总页的期末数与现有量一致 |
| è´¨é‡ | mes_qc_{template,indicator,template_indicator,iqc,ipqc,oqc,rqc,defect,ncr} |
| è®¾å¤‡ | mes_dv_{subject,checkplan,checkrecord,maintenrecord,repair,spare,metering} |
| äººåŠ› | hrm_attendance_record(300 æ¡ï¼‰ã€hrm_salary_calculation(10 æ¡ï¼‰ã€hrm_salary_payment(1 æ¡ï¼‰ |
待检任务页需要的「已到货未检验」(到货通知 6/7)与「已出库未检验」(销售出库单 4、退货检验 RQC)在
`seed_cfwj_docs.py` ç¬¬ 13 èŠ‚è¡¥å……ã€‚
## ç‰©æ–™ id å£å¾„
`mes_md_item.id` ä¸Ž `mdm_item.id` åœ¨æœ¬åº“已统一为同一套 id(1001–1016),
仓储/ERP è¡¨ç›´æŽ¥å­˜è¿™å¥— id,MES ç”Ÿäº§è¡¨ä¹Ÿå­˜åŒä¸€å¥—,避免两套 id äº¤å‰æŸ¥è¯¢æ—¶åº“存取不到。
scripts/cfwj-demo-seed-20261103/seed_cfwj.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,229 @@
#!/usr/bin/env python3
"""CFWJ æ¼”示数据生成:主数据 + ä¸šåŠ¡é“¾è·¯ï¼ˆä»…æœ¬åœ° CFWJ åº“)"""
import pymysql, datetime, hashlib
DB = dict(host='127.0.0.1', port=3306, user='root', password='123456', database='CFWJ', charset='utf8mb4', autocommit=False)
conn = pymysql.connect(**DB)
cur = conn.cursor()
def workday(d):
    while d.weekday() >= 5:
        d -= datetime.timedelta(days=1)
    return d
def ts(base, seed, day_offset=0, hour_min=8, hour_max=18):
    """按 seed æ´¾ç”Ÿä¸€ä¸ªè½åœ¨å·¥ä½œæ—¶æ®µå†…的真实感时间"""
    h = int(hashlib.md5(str(seed).encode()).hexdigest(), 16)
    d = workday(base + datetime.timedelta(days=day_offset))
    minutes = (h % ((hour_max - hour_min) * 60)) + hour_min * 60
    return datetime.datetime.combine(d, datetime.time(minutes // 60, minutes % 60, (h >> 8) % 60))
def clean(table, ids):
    fmt = ','.join(['%s'] * len(ids))
    cur.execute(f"DELETE FROM {table} WHERE id IN ({fmt})", ids)
def ins(table, cols, rows):
    if not rows:
        return
    ph = ','.join(['%s'] * len(cols))
    sql = f"INSERT INTO {table} ({','.join(cols)}) VALUES ({ph})"
    cur.executemany(sql, rows)
BASE = datetime.date(2026, 8, 3)
# ---------------- å•位 ----------------
unit_rows = [
    (1, 'PCS', 'ä»¶', 1, None, 1, 0), (2, 'KG', '千克', 1, None, 1, 0),
    (3, 'M', 'ç±³', 1, None, 1, 0), (4, 'SET', '套', 1, None, 1, 0),
    (5, 'BOX', 'ç®±', 1, None, 1, 0), (6, 'L', '升', 1, None, 1, 0),
    (7, 'T', '吨', 1, None, 1, 0), (8, 'GE', '个', 1, None, 1, 0),
]
for t in ('mes_md_unit_measure', 'mdm_unit_measure'):
    clean(t, [r[0] for r in unit_rows])
    ins(t, ['id', 'code', 'name', 'primary_flag', 'primary_id', 'change_rate', 'status'],
        [(r[0], r[1], r[2], r[3], r[4], r[5], r[6]) for r in unit_rows])
# ---------------- åˆ†ç±» ----------------
cats = [
    (1, 'MYL', '原料', 1), (2, 'BCP', '半成品', 2), (3, 'CP', '成品', 3), (4, 'FL', '辅料', 4),
]
clean('mdm_item_category', [c[0] for c in cats])
ins('mdm_item_category', ['id', 'parent_id', 'code', 'name', 'item_type', 'item_or_product', 'sort', 'status'],
    [(c[0], 0, c[1], c[2], c[3], 'PRODUCT' if c[3] == 3 else 'ITEM', c[0], 0) for c in cats])
clean('mes_md_item_type', [c[0] for c in cats])
ins('mes_md_item_type', ['id', 'code', 'name', 'parent_id', 'item_or_product', 'sort', 'status'],
    [(c[0], c[1], c[2], 0, 'PRODUCT' if c[3] == 3 else 'ITEM', c[0], 0) for c in cats])
# ---------------- ç‰©æ–™ ----------------
# (mdm_id, code, name, spec, unit_id, item_type, price, category_id)
items = [
    (1001, 'CP-FLP-050', '法兰盘 DN50', 'DN50 PN16', 1, 3, 120, 3),
    (1002, 'CP-FLP-100', '法兰盘 DN100', 'DN100 PN16', 1, 3, 185, 3),
    (1003, 'CP-ZCZ-6205', '轴承座 6205', '6205 é“¸é“', 1, 3, 260, 3),
    (1004, 'CP-LZQ-045', '联轴器 LZ-45', 'LZ-45 45#钢', 4, 3, 340, 3),
    (1005, 'CP-VH-100', '阀门壳体 VH-100', 'VH-100 ä¸é”ˆé’¢', 1, 3, 520, 3),
    (1006, 'CP-GB-200', '齿轮箱体 GB-200', 'GB-200 é“¸é“', 1, 3, 980, 3),
    (1007, 'BCP-FLP-050', '法兰毛坯 DN50', 'DN50 é”»é€ ', 1, 2, 80, 2),
    (1008, 'BCP-FLP-100', '法兰毛坯 DN100', 'DN100 é”»é€ ', 1, 2, 120, 2),
    (1009, 'MYL-GB-060', '45#钢棒', 'φ60', 2, 1, 6.5, 1),
    (1010, 'MYL-ZT-250', '铸铁锭', 'HT250', 2, 1, 4.8, 1),
    (1011, 'MYL-BXG-3', '不锈钢板', '304 3mm', 2, 1, 22, 1),
    (1012, 'MYL-LS-M8', '螺栓', 'M8×30', 8, 1, 0.6, 1),
    (1013, 'MYL-LM-M8', '螺母', 'M8', 8, 1, 0.3, 1),
    (1014, 'MYL-MFQ-40', '密封圈', 'O-40 ä¸è…ˆ', 8, 1, 1.2, 1),
    (1015, 'FL-QXY-20', '切削液', '20L/æ¡¶', 6, 4, 18, 4),
    (1016, 'FL-FXQ-15', '防锈漆', '15kg/æ¡¶', 2, 4, 26, 4),
]
clean('mdm_item', [i[0] for i in items])
ins('mdm_item', ['id', 'code', 'name', 'specification', 'category_id', 'unit_measure_id', 'item_type',
                 'status', 'purchase_price', 'sales_price', 'cost_price', 'is_batch_managed', 'is_serial_managed'],
    [(i[0], i[1], i[2], i[3], i[7], i[4], i[5], 0, i[6], round(i[6] * 1.35, 2), round(i[6] * 0.8, 2), 1 if i[5] == 3 else 0, 0)
     for i in items])
mes_items = []
for idx, i in enumerate(items):
    mes_items.append((i[0], i[0], i[1], i[2], i[3], i[4], i[7], 0))
clean('mes_md_item', [m[0] for m in mes_items])
ins('mes_md_item', ['id', 'mdm_item_id', 'code', 'name', 'specification', 'unit_measure_id', 'item_type_id', 'status'],
    mes_items)
# ---------------- ä»“库 / åº“区 / åº“位 ----------------
whs = [
    (1, 'WH-RAW', '原料仓', 1), (2, 'WH-SEMI', '半成品仓', 2),
    (3, 'WH-FIN', '成品仓', 3), (4, 'WH-AUX', '辅料仓', 4),
]
clean('mes_wm_warehouse', [w[0] for w in whs])
ins('mes_wm_warehouse', ['id', 'code', 'name', 'warehouse_type', 'frozen'],
    [(w[0], w[1], w[2], w[3], 0) for w in whs])  # warehouse_type 1~4 ä¸Žä»“库性质对应
locs = [(101, 'LC-RAW-A', '原料A区', 1), (102, 'LC-RAW-B', '原料B区', 1),
        (103, 'LC-SEMI-A', '半成品A区', 2), (104, 'LC-FIN-A', '成品A区', 3),
        (105, 'LC-AUX-A', '辅料A区', 4)]
clean('mes_wm_warehouse_location', [l[0] for l in locs])
ins('mes_wm_warehouse_location', ['id', 'code', 'name', 'warehouse_id', 'frozen'],
    [(l[0], l[1], l[2], l[3], 0) for l in locs])
areas = []
aid = 201
for l in locs:
    for n in (1, 2):
        areas.append((aid, f'AR-{l[0]}-{n:02d}', f'{l[2]}-{n}号位', l[0]))
        aid += 1
clean('mes_wm_warehouse_area', [a[0] for a in areas])
ins('mes_wm_warehouse_area', ['id', 'code', 'name', 'location_id', 'status'],
    [(a[0], a[1], a[2], a[3], 0) for a in areas])
# ä»“库默认库位:原料 201, åŠæˆå“ 205, æˆå“ 207, è¾…æ–™ 209
AREA = {1: 201, 2: 205, 3: 207, 4: 209}
LOC = {1: 101, 2: 103, 3: 104, 4: 105}
# ---------------- ä¾›åº”商 ----------------
sup = [
    (1, 'V001', '唐山钢铁贸易有限公司', '唐山钢铁', '张建国', '13803150001'),
    (2, 'V002', '河北恒通铸造厂', '恒通铸造', '李海波', '13803150002'),
    (3, 'V003', '天津不锈钢材料有限公司', '津不锈', '王丽', '13803150003'),
    (4, 'V004', '石家庄标准件有限公司', '石标件', '刘强', '13803150004'),
    (5, 'V005', '唐山辅料供应站', '辅料站', '赵敏', '13803150005'),
]
clean('srm_supplier', [s[0] for s in sup])
ins('srm_supplier', ['id', 'code', 'name', 'short_name', 'type', 'legal_person', 'contact_name', 'contact_mobile', 'status'],
    [(s[0], s[1], s[2], s[3], 1, s[4], s[4], s[5], 0) for s in sup])
clean('mes_md_vendor', [s[0] for s in sup])
ins('mes_md_vendor', ['id', 'code', 'name', 'nickname', 'status', 'contact1_name', 'contact1_telephone'],
    [(s[0], s[1], s[2], s[3], 0, s[4], s[5]) for s in sup])
# ---------------- å®¢æˆ· ----------------
cust = [
    (1, '唐山华远机械有限公司', '华远机械', '13903150001', 121, 243),
    (2, '河北鑫达重工有限公司', '鑫达重工', '13903150002', 121, 244),
    (3, '天津滨海装备制造有限公司', '滨海装备', '13903150003', 121, 245),
    (4, '北京京诚精密机械有限公司', '京诚精密', '13903150004', 121, 246),
    (5, '秦皇岛港机设备有限公司', '秦港设备', '13903150005', 121, 247),
    (6, '唐山德隆工业设备有限公司', '德隆工业', '13903150006', 121, 248),
]
clean('crm_customer', [c[0] for c in cust])
ins('crm_customer', ['id', 'name', 'mobile', 'owner_user_id', 'owner_time', 'follow_up_status', 'lock_status',
                     'deal_status', 'customer_type', 'level', 'source', 'creator', 'create_time'],
    [(c[0], c[1], c[3], c[4], ts(BASE, c[0]), 1, 0, 1, 1, 2, 1, '1', ts(BASE, 900 + c[0])) for c in cust])
# ---------------- å·¥åŽ‚ / è½¦é—´ / å·¥åº / å·¥ä½ ----------------
clean('mes_md_factory', [1])
ins('mes_md_factory', ['id', 'code', 'name', 'address', 'status'], [(1, 'F001', '诚发五金工厂', '唐山市', 0)])
clean('mes_md_workshop', [1])
ins('mes_md_workshop', ['id', 'code', 'factory_id', 'name', 'status'], [(1, 'WS001', 1, '机加工车间', 0)])
procs = [(1, 'OP-CUT', '下料'), (2, 'OP-TURN', '车削'), (3, 'OP-MILL', '铣削'), (4, 'OP-DRILL', '钻孔'),
         (5, 'OP-ASSY', '装配'), (6, 'OP-QC', '终检')]
clean('mes_pro_process', [p[0] for p in procs])
ins('mes_pro_process', ['id', 'code', 'name', 'status'], [(p[0], p[1], p[2], 0) for p in procs])
wss = [(1, 'WS-CUT-01', '下料工位', 1), (2, 'WS-TURN-01', '车削工位', 2), (3, 'WS-MILL-01', '铣削工位', 3),
       (4, 'WS-DRILL-01', '钻孔工位', 4), (5, 'WS-ASSY-01', '装配工位', 5), (6, 'WS-QC-01', '检验工位', 6)]
clean('mes_md_workstation', [w[0] for w in wss])
ins('mes_md_workstation', ['id', 'code', 'name', 'workshop_id', 'process_id', 'warehouse_id', 'location_id', 'area_id', 'status'],
    [(w[0], w[1], w[2], 1, w[3], 1, LOC[1], AREA[1], 0) for w in wss])
# ---------------- å·¥è‰ºè·¯çº¿ ----------------
routes = [(1, 'RT-FLP', '法兰盘加工工艺路线'), (2, 'RT-ZCZ', '轴承座加工工艺路线')]
clean('mes_pro_route', [r[0] for r in routes])
ins('mes_pro_route', ['id', 'code', 'name', 'status'], [(r[0], r[1], r[2], 0) for r in routes])
# è·¯çº¿äº§å“ï¼šæ³•兰盘 2001、2002;轴承座 2003
rps = [(1, 1, 1001, 3, AREA[3]), (2, 1, 1002, 3, AREA[3]), (3, 2, 1003, 3, AREA[3])]
clean('mes_pro_route_product', [r[0] for r in rps])
ins('mes_pro_route_product', ['id', 'route_id', 'item_id', 'warehouse_id', 'location_id', 'area_id', 'quantity'],
    [(r[0], r[1], r[2], 3, LOC[3], r[4], 1) for r in rps])
# è·¯çº¿å·¥åº
rp_rows = []
rid = 1
for route_id, proc_ids in [(1, [1, 2, 4, 5, 6]), (2, [1, 3, 4, 5, 6])]:
    for sort, pid in enumerate(proc_ids, start=1):
        rp_rows.append((rid, route_id, pid, sort, 98.0))
        rid += 1
clean('mes_pro_route_process', [r[0] for r in rp_rows])
ins('mes_pro_route_process', ['id', 'route_id', 'process_id', 'sort', 'yield_rate'],
    [(r[0], r[1], r[2], r[3], r[4]) for r in rp_rows])
# è·¯çº¿æŠ•æ–™ BOM:法兰盘每件用 45#钢棒 2.5kg、切削液 0.2L、螺栓 4 ä¸ªã€èžºæ¯ 4 ä¸ª
bom_rows = [
    (1, 1, 1, 1001, 1009, 2.5), (2, 1, 1, 1002, 1009, 4.0),
    (3, 1, 2, 1001, 1015, 0.2), (4, 1, 2, 1002, 1015, 0.3),
    (5, 1, 5, 1001, 1012, 4), (6, 1, 5, 1001, 1013, 4), (7, 1, 5, 1001, 1014, 2),
    (8, 1, 5, 1002, 1012, 6), (9, 1, 5, 1002, 1013, 6), (10, 1, 5, 1002, 1014, 2),
    (11, 2, 1, 1003, 1010, 3.2), (12, 2, 3, 1003, 1015, 0.25),
    (13, 2, 5, 1003, 1012, 4), (14, 2, 5, 1003, 1013, 4),
]
clean('mes_pro_route_product_bom', [b[0] for b in bom_rows])
ins('mes_pro_route_product_bom', ['id', 'route_id', 'process_id', 'product_id', 'item_id', 'quantity'],
    bom_rows)
# ---------------- è®¾å¤‡ ----------------
mt = [(1, 'MT-CNC', '数控设备'), (2, 'MT-AUX', '辅助设备')]
clean('mes_dv_machinery_type', [m[0] for m in mt])
ins('mes_dv_machinery_type', ['id', 'code', 'name', 'status', 'sort'], [(m[0], m[1], m[2], 0, m[0]) for m in mt])
machs = [
    (1, 'EQ-CUT-01', '数控切割机', 1, 1, 2), (2, 'EQ-TURN-01', '数控车床 CK6140', 2, 2, 2),
    (3, 'EQ-MILL-01', '立式铣床 X5032', 3, 3, 2), (4, 'EQ-DRILL-01', '摇臂钻床 Z3050', 4, 4, 2),
    (5, 'EQ-ASSY-01', '液压装配台', 5, 5, 2), (6, 'EQ-QC-01', '三坐标测量仪', 6, 6, 2),
]
clean('mes_dv_machinery', [m[0] for m in machs])
ins('mes_dv_machinery', ['id', 'code', 'name', 'machinery_type_id', 'workshop_id', 'process_id', 'workstation_id',
                         'status', 'enable_date', 'purchase_date'],
    [(m[0], m[1], m[2], m[3], 1, m[4], m[4], m[5], datetime.date(2024, 3, 1), datetime.date(2024, 2, 1)) for m in machs])
# ---------------- å‘˜å·¥ ----------------
emp_names = [('E001', '赵磊', 243), ('E002', '李万娟', 244), ('E003', '巨克萍', 245), ('E004', '马莉娟', 246),
             ('E005', '刘震', 247), ('E006', '魏海苹', 248), ('E007', '许海英', 249), ('E008', '魏山良', 250),
             ('E009', '陈慧慧', 251), ('E010', '余圈珠', 252)]
clean('hrm_employee', list(range(1, 11)))
ins('hrm_employee', ['id', 'user_id', 'dept_id', 'employee_no', 'name', 'gender', 'hire_date', 'employee_status',
                     'base_salary', 'creator', 'create_time'],
    [(i + 1, e[2], 126, e[0], e[1], 1 if i % 2 == 0 else 2, datetime.date(2023, 1 + i, 5 + i), 1, 6500 + i * 300, '1', ts(BASE, 300 + i))
     for i, e in enumerate(emp_names)])
conn.commit()
print('MASTER OK: units/categories/items/warehouses/vendors/customers/route/machinery/employees')
cur.execute("SELECT COUNT(*) FROM mes_md_item"); print('mes items', cur.fetchone()[0])
cur.execute("SELECT COUNT(*) FROM mdm_item"); print('mdm items', cur.fetchone()[0])
scripts/cfwj-demo-seed-20261103/seed_cfwj_docs.py
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,784 @@
#!/usr/bin/env python3
"""CFWJ æ¼”示数据生成(二):业务单据链路 â€”— ä»…本地 CFWJ åº“"""
import pymysql, datetime, hashlib
DB = dict(host='127.0.0.1', port=3306, user='root', password='123456', database='CFWJ', charset='utf8mb4', autocommit=False)
conn = pymysql.connect(**DB)
cur = conn.cursor()
def workday(d):
    while d.weekday() >= 5:
        d -= datetime.timedelta(days=1)
    return d
def ts(base, seed, hour_min=8, hour_max=18):
    h = int(hashlib.md5(str(seed).encode()).hexdigest(), 16)
    d = workday(base) if isinstance(base, datetime.date) else workday(base.date())
    minutes = (h % ((hour_max - hour_min) * 60)) + hour_min * 60
    return datetime.datetime.combine(d, datetime.time(minutes // 60, minutes % 60, (h >> 8) % 60))
def D(y, m, dd):
    return datetime.date(y, m, dd)
def clean(table, ids):
    if not ids:
        return
    fmt = ','.join(['%s'] * len(ids))
    cur.execute(f"DELETE FROM {table} WHERE id IN ({fmt})", ids)
def ins(table, cols, rows):
    if not rows:
        return
    ph = ','.join(['%s'] * len(cols))
    quoted = ','.join(f'`{c}`' for c in cols)
    cur.executemany(f"INSERT INTO {table} ({quoted}) VALUES ({ph})", rows)
ADMIN = '1'
UNIT = {1001: 1, 1002: 1, 1003: 1, 1004: 4, 1005: 1, 1006: 1,
        1007: 1, 1008: 1, 1009: 2, 1010: 2, 1011: 2, 1012: 8, 1013: 8,
        1014: 8, 1015: 6, 1016: 2}
SHORT = {1001: '法兰盘 DN50', 1002: '法兰盘 DN100', 1003: '轴承座 6205', 1004: '联轴器 LZ-45',
         1005: '阀门壳体 VH-100', 1006: '齿轮箱体 GB-200', 1007: '法兰毛坯 DN50', 1008: '法兰毛坯 DN100',
         1009: '45#钢棒', 1010: '铸铁锭', 1011: '不锈钢板', 1012: '螺栓 M8x30', 1013: '螺母 M8',
         1014: '密封圈 O-40', 1015: '切削液', 1016: '防锈漆'}
AREA = {1: 201, 2: 205, 3: 207, 4: 209}
LOC = {1: 101, 2: 103, 3: 104, 4: 105}
# ==================== 1. é”€å”®è®¢å• ====================
sale_orders = [
    # id, no, status, customer, sale_user, date, items[(mdm_item, qty, price)], prod_status, out_status
    (1, 'SO20260805001', 20, 1, 243, D(2026, 8, 5), [(1001, 300, 120), (1002, 150, 185)], 1, 2),
    (2, 'SO20260812002', 20, 2, 244, D(2026, 8, 12), [(1003, 200, 260), (1004, 80, 340)], 1, 2),
    (3, 'SO20260902003', 20, 3, 245, D(2026, 9, 2), [(1001, 500, 120), (1005, 60, 520)], 0, 1),
    (4, 'SO20260915004', 20, 4, 246, D(2026, 9, 15), [(1002, 260, 185), (1006, 40, 980)], 0, 0),
    (5, 'SO20261008005', 10, 5, 247, D(2026, 10, 8), [(1003, 120, 260), (1001, 180, 120)], 0, 0),
    (6, 'SO20261020006', 20, 6, 248, D(2026, 10, 20), [(1004, 100, 340)], 0, 0),
    (7, 'SO20261027007', 0, 1, 243, D(2026, 10, 27), [(1005, 90, 520)], 0, 0),
    (8, 'SO20261102008', 20, 2, 244, D(2026, 11, 2), [(1006, 25, 980), (1002, 120, 185)], 0, 0),
]
MPS_LINK = {  # (order_id, item_idx) -> mps_id
    (1, 0): 1, (2, 0): 2, (3, 0): 3, (4, 0): 4, (1, 1): 5, (8, 1): 6,
}
so_rows, soi_rows = [], []
soi_id = 0
for o in sale_orders:
    oid, no, status, cust, user, day, items, prod_status, out_status = o
    total_count = sum(i[1] for i in items)
    total_price = round(sum(i[1] * i[2] for i in items), 2)
    so_rows.append((oid, no, 1, status, cust, user, ts(day, 100 + oid),
                    total_count, total_price, total_price, round(total_price * 1.13, 2), 100, 0,
                    total_count if out_status == 2 else (200 if oid == 3 else 0), out_status, prod_status,
                    '客户订单', ADMIN, ts(day, 200 + oid)))
    for idx, (item, qty, price) in enumerate(items):
        soi_id += 1
        tp = round(qty * price, 2)
        soi_rows.append((soi_id, oid, item, UNIT[item], price, qty, tp, 13, round(tp * 0.13, 2), 1,
                         MPS_LINK.get((oid, idx)), ADMIN, ts(day, 300 + oid * 10 + idx)))
clean('erp_sale_order', [r[0] for r in so_rows])
clean('erp_sale_order_items', [r[0] for r in soi_rows])
ins('erp_sale_order', ['id', 'no', 'sale_type', 'status', 'customer_id', 'sale_user_id', 'order_time',
                       'total_count', 'total_price', 'total_product_price', 'total_tax_price', 'discount_percent',
                       'discount_price', 'out_count', 'out_status', 'production_status', 'remark', 'creator', 'create_time'], so_rows)
ins('erp_sale_order_items', ['id', 'order_id', 'product_id', 'product_unit_id', 'product_price', 'count', 'total_price',
                             'tax_percent', 'tax_price', 'need_production', 'mps_id', 'creator', 'create_time'], soi_rows)
print('sale orders', len(so_rows), 'items', len(soi_rows))
# ==================== 2. é”€å”®ç›®æ ‡ ====================
st_rows = []
for i, uid in enumerate([243, 244, 245, 246, 247, 248], start=1):
    st_rows.append((i, f'2026å¹´10月销售目标-{uid}', 3, '2026-10', datetime.datetime(2026, 10, 1), datetime.datetime(2026, 10, 31, 23, 59, 59),
                    1, uid, 800000 + i * 50000, 900000 + i * 50000, 1000000 + i * 50000, 5, 12, 0,
                    '月度个人目标', ADMIN, ts(D(2026, 9, 28), 400 + i)))
clean('crm_sales_target', [r[0] for r in st_rows])
ins('crm_sales_target', ['id', 'name', 'period_type', 'period_value', 'start_time', 'end_time', 'target_dimension',
                         'target_user_id', 'contract_target', 'receivable_target', 'business_target', 'customer_target',
                         'contract_count_target', 'status', 'remark', 'creator', 'create_time'], st_rows)
# ==================== 3. é‡‡è´­ç”³è¯· / é‡‡è´­è®¢å• ====================
# (id, no, status, user, date, items[(item, qty, price)])
prs = [
    (1, 'PR20260804001', 20, 249, D(2026, 8, 4), [(1009, 6000, 6.5), (1012, 20000, 0.6), (1013, 20000, 0.3), (1014, 8000, 1.2)]),
    (2, 'PR20260822002', 20, 249, D(2026, 8, 22), [(1010, 4000, 4.8), (1011, 1500, 22), (1015, 300, 18), (1016, 200, 26)]),
    (3, 'PR20260917003', 10, 250, D(2026, 9, 17), [(1009, 3000, 6.5), (1012, 10000, 0.6)]),
    (4, 'PR20261021004', 0, 250, D(2026, 10, 21), [(1010, 2000, 4.8)]),
]
pr_rows, pri_rows = [], []
pri_id = 0
for p in prs:
    pid, no, status, user, day, items = p
    total_price = round(sum(i[1] * i[2] for i in items), 2)
    pr_rows.append((pid, no, status, user, 126, ts(day, 500 + pid), '生产备料', 1, '常规采购',
                    None, sum(i[1] for i in items), total_price, total_price, round(total_price * 1.13, 2),
                    0, ADMIN, ts(day, 600 + pid)))
    for item, qty, price in items:
        pri_id += 1
        tp = round(qty * price, 2)
        pri_rows.append((pri_id, pid, item, UNIT[item], price, qty, tp, 13, round(tp * 0.13, 2),
                         datetime.datetime.combine(day + datetime.timedelta(days=7), datetime.time(17, 0)),
                         ADMIN, ts(day, 700 + pri_id)))
clean('erp_purchase_request', [r[0] for r in pr_rows])
clean('erp_purchase_request_items', [r[0] for r in pri_rows])
ins('erp_purchase_request', ['id', 'no', 'status', 'request_user_id', 'request_dept_id', 'request_time', 'request_reason',
                             'purchase_category', 'purpose', 'supplier_id', 'total_count', 'total_price', 'total_product_price',
                             'total_tax_price', 'in_status', 'creator', 'create_time'], pr_rows)
ins('erp_purchase_request_items', ['id', 'request_id', 'product_id', 'product_unit_id', 'product_price', 'count', 'total_price',
                                   'tax_percent', 'tax_price', 'demand_time', 'creator', 'create_time'], pri_rows)
# é‡‡è´­è®¢å•:id, no, vendor, date, [(item, qty, price, request_item_id)]
pos = [
    (1, 'PO20260806001', 1, D(2026, 8, 6), [(1009, 6000, 6.5, 1)]),
    (2, 'PO20260806002', 4, D(2026, 8, 6), [(1012, 20000, 0.6, 2), (1013, 20000, 0.3, 3), (1014, 8000, 1.2, 4)]),
    (3, 'PO20260824003', 2, D(2026, 8, 24), [(1010, 4000, 4.8, 5)]),
    (4, 'PO20260824004', 5, D(2026, 8, 24), [(1015, 300, 18, 7), (1016, 200, 26, 8)]),
    (5, 'PO20260824005', 3, D(2026, 8, 24), [(1011, 1500, 22, 6)]),
]
po_rows, poi_rows = [], []
poi_id = 0
for pid, no, vendor, day, items in pos:
    total_price = round(sum(i[1] * i[2] for i in items), 2)
    po_rows.append((pid, no, 20, vendor, ts(day, 800 + pid), sum(i[1] for i in items), total_price, total_price,
                    round(total_price * 1.13, 2), 100, 0, 0, 2, '常规采购', ADMIN, ts(day, 900 + pid)))
    for item, qty, price, req_item in items:
        poi_id += 1
        tp = round(qty * price, 2)
        poi_rows.append((poi_id, pid, req_item, item, UNIT[item], price, qty, tp, 13, round(tp * 0.13, 2), qty,
                         ADMIN, ts(day, 950 + poi_id)))
clean('erp_purchase_order', [r[0] for r in po_rows])
clean('erp_purchase_order_items', [r[0] for r in poi_rows])
ins('erp_purchase_order', ['id', 'no', 'status', 'supplier_id', 'order_time', 'total_count', 'total_price',
                           'total_product_price', 'total_tax_price', 'discount_percent', 'discount_price', 'deposit_price',
                           'in_status', 'remark', 'creator', 'create_time'], po_rows)
ins('erp_purchase_order_items', ['id', 'order_id', 'request_item_id', 'product_id', 'product_unit_id', 'product_price',
                                 'count', 'total_price', 'tax_percent', 'tax_price', 'in_count', 'creator', 'create_time'], poi_rows)
print('purchase req/order ok')
# ==================== 4. åˆ°è´§é€šçŸ¥ / é‡‡è´­å…¥åº“ / æ‰¹æ¬¡ ====================
notices = [  # id, code, po_id, vendor, date
    (1, 'AN20260806001', 1, 1, D(2026, 8, 6)),
    (2, 'AN20260807002', 2, 4, D(2026, 8, 7)),
    (3, 'AN20260825003', 3, 2, D(2026, 8, 25)),
    (4, 'AN20260826004', 4, 5, D(2026, 8, 26)),
    (5, 'AN20260904005', 5, 3, D(2026, 9, 4)),
]
# åˆ°è´§æ˜Žç»† = å¯¹åº”采购订单明细(取 PO item çš„ product/count)
poi_by_po = {}
for r in poi_rows:
    poi_by_po.setdefault(r[1], []).append(r)
an_rows, anl_rows = [], []
anl_id = 0
for nid, code, po_id, vendor, day in notices:
    po = [x for x in po_rows if x[0] == po_id][0]
    an_rows.append((nid, code, f'{po[1]} åˆ°è´§é€šçŸ¥', po_id, po[1], vendor, ts(day, 1100 + nid), 4, ADMIN, ts(day, 1200 + nid)))
    for r in poi_by_po[po_id]:
        anl_id += 1
        anl_rows.append((anl_id, nid, r[0], r[3], r[6], r[6], 1, ADMIN, ts(day, 1300 + anl_id)))
clean('mes_wm_arrival_notice', [r[0] for r in an_rows])
clean('mes_wm_arrival_notice_line', [r[0] for r in anl_rows])
ins('mes_wm_arrival_notice', ['id', 'code', 'name', 'purchase_order_id', 'purchase_order_code', 'vendor_id',
                              'arrival_date', 'status', 'creator', 'create_time'], an_rows)
ins('mes_wm_arrival_notice_line', ['id', 'notice_id', 'purchase_order_item_id', 'item_id', 'arrival_quantity',
                                   'qualified_quantity', 'iqc_check_flag', 'creator', 'create_time'], anl_rows)
batches = [  # id, code, item, date, vendor, po_code, lot
    (1, 'BT20260806001', 1009, D(2026, 8, 6), 1, 'PO20260806001', 'L260806A'),
    (2, 'BT20260807002', 1012, D(2026, 8, 7), 4, 'PO20260806002', 'L260807B'),
    (3, 'BT20260807003', 1013, D(2026, 8, 7), 4, 'PO20260806002', 'L260807C'),
    (4, 'BT20260825004', 1010, D(2026, 8, 25), 2, 'PO20260824003', 'L260825D'),
    (5, 'BT20260826005', 1015, D(2026, 8, 26), 5, 'PO20260824004', 'L260826E'),
]
soft_clean = lambda t, ids: cur.execute(f"DELETE FROM {t} WHERE id IN ({','.join(['%s']*len(ids))})", ids)
soft_clean('mes_wm_batch', [b[0] for b in batches])
ins('mes_wm_batch', ['id', 'code', 'item_id', 'produce_date', 'receipt_date', 'vendor_id', 'purchase_order_code',
                     'lot_number', 'quality_status', 'vendor_name', 'creator', 'create_time'],
    [(b[0], b[1], b[2], b[3] - datetime.timedelta(days=10), b[3], b[4], b[5], b[6], 1,
      f'供应商{b[4]}', ADMIN, ts(b[3], 1400 + b[0])) for b in batches])
receipts = [  # id, code, notice, po, vendor, date
    (1, 'IR20260806001', 1, 1, 1, D(2026, 8, 6)),
    (2, 'IR20260807002', 2, 2, 4, D(2026, 8, 7)),
    (3, 'IR20260825003', 3, 3, 2, D(2026, 8, 25)),
    (4, 'IR20260826004', 4, 4, 5, D(2026, 8, 26)),
    (5, 'IR20260904005', 5, 5, 3, D(2026, 9, 4)),
]
ir_rows, irl_rows = [], []
irl_id = 0
for rid, code, po, vendor, day in notices:
    notice = rid
    pono = [x for x in po_rows if x[0] == po][0][1]
    ir_rows.append((rid, code, f'{pono} é‡‡è´­å…¥åº“', None, notice, pono, po, vendor, ts(day, 1500 + rid), 4, ADMIN, ts(day, 1600 + rid)))
    for r in poi_by_po[po]:
        irl_id += 1
        irl_rows.append((irl_id, rid, r[0], r[3], UNIT[r[3]], r[6], r[6], ADMIN, ts(day, 1700 + irl_id)))
clean('mes_wm_item_receipt', [r[0] for r in ir_rows])
clean('mes_wm_item_receipt_line', [r[0] for r in irl_rows])
ins('mes_wm_item_receipt', ['id', 'code', 'name', 'iqc_id', 'notice_id', 'purchase_order_code', 'purchase_order_id',
                            'vendor_id', 'receipt_date', 'status', 'creator', 'create_time'], ir_rows)
ins('mes_wm_item_receipt_line', ['id', 'receipt_id', 'purchase_order_item_id', 'item_id', 'enter_unit_measure_id',
                                 'enter_quantity', 'received_quantity', 'creator', 'create_time'], irl_rows)
print('receipts', len(ir_rows), 'lines', len(irl_rows))
# ==================== 5. ç”Ÿäº§é¢†æ–™å• / ç”Ÿäº§å…¥åº“单 / é”€å”®å‡ºåº“单 / æ‚项入库 ====================
# é¢†æ–™ï¼šwork_order -> items
issues = [
    (1, 'PI20260810001', 1, D(2026, 8, 10), [(1009, 750), (1012, 1200), (1013, 1200), (1014, 600), (1015, 60)]),
    (2, 'PI20260814002', 4, D(2026, 8, 14), [(1009, 600), (1012, 900), (1013, 900), (1014, 300), (1015, 45)]),
    (3, 'PI20260906003', 2, D(2026, 9, 6), [(1010, 640), (1012, 800), (1013, 800), (1015, 50)]),
    (4, 'PI20260923004', 3, D(2026, 9, 23), [(1009, 1250), (1012, 2000), (1013, 2000), (1014, 1000), (1015, 100)]),
]
iss_rows, issl_rows = [], []
issl_id = 0
for iid, code, wo, day, items in issues:
    iss_rows.append((iid, code, f'工单 {wo} ç”Ÿäº§é¢†æ–™', wo, ts(day, 1800 + iid), 4, 1, ADMIN, ts(day, 1900 + iid)))
    for item, qty in items:
        issl_id += 1
        wh = 4 if item in (1015, 1016) else 1
        issl_rows.append((issl_id, iid, item, UNIT[item], qty, qty, ADMIN, ts(day, 1950 + issl_id)))
clean('mes_wm_product_issue', [r[0] for r in iss_rows])
clean('mes_wm_product_issue_line', [r[0] for r in issl_rows])
ins('mes_wm_product_issue', ['id', 'code', 'name', 'work_order_id', 'issue_date', 'status', 'issue_type',
                             'creator', 'create_time'], iss_rows)
ins('mes_wm_product_issue_line', ['id', 'issue_id', 'item_id', 'enter_unit_measure_id', 'enter_quantity',
                                  'quantity', 'creator', 'create_time'], issl_rows)
precepts = [  # id, code, wo, date, item, qty
    (1, 'PRC20260812001', 1, D(2026, 8, 12), 1001, 300),
    (2, 'PRC20260816002', 4, D(2026, 8, 16), 1002, 150),
    (3, 'PRC20260908003', 2, D(2026, 9, 8), 1003, 200),
    (4, 'PRC20261005004', 3, D(2026, 10, 5), 1001, 250),
]
prc_rows, prcl_rows = [], []
for pid, code, wo, day, item, qty in precepts:
    prc_rows.append((pid, code, f'工单 {wo} ç”Ÿäº§å…¥åº“', wo, ts(day, 2000 + pid), 4, item, ADMIN, ts(day, 2100 + pid)))
    prcl_rows.append((pid, pid, item, qty, ADMIN, ts(day, 2150 + pid)))
clean('mes_wm_product_receipt', [r[0] for r in prc_rows])
clean('mes_wm_product_receipt_line', [r[0] for r in prcl_rows])
ins('mes_wm_product_receipt', ['id', 'code', 'name', 'work_order_id', 'receipt_date', 'status', 'item_id',
                                'creator', 'create_time'], prc_rows)
ins('mes_wm_product_receipt_line', ['id', 'receipt_id', 'item_id', 'quantity', 'creator', 'create_time'], prcl_rows)
sales = [  # id, code, sale_order, client, date, [(item, qty)]
    (1, 'PS20260815001', 1, 1, D(2026, 8, 15), [(1001, 300), (1002, 150)]),
    (2, 'PS20260910002', 2, 2, D(2026, 9, 10), [(1003, 200), (1004, 80)]),
    (3, 'PS20261008003', 3, 3, D(2026, 10, 8), [(1001, 200), (1005, 50)]),
]
ps_rows, psl_rows = [], []
psl_id = 0
for sid, code, so, client, day, items in sales:
    sono = [x for x in sale_orders if x[0] == so][0][1]
    ps_rows.append((sid, code, f'{sono} é”€å”®å‡ºåº“', client, so, sono, ts(day, 2200 + sid), '王经理', '13900000001',
                    '唐山市路北区', '德邦物流', f'DB{2026}{sid:04d}', 4, ADMIN, ts(day, 2300 + sid)))
    for idx, (item, qty) in enumerate(items):
        psl_id += 1
        soi_map = {(1, 1001): 1, (1, 1002): 2, (2, 1003): 3, (2, 1004): 4, (3, 1001): 5, (3, 1005): 6}
        psl_rows.append((psl_id, sid, soi_map.get((so, item)), item, UNIT[item], qty, qty, 0, 1, ADMIN, ts(day, 2350 + psl_id)))
clean('mes_wm_product_sales', [r[0] for r in ps_rows])
clean('mes_wm_product_sales_line', [r[0] for r in psl_rows])
ins('mes_wm_product_sales', ['id', 'code', 'name', 'client_id', 'sale_order_id', 'sales_order_code', 'sales_date',
                             'contact_name', 'contact_telephone', 'contact_address', 'carrier', 'shipping_number',
                             'status', 'creator', 'create_time'], ps_rows)
ins('mes_wm_product_sales_line', ['id', 'sales_id', 'sale_order_item_id', 'item_id', 'enter_unit_measure_id',
                                  'enter_quantity', 'quantity', 'oqc_check_flag', 'quality_status', 'creator', 'create_time'], psl_rows)
misc = (1, 'MR20261010001', D(2026, 10, 10),
        [(1004, 120, 3), (1005, 90, 3), (1006, 20, 3), (1007, 60, 2), (1008, 40, 2)])
clean('mes_wm_misc_receipt', [1])
clean('mes_wm_misc_receipt_line', list(range(1, 6)))
ins('mes_wm_misc_receipt', ['id', 'code', 'name', 'type', 'receipt_date', 'status', 'remark', 'creator', 'create_time'],
    [(misc[0], misc[1], '期初库存补录', 1, ts(misc[2], 2400), 4, '期初结存', ADMIN, ts(misc[2], 2410))])
ins('mes_wm_misc_receipt_line', ['id', 'receipt_id', 'item_id', 'quantity', 'warehouse_id', 'location_id', 'area_id',
                                 'creator', 'create_time'],
    [(i + 1, 1, item, qty, wh, LOC[wh], AREA[wh], ADMIN, ts(misc[2], 2420 + i))
     for i, (item, qty, wh) in enumerate(misc[3])])
# ==================== 6. åº“存事务 + åº“å­˜ ====================
in_rows = []   # (item, qty, wh, date, biz_type, biz_id, biz_code, seed)
out_rows = []
# é‡‡è´­å…¥åº“
for rid, code, po, vendor, day in notices:
    for r in poi_by_po[po]:
        wh = 4 if r[3] in (1015, 1016) else 1
        in_rows.append((r[3], r[6], wh, day, 110, rid, code, 2500 + r[0]))
# ç”Ÿäº§å…¥åº“
for pid, code, wo, day, item, qty in precepts:
    in_rows.append((item, qty, 3, day, 118, pid, code, 2600 + pid))
# æ‚项入库
for i, (item, qty, wh) in enumerate(misc[3]):
    in_rows.append((item, qty, wh, misc[2], 115, 1, misc[1], 2700 + i))
# ç”Ÿäº§é¢†æ–™ï¼ˆå‡ºåº“)
for iid, code, wo, day, items in issues:
    for item, qty in items:
        wh = 4 if item in (1015, 1016) else 1
        out_rows.append((item, qty, wh, day, 116, iid, code, 2800 + iid * 10 + item % 10))
# é”€å”®å‡ºåº“
for sid, code, so, client, day, items in sales:
    for item, qty in items:
        out_rows.append((item, qty, 3, day, 119, sid, code, 2900 + sid * 10 + item % 10))
tx_rows = []
tx_id = 0
for item, qty, wh, day, biz_type, biz_id, biz_code, seed in in_rows:
    tx_id += 1
    tx_rows.append((tx_id, 1, biz_type, biz_id, biz_code, item, qty, None, wh, LOC[wh], AREA[wh], ts(day, seed), ADMIN, ts(day, seed)))
for item, qty, wh, day, biz_type, biz_id, biz_code, seed in out_rows:
    tx_id += 1
    tx_rows.append((tx_id, 2, biz_type, biz_id, biz_code, item, -qty, None, wh, LOC[wh], AREA[wh], ts(day, seed), ADMIN, ts(day, seed)))
clean('mes_wm_transaction', [r[0] for r in tx_rows])
ins('mes_wm_transaction', ['id', 'type', 'biz_type', 'biz_id', 'biz_code', 'item_id', 'quantity', 'batch_id',
                           'warehouse_id', 'location_id', 'area_id', 'transaction_time', 'creator', 'create_time'], tx_rows)
# åº“å­˜ = äº‹åŠ¡æ±‡æ€»
agg = {}
for t in tx_rows:
    key = (t[5], t[8])  # item, warehouse
    agg[key] = agg.get(key, 0) + float(t[6])
stock_rows = []
sid = 0
for (item, wh), qty in sorted(agg.items()):
    if abs(qty) < 1e-9:
        continue
    sid += 1
    stock_rows.append((sid, item, wh, LOC[wh], AREA[wh], round(qty, 3), 0, 0, 0, ADMIN,
                       ts(D(2026, 10, 10), 3000 + sid)))
clean('mes_wm_material_stock', list(range(1, 200)))
ins('mes_wm_material_stock', ['id', 'item_id', 'warehouse_id', 'location_id', 'area_id', 'quantity', 'frozen_quantity',
                              'reserved_quantity', 'in_transit_quantity', 'creator', 'create_time'], stock_rows)
print('transactions', len(tx_rows), 'stock rows', len(stock_rows))
# ==================== 7. MPS / å·¥å• / å·¥å•工序 / æŠ¥å·¥ / æµè½¬å¡ ====================
mps = [
    # id, code, status, so, so_item, mes_product, qty, produced, client, request_date, plan_start, issue, finish, wo, wo_code
    (1, 'MPS20260805001', 2, 1, 1, 1001, 300, 300, 1, D(2026, 8, 20), D(2026, 8, 8), D(2026, 8, 8), D(2026, 8, 12), 1, 'WO20260808001'),
    (2, 'MPS20260812002', 2, 2, 3, 1003, 200, 200, 2, D(2026, 8, 28), D(2026, 8, 15), D(2026, 8, 15), D(2026, 9, 8), 2, 'WO20260815002'),
    (3, 'MPS20260902003', 1, 3, 5, 1001, 500, 250, 3, D(2026, 9, 30), D(2026, 9, 5), D(2026, 9, 5), None, 3, 'WO20260905003'),
    (4, 'MPS20260915004', 5, 4, 7, 1002, 260, 0, 4, D(2026, 10, 15), D(2026, 9, 20), None, None, None, None),
    (5, 'MPS20260805005', 2, 1, 2, 1002, 150, 150, 1, D(2026, 8, 22), D(2026, 8, 10), D(2026, 8, 10), D(2026, 8, 16), 4, 'WO20260810004'),
    (6, 'MPS20261102006', 0, 8, 14, 1002, 120, 0, 2, D(2026, 11, 30), D(2026, 11, 10), None, None, None, None),
]
mps_rows = []
for m in mps:
    mid, code, status, so, soi, prod, qty, produced, client, req, plan, issue, finish, wo, wocode = m
    sono = [x for x in sale_orders if x[0] == so][0][1]
    mps_rows.append((mid, code, status, so, sono, soi, prod, qty, produced, 1, client, req, plan, issue, finish,
                     wo, wocode, 0, 1, ts(plan, 3100 + mid), ADMIN, ts(plan, 3200 + mid)))
clean('mes_pro_mps', [r[0] for r in mps_rows])
ins('mes_pro_mps', ['id', 'code', 'status', 'sale_order_id', 'sale_order_no', 'sale_order_item_id', 'product_id',
                    'quantity', 'quantity_produced', 'unit_measure_id', 'client_id', 'request_date', 'plan_start_date',
                    'issue_date', 'finish_date', 'work_order_id', 'work_order_code', 'merge_flag', 'audit_user_id',
                    'audit_time', 'creator', 'create_time'], mps_rows)
wos = [
    # id, code, mes_product, name, qty, produced, client, route, start, finish, status, so_code
    (1, 'WO20260808001', 1001, '法兰盘 DN50', 300, 300, 1, 1, D(2026, 8, 8), D(2026, 8, 12), 2, 'SO20260805001'),
    (2, 'WO20260815002', 1003, '轴承座 6205', 200, 200, 2, 2, D(2026, 8, 15), D(2026, 9, 8), 2, 'SO20260812002'),
    (3, 'WO20260905003', 1001, '法兰盘 DN50', 500, 250, 3, 1, D(2026, 9, 5), None, 1, 'SO20260902003'),
    (4, 'WO20260810004', 1002, '法兰盘 DN100', 150, 150, 1, 1, D(2026, 8, 10), D(2026, 8, 16), 2, 'SO20260805001'),
]
wo_rows = []
for w in wos:
    wid, code, prod, name, qty, produced, client, route, start, finish, status, so_code = w
    wo_rows.append((wid, code, name, 1, 1, 1, 1, so_code, prod, qty, produced, qty, client, route,
                    ts(start, 3300 + wid), ts(start, 3400 + wid), 249, finish, status, None, ADMIN, ts(start, 3500 + wid)))
clean('mes_pro_work_order', [r[0] for r in wo_rows])
ins('mes_pro_work_order', ['id', 'code', 'name', 'type', 'factory_id', 'workshop_id', 'order_source_type',
                           'order_source_code', 'product_id', 'quantity', 'quantity_produced', 'quantity_scheduled',
                           'client_id', 'route_id', 'request_date', 'actual_start_time', 'start_user_id', 'finish_date',
                           'status', 'remark', 'creator', 'create_time'], wo_rows)
route_procs = {1: [1, 2, 4, 5, 6], 2: [1, 3, 4, 5, 6]}
wop_rows = []
wop_id = 0
for w in wos:
    wid, route = w[0], w[7]
    proc_ids = route_procs[route]
    for sort, pid in enumerate(proc_ids, start=1):
        wop_id += 1
        nxt = proc_ids[sort] if sort < len(proc_ids) else None
        wh = 1
        wop_rows.append((wop_id, wid, pid, sort, 98.0, nxt, wh, LOC[wh], AREA[wh], ADMIN, ts(w[8], 3600 + wop_id)))
clean('mes_pro_work_order_process', [r[0] for r in wop_rows])
ins('mes_pro_work_order_process', ['id', 'work_order_id', 'process_id', 'sort', 'yield_rate', 'next_process_id',
                                   'warehouse_id', 'location_id', 'area_id', 'creator', 'create_time'], wop_rows)
feedbacks = [
    # id, code, wo, mes_item, qty, qualified, status, date, process, user
    (1, 'FB20260810001', 1, 1001, 150, 150, 4, D(2026, 8, 10), 5, 249),
    (2, 'FB20260812002', 1, 1001, 150, 150, 4, D(2026, 8, 12), 5, 250),
    (3, 'FB20260816003', 4, 1002, 150, 150, 4, D(2026, 8, 16), 5, 249),
    (4, 'FB20260908004', 2, 1003, 200, 198, 4, D(2026, 9, 8), 5, 250),
    (5, 'FB20261005005', 3, 1001, 250, 250, 3, D(2026, 10, 5), 5, 251),
]
fb_rows = []
for f in feedbacks:
    fid, code, wo, item, qty, qualified, status, day, proc, user = f
    fb_rows.append((fid, code, 1, 1, ts(day, 3700 + fid), proc, 1, proc, wo, item, qty, qty, qualified,
                    qty - qualified, 0 if status == 4 else qty, user, status, ADMIN, ts(day, 3750 + fid)))
clean('mes_pro_feedback', [r[0] for r in fb_rows])
ins('mes_pro_feedback', ['id', 'code', 'type', 'channel', 'feedback_time', 'workstation_id', 'route_id', 'process_id',
                         'work_order_id', 'item_id', 'scheduled_quantity', 'feedback_quantity', 'qualified_quantity',
                         'unqualified_quantity', 'uncheck_quantity', 'feedback_user_id', 'status', 'creator', 'create_time'], fb_rows)
cards = [(1, 'CARD20260808001', 1, 1001, 'B-WO1', 300, 2, D(2026, 8, 8)),
         (2, 'CARD20260815002', 2, 1003, 'B-WO2', 200, 2, D(2026, 8, 15)),
         (3, 'CARD20260905003', 3, 1001, 'B-WO3', 250, 1, D(2026, 9, 5)),
         (4, 'CARD20260810004', 4, 1002, 'B-WO4', 150, 2, D(2026, 8, 10))]
clean('mes_pro_card', [c[0] for c in cards])
ins('mes_pro_card', ['id', 'code', 'work_order_id', 'item_id', 'batch_code', 'transfered_quantity', 'status',
                     'creator', 'create_time'],
    [(c[0], c[1], c[2], c[3], c[4], c[5], c[6], ADMIN, ts(c[7], 3800 + c[0])) for c in cards])
andon = [(1, 5, 249, 1, 2, '设备异响,申请点检', 2, 20, D(2026, 8, 11)),
         (2, 3, 250, 3, 3, '来料尺寸偏差,待确认', 1, 20, D(2026, 9, 26))]
clean('mes_pro_andon_record', [a[0] for a in andon])
ins('mes_pro_andon_record', ['id', 'workstation_id', 'user_id', 'work_order_id', 'process_id', 'reason', 'level',
                             'status', 'handle_time', 'handler_user_id', 'creator', 'create_time'],
    [(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], ts(a[8], 3900 + a[0]), 249, ADMIN, ts(a[8], 3950 + a[0])) for a in andon])
print('mps/wo/feedback ok')
# ==================== 8. è´¨æ£€ ====================
templates = [(1, 'QC-TPL-IQC', '来料检验方案', '1', 'GB/T 2828.1 æ­£å¸¸æ£€éªŒä¸€æ¬¡æŠ½æ ·'),
             (2, 'QC-TPL-IPQC', '过程检验方案', '2', 'GB/T 2828.1 æ­£å¸¸æ£€éªŒä¸€æ¬¡æŠ½æ ·'),
             (3, 'QC-TPL-OQC', '出货检验方案', '3', 'GB/T 2828.1 æ­£å¸¸æ£€éªŒä¸€æ¬¡æŠ½æ ·')]
clean('mes_qc_template', [t[0] for t in templates])
ins('mes_qc_template', ['id', 'code', 'name', 'types', 'default_execute_standard', 'status', 'creator', 'create_time'],
    [(t[0], t[1], t[2], t[3], t[4], 0, ADMIN, ts(D(2026, 7, 20), 4000 + t[0])) for t in templates])
inds = [
    (1, 'QC-APPEAR', '外观', 3, '无', 1, '表面无划伤、无锈蚀'),
    (2, 'QC-SIZE', '尺寸公差', 1, 'mm', 2, 'φ60±0.5'),
    (3, 'QC-MAT', '材质报告', 5, None, 3, '随货材质单'),
    (4, 'QC-DIM', '加工尺寸', 1, 'mm', 4, '按图纸公差'),
    (5, 'QC-HARD', '硬度', 1, 'HB', 5, 'HB170~210'),
    (6, 'QC-PACK', '包装外观', 3, None, 6, '包装完好无破损'),
]
clean('mes_qc_indicator', [i[0] for i in inds])
ins('mes_qc_indicator', ['id', 'code', 'name', 'item_type', 'type', 'unit_text', 'sort_order', 'tool', 'remark',
                         'result_specification', 'creator', 'create_time'],
    [(i[0], i[1], i[2], 1, i[3], i[4], i[5], '游标卡尺/目测', None, i[6], ADMIN, ts(D(2026, 7, 20), 4100 + i[0])) for i in inds])
ti = [(1, 1, 1, 1, None, None, None, '表面无划伤、无锈蚀'),
      (2, 1, 2, 2, 60.5, 61, 59, 'φ60±0.5mm'),
      (3, 1, 3, 3, None, None, None, '随货材质单'),
      (4, 2, 4, 1, None, None, None, '按图纸公差'),
      (5, 2, 5, 2, 210, 210, 170, 'HB170~210'),
      (6, 3, 6, 1, None, None, None, '包装完好无破损'),
      (7, 3, 1, 2, None, None, None, '表面无划伤')]
clean('mes_qc_template_indicator', [r[0] for r in ti])
ins('mes_qc_template_indicator', ['id', 'template_id', 'indicator_id', 'sort_order', 'judge_flag', 'standard_value',
                                  'threshold_max', 'threshold_min', 'remark', 'creator', 'create_time'],
    [(r[0], r[1], r[2], r[3], 1, r[4], r[5], r[6], r[7], ADMIN, ts(D(2026, 7, 21), 4200 + r[0])) for r in ti])
iqcs = [  # id, code, receipt, item, vendor, received, date, qualified
    (1, 'IQC20260806001', 1, 1009, 1, 6000, D(2026, 8, 6), 6000),
    (2, 'IQC20260807002', 2, 1012, 4, 20000, D(2026, 8, 7), 19920),
    (3, 'IQC20260825003', 3, 1010, 2, 4000, D(2026, 8, 25), 4000),
]
clean('mes_qc_iqc', [r[0] for r in iqcs])
ins('mes_qc_iqc', ['id', 'code', 'name', 'template_id', 'source_doc_type', 'source_doc_id', 'source_doc_code', 'vendor_id',
                   'vendor_name', 'item_id', 'received_quantity', 'check_quantity', 'qualified_quantity',
                   'unqualified_quantity', 'check_result', 'receive_date', 'inspect_date', 'inspector_user_id',
                   'status', 'execute_standard', 'creator', 'create_time'],
    [(r[0], r[1], f'{SHORT[r[3]]} æ¥æ–™æ£€éªŒ', 1, 100, r[2], f'IR2026080{r[0]}001', r[4], f'供应商{r[4]}', r[3],
      r[5], min(r[5], 200), r[7], r[5] - r[7], 1 if r[5] == r[7] else 2, r[6], ts(r[6], 4300 + r[0]), 249, 4,
      'GB/T 2828.1 æ­£å¸¸æ£€éªŒä¸€æ¬¡æŠ½æ ·', ADMIN, ts(r[6], 4350 + r[0])) for r in iqcs])
iqcl = []
for i, r in enumerate(iqcs, start=1):
    iqcl.append((i * 2 - 1, r[0], 1, '目测', '外观检查', None, None, None, None, '无划伤锈蚀', ADMIN, ts(r[6], 4400 + i)))
    iqcl.append((i * 2, r[0], 2, '游标卡尺', '测量', 60.0, 1, 61.0, 59.0, 'φ60±0.5', ADMIN, ts(r[6], 4450 + i)))
clean('mes_qc_iqc_line', [r[0] for r in iqcl])
ins('mes_qc_iqc_line', ['id', 'iqc_id', 'indicator_id', 'tool', 'check_method', 'standard_value', 'unit_measure_id',
                        'max_threshold', 'min_threshold', 'remark', 'creator', 'create_time'], iqcl)
ipqcs = [  # id, code, feedback, wo, item, qty, qualified, date
    (1, 'IPQC20260810001', 1, 1, 1001, 150, 150, D(2026, 8, 10)),
    (2, 'IPQC20260908002', 4, 2, 1003, 200, 198, D(2026, 9, 8)),
    (3, 'IPQC20261005003', 5, 3, 1001, 250, 250, D(2026, 10, 5)),
]
clean('mes_qc_ipqc', [r[0] for r in ipqcs])
ins('mes_qc_ipqc', ['id', 'code', 'name', 'type', 'template_id', 'source_doc_type', 'source_doc_id', 'source_doc_code',
                    'work_order_id', 'workstation_id', 'process_id', 'item_id', 'check_quantity', 'qualified_quantity',
                    'unqualified_quantity', 'check_result', 'inspect_date', 'inspector_user_id', 'status',
                    'execute_standard', 'creator', 'create_time'],
    [(r[0], r[1], f'{SHORT[r[4]]} è¿‡ç¨‹æ£€éªŒ', 2, 2, 304, r[2], f'FB2026{r[0]}', r[3], 5, 5, r[4], r[5], r[6],
      r[5] - r[6], 1 if r[5] == r[6] else 2, ts(r[7], 4500 + r[0]), 250, 4,
      'GB/T 2828.1 æ­£å¸¸æ£€éªŒä¸€æ¬¡æŠ½æ ·', ADMIN, ts(r[7], 4550 + r[0])) for r in ipqcs])
ipql = []
for i, r in enumerate(ipqcs, start=1):
    ipql.append((i * 2 - 1, r[0], 4, '游标卡尺', '测量', None, 1, None, None, '按图纸公差', ADMIN, ts(r[7], 4600 + i)))
    ipql.append((i * 2, r[0], 5, '硬度计', '打点', 190, None, 210, 170, 'HB170~210', ADMIN, ts(r[7], 4650 + i)))
clean('mes_qc_ipqc_line', [r[0] for r in ipql])
ins('mes_qc_ipqc_line', ['id', 'ipqc_id', 'indicator_id', 'tool', 'check_method', 'standard_value', 'unit_measure_id',
                         'max_threshold', 'min_threshold', 'remark', 'creator', 'create_time'], ipql)
oqcs = [  # id, code, sales, item, client, out_qty, date, qualified
    (1, 'OQC20260815001', 1, 1001, 1, 300, D(2026, 8, 15), 300),
    (2, 'OQC20260910002', 2, 1003, 2, 200, D(2026, 9, 10), 200),
]
clean('mes_qc_oqc', [r[0] for r in oqcs])
ins('mes_qc_oqc', ['id', 'code', 'name', 'template_id', 'source_doc_type', 'source_doc_id', 'source_doc_code', 'client_id',
                   'item_id', 'out_quantity', 'check_quantity', 'qualified_quantity', 'unqualified_quantity',
                   'check_result', 'out_date', 'inspect_date', 'inspector_user_id', 'status', 'execute_standard',
                   'creator', 'create_time'],
    [(r[0], r[1], f'{SHORT[r[3]]} å‡ºè´§æ£€éªŒ', 3, 119, r[2], f'PS2026{r[0]}', r[4], r[3], r[5], r[5], r[7],
      r[5] - r[7], 1, r[6], ts(r[6], 4700 + r[0]), 250, 4, 'GB/T 2828.1 æ­£å¸¸æ£€éªŒä¸€æ¬¡æŠ½æ ·', ADMIN, ts(r[6], 4750 + r[0]))
     for r in oqcs])
oqcl = []
for i, r in enumerate(oqcs, start=1):
    oqcl.append((i * 2 - 1, r[0], 6, '目测', '外观检查', None, None, None, None, '包装完好', ADMIN, ts(r[6], 4800 + i)))
    oqcl.append((i * 2, r[0], 1, '目测', '外观检查', None, None, None, None, '无划伤', ADMIN, ts(r[6], 4850 + i)))
clean('mes_qc_oqc_line', [r[0] for r in oqcl])
ins('mes_qc_oqc_line', ['id', 'oqc_id', 'indicator_id', 'tool', 'check_method', 'standard_value', 'unit_measure_id',
                        'max_threshold', 'min_threshold', 'remark', 'creator', 'create_time'], oqcl)
defects = [(1, 'DF-01', '表面划伤', 1, 3), (2, 'DF-02', '尺寸超差', 1, 2),
           (3, 'DF-03', '毛刺未清', 1, 3), (4, 'DF-04', '材质不符', 1, 1)]
clean('mes_qc_defect', [d[0] for d in defects])
ins('mes_qc_defect', ['id', 'code', 'name', 'type', 'level', 'creator', 'create_time'],
    [(d[0], d[1], d[2], d[3], d[4], ADMIN, ts(D(2026, 7, 22), 4900 + d[0])) for d in defects])
defrec = [(1, 2, 2, 2, '尺寸超差', 2, 8, D(2026, 8, 7)),
          (2, 2, 2, 3, '毛刺未清', 3, 12, D(2026, 8, 7))]
clean('mes_qc_defect_record', [d[0] for d in defrec])
ins('mes_qc_defect_record', ['id', 'qc_type', 'qc_id', 'name', 'level', 'quantity', 'creator', 'create_time'],
    [(d[0], d[1], d[2], d[4], d[5], d[6], ADMIN, ts(d[7], 4950 + d[0])) for d in defrec])
ncr = (1, 'NCR20260807001', '螺栓来料尺寸超差', 1, 2, 'IQC20260807002', 1012, 2, 80,
       '螺栓 M8x30 æ¥æ–™ä¸­ 80 ä»¶å¤–径超差', 1, 126, 250, D(2026, 8, 8), '判定退货', 249, D(2026, 8, 9),
       '已退供应商,供应商补发', '供应商模具磨损', '要求供应商更换模具并全检', '进货检验加严抽检', 250, D(2026, 8, 12),
       '已闭环', 4, D(2026, 8, 7))
clean('mes_qc_ncr', [ncr[0]])
ins('mes_qc_ncr', ['id', 'code', 'name', 'qc_type', 'qc_id', 'qc_code', 'item_id', 'defect_level', 'unqualified_quantity',
                   'defect_description', 'disposition', 'responsible_dept_id', 'reviewer_user_id', 'review_date',
                   'review_opinion', 'handler_user_id', 'handle_date', 'handle_result', 'root_cause', 'corrective_action',
                   'preventive_action', 'close_user_id', 'close_date', 'close_opinion', 'status', 'creator', 'create_time'],
    [(ncr[0], ncr[1], ncr[2], ncr[3], ncr[4], ncr[5], ncr[6], ncr[7], ncr[8], ncr[9], ncr[10], ncr[11], ncr[12],
      ts(ncr[13], 5000), ncr[14], ncr[15], ts(ncr[16], 5010), ncr[17], ncr[18], ncr[19], ncr[20], ncr[21],
      ts(ncr[22], 5020), ncr[23], ncr[24], ADMIN, ts(ncr[25], 5030))])
print('qc ok')
# ==================== 9. è®¾å¤‡ç®¡ç† ====================
subjects = [(1, 'SUB-APPEAR', '设备外观', 1, '检查设备表面是否清洁、无油污、无锈蚀', '表面清洁'),
            (2, 'SUB-LUBE', '润滑系统', 1, '检查油位、油质及润滑点', '油位在刻度线内'),
            (3, 'SUB-ELEC', '电气系统', 1, '检查线路、按钮、指示灯', '无裸露线路'),
            (4, 'SUB-SAFE', '安全防护', 1, '检查防护罩、急停按钮', '防护齐全有效'),
            (5, 'SUB-PRECISE', '精度检查', 1, '检查各轴定位精度', '符合出厂标准'),
            (6, 'SUB-CLEAN', '清洁保养', 2, '清理铁屑、擦拭导轨', '无铁屑堆积')]
clean('mes_dv_subject', [s[0] for s in subjects])
ins('mes_dv_subject', ['id', 'code', 'name', 'type', 'content', 'standard', 'status', 'creator', 'create_time'],
    [(s[0], s[1], s[2], s[3], s[4], s[5], 0, ADMIN, ts(D(2026, 7, 25), 5100 + s[0])) for s in subjects])
plans = [(1, 'CP20260901001', '机床日常点检计划', 1, D(2026, 9, 1), D(2027, 8, 31), 1, 1),
         (2, 'CP20260901002', '机床月度保养计划', 2, D(2026, 9, 1), D(2027, 8, 31), 3, 1)]
clean('mes_dv_check_plan', [p[0] for p in plans])
ins('mes_dv_check_plan', ['id', 'code', 'name', 'type', 'start_date', 'end_date', 'cycle_type', 'cycle_count',
                          'status', 'creator', 'create_time'],
    [(p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], 1, ADMIN, ts(D(2026, 8, 28), 5200 + p[0])) for p in plans])
pm_rows = [(i, 1, i, ADMIN, ts(D(2026, 8, 28), 5300 + i)) for i in range(1, 7)]
clean('mes_dv_check_plan_machinery', [r[0] for r in pm_rows])
ins('mes_dv_check_plan_machinery', ['id', 'plan_id', 'machinery_id', 'creator', 'create_time'], pm_rows)
ps_rows = []
ps_id = 0
for plan, subj_ids in [(1, [1, 2, 3]), (2, [4, 5, 6])]:
    for sid in subj_ids:
        ps_id += 1
        ps_rows.append((ps_id, plan, sid, ADMIN, ts(D(2026, 8, 28), 5400 + ps_id)))
clean('mes_dv_check_plan_subject', [r[0] for r in ps_rows])
ins('mes_dv_check_plan_subject', ['id', 'plan_id', 'subject_id', 'creator', 'create_time'], ps_rows)
records = []
rec_id = 0
for day in (D(2026, 9, 15), D(2026, 10, 20)):
    for mach in range(1, 7):
        rec_id += 1
        records.append((rec_id, 1, mach, ts(day, 5500 + rec_id), 249, 20, ADMIN, ts(day, 5550 + rec_id)))
clean('mes_dv_check_record', [r[0] for r in records])
ins('mes_dv_check_record', ['id', 'plan_id', 'machinery_id', 'check_time', 'user_id', 'status', 'creator', 'create_time'], records)
lines = []
lid = 0
for r in records:
    for sid in (1, 2, 3):
        lid += 1
        lines.append((lid, r[0], sid, 1, '正常', ADMIN, ts(r[3].date(), 5600 + lid)))
clean('mes_dv_check_record_line', [r[0] for r in lines])
ins('mes_dv_check_record_line', ['id', 'record_id', 'subject_id', 'check_status', 'check_result', 'creator', 'create_time'], lines)
mrec = []
for mach in range(1, 7):
    mrec.append((mach, 2, mach, ts(D(2026, 10, 12), 5700 + mach), 250, 4, ADMIN, ts(D(2026, 10, 12), 5750 + mach)))
clean('mes_dv_mainten_record', [r[0] for r in mrec])
ins('mes_dv_mainten_record', ['id', 'plan_id', 'machinery_id', 'mainten_time', 'user_id', 'status', 'creator', 'create_time'], mrec)
mlines = []
mid = 0
for r in mrec:
    for sid in (4, 5, 6):
        mid += 1
        mlines.append((mid, r[0], sid, 1, '已保养', ADMIN, ts(D(2026, 10, 12), 5800 + mid)))
clean('mes_dv_mainten_record_line', [r[0] for r in mlines])
ins('mes_dv_mainten_record_line', ['id', 'record_id', 'subject_id', 'status', 'result', 'creator', 'create_time'], mlines)
repairs = [(1, 'RP20261012001', '车床主轴异响维修', 2, D(2026, 10, 12), D(2026, 10, 13), D(2026, 10, 14), 8, 249, 250, 4),
           (2, 'RP20261018002', '钻床进给手柄卡滞维修', 4, D(2026, 10, 18), D(2026, 10, 18), D(2026, 10, 19), 3, 250, 249, 4)]
clean('mes_dv_repair', [r[0] for r in repairs])
ins('mes_dv_repair', ['id', 'code', 'name', 'machinery_id', 'require_date', 'finish_date', 'confirm_date', 'duration_hours',
                      'accepted_user_id', 'confirm_user_id', 'status', 'creator', 'create_time'],
    [(r[0], r[1], r[2], r[3], ts(r[4], 5900 + r[0]), ts(r[5], 5910 + r[0]), ts(r[6], 5920 + r[0]), r[7], r[8], r[9],
      r[10], ADMIN, ts(r[4], 5930 + r[0])) for r in repairs])
rl = [(1, 1, 5, '主轴轴承磨损', '更换轴承并重新调整间隙', ADMIN, ts(D(2026, 10, 12), 5940)),
      (2, 2, 4, '手柄丝杆缺油卡滞', '清洗丝杆并加注润滑脂', ADMIN, ts(D(2026, 10, 18), 5950))]
clean('mes_dv_repair_line', [r[0] for r in rl])
ins('mes_dv_repair_line', ['id', 'repair_id', 'subject_id', 'malfunction', 'description', 'creator', 'create_time'], rl)
spares = [(1, 'SP-BEAR-6205', '主轴轴承 6205', '6205', '个', '备件库A-01', 24, 8),
          (2, 'SP-BELT-A', '传动皮带 A型', 'A-1200', '条', '备件库A-02', 15, 5),
          (3, 'SP-OIL-46', '液压油 46#', '18L/æ¡¶', 'æ¡¶', '备件库B-01', 12, 4),
          (4, 'SP-FUSE-10A', '保险管 10A', '10A', '个', '备件库B-02', 50, 20),
          (5, 'SP-SEAL-O40', '油封 O-40', 'O-40', '个', '备件库A-03', 30, 10)]
clean('mes_dv_spare_part', [s[0] for s in spares])
ins('mes_dv_spare_part', ['id', 'code', 'name', 'specification', 'unit', 'location', 'stock_quantity', 'safety_quantity',
                          'status', 'creator', 'create_time'],
    [(s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], 0, ADMIN, ts(D(2026, 8, 1), 6000 + s[0])) for s in spares])
mov = [(1, 'REQ20261012001', 1, 2, 1, 1, D(2026, 10, 12)),
       (2, 'REQ20261018002', 4, 2, 2, 2, D(2026, 10, 18)),
       (3, 'REQ20261020003', 3, 1, 1, None, D(2026, 10, 20))]
clean('mes_dv_spare_movement', [m[0] for m in mov])
ins('mes_dv_spare_movement', ['id', 'request_key', 'spare_part_id', 'spare_part_code', 'spare_part_name', 'unit', 'type',
                              'quantity', 'before_quantity', 'after_quantity', 'repair_id', 'machinery_name',
                              'operator_user_id', 'remark', 'creator', 'create_time'],
    [(m[0], m[1], m[2], spares[m[2] - 1][1], spares[m[2] - 1][2], spares[m[2] - 1][4], m[3], m[4],
      spares[m[2] - 1][6], spares[m[2] - 1][6] + (m[4] if m[3] == 2 else -m[4]),
      m[5], f'设备{m[2]}', 249, '维修领用' if m[3] == 2 else '采购入库', ADMIN, ts(m[6], 6100 + m[0])) for m in mov])
meter = [(1, 'MT-001', '游标卡尺 0-150', '0-150mm', '三丰', 1, '0.02mm', '0-150mm', 8, D(2026, 3, 10), D(2026, 9, 10), D(2027, 3, 10)),
         (2, 'MT-002', '外径千分尺 0-25', '0-25mm', '三丰', 2, '0.001mm', '0-25mm', 10, D(2026, 4, 15), D(2026, 10, 15), D(2027, 4, 15)),
         (3, 'MT-003', '硬度计 TH110', 'TH110', '时代', 7, '±3HB', '170-210HB', 7, D(2026, 5, 20), D(2026, 11, 20), D(2027, 5, 20))]
clean('mes_dv_metering', [m[0] for m in meter])
ins('mes_dv_metering', ['id', 'code', 'name', 'specification', 'brand', 'category', 'precision', 'range', 'check_cycle_count',
                        'use_date', 'last_check_date', 'next_check_date', 'status', 'creator', 'create_time'],
    [(m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9], m[10], m[11], 0, ADMIN, ts(D(2026, 3, 1), 6200 + m[0]))
     for m in meter])
mcheck = [(1, 1, 'JC20260910001', D(2026, 9, 10), '唐山市计量所', 1, 'CERT-2026-0910'),
          (2, 2, 'JC20261015002', D(2026, 10, 15), '唐山市计量所', 1, 'CERT-2026-1015')]
clean('mes_dv_metering_check', [c[0] for c in mcheck])
ins('mes_dv_metering_check', ['id', 'metering_id', 'check_code', 'check_date', 'check_org', 'check_result', 'cert_no',
                              'check_user_id', 'check_user_name', 'next_check_date', 'creator', 'create_time'],
    [(c[0], c[1], c[2], c[3], c[4], c[5], c[6], 249, '赵磊', c[3] + datetime.timedelta(days=180), ADMIN, ts(c[3], 6300 + c[0]))
     for c in mcheck])
print('dv ok')
# ==================== 10. äººåŠ›èµ„æº ====================
EMP_USERS = [(243, '赵磊'), (244, '李万娟'), (245, '巨克萍'), (246, '马莉娟'), (247, '刘震'),
             (248, '魏海苹'), (249, '许海英'), (250, '魏山良'), (251, '陈慧慧'), (252, '余圈珠')]
cur.execute("DELETE FROM hrm_attendance_record WHERE user_id IN (%s)" % ','.join(['%s'] * 10), [u[0] for u in EMP_USERS])
att_rows = []
day = D(2026, 9, 21)
seed = 7000
while day <= D(2026, 10, 31):
    if day.weekday() < 5:
        for uid, name in EMP_USERS:
            seed += 1
            h = int(hashlib.md5(f'{uid}-{day}'.encode()).hexdigest(), 16)
            in_min = h % 25 - 10  # -10 ~ +14 åˆ†é’Ÿ
            out_min = (h >> 8) % 40 - 5
            ci = datetime.datetime.combine(day, datetime.time(8, 0)) + datetime.timedelta(minutes=in_min)
            co = datetime.datetime.combine(day, datetime.time(17, 30)) + datetime.timedelta(minutes=out_min)
            if h % 13 == 0:
                co = datetime.datetime.combine(day, datetime.time(19, 0)) + datetime.timedelta(minutes=h % 30)
            hours = round((co - ci).total_seconds() / 3600, 2)
            att_rows.append((uid, 126, day, ci, co, 1, 1, hours, max(0, round(hours - 8, 2)), '唐山市诚发五金厂区',
                             '正常出勤', 1, ADMIN, ci))
    day += datetime.timedelta(days=1)
ins('hrm_attendance_record', ['user_id', 'dept_id', 'date', 'clock_in_time', 'clock_out_time', 'clock_in_type',
                              'clock_out_type', 'work_hours', 'overtime_hours', 'location', 'remark', 'data_source',
                              'creator', 'create_time'], att_rows)
print('attendance', len(att_rows))
cur.execute("DELETE FROM hrm_salary_calculation WHERE period = '2026-10'")
sal_rows = []
for i, (uid, name) in enumerate(EMP_USERS):
    base = 6500 + (i % 10) * 300
    perf = 800 + (i % 5) * 200
    overtime = round(20 + (i % 6) * 6, 2)
    meal, trans, other = 300, 200, 100
    social = round(base * 0.105, 2)
    fund = round(base * 0.07, 2)
    tax = round(max(0, (base + perf + overtime * 30 - social - fund - 5000) * 0.03), 2)
    actual = round(base + perf + overtime * 30 + meal + trans + other - social - fund - tax, 2)
    status = 20 if i < 6 else 10
    sal_rows.append((f'SC202610{i + 1:04d}', '2026-10', uid, 126, base, perf, round(overtime * 30, 2), meal, trans, other,
                     social, fund, tax, 0, 0, actual, 31, overtime, 176, status,
                     ts(D(2026, 11, 1), 7100 + i), 249, ts(D(2026, 11, 2), 7200 + i) if status == 20 else None,
                     ADMIN, ts(D(2026, 11, 1), 7300 + i)))
ins('hrm_salary_calculation', ['no', 'period', 'user_id', 'dept_id', 'base_salary', 'performance_salary', 'overtime_pay',
                               'meal_allowance', 'transport_allowance', 'other_allowance', 'social_security_deduction',
                               'housing_fund_deduction', 'tax_deduction', 'other_deduction', 'leave_deduction',
                               'actual_salary', 'work_days', 'overtime_hours', 'total_work_hours', 'status',
                               'confirm_time', 'confirm_user_id', 'payment_time', 'creator', 'create_time'], sal_rows)
cur.execute("DELETE FROM hrm_salary_payment WHERE period = '2026-10'")
ins('hrm_salary_payment', ['no', 'period', 'total_amount', 'total_count', 'payment_method', 'payment_time', 'status',
                           'remark', 'creator', 'create_time'],
    [('SP2026100001', '2026-10', sum(float(r[15]) for r in sal_rows[:6]), 6, 1, ts(D(2026, 11, 5), 7400), 10,
      '10月工资(第一批)', ADMIN, ts(D(2026, 11, 5), 7450))])
print('salary ok')
# ==================== 13. å¾…检任务补充(补货到货 + é€€è´§æ£€éªŒï¼‰ ====================
# å¾…检任务页需要「已到货未检验」与「已出库未检验」两类数据,主链路的单据都已检验完,这里补几条
extra_notices = [  # id, code, po_id, vendor, date, [(item, qty)]
    (6, 'AN20261028006', 3, 2, D(2026, 10, 28), [(1010, 500)]),
    (7, 'AN20261029007', 4, 5, D(2026, 10, 29), [(1015, 80), (1016, 60)]),
]
ex_an, ex_anl = [], []
ex_anl_id = 20
for nid, code, po_id, vendor, day, items in extra_notices:
    pono = [x for x in po_rows if x[0] == po_id][0][1]
    ex_an.append((nid, code, f'{pono} è¡¥è´§åˆ°è´§é€šçŸ¥', po_id, pono, vendor, ts(day, 1100 + nid), 2, ADMIN, ts(day, 1200 + nid)))
    for item, qty in items:
        poi = [r for r in poi_by_po[po_id] if r[3] == item][0]
        ex_anl.append((ex_anl_id, nid, poi[0], item, qty, qty, 1, ADMIN, ts(day, 1300 + ex_anl_id)))
        ex_anl_id += 1
clean('mes_wm_arrival_notice', [r[0] for r in ex_an])
clean('mes_wm_arrival_notice_line', [r[0] for r in ex_anl])
ins('mes_wm_arrival_notice', ['id', 'code', 'name', 'purchase_order_id', 'purchase_order_code', 'vendor_id',
                              'arrival_date', 'status', 'creator', 'create_time'], ex_an)
ins('mes_wm_arrival_notice_line', ['id', 'notice_id', 'purchase_order_item_id', 'item_id', 'arrival_quantity',
                                   'qualified_quantity', 'iqc_check_flag', 'creator', 'create_time'], ex_anl)
ex_ps = (4, 'PS20261028004', 4, 4, D(2026, 10, 28), [(1002, 100), (1006, 20)])
sid, code, so, client, day, items = ex_ps
sono = [x for x in sale_orders if x[0] == so][0][1]
soi_by_item = {r[2]: r[0] for r in soi_rows if r[1] == so}
ex_ps_row = (sid, code, f'{sono} é”€å”®å‡ºåº“', client, so, sono, ts(day, 2200 + sid), '刘经理', '13900000004',
             '北京市大兴区', '顺丰物流', f'SF2026102{sid:03d}', 1, ADMIN, ts(day, 2300 + sid))
ex_psl_rows = [(20 + i, sid, soi_by_item.get(item), item, UNIT[item], qty, qty, 1, 0, ADMIN, ts(day, 2350 + i))
               for i, (item, qty) in enumerate(items)]
clean('mes_wm_product_sales', [sid])
clean('mes_wm_product_sales_line', [r[0] for r in ex_psl_rows])
ins('mes_wm_product_sales', ['id', 'code', 'name', 'client_id', 'sale_order_id', 'sales_order_code', 'sales_date',
                             'contact_name', 'contact_telephone', 'contact_address', 'carrier', 'shipping_number',
                             'status', 'creator', 'create_time'], [ex_ps_row])
ins('mes_wm_product_sales_line', ['id', 'sales_id', 'sale_order_item_id', 'item_id', 'enter_unit_measure_id',
                                  'enter_quantity', 'quantity', 'oqc_check_flag', 'quality_status', 'creator', 'create_time'], ex_psl_rows)
# é€€è´§æ£€éªŒï¼šå®¢æˆ·é€€å›žæ³•兰盘 10 ä»¶ï¼Œ8 ä»¶åˆæ ¼ 2 ä»¶åˆ’伤
rqc_row = (1, 'RQC20261026001', '法兰盘 DN50 é€€è´§æ£€éªŒ', 1, 120, 1, None, 'PS20260815001', 1, 1001, 'BT20260806001',
           10, 8, 2, None, None, None, None, None, None, 3, ts(D(2026, 10, 26), 5100), 250, 4, None, None,
           'GB/T 2828.1 æ­£å¸¸æ£€éªŒä¸€æ¬¡æŠ½æ ·', None, None, None, ADMIN, ts(D(2026, 10, 26), 5050))
clean('mes_qc_rqc', [rqc_row[0]])
ins('mes_qc_rqc', ['id', 'code', 'name', 'template_id', 'source_doc_type', 'source_doc_id', 'source_line_id',
                   'source_doc_code', 'type', 'item_id', 'batch_code', 'check_quantity', 'qualified_quantity',
                   'unqualified_quantity', 'critical_rate', 'major_rate', 'minor_rate', 'critical_quantity',
                   'major_quantity', 'minor_quantity', 'check_result', 'inspect_date', 'inspector_user_id', 'status',
                   'sampling_location', 'certificate_no', 'execute_standard', 'audit_user_id', 'approve_user_id',
                   'remark', 'creator', 'create_time'], [rqc_row])
rqcl = [
    (1, 1, 1, '目测', '外观检查', None, None, None, None, None, None, None, '表面划伤 2 ä»¶', ADMIN, ts(D(2026, 10, 26), 5100)),
    (2, 1, 2, '游标卡尺', '测量', 30, 1, None, None, None, None, None, '尺寸合格', ADMIN, ts(D(2026, 10, 26), 5120)),
]
clean('mes_qc_rqc_line', [r[0] for r in rqcl])
ins('mes_qc_rqc_line', ['id', 'rqc_id', 'indicator_id', 'tool', 'check_method', 'standard_value', 'unit_measure_id',
                        'max_threshold', 'min_threshold', 'critical_quantity', 'major_quantity', 'minor_quantity',
                        'remark', 'creator', 'create_time'], rqcl)
print('pending-inspect extra ok')
conn.commit()
print('DOCS OK')
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/mps/MesProMpsController.java
@@ -10,6 +10,7 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsPreparationCheckRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderItemRespVO;
@@ -158,6 +159,14 @@
        return success(mpsService.getMpsItemList(id));
    }
    @GetMapping("/preparation-check")
    @Operation(summary = "生产准备检查(物料、设备)")
    @Parameter(name = "id", description = "MPS ç¼–号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:pro-mps:query')")
    public CommonResult<MesProMpsPreparationCheckRespVO> getPreparationCheck(@RequestParam("id") Long id) {
        return success(mpsService.getPreparationCheck(id));
    }
    @PostMapping("/generate-purchase-request")
    @Operation(summary = "根据生产主计划辅料缺口生成采购申请单")
    @Parameter(name = "id", description = "MPS ç¼–号", required = true)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/mps/vo/MesProMpsPreparationCheckRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
package cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
 * MES ç”Ÿäº§ä¸»è®¡åˆ’ - ç”Ÿäº§å‡†å¤‡æ£€æŸ¥ç»“æžœ Response VO
 *
 * åŸºäºŽç”Ÿäº§è®¡åˆ’检查物料、设备两类生产要素是否齐备
 */
@Schema(description = "管理后台 - ç”Ÿäº§å‡†å¤‡æ£€æŸ¥ç»“æžœ Response VO")
@Data
public class MesProMpsPreparationCheckRespVO {
    @Schema(description = "计划编号", example = "1")
    private Long mpsId;
    @Schema(description = "计划单号", example = "MPS20261103001")
    private String mpsCode;
    @Schema(description = "产品编号", example = "1")
    private Long productId;
    @Schema(description = "产品名称", example = "法兰盘")
    private String productName;
    @Schema(description = "计划数量", example = "100")
    private BigDecimal quantity;
    @Schema(description = "物料是否齐备")
    private Boolean materialReady;
    @Schema(description = "设备是否齐备")
    private Boolean equipmentReady;
    @Schema(description = "是否可以进行生产")
    private Boolean ready;
    @Schema(description = "检查结论说明,例如「3 é¡¹ç‰©æ–™çŸ­ç¼ºã€2 å°è®¾å¤‡ä¸å¯ç”¨ã€")
    private String summary;
    @Schema(description = "物料检查明细")
    private List<MaterialCheck> materials = new ArrayList<>();
    @Schema(description = "设备检查明细")
    private List<EquipmentCheck> equipments = new ArrayList<>();
    @Schema(description = "物料检查明细")
    @Data
    public static class MaterialCheck {
        @Schema(description = "物料编号")
        private Long itemId;
        @Schema(description = "物料编码")
        private String itemCode;
        @Schema(description = "物料名称")
        private String itemName;
        @Schema(description = "规格")
        private String specification;
        @Schema(description = "单位")
        private String unitMeasureName;
        @Schema(description = "需求数量")
        private BigDecimal requiredQuantity;
        @Schema(description = "现有库存")
        private BigDecimal stockQuantity;
        @Schema(description = "在途采购数量")
        private BigDecimal purchaseOrderQuantity;
        @Schema(description = "缺口数量")
        private BigDecimal shortageQuantity;
        @Schema(description = "是否充足")
        private Boolean sufficient;
    }
    @Schema(description = "设备检查明细")
    @Data
    public static class EquipmentCheck {
        @Schema(description = "工序编号")
        private Long processId;
        @Schema(description = "工序名称")
        private String processName;
        @Schema(description = "设备编号")
        private Long machineryId;
        @Schema(description = "设备编码")
        private String machineryCode;
        @Schema(description = "设备名称")
        private String machineryName;
        @Schema(description = "设备状态(1-停机 2-生产中 3-维护中)")
        private Integer status;
        @Schema(description = "设备状态名称")
        private String statusName;
        @Schema(description = "是否可用")
        private Boolean available;
        @Schema(description = "不可用原因")
        private String reason;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/MesWmTransactionController.java
@@ -3,8 +3,11 @@
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmStockSummaryReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmStockSummaryRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmTransactionPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmTransactionRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transaction.MesWmTransactionDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
@@ -12,6 +15,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
@@ -52,6 +56,8 @@
    private MesWmWarehouseLocationService locationService;
    @Resource
    private MesWmWarehouseAreaService areaService;
    @Resource
    private MesMdUnitMeasureService unitMeasureService;
    @GetMapping("/page")
    @Operation(summary = "获取库存事务流水分页")
@@ -110,6 +116,42 @@
        return success(new PageResult<>(voList, pageResult.getTotal()));
    }
    @GetMapping("/summary")
    @Operation(summary = "获取仓储收发存汇总(按物料 + ä»“库)")
    @PreAuthorize("@ss.hasPermission('mes:wm-transaction:query')")
    public CommonResult<PageResult<MesWmStockSummaryRespVO>> getStockSummary(@Valid MesWmStockSummaryReqVO reqVO) {
        PageResult<MesWmStockSummaryRespVO> pageResult = transactionService.getStockSummaryPage(reqVO);
        if (pageResult.getList().isEmpty()) {
            return success(PageResult.empty(pageResult.getTotal()));
        }
        // å…³è”物料、仓库、单位名称
        Map<Long, MesMdmItemDO> itemMap = mdmItemService.getItemMap(
                convertSet(pageResult.getList(), MesWmStockSummaryRespVO::getItemId));
        Map<Long, MesWmWarehouseDO> warehouseMap = warehouseService.getWarehouseMap(
                convertSet(pageResult.getList(), MesWmStockSummaryRespVO::getWarehouseId));
        Map<Long, MesMdUnitMeasureDO> unitMap = unitMeasureService.getUnitMeasureMap(
                itemMap.values().stream().map(MesMdmItemDO::getUnitMeasureId)
                        .filter(java.util.Objects::nonNull).collect(Collectors.toSet()));
        for (MesWmStockSummaryRespVO vo : pageResult.getList()) {
            MesMdmItemDO item = itemMap.get(vo.getItemId());
            if (item != null) {
                vo.setItemCode(item.getCode());
                vo.setItemName(item.getName());
                vo.setSpecification(item.getSpecification());
                MesMdUnitMeasureDO unit = unitMap.get(item.getUnitMeasureId());
                if (unit != null) {
                    vo.setUnitMeasureName(unit.getName());
                }
            }
            MesWmWarehouseDO warehouse = warehouseMap.get(vo.getWarehouseId());
            if (warehouse != null) {
                vo.setWarehouseName(warehouse.getName());
            }
        }
        return success(pageResult);
    }
    /**
     * èŽ·å–ä¸šåŠ¡ç±»åž‹åç§°
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmStockSummaryReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
/**
 * ä»“储收发存汇总查询 Request VO
 */
@Schema(description = "管理后台 - ä»“储收发存汇总查询 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesWmStockSummaryReqVO extends PageParam {
    @Schema(description = "物料ID", example = "1")
    private Long itemId;
    @Schema(description = "仓库ID", example = "1")
    private Long warehouseId;
    @Schema(description = "期初/查询开始时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime startTime;
    @Schema(description = "期末/查询结束时间")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
    private LocalDateTime endTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/transaction/vo/MesWmStockSummaryRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
 * ä»“储收发存汇总 Response VO
 */
@Schema(description = "管理后台 - ä»“储收发存汇总 Response VO")
@Data
public class MesWmStockSummaryRespVO {
    @Schema(description = "物料ID", example = "1")
    private Long itemId;
    @Schema(description = "物料编码", example = "MAT001")
    private String itemCode;
    @Schema(description = "物料名称", example = "法兰盘")
    private String itemName;
    @Schema(description = "规格", example = "DN100")
    private String specification;
    @Schema(description = "单位", example = "ä»¶")
    private String unitMeasureName;
    @Schema(description = "仓库ID", example = "1")
    private Long warehouseId;
    @Schema(description = "仓库名称", example = "成品仓")
    private String warehouseName;
    @Schema(description = "期初数量")
    private BigDecimal openingQuantity;
    @Schema(description = "本期入库数量")
    private BigDecimal inQuantity;
    @Schema(description = "本期出库数量")
    private BigDecimal outQuantity;
    @Schema(description = "期末数量")
    private BigDecimal closingQuantity;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/transaction/MesWmTransactionMapper.java
@@ -3,9 +3,15 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmStockSummaryRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmTransactionPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transaction.MesWmTransactionDO;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
/**
 * MES åº“存事务流水 Mapper
@@ -30,6 +36,32 @@
    }
    /**
     * æ”¶å‘存汇总:按期初、本期入库、本期出库、期末四个口径按「物料 + ä»“库」汇总
     */
    @Select("""
            <script>
            SELECT t.item_id AS itemId,
                   t.warehouse_id AS warehouseId,
                   COALESCE(SUM(CASE WHEN t.transaction_time &lt; #{startTime} THEN t.quantity ELSE 0 END), 0) AS openingQuantity,
                   COALESCE(SUM(CASE WHEN t.transaction_time &gt;= #{startTime} AND t.transaction_time &lt;= #{endTime} AND t.quantity &gt; 0 THEN t.quantity ELSE 0 END), 0) AS inQuantity,
                   COALESCE(SUM(CASE WHEN t.transaction_time &gt;= #{startTime} AND t.transaction_time &lt;= #{endTime} AND t.quantity &lt; 0 THEN -t.quantity ELSE 0 END), 0) AS outQuantity,
                   COALESCE(SUM(CASE WHEN t.transaction_time &lt;= #{endTime} THEN t.quantity ELSE 0 END), 0) AS closingQuantity
            FROM mes_wm_transaction t
            WHERE t.deleted = 0
              AND t.transaction_time &lt;= #{endTime}
              <if test="itemId != null"> AND t.item_id = #{itemId}</if>
              <if test="warehouseId != null"> AND t.warehouse_id = #{warehouseId}</if>
            GROUP BY t.item_id, t.warehouse_id
            ORDER BY t.item_id, t.warehouse_id
            </script>
            """)
    IPage<MesWmStockSummaryRespVO> selectStockSummaryPage(IPage<MesWmStockSummaryRespVO> page,
                                                          @Param("itemId") Long itemId,
                                                          @Param("warehouseId") Long warehouseId,
                                                          @Param("startTime") LocalDateTime startTime,
                                                          @Param("endTime") LocalDateTime endTime);
    /**
     * æŸ¥æ‰¾è™šæ‹Ÿçº¿è¾¹åº“库存记录对应的原始出库仓库信息
     * <p>
     * é€šè¿‡è™šæ‹Ÿçº¿è¾¹åº“çš„ materialStockId,找到首次入库事务(IN),
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMachineryStatusEnum.java
@@ -21,6 +21,10 @@
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesDvMachineryStatusEnum::getStatus).toArray(Integer[]::new);
    public static MesDvMachineryStatusEnum valueOfStatus(Integer status) {
        return Arrays.stream(values()).filter(e -> e.status.equals(status)).findFirst().orElse(null);
    }
    /**
     * çŠ¶æ€å€¼
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/material/MesProMaterialRequirementServiceImpl.java
@@ -26,6 +26,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -145,8 +146,16 @@
                convertSet(itemMap.values(), MesMdItemDO::getItemTypeId));
        // 2. æ‰¹é‡æŸ¥è¯¢åº“存,按 itemId æ±‡æ€»å¯ç”¨é‡ï¼ˆåœ¨åº“ - å†»ç»“ - å ç”¨ï¼‰
        // åº“存表存的是 mdm_item_id,而 BOM å±•开得到的是 mes_md_item.id,需要先换算再查库存
        Map<Long, BigDecimal> stockMap = new HashMap<>();
        List<MesWmMaterialStockDO> stockList = materialStockMapper.selectListByItemIds(requirements.keySet());
        Map<Long, Long> mesToMdmItemId = new HashMap<>();
        for (MesMdItemDO item : itemMap.values()) {
            if (item.getMdmItemId() != null) {
                mesToMdmItemId.put(item.getId(), item.getMdmItemId());
            }
        }
        List<MesWmMaterialStockDO> stockList = materialStockMapper.selectListByItemIds(
                new HashSet<>(mesToMdmItemId.values()));
        for (MesWmMaterialStockDO stock : stockList) {
            BigDecimal available = nullToZero(stock.getQuantity())
                    .subtract(nullToZero(stock.getFrozenQuantity()))
@@ -164,7 +173,8 @@
                continue;
            }
            BigDecimal requiredQty = entry.getValue();
            BigDecimal stockQty = stockMap.getOrDefault(item.getId(), BigDecimal.ZERO);
            BigDecimal stockQty = stockMap.getOrDefault(
                    mesToMdmItemId.getOrDefault(item.getId(), item.getId()), BigDecimal.ZERO);
            BigDecimal purchaseQty = purchaseOrderQtyMap.getOrDefault(item.getId(), BigDecimal.ZERO);
            MesProWorkOrderItemRespVO vo = new MesProWorkOrderItemRespVO()
                    .setItemId(item.getId()).setQuantity(requiredQty)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsService.java
@@ -2,6 +2,7 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsPreparationCheckRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsDO;
import jakarta.validation.Valid;
@@ -88,6 +89,14 @@
    List<cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderItemRespVO> getMpsItemList(Long id);
    /**
     * ç”Ÿäº§å‡†å¤‡æ£€æŸ¥ï¼šåŸºäºŽç”Ÿäº§è®¡åˆ’检查物料、设备是否齐备
     *
     * @param id MPS ç¼–号
     * @return æ£€æŸ¥ç»“æžœ
     */
    MesProMpsPreparationCheckRespVO getPreparationCheck(Long id);
    /**
     * æ ¹æ®è®¡åˆ’的辅料缺口生成采购申请单(草稿)
     *
     * @param id MPS ç¼–号
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/mps/MesProMpsServiceImpl.java
@@ -15,9 +15,11 @@
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderItemRespDTO;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseRequestCreateReqDTO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsPreparationCheckRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderItemRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsMergeItemDO;
@@ -29,14 +31,18 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.enums.dv.MesDvMachineryStatusEnum;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderSourceTypeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum;
import cn.iocoder.yudao.module.mes.service.dv.machinery.MesDvMachineryService;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.pro.material.MesProMaterialRequirementService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
@@ -110,6 +116,10 @@
    private MesProWorkOrderProcessService workOrderProcessService;
    @Resource
    private MesProMaterialRequirementService materialRequirementService;
    @Resource
    private MesProProcessService processService;
    @Resource
    private MesDvMachineryService machineryService;
    @Resource
    private ErpPurchaseOrderApi purchaseOrderApi;
    @Resource
@@ -305,6 +315,120 @@
    }
    @Override
    public MesProMpsPreparationCheckRespVO getPreparationCheck(Long id) {
        MesProMpsDO mps = validateMpsExists(id);
        MesProMpsPreparationCheckRespVO result = new MesProMpsPreparationCheckRespVO();
        result.setMpsId(mps.getId());
        result.setMpsCode(mps.getCode());
        result.setProductId(mps.getProductId());
        result.setQuantity(mps.getQuantity());
        MesMdItemDO product = itemService.getItem(mps.getProductId());
        result.setProductName(product != null ? product.getName() : String.valueOf(mps.getProductId()));
        // 1. ç‰©æ–™æ£€æŸ¥ï¼šéœ€æ±‚ - å¯ç”¨åº“å­˜ - åœ¨é€”采购
        List<MesProWorkOrderItemRespVO> materialItems = getMpsItemList(id);
        int materialShortageCount = 0;
        for (MesProWorkOrderItemRespVO item : materialItems) {
            MesProMpsPreparationCheckRespVO.MaterialCheck check = new MesProMpsPreparationCheckRespVO.MaterialCheck();
            check.setItemId(item.getItemId());
            check.setItemCode(item.getItemCode());
            check.setItemName(item.getItemName());
            check.setSpecification(item.getItemSpecification());
            check.setUnitMeasureName(item.getUnitMeasureName());
            check.setRequiredQuantity(nullToZero(item.getQuantity()));
            check.setStockQuantity(nullToZero(item.getStockQuantity()));
            check.setPurchaseOrderQuantity(nullToZero(item.getPurchaseOrderQuantity()));
            BigDecimal supply = nullToZero(item.getStockQuantity()).add(nullToZero(item.getPurchaseOrderQuantity()));
            BigDecimal shortage = nullToZero(item.getQuantity()).subtract(supply);
            if (shortage.compareTo(BigDecimal.ZERO) < 0) {
                shortage = BigDecimal.ZERO;
            }
            check.setShortageQuantity(shortage);
            check.setSufficient(shortage.compareTo(BigDecimal.ZERO) <= 0);
            if (!Boolean.TRUE.equals(check.getSufficient())) {
                materialShortageCount++;
            }
            result.getMaterials().add(check);
        }
        result.setMaterialReady(materialShortageCount == 0);
        // 2. è®¾å¤‡æ£€æŸ¥ï¼šæŒ‰äº§å“çš„工艺路线逐工序查找绑定设备
        List<MesProRouteProcessDO> routeProcesses = routeProcessService.getRouteProcessListByProductId(mps.getProductId());
        int equipmentUnavailableCount = 0;
        if (CollUtil.isEmpty(routeProcesses)) {
            MesProMpsPreparationCheckRespVO.EquipmentCheck check = new MesProMpsPreparationCheckRespVO.EquipmentCheck();
            check.setProcessName("工艺路线");
            check.setAvailable(false);
            check.setReason("产品未配置工艺路线,无法确定所需设备;请先在「工艺路线」中维护该产品的工艺路线后再检查");
            result.getEquipments().add(check);
            equipmentUnavailableCount++;
        } else {
            Map<Long, MesProRouteProcessDO> processIdMap = routeProcesses.stream()
                    .filter(rp -> rp.getProcessId() != null)
                    .collect(Collectors.toMap(MesProRouteProcessDO::getProcessId, rp -> rp, (a, b) -> a));
            Map<Long, MesProProcessDO> processMap = processService.getProcessMap(processIdMap.keySet());
            // è®¾å¤‡ä¸Žå·¥åº/工位的绑定:优先取设备上的工序 ID,其次回退工位 ID(历史数据中两者一致)
            List<MesDvMachineryDO> machineries = machineryService.getMachineryList();
            Map<Long, List<MesDvMachineryDO>> machineryMap = machineries.stream()
                    .filter(m -> m.getProcessId() != null || m.getWorkstationId() != null)
                    .collect(Collectors.groupingBy(m -> m.getProcessId() != null ? m.getProcessId() : m.getWorkstationId()));
            for (MesProRouteProcessDO rp : routeProcesses) {
                MesProProcessDO process = processMap.get(rp.getProcessId());
                String processName = process != null ? process.getName() : String.valueOf(rp.getProcessId());
                List<MesDvMachineryDO> boundMachineries = machineryMap.get(rp.getProcessId());
                if (CollUtil.isEmpty(boundMachineries)) {
                    MesProMpsPreparationCheckRespVO.EquipmentCheck check = new MesProMpsPreparationCheckRespVO.EquipmentCheck();
                    check.setProcessId(rp.getProcessId());
                    check.setProcessName(processName);
                    check.setAvailable(false);
                    check.setReason("该工序未绑定设备,请到「设备基础信息管理」为该工序绑定设备");
                    result.getEquipments().add(check);
                    equipmentUnavailableCount++;
                    continue;
                }
                for (MesDvMachineryDO machinery : boundMachineries) {
                    MesProMpsPreparationCheckRespVO.EquipmentCheck check = new MesProMpsPreparationCheckRespVO.EquipmentCheck();
                    check.setProcessId(rp.getProcessId());
                    check.setProcessName(processName);
                    check.setMachineryId(machinery.getId());
                    check.setMachineryCode(machinery.getCode());
                    check.setMachineryName(machinery.getName());
                    check.setStatus(machinery.getStatus());
                    MesDvMachineryStatusEnum statusEnum = MesDvMachineryStatusEnum.valueOfStatus(machinery.getStatus());
                    check.setStatusName(statusEnum != null ? statusEnum.getName() : "未知");
                    boolean available = statusEnum != MesDvMachineryStatusEnum.MAINTENANCE;
                    check.setAvailable(available);
                    if (available) {
                        check.setReason(statusEnum == MesDvMachineryStatusEnum.PRODUCING ? "设备占用中,排产时需确认可用时段" : null);
                    } else {
                        check.setReason("设备处于维护中,请等待维护完成或改派其他设备");
                        equipmentUnavailableCount++;
                    }
                    result.getEquipments().add(check);
                }
            }
        }
        result.setEquipmentReady(equipmentUnavailableCount == 0);
        boolean ready = Boolean.TRUE.equals(result.getMaterialReady()) && Boolean.TRUE.equals(result.getEquipmentReady());
        result.setReady(ready);
        StringBuilder summary = new StringBuilder();
        if (materialShortageCount > 0) {
            summary.append(materialShortageCount).append(" é¡¹ç‰©æ–™çŸ­ç¼º");
        }
        if (equipmentUnavailableCount > 0) {
            if (summary.length() > 0) {
                summary.append(",");
            }
            summary.append(equipmentUnavailableCount).append(" é¡¹è®¾å¤‡ä¸å¯ç”¨");
        }
        if (summary.length() == 0) {
            summary.append("物料、设备均已备齐,可以安排生产");
        }
        result.setSummary(summary.toString());
        return result;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long generatePurchaseRequest(Long id) {
        MesProMpsDO mps = validateMpsExists(id);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionService.java
@@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.mes.service.wm.transaction;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmStockSummaryReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmStockSummaryRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmTransactionPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transaction.MesWmTransactionDO;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
@@ -38,4 +40,12 @@
     */
    PageResult<MesWmTransactionDO> getTransactionPage(MesWmTransactionPageReqVO pageReqVO);
    /**
     * ä»“储收发存汇总(按物料 + ä»“库)
     *
     * @param reqVO æŸ¥è¯¢æ¡ä»¶ï¼ˆç‰©æ–™ã€ä»“库、起止时间)
     * @return æ±‡æ€»åˆ†é¡µç»“æžœ
     */
    PageResult<MesWmStockSummaryRespVO> getStockSummaryPage(@Valid MesWmStockSummaryReqVO reqVO);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transaction/MesWmTransactionServiceImpl.java
@@ -5,6 +5,8 @@
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmStockSummaryReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmStockSummaryRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.transaction.vo.MesWmTransactionPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO;
@@ -24,6 +26,8 @@
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseDefaultService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -175,6 +179,23 @@
        return transactionMapper.selectPage(pageReqVO);
    }
    @Override
    public PageResult<MesWmStockSummaryRespVO> getStockSummaryPage(MesWmStockSummaryReqVO reqVO) {
        // æœªæŒ‡å®šæ—¶é—´æ—¶é»˜è®¤ç»Ÿè®¡è¿‘三个月:期初为两个月前 1 æ—¥ 0 ç‚¹ï¼ŒæœŸæœ«ä¸ºå½“前时间
        LocalDateTime endTime = reqVO.getEndTime() != null ? reqVO.getEndTime() : LocalDateTime.now();
        LocalDateTime startTime = reqVO.getStartTime() != null ? reqVO.getStartTime()
                : endTime.toLocalDate().withDayOfMonth(1).minusMonths(2).atStartOfDay();
        if (startTime.isAfter(endTime)) {
            throw new ServiceException(400, StrUtil.format(
                    "查询开始时间[{}]晚于结束时间[{}],请调整时间范围后重试",
                    startTime, endTime));
        }
        IPage<MesWmStockSummaryRespVO> page = transactionMapper.selectStockSummaryPage(
                new Page<>(reqVO.getPageNo(), reqVO.getPageSize()),
                reqVO.getItemId(), reqVO.getWarehouseId(), startTime, endTime);
        return new PageResult<>(page.getRecords(), page.getTotal());
    }
    // ==================== ç§æœ‰æ ¡éªŒæ–¹æ³• ====================
    /**