实践案例|业务批量化工作:实现复制、编辑、审批一体化流程

栏目:云苍穹知识作者:金蝶来源:金蝶云社区发布:2024-09-23浏览:1

实践案例|业务批量化工作:实现复制、编辑、审批一体化流程

在快节奏的商业世界中,合同批量续签的效率至关重要。本文将向您展示如何通过创新的批量复制、编辑和审批流程,将繁琐的合同管理工作转变为高效、自动化的解决方案,让企业在合同管理上更加得心应手。

作者在案例基础上,也解析了相关技术知识,以帮助读者更好地理解实现思路,一起看看吧~



业务背景


某公司在进行项目管理时,常需与合作伙伴签订多份合同。当这些合同到期时,为了保持业务的连续性,公司希望能够实现自动续签功能。在续签过程中,公司需要统一修改每份合同的生效日期截止日期,以确保新合同的时效性与旧合同相衔接。


同时,为了保障合同内容的一致性和完整性,其他合同信息在续签时将不允许修改


此外,为了提升业务处理效率,公司还希望能够对批量续签的合同进行批量审批,以减少人工操作,加快审批流程,从而加速业务进行速度。以商超与供应商之间的合作为例,当面临多批次、多类型的供应合同到期时,公司可以根据旧有协议,对同一供应商的所有供应货物进行批量续签,并通过批量审批快速完成合同的更新和确认。


解决方案


在合同管理过程中,续签通常涉及到单据的复制、编辑和审批三个关键环节。尽管单个环节的复制、编辑和审批功能在现有的系统中都能得到较好的支持,但当需要实现这三个环节的批量操作时,尤其是要将它们在一个流程中统一,苍穹系统目前未提供标准化的解决方案。


现有的单个环节解决方案,在整合使用时存在两个问题

1. 这些方案之间的匹配度不高,可能无法覆盖所有必要的业务场景,流程存在缺失环节;

2. 这种方式无法直观地展示三个环节统一后的业务逻辑,使得操作变得复杂且不够直观。


为了解决这些问题,我们决定重新设计并整合这三个环节,将它们统一到一个新的“批量续签单据”中。

通过这个新的单据,我们可以对同一合同类型的多份合同进行批量续签的编辑和审批操作。这样做的好处是,所有批量续签的合同都将有一个统一的入口进行展示和编辑,领导在审批过程中也能直观地看到每个待审批的单据,并可以根据需要单独控制每个合同的审批状态。这种整合方式不仅提高了操作的便捷性和效率,也使整个续签过程更加清晰和可控。


方案效果图展示如下:


上传图片


本文所涉及的知识点包括父子页面的交互、前后台数据的交互、DynamicObject的序列化和反序列化等,请参考以下社区文档:

父子页面交互、前后台交互的知识点总结

https://vip.kingdee.com/link/s/l49m4


1. 绘制批量审批单据


上传图片


为了提升批量续签合同的操作效率和用户体验,我们将整个流程划分为三个主要部分:

  • 数据编辑面板用于批量编辑信息;

  • 分录面板用于记录续签合同与原单据的关联关系;

  • 页签展示面板详细展示续签合同的各项信息。


业务呈现方式如下:

(1)操作员首先进入合同列表页面,从中勾选出需要进行批量续签的合同。随后,携带这些选定的合同信息,操作员将进入批量续签的单据页面。在此页面,操作员将原合同的关键信息填写到单据体中,确保信息的准确性。

(2)接下来,操作员检查并设置续签合同的相关信息,这些信息将展示在页签上。请注意,由于系统限制,续签合同的数量不得超过页签的上限数量,即最多10份

(3)完成合同信息的设置后,操作员可以利用上方的批量编辑面板,对需要统一修改的字段进行批量编辑。填写完毕后,通过保存操作,这些更改将自动应用到所有选定的合同单据中。

(4)最后,操作员确认所有信息无误,即可退出批量续签页面,完成整个批量续签流程。


整个流程如下:

进入合同列表页面 → 勾选需续签的合同 → 进入批量续签页面 → 填写原合同关键信息 → 检查并设置续签合同信息 → 使用批量编辑面板进行编辑 → 保存并应用更改 → 确认并退出。


2. 批量单据复制实操


在处理单据的批量续签时,苍穹系统提供了“copy”操作代码来简化复制流程。尽管“copy”操作能够自动处理新单据的页签显示、原单数据的携带以及编码规则的获取等步骤,但在本次的特定需求中,我们需要将批量复制的单据展示在特定的页签上。由于“copy”操作背后的逻辑在常规跟踪过程中并不直接可见,我们需要采取一种不同的方法来实现这一需求。


我们考虑了两个方案


方案一:我们计划直接在页签上打开并展示指定数量的单据,通过程序逻辑将数据赋值到相应的页面元素上。这种方案能够确保客户在数据保存之前就能直观地看到并确认每张续签单据的信息。


方案二:先将所有批量续签的数据保存到数据库中,并做好相应的标记,然后再从数据库中加载这些数据并展示在页签上。虽然这种方法在技术上相对简单,但它有一个显著的缺点:在数据保存到数据库之前,客户无法直接看到和确认这些单据,这可能会遗漏一些必要的配置或要修改的内容。


鉴于上述考虑,我们选择了方案一。然而,这也带来了一个挑战如何将携带有特定数据的单据准确地展示在指定的页签上


为了解决这个问题,我们将采用一种循环加载的策略。具体来说,我们会根据待续签的单据数量,动态地创建并加载相应数量的页签,然后通过编程逻辑将每份单据的数据赋值到对应的页签上。最后,我们会通过激活第一个页签的pageId,使其成为当前默认展示的页签。这样,客户就能在一个统一的界面中,直观地查看、编辑并确认所有的续签单据。


private String showBillInTab(int i){ 
    String billFormId = (String)getModel().getValue("isv_bill_type"); 
    Bil1ShowParameter parameter = new BillShowParameter(); 
    parameter.setFormId(billFormId); 
    parameter.setParentFormId(getView().getPageId()); 
    parameter.setStatus(Operationstatus.ADDNEW); 
    parameter.geOpenstyle().setShowType(ShowType.NewTabPage); 
    parameter.geOpenstyle().setTargetKey("isv_tabap"); 
    parameter.setCustomParam("billFormId", billFormId); 
    parameter.setBillTypeId(billFormId); 
    // 为了对应续签前的原单据数据,我们可以将对应续签单据体的行号传进来
    parameter.getCustomParams().put("index", i);
    parameter.addCustPlugin("isv.xxx.Batchshowplugin"); 
    parameter.setHasRight(true); 
    getview().showForm(parameter); 
    return parameter.gePageld();
}

private void activeDefaultTab(String pageId) { 
    Tab tab = getcontrol("isv_tabap"); 
    tab.activeTab(pageld);
}


通过这样的方式展示,我们最终能得到指定数量的、新的、携带默认值而不是复制得到的原单据数据的单据,相关的批量逻辑控制如不允许编辑指定相关字段等,可以通过注册动态插件来完成。所以我们接下来的重点工作是如何分别把原单数据携带到我们刚刚创建的单据中


查看指定的数据一般是通过“parameter.setPkId()”来完成,但是我们未落库的数据查看,就需要借助特殊的方法去完成了。

对于已加载的页面,可以通过“model.push(dynamicObj)”来完成数据的加载,但由于苍穹在页签加载过程中,未被激活过的页签是不会实际加载页面的,也就无法完成数据的刷新,所以我们需要进一步进行逻辑处理。

我们分为两步走,当页签页面被激活后,我们通过上述方案刷新数据,如果没有激活我们可以缓存下来,直到页面激活进入真正的生命周期我们加载数据,于是有了以下方案:


批量续签的插件
public void afterCreateNewData(EventObject e){ 
    //准备数据 
    Dynanicobject[] originData = BusinessDataServiceHelper.load();
    for(Dynanicobject dataEntity : originData) {
        if (getView().getView(pageld).getModel().isDataLoaded()){ 
            // 页面已完成加载刷新数据,对应tab已经active的页签
            getView().getView(papeld).getModel().push(dataEntity); 
        } else { 
            //页面未完成加载记录到缓存
            getPageCache().put(pageId, DataEntityserializer.serializerToString(dataEntity, option));
        }
    }
}
    
// 动志注册的子页面的插件 isv.xxx.BatchShowPlugin
public void createNewData(BizDataEventArgs e) { 
    super.createNewData(); 
    String data = getView().getParentView().getPageCache().get(getView().getPageId()); 
    // 如果准备了数实则使用运存中的数据 
    if (!StringUtils.isEmpty(data)) { 
        DataEntityDeserializerOption option = new DataEntityDeserializerOption(); 
        DynamicObject saveData = (DymamiOobject)DataEntitySerializer.deSerializerFromString(data, entityType, option); 
        BusinessDataServiceHelper.loadRefence(new object[]{saveData}, entityType); 
        e.setDataEntity(saveData): 
    }
}


这样,我们就完成了数据的后台创建,并能推送到前台显示。


3. 批量编辑与数据加载


有了合适的数据及界面展示,剩下的工作就是批量编辑。如果有N个字段需要编辑,我们在设计器上画上N个对应类型的字段,在“保存”按钮点击时,就将这N个字段的值反写回去。

当然,可以根据业务需要自行设计保存逻辑为前端页面激活保存、无前端状态保存、数据库直接保存,前两者适合有前端插件参与的情况,后者可对没有激活的页面进行临时数据保存,请根据业务需要自行选择。

@Override
publ1c void beforeDoOperation(BeforeDoOperationEventArgs args) { 
    switch(opkey){ 
        case "save": 
            if (getView().getView(papeld).getModel().isDataLoaded()) { 
                // 页面已完成加载直接执行页面的保存操作  
                getView().getView(pageId).getModel().setValue("xxx",getModule,getValue("xxx"));    
                // 前端页面激活保存
                OperationResult save = getView(pageId).invokeOperation("save"); 
                getView().sendFormAction(getView().getView(pageId)); 
            } else { 
                // 页面未完成加载执行后台保存操作,并根据需要是否落库 
                DataEntityDeserializerOption option = new DataEntityDeserializerOption(); 
                String cache = getPageCache().get(pageId); 
                DynamicObject saveData = DataEntitySerializer.deSerializerFromString(cache, entityType, option); 
                // 设置批量编辑的字段值
                saveData.set("xxx", getModule().getVaule("xxx")); 
                // 数据库直接保存
                saveByDataBase(pageId, saveData);
                // 无前端状态保存
                // saveByBackground(pageId, saveData);
            } 
            break; 
        default: 
            break;
    }
}

private boolean saveByDataBase(String pageId, DynamicObject saveData){
    CodeRuleInfo codeRule = CodeRuleServiceHelper.getCodeRule(saveData.getDataEntityType().getName(), saveData, String.valueOf(RequestContext.get().getOrgId()));
    String number = CodeRuleServiceHelper.getNumber(codeRule, saveData);
    saveData.set(BILLNO, number);
    // 记得保存完成之后将修改过的值存入缓存,避免未激活的页面未获取到最新的修改
    cache = DataEntitySerializer.serializerToString(saveData, new DataEntitySerializerOption());
    getPageCache().put(pageId, cache);
}

private boolean saveByBackground(String pageId, DynamicObject saveData){
    // 方案一 
    String cache = DataEntitySerializer.serializerToString(saveData, new DataEntitySerializerOption());
    getPageCache().put(pageId, cache);
    IFormView billViews = (IBillView) SessionManager.getCurrent().getView(pageId);
    billView.getModel().beginInit();
    billView.getModel().createNewData(saveData);
    billView.getModel().setCacheExpireAfter(true);
    billView.getModel().endInit()
    billView.updateView();
    billView.invokeOperation("save");

 // 方案二,模拟前端F12加载新页面的过程,kd.bos.mservice.form.FormServiceImpl#batchInvokeAction(java.lang.String, java.lang.String, java.util.Map<java.lang.String,java.lang.Object>)
    // ReflectUtils.invokeCosmicMethod("kd.bos.service.ServiceFactory", "FormService", "batchInvokeAction", pageId, "[{\"key\":\"\",\"methodName\":\"loadData\",\"args\":[],\"postData\":[]}]");
    // IFormView billViews= (IBillView) SessionManager.getCurrent().getView(pageId);
}

// 动志注册的子页面的插件 isv.xxx.BatchshowPlugin
@Override
publie void afterCreateNewData(EventObject e){ 
    super.afterCreateNewData(e); 
    getPageCache().put("late_aftercreatenewdate_to_afterbinddateAbstractCodeRule", string.valueOf(false));
}


当然为了保证编码的连续性问题,我们需要在动态注册的插件中加入编码标志,来保证我们的单据数据不会占用两个编码,导致断号问题:


// 动志注册的子页面的插件 isv.xxx.BatchShowPlugin
@Override
publie void afterCreateNewData(EventObject e){ 
    super.afterCreateNewData(e); 
    getPageCache().put("late_aftercreatenewdate_to_afterbinddateAbstractCodeRule", string.valueOf(false));
}


完成了批量编辑后,我们就可以进行提交,并进入审核阶段了。

和保存的逻辑一样,我们同样区分前台提交和后台提交,但为了保证数据状态的统一性,我们需要会对提交的结果进行校验,如果有一个单据提交校验失败,回退所有已提交成功的单据。


@Override 
public void beforeDoOperation(BeforeDoOperationEventArgs args) { 
    super.beforeDoOperation(args); 
    switch (operate.getOperateKey()){
        case "submit": 
            List<Long> successPKids; 
            List<String> successPageids; 
            boolean isError = false;
            // 需要后台执行submit的数据,也可以参考保存,做成模拟前台提交
            List<DynamicObject> list = new ArrayList();
            for (int i = 0; i < rowCount; i++) {
                // 页面已完成加载直接执行页面的提交操作 
                String pageId = getPageCache().get(String.valueOf(getModel().getValue("fxyz_old_contractid", i))); 
                if (getView().getView(pageId).getModel().isDataLoaded()){ 
                    getView().getView(pageId).getModel().setValue("xxx", "xxx"); 
                    OperationResult submit = getView().getView(pageId).invokeOperation("submit"); 
                    getView().sendFormAction(getView().getView(pageId)); 
                    if (submit.isSuccess()){ 
                        successPageId.add(pageId);
                    } else {
                        break;
                    }
                } else { 
                    // 页面未完成加载,根据需要是否执行后台提交操作 
                    String data = getPageCache().get(getPageCache().get(String.valueOf(getModel().getValue("fxyz_old_contractid", i)))); 
                    DynamicObject submitData = (DynamicObject) DataEntitySerializer.deSerializerFromString(data, dataEntityType); 
                    BusinessDataServiceHelper.loadRefence(new Object[]{submitData}, dataEntityType);
                    submitData.set("xxx", "xxx"); 
                    list.add(submitData);
                }
            }
            // 如果有失败的记录,则回退所有执行成果的前台提交和后台提交 
            if (isError){ 
                for(successPageids) {
                    OperationResult submit = getView().getView(pageId).invokeOperation("unsubmit");
                }
                for(successPKids){ 
                    OperationResult operationResult = OperationServiceHelper.executeOperate("unsubmit", billFormId, new DynamicObject[]{submitData}, OperateOption.create()); 
                    getView().sendFormAction(getView().getView(pageId));
                }
            } else {
                    OperationResult operationResult = OperationServiceHelper.executeOperate("submit", billFormId, new DynamicObject[]{submitData}, OperateOption.create()); 
                    List<Object> pkIds = operatoperationResult.getSuccessPKIds(); 
                    for(pkIds){ 
                        successPKids.add(pkId);
                    }
                    // TODO 如果失败,处理所有成功的记录,注意提交后会创建审批流工作流,未创建完成等情况会撤销报错,建议设置修复状态
            }
            break;
        default: 
            break;
    }
}


当然,刚刚提交的流程会存在一定延时以创建流程实例,所以有时会遇到“流程已挂起或已结束不能撤销”等提示,这个时候可以通过状态码“errorcode_001“ 或者”atttimeout“来进行判断、设置循环等,来修复状态不统一的问题,或手动添加修正功能,保证所有单据状态的审批状态统一。



最后我们特别设计了一个独立的批量续签审批流程,以确保批量续签的合同能够与其他合同在审批过程中有所区分。对于携带有批量续签标记的合同,它们将不再遵循原有的合同审批流程,而是直接在批量续签单据的审核操作中进行审批。

为了实现对个别合同的灵活控制,我们在批量续签单据的体上增加了复选框功能。这样,在审批过程中,审批人员可以根据实际情况选择需要独立审批的合同,并对它们进行单独处理。这种设计既提高了审批的灵活性,也确保了每个合同都能得到适当的处理。


上传图片


另外,合理运用本文中提供的方案,我们也可以做到批量新增的功能,来简化业务流程。


方案的可推广价值


本方案通过一体化设计,实现了批量复制、批量编辑、批量审批的全程追踪和管理,无需增加额外的复杂环节,对原有业务无影响。它采用非入侵式的改造方式,有效提升了批量业务处理的效率和便捷性。

未来,我们还可以根据需要,对批量业务中的字段锁定性、可批量编辑字段范围等进行动态控制,以适应更多业务场景。

整体而言,本方案性能压力较小,适用范围广泛,除了为企业的合同管理提供了强有力的支持,也适用其他业务场景。


注意:方案入侵底层的内容较多,尚不清楚对于平台迭代升级的影响有多大,请评估后使用。


相关资料

父子页面交互、前后台交互知识点总结




#往期推荐

实践案例 | 把控安全,不挑业务系统的授权方式

实践案例 | 巧用苍穹,实现页面插件注册情况检查小工具开发

实践案例 | 如何实现苍穹对接云之家智能审批单

实践案例 | 多业务系统间的高效数据互通方案


更多精彩内容,“码”上了解!↓

上传图片


实践案例|业务批量化工作:实现复制、编辑、审批一体化流程

在快节奏的商业世界中,合同批量续签的效率至关重要。本文将向您展示如何通过创新的批量复制、编辑和审批流程,将繁琐的合同管理工作转变为...
点击下载文档
确认删除?
回到顶部
客服QQ
  • 客服QQ点击这里给我发消息