| | |
| | | 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; |
| | |
| | | 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) { |
| | |
| | | * 合并生产计划并下发生产订单。 |
| | | * 约束: |
| | | * 1. 仅允许同一产品型号的计划合并; |
| | | * 2. 已下发或部分下发的计划不允许再次合并; |
| | | * 3. 下发数量不能大于所选计划剩余需求总量; |
| | | * 4. 下发时统一调用 ProductionOrderService.saveProductionOrder,确保后续工艺/BOM/领料逻辑一致。 |
| | | * 2. 下发数量只受输入数量和基础数据校验约束; |
| | | * 3. 下发时统一调用 ProductionOrderService.saveProductionOrder,确保后续工艺/BOM/领料逻辑一致。 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | 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(); |
| | |
| | | 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); |
| | |
| | | @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")); |
| | |
| | | throw new ServiceException("编辑失败,数据不能为空"); |
| | | } |
| | | |
| | | validateSalesSource(dto); |
| | | ProductionPlan old = getById(dto.getId()); |
| | | if (old == null) { |
| | | throw new ServiceException("编辑失败,生产计划不存在"); |
| | |
| | | BeanUtils.copyProperties(dto, entity); |
| | | entity.setProductModelId(resolveProductModelId(dto, i + 2, allModels, productNameById)); |
| | | entity.setStatus(PLAN_STATUS_WAIT); |
| | | entity.setSource(StringUtils.isNotEmpty(dto.getSource()) ? StringUtils.trim(dto.getSource()) : "内部"); |
| | | entity.setSource("内部"); |
| | | entity.setQuantityIssued(BigDecimal.ZERO); |
| | | entity.setCreateTime(now); |
| | | entity.setUpdateTime(now); |
| | |
| | | |
| | | ExcelUtil<ProductionPlanImportDto> util = new ExcelUtil<>(ProductionPlanImportDto.class); |
| | | 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("生产计划产品型号与销售产品不一致"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | } |
| | | 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); |
| | | } |
| | | |
| | | /** |