chen.lin
16 小时以前 82065a03737fa1370eb9f4f01ab5332933baf08a
云仓WMS+RCS+自动入库临时方法配置
2个文件已添加
24个文件已修改
1321 ■■■■ 已修改文件
rsf-admin/src/i18n/en.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/zh.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/asnOrder/AsnOrderEdit.jsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/asnOrder/AsnOrderItemList.jsx 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/asnOrder/AsnOrderList.jsx 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/MobileServiceImpl.java 89 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/ReceiveMsgServiceImpl.java 114 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/WcsServiceImpl.java 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/WkOrderController.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/WkOrderItemController.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/params/WaitPakinParam.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/WarehouseAreasItem.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/WkOrder.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/WkOrderItem.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/enums/AsnExceStatus.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/MaterialAutoSchedules.java 339 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/ScheduleJobs.java 275 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/TaskSchedules.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/AsnOrderItemService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/AsnOrderService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/AsnOrderItemServiceImpl.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/AsnOrderServiceImpl.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java 47 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/WaitPakinServiceImpl.java 132 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/constant/GlobalConfigCode.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
version/db/material_auto_config.sql 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/en.js
@@ -605,6 +605,7 @@
                wkType: "wkType",
                anfme: "anfme",
                qty: "qty",
                palletQty: "Pallet Qty",
                logisNo: "logisNo",
                arrTime: "Arrived",
                rleStatus: "Release",
@@ -774,6 +775,7 @@
                purUnit: "purUnit",
                unit: 'Unit',
                qty: "qty",
                palletQty: "Pallet Qty",
                safeQty: 'Safe Qty',
                disQty: 'Def Qty',
                splrCode: "splrCode",
rsf-admin/src/i18n/zh.js
@@ -658,6 +658,7 @@
                wkType: "业务类型",
                anfme: "计划数量",
                qty: "完成数量",
                palletQty: "组托数量",
                logisNo: "物流单号",
                arrTime: "预计到达时间",
                rleStatus: "释放状态",
@@ -809,6 +810,7 @@
                purUnit: "采购单位",
                unit: '单位',
                qty: "完成数",
                palletQty: "组托数量",
                safeQty: '合格数',
                disQty: '不合格数',
                splrBatch: "批次",
rsf-admin/src/page/orders/asnOrder/AsnOrderEdit.jsx
@@ -122,6 +122,12 @@
                                    readOnly
                                    parse={v => v}
                                />
                                <TextInput
                                    label="table.field.asnOrder.palletQty"
                                    source="palletQty"
                                    readOnly
                                    parse={v => v}
                                />
                                <DateInput
                                    label="table.field.asnOrder.arrTime"
                                    source="arrTime"
rsf-admin/src/page/orders/asnOrder/AsnOrderItemList.jsx
@@ -192,6 +192,7 @@
        <TextField source="model" label="table.field.asnOrderItem.model" />,
        <NumberField source="anfme" label="table.field.asnOrderItem.anfme" options={{ minimumFractionDigits: 2, maximumFractionDigits: 2 }} />,
        <NumberField source="qty" label="table.field.asnOrderItem.qty" options={{ minimumFractionDigits: 2, maximumFractionDigits: 2 }} />,
        <NumberField source="palletQty" label="table.field.asnOrderItem.palletQty" options={{ minimumFractionDigits: 2, maximumFractionDigits: 2 }} />,
        <TextField source="stockUnit" label="table.field.asnOrderItem.stockUnit" />,
        <NumberField source="purQty" label="table.field.asnOrderItem.purQty" options={{ minimumFractionDigits: 2, maximumFractionDigits: 2 }} />,
        <TextField source="purUnit" label="table.field.asnOrderItem.purUnit" />,
@@ -202,7 +203,9 @@
        <TextField source="trackCode" label="table.field.asnOrderItem.barcode" />,
        // <TextField source="packName" label="table.field.asnOrderItem.packName" />, // 包装已注释
      ]
      const fields = data.map(el => <TextField key={el.fields} source={`extendFields.[${el.fields}]`} label={el.fieldsAlise} />)
      const fields = data
        .filter(el => el.fields !== 'crushNo' && el.fieldsAlise !== '现品票号')
        .map(el => <TextField key={el.fields} source={`extendFields.[${el.fields}]`} label={el.fieldsAlise} />)
      const lastArr = [
        <DateField source="updateTime" label="common.field.updateTime" showTime />,
        <ReferenceField source="updateBy" label="common.field.updateBy" reference="user" link={false} sortable={false}>
@@ -238,7 +241,7 @@
          preferenceKey='asnOrderItem'
          bulkActionButtons={false}
          rowClick={(id, resource, record) => false}
          omit={['id', 'orderId', 'orderCode', 'poDetlId', 'poDetlCode', 'matnrId', 'spec', 'model', 'purQty', 'purUnit', 'qrcode', 'trackCode', 'splrCode', 'platWorkCode', 'projectCode', 'platItemId', 'isptResult$', 'packName']}
          omit={['id', 'orderId', 'orderCode', 'poDetlId', 'poDetlCode', 'matnrId', 'spec', 'model', 'purQty', 'purUnit', 'qrcode', 'trackCode', 'splrCode', 'platWorkCode', 'projectCode', 'platItemId', 'fieldsIndex', 'isptResult$', 'packName']}
        >
          {columns.map((column) => column)}
        </StyledDatagrid>}
rsf-admin/src/page/orders/asnOrder/AsnOrderList.jsx
@@ -168,6 +168,7 @@
          <TextField cellClassName="wkType" source="wkType$" label="table.field.asnOrder.wkType" />
          <NumberField source="anfme" label="table.field.asnOrder.anfme" options={{ minimumFractionDigits: 2, maximumFractionDigits: 2 }} />
          <NumberField source="qty" label="table.field.asnOrder.qty" options={{ minimumFractionDigits: 2, maximumFractionDigits: 2 }} />
          <NumberField source="palletQty" label="table.field.asnOrder.palletQty" options={{ minimumFractionDigits: 2, maximumFractionDigits: 2 }} />
          <DateField source="arrTime" label="table.field.asnOrder.arrTime" showTime />
          <TextField source="rleStatus$" label="table.field.asnOrder.rleStatus" sortable={false} />
          <TextField source="logisNo" label="table.field.asnOrder.logisNo" />
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/MobileServiceImpl.java
@@ -30,6 +30,7 @@
import com.vincent.rsf.server.system.entity.*;
import com.vincent.rsf.server.system.mapper.FieldsMapper;
import com.vincent.rsf.server.system.mapper.TenantMapper;
import com.vincent.rsf.server.system.entity.Config;
import com.vincent.rsf.server.system.service.ConfigService;
import com.vincent.rsf.server.system.service.FieldsItemService;
import com.vincent.rsf.server.system.service.UserLoginService;
@@ -260,8 +261,10 @@
        }
        //TODO /**收货数量累加,1. 会出超收情况 2. 会有收货不足情况*/
        Double rcptedQty = QuantityUtils.add(wkOrder.getQty(), receiptQty);
        // 新顺序:未执行(组托)→任务执行中→已完成,不再设置执行中/收货完成
        wkOrder.setQty(rcptedQty); // .setExceStatus(AsnExceStatus.ASN_EXCE_STATUS_EXCE_ING.val)
        wkOrder.setQty(rcptedQty);
        if (AsnExceStatus.ASN_EXCE_STATUS_UN_EXCE.val.equals(wkOrder.getExceStatus())) {
            wkOrder.setExceStatus(AsnExceStatus.ASN_EXCE_STATUS_EXCE_ING.val);
        }
        if (!asnOrderMapper.updateById(wkOrder)) {
            throw new CoolException("已收货数量修改失败!!");
        }
@@ -352,18 +355,21 @@
            if (asnOrderItemMapper.updateById(orderItem) < 1) {
                throw new CoolException("通知单明细数量修改失败!!");
            }
            // 收货区已停用,不再保存至收货区
            // extracted(loginUserId, dto, areasItem, orderItem, wkOrder, matnr);
            // DirectWaitPakin 启用时保存至收货区,形成闭环;未启用时不写收货区
            Config directPakinConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.DIRECT_WAIT_PAKIN));
            if (directPakinConfig != null && Boolean.parseBoolean(directPakinConfig.getVal())) {
                extracted(loginUserId, dto, areasItem, orderItem, wkOrder, matnr);
            }
        }
        // 新顺序:未执行(组托)→任务执行中→已完成,不再设置收货完成
        // WkOrder order = asnOrderMapper.getOne(new LambdaQueryWrapper<WkOrder>().eq(WkOrder::getCode, asnCode));
        // if (order.getQty().compareTo(order.getAnfme()) >= 0.00) {
        //     order.setExceStatus(AsnExceStatus.ASN_EXCE_STATUS_RECEIPT_DONE.val).setRleStatus(Short.valueOf("1"));
        //     if (!asnOrderMapper.updateById(order)) {
        //         throw new CoolException("订单状态修改失败!!");
        //     }
        // }
        // 已收数量 >= 计划数量时置为收货完成(2)
        WkOrder order = asnOrderMapper.getOne(new LambdaQueryWrapper<WkOrder>().eq(WkOrder::getCode, asnCode));
        if (order != null && order.getAnfme() != null && QuantityUtils.compare(order.getQty(), order.getAnfme()) >= 0) {
            order.setExceStatus(AsnExceStatus.ASN_EXCE_STATUS_RECEIPT_DONE.val).setRleStatus(Short.valueOf("1"));
            if (!asnOrderMapper.updateById(order)) {
                throw new CoolException("订单状态修改失败!!");
            }
        }
        return R.ok("收货成功!!");
    }
@@ -512,13 +518,16 @@
     * @description 获取收货区
     * @time 2025/3/11 10:12
     */
    /** 收货区已停用,返回空列表 */
    /** DirectWaitPakin 启用时返回收货区列表,未启用时返回空列表(不影响组托与云仓入库流程) */
    @Override
    public R getReceiptAreas() {
        // List<WarehouseAreas> areas = warehouseAreasService.list(new LambdaQueryWrapper<WarehouseAreas>()
        //         .eq(WarehouseAreas::getType, WarehouseAreaType.WAREHOUSE_AREA_RECEIPT.type));
        // return R.ok(areas);
        Config directPakinConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.DIRECT_WAIT_PAKIN));
        if (directPakinConfig == null || !Boolean.parseBoolean(directPakinConfig.getVal())) {
        return R.ok(Collections.emptyList());
        }
        List<WarehouseAreas> areas = warehouseAreasService.list(new LambdaQueryWrapper<WarehouseAreas>()
                .eq(WarehouseAreas::getType, WarehouseAreaType.WAREHOUSE_AREA_RECEIPT.type));
        return R.ok(areas);
    }
    /**
@@ -623,15 +632,48 @@
            return R.ok(resultList);
        }
        
        // 收货区已停用:有ASN单号时从订单明细查可组托物料;可组盘数量 = 计划数量 - 已组托数量 - 已上架数量
        // 有ASN单号时:DirectWaitPakin 启用则从收货区查可组托物料,未启用则从订单明细查(可组盘数量 = 计划 - 已组托 - 已上架)
        if (!Cools.isEmpty(asnCode)) {
            Config directPakinConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.DIRECT_WAIT_PAKIN));
            if (directPakinConfig != null && Boolean.parseBoolean(directPakinConfig.getVal())) {
                LambdaQueryWrapper<WarehouseAreasItem> receiptWrapper = new LambdaQueryWrapper<WarehouseAreasItem>().eq(WarehouseAreasItem::getAsnCode, asnCode);
                if (!Cools.isEmpty(matnrCode)) receiptWrapper.eq(WarehouseAreasItem::getMatnrCode, matnrCode);
                if (!Cools.isEmpty(batch)) receiptWrapper.eq(WarehouseAreasItem::getSplrBatch, batch);
                if (!Objects.isNull(fieldIndex)) receiptWrapper.eq(WarehouseAreasItem::getFieldsIndex, fieldIndex);
                if (!Cools.isEmpty(code)) receiptWrapper.eq(WarehouseAreasItem::getTrackCode, code);
                List<WarehouseAreasItem> receiptList = warehouseAreasItemService.list(receiptWrapper);
                // 组托通知档(WaitPakinItem)的已组托数量也要扣减:收货区 workQty 仅反映从收货区组托的部分,从订单直接组托的在此汇总
                Map<Long, Double> waitPakinSumByItemId = new java.util.HashMap<>();
                if (!receiptList.isEmpty() && receiptList.stream().anyMatch(e -> e.getAsnId() != null && e.getAsnItemId() != null)) {
                    List<Long> asnItemIds = receiptList.stream().map(WarehouseAreasItem::getAsnItemId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
                    Long asnId = receiptList.get(0).getAsnId();
                    if (asnId != null && !asnItemIds.isEmpty()) {
                        List<WaitPakinItem> wpItems = waitPakinItemService.list(
                                new LambdaQueryWrapper<WaitPakinItem>().eq(WaitPakinItem::getAsnId, asnId).in(WaitPakinItem::getAsnItemId, asnItemIds).eq(WaitPakinItem::getDeleted, 0));
                        wpItems.forEach(w -> waitPakinSumByItemId.merge(w.getAsnItemId(), w.getAnfme() != null ? w.getAnfme() : 0.0, (a, b) -> (a != null ? a : 0.0) + (b != null ? b : 0.0)));
                        waitPakinSumByItemId.replaceAll((k, v) -> QuantityUtils.roundToScale(v));
                    }
                }
                // 可组托 = 计划(anfme) - 已组托(waitPakinSumByItemId),不叠加 areaWorkQty 避免与组托档汇总重复计算
                receiptList.removeIf(e -> {
                    Double anfme = e.getAnfme() != null ? e.getAnfme() : 0.0;
                    Double qty = e.getQty() != null ? e.getQty() : 0.0;
                    Double alreadyInWaitPakin = e.getAsnItemId() != null ? waitPakinSumByItemId.getOrDefault(e.getAsnItemId(), 0.0) : 0.0;
                    Double totalAlready = QuantityUtils.roundToScale(alreadyInWaitPakin);
                    Double available = QuantityUtils.roundToScale(QuantityUtils.subtract(QuantityUtils.subtract(anfme, totalAlready), qty));
                    e.setWorkQty(totalAlready);
                    e.setAvailablePalletQty(available);
                    return QuantityUtils.compare(available, 0.0) <= 0;
                });
                logger.info("=== 从收货区查询可组托物料(DirectWaitPakin)asnCode: {} 返回 {} 条", asnCode, receiptList.size());
                return R.ok(receiptList);
            }
            WkOrder order = asnOrderMapper.getOne(new LambdaQueryWrapper<WkOrder>().eq(WkOrder::getCode, asnCode));
            if (order == null) {
                logger.info("未找到ASN单号: {}", asnCode);
                return R.ok(Collections.emptyList());
            }
            // 按明细汇总已组托数量(组托数量不会因改单而变)
            List<WaitPakinItem> pakinItems = waitPakinItemService.list(new LambdaQueryWrapper<WaitPakinItem>().eq(WaitPakinItem::getAsnId, order.getId()));
            List<WaitPakinItem> pakinItems = waitPakinItemService.list(new LambdaQueryWrapper<WaitPakinItem>().eq(WaitPakinItem::getAsnId, order.getId()).eq(WaitPakinItem::getDeleted, 0));
            Map<Long, Double> palletizedByItemId = pakinItems.stream()
                    .collect(Collectors.groupingBy(WaitPakinItem::getAsnItemId, Collectors.summingDouble(w -> w.getAnfme() != null ? w.getAnfme() : 0.0)));
            palletizedByItemId.replaceAll((k, v) -> QuantityUtils.roundToScale(v));
@@ -645,8 +687,10 @@
            for (WkOrderItem oi : orderItems) {
                Double anfme = QuantityUtils.roundToScale(oi.getAnfme() != null ? oi.getAnfme() : 0.0);
                Double qty = QuantityUtils.roundToScale(oi.getQty() != null ? oi.getQty() : 0.0);
                Double workQty = palletizedByItemId.getOrDefault(oi.getId(), 0.0); // 已组托数量
                if (QuantityUtils.compare(QuantityUtils.subtract(QuantityUtils.subtract(anfme, workQty), qty), 0.0) <= 0) continue; // 可组盘数量<=0 不返回
                Double workQty = palletizedByItemId.getOrDefault(oi.getId(), 0.0);
                // 可组盘数量 = 计划 - 已组托(不扣减明细完成数量 qty,避免云仓同步完成数量=计划后导致可组托被算成 0)
                Double availablePalletQty = QuantityUtils.roundToScale(QuantityUtils.subtract(anfme, workQty));
                if (QuantityUtils.compare(availablePalletQty, 0.0) <= 0) continue;
                WarehouseAreasItem v = new WarehouseAreasItem();
                v.setId(oi.getId());
                v.setAsnItemId(oi.getId());
@@ -655,6 +699,7 @@
                v.setAnfme(anfme);
                v.setQty(qty);
                v.setWorkQty(QuantityUtils.roundToScale(workQty));
                v.setAvailablePalletQty(availablePalletQty);
                v.setMatnrCode(oi.getMatnrCode());
                v.setMaktx(oi.getMaktx());
                v.setSplrBatch(oi.getSplrBatch());
@@ -669,7 +714,7 @@
                }
                list.add(v);
            }
            logger.info("=== 从订单明细查询可组托物料(收货区已停用)asnCode: {} 返回 {} 条", asnCode, list.size());
            logger.info("=== 从订单明细查询可组托物料 asnCode: {} 返回 {} 条", asnCode, list.size());
            return R.ok(list);
        }
        
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/ReceiveMsgServiceImpl.java
@@ -26,7 +26,10 @@
import com.vincent.rsf.server.manager.service.*;
import com.vincent.rsf.server.manager.service.impl.MatnrServiceImpl;
import com.vincent.rsf.server.system.constant.DictTypeCode;
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.entity.DictData;
import com.vincent.rsf.server.system.entity.DictType;
import com.vincent.rsf.server.system.entity.Fields;
@@ -113,6 +116,8 @@
    private WaitPakinItemService waitPakinItemService;
    @Autowired
    private WarehouseAreasItemService warehouseAreasItemService;
    @Autowired
    private ConfigService configService;
    /**
@@ -461,22 +466,27 @@
                            .eq(WkOrder::getPoCode, syncOrder.getOrderInternalCode()));
                    if (!Objects.isNull(order)) {
                        // 仅未执行状态可被 order/add 修改(入库未执行、出库任务初始)
                        List<Short> editableStatus = Arrays.asList(AsnExceStatus.ASN_EXCE_STATUS_UN_EXCE.val, AsnExceStatus.OUT_STOCK_STATUS_TASK_INIT.val);
                        List<Short> editableStatus = Arrays.asList(AsnExceStatus.ASN_EXCE_STATUS_UN_EXCE.val
                                ,AsnExceStatus.ASN_EXCE_STATUS_UN_EXCE.val
                                ,AsnExceStatus.ASN_EXCE_STATUS_RECEIPT_DONE.val
                                , AsnExceStatus.OUT_STOCK_STATUS_TASK_INIT.val);
                        if (!editableStatus.contains(order.getExceStatus())) {
                            throw new CoolException("仅未执行状态的单据可修改!!");
                        }
                        // 存在则修改(1 和 2 均走此处),组托校验在 mergeOrderWithPakin/updateOrderNoPakin 内
                        long pakinCount = waitPakinItemService.count(new LambdaQueryWrapper<WaitPakinItem>()
                                .eq(WaitPakinItem::getAsnId, order.getId()));
                                .eq(WaitPakinItem::getAsnId, order.getId()).eq(WaitPakinItem::getDeleted, 0));
                        if (pakinCount > 0) {
                            // 已组托:按 lineId(platItemId)合并,校验数量与删除
                            mergeOrderWithPakin(order, syncOrder, resolvedOrderType, typeCode, loginUserId);
                            // 收货区已停用 // syncReceiptAreaByOrder(order.getId());
                            return; // 本单已处理,跳过下方“新建主单+明细”
                            if (isDirectWaitPakin()) {
                                syncReceiptAreaByOrder(order.getId());
                        }
                        // 未组托:在原单上更新主单+明细,保留 exceStatus/qty/workQty,避免再次触发定时任务导致重复收货
                            return;
                        }
                        updateOrderNoPakin(order, syncOrder, loginUserId);
                        // 收货区已停用 // syncReceiptAreaByOrder(order.getId());
                        if (isDirectWaitPakin()) {
                            syncReceiptAreaByOrder(order.getId());
                        }
                        return;
                    } else if (Integer.valueOf(2).equals(syncOrder.getOperateType())) {
                        // 仅 operateType=2 时要求单据必须存在
@@ -551,7 +561,7 @@
            }
        }
        List<WkOrderItem> existingItems = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>().eq(WkOrderItem::getOrderId, order.getId()));
        List<WaitPakinItem> pakinItems = waitPakinItemService.list(new LambdaQueryWrapper<WaitPakinItem>().eq(WaitPakinItem::getAsnId, order.getId()));
        List<WaitPakinItem> pakinItems = waitPakinItemService.list(new LambdaQueryWrapper<WaitPakinItem>().eq(WaitPakinItem::getAsnId, order.getId()).eq(WaitPakinItem::getDeleted, 0));
        Map<Long, Double> palletizedByItemId = pakinItems.stream()
                .collect(Collectors.groupingBy(WaitPakinItem::getAsnItemId, Collectors.summingDouble(w -> w.getAnfme() != null ? w.getAnfme() : 0.0)));
        palletizedByItemId.replaceAll((k, v) -> QuantityUtils.roundToScale(v));
@@ -567,6 +577,15 @@
                .map(e -> StringUtils.isNotBlank(e.getPlatItemId()) ? e.getPlatItemId().trim() : null)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        // 已组托单据修改只能增加数量不能减少数量:云仓下发的总数量不得小于已组托总量
        double totalPalletized = QuantityUtils.roundToScale(pakinItems.stream().mapToDouble(w -> w.getAnfme() != null ? w.getAnfme() : 0.0).sum());
        double incomingTotal = syncOrder.getOrderItems() == null ? 0.0 : syncOrder.getOrderItems().stream()
                .mapToDouble(it -> it.getAnfme() != null ? it.getAnfme() : 0.0).sum();
        incomingTotal = QuantityUtils.roundToScale(incomingTotal);
        if (QuantityUtils.compare(incomingTotal, totalPalletized) < 0) {
            throw new CoolException("已组托单据修改只能增加数量不能减少数量(当前已组托总量 " + totalPalletized + ",下发总量 " + incomingTotal + ")!!");
        }
        for (WkOrderItem existing : existingItems) {
            String lineId = StringUtils.isNotBlank(existing.getPlatItemId()) ? existing.getPlatItemId().trim() : null;
@@ -603,7 +622,16 @@
                continue;
            }
            SyncOrdersItem inc = incomingByLineId.get(lineId);
            existing.setAnfme(QuantityUtils.roundToScale(inc.getAnfme() != null ? inc.getAnfme() : existing.getAnfme()));
            Double newAnfme = QuantityUtils.roundToScale(inc.getAnfme() != null ? inc.getAnfme() : existing.getAnfme());
            existing.setAnfme(newAnfme);
            if (inc.getQty() != null) {
                existing.setQty(QuantityUtils.roundToScale(inc.getQty()));
            } else {
                Double curQty = existing.getQty() != null ? existing.getQty() : 0.0;
                if (QuantityUtils.compare(curQty, 0.0) != 0) {
                    existing.setQty(QuantityUtils.roundToScale(newAnfme));
                }
            }
            existing.setMaktx(inc.getMaktx());
            existing.setSpec(inc.getSpec());
            existing.setModel(inc.getModel());
@@ -635,6 +663,9 @@
        Double sum = QuantityUtils.roundToScale(asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>().eq(WkOrderItem::getOrderId, order.getId()))
                .stream().mapToDouble(WkOrderItem::getAnfme).sum());
        order.setAnfme(sum);
        List<WkOrderItem> afterItems = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>().eq(WkOrderItem::getOrderId, order.getId()));
        double qtySum = afterItems.stream().mapToDouble(oi -> oi.getQty() != null ? oi.getQty() : 0.0).sum();
        order.setQty(QuantityUtils.roundToScale(qtySum));
        asnOrderService.updateById(order);
    }
@@ -672,12 +703,23 @@
        for (WkOrderItem existing : existingItems) {
            String lineId = StringUtils.isNotBlank(existing.getPlatItemId()) ? existing.getPlatItemId().trim() : null;
            if (lineId == null || !incomingByLineId.containsKey(lineId)) {
                // 收货区已停用 // warehouseAreasItemService.remove(new LambdaQueryWrapper<WarehouseAreasItem>().eq(WarehouseAreasItem::getAsnItemId, existing.getId()));
                if (isDirectWaitPakin()) {
                    warehouseAreasItemService.remove(new LambdaQueryWrapper<WarehouseAreasItem>().eq(WarehouseAreasItem::getAsnItemId, existing.getId()));
                }
                asnOrderItemService.removeById(existing.getId());
                continue;
            }
            SyncOrdersItem inc = incomingByLineId.get(lineId);
            existing.setAnfme(QuantityUtils.roundToScale(inc.getAnfme() != null ? inc.getAnfme() : existing.getAnfme()));
            Double newAnfme = QuantityUtils.roundToScale(inc.getAnfme() != null ? inc.getAnfme() : existing.getAnfme());
            existing.setAnfme(newAnfme);
            if (inc.getQty() != null) {
                existing.setQty(QuantityUtils.roundToScale(inc.getQty()));
            } else {
                Double curQty = existing.getQty() != null ? existing.getQty() : 0.0;
                if (QuantityUtils.compare(curQty, 0.0) != 0) {
                    existing.setQty(QuantityUtils.roundToScale(newAnfme));
                }
            }
            existing.setMaktx(inc.getMaktx());
            existing.setSpec(inc.getSpec());
            existing.setModel(inc.getModel());
@@ -709,31 +751,37 @@
        Double sum = QuantityUtils.roundToScale(asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>().eq(WkOrderItem::getOrderId, order.getId()))
                .stream().mapToDouble(WkOrderItem::getAnfme).sum());
        order.setAnfme(sum);
        List<WkOrderItem> afterItems = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>().eq(WkOrderItem::getOrderId, order.getId()));
        double qtySum = afterItems.stream().mapToDouble(oi -> oi.getQty() != null ? oi.getQty() : 0.0).sum();
        order.setQty(QuantityUtils.roundToScale(qtySum));
        asnOrderService.updateById(order);
    }
    /** 收货区已停用,方法整体注释
     * 订单修改后同步收货区:按 asnItemId 将收货区记录的 anfme 更新为订单明细的 anfme。
     */
//    private void syncReceiptAreaByOrder(Long orderId) {
//        List<WkOrderItem> orderItems = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>().eq(WkOrderItem::getOrderId, orderId));
//        if (orderItems.isEmpty()) {
//            return;
//        }
//        List<WarehouseAreasItem> areaItems = warehouseAreasItemService.list(new LambdaQueryWrapper<WarehouseAreasItem>().eq(WarehouseAreasItem::getAsnId, orderId));
//        Map<Long, Double> itemAnfme = orderItems.stream().collect(Collectors.toMap(WkOrderItem::getId, i -> i.getAnfme() != null ? i.getAnfme() : 0.0, (a, b) -> b));
//        for (WarehouseAreasItem area : areaItems) {
//            if (area.getAsnItemId() == null || !itemAnfme.containsKey(area.getAsnItemId())) {
//                continue;
//            }
//            Double anfme = itemAnfme.get(area.getAsnItemId());
//            if (area.getAnfme() != null && area.getAnfme().equals(anfme)) {
//                continue;
//            }
//            area.setAnfme(anfme);
//            warehouseAreasItemService.updateById(area);
//        }
//    }
    /** DirectWaitPakin 启用时:订单修改后同步收货区,按 asnItemId 将收货区记录的 anfme 更新为订单明细的 anfme。 */
    private void syncReceiptAreaByOrder(Long orderId) {
        List<WkOrderItem> orderItems = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>().eq(WkOrderItem::getOrderId, orderId));
        if (orderItems.isEmpty()) {
            return;
        }
        List<WarehouseAreasItem> areaItems = warehouseAreasItemService.list(new LambdaQueryWrapper<WarehouseAreasItem>().eq(WarehouseAreasItem::getAsnId, orderId));
        Map<Long, Double> itemAnfme = orderItems.stream().collect(Collectors.toMap(WkOrderItem::getId, i -> i.getAnfme() != null ? i.getAnfme() : 0.0, (a, b) -> b));
        for (WarehouseAreasItem area : areaItems) {
            if (area.getAsnItemId() == null || !itemAnfme.containsKey(area.getAsnItemId())) {
                continue;
            }
            Double anfme = itemAnfme.get(area.getAsnItemId());
            if (area.getAnfme() != null && area.getAnfme().equals(anfme)) {
                continue;
            }
            area.setAnfme(anfme);
            warehouseAreasItemService.updateById(area);
        }
    }
    private boolean isDirectWaitPakin() {
        Config config = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.DIRECT_WAIT_PAKIN));
        return config != null && Boolean.parseBoolean(config.getVal());
    }
    /**
     * @author Ryan
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/WcsServiceImpl.java
@@ -28,7 +28,12 @@
import com.vincent.rsf.server.api.utils.SlaveProperties;
import com.vincent.rsf.server.manager.entity.*;
import com.vincent.rsf.server.manager.service.*;
import com.vincent.rsf.server.manager.controller.params.PakinItem;
import com.vincent.rsf.server.manager.controller.params.WaitPakinParam;
import com.vincent.rsf.server.manager.service.impl.LocServiceImpl;
import com.vincent.rsf.server.system.constant.GlobalConfigCode;
import com.vincent.rsf.server.system.entity.Config;
import com.vincent.rsf.server.system.service.ConfigService;
import com.vincent.rsf.server.system.utils.SystemAuthUtils;
import com.vincent.rsf.server.system.constant.SerialRuleCode;
import com.vincent.rsf.server.manager.enums.LocStsType;
@@ -45,6 +50,7 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
@@ -87,6 +93,16 @@
    private RestTemplate restTemplate;
    @Autowired
    private RemotesInfoProperties.RcsApi rcsApi;
    @Autowired
    private ConfigService configService;
    @Autowired
    private MatnrService matnrService;
    @Autowired
    private AsnOrderService asnOrderService;
    @Autowired
    private AsnOrderItemService asnOrderItemService;
    @Autowired
    private com.vincent.rsf.server.api.service.MobileService mobileService;
    @Override
@@ -306,6 +322,16 @@
        // 2. 若未命中拣料/盘点入库,再校验组托并继续其他入库逻辑
        if (pickInTask == null && checkInTask == null) {
            waitPakin = waitPakinService.getOne(new LambdaQueryWrapper<WaitPakin>()
                    .eq(WaitPakin::getBarcode, param.getBarcode())
                    .in(WaitPakin::getIoStatus, PakinIOStatus.PAKIN_IO_STATUS_DONE.val, PakinIOStatus.PAKIN_IO_STATUS_TASK_EXCE.val));
            // 空托盘无组托时:若配置启用则按 AUTO_FULL_OUT_MATNR_CODE 自动组托并生成入库单,再继续入库任务逻辑
            if (waitPakin == null) {
                tryAutoPakinForBarcode(param.getBarcode());
                waitPakin = waitPakinService.getOne(new LambdaQueryWrapper<WaitPakin>()
                        .eq(WaitPakin::getBarcode, param.getBarcode())
                        .in(WaitPakin::getIoStatus, PakinIOStatus.PAKIN_IO_STATUS_DONE.val, PakinIOStatus.PAKIN_IO_STATUS_TASK_EXCE.val));
            }
            waitPakin = validateWaitPakin(param.getBarcode());
            waitPakinItems = waitPakinItemService.list(
                    new LambdaQueryWrapper<WaitPakinItem>().eq(WaitPakinItem::getPakinId, waitPakin.getId()));
@@ -375,6 +401,9 @@
            throw new RuntimeException(e);
        }
        if (waitPakin == null) {
            throw new CoolException("请检查组拖状态是否完成!!");
        }
        // 创建并保存任务
        Task task = createTask(ruleCode, locNo.getLocNo(), waitPakin.getBarcode(),
                deviceSite.getDeviceSite(), param.getSourceStaNo().toString(), param.getUser());
@@ -399,6 +428,93 @@
    /**
     * RCS 入库申请时若 barcode 无组托且配置启用:按 AUTO_FULL_OUT_MATNR_CODE 无订单组托并生成入库单,便于后续生成入库任务。
     */
    private void tryAutoPakinForBarcode(String barcode) {
        Config enabledConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_PAKIN_ON_ASN_ENABLED));
        if (enabledConfig == null || !Boolean.parseBoolean(enabledConfig.getVal())) {
            return;
        }
        Config matnrConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_FULL_OUT_MATNR_CODE));
        if (matnrConfig == null || StringUtils.isBlank(matnrConfig.getVal())) {
            return;
        }
        Config qtyConfig = configService.getOne(new LambdaQueryWrapper<Config>().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<Matnr>().eq(Matnr::getCode, matnrCode));
        if (matnr == null) {
            log.warn("[RCS入库申请-自动组托] 物料不存在: {}", matnrCode);
            return;
        }
        List<PakinItem> pakinItems = new ArrayList<>();
        PakinItem pi = new PakinItem();
        pi.setMatnrId(matnr.getId());
        pi.setReceiptQty(autoQty);
        pi.setAsnCode(null);
        pi.setId(null);
        pakinItems.add(pi);
        WaitPakinParam param = new WaitPakinParam();
        param.setBarcode(barcode);
        param.setItems(pakinItems);
        WaitPakin waitPakin;
        try {
            waitPakin = mobileService.mergeItems(param, 1L);
        } catch (Exception e) {
            log.warn("[RCS入库申请-自动组托] 组托失败, barcode={}: {}", barcode, e.getMessage());
            return;
        }
        String ruleCode = SerialRuleUtils.generateRuleCode(SerialRuleCode.SYS_ASN_ORDER, null);
        if (StringUtils.isBlank(ruleCode)) {
            log.warn("[RCS入库申请-自动组托] 入库单编码规则未配置");
            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(1L)
                .setUpdateBy(1L);
        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(1L)
                .setUpdateBy(1L);
        if (!asnOrderItemService.save(orderItem)) {
            throw new CoolException("入库明细保存失败");
        }
        waitPakinItemService.update(new LambdaUpdateWrapper<WaitPakinItem>()
                .eq(WaitPakinItem::getPakinId, waitPakin.getId())
                .set(WaitPakinItem::getAsnId, order.getId())
                .set(WaitPakinItem::getAsnCode, order.getCode())
                .set(WaitPakinItem::getAsnItemId, orderItem.getId()));
        log.info("[RCS入库申请-自动组托] 已组托并生成入库单: {}, barcode: {}, 物料: {}, 数量: {}", order.getCode(), barcode, matnrCode, autoQty);
    }
    /**
     * 验证设备站点
     */
    private DeviceSite validateDeviceSite(TaskInParam param) {
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/WkOrderController.java
@@ -57,7 +57,9 @@
        QueryWrapper<WkOrder> queryWrapper = pageParam.buildWrapper(true);
        List<String> asList = Arrays.asList(OrderType.ORDER_IN.type);
        queryWrapper.in("type", asList);
        return R.ok().add(asnOrderService.page(pageParam, queryWrapper));
        Page<WkOrder> pageResult = asnOrderService.page(pageParam, queryWrapper);
        asnOrderService.fillPalletQty(pageResult.getRecords());
        return R.ok().add(pageResult);
    }
    @ApiOperation("获取首页表头数据")
@@ -84,14 +86,20 @@
    @PreAuthorize("hasAuthority('manager:asnOrder:list')")
    @PostMapping({"/asnOrder/many/{ids}", "/asnOrders/many/{ids}"})
    public R many(@PathVariable Long[] ids) {
        return R.ok().add(asnOrderService.listByIds(Arrays.asList(ids)));
        List<WkOrder> list = asnOrderService.listByIds(Arrays.asList(ids));
        asnOrderService.fillPalletQty(list);
        return R.ok().add(list);
    }
    @PreAuthorize("hasAuthority('manager:asnOrder:list')")
    @OperationLog("表单查询")
    @GetMapping("/asnOrder/{id}")
    public R get(@PathVariable("id") Long id) {
        return R.ok().add(asnOrderService.getById(id));
        WkOrder order = asnOrderService.getById(id);
        if (order != null) {
            asnOrderService.fillPalletQty(Collections.singletonList(order));
        }
        return R.ok().add(order);
    }
    @PreAuthorize("hasAuthority('manager:asnOrder:save')")
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/WkOrderItemController.java
@@ -57,6 +57,7 @@
                record.setExtendFields(fields);
            }
        }
        asnOrderItemService.fillPalletQty(records);
        page.setRecords(records);
        return R.ok(page);
    }
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/params/WaitPakinParam.java
@@ -25,5 +25,8 @@
    @ApiModelProperty("组拖类型{null: 组拖, defective: 不良品}")
    private String type;
    @ApiModelProperty("是否半箱:true 时同一料箱码可继续组托追加,false 时料箱码已组托则不可再用")
    private Boolean isHalf;
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/WarehouseAreasItem.java
@@ -229,6 +229,11 @@
    @TableField(exist = false)
    private Map<String, String> extendFields;
    /** 可组盘数量 = 计划(anfme) - 已组托(workQty) - 已上架(qty),仅组盘明细接口返回时填充,供前端直接展示 */
    @ApiModelProperty(value = "可组盘数量(计划-已组托-已上架)")
    @TableField(exist = false)
    private Double availablePalletQty;
    /**
     * 备注
     */
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/WkOrder.java
@@ -96,6 +96,11 @@
    @ApiModelProperty(value = "已收数量")
    private Double qty;
    /** 组托数量(非持久化,列表展示用:该单下 WaitPakinItem.anfme 汇总) */
    @ApiModelProperty(value = "组托数量")
    @com.baomidou.mybatisplus.annotation.TableField(exist = false)
    private Double palletQty;
    /**
     * 物流单号
     */
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/WkOrderItem.java
@@ -128,6 +128,11 @@
    @ApiModelProperty(value= "送货数量")
    private Double anfme;
    /** 组托数量(非持久化,列表展示:该明细 WaitPakinItem.anfme 汇总) */
    @ApiModelProperty(value = "组托数量")
    @TableField(exist = false)
    private Double palletQty;
    /**
     * 库存单位
     */
rsf-server/src/main/java/com/vincent/rsf/server/manager/enums/AsnExceStatus.java
@@ -9,12 +9,12 @@
 */
public enum AsnExceStatus {
    // ASN/入库单执行状态(新顺序):未执行(组托) → 任务执行中 → 已完成
    // ASN/入库单执行状态:未执行(组托) → 任务执行中 → 已完成
    ASN_EXCE_STATUS_UN_EXCE("0", "未执行"),
    /** @deprecated 已停用,新流程不再使用 */
    ASN_EXCE_STATUS_EXCE_ING("1", "执行中"),
    /** @deprecated 已停用,新流程不再使用 */
    ASN_EXCE_STATUS_RECEIPT_DONE("2", "收货完成"),
    ASN_EXCE_STATUS_TASK_EXCE("3", "任务执行中"),
    /** 已完成:整单全流程结束(收货+组托+上架/入库任务等),可归档历史、上报 ERP;由组托/任务定时或移历史时设置 */
    ASN_EXCE_STATUS_TASK_DONE("4", "已完成"),
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/MaterialAutoSchedules.java
New file
@@ -0,0 +1,339 @@
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:指定物料有库存时自动生成全版出库单(每 2 分钟)
     * 配置:AUTO_FULL_OUT_MATNR_CODE(物料编码)、AUTO_FULL_OUT_ENABLED(true 启用)
     */
    @Scheduled(cron = "0 0/2 * * * ?")
    @Transactional(rollbackFor = Exception.class)
    public void autoCreateFullOutOrder() {
        Config enabledConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_FULL_OUT_ENABLED));
        if (enabledConfig == null || !Boolean.parseBoolean(enabledConfig.getVal())) {
            return;
        }
        Config matnrConfig = configService.getOne(new LambdaQueryWrapper<Config>().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<Matnr>().eq(Matnr::getCode, matnrCode));
        if (matnr == null) {
            log.warn("[自动全版出库单] 物料不存在: {}", matnrCode);
            return;
        }
        // 已有该物料未下发的出库单则本轮不再生成,等下发完后再生成(避免重复)
        List<Long> initOrderIds = outStockService.list(new LambdaQueryWrapper<WkOrder>()
                        .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<WkOrderItem>()
                    .in(WkOrderItem::getOrderId, initOrderIds)
                    .eq(WkOrderItem::getMatnrCode, matnrCode));
            if (hasMatnr > 0) {
                return;
            }
        }
        // 按库位分组:该物料、库位状态为 F 的在库;每个库位各生成一张出库单
        List<LocItem> items = locItemService.list(new LambdaQueryWrapper<LocItem>()
                .eq(LocItem::getMatnrCode, matnrCode)
                .gt(LocItem::getAnfme, 0));
        if (items.isEmpty()) {
            return;
        }
        Map<Long, List<LocItem>> 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<LocItem> 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<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_FULL_OUT_DISPATCH_ENABLED));
        if (enabledConfig == null || !Boolean.parseBoolean(enabledConfig.getVal())) {
            return;
        }
        Config matnrConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_FULL_OUT_MATNR_CODE));
        if (matnrConfig == null || StringUtils.isBlank(matnrConfig.getVal())) {
            return;
        }
        String matnrCode = matnrConfig.getVal().trim();
        List<WkOrder> orders = outStockService.list(new LambdaQueryWrapper<WkOrder>()
                .eq(WkOrder::getType, OrderType.ORDER_OUT.type)
                .eq(WkOrder::getExceStatus, AsnExceStatus.OUT_STOCK_STATUS_TASK_INIT.val));
        for (WkOrder order : orders) {
            List<WkOrderItem> orderItems = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>()
                    .eq(WkOrderItem::getOrderId, order.getId())
                    .eq(WkOrderItem::getMatnrCode, matnrCode));
            if (orderItems.isEmpty()) continue;
            List<OutStockToTaskParams> 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<LocItem> 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<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getSourceId, order.getId()));
                if (!taskItems.isEmpty()) {
                    Set<Long> taskIds = taskItems.stream().map(TaskItem::getTaskId).collect(Collectors.toSet());
                    List<Task> 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:无订单组托 + 自动生成入库单(仅针对配置物料,每 2 分钟)
     * 先按配置物料与数量做无订单组托,再生成入库单并关联组托明细,便于 RCS 入库闭环。
     * 配置:AUTO_PAKIN_ON_ASN_ENABLED(true)、AUTO_FULL_OUT_MATNR_CODE、AUTO_PAKIN_QTY(数量)
     */
    @Scheduled(cron = "0 0/2 * * * ?")
    @Transactional(rollbackFor = Exception.class)
    public void autoPakinOnInbound() {
        Config enabledConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_PAKIN_ON_ASN_ENABLED));
        if (enabledConfig == null || !Boolean.parseBoolean(enabledConfig.getVal())) {
            return;
        }
        Config matnrConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.AUTO_FULL_OUT_MATNR_CODE));
        if (matnrConfig == null || StringUtils.isBlank(matnrConfig.getVal())) {
            return;
        }
        Config qtyConfig = configService.getOne(new LambdaQueryWrapper<Config>().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<Matnr>().eq(Matnr::getCode, matnrCode));
        if (matnr == null) {
            log.warn("[无订单自动组托] 物料不存在: {}", matnrCode);
            return;
        }
        // 已有该物料未执行入库单则本轮不继续生成,避免堆积
        List<Long> initAsnIds = asnOrderService.list(new LambdaQueryWrapper<WkOrder>()
                        .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<WkOrderItem>()
                    .in(WkOrderItem::getOrderId, initAsnIds)
                    .eq(WkOrderItem::getMatnrCode, matnrCode));
            if (hasMatnr > 0) {
                return;
            }
        }
        // 1)无订单组托:仅物料 + 数量,不传 asnCode/id
        List<PakinItem> 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());
            return;
        }
        // 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<WaitPakinItem>()
                .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);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/ScheduleJobs.java
@@ -69,156 +69,133 @@
    @Autowired
    private FieldsItemService fieldsItemService;
//    /**
//     * @author Ryan
//     * @date 2025/5/9
//     * @description: 直接组托开关为true,将收货单直接加入临时库存
//     * @version 1.0
//     */
//    @Scheduled(cron = "0/25 * * * * ?")
//    @Transactional(rollbackFor = Exception.class)
//    public synchronized void IgnoreReceipt() {
//        Config config = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.DIRECT_WAIT_PAKIN));
//        if (Objects.isNull(config)) {
//            return;
//        }
//        if (!Boolean.parseBoolean(config.getVal())) {
//            return;
//        }
//        //自动收货单
//        List<WkOrder> orders = asnOrderService.list(new LambdaQueryWrapper<WkOrder>()
//                .eq(WkOrder::getType, OrderType.ORDER_IN.type)
//                .eq(WkOrder::getExceStatus, AsnExceStatus.ASN_EXCE_STATUS_UN_EXCE.val));
//         if (!orders.isEmpty()) {
//            for (WkOrder order : orders) {
//                if (order.getWkType().equals(OrderWorkType.ORDER_WORK_TYPE_OTHER_TERANSFER.type)) {
//                    WkOrder one = outStockService.getOne(new LambdaQueryWrapper<WkOrder>()
//                            .eq(WkOrder::getPoCode, order.getPoCode())
//                            .eq(WkOrder::getWkType, OrderWorkType.ORDER_WORK_TYPE_STOCK_TERANSFER.type));
//                    if (Objects.isNull(one)) {
//                        throw new CoolException("数据错误");
//                    }
//                    if (!one.getExceStatus().equals(AsnExceStatus.OUT_STOCK_STATUS_TASK_DONE.val)) {
//                        continue;
//                    }
//                }
//
//                List<WkOrderItem> orderItems = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>().eq(WkOrderItem::getOrderId, order.getId()));
//                if (orderItems.isEmpty()) {
//                    return;
//                }
//                // 收货区已停用:不再写入收货区,仅更新单据状态与数量
////                List<WarehouseAreas> receiptAreas = warehouseAreasService.list(new LambdaQueryWrapper<WarehouseAreas>()
////                        .eq(WarehouseAreas::getType, WarehouseAreasType.WAREHOUSE_AREAS_TYPE_RECEIPT.type));
////                WarehouseAreas receiptArea = receiptAreas.isEmpty() ? null : receiptAreas.get(0);
////                List<WarehouseAreasItem> items = new ArrayList<>();
//                for (WkOrderItem item : orderItems) {
////                    Long areaId = receiptArea == null ? null : receiptArea.getId();
////                    String areaName = receiptArea == null ? null : receiptArea.getName();
////                    WarehouseAreasItem param = new WarehouseAreasItem();
////                    BeanUtils.copyProperties(item, param);
////                    param.setAsnCode(order.getCode()).setAsnId(order.getId());
////                    if (areaId != null) {
////                        param.setAreaId(areaId).setAreaName(areaName);
////                    }
////                    items.add(param);
////                    Matnr matnr = matnrService.getOne(new LambdaQueryWrapper<Matnr>().eq(Matnr::getId, item.getMatnrId()));
////                    if (Objects.isNull(matnr)) {
////                        throw new CoolException("物料不存在!!");
////                    }
////                    if (receiptArea != null) {
////                        try {
////                            updateReceipt(receiptArea, item, order, matnr);
////                        } catch (Exception e) {
////                            throw new CoolException(e.getMessage());
////                        }
////                    }
//                    if (!asnOrderItemService.update(new LambdaUpdateWrapper<WkOrderItem>().set(WkOrderItem::getQty, item.getAnfme()).eq(WkOrderItem::getId, item.getId()))) {
//                        throw new CoolException("收货单明细完成数量修改失败!!");
//                    }
//                }
////                if (!warehouseAreasItemService.saveBatch(items)) {
////                    throw new CoolException("收货单保存至收货区执行失败!!");
////                }
//
//                if (!asnOrderService.update(new LambdaUpdateWrapper<WkOrder>()
//                        .set(WkOrder::getQty, order.getAnfme())
//                        .set(WkOrder::getExceStatus, AsnExceStatus.ASN_EXCE_STATUS_RECEIPT_DONE.val)
//                        .eq(WkOrder::getId, order.getId()))) {
//                    throw new CoolException("收货单状态修改失败!!");
//                }
//            }
//        }
//    }
    /** 收货区已停用,方法整体注释
     * @author Ryan
     * @date 2025/5/12
     * @description: 收货区库存更新
    /**
     * 自动收货:仅当 DirectWaitPakin 配置为 true 时执行,将未执行入库单直接收货并写入收货区,更新单据状态为收货完成。
     * 不影响云仓WMS入库接口(8.3 同步单)与组托流程;关闭时不做任何操作。
     */
//    @Transactional(rollbackFor = Exception.class)
//    public void updateReceipt(WarehouseAreas areasItem, WkOrderItem orderItem, WkOrder wkOrder, Matnr matnr) throws Exception {
//        Companys companys = new Companys();
//        if (StringUtils.isNoneBlank(orderItem.getSplrCode())) {
//            companys = companysService.getOne(new LambdaQueryWrapper<Companys>().eq(Companys::getCode, orderItem.getSplrCode()));
//        }
//        WarehouseAreasItem item = new WarehouseAreasItem();
//        item.setTrackCode(orderItem.getBarcode())
//                .setAreaName(areasItem.getName())
//                .setAreaId(areasItem.getId())
//                .setAsnItemId(orderItem.getId())
//                .setAsnCode(wkOrder.getCode())
//                .setAsnId(wkOrder.getId())
//                .setProdTime(orderItem.getProdTime())
//                .setPlatItemId(orderItem.getPlatItemId())
//                .setPlatOrderCode(orderItem.getPlatOrderCode())
//                .setPlatWorkCode(orderItem.getPlatWorkCode())
//                .setProjectCode(orderItem.getProjectCode())
//                .setSplrId(companys.getId())
//                .setUnit(orderItem.getStockUnit())
//                .setStockUnit(orderItem.getStockUnit())
//                .setMatnrCode(matnr.getCode())
//                .setAnfme(orderItem.getAnfme())
//                .setMatnrId(matnr.getId())
//                .setIsptResult(orderItem.getIsptResult())
//                .setMaktx(matnr.getName())
//                .setSplrBatch(orderItem.getSplrBatch())
//                .setWeight(matnr.getWeight())
//                .setFieldsIndex(orderItem.getFieldsIndex())
//                .setShipperId(matnr.getShipperId());
//        List<WarehouseAreasItem> warehousList = StringUtils.isNotBlank(orderItem.getFieldsIndex())
//                ? warehouseAreasItemService.list(new LambdaQueryWrapper<WarehouseAreasItem>().eq(WarehouseAreasItem::getFieldsIndex, orderItem.getFieldsIndex()))
//                : Collections.emptyList();
//        WarehouseAreasItem warehousItem = warehousList.isEmpty() ? null : warehousList.get(0);
//        if (!Objects.isNull(warehousItem)) {
//            List<FieldsItem> fieldsList = fieldsItemService.list(new LambdaQueryWrapper<FieldsItem>()
//                    .eq(FieldsItem::getUuid, orderItem.getFieldsIndex()).last("LIMIT 1"));
//            FieldsItem fieldsItem = fieldsList.isEmpty() ? null : fieldsList.get(0);
//            if (!Objects.isNull(fieldsItem)) {
//                throw new CoolException("票号:" + fieldsItem.getValue() + "已在收货区,不可推送相当票号数据。请联系管理员!!" );
//            }
//        }
//        LambdaQueryWrapper<WarehouseAreasItem> queryWrapper = new LambdaQueryWrapper<WarehouseAreasItem>()
//                .eq(WarehouseAreasItem::getMatnrCode, orderItem.getMatnrCode())
//                .eq(!Cools.isEmpty(orderItem.getFieldsIndex()), WarehouseAreasItem::getFieldsIndex, orderItem.getFieldsIndex())
//                .eq(WarehouseAreasItem::getAsnCode, orderItem.getOrderCode())
//                .eq(StringUtils.isNotBlank(orderItem.getSplrBatch()), WarehouseAreasItem::getSplrBatch, orderItem.getSplrBatch());
//        if (Objects.isNull(orderItem.getIsptResult())) {
//            queryWrapper.isNull(WarehouseAreasItem::getIsptResult);
//        } else {
//            queryWrapper.eq(WarehouseAreasItem::getIsptResult, orderItem.getIsptResult());
//        }
//        List<WarehouseAreasItem> serviceList = warehouseAreasItemService.list(queryWrapper);
//        WarehouseAreasItem serviceOne = serviceList.isEmpty() ? null : serviceList.get(0);
//        if (!Objects.isNull(serviceOne)) {
//            item.setId(serviceOne.getId());
//            item.setAnfme(orderItem.getAnfme());
//        }
//        if (!warehouseAreasItemService.saveOrUpdate(item)) {
//            throw new CoolException("收货失败!!");
//        }
//    }
    @Scheduled(cron = "0/25 * * * * ?")
    @Transactional(rollbackFor = Exception.class)
    public synchronized void IgnoreReceipt() {
        Config config = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.DIRECT_WAIT_PAKIN));
        if (Objects.isNull(config) || !Boolean.parseBoolean(config.getVal())) {
            return;
        }
        List<WkOrder> orders = asnOrderService.list(new LambdaQueryWrapper<WkOrder>()
                .eq(WkOrder::getType, OrderType.ORDER_IN.type)
                .eq(WkOrder::getExceStatus, AsnExceStatus.ASN_EXCE_STATUS_UN_EXCE.val));
        if (orders.isEmpty()) {
            return;
        }
        WarehouseAreas receiptArea = warehouseAreasService.getOne(new LambdaQueryWrapper<WarehouseAreas>()
                .eq(WarehouseAreas::getType, WarehouseAreasType.WAREHOUSE_AREAS_TYPE_RECEIPT.type), false);
        for (WkOrder order : orders) {
            if (OrderWorkType.ORDER_WORK_TYPE_OTHER_TERANSFER.type.equals(order.getWkType())) {
                WkOrder one = outStockService.getOne(new LambdaQueryWrapper<WkOrder>()
                        .eq(WkOrder::getPoId, order.getPoId())
                        .eq(WkOrder::getWkType, OrderWorkType.ORDER_WORK_TYPE_STOCK_TERANSFER.type));
                if (Objects.isNull(one)) {
                    throw new CoolException("数据错误");
                }
                if (!one.getExceStatus().equals(AsnExceStatus.OUT_STOCK_STATUS_TASK_DONE.val)) {
                    continue;
                }
            }
            List<WkOrderItem> orderItems = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>().eq(WkOrderItem::getOrderId, order.getId()));
            if (orderItems.isEmpty()) {
                continue;
            }
            for (WkOrderItem item : orderItems) {
                if (receiptArea != null) {
                    Matnr matnr = matnrService.getOne(new LambdaQueryWrapper<Matnr>().eq(Matnr::getId, item.getMatnrId()));
                    if (Objects.isNull(matnr)) {
                        throw new CoolException("物料不存在!!");
                    }
                    try {
                        updateReceipt(receiptArea, item, order, matnr);
                    } catch (Exception e) {
                        throw new CoolException(e.getMessage());
                    }
                }
                if (!asnOrderItemService.update(new LambdaUpdateWrapper<WkOrderItem>().set(WkOrderItem::getQty, item.getAnfme()).eq(WkOrderItem::getId, item.getId()))) {
                    throw new CoolException("收货单明细完成数量修改失败!!");
                }
            }
            if (!asnOrderService.update(new LambdaUpdateWrapper<WkOrder>()
                    .set(WkOrder::getQty, order.getAnfme())
                    .set(WkOrder::getExceStatus, AsnExceStatus.ASN_EXCE_STATUS_RECEIPT_DONE.val)
                    .eq(WkOrder::getId, order.getId()))) {
                throw new CoolException("收货单状态修改失败!!");
            }
        }
    }
    /**
     * 收货区库存更新(仅 DirectWaitPakin 启用时由 IgnoreReceipt 调用)
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateReceipt(WarehouseAreas areasItem, WkOrderItem orderItem, WkOrder wkOrder, Matnr matnr) throws Exception {
        Companys companys = new Companys();
        if (StringUtils.isNoneBlank(orderItem.getSplrCode())) {
            companys = companysService.getOne(new LambdaQueryWrapper<Companys>().eq(Companys::getCode, orderItem.getSplrCode()));
        }
        if (Objects.isNull(companys)) {
            companys = new Companys();
        }
        WarehouseAreasItem item = new WarehouseAreasItem();
        item.setTrackCode(orderItem.getBarcode())
                .setAreaName(areasItem.getName())
                .setAreaId(areasItem.getId())
                .setAsnItemId(orderItem.getId())
                .setAsnCode(wkOrder.getCode())
                .setAsnId(wkOrder.getId())
                .setProdTime(orderItem.getProdTime())
                .setPlatItemId(orderItem.getPlatItemId())
                .setPlatOrderCode(orderItem.getPlatOrderCode())
                .setPlatWorkCode(orderItem.getPlatWorkCode())
                .setProjectCode(orderItem.getProjectCode())
                .setSplrId(companys.getId())
                .setUnit(orderItem.getStockUnit())
                .setStockUnit(orderItem.getStockUnit())
                .setMatnrCode(matnr.getCode())
                .setAnfme(orderItem.getAnfme())
                .setMatnrId(matnr.getId())
                .setIsptResult(orderItem.getIsptResult())
                .setMaktx(matnr.getName())
                .setSplrBatch(orderItem.getSplrBatch())
                .setWeight(matnr.getWeight())
                .setFieldsIndex(orderItem.getFieldsIndex())
                .setShipperId(matnr.getShipperId());
        if (StringUtils.isNotBlank(orderItem.getFieldsIndex())) {
            List<WarehouseAreasItem> warehousList = warehouseAreasItemService.list(new LambdaQueryWrapper<WarehouseAreasItem>().eq(WarehouseAreasItem::getFieldsIndex, orderItem.getFieldsIndex()));
            WarehouseAreasItem warehousItem = warehousList.isEmpty() ? null : warehousList.get(0);
            if (Objects.nonNull(warehousItem)) {
                List<FieldsItem> fieldsList = fieldsItemService.list(new LambdaQueryWrapper<FieldsItem>().eq(FieldsItem::getUuid, orderItem.getFieldsIndex()).last("LIMIT 1"));
                FieldsItem fieldsItem = fieldsList.isEmpty() ? null : fieldsList.get(0);
                if (Objects.nonNull(fieldsItem)) {
                    throw new CoolException("票号:" + fieldsItem.getValue() + "已在收货区,不可推送相当票号数据。请联系管理员!!");
                }
            }
        }
        LambdaQueryWrapper<WarehouseAreasItem> queryWrapper = new LambdaQueryWrapper<WarehouseAreasItem>()
                .eq(WarehouseAreasItem::getMatnrCode, orderItem.getMatnrCode())
                .eq(!Cools.isEmpty(orderItem.getFieldsIndex()), WarehouseAreasItem::getFieldsIndex, orderItem.getFieldsIndex())
                .eq(WarehouseAreasItem::getAsnCode, orderItem.getOrderCode())
                .eq(StringUtils.isNotBlank(orderItem.getSplrBatch()), WarehouseAreasItem::getSplrBatch, orderItem.getSplrBatch());
        if (Objects.isNull(orderItem.getIsptResult())) {
            queryWrapper.isNull(WarehouseAreasItem::getIsptResult);
        } else {
            queryWrapper.eq(WarehouseAreasItem::getIsptResult, orderItem.getIsptResult());
        }
        WarehouseAreasItem serviceOne = warehouseAreasItemService.getOne(queryWrapper);
        if (Objects.nonNull(serviceOne)) {
            item.setId(serviceOne.getId());
            Double anfme = Math.round((item.getAnfme() + serviceOne.getAnfme()) * 10000) / 10000.0;
            item.setAnfme(anfme);
        }
        if (!warehouseAreasItemService.saveOrUpdate(item)) {
            throw new CoolException("收货失败!!");
        }
    }
    /**
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/TaskSchedules.java
@@ -254,7 +254,7 @@
    @Scheduled(cron = "0/35 * * * * ?  ")
    @Transactional(rollbackFor = Exception.class)
    public void pubTaskToWcs() {
        log.info("定时任务开始执行:任务下发到RCS");
        log.debug("定时任务开始执行:任务下发到RCS");
        Long loginUserId = SystemAuthUtils.getLoginUserId();
        List<Integer> list = Arrays.asList(
                 TaskType.TASK_TYPE_LOC_MOVE.type
@@ -274,7 +274,7 @@
                .in(Task::getTaskType, list)
                .in(Task::getTaskStatus, integers)
                .orderByDesc(Task::getSort));
        log.info("查询到待下发任务数量:{}", tasks.size());
        log.debug("查询到待下发任务数量:{}", tasks.size());
        if (tasks.isEmpty()) {
            log.debug("没有待下发的任务,定时任务结束");
            return;
@@ -298,9 +298,9 @@
//            }
//        }
        /**下发普通站点任务,报错回滚,不再往下执行*/
        log.info("开始下发{}个任务到RCS", tasks.size());
        log.debug("开始下发{}个任务到RCS", tasks.size());
        taskService.pubTaskToWcs(tasks);
        log.info("定时任务执行完成:任务下发到RCS");
        log.debug("定时任务执行完成:任务下发到RCS");
    }
    /**
@@ -619,37 +619,38 @@
                }
            } else if ((task.getTaskType() >= TaskType.TASK_TYPE_OUT.type && task.getTaskType() <= TaskType.TASK_TYPE_EMPITY_OUT.type)
                    || task.getTaskType().equals(TaskType.TASK_TYPE_PICK_IN.type)) {
                /**判断单据是否完成**/
                // 只有波次类型的任务才需要查询波次关联单
                /**判断单据是否完成:波次下发、按单下发(点击下发任务)完成后均将出库单置为完结*/
                Set<Long> orderIdsToDone = new HashSet<>();
                if (task.getResource() != null && task.getResource().equals(TaskResouceType.TASK_RESOUCE_WAVE_TYPE.val)) {
                    Set<Long> longSet = taskItems.stream()
                            .map(TaskItem::getSourceId)
                            .filter(Objects::nonNull)
                            .collect(Collectors.toSet());
                    if (longSet.isEmpty()) {
                        logger.warn("任务{}的任务明细中没有有效的sourceId,跳过波次关联单查询。任务编码:{},任务类型:{}",
                                task.getId(), task.getTaskCode(), task.getTaskType());
                    } else {
                    if (!longSet.isEmpty()) {
                        List<WaveOrderRela> waveOrderRelas = waveOrderRelaService.list(new LambdaQueryWrapper<WaveOrderRela>()
                                .in(WaveOrderRela::getWaveId, longSet));
                        if (Cools.isEmpty(waveOrderRelas)) {
                            logger.warn("任务{}的波次对应关联单未找到,可能是数据不一致或任务不是通过波次创建的。任务编码:{},sourceIds:{}",
                                    task.getId(), task.getTaskCode(), longSet);
                        } else {
                            Set<Long> orderIds = waveOrderRelas.stream().map(WaveOrderRela::getOrderId).collect(Collectors.toSet());
                            List<WkOrder> wkOrders = asnOrderService.listByIds(orderIds);
                            if (wkOrders.isEmpty()) {
                                logger.warn("任务{}的关联单据不存在。任务编码:{},orderIds:{}",
                                        task.getId(), task.getTaskCode(), orderIds);
                            } else {
                        if (!Cools.isEmpty(waveOrderRelas)) {
                            orderIdsToDone.addAll(waveOrderRelas.stream().map(WaveOrderRela::getOrderId).collect(Collectors.toSet()));
                        }
                    }
                } else if (task.getResource() != null && task.getResource().equals(TaskResouceType.TASK_RESOUCE_ORDER_TYPE.val)) {
                    // 按单下发:任务明细 sourceId 为出库单ID
                    Set<Long> ids = taskItems.stream()
                            .map(TaskItem::getSourceId)
                            .filter(Objects::nonNull)
                            .collect(Collectors.toSet());
                    orderIdsToDone.addAll(ids);
                }
                if (!orderIdsToDone.isEmpty()) {
                    List<WkOrder> wkOrders = asnOrderService.listByIds(orderIdsToDone);
                    if (!wkOrders.isEmpty()) {
                                Config allowChang = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.ALLOW_OVER_CHANGE));
                                wkOrders.forEach(order -> {
                                    //判断是否允许超收,不允许超收添加拒收判断
                                    if (!Objects.isNull(allowChang)) {
                                        if (!Boolean.parseBoolean(allowChang.getVal())) {
                                            if (order.getAnfme().compareTo(order.getQty()) == 0) {
                            if (order.getAnfme() == null) return;
                            boolean canDone = Boolean.TRUE.equals(allowChang != null && Boolean.parseBoolean(allowChang.getVal()))
                                    ? (order.getQty() != null && order.getAnfme().compareTo(order.getQty()) <= 0)
                                    : (order.getQty() != null && order.getAnfme().compareTo(order.getQty()) == 0);
                            if (canDone) {
                                                order.setExceStatus(AsnExceStatus.OUT_STOCK_STATUS_TASK_DONE.val);
                                                if (order.getQty() == null || order.getQty().compareTo(0.0) == 0) {
                                                    order.setQty(order.getWorkQty() != null ? order.getWorkQty() : 0.0);
@@ -658,27 +659,8 @@
                                                    logger.error("出库单更新状态失败。订单ID:{},订单编码:{}", order.getId(), order.getCode());
                                                }
                                            }
                                        } else {
                                            if (order.getAnfme().compareTo(order.getQty()) <= 0) {
                                                order.setExceStatus(AsnExceStatus.OUT_STOCK_STATUS_TASK_DONE.val);
                                                if (order.getQty() == null || order.getQty().compareTo(0.0) == 0) {
                                                    order.setQty(order.getWorkQty() != null ? order.getWorkQty() : 0.0);
                                                }
                                                if (!asnOrderService.updateById(order)) {
                                                    logger.error("出库单更新状态失败。订单ID:{},订单编码:{}", order.getId(), order.getCode());
                                                }
                                            }
                                        }
                                    }
                                    //检查单据是否完成
                                });
                            }
                        }
                    }
                } else {
                    logger.debug("任务{}不是波次类型任务(资源类型:{}),跳过波次关联单查询。任务编码:{}",
                            task.getId(), task.getResource(), task.getTaskCode());
                }
                
                //出库单上报RCS修改库位状态
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/AsnOrderItemService.java
@@ -24,4 +24,7 @@
    IPage<Map<String, Object>> listByAsnId(PageParam<WkOrderItem, BaseParam> pageParam, QueryWrapper<WkOrderItem> buildWrapper);
    /** 为收货明细列表填充组托数量(palletQty),来源于组托档 WaitPakinItem 按 asnItemId 汇总 */
    void fillPalletQty(List<WkOrderItem> items);
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/AsnOrderService.java
@@ -32,4 +32,7 @@
    R getDashbord();
    R getStockTrand();
    /** 为入库通知单列表填充组托数量(palletQty),用于列表展示 */
    void fillPalletQty(List<WkOrder> orders);
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/AsnOrderItemServiceImpl.java
@@ -10,6 +10,8 @@
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.common.utils.QuantityUtils;
import com.vincent.rsf.server.manager.entity.WaitPakinItem;
import com.vincent.rsf.server.manager.entity.WkOrder;
import com.vincent.rsf.server.manager.entity.WkOrderItem;
import com.vincent.rsf.server.manager.enums.OrderType;
@@ -27,6 +29,7 @@
import com.vincent.rsf.server.manager.service.AsnOrderItemService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.server.manager.service.AsnOrderService;
import com.vincent.rsf.server.manager.service.WaitPakinItemService;
import com.vincent.rsf.server.manager.service.CompanysService;
import com.vincent.rsf.server.manager.service.MatnrService;
import com.vincent.rsf.server.system.constant.SerialRuleCode;
@@ -70,6 +73,9 @@
    private DictDataService dictDataService;
    @Autowired
    private AsnOrderService asnOrderService;
    @Autowired
    private WaitPakinItemService waitPakinItemService;
    @Override
    public R generateBarcode(List<Long> orders) {
@@ -301,4 +307,26 @@
        return hsahMap;
    }
    @Override
    public void fillPalletQty(List<WkOrderItem> items) {
        if (items == null || items.isEmpty()) {
            return;
        }
        List<Long> itemIds = items.stream().map(WkOrderItem::getId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
        if (itemIds.isEmpty()) {
            return;
        }
        List<WaitPakinItem> allItems = waitPakinItemService.list(
                new LambdaQueryWrapper<WaitPakinItem>().in(WaitPakinItem::getAsnItemId, itemIds).eq(WaitPakinItem::getDeleted, 0));
        Map<Long, Double> sumByAsnItemId = allItems.stream()
                .collect(Collectors.groupingBy(WaitPakinItem::getAsnItemId,
                        Collectors.summingDouble(w -> w.getAnfme() != null ? w.getAnfme() : 0.0)));
        sumByAsnItemId.replaceAll((k, v) -> QuantityUtils.roundToScale(v));
        for (WkOrderItem item : items) {
            if (item.getId() != null) {
                item.setPalletQty(sumByAsnItemId.getOrDefault(item.getId(), 0.0));
            }
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/AsnOrderServiceImpl.java
@@ -17,6 +17,7 @@
import com.vincent.rsf.server.manager.controller.params.BatchUpdateParam;
import com.vincent.rsf.server.manager.entity.*;
import com.vincent.rsf.server.manager.enums.*;
import com.vincent.rsf.server.common.utils.QuantityUtils;
import com.vincent.rsf.server.manager.mapper.AsnOrderMapper;
import com.vincent.rsf.server.manager.service.*;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -383,7 +384,7 @@
    public R removeOrders(List<Long> ids) {
        // 已组托不可删除,需先解除组托
        long palletizedCount = waitPakinItemService.count(new LambdaQueryWrapper<WaitPakinItem>()
                .in(WaitPakinItem::getAsnId, ids));
                .in(WaitPakinItem::getAsnId, ids).eq(WaitPakinItem::getDeleted, 0));
        if (palletizedCount > 0) {
            throw new CoolException("单据已组托,请先解除组托后再删除!!");
        }
@@ -560,4 +561,26 @@
            throw new CoolException("原单据删除失败!!");
        }
    }
    @Override
    public void fillPalletQty(List<WkOrder> orders) {
        if (orders == null || orders.isEmpty()) {
            return;
        }
        List<Long> orderIds = orders.stream().map(WkOrder::getId).filter(Objects::nonNull).collect(Collectors.toList());
        if (orderIds.isEmpty()) {
            return;
        }
        List<WaitPakinItem> allItems = waitPakinItemService.list(
                new LambdaQueryWrapper<WaitPakinItem>().in(WaitPakinItem::getAsnId, orderIds).eq(WaitPakinItem::getDeleted, 0));
        Map<Long, Double> sumByAsnId = allItems.stream()
                .collect(Collectors.groupingBy(WaitPakinItem::getAsnId,
                        Collectors.summingDouble(w -> w.getAnfme() != null ? w.getAnfme() : 0.0)));
        sumByAsnId.replaceAll((k, v) -> QuantityUtils.roundToScale(v));
        for (WkOrder order : orders) {
            if (order.getId() != null) {
                order.setPalletQty(sumByAsnId.getOrDefault(order.getId(), 0.0));
            }
        }
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java
@@ -1571,11 +1571,11 @@
                    if (Objects.isNull(waveItem)) {
                        throw new CoolException("波次明细不存在!!");
                    }
//                    try {
//                        saveOutStockItem(maps.get(key), null, waveItem, null, loginUserId);
//                    } catch (Exception e) {
//                        throw new CoolException(e.getMessage());
//                    }
                    try {
                        saveOutStockItem(maps.get(key), null, waveItem, null, loginUserId);
                    } catch (Exception e) {
                        throw new CoolException(e.getMessage());
                    }
                } else if (task.getResource().equals(TaskResouceType.TASK_RESOUCE_ORDER_TYPE.val)) {
                    WkOrderItem orderItem = asnOrderItemService.getById(key);
                    if (Objects.isNull(orderItem)) {
@@ -1703,8 +1703,35 @@
                .setUpdateTime(new Date())
                .setAnfme(sum);
        if (!Objects.isNull(waveItem)) {
            //TODO 生成波次时需要将波次号写入单据,通过物料,批次,动态字段等唯一值反查单据信息
            // 波次出库:更新出库单完成数量及出库单明细执行数量
            stock.setSourceId(waveItem.getId()).setType(OrderType.ORDER_OUT.type);
            Long orderId = waveItem.getOrderId();
            if (orderId != null) {
                WkOrder wkOrder = asnOrderService.getById(orderId);
                if (wkOrder != null) {
                    Double curQty = wkOrder.getQty() != null ? wkOrder.getQty() : 0.0;
                    Double newQty = QuantityUtils.roundToScale(curQty + sum);
                    wkOrder.setExceStatus(AsnExceStatus.OUT_STOCK_STATUS_TASK_WORKING.val).setQty(newQty);
                    if (!asnOrderService.updateById(wkOrder)) {
                        throw new CoolException("出库单完成数量更新失败!!");
                    }
                }
                // 按出库单明细汇总本次任务数量,更新各明细 workQty
                Map<Long, Double> sumByOrderItemId = taskItems.stream()
                        .filter(ti -> ti.getOrderItemId() != null)
                        .collect(Collectors.groupingBy(TaskItem::getOrderItemId,
                                Collectors.summingDouble(ti -> ti.getAnfme() != null ? ti.getAnfme() : 0.0)));
                for (Map.Entry<Long, Double> e : sumByOrderItemId.entrySet()) {
                    WkOrderItem oi = outStockItemService.getById(e.getKey());
                    if (oi != null) {
                        Double wq = oi.getWorkQty() != null ? oi.getWorkQty() : 0.0;
                        oi.setWorkQty(QuantityUtils.roundToScale(wq + e.getValue()));
                        if (!outStockItemService.updateById(oi)) {
                            throw new CoolException("出库单明细执行数量更新失败!!");
                        }
                    }
                }
            }
        } else if (!Objects.isNull(orderItem) && StringUtils.isNotBlank(orderItem.getId() + "")) {
            WkOrder wkOrder = asnOrderService.getById(orderItem.getOrderId());
            Double qty = Math.round((wkOrder.getQty() + sum) * 1000000) / 1000000.0;
@@ -2229,10 +2256,10 @@
            List<TaskItem> items = orderMap.get(key);
            //保存入出库明细
            saveStockItems(items, task, pakinItem.getId(), pakinItem.getAsnCode(), pakinItem.getWkType(), pakinItem.getType(), loginUserId);
            // 收货区已停用,不再移出收货区库存
            // if (Objects.nonNull(pakinItem.getSource())) {
            //     removeReceiptStock(pakinItem, loginUserId);
            // }
            // DirectWaitPakin 启用时组托来自收货区,入库完成后移出收货区库存;未启用时 source 为 null 不执行
            if (Objects.nonNull(pakinItem.getSource())) {
                removeReceiptStock(pakinItem, loginUserId);
            }
        });
        Set<Long> pkinItemIds = taskItems.stream().map(TaskItem::getSource).collect(Collectors.toSet());
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/WaitPakinServiceImpl.java
@@ -12,10 +12,12 @@
import com.vincent.rsf.server.manager.service.*;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.server.common.utils.QuantityUtils;
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 org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -45,6 +47,8 @@
    private TaskItemService taskItemService;
    @Autowired
    private MatnrMapper matnrMapper;
    @Autowired
    private ConfigService configService;
    /**
@@ -67,8 +71,8 @@
        WaitPakin pakin = waitPakinService.getOne(new LambdaQueryWrapper<WaitPakin>()
                .eq(WaitPakin::getBarcode, waitPakin.getBarcode()));
        // 如果料箱码已经组托过,提示已组托,请更换料箱码
        if (!Objects.isNull(pakin)) {
        // 半箱时允许同一料箱码继续组托追加;非半箱时料箱码已组托则不可再用
        if (!Objects.isNull(pakin) && !Boolean.TRUE.equals(waitPakin.getIsHalf())) {
            throw new CoolException("已组托,请更换料箱码");
        }
@@ -85,14 +89,17 @@
        Double sum = QuantityUtils.roundToScale(waitPakin.getItems().stream().mapToDouble(PakinItem::getReceiptQty).sum());
        WaitPakin waitPakin1 = new WaitPakin();
        Config directPakinConfig = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.DIRECT_WAIT_PAKIN));
        boolean directWaitPakin = directPakinConfig != null && Boolean.parseBoolean(directPakinConfig.getVal());
        WaitPakin waitPakin1;
        if (Objects.isNull(pakin)) {
            waitPakin1 = new WaitPakin();
            String ruleCode = SerialRuleUtils.generateRuleCode(SerialRuleCode.SYS_WAIT_PAKIN_CODE, null);
            if (StringUtils.isBlank(ruleCode)) {
                throw new CoolException("编码规则错误: 编码规则「SYS_WAIT_PAKIN_CODE」规则是不存在");
            }
            waitPakin1.setCode(ruleCode)
                    //状态修改为入库中
                    .setIoStatus(PakinIOStatus.PAKIN_IO_STATUS_DONE.val)
                    .setAnfme(sum)
                    .setUpdateBy(userId)
@@ -102,14 +109,94 @@
                throw new CoolException("主单保存失败!!");
            }
        } else {
            BeanUtils.copyProperties(pakin, waitPakin1);
            // 半箱追加:沿用已有组托单,只追加明细
            waitPakin1 = pakin;
        }
        List<WaitPakinItem> items = new ArrayList<>();
        if (directWaitPakin) {
            // DirectWaitPakin 启用:组托来自收货区,param 中 id 为 WarehouseAreasItem.id
            for (PakinItem pakinItem1 : waitPakin.getItems()) {
                WarehouseAreasItem areaItem = warehouseAreasItemService.getById(pakinItem1.getId());
                if (areaItem == null) {
                    throw new CoolException("物料未送至收货区!!");
                }
                WaitPakinItem pakinItem = new WaitPakinItem();
                pakinItem.setPakinId(waitPakin1.getId())
                        .setSource(areaItem.getId())
                        .setAsnId(areaItem.getAsnId())
                        .setAsnCode(areaItem.getAsnCode())
                        .setAsnItemId(areaItem.getAsnItemId())
                        .setIsptResult(areaItem.getIsptResult())
                        .setPlatItemId(areaItem.getPlatItemId())
                        .setPlatOrderCode(areaItem.getPlatOrderCode())
                        .setPlatWorkCode(areaItem.getPlatWorkCode())
                        .setProjectCode(areaItem.getProjectCode())
                        .setBatch(areaItem.getSplrBatch())
                        .setUnit(areaItem.getStockUnit())
                        .setFieldsIndex(areaItem.getFieldsIndex())
                        .setMatnrId(areaItem.getMatnrId())
                        .setMaktx(areaItem.getMaktx())
                        .setUpdateBy(userId)
                        .setCreateBy(userId)
                        .setMatnrCode(areaItem.getMatnrCode());
                WkOrder order = asnOrderService.getById(areaItem.getAsnId());
                if (order != null) {
                    pakinItem.setType(order.getType()).setWkType(StringUtils.isNotBlank(order.getWkType()) ? Short.parseShort(order.getWkType()) : null);
                }
                if (pakinItem1.getReceiptQty() == null || pakinItem1.getReceiptQty().compareTo(0.0) <= 0) {
                    throw new CoolException("组托数量不能小于等于零!!");
                }
                if (QuantityUtils.compare(pakinItem1.getReceiptQty(), areaItem.getAnfme()) > 0) {
                    throw new CoolException("组托数量不能大于收货数量!!");
                }
                pakinItem.setAnfme(QuantityUtils.roundToScale(pakinItem1.getReceiptQty())).setTrackCode(pakinItem1.getTrackCode());
                items.add(pakinItem);
            }
            if (!waitPakinItemService.saveBatch(items)) {
                throw new CoolException("组托明细保存失败!!");
            }
            for (WaitPakinItem pakinItem : items) {
                WarehouseAreasItem one = warehouseAreasItemService.getOne(new LambdaQueryWrapper<WarehouseAreasItem>().eq(WarehouseAreasItem::getId, pakinItem.getSource()));
                if (one == null) {
                    throw new CoolException("收货区数据错误!!");
                }
                Double workQty = QuantityUtils.roundToScale(QuantityUtils.add(one.getWorkQty() != null ? one.getWorkQty() : 0.0, pakinItem.getAnfme()));
                Double qty = QuantityUtils.roundToScale(QuantityUtils.add(workQty, one.getQty() != null ? one.getQty() : 0.0));
                one.setWorkQty(workQty);
                if (QuantityUtils.compare(qty, one.getAnfme() != null ? one.getAnfme() : 0.0) > 0) {
                    throw new CoolException("组托数量不能大于收货数量!!");
                }
                if (!warehouseAreasItemService.saveOrUpdate(one)) {
                    throw new CoolException("收货区执行数量修改失败!!");
                }
            }
            // 半箱追加时需按该组托单下全部明细重算总数量
            List<WaitPakinItem> allItems = waitPakinItemService.list(new LambdaQueryWrapper<WaitPakinItem>().eq(WaitPakinItem::getPakinId, waitPakin1.getId()));
            waitPakin1.setAnfme(QuantityUtils.roundToScale(allItems.stream().mapToDouble(w -> w.getAnfme() != null ? w.getAnfme() : 0.0).sum()));
            if (!this.updateById(waitPakin1)) {
                throw new CoolException("组托数量修改失败!!");
            }
            return waitPakin1;
        }
        // 有ASN时:按订单明细汇总已组托数量,校验 已组托+本次组盘 ≤ 计划数量
        Map<Long, Double> alreadyPalletizedByItemId = new java.util.HashMap<>();
        for (PakinItem p : waitPakin.getItems()) {
            if (StringUtils.isNotBlank(p.getAsnCode()) && p.getId() != null) {
                alreadyPalletizedByItemId.putIfAbsent(p.getId(), null);
            }
        }
        if (!alreadyPalletizedByItemId.isEmpty()) {
            for (Long asnItemId : alreadyPalletizedByItemId.keySet()) {
                double alreadySum = waitPakinItemService.list(new LambdaQueryWrapper<WaitPakinItem>().eq(WaitPakinItem::getAsnItemId, asnItemId).eq(WaitPakinItem::getDeleted, 0))
                        .stream().mapToDouble(w -> w.getAnfme() != null ? w.getAnfme() : 0.0).sum();
                alreadyPalletizedByItemId.put(asnItemId, QuantityUtils.roundToScale(alreadySum));
            }
        }
        for (PakinItem pakinItem1 : waitPakin.getItems()) {
            WaitPakinItem pakinItem = new WaitPakinItem();
            // 如果ASN单号为空,从物料信息表获取物料信息,不查询收货区
            if (StringUtils.isBlank(pakinItem1.getAsnCode())) {
                if (Objects.isNull(pakinItem1.getMatnrId())) {
                    throw new CoolException("物料ID不能为空!!");
@@ -184,8 +271,10 @@
                        if (waitPakinItem.getReceiptQty() == null || waitPakinItem.getReceiptQty().compareTo(0.0) <= 0) {
                            throw new CoolException("组托数量不能小于等于零!!");
                        }
                        if (QuantityUtils.compare(waitPakinItem.getReceiptQty(), orderItem.getAnfme()) > 0) {
                            throw new CoolException("组托数量不能大于计划数量!!");
                        Double anfme = orderItem.getAnfme() != null ? orderItem.getAnfme() : 0.0;
                        Double already = alreadyPalletizedByItemId.getOrDefault(orderItem.getId(), 0.0);
                        if (QuantityUtils.compare(QuantityUtils.add(already, waitPakinItem.getReceiptQty()), anfme) > 0) {
                            throw new CoolException("组托数量不能超过可组盘数量(计划" + anfme + ",已组托" + already + ",本次最多" + QuantityUtils.subtract(anfme, already) + ")!!");
                        }
                        pakinItem.setAnfme(QuantityUtils.roundToScale(waitPakinItem.getReceiptQty()))
                                .setTrackCode(waitPakinItem.getTrackCode());
@@ -284,16 +373,19 @@
                                throw new CoolException("组托明细删除失败!!");
                            }
                        }
                        // 收货区已停用,不再回写收货区
                        // for (int i = 0; i < warehouseAreasItems.size(); i++) {
                        //     if (warehouseAreasItems.get(i).getId().equals(pakinItems.get(i1).getSource())) {
                        //         double v = Math.round((warehouseAreasItems.get(i).getWorkQty() - item.getReceiptQty()) * 1000000) / 1000000.0;
                        //         warehouseAreasItems.get(i).setWorkQty(v);
                        //         if (!warehouseAreasItemService.updateById(warehouseAreasItems.get(i))) {
                        //             throw new CoolException("收货区数量修改失败!!");
                        //         }
                        //     }
                        // }
                        // DirectWaitPakin 启用时组托来自收货区,解绑需回写收货区 workQty
                        if (Objects.nonNull(pakinItems.get(i1).getSource())) {
                            for (WarehouseAreasItem areaItem : warehouseAreasItems) {
                                if (areaItem.getId().equals(pakinItems.get(i1).getSource())) {
                                    Double v = QuantityUtils.roundToScale(QuantityUtils.subtract(areaItem.getWorkQty() != null ? areaItem.getWorkQty() : 0.0, item.getReceiptQty()));
                                    areaItem.setWorkQty(v);
                                    if (!warehouseAreasItemService.updateById(areaItem)) {
                                        throw new CoolException("收货区数量修改失败!!");
                                    }
                                    break;
                                }
                            }
                        }
                    }
                }
            }
rsf-server/src/main/java/com/vincent/rsf/server/system/constant/GlobalConfigCode.java
@@ -42,4 +42,15 @@
    /** 云仓通知状态:失败 */
    public final static String CLOUD_WMS_NOTIFY_STATUS_FAIL = "CLOUD_WMS_NOTIFY_STATUS_FAIL";
    /** 指定物料自动全版出库:物料编码(val=物料编码时启用按物料自动生成全版出库单) */
    public final static String AUTO_FULL_OUT_MATNR_CODE = "AUTO_FULL_OUT_MATNR_CODE";
    /** 是否启用:有库存时自动生成全版出库单 */
    public final static String AUTO_FULL_OUT_ENABLED = "AUTO_FULL_OUT_ENABLED";
    /** 是否启用:该物料出库单自动下发任务 */
    public final static String AUTO_FULL_OUT_DISPATCH_ENABLED = "AUTO_FULL_OUT_DISPATCH_ENABLED";
    /** 是否启用:RCS 入库通知时自动组托 */
    public final static String AUTO_PAKIN_ON_ASN_ENABLED = "AUTO_PAKIN_ON_ASN_ENABLED";
    /** 自动组托数量(与 AUTO_PAKIN_ON_ASN_ENABLED 配合,每条明细组托数量) */
    public final static String AUTO_PAKIN_QTY = "AUTO_PAKIN_QTY";
}
version/db/material_auto_config.sql
New file
@@ -0,0 +1,10 @@
-- 指定物料自动化定时任务配置(物料编码、自动全版出库、自动下发、入库自动组托及数量)
-- 使用方式:在系统配置中维护以下 flag,val 填物料编码或 true/false/数字
INSERT INTO `sys_config` (`uuid`, `name`, `flag`, `type`, `val`, `content`, `status`, `deleted`, `tenant_id`, `create_by`, `create_time`, `update_by`, `update_time`, `memo`)
VALUES
(UPPER(UUID()), '指定物料编码(自动全版出库/自动组托)', 'AUTO_FULL_OUT_MATNR_CODE', 3, '', '填物料编码时参与自动全版出库与自动组托', 1, 0, 1, NULL, NOW(), NULL, NOW(), '与 AUTO_FULL_OUT_ENABLED 等配合'),
(UPPER(UUID()), '启用:有库存时自动生成全版出库单', 'AUTO_FULL_OUT_ENABLED', 1, 'false', 'true/false', 1, 0, 1, NULL, NOW(), NULL, NOW(), '需配置 AUTO_FULL_OUT_MATNR_CODE'),
(UPPER(UUID()), '启用:该物料出库单自动下发任务', 'AUTO_FULL_OUT_DISPATCH_ENABLED', 1, 'false', 'true/false', 1, 0, 1, NULL, NOW(), NULL, NOW(), '需配置 AUTO_FULL_OUT_MATNR_CODE'),
(UPPER(UUID()), '启用:RCS入库通知时自动组托', 'AUTO_PAKIN_ON_ASN_ENABLED', 1, 'false', 'true/false', 1, 0, 1, NULL, NOW(), NULL, NOW(), '需配置 AUTO_FULL_OUT_MATNR_CODE、AUTO_PAKIN_QTY'),
(UPPER(UUID()), '自动组托数量', 'AUTO_PAKIN_QTY', 2, '1', '每条入库明细自动组托数量', 1, 0, 1, NULL, NOW(), NULL, NOW(), '与 AUTO_PAKIN_ON_ASN_ENABLED 配合')
;