13 小时以前 46944d762174e5b7e7a627c59928e2a3c9e03dd4
feat(quality): 增加检验管理功能并完善库存预警

- 新增批量快速检验功能,支持一键通过并提交多个检验单
- 实现出厂检验直接绑定销售订单功能,无需通过生产工单链路
- 完善备件库存预警机制,在设备维护和维修时触发预警通知
- 优化采购台账收货状态管理,支持待收货、收货中、已收货状态
- 增加销售台账关联采购合同号功能,便于合同追溯管理
- 完善备件库存预警通知,当库存低于预警数量时自动通知相关人员
已添加5个文件
已修改24个文件
479 ■■■■■ 文件已修改
docs/quality_inspect_add_sales_ledger_id.sql 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sales_ledger_add_purchase_ledger_id.sql 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/出厂检验-销售订单-联调文档.md 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/检验管理-批量快速检验-联调文档.md 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/销售台账-采购合同号-联调文档.md 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/DeviceMaintenanceServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/device/service/impl/DeviceRepairServiceImpl.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/dto/SparePartsDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/pojo/SpareParts.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/ISysNoticeService.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/PurchaseLedger.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityInspectController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/IQualityInspectService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/ShippingInfoDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedger.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/ShippingInfo.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/SalesLedgerVo.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockUninventoryServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/measuringinstrumentledger/SparePartsMapper.xml 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/PurchaseLedgerMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityInspectMapper.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerMapper.xml 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/ShippingInfoMapper.xml 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/quality_inspect_add_sales_ledger_id.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,4 @@
-- å‡ºåŽ‚æ£€éªŒç»‘å®šé”€å”®è®¢å•
-- åœ¨ quality_inspect è¡¨æ–°å¢ž sales_ledger_id å­—段
ALTER TABLE quality_inspect
    ADD COLUMN sales_ledger_id BIGINT DEFAULT NULL COMMENT '销售台账id(出厂检验绑定销售订单)';
docs/sales_ledger_add_purchase_ledger_id.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
-- ============================================================
-- é”€å”®å°è´¦(sales_ledger)添加采购台账关联字段
-- è¯´æ˜Žï¼šé”€å”®å°è´¦å…³è”采购台账,用于在销售台账列表中展示采购合同号
-- æ‰§è¡Œå‰è¯·å¤‡ä»½æ•°æ®åº“
-- ============================================================
-- 1. æ·»åŠ é‡‡è´­å°è´¦id字段
ALTER TABLE sales_ledger
ADD COLUMN purchase_ledger_id BIGINT NULL COMMENT '采购台账id'
AFTER delivery_date;
-- 2. æ·»åŠ ç´¢å¼•ï¼ˆå¯é€‰ï¼Œå¦‚éœ€æŒ‰é‡‡è´­å°è´¦id查询时建议添加)
-- ALTER TABLE sales_ledger ADD INDEX idx_purchase_ledger_id (purchase_ledger_id);
docs/³ö³§¼ìÑé-ÏúÊÛ¶©µ¥-Áªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,94 @@
# å‡ºåŽ‚æ£€éªŒ - é”€å”®è®¢å•绑定联调文档
## å˜æ›´æ¦‚è¿°
出厂检验(`inspectType = 2`)支持直接绑定销售订单,无需通过生产工单链路获取销售合同号。
## ä¸€ã€æ•°æ®åº“变更
执行 `docs/quality_inspect_add_sales_ledger_id.sql`:
```sql
ALTER TABLE quality_inspect
    ADD COLUMN sales_ledger_id BIGINT DEFAULT NULL COMMENT '销售台账id(出厂检验绑定销售订单)';
```
## äºŒã€åŽç«¯æŽ¥å£å˜æ›´
### 2.1 æ–°å¢ž/编辑出厂检验
**POST** `/quality/qualityInspect/add`
**POST** `/quality/qualityInspect/update`
新增字段:
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| `salesLedgerId` | Long | å¦ | é”€å”®å°è´¦ID,出厂检验时选择绑定的销售订单 |
请求体示例:
```json
{
    "inspectType": 2,
    "salesLedgerId": 1001,
    "checkTime": "2026-05-30",
    "customer": "客户名称",
    "productName": "产品A",
    ...
}
```
### 2.2 åˆ—表查询
**GET** `/quality/qualityInspect/listPage?inspectType=2`
返回字段不变,`salesContractNo` çŽ°åœ¨ä¼˜å…ˆä»Žç›´æŽ¥ç»‘å®šçš„ `sales_ledger_id` èŽ·å–ï¼Œä¸ºç©ºæ—¶å›žé€€åˆ°ç”Ÿäº§å·¥å•é“¾è·¯ã€‚
```json
{
    "records": [
        {
            "id": 1,
            "inspectType": 2,
            "salesLedgerId": 1001,
            "salesContractNo": "XS-2026-0001",
            "workOrderNo": "GD-2026-001",
            ...
        }
    ]
}
```
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| `salesLedgerId` | Long | ç»‘定的销售台账ID |
| `salesContractNo` | String | é”€å”®åˆåŒå·ï¼ˆå­—符串,优先取直接绑定) |
| `workOrderNo` | String | å·¥å•号(通过报工链路获取,出厂检验可能为空) |
## ä¸‰ã€å‰ç«¯å¯¹æŽ¥è¦ç‚¹
### 3.1 å‡ºåŽ‚æ£€éªŒè¡¨å•
在出厂检验(`inspectType = 2`)的新增/编辑表单中,新增"销售订单"选择器:
- **字段名**:`salesLedgerId`
- **组件类型**:下拉选择 / å¼¹çª—选择
- **数据源**:调用销售台账列表接口,展示 `salesContractNo`(销售合同号)
- **显示值**:销售合同号
- **绑定值**:销售台账 `id`
- **交互建议**:出厂检验时,允许用户直接搜索/选择销售订单,选中后自动填入 `salesLedgerId`
### 3.2 åˆ—表页
出厂检验列表页已有 `salesContractNo` åˆ—,无需额外改动。后端已自动返回直接绑定的销售合同号。
### 3.3 è¯¦æƒ…页
详情接口 **GET** `/quality/qualityInspect/{id}` è¿”回的 `salesLedgerId` å¯ç”¨äºŽå›žæ˜¾å·²ç»‘定的销售订单。
## å››ã€æ³¨æ„äº‹é¡¹
1. `salesLedgerId` ä»…在 `inspectType = 2`(出厂检验)时使用,原材料检验和过程检验忽略此字段
2. å‡ºåŽ‚æ£€éªŒé€šè¿‡æŠ¥å·¥ï¼ˆ`productMainId`)绑定的工单号(`workOrderNo`)仍然保留,不受影响
3. `salesContractNo` çš„取值优先级:直接绑定 > ç”Ÿäº§å·¥å•链路
docs/¼ìÑé¹ÜÀí-ÅúÁ¿¿ìËÙ¼ìÑé-Áªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,85 @@
# æ£€éªŒç®¡ç† - æ‰¹é‡å¿«é€Ÿæ£€éªŒè”调文档
## å˜æ›´æ¦‚è¿°
原材料检验、过程检验、出厂检验三种类型均支持"快速检验":选中多条未提交的检验单,一键自动通过并提交。
## ä¸€ã€æŽ¥å£è¯´æ˜Ž
### æ‰¹é‡å¿«é€Ÿæ£€éªŒ
**POST** `/quality/qualityInspect/batchQuickInspect`
请求体:
```json
[1001, 1002, 1003]
```
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| `ids` | Long[] | æ˜¯ | éœ€è¦å¿«é€Ÿæ£€éªŒçš„æ£€éªŒå•ID数组 |
返回示例:
```json
{
    "code": 200,
    "msg": "快速检验完成:成功 3 æ¡ï¼Œå¤±è´¥ 0 æ¡"
}
```
## äºŒã€ä¸šåŠ¡é€»è¾‘
对每个选中的检验单执行以下流程(复用 `autoSubmit` é€»è¾‘):
1. **校验**:检验单不存在 â†’ è·³è¿‡ï¼Œè®¡å¤±è´¥
2. **跳过已提交**:`inspectState == 1` çš„æ£€éªŒå•直接返回成功,不重复处理
3. **自动填充合格**:
   - `checkResult` ä¸ºç©º â†’ è®¾ä¸º `"合格"`
   - `qualifiedQuantity` ä¸ºç©º â†’ è®¾ä¸º `quantity`(总数)
   - `unqualifiedQuantity` ä¸ºç©º â†’ è®¾ä¸º `0`
4. **提交入库**:
   - åˆæ ¼æ•°é‡ > 0 â†’ åˆ›å»ºå…¥åº“记录(`stock_in_record`)
   - ä¸åˆæ ¼æ•°é‡ > 0 â†’ åˆ›å»ºä¸åˆæ ¼å“è®°å½•(`quality_unqualified`)
   - `inspectState` æ›´æ–°ä¸º `1`(已提交)
返回成功/失败计数。
## ä¸‰ã€å‰ç«¯å¯¹æŽ¥è¦ç‚¹
### 3.1 æŒ‰é’®ä½ç½®
在检验列表页(原材料/过程/出厂检验)的工具栏增加"快速检验"按钮。
### 3.2 äº¤äº’流程
```
1. ç”¨æˆ·å‹¾é€‰å¤šæ¡æ£€éªŒå•(inspectState = 0,未提交状态)
2. ç‚¹å‡»"快速检验"按钮
3. å¼¹å‡ºç¡®è®¤æ¡†ï¼š
   â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
   â”‚  å¿«é€Ÿæ£€éªŒ                      â”‚
   â”‚                              â”‚
   â”‚  å·²é€‰æ‹© X æ¡æ£€éªŒå•             â”‚
   â”‚                              â”‚
   â”‚  ç¡®è®¤åŽå°†è‡ªåŠ¨ï¼š                 â”‚
   â”‚  Â· æ£€éªŒç»“果设为"合格"          â”‚
   â”‚  Â· åˆæ ¼æ•°é‡è®¾ä¸ºæ€»æ•°             â”‚
   â”‚  Â· ä¸åˆæ ¼æ•°é‡è®¾ä¸º 0            â”‚
   â”‚  Â· æäº¤å¹¶å…¥åº“                  â”‚
   â”‚                              â”‚
   â”‚  å·²æäº¤çš„æ£€éªŒå•将自动跳过        â”‚
   â”‚                              â”‚
   â”‚      [取消]    [确认]          â”‚
   â””─────────────────────────────┘
4. ç¡®è®¤åŽè°ƒç”¨ /batchQuickInspect
5. æ ¹æ®è¿”回结果刷新列表,提示成功/失败数量
```
### 3.3 æ³¨æ„äº‹é¡¹
- æ‰¹é‡å¿«é€Ÿæ£€éªŒä»…对**未提交**(`inspectState = 0`)的检验单生效
- å·²æäº¤çš„æ£€éªŒå•会自动跳过,不会重复处理
- ä¸‰ç§æ£€éªŒç±»åž‹ï¼ˆåŽŸææ–™/过程/出厂)共用同一个接口
- å‰ç«¯å»ºè®®åœ¨åˆ—表数据中通过 `inspectState` å­—段**置灰已提交行**,防止用户误选
docs/ÏúÊŲ̂ÕË-²É¹ººÏͬºÅ-Áªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,99 @@
# é”€å”®å°è´¦åˆ—表 - æ–°å¢žé‡‡è´­åˆåŒå·å­—段
## å˜æ›´æ¦‚è¿°
销售台账列表接口新增 **采购合同号** å­—段返回,同时 `sales_ledger` è¡¨æ–°å¢ž `purchase_ledger_id` å­—段用于关联采购台账。
---
## ä¸€ã€æ•°æ®åº“变更
**执行文件**: `docs/sales_ledger_add_purchase_ledger_id.sql`
| è¡¨å | å˜æ›´ç±»åž‹ | å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|---------|------|------|------|
| sales_ledger | æ–°å¢ž | purchase_ledger_id | BIGINT | å…³è” purchase_ledger.id |
---
## äºŒã€æŽ¥å£å˜æ›´
### æ¶‰åŠæŽ¥å£
```
GET /sales/ledger/listPage
```
### è¯·æ±‚参数
无变更,沿用现有参数。
### å“åº”变更
响应外层结构不变(仍是 `IPage<SalesLedgerVo>` åˆ†é¡µæ ¼å¼ï¼‰ï¼Œ`records` ä¸­æ¯æ¡è®°å½•新增一个字段:
| å­—段名 | ç±»åž‹ | è¯´æ˜Ž | ç¤ºä¾‹å€¼ |
|--------|------|------|--------|
| `purchaseContractNumber` | String | é‡‡è´­åˆåŒå·ï¼Œæœªå…³è”采购台账时为 null | `"CG202605001"` |
### å“åº”示例(仅展示新增字段)
```json
{
  "records": [
    {
      "id": 1,
      "salesContractNo": "XS202605001",
      "customerName": "XX公司",
      "contractAmount": 100000.00,
      "purchaseContractNumber": "CG202605001"
    },
    {
      "id": 2,
      "salesContractNo": "XS202605002",
      "customerName": "YY公司",
      "contractAmount": 50000.00,
      "purchaseContractNumber": null
    }
  ],
  "total": 2,
  "current": 1,
  "size": 10
}
```
### æ•°æ®æ¥æºè¯´æ˜Ž
- `purchaseContractNumber` é€šè¿‡ `sales_ledger.purchase_ledger_id` å…³è” `purchase_ledger.id` èŽ·å–
- å–自 `purchase_ledger.purchase_contract_number` å­—段
- é€šè¿‡ LEFT JOIN æŸ¥è¯¢ï¼Œæœªå…³è”时不阻塞列表展示
---
## ä¸‰ã€ä¿å­˜/编辑接口说明
保存销售台账时如需关联采购台账,传入新增字段:
```json
POST /sales/ledger/addOrUpdateSalesLedger
{
  "id": 1,
  "salesContractNo": "XS202605001",
  "purchaseLedgerId": 10,
  "...": "..."
}
```
| å­—段名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| `purchaseLedgerId` | Long | å¦ | é‡‡è´­å°è´¦id,对应 purchase_ledger.id |
---
## å››ã€å‰ç«¯å¯¹æŽ¥æ¸…单
- [ ] é”€å”®å°è´¦**列表页**:新增"采购合同号"列,绑定 `record.purchaseContractNumber`
- [ ] é”€å”®å°è´¦**新增/编辑表单**:新增"关联采购台账"下拉选择框,绑定 `form.purchaseLedgerId`
- [ ] é‡‡è´­åˆåŒå·åˆ—建议设置为可点击链接,跳转至对应采购台账详情
- [ ] å¦‚æžœ `purchaseContractNumber` ä¸º null,列展示 "-" æˆ–置空
src/main/java/com/ruoyi/device/service/impl/DeviceMaintenanceServiceImpl.java
@@ -19,6 +19,7 @@
import com.ruoyi.device.vo.DeviceRepairVo;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.measuringinstrumentledger.mapper.SparePartsMapper;
import com.ruoyi.project.system.service.ISysNoticeService;
import com.ruoyi.measuringinstrumentledger.pojo.SpareParts;
import com.ruoyi.measuringinstrumentledger.pojo.SparePartsRequisitionRecord;
import com.ruoyi.measuringinstrumentledger.service.SparePartsRequisitionRecordService;
@@ -42,6 +43,7 @@
    private final SparePartsMapper sparePartsMapper;
    private final SparePartsRequisitionRecordService sparePartsRequisitionRecordService;
    private final FileUtil fileUtil;
    private final ISysNoticeService sysNoticeService;
    @Override
    public IPage<DeviceMaintenanceDto> queryPage(Page page, DeviceMaintenanceDto deviceMaintenanceDto) {
@@ -94,6 +96,18 @@
                        sparePartsMapper.updateById(spareParts);
                        sparePartIds.add(sparePartUse.getId());
                        // åº“存预警通知
                        if (spareParts.getWarnNum() != null && spareParts.getNotifyPersonId() != null
                                && spareParts.getQuantity().compareTo(spareParts.getWarnNum()) < 0) {
                            sysNoticeService.simpleNoticeByUser(
                                    "备件库存预警",
                                    "备件【" + spareParts.getName() + "】当前库存(" + spareParts.getQuantity()
                                            + ")已低于预警数量(" + spareParts.getWarnNum() + "),请及时采购补充。",
                                    List.of(spareParts.getNotifyPersonId()),
                                    "/equipmentManagement/spareParts"
                                    );
                        }
                        // åˆ›å»ºå¤‡ä»¶é¢†ç”¨è®°å½•
                        SparePartsRequisitionRecord record = new SparePartsRequisitionRecord();
                        record.setSourceType(1); // 1 ä¿å…»
src/main/java/com/ruoyi/device/service/impl/DeviceRepairServiceImpl.java
@@ -20,12 +20,14 @@
import com.ruoyi.device.vo.DeviceRepairVo;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.measuringinstrumentledger.mapper.SparePartsMapper;
import com.ruoyi.project.system.service.ISysNoticeService;
import com.ruoyi.measuringinstrumentledger.pojo.SpareParts;
import com.ruoyi.measuringinstrumentledger.pojo.SparePartsRequisitionRecord;
import com.ruoyi.measuringinstrumentledger.service.SparePartsRequisitionRecordService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.Lists;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -46,6 +48,7 @@
    private final SparePartsMapper sparePartsMapper;
    private final SparePartsRequisitionRecordService sparePartsRequisitionRecordService;
    private final FileUtil fileUtil;
    private final ISysNoticeService sysNoticeService;
    private static final int STATUS_PENDING_REPAIR = 0;
    private static final int STATUS_COMPLETED = 1;
@@ -116,6 +119,17 @@
                        sparePartsMapper.updateById(spareParts);
                        sparePartIds.add(sparePartUse.getId());
                        // åº“存预警通知
                        if (spareParts.getWarnNum() != null && spareParts.getNotifyPersonId() != null
                                && spareParts.getQuantity().compareTo(spareParts.getWarnNum()) < 0) {
                            sysNoticeService.simpleNoticeByUser(
                                    "备件库存预警",
                                    "备件【" + spareParts.getName() + "】当前库存(" + spareParts.getQuantity()
                                            + ")已低于预警数量(" + spareParts.getWarnNum() + "),请及时采购补充。",
                                    List.of(spareParts.getNotifyPersonId()),
                                    "/equipmentManagement/spareParts");
                        }
                        // åˆ›å»ºå¤‡ä»¶é¢†ç”¨è®°å½•
                        SparePartsRequisitionRecord record = new SparePartsRequisitionRecord();
                        record.setSourceType(0); // 0 ç»´ä¿®
src/main/java/com/ruoyi/measuringinstrumentledger/dto/SparePartsDto.java
@@ -1,6 +1,7 @@
package com.ruoyi.measuringinstrumentledger.dto;
import com.ruoyi.measuringinstrumentledger.pojo.SpareParts;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@@ -11,5 +12,7 @@
     * å¤‡ä»¶åˆ†ç±»çˆ¶åç§°
     */
    private String parentName;
    @Schema(description = "预警通知人昵称")
    private String notifyPersonName;
    private List<SparePartsDto> children;
}
src/main/java/com/ruoyi/measuringinstrumentledger/pojo/SpareParts.java
@@ -53,6 +53,10 @@
     * å¤‡ä»¶åˆ†ç±»æè¿°
     */
    private String description;
    @Schema(description = "库存预警数量")
    private BigDecimal warnNum;
    @Schema(description = "预警通知人id")
    private Long notifyPersonId;
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
src/main/java/com/ruoyi/project/system/service/ISysNoticeService.java
@@ -71,6 +71,7 @@
     * @param title æ ‡é¢˜
     * @param message æ¶ˆæ¯
     * @param jumpPath è·³è½¬åœ°å€
     * @param consigneeId  æ”¶ä»¶äººid
     */
    void simpleNoticeByUser(final String title, final String message, final List<Long> consigneeId,  final String jumpPath);
src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java
@@ -185,6 +185,8 @@
    private String paymentMethod;
    @Schema(description = "审批状态")
    private Integer approvalStatus;
    @Schema(description = "收货状态 1-待收货 2-收货中 3-已收货")
    private Integer status;
    @Schema(description = "模板名称")
    private String templateName;
    @Schema(description = "审批人id")
src/main/java/com/ruoyi/purchase/pojo/PurchaseLedger.java
@@ -146,6 +146,9 @@
    @Excel(name = "审批状态", readConverterExp = "1=待审核,2=审批中,3=审批通过,4=审批失败")
    private Integer approvalStatus;
    @Schema(description = "收货状态 1-待收货 2-收货中 3-已收货")
    private Integer status;
    @Schema(description = "模板名称")
    private String templateName;
    @Schema(description = "审批人id")
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
@@ -33,6 +33,8 @@
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.measuringinstrumentledger.mapper.SparePartsMapper;
import com.ruoyi.measuringinstrumentledger.pojo.SpareParts;
import com.ruoyi.other.mapper.TempFileMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
@@ -116,6 +118,7 @@
    private final StockInRecordService stockInRecordService;
    private final StockUtils stockUtils;
    private final ApprovalTemplateMapper approvalTemplateMapper;
    private final SparePartsMapper sparePartsMapper;
    @Override
    public List<PurchaseLedger> selectPurchaseLedgerList(PurchaseLedger purchaseLedger) {
@@ -162,6 +165,9 @@
        purchaseLedger.setRecorderId(purchaseLedgerDto.getRecorderId());
        purchaseLedger.setApprovalStatus(1);
        if (purchaseLedger.getId() == null) {
            purchaseLedger.setStatus(1);
        }
        // 3. æ–°å¢žæˆ–更新主表
        if (purchaseLedger.getId() == null) {
            purchaseLedgerMapper.insert(purchaseLedger);
@@ -329,7 +335,9 @@
                for (SalesLedgerProduct product : products) {
                    try {
                        boolean processed;
                        if (Boolean.TRUE.equals(product.getIsChecked())) {
                        if (product.getProductType() != null && product.getProductType() == 2) {
                            processed = processPurchaseSparePart(purchaseLedger, product);
                        } else if (Boolean.TRUE.equals(product.getIsChecked())) {
                            processed = processPurchaseQualityProduct(purchaseLedger, product);
                        } else {
                            processed = processPurchaseDirectProduct(purchaseLedger, product);
@@ -359,6 +367,35 @@
                detail.put("message", ex.getMessage());
                details.add(detail);
                log.error("批量推进采购台账失败, purchaseLedgerId={}", id, ex);
            }
        }
        // æ›´æ–°æ”¶è´§çŠ¶æ€
        List<Long> processedIds = details.stream()
                .filter(d -> "SUCCESS".equals(d.get("status")) || "PARTIAL".equals(d.get("status")))
                .map(d -> (Long) d.get("purchaseLedgerId"))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        if (!processedIds.isEmpty()) {
            PurchaseLedgerDto statusQuery = new PurchaseLedgerDto();
            statusQuery.setIds(processedIds);
            IPage<PurchaseLedgerDto> statusPage = this.selectPurchaseLedgerListPage(
                    new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(1, processedIds.size()),
                    statusQuery
            );
            if (statusPage != null && CollectionUtils.isNotEmpty(statusPage.getRecords())) {
                for (PurchaseLedgerDto dto : statusPage.getRecords()) {
                    PurchaseLedger update = new PurchaseLedger();
                    update.setId(dto.getId());
                    if ("完全入库".equals(dto.getStockInStatus())) {
                        update.setStatus(3);
                    } else if ("入库中".equals(dto.getStockInStatus())) {
                        update.setStatus(2);
                    } else {
                        update.setStatus(1);
                    }
                    purchaseLedgerMapper.updateById(update);
                }
            }
        }
@@ -517,6 +554,22 @@
        return hasApprovedStockRecord(stockRecords);
    }
    private boolean processPurchaseSparePart(PurchaseLedger purchaseLedger, SalesLedgerProduct product) {
        if (purchaseLedger == null || product == null || product.getProductModelId() == null) {
            return false;
        }
        SpareParts spareParts = sparePartsMapper.selectById(product.getProductModelId());
        if (spareParts == null) {
            return false;
        }
        BigDecimal newQty = spareParts.getQuantity() != null
                ? spareParts.getQuantity().add(product.getQuantity())
                : product.getQuantity();
        spareParts.setQuantity(newQty);
        sparePartsMapper.updateById(spareParts);
        return true;
    }
    private LocalDateTime toStartOfDayPlusDays(Date date, int days) {
        if (date == null) {
            return null;
@@ -656,6 +709,13 @@
            if (productModelId != null && modelMap.containsKey(productModelId)) {
                product.setSpecificationModel(modelMap.get(productModelId));
            }
            if (product.getProductType() != null && product.getProductType() == 2) {
                SpareParts spareParts = sparePartsMapper.selectById(productModelId);
                if (spareParts != null) {
                    product.setProductCategory(spareParts.getName());
                }
            }
        }
        // åˆ†ç»„处理
src/main/java/com/ruoyi/quality/controller/QualityInspectController.java
@@ -143,6 +143,16 @@
    }
    /**
     * æ‰¹é‡å¿«é€Ÿæ£€éªŒï¼šä¸€é”®é€šè¿‡å¹¶æäº¤
     */
    @PostMapping("/batchQuickInspect")
    @Operation(summary = "批量快速检验")
    @Log(title = "批量快速检验", businessType = BusinessType.OTHER)
    public R<?> batchQuickInspect(@RequestBody List<Long> ids) {
        return qualityInspectService.batchQuickInspect(ids);
    }
    /**
     * ä¸‹è½½
     *
     * @param response
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
@@ -148,6 +148,11 @@
    private Long purchaseLedgerId;
    /**
     * é”€å”®å°è´¦id(出厂检验)
     */
    private Long salesLedgerId;
    /**
     * æŠ¥å·¥id
     */
    private Long productMainId;
src/main/java/com/ruoyi/quality/service/IQualityInspectService.java
@@ -8,6 +8,7 @@
import com.ruoyi.quality.pojo.QualityInspect;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
public interface IQualityInspectService extends IService<QualityInspect> {
@@ -26,5 +27,10 @@
    R autoSubmit(Long id);
    /**
     * æ‰¹é‡å¿«é€Ÿæ£€éªŒï¼šä¸€é”®é€šè¿‡å¹¶æäº¤
     */
    R batchQuickInspect(List<Long> ids);
    void down(HttpServletResponse response, QualityInspect qualityInspect);
}
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -186,6 +186,24 @@
        return rows > 0 ? R.ok("检验单提交成功") : R.fail("检验单提交失败");
    }
    @Override
    public R batchQuickInspect(List<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return R.fail("请选择至少一条检验单");
        }
        int success = 0;
        int fail = 0;
        for (Long id : ids) {
            R result = autoSubmit(id);
            if (R.isSuccess(result)) {
                success++;
            } else {
                fail++;
            }
        }
        return R.ok(String.format("快速检验完成:成功 %d æ¡ï¼Œå¤±è´¥ %d æ¡", success, fail));
    }
    private String resolveProductionBatchNo(Long productionProductMainId,
                                            Long qualityInspectId,
                                            Long productModelId) {
src/main/java/com/ruoyi/sales/dto/ShippingInfoDto.java
@@ -47,6 +47,9 @@
    //发货数量
    private BigDecimal totalQuantity;
    //采购合同号
    private String purchaseContractNumber;
    private Long templateId;
    private String templateName;
src/main/java/com/ruoyi/sales/pojo/SalesLedger.java
@@ -140,5 +140,8 @@
    @TableField(exist = false)
    private Boolean hasProductionRecord;
    @Schema(description = "采购台账id")
    private Long purchaseLedgerId;
}
src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java
@@ -182,6 +182,9 @@
    //针对销售台账,是否生产
    private Boolean isProduction;
    @Schema(description = "产品类型 1-产品 2-设备备件")
    private Integer productType;
    @TableField(exist = false)
    @Schema(description = "待发货数量")
    private BigDecimal noQuantity;
src/main/java/com/ruoyi/sales/pojo/ShippingInfo.java
@@ -28,6 +28,10 @@
    @Excel(name = "客户名称")
    private String customerName;
    @TableField(exist = false)
    @Schema(description = "客户id")
    private Long customerId;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
src/main/java/com/ruoyi/sales/vo/SalesLedgerVo.java
@@ -2,6 +2,7 @@
import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.sales.pojo.SalesLedger;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@@ -10,4 +11,7 @@
public class SalesLedgerVo extends SalesLedger {
    private List<StorageBlobVO> storageBlobVOs;
    @Schema(description = "采购合同号")
    private String purchaseContractNumber;
}
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java
@@ -92,6 +92,7 @@
        stockInRecordDto.setBatchNo(stockInventoryDto.getBatchNo());
        stockInRecordDto.setProductModelId(stockInventoryDto.getProductModelId());
        stockInRecordDto.setType("0");
        stockInRecordDto.setCreateTime(stockInventoryDto.getCreateTime());
        stockInRecordService.add(stockInRecordDto);
        //再进行新增库存数量库存
        //先查询库存表中的产品是否存在,不存在新增,存在更新
@@ -169,6 +170,7 @@
        stockInRecordDto.setType("0");
        stockInRecordDto.setRemark(stockInventoryDto.getRemark());
        stockInRecordDto.setWarnNum(stockInventoryDto.getWarnNum());
        stockInRecordDto.setCreateTime(stockInventoryDto.getCreateTime());
        stockInRecordService.add(stockInRecordDto);
        return true;
    }
src/main/java/com/ruoyi/stock/service/impl/StockUninventoryServiceImpl.java
@@ -79,6 +79,7 @@
        stockInRecordDto.setBatchNo(stockUninventoryDto.getBatchNo());
        stockInRecordDto.setProductModelId(stockUninventoryDto.getProductModelId());
        stockInRecordDto.setType("1");
        stockInRecordDto.setCreateTime(stockUninventoryDto.getCreateTime());
        stockInRecordService.add(stockInRecordDto);
        //审批再添加
        return 1;
@@ -121,6 +122,7 @@
        stockInRecordDto.setProductModelId(stockUninventoryDto.getProductModelId());
        stockInRecordDto.setType("1");
        stockInRecordDto.setRemark(stockUninventoryDto.getRemark());
        stockInRecordDto.setCreateTime(stockUninventoryDto.getCreateTime());
        stockInRecordService.add(stockInRecordDto);
        return 1;
    }
src/main/resources/mapper/measuringinstrumentledger/SparePartsMapper.xml
@@ -2,9 +2,10 @@
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.measuringinstrumentledger.mapper.SparePartsMapper">
    <select id="listPage" resultType="com.ruoyi.measuringinstrumentledger.dto.SparePartsDto">
        select sp.*,sp1.name as parentName
        select sp.*,sp1.name as parentName, su.nick_name as notifyPersonName
        from spare_parts sp
        left join spare_parts sp1 on sp1.id = sp.parent_id
        left join sys_user su on su.user_id = sp.notify_person_id
        <where>
            <if test="spareParts.name != null and spareParts.name != ''">
                and sp.name like concat('%',#{spareParts.name},'%')
src/main/resources/mapper/purchase/PurchaseLedgerMapper.xml
@@ -35,6 +35,7 @@
                pl.approve_user_ids,
                sm.is_white,
                pl.approval_status,
                pl.status,
                pl.payment_method,
                pl.remarks,
                CASE
src/main/resources/mapper/quality/QualityInspectMapper.xml
@@ -10,7 +10,7 @@
            </when>
            <otherwise>
                pot.work_order_no,
                po_sales.sales_contract_no
                COALESCE(sl.sales_contract_no, po_sales.sales_contract_no) as sales_contract_no
            </otherwise>
        </choose>
        FROM
@@ -22,6 +22,7 @@
            <otherwise>
                LEFT JOIN production_product_main ppm ON qi.product_main_id = ppm.id
                LEFT JOIN production_operation_task pot ON ppm.production_operation_task_id = pot.id
                LEFT JOIN sales_ledger sl ON sl.id = qi.sales_ledger_id
                left join production_order po ON po.id = pot.production_order_id
                left join (
                    select po2.id as order_id,
@@ -61,7 +62,8 @@
            AND pot.work_order_no like concat('%',#{qualityInspect.workOrderNo},'%')
        </if>
        <if test="qualityInspect.salesContractNo != null and qualityInspect.salesContractNo != '' ">
            AND po_sales.sales_contract_no like concat('%',#{qualityInspect.salesContractNo},'%')
            AND (sl.sales_contract_no like concat('%',#{qualityInspect.salesContractNo},'%')
                OR po_sales.sales_contract_no like concat('%',#{qualityInspect.salesContractNo},'%'))
        </if>
        ORDER BY qi.check_time DESC
    </select>
src/main/resources/mapper/sales/SalesLedgerMapper.xml
@@ -62,7 +62,8 @@
        T1.payment_method,
        T1.delivery_date,
        DATEDIFF(T1.delivery_date, CURDATE()) AS delivery_days_diff,
        IFNULL(shipping_status_counts.is_all_shipped, FALSE) AS is_fh
        IFNULL(shipping_status_counts.is_all_shipped, FALSE) AS is_fh,
        T3.purchase_contract_number
        FROM sales_ledger T1
        LEFT JOIN sys_user T2 ON T1.entry_person = T2.user_id
        LEFT JOIN (
@@ -74,6 +75,7 @@
        FROM shipping_info
        GROUP BY sales_ledger_id
        ) shipping_status_counts ON T1.id = shipping_status_counts.sales_ledger_id
        LEFT JOIN purchase_ledger T3 ON T1.purchase_ledger_id = T3.id
        <where>
            <if test="salesLedgerDto.customerName != null and salesLedgerDto.customerName != '' ">
src/main/resources/mapper/sales/ShippingInfoMapper.xml
@@ -24,10 +24,12 @@
        p.product_name,
        sl.customer_name,
        spd.totalQuantity,
        sor.outboundBatches
        sor.outboundBatches,
        pl.purchase_contract_number
        FROM shipping_info s
        LEFT JOIN (select shipping_info_id,sum(quantity) totalQuantity from shipping_product_detail GROUP BY shipping_info_id) spd ON spd.shipping_info_id = s.id
        LEFT JOIN sales_ledger sl ON s.sales_ledger_id = sl.id
        LEFT JOIN purchase_ledger pl ON sl.purchase_ledger_id = pl.id
        LEFT JOIN sales_ledger_product slp ON s.sales_ledger_product_id = slp.id and slp.type = 1
        left join product_model pm on slp.product_model_id = pm.id
        left join product p on pm.product_id = p.id
@@ -48,6 +50,9 @@
        <if test="req.expressNumber != null and req.expressNumber != ''">
            AND s.express_number LIKE CONCAT('%',#{req.expressNumber},'%')
        </if>
        <if test="req.customerId != null">
            AND sl.customer_id = #{req.customerId}
        </if>
        order by create_time DESC
    </select>
    <select id="listAll" resultType="com.ruoyi.sales.pojo.ShippingInfo">