借助zentao提供的二次开发,实现在zentao中直接基于story / task / bug 粒度的完成情况,向公司内部ERP进行工作量同步(也就是俗称的"ERP每日报工")。
1. 前言
"上ERP找死,不上ERP等死”,这短短的一句话在揭示启用ERP的必要性同时,也暴露了推进ERP的艰难。
笔者所在的公司属于传统软件企业,秉承资源高效利用和业务特殊性考虑,决策层最终采取了自主进行ERP开发的路线。
同时为了对项目进行精细化管理,精确把控项目进度,公司又专门引入了zentao作为项目全生命周期管理平台。于是出现了:
- 人员需要在zentao中完成story / task / bug 状态扭转,接着在ERP中就所完成的工作进行填报。
- 人员需要在erp中进行项目计划的制定,接着在zentao中进行计划的再次创建。
- …凡此种种重复性的工作。
本文尝试解决第一个重复性问题,实现在zentao中,直接基于story / task / bug 粒度的完成情况,向公司内部ERP进行工作量同步(也就是俗称的"ERP每日报工")。
2. 优点
做任何一件事都必须得有理由,本文要介绍的工作也不例外。通过“zentao + ERP报工同步”,我们可以:
- 降低人员心智负担,通过减少重复性工作,减少低级错误出现频率,提高事情的完成度。
- 推进ERP报工日志格式的统一,为之后的各类报表打下坚实基础(周报 / 半月报 / 月报等)。
- 减少需求管理规范化的推进阻力。公司引入zentao是为了进行项目需求的有序性管理,只是就个人而言,原本一句话就能解决的问题现在必须在zentao上大费周章,因此"zentao规范化使用"的推进过程中势必困难重重,而通过这样的同步打通,将能够有效降低推进阻力。
3. 效果
先让我们看看最终的效果(笔者的主职语言为Java后端,php是初次接触,界面美观啥的还请读者多多包涵)。
- 在 story / task / bug 操作页面, 提供“ERP报工”按钮。
- 点击以上按钮,弹出"ERP报工"填报页面。
- 点击"上传"按钮,等待"ERP报工"结果反馈。
4. 实现
关键文件说明。
4.1 文件module\common\model.php
/**
* `module\common\model.php`文件
*/
public static function printDailyTaskIcon($storyOrBugOrTask, $session)
{
global $lang;
echo html::commonButton(' ' . "对当前需求进行ERP报工", 'style="font-weight:bold" ', 'btn btn-link pull-right btn-dailyWork');
// 模拟登录, 并从ERP中获取当前员工所参与的项目情况
$cookie = common::loginErp("username","password");
$taskList = common::http("http://xxxx/xxxx_queryDailyTask.action",array("rwzt" => 0, "sdate" => "", "edate" =>"", "xmmc" => "", "rwmc"=>""),array(CURLOPT_COOKIE => $cookie));
$taskListForCurrentUser = json_decode($taskList)->data->tasks;
// 将从erp查询到的员工参与项目情况存入session, 留作后用
$session->set('taskListForCurrentUser', $taskListForCurrentUser);
foreach ($taskListForCurrentUser as $key => $value) {
$arr2[sprintf("%s-%s", $value->xmId,$value->rwId)] = sprintf("%s > %s",$value->xmmc, $value->rwmc);
}
// 上面对key进行了特殊处理, 所以这里就不排序
//rsort($arr2);
// html
$projectHtmlSelect = html::select('taskSelected', $arr2, "", 'class="form-control chosen"');
// 报工内容
$dailyWorkContent = sprintf("从事禅道开发-%s %s", $storyOrBugOrTask->id, $storyOrBugOrTask->title);
list($module,$method,$objectType,$objectId) = explode('-', $commentFormLink);
$commentFormLink = sprintf("/zentao/action-dailyTask-%s-%s.html",$objectType,explode('.',$objectId)[0]);
/* */
echo <<<EOD
EOD;
}
/**
* 推送每日报工 (`module\common\model.php`文件)
*/
public static function writeDailyTask($data = array())
{
$cookie = common::loginErp("username","password");
return common::http("http://xxxx/writeDiaryTask.action", $data, array(CURLOPT_COOKIE => $cookie));
}
//登录接口,获取cookie
static function loginErp($erpUsername, $erpPassowrd)
{
$re = common::http(sprintf("http://xxxxx/login.action?name=%s&pswd=%s",$erpUsername,$erpPassowrd), null, array(CURLOPT_HEADER=>true));
// 解析HTTP数据流
list($header, $body) = explode("\r\n\r\n", $re);
// 解析COOKIE
preg_match_all("/set\-cookie:([^\r\n]*)/i", $header, $matches);
return $matches[1][1];
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
4.2 文件module\action\control.php
针对url请求 /zentao/action-dailyTask.html
。
// module\action\control.php 文件
public function dailyTask($objectType, $objectID)
{
// 工时
$zctrgs = $this->post->zctrgs;
if(!filter_var($zctrgs, FILTER_VALIDATE_INT)){
die(js::alert("[工时]必须为数值"));
}
if(((int)$zctrgs) > 8){
die(js::alert("[工时]必须小于等于8"));
}
$taskSelected = $this->post->taskSelected;
list($xmId,$rwId) = explode('-', $taskSelected);
$item = null;
foreach ($this->session->taskListForCurrentUser as $key => $value) {
if($value->xmId == $xmId and $value->rwId == $rwId){
$item = $value;
break;
}
}
if(!$item){
die(js::alert("没有找到可报工的项目!"));
return;
}
$result = common::writeDailyTask(array("xmbh" => $xmId, "rwId" => $rwId, "rwmc" =>$item->rwmc, "txrq" => date("Y-m-d")));
// 同时向zentao的当前story写入一条comment
if($result->status == 0){
$this->action->create($objectType, $objectID, 'Commented', sprintf("ERP-%s-%s-%s-%s-%s-%s", $xmId, $rwId, $item->xmmc, $item->rwmc,$this->post->zctrgs,$this->post->wcbfb));
}
if(defined('RUN_MODE') && RUN_MODE == 'api')
{
die(array('status' => 'success', 'data' => $actionID));
}
else
{
// 为了最大化真实地反馈报工情况,我们直接将ERP的返回值进行弹框展示, 不进行二次封装.
echo js::alert(json_encode($result));
die(js::reload('parent'));
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
4.3 更新权限 zt_grouppriv
。
这一步操作的必要性是实际的试点过程中发现的,我们需要为当前操作用户授予填写报工的权限。
鉴权初期管理还没有细化,这里我们直接给出为所有用户组赋予填写报工的权限。
-- 创建存储过程之前需判断该存储过程是否已存在,若存在则删除
DROP PROCEDURE IF EXISTS shxc40;
-- 创建存储过程
CREATE PROCEDURE shxc40()
BEGIN
-- 定义变量
DECLARE s int DEFAULT 0;
DECLARE gro int;
DECLARE p varchar(255);
-- 定义游标,并将sql结果集赋值到游标中
DECLARE groups CURSOR FOR select distinct `group` from zt_grouppriv;
-- 声明当游标遍历完后将标志变量置成某个值
DECLARE CONTINUE HANDLER FOR NOT FOUND SET s=1;
-- 打开游标
open groups;
-- 将游标中的值赋值给变量,注意:变量名不要和返回的列名同名,变量顺序要和sql结果列的顺序一致
fetch groups into gro;
-- 当s不等于1,也就是未遍历完时,会一直循环
while s<>1 do
-- 执行业务逻辑
INSERT INTO zt_grouppriv (`group`, module, method) VALUES(gro, 'action', 'dailyTask');
-- 当s等于1时表明遍历以完成,退出循环
fetch groups into gro;
end while;
-- 关闭游标
close groups;
END;
-- 执行存储过程
call shxc40()
-- // Other
-- select distinct `group` from zt_grouppriv
-- select * from zt_grouppriv where method='dailyTask'
-- delete from zt_grouppriv where method='dailyTask'
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
4.4 备注说明
module\story\control.php
文件。
a. 其中的view($storyID, $version = 0, $from = 'product', $param = 0)
方法负责请求story-view-xxx.html
的响应 。module\story\view\view.html.php
文件。
a. 请求story-view-xxx.html
的响应页面,这属于zentao中默认的二次开发约定。
b. 其中的属于以模块化引入的"历史修改记录"模块。
module\common\view\action.html.php
文件。
a. 上一步以模块化引入的"历史修改记录"模块文件。
b. 其中的正是zentao中默认的"添加备注"按钮的实现,也是本次我们的借鉴对象。
c. 最终我们的实现为:app->user->account, array('admin','xxxx')) and isset($actionFormLink)) echo common::printDailyTaskIcon($actionFormLink, $story, $this->session);?>
。module\common\model.php
文件。
a. 上一步中common::printCommentIcon($actionFormLink);
的实现位置。www\js\zui\min.js
文件。
a. 让我们扩展的按钮点击生效。
b. 实现代码:;n.on("click",".btn-comment",function(t){r.modal("toggle"),t.preventDefault()}).on("click",".btn-dailyWork",function(t){var r = n.find(".modal-dailyWork");
www\theme\zui\css\min.css
文件。
a. 按钮前面icon的来源,例如这里的icon-cards-view
。module\action\control.php
文件。
a. 添加public function dailyTask()
方法。\lib\base\front\front.class.php
文件。
a.html::commonButton
的来源。- 权限配置。
a. 按钮执行是需要权限的。相关表为zt_grouppriv
。
b. 执行SQL:INSERT INTO zt_grouppriv (
group, module, method) VALUES('2', 'action', 'dailyTask')
。其中 2 为 group Id,需要自行查找用户所在组对应的id。
5. 优化方向
目前想到的,还未来得及做的:
- 已有报工展示。针对当天,人员尚未报工的时长进行展示,方便人员知晓当前报工情况,避免人员需要再次登录ERP查看。
- 更新。当出现填报错误时候,能够用新的报工替换掉之前的报工。
- 界面输入参数校验。例如需求完成用时只能为数值。