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产品组合 
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. 验证正式环境数据(包括历史数据和新数据)
2024-06-26
1 0