17 小时以前 eccad5a129106377a275be4f7cdc58e99e9b95d4
feat(mes): 实现库存移库功能

- 移除MES模块禁用的兜底映射配置
- 添加库存移库API接口和相关请求对象定义
- 实现库存移库业务逻辑,包括数量校验和事务处理
- 添加移库相关的错误码和数据库查询方法
- 更新销售订单发货通知单生成逻辑,避免重复创建
- 完善调拨流程中的供应商和批次信息处理
- 编写详细的前端联调文档说明移库功能实现方案
已添加2个文件
已修改9个文件
316 ■■■■■ 文件已修改
docs/material_stock_move.md 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sale_order_production_status.md 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/MesWmMaterialStockController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockMoveReqVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstock/MesWmMaterialStockMapper.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transfer/MesWmTransferServiceImpl.java 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/material_stock_move.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
# åº“存移库 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- åº“存台账页面(/mes/wm/material-stock)
## ä¸šåŠ¡åœºæ™¯
在库存台账页面,用户可以选择一条库存记录,点击"移库"按钮,将该库存从当前库位移动到目标库位。移库操作会生成两条成对的库存事务流水(调拨移出 + è°ƒæ‹¨ç§»å…¥ï¼‰ï¼Œç¡®ä¿åº“存变动可追溯。
## æ–°å¢ž API
### åº“存移库
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /mes/wm/material-stock/move | åº“存移库 |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| materialStockId | Long | æ˜¯ | åº“存记录ID |
| toWarehouseId | Long | æ˜¯ | ç›®æ ‡ä»“库ID |
| toLocationId | Long | æ˜¯ | ç›®æ ‡åº“区ID |
| toAreaId | Long | æ˜¯ | ç›®æ ‡åº“位ID |
| quantity | BigDecimal | æ˜¯ | ç§»åº“数量 |
| remark | String | å¦ | å¤‡æ³¨ |
**请求示例:**
```json
{
  "materialStockId": 1024,
  "toWarehouseId": 1,
  "toLocationId": 2,
  "toAreaId": 3,
  "quantity": 100,
  "remark": "库位调整"
}
```
**响应:**
```json
{
  "code": 0,
  "msg": "操作成功",
  "data": true
}
```
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. åº“存台账列表页
- åœ¨æ“ä½œåˆ—添加"移库"按钮
- ç‚¹å‡»åŽæ‰“开移库弹窗
### 2. ç§»åº“弹窗
**表单字段:**
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| ç‰©æ–™ç¼–码 | æ–‡æœ¬ | - | åªè¯»ï¼Œä»Žé€‰ä¸­è¡Œå¸¦å…¥ |
| ç‰©æ–™åç§° | æ–‡æœ¬ | - | åªè¯»ï¼Œä»Žé€‰ä¸­è¡Œå¸¦å…¥ |
| æ‰¹æ¬¡å· | æ–‡æœ¬ | - | åªè¯»ï¼Œä»Žé€‰ä¸­è¡Œå¸¦å…¥ |
| å½“前仓库 | æ–‡æœ¬ | - | åªè¯»ï¼Œä»Žé€‰ä¸­è¡Œå¸¦å…¥ |
| å½“前库区 | æ–‡æœ¬ | - | åªè¯»ï¼Œä»Žé€‰ä¸­è¡Œå¸¦å…¥ |
| å½“前库位 | æ–‡æœ¬ | - | åªè¯»ï¼Œä»Žé€‰ä¸­è¡Œå¸¦å…¥ |
| å½“前数量 | æ•°å­— | - | åªè¯»ï¼Œä»Žé€‰ä¸­è¡Œå¸¦å…¥ |
| ç›®æ ‡ä»“库 | ä¸‹æ‹‰ | æ˜¯ | é€‰æ‹©ç›®æ ‡ä»“库 |
| ç›®æ ‡åº“区 | ä¸‹æ‹‰ | æ˜¯ | æ ¹æ®ç›®æ ‡ä»“库联动 |
| ç›®æ ‡åº“位 | ä¸‹æ‹‰ | æ˜¯ | æ ¹æ®ç›®æ ‡åº“区联动 |
| ç§»åº“数量 | æ•°å­— | æ˜¯ | è¾“入移库数量,不能超过当前数量 |
| å¤‡æ³¨ | æ–‡æœ¬ | å¦ | å¤‡æ³¨ä¿¡æ¯ |
**联动逻辑:**
1. é€‰æ‹©ç›®æ ‡ä»“库后,加载该仓库下的库区列表
2. é€‰æ‹©ç›®æ ‡åº“区后,加载该库区下的库位列表
**提交逻辑:**
1. æ ¡éªŒç§»åº“数量不能超过当前库存数量
2. æ ¡éªŒç§»åº“数量不能超过可用量(当前数量 - å†»ç»“数量 - å ç”¨é‡ï¼‰
3. è°ƒç”¨ç§»åº“接口
4. æˆåŠŸåŽå…³é—­å¼¹çª—ï¼Œåˆ·æ–°åˆ—è¡¨
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| åº“存数量校验 | ç§»åº“数量不能超过可用量(在库数量 - å†»ç»“数量 - å ç”¨é‡ï¼‰ |
| å†»ç»“校验 | åº“存记录已冻结时,不允许移库 |
| åº“位混放规则 | ç›®æ ‡åº“位需满足混放规则(物料混放、批次混放) |
| åŒåº“位移库 | æºåº“位和目标库位相同时,不允许移库 |
## åº“存事务流水
移库操作会生成两条成对的库存事务流水:
| å­—段 | è°ƒæ‹¨ç§»å‡º | è°ƒæ‹¨ç§»å…¥ |
|------|----------|----------|
| type | 3 (MOVE_OUT) | 4 (MOVE_IN) |
| bizType | 112 (WM_TRANSFER_OUT) | 113 (WM_TRANSFER_IN) |
| bizCode | MOVE_xxx | MOVE_xxx(相同) |
| quantity | è´Ÿæ•°ï¼ˆå¦‚ -100) | æ­£æ•°ï¼ˆå¦‚ 100) |
| warehouseId | æºä»“库 | ç›®æ ‡ä»“库 |
| locationId | æºåº“区 | ç›®æ ‡åº“区 |
| areaId | æºåº“位 | ç›®æ ‡åº“位 |
| relatedTransactionId | null | ç§»å‡ºäº‹åŠ¡ID |
## æ³¨æ„äº‹é¡¹
1. **权限要求**:需要 `mes:wm-material-stock:move` æƒé™
2. **事务一致性**:移库操作在事务中执行,确保出库和入库操作的原子性
3. **批次保留**:移库时批次信息保留,不会改变批次属性
4. **供应商保留**:移库时供应商信息保留
docs/sale_order_production_status.md
@@ -74,7 +74,7 @@
| ç”Ÿäº§çŠ¶æ€åˆå§‹å€¼ | é”€å”®è®¢å•审核通过时,productionStatus é»˜è®¤ä¸º 0(未完成) |
| ç”Ÿäº§çŠ¶æ€æ›´æ–°æ—¶æœº | å½“所有关联的生产订单完成时,系统自动更新为 1(已完成) |
| ç”Ÿäº§å®Œæˆåˆ¤æ–­ | å·²ç”Ÿäº§æ•°é‡ >= è®¡åˆ’数量 ä¸” æ‰€æœ‰æŠ¥å·¥å•已完成(含质检完成) |
| å‘货通知单生成 | å½“ productionStatus=1 æ—¶ï¼ŒcanCreateSalesNotice ä¸º true |
| å‘货通知单生成 | å½“ productionStatus=1 ä¸”未生成发货通知单时,canCreateSalesNotice ä¸º true |
## å±•示逻辑说明
@@ -99,7 +99,9 @@
## æ³¨æ„äº‹é¡¹
1. **状态联动**:`productionStatus` ä¸Ž `canCreateSalesNotice` å…³è”,生产未完成时无法生成发货通知单
1. **状态联动**:`canCreateSalesNotice` éœ€åŒæ—¶æ»¡è¶³ä¸¤ä¸ªæ¡ä»¶ï¼šç”Ÿäº§å·²å®Œæˆï¼ˆproductionStatus=1)且未生成发货通知单(hasSalesNotice=false)
2. **已生成发货通知单**:当 `hasSalesNotice=true` æ—¶ï¼Œä¸æ˜¾ç¤º"生成发货通知单"按钮
2. **无需生产的订单**:`needProduction !== 1` çš„订单,`productionStatus` å­—段为 null,前端应显示"无需生产"或不显示
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/ErpSaleOrderController.java
@@ -218,8 +218,8 @@
            MapUtils.findAndThen(userMap, Long.parseLong(saleOrder.getCreator()), user -> saleOrder.setCreatorName(user.getNickname()));
            // è®¾ç½®æ˜¯å¦å·²ç”Ÿæˆå‘货通知单
            saleOrder.setHasSalesNotice(salesNoticeMap.getOrDefault(saleOrder.getId(), false));
            // è®¾ç½®æ˜¯å¦å¯ä»¥ç”Ÿæˆå‘货通知单
            saleOrder.setCanCreateSalesNotice(canCreateSalesNoticeMap.getOrDefault(saleOrder.getId(), true));
            // è®¾ç½®æ˜¯å¦å¯ä»¥ç”Ÿæˆå‘货通知单:已生成发货通知单时不显示按钮
            saleOrder.setCanCreateSalesNotice(!saleOrder.getHasSalesNotice() && canCreateSalesNoticeMap.getOrDefault(saleOrder.getId(), true));
            // è®¾ç½®æ˜¯å¦æœ‰éœ€è¦ç”Ÿäº§çš„æ˜Žç»†
            boolean hasNeedProduction = saleOrder.getItems().stream()
                    .anyMatch(item -> item.getNeedProduction() != null && item.getNeedProduction() == 1);
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/MesWmMaterialStockController.java
@@ -9,9 +9,10 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockAdjustReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockFreezeReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockMoveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockFreezeReqVO;
import cn.iocoder.yudao.module.erp.api.purchase.ErpSupplierApi;
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpSupplierRespDTO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
@@ -114,6 +115,14 @@
        return success(materialStockService.adjustMaterialStockOut(reqVO));
    }
    @PostMapping("/move")
    @Operation(summary = "库存移库")
    @PreAuthorize("@ss.hasPermission('mes:wm-material-stock:update')")
    public CommonResult<Boolean> moveMaterialStock(@Valid @RequestBody MesWmMaterialStockMoveReqVO reqVO) {
        materialStockService.moveMaterialStock(reqVO);
        return success(true);
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出库存台账 Excel")
    @PreAuthorize("@ss.hasPermission('mes:wm-material-stock:export')")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockMoveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - åº“存移库 Request VO")
@Data
public class MesWmMaterialStockMoveReqVO {
    @Schema(description = "库存记录ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @NotNull(message = "库存记录ID不能为空")
    private Long materialStockId;
    @Schema(description = "目标仓库ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "目标仓库不能为空")
    private Long toWarehouseId;
    @Schema(description = "目标库区ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "目标库区不能为空")
    private Long toLocationId;
    @Schema(description = "目标库位ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "目标库位不能为空")
    private Long toAreaId;
    @Schema(description = "移库数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
    @NotNull(message = "移库数量不能为空")
    private BigDecimal quantity;
    @Schema(description = "备注", example = "库位调整")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstock/MesWmMaterialStockMapper.java
@@ -146,6 +146,28 @@
        return selectOne(wrapper);
    }
    /**
     * æŒ‰ç»„合键查询库存记录(忽略供应商维度)
     *
     * ç”¨äºŽç§»åº“场景:移库时将库存合并到目标库位已有库存,不区分供应商
     */
    default MesWmMaterialStockDO selectByCompositeKeyIgnoreVendor(Long itemId, Long warehouseId, Long locationId,
                                                                    Long areaId, Long batchId) {
        LambdaQueryWrapperX<MesWmMaterialStockDO> wrapper = new LambdaQueryWrapperX<MesWmMaterialStockDO>()
                .eq(MesWmMaterialStockDO::getItemId, itemId)
                .eq(MesWmMaterialStockDO::getWarehouseId, warehouseId)
                .eq(MesWmMaterialStockDO::getLocationId, locationId)
                .eq(MesWmMaterialStockDO::getAreaId, areaId);
        // batchId=null æ—¶ç²¾ç¡®åŒ¹é… is null
        if (batchId != null) {
            wrapper.eq(MesWmMaterialStockDO::getBatchId, batchId);
        } else {
            wrapper.isNull(MesWmMaterialStockDO::getBatchId);
        }
        wrapper.last("LIMIT 1");
        return selectOne(wrapper);
    }
    default List<MesWmMaterialStockDO> selectList(MesWmMaterialStockListReqVO reqVO) {
        return selectList(new LambdaQueryWrapperX<MesWmMaterialStockDO>()
                .eqIfPresent(MesWmMaterialStockDO::getWarehouseId, reqVO.getWarehouseId())
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -460,6 +460,7 @@
    ErrorCode WM_MATERIAL_STOCK_REQUIRED = new ErrorCode(1_040_703_013, "库存记录不能为空");
    ErrorCode WM_MATERIAL_STOCK_SELECTION_MISMATCH = new ErrorCode(1_040_703_014, "库存记录与提交的物料、批次或库位信息不一致");
    ErrorCode WM_MATERIAL_STOCK_QUANTITY_INVALID = new ErrorCode(1_040_703_015, "调整数量不能为空或零");
    ErrorCode WM_MATERIAL_STOCK_FROZEN = new ErrorCode(1_040_703_016, "库存记录已冻结,无法移库");
    // ========== MES ä»“库管理-到货通知单(1-040-704-000) ==========
    ErrorCode WM_ARRIVAL_NOTICE_NOT_EXISTS = new ErrorCode(1_040_704_000, "到货通知单不存在");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockService.java
@@ -4,6 +4,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockAdjustReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockFreezeReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockListReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockMoveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import jakarta.validation.Valid;
@@ -179,4 +180,11 @@
     */
    Long adjustMaterialStockOut(@Valid MesWmMaterialStockAdjustReqVO reqVO);
    /**
     * åº“存移库
     *
     * @param reqVO ç§»åº“请求
     */
    void moveMaterialStock(@Valid MesWmMaterialStockMoveReqVO reqVO);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java
@@ -8,6 +8,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockAdjustReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockFreezeReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockListReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockMoveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
@@ -415,4 +416,82 @@
        throw new IllegalArgumentException("无效的质量状态类型: " + qualityStatus.getClass().getName());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void moveMaterialStock(MesWmMaterialStockMoveReqVO reqVO) {
        // 1. æŸ¥è¯¢åŽŸåº“å­˜è®°å½•
        MesWmMaterialStockDO sourceStock = validateMaterialStockExists(reqVO.getMaterialStockId());
        // 2. æ ¡éªŒåº“存数量充足(可用量 = åœ¨åº“数量 - å†»ç»“数量 - å ç”¨é‡ï¼‰
        BigDecimal availableQty = sourceStock.getQuantity() != null ? sourceStock.getQuantity() : BigDecimal.ZERO;
        if (sourceStock.getFrozenQuantity() != null) {
            availableQty = availableQty.subtract(sourceStock.getFrozenQuantity());
        }
        if (sourceStock.getReservedQuantity() != null) {
            availableQty = availableQty.subtract(sourceStock.getReservedQuantity());
        }
        if (availableQty.compareTo(reqVO.getQuantity()) < 0) {
            throw exception(WM_MATERIAL_STOCK_INSUFFICIENT);
        }
        // 3. æ ¡éªŒåº“存未冻结
        if (Boolean.TRUE.equals(sourceStock.getFrozen())) {
            throw exception(WM_MATERIAL_STOCK_FROZEN);
        }
        // 4. æ ¡éªŒç›®æ ‡åº“位混放规则
        checkAreaMixingRule(reqVO.getToAreaId(), sourceStock.getItemId(), sourceStock.getBatchId());
        // 5. ç”Ÿæˆç§»åº“单号
        String moveCode = "MOVE_" + System.currentTimeMillis();
        // 6. åˆ›å»ºè°ƒæ‹¨ç§»å‡ºäº‹åŠ¡ï¼ˆè´Ÿæ•°ï¼‰
        MesWmTransactionSaveReqDTO outTransaction = new MesWmTransactionSaveReqDTO()
                .setType(MesWmTransactionTypeEnum.MOVE_OUT.getType())
                .setBizType(MesBizTypeConstants.WM_TRANSFER_OUT)
                .setBizId(sourceStock.getId())
                .setBizCode(moveCode)
                .setBizLineId(sourceStock.getId())
                .setItemId(sourceStock.getItemId())
                .setQuantity(reqVO.getQuantity().negate())
                .setBatchId(sourceStock.getBatchId())
                .setBatchCode(sourceStock.getBatchCode())
                .setWarehouseId(sourceStock.getWarehouseId())
                .setLocationId(sourceStock.getLocationId())
                .setAreaId(sourceStock.getAreaId())
                .setVendorId(sourceStock.getVendorId())
                .setMaterialStockId(sourceStock.getId())
                .setCheckFlag(true);
        Long outTransactionId = wmTransactionService.createTransaction(outTransaction);
        // 7. æŸ¥æ‰¾ç›®æ ‡åº“位的库存记录(忽略供应商),用于合并库存
        MesWmMaterialStockDO targetStock = materialStockMapper.selectByCompositeKeyIgnoreVendor(
                sourceStock.getItemId(),
                reqVO.getToWarehouseId(),
                reqVO.getToLocationId(),
                reqVO.getToAreaId(),
                sourceStock.getBatchId()
        );
        // 8. åˆ›å»ºè°ƒæ‹¨ç§»å…¥äº‹åŠ¡ï¼ˆæ­£æ•°ï¼‰ï¼Œå…³è”å‡ºåº“äº‹åŠ¡
        // ä½¿ç”¨ç›®æ ‡åº“位已有库存记录的 vendorId(如果有),否则不传
        MesWmTransactionSaveReqDTO inTransaction = new MesWmTransactionSaveReqDTO()
                .setType(MesWmTransactionTypeEnum.MOVE_IN.getType())
                .setBizType(MesBizTypeConstants.WM_TRANSFER_IN)
                .setBizId(sourceStock.getId())
                .setBizCode(moveCode)
                .setBizLineId(sourceStock.getId())
                .setItemId(sourceStock.getItemId())
                .setQuantity(reqVO.getQuantity())
                .setBatchId(sourceStock.getBatchId())
                .setBatchCode(sourceStock.getBatchCode())
                .setWarehouseId(reqVO.getToWarehouseId())
                .setLocationId(reqVO.getToLocationId())
                .setAreaId(reqVO.getToAreaId())
                .setVendorId(targetStock != null ? targetStock.getVendorId() : null)
                .setRelatedTransactionId(outTransactionId)
                .setCheckFlag(false);
        wmTransactionService.createTransaction(inTransaction);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/transfer/MesWmTransferServiceImpl.java
@@ -10,6 +10,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transfer.MesWmTransferDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transfer.MesWmTransferDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.transfer.MesWmTransferLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.transfer.MesWmTransferMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
@@ -189,20 +190,33 @@
        // 2. éåŽ†è¡Œå’Œå¯¹åº”çš„æ˜Žç»†ï¼Œåˆ›å»ºäº‹åŠ¡
        for (MesWmTransferLineDO line : lines) {
            List<MesWmTransferDetailDO> details = detailMap.getOrDefault(line.getId(), Collections.emptyList());
            // ä»Ž materialStockId èŽ·å–æºåº“å­˜ä¿¡æ¯ï¼ˆåŒ…æ‹¬ vendor_id, batch_id ç­‰ï¼‰
            Long vendorId = null;
            String batchCode = null;
            if (line.getMaterialStockId() != null) {
                MesWmMaterialStockDO sourceStock = materialStockService.getMaterialStock(line.getMaterialStockId());
                if (sourceStock != null) {
                    vendorId = sourceStock.getVendorId();
                    batchCode = sourceStock.getBatchCode();
                }
            }
            for (MesWmTransferDetailDO detail : details) {
                // 2.1 å…ˆæ‰§è¡Œå‡ºåº“:调拨移出(从源仓库扣减库存),出库数量基于当前明细的数量
                Long outTransactionId = wmTransactionService.createTransaction(new MesWmTransactionSaveReqDTO()
                        .setType(MesWmTransactionTypeEnum.MOVE_OUT.getType()).setItemId(line.getItemId())
                        .setQuantity(detail.getQuantity().negate()) // åº“存减少(按 detail çš„æ•°é‡å‡ºï¼‰
                        .setBatchId(line.getBatchId())
                        .setBatchId(line.getBatchId()).setBatchCode(batchCode)
                        .setVendorId(vendorId)
                        .setMaterialStockId(line.getMaterialStockId())
                        .setWarehouseId(line.getFromWarehouseId()).setLocationId(line.getFromLocationId()).setAreaId(line.getFromAreaId())
                        .setBizType(MesBizTypeConstants.WM_TRANSFER_OUT).setBizId(transfer.getId())
                        .setBizCode(transfer.getCode()).setBizLineId(line.getId()));
                // 2.2 å†æ‰§è¡Œå…¥åº“:调拨移入(向目标仓库增加库存),入库数量等于相同明细的数量
                // æ³¨æ„ï¼šå…¥åº“时不传 vendorId,以忽略供应商维度,合并到目标库位已有库存
                wmTransactionService.createTransaction(new MesWmTransactionSaveReqDTO()
                        .setType(MesWmTransactionTypeEnum.MOVE_IN.getType()).setItemId(line.getItemId()) // ç‰©æ–™ä¿¡æ¯æ¥è‡ª line
                        .setQuantity(detail.getQuantity()) // åº“存增加(按 detail çš„æ•°é‡å…¥ï¼‰
                        .setBatchId(line.getBatchId()) // æ‰¹æ¬¡ä¿¡æ¯æ¥è‡ª line
                        .setBatchId(line.getBatchId()).setBatchCode(batchCode) // æ‰¹æ¬¡ä¿¡æ¯æ¥è‡ª line
                        .setWarehouseId(detail.getToWarehouseId()).setLocationId(detail.getToLocationId()).setAreaId(detail.getToAreaId())
                        .setBizType(MesBizTypeConstants.WM_TRANSFER_IN).setBizId(transfer.getId())
                        .setBizCode(transfer.getCode()).setBizLineId(line.getId())
yudao-server/src/main/java/cn/iocoder/yudao/server/controller/DefaultController.java
@@ -58,11 +58,12 @@
                "[CRM æ¨¡å— yudao-module-crm - å·²ç¦ç”¨][参考 https://doc.iocoder.cn/crm/build/ å¼€å¯]");
    }
    @RequestMapping(value = { "/admin-api/mes/**"})
    public CommonResult<Boolean> mes404() {
        return CommonResult.error(NOT_IMPLEMENTED.getCode(),
                "[MES ç³»ç»Ÿ yudao-module-mes - å·²ç¦ç”¨][参考 https://doc.iocoder.cn/mes/build/ å¼€å¯]");
    }
    // MES æ¨¡å—已启用,移除兜底映射
    // @RequestMapping(value = { "/admin-api/mes/**"})
    // public CommonResult<Boolean> mes404() {
    //     return CommonResult.error(NOT_IMPLEMENTED.getCode(),
    //             "[MES ç³»ç»Ÿ yudao-module-mes - å·²ç¦ç”¨][参考 https://doc.iocoder.cn/mes/build/ å¼€å¯]");
    // }
    @RequestMapping(value = { "/admin-api/im/**"})
    public CommonResult<Boolean> im404() {