| | |
| | | package cn.iocoder.yudao.module.mes.service.wm.productsales; |
| | | |
| | | import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; |
| | | import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi; |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | | import cn.hutool.core.util.ObjUtil; |
| | |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.productsales.MesWmProductSalesLineMapper; |
| | | import cn.iocoder.yudao.module.mes.enums.qc.MesQcCheckResultEnum; |
| | | import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; |
| | | import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService; |
| | | import cn.iocoder.yudao.module.mes.service.wm.batch.MesWmBatchService; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.batch.MesWmBatchDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.salesnotice.MesWmSalesNoticeLineDO; |
| | | import cn.iocoder.yudao.module.mes.service.wm.salesnotice.MesWmSalesNoticeLineService; |
| | | import cn.iocoder.yudao.module.mes.service.qc.oqc.MesQcOqcService; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.context.annotation.Lazy; |
| | |
| | | /** |
| | | * MES 销售出库单行 Service 实现类 |
| | | * |
| | | * @author 芋道源码 |
| | | * @author 超级管理员 |
| | | */ |
| | | @Service |
| | | @Validated |
| | |
| | | @Resource |
| | | private MesWmProductSalesDetailService productSalesDetailService; |
| | | @Resource |
| | | private MesMdItemService itemService; |
| | | private MesMdmItemService mdmItemService; |
| | | @Resource |
| | | private MesWmBatchService batchService; |
| | | @Resource |
| | |
| | | @Resource |
| | | @Lazy |
| | | private MesWmSalesNoticeLineService salesNoticeLineService; |
| | | @Resource |
| | | @Lazy |
| | | private MesQcOqcService oqcService; |
| | | @Resource |
| | | private ErpSaleOrderApi saleOrderApi; |
| | | |
| | | @Override |
| | | public Long createProductSalesLine(MesWmProductSalesLineSaveReqVO createReqVO) { |
| | | // 校验数据 |
| | | validateLineSaveData(createReqVO); |
| | | MesWmSalesNoticeLineDO noticeLine = validateLineSaveData(createReqVO); |
| | | |
| | | // 新增 |
| | | MesWmProductSalesLineDO line = BeanUtils.toBean(createReqVO, MesWmProductSalesLineDO.class); |
| | | if (noticeLine != null) { |
| | | line.setSaleOrderItemId(noticeLine.getSaleOrderItemId()); |
| | | } |
| | | fillProductSalesLinePrice(line); |
| | | productSalesLineMapper.insert(line); |
| | | return line.getId(); |
| | | } |
| | | |
| | | /** |
| | | * 按销售订单行单价回填出库行金额,供开票/对账使用 |
| | | */ |
| | | private void fillProductSalesLinePrice(MesWmProductSalesLineDO line) { |
| | | if (line.getSaleOrderItemId() == null) { |
| | | return; |
| | | } |
| | | try { |
| | | var item = saleOrderApi.getSaleOrderItem(line.getSaleOrderItemId()).getData(); |
| | | if (item == null || item.getProductPrice() == null) { |
| | | return; |
| | | } |
| | | line.setPrice(item.getProductPrice()); |
| | | if (line.getQuantity() != null) { |
| | | line.setTotalPrice(item.getProductPrice().multiply(line.getQuantity())); |
| | | } |
| | | } catch (Exception ex) { |
| | | // 价格只是快照,订单行取不到时不阻断开单 |
| | | log.warn("[fillProductSalesLinePrice][销售订单行({}) 价格快照失败]", line.getSaleOrderItemId(), ex); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | |
| | | MesWmProductSalesLineDO oldLine = validateProductSalesLineExists(updateReqVO.getId()); |
| | | // 校验数据 |
| | | updateReqVO.setSalesId(oldLine.getSalesId()); |
| | | validateLineSaveData(updateReqVO); |
| | | MesWmSalesNoticeLineDO noticeLine = validateLineSaveData(updateReqVO); |
| | | |
| | | // 更新 |
| | | MesWmProductSalesLineDO updateObj = BeanUtils.toBean(updateReqVO, MesWmProductSalesLineDO.class); |
| | | if (noticeLine != null) { |
| | | updateObj.setSaleOrderItemId(noticeLine.getSaleOrderItemId()); |
| | | } |
| | | fillProductSalesLinePrice(updateObj); |
| | | productSalesLineMapper.updateById(updateObj); |
| | | |
| | | // 提交出库单后由质检人员选择已完成的出厂检验单。 |
| | | } |
| | | |
| | | @Override |
| | |
| | | /** |
| | | * 校验保存时的关联数据 |
| | | */ |
| | | private void validateLineSaveData(MesWmProductSalesLineSaveReqVO reqVO) { |
| | | private MesWmSalesNoticeLineDO validateLineSaveData(MesWmProductSalesLineSaveReqVO reqVO) { |
| | | // 校验主单存在且为草稿状态 |
| | | MesWmProductSalesDO sales = productSalesService.validateProductSalesExistsAndDraft(reqVO.getSalesId()); |
| | | // 校验物料存在 |
| | | itemService.validateItemExistsAndEnable(reqVO.getItemId()); |
| | | mdmItemService.validateItemExistsAndEnable(reqVO.getItemId()); |
| | | // 校验关联发货通知单行(存在性 + 归属 + 字段一致性) |
| | | validateSalesNoticeLine(sales, reqVO); |
| | | MesWmSalesNoticeLineDO noticeLine = validateSalesNoticeLine(sales, reqVO); |
| | | if (noticeLine == null && sales.getSaleOrderId() != null) { |
| | | if (reqVO.getSaleOrderItemId() == null) { |
| | | throw exception(WM_SALES_NOTICE_SALE_ORDER_ITEM_EMPTY); |
| | | } |
| | | var item = saleOrderApi.getSaleOrderItem(reqVO.getSaleOrderItemId()).getCheckedData(); |
| | | if (item == null || !Objects.equals(item.getOrderId(), sales.getSaleOrderId()) |
| | | || !Objects.equals(item.getProductId(), reqVO.getItemId())) { |
| | | throw exception(WM_SALES_NOTICE_LINE_SALE_ITEM_MISMATCH); |
| | | } |
| | | } |
| | | // 根据 batchCode 解析 batchId |
| | | fillBatchId(reqVO); |
| | | return noticeLine; |
| | | } |
| | | |
| | | /** |
| | |
| | | * @param sales 出库单 |
| | | * @param reqVO 出库行请求 |
| | | */ |
| | | private void validateSalesNoticeLine(MesWmProductSalesDO sales, MesWmProductSalesLineSaveReqVO reqVO) { |
| | | private MesWmSalesNoticeLineDO validateSalesNoticeLine(MesWmProductSalesDO sales, MesWmProductSalesLineSaveReqVO reqVO) { |
| | | Long noticeLineId = reqVO.getNoticeLineId(); |
| | | // 情况一:如果出库单关联了发货通知单,则必须关联发货通知单行 |
| | | if (sales.getNoticeId() != null) { |
| | |
| | | } |
| | | MesWmSalesNoticeLineDO noticeLine = salesNoticeLineService.validateSalesNoticeLineExists( |
| | | noticeLineId, sales.getNoticeId()); |
| | | // 校验关键字段一致性:物料、数量、批次号、OQC 检验标识 |
| | | // 校验关键字段一致性:物料、数量、批次号 |
| | | // 注:OQC 检验标识允许在出库时独立设置,不强制与通知单一致 |
| | | if (ObjUtil.notEqual(reqVO.getItemId(), noticeLine.getItemId())) { |
| | | throw exception(WM_PRODUCT_SALES_LINE_NOTICE_LINE_ITEM_MISMATCH); |
| | | } |
| | |
| | | && ObjUtil.notEqual(reqVO.getBatchCode(), noticeLine.getBatchCode())) { |
| | | throw exception(WM_PRODUCT_SALES_LINE_NOTICE_LINE_BATCH_MISMATCH); |
| | | } |
| | | if (noticeLine.getOqcCheckFlag() != null |
| | | && ObjUtil.notEqual(reqVO.getOqcCheckFlag(), noticeLine.getOqcCheckFlag())) { |
| | | throw exception(WM_PRODUCT_SALES_LINE_NOTICE_LINE_OQC_MISMATCH); |
| | | } |
| | | return; |
| | | return noticeLine; |
| | | } |
| | | |
| | | // 情况二:如果出库单没有关联发货通知单,则不允许关联发货通知单行 |
| | | if (noticeLineId != null) { |
| | | throw exception(WM_PRODUCT_SALES_LINE_SALES_NOTICE_LINE_NOT_ALLOWED); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void updateProductSalesLineWhenOqcFinish(Long id, Long oqcId, Integer checkResult) { |
| | | // 1. 更新行的 oqcId + qualityStatus |
| | | Integer qualityStatus = Objects.equals(checkResult, MesQcCheckResultEnum.PASS.getType()) |
| | | Integer qualityStatus = Objects.equals(checkResult, MesQcCheckResultEnum.QUALIFIED.getType()) |
| | | ? MesWmQualityStatusEnum.PASS.getStatus() : MesWmQualityStatusEnum.FAIL.getStatus(); |
| | | productSalesLineMapper.updateById(new MesWmProductSalesLineDO() |
| | | .setId(id).setOqcId(oqcId).setQualityStatus(qualityStatus)); |
| | |
| | | Long salesId = currentLine.getSalesId(); |
| | | List<MesWmProductSalesLineDO> allLines = productSalesLineMapper.selectListBySalesId(salesId); |
| | | |
| | | // 2.1 检查是否有不合格行 → 取消出库单 |
| | | // 不合格行留待后续处置,不能因为检验结果自动取消整张出库单。 |
| | | boolean hasNg = CollectionUtils.anyMatch(allLines, |
| | | line -> Boolean.TRUE.equals(line.getOqcCheckFlag()) |
| | | && Objects.equals(line.getQualityStatus(), MesWmQualityStatusEnum.FAIL.getStatus())); |
| | | if (hasNg) { |
| | | log.info("[updateProductSalesLineWhenOqcFinish][销售出库单({}) 存在质检不合格行,取消出库单]", salesId); |
| | | productSalesService.cancelProductSales(salesId); |
| | | log.info("[updateProductSalesLineWhenOqcFinish][销售出库单({}) 存在质检不合格行,等待后续处置]", salesId); |
| | | return; |
| | | } |
| | | |
| | | // 2.2 检查是否所有 OQC 行都已完成,自动流转 CONFIRMED → APPROVING |
| | | boolean hasUnchecked = CollectionUtils.anyMatch(allLines, |
| | | line -> Boolean.TRUE.equals(line.getOqcCheckFlag()) && line.getOqcId() == null); |
| | | line -> Boolean.TRUE.equals(line.getOqcCheckFlag()) && !Objects.equals(line.getQualityStatus(), MesWmQualityStatusEnum.PASS.getStatus())); |
| | | if (!hasUnchecked) { |
| | | log.info("[updateProductSalesLineWhenOqcFinish][销售出库单({}) 所有 OQC 行检验完成,流转到待拣货]", salesId); |
| | | productSalesService.confirmProductSales(salesId); |