8 小时以前 641206421e76861db238ecf24094b2a04e7c6371
feat(account): 新增付款和回款功能并优化生产计划下发规则

- 在AccountingReportController中添加savePayment接口实现新增付款功能
- 在MetricStatisticsController中添加saveReceiptPayment接口实现新增回款功能
- 扩展AccountPurchasePaymentService和AccountSalesCollectionService支持付款回款业务
- 添加数据库查询方法selectStockInRecordIdsByPurchaseLedgerId和selectStockOutRecordIdsBySalesLedgerId
- 优化生产计划下发逻辑,移除下发数量与需求数量的限制校验
- 调整销售发货规则,移除销售产品数量限制但仍保留库存校验
- 在供应商管理中添加银行代码字段bankCode支持
- 修复数据库查询中的聚合函数使用问题,避免重复计算
- 更新开发环境数据库配置连接到正确的数据库实例
已添加4个文件
已修改19个文件
753 ■■■■ 文件已修改
.mcp.json 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260826_生产计划与销售发货数量规则前端联调文档.md 331 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/product-inventory-management-new-pro.sql 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/purchase/AccountPurchasePaymentMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/mapper/sales/AccountSalesCollectionMapper.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/purchase/AccountPurchasePaymentServiceImpl.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/impl/sales/AccountSalesCollectionServiceImpl.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/purchase/AccountPurchasePaymentService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/account/service/sales/AccountSalesCollectionService.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/excel/SupplierManageExcelDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/pojo/SupplierManage.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java 94 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/AccountingReportController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/SavePaymentDto.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/MetricStatisticsController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/SaveReceiptPaymentDto.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/purchase/AccountPurchasePaymentMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/account/sales/AccountSalesCollectionMapper.xml 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/CustomerMapper.xml 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/SupplierManageMapper.xml 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
.mcp.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
{
  "mcpServers": {
    "mysql": {
      "type": "stdio",
      "command": "npx",
      "args": [
        "@benborla29/mcp-server-mysql"
      ],
      "env": {
        "MYSQL_HOST": "127.0.0.1",
        "MYSQL_PORT": "3306",
        "MYSQL_USER": "root",
        "MYSQL_PASS": "123456",
        "MYSQL_DB": "product-inventory-management-new-pro",
        "ALLOW_INSERT_OPERATION": "false",
        "ALLOW_UPDATE_OPERATION": "false",
        "ALLOW_DELETE_OPERATION": "false"
      }
    }
  }
}
doc/20260826_Éú²ú¼Æ»®ÓëÏúÊÛ·¢»õÊýÁ¿¹æÔòǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,331 @@
# ç”Ÿäº§è®¡åˆ’与销售发货数量规则前端联调文档
更新时间:2026-08-26
## 1. æœ¬æ¬¡è°ƒæ•´æ¦‚è¿°
### 1.1 ç”Ÿäº§è®¡åˆ’下发
生产计划下发不再受以下规则限制:
- ä¸å†é™åˆ¶æœ¬æ¬¡ä¸‹å‘数量不能超过需求数量剩余量。
- å·²ä¸‹å‘数量达到需求数量后,仍可以继续下发。
- `quantityIssued` å…è®¸ç´¯è®¡è¶…过 `qtyRequired`。
仍然保留以下校验:
- è‡³å°‘选择一个生产计划。
- `totalAssignedQuantity` å¿…须大于 0。
- é€‰æ‹©çš„生产计划必须存在。
- åˆå¹¶ä¸‹å‘的生产计划必须属于同一个产品型号。
### 1.2 é”€å”®å‘è´§
销售发货不再受以下规则限制:
- ä¸å†å—销售产品 `quantity` é™åˆ¶ã€‚
- ä¸å†å—已发货数量限制。
- ä¸å†å—待审批发货数量限制。
- é”€å”®äº§å“å·²æ˜¾ç¤ºâ€œå·²å‘货”后,仍可以继续提交发货。
销售发货仍然必须满足库存校验:
```text
可用库存 = åº“存数量 - å†»ç»“库存 - å¾…出库数量
```
发货数量超过可用库存时,接口仍然返回失败。
## 2. é€šç”¨å“åº”
成功响应示例:
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": true
}
```
失败响应示例:
```json
{
  "code": 500,
  "msg": "下发失败,下发数量必须大于0",
  "data": null
}
```
## 3. ç”Ÿäº§è®¡åˆ’下发
### 3.1 æŽ¥å£åœ°å€
```text
POST /productionPlan/combine
```
### 3.2 è¯·æ±‚体
```json
{
  "ids": [101, 102],
  "totalAssignedQuantity": 500,
  "planCompleteTime": "2026-08-30"
}
```
字段说明:
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
| --- | --- | --- | --- |
| `ids` | `number[]` | æ˜¯ | è¦ä¸‹å‘的生产计划 ID é›†åˆï¼Œå»ºè®®å‰ç«¯åŽ»é‡ |
| `totalAssignedQuantity` | `number` | æ˜¯ | æœ¬æ¬¡æ€»ç”Ÿäº§æ•°é‡ï¼Œåªéœ€å¤§äºŽ 0,不再与需求剩余数量比较 |
| `planCompleteTime` | `string` | å¦ | è®¡åˆ’完成日期,格式 `yyyy-MM-dd` |
### 3.3 å‰ç«¯å¤„理要求
1. ä¸‹å‘数量输入框只校验大于 0,不要再设置“剩余需求总量”作为最大值。
2. ç”Ÿäº§è®¡åˆ’ `status=2` çš„记录允许再次勾选和下发。
3. ä¸è¦å› ä¸º `quantityIssued >= qtyRequired` ç¦ç”¨ä¸‹å‘按钮。
4. ä¸‹å‘成功后刷新生产计划列表和生产订单列表。
5. `quantityIssued` å¯èƒ½å¤§äºŽ `qtyRequired`,前端不要做超量截断或异常提示。
6. ä»ç„¶ç¦æ­¢é€‰æ‹©ä¸åŒäº§å“åž‹å·çš„计划进行合并下发,后端会再次校验。
### 3.4 ç”Ÿäº§è®¡åˆ’状态
| çŠ¶æ€å€¼ | å«ä¹‰ |
| --- | --- |
| `0` | æœªä¸‹å‘ |
| `1` | éƒ¨åˆ†ä¸‹å‘ |
| `2` | å·²ä¸‹å‘ |
注意:状态 `2` çŽ°åœ¨åªè¡¨ç¤ºåŽŸéœ€æ±‚æ•°é‡å·²å®Œæˆï¼Œä¸ä»£è¡¨è¯¥è®¡åˆ’ä¸èƒ½å†æ¬¡ä¸‹å‘ã€‚çŠ¶æ€ä¸º `2` çš„计划再次下发后,状态仍可能保持 `2`,同时 `quantityIssued` ä¼šç»§ç»­ç´¯è®¡ã€‚
### 3.5 è”调场景
#### åœºæ™¯ä¸€ï¼šä¸‹å‘数量超过需求数量
假设:
- éœ€æ±‚数量:100
- å·²ä¸‹å‘数量:100
- æœ¬æ¬¡ä¸‹å‘数量:20
请求:
```json
{
  "ids": [101],
  "totalAssignedQuantity": 20
}
```
预期:
- è¯·æ±‚成功。
- æ–°å»ºç”Ÿäº§è®¢å•数量为 20。
- ç”Ÿäº§è®¡åˆ’ `quantityIssued` æ›´æ–°ä¸º 120。
- ä¸è¿”回“不能大于计划剩余需求总量”。
#### åœºæ™¯äºŒï¼šä¸€æ¬¡é€‰æ‹©å¤šä¸ªåŒåž‹å·è®¡åˆ’
```json
{
  "ids": [101, 102, 103],
  "totalAssignedQuantity": 500
}
```
预期生产订单的总数量为 500,不再根据各计划需求数量截断。
#### åœºæ™¯ä¸‰ï¼šé€‰æ‹©ä¸åŒåž‹å·è®¡åˆ’
请求仍然失败,提示所选生产计划的产品型号不一致。该校验没有取消。
## 4. é”€å”®äº§å“åˆ—表
销售产品列表中的 `noQuantity` ä¸å†è¡¨ç¤ºé”€å”®è®¢å•剩余可发数量。
后端返回:
```json
{
  "quantity": 10,
  "shippedQuantity": 10,
  "pendingApprovalQuantity": 0,
  "noQuantity": null
}
```
字段说明:
| å­—段 | è¯´æ˜Ž |
| --- | --- |
| `quantity` | é”€å”®äº§å“åŽŸå§‹æ•°é‡ï¼Œä»…ç”¨äºŽé”€å”®ä¸šåŠ¡å±•ç¤ºï¼Œä¸ä½œä¸ºå‘è´§æ•°é‡ä¸Šé™ |
| `shippedQuantity` | å·²å‘货数量,仅用于统计展示 |
| `pendingApprovalQuantity` | å¾…审核或审核中的发货数量,仅用于统计展示 |
| `noQuantity` | ä¸å†æä¾›é”€å”®å‰©ä½™å¯å‘数量,值为 `null` |
前端注意:
- ä¸è¦ä½¿ç”¨ `noQuantity` ä½œä¸ºå‘货数量输入框的 `max`。
- ä¸è¦å› ä¸º `noQuantity <= 0` ç¦ç”¨å‘货按钮。
- ä¸è¦ç”¨ `quantity - shippedQuantity - pendingApprovalQuantity` é‡æ–°è®¡ç®—发货上限。
- å‘货数量输入框的最大值应由实际库存信息或用户选择的库存批次决定。
- `shippingStatus` ä»å¯ç”¨äºŽå±•示,但不能作为是否允许继续发货的硬性条件。
## 5. æ–°å¢žé”€å”®å‘è´§
### 5.1 æŽ¥å£åœ°å€
```text
POST /shippingInfo/add
```
### 5.2 è¯·æ±‚体示例
```json
{
  "salesLedgerId": 2001,
  "salesLedgerProductId": 3001,
  "type": "销售发货",
  "batchNoDetailList": [
    {
      "productModelId": 501,
      "batchNo": "BATCH-20260826-001",
      "quantity": 30
    }
  ],
  "storageBlobDTOs": []
}
```
字段说明:
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
| --- | --- | --- | --- |
| `salesLedgerId` | `number` | æŒ‰ä¸šåŠ¡ | é”€å”®å°è´¦ ID |
| `salesLedgerProductId` | `number` | å»ºè®®ä¼ å…¥ | é”€å”®å°è´¦äº§å“ ID,用于关联销售产品及重复提交校验 |
| `type` | `string` | å¦ | å‘货类型 |
| `batchNoDetailList` | `array` | æ˜¯ | å‘货批次明细 |
| `batchNoDetailList[].productModelId` | `number` | æ˜¯ | äº§å“åž‹å· ID |
| `batchNoDetailList[].batchNo` | `string` | æ˜¯ | åº“存批次号 |
| `batchNoDetailList[].quantity` | `number` | æ˜¯ | æœ¬æ‰¹æ¬¡å‘货数量 |
| `storageBlobDTOs` | `array` | å¦ | é™„件列表,没有附件时传空数组 |
### 5.3 é‡è¦è§„则
1. å‘货数量可以大于销售产品剩余数量。
2. æ¯ä¸ªæ‰¹æ¬¡éƒ½å¿…须存在库存记录。
3. æ¯ä¸ªæ‰¹æ¬¡çš„发货数量不能超过该批次可用库存。
4. å¯ç”¨åº“存会扣除冻结库存和已有待出库数量。
5. åŒä¸€é”€å”®äº§å“å·²æœ‰â€œå¾…审核”或“审核中”发货记录时,仍不允许重复提交。
6. æ–°å¢žæˆåŠŸåŽï¼Œå‘è´§å•è¿›å…¥å®¡æ‰¹æµç¨‹ï¼Œé€šå¸¸çŠ¶æ€ä¸ºâ€œå®¡æ ¸ä¸­â€ã€‚
库存不足时可能返回:
```text
库存记录不存在
```
或:
```text
申请数量超过可用库存,当前可用库存为:xxx
```
### 5.4 è”调场景
#### åœºæ™¯ä¸€ï¼šé”€å”®æ•°é‡å·²å‘完,但库存充足
假设:
- é”€å”®æ•°é‡ï¼š10
- å·²å‘货数量:10
- å½“前批次可用库存:30
- æœ¬æ¬¡å‘货数量:20
预期:
- `/shippingInfo/add` è¯·æ±‚允许提交。
- ä¸å› ä¸ºé”€å”®æ•°é‡å·²ç”¨å®Œè€Œå¤±è´¥ã€‚
- åŽç»­ä»æŒ‰ç…§å®¡æ‰¹æµç¨‹å¤„理。
#### åœºæ™¯äºŒï¼šé”€å”®æ•°é‡æœªå‘完,但库存不足
假设:
- é”€å”®å‰©ä½™æ•°é‡ï¼š100
- å½“前批次可用库存:5
- æœ¬æ¬¡å‘货数量:6
预期:
- è¯·æ±‚失败。
- è¿”回库存不足信息。
- ä¸åº”认为销售剩余数量足够就允许发货。
#### åœºæ™¯ä¸‰ï¼šå¤šä¸ªåº“存批次
```json
{
  "salesLedgerProductId": 3001,
  "batchNoDetailList": [
    {
      "productModelId": 501,
      "batchNo": "BATCH-A",
      "quantity": 10
    },
    {
      "productModelId": 501,
      "batchNo": "BATCH-B",
      "quantity": 15
    }
  ]
}
```
每个批次独立校验库存;任意一个批次库存不足时,整次请求失败并回滚。
## 6. å®¡æ‰¹é€šè¿‡åŽç¡®è®¤å‘è´§
### 6.1 æŽ¥å£åœ°å€
```text
POST /shippingInfo/deductStock
```
### 6.2 è¯·æ±‚体示例
```json
{
  "id": 4001,
  "expressCompany": "顺丰",
  "expressNumber": "SF1234567890",
  "shippingCarNumber": "苏A12345",
  "shippingDate": "2026-08-26"
}
```
说明:
- `id` ä¸ºå‘货单 ID,必填。
- æŽ¥å£ä¼šå†æ¬¡æŒ‰å‘货明细校验库存。
- æˆåŠŸåŽå‘è´§å•çŠ¶æ€æ›´æ–°ä¸ºâ€œå·²å‘è´§â€ã€‚
- å½“前接口不会根据销售产品原始数量限制发货。
- å·²ç»æ˜¯â€œå·²å‘货”状态时,不会重复扣减库存。
## 7. å‰ç«¯è”调检查清单
- [ ] ç”Ÿäº§è®¡åˆ’下发数量输入框不再绑定需求剩余量最大值。
- [ ] çŠ¶æ€ä¸º `2` çš„生产计划仍可选择下发。
- [ ] ç”Ÿäº§è®¡åˆ’下发成功后允许 `quantityIssued > qtyRequired` å±•示。
- [ ] é”€å”®äº§å“åˆ—表不使用 `noQuantity` é™åˆ¶å‘货数量。
- [ ] `noQuantity=null` æ—¶é¡µé¢ä¸æ˜¾ç¤ºä¸ºâ€œä¸å¯å‘货”。
- [ ] é”€å”®äº§å“çŠ¶æ€â€œå·²å‘è´§â€ä¸å†ç›´æŽ¥ç¦ç”¨å‘è´§æ“ä½œã€‚
- [ ] å‘货数量改由库存批次可用量控制。
- [ ] åº“存不足时正确展示后端错误信息。
- [ ] åŒä¸€é”€å”®äº§å“å¾…审核/审核中重复提交仍按后端提示处理。
- [ ] ç”Ÿäº§è®¡åˆ’、销售发货提交成功后刷新列表数据。
sql/product-inventory-management-new-pro.sql
@@ -4895,6 +4895,7 @@
  `company_phone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '公司电话',
  `bank_account_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '开户行',
  `bank_account_num` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '账号',
  `bank_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '银联行号',
  `contact_user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '联系人',
  `contact_user_phone` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '联系电话',
  `maintain_user_id` bigint NULL DEFAULT NULL COMMENT '维护人ID',
src/main/java/com/ruoyi/account/mapper/purchase/AccountPurchasePaymentMapper.java
@@ -26,4 +26,6 @@
    IPage<AccountPurchasePaymentVo> listPageAccountPurchasePayment(Page page, @Param("req") AccountPurchasePaymentDto accountPurchasePaymentDto);
    List<IncomeExpenseAnalysisDto> selectPayment(@Param("startStr") String startStr, @Param("endStr") String endStr, @Param("dateFormat") String dateFormat);
    List<Long> selectStockInRecordIdsByPurchaseLedgerId(@Param("purchaseLedgerId") Long purchaseLedgerId);
}
src/main/java/com/ruoyi/account/mapper/sales/AccountSalesCollectionMapper.java
@@ -32,4 +32,6 @@
    List<SalesOutboundVo> getOutboundBatchesByCustomer(@Param("customerId") Integer customerId);
    List<IncomeExpenseAnalysisDto> selectIncomeStats(@Param("startStr") String startStr, @Param("endStr") String endStr, @Param("dateFormat") String dateFormat);
    List<Long> selectStockOutRecordIdsBySalesLedgerId(@Param("salesLedgerId") Long salesLedgerId);
}
src/main/java/com/ruoyi/account/service/impl/purchase/AccountPurchasePaymentServiceImpl.java
@@ -1,10 +1,11 @@
package com.ruoyi.account.service.impl.purchase;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.xiaoymin.knife4j.core.util.CollectionUtils;
import com.ruoyi.account.bean.dto.purchase.AccountPurchasePaymentDto;
import com.ruoyi.account.bean.vo.purchase.AccountPurchasePaymentVo;
import com.ruoyi.account.mapper.AccountStatementDetailsMapper;
@@ -17,15 +18,20 @@
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.purchase.dto.SavePaymentDto;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
/**
 * <p>
@@ -43,6 +49,7 @@
    private final AccountPurchasePaymentMapper accountPurchasePaymentMapper;
    private final AccountStatementDetailsMapper accountStatementDetailsMapper;
    private final AccountPaymentApplicationMapper accountPaymentApplicationMapper;
    private final PurchaseLedgerMapper purchaseLedgerMapper;
    @Override
    public IPage<AccountPurchasePaymentVo> listPageAccountPurchasePayment(Page page, AccountPurchasePaymentDto accountPurchasePaymentDto) {
@@ -84,6 +91,60 @@
        return removeByIds(ids);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean savePayment(SavePaymentDto dto) {
        if (dto.getPaymentDate() == null) {
            throw new ServiceException("请选择付款日期");
        }
        if (dto.getPaymentAmount() == null || dto.getPaymentAmount().compareTo(BigDecimal.valueOf(0.01)) < 0) {
            throw new ServiceException("请输入付款金额");
        }
        if (StringUtils.isEmpty(dto.getPurchaseContractNumber())) {
            throw new ServiceException("采购合同号不能为空");
        }
        if (dto.getSupplierId() == null) {
            throw new ServiceException("供应商不能为空");
        }
        PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectOne(new LambdaQueryWrapper<PurchaseLedger>()
                .eq(PurchaseLedger::getPurchaseContractNumber, dto.getPurchaseContractNumber()));
        if (purchaseLedger == null) {
            throw new ServiceException("采购合同不存在");
        }
        if (purchaseLedger.getSupplierId() == null || purchaseLedger.getSupplierId().longValue() != dto.getSupplierId().longValue()) {
            throw new ServiceException("供应商与合同不匹配");
        }
        List<Long> stockInRecordIds = accountPurchasePaymentMapper.selectStockInRecordIdsByPurchaseLedgerId(purchaseLedger.getId());
        AccountPaymentApplication application = new AccountPaymentApplication();
        application.setSupplierId(dto.getSupplierId().intValue());
        application.setStockInRecordIds(stockInRecordIds.stream().map(String::valueOf).collect(Collectors.joining(",")));
        application.setInvoiceApplicationNo(genPaymentApplicationNo());
        application.setPaymentMethod(dto.getPaymentMethod());
        application.setPaymentContent(dto.getPurchaseContractNumber());
        application.setApplyDate(dto.getPaymentDate());
        application.setRemark(dto.getRemark());
        application.setStatus(1);
        application.setPaymentAmount(dto.getPaymentAmount());
        accountPaymentApplicationMapper.insert(application);
        AccountPurchasePayment payment = new AccountPurchasePayment();
        payment.setAccountPaymentApplicationId(application.getId());
        payment.setSupplierId(dto.getSupplierId().intValue());
        payment.setPaymentDate(dto.getPaymentDate());
        payment.setPaymentMethod(dto.getPaymentMethod());
        payment.setPaymentAmount(dto.getPaymentAmount());
        payment.setPaymentNumber(genAccountPurchasePaymentNo());
        payment.setRemark(dto.getRemark());
        return save(payment);
    }
    private String genPaymentApplicationNo() {
        return "FK" + LocalDateTime.now().format(CODE_TIME_FORMATTER) + new Random().nextInt(10);
    }
    private String genAccountPurchasePaymentNo() {
        return "SK" + LocalDateTime.now().format(CODE_TIME_FORMATTER) + new Random().nextInt(10);
    }
src/main/java/com/ruoyi/account/service/impl/sales/AccountSalesCollectionServiceImpl.java
@@ -1,5 +1,6 @@
package com.ruoyi.account.service.impl.sales;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -16,13 +17,16 @@
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.sales.dto.SaveReceiptPaymentDto;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.pojo.SalesLedger;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
@@ -40,6 +44,7 @@
    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
    private final AccountStatementDetailsMapper accountStatementDetailsMapper;
    private final SalesLedgerMapper salesLedgerMapper;
    private static final DateTimeFormatter CODE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyMMddHHmmss");
@@ -90,6 +95,43 @@
        return accountSalesCollectionMapper.getOutboundBatchesByCustomer(customerId);
    }
    @Override
    public boolean saveReceiptPayment(SaveReceiptPaymentDto dto) {
        if (dto.getPaymentDate() == null) {
            throw new ServiceException("请选择回款日期");
        }
        if (dto.getPaymentAmount() == null || dto.getPaymentAmount().compareTo(BigDecimal.valueOf(0.01)) < 0) {
            throw new ServiceException("请输入回款金额");
        }
        if (StringUtils.isEmpty(dto.getSalesContractNo())) {
            throw new ServiceException("销售合同号不能为空");
        }
        if (dto.getCustomerId() == null) {
            throw new ServiceException("客户不能为空");
        }
        SalesLedger salesLedger = salesLedgerMapper.selectOne(new LambdaQueryWrapper<SalesLedger>()
                .eq(SalesLedger::getSalesContractNo, dto.getSalesContractNo()));
        if (salesLedger == null) {
            throw new ServiceException("销售合同不存在");
        }
        if (salesLedger.getCustomerId() == null || salesLedger.getCustomerId().longValue() != dto.getCustomerId().longValue()) {
            throw new ServiceException("客户与合同不匹配");
        }
        List<Long> stockOutRecordIds = accountSalesCollectionMapper.selectStockOutRecordIdsBySalesLedgerId(salesLedger.getId());
        AccountSalesCollection collection = new AccountSalesCollection();
        collection.setCustomerId(dto.getCustomerId().intValue());
        collection.setStockOutRecordIds(stockOutRecordIds.stream().map(String::valueOf).collect(java.util.stream.Collectors.joining(",")));
        collection.setCollectionDate(dto.getPaymentDate());
        collection.setCollectionAmount(dto.getPaymentAmount());
        collection.setCollectionMethod(dto.getPaymentMethod());
        collection.setCollectionNumber(genAccountSalesCollectionNo());
        collection.setRemark(dto.getRemark());
        return save(collection);
    }
    private String genAccountSalesCollectionNo() {
        return "SK" + LocalDateTime.now().format(CODE_TIME_FORMATTER) + new Random().nextInt(10);
    }
src/main/java/com/ruoyi/account/service/purchase/AccountPurchasePaymentService.java
@@ -5,6 +5,7 @@
import com.ruoyi.account.bean.dto.purchase.AccountPurchasePaymentDto;
import com.ruoyi.account.bean.vo.purchase.AccountPurchasePaymentVo;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
import com.ruoyi.purchase.dto.SavePaymentDto;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.servlet.http.HttpServletResponse;
@@ -27,4 +28,12 @@
    void exportAccountPurchasePayment(HttpServletResponse response, AccountPurchasePaymentDto accountPurchasePaymentDto);
    boolean deleteAccountPurchasePayment(List<Long> ids);
    /**
     * é‡‡è´­ç®¡ç†--供应商往来 æ–°å¢žä»˜æ¬¾
     *
     * @param dto ä»˜æ¬¾è¯·æ±‚参数
     * @return æ˜¯å¦æˆåŠŸ
     */
    boolean savePayment(SavePaymentDto dto);
}
src/main/java/com/ruoyi/account/service/sales/AccountSalesCollectionService.java
@@ -6,6 +6,7 @@
import com.ruoyi.account.bean.vo.sales.AccountSalesCollectionVo;
import com.ruoyi.account.bean.vo.sales.SalesOutboundVo;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.sales.dto.SaveReceiptPaymentDto;
import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.servlet.http.HttpServletResponse;
@@ -30,4 +31,12 @@
    boolean deleteAccountSalesCollection(List<Long> ids);
    List<SalesOutboundVo> getOutboundBatchesByCustomer(Integer customerId);
    /**
     * è¥é”€ç®¡ç†--客户往来 æ–°å¢žå›žæ¬¾
     *
     * @param dto å›žæ¬¾è¯·æ±‚参数
     * @return æ˜¯å¦æˆåŠŸ
     */
    boolean saveReceiptPayment(SaveReceiptPaymentDto dto);
}
src/main/java/com/ruoyi/basic/excel/SupplierManageExcelDto.java
@@ -30,6 +30,9 @@
    @Excel(name = "账号")
    private String bankAccountNum;
    @Excel(name = "银联行号")
    private String bankCode;
    @Excel(name = "联系人")
    private String contactUserName;
src/main/java/com/ruoyi/basic/pojo/SupplierManage.java
@@ -40,6 +40,10 @@
    @Excel(name = "账号")
    private String bankAccountNum;
    @Schema(description = "银联行号")
    @Excel(name = "银联行号")
    private String bankCode;
    @Schema(description = "联系人")
    @Excel(name = "联系人")
    private String contactUserName;
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -666,9 +666,6 @@
        if (productModelIds.size() > 1) {
            throw new ServiceException("所选生产计划必须属于同一产品规格");
        }
        if (productionPlans.stream().anyMatch(item -> item.getStatus() != null && item.getStatus() == 2)) {
            throw new ServiceException("所选生产计划已下发");
        }
        ProductionPlan firstPlan = mainPlan;
        if (productionOrder.getProductModelId() == null) {
            productionOrder.setProductModelId(firstPlan.getProductModelId());
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java
@@ -61,9 +61,8 @@
     * åˆå¹¶ç”Ÿäº§è®¡åˆ’并下发生产订单。
     * çº¦æŸï¼š
     * 1. ä»…允许同一产品型号的计划合并;
     * 2. å·²ä¸‹å‘或部分下发的计划不允许再次合并;
     * 3. ä¸‹å‘数量不能大于所选计划剩余需求总量;
     * 4. ä¸‹å‘时统一调用 ProductionOrderService.saveProductionOrder,确保后续工艺/BOM/领料逻辑一致。
     * 2. ä¸‹å‘数量只受输入数量和基础数据校验约束;
     * 3. ä¸‹å‘时统一调用 ProductionOrderService.saveProductionOrder,确保后续工艺/BOM/领料逻辑一致。
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -101,49 +100,18 @@
            throw new BaseException("合并失败,所选生产计划的产品型号不一致");
        }
        // ä»…“已下发”计划不允许再次参与合并下发;
        // â€œå¾…下发/部分下发”允许继续下发剩余数量。
        boolean hasFullyIssuedPlan = planLists.stream()
                .anyMatch(item -> item.getStatus() != null
                        && item.getStatus() == PLAN_STATUS_ISSUED);
        if (hasFullyIssuedPlan) {
            throw new BaseException("合并失败,所选生产计划存在已下发的数据");
        }
        // è®¡ç®—本次可下发的剩余需求总量
        BigDecimal totalRequiredQuantity = planLists.stream()
                .map(this::resolveRemainingQuantity)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        if (totalRequiredQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("下发失败,所选生产计划剩余需求总量必须大于0");
        }
        // æ ¡éªŒä¸‹å‘数量
        BigDecimal assignedQuantity = productionPlanDto.getTotalAssignedQuantity();
        if (assignedQuantity == null || assignedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            throw new ServiceException("下发失败,下发数量必须大于0");
        }
        if (assignedQuantity.compareTo(totalRequiredQuantity) > 0) {
            throw new ServiceException("下发失败,下发数量不能大于计划剩余需求总量");
        }
        // æŒ‰è®¡åˆ’顺序分摊下发数量,收集实际参与下发的计划 ID
        BigDecimal remainingForOrderBind = assignedQuantity;
        List<Long> issuedPlanIds = new ArrayList<>();
        for (ProductionPlanDto plan : planLists) {
            BigDecimal remainingQuantity = resolveRemainingQuantity(plan);
            if (remainingForOrderBind.compareTo(BigDecimal.ZERO) <= 0 || remainingQuantity.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal issueForThisPlan = remainingForOrderBind.min(remainingQuantity);
            remainingForOrderBind = remainingForOrderBind.subtract(issueForThisPlan);
            if (issueForThisPlan.compareTo(BigDecimal.ZERO) > 0) {
                issuedPlanIds.add(plan.getId());
            }
        }
        if (issuedPlanIds.isEmpty()) {
            throw new ServiceException("下发失败,无可下发数量");
        }
        // æœ¬æ¬¡ä¸‹å‘数量不再按需求量限制,所有选中计划共同作为订单来源
        List<Long> issuedPlanIds = planLists.stream()
                .map(ProductionPlanDto::getId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        // ç”Ÿæˆç”Ÿäº§è®¢å•主单,并绑定本次下发关联的计划
        ProductionOrder productionOrder = new ProductionOrder();
@@ -155,24 +123,18 @@
            throw new ServiceException("下发失败,生产订单保存失败");
        }
        // å›žå†™æ¯æ¡è®¡åˆ’的累计下发量和状态
        // å›žå†™æ¯æ¡è®¡åˆ’的累计下发量和状态,允许累计下发量超过需求量
        BigDecimal remainingAssignedQuantity = assignedQuantity;
        List<ProductionPlan> updates = new ArrayList<>();
        for (ProductionPlanDto plan : planLists) {
            BigDecimal requiredQuantity = Optional.ofNullable(plan.getQtyRequired()).orElse(BigDecimal.ZERO);
            if (requiredQuantity.compareTo(BigDecimal.ZERO) < 0) {
                requiredQuantity = BigDecimal.ZERO;
            }
            BigDecimal remainingQuantity = resolveRemainingQuantity(plan);
            BigDecimal historicalIssuedQuantity = requiredQuantity.subtract(remainingQuantity);
            BigDecimal issuedQuantity = BigDecimal.ZERO;
            if (remainingAssignedQuantity.compareTo(BigDecimal.ZERO) > 0 && remainingQuantity.compareTo(BigDecimal.ZERO) > 0) {
                issuedQuantity = remainingAssignedQuantity.min(remainingQuantity);
                remainingAssignedQuantity = remainingAssignedQuantity.subtract(issuedQuantity);
            }
            BigDecimal historicalIssuedQuantity = Optional.ofNullable(plan.getQuantityIssued()).orElse(BigDecimal.ZERO);
            BigDecimal issuedQuantity = remainingAssignedQuantity.min(assignedQuantity);
            remainingAssignedQuantity = remainingAssignedQuantity.subtract(issuedQuantity);
            BigDecimal totalIssuedQuantity = historicalIssuedQuantity.add(issuedQuantity);
            int planStatus = resolvePlanStatus(requiredQuantity, totalIssuedQuantity);
            int planStatus = plan.getStatus() != null && plan.getStatus() == PLAN_STATUS_ISSUED
                    ? PLAN_STATUS_ISSUED
                    : resolvePlanStatus(plan.getQtyRequired(), totalIssuedQuantity);
            ProductionPlan update = new ProductionPlan();
            update.setId(plan.getId());
            update.setStatus(planStatus);
@@ -455,32 +417,6 @@
            }
        }
        return sequence;
    }
    /**
     * è®¡ç®—生产计划的剩余未下发数量(需求量 - å·²ä¸‹å‘量,最小为 0)。
     */
    private BigDecimal resolveRemainingQuantity(ProductionPlan plan) {
        // ç©ºå¯¹è±¡æŒ‰ 0 å¤„理
        if (plan == null) {
            return BigDecimal.ZERO;
        }
        // éœ€æ±‚量为空或小于等于 0,视为无剩余
        BigDecimal requiredQuantity = Optional.ofNullable(plan.getQtyRequired()).orElse(BigDecimal.ZERO);
        if (requiredQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        // å·²ä¸‹å‘量为空或小于等于 0,剩余即需求量
        BigDecimal issuedQuantity = Optional.ofNullable(plan.getQuantityIssued()).orElse(BigDecimal.ZERO);
        if (issuedQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return requiredQuantity;
        }
        // å·²ä¸‹å‘量大于等于需求量,剩余归零
        if (issuedQuantity.compareTo(requiredQuantity) >= 0) {
            return BigDecimal.ZERO;
        }
        // æ­£å¸¸åœºæ™¯è¿”回差值
        return requiredQuantity.subtract(issuedQuantity);
    }
    /**
src/main/java/com/ruoyi/purchase/controller/AccountingReportController.java
@@ -1,11 +1,13 @@
package com.ruoyi.purchase.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.service.purchase.AccountPurchasePaymentService;
import com.ruoyi.basic.service.ISupplierService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.purchase.dto.SavePaymentDto;
import com.ruoyi.purchase.dto.VatDto;
import com.ruoyi.purchase.service.PurchaseReportService;
import com.ruoyi.purchase.vo.PurchaseReportVo;
@@ -15,6 +17,7 @@
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -28,6 +31,7 @@
    private final ISupplierService supplierService;
    private final PurchaseReportService purchaseReportService;
    private final AccountPurchasePaymentService accountPurchasePaymentService;
    @GetMapping("/list")
@@ -74,4 +78,11 @@
    public R supplierTransactionsDetails(Page page, Long supplierId) {
        return R.ok(supplierService.supplierTransactionsDetails(page,supplierId));
    }
    @PostMapping("/savePayment")
    @Log(title = "新增付款", businessType = BusinessType.INSERT)
    @Operation(summary = "新增付款")
    public R savePayment(@RequestBody SavePaymentDto dto) {
        return R.ok(accountPurchasePaymentService.savePayment(dto));
    }
}
src/main/java/com/ruoyi/purchase/dto/SavePaymentDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package com.ruoyi.purchase.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * é‡‡è´­ç®¡ç†--供应商往来 æ–°å¢žä»˜æ¬¾è¯·æ±‚参数
 */
@Data
@Schema(name = "SavePaymentDto", description = "新增付款请求参数")
public class SavePaymentDto {
    @Schema(description = "供应商ID")
    private Long supplierId;
    @Schema(description = "采购合同号")
    private String purchaseContractNumber;
    @Schema(description = "付款日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate paymentDate;
    @Schema(description = "付款金额")
    private BigDecimal paymentAmount;
    @Schema(description = "付款方式")
    private String paymentMethod;
    @Schema(description = "备注")
    private String remark;
}
src/main/java/com/ruoyi/sales/controller/MetricStatisticsController.java
@@ -1,18 +1,22 @@
package com.ruoyi.sales.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.account.service.sales.AccountSalesCollectionService;
import com.ruoyi.basic.service.ICustomerService;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.sales.dto.SaveReceiptPaymentDto;
import com.ruoyi.sales.dto.StatisticsTableDto;
import com.ruoyi.sales.service.impl.MetricStatisticsServiceImpl;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -28,6 +32,7 @@
    private final MetricStatisticsServiceImpl metricStatisticsService;
    private final ICustomerService customerService;
    private final AccountSalesCollectionService accountSalesCollectionService;
    @Operation(summary = "头部总计")
    @GetMapping("/total")
@@ -55,4 +60,11 @@
        return R.ok(customerService.customewTransactionsDetails(page,customerId));
    }
    @PostMapping("/saveReceiptPayment")
    @Log(title = "新增回款", businessType = BusinessType.INSERT)
    @Operation(summary = "新增回款")
    public R saveReceiptPayment(@RequestBody SaveReceiptPaymentDto dto) {
        return R.ok(accountSalesCollectionService.saveReceiptPayment(dto));
    }
}
src/main/java/com/ruoyi/sales/dto/SaveReceiptPaymentDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package com.ruoyi.sales.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * è¥é”€ç®¡ç†--客户往来 æ–°å¢žå›žæ¬¾è¯·æ±‚参数
 */
@Data
@Schema(name = "SaveReceiptPaymentDto", description = "新增回款请求参数")
public class SaveReceiptPaymentDto {
    @Schema(description = "客户ID")
    private Long customerId;
    @Schema(description = "销售合同号")
    private String salesContractNo;
    @Schema(description = "回款日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate paymentDate;
    @Schema(description = "回款金额")
    private BigDecimal paymentAmount;
    @Schema(description = "回款方式")
    private String paymentMethod;
    @Schema(description = "备注")
    private String remark;
}
src/main/resources/application-dev.yml
@@ -74,7 +74,7 @@
    druid:
      # ä¸»åº“数据源
      master:
        url: jdbc:mysql://localhost:3306/product-inventory-management-bdpro?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        url: jdbc:mysql://localhost:3306/product-inventory-management?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: 123456
      # ä»Žåº“数据源
src/main/resources/mapper/account/purchase/AccountPurchasePaymentMapper.xml
@@ -54,5 +54,13 @@
        GROUP BY dateStr
        ORDER BY dateStr
    </select>
    <select id="selectStockInRecordIdsByPurchaseLedgerId" resultType="java.lang.Long">
        SELECT sir.id
        FROM stock_in_record sir
        LEFT JOIN quality_inspect qi ON sir.record_type = 10 AND sir.record_id = qi.id
        WHERE sir.approval_status = 1
          AND sir.record_type IN ('7','10')
          AND IF(sir.record_type = 7, sir.record_id, qi.purchase_ledger_id) = #{purchaseLedgerId}
    </select>
</mapper>
src/main/resources/mapper/account/sales/AccountSalesCollectionMapper.xml
@@ -112,5 +112,13 @@
         GROUP BY dateStr
         ORDER BY dateStr
    </select>
    <select id="selectStockOutRecordIdsBySalesLedgerId" resultType="java.lang.Long">
        SELECT sor.id
        FROM stock_out_record sor
        JOIN shipping_info s ON sor.record_id = s.id
        WHERE s.sales_ledger_id = #{salesLedgerId}
          AND sor.record_type = '13'
          AND sor.approval_status = 1
    </select>
</mapper>
src/main/resources/mapper/basic/CustomerMapper.xml
@@ -177,14 +177,20 @@
        left join (
            select
                sl.id,
                sum(ascc.collection_amount) as receiptPaymentAmount
            from account_sales_collection ascc
            left join stock_out_record sor on FIND_IN_SET(sor.id, ascc.stock_out_record_ids) > 0
            left join shipping_info s on sor.record_id = s.id
            LEFT JOIN sales_ledger sl ON s.sales_ledger_id = sl.id
            WHERE sor.record_type='13'
              and sor.approval_status=1
            group by  sl.id
                sum(t.collection_amount) as receiptPaymentAmount
            from (
                select
                    ascc.collection_amount,
                    s.sales_ledger_id
                from account_sales_collection ascc
                left join stock_out_record sor on FIND_IN_SET(sor.id, ascc.stock_out_record_ids) > 0
                left join shipping_info s on sor.record_id = s.id
                where sor.record_type='13'
                  and sor.approval_status=1
                group by ascc.id, s.sales_ledger_id
            ) t
            left join sales_ledger sl on t.sales_ledger_id = sl.id
            group by sl.id
        )T1 on T1.id = sl.id
        left join (
            select sl.id,
src/main/resources/mapper/basic/SupplierManageMapper.xml
@@ -13,6 +13,7 @@
        T1.company_phone,
        T1.bank_account_name,
        T1.bank_account_num,
        T1.bank_code,
        T1.contact_user_name,
        T1.contact_user_phone,
        T1.maintain_user_id,
@@ -47,6 +48,7 @@
        T1.company_phone,
        T1.bank_account_name,
        T1.bank_account_num,
        T1.bank_code,
        T1.contact_user_name,
        T1.contact_user_phone,
        T1.maintain_user_id,
@@ -106,17 +108,25 @@
       left join (
           select
               pl.id,
               sum(app.payment_amount) as paymentAmount
           from account_purchase_payment app
           left join account_payment_application apa on app.account_payment_application_id = apa.id
           left join stock_in_record sir on FIND_IN_SET(sir.id, apa.stock_in_record_ids) > 0
               -- 10 ç±»åž‹æ‰å…³è”质检表
           LEFT JOIN quality_inspect qi ON sir.record_type = 10 AND sir.record_id = qi.id
               -- åŠ¨æ€å…³è”é‡‡è´­ï¼ˆè‡ªåŠ¨é€‚é… 7 å’Œ 10)
           LEFT JOIN purchase_ledger pl
                     ON pl.id = IF(sir.record_type = 7, sir.record_id, qi.purchase_ledger_id)
           WHERE sir.approval_status = 1
             AND sir.record_type IN ('7','10')
               sum(t.payment_amount) as paymentAmount
           from (
               select
                   app.id,
                   app.payment_amount,
                   pl.id as purchase_ledger_id
               from account_purchase_payment app
               left join account_payment_application apa on app.account_payment_application_id = apa.id
               left join stock_in_record sir on FIND_IN_SET(sir.id, apa.stock_in_record_ids) > 0
                   -- 10 ç±»åž‹æ‰å…³è”质检表
               LEFT JOIN quality_inspect qi ON sir.record_type = 10 AND sir.record_id = qi.id
                   -- åŠ¨æ€å…³è”é‡‡è´­ï¼ˆè‡ªåŠ¨é€‚é… 7 å’Œ 10)
               LEFT JOIN purchase_ledger pl
                         ON pl.id = IF(sir.record_type = 7, sir.record_id, qi.purchase_ledger_id)
               WHERE sir.approval_status = 1
                 AND sir.record_type IN ('7','10')
               group by app.id, pl.id
           ) t
           left join purchase_ledger pl on t.purchase_ledger_id = pl.id
           group by pl.id
       )T1 on T1.id = pl.id
       left join (
src/main/resources/mapper/sales/SalesLedgerProductMapper.xml
@@ -33,7 +33,7 @@
        WHEN (IFNULL(t2.qualitity, 0) - IFNULL(t2.locked_quantity, 0)) >0 THEN 1
        ELSE 0
        END as has_sufficient_stock,
        (IFNULL(T1.quantity, 0) - IFNULL(t3.shipped_quantity, 0) - IFNULL(t5.pending_approval_quantity, 0)) as no_quantity,
        NULL as no_quantity,
        IFNULL(t3.shipped_quantity, 0) as shipped_quantity,
        IFNULL(t6.return_quantity, 0) as return_quantity,
        IFNULL(t4.approved_stock_in_num, 0) as approved_stock_in_num,