| | |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderBomMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.warehouse.MesWmWarehouseMapper; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO; |
| | | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| | | import cn.iocoder.yudao.module.mes.enums.pro.*; |
| | | import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService; |
| | | import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; |
| | |
| | | public class MesProBatchScheduleService { |
| | | @Resource private MesProTaskMapper taskMapper; |
| | | @Resource private MesProWorkOrderMapper workOrderMapper; |
| | | @Resource private MesProWorkOrderBomMapper workOrderBomMapper; |
| | | @Resource private MesWmMaterialStockMapper materialStockMapper; |
| | | @Resource private MesWmWarehouseMapper warehouseMapper; |
| | | @Resource private MesDvMachineryMapper machineryMapper; |
| | | @Resource private MesMdWorkstationService workstationService; |
| | | @Resource private MesProWorkOrderProcessService workOrderProcessService; |
| | |
| | | result.setPreviewTime(now()); |
| | | var orders = workOrderMapper.selectByIds(request.getWorkOrderIds()); |
| | | var orderMap = orders.stream().collect(Collectors.toMap(MesProWorkOrderDO::getId, order -> order)); |
| | | var bomByOrder = new HashMap<Long, List<MesProWorkOrderBomDO>>(); |
| | | for (var order : orders) bomByOrder.put(order.getId(), workOrderBomMapper.selectListByWorkOrderId(order.getId()).stream() |
| | | .sorted(Comparator.comparing(MesProWorkOrderBomDO::getId)).toList()); |
| | | var materialIds = bomByOrder.values().stream().flatMap(Collection::stream).map(MesProWorkOrderBomDO::getItemId) |
| | | .filter(Objects::nonNull).collect(Collectors.toSet()); |
| | | var warehouses = warehouseMapper.selectList().stream().collect(Collectors.toMap(MesWmWarehouseDO::getId, w -> w)); |
| | | var stocks = materialIds.isEmpty() ? List.<MesWmMaterialStockDO>of() : materialStockMapper.selectList( |
| | | new LambdaQueryWrapperX<MesWmMaterialStockDO>().in(MesWmMaterialStockDO::getItemId, materialIds)); |
| | | Map<Long, BigDecimal> availableStock = new HashMap<>(); |
| | | for (var stock : stocks) { |
| | | var warehouse = warehouses.get(stock.getWarehouseId()); |
| | | if (warehouse == null || Boolean.TRUE.equals(warehouse.getFrozen()) || Boolean.TRUE.equals(stock.getFrozen()) |
| | | || MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE.equals(warehouse.getCode())) continue; |
| | | var available = Optional.ofNullable(stock.getQuantity()).orElse(BigDecimal.ZERO) |
| | | .subtract(Optional.ofNullable(stock.getFrozenQuantity()).orElse(BigDecimal.ZERO)) |
| | | .subtract(Optional.ofNullable(stock.getReservedQuantity()).orElse(BigDecimal.ZERO)).max(BigDecimal.ZERO); |
| | | availableStock.merge(stock.getItemId(), available, BigDecimal::add); |
| | | } |
| | | var materialInfo = materialIds.isEmpty() ? Map.<Long, cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO>of() |
| | | : itemService.getItemMap(materialIds); |
| | | for (Long itemId : materialIds) { |
| | | var item = materialInfo.get(itemId); |
| | | if (item != null && Boolean.TRUE.equals(item.getSafeStockFlag()) && item.getMinStock() != null) { |
| | | availableStock.compute(itemId, (key, value) -> Optional.ofNullable(value).orElse(BigDecimal.ZERO) |
| | | .subtract(item.getMinStock().max(BigDecimal.ZERO)).max(BigDecimal.ZERO)); |
| | | } |
| | | } |
| | | var stockSnapshot = new TreeMap<>(availableStock); |
| | | var stations = workstationService.getWorkstationListByStatus(0).stream() |
| | | .sorted(Comparator.comparing(MesMdWorkstationDO::getId)).toList(); |
| | | var machineMap = machineryMapper.selectList().stream().collect(Collectors.toMap(MesDvMachineryDO::getId, machine -> machine)); |
| | |
| | | stationMachines.getOrDefault(task.getWorkstationId(), Set.of()), task.getStartTime(), task.getEndTime())); |
| | | } |
| | | } |
| | | var sorted = orders.stream().sorted(Comparator.comparing(MesProWorkOrderDO::getRequestDate, |
| | | Comparator.nullsLast(Comparator.naturalOrder())).thenComparing(MesProWorkOrderDO::getId)).toList(); |
| | | var sorted = orders.stream().sorted(MesSchedulePriority.comparator()).toList(); |
| | | List<Long> orderSequence = new ArrayList<>(); |
| | | Set<Long> visiting = new HashSet<>(); |
| | | for (var order : sorted) addChildrenFirst(order.getId(), orderMap, orderSequence, visiting); |
| | |
| | | || (order.getQuantityProduced() != null && order.getQuantityProduced().compareTo(order.getQuantity()) >= 0)) { |
| | | throw new IllegalArgumentException("工单数量无效或已生产完成"); |
| | | } |
| | | var boms = bomByOrder.getOrDefault(id, List.of()); |
| | | if (boms.isEmpty()) throw new IllegalArgumentException("工单没有 BOM,无法核对物料齐套,请先维护"); |
| | | BigDecimal unscheduled = order.getQuantity().subtract(Optional.ofNullable(order.getQuantityScheduled()).orElse(BigDecimal.ZERO)); |
| | | if (unscheduled.signum() <= 0) throw new IllegalArgumentException("工单已完成排产"); |
| | | Map<Long, BigDecimal> demand = new HashMap<>(); |
| | | for (var bom : boms) { |
| | | if (bom.getItemId() == null || bom.getQuantity() == null || bom.getQuantity().signum() <= 0) |
| | | throw new IllegalArgumentException("工单 BOM 物料或需求数量无效,请先维护"); |
| | | var requiredItem = materialInfo.get(bom.getItemId()); |
| | | if (requiredItem == null || !Objects.equals(requiredItem.getStatus(), 0)) |
| | | throw new IllegalArgumentException("工单 BOM 存在已删除或停用的物料,请先维护"); |
| | | demand.merge(bom.getItemId(), bom.getQuantity().multiply(unscheduled).divide(order.getQuantity(), 6, RoundingMode.UP), BigDecimal::add); |
| | | } |
| | | var trialStock = MesScheduleMaterialCheck.allocate(availableStock, demand); |
| | | LocalDateTime earliest = calendar.nextWorkingTime(request.getStartTime()); |
| | | // 父工单必须等子工单;同一批次按依赖顺序排,未纳入的未完成子工单阻止排产。 |
| | | for (var child : workOrderMapper.selectList(MesProWorkOrderDO::getParentId, id)) { |
| | |
| | | } |
| | | if (plans.isEmpty()) throw new IllegalArgumentException("所有工序已完成排产,不可重复提交"); |
| | | if (result.getTasks().size() + plans.size() > 200) throw new IllegalArgumentException("一次最多生成200个任务,请拆分工单批次排产"); |
| | | result.getTasks().addAll(plans); busy = trialBusy; |
| | | result.getTasks().addAll(plans); busy = trialBusy; availableStock = trialStock; |
| | | orderPlan.setTaskCount(plans.size()); orderPlan.setEndTime(earliest); ends.put(id, earliest); |
| | | if (order.getRequestDate() != null && earliest.toLocalDate().isAfter(order.getRequestDate().toLocalDate())) { |
| | | orderPlan.setLate(true); result.getWarnings().add("工单" + order.getCode() + "预计完成时间晚于交期"); |
| | |
| | | fingerprint.put("tasks", result.getTasks()); fingerprint.put("holidays", holidayDates.stream().sorted().toList()); |
| | | fingerprint.put("workOrders", orders.stream().sorted(Comparator.comparing(MesProWorkOrderDO::getId)).toList()); |
| | | fingerprint.put("paramTemplates", templateHashes); |
| | | fingerprint.put("materialStock", stockSnapshot); |
| | | fingerprint.put("workOrderBoms", new TreeMap<>(bomByOrder)); |
| | | result.setPreviewHash(DigestUtil.sha256Hex(JsonUtils.toJsonString(fingerprint))); |
| | | return result; |
| | | } |