概念和知识
BOM(Bill of Materials)是制造业中常用的术语,意为“物料清单”或“零部件清单”。BOM 是一个产品的全部组成部分的详细列表,包括每个部分的名称、数量、规格等信息,它为生产、采购、成本核算、产品设计等提供了基础。
CPQ(Configure, Price, Quote)是一个销售工具,意为“配置、定价、报价”。CPQ 系统可以帮助销售人员快速准确地提供产品或服务的报价,尤其是在产品或服务有多种配置、定价复杂的情况下。CPQ 系统通常会与 CRM(客户关系管理)系统、ERP(企业资源规划)系统等集成,以提供完整的销售解决方案。
(来源:ChatGPT-4)
云星空相关知识
云星空设置
套件管理:【销售管理系统参数】- 【套件业务参数】-【启用套件管理】
- 当需要从 CRM 同步套件子项的订单产品到云星空时,需要设置不自动展开子项。
- 当仅将 CRM 同步【套件父项】时,如果配置了订单字段展开,需要提工单给集成平台研发过滤掉云星空上自动展开的套件子项,不处理。
CRM 设置
CPQ 及标准 BOM 开关
管理页:CPQ 配置,【CPQ 开启开关】【开启标准 BOM】
产品组合
仍在灰度,集成平台灰度咨询@谢嘉裕Ken,产品组合对象灰度咨询@李赵丹Linda
对接局限性
由于双方系统设计仍存在差异,对接也存在一些局限性,以下是一些已知情况。
- CRM 使用标准 BOM 时,订单产品子项将无法修改数量和价格
- 云星空物料清单的比例关系使用分子(子项)、分母(父项)表示,CRM 的产品选配明细只有数量字段,无法准确表示比如子项对父项为 2 比 3 的情况。
- 云星空通过【产品配置】,从配置BOM生成标准BOM,配置BOM和标准BOM存在关联关系。集成平台无法同步这个关联关系。即:产品配置这个动作无法实现同步
对接方案
由于 CPQ 对接涉及到的业务比较多,变化也可以很多。暂且提供几个场景的对接方案供参考。
注意事项
方案一:ERP标准BOM同步到CRM配置BOM
方案描述
- ERP 物料清单(标准 BOM 类型)同步到 CRM 的产品组合(配置 BOM类型)
- 订单从CRM同步到ERP
- CRM 下单,订单产品子项支持修改价格、数量等
- ERP订单开启套件管理
- 支持多级BOM
此方案考虑了多级BOM,会有【跳层】的处理,如果不存在多级BOM,或者仅同步两级到CRM且子项不存在跳层,可去除相应逻辑。
前置条件
- 金蝶云星空 套件 不能设置为在销售订单自动展开
- CRM 购买并开启 CPQ,灰度
- CRM 不需要开启标准 BOM,统一使用配置 BOM
对接流程
1、产品增加字段
是否套件 is_kit__c 单选
是;true
否;false
其他;other
2、物料集成流增加套件字段对接
3、CRM产品组合
CRM 产品选配明细增加字段 跳层 is_skip__c 布尔值 :关联产品组合并跳层时,订单明细同步其子项,不同步自身
4、新增 中间对象 物料清单【ENG_BOM】
4.1、 物料清单主键修改,原 id 字段修改为文本类型,【BOM 版本】字段修改为主键。
4.2、物料清单-子项明细 增加字段 【quantity】,集成平台会通过数量(分子)/数量(分母)计算赋值到该字段。
5、新增「 物料清单 」集成流
5.1、集成流
5.2、数据范围设置:BOM 分类 等于 标准 BOM
5.3、字段映射参考:BOM类型CRM只有配置BOM
6、订单产品增加字段
6.1、产品类型 product_type__c 单选
标准产品;standard
套件父项;parent
套件子项;son
跳过(不同步);skip
其他;other
6.2、父项产品(仅套件子项)parent_product__c 查找关联 产品
6.3、产品选配明细(仅套件子项)son_bom_id__c 查找关联 产品选配明细
6.4、跳层 is_skip__c 引用 产品选配明细 跳
6.5、是否套件 is_kit__c 引用 产品名称 是否套件
7、订单新建和编辑按钮 增加后函数,用于在赋值产品类型、父项产品等字段
def details = context.details.'SalesOrderProductObj'
log.info(json.toJson(details))
Map detailId2Type = [:]
//保存父子,key:父节点detailId, value:子节点detailIds
Map pkg2ObjMap = [:]
Map id2ObjMap = [:]
Set sonSkipIds = []
//会按顺序,先父后子遍历
details.each { it ->
def detail = it as Map
String id = detail.'_id'
if (!detail.'bom_id') {
//不关联BOM,为标准产品
detailId2Type[id] = 'standard'
} else {
id2ObjMap[id] = detail
def pkg = detail['prod_pkg_key']
def parentPkg = detail['parent_prod_pkg_key']
def rootPkg = detail['root_prod_pkg_key']
def productId = detail['product_id']
pkg2ObjMap[pkg] = detail
if (parentPkg == null) {
//根节点
if (detail['is_kit__c__q'] == 'true') {
//套件
detailId2Type[id] = 'parent'
} else {
//非套件
detailId2Type[id] = 'standard'
sonSkipIds.add(id)
}
} else {
//非根节点
def parentId = pkg2ObjMap[parentPkg]['_id']
if (sonSkipIds.contains(parentId)) {
//直接跳过
detailId2Type[id] = 'skip'
sonSkipIds.add(id)
} else {
if (detail['bom_core_id']) {
//关联产品组合
if (detail['is_skip__c__q']) {
//是跳层,自身不同步
detailId2Type[id] = 'skip'
} else {
//不是跳层,自身同步,子不同步
detailId2Type[id] = 'son'
sonSkipIds.add(id)
}
} else {
detailId2Type[id] = 'son'
}
}
}
}
}
log.info(json.toJson(detailId2Type))
//修改订单产品的类型
Map updateArg = [:]
detailId2Type.each { detailId, type ->
def fields = ['product_type__c': type]
if (type == 'son' ) {
//父项产品关联到根产品
def detail = id2ObjMap[detailId]
def rootDetail = pkg2ObjMap[detail['root_prod_pkg_key']]
fields['parent_product__c'] = rootDetail['product_id']
fields['son_bom_id__c'] = detail['bom_id']
}
updateArg[detailId] = fields
}
APIResult result = object.batchUpdate("SalesOrderProductObj", updateArg)
if (result.isError()) {
message.throwErrorMessage("更新订单产品类型失败:" + result.message())
}
8、订单集成流
8.1、增加数据范围
8.2、字段映射
方案二:CRM支持选配并生成标准BOM,同步到ERP
方案描述
- 下单可选择 配置BOM 或者标准 BOM,订单从CRM同步到ERP
- 对CRM关联配置BOM的订单产品,CRM自动生成标准BOM,同步到ERP
- ERP订单开启套件管理
- 支持多级BOM,但方案未考虑跳层。即订单产品只会同步父项和一级的子项。
注意事项
- 方案中,存在对物料套件的判断。(当产品是套件时,才会按套件父项同步订单产品,并同步其子项的订单产品)。如果保持该逻辑,需要注意物料历史数据处理。
- 方案中,从CRM管理标准BOM,如有需要,可新建ERP往CRM的物料清单集成流,将历史标准BOM同步到CRM。
- 方案中,不考虑BOM使用跳层。(概念参考上方多级BOM文档)
前置条件
- 金蝶云星空 销售管理系统参数- 启用套件管理,不自动展开
- CRM CPQ配置-开启标准BOM,根据下单选配结果生成标准BOM 选择【自动生成标准BOM】
对接流程
1 产品增加字段 是否套件 is_kit__c 单选
是;true否;false其他;other
2 物料集成流增加套件字段对接
3 新增 物料清单 CRM往ERP同步集成流
3.1、数据范围组件
BOM类型 等于 标准BOM
3.2、字段映射参考
- 产品组合 → 物料清单
- 产品选配明细 → 物料清单-子项明细
3.3、回写组件
订单产品增加以下字段
字段名 | apiName | 类型 | 其他 |
标准产品组合(同步前赋值) | std_bom_core__c | 查找关联 产品组合 | 是否可复制 否,是否必填 否 |
产品类型 | product_type__c | 单选标准产品;standard标准产品(带BOM);standardWithBom套件父项;parent套件子项;son非套件(不同步);nonKit跳过(不同步);skip其他;other | 是否可复制 否,是否必填 否 |
产品选配明细(仅套件子项) | son_bom_id__c | 查找关联 产品选配明细 | 是否可复制 否,是否必填 否 |
父项产品(仅套件子项) | parent_product__c | 查找关联 产品 | 是否可复制 否,是否必填 否 |
4 订单集成流
数据范围增加
是否生成标准BOM 等于 是
字段映射增加以下映射
同步前函数增加
/**
* 同步前函数,支持数据过滤
*/
@Override
IntegrationStreamSync.BeforeSyncResult executeBeforeSync(FunctionContext context, IntegrationStreamSync.BeforeSyncArg arg) {
Integer sourceEventType = arg.getSourceEventType() //源系统触发事件-已废弃,请勿使用
String sourceObjectApiName = arg.getSourceObjectApiName() // 源系统对象apiName
if( sourceObjectApiName !='SalesOrderObj' ){
//非主对象不处理
log.info("ignore "+sourceObjectApiName)
return null
}
String destObjectApiName = arg.getDestObjectApiName() // 目标系统对象apiName
ErpObjectData objectData = arg.getObjectData() // 来源系统主对象字段信息
Map<String, List<ErpObjectData>> details = arg.getDetails() // 来源系统从对象信息 key:从对象apiName value:从对象字段信息列表
String sourceDataId = arg.getSourceDataId() // 源系统数据id,只有crm->erp方向有这个字段
String name = objectData.getName() // 来源系统对象数据主属性
def lines = details.'SalesOrderProductObj'
Map detailId2Type = [:]
//保存父子,key:父节点detailId, value:子节点detailIds
Map pkg2ObjMap = [:]
Map id2ObjMap = [:]
Set sonSkipIds = []
//会按顺序,先父后子遍历
lines.each { it ->
def detail = it as Map
String id = detail.'_id'
id2ObjMap[id] = detail
if (!detail.'bom_id') {
//不关联BOM,为标准产品
detailId2Type[id] = 'standard'
} else {
def pkg = detail['prod_pkg_key']
def parentPkg = detail['parent_prod_pkg_key']
def rootPkg = detail['root_prod_pkg_key']
def productId = detail['product_id']
pkg2ObjMap[pkg] = detail
if (!parentPkg) {
//根节点
if (detail['is_kit__c__v'] == 'true') {
//套件
detailId2Type[id] = 'parent'
} else {
//非套件
detailId2Type[id] = 'standard'
sonSkipIds.add(id)
}
} else {
//非根节点
def parentId = pkg2ObjMap[parentPkg]['_id']
if (sonSkipIds.contains(parentId)) {
//直接跳过
detailId2Type[id] = 'skip'
sonSkipIds.add(id)
} else {
if (detail['bom_core_id']) {
//关联产品组合
//不考虑跳层,自身同步,子不同步
detailId2Type[id] = 'son'
sonSkipIds.add(id)
} else {
detailId2Type[id] = 'son'
}
}
}
}
}
log.info(json.toJson(detailId2Type))
//修改订单产品的类型
Map updateArg = [:]
//筛选明细
def newDetails = []
detailId2Type.each { detailId, type ->
def detail = id2ObjMap[detailId]
def fields = ['product_type__c': type]
detail['product_type__c'] = type
if( type == 'parent' ){
//套件父项 关联标准产品组合
def std_com_core = detail['standard_bom_id']
detail['std_bom_core__c'] = std_com_core
fields['std_bom_core__c'] = std_com_core
}
if (type == 'son' ) {
//套件子项 关联 标准产品选配明细 + 父项产品
def rootDetail = pkg2ObjMap[detail['root_prod_pkg_key']]
fields['parent_product__c'] = rootDetail['product_id']
detail['parent_product__c'] = rootDetail['product_id']
fields['son_bom_id__c'] = detail['standard_bom_line_id']
detail['son_bom_id__c'] = detail['standard_bom_line_id']
}
if( type == 'parent'|| type == 'son'||type == 'standard'){
//标准 或 父项 子项 则同步
newDetails.add(detail)
//标准产品也需要关联标准产品组合
detail['std_bom_core__c'] = std_com_core
fields['std_bom_core__c'] = std_com_core
}
updateArg[detailId] = fields
}
APIResult updateResult = object.batchUpdate("SalesOrderProductObj", updateArg)
if (updateResult.isError()) {
message.throwErrorMessage("更新订单产品类型失败:" + updateResult.message())
}
details['SalesOrderProductObj'] = newDetails
def data = new IntegrationStreamSync.BeforeSyncData();
data.setIsExec(true) // 为false 数据过滤,不做同步
data.setIsCover(true) // 不给值或为false 不修改源数据
data.setObjectData(objectData) // 替换源数据,为null主从对象都不做替换 如果只需要替换从对象,可以设置 data.setObjectData(objectData)
data.setDetails(details) // 替换从对象信息,为null不做替换 key:从对象apiName value:从对象信息列表
def result = new IntegrationStreamSync.BeforeSyncResult()
result.setCode("0") // 返回结果状态 0为正常,其他都是失败
result.setMessage("success") // 返回的错误信息
result.setData(data)
return result
}
方案三:ERP标准BOM同步到CRM的 标准BOM
方案描述
这个方案和方案一基本一直,区别是在CRM使用标准BOM。这时CRM下单时不能修改订单产品的子项数据
- ERP 物料清单(标准 BOM 类型)同步到 CRM 的产品组合(标准 BOM类型)
- 订单从CRM同步到ERP
- CRM 下单,订单产品子项不支持修改价格、数量等
ERP订单开启套件管理
支持多级BOM
此方案考虑了多级BOM,会有【跳层】的处理,如果不存在多级BOM,或者仅同步两级到CRM且子项不存在跳层,可去除相应逻辑。
前置条件
金蝶云星空 套件 不能设置为在销售订单自动展开
CRM 购买并开启 CPQ,灰度
CRM 开启标准 BOM,且不使用配置 BOM
对接流程
1、产品增加字段 是否套件 is_kit__c 单选
是;true
否;false
其他;other
2、物料集成流增加套件字段对接
3、CRM产品组合 增加字段 父项产品是否套件 is_kit__c 引用 父项产品 是否套件
4、CRM 产品选配明细增加字段 跳层 is_skip__c 布尔值 :关联产品组合并跳层时,订单明细同步其子项,不同步自身
5、增 中间对象 物料清单【ENG_BOM】
5.1、物料清单主键修改,原 id 字段修改为文本类型,【BOM 版本】字段修改为主键。
5.2、物料清单-子项明细 增加字段 【quantity】,集成平台会通过数量(分子)/数量(分母)计算赋值到该字段。
6、新增 物料清单 集成流
6.1、集成流
6.2、数据范围设置:BOM 分类 等于 标准 BOM
6.3、字段映射参考:BOM类型CRM只对接标准BOM
7、订单产品增加字段
7.1、产品类型 product_type__c 单选
标准产品;standard
套件父项;parent
套件子项;son
跳过(不同步);skip
其他;other
7.2、父项产品(仅套件子项)parent_product__c 查找关联 产品
7.3、产品选配明细(仅套件子项)son_bom_id__c 查找关联 产品选配明细
7.4、跳层 is_skip__c 引用 产品选配明细 跳层
7.5、是否套件 is_kit__c 引用 产品名称 是否套件
8、订单新建和编辑按钮 增加后函数,用于在赋值产品类型、父项产品等字段
def details = context.details.'SalesOrderProductObj'
log.info(json.toJson(details))
Map detailId2Type = [:]
//保存父子,key:父节点detailId, value:子节点detailIds
Map pkg2ObjMap = [:]
Map id2ObjMap = [:]
Set sonSkipIds = []
//会按顺序,先父后子遍历
details.each { it ->
def detail = it as Map
String id = detail.'_id'
if (!detail.'bom_id') {
//不关联BOM,为标准产品
detailId2Type[id] = 'standard'
} else {
id2ObjMap[id] = detail
def pkg = detail['prod_pkg_key']
def parentPkg = detail['parent_prod_pkg_key']
def rootPkg = detail['root_prod_pkg_key']
def productId = detail['product_id']
pkg2ObjMap[pkg] = detail
if (parentPkg == null) {
//根节点
if (detail['is_kit__c__q'] == 'true') {
//套件
detailId2Type[id] = 'parent'
} else {
//非套件
detailId2Type[id] = 'standard'
sonSkipIds.add(id)
}
} else {
//非根节点
def parentId = pkg2ObjMap[parentPkg]['_id']
if (sonSkipIds.contains(parentId)) {
//直接跳过
detailId2Type[id] = 'skip'
sonSkipIds.add(id)
} else {
if (detail['bom_core_id']) {
//关联产品组合
if (detail['is_skip__c__q']) {
//是跳层,自身不同步
detailId2Type[id] = 'skip'
} else {
//不是跳层,自身同步,子不同步
detailId2Type[id] = 'son'
sonSkipIds.add(id)
}
} else {
detailId2Type[id] = 'son'
}
}
}
}
}
log.info(json.toJson(detailId2Type))
//修改订单产品的类型
Map updateArg = [:]
detailId2Type.each { detailId, type ->
def fields = ['product_type__c': type]
if (type == 'son' ) {
//父项产品关联到根产品
def detail = id2ObjMap[detailId]
def rootDetail = pkg2ObjMap[detail['root_prod_pkg_key']]
fields['parent_product__c'] = rootDetail['product_id']
fields['son_bom_id__c'] = detail['bom_id']
}
updateArg[detailId] = fields
}
APIResult result = object.batchUpdate("SalesOrderProductObj", updateArg)
if (result.isError()) {
message.throwErrorMessage("更新订单产品类型失败:" + result.message())
}
9、订单集成流
9.1、增加数据范围
9.2、字段映射
迁移方案
调整CRM的BOM数据结构,和集成平台配置、中间表,不调整ERP的结构
历史场景的迁移方案
场景一
连接器:金蝶云星空
原对接场景:ERP物料清单同步到CRM的BOMObj,物料清单表头会生成虚拟主产品(带版本号)进行对接。下单选择BOM时,是选择虚拟产品
原来的CRM假数据全部作废或删除(物料清单生成的产品、BOMObj、集成平台中间表)
升级CRM BOMObj为BomCoreObj
将物料清单从ERP同步到CRM的BomCoreObj(主从结构)
可能的影响:历史订单不可修改!BOMObj在CRM的历史报表不准确
按新方案调整对接配置
场景二
连接器:金蝶云星空
原对接场景:CRM的BomInstance,同步到ERP的物料清单
创建沙盒环境(CRM和云星空)
按上方方案二,在沙盒环境配置 并 验证流程。(如果需要可根据客户场景调整)
验证后,确认 配置迁移 和 历史数据的处理流程
正式环境 升级,配置迁移,历史数据处理
以下是历史数据处理的推荐方案:
升级CRM BOMObj为BomCoreObj ,数据不处理(CRM侧)
(可选)配置一个ERP往CRM方向的物料清单集成流(参考方案三的部分),同步历史标准BOM到CRM
验证后,删除用于导入历史数据的ERP往CRM的物料清单集成流。
不需要处理集成平台的中间表数据
可能的影响:历史订单不可修改!BOMObj在CRM的历史报表不准确
场景三
连接器:其他
原对接场景:ERP的对象,同步到CRM的BOMObj
需逐个分析,可参考上述的金蝶云星空的方案
确认事项:
1. CRM数据结构调整确认:默认认为CRM数据结构调整在CRM侧覆盖旧结构的需求。但需要确认新的结构,对接上能否满足客户需求。云星空直接参考本文配置,其他渠道的参考CRM结构的部分。
2. 历史数据迁移能力确认:确认历史数据能否迁移,且按CRM新数据生成中间映射。CRM数据找@陈川华确认,应该都是可以的。ERP数据不迁移,根据方案,可能需要重新导入数据到CRM,旧数据作废。
3. 确认CRM侧下游单据的影响:订单等
4. 确认迁移步骤: 针对不同项目,迁移步骤可能会有所不同。需要明确是否处理历史数据,以及是进行数据迁移还是删除后重新导入等
迁移步骤:
1. 沙盒环境调整与验证: 在沙盒环境中进行按新数据结构进行对接配置,并验证。
2. 迁移CRM的BOMObj及刷历史数据:联系陈川华
3. 修改正式环境集成平台配置,导入中间表(如有需要)。
4. 验证正式环境数据(包括历史数据和新数据)