昨天 c89f7dcf9344a35efde2a00edb10491b401ba000
feat(trace): 添加种子流向信息展示功能

- 在 MesProTracePublicRespVO 中新增 seedFlows 字段用于存储批次出入库流水
- 实现 getBatchFlowList 方法查询批次出入库流水数据
- 在 H5 溯源页面添加种子流向信息展示区块,显示入库出库流水详情
- 修复销售去向查询逻辑,支持按明细表批次号聚合匹配销售出库信息
- 更新 trace.html 静态页面支持新的种子流向信息展示格式
已添加2个文件
已修改7个文件
198 ■■■■■ 文件已修改
docs/seed_flow_trace_integration.md 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/batch/vo/MesWmBatchFlowRespVO.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/pub/trace/vo/MesProTracePublicRespVO.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/batch/MesWmBatchMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/resources/mapper/wm/batch/MesWmBatchMapper.xml 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/resources/static/trace.html 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/seed_flow_trace_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
# ç§å­æµå‘信息(销售出库出入库流水) - H5 æº¯æºè”调说明
## èƒŒæ™¯
H5 æ‰«ç æº¯æºé¡µï¼ˆ`mes.trace-base-url` æŒ‡å‘çš„ `trace.html`)新增「种子流向信息」区块:
按**批次全流水**展示该批次的入库与出库记录(数据源为销售出库 `/sales-outbound/product-sales` æ¨¡å—对应的库存事务)。
同时修复既有「销售去向」数据链路:种子销售出库按袋码/托盘扫码拣货时,批次号只写入出库**明细表**
(`mes_wm_product_sales_detail.batch_code`),原查询只匹配出库**行表**批次号,导致溯源永远查不到销售出库信息。
## æ¶‰åŠæŽ¥å£
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /public/mes/trace/query?code={追溯码} | H5 æº¯æºæŸ¥è¯¢ï¼ˆå…ç™»å½•),响应新增 `seedFlows` åŒºå— |
**响应新增字段 `seedFlows`(批次出入库流水,按事务时间升序):**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| type | Integer | äº‹åŠ¡æ–¹å‘ï¼š1-入库 2-出库 |
| bizType | Integer | ä¸šåŠ¡ç±»åž‹ï¼ˆ118-生产入库、119-销售出库、124-产品产出、120-销售退货入库等,见 MesBizTypeConstants) |
| bizCode | String | æ¥æºä¸šåŠ¡å•å·ï¼ˆå¦‚é”€å”®å‡ºåº“å•å· SL_xxx) |
| quantity | BigDecimal | å˜åŠ¨æ•°é‡ï¼ˆå‡ºåº“ä¸ºè´Ÿæ•°ï¼Œå±•ç¤ºæ—¶å–ç»å¯¹å€¼ï¼‰ |
| transactionTime | LocalDateTime | äº‹åŠ¡å‘ç”Ÿæ—¶é—´ |
| warehouseName | String | ä»“库名称 |
| locationName | String | åº“区名称 |
| areaName | String | åº“位名称 |
| clientName | String | å®¢æˆ·åç§°ï¼ˆä»…销售出库流水带出,来自出库单快照) |
**`salesTargets` å­—段变化(同接口,向后兼容):**
- æŸ¥è¯¢å£å¾„扩展:批次号除匹配出库行外,同时按出库明细聚合匹配(排除已取消单据)。
- æ–°å¢žå¡«å…… `clientName`(客户名称快照)、`clientCode` ä¸å˜ï¼›æ­¤å‰æ’为 null。
## H5 é¡µé¢å±•示规则
| åŒºå— | å±•示条件 | åˆ— |
|------|----------|----|
| ç§å­æµå‘信息 | `seedFlows` éžç©ºï¼ˆä¼˜å…ˆï¼‰ | æ–¹å‘(入库/出库徽标)、业务、来源单号、客户、数量、时间、仓库/库区 |
| ç§å­æµå‘信息(回退) | `seedFlows` ä¸ºç©ºä½† `salesTargets` éžç©º | é”€å”®å‡ºåº“单、客户、数量、日期 |
## éƒ¨ç½²æ³¨æ„äº‹é¡¹
1. `trace.html` ä¸º `yudao-server` é™æ€èµ„源(`src/main/resources/static/trace.html`),需**重新打包发布后端**;
   è‹¥ç”Ÿäº§ nginx çš„ `/prod/trace.html`(如 `http://47.97.181.254:9008/prod/trace.html`)是独立拷贝部署的,需同步替换该文件。
2. é”€å”®å‡ºåº“流水的客户名称取 `mes_wm_product_sales.client_name` å¿«ç…§å­—段,要求环境库包含该列;
   ç»æŽ’查 `mom-thzy` åº“已有,**`mom-xgdl` åº“缺失 `client_name` / `outbound_time` / `outbound_user_id` åˆ—**(落后于代码),使用 local çŽ¯å¢ƒæ—¶éœ€å…ˆè¡¥åˆ—ã€‚
3. åŽ†å²å‡ºåº“å•ä¸ä¼šè¡¥ç”Ÿæˆæµæ°´ï¼Œä»…æ–°å‡ºåº“å¯çœ‹åˆ°å®Œæ•´å…¥+出记录;无流水时回退展示销售出库单。
## éªŒè¯æ•°æ®ï¼ˆmom-thzy)
批次 `PC20260819twoITEM_001`:入库 100(产品产出,虚拟线边仓库)→ å‡ºåº“ 1(销售出库 SL_20260901002,成品合格库,南通客户)。
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/batch/vo/MesWmBatchFlowRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES æ‰¹æ¬¡å‡ºå…¥åº“流水(种子流向)Response VO")
@Data
public class MesWmBatchFlowRespVO {
    @Schema(description = "事务类型:1-入库 2-出库", example = "2")
    private Integer type;
    @Schema(description = "业务类型(见 MesBizTypeConstants,如 119-销售出库、124-产品产出)", example = "119")
    private Integer bizType;
    @Schema(description = "来源业务单号", example = "SL_20260901002")
    private String bizCode;
    @Schema(description = "本次变动数量(出库为负数)", example = "-1")
    private BigDecimal quantity;
    @Schema(description = "事务发生时间")
    private LocalDateTime transactionTime;
    @Schema(description = "仓库名称", example = "成品合格库")
    private String warehouseName;
    @Schema(description = "库区名称", example = "成品合格库区")
    private String locationName;
    @Schema(description = "库位名称", example = "成品合格库位")
    private String areaName;
    @Schema(description = "客户名称(销售出库/销售退货时)", example = "客户A")
    private String clientName;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/pub/trace/vo/MesProTracePublicRespVO.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.mes.controller.pub.trace.vo;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchFlowRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPurchaseSourceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO;
@@ -14,7 +15,7 @@
 * H5 æ‰«ç æº¯æº - å…¨ç¨‹æº¯æºä¿¡æ¯ Response VO
 * <p>
 * ä»¥ã€Œä¿¡æ¯åŒºå—」模块化组织,核心信息(品种/批次/人员/质检/入库)始终返回;
 * é‡‡è´­æ¥æºã€é”€å”®åŽ»å‘ã€åŠ å·¥æµè½¬ä¸ºå¯é€‰æ‰©å±•åŒºå—ï¼ˆæœ‰æ•°æ®æ‰éžç©ºï¼‰ã€‚åŽç»­æ–°å¢žæ•°æ®æºæŒ‰åŒä¸€æ¨¡å¼è¿½åŠ åŒºå—å­—æ®µã€‚
 * é‡‡è´­æ¥æºã€é”€å”®åŽ»å‘ã€ç§å­æµå‘ï¼ˆæ‰¹æ¬¡å‡ºå…¥åº“æµæ°´ï¼‰ã€åŠ å·¥æµè½¬ä¸ºå¯é€‰æ‰©å±•åŒºå—ï¼ˆæœ‰æ•°æ®æ‰éžç©ºï¼‰ã€‚åŽç»­æ–°å¢žæ•°æ®æºæŒ‰åŒä¸€æ¨¡å¼è¿½åŠ åŒºå—å­—æ®µã€‚
 */
@Schema(description = "H5 æ‰«ç æº¯æº - å…¨ç¨‹æº¯æºä¿¡æ¯ Response VO")
@Data
@@ -116,6 +117,9 @@
    @Schema(description = "销售去向(可选)")
    private List<MesWmBatchSalesTargetRespVO> salesTargets;
    @Schema(description = "种子流向信息(批次出入库流水:生产入库 â†’ é”€å”®å‡ºåº“等,可选)")
    private List<MesWmBatchFlowRespVO> seedFlows;
    @Schema(description = "加工流转(可选)")
    private List<MesWmBatchProcessTraceRespVO> processTraces;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/batch/MesWmBatchMapper.java
@@ -6,6 +6,7 @@
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.batch.vo.MesWmBatchFlowRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPurchaseSourceRespVO;
@@ -205,6 +206,14 @@
    List<MesWmBatchSalesTargetRespVO> selectSalesTargetList(String batchCode);
    /**
     * æŸ¥è¯¢æ‰¹æ¬¡å‡ºå…¥åº“流水(种子流向)
     *
     * @param batchCode æ‰¹æ¬¡å·
     * @return å‡ºå…¥åº“流水列表
     */
    List<MesWmBatchFlowRespVO> selectBatchFlowList(String batchCode);
    /**
     * æŸ¥è¯¢æ‰¹æ¬¡å·¥åºæµè½¬ï¼ˆäº§å‡º/投入事件)
     *
     * @param batchCode æ‰¹æ¬¡å·
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/trace/MesProTracePublicServiceImpl.java
@@ -2,6 +2,7 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchFlowRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPurchaseSourceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO;
@@ -333,12 +334,15 @@
            resp.setPurchaseSources(CollUtil.isNotEmpty(purchaseSources) ? purchaseSources : List.of());
            resp.setSalesTargets(CollUtil.isNotEmpty(salesTargets) ? salesTargets : List.of());
            resp.setProcessTraces(CollUtil.isNotEmpty(processTraces) ? processTraces : List.of());
            List<MesWmBatchFlowRespVO> seedFlows = mesWmBatchService.getBatchFlowList(batch.getCode());
            resp.setSeedFlows(CollUtil.isNotEmpty(seedFlows) ? seedFlows : List.of());
        } catch (Exception ex) {
            // ä»“库批次体系(采购/销售/流转)未启用时容错,不影响核心追溯
            log.warn("[fillOptionalModules][批次 {} æ‰©å±•模块查询失败: {}]", batch.getCode(), ex.getMessage());
            resp.setPurchaseSources(List.of());
            resp.setSalesTargets(List.of());
            resp.setProcessTraces(List.of());
            resp.setSeedFlows(List.of());
        }
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchService.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mes.service.wm.batch;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchFlowRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
@@ -125,6 +126,14 @@
    List<MesWmBatchProcessTraceRespVO> getProcessTraceList(String code);
    /**
     * èŽ·å¾—æ‰¹æ¬¡å‡ºå…¥åº“æµæ°´ï¼ˆç§å­æµå‘ï¼šå…¥åº“ + é”€å”®å‡ºåº“等全部库存事务)
     *
     * @param code æ‰¹æ¬¡å·
     * @return å‡ºå…¥åº“流水列表
     */
    List<MesWmBatchFlowRespVO> getBatchFlowList(String code);
    /**
     * æ ¹æ® SN ç èŽ·å¾—æ‰¹æ¬¡
     *
     * @param sn SN ç 
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/batch/MesWmBatchServiceImpl.java
@@ -4,6 +4,7 @@
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.batch.vo.MesWmBatchFlowRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchProcessTraceRespVO;
@@ -409,6 +410,11 @@
    }
    @Override
    public List<MesWmBatchFlowRespVO> getBatchFlowList(String code) {
        return batchMapper.selectBatchFlowList(code);
    }
    @Override
    public MesWmBatchDO getBatchBySn(String sn) {
        MesWmSnDO snDO = snMapper.selectOne(MesWmSnDO::getCode, sn);
        if (snDO == null || StrUtil.isBlank(snDO.getBatchCode())) {
yudao-module-mes/src/main/resources/mapper/wm/batch/MesWmBatchMapper.xml
@@ -55,16 +55,50 @@
        WHERE rl.batch_code = #{batchCode} AND rl.deleted = 0
    </select>
    <!-- æ‰¹æ¬¡é”€å”®åŽ»å‘ï¼šæ‰¹æ¬¡ â†’ é”€å”®å‡ºåº“行 â†’ é”€å”®å‡ºåº“单 â†’ OQC -->
    <!--
        æ‰¹æ¬¡é”€å”®åŽ»å‘ï¼ˆç§å­æµå‘ï¼‰ï¼š
        â‘  è¡Œçº§åŒ¹é…ï¼šæ‰¹æ¬¡ â†’ é”€å”®å‡ºåº“行 â†’ é”€å”®å‡ºåº“单 â†’ OQC(行上直接记录批次的流程);
        â‘¡ æ˜Žç»†çº§åŒ¹é…ï¼šæ‰¹æ¬¡ â†’ æ‰«ç å‡ºåº“明细 â†’ é”€å”®å‡ºåº“单 â†’ OQC(种子袋码/托盘扫码出库时批次只写入明细表,按单据聚合数量)。
        åŒä¸€å‡ºåº“行若已有明细级匹配则不再重复出行级记录;排除已取消(状态 5)单据。
    -->
    <select id="selectSalesTargetList" parameterType="String"
            resultType="cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchSalesTargetRespVO">
        SELECT s.code AS sales_code, s.sales_date, s.sales_order_code, s.client_id,
        SELECT s.code AS sales_code, s.sales_date, s.sales_order_code, s.client_id, s.client_name,
               oqc.code AS oqc_code, oqc.check_result AS check_result,
               sl.batch_code, sl.item_id, sl.quantity, sl.quality_status
        FROM mes_wm_product_sales_line sl
        JOIN mes_wm_product_sales s ON s.id = sl.sales_id AND s.deleted = 0
        LEFT JOIN mes_qc_oqc oqc ON oqc.id = sl.oqc_id AND oqc.deleted = 0
        WHERE sl.batch_code = #{batchCode} AND sl.deleted = 0
        WHERE sl.batch_code = #{batchCode} AND sl.deleted = 0 AND s.status != 5
          AND NOT EXISTS (SELECT 1 FROM mes_wm_product_sales_detail d
                          WHERE d.line_id = sl.id AND d.batch_code = #{batchCode} AND d.deleted = 0)
        UNION ALL
        SELECT s.code AS sales_code, s.sales_date, s.sales_order_code, s.client_id, s.client_name,
               oqc.code AS oqc_code, oqc.check_result AS check_result,
               d.batch_code, d.item_id, SUM(d.quantity) AS quantity, MAX(sl.quality_status) AS quality_status
        FROM mes_wm_product_sales_detail d
        JOIN mes_wm_product_sales s ON s.id = d.sales_id AND s.deleted = 0
        JOIN mes_wm_product_sales_line sl ON sl.id = d.line_id AND sl.deleted = 0
        LEFT JOIN mes_qc_oqc oqc ON oqc.id = sl.oqc_id AND oqc.deleted = 0
        WHERE d.batch_code = #{batchCode} AND d.deleted = 0 AND s.status != 5
        GROUP BY s.id, s.code, s.sales_date, s.sales_order_code, s.client_id, s.client_name,
                 oqc.code, oqc.check_result, d.batch_code, d.item_id
        ORDER BY sales_date
    </select>
    <!-- æ‰¹æ¬¡å‡ºå…¥åº“流水(种子流向):库存事务 â†’ ä»“库/库区/库位 â†’ é”€å”®å‡ºåº“单(biz_type 119 å¸¦å‡ºå®¢æˆ·åç§°ï¼‰ -->
    <select id="selectBatchFlowList" parameterType="String"
            resultType="cn.iocoder.yudao.module.mes.controller.admin.wm.batch.vo.MesWmBatchFlowRespVO">
        SELECT tx.type, tx.biz_type, tx.biz_code, tx.quantity, tx.transaction_time,
               w.name AS warehouse_name, loc.name AS location_name, area.name AS area_name,
               sales.client_name
        FROM mes_wm_transaction tx
        LEFT JOIN mes_wm_warehouse w ON w.id = tx.warehouse_id AND w.deleted = 0
        LEFT JOIN mes_wm_warehouse_location loc ON loc.id = tx.location_id AND loc.deleted = 0
        LEFT JOIN mes_wm_warehouse_area area ON area.id = tx.area_id AND area.deleted = 0
        LEFT JOIN mes_wm_product_sales sales ON tx.biz_type = 119 AND sales.id = tx.biz_id AND sales.deleted = 0
        WHERE tx.batch_code = #{batchCode} AND tx.deleted = 0
        ORDER BY tx.transaction_time
    </select>
    <!-- æ‰¹æ¬¡å·¥åºæµè½¬ï¼šäº§å‡º(OUT)/投入(IN) äº‹ä»¶èšåˆï¼Œæ— éœ€å»ºè¡¨ -->
yudao-server/src/main/resources/static/trace.html
@@ -207,6 +207,14 @@
  var BAG_STATUS = { 10: ['已生成', 'gray'], 20: ['已导出印刷', 'blue'], 30: ['已作废', 'red'] };
  var QUALITY_RESULT = { 0: ['处理中', 'gray'], 1: ['合格', 'green'], 2: ['不合格', 'red'] };
  var WORKER_TYPE = { 10: '组长', 20: '操作工', 30: '包装工', 40: '质检员', 50: '其他' };
  var FLOW_DIRECTION = { 1: ['入库', 'green'], 2: ['出库', 'blue'] };
  var FLOW_BIZ_TYPE = {
    110: '采购入库', 111: '采购入库取消', 112: '调拨出库', 113: '调拨入库',
    114: '其他出库', 115: '其他入库', 116: '生产领料', 117: '生产退料',
    118: '生产入库', 119: '销售出库', 120: '销售退货入库', 121: '外协发料',
    122: '外协入库', 123: '生产消耗', 124: '产品产出', 125: '供应商退货出库',
    126: '库存调整入库', 127: '库存调整出库'
  };
  // ========== å·¥å…·å‡½æ•° ==========
  function esc(v) {
@@ -470,7 +478,7 @@
    );
  }
  // åŒºå—8:可选扩展(采购来源 / é”€å”®å޻向 / åŠ å·¥æµè½¬ï¼‰ï¼Œæœ‰æ•°æ®æ‰å±•ç¤º
  // åŒºå—8:可选扩展(采购来源 / ç§å­æµå‘信息(销售出库) / åŠ å·¥æµè½¬ï¼‰ï¼Œæœ‰æ•°æ®æ‰å±•ç¤º
  function buildOptionalCard(d) {
    var html = '';
    if (!isEmptyList(d.purchaseSources)) {
@@ -481,13 +489,24 @@
      rows += '</tbody></table>';
      html += card('采购来源', null, rows, { collapsible: true, open: false });
    }
    if (!isEmptyList(d.salesTargets)) {
      var rows2 = '<table class="mini-table"><thead><tr><th>销售出库单</th><th>客户</th><th>数量</th><th>日期</th></tr></thead><tbody>';
      d.salesTargets.forEach(function (s) {
        rows2 += '<tr><td>' + text(s.outCode || s.salesOrderCode || '') + '</td><td>' + text(s.customerName) + '</td><td>' + fmtNum(s.quantity) + '</td><td>' + fmtDate(s.outTime) + '</td></tr>';
    if (!isEmptyList(d.seedFlows)) {
      var rows2 = '<table class="mini-table"><thead><tr><th>方向</th><th>业务</th><th>来源单号</th><th>客户</th><th>数量</th><th>时间</th><th>仓库/库区</th></tr></thead><tbody>';
      d.seedFlows.forEach(function (f) {
        var qty = (f.quantity === null || f.quantity === undefined) ? '-' : fmtNum(Math.abs(Number(f.quantity)));
        var wh = [f.warehouseName, f.locationName].filter(Boolean).join(' / ') || '-';
        rows2 += '<tr><td>' + statusBadge(FLOW_DIRECTION, f.type) + '</td><td>' + text(FLOW_BIZ_TYPE[f.bizType] || f.bizType) +
          '</td><td>' + text(f.bizCode) + '</td><td>' + text(f.clientName) + '</td><td>' + qty + '</td><td>' +
          fmtDate(f.transactionTime) + '</td><td>' + text(wh) + '</td></tr>';
      });
      rows2 += '</tbody></table>';
      html += card('销售去向', null, rows2, { collapsible: true, open: false });
      html += card('种子流向信息', null, rows2, { collapsible: true, open: false });
    } else if (!isEmptyList(d.salesTargets)) {
      var rows2b = '<table class="mini-table"><thead><tr><th>销售出库单</th><th>客户</th><th>数量</th><th>日期</th></tr></thead><tbody>';
      d.salesTargets.forEach(function (s) {
        rows2b += '<tr><td>' + text(s.salesCode || s.salesOrderCode || '') + '</td><td>' + text(s.clientName) + '</td><td>' + fmtNum(s.quantity) + '</td><td>' + fmtDate(s.salesDate) + '</td></tr>';
      });
      rows2b += '</tbody></table>';
      html += card('种子流向信息', null, rows2b, { collapsible: true, open: false });
    }
    if (!isEmptyList(d.processTraces)) {
      var rows3 = '<table class="mini-table"><thead><tr><th>节点</th><th>说明</th><th>时间</th></tr></thead><tbody>';