已添加5个文件
已修改25个文件
1198 ■■■■ 文件已修改
.mcp.json 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260826_生产计划与销售发货数量规则前端联调文档.md 334 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/product-inventory-management-new-pro.sql 2 ●●●●● 补丁 | 查看 | 原始文档 | 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/approve/service/impl/ApprovalTemplateServiceImpl.java 17 ●●●● 补丁 | 查看 | 原始文档 | 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/ProductionBomStructureServiceImpl.java 80 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 65 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java 94 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/system/service/impl/SysUserServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | 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/java/com/ruoyi/sales/pojo/ShippingInfo.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-lt.yml 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application.yml 6 ●●●● 补丁 | 查看 | 原始文档 | 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 | 历史
src/main/resources/mapper/sales/ShippingInfoMapper.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,334 @@
# ç”Ÿäº§è®¡åˆ’与销售发货数量规则前端联调文档
更新时间: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": "销售发货",
  "shippingAddress": "江苏省南通市崇川区示例路 100 å·",
  "batchNoDetailList": [
    {
      "productModelId": 501,
      "batchNo": "BATCH-20260826-001",
      "quantity": 30
    }
  ],
  "storageBlobDTOs": []
}
```
字段说明:
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
| --- | --- | --- | --- |
| `salesLedgerId` | `number` | æŒ‰ä¸šåŠ¡ | é”€å”®å°è´¦ ID |
| `salesLedgerProductId` | `number` | å»ºè®®ä¼ å…¥ | é”€å”®å°è´¦äº§å“ ID,用于关联销售产品及重复提交校验 |
| `type` | `string` | å¦ | å‘货类型 |
| `shippingAddress` | `string` | å¦ | å‘货地址,最长 500 ä¸ªå­—符 |
| `batchNoDetailList` | `array` | æ˜¯ | å‘货批次明细 |
| `batchNoDetailList[].productModelId` | `number` | æ˜¯ | äº§å“åž‹å· ID |
| `batchNoDetailList[].batchNo` | `string` | æ˜¯ | åº“存批次号 |
| `batchNoDetailList[].quantity` | `number` | æ˜¯ | æœ¬æ‰¹æ¬¡å‘货数量 |
| `storageBlobDTOs` | `array` | å¦ | é™„件列表,没有附件时传空数组 |
### 5.3 é‡è¦è§„则
1. å‘货数量可以大于销售产品剩余数量。
2. æ¯ä¸ªæ‰¹æ¬¡éƒ½å¿…须存在库存记录。
3. æ¯ä¸ªæ‰¹æ¬¡çš„发货数量不能超过该批次可用库存。
4. å¯ç”¨åº“存会扣除冻结库存和已有待出库数量。
5. åŒä¸€é”€å”®äº§å“å·²æœ‰â€œå¾…审核”或“审核中”发货记录时,仍不允许重复提交。
6. æ–°å¢žæˆåŠŸåŽï¼Œå‘è´§å•è¿›å…¥å®¡æ‰¹æµç¨‹ï¼Œé€šå¸¸çŠ¶æ€ä¸ºâ€œå®¡æ ¸ä¸­â€ã€‚
7. `shippingAddress` ä¼šä¿å­˜åˆ°å‘货单,并在发货列表及导出数据中返回,用于回显。
库存不足时可能返回:
```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
@@ -4416,6 +4416,7 @@
  `sales_ledger_id` bigint NULL DEFAULT NULL COMMENT '销售台账id',
  `shipping_date` datetime NULL DEFAULT NULL COMMENT '发货日期',
  `shipping_car_number` varchar(30) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin NULL DEFAULT NULL COMMENT '发货车牌号',
  `shipping_address` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '发货地址',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `create_user` bigint NULL DEFAULT NULL COMMENT '创建用户',
  `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
@@ -4895,6 +4896,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/approve/service/impl/ApprovalTemplateServiceImpl.java
@@ -85,8 +85,8 @@
    @Override
    public Boolean updateApprovalTemplateDto(ApprovalTemplateDto approvalTemplateDto) {
        // æ£€æŸ¥æ¨¡æ¿æ˜¯å¦è¢«å®¡æ‰¹å®žä¾‹å¼•用
        if (isTemplateReferenced(approvalTemplateDto.getId())) {
        // æ£€æŸ¥æ¨¡æ¿æ˜¯å¦è¢«è¿›è¡Œä¸­çš„审批实例引用
        if (hasPendingReference(approvalTemplateDto.getId())) {
            throw new RuntimeException("该审批模板已被审批流程引用,无法编辑");
        }
        approvalTemplateMapper.updateById(approvalTemplateDto);
@@ -305,4 +305,17 @@
        );
        return count != null && count > 0;
    }
    private boolean hasPendingReference(Long templateId) {
        if (templateId == null) {
            return false;
        }
        Long count = approvalInstanceMapper.selectCount(
                new LambdaQueryWrapper<ApprovalInstance>()
                        .eq(ApprovalInstance::getTemplateId, templateId)
                        .eq(ApprovalInstance::getDeleted, 0)
                        .eq(ApprovalInstance::getStatus, "PENDING")
        );
        return count != null && count > 0;
    }
}
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/ProductionBomStructureServiceImpl.java
@@ -31,6 +31,7 @@
import com.ruoyi.technology.pojo.TechnologyOperationParam;
import com.ruoyi.technology.pojo.TechnologyParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -49,6 +50,7 @@
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-04-21 03:55:52
 */
@Slf4j
@Service
@RequiredArgsConstructor()
public class ProductionBomStructureServiceImpl extends ServiceImpl<ProductionBomStructureMapper, ProductionBomStructure> implements ProductionBomStructureService {
@@ -137,6 +139,9 @@
            entity.setProductionOrderBomId(orderBomId);
            if (item.getId() == null) {
                entity.setParentId(null);
                // æ–°å¢žèŠ‚ç‚¹çš„éœ€æ±‚é‡ç”±åŽç«¯æŒ‰BOM树形结构重新计算,不清空可能导致前端传的值溢出
                entity.setDemandedQuantity(null);
                entity.setId(null);
                insertList.add(entity);
                tempEntityMap.put(item.getTempId(), entity);
            } else {
@@ -146,6 +151,11 @@
        // æ‰¹é‡æ–°å¢žï¼Œæ‹¿åˆ°æ•°æ®åº“生成的真实ID
        if (!insertList.isEmpty()) {
            for (ProductionBomStructure entity : insertList) {
                log.info("准备新增BOM结构节点: productModelId={}, technologyOperationId={}, unitQuantity={}, demandedQuantity={}, unit={}",
                        entity.getProductModelId(), entity.getTechnologyOperationId(),
                        entity.getUnitQuantity(), entity.getDemandedQuantity(), entity.getUnit());
            }
            this.saveBatch(insertList);
        }
@@ -216,24 +226,76 @@
        if (structureList == null || structureList.isEmpty()) {
            return;
        }
        List<ProductionBomStructure> updateList = new ArrayList<>();
        BigDecimal lastProcessDemandedQuantity = orderQuantity;
        log.info("开始按BOM树形结构计算需求量,订单数量={}, BOM节点总数={}", orderQuantity, structureList.size());
        // æž„建 id â†’ structure æ˜ å°„å’Œ parentId â†’ children æ˜ å°„
        Map<Long, ProductionBomStructure> structureById = new HashMap<>();
        Map<Long, List<ProductionBomStructure>> childrenMap = new LinkedHashMap<>();
        for (ProductionBomStructure structure : structureList) {
            if (structure == null || structure.getId() == null) {
                continue;
            }
            structureById.put(structure.getId(), structure);
            Long parentId = structure.getParentId();
            childrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(structure);
        }
            BigDecimal demandedQuantity = lastProcessDemandedQuantity.multiply(defaultDecimal(structure.getUnitQuantity()));
//            if (compareDecimal(structure.getDemandedQuantity(), demandedQuantity) == 0) {
//                continue;
//            }
        List<ProductionBomStructure> updateList = new ArrayList<>();
        // ä»Žæ ¹èŠ‚ç‚¹å¼€å§‹ï¼ŒæŒ‰æ ‘å½¢å±‚çº§é€å±‚è®¡ç®—éœ€æ±‚é‡
        // æ ¹èŠ‚ç‚¹ parentId ä¸º null æˆ– 0
        Deque<Long> queue = new ArrayDeque<>();
        List<ProductionBomStructure> rootNodes = childrenMap.get(null);
        if (rootNodes != null) {
            for (ProductionBomStructure root : rootNodes) {
                queue.add(root.getId());
            }
        }
        // parentId=0 çš„也视为根节点
        List<ProductionBomStructure> zeroParentNodes = childrenMap.get(0L);
        if (zeroParentNodes != null) {
            for (ProductionBomStructure root : zeroParentNodes) {
                if (!queue.contains(root.getId())) {
                    queue.add(root.getId());
                }
            }
        }
        while (!queue.isEmpty()) {
            Long nodeId = queue.poll();
            ProductionBomStructure node = structureById.get(nodeId);
            if (node == null) {
                continue;
            }
            // è®¡ç®—当前节点的需求数量 = çˆ¶èŠ‚ç‚¹éœ€æ±‚é‡ Ã— è‡ªèº« unitQuantity
            // æ ¹èŠ‚ç‚¹ï¼šéœ€æ±‚é‡ = è®¢å•数量 Ã— unitQuantity
            BigDecimal parentDemandedQuantity = orderQuantity;
            Long parentId = node.getParentId();
            if (parentId != null && parentId != 0L) {
                ProductionBomStructure parent = structureById.get(parentId);
                if (parent != null && parent.getDemandedQuantity() != null) {
                    parentDemandedQuantity = parent.getDemandedQuantity();
                }
            }
            BigDecimal demandedQuantity = parentDemandedQuantity.multiply(defaultDecimal(node.getUnitQuantity()));
            log.info("BOM需求量计算: nodeId={}, parentId={}, parentDemandedQuantity={}, unitQuantity={}, demandedQuantity={}",
                    node.getId(), node.getParentId(), parentDemandedQuantity,
                    defaultDecimal(node.getUnitQuantity()), demandedQuantity);
            ProductionBomStructure update = new ProductionBomStructure();
            update.setId(structure.getId());
            update.setId(node.getId());
            update.setDemandedQuantity(demandedQuantity);
            updateList.add(update);
            structure.setDemandedQuantity(demandedQuantity);
            lastProcessDemandedQuantity = demandedQuantity;
            node.setDemandedQuantity(demandedQuantity);
            // å°†å­èŠ‚ç‚¹åŠ å…¥é˜Ÿåˆ—
            List<ProductionBomStructure> children = childrenMap.get(nodeId);
            if (children != null) {
                for (ProductionBomStructure child : children) {
                    queue.add(child.getId());
                }
            }
        }
        if (!updateList.isEmpty()) {
            this.updateBatchById(updateList);
        }
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -439,9 +439,54 @@
        productionOrderBomMapper.insert(orderBom);
        Map<Long, Long> idMap = new HashMap<>();
        BigDecimal lastProcessDemandedQuantity = orderQuantity;
        // æž„建 sourceId â†’ sourceNode æ˜ å°„ å’Œ parentId â†’ children æ˜ å°„
        Map<Long, TechnologyBomStructure> sourceById = new HashMap<>();
        Map<Long, List<TechnologyBomStructure>> childrenMap = new LinkedHashMap<>();
        for (TechnologyBomStructure source : structureList) {
            // å­èŠ‚ç‚¹ parentId éœ€è¦æ˜ å°„成新快照节点 id,才能保留原始 BOM å±‚级。
            if (source == null || source.getId() == null) {
                continue;
            }
            sourceById.put(source.getId(), source);
            Long parentId = source.getParentId();
            childrenMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(source);
        }
        // BFS ä»Žæ ¹èŠ‚ç‚¹å¼€å§‹ï¼Œçˆ¶èŠ‚ç‚¹å…ˆäºŽå­èŠ‚ç‚¹å¤„ç†
        Deque<TechnologyBomStructure> queue = new ArrayDeque<>();
        List<TechnologyBomStructure> rootNodes = childrenMap.get(null);
        if (rootNodes != null) {
            queue.addAll(rootNodes);
        }
        // parentId=0 çš„也视为根节点
        List<TechnologyBomStructure> zeroParentNodes = childrenMap.get(0L);
        if (zeroParentNodes != null) {
            for (TechnologyBomStructure node : zeroParentNodes) {
                if (!queue.contains(node)) {
                    queue.add(node);
                }
            }
        }
        while (!queue.isEmpty()) {
            TechnologyBomStructure source = queue.poll();
            if (source == null || source.getId() == null) {
                continue;
            }
            // è®¡ç®—需求量 = çˆ¶èŠ‚ç‚¹éœ€æ±‚é‡ Ã— è‡ªèº« unitQuantity
            BigDecimal parentDemandedQuantity = orderQuantity;
            Long sourceParentId = source.getParentId();
            if (sourceParentId != null && sourceParentId != 0L) {
                Long parentTargetId = idMap.get(sourceParentId);
                if (parentTargetId != null) {
                    // çˆ¶èŠ‚ç‚¹å·²ç»æ’å…¥ï¼Œç”¨çˆ¶èŠ‚ç‚¹çš„ targetId ä»Žå·²æ’入列表找
                    // è¿™é‡Œæˆ‘们直接用存下来的父节点需求量
                    TechnologyBomStructure parentSource = sourceById.get(sourceParentId);
                    if (parentSource != null && parentSource.getDemandedQuantity() != null) {
                        parentDemandedQuantity = parentSource.getDemandedQuantity();
                    }
                }
            }
            ProductionBomStructure target = new ProductionBomStructure();
            target.setProductionOrderId(productionOrder.getId());
            target.setProductionOrderBomId(orderBom.getId());
@@ -449,11 +494,20 @@
            target.setProductModelId(source.getProductModelId());
            target.setTechnologyOperationId(source.getOperationId());
            target.setUnitQuantity(source.getUnitQuantity());
            target.setDemandedQuantity(lastProcessDemandedQuantity.multiply(source.getUnitQuantity()));
            target.setDemandedQuantity(parentDemandedQuantity.multiply(defaultDecimal(source.getUnitQuantity())));
            target.setUnit(source.getUnit());
            productionBomStructureMapper.insert(target);
            idMap.put(source.getId(), target.getId());
            lastProcessDemandedQuantity = target.getDemandedQuantity();
            // å°†å½“前节点的 demandedQuantity å›žå†™åˆ° source å¯¹è±¡ä¸Šï¼Œä¾›å­èŠ‚ç‚¹è®¡ç®—ä½¿ç”¨
            source.setDemandedQuantity(target.getDemandedQuantity());
            // å°†å­èŠ‚ç‚¹åŠ å…¥é˜Ÿåˆ—
            List<TechnologyBomStructure> children = childrenMap.get(source.getId());
            if (children != null) {
                for (TechnologyBomStructure child : children) {
                    queue.add(child);
                }
            }
        }
        return orderBom;
    }
@@ -611,9 +665,6 @@
                .collect(Collectors.toSet());
        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) {
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/project/system/service/impl/SysUserServiceImpl.java
@@ -51,7 +51,7 @@
     * @return ç”¨æˆ·ä¿¡æ¯é›†åˆä¿¡æ¯
     */
    @Override
    @DataScope(deptAlias = "d", userAlias = "u")
    @DataScope(userAlias = "u")
    public List<SysUser> selectUserList(SysUser user)
    {
        return userMapper.selectUserList(user);
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/java/com/ruoyi/sales/pojo/ShippingInfo.java
@@ -67,6 +67,10 @@
    @Excel(name = "发货车牌号")
    private String shippingCarNumber;
    @Schema(description = "发货地址")
    @Excel(name = "发货地址")
    private String shippingAddress;
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
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/application-lt.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,268 @@
# é¡¹ç›®ç›¸å…³é…ç½®
ruoyi:
  # åç§°
  name: RuoYi
  # ç‰ˆæœ¬
  version: 3.8.9
  # ç‰ˆæƒå¹´ä»½
  copyrightYear: 2025
  # æ–‡ä»¶è·¯å¾„ ç¤ºä¾‹ï¼ˆ Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
  profile: /data/javaWork/product-inventory-management/file
  # èŽ·å–ip地址开关
  addressEnabled: false
  # éªŒè¯ç ç±»åž‹ math æ•°å­—计算 char å­—符验证
  captchaType: math
  # ååŒå®¡æ‰¹ç¼–号前缀(配置文件后缀命名)
  approvalNumberPrefix: NEW
  # ä¸ªæŽ¨ Unipush é…ç½®
  getui:
    appId: PfjyAAE0FK64FaO1w2CMb1
    appKey: zTMb831OEL6J4GK1uE3Ob4
    masterSecret: K1GFtsv42v61tXGnF7SGE5
    domain: https://restapi.getui.cn/v2/
    # ç¦»çº¿æŽ¨é€ä½¿ç”¨çš„包名/组件名
    intentComponent: uni.app.UNI099A590/io.dcloud.PandoraEntry
# å¼€å‘环境配置
server:
  # æœåŠ¡å™¨çš„HTTP端口,默认为8080
  port: 9003
  servlet:
    # åº”用的访问路径
    context-path: /
  tomcat:
    # tomcat的URI编码
    uri-encoding: UTF-8
    # è¿žæŽ¥æ•°æ»¡åŽçš„æŽ’队数,默认为100
    accept-count: 1000
    threads:
      # tomcat最大线程数,默认为200
      max: 800
      # Tomcat启动初始化的线程数,默认值10
      min-spare: 100
# æ—¥å¿—配置
logging:
  level:
    com.ruoyi: warn
    org.springframework: warn
minio:
  endpoint: http://114.132.189.42/
  port: 7019
  secure: false
  accessKey: admin
  secretKey: 12345678
  preview-expiry: 24 # é¢„览地址默认24小时
  default-bucket: jxc
# ç”¨æˆ·é…ç½®
user:
  password:
    # å¯†ç æœ€å¤§é”™è¯¯æ¬¡æ•°
    maxRetryCount: 5
    # å¯†ç é”å®šæ—¶é—´ï¼ˆé»˜è®¤10分钟)
    lockTime: 10
# Spring配置
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    druid:
      # ä¸»åº“数据源
      master:
        url: jdbc:mysql://172.17.0.1:11101/product-inventory-management-lt?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: xd@123456..
      # ä»Žåº“数据源
      slave:
        # ä»Žæ•°æ®æºå¼€å…³/默认关闭
        enabled: false
        url:
        username:
        password:
      # åˆå§‹è¿žæŽ¥æ•°
      initialSize: 5
      # æœ€å°è¿žæŽ¥æ± æ•°é‡
      minIdle: 10
      # æœ€å¤§è¿žæŽ¥æ± æ•°é‡
      maxActive: 20
      # é…ç½®èŽ·å–è¿žæŽ¥ç­‰å¾…è¶…æ—¶çš„æ—¶é—´
      maxWait: 60000
      # é…ç½®è¿žæŽ¥è¶…æ—¶æ—¶é—´
      connectTimeout: 30000
      # é…ç½®ç½‘络超时时间
      socketTimeout: 60000
      # é…ç½®é—´éš”多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å°ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
      minEvictableIdleTimeMillis: 300000
      # é…ç½®ä¸€ä¸ªè¿žæŽ¥åœ¨æ± ä¸­æœ€å¤§ç”Ÿå­˜çš„æ—¶é—´ï¼Œå•位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # é…ç½®æ£€æµ‹è¿žæŽ¥æ˜¯å¦æœ‰æ•ˆ
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      webStatFilter:
        enabled: true
      statViewServlet:
        enabled: true
        # è®¾ç½®ç™½åå•,不填则允许所有访问
        allow:
        url-pattern: /druid/*
        # æŽ§åˆ¶å°ç®¡ç†ç”¨æˆ·åå’Œå¯†ç 
        login-username: ruoyi
        login-password: 123456
      filter:
        stat:
          enabled: true
          # æ…¢SQL记录
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true
  # èµ„源信息
  messages:
    # å›½é™…化资源文件路径
    basename: i18n/messages
  # æ–‡ä»¶ä¸Šä¼ 
  servlet:
    multipart:
      # å•个文件大小
      max-file-size: 1GB
      # è®¾ç½®æ€»ä¸Šä¼ çš„æ–‡ä»¶å¤§å°
      max-request-size: 2GB
  # æœåŠ¡æ¨¡å—
  devtools:
    restart:
      # çƒ­éƒ¨ç½²å¼€å…³
      enabled: false
  # redis é…ç½®
  data:
    mongodb:
      uri: mongodb://114.132.189.42:9028/chat_memory_db_lt
    # redis é…ç½®
    redis:
      # åœ°å€
#      host: 127.0.0.1
      host: 172.17.0.1
      # ç«¯å£ï¼Œé»˜è®¤ä¸º6379
      port: 11102
      # æ•°æ®åº“索引
      database: 0
      # å¯†ç 
      #    password: root2022!
      password:
      # è¿žæŽ¥è¶…æ—¶æ—¶é—´
      timeout: 10s
      lettuce:
        pool:
          # è¿žæŽ¥æ± ä¸­çš„æœ€å°ç©ºé—²è¿žæŽ¥
          min-idle: 0
          # è¿žæŽ¥æ± ä¸­çš„æœ€å¤§ç©ºé—²è¿žæŽ¥
          max-idle: 8
          # è¿žæŽ¥æ± çš„æœ€å¤§æ•°æ®åº“连接数
          max-active: 8
          # #连接池最大阻塞等待时间(使用负值表示没有限制)
          max-wait: -1ms
  # Quartz定时任务配置(新增部分)
  quartz:
    job-store-type: jdbc  # ä½¿ç”¨æ•°æ®åº“存储
    jdbc:
      initialize-schema: never  # é¦–次运行时自动创建表结构,成功后改为never
      schema: classpath:org/quartz/impl/jdbcjobstore/tables_mysql_innodb.sql  # MySQL表结构脚本
    properties:
      org:
        quartz:
          scheduler:
            instanceName: RuoYiScheduler
            instanceId: AUTO
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate  # MySQL适配
            tablePrefix: qrtz_  # è¡¨åå‰ç¼€ï¼Œä¸Žè„šæœ¬ä¸€è‡´
            isClustered: false  # å•节点模式(集群需改为true)
            clusterCheckinInterval: 10000
            txIsolationLevelSerializable: true
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10  # çº¿ç¨‹æ± å¤§å°
            threadPriority: 5
            makeThreadsDaemons: true
          updateCheck: false  # å…³é—­ç‰ˆæœ¬æ£€æŸ¥
# token配置
token:
  # ä»¤ç‰Œè‡ªå®šä¹‰æ ‡è¯†
  header: Authorization
  # ä»¤ç‰Œå¯†é’¥
  secret: xpAVjhCjQDaDB7mjPAzMDSbQWXNu2zYkTdDNUsPMS5Xx8QMmQVYN7n74eZrYJxDJ
  # ä»¤ç‰Œæœ‰æ•ˆæœŸï¼ˆé»˜è®¤30分钟)
  expireTime: 450
# MyBatis Plus配置
mybatis-plus:
  # æœç´¢æŒ‡å®šåŒ…别名   æ ¹æ®è‡ªå·±çš„项目来
  typeAliasesPackage: com.ruoyi.**.pojo
  # é…ç½®mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath*:mapper/**/*Mapper.xml
  # åŠ è½½å…¨å±€çš„é…ç½®æ–‡ä»¶
  configLocation: classpath:mybatis/mybatis-config.xml
  global-config:
    enable-sql-runner: true
    db-config:
      id-type: auto
# PageHelper分页插件
pagehelper:
  helperDialect: mysql
  supportMethodsArguments: true
  params: count=countSql
# Swagger配置
swagger:
  # æ˜¯å¦å¼€å¯swagger
  enabled: true
  # è¯·æ±‚前缀
  pathMapping: /dev-api
# é˜²æ­¢XSS攻击
xss:
  # è¿‡æ»¤å¼€å…³
  enabled: true
  # æŽ’除链接(多个用逗号分隔)
  excludes: /system/notice
  # åŒ¹é…é“¾æŽ¥
  urlPatterns: /system/*,/monitor/*,/tool/*
# ä»£ç ç”Ÿæˆ
gen:
  # ä½œè€…
  author: ruoyi
  # é»˜è®¤ç”ŸæˆåŒ…路径 system éœ€æ”¹æˆè‡ªå·±çš„æ¨¡å—名称 å¦‚ system monitor tool
  packageName: com.ruoyi.project.system
  # è‡ªåŠ¨åŽ»é™¤è¡¨å‰ç¼€ï¼Œé»˜è®¤æ˜¯true
  autoRemovePre: false
  # è¡¨å‰ç¼€ï¼ˆç”Ÿæˆç±»åä¸ä¼šåŒ…含表前缀,多个用逗号分隔)
  tablePrefix: sys_
  # æ˜¯å¦å…è®¸ç”Ÿæˆæ–‡ä»¶è¦†ç›–到本地(自定义路径),默认不允许
  allowOverwrite: false
# æ–‡ä»¶ä¸Šä¼ é…ç½®
file:
  temp-dir: /data/javaWork/product-inventory-management/file/temp/uploads   # ä¸´æ—¶ç›®å½•
  upload-dir: /data/javaWork/product-inventory-management/file/prod/uploads # æ­£å¼ç›®å½•
  path: /data/javaWork/product-inventory-management/file # ä¸Šä¼ ç›®å½•
  urlPrefix: /prod-api/common # é“¾æŽ¥å‰ç¼€
  domain: http://36.213.180.217:9200 # åŸŸåå‰ç¼€
  expired: 120 # è¿‡æœŸæ—¶é—´(单位:分钟)
  useLimit: 10 # ä½¿ç”¨æ¬¡æ•°
  compress: true # æ˜¯å¦åŽ‹ç¼©
  needCompressSize: 10MB # åŽ‹ç¼©é˜ˆå€¼
  compressQuality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
src/main/resources/application.yml
@@ -28,14 +28,14 @@
        api-key: sk-bd235cc13cd74e2388aa8984c84f691f
        model-name: "qwen-max"
      embedding-model:
        api-key: sk-9748859926b94096b920baaa12c343f5
        api-key: sk-bd235cc13cd74e2388aa8984c84f691f
        model-name: "text-embedding-v3"
      chat-model:
        api-key: sk-9748859926b94096b920baaa12c343f5
        api-key: sk-bd235cc13cd74e2388aa8984c84f691f
        model-name: "qwen-max"
  open-ai:
    chat-model:
      api-key: sk-9748859926b94096b920baaa12c343f5
      api-key: sk-bd235cc13cd74e2388aa8984c84f691f
      base-url: "https://dashscope.aliyuncs.com/compatible-mode/v1"
      model-name: "deepseek-v3"
      log-requests: true
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,
src/main/resources/mapper/sales/ShippingInfoMapper.xml
@@ -8,6 +8,7 @@
        s.sales_ledger_id,
        s.shipping_date,
        s.shipping_car_number,
        s.shipping_address,
        s.express_number,
        s.express_company,
        s.shipping_no,
@@ -56,6 +57,7 @@
            s.sales_ledger_id,
            s.shipping_date,
            s.shipping_car_number,
            s.shipping_address,
            s.create_time,
            s.update_time,
            s.create_user,