6、明细记录中有删除操作的,需设置权限;
7、质量管理要设置检验规则;
已添加15个文件
已修改27个文件
3154 ■■■■■ 文件已修改
docs/20260618_customer_transactions_optimization.md 839 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/20260618_return_status_field.md 254 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/20260618_shipping_info_for_return.md 365 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/master_contract_no.md 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/master_contract_no.sql 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/quality_auto_judge.md 385 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/quality_auto_judge.sql 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/mapper/CustomerMapper.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/ICustomerService.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/bean/dto/ReturnManagementDto.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/pojo/PurchaseLedger.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityInspectController.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/dto/AutoJudgeAllResponse.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/dto/AutoJudgeRequest.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/dto/AutoJudgeResponse.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityInspectParam.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityTestStandardParam.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/IQualityInspectService.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/utils/QualityJudgeUtil.java 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/MetricStatisticsController.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/controller/ShippingInfoController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/SalesLedgerDto.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/ShippingInfoDto.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/mapper/ShippingInfoMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedger.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/ShippingInfoService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsDetailsVo.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsProductVo.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsShipmentVo.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsSummaryVo.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/SalesLedgerVo.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/vo/ShippingInfoForReturnVo.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/CustomerMapper.xml 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/purchase/PurchaseLedgerMapper.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerMapper.xml 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/ShippingInfoMapper.xml 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/20260618_customer_transactions_optimization.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,839 @@
# å®¢æˆ·å¾€æ¥å¤šç»´åº¦æ˜Žç»†åŠŸèƒ½å‰ç«¯è”è°ƒæ–‡æ¡£
> ä¼˜åŒ–客户往来功能,新增多维度明细查询接口,支持产品明细和发货明细维度
## æ¶‰åŠé¡µé¢
- è¥é”€ç®¡ç† / å®¢æˆ·å¾€æ¥ - å®¢æˆ·å¾€æ¥åˆ—表页
- è¥é”€ç®¡ç† / å®¢æˆ·å¾€æ¥ - å®¢æˆ·å¾€æ¥è¯¦æƒ…页(新增)
- è¥é”€ç®¡ç† / å®¢æˆ·å¾€æ¥ - äº§å“æ˜Žç»†Tab(新增)
- è¥é”€ç®¡ç† / å®¢æˆ·å¾€æ¥ - å‘货明细Tab(新增)
## API
### 1. å®¢æˆ·å¾€æ¥åˆ—表(原有接口,未变更)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /metricStatistics/customewTransactions | å®¢æˆ·å¾€æ¥åˆ—表 |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| pageNum | Long | å¦ | é¡µç ï¼Œé»˜è®¤1 |
| pageSize | Long | å¦ | æ¯é¡µæ¡æ•°ï¼Œé»˜è®¤10 |
| customerName | String | å¦ | å®¢æˆ·åç§°ï¼ˆæ¨¡ç³Šæœç´¢ï¼‰ |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| customerId | Long | å®¢æˆ·ID |
| customerName | String | å®¢æˆ·åç§° |
| contractAmounts | BigDecimal | åˆåŒæ€»é‡‘额 |
| receiptPaymentAmount | BigDecimal | æ”¶æ¬¾é‡‘额 |
| receiptableAmount | BigDecimal | åº”收金额 |
---
### 1.5 å®¢æˆ·å¾€æ¥æ˜Žç»†ï¼ˆåŽŸæœ‰æŽ¥å£ï¼Œæ–°å¢žå­—æ®µï¼‰
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /metricStatistics/customewTransactionsDetails | å®¢æˆ·å¾€æ¥æ˜Žç»†ï¼ˆåˆåŒç»´åº¦ï¼‰ |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| customerId | Long | æ˜¯ | å®¢æˆ·ID |
| pageNum | Long | å¦ | é¡µç ï¼Œé»˜è®¤1 |
| pageSize | Long | å¦ | æ¯é¡µæ¡æ•°ï¼Œé»˜è®¤10 |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| salesLedgerId | Long | é”€å”®å°è´¦ID |
| salesContractNo | String | é”€å”®åˆåŒå· |
| executionDate | LocalDate | åˆåŒç­¾è®¢æ—¥æœŸ |
| contractAmount | BigDecimal | åˆåŒé‡‘额 |
| **productNames** | String | **产品名称列表(逗号分隔)【新增】** |
| receiptPaymentAmount | BigDecimal | æ”¶æ¬¾é‡‘额 |
| receiptableAmount | BigDecimal | åº”收金额 |
**响应示例:**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [
      {
        "salesLedgerId": 1,
        "salesContractNo": "HT-2026-001",
        "executionDate": "2026-06-01",
        "contractAmount": 50000.00,
        "productNames": "产品A,产品B,产品C",
        "receiptPaymentAmount": 30000.00,
        "receiptableAmount": 20000.00
      }
    ],
    "total": 10
  }
}
```
---
### 2. å®¢æˆ·å¾€æ¥ç»Ÿè®¡æ±‡æ€»ï¼ˆæ–°å¢žï¼‰
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /metricStatistics/customerTransactionsSummary | å®¢æˆ·å¾€æ¥ç»Ÿè®¡æ±‡æ€» |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| customerId | Long | æ˜¯ | å®¢æˆ·ID |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| customerId | Long | å®¢æˆ·ID |
| customerName | String | å®¢æˆ·åç§° |
| contractAmounts | BigDecimal | åˆåŒæ€»é‡‘额 |
| contractCount | Integer | åˆåŒæ•°é‡ |
| productCount | Integer | äº§å“ç§ç±»æ•° |
| shippedAmounts | BigDecimal | å‘货总金额 |
| shippedQuantity | BigDecimal | å‘货总数量 |
| receivedAmounts | BigDecimal | æ”¶æ¬¾é‡‘额 |
| receivableAmounts | BigDecimal | åº”收金额 |
| returnAmounts | BigDecimal | é€€è´§é‡‘额 |
| unshippedAmounts | BigDecimal | æœªå‘货金额 |
| receivedRate | BigDecimal | æ”¶æ¬¾çއ(%) |
| shippedRate | BigDecimal | å‘货率(%) |
**响应示例:**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "customerId": 1,
    "customerName": "客户A",
    "contractAmounts": 100000.00,
    "contractCount": 5,
    "productCount": 12,
    "shippedAmounts": 80000.00,
    "shippedQuantity": 500,
    "receivedAmounts": 60000.00,
    "receivableAmounts": 20000.00,
    "returnAmounts": 5000.00,
    "unshippedAmounts": 20000.00,
    "receivedRate": 75.00,
    "shippedRate": 80.00
  }
}
```
---
### 3. å®¢æˆ·å¾€æ¥äº§å“æ˜Žç»†ï¼ˆæ–°å¢žï¼‰
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /metricStatistics/customerTransactionsProducts | å®¢æˆ·å¾€æ¥äº§å“æ˜Žç»† |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| customerId | Long | æ˜¯ | å®¢æˆ·ID |
| salesLedgerId | Long | å¦ | é”€å”®å°è´¦ID(可选,用于筛选某合同) |
| pageNum | Long | å¦ | é¡µç ï¼Œé»˜è®¤1 |
| pageSize | Long | å¦ | æ¯é¡µæ¡æ•°ï¼Œé»˜è®¤10 |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| salesLedgerId | Long | é”€å”®å°è´¦ID |
| salesContractNo | String | é”€å”®åˆåŒå· |
| productId | Long | äº§å“ID |
| productName | String | äº§å“åç§° |
| model | String | è§„格型号 |
| unit | String | å•位 |
| contractQuantity | BigDecimal | åˆåŒæ•°é‡ |
| taxInclusiveUnitPrice | BigDecimal | åˆåŒå•ä»·(含税) |
| contractAmount | BigDecimal | åˆåŒé‡‘额 |
| shippedQuantity | BigDecimal | å·²å‘货数量 |
| shippedAmount | BigDecimal | å·²å‘货金额 |
| receivedAmount | BigDecimal | å·²æ”¶æ¬¾é‡‘额 |
| receivableAmount | BigDecimal | åº”收金额 |
**响应示例:**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [
      {
        "salesLedgerId": 1,
        "salesContractNo": "HT-2026-001",
        "productId": 10,
        "productName": "产品A",
        "model": "规格1",
        "unit": "ä»¶",
        "contractQuantity": 100,
        "taxInclusiveUnitPrice": 50.00,
        "contractAmount": 5000.00,
        "shippedQuantity": 80,
        "shippedAmount": 4000.00,
        "receivedAmount": 3000.00,
        "receivableAmount": 1000.00
      }
    ],
    "total": 25,
    "pageNum": 1,
    "pageSize": 10
  }
}
```
---
### 4. å®¢æˆ·å¾€æ¥å‘货明细(新增)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /metricStatistics/customerTransactionsShipments | å®¢æˆ·å¾€æ¥å‘货明细 |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| customerId | Long | æ˜¯ | å®¢æˆ·ID |
| salesLedgerId | Long | å¦ | é”€å”®å°è´¦ID(可选,用于筛选某合同) |
| pageNum | Long | å¦ | é¡µç ï¼Œé»˜è®¤1 |
| pageSize | Long | å¦ | æ¯é¡µæ¡æ•°ï¼Œé»˜è®¤10 |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| salesLedgerId | Long | é”€å”®å°è´¦ID |
| salesContractNo | String | é”€å”®åˆåŒå· |
| shippingId | Long | å‘货单ID |
| shippingNo | String | å‘货单号 |
| productName | String | äº§å“åç§° |
| model | String | è§„格型号 |
| shippingQuantity | BigDecimal | å‘货数量 |
| shippingAmount | BigDecimal | å‘货金额(含税) |
| batchNo | String | å‡ºåº“批号 |
| shippingDate | LocalDate | å‘货日期 |
| approvalStatus | Integer | å®¡æ‰¹çŠ¶æ€(0待审/1已审) |
| receivedAmount | BigDecimal | å·²æ”¶æ¬¾é‡‘额 |
| receivableAmount | BigDecimal | åº”收金额 |
**响应示例:**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [
      {
        "salesLedgerId": 1,
        "salesContractNo": "HT-2026-001",
        "shippingId": 100,
        "shippingNo": "FH-2026-001",
        "productName": "产品A",
        "model": "规格1",
        "shippingQuantity": 50,
        "shippingAmount": 2500.00,
        "batchNo": "20260618001",
        "shippingDate": "2026-06-18",
        "approvalStatus": 1,
        "receivedAmount": 2000.00,
        "receivableAmount": 500.00
      }
    ],
    "total": 30,
    "pageNum": 1,
    "pageSize": 10
  }
}
```
---
## å‰ç«¯é¡µé¢è®¾è®¡
### 1. å®¢æˆ·å¾€æ¥åˆ—表页(优化)
```html
<template>
  <div class="app-container">
    <!-- æœç´¢æ  -->
    <el-form :model="queryParams" ref="queryForm" :inline="true">
      <el-form-item label="客户名称" prop="customerName">
        <el-input v-model="queryParams.customerName" placeholder="请输入客户名称" clearable />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getList">搜索</el-button>
        <el-button @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <!-- æ•°æ®è¡¨æ ¼ -->
    <el-table :data="list" v-loading="loading">
      <el-table-column label="客户名称" prop="customerName" />
      <el-table-column label="合同总金额" prop="contractAmounts" align="right">
        <template #default="{ row }">
          {{ formatMoney(row.contractAmounts) }}
        </template>
      </el-table-column>
      <el-table-column label="收款金额" prop="receiptPaymentAmount" align="right">
        <template #default="{ row }">
          {{ formatMoney(row.receiptPaymentAmount) }}
        </template>
      </el-table-column>
      <el-table-column label="应收金额" prop="receiptableAmount" align="right">
        <template #default="{ row }">
          <span :class="{ 'text-danger': row.receiptableAmount > 0 }">
            {{ formatMoney(row.receiptableAmount) }}
          </span>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="150" align="center">
        <template #default="{ row }">
          <el-button type="text" @click="viewDetail(row)">查看明细</el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- åˆ†é¡µ -->
    <el-pagination
      v-show="total > 0"
      :total="total"
      :page.sync="queryParams.pageNum"
      :limit.sync="queryParams.pageSize"
      @pagination="getList"
    />
  </div>
</template>
<script>
export default {
  name: 'CustomerTransactions',
  data() {
    return {
      loading: false,
      list: [],
      total: 0,
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        customerName: ''
      }
    }
  },
  created() {
    this.getList()
  },
  methods: {
    getList() {
      this.loading = true
      this.$axios.get('/metricStatistics/customewTransactions', { params: this.queryParams })
        .then(res => {
          this.list = res.data.records
          this.total = res.data.total
        })
        .finally(() => {
          this.loading = false
        })
    },
    resetQuery() {
      this.queryParams.customerName = ''
      this.getList()
    },
    formatMoney(value) {
      if (!value) return '0.00'
      return Number(value).toFixed(2)
    },
    viewDetail(row) {
      this.$router.push({ path: '/sales/customerTransactions/detail', query: { customerId: row.customerId } })
    }
  }
}
</script>
```
---
### 2. å®¢æˆ·å¾€æ¥è¯¦æƒ…页(新增)
```html
<template>
  <div class="app-container">
    <!-- é¡¶éƒ¨ç»Ÿè®¡å¡ç‰‡ -->
    <el-card class="summary-card">
      <div slot="header">
        <span>{{ summary.customerName }} - å¾€æ¥ç»Ÿè®¡</span>
      </div>
      <el-row :gutter="20">
        <el-col :span="4">
          <div class="stat-item">
            <div class="stat-label">合同总金额</div>
            <div class="stat-value">{{ formatMoney(summary.contractAmounts) }}</div>
          </div>
        </el-col>
        <el-col :span="4">
          <div class="stat-item">
            <div class="stat-label">合同数量</div>
            <div class="stat-value">{{ summary.contractCount }}份</div>
          </div>
        </el-col>
        <el-col :span="4">
          <div class="stat-item">
            <div class="stat-label">产品种类</div>
            <div class="stat-value">{{ summary.productCount }}种</div>
          </div>
        </el-col>
        <el-col :span="4">
          <div class="stat-item">
            <div class="stat-label">发货金额</div>
            <div class="stat-value">{{ formatMoney(summary.shippedAmounts) }}</div>
          </div>
        </el-col>
        <el-col :span="4">
          <div class="stat-item">
            <div class="stat-label">收款金额</div>
            <div class="stat-value text-success">{{ formatMoney(summary.receivedAmounts) }}</div>
          </div>
        </el-col>
        <el-col :span="4">
          <div class="stat-item">
            <div class="stat-label">应收金额</div>
            <div class="stat-value text-danger">{{ formatMoney(summary.receivableAmounts) }}</div>
          </div>
        </el-col>
      </el-row>
      <el-row :gutter="20" style="margin-top: 15px;">
        <el-col :span="4">
          <div class="stat-item">
            <div class="stat-label">退货金额</div>
            <div class="stat-value">{{ formatMoney(summary.returnAmounts) }}</div>
          </div>
        </el-col>
        <el-col :span="4">
          <div class="stat-item">
            <div class="stat-label">未发货金额</div>
            <div class="stat-value text-warning">{{ formatMoney(summary.unshippedAmounts) }}</div>
          </div>
        </el-col>
        <el-col :span="4">
          <div class="stat-item">
            <div class="stat-label">收款率</div>
            <div class="stat-value">
              <el-progress :percentage="summary.receivedRate || 0" :stroke-width="18" />
            </div>
          </div>
        </el-col>
        <el-col :span="4">
          <div class="stat-item">
            <div class="stat-label">发货率</div>
            <div class="stat-value">
              <el-progress :percentage="summary.shippedRate || 0" :stroke-width="18" color="#67c23a" />
            </div>
          </div>
        </el-col>
      </el-row>
    </el-card>
    <!-- Tab åˆ‡æ¢ -->
    <el-tabs v-model="activeTab" @tab-click="handleTabChange">
      <el-tab-pane label="产品明细" name="products">
        <product-table :customerId="customerId" />
      </el-tab-pane>
      <el-tab-pane label="发货明细" name="shipments">
        <shipment-table :customerId="customerId" />
      </el-tab-pane>
      <el-tab-pane label="合同明细" name="contracts">
        <contract-table :customerId="customerId" />
      </el-tab-pane>
    </el-tabs>
  </div>
</template>
<script>
import ProductTable from './components/ProductTable.vue'
import ShipmentTable from './components/ShipmentTable.vue'
import ContractTable from './components/ContractTable.vue'
export default {
  name: 'CustomerTransactionsDetail',
  components: { ProductTable, ShipmentTable, ContractTable },
  data() {
    return {
      customerId: null,
      summary: {},
      activeTab: 'products'
    }
  },
  created() {
    this.customerId = this.$route.query.customerId
    if (this.customerId) {
      this.getSummary()
    }
  },
  methods: {
    getSummary() {
      this.$axios.get('/metricStatistics/customerTransactionsSummary', {
        params: { customerId: this.customerId }
      }).then(res => {
        this.summary = res.data
      })
    },
    formatMoney(value) {
      if (!value) return '0.00'
      return Number(value).toFixed(2)
    },
    handleTabChange(tab) {
      // Tab åˆ‡æ¢æ—¶åˆ·æ–°å­ç»„件数据
    }
  }
}
</script>
<style scoped>
.summary-card {
  margin-bottom: 20px;
}
.stat-item {
  text-align: center;
}
.stat-label {
  font-size: 14px;
  color: #909399;
}
.stat-value {
  font-size: 18px;
  font-weight: bold;
  margin-top: 5px;
}
.text-success {
  color: #67c23a;
}
.text-danger {
  color: #f56c6c;
}
.text-warning {
  color: #e6a23c;
}
</style>
```
---
### 3. äº§å“æ˜Žç»†ç»„ä»¶ (ProductTable.vue)
```html
<template>
  <div>
    <!-- ç­›é€‰ -->
    <el-form :inline="true" size="small">
      <el-form-item label="合同号">
        <el-select v-model="filterData.salesLedgerId" clearable placeholder="全部合同" @change="getList">
          <el-option v-for="item in contractList" :key="item.id" :label="item.salesContractNo" :value="item.id" />
        </el-select>
      </el-form-item>
    </el-form>
    <!-- è¡¨æ ¼ -->
    <el-table :data="list" v-loading="loading" size="small">
      <el-table-column label="合同号" prop="salesContractNo" width="150" />
      <el-table-column label="产品名称" prop="productName" />
      <el-table-column label="规格型号" prop="model" width="120" />
      <el-table-column label="单位" prop="unit" width="80" />
      <el-table-column label="合同数量" prop="contractQuantity" align="right" width="100" />
      <el-table-column label="合同单价" prop="taxInclusiveUnitPrice" align="right" width="100">
        <template #default="{ row }">
          {{ formatMoney(row.taxInclusiveUnitPrice) }}
        </template>
      </el-table-column>
      <el-table-column label="合同金额" prop="contractAmount" align="right" width="120">
        <template #default="{ row }">
          {{ formatMoney(row.contractAmount) }}
        </template>
      </el-table-column>
      <el-table-column label="已发货数量" prop="shippedQuantity" align="right" width="100" />
      <el-table-column label="已发货金额" prop="shippedAmount" align="right" width="120">
        <template #default="{ row }">
          {{ formatMoney(row.shippedAmount) }}
        </template>
      </el-table-column>
      <el-table-column label="已收款金额" prop="receivedAmount" align="right" width="120">
        <template #default="{ row }">
          <span class="text-success">{{ formatMoney(row.receivedAmount) }}</span>
        </template>
      </el-table-column>
      <el-table-column label="应收金额" prop="receivableAmount" align="right" width="120">
        <template #default="{ row }">
          <span :class="{ 'text-danger': row.receivableAmount > 0 }">
            {{ formatMoney(row.receivableAmount) }}
          </span>
        </template>
      </el-table-column>
      <el-table-column label="发货进度" width="150">
        <template #default="{ row }">
          <el-progress
            :percentage="calcPercent(row.shippedQuantity, row.contractQuantity)"
            :stroke-width="10"
          />
        </template>
      </el-table-column>
    </el-table>
    <!-- åˆ†é¡µ -->
    <el-pagination
      v-show="total > 0"
      :total="total"
      :page.sync="queryParams.pageNum"
      :limit.sync="queryParams.pageSize"
      layout="total, prev, pager, next"
      @pagination="getList"
    />
  </div>
</template>
<script>
export default {
  name: 'ProductTable',
  props: {
    customerId: {
      type: Number,
      required: true
    }
  },
  data() {
    return {
      loading: false,
      list: [],
      total: 0,
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        customerId: null,
        salesLedgerId: null
      },
      filterData: {
        salesLedgerId: null
      },
      contractList: []
    }
  },
  watch: {
    customerId(val) {
      if (val) {
        this.queryParams.customerId = val
        this.getList()
        this.getContractList()
      }
    }
  },
  methods: {
    getList() {
      this.loading = true
      this.queryParams.salesLedgerId = this.filterData.salesLedgerId
      this.$axios.get('/metricStatistics/customerTransactionsProducts', { params: this.queryParams })
        .then(res => {
          this.list = res.data.records
          this.total = res.data.total
        })
        .finally(() => {
          this.loading = false
        })
    },
    getContractList() {
      // èŽ·å–è¯¥å®¢æˆ·çš„åˆåŒåˆ—è¡¨ç”¨äºŽç­›é€‰
      this.$axios.get('/metricStatistics/customewTransactionsDetails', {
        params: { customerId: this.customerId, pageNum: 1, pageSize: 100 }
      }).then(res => {
        this.contractList = res.data.records
      })
    },
    formatMoney(value) {
      if (!value) return '0.00'
      return Number(value).toFixed(2)
    },
    calcPercent(shipped, total) {
      if (!total || total === 0) return 0
      return Math.round((shipped / total) * 100)
    }
  }
}
</script>
```
---
### 4. å‘货明细组件 (ShipmentTable.vue)
```html
<template>
  <div>
    <!-- ç­›é€‰ -->
    <el-form :inline="true" size="small">
      <el-form-item label="合同号">
        <el-select v-model="filterData.salesLedgerId" clearable placeholder="全部合同" @change="getList">
          <el-option v-for="item in contractList" :key="item.id" :label="item.salesContractNo" :value="item.id" />
        </el-select>
      </el-form-item>
    </el-form>
    <!-- è¡¨æ ¼ -->
    <el-table :data="list" v-loading="loading" size="small">
      <el-table-column label="合同号" prop="salesContractNo" width="150" />
      <el-table-column label="发货单号" prop="shippingNo" width="150" />
      <el-table-column label="产品名称" prop="productName" />
      <el-table-column label="规格型号" prop="model" width="120" />
      <el-table-column label="发货数量" prop="shippingQuantity" align="right" width="100" />
      <el-table-column label="发货金额" prop="shippingAmount" align="right" width="120">
        <template #default="{ row }">
          {{ formatMoney(row.shippingAmount) }}
        </template>
      </el-table-column>
      <el-table-column label="出库批号" prop="batchNo" width="150" />
      <el-table-column label="发货日期" prop="shippingDate" width="120" />
      <el-table-column label="审批状态" prop="approvalStatus" width="100">
        <template #default="{ row }">
          <el-tag :type="row.approvalStatus === 1 ? 'success' : 'warning'" size="mini">
            {{ row.approvalStatus === 1 ? '已审' : '待审' }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="已收款金额" prop="receivedAmount" align="right" width="120">
        <template #default="{ row }">
          <span class="text-success">{{ formatMoney(row.receivedAmount) }}</span>
        </template>
      </el-table-column>
      <el-table-column label="应收金额" prop="receivableAmount" align="right" width="120">
        <template #default="{ row }">
          <span :class="{ 'text-danger': row.receivableAmount > 0 }">
            {{ formatMoney(row.receivableAmount) }}
          </span>
        </template>
      </el-table-column>
    </el-table>
    <!-- åˆ†é¡µ -->
    <el-pagination
      v-show="total > 0"
      :total="total"
      :page.sync="queryParams.pageNum"
      :limit.sync="queryParams.pageSize"
      layout="total, prev, pager, next"
      @pagination="getList"
    />
  </div>
</template>
<script>
export default {
  name: 'ShipmentTable',
  props: {
    customerId: {
      type: Number,
      required: true
    }
  },
  data() {
    return {
      loading: false,
      list: [],
      total: 0,
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        customerId: null,
        salesLedgerId: null
      },
      filterData: {
        salesLedgerId: null
      },
      contractList: []
    }
  },
  watch: {
    customerId(val) {
      if (val) {
        this.queryParams.customerId = val
        this.getList()
        this.getContractList()
      }
    }
  },
  methods: {
    getList() {
      this.loading = true
      this.queryParams.salesLedgerId = this.filterData.salesLedgerId
      this.$axios.get('/metricStatistics/customerTransactionsShipments', { params: this.queryParams })
        .then(res => {
          this.list = res.data.records
          this.total = res.data.total
        })
        .finally(() => {
          this.loading = false
        })
    },
    getContractList() {
      this.$axios.get('/metricStatistics/customewTransactionsDetails', {
        params: { customerId: this.customerId, pageNum: 1, pageSize: 100 }
      }).then(res => {
        this.contractList = res.data.records
      })
    },
    formatMoney(value) {
      if (!value) return '0.00'
      return Number(value).toFixed(2)
    }
  }
}
</script>
```
---
## æ³¨æ„äº‹é¡¹
1. **路由配置**:需在路由中新增客户往来详情页路由 `/sales/customerTransactions/detail`
2. **组件拆分**:产品明细和发货明细建议拆分为独立组件,便于复用和维护
3. **筛选联动**:产品明细和发货明细支持按合同筛选,合同列表从原有接口获取
4. **数据格式**:金额字段需统一使用 `formatMoney` æ–¹æ³•格式化显示
5. **进度条显示**:产品明细中的发货进度使用 `el-progress` ç»„件直观展示
6. **状态标识**:应收金额大于0时使用红色标识,已收款使用绿色标识
7. **审批状态**:发货明细中的审批状态使用 `el-tag` å±•示,已审为绿色,待审为黄色
---
## æ•°æ®å¯¹æ¯”
### ä¼˜åŒ–前 vs ä¼˜åŒ–后
| ç»´åº¦ | ä¼˜åŒ–前 | ä¼˜åŒ–后 |
|------|--------|--------|
| å®¢æˆ·å¾€æ¥ | åªæœ‰åˆåŒé‡‘额、收款、应收 | æ–°å¢žåˆåŒæ•°ã€äº§å“æ•°ã€å‘货率、收款率等12项指标 |
| æ˜Žç»†ç»´åº¦ | ä»…合同明细 | æ–°å¢žäº§å“æ˜Žç»†ã€å‘货明细 |
| ç­›é€‰èƒ½åŠ› | ä»…按客户名筛选 | æ”¯æŒæŒ‰åˆåŒç­›é€‰äº§å“/发货明细 |
| æ•°æ®è¿½æº¯ | æ— æ³•追溯具体发货 | å¯è¿½æº¯æ¯æ¡å‘货记录的收款情况 |
| è¿›åº¦å±•示 | æ—  | å‘货进度条直观展示 |
docs/20260618_return_status_field.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,254 @@
# é€€è´§çŠ¶æ€å­—æ®µæŽ¥å£å˜æ›´å‰ç«¯è”è°ƒæ–‡æ¡£
> é”€å”®é€€è´§ã€å‘货台账、销售台账接口新增退货状态字段
## æ¶‰åŠé¡µé¢
- é”€å”®ç®¡ç† / é”€å”®é€€è´§åˆ—表页面
- é”€å”®ç®¡ç† / é”€å”®é€€è´§ - æ–°å¢ž/编辑退货单页面
- é”€å”®ç®¡ç† / å‘货台账列表页面
- é”€å”®ç®¡ç† / é”€å”®å°è´¦åˆ—表页面
---
## API å˜æ›´
### 1. é”€å”®é€€è´§åˆ—表
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /returnManagement/listPage | é”€å”®é€€è´§åˆ†é¡µåˆ—表 |
**新增响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| shippingQuantity | BigDecimal | å‘货数量 |
| returnedQuantity | BigDecimal | å·²é€€è´§æ•°é‡ |
| returnStatus | String | é€€è´§çŠ¶æ€ï¼šæ— é€€è´§/部分退货/全部退货 |
**响应示例:**
```json
{
  "code": 200,
  "data": {
    "records": [
      {
        "id": 1,
        "returnNo": "TH-2026-001",
        "shippingNo": "FH-2026-001",
        "shippingQuantity": 50,
        "returnedQuantity": 10,
        "returnStatus": "部分退货"
      }
    ]
  }
}
```
---
### 2. é”€å”®é€€è´§-发货信息查询
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /shippingInfo/getForReturn | é”€å”®é€€è´§-通过客户名称查询发货信息 |
**新增响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| returnedQuantity | BigDecimal | å·²é€€è´§æ•°é‡ |
| returnStatus | String | é€€è´§çŠ¶æ€ï¼šæ— é€€è´§/部分退货/全部退货 |
**响应示例:**
```json
{
  "code": 200,
  "data": [
    {
      "shippingId": 100,
      "shippingNo": "FH-2026-001",
      "shippingQuantity": 50,
      "returnedQuantity": 10,
      "returnStatus": "部分退货",
      "batchNo": "20260618001",
      "displayLabel": "20260618001-京A12345-2026-06-18"
    }
  ]
}
```
---
### 3. å‘货台账列表
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /shippingInfo/listPage | å‘货信息分页列表 |
**新增响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| returnedQuantity | BigDecimal | å·²é€€è´§æ•°é‡ |
| returnStatus | String | é€€è´§çŠ¶æ€ï¼šæ— é€€è´§/部分退货/全部退货 |
**响应示例:**
```json
{
  "code": 200,
  "data": {
    "records": [
      {
        "id": 100,
        "shippingNo": "FH-2026-001",
        "totalQuantity": 50,
        "returnedQuantity": 0,
        "returnStatus": "无退货"
      }
    ]
  }
}
```
---
### 4. é”€å”®å°è´¦åˆ—表
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /salesLedger/listPage | é”€å”®å°è´¦åˆ†é¡µåˆ—表 |
**新增响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| returnedQuantity | BigDecimal | å·²é€€è´§æ•°é‡ï¼ˆè¯¥é”€å”®åˆåŒä¸‹æ‰€æœ‰å‘货的累计退货数) |
| returnStatus | String | é€€è´§çŠ¶æ€ï¼šæ— é€€è´§/部分退货/全部退货 |
**响应示例:**
```json
{
  "code": 200,
  "data": {
    "records": [
      {
        "id": 1,
        "salesContractNo": "HT-2026-001",
        "contractAmount": 100000,
        "returnedQuantity": 30,
        "returnStatus": "部分退货"
      }
    ]
  }
}
```
---
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. é”€å”®é€€è´§åˆ—表页面
**新增表格列:**
```html
<el-table-column label="退货状态" prop="returnStatus" width="100">
  <template #default="{ row }">
    <el-tag
      :type="row.returnStatus === '无退货' ? 'success' : (row.returnStatus === '全部退货' ? 'danger' : 'warning')"
    >
      {{ row.returnStatus }}
    </el-tag>
  </template>
</el-table-column>
```
### 2. å‘货台账列表页面
**新增表格列:**
```html
<el-table-column label="退货状态" prop="returnStatus" width="100">
  <template #default="{ row }">
    <el-tag
      :type="row.returnStatus === '无退货' ? 'success' : (row.returnStatus === '全部退货' ? 'danger' : 'warning')"
    >
      {{ row.returnStatus }}
    </el-tag>
  </template>
</el-table-column>
<el-table-column label="已退货数量" prop="returnedQuantity" width="120" />
```
### 3. é”€å”®å°è´¦åˆ—表页面
**新增表格列:**
```html
<el-table-column label="退货状态" prop="returnStatus" width="100">
  <template #default="{ row }">
    <el-tag
      :type="row.returnStatus === '无退货' ? 'success' : (row.returnStatus === '全部退货' ? 'danger' : 'warning')"
    >
      {{ row.returnStatus }}
    </el-tag>
  </template>
</el-table-column>
```
### 4. é”€å”®é€€è´§é¡µé¢ - å‘货信息选择
**下拉选项增强显示:**
```html
<el-select v-model="form.shippingId" placeholder="请选择发货信息" filterable>
  <el-option
    v-for="item in shippingList"
    :key="item.shippingId"
    :label="item.displayLabel"
    :value="item.shippingId"
  >
    <div style="display: flex; justify-content: space-between;">
      <span>{{ item.displayLabel }}</span>
      <el-tag
        size="small"
        :type="item.returnStatus === '无退货' ? 'success' : 'warning'"
        style="margin-left: 8px;"
      >
        {{ item.returnStatus }}
      </el-tag>
    </div>
  </el-option>
</el-select>
```
---
## é€€è´§çŠ¶æ€è®¡ç®—é€»è¾‘
| æ¡ä»¶ | çŠ¶æ€ |
|------|------|
| é€€è´§æ•°é‡ = 0 | æ— é€€è´§ |
| é€€è´§æ•°é‡ >= å‘货数量 | å…¨éƒ¨é€€è´§ |
| é€€è´§æ•°é‡ > 0 ä¸” < å‘货数量 | éƒ¨åˆ†é€€è´§ |
---
## æ³¨æ„äº‹é¡¹
1. **销售台账的退货数量**:是按销售合同维度的汇总,即该合同下所有发货单的累计退货数量
2. **发货台账的退货数量**:是按单条发货记录维度的退货数量
3. **前端展示建议**:
   - æ— é€€è´§ï¼šç»¿è‰²æ ‡ç­¾ (`success`)
   - éƒ¨åˆ†é€€è´§ï¼šé»„色标签 (`warning`)
   - å…¨éƒ¨é€€è´§ï¼šçº¢è‰²æ ‡ç­¾ (`danger`)
4. **销售退货选择发货信息时**:前端可以根据 `returnStatus` è¿‡æ»¤æˆ–提示用户,避免选择已全部退货的发货记录
docs/20260618_shipping_info_for_return.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,365 @@
# é”€å”®é€€è´§å‘货信息接口优化前端联调文档
> ä¼˜åŒ–销售退货场景下的发货信息查询接口,新增批次号字段,并提供组合显示标签
## æ¶‰åŠé¡µé¢
- é”€å”®ç®¡ç† / é”€å”®é€€è´§ - æ–°å¢ž/编辑退货单页面
- é”€å”®é€€è´§ - é€‰æ‹©å‘货单下拉框
## API
### æ–°å¢žæŽ¥å£ï¼šé”€å”®é€€è´§-发货信息查询
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /shippingInfo/getForReturn | é”€å”®é€€è´§-通过客户名称查询发货信息(含批次号) |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| customerName | String | æ˜¯ | å®¢æˆ·åç§° |
**响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| shippingId | Long | å‘货单ID |
| shippingNo | String | å‘货单号 |
| salesContractNo | String | é”€å”®åˆåŒå· |
| customerName | String | å®¢æˆ·åç§° |
| productName | String | äº§å“åç§° |
| model | String | è§„格型号 |
| shippingQuantity | BigDecimal | å‘货数量 |
| batchNo | String | æ‰¹æ¬¡å· |
| shippingCarNumber | String | è½¦ç‰Œå· |
| createTime | LocalDateTime | åˆ›å»ºæ—¶é—´ |
| displayLabel | String | æ˜¾ç¤ºæ ‡ç­¾ï¼ˆæ‰¹æ¬¡å·-车牌号-创建时间) |
**响应示例:**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": [
    {
      "shippingId": 100,
      "shippingNo": "FH-2026-001",
      "salesContractNo": "HT-2026-001",
      "customerName": "客户A",
      "productName": "产品A",
      "model": "规格1",
      "shippingQuantity": 50,
      "batchNo": "20260618001",
      "shippingCarNumber": "京A12345",
      "createTime": "2026-06-18 10:30:00",
      "displayLabel": "20260618001-京A12345-2026-06-18"
    },
    {
      "shippingId": 101,
      "shippingNo": "FH-2026-002",
      "salesContractNo": "HT-2026-002",
      "customerName": "客户A",
      "productName": "产品B",
      "model": "规格2",
      "shippingQuantity": 30,
      "batchNo": "20260618002",
      "shippingCarNumber": "京B67890",
      "createTime": "2026-06-17 14:20:00",
      "displayLabel": "20260618002-京B67890-2026-06-17"
    }
  ]
}
```
---
## åŽŸæœ‰æŽ¥å£ï¼ˆä¿ç•™ä¸å˜ï¼‰
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /shippingInfo/getByCustomerName | é€šè¿‡å®¢æˆ·åç§°æŸ¥è¯¢å…³è”的发货单号(原有接口) |
---
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. é”€å”®é€€è´§é¡µé¢ - å‘货单选择下拉框
**修改前:**
```html
<el-form-item label="发货单号">
  <el-select v-model="form.shippingId" placeholder="请选择发货单号">
    <el-option
      v-for="item in shippingList"
      :key="item.id"
      :label="item.shippingNo"
      :value="item.id"
    />
  </el-select>
</el-form-item>
```
**修改后:**
```html
<el-form-item label="发货单号">
  <el-select
    v-model="form.shippingId"
    placeholder="请选择发货单号"
    filterable
    @change="onShippingChange"
  >
    <el-option
      v-for="item in shippingList"
      :key="item.shippingId"
      :label="item.displayLabel"
      :value="item.shippingId"
    >
      <span style="float: left">{{ item.displayLabel }}</span>
      <span style="float: right; color: #8492a6; font-size: 12px">
        {{ item.productName }} - {{ item.shippingQuantity }}{{ item.model ? '(' + item.model + ')' : '' }}
      </span>
    </el-option>
  </el-select>
</el-form-item>
```
### 2. data æ•°æ®
```js
data() {
  return {
    form: {
      shippingId: null,
      batchNo: '',
      shippingCarNumber: '',
      productName: '',
      model: '',
      shippingQuantity: null,
    },
    shippingList: [],
  }
}
```
### 3. methods æ–¹æ³•
```js
methods: {
  // æŸ¥è¯¢å‘货信息列表
  getShippingList(customerName) {
    if (!customerName) {
      this.shippingList = [];
      return;
    }
    this.$axios.get('/shippingInfo/getForReturn', {
      params: { customerName }
    }).then(res => {
      this.shippingList = res.data || [];
    });
  },
  // å‘货单选择变化
  onShippingChange(shippingId) {
    const selected = this.shippingList.find(item => item.shippingId === shippingId);
    if (selected) {
      // å›žæ˜¾æ•°æ®
      this.form.batchNo = selected.batchNo || '';
      this.form.shippingCarNumber = selected.shippingCarNumber || '';
      this.form.productName = selected.productName || '';
      this.form.model = selected.model || '';
      this.form.shippingQuantity = selected.shippingQuantity || 0;
      this.form.salesContractNo = selected.salesContractNo || '';
      this.form.shippingNo = selected.shippingNo || '';
    }
  },
}
```
### 4. å®Œæ•´ç¤ºä¾‹ï¼ˆé”€å”®é€€è´§è¡¨å•)
```html
<template>
  <div class="app-container">
    <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
      <!-- å®¢æˆ·é€‰æ‹© -->
      <el-form-item label="客户名称" prop="customerName">
        <el-select
          v-model="form.customerName"
          placeholder="请选择客户"
          filterable
          @change="onCustomerChange"
        >
          <el-option
            v-for="item in customerList"
            :key="item.id"
            :label="item.customerName"
            :value="item.customerName"
          />
        </el-select>
      </el-form-item>
      <!-- å‘货单选择 -->
      <el-form-item label="发货信息" prop="shippingId">
        <el-select
          v-model="form.shippingId"
          placeholder="请选择发货单号"
          filterable
          :disabled="!form.customerName"
          @change="onShippingChange"
        >
          <el-option
            v-for="item in shippingList"
            :key="item.shippingId"
            :label="item.displayLabel"
            :value="item.shippingId"
          >
            <div style="display: flex; justify-content: space-between;">
              <span>{{ item.displayLabel }}</span>
              <span style="color: #909399; font-size: 12px">
                {{ item.productName }}
              </span>
            </div>
          </el-option>
        </el-select>
      </el-form-item>
      <!-- å›žæ˜¾ä¿¡æ¯ï¼ˆåªè¯»ï¼‰ -->
      <el-row :gutter="20">
        <el-col :span="8">
          <el-form-item label="批次号">
            <el-input v-model="form.batchNo" disabled />
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item label="车牌号">
            <el-input v-model="form.shippingCarNumber" disabled />
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item label="产品名称">
            <el-input v-model="form.productName" disabled />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row :gutter="20">
        <el-col :span="8">
          <el-form-item label="规格型号">
            <el-input v-model="form.model" disabled />
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item label="发货数量">
            <el-input v-model="form.shippingQuantity" disabled />
          </el-form-item>
        </el-col>
        <el-col :span="8">
          <el-form-item label="销售合同号">
            <el-input v-model="form.salesContractNo" disabled />
          </el-form-item>
        </el-col>
      </el-row>
      <!-- å…¶ä»–退货表单字段... -->
    </el-form>
  </div>
</template>
<script>
export default {
  name: 'SalesReturnForm',
  data() {
    return {
      form: {
        customerName: '',
        shippingId: null,
        batchNo: '',
        shippingCarNumber: '',
        productName: '',
        model: '',
        shippingQuantity: null,
        salesContractNo: '',
        shippingNo: '',
      },
      rules: {
        customerName: [{ required: true, message: '请选择客户', trigger: 'change' }],
        shippingId: [{ required: true, message: '请选择发货信息', trigger: 'change' }],
      },
      customerList: [],
      shippingList: [],
    };
  },
  methods: {
    // å®¢æˆ·é€‰æ‹©å˜åŒ–
    onCustomerChange(customerName) {
      // æ¸…空发货单选择
      this.form.shippingId = null;
      this.clearShippingInfo();
      // æŸ¥è¯¢å‘货信息
      this.getShippingList(customerName);
    },
    // æŸ¥è¯¢å‘货信息列表
    getShippingList(customerName) {
      if (!customerName) {
        this.shippingList = [];
        return;
      }
      this.$axios.get('/shippingInfo/getForReturn', {
        params: { customerName }
      }).then(res => {
        this.shippingList = res.data || [];
      });
    },
    // å‘货单选择变化
    onShippingChange(shippingId) {
      const selected = this.shippingList.find(item => item.shippingId === shippingId);
      if (selected) {
        this.form.batchNo = selected.batchNo || '';
        this.form.shippingCarNumber = selected.shippingCarNumber || '';
        this.form.productName = selected.productName || '';
        this.form.model = selected.model || '';
        this.form.shippingQuantity = selected.shippingQuantity;
        this.form.salesContractNo = selected.salesContractNo || '';
        this.form.shippingNo = selected.shippingNo || '';
      } else {
        this.clearShippingInfo();
      }
    },
    // æ¸…空发货信息
    clearShippingInfo() {
      this.form.batchNo = '';
      this.form.shippingCarNumber = '';
      this.form.productName = '';
      this.form.model = '';
      this.form.shippingQuantity = null;
      this.form.salesContractNo = '';
      this.form.shippingNo = '';
    },
  },
};
</script>
```
---
## æ³¨æ„äº‹é¡¹
1. **新接口 vs åŽŸæŽ¥å£**:
   - `getForReturn`(新增):专门用于销售退货场景,返回批次号和组合显示标签
   - `getByCustomerName`(原有):保持不变,其他场景可继续使用
2. **displayLabel æ ¼å¼**:`批次号-车牌号-创建时间(yyyy-MM-dd)`
   - ç¤ºä¾‹ï¼š`20260618001-京A12345-2026-06-18`
   - å¦‚果某字段为空,会显示空字符串,如 `--2026-06-18`
3. **数据回显**:选择发货单后,批次号、车牌号、产品名称等字段自动填充到表单中,前端只需展示即可
4. **客户切换**:切换客户时,需清空已选的发货单和相关回显数据
5. **下拉筛选**:建议开启 `filterable` å±žæ€§ï¼Œæ–¹ä¾¿ç”¨æˆ·é€šè¿‡æ‰¹æ¬¡å·æˆ–车牌号快速筛选
docs/master_contract_no.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,161 @@
# é‡‡è´­ã€é”€å”®å°è´¦å¢žåŠ æ€»åˆåŒå·å­—æ®µ
## æ¶‰åŠé¡µé¢
- é‡‡è´­å°è´¦ç®¡ç†é¡µé¢
- é”€å”®å°è´¦ç®¡ç†é¡µé¢
## æ•°æ®åº“变更
执行 `docs/master_contract_no.sql` è„šæœ¬ï¼Œæ–°å¢žä»¥ä¸‹å­—æ®µï¼š
### purchase_ledger(采购台账表)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| master_contract_no | VARCHAR(100) | æ€»åˆåŒå· |
### sales_ledger(销售台账表)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| master_contract_no | VARCHAR(100) | æ€»åˆåŒå· |
## API
### é‡‡è´­å°è´¦åˆ†é¡µæŸ¥è¯¢
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /purchase/purchaseLedger/listPage | é‡‡è´­å°è´¦åˆ†é¡µæŸ¥è¯¢ |
**新增请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| masterContractNo | String | å¦ | æ€»åˆåŒå·ï¼ˆæ¨¡ç³ŠæŸ¥è¯¢ï¼‰ |
**响应变更:**
响应数据中新增 `masterContractNo` å­—段。
### é”€å”®å°è´¦åˆ†é¡µæŸ¥è¯¢
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /sales/salesLedger/listPage | é”€å”®å°è´¦åˆ†é¡µæŸ¥è¯¢ |
**新增请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| masterContractNo | String | å¦ | æ€»åˆåŒå·ï¼ˆæ¨¡ç³ŠæŸ¥è¯¢ï¼‰ |
**响应变更:**
响应数据中新增 `masterContractNo` å’Œ `purchaseMasterContractNo` å­—段。
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. é‡‡è´­å°è´¦ç®¡ç†é¡µé¢
#### æŸ¥è¯¢è¡¨å•增加字段
```html
<el-form-item label="总合同号">
  <el-input
    v-model="queryParams.masterContractNo"
    placeholder="请输入总合同号"
    clearable
    @keyup.enter="handleQuery"
  />
</el-form-item>
```
#### è¡¨æ ¼å¢žåŠ åˆ—
```html
<el-table-column prop="masterContractNo" label="总合同号" min-width="150" show-overflow-tooltip />
```
#### æ–°å¢ž/编辑表单增加字段
```html
<el-form-item label="总合同号" prop="masterContractNo">
  <el-input v-model="form.masterContractNo" placeholder="请输入总合同号" />
</el-form-item>
```
#### data æ•°æ®
```js
data() {
  return {
    queryParams: {
      // ... åŽŸæœ‰å­—æ®µ
      masterContractNo: ''
    },
    form: {
      // ... åŽŸæœ‰å­—æ®µ
      masterContractNo: ''
    }
  }
}
```
### 2. é”€å”®å°è´¦ç®¡ç†é¡µé¢
#### æŸ¥è¯¢è¡¨å•增加字段
```html
<el-form-item label="总合同号">
  <el-input
    v-model="queryParams.masterContractNo"
    placeholder="请输入总合同号"
    clearable
    @keyup.enter="handleQuery"
  />
</el-form-item>
```
#### è¡¨æ ¼å¢žåŠ åˆ—
```html
<el-table-column prop="masterContractNo" label="销售总合同号" min-width="150" show-overflow-tooltip />
<el-table-column prop="purchaseMasterContractNo" label="采购总合同号" min-width="150" show-overflow-tooltip />
```
#### æ–°å¢ž/编辑表单增加字段
```html
<el-form-item label="总合同号" prop="masterContractNo">
  <el-input v-model="form.masterContractNo" placeholder="请输入总合同号" />
</el-form-item>
```
#### data æ•°æ®
```js
data() {
  return {
    queryParams: {
      // ... åŽŸæœ‰å­—æ®µ
      masterContractNo: ''
    },
    form: {
      // ... åŽŸæœ‰å­—æ®µ
      masterContractNo: ''
    }
  }
}
```
## å¯¼å‡ºåŠŸèƒ½
导出 Excel æ—¶ä¼šè‡ªåŠ¨åŒ…å«æ€»åˆåŒå·å­—æ®µï¼Œæ— éœ€é¢å¤–ä¿®æ”¹ã€‚
## æ³¨æ„äº‹é¡¹
- æ€»åˆåŒå·æ”¯æŒæ¨¡ç³ŠæŸ¥è¯¢ï¼Œè¾“入部分内容即可查询
- é”€å”®å°è´¦åˆ—表页面会同时展示销售总合同号和关联采购台账的总合同号
- æ–°å¢žå’Œç¼–辑时总合同号为非必填项,用户可根据实际情况填写
docs/master_contract_no.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
-- =====================================================
-- é‡‡è´­å°è´¦ã€é”€å”®å°è´¦å¢žåŠ æ€»åˆåŒå·å­—æ®µ
-- æ‰§è¡Œæ—¶é—´ï¼š2026-06-22
-- åŠŸèƒ½è¯´æ˜Žï¼š
--   1. é‡‡è´­å°è´¦å¢žåŠ æ€»åˆåŒå·å­—æ®µ
--   2. é”€å”®å°è´¦å¢žåŠ æ€»åˆåŒå·å­—æ®µ
-- =====================================================
-- 1. é‡‡è´­å°è´¦å¢žåŠ æ€»åˆåŒå·å­—æ®µ
ALTER TABLE purchase_ledger
ADD COLUMN master_contract_no VARCHAR(100) DEFAULT NULL COMMENT '总合同号' AFTER purchase_contract_number;
-- 2. é”€å”®å°è´¦å¢žåŠ æ€»åˆåŒå·å­—æ®µ
ALTER TABLE sales_ledger
ADD COLUMN master_contract_no VARCHAR(100) DEFAULT NULL COMMENT '总合同号' AFTER sales_contract_no;
-- =====================================================
-- å›žæ»šè„šæœ¬ï¼ˆå¦‚éœ€å›žæ»šï¼Œè¯·æ‰§è¡Œä»¥ä¸‹è¯­å¥ï¼‰
-- =====================================================
-- ALTER TABLE purchase_ledger DROP COLUMN master_contract_no;
-- ALTER TABLE sales_ledger DROP COLUMN master_contract_no;
docs/quality_auto_judge.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,385 @@
# è´¨æ£€å‚数项自动判断功能
## æ¶‰åŠé¡µé¢
- è´¨æ£€æŒ‡æ ‡ç»´æŠ¤é¡µé¢ï¼ˆæ£€æµ‹æ ‡å‡†å‚数管理)
- è´¨æ£€å•填写页面(原材料检验、过程检验、出厂检验)
- è´¨æ£€å•详情页面
## æ•°æ®åº“变更
执行 `docs/quality_auto_judge.sql` è„šæœ¬ï¼Œæ–°å¢žä»¥ä¸‹å­—æ®µï¼š
### quality_test_standard_param(检测标准参数表)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| judge_type | VARCHAR(20) | åˆ¤æ–­ç±»åž‹ï¼šå¤§äºŽç­‰äºŽ/小于等于/范围/文字描述 |
| is_required | TINYINT(1) | æ˜¯å¦å¿…要判断:0-否,1-是 |
| min_value | DECIMAL(20,4) | èŒƒå›´ä¸‹é™å€¼ï¼ˆèŒƒå›´ç±»åž‹ä½¿ç”¨ï¼‰ |
| max_value | DECIMAL(20,4) | èŒƒå›´ä¸Šé™å€¼ï¼ˆèŒƒå›´ç±»åž‹ä½¿ç”¨ï¼‰ |
### quality_inspect_param(检验记录参数表)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| judge_type | VARCHAR(20) | åˆ¤æ–­ç±»åž‹ |
| is_required | TINYINT(1) | æ˜¯å¦å¿…要判断 |
| is_qualified | TINYINT(1) | å•项是否合格:0-不合格,1-合格,null-未判断 |
| min_value | DECIMAL(20,4) | èŒƒå›´ä¸‹é™å€¼ |
| max_value | DECIMAL(20,4) | èŒƒå›´ä¸Šé™å€¼ |
### quality_inspect(质检主表)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| auto_judge_result | VARCHAR(20) | è‡ªåŠ¨åˆ¤æ–­ç»“æžœï¼šåˆæ ¼/不合格/null(需手动判断) |
## API
### 1. å•参数项自动判断
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /quality/qualityInspect/autoJudge | å•参数项自动判断(实时调用) |
**请求参数:**
```json
{
  "testValue": "100",
  "standardValue": "80",
  "minValue": null,
  "maxValue": null,
  "judgeType": "大于等于"
}
```
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| testValue | String | æ˜¯ | æ£€éªŒå€¼ |
| standardValue | String | å¦ | æ ‡å‡†å€¼ï¼ˆå¤§äºŽç­‰äºŽã€å°äºŽç­‰äºŽç±»åž‹ä½¿ç”¨ï¼‰ |
| minValue | BigDecimal | å¦ | èŒƒå›´ä¸‹é™å€¼ï¼ˆèŒƒå›´ç±»åž‹ä½¿ç”¨ï¼‰ |
| maxValue | BigDecimal | å¦ | èŒƒå›´ä¸Šé™å€¼ï¼ˆèŒƒå›´ç±»åž‹ä½¿ç”¨ï¼‰ |
| judgeType | String | æ˜¯ | åˆ¤æ–­ç±»åž‹ï¼šå¤§äºŽç­‰äºŽ/小于等于/范围/文字描述 |
**响应:**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "isQualified": true,
    "message": "检验值100 >= æ ‡å‡†å€¼80,合格"
  }
}
```
### 2. æ•´ä½“质检单参数项自动判断
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /quality/qualityInspect/autoJudgeAll/{inspectId} | æ•´ä½“质检单参数项自动判断 |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| inspectId | Long | æ˜¯ | è´¨æ£€å•ID(路径参数) |
**响应:**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "autoJudgeResult": "不合格",
    "hasRequiredUnqualified": true,
    "allTextDescription": false,
    "paramResults": [
      {
        "paramId": 1,
        "parameterItem": "水分含量",
        "isQualified": true,
        "message": "检验值12 <= æ ‡å‡†å€¼15,合格",
        "isRequired": true
      },
      {
        "paramId": 2,
        "parameterItem": "蛋白质含量",
        "isQualified": false,
        "message": "检验值18 >= æ ‡å‡†å€¼20,不合格",
        "isRequired": true
      },
      {
        "paramId": 3,
        "parameterItem": "外观",
        "isQualified": null,
        "message": "文字描述类型需手动判断",
        "isRequired": false
      }
    ]
  }
}
```
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. è´¨æ£€æŒ‡æ ‡ç»´æŠ¤é¡µé¢
#### è¡¨å•增加字段
```html
<el-form-item label="判断类型">
  <el-select v-model="form.judgeType" placeholder="请选择判断类型" @change="handleJudgeTypeChange">
    <el-option label="大于等于" value="大于等于"></el-option>
    <el-option label="小于等于" value="小于等于"></el-option>
    <el-option label="范围" value="范围"></el-option>
    <el-option label="文字描述" value="文字描述"></el-option>
  </el-select>
</el-form-item>
<el-form-item label="是否必要判断">
  <el-switch v-model="form.isRequired" :active-value="1" :inactive-value="0"></el-switch>
</el-form-item>
<!-- èŒƒå›´ç±»åž‹æ—¶æ˜¾ç¤º -->
<el-form-item label="范围下限值" v-if="form.judgeType === '范围'">
  <el-input-number v-model="form.minValue" :precision="4" :min="0"></el-input-number>
</el-form-item>
<el-form-item label="范围上限值" v-if="form.judgeType === '范围'">
  <el-input-number v-model="form.maxValue" :precision="4" :min="0"></el-input-number>
</el-form-item>
```
#### data æ•°æ®
```js
data() {
  return {
    form: {
      // ... åŽŸæœ‰å­—æ®µ
      judgeType: null,
      isRequired: 0,
      minValue: null,
      maxValue: null
    }
  }
}
```
#### æ–¹æ³•
```js
methods: {
  // åˆ¤æ–­ç±»åž‹å˜æ›´æ—¶ï¼Œæ¸…空范围值
  handleJudgeTypeChange(val) {
    if (val !== '范围') {
      this.form.minValue = null;
      this.form.maxValue = null;
    }
  }
}
```
### 2. è´¨æ£€å•填写页面
#### å‚数项表格增加列
```html
<el-table-column prop="judgeType" label="判断类型" width="100">
  <template #default="{ row }">
    <el-tag v-if="row.judgeType === '大于等于'" type="success">≥</el-tag>
    <el-tag v-else-if="row.judgeType === '小于等于'" type="warning">≤</el-tag>
    <el-tag v-else-if="row.judgeType === '范围'" type="info">范围</el-tag>
    <el-tag v-else type="">文字</el-tag>
  </template>
</el-table-column>
<el-table-column prop="isRequired" label="必要判断" width="80">
  <template #default="{ row }">
    <el-tag v-if="row.isRequired === 1" type="danger">是</el-tag>
    <el-tag v-else type="info">否</el-tag>
  </template>
</el-table-column>
<el-table-column prop="testValue" label="检验值" width="150">
  <template #default="{ row }">
    <el-input
      v-model="row.testValue"
      placeholder="请输入检验值"
      @change="handleTestValueChange(row)"
    ></el-input>
  </template>
</el-table-column>
<el-table-column prop="isQualified" label="是否合格" width="100">
  <template #default="{ row }">
    <!-- éžæ–‡å­—描述类型显示自动判断结果 -->
    <template v-if="row.judgeType !== '文字描述'">
      <el-tag v-if="row.isQualified === 1" type="success">合格</el-tag>
      <el-tag v-else-if="row.isQualified === 0" type="danger">不合格</el-tag>
      <span v-else>-</span>
    </template>
    <!-- æ–‡å­—描述类型允许用户选择 -->
    <template v-else>
      <el-select v-model="row.isQualified" placeholder="请选择" size="small">
        <el-option :value="1" label="合格"></el-option>
        <el-option :value="0" label="不合格"></el-option>
      </el-select>
    </template>
  </template>
</el-table-column>
```
#### æ•´ä½“判断结果显示
```html
<el-form-item label="自动判断结果">
  <template v-if="autoJudgeResult">
    <el-tag v-if="autoJudgeResult === '合格'" type="success" size="large">合格</el-tag>
    <el-tag v-else type="danger" size="large">不合格</el-tag>
    <span style="margin-left: 10px; color: #909399;">
      ï¼ˆå¿…要判断参数有不合格项,不可手动修改)
    </span>
  </template>
  <template v-else>
    <el-tag type="info" size="large">需手动判断</el-tag>
    <span style="margin-left: 10px; color: #909399;">
      ï¼ˆæ‰€æœ‰å‚数均为文字描述类型,请手动选择)
    </span>
  </template>
</el-form-item>
<el-form-item label="最终判定" v-if="!autoJudgeResult || autoJudgeResult === '合格'">
  <el-radio-group v-model="form.checkResult" :disabled="autoJudgeResult === '不合格'">
    <el-radio label="合格">合格</el-radio>
    <el-radio label="不合格">不合格</el-radio>
    <el-radio label="部分合格">部分合格</el-radio>
  </el-radio-group>
</el-form-item>
```
#### data æ•°æ®
```js
data() {
  return {
    form: {
      // ... åŽŸæœ‰å­—æ®µ
      autoJudgeResult: null
    },
    autoJudgeResult: null
  }
}
```
#### æ–¹æ³•
```js
methods: {
  // æ£€éªŒå€¼å˜æ›´æ—¶å®žæ—¶è‡ªåŠ¨åˆ¤æ–­
  async handleTestValueChange(row) {
    if (!row.testValue || !row.judgeType || row.judgeType === '文字描述') {
      return;
    }
    try {
      const res = await autoJudge({
        testValue: row.testValue,
        standardValue: row.standardValue,
        minValue: row.minValue,
        maxValue: row.maxValue,
        judgeType: row.judgeType
      });
      if (res.code === 200) {
        row.isQualified = res.data.isQualified ? 1 : 0;
        this.$message.success(res.data.message);
        // è§¦å‘整体判断
        this.autoJudgeAll();
      }
    } catch (error) {
      console.error('自动判断失败', error);
    }
  },
  // æ•´ä½“自动判断
  async autoJudgeAll() {
    try {
      const res = await autoJudgeAll(this.form.id);
      if (res.code === 200) {
        this.autoJudgeResult = res.data.autoJudgeResult;
        this.form.autoJudgeResult = res.data.autoJudgeResult;
        // æ›´æ–°å‚数项的判断结果
        res.data.paramResults.forEach(item => {
          const param = this.form.qualityInspectParams.find(p => p.id === item.paramId);
          if (param) {
            param.isQualified = item.isQualified ? 1 : (item.isQualified === false ? 0 : null);
          }
        });
        // å¦‚果必要判断参数不合格,自动设置检测结果
        if (res.data.hasRequiredUnqualified) {
          this.form.checkResult = '不合格';
        }
      }
    } catch (error) {
      console.error('整体判断失败', error);
    }
  }
}
```
### 3. API è¯·æ±‚方法
```js
// api/quality.js
// å•参数项自动判断
export function autoJudge(data) {
  return request({
    url: '/quality/qualityInspect/autoJudge',
    method: 'post',
    data
  })
}
// æ•´ä½“自动判断
export function autoJudgeAll(inspectId) {
  return request({
    url: `/quality/qualityInspect/autoJudgeAll/${inspectId}`,
    method: 'get'
  })
}
```
## åˆ¤æ–­ç±»åž‹è¯´æ˜Ž
| åˆ¤æ–­ç±»åž‹ | ç¬¦å· | åˆ¤æ–­é€»è¾‘ | ä½¿ç”¨å­—段 |
|----------|------|----------|----------|
| å¤§äºŽç­‰äºŽ | â‰¥ | testValue >= standardValue | standardValue |
| å°äºŽç­‰äºŽ | â‰¤ | testValue <= standardValue | standardValue |
| èŒƒå›´ | - | minValue <= testValue <= maxValue | minValue, maxValue |
| æ–‡å­—描述 | - | ä¸è‡ªåŠ¨åˆ¤æ–­ï¼Œç”¨æˆ·æ‰‹åŠ¨é€‰æ‹© | æ—  |
## ä¸šåŠ¡é€»è¾‘è¯´æ˜Ž
1. **自动判断触发时机**:用户填写检验值后实时自动判断
2. **必要判断参数**:`isRequired = 1` çš„参数项为必要判断参数
3. **整体判断逻辑**:
   - å¦‚果有任一必要判断参数不合格 â†’ æ•´ä½“判定为"不合格",禁用用户修改
   - å¦‚果所有必要判断参数都合格 â†’ æ•´ä½“判定为"合格"
   - å¦‚果全是文字描述类型 â†’ `autoJudgeResult = null`,允许用户手动选择
4. **文字描述类型**:不进行自动判断,由用户手动选择"合格"或"不合格"
## æ³¨æ„äº‹é¡¹
- èŒƒå›´ç±»åž‹å¿…须填写下限值和上限值,否则无法判断
- æ ‡å‡†å€¼å’Œæ£€éªŒå€¼éœ€ä¸ºæœ‰æ•ˆæ•°å€¼ï¼Œå¦åˆ™æ— æ³•判断
- å¿…要判断参数不合格时,最终判定强制为"不合格",不可手动修改
- è´¨æ£€å•提交时,系统会根据自动判断结果进行校验
docs/quality_auto_judge.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
-- =====================================================
-- è´¨æ£€å‚数项自动判断功能 SQL å˜æ›´è„šæœ¬
-- æ‰§è¡Œæ—¶é—´ï¼š2026-06-22
-- åŠŸèƒ½è¯´æ˜Žï¼š
--   1. æ£€æµ‹æ ‡å‡†å‚数表增加判断类型、是否必要判断、范围上下限字段
--   2. æ£€éªŒè®°å½•参数表增加相应字段
--   3. è´¨æ£€ä¸»è¡¨å¢žåŠ è‡ªåŠ¨åˆ¤æ–­ç»“æžœå­—æ®µ
-- =====================================================
-- 1. æ£€æµ‹æ ‡å‡†å‚数表增加判断类型、是否必要判断、范围上下限
ALTER TABLE quality_test_standard_param
ADD COLUMN judge_type VARCHAR(20) DEFAULT NULL COMMENT '判断类型:大于等于/小于等于/范围/文字描述' AFTER default_value;
ALTER TABLE quality_test_standard_param
ADD COLUMN is_required TINYINT(1) DEFAULT 0 COMMENT '是否必要判断:0-否,1-是' AFTER judge_type;
ALTER TABLE quality_test_standard_param
ADD COLUMN min_value DECIMAL(20,4) DEFAULT NULL COMMENT '范围下限值(范围类型使用)' AFTER is_required;
ALTER TABLE quality_test_standard_param
ADD COLUMN max_value DECIMAL(20,4) DEFAULT NULL COMMENT '范围上限值(范围类型使用)' AFTER min_value;
-- 2. æ£€éªŒè®°å½•参数表增加相应字段
ALTER TABLE quality_inspect_param
ADD COLUMN judge_type VARCHAR(20) DEFAULT NULL COMMENT '判断类型' AFTER test_value;
ALTER TABLE quality_inspect_param
ADD COLUMN is_required TINYINT(1) DEFAULT 0 COMMENT '是否必要判断' AFTER judge_type;
ALTER TABLE quality_inspect_param
ADD COLUMN is_qualified TINYINT(1) DEFAULT NULL COMMENT '单项是否合格:0-不合格,1-合格,null-未判断' AFTER is_required;
ALTER TABLE quality_inspect_param
ADD COLUMN min_value DECIMAL(20,4) DEFAULT NULL COMMENT '范围下限值' AFTER is_qualified;
ALTER TABLE quality_inspect_param
ADD COLUMN max_value DECIMAL(20,4) DEFAULT NULL COMMENT '范围上限值' AFTER min_value;
-- 3. è´¨æ£€ä¸»è¡¨å¢žåŠ è‡ªåŠ¨åˆ¤æ–­ç»“æžœ
ALTER TABLE quality_inspect
ADD COLUMN auto_judge_result VARCHAR(20) DEFAULT NULL COMMENT '自动判断结果:合格/不合格/null(需手动判断)' AFTER sample_quantity;
-- =====================================================
-- å›žæ»šè„šæœ¬ï¼ˆå¦‚éœ€å›žæ»šï¼Œè¯·æ‰§è¡Œä»¥ä¸‹è¯­å¥ï¼‰
-- =====================================================
-- ALTER TABLE quality_test_standard_param DROP COLUMN judge_type;
-- ALTER TABLE quality_test_standard_param DROP COLUMN is_required;
-- ALTER TABLE quality_test_standard_param DROP COLUMN min_value;
-- ALTER TABLE quality_test_standard_param DROP COLUMN max_value;
-- ALTER TABLE quality_inspect_param DROP COLUMN judge_type;
-- ALTER TABLE quality_inspect_param DROP COLUMN is_required;
-- ALTER TABLE quality_inspect_param DROP COLUMN is_qualified;
-- ALTER TABLE quality_inspect_param DROP COLUMN min_value;
-- ALTER TABLE quality_inspect_param DROP COLUMN max_value;
-- ALTER TABLE quality_inspect DROP COLUMN auto_judge_result;
src/main/java/com/ruoyi/basic/mapper/CustomerMapper.java
@@ -8,6 +8,9 @@
import com.ruoyi.basic.vo.CustomerVo;
import com.ruoyi.sales.vo.CustomerTransactionsDetailsVo;
import com.ruoyi.sales.vo.CustomerTransactionsVo;
import com.ruoyi.sales.vo.CustomerTransactionsProductVo;
import com.ruoyi.sales.vo.CustomerTransactionsShipmentVo;
import com.ruoyi.sales.vo.CustomerTransactionsSummaryVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -77,4 +80,21 @@
    IPage<CustomerTransactionsVo> customewTransactions(Page page, @Param("customerName") String customerName);
    IPage<CustomerTransactionsDetailsVo> customewTransactionsDetails(Page page, @Param("customerId") Long customerId);
    /**
     * æŸ¥è¯¢å®¢æˆ·å¾€æ¥ç»Ÿè®¡æ±‡æ€»
     */
    CustomerTransactionsSummaryVo getCustomerTransactionsSummary(@Param("customerId") Long customerId);
    /**
     * æŸ¥è¯¢å®¢æˆ·å¾€æ¥äº§å“æ˜Žç»†
     */
    IPage<CustomerTransactionsProductVo> getCustomerTransactionsProducts(Page page,
            @Param("customerId") Long customerId, @Param("salesLedgerId") Long salesLedgerId);
    /**
     * æŸ¥è¯¢å®¢æˆ·å¾€æ¥å‘货明细
     */
    IPage<CustomerTransactionsShipmentVo> getCustomerTransactionsShipments(Page page,
            @Param("customerId") Long customerId, @Param("salesLedgerId") Long salesLedgerId);
}
src/main/java/com/ruoyi/basic/service/ICustomerService.java
@@ -9,6 +9,9 @@
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.sales.vo.CustomerTransactionsDetailsVo;
import com.ruoyi.sales.vo.CustomerTransactionsVo;
import com.ruoyi.sales.vo.CustomerTransactionsProductVo;
import com.ruoyi.sales.vo.CustomerTransactionsShipmentVo;
import com.ruoyi.sales.vo.CustomerTransactionsSummaryVo;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@@ -111,4 +114,29 @@
     * @return
     */
    IPage<CustomerTransactionsDetailsVo> customewTransactionsDetails(Page page, Long customerId);
    /**
     * æŸ¥è¯¢å®¢æˆ·å¾€æ¥ç»Ÿè®¡æ±‡æ€»ï¼ˆä¼˜åŒ–版)
     * @param customerId å®¢æˆ·ID
     * @return ç»Ÿè®¡æ±‡æ€»æ•°æ®
     */
    CustomerTransactionsSummaryVo getCustomerTransactionsSummary(Long customerId);
    /**
     * æŸ¥è¯¢å®¢æˆ·å¾€æ¥äº§å“æ˜Žç»†
     * @param page åˆ†é¡µå‚æ•°
     * @param customerId å®¢æˆ·ID
     * @param salesLedgerId é”€å”®å°è´¦ID(可选)
     * @return äº§å“æ˜Žç»†åˆ†é¡µæ•°æ®
     */
    IPage<CustomerTransactionsProductVo> getCustomerTransactionsProducts(Page page, Long customerId, Long salesLedgerId);
    /**
     * æŸ¥è¯¢å®¢æˆ·å¾€æ¥å‘货明细
     * @param page åˆ†é¡µå‚æ•°
     * @param customerId å®¢æˆ·ID
     * @param salesLedgerId é”€å”®å°è´¦ID(可选)
     * @return å‘货明细分页数据
     */
    IPage<CustomerTransactionsShipmentVo> getCustomerTransactionsShipments(Page page, Long customerId, Long salesLedgerId);
}
src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java
@@ -29,6 +29,9 @@
import com.ruoyi.sales.pojo.SalesQuotation;
import com.ruoyi.sales.vo.CustomerTransactionsDetailsVo;
import com.ruoyi.sales.vo.CustomerTransactionsVo;
import com.ruoyi.sales.vo.CustomerTransactionsProductVo;
import com.ruoyi.sales.vo.CustomerTransactionsShipmentVo;
import com.ruoyi.sales.vo.CustomerTransactionsSummaryVo;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
@@ -413,6 +416,21 @@
        return customerMapper.customewTransactionsDetails(page, customerId);
    }
    @Override
    public CustomerTransactionsSummaryVo getCustomerTransactionsSummary(Long customerId) {
        return customerMapper.getCustomerTransactionsSummary(customerId);
    }
    @Override
    public IPage<CustomerTransactionsProductVo> getCustomerTransactionsProducts(Page page, Long customerId, Long salesLedgerId) {
        return customerMapper.getCustomerTransactionsProducts(page, customerId, salesLedgerId);
    }
    @Override
    public IPage<CustomerTransactionsShipmentVo> getCustomerTransactionsShipments(Page page, Long customerId, Long salesLedgerId) {
        return customerMapper.getCustomerTransactionsShipments(page, customerId, salesLedgerId);
    }
    /**
     * ä¸‹åˆ’线命名转驼峰命名
     */
src/main/java/com/ruoyi/procurementrecord/bean/dto/ReturnManagementDto.java
@@ -4,6 +4,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
@@ -36,4 +37,13 @@
    @Schema(description = "销售产品对象数组")
    private List<ReturnSaleProductDto> returnSaleProducts;
    @Schema(description = "发货数量")
    private BigDecimal shippingQuantity;
    @Schema(description = "已退货数量")
    private BigDecimal returnedQuantity;
    @Schema(description = "退货状态:无退货/部分退货/全部退货")
    private String returnStatus;
}
src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java
@@ -38,6 +38,12 @@
    @Excel(name = "采购合同号")
    private String purchaseContractNumber;
    /**
     * æ€»åˆåŒå·
     */
    @Excel(name = "总合同号")
    private String masterContractNo;
    /**
src/main/java/com/ruoyi/purchase/pojo/PurchaseLedger.java
@@ -33,6 +33,12 @@
    private String purchaseContractNumber;
    /**
     * æ€»åˆåŒå·
     */
    @Excel(name = "总合同号")
    private String masterContractNo;
    /**
     * ä¾›åº”商名称id
     */
    private Long supplierId;
src/main/java/com/ruoyi/quality/controller/QualityInspectController.java
@@ -5,6 +5,9 @@
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.quality.dto.AutoJudgeAllResponse;
import com.ruoyi.quality.dto.AutoJudgeRequest;
import com.ruoyi.quality.dto.AutoJudgeResponse;
import com.ruoyi.quality.dto.BatchQuickInspectRequest;
import com.ruoyi.quality.dto.QualityInspectDto;
import com.ruoyi.quality.pojo.QualityInspect;
@@ -195,4 +198,24 @@
        String templatePath = "/static/" + template + ".doc";
        return R.ok(qualityInspectService.analyzeTemplate(templatePath));
    }
    /**
     * å•参数项自动判断
     */
    @PostMapping("/autoJudge")
    @Operation(summary = "单参数项自动判断")
    @Log(title = "单参数项自动判断", businessType = BusinessType.OTHER)
    public R<AutoJudgeResponse> autoJudge(@RequestBody AutoJudgeRequest request) {
        return R.ok(qualityInspectService.autoJudge(request));
    }
    /**
     * æ•´ä½“质检单参数项自动判断
     */
    @GetMapping("/autoJudgeAll/{inspectId}")
    @Operation(summary = "整体质检单参数项自动判断")
    @Log(title = "整体质检单参数项自动判断", businessType = BusinessType.OTHER)
    public R<AutoJudgeAllResponse> autoJudgeAll(@PathVariable("inspectId") Long inspectId) {
        return R.ok(qualityInspectService.autoJudgeAll(inspectId));
    }
}
src/main/java/com/ruoyi/quality/dto/AutoJudgeAllResponse.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package com.ruoyi.quality.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
/**
 * æ•´ä½“自动判断响应DTO
 */
@Data
@Schema(name = "AutoJudgeAllResponse", description = "整体自动判断响应")
public class AutoJudgeAllResponse {
    @Schema(description = "自动判断结果:合格/不合格/null(需手动判断)")
    private String autoJudgeResult;
    @Schema(description = "是否有必要判断参数不合格")
    private Boolean hasRequiredUnqualified;
    @Schema(description = "是否全是文字描述类型")
    private Boolean allTextDescription;
    @Schema(description = "参数项判断结果列表")
    private List<ParamJudgeResult> paramResults;
    @Data
    @Schema(name = "ParamJudgeResult", description = "参数项判断结果")
    public static class ParamJudgeResult {
        @Schema(description = "参数项ID")
        private Long paramId;
        @Schema(description = "参数项名称")
        private String parameterItem;
        @Schema(description = "是否合格:true-合格,false-不合格,null-未判断")
        private Boolean isQualified;
        @Schema(description = "判断说明")
        private String message;
        @Schema(description = "是否必要判断")
        private Boolean isRequired;
    }
}
src/main/java/com/ruoyi/quality/dto/AutoJudgeRequest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package com.ruoyi.quality.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
 * è‡ªåŠ¨åˆ¤æ–­è¯·æ±‚DTO
 */
@Data
@Schema(name = "AutoJudgeRequest", description = "自动判断请求")
public class AutoJudgeRequest {
    @Schema(description = "检验值")
    private String testValue;
    @Schema(description = "标准值(大于等于、小于等于类型使用)")
    private String standardValue;
    @Schema(description = "范围下限值(范围类型使用)")
    private BigDecimal minValue;
    @Schema(description = "范围上限值(范围类型使用)")
    private BigDecimal maxValue;
    @Schema(description = "判断类型:大于等于/小于等于/范围/文字描述")
    private String judgeType;
}
src/main/java/com/ruoyi/quality/dto/AutoJudgeResponse.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package com.ruoyi.quality.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
 * è‡ªåŠ¨åˆ¤æ–­å“åº”DTO
 */
@Data
@Schema(name = "AutoJudgeResponse", description = "自动判断响应")
public class AutoJudgeResponse {
    @Schema(description = "是否合格:true-合格,false-不合格,null-无法判断")
    private Boolean isQualified;
    @Schema(description = "判断说明")
    private String message;
    public AutoJudgeResponse() {
    }
    public AutoJudgeResponse(Boolean isQualified, String message) {
        this.isQualified = isQualified;
        this.message = message;
    }
}
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
@@ -182,6 +182,10 @@
    @Schema(description = "抽检数量, ä»…inspectRule=1时有效")
    private BigDecimal sampleQuantity;
    @Schema(description = "自动判断结果:合格/不合格/null(需手动判断)")
    @Excel(name = "自动判断结果")
    private String autoJudgeResult;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/quality/pojo/QualityInspectParam.java
@@ -56,6 +56,36 @@
    private String testValue;
    /**
     * åˆ¤æ–­ç±»åž‹ï¼šå¤§äºŽç­‰äºŽ/小于等于/范围/文字描述
     */
    @Excel(name = "判断类型")
    private String judgeType;
    /**
     * æ˜¯å¦å¿…要判断:0-否,1-是
     */
    @Excel(name = "是否必要判断")
    private Integer isRequired;
    /**
     * å•项是否合格:0-不合格,1-合格,null-未判断
     */
    @Excel(name = "单项是否合格")
    private Integer isQualified;
    /**
     * èŒƒå›´ä¸‹é™å€¼
     */
    @Excel(name = "范围下限值")
    private java.math.BigDecimal minValue;
    /**
     * èŒƒå›´ä¸Šé™å€¼
     */
    @Excel(name = "范围上限值")
    private java.math.BigDecimal maxValue;
    /**
     * æ£€éªŒID
     */
    @Excel(name = "检验ID")
src/main/java/com/ruoyi/quality/pojo/QualityTestStandardParam.java
@@ -68,6 +68,18 @@
    @Schema(description = "默认值")
    private String defaultValue;
    @Schema(description = "判断类型:大于等于/小于等于/范围/文字描述")
    private String judgeType;
    @Schema(description = "是否必要判断:0-否,1-是")
    private Integer isRequired;
    @Schema(description = "范围下限值(范围类型使用)")
    private java.math.BigDecimal minValue;
    @Schema(description = "范围上限值(范围类型使用)")
    private java.math.BigDecimal maxValue;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/quality/service/IQualityInspectService.java
@@ -4,6 +4,9 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.dto.AutoJudgeAllResponse;
import com.ruoyi.quality.dto.AutoJudgeRequest;
import com.ruoyi.quality.dto.AutoJudgeResponse;
import com.ruoyi.quality.dto.BatchQuickInspectRequest;
import com.ruoyi.quality.dto.QualityInspectDto;
import com.ruoyi.quality.pojo.QualityInspect;
@@ -44,4 +47,14 @@
     * åˆ†æžæ¨¡æ¿ç»“æž„
     */
    String analyzeTemplate(String templatePath);
    /**
     * å•参数项自动判断
     */
    AutoJudgeResponse autoJudge(AutoJudgeRequest request);
    /**
     * æ•´ä½“质检单参数项自动判断
     */
    AutoJudgeAllResponse autoJudgeAll(Long inspectId);
}
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -17,6 +17,9 @@
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.procurementrecord.service.ProcurementRecordService;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.quality.dto.AutoJudgeAllResponse;
import com.ruoyi.quality.dto.AutoJudgeRequest;
import com.ruoyi.quality.dto.AutoJudgeResponse;
import com.ruoyi.quality.dto.BatchQuickInspectRequest;
import com.ruoyi.quality.dto.QualityInspectDto;
import com.ruoyi.quality.mapper.QualityInspectMapper;
@@ -26,6 +29,7 @@
import com.ruoyi.quality.pojo.QualityInspectParam;
import com.ruoyi.quality.pojo.QualityUnqualified;
import com.ruoyi.quality.utils.QualityInspectTemplateExportHelper;
import com.ruoyi.quality.utils.QualityJudgeUtil;
import com.ruoyi.stock.pojo.StockInRecord;
import com.ruoyi.stock.service.StockInRecordService;
import com.ruoyi.quality.service.IQualityInspectParamService;
@@ -561,4 +565,91 @@
        return qualityInspectTemplateExportHelper.analyzeTemplate(templatePath);
    }
    @Override
    public AutoJudgeResponse autoJudge(AutoJudgeRequest request) {
        QualityJudgeUtil.JudgeResult result = QualityJudgeUtil.judge(
                request.getTestValue(),
                request.getStandardValue(),
                request.getMinValue(),
                request.getMaxValue(),
                request.getJudgeType()
        );
        return new AutoJudgeResponse(result.getQualified(), result.getMessage());
    }
    @Override
    public AutoJudgeAllResponse autoJudgeAll(Long inspectId) {
        List<QualityInspectParam> params = qualityInspectParamService.list(
                Wrappers.<QualityInspectParam>lambdaQuery().eq(QualityInspectParam::getInspectId, inspectId));
        AutoJudgeAllResponse response = new AutoJudgeAllResponse();
        List<AutoJudgeAllResponse.ParamJudgeResult> paramResults = new ArrayList<>();
        boolean hasRequiredUnqualified = false;
        boolean allTextDescription = true;
        boolean hasRequiredParam = false;
        for (QualityInspectParam param : params) {
            AutoJudgeAllResponse.ParamJudgeResult paramResult = new AutoJudgeAllResponse.ParamJudgeResult();
            paramResult.setParamId(param.getId());
            paramResult.setParameterItem(param.getParameterItem());
            paramResult.setIsRequired(param.getIsRequired() != null && param.getIsRequired() == 1);
            if (paramResult.getIsRequired()) {
                hasRequiredParam = true;
            }
            String judgeType = param.getJudgeType();
            if (QualityJudgeUtil.JUDGE_TYPE_TEXT_DESCRIPTION.equals(judgeType)) {
                paramResult.setIsQualified(null);
                paramResult.setMessage("文字描述类型需手动判断");
            } else {
                allTextDescription = false;
                QualityJudgeUtil.JudgeResult result = QualityJudgeUtil.judge(
                        param.getTestValue(),
                        param.getStandardValue(),
                        param.getMinValue(),
                        param.getMaxValue(),
                        judgeType
                );
                paramResult.setIsQualified(result.getQualified());
                paramResult.setMessage(result.getMessage());
                // æ›´æ–°å‚数项的判断结果
                if (result.getQualified() != null) {
                    param.setIsQualified(result.getQualified() ? 1 : 0);
                    qualityInspectParamService.updateById(param);
                }
                // æ£€æŸ¥å¿…要判断参数是否不合格
                if (paramResult.getIsRequired() && Boolean.FALSE.equals(result.getQualified())) {
                    hasRequiredUnqualified = true;
                }
            }
            paramResults.add(paramResult);
        }
        response.setParamResults(paramResults);
        response.setHasRequiredUnqualified(hasRequiredUnqualified);
        response.setAllTextDescription(allTextDescription);
        // ç¡®å®šæ•´ä½“判断结果
        if (allTextDescription) {
            response.setAutoJudgeResult(null);
        } else if (hasRequiredUnqualified) {
            response.setAutoJudgeResult("不合格");
        } else {
            // æ‰€æœ‰éœ€è¦è‡ªåŠ¨åˆ¤æ–­çš„å‚æ•°é¡¹éƒ½åˆæ ¼ï¼Œæ•´ä½“ä¸ºåˆæ ¼
            response.setAutoJudgeResult("合格");
        }
        // æ›´æ–°è´¨æ£€ä¸»è¡¨çš„自动判断结果
        QualityInspect inspect = qualityInspectMapper.selectById(inspectId);
        if (inspect != null) {
            inspect.setAutoJudgeResult(response.getAutoJudgeResult());
            qualityInspectMapper.updateById(inspect);
        }
        return response;
    }
}
src/main/java/com/ruoyi/quality/utils/QualityJudgeUtil.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,142 @@
package com.ruoyi.quality.utils;
import java.math.BigDecimal;
/**
 * è´¨æ£€å‚数判断工具类
 */
public class QualityJudgeUtil {
    public static final String JUDGE_TYPE_GREATER_EQUAL = "大于等于";
    public static final String JUDGE_TYPE_LESS_EQUAL = "小于等于";
    public static final String JUDGE_TYPE_RANGE = "范围";
    public static final String JUDGE_TYPE_TEXT_DESCRIPTION = "文字描述";
    /**
     * åˆ¤æ–­å‚数项是否合格
     *
     * @param testValue     æ£€éªŒå€¼
     * @param standardValue æ ‡å‡†å€¼ï¼ˆå¤§äºŽç­‰äºŽã€å°äºŽç­‰äºŽç±»åž‹ä½¿ç”¨ï¼‰
     * @param minValue      èŒƒå›´ä¸‹é™å€¼ï¼ˆèŒƒå›´ç±»åž‹ä½¿ç”¨ï¼‰
     * @param maxValue      èŒƒå›´ä¸Šé™å€¼ï¼ˆèŒƒå›´ç±»åž‹ä½¿ç”¨ï¼‰
     * @param judgeType     åˆ¤æ–­ç±»åž‹
     * @return åˆ¤æ–­ç»“果对象
     */
    public static JudgeResult judge(String testValue, String standardValue,
                                    BigDecimal minValue, BigDecimal maxValue, String judgeType) {
        JudgeResult result = new JudgeResult();
        // æ–‡å­—描述类型不自动判断
        if (JUDGE_TYPE_TEXT_DESCRIPTION.equals(judgeType)) {
            result.setQualified(null);
            result.setMessage("文字描述类型需手动判断");
            return result;
        }
        // æ£€éªŒå€¼ä¸ºç©ºæ—¶æ— æ³•判断
        if (testValue == null || testValue.trim().isEmpty()) {
            result.setQualified(null);
            result.setMessage("检验值为空,无法判断");
            return result;
        }
        try {
            BigDecimal testVal = new BigDecimal(testValue.trim());
            switch (judgeType) {
                case JUDGE_TYPE_GREATER_EQUAL:
                    return judgeGreaterEqual(testVal, standardValue);
                case JUDGE_TYPE_LESS_EQUAL:
                    return judgeLessEqual(testVal, standardValue);
                case JUDGE_TYPE_RANGE:
                    return judgeRange(testVal, minValue, maxValue);
                default:
                    result.setQualified(null);
                    result.setMessage("未知的判断类型:" + judgeType);
                    return result;
            }
        } catch (NumberFormatException e) {
            result.setQualified(null);
            result.setMessage("检验值格式不正确,无法转换为数值");
            return result;
        }
    }
    /**
     * å¤§äºŽç­‰äºŽåˆ¤æ–­
     */
    private static JudgeResult judgeGreaterEqual(BigDecimal testValue, String standardValue) {
        JudgeResult result = new JudgeResult();
        try {
            BigDecimal standardVal = new BigDecimal(standardValue.trim());
            boolean qualified = testValue.compareTo(standardVal) >= 0;
            result.setQualified(qualified);
            result.setMessage(String.format("检验值%s >= æ ‡å‡†å€¼%s,%s",
                    testValue, standardVal, qualified ? "合格" : "不合格"));
        } catch (Exception e) {
            result.setQualified(null);
            result.setMessage("标准值格式不正确:" + standardValue);
        }
        return result;
    }
    /**
     * å°äºŽç­‰äºŽåˆ¤æ–­
     */
    private static JudgeResult judgeLessEqual(BigDecimal testValue, String standardValue) {
        JudgeResult result = new JudgeResult();
        try {
            BigDecimal standardVal = new BigDecimal(standardValue.trim());
            boolean qualified = testValue.compareTo(standardVal) <= 0;
            result.setQualified(qualified);
            result.setMessage(String.format("检验值%s <= æ ‡å‡†å€¼%s,%s",
                    testValue, standardVal, qualified ? "合格" : "不合格"));
        } catch (Exception e) {
            result.setQualified(null);
            result.setMessage("标准值格式不正确:" + standardValue);
        }
        return result;
    }
    /**
     * èŒƒå›´åˆ¤æ–­
     */
    private static JudgeResult judgeRange(BigDecimal testValue, BigDecimal minValue, BigDecimal maxValue) {
        JudgeResult result = new JudgeResult();
        if (minValue == null || maxValue == null) {
            result.setQualified(null);
            result.setMessage("范围类型的上下限值不能为空");
            return result;
        }
        boolean qualified = testValue.compareTo(minValue) >= 0 && testValue.compareTo(maxValue) <= 0;
        result.setQualified(qualified);
        result.setMessage(String.format("检验值%s在范围[%s, %s]%s,%s",
                testValue, minValue, maxValue,
                qualified ? "内" : "外", qualified ? "合格" : "不合格"));
        return result;
    }
    /**
     * åˆ¤æ–­ç»“果对象
     */
    public static class JudgeResult {
        private Boolean qualified;
        private String message;
        public Boolean getQualified() {
            return qualified;
        }
        public void setQualified(Boolean qualified) {
            this.qualified = qualified;
        }
        public String getMessage() {
            return message;
        }
        public void setMessage(String message) {
            this.message = message;
        }
    }
}
src/main/java/com/ruoyi/sales/controller/MetricStatisticsController.java
@@ -55,4 +55,25 @@
        return R.ok(customerService.customewTransactionsDetails(page,customerId));
    }
    @GetMapping("/customerTransactionsSummary")
    @Log(title = "客户往来统计汇总", businessType = BusinessType.OTHER)
    @Operation(summary = "客户往来统计汇总")
    public R customerTransactionsSummary(Long customerId) {
        return R.ok(customerService.getCustomerTransactionsSummary(customerId));
    }
    @GetMapping("/customerTransactionsProducts")
    @Log(title = "客户往来产品明细", businessType = BusinessType.OTHER)
    @Operation(summary = "客户往来产品明细")
    public R customerTransactionsProducts(Page page, Long customerId, Long salesLedgerId) {
        return R.ok(customerService.getCustomerTransactionsProducts(page, customerId, salesLedgerId));
    }
    @GetMapping("/customerTransactionsShipments")
    @Log(title = "客户往来发货明细", businessType = BusinessType.OTHER)
    @Operation(summary = "客户往来发货明细")
    public R customerTransactionsShipments(Page page, Long customerId, Long salesLedgerId) {
        return R.ok(customerService.getCustomerTransactionsShipments(page, customerId, salesLedgerId));
    }
}
src/main/java/com/ruoyi/sales/controller/ShippingInfoController.java
@@ -101,6 +101,12 @@
        return AjaxResult.success(shippingInfoService.getShippingInfoByCustomerName(customerName));
    }
    @GetMapping("/getForReturn")
    @Operation(summary = "销售退货-通过客户名称查询发货信息(含批次号)")
    public AjaxResult getForReturn(String customerName) {
        return AjaxResult.success(shippingInfoService.getShippingInfoForReturn(customerName));
    }
    @GetMapping("/getDateil/{id}")
    @Operation(summary = "通过id查询详情")
    public R getDateil(@PathVariable("id") Long id) {
src/main/java/com/ruoyi/sales/dto/SalesLedgerDto.java
@@ -18,6 +18,12 @@
    private Long id;
    private String salesContractNo;
    private String customerContractNo;
    /**
     * æ€»åˆåŒå·
     */
    private String masterContractNo;
    private String projectName;
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date entryDate;
src/main/java/com/ruoyi/sales/dto/ShippingInfoDto.java
@@ -6,6 +6,7 @@
import com.ruoyi.sales.pojo.CommonFile;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.sales.pojo.ShippingProductDetail;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
@@ -54,5 +55,10 @@
    private String templateName;
    @Schema(description = "已退货数量")
    private BigDecimal returnedQuantity;
    @Schema(description = "退货状态:无退货/部分退货/全部退货")
    private String returnStatus;
}
src/main/java/com/ruoyi/sales/mapper/ShippingInfoMapper.java
@@ -6,6 +6,7 @@
import com.ruoyi.procurementrecord.bean.vo.ShippingProductVo;
import com.ruoyi.sales.dto.ShippingInfoDto;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.sales.vo.ShippingInfoForReturnVo;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -22,4 +23,9 @@
    List<ShippingProductVo> getReturnManagementDtoById(@Param("shippingId")Long shippingId);
    List<ShippingInfo> getShippingInfoByCustomerName(String customerName);
    /**
     * æŸ¥è¯¢å‘货信息(销售退货用,含批次号)
     */
    List<ShippingInfoForReturnVo> getShippingInfoForReturn(String customerName);
}
src/main/java/com/ruoyi/sales/pojo/SalesLedger.java
@@ -35,6 +35,12 @@
    private String salesContractNo;
    /**
     * æ€»åˆåŒå·
     */
    @Excel(name = "总合同号")
    private String masterContractNo;
    /**
     * å®¢æˆ·åˆåŒå·
     */
    @Excel(name = "客户合同号", type = Excel.Type.IMPORT)
src/main/java/com/ruoyi/sales/service/ShippingInfoService.java
@@ -8,6 +8,7 @@
import com.ruoyi.sales.dto.ShippingInfoDto;
import com.ruoyi.sales.dto.ShippingProductDetailDto;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.sales.vo.ShippingInfoForReturnVo;
import java.util.List;
@@ -26,6 +27,11 @@
    List<ShippingInfo> getShippingInfoByCustomerName(String customerName);
    /**
     * æŸ¥è¯¢å‘货信息(销售退货用,含批次号)
     */
    List<ShippingInfoForReturnVo> getShippingInfoForReturn(String customerName);
    boolean add(ShippingInfoDto req);
    List<ShippingProductDetailDto> getDetail(Long id);
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java
@@ -28,6 +28,7 @@
import com.ruoyi.sales.mapper.ShippingProductDetailMapper;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.sales.pojo.ShippingProductDetail;
import com.ruoyi.sales.vo.ShippingInfoForReturnVo;
import com.ruoyi.sales.service.ShippingInfoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -141,6 +142,11 @@
    }
    @Override
    public List<ShippingInfoForReturnVo> getShippingInfoForReturn(String customerName) {
        return shippingInfoMapper.getShippingInfoForReturn(customerName);
    }
    @Override
    public boolean add(ShippingInfoDto req) {
        // æ ¡éªŒoutboundBatches唯一性
        if (req.getOutboundBatches() != null && !req.getOutboundBatches().isEmpty()) {
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsDetailsVo.java
@@ -24,6 +24,9 @@
    @Schema(description = "合同金额")
    private BigDecimal contractAmount;
    @Schema(description = "产品名称列表(逗号分隔)")
    private String productNames;
    @Schema(description = "收款金额")
    private BigDecimal receiptPaymentAmount;
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsProductVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
package com.ruoyi.sales.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
 * å®¢æˆ·å¾€æ¥äº§å“æ˜Žç»†VO
 */
@Data
@Schema(name = "CustomerTransactionsProductVo", description = "客户往来产品明细")
public class CustomerTransactionsProductVo {
    @Schema(description = "销售台账ID")
    private Long salesLedgerId;
    @Schema(description = "销售合同号")
    private String salesContractNo;
    @Schema(description = "产品ID")
    private Long productId;
    @Schema(description = "产品名称")
    private String productName;
    @Schema(description = "规格型号")
    private String model;
    @Schema(description = "单位")
    private String unit;
    @Schema(description = "合同数量")
    private BigDecimal contractQuantity;
    @Schema(description = "合同单价(含税)")
    private BigDecimal taxInclusiveUnitPrice;
    @Schema(description = "合同金额")
    private BigDecimal contractAmount;
    @Schema(description = "已发货数量")
    private BigDecimal shippedQuantity;
    @Schema(description = "已发货金额")
    private BigDecimal shippedAmount;
    @Schema(description = "已收款金额")
    private BigDecimal receivedAmount;
    @Schema(description = "应收金额")
    private BigDecimal receivableAmount;
}
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsShipmentVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package com.ruoyi.sales.vo;
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;
/**
 * å®¢æˆ·å¾€æ¥å‘货明细VO
 */
@Data
@Schema(name = "CustomerTransactionsShipmentVo", description = "客户往来发货明细")
public class CustomerTransactionsShipmentVo {
    @Schema(description = "销售台账ID")
    private Long salesLedgerId;
    @Schema(description = "销售合同号")
    private String salesContractNo;
    @Schema(description = "发货单ID")
    private Long shippingId;
    @Schema(description = "发货单号")
    private String shippingNo;
    @Schema(description = "产品名称")
    private String productName;
    @Schema(description = "规格型号")
    private String model;
    @Schema(description = "发货数量")
    private BigDecimal shippingQuantity;
    @Schema(description = "发货金额(含税)")
    private BigDecimal shippingAmount;
    @Schema(description = "出库批号")
    private String batchNo;
    @Schema(description = "发货日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate shippingDate;
    @Schema(description = "审批状态(0待审/1已审)")
    private Integer approvalStatus;
    @Schema(description = "已收款金额")
    private BigDecimal receivedAmount;
    @Schema(description = "应收金额")
    private BigDecimal receivableAmount;
}
src/main/java/com/ruoyi/sales/vo/CustomerTransactionsSummaryVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,53 @@
package com.ruoyi.sales.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
 * å®¢æˆ·å¾€æ¥ç»Ÿè®¡æ±‡æ€»VO(优化版)
 */
@Data
@Schema(name = "CustomerTransactionsSummaryVo", description = "客户往来统计汇总")
public class CustomerTransactionsSummaryVo {
    @Schema(description = "客户ID")
    private Long customerId;
    @Schema(description = "客户名称")
    private String customerName;
    @Schema(description = "合同总金额")
    private BigDecimal contractAmounts;
    @Schema(description = "合同数量")
    private Integer contractCount;
    @Schema(description = "产品种类数")
    private Integer productCount;
    @Schema(description = "发货总金额")
    private BigDecimal shippedAmounts;
    @Schema(description = "发货总数量")
    private BigDecimal shippedQuantity;
    @Schema(description = "收款金额")
    private BigDecimal receivedAmounts;
    @Schema(description = "应收金额")
    private BigDecimal receivableAmounts;
    @Schema(description = "退货金额")
    private BigDecimal returnAmounts;
    @Schema(description = "未发货金额")
    private BigDecimal unshippedAmounts;
    @Schema(description = "收款率(%)")
    private BigDecimal receivedRate;
    @Schema(description = "发货率(%)")
    private BigDecimal shippedRate;
}
src/main/java/com/ruoyi/sales/vo/SalesLedgerVo.java
@@ -5,6 +5,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
@@ -14,4 +15,13 @@
    @Schema(description = "采购合同号")
    private String purchaseContractNumber;
    @Schema(description = "采购台账总合同号")
    private String purchaseMasterContractNo;
    @Schema(description = "已退货数量")
    private BigDecimal returnedQuantity;
    @Schema(description = "退货状态:无退货/部分退货/全部退货")
    private String returnStatus;
}
src/main/java/com/ruoyi/sales/vo/ShippingInfoForReturnVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package com.ruoyi.sales.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * å‘货信息-销售退货用VO
 */
@Data
@Schema(name = "ShippingInfoForReturnVo", description = "发货信息(销售退货用)")
public class ShippingInfoForReturnVo {
    @Schema(description = "发货单ID")
    private Long shippingId;
    @Schema(description = "发货单号")
    private String shippingNo;
    @Schema(description = "销售合同号")
    private String salesContractNo;
    @Schema(description = "客户名称")
    private String customerName;
    @Schema(description = "产品名称")
    private String productName;
    @Schema(description = "规格型号")
    private String model;
    @Schema(description = "发货数量")
    private BigDecimal shippingQuantity;
    @Schema(description = "已退货数量")
    private BigDecimal returnedQuantity;
    @Schema(description = "退货状态:无退货/部分退货/全部退货")
    private String returnStatus;
    @Schema(description = "批次号")
    private String batchNo;
    @Schema(description = "车牌号")
    private String shippingCarNumber;
    @Schema(description = "创建时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @Schema(description = "显示标签(批次号-车牌号-创建时间)")
    private String displayLabel;
}
src/main/resources/mapper/basic/CustomerMapper.xml
@@ -154,6 +154,7 @@
               sl.sales_contract_no,
               sl.execution_date,
               sl.contract_amount,
               T4.productNames,
               IFNULL(T1.receiptPaymentAmount, 0) AS receiptPaymentAmount,
               IFNULL(T2.outboundAmount, 0) - IFNULL(T3.returnAmount, 0) AS receiptableAmount
        from sales_ledger sl
@@ -191,7 +192,167 @@
            where rm.status=1
            group by sl.id
        )T3 on T3.id = sl.id
        left join (
            select slp.sales_ledger_id, GROUP_CONCAT(p.product_name) as productNames
            from sales_ledger_product slp
            left join product p on slp.product_id = p.id
            where slp.type = 1
            group by slp.sales_ledger_id
        ) T4 on T4.sales_ledger_id = sl.id
        where sl.customer_id = #{customerId}
        order by sl.id desc
    </select>
    <!-- å®¢æˆ·å¾€æ¥ç»Ÿè®¡æ±‡æ€» -->
    <select id="getCustomerTransactionsSummary" resultType="com.ruoyi.sales.vo.CustomerTransactionsSummaryVo">
        SELECT
            c.id AS customerId,
            c.customer_name AS customerName,
            IFNULL(T1.contractAmounts, 0) AS contractAmounts,
            IFNULL(T1.contractCount, 0) AS contractCount,
            IFNULL(T5.productCount, 0) AS productCount,
            IFNULL(T2.shippedAmounts, 0) AS shippedAmounts,
            IFNULL(T2.shippedQuantity, 0) AS shippedQuantity,
            IFNULL(T3.receivedAmounts, 0) AS receivedAmounts,
            IFNULL(T2.shippedAmounts, 0) - IFNULL(T3.receivedAmounts, 0) AS receivableAmounts,
            IFNULL(T4.returnAmounts, 0) AS returnAmounts,
            IFNULL(T1.contractAmounts, 0) - IFNULL(T2.shippedAmounts, 0) AS unshippedAmounts,
            CASE WHEN IFNULL(T2.shippedAmounts, 0) > 0
                 THEN ROUND(IFNULL(T3.receivedAmounts, 0) / T2.shippedAmounts * 100, 2)
                 ELSE 0 END AS receivedRate,
            CASE WHEN IFNULL(T1.contractAmounts, 0) > 0
                 THEN ROUND(IFNULL(T2.shippedAmounts, 0) / T1.contractAmounts * 100, 2)
                 ELSE 0 END AS shippedRate
        FROM customer c
        LEFT JOIN (
            SELECT customer_id, SUM(contract_amount) AS contractAmounts, COUNT(*) AS contractCount
            FROM sales_ledger WHERE customer_id = #{customerId}
            GROUP BY customer_id
        ) T1 ON T1.customer_id = c.id
        LEFT JOIN (
            SELECT
                sl.customer_id,
                SUM(sor.stock_out_num) AS shippedQuantity,
                SUM(sor.stock_out_num * slp.tax_inclusive_unit_price) AS shippedAmounts
            FROM stock_out_record sor
            LEFT JOIN shipping_info s ON sor.record_id = s.id
            LEFT JOIN sales_ledger sl ON s.sales_ledger_id = sl.id
            LEFT JOIN sales_ledger_product slp ON s.sales_ledger_product_id = slp.id
            WHERE sor.record_type = '13' AND sor.approval_status = 1 AND slp.type = 1
              AND sl.customer_id = #{customerId}
            GROUP BY sl.customer_id
        ) T2 ON T2.customer_id = c.id
        LEFT JOIN (
            SELECT customer_id, SUM(collection_amount) AS receivedAmounts
            FROM account_sales_collection WHERE customer_id = #{customerId}
            GROUP BY customer_id
        ) T3 ON T3.customer_id = c.id
        LEFT JOIN (
            SELECT
                sl.customer_id,
                SUM(rm.refund_amount) AS returnAmounts
            FROM return_management rm
            LEFT JOIN shipping_info si ON rm.shipping_id = si.id
            LEFT JOIN sales_ledger sl ON si.sales_ledger_id = sl.id
            WHERE rm.status = 1 AND sl.customer_id = #{customerId}
            GROUP BY sl.customer_id
        ) T4 ON T4.customer_id = c.id
        LEFT JOIN (
            SELECT sl.customer_id, COUNT(DISTINCT pm.product_id) AS productCount
            FROM sales_ledger sl
            LEFT JOIN sales_ledger_product slp ON sl.id = slp.sales_ledger_id
            LEFT JOIN product_model pm ON slp.product_id = pm.id
            WHERE sl.customer_id = #{customerId} AND slp.type = 1
            GROUP BY sl.customer_id
        ) T5 ON T5.customer_id = c.id
        WHERE c.id = #{customerId}
    </select>
    <!-- å®¢æˆ·å¾€æ¥äº§å“æ˜Žç»† -->
    <select id="getCustomerTransactionsProducts" resultType="com.ruoyi.sales.vo.CustomerTransactionsProductVo">
        SELECT
            sl.id AS salesLedgerId,
            sl.sales_contract_no AS salesContractNo,
            slp.product_id AS productId,
            IFNULL(p.product_name, slp.product_category) AS productName,
            IFNULL(pm.model, slp.specification_model) AS model,
            IFNULL(pm.unit, slp.unit) AS unit,
            slp.quantity AS contractQuantity,
            slp.tax_inclusive_unit_price AS taxInclusiveUnitPrice,
            slp.quantity * slp.tax_inclusive_unit_price AS contractAmount,
            IFNULL(T1.shippedQuantity, 0) AS shippedQuantity,
            IFNULL(T1.shippedAmount, 0) AS shippedAmount,
            IFNULL(T2.receivedAmount, 0) AS receivedAmount,
            IFNULL(T1.shippedAmount, 0) - IFNULL(T2.receivedAmount, 0) AS receivableAmount
        FROM sales_ledger sl
        LEFT JOIN sales_ledger_product slp ON sl.id = slp.sales_ledger_id
        LEFT JOIN product_model pm ON slp.product_id = pm.id
        LEFT JOIN product p ON pm.product_id = p.id
        LEFT JOIN (
            SELECT
                s.sales_ledger_id,
                s.sales_ledger_product_id,
                SUM(sor.stock_out_num) AS shippedQuantity,
                SUM(sor.stock_out_num * slp2.tax_inclusive_unit_price) AS shippedAmount
            FROM stock_out_record sor
            LEFT JOIN shipping_info s ON sor.record_id = s.id
            LEFT JOIN sales_ledger_product slp2 ON s.sales_ledger_product_id = slp2.id
            WHERE sor.record_type = '13' AND sor.approval_status = 1
            GROUP BY s.sales_ledger_id, s.sales_ledger_product_id
        ) T1 ON T1.sales_ledger_id = sl.id AND T1.sales_ledger_product_id = slp.id
        LEFT JOIN (
            SELECT
                s.sales_ledger_id,
                s.sales_ledger_product_id,
                SUM(ascc.collection_amount) AS receivedAmount
            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 s.sales_ledger_id, s.sales_ledger_product_id
        ) T2 ON T2.sales_ledger_id = sl.id AND T2.sales_ledger_product_id = slp.id
        WHERE sl.customer_id = #{customerId} AND slp.type = 1
        <if test="salesLedgerId != null">
            AND sl.id = #{salesLedgerId}
        </if>
        ORDER BY sl.id DESC, slp.id DESC
    </select>
    <!-- å®¢æˆ·å¾€æ¥å‘货明细 -->
    <select id="getCustomerTransactionsShipments" resultType="com.ruoyi.sales.vo.CustomerTransactionsShipmentVo">
        SELECT
            sl.id AS salesLedgerId,
            sl.sales_contract_no AS salesContractNo,
            s.id AS shippingId,
            s.shipping_no AS shippingNo,
            IFNULL(p.product_name, slp.product_category) AS productName,
            IFNULL(pm.model, slp.specification_model) AS model,
            sor.stock_out_num AS shippingQuantity,
            sor.stock_out_num * slp.tax_inclusive_unit_price AS shippingAmount,
            sor.batch_no AS batchNo,
            s.shipping_date AS shippingDate,
            sor.approval_status AS approvalStatus,
            IFNULL(T1.receivedAmount, 0) AS receivedAmount,
            sor.stock_out_num * slp.tax_inclusive_unit_price - IFNULL(T1.receivedAmount, 0) AS receivableAmount
        FROM stock_out_record sor
        LEFT JOIN shipping_info s ON sor.record_id = s.id
        LEFT JOIN sales_ledger sl ON s.sales_ledger_id = sl.id
        LEFT JOIN sales_ledger_product slp ON s.sales_ledger_product_id = slp.id
        LEFT JOIN product_model pm ON slp.product_id = pm.id
        LEFT JOIN product p ON pm.product_id = p.id
        LEFT JOIN (
            SELECT
                sor2.id AS stock_out_record_id,
                SUM(ascc.collection_amount) AS receivedAmount
            FROM account_sales_collection ascc
            LEFT JOIN stock_out_record sor2 ON FIND_IN_SET(sor2.id, ascc.stock_out_record_ids) > 0
            WHERE ascc.customer_id = #{customerId}
            GROUP BY sor2.id
        ) T1 ON T1.stock_out_record_id = sor.id
        WHERE sor.record_type = '13' AND sl.customer_id = #{customerId}
        <if test="salesLedgerId != null">
            AND sl.id = #{salesLedgerId}
        </if>
        ORDER BY sor.id DESC
    </select>
</mapper>
src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml
@@ -8,11 +8,31 @@
               si.shipping_no,
               sl.project_name,
               sl.sales_contract_no,
               sl.salesman
               sl.salesman,
               IFNULL(sq.shipping_qty, 0) AS shippingQuantity,
               IFNULL(rs.total_return_num, 0) AS returnedQuantity,
               CASE
                   WHEN IFNULL(rs.total_return_num, 0) = 0 THEN '无退货'
                   WHEN IFNULL(rs.total_return_num, 0) >= IFNULL(sq.shipping_qty, 0) THEN '全部退货'
                   ELSE '部分退货'
               END AS returnStatus
        from return_management rm
                 left join shipping_info si on rm.shipping_id = si.id
                 left join customer c on rm.customer_id = c.id
                 left join sales_ledger sl on si.sales_ledger_id = sl.id
                 left join (
                     SELECT sor.record_id, SUM(sor.stock_out_num) AS shipping_qty
                     FROM stock_out_record sor
                     WHERE sor.record_type = '13' AND sor.approval_status = 1
                     GROUP BY sor.record_id
                 ) sq ON sq.record_id = si.id
                 left join (
                     SELECT rm2.shipping_id, SUM(rsp.num) AS total_return_num
                     FROM return_sale_product rsp
                     LEFT JOIN return_management rm2 ON rm2.id = rsp.return_management_id
                     WHERE rm2.status = 1
                     GROUP BY rm2.shipping_id
                 ) rs ON rs.shipping_id = si.id
        <where>
            <if test="req.returnNo != null and req.returnNo != ''">
                and rm.return_no like concat('%',#{req.returnNo},'%')
src/main/resources/mapper/purchase/PurchaseLedgerMapper.xml
@@ -22,6 +22,7 @@
            SELECT
                pl.id,
                pl.purchase_contract_number,
                pl.master_contract_no,
                pl.sales_contract_no,
                pl.supplier_id,
                pl.supplier_name,
@@ -100,6 +101,9 @@
                <if test="c.purchaseContractNumber != null and c.purchaseContractNumber != ''">
                    AND pl.purchase_contract_number LIKE CONCAT('%', #{c.purchaseContractNumber}, '%')
                </if>
                <if test="c.masterContractNo != null and c.masterContractNo != ''">
                    AND pl.master_contract_no LIKE CONCAT('%', #{c.masterContractNo}, '%')
                </if>
                <if test="c.approvalStatus != null and c.approvalStatus != ''">
                    AND pl.approval_status = #{c.approvalStatus}
                </if>
src/main/resources/mapper/sales/SalesLedgerMapper.xml
@@ -45,6 +45,7 @@
    <select id="selectSalesLedgerListPage" resultType="com.ruoyi.sales.vo.SalesLedgerVo">
        SELECT T1.id,
        T1.sales_contract_no,
        T1.master_contract_no,
        T1.customer_contract_no,
        T1.project_name,
        T1.entry_date,
@@ -63,7 +64,14 @@
        T1.delivery_date,
        DATEDIFF(T1.delivery_date, CURDATE()) AS delivery_days_diff,
        IFNULL(shipping_status_counts.is_all_shipped, FALSE) AS is_fh,
        T3.purchase_contract_number
        T3.purchase_contract_number,
        T3.master_contract_no AS purchaseMasterContractNo,
        IFNULL(rs.total_return_num, 0) AS returnedQuantity,
        CASE
            WHEN IFNULL(rs.total_return_num, 0) = 0 THEN '无退货'
            WHEN IFNULL(rs.total_return_num, 0) >= IFNULL(shipping_qty.total_qty, 0) THEN '全部退货'
            ELSE '部分退货'
        END AS returnStatus
        FROM sales_ledger T1
        LEFT JOIN sys_user T2 ON T1.entry_person = T2.user_id
        LEFT JOIN (
@@ -76,6 +84,20 @@
        GROUP BY sales_ledger_id
        ) shipping_status_counts ON T1.id = shipping_status_counts.sales_ledger_id
        LEFT JOIN purchase_ledger T3 ON T1.purchase_ledger_id = T3.id
        LEFT JOIN (
            SELECT si.sales_ledger_id, SUM(sor.stock_out_num) AS total_qty
            FROM shipping_info si
            LEFT JOIN stock_out_record sor ON sor.record_id = si.id AND sor.record_type = '13' AND sor.approval_status = 1
            GROUP BY si.sales_ledger_id
        ) shipping_qty ON shipping_qty.sales_ledger_id = T1.id
        LEFT JOIN (
            SELECT si.sales_ledger_id, SUM(rsp.num) AS total_return_num
            FROM return_sale_product rsp
            LEFT JOIN return_management rm ON rm.id = rsp.return_management_id
            LEFT JOIN shipping_info si ON si.id = rm.shipping_id
            WHERE rm.status = 1
            GROUP BY si.sales_ledger_id
        ) rs ON rs.sales_ledger_id = T1.id
        <where>
            <if test="salesLedgerDto.customerName != null and salesLedgerDto.customerName != '' ">
@@ -87,6 +109,9 @@
            <if test="salesLedgerDto.salesContractNo != null and salesLedgerDto.salesContractNo != '' ">
                AND T1.sales_contract_no LIKE CONCAT('%',#{salesLedgerDto.salesContractNo},'%')
            </if>
            <if test="salesLedgerDto.masterContractNo != null and salesLedgerDto.masterContractNo != '' ">
                AND T1.master_contract_no LIKE CONCAT('%',#{salesLedgerDto.masterContractNo},'%')
            </if>
            <if test="salesLedgerDto.projectName != null and salesLedgerDto.projectName != '' ">
                AND T1.project_name LIKE CONCAT('%',#{salesLedgerDto.projectName},'%')
            </if>
src/main/resources/mapper/sales/ShippingInfoMapper.xml
@@ -26,7 +26,13 @@
        sl.customer_name,
        spd.totalQuantity,
        sor.outboundBatches,
        pl.purchase_contract_number
        pl.purchase_contract_number,
        IFNULL(rs.total_return_num, 0) AS returnedQuantity,
        CASE
            WHEN IFNULL(rs.total_return_num, 0) = 0 THEN '无退货'
            WHEN IFNULL(rs.total_return_num, 0) >= spd.totalQuantity THEN '全部退货'
            ELSE '部分退货'
        END AS returnStatus
        FROM shipping_info s
        LEFT JOIN (select shipping_info_id,sum(quantity) totalQuantity from shipping_product_detail GROUP BY shipping_info_id) spd ON spd.shipping_info_id = s.id
        LEFT JOIN sales_ledger sl ON s.sales_ledger_id = sl.id
@@ -38,6 +44,16 @@
                   from stock_out_record
                   where record_type='13'and approval_status=1
                   group by record_id)sor on sor.record_id= s.id
        LEFT JOIN (
            SELECT
                si.id AS shipping_info_id,
                SUM(rsp.num) AS total_return_num
            FROM return_sale_product rsp
            LEFT JOIN return_management rm ON rm.id = rsp.return_management_id
            LEFT JOIN shipping_info si ON si.id = rm.shipping_id
            WHERE rm.status = 1
            GROUP BY si.id
        ) rs ON rs.shipping_info_id = s.id
        WHERE 1=1
        <if test="req.salesContractNo != null and req.salesContractNo != ''">
            AND sl.sales_contract_no LIKE CONCAT('%',#{req.salesContractNo},'%')
@@ -111,4 +127,49 @@
        left join sales_ledger sl on si.sales_ledger_id = sl.id
        where  sl.customer_name = #{customerName}
    </select>
    <!-- é”€å”®é€€è´§ç”¨ï¼šæŸ¥è¯¢å‘货信息含批次号 -->
    <select id="getShippingInfoForReturn" resultType="com.ruoyi.sales.vo.ShippingInfoForReturnVo">
        SELECT
            si.id AS shippingId,
            si.shipping_no AS shippingNo,
            sl.sales_contract_no AS salesContractNo,
            sl.customer_name AS customerName,
            IFNULL(p.product_name, slp.product_category) AS productName,
            IFNULL(pm.model, slp.specification_model) AS model,
            sor.stock_out_num AS shippingQuantity,
            IFNULL(rs.total_return_num, 0) AS returnedQuantity,
            CASE
                WHEN IFNULL(rs.total_return_num, 0) = 0 THEN '无退货'
                WHEN IFNULL(rs.total_return_num, 0) >= sor.stock_out_num THEN '全部退货'
                ELSE '部分退货'
            END AS returnStatus,
            sor.batch_no AS batchNo,
            si.shipping_car_number AS shippingCarNumber,
            si.create_time AS createTime,
            CONCAT(
                IFNULL(sor.batch_no, ''),
                '-',
                IFNULL(si.shipping_car_number, ''),
                '-',
                DATE_FORMAT(si.create_time, '%Y-%m-%d')
            ) AS displayLabel
        FROM shipping_info si
        LEFT JOIN sales_ledger sl ON si.sales_ledger_id = sl.id
        LEFT JOIN sales_ledger_product slp ON si.sales_ledger_product_id = slp.id AND slp.type = 1
        LEFT JOIN product_model pm ON slp.product_id = pm.id
        LEFT JOIN product p ON pm.product_id = p.id
        LEFT JOIN stock_out_record sor ON sor.record_id = si.id AND sor.record_type = '13' AND sor.approval_status = 1
        LEFT JOIN (
            SELECT
                stock_out_record_id,
                SUM(num) AS total_return_num
            FROM return_sale_product rsp
            LEFT JOIN return_management rm ON rm.id = rsp.return_management_id
            WHERE rm.status = 1
            GROUP BY stock_out_record_id
        ) rs ON rs.stock_out_record_id = sor.id
        WHERE sl.customer_name = #{customerName}
        ORDER BY si.create_time DESC
    </select>
</mapper>