yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderServiceImpl.java
@@ -7,8 +7,12 @@
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemBatchConfigDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.mps.MesProMpsMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.retentionsample.MesQcRetentionSampleMapper;
import cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.BarcodeBizTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemBatchConfigService;
@@ -55,6 +59,10 @@
    @Resource
    private MesProWorkOrderMapper workOrderMapper;
    @Resource
    private MesProMpsMapper mpsMapper;
    @Resource
    private MesQcRetentionSampleMapper retentionSampleMapper;
    @Resource
    private MesProWorkOrderBomService workOrderBomService;
@@ -74,6 +82,8 @@
    private MesMdWorkstationService workstationService;
    @Resource
    private CrmCustomerApi customerApi;
    @Resource
    private cn.iocoder.yudao.module.mes.service.pro.cost.MesProWorkOrderCostService workOrderCostService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -92,6 +102,8 @@
        // 3. 自动生成 BOM:根据产品 BOM 生成工单 BOM
        workOrderBomService.generateWorkOrderBom(workOrder.getId(), createReqVO, false);
        // 3.1 计算标准物料成本
        workOrderCostService.recalculateWorkOrderCost(workOrder.getId());
        // 4. 工序由前端选择产品后带入,不再自动生成
@@ -123,6 +135,11 @@
        // 3. 更新
        MesProWorkOrderDO updateObj = BeanUtils.toBean(updateReqVO, MesProWorkOrderDO.class);
        workOrderMapper.updateById(updateObj);
        // 4. 产品或数量变化后重算标准物料成本
        if (productChanged || quantityChanged) {
            workOrderCostService.recalculateWorkOrderCost(updateReqVO.getId());
        }
    }
    @Override
@@ -221,14 +238,53 @@
        if (ObjUtil.notEqual(workOrder.getStatus(), MesProWorkOrderStatusEnum.CONFIRMED.getStatus())) {
            throw exception(PRO_WORK_ORDER_NOT_CONFIRMED);
        }
        // 2. 校验已生产数量必须大于 0(没有产出不允许完成)
        BigDecimal quantityProduced = ObjUtil.defaultIfNull(workOrder.getQuantityProduced(), BigDecimal.ZERO);
        if (quantityProduced.compareTo(BigDecimal.ZERO) <= 0) {
            throw exception(PRO_WORK_ORDER_NO_PRODUCED_QUANTITY, workOrder.getCode());
        }
        // 3. 校验留样记录(所有工单完成前必须登记留样)
        if (retentionSampleMapper.selectByWorkOrderId(id) == null) {
            throw exception(PRO_WORK_ORDER_RETENTION_SAMPLE_REQUIRED, workOrder.getCode());
        }
        // 2. 级联完成所有关联任务
        // 4. 级联完成所有关联任务
        taskService.finishTaskByOrderId(id);
        // 3. 更新工单状态为已完成
        // 5. 更新工单状态为已完成
        workOrderMapper.updateById(new MesProWorkOrderDO().setId(id)
                .setStatus(MesProWorkOrderStatusEnum.FINISHED.getStatus())
                .setFinishDate(LocalDateTime.now()));
        // 6. 同步关联 MPS 状态
        finishRelatedMps(workOrder, quantityProduced);
    }
    /**
     * 工单完成时同步 MPS:已下发 → 已完成,并回填完成时间。
     * 合并下发时一个工单对应多条 MPS,已生产数量由各 MPS 自身维护,这里不回填,避免重复累计。
     */
    private void finishRelatedMps(MesProWorkOrderDO workOrder, BigDecimal quantityProduced) {
        List<MesProMpsDO> mpsList = mpsMapper.selectListByWorkOrderId(workOrder.getId());
        if (CollUtil.isEmpty(mpsList)) {
            return;
        }
        boolean singleMps = mpsList.size() == 1;
        LocalDateTime now = LocalDateTime.now();
        for (MesProMpsDO mps : mpsList) {
            // 已取消/已完成的 MPS 不做状态回写
            if (!ObjUtil.equal(mps.getStatus(), MesProMpsStatusEnum.ISSUED.getStatus())) {
                continue;
            }
            MesProMpsDO updateObj = new MesProMpsDO().setId(mps.getId())
                    .setStatus(MesProMpsStatusEnum.FINISHED.getStatus())
                    .setFinishDate(now);
            if (singleMps) {
                BigDecimal currentProduced = ObjUtil.defaultIfNull(mps.getQuantityProduced(), BigDecimal.ZERO);
                updateObj.setQuantityProduced(quantityProduced.max(currentProduced));
            }
            mpsMapper.updateById(updateObj);
        }
    }
    @Override