| | |
| | | import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.ruoyi.basic.mapper.ProductMapper; |
| | | import com.ruoyi.basic.mapper.ProductModelMapper; |
| | | import com.ruoyi.basic.pojo.Product; |
| | | import com.ruoyi.basic.pojo.ProductModel; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | import com.ruoyi.common.exception.base.BaseException; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | |
| | | private final ProductionPlanMapper productionPlanMapper; |
| | | private final ProductionOrderMapper productionOrderMapper; |
| | | private final ProductionOrderService productionOrderService; |
| | | private final ProductModelMapper productModelMapper; |
| | | private final ProductMapper productMapper; |
| | | |
| | | @Override |
| | | public IPage<ProductionPlanVo> listPage(Page<ProductionPlanDto> page, ProductionPlanDto productionPlanDto) { |
| | | // 分页查询主生产计划列表 |
| | | return productionPlanMapper.listPage(page, productionPlanDto); |
| | | } |
| | | |
| | | /** |
| | | * 合并生产计划并下发生产订单。 |
| | | * 业务约束: |
| | | * 约束: |
| | | * 1. 仅允许同一产品型号的计划合并; |
| | | * 2. 已下发或部分下发的计划禁止再次合并; |
| | | * 3. 下发数量不能大于所选计划需求总量; |
| | | * 4. 订单创建统一复用 ProductionOrderService.saveProductionOrder,确保工艺/BOM/领料主单等后续逻辑一致。 |
| | | * 2. 已下发或部分下发的计划不允许再次合并; |
| | | * 3. 下发数量不能大于所选计划剩余需求总量; |
| | | * 4. 下发时统一调用 ProductionOrderService.saveProductionOrder,确保后续工艺/BOM/领料逻辑一致。 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public boolean combine(ProductionPlanDto productionPlanDto) { |
| | | // 基础入参校验:没有可下发计划则直接返回 false |
| | | if (productionPlanDto == null || productionPlanDto.getIds() == null || productionPlanDto.getIds().isEmpty()) { |
| | | return false; |
| | | } |
| | | |
| | | // 去空、去重,得到本次参与合并的计划 ID |
| | | List<Long> planIds = productionPlanDto.getIds().stream() |
| | | .filter(Objects::nonNull) |
| | | .distinct() |
| | |
| | | throw new ServiceException("下发失败,未选择生产计划"); |
| | | } |
| | | |
| | | List<ProductionPlanDto> plans = productionPlanMapper.selectWithMaterialByIds(planIds); |
| | | if (plans == null || plans.isEmpty() || plans.size() != planIds.size()) { |
| | | // 查询并校验计划是否都存在 |
| | | List<ProductionPlanDto> planLists = productionPlanMapper.selectWithMaterialByIds(planIds); |
| | | if (planLists == null || planLists.isEmpty() || planLists.size() != planIds.size()) { |
| | | throw new ServiceException("下发失败,生产计划不存在或已被删除"); |
| | | } |
| | | |
| | | ProductionPlanDto firstPlan = plans.getFirst(); |
| | | // 以第一条计划作为型号基准 |
| | | ProductionPlanDto firstPlan = planLists.getFirst(); |
| | | if (firstPlan.getProductModelId() == null) { |
| | | throw new ServiceException("下发失败,生产计划缺少产品型号"); |
| | | } |
| | | |
| | | boolean hasDifferentModel = plans.stream() |
| | | // 仅允许同型号计划合并下发 |
| | | boolean hasDifferentModel = planLists.stream() |
| | | .anyMatch(item -> !Objects.equals(item.getProductModelId(), firstPlan.getProductModelId())); |
| | | if (hasDifferentModel) { |
| | | throw new BaseException("合并失败,所选生产计划的产品型号不一致"); |
| | | } |
| | | |
| | | boolean hasIssuedPlan = plans.stream() |
| | | .anyMatch(item -> item.getStatus() != null && item.getStatus() != PLAN_STATUS_WAIT); |
| | | if (hasIssuedPlan) { |
| | | throw new BaseException("合并失败,所选生产计划存在已下发或部分下发数据"); |
| | | // 仅“已下发”计划不允许再次参与合并下发; |
| | | // “待下发/部分下发”允许继续下发剩余数量。 |
| | | boolean hasFullyIssuedPlan = planLists.stream() |
| | | .anyMatch(item -> item.getStatus() != null |
| | | && item.getStatus() == PLAN_STATUS_ISSUED); |
| | | if (hasFullyIssuedPlan) { |
| | | throw new BaseException("合并失败,所选生产计划存在已下发的数据"); |
| | | } |
| | | |
| | | BigDecimal totalRequiredQuantity = plans.stream() |
| | | .map(ProductionPlan::getQtyRequired) |
| | | .filter(Objects::nonNull) |
| | | // 计算本次可下发的剩余需求总量 |
| | | BigDecimal totalRequiredQuantity = planLists.stream() |
| | | .map(this::resolveRemainingQuantity) |
| | | .reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | if (totalRequiredQuantity.compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw new ServiceException("下发失败,所选生产计划需求总量必须大于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("下发失败,下发数量不能大于计划需求总量"); |
| | | 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("下发失败,无可下发数量"); |
| | | } |
| | | |
| | | // 生成生产订单主单,并绑定本次下发关联的计划 |
| | | ProductionOrder productionOrder = new ProductionOrder(); |
| | | productionOrder.setProductionPlanIds(formatPlanIds(planIds)); |
| | | productionOrder.setProductionPlanIds(formatPlanIds(issuedPlanIds)); |
| | | productionOrder.setProductModelId(firstPlan.getProductModelId()); |
| | | productionOrder.setQuantity(assignedQuantity); |
| | | productionOrder.setPlanCompleteTime(productionPlanDto.getPlanCompleteTime()); |
| | | |
| | | boolean saved = productionOrderService.saveProductionOrder(productionOrder); |
| | | if (!saved) { |
| | | if (!productionOrderService.saveProductionOrder(productionOrder)) { |
| | | throw new ServiceException("下发失败,生产订单保存失败"); |
| | | } |
| | | |
| | | int targetStatus = assignedQuantity.compareTo(totalRequiredQuantity) < 0 ? PLAN_STATUS_PARTIAL : PLAN_STATUS_ISSUED; |
| | | List<ProductionPlan> updates = planIds.stream().map(id -> { |
| | | // 回写每条计划的累计下发量和状态 |
| | | 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 totalIssuedQuantity = historicalIssuedQuantity.add(issuedQuantity); |
| | | int planStatus = resolvePlanStatus(requiredQuantity, totalIssuedQuantity); |
| | | ProductionPlan update = new ProductionPlan(); |
| | | update.setId(id); |
| | | update.setStatus(targetStatus); |
| | | return update; |
| | | }).collect(Collectors.toList()); |
| | | update.setId(plan.getId()); |
| | | update.setStatus(planStatus); |
| | | update.setQuantityIssued(totalIssuedQuantity); |
| | | updates.add(update); |
| | | } |
| | | if (!updates.isEmpty()) { |
| | | // 批量更新计划状态与数量 |
| | | this.updateBatchById(updates); |
| | | } |
| | | return true; |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public boolean add(ProductionPlanDto dto) { |
| | | // 新增主生产计划 |
| | | if (StringUtils.isBlank(dto.getMpsNo())) { |
| | | dto.setMpsNo(generateNextPlanNo(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")))); |
| | | }else checkMpsNoUnique(dto.getMpsNo(), null); |
| | | String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); |
| | | dto.setMpsNo(buildPlanNo(datePrefix, resolveNextPlanSequence(datePrefix))); |
| | | } else { |
| | | checkMpsNoUnique(dto.getMpsNo(), null); |
| | | } |
| | | dto.setStatus(PLAN_STATUS_WAIT); |
| | | dto.setSource("内部"); |
| | | if (StringUtils.isBlank(dto.getSource())) { |
| | | dto.setSource("内部"); |
| | | } |
| | | return productionPlanMapper.insert(dto) > 0; |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public boolean update(ProductionPlanDto dto) { |
| | | // 更新主生产计划 |
| | | if (dto == null || dto.getId() == null) { |
| | | throw new ServiceException("编辑失败,数据不能为空"); |
| | | } |
| | |
| | | return productionPlanMapper.updateById(dto) > 0; |
| | | } |
| | | |
| | | private void checkMpsNoUnique(String mpsNo, Long excludeId) { |
| | | LambdaQueryWrapper<ProductionPlan> wrapper = Wrappers.lambdaQuery(); |
| | | wrapper.eq(ProductionPlan::getMpsNo, mpsNo); |
| | | if (excludeId != null) { |
| | | wrapper.ne(ProductionPlan::getId, excludeId); |
| | | } |
| | | if (productionPlanMapper.selectCount(wrapper) > 0) { |
| | | throw new ServiceException("生产计划号 " + mpsNo + " 已存在"); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public boolean delete(List<Long> ids) { |
| | | // 删除主生产计划 |
| | | if (productionPlanMapper.selectList(Wrappers.<ProductionPlan>lambdaQuery().in(ProductionPlan::getId, ids)) |
| | | .stream() |
| | | .anyMatch(p -> p.getStatus() == PLAN_STATUS_PARTIAL || p.getStatus() == PLAN_STATUS_ISSUED)) { |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void importProdData(MultipartFile file) { |
| | | // 参数与前置条件校验 |
| | | if (file == null || file.isEmpty()) { |
| | | throw new ServiceException("导入数据不能为空"); |
| | | } |
| | |
| | | if (list == null || list.isEmpty()) { |
| | | throw new ServiceException("Excel没有数据"); |
| | | } |
| | | |
| | | String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); |
| | | int nextSequence = resolveNextPlanSequence(datePrefix); |
| | | Set<String> mpsNos = new HashSet<>(); |
| | | // 遍历处理数据并组装结果 |
| | | for (int i = 0; i < list.size(); i++) { |
| | | ProductionPlanImportDto dto = list.get(i); |
| | | String mpsNo = dto.getMpsNo(); |
| | | String mpsNo = StringUtils.trim(dto.getMpsNo()); |
| | | if (StringUtils.isEmpty(mpsNo)) { |
| | | generateNextPlanNo(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))); |
| | | mpsNo = buildPlanNo(datePrefix, nextSequence++); |
| | | } |
| | | dto.setMpsNo(mpsNo); |
| | | if (!mpsNos.add(mpsNo)) { |
| | | throw new ServiceException("导入失败:Excel 中存在重复的申请单编号 " + mpsNo); |
| | | throw new ServiceException("导入失败,Excel中存在重复的主生产计划号 " + mpsNo); |
| | | } |
| | | if (dto.getQtyRequired() == null || dto.getQtyRequired().compareTo(BigDecimal.ZERO) <= 0) { |
| | | throw new ServiceException("导入失败:第" + (i + 2) + "行需求数量必须大于0"); |
| | | } |
| | | } |
| | | Long existApplyNoCount = baseMapper.selectCount(Wrappers.<ProductionPlan>lambdaQuery() |
| | | .in(ProductionPlan::getMpsNo, mpsNos)); |
| | | |
| | | // 查询并准备业务数据 |
| | | Long existApplyNoCount = baseMapper.selectCount(Wrappers.<ProductionPlan>lambdaQuery().in(ProductionPlan::getMpsNo, mpsNos)); |
| | | if (existApplyNoCount > 0) { |
| | | List<String> existMpsNos = baseMapper.selectList(Wrappers.<ProductionPlan>lambdaQuery() |
| | | .in(ProductionPlan::getMpsNo, mpsNos)) |
| | | List<String> existMpsNos = baseMapper.selectList(Wrappers.<ProductionPlan>lambdaQuery().in(ProductionPlan::getMpsNo, mpsNos)) |
| | | .stream() |
| | | .map(ProductionPlan::getMpsNo) |
| | | .collect(Collectors.toList()); |
| | | throw new ServiceException("导入失败,生产计划号已存在: " + String.join(", ", existMpsNos)); |
| | | throw new ServiceException("导入失败,主生产计划号已存在: " + String.join(", ", existMpsNos)); |
| | | } |
| | | |
| | | List<ProductModel> allModels = productModelMapper.selectList(Wrappers.<ProductModel>lambdaQuery()); |
| | | Set<Long> productIds = allModels.stream() |
| | | .map(ProductModel::getProductId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toSet()); |
| | | Map<Long, String> productNameById = productIds.isEmpty() |
| | | ? Collections.emptyMap() |
| | | : productMapper.selectBatchIds(productIds).stream() |
| | | .collect(Collectors.toMap(Product::getId, Product::getProductName, (a, b) -> a)); |
| | | |
| | | LocalDateTime now = LocalDateTime.now(); |
| | | List<ProductionPlan> entityList = list.stream().map(dto -> { |
| | | List<ProductionPlan> entityList = new ArrayList<>(); |
| | | for (int i = 0; i < list.size(); i++) { |
| | | ProductionPlanImportDto dto = list.get(i); |
| | | ProductionPlan entity = new ProductionPlan(); |
| | | BeanUtils.copyProperties(dto, entity); |
| | | entity.setProductModelId(resolveProductModelId(dto, i + 2, allModels, productNameById)); |
| | | entity.setStatus(PLAN_STATUS_WAIT); |
| | | entity.setSource("内部"); |
| | | entity.setSource(StringUtils.isNotEmpty(dto.getSource()) ? StringUtils.trim(dto.getSource()) : "内部"); |
| | | entity.setQuantityIssued(BigDecimal.ZERO); |
| | | entity.setCreateTime(now); |
| | | entity.setUpdateTime(now); |
| | | return entity; |
| | | }).collect(Collectors.toList()); |
| | | this.saveBatch(entityList); |
| | | entityList.add(entity); |
| | | } |
| | | // 持久化或输出处理结果 |
| | | if (!this.saveBatch(entityList)) { |
| | | throw new ServiceException("导入失败,保存生产计划数据失败"); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void exportProdData(HttpServletResponse response, List<Long> ids) { |
| | | List<ProductionPlanDto> list = productionPlanMapper.selectWithMaterialByIds(ids); |
| | | public void exportProdData(HttpServletResponse response, ProductionPlanDto requestDto) { |
| | | // 导出主生产计划数据 |
| | | List<Long> ids = requestDto == null || requestDto.getIds() == null |
| | | ? Collections.emptyList() |
| | | : requestDto.getIds().stream().filter(Objects::nonNull).distinct().collect(Collectors.toList()); |
| | | |
| | | List<ProductionPlanImportDto> exportList = new ArrayList<>(); |
| | | for (ProductionPlanDto entity : list) { |
| | | ProductionPlanImportDto dto = new ProductionPlanImportDto(); |
| | | BeanUtils.copyProperties(entity, dto); |
| | | exportList.add(dto); |
| | | if (!ids.isEmpty()) { |
| | | List<ProductionPlanDto> list = productionPlanMapper.selectWithMaterialByIds(ids); |
| | | for (ProductionPlanDto item : list) { |
| | | ProductionPlanImportDto dto = new ProductionPlanImportDto(); |
| | | BeanUtils.copyProperties(item, dto); |
| | | dto.setAssignedQuantity(item.getQuantityIssued()); |
| | | exportList.add(dto); |
| | | } |
| | | } else { |
| | | ProductionPlanDto query = new ProductionPlanDto(); |
| | | if (requestDto != null) { |
| | | BeanUtils.copyProperties(requestDto, query); |
| | | } |
| | | IPage<ProductionPlanVo> page = productionPlanMapper.listPage(new Page<>(1, -1), query); |
| | | if (page != null && page.getRecords() != null) { |
| | | for (ProductionPlanVo item : page.getRecords()) { |
| | | ProductionPlanImportDto dto = new ProductionPlanImportDto(); |
| | | BeanUtils.copyProperties(item, dto); |
| | | dto.setAssignedQuantity(item.getQuantityIssued()); |
| | | exportList.add(dto); |
| | | } |
| | | } |
| | | } |
| | | |
| | | ExcelUtil<ProductionPlanImportDto> util = new ExcelUtil<>(ProductionPlanImportDto.class); |
| | | util.exportExcel(response, exportList, "主生产计划"); |
| | | } |
| | | |
| | | private String formatPlanIds(List<Long> planIds) { |
| | | return planIds.stream() |
| | | .filter(Objects::nonNull) |
| | | .distinct() |
| | | .map(String::valueOf) |
| | | .collect(Collectors.joining(",", "[", "]")); |
| | | /** |
| | | * 校验主生产计划号唯一性,可通过 excludeId 排除当前记录。 |
| | | */ |
| | | private void checkMpsNoUnique(String mpsNo, Long excludeId) { |
| | | // 按主生产计划号查询重复记录 |
| | | LambdaQueryWrapper<ProductionPlan> wrapper = Wrappers.lambdaQuery(); |
| | | wrapper.eq(ProductionPlan::getMpsNo, mpsNo); |
| | | if (excludeId != null) { |
| | | // 更新时排除当前记录本身 |
| | | wrapper.ne(ProductionPlan::getId, excludeId); |
| | | } |
| | | if (productionPlanMapper.selectCount(wrapper) > 0) { |
| | | // 存在重复计划号,直接拦截 |
| | | throw new ServiceException("生产计划号 " + mpsNo + " 已存在"); |
| | | } |
| | | } |
| | | |
| | | private String generateNextPlanNo(String datePrefix) { |
| | | /** |
| | | * 根据导入行的型号、产品名称、单位定位唯一的产品型号 ID。 |
| | | */ |
| | | private Long resolveProductModelId(ProductionPlanImportDto dto, int rowNo, List<ProductModel> allModels, |
| | | Map<Long, String> productNameById) { |
| | | // 先按规格型号做第一轮过滤 |
| | | String model = StringUtils.trim(dto.getModel()); |
| | | if (StringUtils.isEmpty(model)) { |
| | | throw new ServiceException("导入失败:第" + rowNo + "行规格型号不能为空"); |
| | | } |
| | | |
| | | List<ProductModel> candidates = allModels.stream() |
| | | .filter(item -> model.equals(StringUtils.trim(item.getModel()))) |
| | | .collect(Collectors.toList()); |
| | | if (candidates.isEmpty()) { |
| | | throw new ServiceException("导入失败:第" + rowNo + "行规格型号不存在,型号:" + model); |
| | | } |
| | | |
| | | // 若传了产品名称,再做第二轮过滤 |
| | | String productName = StringUtils.trim(dto.getProductName()); |
| | | if (StringUtils.isNotEmpty(productName)) { |
| | | candidates = candidates.stream() |
| | | .filter(item -> productName.equals(StringUtils.trim(productNameById.get(item.getProductId())))) |
| | | .collect(Collectors.toList()); |
| | | if (candidates.isEmpty()) { |
| | | throw new ServiceException("导入失败:第" + rowNo + "行产品名称与规格型号不匹配"); |
| | | } |
| | | } |
| | | |
| | | // 若传了单位,再做第三轮过滤 |
| | | String unit = StringUtils.trim(dto.getUnit()); |
| | | if (StringUtils.isNotEmpty(unit)) { |
| | | candidates = candidates.stream() |
| | | .filter(item -> unit.equals(StringUtils.trim(item.getUnit()))) |
| | | .collect(Collectors.toList()); |
| | | if (candidates.isEmpty()) { |
| | | throw new ServiceException("导入失败:第" + rowNo + "行单位与规格型号不匹配"); |
| | | } |
| | | } |
| | | |
| | | // 仍然多条说明信息不足以唯一定位 |
| | | if (candidates.size() > 1) { |
| | | throw new ServiceException("导入失败:第" + rowNo + "行规格型号匹配到多个产品,请补充产品名称或单位"); |
| | | } |
| | | return candidates.get(0).getId(); |
| | | } |
| | | |
| | | /** |
| | | * 生成主生产计划号,格式:JH + yyyyMMdd + 4位流水号。 |
| | | */ |
| | | private String buildPlanNo(String datePrefix, int sequence) { |
| | | // 统一计划号格式:JH + 日期 + 4位流水号 |
| | | return "JH" + datePrefix + String.format("%04d", sequence); |
| | | } |
| | | |
| | | /** |
| | | * 查询当日已存在的最大流水号,并返回下一个可用流水号。 |
| | | */ |
| | | private int resolveNextPlanSequence(String datePrefix) { |
| | | // 查询当日最新一条计划号 |
| | | QueryWrapper<ProductionPlan> queryWrapper = new QueryWrapper<>(); |
| | | queryWrapper.likeRight("mps_no", "JH" + datePrefix); |
| | | queryWrapper.orderByDesc("mps_no"); |
| | | queryWrapper.last("LIMIT 1"); |
| | | ProductionPlan latestPlan = productionPlanMapper.selectOne(queryWrapper); |
| | | |
| | | // 默认从 0001 开始 |
| | | int sequence = 1; |
| | | if (latestPlan != null && latestPlan.getMpsNo() != null && !latestPlan.getMpsNo().isEmpty()) { |
| | | if (latestPlan != null && StringUtils.isNotEmpty(latestPlan.getMpsNo())) { |
| | | // 截取末尾流水号并递增 |
| | | String sequenceStr = latestPlan.getMpsNo().substring(("JH" + datePrefix).length()); |
| | | try { |
| | | sequence = Integer.parseInt(sequenceStr) + 1; |
| | | } catch (NumberFormatException e) { |
| | | } catch (NumberFormatException ignored) { |
| | | // 历史数据格式异常时回退到 0001 |
| | | sequence = 1; |
| | | } |
| | | } |
| | | return "JH" + datePrefix + String.format("%04d", sequence); |
| | | 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); |
| | | } |
| | | |
| | | /** |
| | | * 按需求量与累计下发量推导计划状态。 |
| | | */ |
| | | private int resolvePlanStatus(BigDecimal requiredQuantity, BigDecimal issuedQuantity) { |
| | | // 无有效需求量时,状态保持待下发 |
| | | if (requiredQuantity == null || requiredQuantity.compareTo(BigDecimal.ZERO) <= 0) { |
| | | return PLAN_STATUS_WAIT; |
| | | } |
| | | // 有需求但未下发,状态仍为待下发 |
| | | if (issuedQuantity == null || issuedQuantity.compareTo(BigDecimal.ZERO) <= 0) { |
| | | return PLAN_STATUS_WAIT; |
| | | } |
| | | // 已下发量小于需求量为部分下发,否则为已下发 |
| | | return issuedQuantity.compareTo(requiredQuantity) < 0 ? PLAN_STATUS_PARTIAL : PLAN_STATUS_ISSUED; |
| | | } |
| | | |
| | | /** |
| | | * 将计划 ID 集合转成 [1,2,3] 形式,写入生产订单关联字段。 |
| | | */ |
| | | private String formatPlanIds(List<Long> planIds) { |
| | | // 去重并拼接为 [1,2,3] 形式的字符串 |
| | | return planIds.stream() |
| | | .filter(Objects::nonNull) |
| | | .distinct() |
| | | .map(String::valueOf) |
| | | .collect(Collectors.joining(",", "[", "]")); |
| | | } |
| | | } |