package cn.iocoder.yudao.module.mes.service.pro.mps;
|
|
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.util.ObjUtil;
|
import cn.hutool.core.util.StrUtil;
|
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.pro.mps.vo.MesProMpsPageReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.pro.mps.vo.MesProMpsSaveReqVO;
|
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.MesProWorkOrderSaveReqVO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsDO;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsMergeItemDO;
|
import cn.iocoder.yudao.module.mes.dal.mysql.pro.mps.MesProMpsMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.pro.mps.MesProMpsMergeItemMapper;
|
import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
|
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
|
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.service.md.autocode.MesMdAutoCodeRecordService;
|
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
|
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
|
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
|
import jakarta.annotation.Resource;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.validation.annotation.Validated;
|
|
import java.math.BigDecimal;
|
import java.time.LocalDateTime;
|
import java.time.format.DateTimeFormatter;
|
import java.util.Collection;
|
import java.util.Collections;
|
import java.util.List;
|
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
|
|
/**
|
* MES 生产主计划(MPS)Service 实现类
|
*
|
* @author 芋道源码
|
*/
|
@Service
|
@Validated
|
@Slf4j
|
public class MesProMpsServiceImpl implements MesProMpsService {
|
|
/**
|
* MPS 编码规则
|
*/
|
private static final String MPS_CODE_RULE = "MPS";
|
|
@Resource
|
private MesProMpsMapper mpsMapper;
|
@Resource
|
private MesProMpsMergeItemMapper mergeItemMapper;
|
@Resource
|
private MesProWorkOrderService workOrderService;
|
@Resource
|
private MesMdItemService itemService;
|
@Resource
|
private MesMdAutoCodeRecordService autoCodeRecordService;
|
@Resource
|
private CrmCustomerApi customerApi;
|
@Resource
|
private MesProFeedbackMapper feedbackMapper;
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Long createMps(Long saleOrderId, String saleOrderNo, Long saleOrderItemId,
|
Long mdmItemId, String productName, BigDecimal quantity, Long clientId,
|
String requestDate, String remark) {
|
// 1. 根据 MDM 物料 ID 查找 MES 产品
|
cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO mesItem = itemService.getItemByMdmItemId(mdmItemId);
|
if (mesItem == null) {
|
// MES 产品不存在,抛出异常并包含产品名称提示
|
throw exception(PRO_MPS_MES_PRODUCT_NOT_EXISTS, productName != null ? productName : mdmItemId);
|
}
|
|
// 2. 生成 MPS 编码
|
String code = generateMpsCode();
|
|
// 3. 解析需求日期
|
LocalDateTime requestDateTime = null;
|
if (StrUtil.isNotBlank(requestDate)) {
|
try {
|
requestDateTime = LocalDateTime.parse(requestDate, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
} catch (Exception e) {
|
requestDateTime = LocalDateTime.parse(requestDate, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
}
|
}
|
|
// 4. 创建 MPS(使用 MES 产品 ID)
|
MesProMpsDO mps = new MesProMpsDO();
|
mps.setCode(code);
|
mps.setStatus(MesProMpsStatusEnum.DRAFT.getStatus());
|
mps.setSaleOrderId(saleOrderId);
|
mps.setSaleOrderNo(saleOrderNo);
|
mps.setSaleOrderItemId(saleOrderItemId);
|
mps.setProductId(mesItem.getId()); // 使用 MES 产品 ID
|
mps.setQuantity(quantity);
|
mps.setQuantityProduced(BigDecimal.ZERO);
|
mps.setUnitMeasureId(mesItem.getUnitMeasureId()); // 从 MES 产品获取计量单位
|
mps.setClientId(clientId);
|
mps.setRequestDate(requestDateTime);
|
mps.setMergeFlag(0);
|
mps.setRemark(remark);
|
|
mpsMapper.insert(mps);
|
log.info("[createMps] 创建 MPS 成功,id={}, code={}, saleOrderItemId={}, mdmItemId={}, productId={}",
|
mps.getId(), mps.getCode(), mps.getSaleOrderItemId(), mdmItemId, mesItem.getId());
|
return mps.getId();
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Long createMps(MesProMpsSaveReqVO createReqVO) {
|
// 校验产品存在
|
itemService.validateItemExists(createReqVO.getProductId());
|
|
// 如果未填写编码,自动生成
|
if (StrUtil.isBlank(createReqVO.getCode())) {
|
createReqVO.setCode(generateMpsCode());
|
} else {
|
// 校验编码唯一
|
validateMpsCodeUnique(null, createReqVO.getCode());
|
}
|
|
// 插入
|
MesProMpsDO mps = BeanUtils.toBean(createReqVO, MesProMpsDO.class);
|
mps.setStatus(MesProMpsStatusEnum.DRAFT.getStatus());
|
mps.setQuantityProduced(BigDecimal.ZERO);
|
mps.setMergeFlag(0);
|
mpsMapper.insert(mps);
|
|
log.info("[createMps] 手动创建 MPS 成功,id={}, code={}", mps.getId(), mps.getCode());
|
return mps.getId();
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void updateMps(MesProMpsSaveReqVO updateReqVO) {
|
// 校验存在 + 草稿状态
|
validateMpsExistsAndDraft(updateReqVO.getId());
|
|
// 校验编码唯一
|
validateMpsCodeUnique(updateReqVO.getId(), updateReqVO.getCode());
|
|
// 更新
|
MesProMpsDO updateObj = BeanUtils.toBean(updateReqVO, MesProMpsDO.class);
|
mpsMapper.updateById(updateObj);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void deleteMps(Long id) {
|
// 校验存在 + 草稿状态
|
MesProMpsDO mps = validateMpsExistsAndDraft(id);
|
|
// 校验是否已关联工单
|
if (mps.getWorkOrderId() != null) {
|
throw exception(PRO_MPS_HAS_WORK_ORDER);
|
}
|
|
// 删除
|
mpsMapper.deleteById(id);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Long issueMps(Long id) {
|
// 校验存在 + 草稿状态
|
MesProMpsDO mps = validateMpsExistsAndDraft(id);
|
|
// 创建生产工单
|
Long workOrderId = createWorkOrderFromMps(mps, mps.getQuantity());
|
|
// 更新 MPS 状态
|
mpsMapper.updateById(new MesProMpsDO()
|
.setId(id)
|
.setStatus(MesProMpsStatusEnum.ISSUED.getStatus())
|
.setWorkOrderId(workOrderId)
|
.setIssueDate(LocalDateTime.now()));
|
|
log.info("[issueMps] MPS 下发成功,mpsId={}, workOrderId={}", id, workOrderId);
|
return workOrderId;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public Long mergeAndIssueMps(List<Long> mpsIds) {
|
// 校验 MPS 列表
|
if (CollUtil.isEmpty(mpsIds) || mpsIds.size() < 2) {
|
throw exception(PRO_MPS_MERGE_AT_LEAST_TWO);
|
}
|
|
List<MesProMpsDO> mpsList = mpsMapper.selectBatchIds(mpsIds);
|
if (mpsList.size() != mpsIds.size()) {
|
throw exception(PRO_MPS_NOT_EXISTS);
|
}
|
|
// 校验:必须是相同产品、草稿状态
|
Long productId = null;
|
for (MesProMpsDO mps : mpsList) {
|
if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus())) {
|
throw exception(PRO_MPS_NOT_DRAFT);
|
}
|
if (productId == null) {
|
productId = mps.getProductId();
|
} else if (!productId.equals(mps.getProductId())) {
|
throw exception(PRO_MPS_MERGE_PRODUCT_MISMATCH);
|
}
|
}
|
|
// 计算合并数量
|
BigDecimal totalQuantity = mpsList.stream()
|
.map(MesProMpsDO::getQuantity)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
// 取第一条 MPS 作为主记录,创建生产工单
|
MesProMpsDO firstMps = mpsList.get(0);
|
Long workOrderId = createWorkOrderFromMps(firstMps, totalQuantity);
|
|
// 生成合并批次号
|
String mergeBatchNo = generateMergeBatchNo();
|
|
// 更新所有 MPS 状态,记录合并关系
|
for (MesProMpsDO mps : mpsList) {
|
mpsMapper.updateById(new MesProMpsDO()
|
.setId(mps.getId())
|
.setStatus(MesProMpsStatusEnum.ISSUED.getStatus())
|
.setWorkOrderId(workOrderId)
|
.setMergeFlag(1)
|
.setMergeBatchNo(mergeBatchNo)
|
.setIssueDate(LocalDateTime.now()));
|
|
// 记录合并明细
|
MesProMpsMergeItemDO mergeItem = new MesProMpsMergeItemDO();
|
mergeItem.setMergeBatchNo(mergeBatchNo);
|
mergeItem.setMpsId(mps.getId());
|
mergeItem.setWorkOrderId(workOrderId);
|
mergeItem.setQuantity(mps.getQuantity());
|
mergeItemMapper.insert(mergeItem);
|
}
|
|
log.info("[mergeAndIssueMps] MPS 合并下发成功,mpsIds={}, workOrderId={}, totalQuantity={}",
|
mpsIds, workOrderId, totalQuantity);
|
return workOrderId;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void cancelMps(Long id) {
|
// 校验存在
|
MesProMpsDO mps = validateMpsExists(id);
|
|
// 校验状态:只有已下发状态才能取消
|
if (!MesProMpsStatusEnum.ISSUED.getStatus().equals(mps.getStatus())) {
|
throw exception(PRO_MPS_NOT_ISSUED);
|
}
|
|
// 级联取消关联的生产工单
|
if (mps.getWorkOrderId() != null) {
|
try {
|
workOrderService.cancelWorkOrder(mps.getWorkOrderId());
|
log.info("[cancelMps] 级联取消生产工单,workOrderId={}", mps.getWorkOrderId());
|
} catch (Exception e) {
|
// 工单可能已完成或已取消,记录日志但不阻止 MPS 取消
|
log.warn("[cancelMps] 取消生产工单失败,workOrderId={},原因:{}", mps.getWorkOrderId(), e.getMessage());
|
}
|
}
|
|
// 更新 MPS 状态
|
mpsMapper.updateById(new MesProMpsDO()
|
.setId(id)
|
.setStatus(MesProMpsStatusEnum.CANCELED.getStatus()));
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void finishMps(Long id) {
|
// 校验存在
|
MesProMpsDO mps = validateMpsExists(id);
|
|
// 校验状态:只有已下发状态才能完成
|
if (!MesProMpsStatusEnum.ISSUED.getStatus().equals(mps.getStatus())) {
|
throw exception(PRO_MPS_NOT_ISSUED);
|
}
|
|
// 更新状态
|
mpsMapper.updateById(new MesProMpsDO()
|
.setId(id)
|
.setStatus(MesProMpsStatusEnum.FINISHED.getStatus())
|
.setFinishDate(LocalDateTime.now()));
|
}
|
|
@Override
|
public MesProMpsDO getMpsBySaleOrderItemId(Long saleOrderItemId) {
|
return mpsMapper.selectBySaleOrderItemId(saleOrderItemId);
|
}
|
|
@Override
|
public MesProMpsDO getMps(Long id) {
|
return mpsMapper.selectById(id);
|
}
|
|
@Override
|
public PageResult<MesProMpsDO> getMpsPage(MesProMpsPageReqVO pageReqVO) {
|
// 获取用户有权限的客户 ID 列表
|
List<Long> permittedClientIds = customerApi.getPermittedCustomerIds();
|
return mpsMapper.selectPage(pageReqVO, permittedClientIds);
|
}
|
|
@Override
|
public List<MesProMpsDO> getMpsList(Collection<Long> ids) {
|
if (CollUtil.isEmpty(ids)) {
|
return Collections.emptyList();
|
}
|
return mpsMapper.selectBatchIds(ids);
|
}
|
|
@Override
|
public List<MesProMpsDO> getMpsListBySaleOrderItemIds(Collection<Long> saleOrderItemIds) {
|
return mpsMapper.selectListBySaleOrderItemIds(saleOrderItemIds);
|
}
|
|
@Override
|
public List<MesProMpsDO> getMergeableMpsList(Long productId) {
|
return mpsMapper.selectMergeableList(productId);
|
}
|
|
@Override
|
public void updateProducedQuantity(Long id, BigDecimal incrQuantity) {
|
mpsMapper.updateProducedQuantity(id, incrQuantity);
|
}
|
|
@Override
|
public Boolean checkSaleOrderProductionFinished(Long saleOrderId) {
|
if (saleOrderId == null) {
|
return false;
|
}
|
// 1. 查询该销售订单所有 MPS(通过 saleOrderId 关联)
|
List<MesProMpsDO> mpsList = mpsMapper.selectList(
|
MesProMpsDO::getSaleOrderId, saleOrderId);
|
if (CollUtil.isEmpty(mpsList)) {
|
// 无 MPS 关联,视为完成
|
return true;
|
}
|
// 2. 检查每个 MPS 是否完成
|
for (MesProMpsDO mps : mpsList) {
|
if (!isMpsFinished(mps)) {
|
return false;
|
}
|
}
|
return true;
|
}
|
|
@Override
|
public MesProMpsDO getMpsByWorkOrderId(Long workOrderId) {
|
if (workOrderId == null) {
|
return null;
|
}
|
return mpsMapper.selectOne(MesProMpsDO::getWorkOrderId, workOrderId);
|
}
|
|
/**
|
* 判断单个 MPS 是否完成
|
*/
|
private boolean isMpsFinished(MesProMpsDO mps) {
|
// 1. 已生产数量 >= 计划数量
|
BigDecimal quantityProduced = ObjUtil.defaultIfNull(mps.getQuantityProduced(), BigDecimal.ZERO);
|
if (quantityProduced.compareTo(mps.getQuantity()) < 0) {
|
return false;
|
}
|
// 2. 检查工单下所有报工是否完成(确保质检完成)
|
if (mps.getWorkOrderId() != null) {
|
Long unfinishedCount = feedbackMapper.selectCount(
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<MesProFeedbackDO>()
|
.eq(MesProFeedbackDO::getWorkOrderId, mps.getWorkOrderId())
|
.ne(MesProFeedbackDO::getStatus, MesProFeedbackStatusEnum.FINISHED.getStatus()));
|
if (unfinishedCount > 0) {
|
return false;
|
}
|
}
|
return true;
|
}
|
|
// ========== 私有方法 ==========
|
|
/**
|
* 生成 MPS 编码
|
*/
|
private String generateMpsCode() {
|
try {
|
return autoCodeRecordService.generateAutoCode(MPS_CODE_RULE);
|
} catch (Exception e) {
|
// 如果编码规则不存在,使用时间戳生成
|
log.warn("[generateMpsCode] 编码规则 {} 不存在,使用时间戳生成", MPS_CODE_RULE);
|
return "MPS" + System.currentTimeMillis();
|
}
|
}
|
|
/**
|
* 生成合并批次号
|
*/
|
private String generateMergeBatchNo() {
|
return "MERGE" + System.currentTimeMillis();
|
}
|
|
/**
|
* 校验 MPS 存在
|
*/
|
private MesProMpsDO validateMpsExists(Long id) {
|
MesProMpsDO mps = mpsMapper.selectById(id);
|
if (mps == null) {
|
throw exception(PRO_MPS_NOT_EXISTS);
|
}
|
return mps;
|
}
|
|
/**
|
* 校验 MPS 存在 + 草稿状态
|
*/
|
private MesProMpsDO validateMpsExistsAndDraft(Long id) {
|
MesProMpsDO mps = validateMpsExists(id);
|
if (!MesProMpsStatusEnum.DRAFT.getStatus().equals(mps.getStatus())) {
|
throw exception(PRO_MPS_NOT_DRAFT);
|
}
|
return mps;
|
}
|
|
/**
|
* 校验 MPS 编码唯一
|
*/
|
private void validateMpsCodeUnique(Long id, String code) {
|
if (StrUtil.isBlank(code)) {
|
return;
|
}
|
MesProMpsDO mps = mpsMapper.selectByCode(code);
|
if (mps == null) {
|
return;
|
}
|
if (id == null) {
|
throw exception(PRO_MPS_CODE_DUPLICATE);
|
}
|
if (!id.equals(mps.getId())) {
|
throw exception(PRO_MPS_CODE_DUPLICATE);
|
}
|
}
|
|
/**
|
* 根据 MPS 创建生产工单
|
*/
|
private Long createWorkOrderFromMps(MesProMpsDO mps, BigDecimal quantity) {
|
MesProWorkOrderSaveReqVO workOrderReq = new MesProWorkOrderSaveReqVO();
|
workOrderReq.setCode(generateWorkOrderCode());
|
workOrderReq.setName("生产工单-" + mps.getCode());
|
workOrderReq.setType(1); // 自行生产
|
workOrderReq.setOrderSourceType(MesProWorkOrderSourceTypeEnum.ORDER.getType());
|
workOrderReq.setOrderSourceCode(mps.getSaleOrderNo());
|
workOrderReq.setProductId(mps.getProductId());
|
workOrderReq.setQuantity(quantity);
|
workOrderReq.setClientId(mps.getClientId());
|
workOrderReq.setRequestDate(mps.getRequestDate() != null ? mps.getRequestDate() : LocalDateTime.now());
|
workOrderReq.setRemark("来自 MPS: " + mps.getCode());
|
|
Long workOrderId = workOrderService.createWorkOrder(workOrderReq);
|
|
// 工单直接确认为已确认状态(跳过草稿)
|
workOrderService.confirmWorkOrder(workOrderId);
|
|
return workOrderId;
|
}
|
|
/**
|
* 生成生产工单编码
|
*/
|
private String generateWorkOrderCode() {
|
try {
|
return autoCodeRecordService.generateAutoCode("WO");
|
} catch (Exception e) {
|
log.warn("[generateWorkOrderCode] 编码规则 WO 不存在,使用时间戳生成");
|
return "WO" + System.currentTimeMillis();
|
}
|
}
|
|
}
|