概念和知识
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产品组合 增加字段 父项产品是否套件 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、增加数据范围
字段映射
方案二:CRM支持选配并生成标准BOM,同步到ERP
方案描述
- ERP物料清单同步到CRM的产品组合,标准BOM和配置BOM分别对应
- 订单从CRM同步到ERP
- CRM下单时,可以选择配置BOM,但是需要通过UI按钮生成标准BOM
- 选配生成的标准BOM 从CRM同步到ERP
- ERP订单开启套件管理
- 支持多级BOM
前置条件
- 金蝶云星空 套件 不能设置为在销售订单自动展开
- CRM 购买并开启 CPQ,灰度
- CRM 开启标准 BOM
对接流程
注意:以方案一进行调整和补充
1、【调整】物料清单集成流ERP往CRM
1.1、去除只同步标准BOM的数据范围
1.2、BOM分类字段字段映射
2、CRM产品组合增加字段 同步到ERP sync2Erp__c 布尔值
3、实现下单时,选配生成标准BOM,订单产品关联到标准BOM
新建UI按钮
函数:UI事件
/* 说明:
* pkg_key是CRM具有树状结构的单据会储存的值,用于表示树。同一个单据内明细的pkg_key唯一。
*
*/
////控制参数
def checkPageLimit = 10 //查重最大翻页
////
//当前明细
def details = context.getDetail('SalesOrderProductObj') as List
//选配BOM的父子结构: key: parentPkgKey value: sonPkgKey List (可以存在既是父节点,也是子节点,多级结构需要创建多个BOM)
Map<String, List> parentSonsMap = Maps.newLinkedHashMap()
//key:pkgKey value: 数据
Map pkgKey2Data = [:]
List stack = []
details.each { it ->
Map detail = it as Map
def pkgKey = detail['prod_pkg_key'] as String
def parentPkgKey = detail['parent_prod_pkg_key'] as String
if (pkgKey == null) {
//非树状结构明细,跳过
return
}
pkgKey2Data[pkgKey] = detail
if (detail['bom_type__q'] == 'configure') {
//当前树,是配置BOM
parentSonsMap.put(pkgKey, [])
stack.add(pkgKey)
}
if (parentSonsMap.containsKey(parentPkgKey)) {
//父节点是配置BOM
parentSonsMap.get(parentPkgKey).add(pkgKey)
}
}
Map pkg2BomId = [:]
Map parent2CoreId = [:]
stack.reverse().each { parent ->
//反向逐一检查每个BOM,子项检查一定在父项前。
def sons = parentSonsMap.get(parent)
if (sons.size() > 100) {
//超过100条子项,不检查,直接报错。(需要支持的话,得再改代码)
message.throwErrorMessage("存在超过100条子项的BOM")
}
//只能在同一个包里面的变量
Map productId2Pkg = [:]
//构建数据
Map mainConfig = pkgKey2Data[parent] as Map
def mainProductId = mainConfig['product_id'] as String
Map sonProductId2Quantity = [:]
List<Map> sonConfigs = []
productId2Pkg[mainProductId] = parent
sons.eachWithIndex { son, i ->
Map sonConfig = pkgKey2Data[son] as Map
def sonProductId = sonConfig['product_id'] as String
productId2Pkg[sonProductId] = son
sonProductId2Quantity[sonProductId] = sonConfig['quantity']
sonConfigs.add(sonConfig)
}
//参考:https://help.fxiaoke.com/0568/fc26/8bf6/22e5e767b0baf64e2b13a4a812595f61#header-11
//查重
//排序后生成特征值
String fea = null
sonConfigs.sort {
return it['product_id']
}.each {
def thisFea = (it['product_id'] as String) + "." + it['related_core_id'] + "." + it['quantity']
fea = fea == null ? thisFea : fea + "-" + thisFea
}
//如果需要查询不包含作废数据、返回满足条件的数据总数.
def isBreak = false
//key:core_id value: product_id.related_core_id.amount-
Map<String, String> core2Fea = [:]
Map<String, Map<String, String>> core2Product2BomId = [:]
def allList = []
Ranges.of(0, checkPageLimit).each {
if (isBreak) {
return
}
String sql1 = "select core_id,product_id,related_core_id,amount from BOMObj " +
"where core_id in (select id from BomCoreObj where product_id = '$mainProductId') " +
"order by core_id,product_id limit 100 offset ${it * 100};"
SelectAttribute att = SelectAttribute.builder()
.needInvalid(false)
.build()
def queryRes1 = object.select(sql1, att).result() as QueryResult
def list = queryRes1.getDataList()
if (list.size() < 100) {
isBreak = true
}
allList.addAll(list)
}
allList.each { it2 ->
def itMap = it2 as Map
def coreId = itMap['core_id'] as String
def sonProductId = itMap['product_id'] as String
def bomId = itMap['_id'] as String
core2Product2BomId.computeIfAbsent(coreId, { [:] }).put(sonProductId, bomId)
//特征不包含父物料
if (sonProductId == mainProductId) {
return
}
def thisFea = sonProductId + "." + itMap['related_core_id'] + "." + itMap['amount']
core2Fea.compute(coreId, { k, v ->
v = v == null ? thisFea : v + "-" + thisFea
return v
})
}
//检查是否存在符合条件的数据
def isMatch = false
core2Fea.each { coreId, thisFea ->
if (isMatch == true) {
return
}
if (thisFea == fea) {
//符合条件
isMatch = true
log.info("找到匹配的标准BOM:" + coreId)
parent2CoreId[parent] = coreId
core2Product2BomId[coreId].each { sonProductId, bomId ->
pkg2BomId[productId2Pkg[sonProductId]] = bomId
}
}
}
if (!isMatch) {
//没有匹配的,则创建
Map main = [
"owner" : ["1000"],
"purpose" : "sale",
"object_describe_api_name": "BomCoreObj",
"product_id" : mainProductId,
"category" : "standard",
"record_type" : "default__c"]
List newDetails = []
sons.eachWithIndex { son, i ->
Map sonConfig = pkgKey2Data[son] as Map
def sonProductId = sonConfig['product_id'] as String
Map newDetail = [
"amount" : sonConfig['quantity'],
"selected_by_default" : true,
"enabled_status" : true,
"order_field" : i,
"amount_editable" : false,
"record_type" : "default__c",
"is_required" : true,
"object_describe_api_name": "BOMObj",
"product_id" : sonProductId,
"price_mode" : "2"
]
newDetails.add(newDetail)
}
def createRes = object.create("BomCoreObj", main, ["BOMObj": newDetails], CreateAttribute.builder().build())
if (createRes.isError()){
message.throwErrorMessage("创建标准BOM失败:"+createRes.getMessage())
}
def ret = createRes.result() as Map
log.info("创建标准BOM:" + json.toJson(ret))
Map newBomCore = ret["data"] as Map
parent2CoreId[parent] = newBomCore["_id"]
ret['details']['BOMObj'].each { it ->
pkg2BomId[productId2Pkg[it['product_id']]] = it['_id']
}
}
}
log.info('parent2CoreId:' + json.toJson(parent2CoreId))
log.info('pkg2BomId:' + json.toJson(pkg2BomId))
//新建UIEvent事件
UIEvent event = UIEvent.build(context) {
//修改选配BOM产品
parentSonsMap.each { parent, sons ->
//修改父产品
def bomCoreId = parent2CoreId[parent] as String
def parentBomId = pkg2BomId[parent] as String
editDetail "SalesOrderProductObj" set("bom_type": "标准BOM", "bom_type__q": "standard", "bom_type__r": "标准BOM",
"bom_core_id": bomCoreId,
"bom_id": parentBomId,
"bom_instance_tree_id": null
) where { x -> (x['prod_pkg_key'] as String) == parent }
//修改子产品
sons.each { son ->
def sonBomId = pkg2BomId[son] as String
editDetail "SalesOrderProductObj" set("bom_type": "标准BOM", "bom_type__q": "standard", "bom_type__r": "标准BOM",
"bom_core_id": bomCoreId,
"bom_id": sonBomId,
"bom_instance_tree_id": null,
"new_bom_path": parentBomId + "." + sonBomId
) where { x -> (x['prod_pkg_key'] as String) == son }
}
}
}
return event
4、新增 物料清单 CRM往ERP同步集成流
4.1、数据范围:同步到ERP 等于 是
4.2、字段映射参考
回写组件:
方案三: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、字段映射