11 小时以前 55d694aaae4e421b5e55cd941500e08f85cb5d4d
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java
@@ -24,6 +24,10 @@
import com.ruoyi.production.pojo.ProductionPlan;
import com.ruoyi.production.service.ProductionOrderService;
import com.ruoyi.production.service.ProductionPlanService;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@@ -50,6 +54,8 @@
    private final ProductionOrderService productionOrderService;
    private final ProductModelMapper productModelMapper;
    private final ProductMapper productMapper;
    private final SalesLedgerMapper salesLedgerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    @Override
    public IPage<ProductionPlanVo> listPage(Page<ProductionPlanDto> page, ProductionPlanDto productionPlanDto) {
@@ -61,9 +67,8 @@
     * 合并生产计划并下发生产订单。
     * 约束:
     * 1. 仅允许同一产品型号的计划合并;
     * 2. 已下发或部分下发的计划不允许再次合并;
     * 3. 下发数量不能大于所选计划剩余需求总量;
     * 4. 下发时统一调用 ProductionOrderService.saveProductionOrder,确保后续工艺/BOM/领料逻辑一致。
     * 2. 下发数量只受输入数量和基础数据校验约束;
     * 3. 下发时统一调用 ProductionOrderService.saveProductionOrder,确保后续工艺/BOM/领料逻辑一致。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -101,49 +106,18 @@
            throw new BaseException("合并失败,所选生产计划的产品型号不一致");
        }
        // 仅“已下发”计划不允许再次参与合并下发;
        // “待下发/部分下发”允许继续下发剩余数量。
        boolean hasFullyIssuedPlan = planLists.stream()
                .anyMatch(item -> item.getStatus() != null
                        && item.getStatus() == PLAN_STATUS_ISSUED);
        if (hasFullyIssuedPlan) {
            throw new BaseException("合并失败,所选生产计划存在已下发的数据");
        }
        // 计算本次可下发的剩余需求总量
        BigDecimal totalRequiredQuantity = planLists.stream()
                .map(this::resolveRemainingQuantity)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        if (totalRequiredQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("下发失败,所选生产计划剩余需求总量必须大于0");
        }
        // 校验下发数量
        BigDecimal assignedQuantity = productionPlanDto.getTotalAssignedQuantity();
        if (assignedQuantity == null || assignedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("下发失败,下发数量必须大于0");
        }
        if (assignedQuantity.compareTo(totalRequiredQuantity) > 0) {
            throw new ServiceException("下发失败,下发数量不能大于计划剩余需求总量");
        }
        // 按计划顺序分摊下发数量,收集实际参与下发的计划 ID
        BigDecimal remainingForOrderBind = assignedQuantity;
        List<Long> issuedPlanIds = new ArrayList<>();
        for (ProductionPlanDto plan : planLists) {
            BigDecimal remainingQuantity = resolveRemainingQuantity(plan);
            if (remainingForOrderBind.compareTo(BigDecimal.ZERO) <= 0 || remainingQuantity.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal issueForThisPlan = remainingForOrderBind.min(remainingQuantity);
            remainingForOrderBind = remainingForOrderBind.subtract(issueForThisPlan);
            if (issueForThisPlan.compareTo(BigDecimal.ZERO) > 0) {
                issuedPlanIds.add(plan.getId());
            }
        }
        if (issuedPlanIds.isEmpty()) {
            throw new ServiceException("下发失败,无可下发数量");
        }
        // 本次下发数量不再按需求量限制,所有选中计划共同作为订单来源
        List<Long> issuedPlanIds = planLists.stream()
                .map(ProductionPlanDto::getId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        // 生成生产订单主单,并绑定本次下发关联的计划
        ProductionOrder productionOrder = new ProductionOrder();
@@ -155,24 +129,18 @@
            throw new ServiceException("下发失败,生产订单保存失败");
        }
        // 回写每条计划的累计下发量和状态
        // 回写每条计划的累计下发量和状态,允许累计下发量超过需求量
        BigDecimal remainingAssignedQuantity = assignedQuantity;
        List<ProductionPlan> updates = new ArrayList<>();
        for (ProductionPlanDto plan : planLists) {
            BigDecimal requiredQuantity = Optional.ofNullable(plan.getQtyRequired()).orElse(BigDecimal.ZERO);
            if (requiredQuantity.compareTo(BigDecimal.ZERO) < 0) {
                requiredQuantity = BigDecimal.ZERO;
            }
            BigDecimal remainingQuantity = resolveRemainingQuantity(plan);
            BigDecimal historicalIssuedQuantity = requiredQuantity.subtract(remainingQuantity);
            BigDecimal issuedQuantity = BigDecimal.ZERO;
            if (remainingAssignedQuantity.compareTo(BigDecimal.ZERO) > 0 && remainingQuantity.compareTo(BigDecimal.ZERO) > 0) {
                issuedQuantity = remainingAssignedQuantity.min(remainingQuantity);
                remainingAssignedQuantity = remainingAssignedQuantity.subtract(issuedQuantity);
            }
            BigDecimal historicalIssuedQuantity = Optional.ofNullable(plan.getQuantityIssued()).orElse(BigDecimal.ZERO);
            BigDecimal issuedQuantity = remainingAssignedQuantity.min(assignedQuantity);
            remainingAssignedQuantity = remainingAssignedQuantity.subtract(issuedQuantity);
            BigDecimal totalIssuedQuantity = historicalIssuedQuantity.add(issuedQuantity);
            int planStatus = resolvePlanStatus(requiredQuantity, totalIssuedQuantity);
            int planStatus = plan.getStatus() != null && plan.getStatus() == PLAN_STATUS_ISSUED
                    ? PLAN_STATUS_ISSUED
                    : resolvePlanStatus(plan.getQtyRequired(), totalIssuedQuantity);
            ProductionPlan update = new ProductionPlan();
            update.setId(plan.getId());
            update.setStatus(planStatus);
@@ -189,6 +157,7 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean add(ProductionPlanDto dto) {
        validateSalesSource(dto);
        // 新增主生产计划
        if (StringUtils.isBlank(dto.getMpsNo())) {
            String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
@@ -211,6 +180,7 @@
            throw new ServiceException("编辑失败,数据不能为空");
        }
        validateSalesSource(dto);
        ProductionPlan old = getById(dto.getId());
        if (old == null) {
            throw new ServiceException("编辑失败,生产计划不存在");
@@ -359,6 +329,32 @@
        util.exportExcel(response, exportList, "主生产计划");
    }
    private void validateSalesSource(ProductionPlanDto dto) {
        if (dto == null) {
            throw new ServiceException("生产计划不能为空");
        }
        if (dto.getSalesLedgerId() == null && dto.getSalesLedgerProductId() == null) {
            return;
        }
        if (dto.getSalesLedgerId() == null || dto.getSalesLedgerProductId() == null) {
            throw new ServiceException("销售台账和销售产品必须同时填写");
        }
        SalesLedgerProduct product = salesLedgerProductMapper.selectById(dto.getSalesLedgerProductId());
        if (product == null || !Objects.equals(product.getSalesLedgerId(), dto.getSalesLedgerId())) {
            throw new ServiceException("销售产品与销售台账不匹配");
        }
        SalesLedger ledger = salesLedgerMapper.selectById(dto.getSalesLedgerId());
        if (ledger == null || (!Objects.equals(ledger.getContractType(), 1)
                && !Objects.equals(ledger.getContractType(), 3))) {
            throw new ServiceException("生产计划只能关联普通销售产品或框架订单产品");
        }
        if (dto.getProductModelId() == null) {
            dto.setProductModelId(product.getProductModelId());
        } else if (!Objects.equals(dto.getProductModelId(), product.getProductModelId())) {
            throw new ServiceException("生产计划产品型号与销售产品不一致");
        }
    }
    /**
     * 校验主生产计划号唯一性,可通过 excludeId 排除当前记录。
     */
@@ -455,32 +451,6 @@
            }
        }
        return sequence;
    }
    /**
     * 计算生产计划的剩余未下发数量(需求量 - 已下发量,最小为 0)。
     */
    private BigDecimal resolveRemainingQuantity(ProductionPlan plan) {
        // 空对象按 0 处理
        if (plan == null) {
            return BigDecimal.ZERO;
        }
        // 需求量为空或小于等于 0,视为无剩余
        BigDecimal requiredQuantity = Optional.ofNullable(plan.getQtyRequired()).orElse(BigDecimal.ZERO);
        if (requiredQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        // 已下发量为空或小于等于 0,剩余即需求量
        BigDecimal issuedQuantity = Optional.ofNullable(plan.getQuantityIssued()).orElse(BigDecimal.ZERO);
        if (issuedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return requiredQuantity;
        }
        // 已下发量大于等于需求量,剩余归零
        if (issuedQuantity.compareTo(requiredQuantity) >= 0) {
            return BigDecimal.ZERO;
        }
        // 正常场景返回差值
        return requiredQuantity.subtract(issuedQuantity);
    }
    /**