chen.lin
昨天 98d88ac8caf7f0991d741079474c262f1e252927
拣货过程中的出库库存匹配
2个文件已添加
23个文件已修改
1144 ■■■■ 已修改文件
rsf-admin/src/i18n/en.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/zh.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/ResourceContent.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/orders/outStock/OutStockPublic.jsx 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/params/LocationAllocateParams.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/impl/WmsRcsServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/WcsController.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/ContainerWaveItemDto.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/QuickPickOrderModuleDto.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/WcsService.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/PdaOutStockServiceImpl.java 371 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/ReportMsgServiceImpl.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/WcsServiceImpl.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/constant/Constants.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/dto/OrderOutItemDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/params/LocToTaskParams.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/AutoRunSchedules.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/MaterialAutoSchedules.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/TaskSchedules.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/LocItemService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/TaskService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/LocItemServiceImpl.java 159 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/RcsTestServiceImpl.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/utils/LocManageUtil.java 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/en.js
@@ -211,6 +211,7 @@
        wave: 'Wave Manage',
        basStation: 'BasStation',
        basContainer: 'BasContainer',
        emptyOutbound: 'Empty Pallet Outbound',
        waveRule: 'Wave Rules',
        checkDiff: 'Check Diff',
        transfer: 'Transfer',
rsf-admin/src/i18n/zh.js
@@ -219,6 +219,7 @@
        outBound: '出库作业',
        checkOutBound: '盘点出库',
        stockTransfer: '库位转移',
        emptyOutbound: '空板出库',
        waveRule: '波次策略',
        checkOrder: '盘点单',
        checkDiff: '盘点差异单',
rsf-admin/src/page/ResourceContent.js
@@ -53,6 +53,7 @@
import outBound from "./work/outBound";
import checkOutBound from "./work/checkOutBound";
import stockTransfer from "./work/stockTransfer";
import emptyOutbound from "./work/emptyOutbound";
import waveRule from "./waveRule";
import check from "./orders/check";
import checkDiff from "./orders/check/checkDiff";
@@ -170,6 +171,8 @@
      return checkOutBound;
    case "stockTransfer":
      return stockTransfer;
    case "emptyOutbound":
      return emptyOutbound;
    case "waveRule":
      return waveRule;
    case "check":
rsf-admin/src/page/orders/outStock/OutStockPublic.jsx
@@ -238,7 +238,8 @@
                            siteNo: defaultSiteNo,
                            staNos: item.staNos || [],
                            sourceId: item.sourceId,
                            source: item.source
                            source: item.source,
                            pickingStatus: item.pickingStatus || null
                        };
                    }
                    // 如果数据已经是扁平结构,直接返回
@@ -399,9 +400,9 @@
        { field: 'unit', headerName: '单位', width: 60 },
        { field: 'outQty', headerName: '出库数量', width: 110, valueFormatter: (v) => formatQuantity(v) },
        {
            field: 'anfme', headerName: '库存数量', width: 110,
            field: 'anfme', headerName: '库存数量', width: 160,
            renderCell: (params) => (
                <OutStockAnfme value={params.value} />
                <OutStockAnfme value={params.value} row={params.row} />
            )
        },
        {
@@ -447,9 +448,18 @@
    }
    const OutStockAnfme = React.memo(function OutStockAnfme(props) {
        const { value } = props;
        const { value, row } = props;
        const pickingStatus = row?.pickingStatus;
        const num = Number(value);
        const hasStock = typeof num === 'number' && !Number.isNaN(num) && num > 1e-6;
        if (pickingStatus) {
            return (
                <Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'center' }}>
                    <span style={{ color: '#1976d2' }}>{pickingStatus}</span>
                    {!hasStock && <span style={{ color: 'red', fontSize: '0.85em' }}>{translate('common.edit.title.insuffInventory')}</span>}
                </Box>
            );
        }
        return (
            hasStock ? (
                <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
rsf-open-api/src/main/java/com/vincent/rsf/openApi/entity/params/LocationAllocateParams.java
@@ -19,6 +19,8 @@
    @ApiModelProperty(value = "入库站点", required = true)
    private String staNo;
    @ApiModelProperty(value = "是否空板:true=空板入库,空/false=普通入库(需组托)")
    private Boolean full;
    @ApiModelProperty(value = "入库类型", required = true)
    private Integer type;
rsf-open-api/src/main/java/com/vincent/rsf/openApi/service/impl/WmsRcsServiceImpl.java
@@ -437,6 +437,7 @@
        requestParams.put("barcode", params.getBarcode());
        requestParams.put("staNo", params.getStaNo());
        requestParams.put("type", params.getType());
        requestParams.put("full", params.getFull());
        log.info("请求参数:{}", requestParams.toJSONString());
        
        HttpHeaders headers = new HttpHeaders();
rsf-server/src/main/java/com/vincent/rsf/server/api/controller/WcsController.java
@@ -7,12 +7,22 @@
import com.vincent.rsf.server.api.entity.params.ExMsgParams;
import com.vincent.rsf.server.api.entity.params.WcsTaskParams;
import com.vincent.rsf.server.common.annotation.OperationLog;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.manager.controller.params.LocToTaskParams;
import com.vincent.rsf.server.manager.entity.Loc;
import com.vincent.rsf.server.manager.enums.LocStsType;
import com.vincent.rsf.server.manager.enums.TaskType;
import com.vincent.rsf.server.manager.service.LocItemService;
import com.vincent.rsf.server.manager.service.LocService;
import com.vincent.rsf.server.api.service.WcsService;
import com.vincent.rsf.server.common.constant.Constants;
import com.vincent.rsf.server.system.controller.BaseController;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -27,7 +37,23 @@
    @Autowired
    private WcsService wcsService;
    @Autowired
    private LocItemService locItemService;
    @Autowired
    private LocService locService;
    @ApiOperation("空板库位列表(分页),仅返回 useStatus=D 的库位,用于空板出库页勾选")
    @PreAuthorize("hasAuthority('manager:emptyOutbound:list')")
    @PostMapping("/empty/locs/page")
    public R emptyLocsPage(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
        PageParam<Loc, BaseParam> pageParam = new PageParam<>(baseParam, Loc.class);
        LambdaQueryWrapper<Loc> qw = new LambdaQueryWrapper<Loc>()
                .eq(Loc::getUseStatus, LocStsType.LOC_STS_TYPE_D.type)
                .orderByAsc(Loc::getId);
        return R.ok().add(locService.page(pageParam, qw));
    }
    @ApiOperation(value = "wcs生成入库任务接口")
    @PostMapping("/create/in/task")
@@ -138,7 +164,14 @@
        String barcode = (String) params.get("barcode");
        String staNo = (String) params.get("staNo");
        Integer type = params.get("type") != null ? Integer.valueOf(params.get("type").toString()) : null;
        Boolean full = null;
        if (params.get("full") != null) {
            if (params.get("full") instanceof Boolean) {
                full = (Boolean) params.get("full");
            } else {
                full = Boolean.parseBoolean(params.get("full").toString());
            }
        }
        if (Cools.isEmpty(barcode)) {
            return R.error("料箱码不能为空!!");
        }
@@ -148,8 +181,33 @@
        if (type == null) {
            return R.error("入库类型不能为空!!");
        }
        return wcsService.allocateLocation(barcode, staNo, type);
        return wcsService.allocateLocation(barcode, staNo, type, full);
    }
    @ApiOperation("空板出库:从指定空板库位生成出库任务至目标站点")
    @PreAuthorize("hasAuthority('manager:emptyOutbound:list')")
    @PostMapping("/empty/outbound")
    public R emptyOutbound(@RequestBody Map<String, Object> params) {
        if (Cools.isEmpty(params)) {
            return R.error("参数不能为空!!");
        }
        String staNo = (String) params.get("staNo");
        String orgLoc = (String) params.get("orgLoc");
        if (Cools.isEmpty(staNo)) {
            return R.error("目标站点 staNo 不能为空!!");
        }
        if (Cools.isEmpty(orgLoc)) {
            return R.error("源库位 orgLoc 不能为空!!");
        }
        LocToTaskParams map = new LocToTaskParams();
        map.setSiteNo(staNo);
        map.setOrgLoc(orgLoc);
        map.setType(Constants.TASK_TYPE_OUT_STOCK_EMPTY);
        Long userId = getLoginUserId();
        if (userId == null) {
            userId = 1L;
        }
        return R.ok("空板出库任务创建成功").add(locItemService.generateTaskEmpty(map, userId));
    }
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/ContainerWaveItemDto.java
New file
@@ -0,0 +1,22 @@
package com.vincent.rsf.server.api.entity.dto;
import com.vincent.rsf.server.manager.entity.WkOrderItem;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
/**
 * PDA 波次拣货列表项:支持单号区分与可追加标识
 */
@Data
@Accessors(chain = true)
@ApiModel(value = "ContainerWaveItemDto", description = "波次拣货明细(含单号、可追加)")
public class ContainerWaveItemDto {
    @ApiModelProperty("出库单明细(含单号等)")
    private WkOrderItem orderItem;
    @ApiModelProperty("是否可追加订单(后续分配的同物料订单,未拣货确认)")
    private Boolean appendable;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/entity/dto/QuickPickOrderModuleDto.java
New file
@@ -0,0 +1,27 @@
package com.vincent.rsf.server.api.entity.dto;
import com.vincent.rsf.server.manager.entity.TaskItem;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.List;
/**
 * 快速拣货按出库单分模块:一个出库单对应一个模块,展示该单的拣货数量
 */
@Data
@Accessors(chain = true)
@ApiModel(value = "QuickPickOrderModuleDto", description = "快速拣货-按订单模块")
public class QuickPickOrderModuleDto {
    @ApiModelProperty("出库单ID")
    private Long orderId;
    @ApiModelProperty("出库单号")
    private String orderCode;
    @ApiModelProperty("该订单下的拣货明细")
    private List<TaskItem> items;
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/WcsService.java
@@ -19,5 +19,5 @@
    R pubWcsTask(WcsTaskParams params);
    R allocateLocation(String barcode, String staNo, Integer type);
    R allocateLocation(String barcode, String staNo, Integer type, Boolean full);
}
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/PdaOutStockServiceImpl.java
@@ -5,6 +5,8 @@
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.api.entity.dto.ContainerWaveDto;
import com.vincent.rsf.server.api.entity.dto.ContainerWaveItemDto;
import com.vincent.rsf.server.api.entity.dto.QuickPickOrderModuleDto;
import com.vincent.rsf.server.api.entity.params.ContainerWaveParam;
import com.vincent.rsf.server.api.entity.params.WavePickItemsParams;
import com.vincent.rsf.server.api.service.PdaOutStockService;
@@ -76,70 +78,132 @@
    @Autowired
    private ConfigServiceImpl configService;
    /**
     * 快速拣货查询:同一箱码可能有多条任务,仅 RCS 出库回调后变为 199 的才展示;该箱码下仍不是 199 的 PDA 不显示。
     * 返回:orders 按出库单分模块、list/taskItems 该箱码下 199 任务明细。
     */
    @Override
    public R getOutStockTaskItem(String barcode) {
        LambdaQueryWrapper<Task> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(Task::getBarcode, barcode)
                .orderByDesc(Task::getId)
                .last("limit 1");
        Task task = taskService.getOne(lambdaQueryWrapper);
        if (null == task) {
            return R.error("未查询到相关任务");
        // 只查 199(WAVE_SEED)/AWAIT:已确认变成 200 的绝不能扫出来,明确排除 200 避免第二次扫到
        List<Task> tasks = taskService.list(new LambdaQueryWrapper<Task>()
                .eq(Task::getBarcode, barcode)
                .in(Task::getTaskStatus, Arrays.asList(TaskStsType.WAVE_SEED.id, TaskStsType.AWAIT.id))
                .ne(Task::getTaskStatus, TaskStsType.UPDATED_OUT.id)
                .orderByAsc(Task::getId));
        if (tasks == null || tasks.isEmpty()) {
            return R.error("未查询到待确认任务");
        }
        List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, task.getId()));
        if (null == taskItems || taskItems.size() <= 0) {
        List<Long> taskIds = tasks.stream().map(Task::getId).collect(Collectors.toList());
        List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().in(TaskItem::getTaskId, taskIds));
        if (taskItems == null || taskItems.isEmpty()) {
            return R.error("任务出错,未查询到相关任务明细");
        }
        return R.ok(taskItems);
        // 同一箱码下可能有多条(多个出库单),按出库单分组;仅返回尚未拣完的订单模块
        String nullKey = "__none__";
        Map<String, List<TaskItem>> byOrder = taskItems.stream()
                .collect(Collectors.groupingBy(ti -> ti.getOrderId() != null ? "o_" + ti.getOrderId() : (StringUtils.isNotBlank(ti.getSourceCode()) ? "s_" + ti.getSourceCode() : nullKey)));
        List<QuickPickOrderModuleDto> orders = new ArrayList<>();
        for (Map.Entry<String, List<TaskItem>> e : byOrder.entrySet()) {
            List<TaskItem> items = e.getValue();
            boolean allPicked = items.stream().allMatch(ti -> ti.getQty() != null && ti.getAnfme() != null && ti.getQty().compareTo(ti.getAnfme()) >= 0);
            if (allPicked) continue;
            TaskItem first = items.get(0);
            orders.add(new QuickPickOrderModuleDto()
                    .setOrderId(first.getOrderId())
                    .setOrderCode(StringUtils.isNotBlank(first.getSourceCode()) ? first.getSourceCode() : ("单号:" + (first.getOrderId() != null ? first.getOrderId() : "—")))
                    .setItems(items));
        }
        R r = orders.isEmpty() ? R.ok("全部拣货已完成") : R.ok();
        r.put("orders", orders);
        r.put("taskItems", taskItems);
        r.put("list", taskItems); // 同一箱码下多条明细,便于直接展示
        return r;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    @Synchronized
    public R saveOutTaskSts(String barcode) {
        LambdaQueryWrapper<Task> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(Task::getBarcode, barcode)
                .orderByDesc(Task::getId)
                .last("limit 1");
        Task task = taskService.getOne(lambdaQueryWrapper);
        if (null == task) {
            throw new CoolException("未找到料箱码对应任务");
        // 只统计当前「待确认」任务:出库单有3单但只下发了2个任务时,2个任务都拣完即可确认并生成拣料入库;有任务被取消则只处理剩余任务
        List<Task> tasks = taskService.list(new LambdaQueryWrapper<Task>()
                .eq(Task::getBarcode, barcode)
                .in(Task::getTaskStatus, Arrays.asList(TaskStsType.WAVE_SEED.id, TaskStsType.AWAIT.id))
                .orderByAsc(Task::getId));
        if (tasks == null || tasks.isEmpty()) {
            throw new CoolException("未找到料箱码对应任务或任务状态不是等待确认");
        }
        // 允许 199(WAVE_SEED 播种中/待确认)或 196(AWAIT 等待确认),与盘点 PDA 逻辑一致
        if (!task.getTaskStatus().equals(TaskStsType.WAVE_SEED.id)
                && !task.getTaskStatus().equals(TaskStsType.AWAIT.id)) {
            return R.error("任务状态不是等待确认");
        }
        Long loginUserId = SystemAuthUtils.getLoginUserId();
        if (loginUserId == null) {
            loginUserId = 1L; // 使用默认值
            loginUserId = 1L;
        }
        try {
            if (task.getTaskType().equals(TaskType.TASK_TYPE_PICK_AGAIN_OUT.type)) {
                // 拣料出库:创建拣料入库任务(形成闭环)
                taskService.pickOrCheckTask(task.getId(), "");
                return R.ok("确认成功,已创建拣料入库任务");
            } else if (task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_OUT.type)) {
                // 盘点出库:创建盘点入库任务(形成闭环)
                taskService.pickOrCheckTask(task.getId(), Constants.TASK_TYPE_OUT_CHECK);
            Task first = tasks.get(0);
            if (first.getTaskType().equals(TaskType.TASK_TYPE_PICK_AGAIN_OUT.type)) {
                // 确认前该箱码下已有 200 的(例如第一次已确认的):本次只把当前 199 置为 200,不生成拣料入库,避免“第二次误确认”导致错误扣减和生成入库
                long already200 = taskService.count(new LambdaQueryWrapper<Task>()
                        .eq(Task::getBarcode, barcode)
                        .eq(Task::getTaskType, TaskType.TASK_TYPE_PICK_AGAIN_OUT.type)
                        .eq(Task::getTaskStatus, TaskStsType.UPDATED_OUT.id));
                // 确认即已确认:当前 199 任务全部置为 200,并回写已拣数量(qty);仅当本次确认前没有任何 200 且确认后全部 200 时才统一扣减并生成拣料入库
                for (Task task : tasks) {
                    task.setTaskStatus(TaskStsType.UPDATED_OUT.id)
                            .setUpdateBy(loginUserId)
                            .setUpdateTime(new Date());
                    if (!taskService.updateById(task)) {
                        return R.error("更新任务状态失败");
                    }
                    List<TaskItem> items = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, task.getId()));
                    for (TaskItem ti : items) {
                        if (ti.getQty() == null || ti.getQty().compareTo(0.0) <= 0) {
                            ti.setQty(ti.getAnfme() != null ? ti.getAnfme() : 0.0);
                            ti.setUpdateBy(loginUserId);
                            ti.setUpdateTime(new Date());
                            taskItemService.updateById(ti);
                        }
                    }
                }
                long not200 = taskService.count(new LambdaQueryWrapper<Task>()
                        .eq(Task::getBarcode, barcode)
                        .ne(Task::getTaskStatus, TaskStsType.UPDATED_OUT.id));
                if (not200 > 0) {
                    return R.ok("确认成功");
                }
                // 本次确认前该箱码下已有 200 的,不在此处生成拣料入库,由定时任务在“全部 200”时统一处理
                if (already200 > 0) {
                    return R.ok("确认成功;同箱已有过确认任务,扣减与拣料入库由系统在全部200后统一处理");
                }
                // 本次确认前没有任何 200,且确认后同箱码已全部 200:统一扣减、有余量才生成拣料入库单
                List<Task> all200 = taskService.list(new LambdaQueryWrapper<Task>()
                        .eq(Task::getBarcode, barcode)
                        .eq(Task::getTaskType, TaskType.TASK_TYPE_PICK_AGAIN_OUT.type)
                        .eq(Task::getTaskStatus, TaskStsType.UPDATED_OUT.id)
                        .orderByAsc(Task::getId));
                for (Task task : all200) {
                    taskService.pickOrCheckTask(task.getId(), "");
                }
                return R.ok("确认成功,已统一扣减并生成拣料入库任务(有余量时)");
            }
            if (first.getTaskType().equals(TaskType.TASK_TYPE_CHECK_OUT.type)) {
                for (Task task : tasks) {
                    taskService.pickOrCheckTask(task.getId(), Constants.TASK_TYPE_OUT_CHECK);
                }
                return R.ok("确认成功,已创建盘点入库任务");
            } else if (task.getTaskType().equals(TaskType.TASK_TYPE_OUT.type)) {
                // 全版出库:更新为200(最终完成,不闭环)
                taskService.completeFullOutStock(task.getId(), loginUserId);
            }
            if (first.getTaskType().equals(TaskType.TASK_TYPE_OUT.type)) {
                for (Task task : tasks) {
                    taskService.completeFullOutStock(task.getId(), loginUserId);
                }
                return R.ok("确认成功,全版出库已完成");
            } else {
                // 其他出库类型:直接更新为200
            }
            for (Task task : tasks) {
                task.setTaskStatus(TaskStsType.UPDATED_OUT.id)
                        .setUpdateBy(loginUserId)
                        .setUpdateTime(new Date());
                if (!taskService.updateById(task)) {
                    return R.error("更新任务状态失败");
                }
                return R.ok("确认成功");
            }
            return R.ok("确认成功");
        } catch (Exception e) {
            throw new CoolException("快速拣货确认失败:" + e.getMessage());
        }
@@ -168,24 +232,47 @@
        if (!task.getTaskStatus().equals(TaskStsType.WAVE_SEED.id)) {
            return R.error("任务状态不是揀料狀態");
        }
        List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, task.getId()));
        Set<Long> longSet = taskItems.stream().map(TaskItem::getSourceId).collect(Collectors.toSet());
        List<WaveOrderRela> waveOrderRelas = waveOrderRelaService.list(new LambdaQueryWrapper<WaveOrderRela>()
                .in(WaveOrderRela::getWaveId, longSet));
        if (Cools.isEmpty(waveOrderRelas)) {
        // 当前料箱对应库位下所有处于「预约出库/拣货中」的任务(含可追加的后续订单)
        String orgLoc = task.getOrgLoc();
        List<Integer> pickingStatuses = Arrays.asList(TaskStsType.GENERATE_OUT.id, TaskStsType.WAVE_SEED.id);
        List<Task> sameLocTasks = taskService.list(new LambdaQueryWrapper<Task>()
                .eq(Task::getOrgLoc, orgLoc)
                .in(Task::getTaskStatus, pickingStatuses));
        Set<Long> waveIds = new java.util.HashSet<>();
        Set<String> matnrCodes = new java.util.HashSet<>();
        for (Task t : sameLocTasks) {
            List<TaskItem> items = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, t.getId()));
            for (TaskItem ti : items) {
                if (ti.getSourceId() != null) waveIds.add(ti.getSourceId());
                if (StringUtils.isNotBlank(ti.getMatnrCode())) matnrCodes.add(ti.getMatnrCode());
            }
        }
        if (waveIds.isEmpty()) {
            throw new CoolException("波次对应关联单未找到");
        }
        List<WaveOrderRela> waveOrderRelas = waveOrderRelaService.list(new LambdaQueryWrapper<WaveOrderRela>()
                .in(WaveOrderRela::getWaveId, waveIds));
        Set<Long> orderIds = waveOrderRelas.stream().map(WaveOrderRela::getOrderId).collect(Collectors.toSet());
        List<WkOrder> wkOrders = asnOrderService.listByIds(orderIds);
        if (wkOrders.isEmpty()) {
            throw new CoolException("单据不存在!!");
        }
        Set<String> codes = taskItems.stream().map(TaskItem::getMatnrCode).collect(Collectors.toSet());
        // 按订单创建时间排序,先创建的为主订单,后续为可追加
        wkOrders.sort(Comparator.comparing(WkOrder::getCreateTime, Comparator.nullsLast(Comparator.naturalOrder())));
        Set<String> codes = matnrCodes.isEmpty() ? taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, task.getId()))
                .stream().map(TaskItem::getMatnrCode).filter(StringUtils::isNotBlank).collect(Collectors.toSet()) : matnrCodes;
        List<WkOrderItem> orderItems = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>()
                .in(WkOrderItem::getMatnrCode, codes)
                .in(WkOrderItem::getOrderId, orderIds));
        return R.ok("查询成功").add(orderItems);
        List<ContainerWaveItemDto> result = new ArrayList<>();
        Long firstOrderId = wkOrders.isEmpty() ? null : wkOrders.get(0).getId();
        for (WkOrderItem item : orderItems) {
            boolean appendable = firstOrderId != null && !firstOrderId.equals(item.getOrderId());
            result.add(new ContainerWaveItemDto().setOrderItem(item).setAppendable(appendable));
        }
        R r = R.ok("查询成功");
        r.put("list", result);
        return r;
//        ArrayList<ContainerWaveDto> containerWaveDtos = new ArrayList<>();
////        List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, task.getId()));
@@ -320,109 +407,52 @@
        }
        List<TaskItem> taskItems = params.getTaskItems();
        Map<String, List<TaskItem>> listMap = taskItems.stream().collect(Collectors.groupingBy(TaskItem::getMatnrCode));
        // 拣货完成仅扣减库位数量并累加 TaskItem.qty,不更新出库单/订单;待托盘全部拣完在 saveWavePick 再按顺序更新库存并校验
        Config config = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.ALLOW_OVER_CHANGE));
        listMap.keySet().forEach(code -> {
            List<TaskItem> items = listMap.get(code);
            //一张出库单,相同的品种不会出现两次
            WkOrderItem orderItem = asnOrderItemService.getOne(new LambdaQueryWrapper<WkOrderItem>()
                    .eq(WkOrderItem::getMatnrCode, code)
                    .eq(WkOrderItem::getOrderId, order.getId()));
            if (Objects.isNull(orderItem)) {
                throw new CoolException("数据错误,拣料不在单据需求中!!");
            }
            //taskItems为拣货明细,作参数上报
            Double summed = items.stream().mapToDouble(TaskItem::getAnfme).sum();
            //加上历史拣料数量
            Double pickQty = Math.round((orderItem.getQty() + summed) * 1000000) / 1000000.0;
            Config config = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.ALLOW_OVER_CHANGE));
            //判断是否允许超收,不允许超收添加拒收判断
            if (!Objects.isNull(config)) {
                if (!Boolean.parseBoolean(config.getVal())) {
                    if (pickQty.compareTo(orderItem.getAnfme()) > 0.0) {
                        throw new CoolException("播种数量不能超出订单需求数量");
                    }
            Double summed = items.stream().mapToDouble(ti -> ti.getAnfme() != null ? ti.getAnfme() : 0.0).sum();
            Double pickQty = Math.round((orderItem.getQty() != null ? orderItem.getQty() : 0.0) + summed) * 1000000.0 / 1000000.0;
            if (!Objects.isNull(config) && !Boolean.parseBoolean(config.getVal())) {
                if (pickQty.compareTo(orderItem.getAnfme()) > 0.0) {
                    throw new CoolException("播种数量不能超出订单需求数量");
                }
            }
            orderItem.setQty(pickQty);
            if (!asnOrderItemService.updateById(orderItem)) {
                throw new CoolException("出库单明细更新失败!!");
            }
            Stock stock = new Stock();
            String ruleCode = SerialRuleUtils.generateRuleCode(SerialRuleCode.SYS_STOCK_CODE, null);
            if (StringUtils.isBlank(ruleCode)) {
                throw new CoolException("当前业务:" + SerialRuleCode.SYS_STOCK_CODE + ",编码规则不存在!!");
            }
            Double sum = taskItems.stream().mapToDouble(TaskItem::getAnfme).sum();
            stock.setCode(ruleCode)
                    .setUpdateBy(SystemAuthUtils.getLoginUserId())
                    .setBarcode(task.getBarcode())
                    .setLocCode(task.getOrgLoc())
                    .setType(order.getType())
                    .setWkType(Short.parseShort(order.getWkType()))
                    .setSourceId(orderItem.getOrderId())
                    .setSourceCode(orderItem.getOrderCode())
                    .setUpdateTime(new Date())
                    .setAnfme(sum);
            if (!stockService.save(stock)) {
                throw new CoolException("出入库历史保存失败!!");
            }
           List<StockItem> stockItems = new ArrayList<>();
            items.forEach(taskItem -> {
                TaskItem item = taskItemService.getById(taskItem.getId());
                //判断是否允许超收,不允许超收添加拒收判断
                if (!Objects.isNull(config)) {
                    TaskItem serviceOne = taskItemService.getOne(new LambdaQueryWrapper<TaskItem>()
                            .eq(TaskItem::getTaskId, task.getId())
                            .eq(TaskItem::getFieldsIndex, item.getFieldsIndex()));
                    if (Objects.isNull(serviceOne)) {
                        throw new CoolException("缓存数据丢失!!");
                    }
                    LocItemWorking workItem = locItemWorkingService.getOne(new LambdaQueryWrapper<LocItemWorking>()
                            .eq(LocItemWorking::getTaskId, task.getId())
                            .eq(LocItemWorking::getFieldsIndex, item.getFieldsIndex()));
                    if (Objects.isNull(workItem)) {
                        throw new CoolException("缓存数据丢失!!");
                    }
                    Double v1 = Math.round((workItem.getAnfme() - serviceOne.getQty()) * 1000000) / 1000000.0;
                    //不管是否允许超收,都需判断是否超出库存范围(票号暂不使用,该判断注释)
                    // if (taskItem.getAnfme().compareTo(v1) > 0) {
                    //     throw new CoolException("拣货数量超出当前票号库存数量!!");
                    // }
                    if (!Boolean.parseBoolean(config.getVal())) {
                        Double v = Math.round((item.getQty() + taskItem.getAnfme()) * 1000000) / 1000000.0;
                        if (item.getAnfme().compareTo(v) < 0.0) {
                            throw new CoolException("前当物料已超出可拣范围,请核对后再操作!!");
                        }
                if (Objects.isNull(item)) {
                    throw new CoolException("任务明细不存在!!");
                }
                if (!Objects.isNull(config) && !Boolean.parseBoolean(config.getVal())) {
                    Double v = Math.round(((item.getQty() != null ? item.getQty() : 0.0) + (taskItem.getAnfme() != null ? taskItem.getAnfme() : 0.0)) * 1000000.0) / 1000000.0;
                    if (item.getAnfme() != null && item.getAnfme().compareTo(v) < 0.0) {
                        throw new CoolException("当前物料已超出可拣范围,请核对后再操作!!");
                    }
                }
                Double picQty = Math.round((item.getQty() + taskItem.getAnfme()) * 1000000) / 1000000.0;
                Double picQty = Math.round(((item.getQty() != null ? item.getQty() : 0.0) + (taskItem.getAnfme() != null ? taskItem.getAnfme() : 0.0)) * 1000000.0) / 1000000.0;
                item.setQty(picQty).setOrderId(order.getId()).setOrderItemId(orderItem.getId());
                if (!taskItemService.updateById(item)) {
                    throw new CoolException("状态完成失败!!");
                    throw new CoolException("拣货数量更新失败!!");
                }
                // 扣减库位明细库存(与出库完成逻辑保持一致)
                if (StringUtils.isNotBlank(task.getOrgLoc())) {
                    LocItem locItem = locItemService.getOne(new LambdaQueryWrapper<LocItem>()
                            .eq(LocItem::getLocCode, task.getOrgLoc())
                            .eq(LocItem::getMatnrId, item.getMatnrId())
                            .eq(StringUtils.isNotBlank(item.getBatch()), LocItem::getBatch, item.getBatch())
                            .eq(StringUtils.isNotBlank(item.getFieldsIndex()), LocItem::getFieldsIndex, item.getFieldsIndex()));
                    if (Objects.nonNull(locItem)) {
                        // 使用实际拣货数量(taskItem.getAnfme())扣减库位明细
                        Double newAnfme = Math.round((locItem.getAnfme() - taskItem.getAnfme()) * 1000000) / 1000000.0;
                        Double pickAmt = taskItem.getAnfme() != null ? taskItem.getAnfme() : 0.0;
                        Double newAnfme = Math.round((locItem.getAnfme() - pickAmt) * 1000000.0) / 1000000.0;
                        if (newAnfme.compareTo(0.0) <= 0) {
                            // 数量小于等于0,删除库位明细
                            locItemService.removeById(locItem.getId());
                        } else {
                            // 更新库位明细数量
                            locItem.setAnfme(newAnfme)
                                    .setUpdateBy(SystemAuthUtils.getLoginUserId())
                                    .setUpdateTime(new Date());
@@ -432,33 +462,8 @@
                        }
                    }
                }
                StockItem stockItem = new StockItem();
                BeanUtils.copyProperties(item, stockItem);
                //taskItem为上报数据
                stockItem.setStockId(stock.getId()).setAnfme(taskItem.getAnfme()).setStockCode(stock.getCode()).setSourceItemId(orderItem.getId());
                stockItems.add(stockItem);
            });
            if (!stockItemService.saveBatch(stockItems)) {
                throw new CoolException("出入库历史明细保存失败!!");
            }
        });
        List<WkOrderItem> orderItems = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>().eq(WkOrderItem::getOrderId, params.getOrderId()));
        Double total = orderItems.stream().mapToDouble(WkOrderItem::getQty).sum();
        Double wkQty = orderItems.stream().mapToDouble(WkOrderItem::getWorkQty).sum();
        double v = order.getWorkQty().compareTo(wkQty) < 0 ? 0.0 : Math.round((total - wkQty) * 1000000) / 1000000.0;
        order.setQty(total).setWorkQty(v);
        if (!asnOrderService.updateById(order)) {
            throw new CoolException("订单数量更新失败!!");
        }
//        //检查单据是否完成
//        if (order.getAnfme().compareTo(order.getQty()) == 0) {
//            order.setExceStatus(AsnExceStatus.OUT_STOCK_STATUS_TASK_DONE.val);
//            if (!asnOrderService.updateById(order)) {
//                throw new CoolException("出库单更新状态失败");
//            }
//        }
        return R.ok();
    }
@@ -582,36 +587,80 @@
            return R.error("任务状态不是待揀狀態");
        }
        Config config = configService.getOne(new LambdaQueryWrapper<Config>().eq(Config::getFlag, GlobalConfigCode.ALLOW_OVER_CHANGE));
        //判断是否允许超收,不允许超收添加拒收判断
        if (!Objects.isNull(config)) {
            if (!Boolean.parseBoolean(config.getVal())) {
                List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, task.getId()));
                taskItems.forEach(taskItem -> {
                    if ((taskItem.getQty().compareTo(taskItem.getAnfme()) < 0)) {
                        throw new CoolException("有单据物料未拣,请拣完后再确认!!");
                    }
                });
        List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, task.getId()));
        // 必须当前托盘关联出库单全部拣货完成才允许确认
        for (TaskItem ti : taskItems) {
            Double q = ti.getQty() != null ? ti.getQty() : 0.0;
            Double a = ti.getAnfme() != null ? ti.getAnfme() : 0.0;
            if (q.compareTo(a) < 0) {
                throw new CoolException("有单据物料未拣完,请完成该托盘下所有订单拣货后再确认!!");
            }
        }
        // 按顺序更新出库单明细、订单及库存流水(与 wavePickItems 原逻辑一致,在全部拣完后统一执行)
        Map<Long, List<TaskItem>> byOrder = taskItems.stream()
                .filter(ti -> ti.getOrderId() != null)
                .collect(Collectors.groupingBy(TaskItem::getOrderId));
        List<Long> orderIds = new ArrayList<>(byOrder.keySet());
        orderIds.sort(Long::compareTo);
        for (Long orderId : orderIds) {
            WkOrder order = asnOrderService.getById(orderId);
            if (order == null) continue;
            List<TaskItem> items = byOrder.get(orderId);
            Map<String, List<TaskItem>> byMatnr = items.stream().collect(Collectors.groupingBy(TaskItem::getMatnrCode));
            for (String code : byMatnr.keySet()) {
                List<TaskItem> matItems = byMatnr.get(code);
                WkOrderItem orderItem = asnOrderItemService.getOne(new LambdaQueryWrapper<WkOrderItem>()
                        .eq(WkOrderItem::getMatnrCode, code)
                        .eq(WkOrderItem::getOrderId, orderId));
                if (orderItem == null) continue;
                Double summed = matItems.stream().mapToDouble(t -> t.getQty() != null ? t.getQty() : 0.0).sum();
                orderItem.setQty(summed);
                asnOrderItemService.updateById(orderItem);
                String ruleCode = SerialRuleUtils.generateRuleCode(SerialRuleCode.SYS_STOCK_CODE, null);
                if (StringUtils.isBlank(ruleCode)) continue;
                Stock stock = new Stock();
                stock.setCode(ruleCode)
                        .setUpdateBy(loginUserId)
                        .setBarcode(task.getBarcode())
                        .setLocCode(task.getOrgLoc())
                        .setType(order.getType())
                        .setWkType(Short.parseShort(order.getWkType()))
                        .setSourceId(orderItem.getOrderId())
                        .setSourceCode(orderItem.getOrderCode())
                        .setUpdateTime(new Date())
                        .setAnfme(summed);
                if (!stockService.save(stock)) continue;
                List<StockItem> stockItems = new ArrayList<>();
                for (TaskItem ti : matItems) {
                    StockItem si = new StockItem();
                    BeanUtils.copyProperties(ti, si);
                    si.setStockId(stock.getId()).setAnfme(ti.getQty()).setStockCode(stock.getCode()).setSourceItemId(orderItem.getId());
                    stockItems.add(si);
                }
                stockItemService.saveBatch(stockItems);
            }
            List<WkOrderItem> ois = asnOrderItemService.list(new LambdaQueryWrapper<WkOrderItem>().eq(WkOrderItem::getOrderId, orderId));
            Double total = ois.stream().mapToDouble(oi -> oi.getQty() != null ? oi.getQty() : 0.0).sum();
            Double wkQty = ois.stream().mapToDouble(oi -> oi.getWorkQty() != null ? oi.getWorkQty() : 0.0).sum();
            double v = (order.getWorkQty() != null && order.getWorkQty().compareTo(wkQty) < 0) ? 0.0 : Math.round((total - wkQty) * 1000000.0) / 1000000.0;
            order.setQty(total).setWorkQty(v);
            asnOrderService.updateById(order);
        }
        try {
            if (task.getTaskType().equals(TaskType.TASK_TYPE_PICK_AGAIN_OUT.type)) {
                // 拣料出库:创建拣料入库任务
                taskService.pickOrCheckTask(task.getId(), "");
            } else if (task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_OUT.type)) {
                // 盘点出库:创建盘点入库任务
                taskService.pickOrCheckTask(task.getId(), Constants.TASK_TYPE_OUT_CHECK);
            } else {
                // 其他出库类型:直接更新为200
                task.setTaskStatus(TaskStsType.UPDATED_OUT.id);
                if (!taskService.updateById(task)) {
                    throw new CoolException("任务状态更新失败");
                }
            }
        } catch (Exception e) {
            throw new CoolException("分拣失败");
            throw new CoolException("分拣失败:" + e.getMessage());
        }
        return R.ok();
    }
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/ReportMsgServiceImpl.java
@@ -302,10 +302,13 @@
                //获取库存中订单库位
                Set<Long> longSet = stockItems.stream().map(StockItem::getStockId).collect(Collectors.toSet());
                Stock stocks = stockService.getOne(new LambdaQueryWrapper<Stock>()
                        .in(Stock::getId, longSet)
                        .eq(Stock::getType, OrderType.ORDER_IN.type)
                        .eq(Stock::getSourceCode, order.getCode()));
                Stock stocks = null;
                if (!longSet.isEmpty()) {
                    stocks = stockService.getOne(new LambdaQueryWrapper<Stock>()
                            .in(Stock::getId, longSet)
                            .eq(Stock::getType, OrderType.ORDER_IN.type)
                            .eq(Stock::getSourceCode, order.getCode()));
                }
                if (!Objects.isNull(stocks)) {
                    param.setZone(stocks.getLocCode());
                }
@@ -319,7 +322,7 @@
                        .setItemCode(orderItem.getMatnrCode())
                        .setEditUser(nickName)
                        .setEditDate(order.getUpdateTime())
                        .setZone(stocks.getLocCode())
                        .setZone(stocks != null ? stocks.getLocCode() : null)
                        // .setGoodsNO(fields.get("crushNo"))  // 票号暂不使用
                        .setMemoDtl(order.getMemo());
@@ -393,6 +396,9 @@
                //过滤拣货入库明细,避免上报
                List<Stock> stockList = stocks.stream().filter(stock -> stock.getType().equals(OrderType.ORDER_OUT.type) && !Objects.isNull(stock.getSourceCode())).collect(Collectors.toList());
                List<Long> list = stockList.stream().map(Stock::getId).collect(Collectors.toList());
                if (list.isEmpty()) {
                    return;
                }
                List<StockItem> stockItems1 = stockItemService.list(new LambdaQueryWrapper<StockItem>().in(StockItem::getStockId, list));
                String finalNickName = nickName;
                stockItems1.forEach(stockItem -> {
@@ -500,11 +506,14 @@
                    .eq(StockItem::getFieldsIndex, orderItem.getFieldsIndex()));
            //获取库存中订单库位
            List<Long> longSet = stockItems.stream().map(StockItem::getStockId).collect(Collectors.toList());
            //获取库存库位信息
            Stock stocks = stockService.getOne(new LambdaQueryWrapper<Stock>()
                    .in(Stock::getId, longSet)
                    .eq(Stock::getType, OrderType.ORDER_IN.type)
                    .eq(Stock::getSourceCode, order.getCode()));
            //获取库存库位信息(避免 longSet 为空时生成 stock_id IN () 导致 SQL 异常)
            Stock stocks = null;
            if (!longSet.isEmpty()) {
                stocks = stockService.getOne(new LambdaQueryWrapper<Stock>()
                        .in(Stock::getId, longSet)
                        .eq(Stock::getType, OrderType.ORDER_IN.type)
                        .eq(Stock::getSourceCode, order.getCode()));
            }
            if (!Objects.isNull(stocks)) {
                param.setZone(stocks.getLocCode());
            }
rsf-server/src/main/java/com/vincent/rsf/server/api/service/impl/WcsServiceImpl.java
@@ -39,6 +39,7 @@
import com.vincent.rsf.server.manager.enums.LocStsType;
import com.vincent.rsf.server.system.utils.SerialRuleUtils;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -497,6 +498,8 @@
            log.warn("[RCS入库申请-自动组托] 入库单编码规则未配置");
            return;
        }
//        val orderWorkTypeOtherIn = OrderWorkType.ORDER_WORK_TYPE_OTHER_IN.type;
        //                .setWkType(orderWorkTypeOtherIn)
        WkOrder order = new WkOrder();
        order.setCode(ruleCode)
                .setType(OrderType.ORDER_IN.type)
@@ -578,10 +581,18 @@
     */
    private Task createTask(String ruleCode, String targetLoc, String barcode,
                            String targetSite, String sourceSiteNo, Long loginUserId) {
        return createTask(ruleCode, targetLoc, barcode, targetSite, sourceSiteNo, loginUserId, TaskType.TASK_TYPE_IN.type);
    }
    /**
     * 创建并保存任务(支持指定任务类型,如空板入库)
     */
    private Task createTask(String ruleCode, String targetLoc, String barcode,
                            String targetSite, String sourceSiteNo, Long loginUserId, Integer taskType) {
        Task task = new Task();
        task.setTaskCode(ruleCode)
                .setTaskStatus(TaskStsType.GENERATE_IN.id)
                .setTaskType(TaskType.TASK_TYPE_IN.type)
                .setTaskType(taskType != null ? taskType : TaskType.TASK_TYPE_IN.type)
                .setWarehType(WarehType.WAREHOUSE_TYPE_CRN.val)
                .setTargLoc(targetLoc)
                .setBarcode(barcode)
@@ -607,6 +618,75 @@
        if (!updated) {
            throw new CoolException("库位预约失败!!");
        }
    }
    /**
     * 空板入库:RCS 申请时 full=true,无需组托,分配库位并创建 TASK_TYPE_EMPITY_IN 任务。
     * 需在设备站点中配置 type=10(空板入库)的站点路径。
     */
    private InTaskMsgDto createInTaskForEmptyPallet(String barcode, String staNo, Integer type) {
        TaskInParam param = new TaskInParam();
        param.setBarcode(barcode);
        param.setSourceStaNo(staNo);
        param.setLocType1(type != null ? type : 1);
        param.setIoType(TaskType.TASK_TYPE_EMPITY_IN.type);
        param.setUser(1L);
        // 校验设备站点(需配置 type=10 空板入库的站点)
        DeviceSite deviceSite = validateDeviceSite(param);
        // 检查该托盘号是否已有空板入库任务,有则复用
        Task existingInTask = taskService.getOne(new LambdaQueryWrapper<Task>()
                .eq(Task::getBarcode, barcode)
                .eq(Task::getTaskType, TaskType.TASK_TYPE_EMPITY_IN.type)
                .orderByDesc(Task::getCreateTime)
                .last("LIMIT 1"));
        if (existingInTask != null) {
            log.info("找到该托盘号已有空板入库任务,复用 - 任务编码:{},箱号:{}", existingInTask.getTaskCode(), barcode);
            if (StringUtils.isNotBlank(staNo) && !staNo.equals(existingInTask.getOrgSite())) {
                existingInTask.setOrgSite(staNo);
                taskService.updateById(existingInTask);
            }
            InTaskMsgDto msgDto = new InTaskMsgDto();
            msgDto.setWorkNo(existingInTask.getTaskCode());
            msgDto.setTaskId(existingInTask.getId());
            msgDto.setLocNo(existingInTask.getTargLoc());
            msgDto.setSourceStaNo(existingInTask.getOrgSite());
            msgDto.setStaNo(existingInTask.getTargSite());
            return msgDto;
        }
        // 该托盘已在库或出库中,不可重复申请空板入库
        List<Loc> inStock = locService.list(new LambdaQueryWrapper<Loc>().eq(Loc::getBarcode, barcode));
        if (!inStock.isEmpty()) {
            throw new CoolException("barcode=" + barcode + ": 该托盘已在库,不可重复申请入库");
        }
        Task outboundTask = taskService.getOne(new LambdaQueryWrapper<Task>()
                .eq(Task::getBarcode, barcode)
                .in(Task::getTaskType, Arrays.asList(TaskType.TASK_TYPE_OUT.type, TaskType.TASK_TYPE_EMPITY_OUT.type,
                        TaskType.TASK_TYPE_PICK_AGAIN_OUT.type, TaskType.TASK_TYPE_CHECK_OUT.type))
                .lt(Task::getTaskStatus, TaskStsType.COMPLETE_OUT.id));
        if (outboundTask != null) {
            throw new CoolException("barcode=" + barcode + ": 该托盘出库中未完成,不可申请入库");
        }
        InTaskMsgDto locNo;
        try {
            locNo = getLocNo(param);
        } catch (Exception e) {
            throw new CoolException("获取空板入库库位失败:" + e.getMessage());
        }
        if (locNo == null || StringUtils.isBlank(locNo.getLocNo())) {
            throw new CoolException("未找到可用的空库位,请检查库区与设备站点配置(空板入库需配置 type=10 的站点)");
        }
        String ruleCode = generateTaskCode();
        String targetSite = StringUtils.isNotBlank(deviceSite.getDeviceSite()) ? deviceSite.getDeviceSite() : staNo;
        Task task = createTask(ruleCode, locNo.getLocNo(), barcode, targetSite, staNo, param.getUser(), TaskType.TASK_TYPE_EMPITY_IN.type);
        updateLocStatus(task.getTargLoc(), barcode);
        locNo.setWorkNo(ruleCode);
        locNo.setTaskId(task.getId());
        log.info("[空板入库] 已创建任务: {}, 库位: {}, 料箱: {}", ruleCode, locNo.getLocNo(), barcode);
        return locNo;
    }
    /**
@@ -1476,9 +1556,19 @@
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R allocateLocation(String barcode, String staNo, Integer type) {
    public R allocateLocation(String barcode, String staNo, Integer type, Boolean full) {
        log.info("========== 开始申请入库任务,分配库位 ==========");
        log.info("料箱码:{},入库站点:{},入库类型:{}", barcode, staNo, type);
        log.info("料箱码:{},入库站点:{},入库类型:{},空板:{}", barcode, staNo, type, full);
        // full=true 时走空板入库(无需组托);否则走普通入库(需组托或自动组托)
        if (Boolean.TRUE.equals(full)) {
            InTaskMsgDto msgDto = createInTaskForEmptyPallet(barcode, staNo, type);
            JSONObject result = new JSONObject();
            result.put("locNo", msgDto.getLocNo());
            result.put("batchNo", msgDto.getWorkNo());
            result.put("taskNo", msgDto.getWorkNo());
            return R.ok(result);
        }
        // 构建 TaskInParam 参数,与 /wcs/create/in/task 接口参数一致
        TaskInParam param = new TaskInParam();
rsf-server/src/main/java/com/vincent/rsf/server/common/constant/Constants.java
@@ -119,6 +119,11 @@
    public static final String TASK_TYPE_OUT_PICK = "pick";
    /**
     * 空板出库
     */
    public static final String TASK_TYPE_OUT_STOCK_EMPTY = "empty";
    /**
     * 排序默认值
     */
    public static final Integer TASK_SORT_DEFAULT_VALUE =  49;
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/dto/OrderOutItemDto.java
@@ -20,6 +20,9 @@
    private String sitesNo;
    /** 拣料出库未确认时:展示「正在拣料中,剩余 X 可用」;仅当剩余不可用时为库存不足 */
    private String pickingStatus;
    private String sourceId;
    private String source;
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/params/LocToTaskParams.java
@@ -14,7 +14,7 @@
@ApiModel(value = "LocToTaskParams", description = "库存出库参数")
public class LocToTaskParams {
    @ApiModelProperty("类型: check:盘点出库, outStock: 库存出库")
    @ApiModelProperty("类型: check:盘点出库, outStock: 库存出库, empty: 空板出库")
    private String type;
    @ApiModelProperty("原单据ID (用户单据出库查找业务类型")
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/AutoRunSchedules.java
@@ -96,45 +96,55 @@
//    }
    /**
     * @author Ryan
     * @date 2025/9/1
     * @description: 自动完成盘点功能
     * @version 1.0
     * 自动完成盘点/拣料:盘点出库逐条处理;拣料出库按箱码分组,同一箱码下全部任务拣完才扣减并生成拣料入库(与PDA确认逻辑一致)。
     */
//    @Scheduled(cron = "0/25 * * * * ?")
    @Transactional(rollbackFor = Exception.class)
    public void autoCheckComplete() {
        //获取任务列表中,为盘点出库的任务
        List<Task> tasks = taskService.list(new LambdaQueryWrapper<Task>()
                .in(Task::getTaskType, Arrays.asList(TaskType.TASK_TYPE_CHECK_OUT.type,
                        TaskType.TASK_TYPE_PICK_IN.type,
                        TaskType.TASK_TYPE_PICK_AGAIN_OUT.type,
                        TaskType.TASK_TYPE_CHECK_IN.type)));
        if (!tasks.isEmpty()) {
            tasks.forEach(task -> {
                if (task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_IN.type)) {
                    if (task.getTaskStatus().equals(TaskStsType.COMPLETE_IN.id)) {
                    }
                } else {
                    if (task.getTaskStatus().equals(TaskStsType.WAVE_SEED.id)) {
                        if (!stationService.update(new LambdaUpdateWrapper<BasStation>()
                                .eq(BasStation::getStationName, task.getTargSite())
                                .set(BasStation::getUseStatus, LocStsType.LOC_STS_TYPE_R.type))) {
                            log.error("站点状态修改完成失败,当前任务状态:", task.getTaskStatus());
//                                throw new CoolException("站点状态修改失败!!");
                        }
                        try {
                            taskService.pickOrCheckTask(task.getId(), task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_OUT.type) ? Constants.TASK_TYPE_OUT_CHECK : "");
                        } catch (Exception e) {
                            log.error("error====>", e);
                        }
                    }
                }
            });
        if (tasks.isEmpty()) {
            return;
        }
        // 拣料出库:按箱码分组,仅当该箱码下所有任务都拣完才处理(扣减并生成拣料入库或库存扣完不生成)
        Map<String, List<Task>> pickOutByBarcode = tasks.stream()
                .filter(t -> TaskType.TASK_TYPE_PICK_AGAIN_OUT.type.equals(t.getTaskType()) && TaskStsType.WAVE_SEED.id.equals(t.getTaskStatus()))
                .collect(Collectors.groupingBy(t -> t.getBarcode() != null ? t.getBarcode() : ""));
        for (Map.Entry<String, List<Task>> e : pickOutByBarcode.entrySet()) {
            if (e.getKey().isEmpty()) continue;
            List<Task> barcodeTasks = e.getValue();
            List<Long> taskIds = barcodeTasks.stream().map(Task::getId).collect(Collectors.toList());
            List<TaskItem> items = taskItemService.list(new LambdaQueryWrapper<TaskItem>().in(TaskItem::getTaskId, taskIds));
            boolean allPicked = items.stream().allMatch(ti ->
                    ti.getQty() != null && ti.getAnfme() != null && ti.getQty().compareTo(ti.getAnfme()) >= 0);
            if (!allPicked) continue;
            for (Task task : barcodeTasks) {
                try {
                    taskService.pickOrCheckTask(task.getId(), "");
                } catch (Exception ex) {
                    log.error("autoCheckComplete 拣料出库 taskId={} error", task.getId(), ex);
                }
            }
        }
        // 盘点出库:逐条处理(不按箱码聚合)
        tasks.stream()
                .filter(t -> TaskType.TASK_TYPE_CHECK_OUT.type.equals(t.getTaskType()) && TaskStsType.WAVE_SEED.id.equals(t.getTaskStatus()))
                .forEach(task -> {
                    if (!stationService.update(new LambdaUpdateWrapper<BasStation>()
                            .eq(BasStation::getStationName, task.getTargSite())
                            .set(BasStation::getUseStatus, LocStsType.LOC_STS_TYPE_R.type))) {
                        log.error("站点状态修改完成失败,当前任务状态:", task.getTaskStatus());
                    }
                    try {
                        taskService.pickOrCheckTask(task.getId(), Constants.TASK_TYPE_OUT_CHECK);
                    } catch (Exception e) {
                        log.error("autoCheckComplete 盘点出库 taskId={} error", task.getId(), e);
                    }
                });
    }
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/MaterialAutoSchedules.java
@@ -20,6 +20,7 @@
import com.vincent.rsf.server.system.service.ConfigService;
import com.vincent.rsf.server.system.utils.SerialRuleUtils;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/TaskSchedules.java
@@ -590,13 +590,34 @@
        }
        tasks.forEach(task -> {
            TaskLog taskLog = new TaskLog();
            BeanUtils.copyProperties(task, taskLog);
            taskLog.setTaskId(task.getId()).setId(null);
            if (!taskLogService.save(taskLog)) {
                throw new CoolException("任务历史档保存失败!!");
            // 只对出库 200 做同箱码检查:同箱码下若有 101/196/198/199 等(未到 200)则跳过,等全部 200 才一次处理;拣料入库 100 不受影响
            List<Task> toProcess = Collections.singletonList(task);
            if (TaskStsType.UPDATED_OUT.id.equals(task.getTaskStatus()) && TaskType.TASK_TYPE_PICK_AGAIN_OUT.type.equals(task.getTaskType()) && StringUtils.isNotBlank(task.getBarcode())) {
                long not200 = taskService.count(new LambdaQueryWrapper<Task>()
                        .eq(Task::getBarcode, task.getBarcode())
                        .ne(Task::getTaskStatus, TaskStsType.UPDATED_OUT.id));
                if (not200 > 0) {
                    return; // 同箱码尚有 101/196/198/199 等非 200 任务,不处理,继续等待
                }
                // 同箱码已全部 200:一次性处理该箱码下所有 200 拣料出库(合计扣减、更新库存、生成一张拣料入库单、更新库位状态)
                List<Task> all200 = taskService.list(new LambdaQueryWrapper<Task>()
                        .eq(Task::getBarcode, task.getBarcode())
                        .eq(Task::getTaskType, TaskType.TASK_TYPE_PICK_AGAIN_OUT.type)
                        .eq(Task::getTaskStatus, TaskStsType.UPDATED_OUT.id)
                        .orderByAsc(Task::getId));
                if (!all200.isEmpty()) {
                    taskService.processPickOutBarcodeAll200(all200);
                    toProcess = all200;
                }
            }
            List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, task.getId()));
            for (Task t : toProcess) {
                TaskLog taskLog = new TaskLog();
                BeanUtils.copyProperties(t, taskLog);
                taskLog.setTaskId(t.getId()).setId(null);
                if (!taskLogService.save(taskLog)) {
                    throw new CoolException("任务历史档保存失败!!");
                }
                List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, t.getId()));
            // 上报ERP暂时注释(/rsf-open-api/erp/report/order)
            // if (task.getTaskType().equals(TaskType.TASK_TYPE_IN.type)) {
@@ -630,13 +651,13 @@
            //         reportMsgService.reportOrderItem(wkOrderItem);
            //     }
            // } else
            if (task.getTaskType().equals(TaskType.TASK_TYPE_IN.type)) {
            if (t.getTaskType().equals(TaskType.TASK_TYPE_IN.type)) {
                // 入库类型仅转历史,不上报ERP(已注释)
            } 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)) {
            } else if ((t.getTaskType() >= TaskType.TASK_TYPE_OUT.type && t.getTaskType() <= TaskType.TASK_TYPE_EMPITY_OUT.type)
                    || t.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)) {
                if (t.getResource() != null && t.getResource().equals(TaskResouceType.TASK_RESOUCE_WAVE_TYPE.val)) {
                    Set<Long> longSet = taskItems.stream()
                            .map(TaskItem::getSourceId)
                            .filter(Objects::nonNull)
@@ -648,7 +669,7 @@
                            orderIdsToDone.addAll(waveOrderRelas.stream().map(WaveOrderRela::getOrderId).collect(Collectors.toSet()));
                        }
                    }
                } else if (task.getResource() != null && task.getResource().equals(TaskResouceType.TASK_RESOUCE_ORDER_TYPE.val)) {
                } else if (t.getResource() != null && t.getResource().equals(TaskResouceType.TASK_RESOUCE_ORDER_TYPE.val)) {
                    // 按单下发:任务明细 sourceId 为出库单ID
                    Set<Long> ids = taskItems.stream()
                            .map(TaskItem::getSourceId)
@@ -680,9 +701,9 @@
                
                //出库单上报RCS修改库位状态
                try {
                    reportStationStatus(task);
                    reportStationStatus(t);
                } catch (Exception e) {
                    logger.error("任务{}上报RCS修改库位状态失败。任务编码:{}", task.getId(), task.getTaskCode(), e);
                    logger.error("任务{}上报RCS修改库位状态失败。任务编码:{}", t.getId(), t.getTaskCode(), e);
                    // 不抛出异常,避免中断定时任务
                }
            }
@@ -692,16 +713,16 @@
                TaskItemLog itemLog = new TaskItemLog();
                BeanUtils.copyProperties(item, itemLog);
                itemLog.setId(null)
                        .setTaskId(task.getId())
                        .setTaskId(t.getId())
                        .setLogId(taskLog.getId())
                        .setTaskItemId(item.getId());
                itemLogs.add(itemLog);
            }
            locItemWorkingService.remove(new LambdaQueryWrapper<LocItemWorking>().eq(LocItemWorking::getTaskId, task.getId()));
            locItemWorkingService.remove(new LambdaQueryWrapper<LocItemWorking>().eq(LocItemWorking::getTaskId, t.getId()));
            if (!taskService.removeById(task.getId())) {
            if (!taskService.removeById(t.getId())) {
                throw new CoolException("原始任务删除失败!!");
            }
@@ -709,11 +730,12 @@
                if (!taskItemLogService.saveBatch(itemLogs)) {
                    throw new CoolException("任务明细历史档保存失败!!");
                }
                if (!taskItemService.remove(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, task.getId()))) {
                if (!taskItemService.remove(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, t.getId()))) {
                    throw new CoolException("原始任务明细删除失败!!");
                }
            }
            }
        });
    }
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/LocItemService.java
@@ -14,5 +14,10 @@
    Task genMoveTask(LocToTaskParams map, Long loginUserId);
    /**
     * 空板出库:从指定空板库位(useStatus=D)生成 TASK_TYPE_EMPITY_OUT 任务至目标站点
     */
    Task generateTaskEmpty(LocToTaskParams map, Long loginUserId);
    List<LocItem> listByMatnr(CheckLocQueryParams matnrs);
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/TaskService.java
@@ -37,4 +37,9 @@
    R menualExceTask(List<Long> ids);
    void pubTaskToWcs(List<Task> tasks);
    /**
     * 同箱码下多条 200 拣料出库一次性处理:按相同物料合计扣减库位、更新出库单/库存明细、生成一张拣料入库单(有余量时)、更新库位状态
     */
    void processPickOutBarcodeAll200(List<Task> all200Tasks);
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/LocItemServiceImpl.java
@@ -1,6 +1,7 @@
package com.vincent.rsf.server.manager.service.impl;
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.api.controller.erp.params.TaskInParam;
import com.vincent.rsf.server.api.entity.dto.InTaskMsgDto;
@@ -95,14 +96,53 @@
            if (Objects.isNull(loc)) {
                throw new CoolException("数据错误:所选库存信息不存在!!");
            }
            if (!loc.getUseStatus().equals(LocStsType.LOC_STS_TYPE_F.type)) {
                throw new CoolException("库位:" + loc.getCode() + ",不处于F.在库状态,不可执行R.出库预约操作!!");
            // 支持 F.在库 或 R.出库预约/拣货中 状态下分配(拣货中可追加同库位订单,分配量不超过已分配剩余)
            if (!loc.getUseStatus().equals(LocStsType.LOC_STS_TYPE_F.type)
                    && !loc.getUseStatus().equals(LocStsType.LOC_STS_TYPE_R.type)) {
                throw new CoolException("库位:" + loc.getCode() + ",不处于F.在库或R.出库预约状态,不可执行出库分配!!");
            }
            loc.setUseStatus(LocStsType.LOC_STS_TYPE_R.type);
            if (!locService.updateById(loc)) {
                throw new CoolException("库位状态更新失败!!");
            if (loc.getUseStatus().equals(LocStsType.LOC_STS_TYPE_F.type)) {
                loc.setUseStatus(LocStsType.LOC_STS_TYPE_R.type);
                if (!locService.updateById(loc)) {
                    throw new CoolException("库位状态更新失败!!");
                }
            }
            // 库位已为 R 时:计算该库位当前任务已分配量,新分配不能超过 (库位数量 - 已分配)
            Map<String, Double> allocatedByKey = new HashMap<>();
            List<Task> existTasks = new ArrayList<>();
            if (loc.getUseStatus().equals(LocStsType.LOC_STS_TYPE_R.type)) {
                existTasks = taskService.list(new LambdaQueryWrapper<Task>()
                        .eq(Task::getOrgLoc, loc.getCode())
                        .in(Task::getTaskStatus, Arrays.asList(TaskStsType.GENERATE_OUT.id, TaskStsType.WAVE_SEED.id)));
                for (Task t : existTasks) {
                    List<TaskItem> existItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, t.getId()));
                    for (TaskItem ti : existItems) {
                        String k = buildAllocKey(ti.getMatnrId(), ti.getBatch(), ti.getFieldsIndex());
                        allocatedByKey.put(k, allocatedByKey.getOrDefault(k, 0.0) + (ti.getAnfme() != null ? ti.getAnfme() : 0.0));
                    }
                }
            }
            // 料箱码:优先用库位;预约出库(R) 时若库位未绑定则从同库位已有任务带出并回写库位;再否则从本次分配明细带出
            String barcodeToUse = StringUtils.isNotBlank(loc.getBarcode()) ? loc.getBarcode() : null;
            if (barcodeToUse == null && !existTasks.isEmpty()) {
                barcodeToUse = existTasks.get(0).getBarcode();
                if (StringUtils.isNotBlank(barcodeToUse)) {
                    locService.update(new LambdaUpdateWrapper<Loc>().eq(Loc::getId, loc.getId())
                            .set(Loc::getBarcode, barcodeToUse).set(Loc::getUpdateBy, loginUserId).set(Loc::getUpdateTime, new Date()));
                }
            }
            if (barcodeToUse == null) {
                List<LocItem> allocItems = listMap.get(key);
                if (allocItems != null) {
                    barcodeToUse = allocItems.stream()
                            .map(LocItem::getBarcode)
                            .filter(StringUtils::isNotBlank)
                            .findFirst()
                            .orElse(null);
                }
            }
            if (barcodeToUse == null) {
                barcodeToUse = loc.getBarcode();
            }
            Task moveTask = new Task();
            String ruleCode = SerialRuleUtils.generateRuleCode(SerialRuleCode.SYS_TASK_CODE, null);
@@ -116,7 +156,7 @@
                    .setCreateTime(new Date())
                    .setUpdateTime(new Date())
                    .setTaskStatus(TaskStsType.GENERATE_OUT.id)
                    .setBarcode(loc.getBarcode())
                    .setBarcode(barcodeToUse)
                    .setMemo(map.getMemo());
            List<LocItem> locItems = this.list(new LambdaQueryWrapper<LocItem>().eq(LocItem::getLocId, key));
@@ -203,10 +243,33 @@
            List<TaskItem> taskItems = new ArrayList<>();
            listMap.get(key).forEach(item -> {
                LocItem locItem = locItemService.getById(item.getId());
                if (Objects.isNull(locItem)) {
                    throw new CoolException("库存信息不存在!");
                }
                if (item.getOutQty().compareTo(0.0) < 0) {
                    throw new CoolException("出库数里不能小于0!!");
                }
                // 预约出库/拣货中追加:新分配量不能超过 (库位数量 - 该物料已分配量)
                Double allocQty = item.getOutQty();
                if (!allocatedByKey.isEmpty()) {
                    String allocKey = buildAllocKey(locItem.getMatnrId(), locItem.getBatch(), locItem.getFieldsIndex());
                    Double already = allocatedByKey.getOrDefault(allocKey, 0.0);
                    Double available = Math.round((locItem.getAnfme() - already) * 1000000) / 1000000.0;
                    if (available.compareTo(0.0) <= 0) {
                        throw new CoolException("库位:" + loc.getCode() + " 该物料已无剩余可分配数量,不可再追加订单!");
                    }
                    if (allocQty.compareTo(available) > 0) {
                        allocQty = available;
                        item.setOutQty(allocQty);
                    }
                    allocatedByKey.put(allocKey, already + allocQty);
                }
                TaskItem taskItem = new TaskItem();
                BeanUtils.copyProperties(item, taskItem);
                taskItem.setTaskId(task.getId())
                        .setAnfme(item.getOutQty())
                        .setAnfme(allocQty)
                        .setBatch(item.getBatch())
                        .setUpdateBy(loginUserId)
                        .setCreateBy(loginUserId)
@@ -231,18 +294,9 @@
                }
                taskItems.add(taskItem);
                Double qty = Math.round((item.getWorkQty() + item.getOutQty()) * 1000000) / 1000000.0;
                LocItem locItem = locItemService.getById(item.getId());
                if (Objects.isNull(locItem)) {
                    throw new CoolException("库存信息不存在!");
                }
                if (item.getOutQty().compareTo(0.0) < 0) {
                    throw new CoolException("出库数里不能小于0!!");
                }
                Double qty = Math.round((item.getWorkQty() != null ? item.getWorkQty() : 0.0) + allocQty) * 1000000.0 / 1000000.0;
                if (locItem.getAnfme().compareTo(qty) < 0) {
                    Double minusQty = Math.round((locItem.getAnfme() - locItem.getWorkQty()) * 1000000) / 1000000.0;
                    Double minusQty = Math.round((locItem.getAnfme() - (locItem.getWorkQty() != null ? locItem.getWorkQty() : 0.0)) * 1000000) / 1000000.0;
                    item.setWorkQty(minusQty);
                } else {
                    item.setWorkQty(qty);
@@ -368,6 +422,66 @@
    }
    /**
     * 空板出库:从指定空板库位(useStatus=D)生成 TASK_TYPE_EMPITY_OUT 任务至目标站点。
     * 需在设备站点中配置 type=110(空板出库)的站点路径。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Task generateTaskEmpty(LocToTaskParams map, Long loginUserId) {
        if (StringUtils.isBlank(map.getSiteNo())) {
            throw new CoolException("目标站点不能为空!");
        }
        if (StringUtils.isBlank(map.getOrgLoc())) {
            throw new CoolException("源库位不能为空!");
        }
        if (!Constants.TASK_TYPE_OUT_STOCK_EMPTY.equals(map.getType())) {
            throw new CoolException("类型必须为 empty(空板出库)!");
        }
        Loc loc = locService.getOne(new LambdaQueryWrapper<Loc>().eq(Loc::getCode, map.getOrgLoc()));
        if (loc == null) {
            throw new CoolException("源库位不存在!");
        }
        if (!LocStsType.LOC_STS_TYPE_D.type.equals(loc.getUseStatus())) {
            throw new CoolException("库位 " + loc.getCode() + " 不处于空板状态(D),不可执行空板出库!");
        }
        DeviceSite deviceSite = deviceSiteService.getOne(new LambdaQueryWrapper<DeviceSite>()
                .eq(DeviceSite::getSite, map.getSiteNo())
                .eq(DeviceSite::getType, TaskType.TASK_TYPE_EMPITY_OUT.type)
                .last("limit 1"));
        if (deviceSite == null) {
            throw new CoolException("站点不支持空板出库或未配置空板出库路径!");
        }
        if (!locService.update(new LambdaUpdateWrapper<Loc>()
                .eq(Loc::getId, loc.getId())
                .set(Loc::getUseStatus, LocStsType.LOC_STS_TYPE_R.type))) {
            throw new CoolException("库位出库预约失败!");
        }
        String ruleCode = SerialRuleUtils.generateRuleCode(SerialRuleCode.SYS_TASK_CODE, null);
        if (StringUtils.isBlank(ruleCode)) {
            throw new CoolException("编码错误:请确认是否已生成!");
        }
        Task task = new Task();
        task.setTaskCode(ruleCode)
                .setTaskStatus(TaskStsType.GENERATE_OUT.id)
                .setTaskType(TaskType.TASK_TYPE_EMPITY_OUT.type)
                .setWarehType(WarehType.WAREHOUSE_TYPE_CRN.val)
                .setOrgLoc(loc.getCode())
                .setTargSite(map.getSiteNo())
                .setBarcode(loc.getBarcode())
                .setSort(Constants.TASK_SORT_DEFAULT_VALUE)
                .setCreateBy(loginUserId)
                .setUpdateBy(loginUserId)
                .setCreateTime(new Date())
                .setUpdateTime(new Date())
                .setMemo(map.getMemo());
        if (!taskService.save(task)) {
            throw new CoolException("空板出库任务创建失败!");
        }
        logger.info("[空板出库] 已创建任务: {}, 源库位: {}, 目标站点: {}", ruleCode, loc.getCode(), map.getSiteNo());
        return task;
    }
    /**
     * @author Ryan
     * @date 2025/7/16
     * @description: 获取当前物料所有库存信息
@@ -381,4 +495,9 @@
                .in(!matnr.getMatnrCode().isEmpty(), LocItem::getMatnrCode, matnr.getMatnrCode());
        return  this.baseMapper.listByMatnr(LocStsType.LOC_STS_TYPE_F.type, matnr.getChannel(), wrapper);
    }
    /** 库位已分配量按物料+批次+票号聚合的 key */
    private static String buildAllocKey(Long matnrId, String batch, String fieldsIndex) {
        return (matnrId != null ? matnrId : "") + "|" + (batch != null ? batch : "") + "|" + (fieldsIndex != null ? fieldsIndex : "");
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/RcsTestServiceImpl.java
@@ -221,8 +221,8 @@
                }
                
                if ("location_allocate".equals(params.getInboundApiType())) {
                    // 使用 location_allocate 接口(内部调用createInTask)
                    R allocateResult = wcsService.allocateLocation(barcode, inboundStation, 1);
                    // 使用 location_allocate 接口(内部调用createInTask);full=null 表示普通入库
                    R allocateResult = wcsService.allocateLocation(barcode, inboundStation, 1, null);
                    if (allocateResult != null) {
                        Object dataObj = allocateResult.get("data");
                        if (dataObj != null) {
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/TaskServiceImpl.java
@@ -1383,6 +1383,15 @@
            });
            log.debug("[拣料入库] 即将扣减库位 locId={}, locCode={}", loc.getId(), loc.getCode());
            subtractLocItemByTaskItems(loc, taskItems, SystemAuthUtils.getLoginUserId());
            // 料箱已全部用完则不生成拣货入库单,仅完结出库
            double totalRemaining = taskItems.stream().mapToDouble(ti -> ti.getAnfme() != null && ti.getAnfme().compareTo(0.0) > 0 ? ti.getAnfme() : 0.0).sum();
            if (totalRemaining <= 0.0) {
                task.setTaskType(TaskType.TASK_TYPE_PICK_AGAIN_OUT.type);
                task.setTaskStatus(TaskStsType.UPDATED_OUT.id);
                this.updateById(task);
                locItemWorkingService.remove(new LambdaQueryWrapper<LocItemWorking>().eq(LocItemWorking::getTaskId, task.getId()));
                return task;
            }
        }
        tempLocs.forEach(working -> {
@@ -1478,6 +1487,137 @@
    }
    /**
     * 同箱码下多条 200 拣料出库一次性处理:按相同物料合计扣减库位、更新出库单/库存明细、生成一张拣料入库单(有余量时)、更新库位状态
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void processPickOutBarcodeAll200(List<Task> all200Tasks) {
        if (all200Tasks == null || all200Tasks.isEmpty()) {
            return;
        }
        Task first = all200Tasks.get(0);
        if (!TaskType.TASK_TYPE_PICK_AGAIN_OUT.type.equals(first.getTaskType()) || !TaskStsType.UPDATED_OUT.id.equals(first.getTaskStatus())) {
            throw new CoolException("非拣料出库200任务,不可批量处理");
        }
        List<Long> taskIds = all200Tasks.stream().map(Task::getId).collect(Collectors.toList());
        List<TaskItem> allItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().in(TaskItem::getTaskId, taskIds));
        if (allItems.isEmpty()) {
            throw new CoolException("任务明细为空");
        }
        Long loginUserId = SystemAuthUtils.getLoginUserId();
        if (loginUserId == null) {
            loginUserId = 1L;
        }
        String orgLoc = first.getOrgLoc();
        Loc loc = locService.getOne(new LambdaQueryWrapper<Loc>().eq(Loc::getCode, orgLoc));
        if (loc == null) {
            throw new CoolException("库位不存在:" + orgLoc);
        }
        // 按物料+批次+票号汇总已拣数量
        Map<String, List<TaskItem>> byKey = allItems.stream().collect(Collectors.groupingBy(ti ->
                (ti.getMatnrId() != null ? ti.getMatnrId() : "") + "_" + (ti.getBatch() != null ? ti.getBatch() : "") + "_" + (ti.getFieldsIndex() != null ? ti.getFieldsIndex() : "")));
        List<TaskItem> aggregatedForDeduct = new ArrayList<>();
        Map<String, Double> remainderByKey = new LinkedHashMap<>();
        for (Map.Entry<String, List<TaskItem>> e : byKey.entrySet()) {
            List<TaskItem> group = e.getValue();
            double totalQty = group.stream().mapToDouble(ti -> ti.getQty() != null && ti.getQty() > 0 ? ti.getQty() : (ti.getAnfme() != null ? ti.getAnfme() : 0)).sum();
            TaskItem rep = group.get(0);
            TaskItem forDeduct = new TaskItem();
            forDeduct.setMatnrId(rep.getMatnrId()).setBatch(rep.getBatch()).setFieldsIndex(rep.getFieldsIndex()).setQty(totalQty);
            aggregatedForDeduct.add(forDeduct);
            LambdaQueryWrapper<LocItem> qw = new LambdaQueryWrapper<LocItem>().eq(LocItem::getLocId, loc.getId()).eq(LocItem::getMatnrId, rep.getMatnrId());
            if (StringUtils.isNotBlank(rep.getBatch())) qw.eq(LocItem::getBatch, rep.getBatch());
            else qw.and(w -> w.isNull(LocItem::getBatch).or().eq(LocItem::getBatch, ""));
            if (StringUtils.isNotBlank(rep.getFieldsIndex())) qw.eq(LocItem::getFieldsIndex, rep.getFieldsIndex());
            else qw.and(w -> w.isNull(LocItem::getFieldsIndex).or().eq(LocItem::getFieldsIndex, ""));
            LocItem li = locItemService.getOne(qw);
            double remainder = (li != null && li.getAnfme() != null ? li.getAnfme() : 0) - totalQty;
            if (remainder > 0) {
                remainderByKey.put(e.getKey(), remainder);
            }
        }
        subtractLocItemByTaskItems(loc, aggregatedForDeduct, loginUserId);
        // 按 source 分组更新出库单并写库存流水
        Map<Long, List<TaskItem>> bySource = allItems.stream().collect(Collectors.groupingBy(TaskItem::getSource));
        for (Map.Entry<Long, List<TaskItem>> e : bySource.entrySet()) {
            Long key = e.getKey();
            List<TaskItem> items = e.getValue();
            if (first.getResource() != null && first.getResource().equals(TaskResouceType.TASK_RESOUCE_WAVE_TYPE.val)) {
                WaveItem waveItem = waveItemService.getById(key);
                if (waveItem != null) {
                    try {
                        saveOutStockItem(items, null, waveItem, null, loginUserId);
                    } catch (Exception ex) {
                        throw new CoolException(ex.getMessage());
                    }
                }
            } else if (first.getResource() != null && first.getResource().equals(TaskResouceType.TASK_RESOUCE_ORDER_TYPE.val)) {
                WkOrderItem orderItem = asnOrderItemService.getById(key);
                if (orderItem != null) {
                    try {
                        saveOutStockItem(items, orderItem, null, null, loginUserId);
                    } catch (Exception ex) {
                        throw new CoolException(ex.getMessage());
                    }
                }
            }
        }
        // 有余量则生成一张拣料入库单
        if (!remainderByKey.isEmpty()) {
            Task pickInTask = new Task();
            pickInTask.setTaskCode(SerialRuleUtils.generateRuleCode(SerialRuleCode.SYS_TASK_CODE, first));
            pickInTask.setTaskType(TaskType.TASK_TYPE_PICK_IN.type);
            pickInTask.setTaskStatus(TaskStsType.GENERATE_IN.id);
            pickInTask.setBarcode(first.getBarcode());
            pickInTask.setOrgLoc(orgLoc);
            pickInTask.setTargLoc(orgLoc);
            pickInTask.setOrgSite(first.getTargSite());
            pickInTask.setTargSite(first.getTargSite());
            pickInTask.setResource(first.getResource());
            if (!this.save(pickInTask)) {
                throw new CoolException("拣料入库任务创建失败");
            }
            List<LocItemWorking> workings = new ArrayList<>();
            for (Map.Entry<String, Double> re : remainderByKey.entrySet()) {
                String k = re.getKey();
                Double rem = re.getValue();
                if (rem == null || rem <= 0) continue;
                List<TaskItem> group = byKey.get(k);
                if (group == null || group.isEmpty()) continue;
                TaskItem rep = group.get(0);
                TaskItem ti = new TaskItem();
                ti.setTaskId(pickInTask.getId());
                ti.setMatnrId(rep.getMatnrId()).setMaktx(rep.getMaktx()).setMatnrCode(rep.getMatnrCode());
                ti.setBatch(rep.getBatch()).setFieldsIndex(rep.getFieldsIndex()).setUnit(rep.getUnit()).setSpec(rep.getSpec()).setModel(rep.getModel());
                ti.setAnfme(rem).setQty(0.0);
                taskItemService.save(ti);
                LocItemWorking w = new LocItemWorking();
                w.setTaskId(pickInTask.getId());
                w.setLocId(loc.getId());
                w.setLocCode(loc.getCode());
                w.setMatnrId(rep.getMatnrId()).setMaktx(rep.getMaktx()).setMatnrCode(rep.getMatnrCode());
                w.setBatch(rep.getBatch()).setFieldsIndex(rep.getFieldsIndex()).setUnit(rep.getUnit());
                w.setAnfme(rem);
                workings.add(w);
            }
            if (!workings.isEmpty()) {
                locItemWorkingService.saveBatch(workings);
            }
            loc.setUseStatus(LocStsType.LOC_STS_TYPE_S.type);
            locService.updateById(loc);
        } else {
            loc.setUseStatus(LocStsType.LOC_STS_TYPE_O.type);
            loc.setBarcode(null);
            loc.setUpdateBy(loginUserId);
            loc.setUpdateTime(new Date());
            locService.updateById(loc);
        }
        for (Long tid : taskIds) {
            locItemWorkingService.remove(new LambdaQueryWrapper<LocItemWorking>().eq(LocItemWorking::getTaskId, tid));
        }
    }
    /**
     * @author Ryan
     * @date 2025/5/20
     * @description: 完成出库任务,更新出库库存信息
@@ -1493,7 +1633,35 @@
        if (Objects.isNull(loc)) {
            throw new CoolException("库位不存在!!");
        }
        // 空板出库:无任务明细,不需要 PDA 拣货确认,RCS 回调后直接完成库位更新并置为 UPDATED_OUT
        if (task.getTaskType().equals(TaskType.TASK_TYPE_EMPITY_OUT.type)) {
            List<TaskItem> emptyItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, task.getId()));
            if (emptyItems.isEmpty()) {
                if (!LocStsType.LOC_STS_TYPE_R.type.equals(loc.getUseStatus())) {
                    log.warn("空板出库任务{}的库位{}状态不是R.出库预约,跳过", task.getId(), loc.getCode());
                    return;
                }
                if (!locService.update(new LambdaUpdateWrapper<Loc>()
                        .eq(Loc::getId, loc.getId())
                        .set(Loc::getUseStatus, LocStsType.LOC_STS_TYPE_O.type)
                        .set(Loc::getBarcode, null)
                        .set(Loc::getUpdateBy, loginUserId)
                        .set(Loc::getUpdateTime, new Date()))) {
                    throw new CoolException("空板出库库位状态更新失败!!");
                }
                if (!this.update(new LambdaUpdateWrapper<Task>()
                        .eq(Task::getId, task.getId())
                        .set(Task::getUpdateBy, loginUserId)
                        .set(Task::getUpdateTime, new Date())
                        .set(Task::getTaskStatus, TaskStsType.UPDATED_OUT.id))) {
                    throw new CoolException("空板出库任务状态更新失败!!");
                }
                log.info("[空板出库] 任务{} RCS回调后已直接完成库位更新,无需PDA确认", task.getTaskCode());
                return;
            }
        }
        List<TaskItem> taskItems = taskItemService.list(new LambdaQueryWrapper<TaskItem>().eq(TaskItem::getTaskId, task.getId()));
        if (taskItems.isEmpty()) {
            throw new CoolException("任务明细不存在!!");
@@ -1610,15 +1778,8 @@
        
        // 根据任务类型更新库位状态
        if (task.getTaskType().equals(TaskType.TASK_TYPE_PICK_AGAIN_OUT.type) || task.getTaskType().equals(TaskType.TASK_TYPE_CHECK_OUT.type)) {
            /**修改为库位状态为S.预约入库,保留原有库位*/
            if (!locService.update(new LambdaUpdateWrapper<Loc>()
                    .set(Loc::getUseStatus, LocStsType.LOC_STS_TYPE_S.type)
                    .set(Loc::getBarcode, null)
                    .set(Loc::getUpdateBy, loginUserId)
                    .set(Loc::getUpdateTime, new Date())
                    .eq(Loc::getId, loc.getId()))) {
                throw new CoolException("库位状态修改失败!!");
            }
            // 拣料出库/盘点出库:在未生成拣料入库单之前保持 R.预约出库,否则下发任务时查不到该库位(只查 F+R)导致“库存不足”
            // 等 PDA 确认并生成拣料入库任务时,再在 pickOrCheckTask 中将目标库位改为 S.预约入库
        } else if (task.getTaskType().equals(TaskType.TASK_TYPE_OUT.type)) {
            // 全版出库:不更新库位状态为O,等待PDA快速拣货确认时再更新
            // 库位状态保持原样(R.出库预约状态)
rsf-server/src/main/java/com/vincent/rsf/server/manager/utils/LocManageUtil.java
@@ -100,13 +100,8 @@
        if (StringUtils.isNotBlank(splrBatch)) {
            locItemQueryWrapper.and(w -> w.eq(LocItem::getBatch, splrBatch).or().isNull(LocItem::getBatch));
        }
        String applySql = String.format(
                "EXISTS (SELECT 1 FROM man_loc ml " +
                        "WHERE ml.use_status = '%s'" +
                        "AND ml.id = man_loc_item.loc_id " +
                        ")",
                LocStsType.LOC_STS_TYPE_F.type
        );
        // 含 F.在库 与 R.出库预约(拣料出库未确认前可再下发,使用剩余可用)
        String applySql = "EXISTS (SELECT 1 FROM man_loc ml WHERE ml.use_status IN ('" + LocStsType.LOC_STS_TYPE_F.type + "','" + LocStsType.LOC_STS_TYPE_R.type + "') AND ml.id = man_loc_item.loc_id)";
        locItemQueryWrapper.apply(applySql);
        LocItemService locItemService = SpringUtils.getBean(LocItemService.class);
        List<LocItem> locItems = locItemService.list(locItemQueryWrapper);
@@ -140,14 +135,9 @@
        } else {
            locItemQueryWrapper.orderByAsc(LocItem::getCreateTime);
        }
        String applySql = String.format(
                "EXISTS (SELECT 1 FROM man_loc ml " +
                        "WHERE ml.use_status = '%s'" +
                        "AND ml.id = man_loc_item.loc_id " +
                        ")",
                LocStsType.LOC_STS_TYPE_F.type
        );
        locItemQueryWrapper.apply(applySql);
        // 含 F.在库 与 R.出库预约(拣料出库未确认前可再下发,使用剩余可用)
        String applySqlR = "EXISTS (SELECT 1 FROM man_loc ml WHERE ml.use_status IN ('" + LocStsType.LOC_STS_TYPE_F.type + "','" + LocStsType.LOC_STS_TYPE_R.type + "') AND ml.id = man_loc_item.loc_id)";
        locItemQueryWrapper.apply(applySqlR);
        LocItemService locItemService = SpringUtils.getBean(LocItemService.class);
        List<LocItem> locItems = locItemService.list(locItemQueryWrapper);
        return locItems;