云
5 天以前 cccbfadcb6e96c23a33c40c0687a27c2f6efa1db
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/returnvendor/MesWmReturnVendorServiceImpl.java
@@ -8,6 +8,7 @@
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnvendor.vo.MesWmReturnVendorPageReqVO;
@@ -32,8 +33,7 @@
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import jakarta.annotation.Resource;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
@@ -55,12 +55,14 @@
 */
@Service
@Validated
@Slf4j
public class MesWmReturnVendorServiceImpl implements MesWmReturnVendorService {
    /**
     * 附件业务类型:MES 供应商退货单
     */
    public static final String RETURN_VENDOR_RECORD_TYPE = "mes_wm_return_vendor";
    private static final String RETURN_VENDOR_APPROVAL_KEY = "purchase_return_vendor_approve";
    @Resource
    private MesWmReturnVendorMapper returnVendorMapper;
@@ -184,26 +186,29 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitReturnVendor(Long id) {
        // 校验存在 + 草稿状态
        validateReturnVendorExistsAndPrepare(id);
        // 校验至少有一条行
        List<MesWmReturnVendorLineDO> lines = returnVendorLineService.getReturnVendorLineListByReturnId(id);
        if (CollUtil.isEmpty(lines)) {
            throw exception(WM_RETURN_VENDOR_NO_LINE);
        }
        // 提交(草稿 -> 待拣货)
        returnVendorMapper.updateById(new MesWmReturnVendorDO()
                .setId(id).setStatus(MesWmReturnVendorStatusEnum.APPROVING.getStatus()));
        submitReturnVendorApproval(id, RETURN_VENDOR_APPROVAL_KEY);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitReturnVendorApproval(Long id, String processDefinitionKey) {
        if (!RETURN_VENDOR_APPROVAL_KEY.equals(processDefinitionKey)) {
            throw exception(WM_RETURN_VENDOR_APPROVAL_PROCESS_INVALID, RETURN_VENDOR_APPROVAL_KEY);
        }
        // 1. 校验存在 + 草稿状态 + 至少一条行
        validateReturnVendorExistsAndPrepare(id);
        MesWmReturnVendorDO returnVendor = validateReturnVendorExistsAndPrepare(id);
        if (CollUtil.isEmpty(returnVendorLineService.getReturnVendorLineListByReturnId(id))) {
            throw exception(WM_RETURN_VENDOR_NO_LINE);
        }
        validateReturnVendorOrderQuantity(returnVendor);
        // 拣货可在审批通过后进行;如已提前录入明细,则提交前必须数量守恒。
        if (CollUtil.isNotEmpty(returnVendorDetailService.getReturnVendorDetailListByReturnId(id))
                && !checkReturnVendorQuantity(id)) {
            throw exception(WM_RETURN_VENDOR_DETAIL_QUANTITY_MISMATCH);
        }
        // 1.1. 质量部发起退货必须上传附件(质检报告、不合格照片等),否则审批人无法判断退货依据
        if (CollUtil.isEmpty(storageAttachmentApi.listAttachments(RETURN_VENDOR_RECORD_TYPE, id))) {
            throw exception(WM_RETURN_VENDOR_ATTACHMENT_REQUIRED, returnVendor.getCode());
        }
        // 2. 全局免审批:直接置为「待拣货」,无需流程模型
@@ -279,6 +284,11 @@
        if (ObjUtil.notEqual(MesWmReturnVendorStatusEnum.APPROVING.getStatus(), returnVendor.getStatus())) {
            throw exception(WM_RETURN_VENDOR_STATUS_INVALID);
        }
        validateReturnVendorApproval(returnVendor);
        if (!checkReturnVendorQuantity(id)) {
            throw exception(WM_RETURN_VENDOR_DETAIL_QUANTITY_MISMATCH);
        }
        validateReturnVendorOrderQuantity(returnVendor);
        // 执行拣货(待拣货 -> 待执行退货)
        returnVendorMapper.updateById(new MesWmReturnVendorDO()
                .setId(id).setStatus(MesWmReturnVendorStatusEnum.APPROVED.getStatus()));
@@ -292,11 +302,16 @@
        if (ObjUtil.notEqual(MesWmReturnVendorStatusEnum.APPROVED.getStatus(), returnVendor.getStatus())) {
            throw exception(WM_RETURN_VENDOR_STATUS_INVALID);
        }
        validateReturnVendorApproval(returnVendor);
        // 1.2 校验明细不为空
        List<MesWmReturnVendorDetailDO> details = returnVendorDetailService.getReturnVendorDetailListByReturnId(id);
        if (CollUtil.isEmpty(details)) {
            throw exception(WM_RETURN_VENDOR_NO_DETAIL);
        }
        if (!checkReturnVendorQuantity(id)) {
            throw exception(WM_RETURN_VENDOR_DETAIL_QUANTITY_MISMATCH);
        }
        validateReturnVendorOrderQuantity(returnVendor);
        // 1.3 校验处理方式(退款 / 换货)
        if (!MesWmReturnVendorHandleTypeEnum.isValid(returnVendor.getHandleType())) {
@@ -321,6 +336,13 @@
        }
    }
    private void validateReturnVendorApproval(MesWmReturnVendorDO returnVendor) {
        if (!RETURN_VENDOR_APPROVAL_KEY.equals(returnVendor.getProcessDefinitionKey())
                || returnVendor.getProcessInstanceId() == null) {
            throw exception(WM_RETURN_VENDOR_APPROVAL_REQUIRED, returnVendor.getCode());
        }
    }
    /**
     * 把退货数量增量回写到关联采购订单的明细行,并同步订单级退货数量
     */
@@ -338,15 +360,58 @@
            return;
        }
        Map<Long, Long> purchaseOrderItemIdMap = new HashMap<>();
        Set<Long> ambiguousProductIds = new HashSet<>();
        orderItems.stream()
                .filter(item -> item.getProductId() != null)
                .forEach(item -> purchaseOrderItemIdMap.putIfAbsent(item.getProductId(), item.getId()));
                .forEach(item -> {
                    if (purchaseOrderItemIdMap.putIfAbsent(item.getProductId(), item.getId()) != null) {
                        ambiguousProductIds.add(item.getProductId());
                    }
                });
        for (MesWmReturnVendorLineDO line : lines) {
            if (ambiguousProductIds.contains(line.getItemId())) {
                throw exception(WM_RETURN_VENDOR_PURCHASE_ITEM_AMBIGUOUS, line.getItemId());
            }
            Long purchaseOrderItemId = purchaseOrderItemIdMap.get(line.getItemId());
            if (purchaseOrderItemId == null || line.getQuantity() == null) {
                continue;
            }
            purchaseOrderApi.updateOrderItemReturnCount(purchaseOrderItemId, line.getQuantity());
            if (!Boolean.TRUE.equals(purchaseOrderApi.updateOrderItemReturnCount(purchaseOrderItemId, line.getQuantity()).getCheckedData())) {
                throw exception(WM_RETURN_VENDOR_ITEM_NOT_IN_PURCHASE_ORDER);
            }
        }
    }
    private void validateReturnVendorOrderQuantity(MesWmReturnVendorDO returnVendor) {
        if (returnVendor.getPurchaseOrderId() == null) {
            return;
        }
        List<ErpPurchaseOrderItemRespDTO> items = purchaseOrderApi
                .getPurchaseOrderItemList(returnVendor.getPurchaseOrderId()).getCheckedData();
        Map<Long, ErpPurchaseOrderItemRespDTO> byProduct = new HashMap<>();
        Set<Long> ambiguousProductIds = new HashSet<>();
        for (ErpPurchaseOrderItemRespDTO item : items) {
            if (item.getProductId() != null && byProduct.putIfAbsent(item.getProductId(), item) != null) {
                ambiguousProductIds.add(item.getProductId());
            }
        }
        Map<Long, BigDecimal> requested = new HashMap<>();
        for (MesWmReturnVendorLineDO line : returnVendorLineService.getReturnVendorLineListByReturnId(returnVendor.getId())) {
            requested.merge(line.getItemId(), line.getQuantity(), BigDecimal::add);
        }
        for (Map.Entry<Long, BigDecimal> entry : requested.entrySet()) {
            if (ambiguousProductIds.contains(entry.getKey())) {
                throw exception(WM_RETURN_VENDOR_PURCHASE_ITEM_AMBIGUOUS, entry.getKey());
            }
            ErpPurchaseOrderItemRespDTO item = byProduct.get(entry.getKey());
            if (item == null) {
                throw exception(WM_RETURN_VENDOR_ITEM_NOT_IN_PURCHASE_ORDER);
            }
            BigDecimal available = (item.getInCount() == null ? BigDecimal.ZERO : item.getInCount())
                    .subtract(item.getReturnCount() == null ? BigDecimal.ZERO : item.getReturnCount());
            if (entry.getValue().compareTo(available) > 0) {
                throw exception(WM_RETURN_VENDOR_QUANTITY_EXCEED, entry.getKey(), entry.getValue(), available);
            }
        }
    }
@@ -471,16 +536,16 @@
     * 获取当前用户编号(BPM 流程发起人、财务人员)
     */
    private Long getCurrentUserId() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.getPrincipal() instanceof Long) {
            return (Long) authentication.getPrincipal();
        }
        return null;
        // 统一从安全框架取登录用户;Spring Security 的 principal 是 LoginUser 而非 Long,
        // 直接强转会取到 null,导致 BPM 流程实例的发起人为字符串 "null"
        return SecurityFrameworkUtils.getLoginUserId();
    }
    private void createTransactionList(MesWmReturnVendorDO returnVendor, List<MesWmReturnVendorDetailDO> details) {
        wmTransactionService.createTransactionList(convertList(details, detail -> new MesWmTransactionSaveReqDTO()
                .setType(MesWmTransactionTypeEnum.OUT.getType()).setItemId(detail.getItemId())
                // 按拣货时选中的库存记录扣减:生产批次可能没有 vendor_id,仅靠 物料+库位+批次+供应商 组合键会找不到库存
                .setMaterialStockId(detail.getMaterialStockId())
                .setQuantity(detail.getQuantity().negate()) // 出库数量为负数
                .setBatchId(detail.getBatchId()).setBatchCode(detail.getBatchCode())
                .setWarehouseId(detail.getWarehouseId()).setLocationId(detail.getLocationId()).setAreaId(detail.getAreaId())
@@ -499,6 +564,15 @@
                MesWmReturnVendorStatusEnum.FINISHED.getStatus(),
                MesWmReturnVendorStatusEnum.CANCELED.getStatus())) {
            throw exception(WM_RETURN_VENDOR_CANCEL_NOT_ALLOWED);
        }
        // 审批中先撤销 BPM 实例,撤销失败时保留单据原状态和待办。
        if (MesWmReturnVendorStatusEnum.BPM_APPROVING.getStatus().equals(returnVendor.getStatus())) {
            if (StrUtil.isBlank(returnVendor.getProcessInstanceId())) {
                throw exception(WM_RETURN_VENDOR_APPROVAL_REQUIRED, returnVendor.getCode());
            }
            processInstanceApi.cancelProcessInstanceByStartUser(getCurrentUserId(),
                    returnVendor.getProcessInstanceId(), "供应商退货单取消");
        }
        // 取消
@@ -581,8 +655,11 @@
        }
        try {
            importPurchaseOrderItems(id);
        } catch (ServiceException ignore) {
            // 采购订单无明细 / 无可退物料等业务异常:单据主体仍正常保存,可在物料区另行处理
        } catch (ServiceException ex) {
            // 采购订单无明细 / 无可退物料等业务异常:单据主体仍正常保存,可在物料区点「重新带入订单物料」补带
            // 打日志便于排查(例如:保存后才入库,此时订单还没有可退数量)
            log.warn("[autoImportLinesFromLinkedOrder][退货单({}) 关联采购订单({}) 自动带入物料失败:{}]",
                    id, returnVendor.getPurchaseOrderId(), ex.getMessage());
        }
    }