package com.vincent.rsf.server.manager.schedules; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.vincent.rsf.framework.exception.CoolException; import com.vincent.rsf.server.manager.controller.params.OutStockToTaskParams; import com.vincent.rsf.server.manager.controller.params.PakinItem; import com.vincent.rsf.server.manager.controller.params.WaitPakinParam; import com.vincent.rsf.server.manager.entity.*; import com.vincent.rsf.server.manager.enums.AsnExceStatus; import com.vincent.rsf.server.manager.enums.OrderType; import com.vincent.rsf.server.manager.enums.OrderWorkType; import com.vincent.rsf.server.manager.enums.TaskStsType; import com.vincent.rsf.server.manager.service.*; import com.vincent.rsf.server.manager.utils.LocManageUtil; import com.vincent.rsf.server.system.constant.GlobalConfigCode; import com.vincent.rsf.server.system.constant.SerialRuleCode; import com.vincent.rsf.server.system.entity.Config; import com.vincent.rsf.server.system.service.ConfigService; import com.vincent.rsf.server.system.utils.SerialRuleUtils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import java.util.*; import java.util.stream.Collectors; /** * 指定物料自动化定时任务:可配置物料编码后, * 1)有库存时自动生成全版出库单; * 2)该物料出库单自动下发任务; * 3)RCS 入库通知时(可选)自动组托,数量可配置。 */ @Slf4j @Component public class MaterialAutoSchedules { private static final Long SYSTEM_USER_ID = 1L; private static final String DEFAULT_SITE_NO = "1001"; @Autowired private ConfigService configService; @Autowired private OutStockService outStockService; @Autowired private AsnOrderItemService asnOrderItemService; @Autowired private LocItemService locItemService; @Autowired private LocService locService; @Autowired private MatnrService matnrService; @Autowired private TaskService taskService; @Autowired private TaskItemService taskItemService; @Autowired private com.vincent.rsf.server.api.service.MobileService mobileService; @Autowired private AsnOrderService asnOrderService; @Autowired private WaitPakinItemService waitPakinItemService; /** * 定时任务1:指定物料有库存时自动生成全版出库单 * 配置:AUTO_FULL_OUT_MATNR_CODE(物料编码)、AUTO_FULL_OUT_ENABLED(true 启用) */ @Scheduled(cron = "0/35 * * * * ?") @Transactional(rollbackFor = Exception.class) public void autoCreateFullOutOrder() { Config enabledConfig = configService.getOne(new LambdaQueryWrapper().eq(Config::getFlag, GlobalConfigCode.AUTO_FULL_OUT_ENABLED)); if (enabledConfig == null || !Boolean.parseBoolean(enabledConfig.getVal())) { return; } Config matnrConfig = configService.getOne(new LambdaQueryWrapper().eq(Config::getFlag, GlobalConfigCode.AUTO_FULL_OUT_MATNR_CODE)); if (matnrConfig == null || StringUtils.isBlank(matnrConfig.getVal())) { return; } String matnrCode = matnrConfig.getVal().trim(); Matnr matnr = matnrService.getOne(new LambdaQueryWrapper().eq(Matnr::getCode, matnrCode)); if (matnr == null) { log.warn("[自动全版出库单] 物料不存在: {}", matnrCode); return; } // 已有该物料未下发的出库单则本轮不再生成,等下发完后再生成(避免重复) List initOrderIds = outStockService.list(new LambdaQueryWrapper() .eq(WkOrder::getType, OrderType.ORDER_OUT.type) .eq(WkOrder::getExceStatus, AsnExceStatus.OUT_STOCK_STATUS_TASK_INIT.val)) .stream().map(WkOrder::getId).collect(Collectors.toList()); if (!initOrderIds.isEmpty()) { long hasMatnr = asnOrderItemService.count(new LambdaQueryWrapper() .in(WkOrderItem::getOrderId, initOrderIds) .eq(WkOrderItem::getMatnrCode, matnrCode)); if (hasMatnr > 0) { return; } } // 按库位分组:该物料、库位状态为 F 的在库;每个库位各生成一张出库单 List items = locItemService.list(new LambdaQueryWrapper() .eq(LocItem::getMatnrCode, matnrCode) .gt(LocItem::getAnfme, 0)); if (items.isEmpty()) { return; } Map> byLocId = items.stream().collect(Collectors.groupingBy(LocItem::getLocId)); for (Long locId : byLocId.keySet()) { Loc loc = locService.getById(locId); if (loc == null || !"F".equals(loc.getUseStatus())) { continue; } List locItems = byLocId.get(locId); double sumQty = locItems.stream().mapToDouble(li -> li.getAnfme() != null ? li.getAnfme() : 0).sum(); if (sumQty <= 0) continue; try { String ruleCode = SerialRuleUtils.generateRuleCode(SerialRuleCode.SYS_OUT_STOCK_CODE, null); if (StringUtils.isBlank(ruleCode)) { log.warn("[自动全版出库单] 出库单编码规则未配置"); break; } WkOrder order = new WkOrder(); order.setCode(ruleCode) .setType(OrderType.ORDER_OUT.type) .setWkType(OrderWorkType.ORDER_WORK_TYPE_STOCK_OUT.type) .setExceStatus(AsnExceStatus.OUT_STOCK_STATUS_TASK_INIT.val) .setAnfme(sumQty) .setWorkQty(0.0) .setQty(0.0) .setCreateBy(SYSTEM_USER_ID) .setUpdateBy(SYSTEM_USER_ID); if (!outStockService.save(order)) { throw new CoolException("出库主单保存失败"); } LocItem first = locItems.get(0); WkOrderItem orderItem = new WkOrderItem(); orderItem.setOrderId(order.getId()) .setOrderCode(order.getCode()) .setMatnrId(matnr.getId()) .setMatnrCode(matnr.getCode()) .setMaktx(matnr.getName()) .setAnfme(sumQty) .setWorkQty(0.0) .setQty(0.0) .setStockUnit(first.getUnit() != null ? first.getUnit() : "个") .setPurUnit(first.getUnit() != null ? first.getUnit() : "个") .setSplrBatch(first.getSplrBatch()) .setBatch(first.getBatch()) .setFieldsIndex(first.getFieldsIndex()) .setCreateBy(SYSTEM_USER_ID) .setUpdateBy(SYSTEM_USER_ID); if (!asnOrderItemService.save(orderItem)) { throw new CoolException("出库明细保存失败"); } log.info("[自动全版出库单] 已生成出库单: {}, 库位: {}, 物料: {}, 数量: {}", order.getCode(), loc.getCode(), matnrCode, sumQty); } catch (Exception e) { log.error("[自动全版出库单] 生成失败, 库位: {}, 物料: {}", loc.getCode(), matnrCode, e); } } } /** * 定时任务2:该物料出库单自动下发任务(每 1 分钟) * 配置:AUTO_FULL_OUT_DISPATCH_ENABLED(true 启用)、AUTO_FULL_OUT_MATNR_CODE */ @Scheduled(cron = "0 0/1 * * * ?") @Transactional(rollbackFor = Exception.class) public void autoDispatchOutTask() { Config enabledConfig = configService.getOne(new LambdaQueryWrapper().eq(Config::getFlag, GlobalConfigCode.AUTO_FULL_OUT_DISPATCH_ENABLED)); if (enabledConfig == null || !Boolean.parseBoolean(enabledConfig.getVal())) { return; } Config matnrConfig = configService.getOne(new LambdaQueryWrapper().eq(Config::getFlag, GlobalConfigCode.AUTO_FULL_OUT_MATNR_CODE)); if (matnrConfig == null || StringUtils.isBlank(matnrConfig.getVal())) { return; } String matnrCode = matnrConfig.getVal().trim(); List orders = outStockService.list(new LambdaQueryWrapper() .eq(WkOrder::getType, OrderType.ORDER_OUT.type) .eq(WkOrder::getExceStatus, AsnExceStatus.OUT_STOCK_STATUS_TASK_INIT.val)); for (WkOrder order : orders) { List orderItems = asnOrderItemService.list(new LambdaQueryWrapper() .eq(WkOrderItem::getOrderId, order.getId()) .eq(WkOrderItem::getMatnrCode, matnrCode)); if (orderItems.isEmpty()) continue; List paramsList = new ArrayList<>(); for (WkOrderItem oi : orderItems) { double remaining = (oi.getAnfme() != null ? oi.getAnfme() : 0) - (oi.getWorkQty() != null ? oi.getWorkQty() : 0); if (remaining <= 0) continue; String batch = oi.getSplrBatch() != null ? oi.getSplrBatch() : oi.getBatch(); List locItems = LocManageUtil.getFirstInFirstOutItemList(oi.getMatnrCode(), batch, remaining); for (LocItem locItem : locItems) { if (remaining <= 0) break; Loc loc = locService.getById(locItem.getLocId()); if (loc == null || !"F".equals(loc.getUseStatus())) continue; double outQty = Math.min(remaining, locItem.getAnfme() != null ? locItem.getAnfme() : 0); if (outQty <= 0) continue; OutStockToTaskParams param = new OutStockToTaskParams(); param.setId(locItem.getId()); param.setLocCode(loc.getCode()); param.setOutQty(outQty); param.setSiteNo(DEFAULT_SITE_NO); param.setBatch(locItem.getBatch()); paramsList.add(param); remaining -= outQty; } } if (paramsList.isEmpty()) continue; try { outStockService.genOutStockTask(paramsList, SYSTEM_USER_ID, order.getId()); List taskItems = taskItemService.list(new LambdaQueryWrapper().eq(TaskItem::getSourceId, order.getId())); if (!taskItems.isEmpty()) { Set taskIds = taskItems.stream().map(TaskItem::getTaskId).collect(Collectors.toSet()); List tasks = taskService.listByIds(taskIds).stream() .filter(t -> TaskStsType.GENERATE_OUT.id.equals(t.getTaskStatus())) .collect(Collectors.toList()); if (!tasks.isEmpty()) { taskService.pubTaskToWcs(tasks); log.info("[自动下发任务] 出库单: {} 已下发任务", order.getCode()); } } } catch (Exception e) { log.error("[自动下发任务] 出库单: {} 下发失败", order.getCode(), e); } } } /** * 定时任务3:无订单组托 + 自动生成入库单(仅针对配置物料,) * 先按配置物料与数量做无订单组托,再生成入库单并关联组托明细,便于 RCS 入库闭环。 * 配置:AUTO_PAKIN_ON_ASN_ENABLED(true)、AUTO_FULL_OUT_MATNR_CODE、AUTO_PAKIN_QTY(数量) */ @Scheduled(cron = "0/35 * * * * ?") @Transactional(rollbackFor = Exception.class) public void autoPakinOnInbound() { Config enabledConfig = configService.getOne(new LambdaQueryWrapper().eq(Config::getFlag, GlobalConfigCode.AUTO_PAKIN_ON_ASN_ENABLED)); if (enabledConfig == null || !Boolean.parseBoolean(enabledConfig.getVal())) { return; } Config matnrConfig = configService.getOne(new LambdaQueryWrapper().eq(Config::getFlag, GlobalConfigCode.AUTO_FULL_OUT_MATNR_CODE)); if (matnrConfig == null || StringUtils.isBlank(matnrConfig.getVal())) { return; } Config qtyConfig = configService.getOne(new LambdaQueryWrapper().eq(Config::getFlag, GlobalConfigCode.AUTO_PAKIN_QTY)); double autoQty = 1.0; if (qtyConfig != null && StringUtils.isNotBlank(qtyConfig.getVal())) { try { autoQty = Double.parseDouble(qtyConfig.getVal().trim()); if (autoQty <= 0) autoQty = 1.0; } catch (NumberFormatException e) { // ignore } } String matnrCode = matnrConfig.getVal().trim(); Matnr matnr = matnrService.getOne(new LambdaQueryWrapper().eq(Matnr::getCode, matnrCode)); if (matnr == null) { log.warn("[无订单自动组托] 物料不存在: {}", matnrCode); return; } // 已有该物料未执行入库单则本轮不继续生成,避免堆积 List initAsnIds = asnOrderService.list(new LambdaQueryWrapper() .eq(WkOrder::getType, OrderType.ORDER_IN.type) .eq(WkOrder::getExceStatus, AsnExceStatus.ASN_EXCE_STATUS_UN_EXCE.val)) .stream().map(WkOrder::getId).collect(Collectors.toList()); if (!initAsnIds.isEmpty()) { long hasMatnr = asnOrderItemService.count(new LambdaQueryWrapper() .in(WkOrderItem::getOrderId, initAsnIds) .eq(WkOrderItem::getMatnrCode, matnrCode)); if (hasMatnr > 0) { return; } } // 1)无订单组托:仅物料 + 数量,不传 asnCode/id List pakinItems = new ArrayList<>(); PakinItem pi = new PakinItem(); pi.setMatnrId(matnr.getId()); pi.setReceiptQty(autoQty); pi.setAsnCode(null); pi.setId(null); pakinItems.add(pi); String barcode = "AUTO-PAKIN-" + System.currentTimeMillis(); WaitPakinParam param = new WaitPakinParam(); param.setBarcode(barcode); param.setItems(pakinItems); WaitPakin waitPakin; try { waitPakin = mobileService.mergeItems(param, SYSTEM_USER_ID); } catch (Exception e) { log.warn("[无订单自动组托] 组托失败: {}", e.getMessage()); throw e; // 重新抛出,避免事务被标记 rollback-only 后仍尝试提交导致 UnexpectedRollbackException } // 2)自动生成入库单(一条明细,配置物料 + 数量) String ruleCode = SerialRuleUtils.generateRuleCode(SerialRuleCode.SYS_ASN_ORDER, null); if (StringUtils.isBlank(ruleCode)) { log.warn("[无订单自动组托] 入库单编码规则未配置"); return; } WkOrder order = new WkOrder(); order.setCode(ruleCode) .setType(OrderType.ORDER_IN.type) .setExceStatus(AsnExceStatus.ASN_EXCE_STATUS_UN_EXCE.val) .setAnfme(autoQty) .setWorkQty(0.0) .setQty(0.0) .setCreateBy(SYSTEM_USER_ID) .setUpdateBy(SYSTEM_USER_ID); if (!asnOrderService.save(order)) { throw new CoolException("入库主单保存失败"); } WkOrderItem orderItem = new WkOrderItem(); orderItem.setOrderId(order.getId()) .setOrderCode(order.getCode()) .setMatnrId(matnr.getId()) .setMatnrCode(matnr.getCode()) .setMaktx(matnr.getName()) .setAnfme(autoQty) .setWorkQty(0.0) .setQty(0.0) .setStockUnit(matnr.getStockUnit() != null ? matnr.getStockUnit() : "个") .setPurUnit(matnr.getPurUnit() != null ? matnr.getPurUnit() : "个") .setFieldsIndex(matnr.getFieldsIndex()) .setCreateBy(SYSTEM_USER_ID) .setUpdateBy(SYSTEM_USER_ID); if (!asnOrderItemService.save(orderItem)) { throw new CoolException("入库明细保存失败"); } // 3)关联组托明细到入库单(asnId / asnCode / asnItemId) boolean updated = waitPakinItemService.update(new LambdaUpdateWrapper() .eq(WaitPakinItem::getPakinId, waitPakin.getId()) .set(WaitPakinItem::getAsnId, order.getId()) .set(WaitPakinItem::getAsnCode, order.getCode()) .set(WaitPakinItem::getAsnItemId, orderItem.getId())); if (!updated) { log.warn("[无订单自动组托] 组托明细关联入库单失败, pakinId={}", waitPakin.getId()); } log.info("[无订单自动组托] 已组托并生成入库单: {}, 物料: {}, 数量: {}", order.getCode(), matnrCode, autoQty); } }