**该应用包含完整的单据申请、权限控制、审批流、审批时不同角色修改单据的不同的内容。**
### **需求价值:**
**当前针对外包预投入项目流程及相应管理,仅依赖于邮件往来及线下EXCEL表格,随着外包业务量需求增加,邮件及表格的形式不利于工作效率提升,而且容易造成数据未经审批即被修改、数据丢失等风险,不利于组织级管理资产的积淀,为解决上述问题,提高管理效率及保证数据准确性需将该流程进行系统化、流程化。**
### **需求描述**
**1、《外包预投入申请单》,流程由机构PMO外包岗发起,经PMO部门经理→机构财务负责人→机构总经理→集团PMO外包岗审核→机构PMO外包岗(即提交人,在此环节,提交人需要对附件有修改权限,可以再次上传附件,但是不允许做其他人修改)→机构财务负责人→集团PMO外包岗;**
** 2、查询、导出功能,《外包预投入申请单》上涉及所有字段均可查询出,且查询出后可导出。查询及导出权限设置:机构PMO可查询导出其机构下所有数据,集团PMO可查询导出全量数据**
**3、申请单表样**
| **预投入申请编号** | | **提报日期** | |
| ----------------------------------------- | -------------------------------- | --------------------------- | -------------------------------------------- |
| *******预投入项目名称** | | ***预计采购金额** | |
| **外包模式** | **整包/分包** | ***外包类型** | **实施/开发/运维/咨询/混合(开发+实施)** |
| ***需求提交人** | | ***需求部门** | |
| ***采购类别** | **实施外包/开发外包/咨询外包** | ***需求组织** | **例:用友网络科技股份有限公司北京分公司** |
| ***销售合同是否已签订** | **是/否** | **销售项目** | |
| **销售合同编码** | | **销售合同实际签订时间** | |
| **销售合同金额** | | **销售合同预计签订时间** | |
| **客户名称** | | **客户所属行业** | |
| **产品线** | | **领域** | |
| **累计已签合同金额** | **xx元** **占比:xx%** | **累计未签申请金额** | **xx元** **占比:xx%** |
| ***供应商** | | **外包顾问人数** | |
| ***外包顾问预计进场时间** | | ***当前主项目阶段** | |
| ***是否签署《外包项目提前开工确认函》** | **是/否** | ***预计外包签约时间** | |
| ***本年预计累计主项目进度** | **格式:百分比** | ***本年预计累计付款金额** | |
| **附件** | | **备注** | |
### **系统设计**
**采用yonbuilder 低代码平台进行开发,主要步骤:数据建模-页面建模-工作流设计-应用发布。**
**审批矩阵的人员指定采用外挂通过接口实现**
#### **数据建模**
**建模时需要先建枚举值,这样在实体建模时,可以很顺畅的做完整个模型**
* **枚举建模**
![]()
* **实体建模**
**因为需要审批,引用接口我们需要勾选审批和自动编码**
**需求提交人、提交人所在部门、组织,有两种方式获取,一是调用后端函数获取当前人信息,然后在前端函数中进行绑定。另一种实体模型的提交人、所属组织、部门设置成单选引用,参照配置数据过滤进行实现绑定当前人信息,我们在这里采用第一种方案**
![]()
**页面建模**
**申请单详情**
**1、如果字段较多,建议用卡片式进行分组显示,有利于用户输入和显示,如下图,我使用了两个卡片,一个申请信息卡片,一个提交人信息卡片。增加卡片的时候,切到层级 找到卡片组,右键插入卡片,将需要分组的字段拖到相应的分组**
**2、如果文本框前的描述文字过多,需要一行显示,可以通过调整单行文字数量达到效果,另外通过调整字段标题布局让字段标题显示在文本框的上边**
![]()
![]()
**3、数据初始化**
**新增单据的时候,需求提交人、提交人所在部门、所在组织这三个字段需要根据当前人信息进行数据初始化,而且只是在单据新增的时候时行绑定,在编辑和预览状态下不需要进行数据初始化,这个地方我们是通过前端函数调用后端函数来实现的。后端函数负责获取当前人的所在部门和组织。**
**后端函数可以在流程&自动化-脚本-新增脚本里进行增加**
![]()
**后端函数脚本获取当前用户的身份信息**
```
let AbstractAPIHandler = require('AbstractAPIHandler');
class MyAPIHandler extends AbstractAPIHandler {
execute(request){
//通过上下文获取当前的用户信息
var currentUser = JSON.parse(AppContext()).currentUser;
var isAdmin= false
// 管理员列表
var adminList=['wenqping@yonyou.com','sunyh3@yonyou.com','suliw@yonyou.com','lujwa@yonyou.com']
if(adminList.indexOf(currentUser.email)>-1){
isAdmin=true
}
var sysId = "diwork";
var tenantId = currentUser.tenantId;
var userids = [currentUser.id];
//获取当前用户的组织和部门信息
var result = listOrgAndDeptByUserIds(sysId, tenantId, userids);
var resultJSON = JSON.parse(result);
var userInfo=""
if ("1" == resultJSON.status && resultJSON.data != null) {
//根据当前用户信息去查询员工表
var userData = resultJSON.data;
// 组装入参的json对象(不要传string)
userInfo = userData[currentUser.id]
} else {
throw new Error("获取员工信息异常");
}
return {"currentUser":userInfo,isAdmin,"yhtId":currentUser.id};
}
}
exports({"entryPoint":MyAPIHandler});
```
**前端函数绑定当前用户信息,**
**权限控制:如果是单据的提交者,在浏览态顶部的按钮可以进行编辑、提交和撤回,在编辑态底部的按钮可以进行保存。其他人员则没有相关的权限**
```
viewModel.on('customInit', function(data) {
// 外包预投入申请单详情--页面初始化
//需要获取当前人的身份信息,确定默认的查询条件
var promise = new cb.promise();
// 调用后端函数
cb.rest.invokeFunction("AT1601E07C17400008.befunc.getUserInfo", {},
function(err, res) {
if (err != null) {
cb.utils.alert('查询数据异常');
return false;
} else {
debugger;
isAdmin = res.isAdmin;
var userInfo = res.currentUser
//获取页面mode
const mode = viewModel.getParams().mode;
//获取审批状态
const verifystate = viewModel.get("verifystate").getValue();
if (mode == 'add') {
viewModel.get("pre_submit_user").setValue(userInfo.name);
viewModel.get("pre_submit_dept").setValue(userInfo.deptName);
viewModel.get("pre_submit_org").setValue**.**Name);
}
const code = viewModel.get("code").getValue();
//打开的页面默认为新增态
if (mode == 'browse') {
cb.rest.invokeFunction("AT1601E07C17400008.befunc.getIsCreator", {
code: code
},
function(err, res_bill) {
if (res_bill.isCreator) {
viewModel.get("btnEdit").setVisible(true);
viewModel.get("btnUnsubmit").setVisible(true);
viewModel.get("btnSubmit").setVisible(true);
} else {
viewModel.get("btnEdit").setVisible(false);
viewModel.get("btnUnsubmit").setVisible(false);
viewModel.get("btnSubmit").setVisible(false);
}
});
}
if (mode == 'edit') {
cb.rest.invokeFunction("AT1601E07C17400008.befunc.getIsCreator", {
code: code
},
function(err, res_bill) {
if (res_bill.isCreator) {
if(verifystate!==2){
viewModel.get("btnSave").setVisible(true);
viewModel.get("btnSaveAndAdd").setVisible(true);
} else {
viewModel.get("btnSave").setVisible(false);
viewModel.get("btnSaveAndAdd").setVisible(false);
}
} else {
viewModel.get("btnSave").setVisible(false);
viewModel.get("btnSaveAndAdd").setVisible(false);
}
});
}
}
promise.resolve();
}
)
return promise;
});
```
**列表页**
**1、权限控制:管理员可以查看全部数据,但不能编辑和删除,其他人员只能查看自己创建的单据,可以进行编辑、删除。方案实现,通过一个后端函数设定一个管理员的列表,判断当前人员是否在管理员列表中,如果是则返回isAadmin为true,否则为false,前端函数中根据后端函数返回的管理员标志,如果不是则按当前人员的yhtid进行数据过滤,如果是则控制不是自己的单据的操作权限,不允许删除、编辑。**
![]()
**后端函数**
```
let AbstractAPIHandler = require('AbstractAPIHandler');
class MyAPIHandler extends AbstractAPIHandler {
execute(request){
//获取当前用户的身份信息-----------
var currentUser = JSON.parse(AppContext()).currentUser; //通过上下文获取当前的用户信息
var isAdmin= false
// 管理员列表
var adminList=['xx@yonyou.com','xx@yonyou.com']
if(adminList.indexOf(currentUser.email)>-1){
isAdmin=true
}
var sysId = "diwork";
var tenantId = currentUser.tenantId;
var userids = [currentUser.id];
var result = listOrgAndDeptByUserIds(sysId, tenantId, userids); //获取当前用户的组织和部门信息
var resultJSON = JSON.parse(result);
var userInfo=""
if ("1" == resultJSON.status && resultJSON.data != null) {
//根据当前用户信息去查询员工表
var userData = resultJSON.data;
// 组装入参的json对象(不要传string)
userInfo = userData[currentUser.id]
} else {
throw new Error("获取员工信息异常");
}
return {"currentUser":userInfo,isAdmin,"yhtId":currentUser.id};
}
}
exports({"entryPoint":MyAPIHandler});
```
**前端函数**
```
viewModel.on('customInit', function(data) {
// 外包预投入申请单--页面初始化
let gridModel = viewModel.getGridModel();
var isAdmin;
viewModel.on('beforeSearch', function(args) {
var promise = new cb.promise();
// 调用后端函数判断是否管理员
cb.rest.invokeFunction("AT1601E07C17400008.befunc.getUserInfo", {},
function(err, res) {
if (err != null) {
cb.utils.alert('查询数据异常');
return false;
} else {
isAdmin = res.isAdmin;
var userInfo = res.currentUser
args.isExtend = true;
var conditions = args.params.condition;
// 不是管理员过滤只属于当前人的列表数据
if (!isAdmin) {
conditions.simpleVOs = [{
"logicOp": "and",
"conditions": [{
"logicOp": "or",
"conditions": [{
"field": "creator",
"op": "eq",
"value1": res.yhtId || ""
}]
}]
}];
}
// 如果是管理员,不显示批量删除按钮
if (isAdmin) {
viewModel.get("btnBatchDelete").setVisible(false);
}
// 如果是管理员,控制行上编辑、删除按钮不显示
gridModel.on('afterSetDataSource', () => {
//获取列表所有数据
const rows = gridModel.getRows();
//从缓存区获取按钮
const actions = gridModel.getCache('actions');
if (!actions) return;
var actionsStates = [];
debugger
rows.forEach(data => {
var actionState = {};
actions.forEach(action => {
if (data.creator == res.yhtId) {
//设置按钮可用不可用
actionState[action.cItemName] = {
visible: true
};
if ((action.cItemName == 'btnDelete' || action.cItemName == 'btnEdit') && data.verifystate == 2) {
actionState[action.cItemName] = {
visible: false
};
}
if (action.cItemName == 'btnDelete' && data.verifystate == 1) {
actionState[action.cItemName] = {
visible: false
};
}
} else {
actionState[action.cItemName] = {
visible: false
};
}
});
actionsStates.push(actionState);
});
gridModel.setActionsState(actionsStates);
});
}
promise.resolve();
}
)
return promise;
});
});
```
**2、导出 **
**列表中的字段默认都是导出的,切换到层级,找到表格下相应的字段,通过调整右侧的允许导出,可以控制哪些字段可以导出,导出时表格里的字段的顺序可以通过调整层级下字段的顺序进行控制(可以通过拖动字段进行调整),上面的在前,下面的在后。**
![]()
**3、审批期间 发起人可以修改外包顾问实际进场时间**
**通过流程字段权限修改相应角色审批时可修改的字段,**
![]()
### **审批流**
**审批流采用外挂审批矩阵的设置和流程变量方式实现。由于原BIP里的基础数据各分子公司对应的机构PMO、机构财务经理、机构总经理角色人员没有相应的数据,需求要求每个机构可以设置自己机构的这几个角色对应的人员,且现有的人员有兼职的情况,比如财务经理,有省区和机构兼职的情况,现有BIP的员工数据不支持以上几种情况,鉴于此,我们使用了与友费控相同的人员审批矩阵的设置,通过api接口设置相应的流程变量,来实现整个审批的流程。**
![]()
#### **审批矩阵**
**根据用户权限,每个机构可以设置自己机构的PMO、财务经理、总经理的人员。职位编码对应审批流里的流程变量的编码。以方便接口获取。**
![]()
#### **定义流程变量**
**定义四个流程变量对应机构相应的四个角色**
![]()
#### **脚本活动获取审批人 **
![]()
**通过API接口获取审批矩阵里的审批人信息,写在第一步里脚本活动里的脚本里**
```
let AbstractTrigger = require('AbstractTrigger');
class MyTrigger extends AbstractTrigger {
execute(context,param){
// 获取当前人所属组织
var res = AppContext();
var curretUser = JSON.parse(res).currentUser;
var orgId**.**Id;
// 审批矩阵api接口地址,参数传入当前人的组织ID
let url = "https://yonbip-core1.diwork.com/iuap-api-gateway/qyic8c7o/ITAPI/WorkFlow/getApproverByCorp?corpId=" + orgId + "&matrixLevel=M2";
let apiResponse = openLinker("GET", url, "AT1601E07C17400008", "");
let jsonResponse = JSON.parse(apiResponse); //将返回的内容转成JSON
let apidata = jsonResponse.data.message; //获取ITAPI返回的内容
let jsonData = JSON.parse(apidata); //将返回的内容转成JSON
let returnMessage = jsonData.data; //获取返回值
let returnData = jsonData.data; //获取返回值
//
/**
* api返回值格式,流程变量接收的格式,variables里的属性为流程变量的编码,值为人员友户通人员的ID
{ "bindType":"multiVar",
"variables":{
"N4":"1f7fb60a-d14a-4c59-8c3a-db99f1ff4d9d",
"N1":"12e2a307-2df3-48da-89bb-cfd57dd9ac91",
"N3":"49d98147-7382-40c2-aef8-52387a9b011e",
"N2":"no-approver"
}}
*/
return returnData;
}
}
exports({"entryPoint":MyTrigger});
```
#### **注意事项**
**退回到制单人后,再次提交时,如果不需要再走一遍流程,直接提交到驳回人,可以按如下设置**
**1、双击开始,退回提交方式修改为提交至退回环节**
![]()
**2、右上角属性设置 退回后重新提交方式 选择第一项**
![]()
### **测试与发布**
**应用做完了,需要测试人员先测试,没有问题后再上线。这时候我们可以使用页面发布创建临时链接分发给测试人员进行测试。登陆方式可以选择友户通的登录方式。**
![]()
![]()
[/md]