| | |
| | | return R.error("正在下发任务给WCS,无法中止"); |
| | | } |
| | | |
| | | if (Objects.equals(param.getExecute(), 1)) { |
| | | // execute=1 的含义已经从“仅确认已生成任务”扩展为“启动/恢复订单”: |
| | | // 1. 订单 status=0 时恢复为 1; |
| | | // 2. 已有 wrk_sts=11 的任务设置 pdcType=Y; |
| | | // 3. 未生成的订单明细立即尝试生成当前可放行批次。 |
| | | // 因此这里直接复用公开执行接口的统一实现,避免 pause 和 execute 两套逻辑分叉。 |
| | | return executePakoutOrder(param.getOrderId()); |
| | | } |
| | | |
| | | OrderPakout orderPakout = orderPakoutService.selectByNo(param.getOrderId()); |
| | | List<WrkMast> activeTasks = findActiveOutboundTasks(param.getOrderId()); |
| | | if (Objects.equals(param.getExecute(), 1)) { |
| | | // execute=1 同时处理两类对象: |
| | | // 1. 未生成任务的出库订单:恢复 order.status=1,让定时器可以继续扫描。 |
| | | // 2. 已生成但待下发的任务:设置 pdcType=Y,允许 WorkMastScheduler 下发给 WCS。 |
| | | Map<String, Object> result = new HashMap<>(); |
| | | result.put("orderNo", param.getOrderId()); |
| | | result.put("execute", param.getExecute()); |
| | | boolean orderStatusUpdated = false; |
| | | if (orderPakout != null && !Objects.equals(orderPakout.getStatus(), 1)) { |
| | | orderPakout.setStatus(1); |
| | | orderPakout.setUpdateBy(9527L); |
| | | orderPakout.setUpdateTime(new Date()); |
| | | if (!orderPakoutService.updateById(orderPakout)) { |
| | | throw new CoolException("启动出库订单失败:" + param.getOrderId()); |
| | | } |
| | | orderStatusUpdated = true; |
| | | } |
| | | int confirmedCount = confirmPendingOutboundTasks(activeTasks); |
| | | R generateResult = null; |
| | | int generatedConfirmedCount = 0; |
| | | if (orderPakout != null) { |
| | | // 启动后立即尝试生成当前可放行的进仓编号批次,避免必须等待下一轮定时扫描。 |
| | | generateResult = generatePendingPakoutOrderTasks(param.getOrderId()); |
| | | // 刚生成的任务初始状态仍是 11,这里再次确认,保持“启动接口调用后即可下发”的语义。 |
| | | generatedConfirmedCount = confirmPendingOutboundTasks(findActiveOutboundTasks(param.getOrderId())); |
| | | } |
| | | result.put("orderStatusUpdated", orderStatusUpdated); |
| | | result.put("confirmedCount", confirmedCount + generatedConfirmedCount); |
| | | result.put("generatedConfirmedCount", generatedConfirmedCount); |
| | | result.put("generateResult", generateResult); |
| | | return R.ok("出库订单启动成功").add(result); |
| | | } |
| | | if (Objects.equals(param.getExecute(), 2)) { |
| | | // execute=2 先关闭订单开关,阻止定时器继续为未生成任务的明细建 WrkMast。 |
| | | // 已经生成的任务按状态分流:11 尚未下发,走本地取消;12/13 已下发或执行中,需要通知 WCS 取消。 |
| | |
| | | result.put("taskCount", activeTasks.size()); |
| | | boolean orderStatusUpdated = false; |
| | | if (orderPakout != null && !Objects.equals(orderPakout.getStatus(), 0)) { |
| | | // status=0 是订单级中止开关。先关订单,再取消任务, |
| | | // 可以阻止定时器在取消过程中又生成下一批未下发任务。 |
| | | orderPakout.setStatus(0); |
| | | orderPakout.setUpdateBy(9527L); |
| | | orderPakout.setUpdateTime(new Date()); |
| | |
| | | hashMap.put("taskNo", wrkMast.getWrkNo()); |
| | | if (!Cools.isEmpty(wrkMast) && Objects.equals(wrkMast.getWrkSts(), 11L)) { |
| | | // wrk_sts=11 还没发到 WCS,本地取消即可,不需要产生 WCS 暂停指令。 |
| | | // cancelWrkMast 会把工作档转历史、释放库位,并回滚订单明细 work_qty。 |
| | | workService.cancelWrkMast(wrkMast.getWrkNo()+"", 9955L); |
| | | cancelledLocalTaskCount++; |
| | | continue; |
| | |
| | | * pdcType 是现有 WorkMastScheduler 的下发开关: |
| | | * - wrk_sts=11 且 pdcType=Y 时,调度器可以继续下发给 WCS。 |
| | | * - 已经是 Y 的任务保持幂等,不重复更新。 |
| | | * |
| | | * 为什么不在 outOrderBatch 里直接强制 Y: |
| | | * - outOrderBatch 仍被其他入口复用; |
| | | * - MQTT/IoT autoConfirm=false 的订单需要先建单但不下发; |
| | | * - 所以放行必须由“订单 status=1 的定时生成”或“执行接口”显式完成。 |
| | | */ |
| | | private int confirmPendingOutboundTasks(List<WrkMast> activeTasks) { |
| | | if (Cools.isEmpty(activeTasks)) { |
| | |
| | | } |
| | | |
| | | @Override |
| | | @Transactional |
| | | public R pakoutOrderExecute(OpenOrderPakoutExecuteParam param) { |
| | | return null; |
| | | if (param == null || Cools.isEmpty(param.getOrderId())) { |
| | | throw new CoolException("orderNo不能为空"); |
| | | } |
| | | if (param.getExecute() == null) { |
| | | throw new CoolException("execute不能为空"); |
| | | } |
| | | if (!Objects.equals(param.getExecute(), 1)) { |
| | | throw new CoolException("execute仅支持1"); |
| | | } |
| | | return executePakoutOrder(param.getOrderId()); |
| | | } |
| | | |
| | | /** |
| | | * 出库订单执行/恢复的统一入口。 |
| | | * |
| | | * 该方法同时服务: |
| | | * - 新增公开执行接口; |
| | | * - pakoutOrderPause(execute=1) 的恢复逻辑; |
| | | * - IoT/MQTT autoConfirm=true 的即时执行逻辑。 |
| | | * |
| | | * 执行语义分三步: |
| | | * 1. 把订单 status 恢复为 1,让定时器和当前调用都可以生成任务; |
| | | * 2. 对已经存在且 wrk_sts=11 的任务设置 pdcType=Y; |
| | | * 3. 立即尝试生成当前允许的一个批次,并再次确认新生成任务可下发。 |
| | | * |
| | | * 幂等性: |
| | | * - 订单已是 status=1 时不会重复更新; |
| | | * - 已经 pdcType=Y 的任务不会重复确认; |
| | | * - 已经 work_qty 覆盖的明细不会再次生成任务。 |
| | | */ |
| | | private R executePakoutOrder(String orderNo) { |
| | | if (Cools.isEmpty(orderNo)) { |
| | | throw new CoolException("orderNo不能为空"); |
| | | } |
| | | OrderPakout orderPakout = orderPakoutService.selectByNo(orderNo); |
| | | List<WrkMast> activeTasks = findActiveOutboundTasks(orderNo); |
| | | |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("orderNo", orderNo); |
| | | result.put("execute", 1); |
| | | boolean orderStatusUpdated = false; |
| | | if (orderPakout != null && !Objects.equals(orderPakout.getStatus(), 1)) { |
| | | orderPakout.setStatus(1); |
| | | orderPakout.setUpdateBy(9527L); |
| | | orderPakout.setUpdateTime(new Date()); |
| | | if (!orderPakoutService.updateById(orderPakout)) { |
| | | throw new CoolException("启动出库订单失败:" + orderNo); |
| | | } |
| | | orderStatusUpdated = true; |
| | | } |
| | | |
| | | int confirmedCount = confirmPendingOutboundTasks(activeTasks); |
| | | R generateResult = null; |
| | | int generatedConfirmedCount = 0; |
| | | if (orderPakout != null) { |
| | | // 主动调用执行接口时,不等下一轮定时器,直接生成当前可放行的一个批次。 |
| | | // 如果当前批次被 OutboundBatchSeqReleaseGuard 阻塞,generateResult 会返回阻塞原因, |
| | | // 不会抛错,也不会提前生成后续批次。 |
| | | generateResult = generatePendingPakoutOrderTasks(orderNo); |
| | | // 新生成的任务初始状态为 11,这里再次确认,让执行接口返回后任务即可被调度器下发。 |
| | | generatedConfirmedCount = confirmPendingOutboundTasks(findActiveOutboundTasks(orderNo)); |
| | | } |
| | | |
| | | List<Integer> wrkNos = collectActiveOutboundWrkNos(orderNo); |
| | | result.put("orderStatusUpdated", orderStatusUpdated); |
| | | result.put("confirmedCount", confirmedCount + generatedConfirmedCount); |
| | | result.put("generatedConfirmedCount", generatedConfirmedCount); |
| | | result.put("generateResult", generateResult); |
| | | result.put("wrkNos", wrkNos); |
| | | if (!wrkNos.isEmpty()) { |
| | | // IoT 单托盘调用依赖顶层 wrkNo 回写任务号;多托盘时返回当前订单活动任务的第一条。 |
| | | result.put("wrkNo", wrkNos.get(0)); |
| | | } |
| | | R response = R.ok("出库订单启动成功").add(result); |
| | | if (!wrkNos.isEmpty()) { |
| | | // add(Map) 在部分调用方会按 data 读取,这里再显式写顶层字段,兼容 IoT 的 resolveWrkNo。 |
| | | response.put("wrkNo", wrkNos.get(0)); |
| | | response.put("wrkNos", wrkNos); |
| | | } |
| | | return response; |
| | | } |
| | | |
| | | private List<Integer> collectActiveOutboundWrkNos(String orderNo) { |
| | | List<WrkMast> activeTasks = findActiveOutboundTasks(orderNo); |
| | | if (Cools.isEmpty(activeTasks)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | List<Integer> wrkNos = new ArrayList<>(); |
| | | for (WrkMast wrkMast : activeTasks) { |
| | | if (wrkMast != null && wrkMast.getWrkNo() != null) { |
| | | wrkNos.add(wrkMast.getWrkNo()); |
| | | } |
| | | } |
| | | return wrkNos; |
| | | } |
| | | |
| | | private List<WrkMast> findActiveOutboundTasks(String orderNo) { |
| | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public R outOrderCreatePakoutOrder(List<OutTaskParam> params) { |
| | | return outOrderCreatePakoutOrder(params, true); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public R outOrderCreatePakoutOrder(List<OutTaskParam> params, boolean executable) { |
| | | if (Cools.isEmpty(params)) { |
| | | return R.ok(); |
| | | } |
| | | |
| | | // 一个 /outOrder 请求可能包含多个 orderId。按订单号分组后分别创建订单头, |
| | | // 保证 orderId 始终对应 man_order_pakout.order_no,明细挂在对应订单下。 |
| | | // |
| | | // 这里不按批次分组落订单头:同一个 ERP 单号下可能同时包含多个 entryWmsCode, |
| | | // 它们应该是同一张出库订单的多个批次,而不是多张订单。 |
| | | Map<String, List<OutTaskParam>> paramsByOrderNo = new LinkedHashMap<>(); |
| | | for (OutTaskParam param : params) { |
| | | validatePendingOutOrderParam(param); |
| | |
| | | String orderNo = entry.getKey(); |
| | | // 延迟建单允许同一个 orderNo 再次下发: |
| | | // 已下发/已完成明细保留追溯;未下发明细删除后,使用本次接口参数重新插入。 |
| | | // 如果当前订单还有活动 WrkMast,则不允许覆盖,避免同一托盘同时存在两条执行链。 |
| | | assertPendingPakoutOrderCanReplace(orderNo); |
| | | |
| | | OrderPakout order = orderPakoutService.selectByNo(orderNo); |
| | | if (order == null) { |
| | | order = createPendingPakoutOrderHeader(orderNo, now); |
| | | order = createPendingPakoutOrderHeader(orderNo, now, executable); |
| | | } else { |
| | | assertNoNonReplaceablePendingDetailConflict(order, entry.getValue()); |
| | | removedUndispatchedDetailCount += removeUndispatchedPendingDetails(order.getId()); |
| | | refreshPendingPakoutOrderForResubmit(order, now); |
| | | refreshPendingPakoutOrderForResubmit(order, now, executable); |
| | | } |
| | | |
| | | for (OutTaskParam param : entry.getValue()) { |
| | | // 明细完整保存接口字段,后续定时器可以无损还原 OutTaskParam 再复用 outOrderBatch。 |
| | | // 订单化的核心是“参数先持久化、任务后生成”:即使服务重启,定时器仍能继续处理未生成明细。 |
| | | OrderDetlPakout detail = buildPendingPakoutOrderDetl(order, param, now); |
| | | if (!orderDetlPakoutService.insert(detail)) { |
| | | throw new CoolException("生成出库订单明细失败:" + orderNo); |
| | |
| | | result.put("detailCount", detailCount); |
| | | result.put("removedUndispatchedDetailCount", removedUndispatchedDetailCount); |
| | | result.put("orderNos", orderNos); |
| | | result.put("executable", executable); |
| | | return R.ok("出库订单生成成功").add(result); |
| | | } |
| | | |
| | |
| | | // status=1 表示未被 pakoutOrderPause 中止; |
| | | // settle in (1,2) 表示未完成全部任务生成; |
| | | // pakin_pakout_status=2 限定出库单,避免误扫入库/其他方向单据。 |
| | | // |
| | | // status=0 的订单不会被这里扫描,典型来源是: |
| | | // - IoT/MQTT autoConfirm=false 预创建订单; |
| | | // - pakoutOrderPause execute=2 中止后的订单。 |
| | | List<OrderPakout> orders = orderPakoutService.selectList(new EntityWrapper<OrderPakout>() |
| | | .eq("status", 1) |
| | | .in("settle", Arrays.asList(1L, 2L)) |
| | |
| | | continue; |
| | | } |
| | | scannedOrderCount++; |
| | | // 每个订单单独走生成逻辑。单订单方法内部只会生成当前允许的一个 entryWmsCode 批次, |
| | | // 每个订单单独走生成逻辑。单订单方法内部只会生成当前允许的一个批次: |
| | | // - 高站点:一个 entryWmsCode 批次; |
| | | // - 低站点:一个 orderNo 批次。 |
| | | // 因此定时器重复执行也不会一次性把后续所有进仓编号全部释放。 |
| | | R r = generatePendingPakoutOrderTasks(order.getOrderNo()); |
| | | details.add(r); |
| | |
| | | return R.ok("出库订单不存在").add(buildGeneratePendingResult(orderNo, null, 0, 0, "order not found")); |
| | | } |
| | | if (!Objects.equals(order.getStatus(), 1)) { |
| | | // 订单 status=0 时只返回“被暂停/中止”,不抛异常。 |
| | | // 这样定时器扫描和人工执行接口都可以幂等调用,调用方根据 reason 判断即可。 |
| | | return R.ok("出库订单已中止").add(buildGeneratePendingResult(orderNo, null, 0, 0, "order status disabled")); |
| | | } |
| | | if (!Objects.equals(order.getSettle(), 1L) && !Objects.equals(order.getSettle(), 2L)) { |
| | | return R.ok("出库订单已处理").add(buildGeneratePendingResult(orderNo, null, 0, 0, "order settle not pending")); |
| | | } |
| | | |
| | | // 按原明细 id 顺序读取,LinkedHashMap 会保持首次出现的 entryWmsCode 顺序。 |
| | | // 这样“进仓编号 A 做完后再生成 B”的顺序和 ERP 原始明细顺序一致。 |
| | | // 按原明细 id 顺序读取,LinkedHashMap 会保持首次出现的批次键顺序。 |
| | | // 高站点批次键为 entryWmsCode,可继续实现“进仓编号 A 做完后再生成 B”; |
| | | // 低站点批次键为 orderNo,同一订单只形成一个批次。 |
| | | List<OrderDetlPakout> details = orderDetlPakoutService.selectList(new EntityWrapper<OrderDetlPakout>() |
| | | .eq("order_id", order.getId()) |
| | | .eq("status", 1) |
| | | .orderBy("id", true)); |
| | | LinkedHashMap<String, List<OrderDetlPakout>> detailsByEntryWmsCode = new LinkedHashMap<>(); |
| | | LinkedHashMap<String, List<OrderDetlPakout>> detailsByTaskBatchSeq = new LinkedHashMap<>(); |
| | | for (OrderDetlPakout detail : details) { |
| | | if (!isPendingPakoutDetail(detail)) { |
| | | continue; |
| | | } |
| | | detailsByEntryWmsCode |
| | | .computeIfAbsent(detail.getEntryWmsCode(), key -> new ArrayList<>()) |
| | | String taskBatchSeq = resolvePakoutTaskBatchSeq(orderNo, detail); |
| | | // LinkedHashMap 保持首次出现顺序,保证同一订单内的批次生成顺序和 ERP 明细顺序一致。 |
| | | // 这对高站点尤其关键:第二个 entryWmsCode 只有在第一个批次满足释放条件后才会被处理。 |
| | | detailsByTaskBatchSeq |
| | | .computeIfAbsent(taskBatchSeq, key -> new ArrayList<>()) |
| | | .add(detail); |
| | | } |
| | | if (detailsByEntryWmsCode.isEmpty()) { |
| | | if (detailsByTaskBatchSeq.isEmpty()) { |
| | | return R.ok("无待生成出库任务").add(buildGeneratePendingResult(orderNo, null, 0, 0, "no pending detail")); |
| | | } |
| | | |
| | | // 一次只取当前最靠前的进仓编号批次。后续进仓编号是否允许生成, |
| | | // 由 OutboundBatchSeqReleaseGuard 根据已有 WrkMast 的 batchSeq、wrk_sts=25 数量阈值等判断。 |
| | | Map.Entry<String, List<OrderDetlPakout>> candidate = detailsByEntryWmsCode.entrySet().iterator().next(); |
| | | String entryWmsCode = candidate.getKey(); |
| | | String blockMsg = outboundBatchSeqReleaseGuard.validateReady(orderNo, entryWmsCode); |
| | | // 一次只取当前最靠前的批次。高站点的后续进仓编号是否允许生成, |
| | | // 由 OutboundBatchSeqReleaseGuard 根据已有 WrkMast.batchSeq、wrk_sts=25 数量阈值等判断。 |
| | | Map.Entry<String, List<OrderDetlPakout>> candidate = detailsByTaskBatchSeq.entrySet().iterator().next(); |
| | | String taskBatchSeq = candidate.getKey(); |
| | | // 批次守卫读取 WrkMast.userNo/orderNo + WrkMast.batchSeq: |
| | | // - 如果前序批次未完成,阻塞; |
| | | // - 如果当前批次 wrk_sts=25 数量达到配置阈值,阻塞; |
| | | // - 否则允许生成当前批次的下一组任务。 |
| | | String blockMsg = outboundBatchSeqReleaseGuard.validateReady(orderNo, taskBatchSeq); |
| | | if (!Cools.isEmpty(blockMsg)) { |
| | | return R.ok("出库进仓编号暂不满足生成条件").add(buildGeneratePendingResult(orderNo, entryWmsCode, 0, candidate.getValue().size(), blockMsg)); |
| | | return R.ok("出库批次暂不满足生成条件").add(buildGeneratePendingResult(orderNo, taskBatchSeq, 0, candidate.getValue().size(), blockMsg)); |
| | | } |
| | | |
| | | List<OutTaskParam> outTaskParams = new ArrayList<>(); |
| | | for (OrderDetlPakout detail : candidate.getValue()) { |
| | | outTaskParams.add(buildOutTaskParam(orderNo, entryWmsCode, detail)); |
| | | outTaskParams.add(buildOutTaskParam(orderNo, taskBatchSeq, detail)); |
| | | } |
| | | Map<String, List<OutTaskParam>> linesByBatchSeq = new LinkedHashMap<>(); |
| | | // outOrderBatch 的分组 key 就是任务 batchSeq。这里强制使用 entryWmsCode, |
| | | // 确保 WrkMast.userNo=orderId 且 WrkMast.batchSeq=entryWmsCode,现有批次守卫才能生效。 |
| | | linesByBatchSeq.put(entryWmsCode, outTaskParams); |
| | | // outOrderBatch 的分组 key 就是任务 batchSeq。这里强制使用计算后的任务批次键, |
| | | // 确保 WrkMast.userNo=orderId 且 WrkMast.batchSeq 与批次守卫口径一致。 |
| | | // |
| | | // 不直接使用订单明细里的 batchSeq,因为低站点要求固定 orderId, |
| | | // 高站点要求固定 entryWmsCode,ERP 原始 batchSeq 只作为追溯字段保存。 |
| | | linesByBatchSeq.put(taskBatchSeq, outTaskParams); |
| | | R r = outOrderBatch(linesByBatchSeq, outTaskParams.size()); |
| | | if (!Objects.equals(r.get("code"), 200)) { |
| | | throw new CoolException("出库订单生成任务失败:" + orderNo); |
| | |
| | | double remaining = getPendingDetailQty(detail); |
| | | // work_qty 表示“已生成任务数量”,不是完成数量。 |
| | | // 任务完成后由 WorkMastHandler 递增 qty,因此重复定时扫描不会为同一托盘重复建任务。 |
| | | // |
| | | // 中止取消时 WorkService.cancelWrkMast 会把这部分 work_qty 回滚; |
| | | // 回滚后再次执行订单,未下发/已取消的明细可以重新生成任务。 |
| | | detail.setWorkQty(safeDouble(detail.getWorkQty()) + remaining); |
| | | detail.setUpdateBy(9527L); |
| | | detail.setUpdateTime(new Date()); |
| | |
| | | } |
| | | } |
| | | |
| | | return R.ok("出库订单生成任务成功").add(buildGeneratePendingResult(orderNo, entryWmsCode, outTaskParams.size(), candidate.getValue().size(), null)); |
| | | // status=1 的订单代表当前允许执行。无论来自 ERP 自动订单还是执行接口恢复后的订单, |
| | | // 生成任务后都立即设置 pdcType=Y,保证 WorkMastScheduler 可以继续下发。 |
| | | int confirmedCount = confirmPendingOutboundTasks(findActiveOutboundTasks(orderNo)); |
| | | Map<String, Object> result = buildGeneratePendingResult(orderNo, taskBatchSeq, outTaskParams.size(), candidate.getValue().size(), null); |
| | | result.put("confirmedCount", confirmedCount); |
| | | return R.ok("出库订单生成任务成功").add(result); |
| | | } |
| | | |
| | | private void validatePendingOutOrderParam(OutTaskParam param) { |
| | |
| | | if (Cools.isEmpty(param.getPalletId())) { |
| | | throw new CoolException("托盘号不能为空"); |
| | | } |
| | | if (Cools.isEmpty(param.getEntryWmsCode())) { |
| | | if (Cools.isEmpty(param.getStationId())) { |
| | | throw new CoolException("托盘「" + param.getPalletId() + "」出库口编码不能为空"); |
| | | } |
| | | try { |
| | | Integer.valueOf(param.getStationId()); |
| | | } catch (NumberFormatException ex) { |
| | | throw new CoolException("托盘「" + param.getPalletId() + "」出库口编码格式错误:" + param.getStationId()); |
| | | } |
| | | if (isPendingOutOrderStation(param.getStationId()) && Cools.isEmpty(param.getEntryWmsCode())) { |
| | | throw new CoolException("托盘「" + param.getPalletId() + "」进仓编号不能为空"); |
| | | } |
| | | if (!isPendingOutOrderStation(param.getStationId())) { |
| | | throw new CoolException("托盘「" + param.getPalletId() + "」出库口不属于延迟生成任务范围"); |
| | | if (Cools.isEmpty(param.getBatchSeq())) { |
| | | // batchSeq 是接口原字段,保存在订单明细用于追溯;实际任务批次键由 resolvePakoutTaskBatchSeq 决定。 |
| | | param.setBatchSeq(param.getOrderId()); |
| | | } |
| | | } |
| | | |
| | | private OrderPakout createPendingPakoutOrderHeader(String orderNo, Date now) { |
| | | private OrderPakout createPendingPakoutOrderHeader(String orderNo, Date now, boolean executable) { |
| | | 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 阻止定时器继续生成。 |
| | | // settle 仍沿用现有订单进度语义: |
| | | // - 1:订单已创建,还没有生成过任务; |
| | | // - 2:至少生成过一个任务批次,后续是否还有待生成明细继续看 anfme - work_qty; |
| | | // - 大于 2:其他业务流程已处理/完成,不允许本接口覆盖。 |
| | | // status 是启动开关: |
| | | // - ERP /outOrder 创建 executable=true,status=1,定时器可自动生成; |
| | | // - IoT/MQTT 创建 executable=false,status=0,必须调用执行接口后才生成。 |
| | | order.setSettle(1L); |
| | | order.setStatus(1); |
| | | order.setStatus(executable ? 1 : 0); |
| | | order.setCreateBy(9527L); |
| | | order.setCreateTime(now); |
| | | order.setUpdateBy(9527L); |
| | |
| | | return order; |
| | | } |
| | | |
| | | private void refreshPendingPakoutOrderForResubmit(OrderPakout order, Date now) { |
| | | private void refreshPendingPakoutOrderForResubmit(OrderPakout order, Date now, boolean executable) { |
| | | if (order == null) { |
| | | return; |
| | | } |
| | | boolean hasDispatchedDetail = hasDispatchedPendingDetail(order.getId()); |
| | | order.setStatus(1); |
| | | // 重复下发时按本次入口重新决定是否立即可执行: |
| | | // ERP 重发继续 status=1;IoT/MQTT 预创建继续 status=0,等待执行接口。 |
| | | // |
| | | // settle 的恢复口径: |
| | | // - 如果已有已下发/已完成明细,保持 2,说明该订单曾进入生成任务阶段; |
| | | // - 如果只剩未下发明细,重置为 1,表示可以作为新订单批次重新生成。 |
| | | order.setStatus(executable ? 1 : 0); |
| | | order.setSettle(hasDispatchedDetail ? 2L : 1L); |
| | | order.setUpdateBy(9527L); |
| | | order.setUpdateTime(now); |
| | |
| | | } |
| | | |
| | | private boolean hasDispatchedPendingDetail(Long orderId) { |
| | | // 只要存在 work_qty>0 或 qty>0 的明细,就认为该订单已经进入过执行链路。 |
| | | // 这类明细不能在重发时直接删除,否则会丢失任务追溯或完成数量。 |
| | | List<OrderDetlPakout> details = orderDetlPakoutService.selectList(new EntityWrapper<OrderDetlPakout>() |
| | | .eq("order_id", orderId) |
| | | .eq("status", 1)); |
| | |
| | | return; |
| | | } |
| | | for (OrderDetlPakout detail : details) { |
| | | // 本次重发如果包含了已经下发/完成的同托盘明细,直接拒绝。 |
| | | // 未下发明细可以删除重建;已下发/完成明细必须保留,以免破坏取消回滚和完成回写。 |
| | | if (detail == null || !newPalletIds.contains(detail.getPalletId()) || isUndispatchedPendingDetail(detail)) { |
| | | continue; |
| | | } |
| | |
| | | } |
| | | |
| | | private boolean isUndispatchedPendingDetail(OrderDetlPakout detail) { |
| | | // “未下发”不是看订单状态,而是看明细进度: |
| | | // work_qty=0 且 qty=0 才说明既没有生成任务,也没有完成量,可以安全删除重建。 |
| | | return detail != null |
| | | && safeDouble(detail.getWorkQty()) <= 0.0D |
| | | && safeDouble(detail.getQty()) <= 0.0D; |
| | |
| | | } |
| | | OrderDetlPakout detail = new OrderDetlPakout(); |
| | | detail.sync(locDetl); |
| | | // order_id/order_no 使用 man_order_pakout 的主档信息,不能使用 ERP 原始字段直接写入 id。 |
| | | detail.setOrderId(order.getId()); |
| | | detail.setOrderNo(order.getOrderNo()); |
| | | detail.setAnfme(resolvePendingOrderAnfme(param, locDetl)); |
| | | // work_qty:已生成任务数量;qty:已完成出库数量。 |
| | | // 两者初始都为 0,生成任务只加 work_qty,任务完成回写才加 qty。 |
| | | detail.setWorkQty(0.0D); |
| | | detail.setQty(0.0D); |
| | | detail.setStatus(1); |
| | |
| | | detail.setUpdateBy(9527L); |
| | | detail.setUpdateTime(now); |
| | | detail.setPakinPakoutStatus(2); |
| | | // 以下字段完整保存 OutTaskParam 原值,保证后续定时器可以还原出 OutTaskParam。 |
| | | // 注意:detail.batchSeq 只是 ERP 原始批次字段;真正生成 WrkMast.batchSeq 时会重新计算。 |
| | | detail.setBatchSeq(param.getBatchSeq()); |
| | | detail.setSeq(param.getSeq()); |
| | | detail.setPalletId(param.getPalletId()); |
| | |
| | | detail.setCubeNumber(param.getCubeNumber()); |
| | | detail.setTeu(param.getTeu()); |
| | | // standby1/standby2 继续镜像进仓编号和出库门号,兼容现有完成回写的 selectItem 匹配逻辑。 |
| | | // 低站点允许 entryWmsCode 为空,也要显式落空值,保证订单明细和 WrkDetl 维度一致。 |
| | | detail.setStandby1(param.getEntryWmsCode()); |
| | | detail.setStandby2(param.getOutDoorNo()); |
| | | return detail; |
| | |
| | | * 这里只看 anfme - work_qty,避免定时器因为任务未完成而重复生成同一托盘任务。 |
| | | */ |
| | | private boolean isPendingPakoutDetail(OrderDetlPakout detail) { |
| | | // 只要还存在待生成数量,并且能计算出任务批次键,就认为该明细可进入生成流程。 |
| | | // 是否真的生成由后续 OutboundBatchSeqReleaseGuard 决定。 |
| | | return detail != null |
| | | && isPendingOutOrderStation(detail.getStationId()) |
| | | && !Cools.isEmpty(detail.getEntryWmsCode()) |
| | | && !Cools.isEmpty(resolvePakoutTaskBatchSeq(detail.getOrderNo(), detail)) |
| | | && getPendingDetailQty(detail) > 0.0D; |
| | | } |
| | | |
| | | /** |
| | | * 订单明细转任务时的批次键。 |
| | | * |
| | | * entryWmsCode 存在时优先作为进仓编号批次,兼容 stationId > 600 的分批放行; |
| | | * entryWmsCode 为空时使用订单号,满足低站点 ERP 出库统一订单化后的批次边界。 |
| | | * |
| | | * stationId <= 600 即使传了 entryWmsCode,也不参与批次键计算; |
| | | * 这是为了满足“低站点批次边界按 orderId 处理”的接口约定。 |
| | | */ |
| | | private String resolvePakoutTaskBatchSeq(String orderNo, OrderDetlPakout detail) { |
| | | if (detail != null && isPendingOutOrderStation(detail.getStationId()) && !Cools.isEmpty(detail.getEntryWmsCode())) { |
| | | return detail.getEntryWmsCode(); |
| | | } |
| | | return orderNo; |
| | | } |
| | | |
| | | /** |
| | | * 从订单明细还原 outOrderBatch 需要的参数。 |
| | | * |
| | | * batchSeq 必须强制改为 entryWmsCode,而不是沿用 ERP 原始 batchSeq; |
| | | * 这样后续 WCS 下发守卫才能以进仓编号为顺序边界。 |
| | | * batchSeq 必须强制改为任务批次键: |
| | | * - 高站点:entryWmsCode,WCS 下发守卫以进仓编号为顺序边界; |
| | | * - 低站点:orderNo,整张 ERP 订单作为一个批次。 |
| | | */ |
| | | private OutTaskParam buildOutTaskParam(String orderNo, String entryWmsCode, OrderDetlPakout detail) { |
| | | private OutTaskParam buildOutTaskParam(String orderNo, String taskBatchSeq, OrderDetlPakout detail) { |
| | | OutTaskParam param = new OutTaskParam(); |
| | | param.setOrderId(orderNo); |
| | | param.setBatchSeq(entryWmsCode); |
| | | param.setBatchSeq(taskBatchSeq); |
| | | param.setSeq(detail.getSeq()); |
| | | param.setPalletId(detail.getPalletId()); |
| | | param.setStationId(detail.getStationId()); |
| | |
| | | return param; |
| | | } |
| | | |
| | | private Map<String, Object> buildGeneratePendingResult(String orderNo, String entryWmsCode, int generatedTaskCount, int pendingDetailCount, String reason) { |
| | | private Map<String, Object> buildGeneratePendingResult(String orderNo, String taskBatchSeq, int generatedTaskCount, int pendingDetailCount, String reason) { |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("orderNo", orderNo); |
| | | result.put("entryWmsCode", entryWmsCode); |
| | | result.put("batchSeq", taskBatchSeq); |
| | | // 保留旧 key,避免外部日志或页面仍按 entryWmsCode 读取高站点批次。 |
| | | result.put("entryWmsCode", taskBatchSeq); |
| | | result.put("generatedTaskCount", generatedTaskCount); |
| | | result.put("pendingDetailCount", pendingDetailCount); |
| | | if (!Cools.isEmpty(reason)) { |