Commit fa473f7da9f9e9763b73089ffad7d7f535151a06
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 "mes_md_auto_code_record"; | @@ -4,3 +4,8 @@ DELETE FROM "mes_md_auto_code_record"; | ||
| 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 "mes_qc_iqc" ( | @@ -146,3 +146,154 @@ CREATE TABLE IF NOT EXISTS "mes_qc_iqc" ( | ||
| 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 | +); |