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;