编辑 | blame | 历史 | 原始文档

CRM 与 ERP 销售一体化改造 - 前端文档(完整版)

概述

本次改造实现 CRM 与 ERP 销售模块的深度集成,包括:
- Phase 1:客户数据统一 - ERP 使用 CRM 客户
- Phase 2:产品数据统一 - CRM/ERP 使用 MDM 物料
- Phase 3:合同生成销售订单
- Phase 4:销售全流程追溯
- Phase 5:营销数据报表
- Phase 6:数据迁移与清理(SQL 部分)


一、改造清单汇总

1.1 新增模块

模块 说明
yudao-module-crm-api CRM API 模块(客户 API、报表 API、追溯 API)
yudao-module-erp-api ERP API 模块(销售订单 API)
yudao-module-mdm-api MDM API 模块(物料 API)

1.2 后端改造文件

文件 改动说明
CrmCustomerApi.java CRM 客户 API 接口
CrmCustomerRespDTO.java 客户响应 DTO
CrmReportApi.java 营销报表 API 接口
CrmSaleTraceApi.java 销售追溯 API 接口
ErpSaleOrderApi.java 销售订单 API 接口
MdmItemApi.java MDM 物料 API 接口
ErpSaleOrderServiceImpl.java 使用 CRM 客户 API
ErpFinanceReceiptServiceImpl.java 使用 CRM 客户 API
CrmContractServiceImpl.java 新增生成销售订单方法
CrmProductServiceImpl.java 使用 MDM 物料 API
ErpProductServiceImpl.java 使用 MDM 物料 API

二、涉及页面

2.1 ERP 销售模块

页面 路径 改动说明
销售订单列表 /erp/sale/order/index 客户选择改为 CRM 客户,新增关联合同列
销售订单表单 /erp/sale/order/form 客户选择改为 CRM 客户选择组件
销售出库列表 /erp/sale/out/index 客户选择改为 CRM 客户选择组件
销售出库表单 /erp/sale/out/form 客户选择改为 CRM 客户选择组件
销售退货列表 /erp/sale/return/index 客户选择改为 CRM 客户选择组件
销售退货表单 /erp/sale/return/form 客户选择改为 CRM 客户选择组件
收款单列表 /erp/finance/receipt/index 客户选择改为 CRM 客户选择组件
收款单表单 /erp/finance/receipt/form 客户选择改为 CRM 客户选择组件

2.2 CRM 合同模块

页面 路径 改动说明
合同列表 /crm/contract/index 添加"生成销售订单"按钮
合同详情 /crm/contract/detail 显示销售订单信息、销售流程追溯

2.3 CRM 商机模块

页面 路径 改动说明
商机详情 /crm/business/detail 添加销售流程追溯入口

2.4 CRM 客户模块

页面 路径 改动说明
客户详情 /crm/customer/detail 添加销售流程追溯列表

2.5 CRM 报表模块(新增)

页面 路径 说明
报表概览 /crm/report/dashboard 数据概览页面

三、API 接口

3.1 CRM 客户 API

方法 路径 说明
GET /crm/customer/simple-list 获取客户精简列表(用于下拉选择)
GET /crm/customer/list-by-ids 批量获取客户信息
GET /rpc-api/crm/customer/get 获取单个客户
POST /rpc-api/crm/customer/validate 校验客户存在

客户精简列表响应:

{
  "code": 0,
  "data": [
    {
      "id": 1,
      "name": "客户A",
      "mobile": "13800138000",
      "telephone": "0571-12345678",
      "ownerUserName": "张三"
    }
  ]
}

3.2 CRM 合同 API(新增)

方法 路径 说明
POST /crm/contract/generate-sale-order 根据合同生成销售订单

请求参数:

参数 类型 必填 说明
id Long 合同编号

响应:

{
  "code": 0,
  "data": 12345,
  "msg": "操作成功"
}

3.3 CRM 销售追溯 API(新增)

方法 路径 说明
GET /rpc-api/crm/sale-trace/get-by-business-id 根据商机编号获取追溯信息
GET /rpc-api/crm/sale-trace/get-by-contract-id 根据合同编号获取追溯信息
GET /rpc-api/crm/sale-trace/get-list-by-customer-id 根据客户编号获取追溯列表

响应示例:

{
  "code": 0,
  "data": {
    "businessId": 1,
    "businessName": "商机A",
    "businessPrice": 100000,
    "customerId": 1,
    "customerName": "客户A",
    "contractId": 1,
    "contractName": "合同A",
    "contractPrice": 95000,
    "contractStatus": 20,
    "orderId": 1,
    "orderNo": "SO001",
    "orderPrice": 95000,
    "receivablePrice": 50000,
    "receivableItems": [
      { "receivableId": 1, "receivableNo": "RK001", "price": 30000 }
    ]
  }
}

3.4 CRM 营销报表 API(新增)

方法 路径 说明
GET /rpc-api/crm/report/get-customer-analyze 获取客户分析统计
GET /rpc-api/crm/report/get-business-analyze 获取商机分析统计
GET /rpc-api/crm/report/get-contract-analyze 获取合同分析统计
GET /rpc-api/crm/report/get-sales-funnel 获取销售漏斗统计
GET /rpc-api/crm/report/get-sales-performance 获取销售业绩统计

客户分析响应:

{
  "code": 0,
  "data": {
    "totalCount": 1000,
    "todayCount": 15,
    "dealCount": 300,
    "dealRate": 30.00,
    "lockCount": 50,
    "poolCount": 200
  }
}

商机分析响应:

{
  "code": 0,
  "data": {
    "totalCount": 500,
    "todayCount": 8,
    "totalPrice": 5000000.00,
    "successCount": 150,
    "successPrice": 2000000.00,
    "successRate": 30.00,
    "processingCount": 250,
    "failCount": 100
  }
}

合同分析响应:

{
  "code": 0,
  "data": {
    "totalCount": 400,
    "todayCount": 5,
    "totalPrice": 3000000.00,
    "approveCount": 300,
    "approvePrice": 2500000.00,
    "processCount": 20,
    "receivablePrice": 1500000.00,
    "receivableRate": 60.00
  }
}

销售漏斗响应:

{
  "code": 0,
  "data": {
    "stages": [
      { "name": "客户", "count": 1000, "price": 0, "conversionRate": 100.00 },
      { "name": "商机", "count": 500, "price": 5000000.00, "conversionRate": 50.00 },
      { "name": "合同", "count": 300, "price": 2500000.00, "conversionRate": 30.00 },
      { "name": "回款", "count": null, "price": 1500000.00, "conversionRate": 15.00 }
    ]
  }
}

销售业绩响应:

{
  "code": 0,
  "data": {
    "items": [
      {
        "userId": 1,
        "userName": "张三",
        "deptName": "销售一部",
        "contractCount": 50,
        "contractPrice": 500000.00,
        "receivablePrice": 300000.00,
        "businessCount": 80,
        "businessPrice": 800000.00
      }
    ]
  }
}

3.5 ERP 销售订单 API(新增 RPC)

方法 路径 说明
POST /rpc-api/erp/sale-order/create 创建销售订单
GET /rpc-api/erp/sale-order/get 获取销售订单
GET /rpc-api/erp/sale-order/get-by-contract-id 根据合同编号获取订单

3.6 MDM 物料 API

方法 路径 说明
POST /rpc-api/mdm/item/create 创建物料
PUT /rpc-api/mdm/item/update 更新物料
GET /rpc-api/mdm/item/get 获取物料
POST /rpc-api/mdm/item/list 获取物料列表
GET /rpc-api/mdm/item/validate-exists 校验物料存在

四、前端修改点

4.1 客户选择组件

ERP 销售模块的客户选择统一使用 CRM 客户选择组件。

<!-- 原来:ERP 客户选择 -->
<el-select v-model="form.customerId" placeholder="请选择客户">
  <el-option
    v-for="item in customerList"
    :key="item.id"
    :label="item.name"
    :value="item.id"
  />
</el-select>

<!-- 改为:CRM 客户选择组件 -->
<CrmCustomerSelect v-model="form.customerId" />

组件实现:

<!-- CrmCustomerSelect.vue -->
<template>
  <el-select
    v-model="selectedValue"
    placeholder="请选择客户"
    filterable
    remote
    :remote-method="searchCustomer"
    @focus="loadCustomers"
  >
    <el-option
      v-for="item in customerList"
      :key="item.id"
      :label="item.name"
      :value="item.id"
    >
      <span>{{ item.name }}</span>
      <span style="color: #999; margin-left: 8px;">{{ item.mobile }}</span>
    </el-option>
  </el-select>
</template>

<script>
import { getCustomerSimpleList } from '@/api/crm/customer'

export default {
  props: {
    value: [Number, String]
  },
  data() {
    return {
      customerList: [],
      selectedValue: this.value
    }
  },
  watch: {
    value(val) {
      this.selectedValue = val
    },
    selectedValue(val) {
      this.$emit('input', val)
    }
  },
  methods: {
    async loadCustomers() {
      if (this.customerList.length === 0) {
        const { data } = await getCustomerSimpleList()
        this.customerList = data
      }
    },
    async searchCustomer(query) {
      if (query) {
        // 可选:实现远程搜索
      }
    }
  }
}
</script>

4.2 合同列表页面

文件路径: /src/views/crm/contract/index.vue

新增按钮:

<!-- 在合同操作列添加 -->
<el-table-column label="操作" width="200">
  <template #default="{ row }">
    <el-button
      v-if="row.auditStatus === 20 && !row.orderId"
      type="success"
      size="small"
      @click="handleGenerateOrder(row)"
    >
      生成销售订单
    </el-button>
    <el-button
      v-if="row.orderId"
      type="primary"
      size="small"
      @click="handleViewOrder(row)"
    >
      查看订单
    </el-button>
  </template>
</el-table-column>

新增方法:

import { generateSaleOrder } from '@/api/crm/contract'

methods: {
  async handleGenerateOrder(row) {
    try {
      await this.$confirm('确认根据该合同生成销售订单?', '提示')
      const { data } = await generateSaleOrder(row.id)
      this.$message.success('生成销售订单成功')
      this.getList()
    } catch (e) {
      if (e !== 'cancel') {
        this.$message.error(e.message || '生成失败')
      }
    }
  },
  handleViewOrder(row) {
    this.$router.push({
      path: '/erp/sale/order/detail',
      query: { id: row.orderId }
    })
  }
}

4.3 销售订单列表页面

文件路径: /src/views/erp/sale/order/index.vue

新增列:

<el-table-column label="关联合同" prop="contractNo" width="150">
  <template #default="{ row }">
    <el-link
      v-if="row.contractId"
      type="primary"
      @click="handleViewContract(row)"
    >
      {{ row.contractNo }}
    </el-link>
    <span v-else>-</span>
  </template>
</el-table-column>

4.4 商机详情页面 - 销售追溯

文件路径: /src/views/crm/business/detail.vue

<el-card header="销售流程追溯">
  <el-timeline v-if="traceInfo">
    <el-timeline-item
      :timestamp="traceInfo.businessCreateTime"
      color="#409EFF"
    >
      <el-card>
        <h4>商机创建</h4>
        <p>商机名称:{{ traceInfo.businessName }}</p>
        <p>商机金额:¥{{ traceInfo.businessPrice }}</p>
      </el-card>
    </el-timeline-item>

    <el-timeline-item
      v-if="traceInfo.contractId"
      :timestamp="traceInfo.contractCreateTime"
      color="#67C23A"
    >
      <el-card>
        <h4>合同签订</h4>
        <p>合同名称:{{ traceInfo.contractName }}</p>
        <p>合同金额:¥{{ traceInfo.contractPrice }}</p>
      </el-card>
    </el-timeline-item>

    <el-timeline-item
      v-if="traceInfo.orderId"
      :timestamp="traceInfo.orderCreateTime"
      color="#E6A23C"
    >
      <el-card>
        <h4>销售订单</h4>
        <p>订单号:
          <el-link type="primary" @click="viewOrder(traceInfo.orderId)">
            {{ traceInfo.orderNo }}
          </el-link>
        </p>
        <p>订单金额:¥{{ traceInfo.orderPrice }}</p>
      </el-card>
    </el-timeline-item>

    <el-timeline-item v-if="traceInfo.receivablePrice > 0" color="#F56C6C">
      <el-card>
        <h4>回款记录</h4>
        <p>已回款金额:¥{{ traceInfo.receivablePrice }}</p>
      </el-card>
    </el-timeline-item>
  </el-timeline>
  <el-empty v-else description="暂无销售流程数据" />
</el-card>

4.5 报表概览页面

文件路径: /src/views/crm/report/dashboard.vue

<template>
  <div class="report-dashboard">
    <!-- 统计卡片 -->
    <el-row :gutter="20">
      <el-col :span="6">
        <el-card class="stat-card">
          <div class="stat-title">客户总数</div>
          <div class="stat-value">{{ customerAnalyze.totalCount }}</div>
          <div class="stat-footer">今日新增:{{ customerAnalyze.todayCount }}</div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card class="stat-card">
          <div class="stat-title">商机总数</div>
          <div class="stat-value">{{ businessAnalyze.totalCount }}</div>
          <div class="stat-footer">转化率:{{ businessAnalyze.successRate }}%</div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card class="stat-card">
          <div class="stat-title">合同金额</div>
          <div class="stat-value">¥{{ contractAnalyze.totalPrice }}</div>
          <div class="stat-footer">已回款:¥{{ contractAnalyze.receivablePrice }}</div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card class="stat-card">
          <div class="stat-title">回款率</div>
          <div class="stat-value">{{ contractAnalyze.receivableRate }}%</div>
        </el-card>
      </el-col>
    </el-row>

    <!-- 销售业绩排行 -->
    <el-card header="销售业绩排行">
      <el-table :data="performanceData.items" stripe>
        <el-table-column prop="userName" label="销售人员" />
        <el-table-column prop="deptName" label="部门" />
        <el-table-column prop="contractCount" label="合同数量" width="100" />
        <el-table-column prop="contractPrice" label="合同金额" width="150" />
        <el-table-column prop="receivablePrice" label="回款金额" width="150" />
      </el-table>
    </el-card>
  </div>
</template>

<script>
import {
  getCustomerAnalyze,
  getBusinessAnalyze,
  getContractAnalyze,
  getSalesPerformance
} from '@/api/crm/report'

export default {
  data() {
    return {
      customerAnalyze: {},
      businessAnalyze: {},
      contractAnalyze: {},
      performanceData: { items: [] }
    }
  },
  created() {
    this.loadData()
  },
  methods: {
    async loadData() {
      const [c, b, ct, p] = await Promise.all([
        getCustomerAnalyze(),
        getBusinessAnalyze(),
        getContractAnalyze(),
        getSalesPerformance()
      ])
      this.customerAnalyze = c.data
      this.businessAnalyze = b.data
      this.contractAnalyze = ct.data
      this.performanceData = p.data
    }
  }
}
</script>

4.6 API 定义文件

文件路径: /src/api/crm/contract.js

// 生成销售订单
export function generateSaleOrder(id) {
  return request({
    url: '/crm/contract/generate-sale-order',
    method: 'post',
    params: { id }
  })
}

文件路径: /src/api/crm/saleTrace.js

// 根据商机编号获取销售流程追溯信息
export function getTraceByBusinessId(businessId) {
  return request({
    url: '/crm/sale-trace/get-by-business-id',
    method: 'get',
    params: { businessId }
  })
}

// 根据合同编号获取销售流程追溯信息
export function getTraceByContractId(contractId) {
  return request({
    url: '/crm/sale-trace/get-by-contract-id',
    method: 'get',
    params: { contractId }
  })
}

// 根据客户编号获取销售流程追溯列表
export function getTraceListByCustomerId(customerId) {
  return request({
    url: '/crm/sale-trace/get-list-by-customer-id',
    method: 'get',
    params: { customerId }
  })
}

文件路径: /src/api/crm/report.js

// 获取客户分析统计
export function getCustomerAnalyze(startTime, endTime) {
  return request({
    url: '/crm/report/get-customer-analyze',
    method: 'get',
    params: { startTime, endTime }
  })
}

// 获取商机分析统计
export function getBusinessAnalyze(startTime, endTime) {
  return request({
    url: '/crm/report/get-business-analyze',
    method: 'get',
    params: { startTime, endTime }
  })
}

// 获取合同分析统计
export function getContractAnalyze(startTime, endTime) {
  return request({
    url: '/crm/report/get-contract-analyze',
    method: 'get',
    params: { startTime, endTime }
  })
}

// 获取销售漏斗统计
export function getSalesFunnel() {
  return request({
    url: '/crm/report/get-sales-funnel',
    method: 'get'
  })
}

// 获取销售业绩统计
export function getSalesPerformance(startTime, endTime) {
  return request({
    url: '/crm/report/get-sales-performance',
    method: 'get',
    params: { startTime, endTime }
  })
}

五、数据模型变更

5.1 CRM 合同表

ALTER TABLE `crm_contract`
ADD COLUMN `order_id` bigint DEFAULT NULL COMMENT '关联 ERP 销售订单编号' AFTER `total_price`,
ADD COLUMN `order_no` varchar(32) DEFAULT NULL COMMENT 'ERP 销售订单单号' AFTER `order_id`;

-- 新增定金金额字段
ALTER TABLE `crm_contract`
ADD COLUMN `deposit_price` decimal(24,6) DEFAULT NULL COMMENT '定金金额,单位:元' AFTER `total_price`;

5.2 CRM 商机表

ALTER TABLE `crm_business`
ADD COLUMN `order_id` bigint DEFAULT NULL COMMENT '关联 ERP 销售订单编号' AFTER `total_price`,
ADD COLUMN `order_no` varchar(32) DEFAULT NULL COMMENT 'ERP 销售订单单号' AFTER `order_id`;

5.3 ERP 销售订单表

ALTER TABLE `erp_sale_order`
ADD COLUMN `contract_id` bigint DEFAULT NULL COMMENT '关联 CRM 合同编号' AFTER `customer_id`,
ADD COLUMN `contract_no` varchar(32) DEFAULT NULL COMMENT 'CRM 合同单号' AFTER `contract_id`;

六、业务规则

6.1 生成销售订单条件

  • 合同必须审批通过(auditStatus = 20)
  • 合同未关联销售订单(orderId 为空)
  • 合同必须包含产品信息

6.2 客户数据规则

  • ERP 销售模块统一使用 CRM 客户
  • 原 ERP 客户表(erp_customer)废弃
  • 客户选择通过 CRM 客户 API 获取

6.3 产品数据规则

  • CRM 和 ERP 产品统一使用 MDM 物料(itemType=2)
  • MDM 物料类型:1-物料,2-产品,3-半成品

七、业务流程图

客户管理(CRM) → 商机跟进(CRM) → 合同签订(CRM) → 合同审批
                                                      ↓
                                              生成销售订单(ERP)
                                                      ↓
                                              订单审批(ERP)
                                                      ↓
                                              销售出库(ERP)
                                                      ↓
                                              客户回款(ERP)

销售漏斗转化:

客户(1000) → 商机(500) → 合同(300) → 回款(150)
    ↓            ↓            ↓           ↓
  100%        50%          30%         15%

八、注意事项

  1. 数据权限:报表数据需要根据用户权限过滤
  2. 状态控制:只有审批通过的合同才能生成订单
  3. 唯一性:一个合同只能生成一个销售订单
  4. 数据同步:生成订单后,合同与订单双向关联
  5. 废弃页面:ERP 客户管理页面(/erp/customer/index)建议在菜单中隐藏

九、完成状态

Phase 内容 状态
Phase 1 客户数据统一 - ERP 使用 CRM 客户 已完成
Phase 2 产品数据统一 - CRM/ERP 使用 MDM 物料 已完成
Phase 3 合同生成销售订单 已完成
Phase 4 销售全流程追溯 已完成
Phase 5 营销数据报表 已完成
Phase 6 数据迁移与清理 已完成

十一、销售报价模块(新增)

11.1 涉及页面

页面 路径 说明
报价单列表 /crm/sale-quotation/index 报价单列表、操作按钮
报价单表单 /crm/sale-quotation/form 新建/编辑报价单
报价单详情 /crm/sale-quotation/detail 查看详情、审批进度

11.2 API 接口

方法 路径 说明
POST /crm/sale-quotation/create 创建报价单
PUT /crm/sale-quotation/update 更新报价单
DELETE /crm/sale-quotation/delete 删除报价单
GET /crm/sale-quotation/get 获取报价单详情
GET /crm/sale-quotation/page 分页查询
GET /crm/sale-quotation/page-by-customer 按客户分页
GET /crm/sale-quotation/page-by-business 按商机分页
PUT /crm/sale-quotation/submit 提交审批(需传 processDefinitionKey)
GET /crm/sale-quotation/approve-process-list 获取可选审批流程列表
POST /crm/sale-quotation/convert-contract 转为合同

请求参数示例(创建报价单):

参数 类型 必填 说明
name String 报价单名称
customerId Long 客户编号
businessId Long 商机编号
contactId Long 联系人编号
quotationTime LocalDateTime 报价日期
validUntil LocalDateTime 有效期至
discountPercent BigDecimal 整单折扣
taxRate BigDecimal 税率
fileUrl String 附件地址
remark String 备注
items List 物料明细列表

物料明细 Item

参数 类型 必填 说明
itemId Long MDM 物料编号
quotationPrice BigDecimal 报价单价
count BigDecimal 数量
taxPercent BigDecimal 税率
remark String 备注

11.3 前端修改点

报价单列表页面

文件路径: /src/views/crm/sale-quotation/index.vue

<!-- 操作列 -->
<el-table-column label="操作" width="280">
  <template #default="{ row }">
    <el-button v-if="row.status === 0" type="primary" size="small" @click="handleUpdate(row)">编辑</el-button>
    <el-button v-if="row.status === 0" type="success" size="small" @click="handleSubmit(row)">提交审批</el-button>
    <el-button v-if="row.status === 20 && !row.contractId" type="warning" size="small" @click="handleConvertContract(row)">转合同</el-button>
    <el-button v-if="row.contractId" type="primary" size="small" @click="handleViewContract(row)">查看合同</el-button>
    <el-button v-if="row.status === 0" type="danger" size="small" @click="handleDelete(row)">删除</el-button>
  </template>
</el-table-column>

提交审批弹窗

<el-dialog title="选择审批流程" v-model="submitDialogVisible" width="400px">
  <el-form label-width="100px">
    <el-form-item label="审批流程" required>
      <el-select v-model="selectedProcessKey" placeholder="请选择" style="width: 100%">
        <el-option v-for="item in processList" :key="item.key" :label="item.name" :value="item.key" />
      </el-select>
    </el-form-item>
  </el-form>
  <template #footer>
    <el-button @click="submitDialogVisible = false">取消</el-button>
    <el-button type="primary" @click="confirmSubmit" :loading="submitLoading">确定</el-button>
  </template>
</el-dialog>

JS 方法

import { getSaleQuotationApproveProcessList, submitSaleQuotation, convertSaleQuotationToContract } from '@/api/crm/saleQuotation'

methods: {
  async handleSubmit(row) {
    const { data } = await getSaleQuotationApproveProcessList()
    this.processList = data
    this.currentRow = row
    this.submitDialogVisible = true
  },
  async confirmSubmit() {
    if (!this.selectedProcessKey) {
      this.$message.warning('请选择审批流程')
      return
    }
    this.submitLoading = true
    try {
      await submitSaleQuotation(this.currentRow.id, this.selectedProcessKey)
      this.$message.success('提交成功')
      this.submitDialogVisible = false
      this.getList()
    } finally {
      this.submitLoading = false
    }
  },
  async handleConvertContract(row) {
    try {
      await this.$confirm('确认将该报价单转为合同?', '提示')
      const { data } = await convertSaleQuotationToContract(row.id)
      this.$message.success('转合同成功')
      this.$router.push({ path: '/crm/contract/detail', query: { id: data } })
    } catch (e) {
      if (e !== 'cancel') this.$message.error(e.message || '转合同失败')
    }
  }
}

报价单表单页面 - MDM 物料选择

文件路径: /src/views/crm/sale-quotation/form.vue

由于报价单使用 MDM 物料,需要使用 MDM 物料选择组件:

<MdmItemSelect v-model="item.itemId" @change="handleItemChange" />

API 定义文件/src/api/crm/saleQuotation.js

import request from '@/utils/request'

// 创建报价单
export function createSaleQuotation(data) {
  return request({ url: '/crm/sale-quotation/create', method: 'post', data })
}

// 更新报价单
export function updateSaleQuotation(data) {
  return request({ url: '/crm/sale-quotation/update', method: 'put', data })
}

// 删除报价单
export function deleteSaleQuotation(ids) {
  return request({ url: '/crm/sale-quotation/delete', method: 'delete', params: { ids } })
}

// 获取报价单详情
export function getSaleQuotation(id) {
  return request({ url: '/crm/sale-quotation/get', method: 'get', params: { id } })
}

// 分页查询
export function getSaleQuotationPage(params) {
  return request({ url: '/crm/sale-quotation/page', method: 'get', params })
}

// 提交审批
export function submitSaleQuotation(id, processDefinitionKey) {
  return request({ url: '/crm/sale-quotation/submit', method: 'put', params: { id, processDefinitionKey } })
}

// 获取审批流程列表
export function getSaleQuotationApproveProcessList() {
  return request({ url: '/crm/sale-quotation/approve-process-list', method: 'get' })
}

// 转合同
export function convertSaleQuotationToContract(id) {
  return request({ url: '/crm/sale-quotation/convert-contract', method: 'post', params: { id } })
}

11.4 业务流程

商机(可选) → 销售报价(草稿) → 提交审批 → 审批中 → 审批通过 → 转合同
                                    ↓
                              审批拒绝 → 重新提交或修改

11.5 注意事项

  1. 报价单和合同统一使用 MDM 物料MdmItemApi),与 ERP 销售订单一致
  2. BPM 分类编码 sale_quotation_approve 需要在 BPM 管理后台创建
  3. 报价单转合同时,物料明细直接复制到合同产品表(字段一致)
  4. 报价单号前缀为 QT,格式:QT + yyyyMMdd + 6位自增序号
  5. 商机关联为可选,报价单可以独立创建不依赖商机

11.6 合同产品字段变更说明

合同产品表 crm_contract_product 已改为使用 MDM 物料:

原字段 新字段 说明
product_id item_id MDM 物料编号
product_price item_price 物料原价

| - | item_unit_id | 计量单位编号 |

前端表单需要使用 MDM 物料选择组件替代 CRM 产品选择组件。


十、产出文档清单

文档 说明
docs/crm_erp_integration_frontend.md 本文档
docs/crm_erp_integration_sql.sql SQL 变更脚本

十二、销售订单作废功能(新增)

12.1 功能说明

销售订单作废功能用于标记已审核的订单为"已作废"状态,实现订单全流程可追溯。

业务规则:
- 只有已审核的订单才能作废
- 已出库或已退货的订单不能作废
- 作废后订单状态变为"已作废"
- 作废后同步更新关联合同状态为"已作废"
- 管理员可以重新启用已作废的订单

12.2 状态枚举

状态值 枚举 说明
10 PROCESS 未审核
20 APPROVE 已审核
30 CANCEL 已作废

12.3 合同状态同步

当销售订单作废/启用时,会自动同步关联合同的状态:

操作 订单状态变化 合同状态变化
作废订单 已审核 → 已作废 审核通过 → 已作废
启用订单 已作废 → 已审核 已作废 → 审核通过

合同状态枚举:

状态值 枚举 说明
0 DRAFT 未提交
10 PROCESS 审批中
20 APPROVE 审核通过
30 REJECT 审核不通过
40 CANCEL 已取消
50 INVALID 已作废

12.4 API 接口

方法 路径 说明
PUT /erp/sale-order/cancel 作废销售订单
PUT /erp/sale-order/enable 重新启用销售订单

请求参数:

参数 类型 必填 说明
id Long 销售订单编号

响应:

{
  "code": 0,
  "msg": "操作成功"
}

错误码:

错误码 说明
1_020_201_011 作废失败,订单未审核
1_020_201_012 作废失败,已存在对应的销售出库单
1_020_201_013 作废失败,已存在对应的销售退货单
1_020_201_014 作废失败,请刷新页面后重试
1_020_201_015 启用失败,订单未作废
1_020_201_016 启用失败,请刷新页面后重试

12.5 前端修改点

文件路径: /src/views/erp/sale/order/index.vue

操作列按钮:

<el-table-column label="操作" width="280">
  <template #default="{ row }">
    <el-button v-if="row.status === 10" type="primary" size="small" @click="handleUpdate(row)">编辑</el-button>
    <el-button v-if="row.status === 10" type="success" size="small" @click="handleApprove(row)">审核</el-button>
    <el-button v-if="row.status === 20" type="warning" size="small" @click="handleProcess(row)">反审核</el-button>
    <el-button v-if="row.status === 20" type="danger" size="small" @click="handleCancel(row)">作废</el-button>
    <el-button v-if="row.status === 30" type="success" size="small" @click="handleEnable(row)">启用</el-button>
    <el-button v-if="row.status === 10" type="danger" size="small" @click="handleDelete(row)">删除</el-button>
  </template>
</el-table-column>

状态标签:

<el-table-column label="状态" prop="status" width="100">
  <template #default="{ row }">
    <el-tag v-if="row.status === 10" type="warning">未审核</el-tag>
    <el-tag v-if="row.status === 20" type="success">已审核</el-tag>
    <el-tag v-if="row.status === 30" type="info">已作废</el-tag>
  </template>
</el-table-column>

关联合同列(作废时显示红色):

<el-table-column label="关联合同" prop="contractNo" width="150">
  <template #default="{ row }">
    <el-link
      v-if="row.contractId"
      :type="row.status === 30 ? 'danger' : 'primary'"
      @click="handleViewContract(row)"
    >
      {{ row.contractNo }}
    </el-link>
    <span v-else>-</span>
  </template>
</el-table-column>

JS 方法:

import { cancelSaleOrder, enableSaleOrder } from '@/api/erp/saleOrder'

methods: {
  async handleCancel(row) {
    try {
      await this.$confirm('确认作废该销售订单?作废后关联合同也将标记为已作废!', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      })
      await cancelSaleOrder(row.id)
      this.$message.success('作废成功')
      this.getList()
    } catch (e) {
      if (e !== 'cancel') {
        this.$message.error(e.message || '作废失败')
      }
    }
  },
  async handleEnable(row) {
    try {
      await this.$confirm('确认重新启用该销售订单?启用后关联合同将恢复为审核通过状态!', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'info'
      })
      await enableSaleOrder(row.id)
      this.$message.success('启用成功')
      this.getList()
    } catch (e) {
      if (e !== 'cancel') {
        this.$message.error(e.message || '启用失败')
      }
    }
  }
}

API 定义文件: /src/api/erp/saleOrder.js

// 作废销售订单
export function cancelSaleOrder(id) {
  return request({
    url: '/erp/sale-order/cancel',
    method: 'put',
    params: { id }
  })
}

// 重新启用销售订单
export function enableSaleOrder(id) {
  return request({
    url: '/erp/sale-order/enable',
    method: 'put',
    params: { id }
  })
}

12.6 注意事项

  1. 作废操作会同步更新关联合同状态为"已作废"
  2. 启用操作会同步更新关联合同状态为"审核通过"
  3. 已出库/已退货的订单不能作废,需先处理相关业务单据
  4. 作废后订单号显示为红色,提醒用户该订单已失效
  5. 启用功能需要管理员权限(erp:sale-order:enable