8.云星空BOM对接

概念和知识

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、字段映射
2024-02-19
0 0