本次改造实现 CRM 与 ERP 销售模块的深度集成,包括:
- Phase 1:客户数据统一 - ERP 使用 CRM 客户
- Phase 2:产品数据统一 - CRM/ERP 使用 MDM 物料
- Phase 3:合同生成销售订单
- Phase 4:销售全流程追溯
- Phase 5:营销数据报表
- Phase 6:数据迁移与清理(SQL 部分)
| 模块 | 说明 |
|---|---|
yudao-module-crm-api |
CRM API 模块(客户 API、报表 API、追溯 API) |
yudao-module-erp-api |
ERP API 模块(销售订单 API) |
yudao-module-mdm-api |
MDM API 模块(物料 API) |
| 文件 | 改动说明 |
|---|---|
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 |
| 页面 | 路径 | 改动说明 |
|---|---|---|
| 销售订单列表 | /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 客户选择组件 |
| 页面 | 路径 | 改动说明 |
|---|---|---|
| 合同列表 | /crm/contract/index |
添加"生成销售订单"按钮 |
| 合同详情 | /crm/contract/detail |
显示销售订单信息、销售流程追溯 |
| 页面 | 路径 | 改动说明 |
|---|---|---|
| 商机详情 | /crm/business/detail |
添加销售流程追溯入口 |
| 页面 | 路径 | 改动说明 |
|---|---|---|
| 客户详情 | /crm/customer/detail |
添加销售流程追溯列表 |
| 页面 | 路径 | 说明 |
|---|---|---|
| 报表概览 | /crm/report/dashboard |
数据概览页面 |
| 方法 | 路径 | 说明 |
|---|---|---|
| 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": "张三"
}
]
}
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /crm/contract/generate-sale-order | 根据合同生成销售订单 |
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | Long | 是 | 合同编号 |
响应:
{
"code": 0,
"data": 12345,
"msg": "操作成功"
}
| 方法 | 路径 | 说明 |
|---|---|---|
| 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 }
]
}
}
| 方法 | 路径 | 说明 |
|---|---|---|
| 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
}
]
}
}
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /rpc-api/erp/sale-order/create | 创建销售订单 |
| GET | /rpc-api/erp/sale-order/get | 获取销售订单 |
| GET | /rpc-api/erp/sale-order/get-by-contract-id | 根据合同编号获取订单 |
| 方法 | 路径 | 说明 |
|---|---|---|
| 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 | 校验物料存在 |
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>
文件路径: /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 }
})
}
}
文件路径: /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>
文件路径: /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>
文件路径: /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>
文件路径: /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 }
})
}
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`;
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`;
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`;
客户管理(CRM) → 商机跟进(CRM) → 合同签订(CRM) → 合同审批
↓
生成销售订单(ERP)
↓
订单审批(ERP)
↓
销售出库(ERP)
↓
客户回款(ERP)
销售漏斗转化:
客户(1000) → 商机(500) → 合同(300) → 回款(150)
↓ ↓ ↓ ↓
100% 50% 30% 15%
| Phase | 内容 | 状态 |
|---|---|---|
| Phase 1 | 客户数据统一 - ERP 使用 CRM 客户 | 已完成 |
| Phase 2 | 产品数据统一 - CRM/ERP 使用 MDM 物料 | 已完成 |
| Phase 3 | 合同生成销售订单 | 已完成 |
| Phase 4 | 销售全流程追溯 | 已完成 |
| Phase 5 | 营销数据报表 | 已完成 |
| Phase 6 | 数据迁移与清理 | 已完成 |
| 页面 | 路径 | 说明 |
|---|---|---|
| 报价单列表 | /crm/sale-quotation/index |
报价单列表、操作按钮 |
| 报价单表单 | /crm/sale-quotation/form |
新建/编辑报价单 |
| 报价单详情 | /crm/sale-quotation/detail |
查看详情、审批进度 |
| 方法 | 路径 | 说明 |
|---|---|---|
| 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 | 否 | 备注 |
文件路径: /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 || '转合同失败')
}
}
}
文件路径: /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 } })
}
商机(可选) → 销售报价(草稿) → 提交审批 → 审批中 → 审批通过 → 转合同
↓
审批拒绝 → 重新提交或修改
MdmItemApi),与 ERP 销售订单一致sale_quotation_approve 需要在 BPM 管理后台创建QT,格式:QT + yyyyMMdd + 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 变更脚本 |
销售订单作废功能用于标记已审核的订单为"已作废"状态,实现订单全流程可追溯。
业务规则:
- 只有已审核的订单才能作废
- 已出库或已退货的订单不能作废
- 作废后订单状态变为"已作废"
- 作废后同步更新关联合同状态为"已作废"
- 管理员可以重新启用已作废的订单
| 状态值 | 枚举 | 说明 |
|---|---|---|
| 10 | PROCESS | 未审核 |
| 20 | APPROVE | 已审核 |
| 30 | CANCEL | 已作废 |
当销售订单作废/启用时,会自动同步关联合同的状态:
| 操作 | 订单状态变化 | 合同状态变化 |
|---|---|---|
| 作废订单 | 已审核 → 已作废 | 审核通过 → 已作废 |
| 启用订单 | 已作废 → 已审核 | 已作废 → 审核通过 |
合同状态枚举:
| 状态值 | 枚举 | 说明 |
|---|---|---|
| 0 | DRAFT | 未提交 |
| 10 | PROCESS | 审批中 |
| 20 | APPROVE | 审核通过 |
| 30 | REJECT | 审核不通过 |
| 40 | CANCEL | 已取消 |
| 50 | INVALID | 已作废 |
| 方法 | 路径 | 说明 |
|---|---|---|
| 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 | 启用失败,请刷新页面后重试 |
文件路径: /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 }
})
}
erp:sale-order:enable)