9 天以前 67c5d3ea86cfaad45c0150b96949dbcb6729b4d0
1.仓储物流-库存可用量管理
已添加7个文件
已修改17个文件
794 ■■■■■ 文件已修改
.claude/rules/frontend-doc-generation.md 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/stock_available_quantity_integration.md 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/pom.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/MesWmMaterialStockController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockRespVO.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/MesWmProductSalesController.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/vo/MesWmProductSalesRespVO.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/MesWmReturnSalesController.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesRespVO.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/MesWmSalesNoticeController.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/vo/MesWmSalesNoticeRespVO.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/materialstock/MesWmMaterialStockDO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/productsales/MesWmProductSalesDO.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnsales/MesWmReturnSalesDO.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/salesnotice/MesWmSalesNoticeDO.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/stockreserve/MesWmStockReserveDO.java 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstock/MesWmMaterialStockMapper.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/stockreserve/MesWmStockReserveMapper.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmStockReserveBizTypeEnum.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmStockReserveStatusEnum.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueServiceImpl.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stockreserve/MesWmStockReserveService.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stockreserve/MesWmStockReserveServiceImpl.java 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.claude/rules/frontend-doc-generation.md
@@ -11,13 +11,24 @@
后端接口变更时,**不生成前端代码**,只生成前端联调方案文档,输出到 `docs/` ç›®å½•。
## ç¦æ­¢å†…容
- âŒ ç¦æ­¢å†™ Vue æ¨¡æ¿ä»£ç ï¼ˆ`<el-table-column>`、`<el-form-item>` ç­‰ï¼‰
- âŒ ç¦æ­¢å†™ JavaScript/TypeScript ä»£ç ï¼ˆæ–¹æ³•、函数、逻辑判断等)
- âŒ ç¦æ­¢å†™ CSS æ ·å¼ä»£ç 
- âŒ ç¦æ­¢å†™å‰ç«¯ç»„件使用示例
## è”调方案内容要求
文档应只包含以下内容:
- **API è¯´æ˜Ž**:路径、方法、请求参数、响应格式
- **涉及的前端页面**:列出需要修改的页面
- **涉及的前端页面**:列出需要修改的页面名称
- **数据带入说明**:重点强调信息如何从上一步带入(如采购订单 -> åˆ°è´§é€šçŸ¥ -> é‡‡è´­å…¥åº“)
- **接口调用时序**:说明何时调用哪个接口获取关联数据
- **表单字段映射**:明确哪些字段从上游数据自动填充,哪些需要用户手动输入
- **字段展示规则**:用表格说明字段展示位置和说明,不写代码
- **业务规则说明**:用文字或表格说明业务逻辑
- **注意事项**:需要前端注意的边界情况
## æ–‡æ¡£æ¨¡æ¿
@@ -46,26 +57,34 @@
|------|------|------|------|
| xxx | String | æ˜¯ | xxx |
**响应:** `{ "code": 200, "msg": "操作成功" }`
**响应字段新增:**
## å‰ç«¯å®žçŽ°è¦ç‚¹
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| xxx | String | xxx |
### 1. æ•°æ®èŽ·å–æ—¶æœº
**响应示例:**
- é¡µé¢æ‰“开时调用 XXX æŽ¥å£èŽ·å–é‡‡è´­è®¢å•ä¿¡æ¯
- é€‰æ‹©é‡‡è´­è®¢å•后,调用 YYY æŽ¥å£èŽ·å–ç‰©æ–™æ˜Žç»†
```json
{
  "code": 0,
  "data": {}
}
```
### 2. å­—段自动填充规则
## å­—段展示规则
| ç›®æ ‡å­—段 | æ•°æ®æ¥æº | å¡«å……时机 |
|----------|----------|----------|
| ç‰©æ–™ç¼–码 | é‡‡è´­è®¢å•行.itemCode | é€‰æ‹©è®¢å•行后 |
| ç‰©æ–™åç§° | é‡‡è´­è®¢å•行.itemName | é€‰æ‹©è®¢å•行后 |
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| å­—段A | åˆ—表、详情 | è¯´æ˜Ž |
| å­—段B | è¯¦æƒ… | è¯´æ˜Ž |
### 3. ç”¨æˆ·å¯ç¼–辑字段
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
- å­—段A:用户可修改
- å­—段B:用户可修改
| åœºæ™¯ | è§„则 |
|------|------|
| åœºæ™¯1 | è§„则说明 |
| åœºæ™¯2 | è§„则说明 |
## æ³¨æ„äº‹é¡¹
docs/stock_available_quantity_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
# åº“存可用量管理 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- åº“存现有量页面
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
本次为库存台账字段扩展,无跨页面数据流转。
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /mes/wm/material-stock/page | åº“存台账分页查询 |
| GET | /mes/wm/material-stock/get | å•条库存查询 |
**响应字段新增:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| frozenQuantity | Decimal | å†»ç»“数量 |
| reservedQuantity | Decimal | å ç”¨é‡ï¼ˆå·²åˆ†é…å¾…出库) |
| inTransitQuantity | Decimal | åœ¨é€”量(调拨在途) |
| availableQuantity | Decimal | å¯ç”¨é‡ï¼ˆåŽç«¯è®¡ç®—:在库数量 - å†»ç»“数量 - å ç”¨é‡ï¼‰ |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "list": [
      {
        "id": 1,
        "itemCode": "M001",
        "itemName": "钢板",
        "quantity": 100.00,
        "frozenQuantity": 10.00,
        "reservedQuantity": 20.00,
        "inTransitQuantity": 5.00,
        "availableQuantity": 70.00
      }
    ],
    "total": 1
  }
}
```
## å‰ç«¯å®žçŽ°è¦ç‚¹
### 1. æ•°æ®èŽ·å–æ—¶æœº
- é¡µé¢æ‰“开时调用库存台账查询接口,响应中已包含新增字段
- æ— éœ€é¢å¤–接口调用
### 2. å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| åœ¨åº“数量 | åˆ—表、详情 | çŽ°æœ‰å­—æ®µï¼Œä¿æŒä¸å˜ |
| å†»ç»“数量 | åˆ—表、详情 | æ–°å¢žå­—段,可能为 null æ—¶æ˜¾ç¤º 0 |
| å ç”¨é‡ | åˆ—表、详情 | æ–°å¢žå­—段,可能为 null æ—¶æ˜¾ç¤º 0 |
| åœ¨é€”量 | åˆ—表、详情 | æ–°å¢žå­—段,可能为 null æ—¶æ˜¾ç¤º 0 |
| å¯ç”¨é‡ | åˆ—表、详情 | åŽç«¯å·²è®¡ç®—,直接展示;为负数时建议高亮提示 |
### 3. ç”¨æˆ·å¯ç¼–辑字段
无新增可编辑字段,本次仅为展示类字段扩展。
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
### 1. å„字段含义
| å­—段 | å«ä¹‰ |
|------|------|
| åœ¨åº“数量 | ä»“库中实际存在的库存 |
| å†»ç»“数量 | å› ç›˜ç‚¹ã€è´¨æ£€ç­‰åŽŸå› è¢«å†»ç»“çš„åº“å­˜ |
| å ç”¨é‡ | å·²åˆ†é…å¾…出库的数量(拣货后未出库) |
| åœ¨é€”量 | è°ƒæ‹¨åœ¨é€”(源仓库已出库,目标仓库未入库) |
| å¯ç”¨é‡ | å®žé™…可用于分配的库存 |
### 2. å ç”¨é‡äº§ç”Ÿä¸Žé‡Šæ”¾åœºæ™¯
| åœºæ™¯ | æ—¶æœº | å½±å“ |
|------|------|------|
| é”€å”®å‡ºåº“拣货 | æ‹£è´§æ“ä½œæ—¶ | å ç”¨é‡å¢žåŠ ï¼Œå¯ç”¨é‡å‡å°‘ |
| ç”Ÿäº§é¢†æ–™æ‹£è´§ | æ‹£è´§æ“ä½œæ—¶ | å ç”¨é‡å¢žåŠ ï¼Œå¯ç”¨é‡å‡å°‘ |
| å‡ºåº“完成 | æ‰§è¡Œå‡ºåº“æ—¶ | å ç”¨é‡å‡å°‘,在库数量减少 |
| å•据取消 | å–消出库单时 | å ç”¨é‡é‡Šæ”¾ï¼Œå¯ç”¨é‡æ¢å¤ |
## æ³¨æ„äº‹é¡¹
- æ–°å¢žå­—段在历史数据中可能为 null,前端展示时需做空值处理,默认显示 0
- å¯ç”¨é‡ç”±åŽç«¯è®¡ç®—,前端直接使用,无需自行计算
- å¯¼å‡º Excel å·²åŒ…含新字段,无需额外处理
yudao-module-mes/pom.xml
@@ -31,6 +31,13 @@
            <version>${revision}</version>
        </dependency>
        <!-- CRM å®¢æˆ·ç®¡ç† API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
            <artifactId>yudao-module-crm-api</artifactId>
            <version>${revision}</version>
        </dependency>
        <!-- BPM å·¥ä½œæµ API -->
        <dependency>
            <groupId>cn.iocoder.boot</groupId>
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/MesWmMaterialStockController.java
@@ -36,6 +36,7 @@
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -141,6 +142,11 @@
                    area -> vo.setAreaName(area.getName()));
            MapUtils.findAndThen(vendorMap, vo.getVendorId(),
                    vendor -> vo.setVendorName(vendor.getName()));
            // è®¡ç®—可用量 = åœ¨åº“数量 - å†»ç»“数量 - å ç”¨é‡
            BigDecimal quantity = vo.getQuantity() != null ? vo.getQuantity() : BigDecimal.ZERO;
            BigDecimal frozenQty = vo.getFrozenQuantity() != null ? vo.getFrozenQuantity() : BigDecimal.ZERO;
            BigDecimal reservedQty = vo.getReservedQuantity() != null ? vo.getReservedQuantity() : BigDecimal.ZERO;
            vo.setAvailableQuantity(quantity.subtract(frozenQty).subtract(reservedQty));
        });
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/materialstock/vo/MesWmMaterialStockRespVO.java
@@ -81,6 +81,22 @@
    @ExcelProperty("在库数量")
    private BigDecimal quantity;
    @Schema(description = "冻结数量", example = "10.0000")
    @ExcelProperty("冻结数量")
    private BigDecimal frozenQuantity;
    @Schema(description = "占用量(已分配待出库)", example = "20.0000")
    @ExcelProperty("占用量")
    private BigDecimal reservedQuantity;
    @Schema(description = "在途量(调拨在途)", example = "5.0000")
    @ExcelProperty("在途量")
    private BigDecimal inTransitQuantity;
    @Schema(description = "可用量(在库数量 - å†»ç»“数量 - å ç”¨é‡ï¼‰", example = "70.0000")
    @ExcelProperty("可用量")
    private BigDecimal availableQuantity;
    @Schema(description = "入库时间")
    @ExcelProperty("入库时间")
    private LocalDateTime receiptTime;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/MesWmProductSalesController.java
@@ -8,14 +8,14 @@
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.productsales.vo.MesWmProductSalesShippingReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.client.MesMdClientDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productsales.MesWmProductSalesDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.salesnotice.MesWmSalesNoticeDO;
import cn.iocoder.yudao.module.mes.service.md.client.MesMdClientService;
import cn.iocoder.yudao.module.mes.service.wm.productsales.MesWmProductSalesService;
import cn.iocoder.yudao.module.mes.service.wm.salesnotice.MesWmSalesNoticeService;
import io.swagger.v3.oas.annotations.Operation;
@@ -44,7 +44,7 @@
    @Resource
    private MesWmProductSalesService productSalesService;
    @Resource
    private MesMdClientService clientService;
    private CrmCustomerApi crmCustomerApi;
    @Resource
    private MesWmSalesNoticeService salesNoticeService;
@@ -164,17 +164,17 @@
            return Collections.emptyList();
        }
        // 1. èŽ·å¾—å…³è”æ•°æ®
        Map<Long, MesMdClientDO> clientMap = clientService.getClientMap(
        Map<Long, CrmCustomerRespDTO> customerMap = crmCustomerApi.getCustomerMap(
                convertSet(list, MesWmProductSalesDO::getClientId));
        Map<Long, MesWmSalesNoticeDO> noticeMap = salesNoticeService.getSalesNoticeMap(
                convertSet(list, MesWmProductSalesDO::getNoticeId));
        // 2. æž„建结果
        return BeanUtils.toBean(list, MesWmProductSalesRespVO.class, vo -> {
            MapUtils.findAndThen(clientMap, vo.getClientId(), client ->
                    vo.setClientName(client.getName()).setClientCode(client.getCode()));
            MapUtils.findAndThen(customerMap, vo.getClientId(), customer ->
                    vo.setClientName(customer.getName()));
            MapUtils.findAndThen(noticeMap, vo.getNoticeId(),
                    notice -> vo.setNoticeCode(notice.getCode()));
        });
    }
}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/productsales/vo/MesWmProductSalesRespVO.java
@@ -46,10 +46,6 @@
    @ExcelProperty("发货通知单编号")
    private String noticeCode;
    @Schema(description = "客户编码", example = "C001")
    @ExcelProperty("客户编码")
    private String clientCode;
    @Schema(description = "收货人", example = "张三")
    @ExcelProperty("收货人")
    private String contactName;
@@ -82,4 +78,4 @@
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/MesWmReturnSalesController.java
@@ -8,12 +8,12 @@
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnsales.vo.MesWmReturnSalesPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnsales.vo.MesWmReturnSalesRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.returnsales.vo.MesWmReturnSalesSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.client.MesMdClientDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales.MesWmReturnSalesDO;
import cn.iocoder.yudao.module.mes.service.md.client.MesMdClientService;
import cn.iocoder.yudao.module.mes.service.wm.returnsales.MesWmReturnSalesService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -43,7 +43,7 @@
    @Resource
    private MesWmReturnSalesService returnSalesService;
    @Resource
    private MesMdClientService clientService;
    private CrmCustomerApi crmCustomerApi;
    @PostMapping("/create")
    @Operation(summary = "创建销售退货单")
@@ -153,12 +153,12 @@
            return Collections.emptyList();
        }
        // 1. èŽ·å¾—å…³è”æ•°æ®
        Map<Long, MesMdClientDO> clientMap = clientService.getClientMap(
        Map<Long, CrmCustomerRespDTO> customerMap = crmCustomerApi.getCustomerMap(
                convertSet(list, MesWmReturnSalesDO::getClientId));
        // 2. æž„建结果
        return BeanUtils.toBean(list, MesWmReturnSalesRespVO.class, vo ->
                MapUtils.findAndThen(clientMap, vo.getClientId(), client ->
                        vo.setClientCode(client.getCode()).setClientName(client.getName()).setClientNickname(client.getNickname())));
                MapUtils.findAndThen(customerMap, vo.getClientId(), customer ->
                        vo.setClientName(customer.getName())));
    }
}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/returnsales/vo/MesWmReturnSalesRespVO.java
@@ -29,14 +29,8 @@
    @Schema(description = "客户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long clientId;
    @Schema(description = "客户编码", example = "C001")
    private String clientCode;
    @Schema(description = "客户名称", example = "XX客户")
    private String clientName;
    @Schema(description = "客户简称", example = "XX")
    private String clientNickname;
    @Schema(description = "退货日期")
    private LocalDateTime returnDate;
@@ -53,4 +47,4 @@
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime createTime;
}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/MesWmSalesNoticeController.java
@@ -8,12 +8,12 @@
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.salesnotice.vo.MesWmSalesNoticePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.salesnotice.vo.MesWmSalesNoticeRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.salesnotice.vo.MesWmSalesNoticeSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.client.MesMdClientDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.salesnotice.MesWmSalesNoticeDO;
import cn.iocoder.yudao.module.mes.service.md.client.MesMdClientService;
import cn.iocoder.yudao.module.mes.service.wm.salesnotice.MesWmSalesNoticeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -43,7 +43,7 @@
    @Resource
    private MesWmSalesNoticeService salesNoticeService;
    @Resource
    private MesMdClientService clientService;
    private CrmCustomerApi crmCustomerApi;
    @PostMapping("/create")
    @Operation(summary = "创建发货通知单")
@@ -126,13 +126,13 @@
            return Collections.emptyList();
        }
        // 1. èŽ·å¾—å…³è”æ•°æ®
        Map<Long, MesMdClientDO> clientMap = clientService.getClientMap(
        Map<Long, CrmCustomerRespDTO> customerMap = crmCustomerApi.getCustomerMap(
                convertSet(list, MesWmSalesNoticeDO::getClientId));
        // 2. æž„建结果
        return BeanUtils.toBean(list, MesWmSalesNoticeRespVO.class, vo -> {
            MapUtils.findAndThen(clientMap, vo.getClientId(), client ->
                    vo.setClientCode(client.getCode()).setClientName(client.getName()));
            MapUtils.findAndThen(customerMap, vo.getClientId(), customer ->
                    vo.setClientName(customer.getName()));
        });
    }
}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/wm/salesnotice/vo/MesWmSalesNoticeRespVO.java
@@ -27,10 +27,6 @@
    @Schema(description = "客户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Long clientId;
    @Schema(description = "客户编码", example = "C001")
    @ExcelProperty("客户编码")
    private String clientCode;
    @Schema(description = "客户名称", example = "某客户")
    @ExcelProperty("客户名称")
    private String clientName;
@@ -70,4 +66,4 @@
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/materialstock/MesWmMaterialStockDO.java
@@ -95,6 +95,18 @@
     */
    private BigDecimal quantity;
    /**
     * å†»ç»“数量(盘点、质检等冻结)
     */
    private BigDecimal frozenQuantity;
    /**
     * å ç”¨é‡ï¼ˆå·²åˆ†é…å¾…出库)
     */
    private BigDecimal reservedQuantity;
    /**
     * åœ¨é€”量(调拨在途)
     */
    private BigDecimal inTransitQuantity;
    /**
     * å…¥åº“æ—¶é—´
     */
    private LocalDateTime receiptTime;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/productsales/MesWmProductSalesDO.java
@@ -43,7 +43,7 @@
    /**
     * å®¢æˆ·ID
     *
     * å…³è” {@link MesMdClientDO#getId()}
     * å…³è” CRM å®¢æˆ· {@link cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO#getId()}
     */
    private Long clientId;
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/returnsales/MesWmReturnSalesDO.java
@@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.wm.returnsales;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.client.MesMdClientDO;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmReturnSalesStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
@@ -46,7 +45,7 @@
    /**
     * å®¢æˆ· ID
     *
     * å…³è” {@link MesMdClientDO#getId()}
     * å…³è” CRM å®¢æˆ· {@link cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO#getId()}
     */
    private Long clientId;
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/salesnotice/MesWmSalesNoticeDO.java
@@ -43,7 +43,7 @@
    /**
     * å®¢æˆ·ç¼–号
     *
     * å…³è” {@link MesMdClientDO#getId()}
     * å…³è” CRM å®¢æˆ· {@link cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO#getId()}
     */
    private Long clientId;
    /**
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/wm/stockreserve/MesWmStockReserveDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,83 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.wm.stockreserve;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockReserveBizTypeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockReserveStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
/**
 * MES åº“存占用记录 DO
 */
@TableName("mes_wm_stock_reserve")
@KeySequence("mes_wm_stock_reserve_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesWmStockReserveDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * åº“存记录ID
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO#getId()}
     */
    private Long materialStockId;
    /**
     * ä¸šåŠ¡ç±»åž‹
     *
     * æžšä¸¾ {@link MesWmStockReserveBizTypeEnum}
     */
    private Integer bizType;
    /**
     * ä¸šåŠ¡å•æ®ID
     */
    private Long bizId;
    /**
     * ä¸šåŠ¡å•å·
     */
    private String bizCode;
    /**
     * ä¸šåŠ¡å•æ®è¡ŒID
     */
    private Long bizLineId;
    /**
     * ç‰©æ–™ID
     */
    private Long itemId;
    /**
     * å ç”¨æ•°é‡
     */
    private BigDecimal quantity;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link MesWmStockReserveStatusEnum}
     */
    private Integer status;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/materialstock/MesWmMaterialStockMapper.java
@@ -86,6 +86,40 @@
    }
    /**
     * å¢žé‡æ›´æ–°åº“存占用量
     *
     * @param id        åº“存记录编号
     * @param count     å˜åŠ¨æ•°é‡ï¼ˆæ­£æ•°=增加占用,负数=减少占用)
     * @param checkFlag æ˜¯å¦æ ¡éªŒå¯ç”¨é‡å……足(为 true æ—¶å ç”¨ä¸å…è®¸è¶…过可用量)
     * @return å½±å“è¡Œæ•°
     */
    default int updateReservedQuantity(Long id, BigDecimal count, boolean checkFlag) {
        LambdaUpdateWrapper<MesWmMaterialStockDO> updateWrapper = new LambdaUpdateWrapper<MesWmMaterialStockDO>()
                .eq(MesWmMaterialStockDO::getId, id)
                .setSql("reserved_quantity = reserved_quantity + " + count);
        if (checkFlag && count.compareTo(BigDecimal.ZERO) > 0) {
            // æ ¡éªŒå¯ç”¨é‡å……足:quantity - frozen_quantity - reserved_quantity >= count
            updateWrapper.setSql("reserved_quantity = reserved_quantity + " + count +
                    " WHERE quantity - IFNULL(frozen_quantity, 0) - IFNULL(reserved_quantity, 0) >= " + count);
        }
        return update(null, updateWrapper);
    }
    /**
     * å¢žé‡æ›´æ–°åº“存在途量
     *
     * @param id    åº“存记录编号
     * @param count å˜åŠ¨æ•°é‡ï¼ˆæ­£æ•°=增加在途,负数=减少在途)
     * @return å½±å“è¡Œæ•°
     */
    default int updateInTransitQuantity(Long id, BigDecimal count) {
        LambdaUpdateWrapper<MesWmMaterialStockDO> updateWrapper = new LambdaUpdateWrapper<MesWmMaterialStockDO>()
                .eq(MesWmMaterialStockDO::getId, id)
                .setSql("in_transit_quantity = in_transit_quantity + " + count);
        return update(null, updateWrapper);
    }
    /**
     * æŒ‰ç»„合键查询库存记录
     *
     * å”¯ä¸€é”®ï¼š(itemId, warehouseId, locationId, areaId, batchId, vendorId)
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/wm/stockreserve/MesWmStockReserveMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
package cn.iocoder.yudao.module.mes.dal.mysql.wm.stockreserve;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stockreserve.MesWmStockReserveDO;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockReserveStatusEnum;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.math.BigDecimal;
import java.util.List;
/**
 * MES åº“存占用记录 Mapper
 */
@Mapper
public interface MesWmStockReserveMapper extends BaseMapperX<MesWmStockReserveDO> {
    /**
     * æŸ¥è¯¢æŒ‡å®šä¸šåŠ¡å•æ®çš„å ç”¨è®°å½•
     */
    default List<MesWmStockReserveDO> selectListByBiz(Integer bizType, Long bizId) {
        return selectList(new LambdaQueryWrapperX<MesWmStockReserveDO>()
                .eq(MesWmStockReserveDO::getBizType, bizType)
                .eq(MesWmStockReserveDO::getBizId, bizId));
    }
    /**
     * æŸ¥è¯¢æŒ‡å®šä¸šåŠ¡å•æ®çš„å ç”¨ä¸­è®°å½•
     */
    default List<MesWmStockReserveDO> selectReservingListByBiz(Integer bizType, Long bizId) {
        return selectList(new LambdaQueryWrapperX<MesWmStockReserveDO>()
                .eq(MesWmStockReserveDO::getBizType, bizType)
                .eq(MesWmStockReserveDO::getBizId, bizId)
                .eq(MesWmStockReserveDO::getStatus, MesWmStockReserveStatusEnum.RESERVING.getStatus()));
    }
    /**
     * æŸ¥è¯¢æŒ‡å®šåº“存记录的占用中记录
     */
    default List<MesWmStockReserveDO> selectReservingListByMaterialStockId(Long materialStockId) {
        return selectList(new LambdaQueryWrapperX<MesWmStockReserveDO>()
                .eq(MesWmStockReserveDO::getMaterialStockId, materialStockId)
                .eq(MesWmStockReserveDO::getStatus, MesWmStockReserveStatusEnum.RESERVING.getStatus()));
    }
    /**
     * æŸ¥è¯¢æŒ‡å®šç‰©æ–™åœ¨æŒ‡å®šä»“库的占用数量汇总
     */
    @Select("SELECT IFNULL(SUM(sr.quantity), 0) FROM mes_wm_stock_reserve sr " +
            "INNER JOIN mes_wm_material_stock ms ON sr.material_stock_id = ms.id " +
            "WHERE sr.deleted = 0 AND sr.status = 1 AND ms.deleted = 0 " +
            "AND sr.item_id = #{itemId} AND ms.warehouse_id = #{warehouseId}")
    BigDecimal sumReservedQuantityByItemAndWarehouse(@Param("itemId") Long itemId, @Param("warehouseId") Long warehouseId);
    /**
     * æŸ¥è¯¢æŒ‡å®šç‰©æ–™çš„占用数量汇总
     */
    @Select("SELECT IFNULL(SUM(quantity), 0) FROM mes_wm_stock_reserve " +
            "WHERE deleted = 0 AND status = 1 AND item_id = #{itemId}")
    BigDecimal sumReservedQuantityByItem(@Param("itemId") Long itemId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmStockReserveBizTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.enums.wm;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES åº“存占用业务类型枚举
 */
@Getter
@AllArgsConstructor
public enum MesWmStockReserveBizTypeEnum implements ArrayValuable<Integer> {
    PRODUCT_SALES(1, "销售出库"),
    PRODUCT_ISSUE(2, "生产领料"),
    OUTSOURCE_ISSUE(3, "外协发料"),
    MISC_ISSUE(4, "杂项出库"),
    TRANSFER(5, "转移调拨");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesWmStockReserveBizTypeEnum::getType).toArray(Integer[]::new);
    /**
     * ç±»åž‹å€¼
     */
    private final Integer type;
    /**
     * ç±»åž‹å
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/wm/MesWmStockReserveStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.mes.enums.wm;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES åº“存占用状态枚举
 */
@Getter
@AllArgsConstructor
public enum MesWmStockReserveStatusEnum implements ArrayValuable<Integer> {
    RESERVING(1, "占用中"),
    RELEASED(2, "已释放"),
    OUTBOUND(3, "已出库");
    public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesWmStockReserveStatusEnum::getStatus).toArray(Integer[]::new);
    /**
     * çŠ¶æ€å€¼
     */
    private final Integer status;
    /**
     * çŠ¶æ€å
     */
    private final String name;
    @Override
    public Integer[] array() {
        return ARRAYS;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productissue/MesWmProductIssueServiceImpl.java
@@ -14,9 +14,11 @@
import cn.iocoder.yudao.module.mes.dal.mysql.wm.productissue.MesWmProductIssueMapper;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductIssueStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockReserveBizTypeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.wm.stockreserve.MesWmStockReserveService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
@@ -63,6 +65,8 @@
    private MesWmWarehouseLocationService locationService;
    @Resource
    private MesWmWarehouseAreaService areaService;
    @Resource
    private MesWmStockReserveService stockReserveService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -143,6 +147,22 @@
            throw exception(WM_PRODUCT_ISSUE_DETAIL_QUANTITY_MISMATCH);
        }
        // åˆ›å»ºåº“存占用(按明细占用库存)
        List<MesWmProductIssueDetailDO> details = issueDetailService.getProductIssueDetailListByIssueId(id);
        for (MesWmProductIssueDetailDO detail : details) {
            if (detail.getMaterialStockId() != null && detail.getQuantity() != null) {
                stockReserveService.createReserve(
                        detail.getMaterialStockId(),
                        MesWmStockReserveBizTypeEnum.PRODUCT_ISSUE.getType(),
                        id,
                        issue.getCode(),
                        detail.getId(),
                        detail.getItemId(),
                        detail.getQuantity()
                );
            }
        }
        // æ‰§è¡Œæ‹£è´§ï¼ˆå¾…拣货 â†’ å¾…执行领出)
        issueMapper.updateById(new MesWmProductIssueDO()
                .setId(id).setStatus(MesWmProductIssueStatusEnum.APPROVED.getStatus()));
@@ -166,10 +186,13 @@
            throw exception(WM_PRODUCT_ISSUE_DETAIL_QUANTITY_MISMATCH);
        }
        // 2. éåŽ†æ‰€æœ‰æ˜Žç»†ï¼Œåˆ›å»ºåº“å­˜äº‹åŠ¡ï¼ˆæ‰£å‡åº“å­˜ + è®°å½•流水)
        // 2. ç¡®è®¤å ç”¨è½¬å‡ºåº“(释放占用量)
        stockReserveService.confirmReserveToOutbound(MesWmStockReserveBizTypeEnum.PRODUCT_ISSUE.getType(), id);
        // 3. éåŽ†æ‰€æœ‰æ˜Žç»†ï¼Œåˆ›å»ºåº“å­˜äº‹åŠ¡ï¼ˆæ‰£å‡åº“å­˜ + è®°å½•流水)
        createTransactionList(issue);
        // 3. æ›´æ–°å‡ºåº“单状态 + é¢†æ–™æ—¥æœŸ
        // 4. æ›´æ–°å‡ºåº“单状态 + é¢†æ–™æ—¥æœŸ
        issueMapper.updateById(new MesWmProductIssueDO()
                .setId(id).setStatus(MesWmProductIssueStatusEnum.FINISHED.getStatus())
                .setIssueDate(LocalDateTime.now()));
@@ -216,6 +239,9 @@
            throw exception(WM_PRODUCT_ISSUE_CANCEL_NOT_ALLOWED);
        }
        // é‡Šæ”¾åº“存占用
        stockReserveService.releaseReserve(MesWmStockReserveBizTypeEnum.PRODUCT_ISSUE.getType(), id);
        // å–消
        issueMapper.updateById(new MesWmProductIssueDO()
                .setId(id).setStatus(MesWmProductIssueStatusEnum.CANCELED.getStatus()));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesServiceImpl.java
@@ -18,10 +18,12 @@
import cn.iocoder.yudao.module.mes.enums.wm.MesWmProductSalesStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmSalesNoticeStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockReserveBizTypeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.client.MesMdClientService;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import cn.iocoder.yudao.module.mes.service.wm.salesnotice.MesWmSalesNoticeService;
import cn.iocoder.yudao.module.mes.service.wm.stockreserve.MesWmStockReserveService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
@@ -61,6 +63,8 @@
    private MesWmTransactionService wmTransactionService;
    @Resource
    private MesWmMaterialStockService materialStockService;
    @Resource
    private MesWmStockReserveService stockReserveService;
    @Resource
    private ErpSaleOrderApi saleOrderApi;
@@ -181,7 +185,7 @@
            if (CollUtil.isEmpty(details)) {
                throw exception(WM_PRODUCT_SALES_DETAILS_EMPTY);
            }
            // æ ¡éªŒåº“存充足
            // æ ¡éªŒåº“存充足并创建占用
            for (MesWmProductSalesDetailDO detail : details) {
                materialStockService.validateSelectedStock(
                        detail.getMaterialStockId(),
@@ -191,6 +195,16 @@
                        detail.getWarehouseId(),
                        detail.getLocationId(),
                        detail.getAreaId(),
                        detail.getQuantity()
                );
                // åˆ›å»ºåº“存占用
                stockReserveService.createReserve(
                        detail.getMaterialStockId(),
                        MesWmStockReserveBizTypeEnum.PRODUCT_SALES.getType(),
                        id,
                        sales.getCode(),
                        detail.getId(),
                        detail.getItemId(),
                        detail.getQuantity()
                );
            }
@@ -227,19 +241,22 @@
            throw exception(WM_PRODUCT_SALES_CANNOT_FINISH);
        }
        // 2. éåŽ†æ‰€æœ‰æ˜Žç»†ï¼Œåˆ›å»ºåº“å­˜äº‹åŠ¡ï¼ˆæ‰£å‡åº“å­˜ + è®°å½•流水)
        // 2. ç¡®è®¤å ç”¨è½¬å‡ºåº“(释放占用量)
        stockReserveService.confirmReserveToOutbound(MesWmStockReserveBizTypeEnum.PRODUCT_SALES.getType(), id);
        // 3. éåŽ†æ‰€æœ‰æ˜Žç»†ï¼Œåˆ›å»ºåº“å­˜äº‹åŠ¡ï¼ˆæ‰£å‡åº“å­˜ + è®°å½•流水)
        createTransactionList(sales);
        // 3. æ›´æ–°å‡ºåº“单状态
        // 4. æ›´æ–°å‡ºåº“单状态
        productSalesMapper.updateById(new MesWmProductSalesDO()
                .setId(id).setStatus(MesWmProductSalesStatusEnum.FINISHED.getStatus()));
        // 4. æ›´æ–°å…³è”的发货通知单状态
        // 5. æ›´æ–°å…³è”的发货通知单状态
        if (sales.getNoticeId() != null) {
            salesNoticeService.finishSalesNotice(sales.getNoticeId());
        }
        // 5. å›žå†™ ERP é”€å”®è®¢å•的出库数量和状态
        // 6. å›žå†™ ERP é”€å”®è®¢å•的出库数量和状态
        updateErpSaleOrderOutCount(sales);
    }
@@ -296,6 +313,8 @@
                MesWmProductSalesStatusEnum.CANCELED.getStatus())) {
            throw exception(WM_PRODUCT_SALES_CANNOT_CANCEL);
        }
        // é‡Šæ”¾åº“存占用
        stockReserveService.releaseReserve(MesWmStockReserveBizTypeEnum.PRODUCT_SALES.getType(), id);
        // å–消
        productSalesMapper.updateById(new MesWmProductSalesDO()
                .setId(id).setStatus(MesWmProductSalesStatusEnum.CANCELED.getStatus()));
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stockreserve/MesWmStockReserveService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,73 @@
package cn.iocoder.yudao.module.mes.service.wm.stockreserve;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stockreserve.MesWmStockReserveDO;
import java.math.BigDecimal;
import java.util.List;
/**
 * MES åº“存占用 Service æŽ¥å£
 */
public interface MesWmStockReserveService {
    /**
     * åˆ›å»ºåº“存占用
     *
     * @param materialStockId åº“存记录ID
     * @param bizType         ä¸šåŠ¡ç±»åž‹
     * @param bizId           ä¸šåŠ¡å•æ®ID
     * @param bizCode         ä¸šåŠ¡å•å·
     * @param bizLineId       ä¸šåŠ¡å•æ®è¡ŒID
     * @param itemId          ç‰©æ–™ID
     * @param quantity        å ç”¨æ•°é‡
     */
    void createReserve(Long materialStockId, Integer bizType, Long bizId, String bizCode, Long bizLineId, Long itemId, BigDecimal quantity);
    /**
     * æ‰¹é‡åˆ›å»ºåº“存占用
     *
     * @param reserves å ç”¨ä¿¡æ¯åˆ—表
     */
    void createReserveBatch(List<ReserveInfo> reserves);
    /**
     * é‡Šæ”¾åº“存占用(取消单据时)
     *
     * @param bizType ä¸šåŠ¡ç±»åž‹
     * @param bizId   ä¸šåŠ¡å•æ®ID
     */
    void releaseReserve(Integer bizType, Long bizId);
    /**
     * ç¡®è®¤å ç”¨è½¬å‡ºåº“(执行出库时)
     * æ›´æ–°å ç”¨è®°å½•状态为已出库,并扣减库存占用量字段
     *
     * @param bizType ä¸šåŠ¡ç±»åž‹
     * @param bizId   ä¸šåŠ¡å•æ®ID
     */
    void confirmReserveToOutbound(Integer bizType, Long bizId);
    /**
     * æŸ¥è¯¢æŒ‡å®šä¸šåŠ¡å•æ®çš„å ç”¨è®°å½•
     *
     * @param bizType ä¸šåŠ¡ç±»åž‹
     * @param bizId   ä¸šåŠ¡å•æ®ID
     * @return å ç”¨è®°å½•列表
     */
    List<MesWmStockReserveDO> getReserveListByBiz(Integer bizType, Long bizId);
    /**
     * æŸ¥è¯¢æŒ‡å®šç‰©æ–™çš„占用数量汇总
     *
     * @param itemId      ç‰©æ–™ID
     * @param warehouseId ä»“库ID(可选)
     * @return å ç”¨æ•°é‡
     */
    BigDecimal sumReservedQuantity(Long itemId, Long warehouseId);
    /**
     * å ç”¨ä¿¡æ¯
     */
    record ReserveInfo(Long materialStockId, Integer bizType, Long bizId, String bizCode, Long bizLineId, Long itemId, BigDecimal quantity) {}
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/stockreserve/MesWmStockReserveServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,152 @@
package cn.iocoder.yudao.module.mes.service.wm.stockreserve;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.stockreserve.MesWmStockReserveDO;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.wm.stockreserve.MesWmStockReserveMapper;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmStockReserveStatusEnum;
import cn.iocoder.yudao.module.mes.service.wm.materialstock.MesWmMaterialStockService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
 * MES åº“存占用 Service å®žçŽ°ç±»
 */
@Service
@Validated
@Slf4j
public class MesWmStockReserveServiceImpl implements MesWmStockReserveService {
    @Resource
    private MesWmStockReserveMapper stockReserveMapper;
    @Resource
    private MesWmMaterialStockMapper materialStockMapper;
    @Resource
    private MesWmMaterialStockService materialStockService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void createReserve(Long materialStockId, Integer bizType, Long bizId, String bizCode, Long bizLineId, Long itemId, BigDecimal quantity) {
        // 1. æ ¡éªŒåº“存记录存在
        MesWmMaterialStockDO stock = materialStockService.getMaterialStock(materialStockId);
        if (stock == null) {
            throw exception(WM_MATERIAL_STOCK_NOT_EXISTS);
        }
        // 2. æ ¡éªŒå¯ç”¨é‡å……足(在库数量 - å†»ç»“数量 - å·²å ç”¨é‡ >= æœ¬æ¬¡å ç”¨æ•°é‡ï¼‰
        BigDecimal availableQty = stock.getQuantity()
                .subtract(stock.getFrozenQuantity() != null ? stock.getFrozenQuantity() : BigDecimal.ZERO)
                .subtract(stock.getReservedQuantity() != null ? stock.getReservedQuantity() : BigDecimal.ZERO);
        if (availableQty.compareTo(quantity) < 0) {
            throw exception(WM_MATERIAL_STOCK_INSUFFICIENT);
        }
        // 3. åˆ›å»ºå ç”¨è®°å½•
        MesWmStockReserveDO reserve = MesWmStockReserveDO.builder()
                .materialStockId(materialStockId)
                .bizType(bizType)
                .bizId(bizId)
                .bizCode(bizCode)
                .bizLineId(bizLineId)
                .itemId(itemId)
                .quantity(quantity)
                .status(MesWmStockReserveStatusEnum.RESERVING.getStatus())
                .build();
        stockReserveMapper.insert(reserve);
        // 4. æ›´æ–°åº“存占用量字段(累加)
        int updatedRows = materialStockMapper.updateReservedQuantity(materialStockId, quantity, true);
        if (updatedRows == 0) {
            throw exception(WM_MATERIAL_STOCK_INSUFFICIENT);
        }
        log.info("[createReserve] åˆ›å»ºåº“存占用成功,materialStockId={}, bizType={}, bizId={}, quantity={}",
                materialStockId, bizType, bizId, quantity);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void createReserveBatch(List<ReserveInfo> reserves) {
        if (CollUtil.isEmpty(reserves)) {
            return;
        }
        for (ReserveInfo reserve : reserves) {
            createReserve(reserve.materialStockId(), reserve.bizType(), reserve.bizId(),
                    reserve.bizCode(), reserve.bizLineId(), reserve.itemId(), reserve.quantity());
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void releaseReserve(Integer bizType, Long bizId) {
        // 1. æŸ¥è¯¢å ç”¨ä¸­çš„记录
        List<MesWmStockReserveDO> reserveList = stockReserveMapper.selectReservingListByBiz(bizType, bizId);
        if (CollUtil.isEmpty(reserveList)) {
            log.warn("[releaseReserve] æœªæ‰¾åˆ°å ç”¨è®°å½•,bizType={}, bizId={}", bizType, bizId);
            return;
        }
        // 2. æ›´æ–°å ç”¨è®°å½•状态为已释放,并扣减库存占用量字段
        for (MesWmStockReserveDO reserve : reserveList) {
            // 2.1 æ›´æ–°å ç”¨è®°å½•状态
            stockReserveMapper.updateById(new MesWmStockReserveDO()
                    .setId(reserve.getId())
                    .setStatus(MesWmStockReserveStatusEnum.RELEASED.getStatus()));
            // 2.2 æ‰£å‡åº“存占用量(负数表示减少)
            materialStockMapper.updateReservedQuantity(reserve.getMaterialStockId(), reserve.getQuantity().negate(), false);
        }
        log.info("[releaseReserve] é‡Šæ”¾åº“存占用成功,bizType={}, bizId={}, count={}", bizType, bizId, reserveList.size());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void confirmReserveToOutbound(Integer bizType, Long bizId) {
        // 1. æŸ¥è¯¢å ç”¨ä¸­çš„记录
        List<MesWmStockReserveDO> reserveList = stockReserveMapper.selectReservingListByBiz(bizType, bizId);
        if (CollUtil.isEmpty(reserveList)) {
            log.warn("[confirmReserveToOutbound] æœªæ‰¾åˆ°å ç”¨è®°å½•,bizType={}, bizId={}", bizType, bizId);
            return;
        }
        // 2. æ›´æ–°å ç”¨è®°å½•状态为已出库,并扣减库存占用量字段
        for (MesWmStockReserveDO reserve : reserveList) {
            // 2.1 æ›´æ–°å ç”¨è®°å½•状态
            stockReserveMapper.updateById(new MesWmStockReserveDO()
                    .setId(reserve.getId())
                    .setStatus(MesWmStockReserveStatusEnum.OUTBOUND.getStatus()));
            // 2.2 æ‰£å‡åº“存占用量(负数表示减少)
            materialStockMapper.updateReservedQuantity(reserve.getMaterialStockId(), reserve.getQuantity().negate(), false);
        }
        log.info("[confirmReserveToOutbound] ç¡®è®¤å ç”¨è½¬å‡ºåº“成功,bizType={}, bizId={}, count={}", bizType, bizId, reserveList.size());
    }
    @Override
    public List<MesWmStockReserveDO> getReserveListByBiz(Integer bizType, Long bizId) {
        return stockReserveMapper.selectListByBiz(bizType, bizId);
    }
    @Override
    public BigDecimal sumReservedQuantity(Long itemId, Long warehouseId) {
        if (warehouseId != null) {
            return stockReserveMapper.sumReservedQuantityByItemAndWarehouse(itemId, warehouseId);
        }
        return stockReserveMapper.sumReservedQuantityByItem(itemId);
    }
}