Commit fa473f7da9f9e9763b73089ffad7d7f535151a06

Authored by YunaiV
1 parent 9f3538f4

✨ feat(mes): 更新来源单据类型为整型,优化数据一致性

将来源单据类型字段从字符串更改为整型,以提高数据一致性和类型安全性。同时,添加了相关的枚举类型以支持新的字段定义。
Showing 18 changed files with 1026 additions and 17 deletions
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcRespVO.java
@@ -37,8 +37,8 @@ public class MesQcIpqcRespVO { @@ -37,8 +37,8 @@ public class MesQcIpqcRespVO {
37 @Schema(description = "来源单据 ID", example = "200") 37 @Schema(description = "来源单据 ID", example = "200")
38 private Long sourceDocId; 38 private Long sourceDocId;
39 39
40 - @Schema(description = "来源单据类型", example = "FEEDBACK")  
41 - private String sourceDocType; 40 + @Schema(description = "来源单据类型", example = "304")
  41 + private Integer sourceDocType;
42 42
43 @Schema(description = "来源单据编号", example = "FB20250101001") 43 @Schema(description = "来源单据编号", example = "FB20250101001")
44 private String sourceDocCode; 44 private String sourceDocCode;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/qc/ipqc/vo/MesQcIpqcSaveReqVO.java
@@ -33,8 +33,8 @@ public class MesQcIpqcSaveReqVO { @@ -33,8 +33,8 @@ public class MesQcIpqcSaveReqVO {
33 33
34 // ========== 来源单据 ========== 34 // ========== 来源单据 ==========
35 35
36 - @Schema(description = "来源单据类型", example = "FEEDBACK")  
37 - private String sourceDocType; 36 + @Schema(description = "来源单据类型", example = "304")
  37 + private Integer sourceDocType;
38 38
39 @Schema(description = "来源单据 ID", example = "200") 39 @Schema(description = "来源单据 ID", example = "200")
40 private Long sourceDocId; 40 private Long sourceDocId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/qc/ipqc/MesQcIpqcDO.java
@@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
5 import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO; 5 import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
6 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO; 6 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
7 import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateDO; 7 import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateDO;
  8 +import cn.iocoder.yudao.module.mes.enums.qc.MesQcSourceDocTypeEnum;
8 import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum; 9 import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
9 import com.baomidou.mybatisplus.annotation.KeySequence; 10 import com.baomidou.mybatisplus.annotation.KeySequence;
10 import com.baomidou.mybatisplus.annotation.TableId; 11 import com.baomidou.mybatisplus.annotation.TableId;
@@ -55,18 +56,26 @@ public class MesQcIpqcDO extends BaseDO { @@ -55,18 +56,26 @@ public class MesQcIpqcDO extends BaseDO {
55 */ 56 */
56 private Long templateId; 57 private Long templateId;
57 58
58 - // ========== 来源单据(TODO @芋艿:PRO/WM 模块联动后接入) ========== 59 + // ========== 来源单据 ==========
59 60
60 /** 61 /**
61 - * 来源单据 ID 62 + * 来源单据类型
  63 + *
  64 + * 枚举 {@link MesQcSourceDocTypeEnum}
62 */ 65 */
63 - private Long sourceDocId; 66 + private Integer sourceDocType;
64 /** 67 /**
65 - * 来源单据类型 68 + * 来源单据 ID
  69 + *
  70 + * 关联:根据 {@link #sourceDocType} 不同:
  71 + * 1. {@link MesQcSourceDocTypeEnum#FEEDBACK} 时,关联 {@link cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO#getId()}
66 */ 72 */
67 - private String sourceDocType; 73 + private Long sourceDocId;
68 /** 74 /**
69 * 来源单据行 ID 75 * 来源单据行 ID
  76 + *
  77 + * 关联:根据 {@link #sourceDocType} 不同:
  78 + * 1. {@link MesQcSourceDocTypeEnum#FEEDBACK} 时,暂不使用(预留)
70 */ 79 */
71 private Long sourceLineId; 80 private Long sourceLineId;
72 81
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/productproduce/MesWmProductProduceMapper.java
@@ -12,4 +12,8 @@ import org.apache.ibatis.annotations.Mapper; @@ -12,4 +12,8 @@ import org.apache.ibatis.annotations.Mapper;
12 @Mapper 12 @Mapper
13 public interface MesWmProductProduceMapper extends BaseMapperX<MesWmProductProduceDO> { 13 public interface MesWmProductProduceMapper extends BaseMapperX<MesWmProductProduceDO> {
14 14
  15 + default MesWmProductProduceDO selectByFeedbackId(Long feedbackId) {
  16 + return selectOne(MesWmProductProduceDO::getFeedbackId, feedbackId);
  17 + }
  18 +
15 } 19 }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/MesBizTypeConstants.java
@@ -57,6 +57,7 @@ public final class MesBizTypeConstants { @@ -57,6 +57,7 @@ public final class MesBizTypeConstants {
57 public static final int PRO_WORKORDER = 301; // 工单:MesProWorkOrderDO 57 public static final int PRO_WORKORDER = 301; // 工单:MesProWorkOrderDO
58 public static final int PRO_TRANS_ORDER = 302; // 流转单:MesProTransOrderDO 58 public static final int PRO_TRANS_ORDER = 302; // 流转单:MesProTransOrderDO
59 public static final int PRO_TASK = 303; // 生产任务:MesProTaskDO 59 public static final int PRO_TASK = 303; // 生产任务:MesProTaskDO
  60 + public static final int PRO_FEEDBACK = 304; // 生产报工:MesProFeedbackDO
60 61
61 // ========== DV 设备模块 [400, 500) ========== 62 // ========== DV 设备模块 [400, 500) ==========
62 63
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/qc/MesQcSourceDocTypeEnum.java
@@ -17,7 +17,8 @@ import java.util.Arrays; @@ -17,7 +17,8 @@ import java.util.Arrays;
17 public enum MesQcSourceDocTypeEnum implements ArrayValuable<Integer> { 17 public enum MesQcSourceDocTypeEnum implements ArrayValuable<Integer> {
18 18
19 ARRIVAL_NOTICE(MesBizTypeConstants.WM_ARRIVAL_NOTICE, "到货通知单"), 19 ARRIVAL_NOTICE(MesBizTypeConstants.WM_ARRIVAL_NOTICE, "到货通知单"),
20 - // OUTSOURCE_RECPT(MesBizTypeConstants.WM_OUTSOURCE_RECPT, "外协入库单"), // TODO 未实现,占位 20 + OUTSOURCE_RECPT(MesBizTypeConstants.WM_OUTSOURCE_RECPT, "外协入库单"),
  21 + FEEDBACK(MesBizTypeConstants.PRO_FEEDBACK, "生产报工"),
21 ; 22 ;
22 23
23 public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesQcSourceDocTypeEnum::getType).toArray(Integer[]::new); 24 public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesQcSourceDocTypeEnum::getType).toArray(Integer[]::new);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackService.java
@@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedba @@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.MesProFeedba
6 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO; 6 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
7 import jakarta.validation.Valid; 7 import jakarta.validation.Valid;
8 8
  9 +import java.math.BigDecimal;
  10 +
9 /** 11 /**
10 * MES 生产报工 Service 接口 12 * MES 生产报工 Service 接口
11 * 13 *
@@ -87,5 +89,21 @@ public interface MesProFeedbackService { @@ -87,5 +89,21 @@ public interface MesProFeedbackService {
87 */ 89 */
88 boolean approveFeedback(Long id, Long userId); 90 boolean approveFeedback(Long id, Long userId);
89 91
  92 + /**
  93 + * IPQC 完成后回调:完成报工单并更新任务/工单进度
  94 + *
  95 + * <p>由 IPQC 检验完成时调用,将报工单状态从待检验改为已完成,
  96 + * 并根据检验结果回写合格/不合格/废品数量,同时更新任务/工单的已生产数量。
  97 + *
  98 + * @param feedbackId 报工记录 ID
  99 + * @param qualifiedQty 合格品数量
  100 + * @param unqualifiedQty 不合格品数量
  101 + * @param laborScrapQty 工废数量
  102 + * @param materialScrapQty 料废数量
  103 + * @param otherScrapQty 其他废品数量
  104 + */
  105 + void completeFeedbackFromIpqc(Long feedbackId, BigDecimal qualifiedQty, BigDecimal unqualifiedQty,
  106 + BigDecimal laborScrapQty, BigDecimal materialScrapQty, BigDecimal otherScrapQty);
  107 +
90 } 108 }
91 109
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImpl.java
@@ -165,8 +165,7 @@ public class MesProFeedbackServiceImpl implements MesProFeedbackService { @@ -165,8 +165,7 @@ public class MesProFeedbackServiceImpl implements MesProFeedbackService {
165 if (keyFlag) { 165 if (keyFlag) {
166 // 4.1 需要检验:生成产出单(质量状态=待检验),更新报工状态为待检验,等质检完成回调后再入库 166 // 4.1 需要检验:生成产出单(质量状态=待检验),更新报工状态为待检验,等质检完成回调后再入库
167 if (checkFlag) { 167 if (checkFlag) {
168 - // TODO @AI(from codex):当前这里只生成待检产出并把报工置为待检验;后续还需要在 IPQC 完成时继续拆分产出行、完成产出入库、  
169 - // 回写任务/工单进度,并把报工状态改为已完成。 168 + // 完成时回调见:MesQcIpqcServiceImpl#finishIpqc → splitPendingAndFinishProduce + completeFeedbackFromIpqc
170 productProduceService.generateProductProduce(feedback, true); 169 productProduceService.generateProductProduce(feedback, true);
171 feedbackMapper.updateById(new MesProFeedbackDO().setId(id) 170 feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
172 .setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus())); 171 .setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus()));
@@ -284,4 +283,32 @@ public class MesProFeedbackServiceImpl implements MesProFeedbackService { @@ -284,4 +283,32 @@ public class MesProFeedbackServiceImpl implements MesProFeedbackService {
284 return taskService.validateTaskNotFinished(reqVO.getTaskId()); 283 return taskService.validateTaskNotFinished(reqVO.getTaskId());
285 } 284 }
286 285
  286 + @Override
  287 + @Transactional(rollbackFor = Exception.class)
  288 + public void completeFeedbackFromIpqc(Long feedbackId, BigDecimal qualifiedQty, BigDecimal unqualifiedQty,
  289 + BigDecimal laborScrapQty, BigDecimal materialScrapQty, BigDecimal otherScrapQty) {
  290 + // 1. 校验报工单存在且为待检验状态
  291 + MesProFeedbackDO feedback = validateFeedbackExists(feedbackId);
  292 + if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.UNCHECK.getStatus())) {
  293 + throw exception(PRO_FEEDBACK_NOT_UNCHECK);
  294 + }
  295 +
  296 + // 2. 拆分待检产出行(合格/不合格),生成明细,完成产出入库
  297 + productProduceService.splitPendingAndFinishProduce(feedbackId, qualifiedQty, unqualifiedQty);
  298 +
  299 + // 3. 回写合格/不合格/废品数量,更新状态为已完成
  300 + feedbackMapper.updateById(new MesProFeedbackDO().setId(feedbackId)
  301 + .setQualifiedQuantity(qualifiedQty)
  302 + .setUnqualifiedQuantity(unqualifiedQty)
  303 + .setUncheckQuantity(BigDecimal.ZERO)
  304 + .setLaborScrapQuantity(laborScrapQty)
  305 + .setMaterialScrapQuantity(materialScrapQty)
  306 + .setOtherScrapQuantity(otherScrapQty)
  307 + .setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus()));
  308 +
  309 + // 4. 更新任务/工单的已生产数量
  310 + feedback.setQualifiedQuantity(qualifiedQty).setUnqualifiedQuantity(unqualifiedQty);
  311 + updateTaskAndWorkOrderByFeedback(feedback);
  312 + }
  313 +
287 } 314 }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImpl.java
1 package cn.iocoder.yudao.module.mes.service.qc.ipqc; 1 package cn.iocoder.yudao.module.mes.service.qc.ipqc;
2 2
3 import cn.hutool.core.util.ObjUtil; 3 import cn.hutool.core.util.ObjUtil;
  4 +import cn.hutool.core.util.ObjectUtil;
4 import cn.iocoder.yudao.framework.common.pojo.PageResult; 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 6 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcPageReqVO; 7 import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcPageReqVO;
@@ -13,8 +14,10 @@ import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper; @@ -13,8 +14,10 @@ import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
13 import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum; 14 import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum;
14 import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum; 15 import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
15 import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum; 16 import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
  17 +import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
16 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; 18 import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
17 import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService; 19 import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
  20 +import cn.iocoder.yudao.module.mes.service.pro.feedback.MesProFeedbackService;
18 import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService; 21 import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
19 import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService; 22 import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
20 import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateDetailService; 23 import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateDetailService;
@@ -42,6 +45,7 @@ import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; @@ -42,6 +45,7 @@ import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
42 @Validated 45 @Validated
43 public class MesQcIpqcServiceImpl implements MesQcIpqcService { 46 public class MesQcIpqcServiceImpl implements MesQcIpqcService {
44 47
  48 +
45 @Resource 49 @Resource
46 private MesQcIpqcMapper ipqcMapper; 50 private MesQcIpqcMapper ipqcMapper;
47 51
@@ -64,6 +68,9 @@ public class MesQcIpqcServiceImpl implements MesQcIpqcService { @@ -64,6 +68,9 @@ public class MesQcIpqcServiceImpl implements MesQcIpqcService {
64 68
65 @Resource 69 @Resource
66 private AdminUserApi adminUserApi; 70 private AdminUserApi adminUserApi;
  71 + @Resource
  72 + @Lazy
  73 + private MesProFeedbackService feedbackService;
67 74
68 @Override 75 @Override
69 @Transactional(rollbackFor = Exception.class) 76 @Transactional(rollbackFor = Exception.class)
@@ -114,6 +121,7 @@ public class MesQcIpqcServiceImpl implements MesQcIpqcService { @@ -114,6 +121,7 @@ public class MesQcIpqcServiceImpl implements MesQcIpqcService {
114 } 121 }
115 122
116 @Override 123 @Override
  124 + @Transactional(rollbackFor = Exception.class)
117 public void finishIpqc(Long id) { 125 public void finishIpqc(Long id) {
118 // 1.1 校验存在 + 草稿状态 126 // 1.1 校验存在 + 草稿状态
119 MesQcIpqcDO ipqc = validateIpqcStatusPrepare(id); 127 MesQcIpqcDO ipqc = validateIpqcStatusPrepare(id);
@@ -123,12 +131,40 @@ public class MesQcIpqcServiceImpl implements MesQcIpqcService { @@ -123,12 +131,40 @@ public class MesQcIpqcServiceImpl implements MesQcIpqcService {
123 } 131 }
124 132
125 // 2. 更新状态为已完成 133 // 2. 更新状态为已完成
126 - MesQcIpqcDO updateObj = new MesQcIpqcDO()  
127 - .setId(id).setStatus(MesQcStatusEnum.FINISHED.getStatus());  
128 - ipqcMapper.updateById(updateObj); 134 + ipqcMapper.updateById(new MesQcIpqcDO()
  135 + .setId(id).setStatus(MesQcStatusEnum.FINISHED.getStatus()));
  136 +
  137 + // 3. 回写来源单据
  138 + writeBackSourceDoc(ipqc);
  139 + }
  140 +
  141 + /**
  142 + * 回写来源单据(IPQC 完成后)
  143 + *
  144 + * <p>当来源为报工时,拆分待检产出行、完成产出入库、回写报工状态和任务/工单进度
  145 + *
  146 + * @param ipqc 过程检验单
  147 + */
  148 + private void writeBackSourceDoc(MesQcIpqcDO ipqc) {
  149 + if (ipqc.getSourceDocType() == null) {
  150 + return;
  151 + }
  152 + if (ipqc.getSourceDocId() == null) {
  153 + throw new IllegalArgumentException(
  154 + "IPQC 单[" + ipqc.getId() + "] sourceDocType 非空但 sourceDocId 为空");
  155 + }
129 156
130 - // TODO @AI(from codex):如果检验来源是报工,需要对待检产出按检验结果拆分合格/不合格行、生成产出明细、完成产品产出入库、  
131 - // 回写任务/工单数量,并将报工状态从待检验改为已完成。 157 + if (Objects.equals(ipqc.getSourceDocType(), MesBizTypeConstants.PRO_FEEDBACK)) {
  158 + feedbackService.completeFeedbackFromIpqc(ipqc.getSourceDocId(),
  159 + ObjectUtil.defaultIfNull(ipqc.getQualifiedQuantity(), BigDecimal.ZERO),
  160 + ObjectUtil.defaultIfNull(ipqc.getUnqualifiedQuantity(), BigDecimal.ZERO),
  161 + ObjectUtil.defaultIfNull(ipqc.getLaborScrapQuantity(), BigDecimal.ZERO),
  162 + ObjectUtil.defaultIfNull(ipqc.getMaterialScrapQuantity(), BigDecimal.ZERO),
  163 + ObjectUtil.defaultIfNull(ipqc.getOtherScrapQuantity(), BigDecimal.ZERO));
  164 + } else {
  165 + throw new IllegalArgumentException(
  166 + "IPQC 单[" + ipqc.getId() + "] sourceDocType=" + ipqc.getSourceDocType() + " 无法识别,无法回写来源单据");
  167 + }
132 } 168 }
133 169
134 @Override 170 @Override
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceLineService.java
@@ -19,6 +19,13 @@ public interface MesWmProductProduceLineService { @@ -19,6 +19,13 @@ public interface MesWmProductProduceLineService {
19 void createProductProduceLine(MesWmProductProduceLineDO line); 19 void createProductProduceLine(MesWmProductProduceLineDO line);
20 20
21 /** 21 /**
  22 + * 更新生产入库单行(内部使用)
  23 + *
  24 + * @param line 行数据
  25 + */
  26 + void updateProductProduceLine(MesWmProductProduceLineDO line);
  27 +
  28 + /**
22 * 根据入库单 ID 获取行列表 29 * 根据入库单 ID 获取行列表
23 * 30 *
24 * @param produceId 入库单 ID 31 * @param produceId 入库单 ID
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceLineServiceImpl.java
@@ -26,6 +26,11 @@ public class MesWmProductProduceLineServiceImpl implements MesWmProductProduceLi @@ -26,6 +26,11 @@ public class MesWmProductProduceLineServiceImpl implements MesWmProductProduceLi
26 } 26 }
27 27
28 @Override 28 @Override
  29 + public void updateProductProduceLine(MesWmProductProduceLineDO line) {
  30 + productProduceLineMapper.updateById(line);
  31 + }
  32 +
  33 + @Override
29 public List<MesWmProductProduceLineDO> getProductProduceLineListByProduceId(Long produceId) { 34 public List<MesWmProductProduceLineDO> getProductProduceLineListByProduceId(Long produceId) {
30 return productProduceLineMapper.selectListByProduceId(produceId); 35 return productProduceLineMapper.selectListByProduceId(produceId);
31 } 36 }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceService.java
@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.mes.service.wm.productproduce; @@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.mes.service.wm.productproduce;
3 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO; 3 import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
4 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO; 4 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
5 5
  6 +import java.math.BigDecimal;
  7 +
6 /** 8 /**
7 * MES 生产入库单 Service 接口 9 * MES 生产入库单 Service 接口
8 */ 10 */
@@ -32,4 +34,16 @@ public interface MesWmProductProduceService { @@ -32,4 +34,16 @@ public interface MesWmProductProduceService {
32 */ 34 */
33 MesWmProductProduceDO generateProductProduce(MesProFeedbackDO feedback, boolean checkFlag); 35 MesWmProductProduceDO generateProductProduce(MesProFeedbackDO feedback, boolean checkFlag);
34 36
  37 + /**
  38 + * IPQC 检验完成回调:将待检产出拆分为合格/不合格行、生成明细、完成入库
  39 + *
  40 + * <p>调用场景:IPQC 完成时,需要将之前 {@link #generateProductProduce} 生成的
  41 + * 待检产出({@code checkFlag=true})按检验结果拆分行,并执行入库。
  42 + *
  43 + * @param feedbackId 报工记录 ID
  44 + * @param qualifiedQty 合格品数量
  45 + * @param unqualifiedQty 不合格品数量
  46 + */
  47 + void splitPendingAndFinishProduce(Long feedbackId, BigDecimal qualifiedQty, BigDecimal unqualifiedQty);
  48 +
35 } 49 }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImpl.java
@@ -221,4 +221,70 @@ public class MesWmProductProduceServiceImpl implements MesWmProductProduceServic @@ -221,4 +221,70 @@ public class MesWmProductProduceServiceImpl implements MesWmProductProduceServic
221 return produce; 221 return produce;
222 } 222 }
223 223
  224 + @Override
  225 + @Transactional(rollbackFor = Exception.class)
  226 + public void splitPendingAndFinishProduce(Long feedbackId, BigDecimal qualifiedQty, BigDecimal unqualifiedQty) {
  227 + // 1.1 查询产出单
  228 + MesWmProductProduceDO produce = productProduceMapper.selectByFeedbackId(feedbackId);
  229 + if (produce == null) {
  230 + throw exception(WM_PRODUCT_PRODUCE_NOT_EXISTS);
  231 + }
  232 + // 1.2 获取虚拟线边库
  233 + MesWmWarehouseDO virtualWarehouse = warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE);
  234 + MesWmWarehouseLocationDO virtualLocation = locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION);
  235 + MesWmWarehouseAreaDO virtualArea = areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA);
  236 +
  237 + // 2. 查找待检验行(checkFlag=true 时只有一行 PENDING)
  238 + List<MesWmProductProduceLineDO> lines = productProduceLineService.getProductProduceLineListByProduceId(produce.getId());
  239 + // TODO @AI:hutool 的 findOne 方法,替代掉 stream;手动 if 判断为空,抛出异常;
  240 + MesWmProductProduceLineDO pendingLine = lines.stream()
  241 + .filter(l -> ObjUtil.equal(l.getQualityStatus(), MesWmQualityStatusEnum.PENDING.getStatus()))
  242 + .findFirst()
  243 + .orElseThrow(() -> exception(WM_PRODUCT_PRODUCE_LINE_NOT_EXISTS));
  244 +
  245 + // 3A. 情况一:存在不合格品数量,需要拆分行
  246 + if (unqualifiedQty != null && unqualifiedQty.compareTo(BigDecimal.ZERO) > 0) {
  247 + // 3A.1 不合格品:新建一行 + 明细
  248 + MesWmProductProduceLineDO unqualifiedLine = MesWmProductProduceLineDO.builder()
  249 + .produceId(produce.getId()).feedbackId(feedbackId)
  250 + .itemId(pendingLine.getItemId()).quantity(unqualifiedQty)
  251 + .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode())
  252 + .expireDate(pendingLine.getExpireDate()).lotNumber(pendingLine.getLotNumber())
  253 + .qualityStatus(MesWmQualityStatusEnum.FAIL.getStatus())
  254 + .build();
  255 + productProduceLineService.createProductProduceLine(unqualifiedLine);
  256 + productProduceDetailService.createProductProduceDetail(MesWmProductProduceDetailDO.builder()
  257 + .produceId(produce.getId()).lineId(unqualifiedLine.getId())
  258 + .itemId(pendingLine.getItemId()).quantity(unqualifiedQty)
  259 + .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode())
  260 + .warehouseId(virtualWarehouse.getId()).locationId(virtualLocation.getId()).areaId(virtualArea.getId())
  261 + .build());
  262 + // 3A.2 合格品:复用原待检行,更新数量和状态 + 明细
  263 + pendingLine.setQuantity(ObjectUtil.defaultIfNull(qualifiedQty, BigDecimal.ZERO));
  264 + pendingLine.setQualityStatus(MesWmQualityStatusEnum.PASS.getStatus());
  265 + productProduceLineService.updateProductProduceLine(pendingLine);
  266 + if (qualifiedQty != null && qualifiedQty.compareTo(BigDecimal.ZERO) > 0) {
  267 + productProduceDetailService.createProductProduceDetail(MesWmProductProduceDetailDO.builder()
  268 + .produceId(produce.getId()).lineId(pendingLine.getId())
  269 + .itemId(pendingLine.getItemId()).quantity(qualifiedQty)
  270 + .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode())
  271 + .warehouseId(virtualWarehouse.getId()).locationId(virtualLocation.getId()).areaId(virtualArea.getId())
  272 + .build());
  273 + }
  274 + } else {
  275 + // 3B. 情况二:全部合格,直接更新行状态 + 明细
  276 + pendingLine.setQualityStatus(MesWmQualityStatusEnum.PASS.getStatus());
  277 + productProduceLineService.updateProductProduceLine(pendingLine);
  278 + productProduceDetailService.createProductProduceDetail(MesWmProductProduceDetailDO.builder()
  279 + .produceId(produce.getId()).lineId(pendingLine.getId())
  280 + .itemId(pendingLine.getItemId()).quantity(pendingLine.getQuantity())
  281 + .batchId(pendingLine.getBatchId()).batchCode(pendingLine.getBatchCode())
  282 + .warehouseId(virtualWarehouse.getId()).locationId(virtualLocation.getId()).areaId(virtualArea.getId())
  283 + .build());
  284 + }
  285 +
  286 + // 4. 完成产出单(创建库存事务 + 更新状态为已完成)
  287 + finishProductProduce(produce.getId());
  288 + }
  289 +
224 } 290 }
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/feedback/MesProFeedbackServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.mes.service.pro.feedback;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  4 +import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
  5 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
  6 +import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
  7 +import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
  8 +import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
  9 +import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
  10 +import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
  11 +import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
  12 +import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
  13 +import cn.iocoder.yudao.module.mes.service.wm.itemconsume.MesWmItemConsumeService;
  14 +import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
  15 +import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceService;
  16 +import jakarta.annotation.Resource;
  17 +import org.junit.jupiter.api.Test;
  18 +import org.springframework.context.annotation.Import;
  19 +import org.springframework.test.context.bean.override.mockito.MockitoBean;
  20 +
  21 +import java.math.BigDecimal;
  22 +import java.util.List;
  23 +
  24 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
  25 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
  26 +import static org.junit.jupiter.api.Assertions.*;
  27 +import static org.mockito.Mockito.*;
  28 +
  29 +/**
  30 + * {@link MesProFeedbackServiceImpl} 的单元测试
  31 + *
  32 + * @author 芋道源码
  33 + */
  34 +@Import(MesProFeedbackServiceImpl.class)
  35 +public class MesProFeedbackServiceImplTest extends BaseDbUnitTest {
  36 +
  37 + @Resource
  38 + private MesProFeedbackServiceImpl feedbackService;
  39 +
  40 + @Resource
  41 + private MesProFeedbackMapper feedbackMapper;
  42 +
  43 + @MockitoBean
  44 + private MesProWorkOrderService workOrderService;
  45 + @MockitoBean
  46 + private MesProRouteProcessService routeProcessService;
  47 + @MockitoBean
  48 + private MesMdWorkstationService workstationService;
  49 + @MockitoBean
  50 + private MesProTaskService taskService;
  51 + @MockitoBean
  52 + private MesWmItemConsumeService itemConsumeService;
  53 + @MockitoBean
  54 + private MesWmProductProduceService productProduceService;
  55 + @MockitoBean
  56 + private MesWmProductProduceLineService produceLineService;
  57 +
  58 + @Test
  59 + public void testCompleteFeedbackFromIpqc_success_withUnqualified() {
  60 + // 准备数据:插入一条待检验状态的报工单
  61 + Long taskId = randomLongId();
  62 + Long workOrderId = randomLongId();
  63 + MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> {
  64 + o.setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus());
  65 + o.setTaskId(taskId);
  66 + o.setWorkOrderId(workOrderId);
  67 + o.setFeedbackQuantity(BigDecimal.valueOf(100));
  68 + });
  69 + feedbackMapper.insert(feedback);
  70 +
  71 + // mock: 产出行(用于 updateTaskAndWorkOrderByFeedback 内的数量聚合)
  72 + MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder()
  73 + .quantity(BigDecimal.valueOf(80))
  74 + .qualityStatus(MesWmQualityStatusEnum.PASS.getStatus())
  75 + .build();
  76 + MesWmProductProduceLineDO unqualifiedLine = MesWmProductProduceLineDO.builder()
  77 + .quantity(BigDecimal.valueOf(20))
  78 + .qualityStatus(MesWmQualityStatusEnum.FAIL.getStatus())
  79 + .build();
  80 + when(produceLineService.getProductProduceLineListByFeedbackId(feedback.getId()))
  81 + .thenReturn(List.of(qualifiedLine, unqualifiedLine));
  82 +
  83 + // 调用
  84 + BigDecimal qualifiedQty = BigDecimal.valueOf(80);
  85 + BigDecimal unqualifiedQty = BigDecimal.valueOf(20);
  86 + BigDecimal laborScrapQty = BigDecimal.valueOf(5);
  87 + BigDecimal materialScrapQty = BigDecimal.valueOf(10);
  88 + BigDecimal otherScrapQty = BigDecimal.valueOf(5);
  89 +
  90 + feedbackService.completeFeedbackFromIpqc(feedback.getId(),
  91 + qualifiedQty, unqualifiedQty, laborScrapQty, materialScrapQty, otherScrapQty);
  92 +
  93 + // 断言 1:调用了 splitPendingAndFinishProduce
  94 + verify(productProduceService).splitPendingAndFinishProduce(
  95 + eq(feedback.getId()), eq(qualifiedQty), eq(unqualifiedQty));
  96 +
  97 + // 断言 2:报工单状态更新为已完成
  98 + MesProFeedbackDO updatedFeedback = feedbackMapper.selectById(feedback.getId());
  99 + assertEquals(MesProFeedbackStatusEnum.FINISHED.getStatus(), updatedFeedback.getStatus());
  100 +
  101 + // 断言 3:数量回写正确
  102 + assertEquals(0, qualifiedQty.compareTo(updatedFeedback.getQualifiedQuantity()));
  103 + assertEquals(0, unqualifiedQty.compareTo(updatedFeedback.getUnqualifiedQuantity()));
  104 + assertEquals(0, BigDecimal.ZERO.compareTo(updatedFeedback.getUncheckQuantity()));
  105 + assertEquals(0, laborScrapQty.compareTo(updatedFeedback.getLaborScrapQuantity()));
  106 + assertEquals(0, materialScrapQty.compareTo(updatedFeedback.getMaterialScrapQuantity()));
  107 + assertEquals(0, otherScrapQty.compareTo(updatedFeedback.getOtherScrapQuantity()));
  108 +
  109 + // 断言 4:更新了任务和工单的已生产数量
  110 + // 注意:数量经过 DB 存取后 scale 可能变化,用 any() 匹配
  111 + verify(taskService).updateProducedQuantity(eq(taskId),
  112 + any(BigDecimal.class), any(BigDecimal.class), any(BigDecimal.class));
  113 + verify(workOrderService).updateProducedQuantity(eq(workOrderId),
  114 + any(BigDecimal.class));
  115 + }
  116 +
  117 + @Test
  118 + public void testCompleteFeedbackFromIpqc_success_allQualified() {
  119 + // 准备数据:全部合格
  120 + Long taskId = randomLongId();
  121 + Long workOrderId = randomLongId();
  122 + MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> {
  123 + o.setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus());
  124 + o.setTaskId(taskId);
  125 + o.setWorkOrderId(workOrderId);
  126 + o.setFeedbackQuantity(BigDecimal.valueOf(50));
  127 + });
  128 + feedbackMapper.insert(feedback);
  129 +
  130 + // mock: 产出行(全部合格)
  131 + MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder()
  132 + .quantity(BigDecimal.valueOf(50))
  133 + .qualityStatus(MesWmQualityStatusEnum.PASS.getStatus())
  134 + .build();
  135 + when(produceLineService.getProductProduceLineListByFeedbackId(feedback.getId()))
  136 + .thenReturn(List.of(qualifiedLine));
  137 +
  138 + // 调用
  139 + feedbackService.completeFeedbackFromIpqc(feedback.getId(),
  140 + BigDecimal.valueOf(50), BigDecimal.ZERO,
  141 + BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
  142 +
  143 + // 断言 1:报工单状态为已完成
  144 + MesProFeedbackDO updatedFeedback = feedbackMapper.selectById(feedback.getId());
  145 + assertEquals(MesProFeedbackStatusEnum.FINISHED.getStatus(), updatedFeedback.getStatus());
  146 +
  147 + // 断言 2:合格品数量正确,不合格品和废品为 0
  148 + assertEquals(0, BigDecimal.valueOf(50).compareTo(updatedFeedback.getQualifiedQuantity()));
  149 + assertEquals(0, BigDecimal.ZERO.compareTo(updatedFeedback.getUnqualifiedQuantity()));
  150 + assertEquals(0, BigDecimal.ZERO.compareTo(updatedFeedback.getLaborScrapQuantity()));
  151 + }
  152 +
  153 + @Test
  154 + public void testCompleteFeedbackFromIpqc_feedbackNotExists() {
  155 + // 调用不存在的 feedbackId,应该抛异常
  156 + Long feedbackId = randomLongId();
  157 + assertThrows(Exception.class, () ->
  158 + feedbackService.completeFeedbackFromIpqc(feedbackId,
  159 + BigDecimal.TEN, BigDecimal.ZERO,
  160 + BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO));
  161 + }
  162 +
  163 + @Test
  164 + public void testCompleteFeedbackFromIpqc_feedbackNotUncheck() {
  165 + // 准备数据:草稿状态(不是 UNCHECK),应该报错
  166 + MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> {
  167 + o.setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus());
  168 + });
  169 + feedbackMapper.insert(feedback);
  170 +
  171 + // 调用,应该抛异常
  172 + assertThrows(Exception.class, () ->
  173 + feedbackService.completeFeedbackFromIpqc(feedback.getId(),
  174 + BigDecimal.TEN, BigDecimal.ZERO,
  175 + BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO));
  176 +
  177 + // 断言:不应该调用任何产出单方法
  178 + verify(productProduceService, never()).splitPendingAndFinishProduce(anyLong(), any(), any());
  179 + }
  180 +
  181 + @Test
  182 + public void testCompleteFeedbackFromIpqc_feedbackAlreadyFinished() {
  183 + // 准备数据:已完成状态,不能再次完成
  184 + MesProFeedbackDO feedback = randomPojo(MesProFeedbackDO.class, o -> {
  185 + o.setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus());
  186 + });
  187 + feedbackMapper.insert(feedback);
  188 +
  189 + // 调用,应该抛异常
  190 + assertThrows(Exception.class, () ->
  191 + feedbackService.completeFeedbackFromIpqc(feedback.getId(),
  192 + BigDecimal.TEN, BigDecimal.ZERO,
  193 + BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO));
  194 + }
  195 +
  196 +}
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/ipqc/MesQcIpqcServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.mes.service.qc.ipqc;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  4 +import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
  5 +import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
  6 +import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
  7 +import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
  8 +import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
  9 +import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
  10 +import cn.iocoder.yudao.module.mes.service.pro.feedback.MesProFeedbackService;
  11 +import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
  12 +import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
  13 +import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateDetailService;
  14 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  15 +import jakarta.annotation.Resource;
  16 +import org.junit.jupiter.api.Test;
  17 +import org.springframework.context.annotation.Import;
  18 +import org.springframework.test.context.bean.override.mockito.MockitoBean;
  19 +
  20 +import java.math.BigDecimal;
  21 +
  22 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
  23 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
  24 +import static org.junit.jupiter.api.Assertions.*;
  25 +import static org.mockito.Mockito.*;
  26 +
  27 +/**
  28 + * {@link MesQcIpqcServiceImpl} 的单元测试
  29 + *
  30 + * @author 芋道源码
  31 + */
  32 +@Import(MesQcIpqcServiceImpl.class)
  33 +public class MesQcIpqcServiceImplTest extends BaseDbUnitTest {
  34 +
  35 + @Resource
  36 + private MesQcIpqcServiceImpl ipqcService;
  37 +
  38 + @Resource
  39 + private MesQcIpqcMapper ipqcMapper;
  40 +
  41 + @MockitoBean
  42 + private MesQcTemplateDetailService templateDetailService;
  43 + @MockitoBean
  44 + private MesQcIpqcLineService ipqcLineService;
  45 + @MockitoBean
  46 + private MesProWorkOrderService workOrderService;
  47 + @MockitoBean
  48 + private MesMdWorkstationService workstationService;
  49 + @MockitoBean
  50 + private MesMdItemService itemService;
  51 + @MockitoBean
  52 + private MesQcDefectRecordService defectRecordService;
  53 + @MockitoBean
  54 + private AdminUserApi adminUserApi;
  55 + @MockitoBean
  56 + private MesProFeedbackService feedbackService;
  57 +
  58 + @Test
  59 + public void testFinishIpqc_writeBack_feedback() {
  60 + // 准备数据:插入一条草稿状态的 IPQC 单,来源为报工单(PRO_FEEDBACK=304)
  61 + Long sourceDocId = randomLongId();
  62 + BigDecimal qualifiedQty = BigDecimal.valueOf(80);
  63 + BigDecimal unqualifiedQty = BigDecimal.valueOf(20);
  64 + BigDecimal laborScrapQty = BigDecimal.valueOf(5);
  65 + BigDecimal materialScrapQty = BigDecimal.valueOf(10);
  66 + BigDecimal otherScrapQty = BigDecimal.valueOf(5);
  67 +
  68 + MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
  69 + o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
  70 + o.setCheckResult(1); // 非空,满足 finishIpqc 的校验
  71 + o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
  72 + o.setSourceDocId(sourceDocId);
  73 + o.setSourceLineId(null); // 报工场景下预留不使用
  74 + o.setQualifiedQuantity(qualifiedQty);
  75 + o.setUnqualifiedQuantity(unqualifiedQty);
  76 + o.setLaborScrapQuantity(laborScrapQty);
  77 + o.setMaterialScrapQuantity(materialScrapQty);
  78 + o.setOtherScrapQuantity(otherScrapQty);
  79 + });
  80 + ipqcMapper.insert(ipqc);
  81 +
  82 + // 调用
  83 + ipqcService.finishIpqc(ipqc.getId());
  84 +
  85 + // 断言 1:状态更新为已完成
  86 + MesQcIpqcDO updatedIpqc = ipqcMapper.selectById(ipqc.getId());
  87 + assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIpqc.getStatus());
  88 +
  89 + // 断言 2:调用了 feedbackService.completeFeedbackFromIpqc,传递正确参数
  90 + // 注意:数量经过 DB 存取后 scale 可能变化(例如 80 → 80.00),所以用 any() 匹配
  91 + verify(feedbackService).completeFeedbackFromIpqc(
  92 + eq(sourceDocId),
  93 + any(BigDecimal.class),
  94 + any(BigDecimal.class),
  95 + any(BigDecimal.class),
  96 + any(BigDecimal.class),
  97 + any(BigDecimal.class));
  98 + }
  99 +
  100 + @Test
  101 + public void testFinishIpqc_writeBack_feedback_withNullQuantities() {
  102 + // 准备数据:合格品/不合格品/废品数量为 null 的场景,应该 defaultIfNull 为 0
  103 + Long sourceDocId = randomLongId();
  104 + MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
  105 + o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
  106 + o.setCheckResult(1);
  107 + o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
  108 + o.setSourceDocId(sourceDocId);
  109 + o.setSourceLineId(null);
  110 + o.setQualifiedQuantity(null);
  111 + o.setUnqualifiedQuantity(null);
  112 + o.setLaborScrapQuantity(null);
  113 + o.setMaterialScrapQuantity(null);
  114 + o.setOtherScrapQuantity(null);
  115 + });
  116 + ipqcMapper.insert(ipqc);
  117 +
  118 + // 调用
  119 + ipqcService.finishIpqc(ipqc.getId());
  120 +
  121 + // 断言:所有数量参数都是 BigDecimal.ZERO(defaultIfNull 处理)
  122 + verify(feedbackService).completeFeedbackFromIpqc(
  123 + eq(sourceDocId),
  124 + eq(BigDecimal.ZERO),
  125 + eq(BigDecimal.ZERO),
  126 + eq(BigDecimal.ZERO),
  127 + eq(BigDecimal.ZERO),
  128 + eq(BigDecimal.ZERO));
  129 + }
  130 +
  131 + @Test
  132 + public void testFinishIpqc_writeBack_noSourceDoc() {
  133 + // 准备数据:sourceDocType 为空,不需要回写
  134 + MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
  135 + o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
  136 + o.setCheckResult(1);
  137 + o.setSourceDocType(null);
  138 + o.setSourceDocId(null);
  139 + o.setSourceLineId(null);
  140 + });
  141 + ipqcMapper.insert(ipqc);
  142 +
  143 + // 调用
  144 + ipqcService.finishIpqc(ipqc.getId());
  145 +
  146 + // 断言 1:状态更新成功
  147 + MesQcIpqcDO updatedIpqc = ipqcMapper.selectById(ipqc.getId());
  148 + assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIpqc.getStatus());
  149 +
  150 + // 断言 2:不应该调用 feedbackService
  151 + verify(feedbackService, never()).completeFeedbackFromIpqc(
  152 + anyLong(), any(), any(), any(), any(), any());
  153 + }
  154 +
  155 + @Test
  156 + public void testFinishIpqc_writeBack_sourceDocTypeDirtyData() {
  157 + // 准备数据:sourceDocType 非空但 sourceDocId 为空,应该抛异常
  158 + MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
  159 + o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
  160 + o.setCheckResult(1);
  161 + o.setSourceDocType(MesBizTypeConstants.PRO_FEEDBACK);
  162 + o.setSourceDocId(null); // 脏数据
  163 + o.setSourceLineId(null);
  164 + });
  165 + ipqcMapper.insert(ipqc);
  166 +
  167 + // 调用,并断言异常
  168 + assertThrows(IllegalArgumentException.class, () -> ipqcService.finishIpqc(ipqc.getId()));
  169 + }
  170 +
  171 + @Test
  172 + public void testFinishIpqc_writeBack_unknownSourceDocType() {
  173 + // 准备数据:sourceDocType 无法识别
  174 + MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
  175 + o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
  176 + o.setCheckResult(1);
  177 + o.setSourceDocType(99999); // 无法识别的类型
  178 + o.setSourceDocId(randomLongId());
  179 + o.setSourceLineId(null);
  180 + });
  181 + ipqcMapper.insert(ipqc);
  182 +
  183 + // 调用,并断言异常
  184 + assertThrows(IllegalArgumentException.class, () -> ipqcService.finishIpqc(ipqc.getId()));
  185 + }
  186 +
  187 + @Test
  188 + public void testFinishIpqc_checkResultEmpty() {
  189 + // 准备数据:checkResult 为空,应该报错
  190 + MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
  191 + o.setStatus(MesQcStatusEnum.DRAFT.getStatus());
  192 + o.setCheckResult(null); // 检测结论为空
  193 + o.setSourceDocType(null);
  194 + });
  195 + ipqcMapper.insert(ipqc);
  196 +
  197 + // 调用,应该抛异常(检测结论必填)
  198 + assertThrows(Exception.class, () -> ipqcService.finishIpqc(ipqc.getId()));
  199 + }
  200 +
  201 + @Test
  202 + public void testFinishIpqc_notDraftStatus() {
  203 + // 准备数据:已完成状态,不能再次完成
  204 + MesQcIpqcDO ipqc = randomPojo(MesQcIpqcDO.class, o -> {
  205 + o.setStatus(MesQcStatusEnum.FINISHED.getStatus());
  206 + o.setCheckResult(1);
  207 + });
  208 + ipqcMapper.insert(ipqc);
  209 +
  210 + // 调用,应该抛异常(不是草稿状态)
  211 + assertThrows(Exception.class, () -> ipqcService.finishIpqc(ipqc.getId()));
  212 + }
  213 +
  214 +}
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/productproduce/MesWmProductProduceServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.mes.service.wm.productproduce;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  4 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDetailDO;
  5 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
  6 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
  7 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseAreaDO;
  8 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
  9 +import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseLocationDO;
  10 +import cn.iocoder.yudao.module.mes.dal.mysql.wm.productproduce.MesWmProductProduceMapper;
  11 +import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductProduceStatusEnum;
  12 +import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
  13 +import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
  14 +import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService;
  15 +import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
  16 +import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
  17 +import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
  18 +import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
  19 +import jakarta.annotation.Resource;
  20 +import org.junit.jupiter.api.Test;
  21 +import org.mockito.ArgumentCaptor;
  22 +import org.springframework.context.annotation.Import;
  23 +import org.springframework.test.context.bean.override.mockito.MockitoBean;
  24 +
  25 +import java.math.BigDecimal;
  26 +import java.util.List;
  27 +
  28 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
  29 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
  30 +import static org.junit.jupiter.api.Assertions.*;
  31 +import static org.mockito.Mockito.*;
  32 +
  33 +/**
  34 + * {@link MesWmProductProduceServiceImpl} 的单元测试
  35 + *
  36 + * @author 芋道源码
  37 + */
  38 +@Import(MesWmProductProduceServiceImpl.class)
  39 +public class MesWmProductProduceServiceImplTest extends BaseDbUnitTest {
  40 +
  41 + @Resource
  42 + private MesWmProductProduceServiceImpl productProduceService;
  43 +
  44 + @Resource
  45 + private MesWmProductProduceMapper productProduceMapper;
  46 +
  47 + @MockitoBean
  48 + private MesWmProductProduceLineService productProduceLineService;
  49 + @MockitoBean
  50 + private MesWmProductProduceDetailService productProduceDetailService;
  51 + @MockitoBean
  52 + private MesProWorkOrderService workOrderService;
  53 + @MockitoBean
  54 + private MesWmBatchService batchService;
  55 + @MockitoBean
  56 + private MesWmTransactionService wmTransactionService;
  57 + @MockitoBean
  58 + private MesWmWarehouseService warehouseService;
  59 + @MockitoBean
  60 + private MesWmWarehouseLocationService locationService;
  61 + @MockitoBean
  62 + private MesWmWarehouseAreaService areaService;
  63 +
  64 + @Test
  65 + public void testSplitPendingAndFinishProduce_withUnqualified() {
  66 + // 准备数据:插入一个 PREPARE 状态的产出单
  67 + Long feedbackId = randomLongId();
  68 + Long itemId = randomLongId();
  69 + Long batchId = randomLongId();
  70 + String batchCode = "BATCH-001";
  71 +
  72 + MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class, o -> {
  73 + o.setFeedbackId(feedbackId);
  74 + o.setStatus(MesWmProductProduceStatusEnum.PREPARE.getStatus());
  75 + });
  76 + productProduceMapper.insert(produce);
  77 +
  78 + // mock: 虚拟线边库
  79 + Long whId = randomLongId(), locId = randomLongId(), areaId = randomLongId();
  80 + MesWmWarehouseDO wh = new MesWmWarehouseDO(); wh.setId(whId);
  81 + MesWmWarehouseLocationDO loc = new MesWmWarehouseLocationDO(); loc.setId(locId);
  82 + MesWmWarehouseAreaDO area = new MesWmWarehouseAreaDO(); area.setId(areaId);
  83 + when(warehouseService.getWarehouseByCode(MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE)).thenReturn(wh);
  84 + when(locationService.getWarehouseLocationByCode(MesWmWarehouseLocationDO.WIP_VIRTUAL_LOCATION)).thenReturn(loc);
  85 + when(areaService.getWarehouseAreaByCode(MesWmWarehouseAreaDO.WIP_VIRTUAL_AREA)).thenReturn(area);
  86 +
  87 + // mock: 返回一条 PENDING 的产出行
  88 + MesWmProductProduceLineDO pendingLine = MesWmProductProduceLineDO.builder()
  89 + .id(100L).produceId(produce.getId()).feedbackId(feedbackId)
  90 + .itemId(itemId).quantity(BigDecimal.valueOf(100))
  91 + .batchId(batchId).batchCode(batchCode)
  92 + .qualityStatus(MesWmQualityStatusEnum.PENDING.getStatus())
  93 + .build();
  94 + when(productProduceLineService.getProductProduceLineListByProduceId(produce.getId()))
  95 + .thenReturn(List.of(pendingLine));
  96 +
  97 + // mock: finishProductProduce 内部会再查一次行和明细(用于创建库存事务)
  98 + // 由于 finishProductProduce 在拆分行后调用,此时行已经变了,但由于 line/detail 是 mock 的,
  99 + // 我们需要 mock 已拆分后的行给 finishProductProduce
  100 + MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder()
  101 + .id(100L).produceId(produce.getId()).itemId(itemId)
  102 + .quantity(BigDecimal.valueOf(80))
  103 + .qualityStatus(MesWmQualityStatusEnum.PASS.getStatus())
  104 + .build();
  105 + MesWmProductProduceLineDO unqualifiedLine = MesWmProductProduceLineDO.builder()
  106 + .id(101L).produceId(produce.getId()).itemId(itemId)
  107 + .quantity(BigDecimal.valueOf(20))
  108 + .qualityStatus(MesWmQualityStatusEnum.FAIL.getStatus())
  109 + .build();
  110 + // finishProductProduce 内部第二次调用 getProductProduceLineListByProduceId
  111 + when(productProduceLineService.getProductProduceLineListByProduceId(produce.getId()))
  112 + .thenReturn(List.of(pendingLine)) // 第 1 次:splitPendingAndFinishProduce 查待检行
  113 + .thenReturn(List.of(qualifiedLine, unqualifiedLine)); // 第 2 次:finishProductProduce 查所有行
  114 +
  115 + // mock: finishProductProduce 中按行查明细
  116 + MesWmProductProduceDetailDO qualifiedDetail = MesWmProductProduceDetailDO.builder()
  117 + .lineId(100L).quantity(BigDecimal.valueOf(80)).build();
  118 + MesWmProductProduceDetailDO unqualifiedDetail = MesWmProductProduceDetailDO.builder()
  119 + .lineId(101L).quantity(BigDecimal.valueOf(20)).build();
  120 + when(productProduceDetailService.getProductProduceDetailListByLineId(100L))
  121 + .thenReturn(List.of(qualifiedDetail));
  122 + when(productProduceDetailService.getProductProduceDetailListByLineId(101L))
  123 + .thenReturn(List.of(unqualifiedDetail));
  124 +
  125 + // 调用
  126 + productProduceService.splitPendingAndFinishProduce(feedbackId, BigDecimal.valueOf(80), BigDecimal.valueOf(20));
  127 +
  128 + // 断言 1:不合格品行 - 新建了一行
  129 + ArgumentCaptor<MesWmProductProduceLineDO> lineCaptor = ArgumentCaptor.forClass(MesWmProductProduceLineDO.class);
  130 + verify(productProduceLineService).createProductProduceLine(lineCaptor.capture());
  131 + MesWmProductProduceLineDO createdLine = lineCaptor.getValue();
  132 + assertEquals(MesWmQualityStatusEnum.FAIL.getStatus(), createdLine.getQualityStatus());
  133 + assertEquals(0, BigDecimal.valueOf(20).compareTo(createdLine.getQuantity()));
  134 + assertEquals(itemId, createdLine.getItemId());
  135 +
  136 + // 断言 2:原待检行 - 更新为合格品
  137 + ArgumentCaptor<MesWmProductProduceLineDO> updateCaptor = ArgumentCaptor.forClass(MesWmProductProduceLineDO.class);
  138 + verify(productProduceLineService).updateProductProduceLine(updateCaptor.capture());
  139 + MesWmProductProduceLineDO updatedLine = updateCaptor.getValue();
  140 + assertEquals(MesWmQualityStatusEnum.PASS.getStatus(), updatedLine.getQualityStatus());
  141 + assertEquals(0, BigDecimal.valueOf(80).compareTo(updatedLine.getQuantity()));
  142 +
  143 + // 断言 3:生成了 2 条明细(不合格品明细 + 合格品明细)
  144 + verify(productProduceDetailService, times(2)).createProductProduceDetail(any());
  145 +
  146 + // 断言 4:产出单状态更新为已完成
  147 + MesWmProductProduceDO updatedProduce = productProduceMapper.selectById(produce.getId());
  148 + assertEquals(MesWmProductProduceStatusEnum.FINISHED.getStatus(), updatedProduce.getStatus());
  149 +
  150 + // 断言 5:创建了库存事务
  151 + verify(wmTransactionService, times(2)).createTransaction(any());
  152 + }
  153 +
  154 + @Test
  155 + public void testSplitPendingAndFinishProduce_allQualified() {
  156 + // 准备数据:全部合格品场景
  157 + Long feedbackId = randomLongId();
  158 + Long itemId = randomLongId();
  159 +
  160 + MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class, o -> {
  161 + o.setFeedbackId(feedbackId);
  162 + o.setStatus(MesWmProductProduceStatusEnum.PREPARE.getStatus());
  163 + });
  164 + productProduceMapper.insert(produce);
  165 +
  166 + // mock: 虚拟线边库
  167 + Long whId = randomLongId(), locId = randomLongId(), areaId = randomLongId();
  168 + MesWmWarehouseDO wh = new MesWmWarehouseDO(); wh.setId(whId);
  169 + MesWmWarehouseLocationDO loc = new MesWmWarehouseLocationDO(); loc.setId(locId);
  170 + MesWmWarehouseAreaDO area = new MesWmWarehouseAreaDO(); area.setId(areaId);
  171 + when(warehouseService.getWarehouseByCode(any())).thenReturn(wh);
  172 + when(locationService.getWarehouseLocationByCode(any())).thenReturn(loc);
  173 + when(areaService.getWarehouseAreaByCode(any())).thenReturn(area);
  174 +
  175 + MesWmProductProduceLineDO pendingLine = MesWmProductProduceLineDO.builder()
  176 + .id(200L).produceId(produce.getId()).feedbackId(feedbackId)
  177 + .itemId(itemId).quantity(BigDecimal.valueOf(50))
  178 + .qualityStatus(MesWmQualityStatusEnum.PENDING.getStatus())
  179 + .build();
  180 +
  181 + // 第 1 次查行(拆分阶段)返回 PENDING 行;第 2 次查行(finishProductProduce)返回已更新的合格行
  182 + MesWmProductProduceLineDO qualifiedLine = MesWmProductProduceLineDO.builder()
  183 + .id(200L).produceId(produce.getId()).itemId(itemId)
  184 + .quantity(BigDecimal.valueOf(50))
  185 + .qualityStatus(MesWmQualityStatusEnum.PASS.getStatus())
  186 + .build();
  187 + when(productProduceLineService.getProductProduceLineListByProduceId(produce.getId()))
  188 + .thenReturn(List.of(pendingLine))
  189 + .thenReturn(List.of(qualifiedLine));
  190 +
  191 + // mock: finishProductProduce 中按行查明细
  192 + MesWmProductProduceDetailDO detail = MesWmProductProduceDetailDO.builder()
  193 + .lineId(200L).quantity(BigDecimal.valueOf(50)).build();
  194 + when(productProduceDetailService.getProductProduceDetailListByLineId(200L))
  195 + .thenReturn(List.of(detail));
  196 +
  197 + // 调用:不合格品数量 = 0
  198 + productProduceService.splitPendingAndFinishProduce(feedbackId, BigDecimal.valueOf(50), BigDecimal.ZERO);
  199 +
  200 + // 断言 1:没有新建行(不合格品数量为 0)
  201 + verify(productProduceLineService, never()).createProductProduceLine(any());
  202 +
  203 + // 断言 2:更新了原待检行为合格
  204 + ArgumentCaptor<MesWmProductProduceLineDO> updateCaptor = ArgumentCaptor.forClass(MesWmProductProduceLineDO.class);
  205 + verify(productProduceLineService).updateProductProduceLine(updateCaptor.capture());
  206 + assertEquals(MesWmQualityStatusEnum.PASS.getStatus(), updateCaptor.getValue().getQualityStatus());
  207 +
  208 + // 断言 3:只生成了 1 条明细(全部合格品)
  209 + verify(productProduceDetailService, times(1)).createProductProduceDetail(any());
  210 +
  211 + // 断言 4:产出单状态更新为已完成
  212 + MesWmProductProduceDO updatedProduce = productProduceMapper.selectById(produce.getId());
  213 + assertEquals(MesWmProductProduceStatusEnum.FINISHED.getStatus(), updatedProduce.getStatus());
  214 + }
  215 +
  216 + @Test
  217 + public void testSplitPendingAndFinishProduce_produceNotExists() {
  218 + // 调用不存在的 feedbackId,应该抛异常
  219 + Long feedbackId = randomLongId();
  220 + assertThrows(Exception.class, () ->
  221 + productProduceService.splitPendingAndFinishProduce(feedbackId, BigDecimal.TEN, BigDecimal.ZERO));
  222 + }
  223 +
  224 + @Test
  225 + public void testSplitPendingAndFinishProduce_noPendingLine() {
  226 + // 准备数据:产出单存在,但没有 PENDING 行
  227 + Long feedbackId = randomLongId();
  228 + MesWmProductProduceDO produce = randomPojo(MesWmProductProduceDO.class, o -> {
  229 + o.setFeedbackId(feedbackId);
  230 + o.setStatus(MesWmProductProduceStatusEnum.PREPARE.getStatus());
  231 + });
  232 + productProduceMapper.insert(produce);
  233 +
  234 + // mock: 虚拟线边库
  235 + MesWmWarehouseDO wh = new MesWmWarehouseDO(); wh.setId(1L);
  236 + MesWmWarehouseLocationDO loc = new MesWmWarehouseLocationDO(); loc.setId(1L);
  237 + MesWmWarehouseAreaDO area = new MesWmWarehouseAreaDO(); area.setId(1L);
  238 + when(warehouseService.getWarehouseByCode(any())).thenReturn(wh);
  239 + when(locationService.getWarehouseLocationByCode(any())).thenReturn(loc);
  240 + when(areaService.getWarehouseAreaByCode(any())).thenReturn(area);
  241 +
  242 + // mock: 返回一条 PASS 状态的行(不是 PENDING)
  243 + MesWmProductProduceLineDO passLine = MesWmProductProduceLineDO.builder()
  244 + .id(300L).produceId(produce.getId())
  245 + .qualityStatus(MesWmQualityStatusEnum.PASS.getStatus())
  246 + .build();
  247 + when(productProduceLineService.getProductProduceLineListByProduceId(produce.getId()))
  248 + .thenReturn(List.of(passLine));
  249 +
  250 + // 调用,应该抛异常(找不到 PENDING 行)
  251 + assertThrows(Exception.class, () ->
  252 + productProduceService.splitPendingAndFinishProduce(feedbackId, BigDecimal.TEN, BigDecimal.ZERO));
  253 + }
  254 +
  255 +}
yudao-module-mes/src/test/resources/sql/clean.sql
@@ -4,3 +4,8 @@ DELETE FROM &quot;mes_md_auto_code_record&quot;; @@ -4,3 +4,8 @@ DELETE FROM &quot;mes_md_auto_code_record&quot;;
4 DELETE FROM "mes_wm_arrival_notice"; 4 DELETE FROM "mes_wm_arrival_notice";
5 DELETE FROM "mes_wm_outsource_receipt"; 5 DELETE FROM "mes_wm_outsource_receipt";
6 DELETE FROM "mes_qc_iqc"; 6 DELETE FROM "mes_qc_iqc";
  7 +DELETE FROM "mes_qc_ipqc";
  8 +DELETE FROM "mes_pro_feedback";
  9 +DELETE FROM "mes_wm_product_produce";
  10 +DELETE FROM "mes_wm_product_produce_line";
  11 +DELETE FROM "mes_wm_product_produce_detail";
yudao-module-mes/src/test/resources/sql/create_tables.sql
@@ -146,3 +146,154 @@ CREATE TABLE IF NOT EXISTS &quot;mes_qc_iqc&quot; ( @@ -146,3 +146,154 @@ CREATE TABLE IF NOT EXISTS &quot;mes_qc_iqc&quot; (
146 "tenant_id" bigint NOT NULL DEFAULT 0, 146 "tenant_id" bigint NOT NULL DEFAULT 0,
147 PRIMARY KEY ("id") 147 PRIMARY KEY ("id")
148 ); 148 );
  149 +
  150 +-- ----------------------------
  151 +-- MES 过程检验单(IPQC)
  152 +-- ----------------------------
  153 +CREATE TABLE IF NOT EXISTS "mes_qc_ipqc" (
  154 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  155 + "code" varchar(64) DEFAULT NULL,
  156 + "name" varchar(500) DEFAULT NULL,
  157 + "type" tinyint DEFAULT NULL,
  158 + "template_id" bigint DEFAULT NULL,
  159 + "source_doc_type" int DEFAULT NULL,
  160 + "source_doc_id" bigint DEFAULT NULL,
  161 + "source_line_id" bigint DEFAULT NULL,
  162 + "work_order_id" bigint DEFAULT NULL,
  163 + "task_id" bigint DEFAULT NULL,
  164 + "workstation_id" bigint DEFAULT NULL,
  165 + "process_id" bigint DEFAULT NULL,
  166 + "item_id" bigint DEFAULT NULL,
  167 + "check_quantity" decimal(14,2) DEFAULT NULL,
  168 + "qualified_quantity" decimal(14,2) DEFAULT NULL,
  169 + "unqualified_quantity" decimal(14,2) DEFAULT NULL,
  170 + "labor_scrap_quantity" decimal(14,2) DEFAULT NULL,
  171 + "material_scrap_quantity" decimal(14,2) DEFAULT NULL,
  172 + "other_scrap_quantity" decimal(14,2) DEFAULT NULL,
  173 + "critical_rate" decimal(14,2) DEFAULT NULL,
  174 + "major_rate" decimal(14,2) DEFAULT NULL,
  175 + "minor_rate" decimal(14,2) DEFAULT NULL,
  176 + "critical_quantity" int DEFAULT NULL,
  177 + "major_quantity" int DEFAULT NULL,
  178 + "minor_quantity" int DEFAULT NULL,
  179 + "check_result" tinyint DEFAULT NULL,
  180 + "inspect_date" timestamp DEFAULT NULL,
  181 + "inspector_user_id" bigint DEFAULT NULL,
  182 + "status" tinyint DEFAULT NULL,
  183 + "remark" varchar(500) DEFAULT NULL,
  184 + "creator" varchar(64) DEFAULT '',
  185 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  186 + "updater" varchar(64) DEFAULT '',
  187 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  188 + "deleted" bit NOT NULL DEFAULT FALSE,
  189 + "tenant_id" bigint NOT NULL DEFAULT 0,
  190 + PRIMARY KEY ("id")
  191 +);
  192 +
  193 +-- ----------------------------
  194 +-- MES 生产报工
  195 +-- ----------------------------
  196 +CREATE TABLE IF NOT EXISTS "mes_pro_feedback" (
  197 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  198 + "code" varchar(64) DEFAULT NULL,
  199 + "type" tinyint DEFAULT NULL,
  200 + "channel" varchar(64) DEFAULT NULL,
  201 + "feedback_time" timestamp DEFAULT NULL,
  202 + "workstation_id" bigint DEFAULT NULL,
  203 + "route_id" bigint DEFAULT NULL,
  204 + "process_id" bigint DEFAULT NULL,
  205 + "work_order_id" bigint DEFAULT NULL,
  206 + "task_id" bigint DEFAULT NULL,
  207 + "item_id" bigint DEFAULT NULL,
  208 + "expire_date" timestamp DEFAULT NULL,
  209 + "lot_number" varchar(64) DEFAULT NULL,
  210 + "scheduled_quantity" decimal(14,2) DEFAULT NULL,
  211 + "feedback_quantity" decimal(14,2) DEFAULT NULL,
  212 + "qualified_quantity" decimal(14,2) DEFAULT NULL,
  213 + "unqualified_quantity" decimal(14,2) DEFAULT NULL,
  214 + "uncheck_quantity" decimal(14,2) DEFAULT NULL,
  215 + "labor_scrap_quantity" decimal(14,2) DEFAULT NULL,
  216 + "material_scrap_quantity" decimal(14,2) DEFAULT NULL,
  217 + "other_scrap_quantity" decimal(14,2) DEFAULT NULL,
  218 + "feedback_user_id" bigint DEFAULT NULL,
  219 + "approve_user_id" bigint DEFAULT NULL,
  220 + "status" tinyint DEFAULT NULL,
  221 + "remark" varchar(500) DEFAULT NULL,
  222 + "creator" varchar(64) DEFAULT '',
  223 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  224 + "updater" varchar(64) DEFAULT '',
  225 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  226 + "deleted" bit NOT NULL DEFAULT FALSE,
  227 + "tenant_id" bigint NOT NULL DEFAULT 0,
  228 + PRIMARY KEY ("id")
  229 +);
  230 +
  231 +-- ----------------------------
  232 +-- MES 生产入库单主表
  233 +-- ----------------------------
  234 +CREATE TABLE IF NOT EXISTS "mes_wm_product_produce" (
  235 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  236 + "work_order_id" bigint DEFAULT NULL,
  237 + "feedback_id" bigint DEFAULT NULL,
  238 + "task_id" bigint DEFAULT NULL,
  239 + "workstation_id" bigint DEFAULT NULL,
  240 + "process_id" bigint DEFAULT NULL,
  241 + "produce_date" timestamp DEFAULT NULL,
  242 + "status" int DEFAULT NULL,
  243 + "remark" varchar(500) DEFAULT NULL,
  244 + "creator" varchar(64) DEFAULT '',
  245 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  246 + "updater" varchar(64) DEFAULT '',
  247 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  248 + "deleted" bit NOT NULL DEFAULT FALSE,
  249 + "tenant_id" bigint NOT NULL DEFAULT 0,
  250 + PRIMARY KEY ("id")
  251 +);
  252 +
  253 +-- ----------------------------
  254 +-- MES 生产入库单行表
  255 +-- ----------------------------
  256 +CREATE TABLE IF NOT EXISTS "mes_wm_product_produce_line" (
  257 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  258 + "produce_id" bigint DEFAULT NULL,
  259 + "feedback_id" bigint DEFAULT NULL,
  260 + "item_id" bigint DEFAULT NULL,
  261 + "quantity" decimal(12,2) DEFAULT NULL,
  262 + "batch_id" bigint DEFAULT NULL,
  263 + "batch_code" varchar(255) DEFAULT NULL,
  264 + "expire_date" timestamp DEFAULT NULL,
  265 + "lot_number" varchar(128) DEFAULT NULL,
  266 + "quality_status" int DEFAULT NULL,
  267 + "remark" varchar(500) DEFAULT NULL,
  268 + "creator" varchar(64) DEFAULT '',
  269 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  270 + "updater" varchar(64) DEFAULT '',
  271 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  272 + "deleted" bit NOT NULL DEFAULT FALSE,
  273 + "tenant_id" bigint NOT NULL DEFAULT 0,
  274 + PRIMARY KEY ("id")
  275 +);
  276 +
  277 +-- ----------------------------
  278 +-- MES 生产入库明细表
  279 +-- ----------------------------
  280 +CREATE TABLE IF NOT EXISTS "mes_wm_product_produce_detail" (
  281 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  282 + "produce_id" bigint DEFAULT NULL,
  283 + "line_id" bigint DEFAULT NULL,
  284 + "item_id" bigint DEFAULT NULL,
  285 + "quantity" decimal(12,2) DEFAULT NULL,
  286 + "batch_id" bigint DEFAULT NULL,
  287 + "batch_code" varchar(255) DEFAULT NULL,
  288 + "warehouse_id" bigint DEFAULT NULL,
  289 + "location_id" bigint DEFAULT NULL,
  290 + "area_id" bigint DEFAULT NULL,
  291 + "remark" varchar(500) DEFAULT NULL,
  292 + "creator" varchar(64) DEFAULT '',
  293 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  294 + "updater" varchar(64) DEFAULT '',
  295 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  296 + "deleted" bit NOT NULL DEFAULT FALSE,
  297 + "tenant_id" bigint NOT NULL DEFAULT 0,
  298 + PRIMARY KEY ("id")
  299 +);