| | |
| | | |
| | | private static final Map<Integer, BigDecimal> INBOUND_WEIGHT_FACTOR_BY_SOURCE_STA; |
| | | private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; |
| | | private static final int DEFAULT_OUT_ORDER_BATCH_PRIORITY = 100; |
| | | private static final int DEFAULT_OUT_ORDER_BATCH_PRIORITY_THRESHOLD = 1; |
| | | // ERP 出库口大于该阈值时,/outOrder 只落出库订单,由定时器后续生成任务。 |
| | | private static final int PENDING_OUT_ORDER_STATION_THRESHOLD = 600; |
| | | // 延迟出库订单使用独立单据类型,便于和人工/页面创建的出库单区分来源。 |
| | |
| | | } |
| | | if (Objects.equals(param.getExecute(), 2)) { |
| | | // execute=2 先关闭订单开关,阻止定时器继续为未生成任务的明细建 WrkMast。 |
| | | // 已经生成的任务按状态分流:11 尚未下发,走本地取消;12/13 已下发或执行中,需要通知 WCS 暂停。 |
| | | // 已经生成的任务按状态分流:11 尚未下发,走本地取消;12/13 已下发或执行中,需要通知 WCS 取消。 |
| | | Map<String, Object> result = new HashMap<>(); |
| | | result.put("orderNo", param.getOrderId()); |
| | | result.put("execute", param.getExecute()); |
| | |
| | | return R.ok("出库订单已中止").add(result); |
| | | } |
| | | List<HashMap<String,Object>> taskList = new ArrayList<>(); |
| | | List<WrkMast> wcsCancelTasks = new ArrayList<>(); |
| | | int cancelledLocalTaskCount = 0; |
| | | for (WrkMast wrkMast : activeTasks) { |
| | | HashMap<String,Object> hashMap = new HashMap<>(); |
| | |
| | | continue; |
| | | } |
| | | taskList.add(hashMap); |
| | | wcsCancelTasks.add(wrkMast); |
| | | } |
| | | int cancelledWcsTaskCount = 0; |
| | | if (!taskList.isEmpty()) { |
| | | // wrk_sts=12/13 等已进入 WCS 侧的任务必须校验 WCS 返回,失败则事务回滚,避免本地误报已暂停。 |
| | | // wrk_sts=12/13 等已进入 WCS 侧的任务必须校验 WCS 返回,失败则事务回滚,避免本地误报已取消。 |
| | | R wcsR = wcsApiService.pauseOutTasks(taskList); |
| | | requireWcsPauseOk(wcsR); |
| | | // WCS 已确认取消后,本地也要取消任务并回滚订单明细 work_qty。 |
| | | // 回滚逻辑在 WorkService.cancelWrkMast 中统一处理,确保 WCS 回调 task_cancel 走同一套口径。 |
| | | for (WrkMast wrkMast : wcsCancelTasks) { |
| | | workService.cancelWrkMast(wrkMast.getWrkNo()+"", 9955L); |
| | | cancelledWcsTaskCount++; |
| | | } |
| | | } |
| | | result.put("cancelledLocalTaskCount", cancelledLocalTaskCount); |
| | | result.put("pausedWcsTaskCount", taskList.size()); |
| | | result.put("cancelledWcsTaskCount", cancelledWcsTaskCount); |
| | | return R.ok("出库订单已中止").add(result); |
| | | } |
| | | throw new CoolException("reason仅支持1或2"); |
| | |
| | | wrkMast.setIoTime(now); |
| | | wrkMast.setWrkSts(11L); // 工作状态:11.生成出库ID |
| | | wrkMast.setIoType(ioType); // 入出库状态 |
| | | wrkMast.setIoPri(13D); // 优先级:13 |
| | | wrkMast.setIoPri(Double.valueOf(i)); // 优先级 |
| | | wrkMast.setCrnNo(locMast.getCrnNo()); |
| | | wrkMast.setSourceStaNo(staDesc.getCrnStn()); // 源站 |
| | | wrkMast.setStaNo(staDesc.getStnNo()); // 目标站 |
| | |
| | | wrkMast.setExitMk("N"); // 退出 |
| | | wrkMast.setEmptyMk("N"); // 空板 |
| | | wrkMast.setLinkMis("N"); |
| | | wrkMast.setPdcType("N"); |
| | | wrkMast.setContainerNo(param.getContainerNo()); |
| | | wrkMast.setTeu(teu); |
| | | wrkMast.setPdcType(locMast.getCrnNo()>=19?"Y":"N");//自动任务下发标记 |
| | | wrkMast.setPlateNo(param.getPlateNo()); |
| | | wrkMast.setTrainNo(param.getTrainNo()); |
| | | wrkMast.setFreqType(param.getFreqType()); |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public R outOrderBatch(Map<String, List<OutTaskParam>> linesByBatchSeq,int count) { |
| | | int i = 0; |
| | | |
| | | for (Map.Entry<String, List<OutTaskParam>> entry : linesByBatchSeq.entrySet()) { |
| | | int i = DEFAULT_OUT_ORDER_BATCH_PRIORITY; |
| | | int j = 0; |
| | | int priorityThreshold = getOutOrderBatchPriorityThreshold(); |
| | | for (OutTaskParam outTaskParam : entry.getValue()) { |
| | | if(outTaskParam.getSeq()!=0){ |
| | | i= outTaskParam.getSeq(); |
| | | }else{ |
| | | i++; |
| | | } |
| | | int teu = Cools.isEmpty(outTaskParam.getTeu())?0:outTaskParam.getTeu(); |
| | | R r = outOrder(outTaskParam, count, teu ,i); |
| | | if (!Objects.equals(r.get("code"), 200)) { |
| | | throw new CoolException("出库建单失败"); |
| | | } |
| | | j++; |
| | | if (j >= priorityThreshold) { |
| | | i--; |
| | | j = 0; |
| | | } |
| | | } |
| | | |
| | | } |
| | | return R.ok(); |
| | | } |
| | | |
| | | private int getOutOrderBatchPriorityThreshold() { |
| | | Parameter parameter = Parameter.get(); |
| | | if (parameter == null || Cools.isEmpty(parameter.getOutOrderBatchPriorityThreshold())) { |
| | | return DEFAULT_OUT_ORDER_BATCH_PRIORITY_THRESHOLD; |
| | | } |
| | | try { |
| | | int threshold = Integer.parseInt(parameter.getOutOrderBatchPriorityThreshold().trim()); |
| | | return threshold <= 0 ? DEFAULT_OUT_ORDER_BATCH_PRIORITY_THRESHOLD : threshold; |
| | | } catch (NumberFormatException ignored) { |
| | | return DEFAULT_OUT_ORDER_BATCH_PRIORITY_THRESHOLD; |
| | | } |
| | | } |
| | | |
| | | @Override |
| | |
| | | Date now = new Date(); |
| | | int orderCount = 0; |
| | | int detailCount = 0; |
| | | int removedUndispatchedDetailCount = 0; |
| | | List<String> orderNos = new ArrayList<>(); |
| | | for (Map.Entry<String, List<OutTaskParam>> entry : paramsByOrderNo.entrySet()) { |
| | | String orderNo = entry.getKey(); |
| | | // 延迟建单允许覆盖“尚未生成任务、未进入执行”的旧订单; |
| | | // 一旦已有活动任务或历史任务,则禁止覆盖,避免重复出库同一订单。 |
| | | // 延迟建单允许同一个 orderNo 再次下发: |
| | | // 已下发/已完成明细保留追溯;未下发明细删除后,使用本次接口参数重新插入。 |
| | | assertPendingPakoutOrderCanReplace(orderNo); |
| | | |
| | | OrderPakout order = orderPakoutService.selectByNo(orderNo); |
| | | if (order != null) { |
| | | orderPakoutService.remove(order.getId()); |
| | | } |
| | | |
| | | DocType docType = docTypeService.selectOrAdd(OUT_ORDER_PENDING_DOC_TYPE, Boolean.FALSE); |
| | | order = new OrderPakout(); |
| | | order.setUuid(String.valueOf(snowflakeIdWorker.nextId())); |
| | | order.setOrderNo(orderNo); |
| | | order.setOrderTime(DateUtils.convert(now)); |
| | | order.setDocType(docType.getDocId()); |
| | | // settle=1 表示待生成任务;生成过至少一个进仓编号批次后置为 2。 |
| | | // status=1 是启动开关,pakoutOrderPause(execute=2) 会置 0 阻止定时器继续生成。 |
| | | order.setSettle(1L); |
| | | order.setStatus(1); |
| | | order.setCreateBy(9527L); |
| | | order.setCreateTime(now); |
| | | order.setUpdateBy(9527L); |
| | | order.setUpdateTime(now); |
| | | // moveStatus 保留现有“备货/移库”语义,这里不复用它做暂停开关。 |
| | | order.setMoveStatus(0); |
| | | // 2 表示出库方向,和 man_order_detl_pakout 明细保持一致。 |
| | | order.setPakinPakoutStatus(2); |
| | | if (!orderPakoutService.insert(order)) { |
| | | throw new CoolException("生成出库订单失败:" + orderNo); |
| | | if (order == null) { |
| | | order = createPendingPakoutOrderHeader(orderNo, now); |
| | | } else { |
| | | assertNoNonReplaceablePendingDetailConflict(order, entry.getValue()); |
| | | removedUndispatchedDetailCount += removeUndispatchedPendingDetails(order.getId()); |
| | | refreshPendingPakoutOrderForResubmit(order, now); |
| | | } |
| | | |
| | | for (OutTaskParam param : entry.getValue()) { |
| | |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("orderCount", orderCount); |
| | | result.put("detailCount", detailCount); |
| | | result.put("removedUndispatchedDetailCount", removedUndispatchedDetailCount); |
| | | result.put("orderNos", orderNos); |
| | | return R.ok("出库订单生成成功").add(result); |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | private OrderPakout createPendingPakoutOrderHeader(String orderNo, Date now) { |
| | | DocType docType = docTypeService.selectOrAdd(OUT_ORDER_PENDING_DOC_TYPE, Boolean.FALSE); |
| | | OrderPakout order = new OrderPakout(); |
| | | order.setUuid(String.valueOf(snowflakeIdWorker.nextId())); |
| | | order.setOrderNo(orderNo); |
| | | order.setOrderTime(DateUtils.convert(now)); |
| | | order.setDocType(docType.getDocId()); |
| | | // settle=1 表示待生成任务;生成过至少一个进仓编号批次后置为 2。 |
| | | // status=1 是启动开关,pakoutOrderPause(execute=2) 会置 0 阻止定时器继续生成。 |
| | | order.setSettle(1L); |
| | | order.setStatus(1); |
| | | order.setCreateBy(9527L); |
| | | order.setCreateTime(now); |
| | | order.setUpdateBy(9527L); |
| | | order.setUpdateTime(now); |
| | | // moveStatus 保留现有“备货/移库”语义,这里不复用它做暂停开关。 |
| | | order.setMoveStatus(0); |
| | | // 2 表示出库方向,和 man_order_detl_pakout 明细保持一致。 |
| | | order.setPakinPakoutStatus(2); |
| | | if (!orderPakoutService.insert(order)) { |
| | | throw new CoolException("生成出库订单失败:" + orderNo); |
| | | } |
| | | return order; |
| | | } |
| | | |
| | | private void refreshPendingPakoutOrderForResubmit(OrderPakout order, Date now) { |
| | | if (order == null) { |
| | | return; |
| | | } |
| | | boolean hasDispatchedDetail = hasDispatchedPendingDetail(order.getId()); |
| | | order.setStatus(1); |
| | | order.setSettle(hasDispatchedDetail ? 2L : 1L); |
| | | order.setUpdateBy(9527L); |
| | | order.setUpdateTime(now); |
| | | order.setPakinPakoutStatus(2); |
| | | if (!orderPakoutService.updateById(order)) { |
| | | throw new CoolException("更新出库订单失败:" + order.getOrderNo()); |
| | | } |
| | | } |
| | | |
| | | private boolean hasDispatchedPendingDetail(Long orderId) { |
| | | List<OrderDetlPakout> details = orderDetlPakoutService.selectList(new EntityWrapper<OrderDetlPakout>() |
| | | .eq("order_id", orderId) |
| | | .eq("status", 1)); |
| | | if (Cools.isEmpty(details)) { |
| | | return false; |
| | | } |
| | | for (OrderDetlPakout detail : details) { |
| | | if (!isUndispatchedPendingDetail(detail)) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * 删除同订单中尚未下发的明细。 |
| | | * |
| | | * 判断口径: |
| | | * - work_qty <= 0:尚未生成有效任务,或中止后任务已被取消并完成回滚。 |
| | | * - qty <= 0:没有业务完成量,删除后不会丢失已完成出库记录。 |
| | | */ |
| | | private int removeUndispatchedPendingDetails(Long orderId) { |
| | | List<OrderDetlPakout> details = orderDetlPakoutService.selectList(new EntityWrapper<OrderDetlPakout>() |
| | | .eq("order_id", orderId) |
| | | .eq("status", 1)); |
| | | if (Cools.isEmpty(details)) { |
| | | return 0; |
| | | } |
| | | int removed = 0; |
| | | for (OrderDetlPakout detail : details) { |
| | | if (!isUndispatchedPendingDetail(detail)) { |
| | | continue; |
| | | } |
| | | if (!orderDetlPakoutService.deleteById(detail.getId())) { |
| | | throw new CoolException("删除未下发出库订单明细失败:" + detail.getOrderNo()); |
| | | } |
| | | removed++; |
| | | } |
| | | return removed; |
| | | } |
| | | |
| | | private void assertNoNonReplaceablePendingDetailConflict(OrderPakout order, List<OutTaskParam> params) { |
| | | if (order == null || Cools.isEmpty(params)) { |
| | | return; |
| | | } |
| | | Set<String> newPalletIds = params.stream() |
| | | .filter(Objects::nonNull) |
| | | .map(OutTaskParam::getPalletId) |
| | | .filter(palletId -> !Cools.isEmpty(palletId)) |
| | | .collect(Collectors.toCollection(LinkedHashSet::new)); |
| | | if (newPalletIds.isEmpty()) { |
| | | return; |
| | | } |
| | | List<OrderDetlPakout> details = orderDetlPakoutService.selectList(new EntityWrapper<OrderDetlPakout>() |
| | | .eq("order_id", order.getId()) |
| | | .eq("status", 1)); |
| | | if (Cools.isEmpty(details)) { |
| | | return; |
| | | } |
| | | for (OrderDetlPakout detail : details) { |
| | | if (detail == null || !newPalletIds.contains(detail.getPalletId()) || isUndispatchedPendingDetail(detail)) { |
| | | continue; |
| | | } |
| | | throw new CoolException("托盘「" + detail.getPalletId() + "」已存在已下发或已完成出库明细,无法覆盖"); |
| | | } |
| | | } |
| | | |
| | | private boolean isUndispatchedPendingDetail(OrderDetlPakout detail) { |
| | | return detail != null |
| | | && safeDouble(detail.getWorkQty()) <= 0.0D |
| | | && safeDouble(detail.getQty()) <= 0.0D; |
| | | } |
| | | |
| | | /** |
| | | * 判断延迟出库订单是否还能被当前接口请求覆盖。 |
| | | * |
| | | * 允许覆盖的边界只限于“已建订单但尚未生成任何任务”的情况。 |
| | | * 只要 WrkMast 或 WrkMastLog 已存在,就说明订单已经进入执行链路或历史链路, |
| | | * 再覆盖会破坏任务、库存和 ERP 回传之间的可追溯关系。 |
| | | * 活动任务仍然存在时不允许覆盖,因为任务、库位、WCS 指令还在执行链路上。 |
| | | * 已经取消并归档到 WrkMastLog 的任务允许重下发;其订单明细 work_qty 已在取消时回滚, |
| | | * 本次接口会删除未下发明细并插入新明细。 |
| | | */ |
| | | private void assertPendingPakoutOrderCanReplace(String orderNo) { |
| | | List<WrkMast> activeTasks = findActiveOutboundTasks(orderNo); |
| | |
| | | int activeWrkCount = wrkMastService.selectCount(new EntityWrapper<WrkMast>() |
| | | .eq("io_type", 101) |
| | | .eq("user_no", orderNo)); |
| | | int historyWrkCount = wrkMastLogService.selectCount(new EntityWrapper<WrkMastLog>() |
| | | .eq("io_type", 101) |
| | | .eq("user_no", orderNo)); |
| | | if (activeWrkCount > 0 || historyWrkCount > 0) { |
| | | throw new CoolException("出库订单已存在任务档或任务历史档,无法覆盖:" + orderNo); |
| | | if (activeWrkCount > 0) { |
| | | throw new CoolException("出库订单已存在任务档,无法覆盖:" + orderNo); |
| | | } |
| | | OrderPakout order = orderPakoutService.selectByNo(orderNo); |
| | | if (order != null && order.getSettle() != null && order.getSettle() > 1L) { |
| | | if (order != null && order.getSettle() != null && order.getSettle() > 2L) { |
| | | throw new CoolException(orderNo + "正在出库,无法修改单据"); |
| | | } |
| | | } |