diff --git a/api/v1/entries/testresults.php b/api/v1/entries/testresults.php
index a915d33511..cc570e834b 100644
--- a/api/v1/entries/testresults.php
+++ b/api/v1/entries/testresults.php
@@ -84,7 +84,7 @@ class testresultsEntry extends entry
$reals[$stepID] = $step->real;
}
$this->setPost('result', $results);
- $this->setPost('reals', $reals);
+ $this->setPost('real', $reals);
}
$control->runCase($runID, $caseID, $version);
diff --git a/config/zentaopms.php b/config/zentaopms.php
index 7f6d012c62..97041891c9 100644
--- a/config/zentaopms.php
+++ b/config/zentaopms.php
@@ -429,6 +429,7 @@ define('TABLE_ACL', '`' . $config->db->prefix . 'acl`');
define('TABLE_DESIGN', '`' . $config->db->prefix . 'design`');
define('TABLE_DESIGNSPEC', '`' . $config->db->prefix . 'designspec`');
+define('TABLE_DELIVERABLE', '`' . $config->db->prefix . 'deliverable`');
define('TABLE_DOCLIB', '`' . $config->db->prefix . 'doclib`');
define('TABLE_DOC', '`' . $config->db->prefix . 'doc`');
define('TABLE_DOCBLOCK', '`' . $config->db->prefix . 'docblock`');
@@ -672,6 +673,7 @@ $config->objectTables['demand'] = TABLE_DEMAND;
$config->objectTables['demandpool'] = TABLE_DEMANDPOOL;
$config->objectTables['demandspec'] = TABLE_DEMANDSPEC;
$config->objectTables['demandreview'] = TABLE_DEMANDREVIEW;
+$config->objectTables['deliverable'] = TABLE_DELIVERABLE;
$config->objectTables['todo'] = TABLE_TODO;
$config->objectTables['custom'] = TABLE_LANG;
$config->objectTables['branch'] = TABLE_BRANCH;
diff --git a/db/update21.6.1.sql b/db/update21.6.1.sql
index ea92b0ec26..169db5f37a 100644
--- a/db/update21.6.1.sql
+++ b/db/update21.6.1.sql
@@ -4,4 +4,4 @@ ALTER TABLE `zt_storyspec` ADD COLUMN `docs` text NULL AFTER `files`;
ALTER TABLE `zt_storyspec` ADD COLUMN `docVersions` text NULL AFTER `docs`;
ALTER TABLE `zt_design` ADD COLUMN `docs` text NULL AFTER `storyVersion`;
ALTER TABLE `zt_design` ADD COLUMN `docVersions` text NULL AFTER `docs`;
-ALTER TABLE `zt_project` ADD `taskDateLimit` varchar(30) NOT NULL DEFAULT 'auto' AFTER `linkType`;
+ALTER TABLE `zt_project` ADD `taskDateLimit` varchar(30) NOT NULL DEFAULT 'auto' AFTER `linkType`;
\ No newline at end of file
diff --git a/db/update21.6.sql b/db/update21.6.sql
deleted file mode 100644
index ce5506e56a..0000000000
--- a/db/update21.6.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-ALTER TABLE `zt_relationoftasks` ADD COLUMN `project` mediumint(8) unsigned NOT NULL DEFAULT 0 AFTER `id`;
-ALTER TABLE `zt_relationoftasks` MODIFY `execution` char(30) NOT NULL DEFAULT '' AFTER `project`;
-UPDATE `zt_relationoftasks` SET `project` = (SELECT `project` FROM `zt_task` WHERE `id` = `zt_relationoftasks`.`task`);
diff --git a/db/update21.7.sql b/db/update21.7.sql
index 8daa59c65f..6805002597 100644
--- a/db/update21.7.sql
+++ b/db/update21.7.sql
@@ -13,4 +13,27 @@ UPDATE `zt_workflowlayout` SET `module` = 'build', `action` = 'browse' WHERE `mo
UPDATE `zt_workflowaction` SET `module` = 'task', `action` = 'browse' WHERE `module` = 'execution' AND `action` = 'task';
UPDATE `zt_workflowlayout` SET `module` = 'task', `action` = 'browse' WHERE `module` = 'execution' AND `action` = 'task';
-ALTER TABLE `zt_workflowgroup` ADD `objectID` mediumint(8) unsigned NOT NULL DEFAULT '0' AFTER `id`;
\ No newline at end of file
+ALTER TABLE `zt_relationoftasks` ADD COLUMN `project` mediumint(8) unsigned NOT NULL DEFAULT 0 AFTER `id`;
+ALTER TABLE `zt_relationoftasks` MODIFY `execution` char(30) NOT NULL DEFAULT '' AFTER `project`;
+UPDATE `zt_relationoftasks` SET `project` = (SELECT `project` FROM `zt_task` WHERE `id` = `zt_relationoftasks`.`task`);
+
+CREATE TABLE IF NOT EXISTS `zt_deliverable` (
+ `id` int(8) NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) NOT NULL,
+ `module` varchar(30) NULL,
+ `method` varchar(30) NULL,
+ `model` text NULL,
+ `type` enum('doc','file') NULL DEFAULT 'file',
+ `desc` text NULL,
+ `files` varchar(255) NULL,
+ `createdBy` varchar(30) NULL,
+ `createdDate` date NULL,
+ `lastEditedBy` varchar(30) NULL,
+ `lastEditedDate` date NULL,
+ `deleted` enum('0','1') NOT NULL DEFAULT '0',
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+ALTER TABLE `zt_workflowgroup` ADD `objectID` mediumint(8) unsigned NOT NULL DEFAULT '0' AFTER `id`;
+ALTER TABLE `zt_workflowgroup` ADD COLUMN `deliverable` text NULL AFTER `editedDate`;
+ALTER TABLE `zt_project` ADD COLUMN `deliverable` text NULL AFTER `maxColWidth`;
\ No newline at end of file
diff --git a/db/zentao.sql b/db/zentao.sql
index 466c885102..a52b03ba99 100755
--- a/db/zentao.sql
+++ b/db/zentao.sql
@@ -578,6 +578,24 @@ CREATE TABLE IF NOT EXISTS `zt_screen` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+-- DROP TABLE IF EXISTS `zt_deliverable`;
+CREATE TABLE IF NOT EXISTS `zt_deliverable` (
+ `id` int(8) NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) NOT NULL,
+ `module` varchar(30) NULL,
+ `method` varchar(30) NULL,
+ `model` text NULL,
+ `type` enum('doc','file') NULL DEFAULT 'file',
+ `desc` text NULL,
+ `files` varchar(255) NULL,
+ `createdBy` varchar(30) NULL,
+ `createdDate` date NULL,
+ `lastEditedBy` varchar(30) NULL,
+ `lastEditedDate` date NULL,
+ `deleted` enum('0','1') NOT NULL DEFAULT '0',
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
-- DROP TABLE IF EXISTS `zt_dimension`;
CREATE TABLE IF NOT EXISTS `zt_dimension` (
`id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
@@ -1596,6 +1614,7 @@ CREATE TABLE IF NOT EXISTS `zt_project` (
`colWidth` smallint(6) NOT NULL DEFAULT '264',
`minColWidth` smallint(6) NOT NULL DEFAULT '200',
`maxColWidth` smallint(6) NOT NULL DEFAULT '384',
+ `deliverable` text NULL,
`deleted` enum('0','1') NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -13250,6 +13269,7 @@ CREATE TABLE IF NOT EXISTS `zt_workflowgroup` (
`createdDate` datetime NULL,
`editedBy` varchar(30) NOT NULL DEFAULT '',
`editedDate` datetime NULL,
+ `deliverable` text NULL,
`deleted` enum('0', '1') NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/lib/zin/func.php b/lib/zin/func.php
index 469d0979a9..e651f5237a 100644
--- a/lib/zin/func.php
+++ b/lib/zin/func.php
@@ -249,6 +249,7 @@ function iconPicker(): iconPicker {return createWg('iconPicker', func_get_args()
function relatedObjectList(): relatedObjectList {return createWg('relatedObjectList', func_get_args());}
function taskAssignedTo(): taskAssignedTo {return createWg('taskAssignedTo', func_get_args());}
function docList(): docList {return createWg('docList', func_get_args());}
+function deliverable(): deliverable {return createWg('deliverable', func_get_args());}
if(is_dir(__DIR__ . DS . 'wg' . DS . 'schedule'))
{
@@ -258,4 +259,4 @@ if(is_dir(__DIR__ . DS . 'wg' . DS . 'schedule'))
if(is_dir(__DIR__ . DS . 'wg' . DS . 'boardeditor'))
{
function boardEditor(): boardEditor {return createWg('boardeditor', func_get_args());}
-}
+}
\ No newline at end of file
diff --git a/lib/zin/wg/backbtn/v1.php b/lib/zin/wg/backbtn/v1.php
index 5b20fca7ac..0f67b0e291 100644
--- a/lib/zin/wg/backbtn/v1.php
+++ b/lib/zin/wg/backbtn/v1.php
@@ -96,7 +96,9 @@ class backBtn extends btn
'approvalflow' => 'approvalflow-browse',
'host' => 'host-browse,my-index',
'deploy' => 'deploy-browse',
- 'program' => 'program-browse,program-productview'
+ 'program' => 'program-browse,program-productview',
+ 'workflowgroup' => 'workflowgroup-project,workflowgroup-deliverable',
+ 'deliverable' => 'deliverable-browse'
);
$props = parent::getProps();
diff --git a/lib/zin/wg/deliverable/js/v1.js b/lib/zin/wg/deliverable/js/v1.js
new file mode 100644
index 0000000000..80fa48a10f
--- /dev/null
+++ b/lib/zin/wg/deliverable/js/v1.js
@@ -0,0 +1,44 @@
+/**
+ * 获取附件的操作按钮。
+ * Get file actions.
+ */
+window.getDeliverableFileActions = function(file, deliverable)
+{
+ let actions = [];
+ if(canDownload && typeof file.id === 'number') actions[0] = {icon: 'download', key: 'download', url: $.createLink('file', 'download', 'id=' + file.id), target: '_blank'};
+ actions[1] = {icon: 'edit', key: 'rename'};
+ actions[2] = {icon: 'trash', key: 'delete'};
+
+ /* 可以预览的文件。 */
+ if(['txt', 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'mp4'].includes(file.extension) && canDownload)
+ {
+ actions[3] = {icon: 'eye', key: 'view', url: $.createLink('file', 'download', `fileID=${file.id}&mouse=left`), 'data-toggle' : 'modal', 'data-size' : 'lg'};
+ }
+
+ return actions;
+}
+
+/**
+ * 获取文档的操作按钮。
+ * Get file actions.
+ */
+window.getDocActions = function(doc, deliverable)
+{
+ let actions = [];
+ actions[0] = {icon: 'eye', key: 'view', url: $.createLink('doc', 'view', 'docID=' + doc.id)};
+ actions[1] = {icon: 'edit', key: 'rename'};
+ actions[2] = {icon: 'trash', key: 'delete'};
+ return actions;
+}
+
+/**
+ * 获取交付物的操作按钮。
+ * Get deliverable actions.
+ */
+window.getDeliverableActions = function(deliverable)
+{
+ let actions = [];
+ actions[0] = {text: addFile, icon: 'file', key: 'selectFile'};
+ if(deliverable.template > 0 && canDownload) actions[1] = {text: downloadTemplate, icon: 'download', key: 'downloadTemplate', url: $.createLink('file', 'download', 'templateID=' + deliverable.template), target: '_blank'};
+ return actions;
+}
diff --git a/lib/zin/wg/deliverable/v1.php b/lib/zin/wg/deliverable/v1.php
new file mode 100644
index 0000000000..dc3c7c86f9
--- /dev/null
+++ b/lib/zin/wg/deliverable/v1.php
@@ -0,0 +1,78 @@
+loadLang('doc');
+ $app->loadLang('file');
+ jsVar('addFile', $lang->doc->addFile);
+ jsVar('downloadTemplate', $lang->doc->downloadTemplate);
+ jsVar('deleteItem', $lang->delete);
+ jsVar('canDownload', hasPriv('file', 'download'));
+
+ return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js');
+ }
+
+ /**
+ * 构建组件。
+ * Build.
+ *
+ * @access protected
+ * @return zui
+ */
+ protected function build(): zui
+ {
+ global $lang, $app;
+ $app->loadLang('doc');
+ $app->loadLang('file');
+
+ $formName = $this->prop('formName') ? $this->prop('formName') : 'deliverable';
+
+ if(!$this->hasProp('maxFileSize'))
+ {
+ $maxFileSize = ini_get('upload_max_filesize');
+ $lastChar = substr($maxFileSize, -1);
+ $fileSizeUnit = array('K', 'M', 'G', 'T');
+ if(in_array($lastChar, $fileSizeUnit)) $maxFileSize .= 'B';
+ $this->setProp('maxFileSize', $maxFileSize);
+ }
+
+ return zui::deliverableList
+ (
+ set::formName($formName),
+ set::items($this->prop('items')),
+ set::docPicker(array('placeholder' => $lang->doc->selectDoc, 'items' => helper::createLink('doc', 'ajaxGetMineDocs', 'keyword={search}'))),
+ set::getFileActions(jsRaw('window.getDeliverableFileActions')),
+ set::getDocActions(jsRaw('window.getDocActions')),
+ set::getEmptyActions(jsRaw('window.getDeliverableActions')),
+ set::maxFileSize($this->prop('maxFileSize')),
+ set::extraCategory($lang->other)
+ );
+ }
+}
diff --git a/lib/zin/wg/dropmenu/css/v1.css b/lib/zin/wg/dropmenu/css/v1.css
index a1308d5d1a..9904ddbb1d 100644
--- a/lib/zin/wg/dropmenu/css/v1.css
+++ b/lib/zin/wg/dropmenu/css/v1.css
@@ -1,4 +1,4 @@
-#pick-pop-admin-menu {width: 139px!important;}
+#pick-pop-admin-menu {width: 145px!important;}
#pick-pop-admin-menu .dropmenu-list {padding: 8px;}
#pick-pop-admin-menu .dropmenu-item + .dropmenu-item {margin-top: 4px}
#pick-pop-admin-menu .dropmenu-item.active {background-color: unset;}
diff --git a/module/action/config.php b/module/action/config.php
index 9899981166..1c26be3a42 100755
--- a/module/action/config.php
+++ b/module/action/config.php
@@ -71,6 +71,7 @@ $config->action->objectNameFields['prompt'] = 'name';
$config->action->objectNameFields['miniprogram'] = 'name';
$config->action->objectNameFields['holiday'] = 'name';
$config->action->objectNameFields['system'] = 'name';
+$config->action->objectNameFields['deliverable'] = 'name';
$config->action->commonImgSize = 870;
diff --git a/module/action/lang/de.php b/module/action/lang/de.php
index 17418cb670..40f0be5363 100644
--- a/module/action/lang/de.php
+++ b/module/action/lang/de.php
@@ -191,6 +191,7 @@ $lang->action->objectTypes['board'] = 'Board';
$lang->action->objectTypes['boardspace'] = 'Board Space';
$lang->action->objectTypes['productline'] = 'Product Line';
$lang->action->objectTypes['system'] = $lang->product->system;
+$lang->action->objectTypes['deliverable'] = 'Deliverable';
/* Used to describe operation history. */
$lang->action->desc = new stdclass();
@@ -937,6 +938,7 @@ $lang->action->label->instance = 'Application|instance|view|id=%s';
$lang->action->label->prompt = 'Prompt|ai|promptview|id=%s';
$lang->action->label->miniprogram = 'Mini Program|aiapp|browseminiprogram|id=%s';
$lang->action->label->holiday = 'Holiday|holiday|browse|';
+$lang->action->label->deliverable = 'Deliverable|deliverable|view|id=%s';
/* Object type. */
$lang->action->search = new stdclass();
diff --git a/module/action/lang/en.php b/module/action/lang/en.php
index bc5d4af4ea..5860a6b5fd 100755
--- a/module/action/lang/en.php
+++ b/module/action/lang/en.php
@@ -191,6 +191,7 @@ $lang->action->objectTypes['board'] = 'Board';
$lang->action->objectTypes['boardspace'] = 'Board Space';
$lang->action->objectTypes['productline'] = 'Product Line';
$lang->action->objectTypes['system'] = $lang->product->system;
+$lang->action->objectTypes['deliverable'] = 'Deliverable';
/* Used to describe operation history. */
$lang->action->desc = new stdclass();
@@ -937,6 +938,7 @@ $lang->action->label->instance = 'Application|instance|view|id=%s';
$lang->action->label->prompt = 'Prompt|ai|promptview|id=%s';
$lang->action->label->miniprogram = 'Mini Program|aiapp|browseminiprogram|id=%s';
$lang->action->label->holiday = 'Holiday|holiday|browse|';
+$lang->action->label->deliverable = 'Deliverable|deliverable|view|id=%s';
/* Object type. */
$lang->action->search = new stdclass();
diff --git a/module/action/lang/fr.php b/module/action/lang/fr.php
index 5fce10c0f7..61ee3a5353 100644
--- a/module/action/lang/fr.php
+++ b/module/action/lang/fr.php
@@ -191,6 +191,7 @@ $lang->action->objectTypes['board'] = 'Board';
$lang->action->objectTypes['boardspace'] = 'Board Space';
$lang->action->objectTypes['productline'] = 'Product Line';
$lang->action->objectTypes['system'] = $lang->product->system;
+$lang->action->objectTypes['deliverable'] = 'Deliverable';
/* Used to describe operation history. */
$lang->action->desc = new stdclass();
@@ -937,6 +938,7 @@ $lang->action->label->instance = 'Application|instance|view|id=%s';
$lang->action->label->prompt = 'Prompt|ai|promptview|id=%s';
$lang->action->label->miniprogram = 'Mini Program|aiapp|browseminiprogram|id=%s';
$lang->action->label->holiday = 'Holiday|holiday|browse|';
+$lang->action->label->deliverable = 'Deliverable|deliverable|view|id=%s';
/* Object type. */
$lang->action->search = new stdclass();
diff --git a/module/action/lang/zh-cn.php b/module/action/lang/zh-cn.php
index 6d33955720..2a0de805e2 100755
--- a/module/action/lang/zh-cn.php
+++ b/module/action/lang/zh-cn.php
@@ -191,6 +191,7 @@ $lang->action->objectTypes['board'] = '白板';
$lang->action->objectTypes['boardspace'] = '白板空间';
$lang->action->objectTypes['productline'] = '产品线';
$lang->action->objectTypes['system'] = $lang->product->system;
+$lang->action->objectTypes['deliverable'] = '交付物';
/* 用来描述操作历史记录。*/
$lang->action->desc = new stdclass();
@@ -937,6 +938,7 @@ $lang->action->label->instance = '服务|instance|view|id=%s';
$lang->action->label->prompt = '提词|ai|promptview|id=%s';
$lang->action->label->miniprogram = '小程序|aiapp|browseminiprogram|id=%s';
$lang->action->label->holiday = '节假日|holiday|browse|';
+$lang->action->label->deliverable = '交付物|deliverable|view|id=%s';
/* Object type. */
$lang->action->search = new stdclass();
diff --git a/module/action/model.php b/module/action/model.php
index 4c0b54675b..701f70d153 100755
--- a/module/action/model.php
+++ b/module/action/model.php
@@ -940,6 +940,12 @@ class actionModel extends model
foreach($actions as $action)
{
$item = new stdClass();
+ if(strlen(trim(($action->comment))) !== 0)
+ {
+ $item->comment = $this->formatActionComment($action->comment);
+ $currentUserCanEdit = $action->id == $endAction->id && $action->actor == $account && $action->action == 'commented';
+ $item->commentEditable = $commentEditable && common::hasPriv('action', 'editComment') && $currentUserCanEdit;
+ }
if($action->action === 'assigned' || $action->action === 'toaudit')
{
@@ -951,7 +957,7 @@ class actionModel extends model
$action->actor = zget($users, $action->actor);
if(str_contains($action->actor, ':')) $action->actor = substr($action->actor, strpos($action->actor, ':') + 1);
- if(!empty($action->history)) $item->historyChanges = $this->renderChanges($action->objectType, $action->history);
+ if(!empty($action->history)) $item->historyChanges = $this->renderChanges($action->objectType, $action->objectID, $action->history);
$item->id = $action->id;
$item->action = $action->action;
@@ -1452,12 +1458,13 @@ class actionModel extends model
* Render histories of every action.
*
* @param string $objectType
+ * @param int $objectID
* @param array $histories
* @param bool $canChangeTag
* @access public
* @return string
*/
- public function renderChanges(string $objectType, array $histories, bool $canChangeTag = true): string
+ public function renderChanges(string $objectType, int $objectID, array $histories, bool $canChangeTag = true): string
{
if(empty($histories)) return '';
@@ -1507,7 +1514,14 @@ class actionModel extends model
}
else
{
- $content .= sprintf($this->lang->action->desc->diff1, $history->fieldLabel, $history->old, $history->new);
+ if($history->field == 'deliverable' && ($objectType == 'project' || $objectType == 'execution'))
+ {
+ $content .= $this->processDeliverableJson($objectType, $objectID, $history);
+ }
+ else
+ {
+ $content .= sprintf($this->lang->action->desc->diff1, $history->fieldLabel, $history->old, $history->new);
+ }
}
}
return $content;
@@ -1518,14 +1532,15 @@ class actionModel extends model
* Print histories of every action.
*
* @param string $objectType
+ * @param int $objectID
* @param array $histories
* @param bool $canChangeTag
* @access public
* @return void
*/
- public function printChanges(string $objectType, array $histories, bool $canChangeTag = true): void
+ public function printChanges(string $objectType, int $objectID, array $histories, bool $canChangeTag = true): void
{
- $content = $this->renderChanges($objectType, $histories, $canChangeTag);
+ $content = $this->renderChanges($objectType, $objectID, $histories, $canChangeTag);
if(is_string($content)) echo $content;
}
diff --git a/module/admin/config.php b/module/admin/config.php
index 48b6d8c167..f205582b85 100755
--- a/module/admin/config.php
+++ b/module/admin/config.php
@@ -8,7 +8,7 @@ if(!isset($config->safe->weak)) $config->safe->weak = '123456,password,12345,123
$config->admin->menuGroup['system'] = array('custom|mode', 'backup', 'cron', 'action|trash', 'admin|xuanxuan', 'setting|xuanxuan', 'admin|license', 'admin|checkweak', 'admin|resetpwdsetting', 'admin|safe', 'cache|setting', 'custom|timezone', 'search|buildindex', 'admin|tableengine', 'ldap', 'custom|libreoffice', 'conference', 'watermark', 'client', 'system|browsebackup', 'system|restorebackup');
$config->admin->menuGroup['company'] = array('dept', 'company', 'user', 'group', 'tutorial');
$config->admin->menuGroup['switch'] = array('admin|setmodule');
-$config->admin->menuGroup['model'] = array('auditcl', 'stage', 'design', 'cmcl', 'reviewcl', 'custom|required', 'custom|set', 'custom|flow', 'custom|code', 'custom|percent','custom|estimate', 'custom|hours', 'subject', 'process', 'activity', 'zoutput', 'classify', 'holiday', 'reviewsetting', 'custom|project');
+$config->admin->menuGroup['model'] = array('auditcl', 'stage', 'deliverable', 'design', 'cmcl', 'reviewcl', 'custom|required', 'custom|set', 'custom|flow', 'custom|code', 'custom|percent','custom|estimate', 'custom|hours', 'subject', 'process', 'activity', 'zoutput', 'classify', 'holiday', 'reviewsetting', 'custom|project');
$config->admin->menuGroup['feature'] = array('custom|set', 'custom|product', 'custom|execution', 'custom|required', 'custom|kanban', 'measurement', 'meetingroom', 'custom|browsestoryconcept', 'custom|kanban', 'sqlbuilder', 'report', 'custom|limittaskdate', 'measurement');
$config->admin->menuGroup['template'] = array('custom|set', 'baseline');
$config->admin->menuGroup['message'] = array('mail', 'webhook', 'sms', 'message');
diff --git a/module/admin/lang/de.php b/module/admin/lang/de.php
index f46edad4ef..11bc8a98f8 100755
--- a/module/admin/lang/de.php
+++ b/module/admin/lang/de.php
@@ -118,6 +118,7 @@ $lang->admin->setModule->opportunitylib = 'Opportunity Lib';
$lang->admin->setModule->practicelib = 'Practice Lib';
$lang->admin->setModule->componentlib = 'Component Lib';
$lang->admin->setModule->devops = 'DevOps';
+$lang->admin->setModule->deliverable = 'Deliverable';
$lang->admin->setModule->kanban = 'Kanban';
$lang->admin->setModule->OA = 'OA';
$lang->admin->setModule->deploy = 'Deploy';
diff --git a/module/admin/lang/en.php b/module/admin/lang/en.php
index ca0b92b227..aec0983c5b 100755
--- a/module/admin/lang/en.php
+++ b/module/admin/lang/en.php
@@ -118,6 +118,7 @@ $lang->admin->setModule->opportunitylib = 'Opportunity Lib';
$lang->admin->setModule->practicelib = 'Practice Lib';
$lang->admin->setModule->componentlib = 'Component Lib';
$lang->admin->setModule->devops = 'DevOps';
+$lang->admin->setModule->deliverable = 'Deliverable';
$lang->admin->setModule->kanban = 'Kanban';
$lang->admin->setModule->OA = 'OA';
$lang->admin->setModule->deploy = 'Deploy';
diff --git a/module/admin/lang/fr.php b/module/admin/lang/fr.php
index 1d5b91d323..38b0490e3a 100755
--- a/module/admin/lang/fr.php
+++ b/module/admin/lang/fr.php
@@ -118,6 +118,7 @@ $lang->admin->setModule->opportunitylib = 'Opportunity Lib';
$lang->admin->setModule->practicelib = 'Practice Lib';
$lang->admin->setModule->componentlib = 'Component Lib';
$lang->admin->setModule->devops = 'DevOps';
+$lang->admin->setModule->deliverable = 'Deliverable';
$lang->admin->setModule->kanban = 'Kanban';
$lang->admin->setModule->OA = 'OA';
$lang->admin->setModule->deploy = 'Deploy';
diff --git a/module/admin/lang/menu.php b/module/admin/lang/menu.php
index cef8ec25a5..8acf9d1353 100644
--- a/module/admin/lang/menu.php
+++ b/module/admin/lang/menu.php
@@ -74,7 +74,7 @@ $lang->admin->menuList->company['menuOrder']['5'] = 'dept';
$lang->admin->menuList->company['menuOrder']['10'] = 'browseUser';
$lang->admin->menuList->company['menuOrder']['15'] = 'group';
-$lang->admin->menuList->model['subMenu']['common'] = array('link' => "{$lang->globalSetting}|custom|required|module=project", 'subModule' => 'custom,subject,holiday,stage', 'exclude' => 'stage-browse,stage-plusbrowse,stage-create,stage-edit,stage-batchcreate');
+$lang->admin->menuList->model['subMenu']['common'] = array('link' => "{$lang->globalSetting}|custom|required|module=project", 'subModule' => 'custom,subject,holiday,stage,deliverable', 'exclude' => 'stage-browse,stage-plusbrowse,stage-create,stage-edit,stage-batchcreate');
$lang->admin->menuList->model['subMenu']['scrum'] = array('link' => "{$lang->scrumModel}|auditcl|scrumbrowse|", 'subModule' => 'auditcl');
$lang->admin->menuList->model['subMenu']['waterfall'] = array('link' => "{$lang->waterfallModel}|stage|browse|", 'subModule' => 'stage', 'exclude' => 'stage-settype,stage-plusbrowse');
$lang->admin->menuList->model['subMenu']['agileplus'] = array('link' => "{$lang->agilePlusModel}|auditcl|agileplusbrowse|", 'subModule' => 'auditcl');
@@ -86,13 +86,14 @@ $lang->admin->menuList->model['menuOrder']['15'] = 'waterfall';
$lang->admin->menuList->model['menuOrder']['20'] = 'agileplus';
$lang->admin->menuList->model['menuOrder']['25'] = 'waterfallplus';
-$lang->admin->menuList->model['tabMenu']['common']['project'] = array('link' => "{$lang->project->common}|custom|required|module=project", 'alias' => 'set,project', 'exclude' => 'custom-required', 'links' => array('custom|set|module=project&field=unitList'));
-if(helper::hasFeature('waterfall') or helper::hasFeature('waterfallplus')) $lang->admin->menuList->model['tabMenu']['common']['stage'] = array('link' => "{$lang->stage->type}|stage|settype|", 'subModule' => 'stage');
-$lang->admin->menuList->model['tabMenu']['common']['build'] = array('link' => "{$lang->build->common}|custom|required|module=build", 'alias' => 'set', 'exclude' => 'custom');
-$lang->admin->menuList->model['tabMenu']['common']['flow'] = array('link' => "{$lang->custom->flow}|custom|flow|", 'divider' => true);
+$lang->admin->menuList->model['tabMenu']['common']['project'] = array('link' => "{$lang->project->common}|custom|required|module=project", 'alias' => 'set,project', 'exclude' => 'custom-required', 'links' => array('custom|set|module=project&field=unitList'));
+if(helper::hasFeature('waterfall') || helper::hasFeature('waterfallplus')) $lang->admin->menuList->model['tabMenu']['common']['stage'] = array('link' => "{$lang->stage->type}|stage|settype|", 'subModule' => 'stage');
+if(helper::hasFeature('deliverable') && ($config->edition == 'max' || $config->edition == 'ipd')) $lang->admin->menuList->model['tabMenu']['common']['deliverable'] = array('link' => "{$lang->deliverable->common}|deliverable|browse|", 'subModule' => 'deliverable');
+$lang->admin->menuList->model['tabMenu']['common']['build'] = array('link' => "{$lang->build->common}|custom|required|module=build", 'alias' => 'set', 'exclude' => 'custom');
+$lang->admin->menuList->model['tabMenu']['common']['flow'] = array('link' => "{$lang->custom->flow}|custom|flow|", 'divider' => true);
-$lang->admin->menuList->model['tabMenu']['common']['percent'] = array('link' => "{$lang->stage->percent}|custom|percent|");
-$lang->admin->menuList->model['tabMenu']['common']['hours'] = array('link' => "{$lang->workingHour}|custom|hours|", 'subModule' => 'holiday', 'links' => array('holiday|browse|', 'custom|hours|'));
+$lang->admin->menuList->model['tabMenu']['common']['percent'] = array('link' => "{$lang->stage->percent}|custom|percent|");
+$lang->admin->menuList->model['tabMenu']['common']['hours'] = array('link' => "{$lang->workingHour}|custom|hours|", 'subModule' => 'holiday', 'links' => array('holiday|browse|', 'custom|hours|'));
if(helper::hasFeature('waterfall')) $lang->admin->menuList->model['tabMenu']['waterfall']['stage'] = array('link' => "{$lang->stage->list}|stage|browse|", 'subModule' => 'stage', 'exclude' => 'stage-plusbrowse');
if(helper::hasFeature('waterfallplus')) $lang->admin->menuList->model['tabMenu']['waterfallplus']['stage'] = array('link' => "{$lang->stage->list}|stage|plusbrowse|", 'subModule' => 'stage', 'exclude' => 'stage-browse');
$lang->admin->menuList->model['tabMenu']['menuOrder']['common']['5'] = 'project';
@@ -103,6 +104,10 @@ $lang->admin->menuList->model['tabMenu']['menuOrder']['common']['40'] = 'p
$lang->admin->menuList->model['tabMenu']['menuOrder']['common']['45'] = 'hours';
if(helper::hasFeature('waterfall')) $lang->admin->menuList->model['tabMenu']['menuOrder']['waterfall']['5'] = 'stage';
if(helper::hasFeature('waterfallplus')) $lang->admin->menuList->model['tabMenu']['menuOrder']['waterfallplus']['5'] = 'stage';
+if($config->edition == 'max' or $config->edition == 'ipd')
+{
+ $lang->admin->menuList->model['tabMenu']['menuOrder']['common']['15'] = 'deliverable';
+}
$lang->admin->menuList->feature['subMenu']['my'] = array('link' => "{$lang->my->common}|custom|set|module=todo&field=priList", 'exclude' => 'set,required');
$lang->admin->menuList->feature['subMenu']['product'] = array('link' => "{$lang->productCommon}|custom|required|module=product", 'exclude' => 'set,required', 'alias' => 'browsestoryconcept,product');
diff --git a/module/admin/lang/zh-cn.php b/module/admin/lang/zh-cn.php
index 2be5c58512..18dbf40abe 100755
--- a/module/admin/lang/zh-cn.php
+++ b/module/admin/lang/zh-cn.php
@@ -118,6 +118,7 @@ $lang->admin->setModule->opportunitylib = '机会库';
$lang->admin->setModule->practicelib = '最佳实践库';
$lang->admin->setModule->componentlib = '组件库';
$lang->admin->setModule->devops = 'DevOps';
+$lang->admin->setModule->deliverable = '交付物';
$lang->admin->setModule->kanban = '通用看板';
$lang->admin->setModule->OA = '办公';
$lang->admin->setModule->deploy = '运维';
@@ -170,7 +171,7 @@ $lang->admin->menuSetting['user']['name'] = '人员管理';
$lang->admin->menuSetting['user']['desc'] = '维护部门、添加人员、分组配置权限。';
$lang->admin->menuSetting['switch']['name'] = '功能开关';
$lang->admin->menuSetting['switch']['desc'] = '打开、关闭系统部分功能。';
-$lang->admin->menuSetting['model']['name'] = '模型配置';
+$lang->admin->menuSetting['model']['name'] = '项目模型配置';
$lang->admin->menuSetting['model']['desc'] = '不同项目管理模型和项目通用要素配置。';
$lang->admin->menuSetting['feature']['name'] = '功能配置';
$lang->admin->menuSetting['feature']['desc'] = '按照功能菜单进行系统的要素配置。';
diff --git a/module/common/lang/common.php b/module/common/lang/common.php
index 01677fd34a..1c242060ad 100644
--- a/module/common/lang/common.php
+++ b/module/common/lang/common.php
@@ -105,8 +105,12 @@ $lang->ops = new stdclass();
$lang->domain = new stdclass();
$lang->service = new stdclass();
$lang->deployment = new stdclass();
+$lang->deliverable = new stdclass();
$lang->metric = new stdclass();
+$lang->projectDeliverable = new stdclass();
+$lang->executionDeliverable = new stdclass();
+
$lang->ai = new stdclass();
$lang->aiapp = new stdclass();
diff --git a/module/common/lang/de.php b/module/common/lang/de.php
index 46ff6aea75..c46dcca65c 100644
--- a/module/common/lang/de.php
+++ b/module/common/lang/de.php
@@ -289,37 +289,40 @@ $lang->aiapp->common = 'AI';
$lang->product->system = 'Application';
$lang->configure->common = 'Configure';
-$lang->programstakeholder->common = 'Stakeholder';
-$lang->featureswitch->common = 'Features On/Off';
-$lang->importdata->common = 'Import data';
-$lang->systemsetting->common = 'System setting';
-$lang->staffmanage->common = 'User management';
-$lang->modelconfig->common = 'Pattern setting';
-$lang->featureconfig->common = 'Features config';
-$lang->doctemplate->common = 'Doc template';
-$lang->notifysetting->common = 'Notification';
-$lang->bidesign->common = 'BI design';
-$lang->personalsettings->common = 'Personal setting ';
-$lang->projectsettings->common = 'Setting';
-$lang->dataaccess->common = 'Data permission';
-$lang->executiongantt->common = 'Gantt chart';
-$lang->executionkanban->common = 'Kanban';
-$lang->executionburn->common = 'Burndown chart';
-$lang->executioncfd->common = 'Cumulative Flow Diagram';
-$lang->executionstory->common = 'Story';
-$lang->executionqa->common = 'QA';
-$lang->executionsettings->common = 'Setting';
-$lang->generalcomment->common = 'Comment';
-$lang->generalping->common = 'Timeout prevention';
-$lang->generaltemplate->common = 'Template';
-$lang->generaleffort->common = 'General log';
-$lang->productsettings->common = 'Product setting';
-$lang->projectreview->common = 'Review';
-$lang->projecttrack->common = 'Matrix';
-$lang->projectqa->common = 'QA';
-$lang->holidayseason->common = 'Holiday';
-$lang->codereview->common = 'Review';
-$lang->repocode->common = 'Code';
+$lang->programstakeholder->common = 'Stakeholder';
+$lang->featureswitch->common = 'Features On/Off';
+$lang->importdata->common = 'Import data';
+$lang->systemsetting->common = 'System setting';
+$lang->staffmanage->common = 'User management';
+$lang->modelconfig->common = 'Pattern setting';
+$lang->featureconfig->common = 'Features config';
+$lang->doctemplate->common = 'Doc template';
+$lang->notifysetting->common = 'Notification';
+$lang->bidesign->common = 'BI design';
+$lang->personalsettings->common = 'Personal setting ';
+$lang->projectsettings->common = 'Setting';
+$lang->dataaccess->common = 'Data permission';
+$lang->executiongantt->common = 'Gantt chart';
+$lang->executionkanban->common = 'Kanban';
+$lang->executionburn->common = 'Burndown chart';
+$lang->executioncfd->common = 'Cumulative Flow Diagram';
+$lang->executionstory->common = 'Story';
+$lang->executionqa->common = 'QA';
+$lang->executionsettings->common = 'Setting';
+$lang->generalcomment->common = 'Comment';
+$lang->generalping->common = 'Timeout prevention';
+$lang->generaltemplate->common = 'Template';
+$lang->generaleffort->common = 'General log';
+$lang->productsettings->common = 'Product setting';
+$lang->projectreview->common = 'Review';
+$lang->projecttrack->common = 'Matrix';
+$lang->projectqa->common = 'QA';
+$lang->holidayseason->common = 'Holiday';
+$lang->codereview->common = 'Review';
+$lang->repocode->common = 'Code';
+$lang->deliverable->common = 'Deliverable';
+$lang->projectDeliverable->common = 'Project Deliverable';
+$lang->executionDeliverable->common = 'Execution Deliverable';
$lang->personnel->common = 'Member';
$lang->personnel->invest = 'Investment';
diff --git a/module/common/lang/en.php b/module/common/lang/en.php
index d15451b81a..2008ab8507 100644
--- a/module/common/lang/en.php
+++ b/module/common/lang/en.php
@@ -289,37 +289,40 @@ $lang->aiapp->common = 'AI';
$lang->product->system = 'Application';
$lang->configure->common = 'Configure';
-$lang->programstakeholder->common = 'Stakeholder';
-$lang->featureswitch->common = 'Features On/Off';
-$lang->importdata->common = 'Import data';
-$lang->systemsetting->common = 'System setting';
-$lang->staffmanage->common = 'User management';
-$lang->modelconfig->common = 'Pattern setting';
-$lang->featureconfig->common = 'Features config';
-$lang->doctemplate->common = 'Doc template';
-$lang->notifysetting->common = 'Notification';
-$lang->bidesign->common = 'BI design';
-$lang->personalsettings->common = 'Personal setting ';
-$lang->projectsettings->common = 'Setting';
-$lang->dataaccess->common = 'Data permission';
-$lang->executiongantt->common = 'Gantt chart';
-$lang->executionkanban->common = 'Kanban';
-$lang->executionburn->common = 'Burndown chart';
-$lang->executioncfd->common = 'Cumulative Flow Diagram';
-$lang->executionstory->common = 'Story';
-$lang->executionqa->common = 'QA';
-$lang->executionsettings->common = 'Setting';
-$lang->generalcomment->common = 'Comment';
-$lang->generalping->common = 'Timeout prevention';
-$lang->generaltemplate->common = 'Template';
-$lang->generaleffort->common = 'General log';
-$lang->productsettings->common = 'Product setting';
-$lang->projectreview->common = 'Review';
-$lang->projecttrack->common = 'Matrix';
-$lang->projectqa->common = 'QA';
-$lang->holidayseason->common = 'Holiday';
-$lang->codereview->common = 'Review';
-$lang->repocode->common = 'Code';
+$lang->programstakeholder->common = 'Stakeholder';
+$lang->featureswitch->common = 'Features On/Off';
+$lang->importdata->common = 'Import data';
+$lang->systemsetting->common = 'System setting';
+$lang->staffmanage->common = 'User management';
+$lang->modelconfig->common = 'Pattern setting';
+$lang->featureconfig->common = 'Features config';
+$lang->doctemplate->common = 'Doc template';
+$lang->notifysetting->common = 'Notification';
+$lang->bidesign->common = 'BI design';
+$lang->personalsettings->common = 'Personal setting ';
+$lang->projectsettings->common = 'Setting';
+$lang->dataaccess->common = 'Data permission';
+$lang->executiongantt->common = 'Gantt chart';
+$lang->executionkanban->common = 'Kanban';
+$lang->executionburn->common = 'Burndown chart';
+$lang->executioncfd->common = 'Cumulative Flow Diagram';
+$lang->executionstory->common = 'Story';
+$lang->executionqa->common = 'QA';
+$lang->executionsettings->common = 'Setting';
+$lang->generalcomment->common = 'Comment';
+$lang->generalping->common = 'Timeout prevention';
+$lang->generaltemplate->common = 'Template';
+$lang->generaleffort->common = 'General log';
+$lang->productsettings->common = 'Product setting';
+$lang->projectreview->common = 'Review';
+$lang->projecttrack->common = 'Matrix';
+$lang->projectqa->common = 'QA';
+$lang->holidayseason->common = 'Holiday';
+$lang->codereview->common = 'Review';
+$lang->repocode->common = 'Code';
+$lang->deliverable->common = 'Deliverable';
+$lang->projectDeliverable->common = 'Project Deliverable';
+$lang->executionDeliverable->common = 'Execution Deliverable';
$lang->personnel->common = 'Member';
$lang->personnel->invest = 'Investment';
diff --git a/module/common/lang/fr.php b/module/common/lang/fr.php
index 1a85fd6b83..fe0936db33 100644
--- a/module/common/lang/fr.php
+++ b/module/common/lang/fr.php
@@ -289,37 +289,40 @@ $lang->aiapp->common = 'AI';
$lang->product->system = 'Application';
$lang->configure->common = 'Configure';
-$lang->programstakeholder->common = 'Stakeholder';
-$lang->featureswitch->common = 'Features On/Off';
-$lang->importdata->common = 'Import data';
-$lang->systemsetting->common = 'System setting';
-$lang->staffmanage->common = 'User management';
-$lang->modelconfig->common = 'Pattern setting';
-$lang->featureconfig->common = 'Features config';
-$lang->doctemplate->common = 'Doc template';
-$lang->notifysetting->common = 'Notification';
-$lang->bidesign->common = 'BI design';
-$lang->personalsettings->common = 'Personal setting ';
-$lang->projectsettings->common = 'Setting';
-$lang->dataaccess->common = 'Data permission';
-$lang->executiongantt->common = 'Gantt chart';
-$lang->executionkanban->common = 'Kanban';
-$lang->executionburn->common = 'Burndown chart';
-$lang->executioncfd->common = 'Cumulative Flow Diagram';
-$lang->executionstory->common = 'Story';
-$lang->executionqa->common = 'QA';
-$lang->executionsettings->common = 'Setting';
-$lang->generalcomment->common = 'Comment';
-$lang->generalping->common = 'Timeout prevention';
-$lang->generaltemplate->common = 'Template';
-$lang->generaleffort->common = 'General log';
-$lang->productsettings->common = 'Product setting';
-$lang->projectreview->common = 'Review';
-$lang->projecttrack->common = 'Matrix';
-$lang->projectqa->common = 'QA';
-$lang->holidayseason->common = 'Holiday';
-$lang->codereview->common = 'Review';
-$lang->repocode->common = 'Code';
+$lang->programstakeholder->common = 'Stakeholder';
+$lang->featureswitch->common = 'Features On/Off';
+$lang->importdata->common = 'Import data';
+$lang->systemsetting->common = 'System setting';
+$lang->staffmanage->common = 'User management';
+$lang->modelconfig->common = 'Pattern setting';
+$lang->featureconfig->common = 'Features config';
+$lang->doctemplate->common = 'Doc template';
+$lang->notifysetting->common = 'Notification';
+$lang->bidesign->common = 'BI design';
+$lang->personalsettings->common = 'Personal setting ';
+$lang->projectsettings->common = 'Setting';
+$lang->dataaccess->common = 'Data permission';
+$lang->executiongantt->common = 'Gantt chart';
+$lang->executionkanban->common = 'Kanban';
+$lang->executionburn->common = 'Burndown chart';
+$lang->executioncfd->common = 'Cumulative Flow Diagram';
+$lang->executionstory->common = 'Story';
+$lang->executionqa->common = 'QA';
+$lang->executionsettings->common = 'Setting';
+$lang->generalcomment->common = 'Comment';
+$lang->generalping->common = 'Timeout prevention';
+$lang->generaltemplate->common = 'Template';
+$lang->generaleffort->common = 'General log';
+$lang->productsettings->common = 'Product setting';
+$lang->projectreview->common = 'Review';
+$lang->projecttrack->common = 'Matrix';
+$lang->projectqa->common = 'QA';
+$lang->holidayseason->common = 'Holiday';
+$lang->codereview->common = 'Review';
+$lang->repocode->common = 'Code';
+$lang->deliverable->common = 'Deliverable';
+$lang->projectDeliverable->common = 'Project Deliverable';
+$lang->executionDeliverable->common = 'Execution Deliverable';
$lang->personnel->common = 'Member';
$lang->personnel->invest = 'Investment';
diff --git a/module/common/lang/menu.php b/module/common/lang/menu.php
index 4d96f5ed33..fd3161b489 100644
--- a/module/common/lang/menu.php
+++ b/module/common/lang/menu.php
@@ -783,6 +783,7 @@ $lang->navGroup->extension = 'admin';
$lang->navGroup->action = 'admin';
$lang->navGroup->convert = 'admin';
$lang->navGroup->stage = 'admin';
+$lang->navGroup->deliverable = 'admin';
$lang->navGroup->featureswitch = 'admin';
$lang->navGroup->importdata = 'admin';
$lang->navGroup->systemsetting = 'admin';
diff --git a/module/common/lang/zh-cn.php b/module/common/lang/zh-cn.php
index 22bd919f27..c6b088a140 100644
--- a/module/common/lang/zh-cn.php
+++ b/module/common/lang/zh-cn.php
@@ -289,37 +289,40 @@ $lang->aiapp->common = 'AI';
$lang->product->system = '应用';
$lang->configure->common = '配置';
-$lang->programstakeholder->common = '干系人';
-$lang->featureswitch->common = '功能开关';
-$lang->importdata->common = '数据导入';
-$lang->systemsetting->common = '系统设置';
-$lang->staffmanage->common = '人员管理';
-$lang->modelconfig->common = '模型配置';
-$lang->featureconfig->common = '功能配置';
-$lang->doctemplate->common = '文档模板';
-$lang->notifysetting->common = '通知设置';
-$lang->bidesign->common = 'BI设计';
-$lang->personalsettings->common = '个人设置';
-$lang->projectsettings->common = '设置';
-$lang->dataaccess->common = '数据权限';
-$lang->executiongantt->common = '甘特图';
-$lang->executionkanban->common = '看板';
-$lang->executionburn->common = '燃尽图';
-$lang->executioncfd->common = '累积流图';
-$lang->executionstory->common = '研发需求';
-$lang->executionqa->common = '测试';
-$lang->executionsettings->common = '设置';
-$lang->generalcomment->common = '备注';
-$lang->generalping->common = '防超时';
-$lang->generaltemplate->common = '模板';
-$lang->generaleffort->common = '通用日志';
-$lang->productsettings->common = '产品设置';
-$lang->projectreview->common = '评审';
-$lang->projecttrack->common = '矩阵';
-$lang->projectqa->common = '测试';
-$lang->holidayseason->common = '节假日';
-$lang->codereview->common = '问题';
-$lang->repocode->common = '代码';
+$lang->programstakeholder->common = '干系人';
+$lang->featureswitch->common = '功能开关';
+$lang->importdata->common = '数据导入';
+$lang->systemsetting->common = '系统设置';
+$lang->staffmanage->common = '人员管理';
+$lang->modelconfig->common = '项目模型配置';
+$lang->featureconfig->common = '功能配置';
+$lang->doctemplate->common = '文档模板';
+$lang->notifysetting->common = '通知设置';
+$lang->bidesign->common = 'BI设计';
+$lang->personalsettings->common = '个人设置';
+$lang->projectsettings->common = '设置';
+$lang->dataaccess->common = '数据权限';
+$lang->executiongantt->common = '甘特图';
+$lang->executionkanban->common = '看板';
+$lang->executionburn->common = '燃尽图';
+$lang->executioncfd->common = '累积流图';
+$lang->executionstory->common = '研发需求';
+$lang->executionqa->common = '测试';
+$lang->executionsettings->common = '设置';
+$lang->generalcomment->common = '备注';
+$lang->generalping->common = '防超时';
+$lang->generaltemplate->common = '模板';
+$lang->generaleffort->common = '通用日志';
+$lang->productsettings->common = '产品设置';
+$lang->projectreview->common = '评审';
+$lang->projecttrack->common = '矩阵';
+$lang->projectqa->common = '测试';
+$lang->holidayseason->common = '节假日';
+$lang->codereview->common = '问题';
+$lang->repocode->common = '代码';
+$lang->deliverable->common = '交付物';
+$lang->projectDeliverable->common = '项目交付物';
+$lang->executionDeliverable->common = '执行交付物';
$lang->personnel->common = '人员';
$lang->personnel->invest = '投入人员';
diff --git a/module/common/view/action.html.php b/module/common/view/action.html.php
index d1dadede7f..46c53b7416 100755
--- a/module/common/view/action.html.php
+++ b/module/common/view/action.html.php
@@ -61,7 +61,7 @@
history)):?>
- action->printChanges($action->objectType, $action->history);?>
+ action->printChanges($action->objectType, $action->objectID, $action->history);?>
comment))) != 0):?>
diff --git a/module/common/view/mail.footer.html.php b/module/common/view/mail.footer.html.php
index 3d8d365619..3f632ce7b5 100644
--- a/module/common/view/mail.footer.html.php
+++ b/module/common/view/mail.footer.html.php
@@ -28,7 +28,7 @@ if(file_exists($extViewFile))
history)):?>
|
- action->printChanges($action->objectType, $action->history, false);?>
+ action->printChanges($action->objectType, $action->objectID, $action->history, false);?>
|
diff --git a/module/doc/lang/de.php b/module/doc/lang/de.php
index efa160a474..e8b1f5eed7 100644
--- a/module/doc/lang/de.php
+++ b/module/doc/lang/de.php
@@ -253,6 +253,8 @@ $lang->doc->uploadFormat = 'Upload Format';
$lang->doc->editedList = 'File editor';
$lang->doc->moveTo = 'Move to';
$lang->doc->notSupportExport = 'This document does not support export';
+$lang->doc->downloadTemplate = 'Download Template';
+$lang->doc->addFile = 'Add File';
$lang->doc->preview = 'Preview';
$lang->doc->insertTitle = 'Insert %s list';
@@ -439,6 +441,7 @@ $lang->doc->noCollectedDoc = 'Sie haben kein Dokument gesammelt.';
$lang->doc->errorEmptyLib = 'No data in document library.';
$lang->doc->confirmUpdateContent = 'You have a document that is not saved from last time. Do you want to continue editing it?';
$lang->doc->selectLibType = 'Please select a type of doc library.';
+$lang->doc->selectDoc = 'Please select a doc';
$lang->doc->noLibreOffice = 'You does not have access to office conversion settings!';
$lang->doc->errorParentChapter = 'The parent chapter cannot be its own chapter or sub chapter!';
$lang->doc->errorOthersCreated = 'There are documents created by others in this library. You cannot move it.';
diff --git a/module/doc/lang/en.php b/module/doc/lang/en.php
index d8ee47e89f..a51c5e3105 100644
--- a/module/doc/lang/en.php
+++ b/module/doc/lang/en.php
@@ -253,6 +253,8 @@ $lang->doc->uploadFormat = 'Upload Format';
$lang->doc->editedList = 'File editor';
$lang->doc->moveTo = 'Move to';
$lang->doc->notSupportExport = 'This document does not support export';
+$lang->doc->downloadTemplate = 'Download Template';
+$lang->doc->addFile = 'Add File';
$lang->doc->preview = 'Preview';
$lang->doc->insertTitle = 'Insert %s list';
@@ -439,6 +441,7 @@ $lang->doc->noCollectedDoc = 'You have not favorited any documents.
$lang->doc->errorEmptyLib = 'No data in document library.';
$lang->doc->confirmUpdateContent = 'You have a document that is not saved from last time. Do you want to continue editing it?';
$lang->doc->selectLibType = 'Please select a type of doc library.';
+$lang->doc->selectDoc = 'Please select a doc';
$lang->doc->noLibreOffice = 'You does not have access to office conversion settings!';
$lang->doc->errorParentChapter = 'The parent chapter cannot be its own chapter or sub chapter!';
$lang->doc->errorOthersCreated = 'There are documents created by others in this library. You cannot move it.';
diff --git a/module/doc/lang/fr.php b/module/doc/lang/fr.php
index dd6f2b11ff..9668928712 100644
--- a/module/doc/lang/fr.php
+++ b/module/doc/lang/fr.php
@@ -253,6 +253,8 @@ $lang->doc->uploadFormat = 'Upload Format';
$lang->doc->editedList = 'File editor';
$lang->doc->moveTo = 'Move to';
$lang->doc->notSupportExport = 'This document does not support export';
+$lang->doc->downloadTemplate = 'Download Template';
+$lang->doc->addFile = 'Add File';
$lang->doc->preview = 'Preview';
$lang->doc->insertTitle = 'Insert %s list';
@@ -439,6 +441,7 @@ $lang->doc->noCollectedDoc = "Vous avez aucun document dans vos fav
$lang->doc->errorEmptyLib = 'No data in document library.';
$lang->doc->confirmUpdateContent = 'You have a document that is not saved from last time. Do you want to continue editing it?';
$lang->doc->selectLibType = 'Please select a type of doc library.';
+$lang->doc->selectDoc = 'Please select a doc';
$lang->doc->noLibreOffice = 'You does not have access to office conversion settings!';
$lang->doc->errorParentChapter = 'The parent chapter cannot be its own chapter or sub chapter!';
$lang->doc->errorOthersCreated = 'There are documents created by others in this library. You cannot move it.';
diff --git a/module/doc/lang/zh-cn.php b/module/doc/lang/zh-cn.php
index 1adcfed06d..99ab4dbb73 100644
--- a/module/doc/lang/zh-cn.php
+++ b/module/doc/lang/zh-cn.php
@@ -253,6 +253,8 @@ $lang->doc->uploadFormat = '上传格式';
$lang->doc->editedList = '文档编辑者';
$lang->doc->moveTo = '移动至';
$lang->doc->notSupportExport = '(此文档暂不支持导出)';
+$lang->doc->downloadTemplate = '下载模板';
+$lang->doc->addFile = '提交文件';
$lang->doc->preview = '预览';
$lang->doc->insertTitle = '插入%s列表';
@@ -439,6 +441,7 @@ $lang->doc->noCollectedDoc = '您还没有收藏任何文档。';
$lang->doc->errorEmptyLib = '文档库暂无数据。';
$lang->doc->confirmUpdateContent = '检查到您有未保存的文档内容,是否继续编辑?';
$lang->doc->selectLibType = '请选择文档库类型';
+$lang->doc->selectDoc = '请选择文档';
$lang->doc->noLibreOffice = '您还没有office转换设置访问权限!';
$lang->doc->errorParentChapter = '父章节不能是自身章节及子章节!';
$lang->doc->errorOthersCreated = '该库下其他人创建的文档暂不支持移动,是否确认移动?';
diff --git a/module/doc/model.php b/module/doc/model.php
index ddc700cf49..72aa61c735 100644
--- a/module/doc/model.php
+++ b/module/doc/model.php
@@ -1571,12 +1571,12 @@ class docModel extends model
}
$files = $this->loadModel('file')->getUpload();
- if($doc->type == 'attachment' && (empty($files) || isset($files['name']))) return dao::$errors['files'] = sprintf($this->lang->error->notempty, $this->lang->doc->uploadFile);
+ if($doc->type == 'attachment' && empty($doc->copy) && (empty($files) || isset($files['name']))) return dao::$errors['files'] = sprintf($this->lang->error->notempty, $this->lang->doc->uploadFile);
$doc->draft = $isDraft ? $docContent->content : '';
$doc->vision = $this->config->vision;
$doc->version = $isDraft ? 0 : 1;
- $this->dao->insert(TABLE_DOC)->data($doc, 'content')->autoCheck()->batchCheck($requiredFields, 'notempty')->exec();
+ $this->dao->insert(TABLE_DOC)->data($doc, 'content,copy')->autoCheck()->batchCheck($requiredFields, 'notempty')->exec();
if(dao::isError()) return false;
$docID = $this->dao->lastInsertID();
diff --git a/module/execution/config/dtable.php b/module/execution/config/dtable.php
index 0fe823bf71..790d50be86 100644
--- a/module/execution/config/dtable.php
+++ b/module/execution/config/dtable.php
@@ -49,6 +49,17 @@ $config->execution->dtable->fieldList['status']['width'] = '80';
$config->execution->dtable->fieldList['status']['group'] = '1';
$config->execution->dtable->fieldList['status']['show'] = true;
+if(in_array($config->edition, array('max', 'ipd')) && helper::hasFeature('deliverable'))
+{
+ $config->execution->dtable->fieldList['deliverable']['title'] = $lang->execution->deliverableAbbr;
+ $config->execution->dtable->fieldList['deliverable']['name'] = 'deliverable';
+ $config->execution->dtable->fieldList['deliverable']['type'] = 'html';
+ $config->execution->dtable->fieldList['deliverable']['width'] = '100px';
+ $config->execution->dtable->fieldList['deliverable']['group'] = '1';
+ $config->execution->dtable->fieldList['deliverable']['show'] = true;
+ $config->execution->dtable->fieldList['deliverable']['sortType'] = false;
+}
+
$config->execution->dtable->fieldList['PM']['title'] = $lang->execution->execPM;
$config->execution->dtable->fieldList['PM']['name'] = 'PM';
$config->execution->dtable->fieldList['PM']['type'] = 'avatarBtn';
diff --git a/module/execution/control.php b/module/execution/control.php
index 2f13d2ae81..4efd825551 100644
--- a/module/execution/control.php
+++ b/module/execution/control.php
@@ -1362,7 +1362,7 @@ class execution extends control
}
$executionIDList = $this->post->executionIDList;
- $executions = $this->dao->select('*')->from(TABLE_EXECUTION)->where('id')->in($executionIDList)->fetchAll('id');
+ $executions = $this->dao->select('*')->from(TABLE_EXECUTION)->where('id')->in($executionIDList)->fetchAll('id', false);
$relatedProjects = $this->dao->select('id,project')->from(TABLE_PROJECT)->where('id')->in($executionIDList)->fetchPairs(); /* 获取执行所属的项目列表。*/
$projects = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->in($relatedProjects)->fetchAll('id'); /* 获取执行所属的项目列表中每个项目的项目信息。*/
@@ -1415,8 +1415,8 @@ class execution extends control
$executionIdList = $this->post->executionIDList;
if(is_string($executionIdList)) $executionIdList = explode(',', $executionIdList);
- $filteredStages = $this->execution->batchChangeStatus($executionIdList, $status);
- if(!$filteredStages) return $this->sendSuccess(array('load' => true));
+ $message = $this->execution->batchChangeStatus($executionIdList, $status);
+ if(empty($message['byChild']) && empty($message['byDeliverable'])) return $this->sendSuccess(array('load' => true));
$alertMsg = '';
if($status == 'wait')
@@ -1426,18 +1426,28 @@ class execution extends control
if(empty($project) or (!empty($project) and strpos($project->model, 'waterfall') !== false))
{
$executionLang = (empty($project) or (!empty($project) and $project->model == 'waterfallplus')) ? $this->lang->execution->common : $this->lang->stage->common;
- $alertMsg = sprintf($this->lang->execution->hasStartedTaskOrSubStage, $executionLang, $filteredStages);
+ $alertMsg = sprintf($this->lang->execution->hasStartedTaskOrSubStage, $executionLang, $message['byChild']);
}
if(!empty($project) and strpos('agileplus,scrum', $project->model) !== false)
{
$executionLang = $project->model == 'scrum' ? $this->lang->executionCommon : $this->lang->execution->common;
- $alertMsg = sprintf($this->lang->execution->hasStartedTask, $executionLang, $filteredStages);
+ $alertMsg = sprintf($this->lang->execution->hasStartedTask, $executionLang, $message['byChild']);
+ }
+ }
+ if($status == 'suspended') $alertMsg = sprintf($this->lang->execution->hasSuspendedOrClosedChildren, $message['byChild']);
+ if($status == 'closed')
+ {
+ if(!empty($message['byChild']))
+ {
+ $alertMsg .= sprintf($this->lang->execution->hasNotClosedChildren, $message['byChild']);
+ }
+ elseif(!empty($message['byDeliverable']))
+ {
+ $alertMsg .= sprintf($this->lang->execution->cannotCloseByDeliverable, $message['byDeliverable']);
}
}
- if($status == 'suspended') $alertMsg = sprintf($this->lang->execution->hasSuspendedOrClosedChildren, $filteredStages);
- if($status == 'closed') $alertMsg = sprintf($this->lang->execution->hasNotClosedChildren, $filteredStages);
- return $this->sendSuccess(array('message' => $alertMsg, 'load' => true));
+ return $this->send(array('load' => array('alert' => $alertMsg), 'result' => 'success'));
}
/**
@@ -1666,7 +1676,11 @@ class execution extends control
if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
$project = $this->loadModel('project')->getById($execution->project);
- if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd'))) $this->loadModel('programplan')->computeProgress($executionID, 'close');
+ if(in_array($project->model, array('waterfall', 'waterfallplus', 'ipd')))
+ {
+ $result = $this->loadModel('programplan')->computeProgress($executionID, 'close');
+ if(is_array($result)) return $this->send($result);
+ }
$this->executeHooks($executionID);
@@ -2893,6 +2907,7 @@ class execution extends control
$this->app->loadLang('stage');
$this->app->loadLang('programplan');
$this->loadModel('product');
+ $this->loadModel('project');
$this->loadModel('datatable');
$from = $this->app->tab;
diff --git a/module/execution/js/batchedit.ui.js b/module/execution/js/batchedit.ui.js
index 9360903740..18c91de382 100644
--- a/module/execution/js/batchedit.ui.js
+++ b/module/execution/js/batchedit.ui.js
@@ -25,7 +25,16 @@ window.renderRowData = function($row, index, row)
options.disabled = row.grade > 1 && parentType != 'mix';
if(projectModel == 'ipd') options.disabled = true;
}
+
$row.attr('data-parent', row.parent);
+
+ $row.find('[data-name="lifetime"]').find('.picker-box').on('inited', function(e, info)
+ {
+ let $attribute = info[0];
+ $attribute.render({items: stageItems, required: true, name: 'attribute[' + row.id + ']', disabled: row.grade > 1 && parentType != 'mix'});
+ if(typeof row != 'undefined' && typeof row.hasDeliverable != 'undefined') $attribute.render({disabled: true});
+ $(e.target).attr('data-parent', row.parent);
+ });
}
$row.find('[data-name="lifetime"]').find('.picker-box').on('inited', function(e, info) { info[0].render(options); });
diff --git a/module/execution/model.php b/module/execution/model.php
index 623e070514..30242b3f4c 100755
--- a/module/execution/model.php
+++ b/module/execution/model.php
@@ -539,17 +539,24 @@ class executionModel extends model
* @param array $executionIdList
* @param string $status
* @access public
- * @return string 返回不符合条件被过滤了的执行,来提示执行下任务或子阶段已经开始,无法修改,已过滤。参见 story#41875。
+ * @return array 返回不符合条件被过滤了的执行,来提示执行下任务或子阶段已经开始,无法修改,已过滤。参见 story#41875。
*/
- public function batchChangeStatus(array $executionIdList, string $status): string
+ public function batchChangeStatus(array $executionIdList, string $status): array
{
/* Sort the IDs, the child stage comes first, and the parent stage follows. */
- $executionIdList = $this->dao->select('id')->from(TABLE_EXECUTION)->where('id')->in($executionIdList)->orderBy('grade_desc')->fetchPairs();
+ $executionList = $this->dao->select('id,name,status,grade,deliverable')->from(TABLE_EXECUTION)->where('id')->in($executionIdList)->orderBy('grade_desc')->fetchAll('id');
$this->loadModel('programplan');
- $filteredStages = '';
- foreach($executionIdList as $executionID)
+ $message = array('byChild' => '', 'byDeliverable' => '');
+ foreach($executionList as $executionID => $execution)
{
+ $needCheckDeliverable = $status == 'closed' && $execution->status == 'doing' && $execution->grade == 1;
+ if(in_array($this->config->edition, array('max', 'ipd')) && $needCheckDeliverable && !$this->canCloseByDeliverable($execution))
+ {
+ $message['byDeliverable'] .= '#' . $execution->id . ' ' . $execution->name . "\n";
+ continue;
+ }
+
/* The state of the parent stage or the sibling stage may be affected by the child stage before the change, so it cannot be checked in advance. */
$selfAndChildrenList = $this->programplan->getSelfAndChildrenList($executionID);
$selfAndChildren = $selfAndChildrenList[$executionID];
@@ -557,7 +564,7 @@ class executionModel extends model
if($status == 'wait' and $execution->status != 'wait')
{
- $filteredStages .= $this->changeStatus2Wait($executionID, $selfAndChildren);
+ $message['byChild'] .= $this->changeStatus2Wait($executionID, $selfAndChildren);
}
if($status == 'doing' and $execution->status != 'doing')
@@ -567,11 +574,12 @@ class executionModel extends model
if(($status == 'suspended' and $execution->status != 'suspended') or ($status == 'closed' and $execution->status != 'closed'))
{
- $filteredStages .= $this->changeStatus2Inactive($executionID, $status, $selfAndChildren);
+ $message['byChild'] .= $this->changeStatus2Inactive($executionID, $status, $selfAndChildren);
}
}
- return trim($filteredStages, ',');
+ $message['byChild'] = trim($message['byChild'], ',');
+ return $message;
}
/**
@@ -906,9 +914,9 @@ class executionModel extends model
* @param int $executionID
* @param object $postData
* @access public
- * @return array|false
+ * @return int|false
*/
- public function close(int $executionID, object $postData): array|false
+ public function close(int $executionID, object $postData): int|false
{
$oldExecution = $this->fetchById($executionID); /* Save previous execution to variable for later compare. */
@@ -937,7 +945,7 @@ class executionModel extends model
}
$this->loadModel('score')->create('execution', 'close', $oldExecution);
- return $changes;
+ return $actionID;
}
/**
@@ -1452,7 +1460,7 @@ class executionModel extends model
->fi()
->orderBy($orderBy)
->page($pager, 't1.id')
- ->fetchAll('id');
+ ->fetchAll('id', false);
}
/**
@@ -1481,6 +1489,7 @@ class executionModel extends model
$today = helper::today();
$burns = $this->getBurnData($executions);
$parentExecutions = $this->dao->select('parent,parent')->from(TABLE_EXECUTION)->where('parent')->ne(0)->andWhere('deleted')->eq(0)->fetchPairs();
+ $statusGroup = $this->dao->select('parent,status')->from(TABLE_EXECUTION)->where('parent')->in(array_keys($executions))->fetchGroup('parent', 'status');
/* Get workingDays. */
$earliestEnd = $today;
@@ -1503,6 +1512,13 @@ class executionModel extends model
if(isset($parentExecutions[$execution->id])) $executions[$execution->id]->isParent = 1;
if(empty($productID) && !empty($productList[$execution->id])) $execution->product = trim($productList[$execution->id]->product, ',');
+ /** 如果子执行都关闭了,则父执行可以手动关闭。 */
+ if(isset($statusGroup[$execution->id]))
+ {
+ $childStatus = array_keys($statusGroup[$execution->id]);
+ if(count($childStatus) == 1 && $childStatus[0] == 'closed') $execution->parentCanClose = true;
+ }
+
/* Judge whether the execution is delayed. */
if($execution->status != 'done' && $execution->status != 'closed' && $execution->status != 'suspended' && !empty($workingDays))
{
@@ -3827,7 +3843,7 @@ class executionModel extends model
$action = strtolower($action);
if($action == 'start') return $execution->status == 'wait';
- if($action == 'close') return $execution->status != 'closed' && (!isset($execution->isParent) || (isset($execution->isParent) && !$execution->isParent));
+ if($action == 'close') return $execution->status != 'closed' && (empty($execution->isParent) || !empty($execution->parentCanClose));
if($action == 'suspend') return $execution->status == 'wait' || $execution->status == 'doing';
if($action == 'putoff') return $execution->status == 'wait' || $execution->status == 'doing';
if($action == 'activate') return $execution->status == 'suspended' || $execution->status == 'closed';
@@ -4797,6 +4813,7 @@ class executionModel extends model
$execution->id = 'pid' . (string)$execution->id;
$execution->projectID = $execution->project;
$execution->project = $execution->projectName;
+ $execution->rawParent = $execution->parent;
$execution->parent = (isset($executionList[$execution->parent]) && $execution->parent && $execution->grade > 1) ? 'pid' . (string)$execution->parent : '';
$execution->hasChild = !empty($execution->isParent);
$execution->isParent = !empty($execution->isParent) or !empty($execution->tasks);
@@ -4841,6 +4858,8 @@ class executionModel extends model
$execution->PMAvatar = zget($avatarList, $execution->PMAccount, '');
}
+ if(in_array($this->config->edition, array('max', 'ipd'))) $execution->deliverable = $this->project->countDeliverable($execution, 'execution');
+
$rows[$execution->id] = $execution;
/* Append tasks and child stages. */
diff --git a/module/execution/tao.php b/module/execution/tao.php
index 984290a0fd..071f91c29d 100644
--- a/module/execution/tao.php
+++ b/module/execution/tao.php
@@ -142,8 +142,8 @@ class executionTao extends executionModel
$executions[$executionID]->lastEditedBy = $this->app->user->account;
$executions[$executionID]->lastEditedDate = helper::now();
- if(isset($postData->code)) $executions[$executionID]->code = $executionCode;
- if(isset($postData->project)) $executions[$executionID]->project = zget($postData->project, $executionID, 0);
+ if(isset($postData->code)) $executions[$executionID]->code = $executionCode;
+ if(isset($postData->project)) $executions[$executionID]->project = zget($postData->project, $executionID, 0);
if(isset($postData->attribute[$executionID])) $executions[$executionID]->attribute = zget($postData->attribute, $executionID, '');
if(isset($postData->lifetime[$executionID])) $executions[$executionID]->lifetime = $postData->lifetime[$executionID];
diff --git a/module/execution/test/tao/setkanbanmenu.php b/module/execution/test/tao/setkanbanmenu.php
index 782884b17c..d5499a2938 100644
--- a/module/execution/test/tao/setkanbanmenu.php
+++ b/module/execution/test/tao/setkanbanmenu.php
@@ -11,7 +11,17 @@ title=测试executionModel->setKanbanMenu();
timeout=0
cid=1
+- 测试替换看板执行的二级菜单第kanban条的link属性 @看板|execution|kanban|executionID=%s
+- 查看交付物菜单第deliverable条的link属性 @交付物|execution|deliverable|executionID=%s
+- 查看构建菜单第build条的link属性 @构建|execution|build|executionID=%s
+- 查看累积流图菜单第CFD条的link属性 @累积流图|execution|cfd|executionID=%s
+- 查看设置菜单第settings条的link属性 @设置|execution|view|executionID=%s
+
*/
$executionTester = new executionTest();
-r($executionTester->setKanbanMenuTest()) && p('kanban:link') && e('看板|execution|kanban|executionID=%s'); // 测试替换看板执行的二级菜单
+r($executionTester->setKanbanMenuTest()) && p('kanban:link') && e('看板|execution|kanban|executionID=%s'); // 测试替换看板执行的二级菜单
+r($executionTester->setKanbanMenuTest()) && p('deliverable:link') && e('交付物|execution|deliverable|executionID=%s'); // 查看交付物菜单
+r($executionTester->setKanbanMenuTest()) && p('build:link') && e('构建|execution|build|executionID=%s'); // 查看构建菜单
+r($executionTester->setKanbanMenuTest()) && p('CFD:link') && e('累积流图|execution|cfd|executionID=%s'); // 查看累积流图菜单
+r($executionTester->setKanbanMenuTest()) && p('settings:link') && e('设置|execution|view|executionID=%s'); // 查看设置菜单
\ No newline at end of file
diff --git a/module/file/model.php b/module/file/model.php
index 611e7bc02e..5b4732dedc 100755
--- a/module/file/model.php
+++ b/module/file/model.php
@@ -185,7 +185,7 @@ class fileModel extends model
$file['objectID'] = $objectID;
$file['addedBy'] = $this->app->user->account;
$file['addedDate'] = $now;
- $file['extra'] = $extra;
+ if($extra) $file['extra'] = $extra;
unset($file['tmpname']);
$this->dao->insert(TABLE_FILE)->data($file)->exec();
$fileTitles[$this->dao->lastInsertId()] = $file['title'];
@@ -279,6 +279,7 @@ class fileModel extends model
$file['title'] = $purifier->purify($file['title']);
$file['size'] = $size[$id];
$file['tmpname'] = $tmp_name[$id];
+ $file['extra'] = !empty($extra[$id]) ? $extra[$id] : '';
$files[] = $file;
}
}
@@ -464,12 +465,12 @@ class fileModel extends model
/**
* Set path name of the uploaded file to be saved.
*
- * @param int $fileID
- * @param string $extension
+ * @param string|int $fileID
+ * @param string $extension
* @access public
* @return string
*/
- public function setPathName(int $fileID, string $extension): string
+ public function setPathName(string|int $fileID, string $extension): string
{
$sessionID = session_id();
$randString = substr($sessionID, mt_rand(0, strlen($sessionID) - 5), 3);
diff --git a/module/group/lang/de.php b/module/group/lang/de.php
index c5242f8351..8c14b1a702 100644
--- a/module/group/lang/de.php
+++ b/module/group/lang/de.php
@@ -488,6 +488,7 @@ $lang->group->package->projectBuild = 'Project Build';
$lang->group->package->importCaseLib = 'Import Case Lib';
$lang->group->package->commonSetting = 'Common Setting';
$lang->group->package->stageSetting = 'Stage Setting';
+$lang->group->package->deliverable = 'Deliverable Setting';
$lang->group->package->classify = 'Classify';
$lang->group->package->cmcl = 'Cmcl';
$lang->group->package->auditcl = 'Auditcl';
@@ -620,5 +621,7 @@ $lang->group->package->application = 'Manage Application';
$lang->group->package->component = 'Component';
$lang->group->package->browseRule = 'Browse Rule';
$lang->group->package->manageRule = 'Manage Rule';
+$lang->group->package->executionDeliverable = 'Execution Deliverable';
+$lang->group->package->projectDeliverable = 'Project Deliverable';
include (dirname(__FILE__) . '/resource.php');
diff --git a/module/group/lang/en.php b/module/group/lang/en.php
index 0077e8d7b3..2d6cb05ddb 100644
--- a/module/group/lang/en.php
+++ b/module/group/lang/en.php
@@ -488,6 +488,7 @@ $lang->group->package->projectBuild = 'Project Build';
$lang->group->package->importCaseLib = 'Import Case Lib';
$lang->group->package->commonSetting = 'Common Setting';
$lang->group->package->stageSetting = 'Stage Setting';
+$lang->group->package->deliverable = 'Deliverable Setting';
$lang->group->package->classify = 'Classify';
$lang->group->package->cmcl = 'Cmcl';
$lang->group->package->auditcl = 'Auditcl';
@@ -620,5 +621,7 @@ $lang->group->package->application = 'Manage Application';
$lang->group->package->component = 'Component';
$lang->group->package->browseRule = 'Browse Rule';
$lang->group->package->manageRule = 'Manage Rule';
+$lang->group->package->executionDeliverable = 'Execution Deliverable';
+$lang->group->package->projectDeliverable = 'Project Deliverable';
include (dirname(__FILE__) . '/resource.php');
diff --git a/module/group/lang/fr.php b/module/group/lang/fr.php
index 8b10db5e55..035195166c 100644
--- a/module/group/lang/fr.php
+++ b/module/group/lang/fr.php
@@ -488,6 +488,7 @@ $lang->group->package->projectBuild = 'Project Build';
$lang->group->package->importCaseLib = 'Import Case Lib';
$lang->group->package->commonSetting = 'Common Setting';
$lang->group->package->stageSetting = 'Stage Setting';
+$lang->group->package->deliverable = 'Deliverable Setting';
$lang->group->package->classify = 'Classify';
$lang->group->package->cmcl = 'Cmcl';
$lang->group->package->auditcl = 'Auditcl';
@@ -620,5 +621,7 @@ $lang->group->package->application = 'Manage Application';
$lang->group->package->component = 'Component';
$lang->group->package->browseRule = 'Browse Rule';
$lang->group->package->manageRule = 'Manage Rule';
+$lang->group->package->executionDeliverable = 'Execution Deliverable';
+$lang->group->package->projectDeliverable = 'Project Deliverable';
include (dirname(__FILE__) . '/resource.php');
diff --git a/module/group/lang/zh-cn.php b/module/group/lang/zh-cn.php
index 91b990594b..8bc71a8b77 100644
--- a/module/group/lang/zh-cn.php
+++ b/module/group/lang/zh-cn.php
@@ -488,6 +488,7 @@ $lang->group->package->projectBuild = '项目构建';
$lang->group->package->importCaseLib = '导入用例库';
$lang->group->package->commonSetting = '通用配置';
$lang->group->package->stageSetting = '阶段列表设置';
+$lang->group->package->deliverable = '交付物设置';
$lang->group->package->classify = '分类项设置';
$lang->group->package->cmcl = '审计设置';
$lang->group->package->auditcl = 'QA检查项设置';
@@ -620,5 +621,7 @@ $lang->group->package->application = '管理应用';
$lang->group->package->component = '组件';
$lang->group->package->browseRule = '浏览规则';
$lang->group->package->manageRule = '创建维护规则';
+$lang->group->package->executionDeliverable = '交付物';
+$lang->group->package->projectDeliverable = '交付物';
include (dirname(__FILE__) . '/resource.php');
diff --git a/module/group/packagemanager.php b/module/group/packagemanager.php
index 0ebcc691a6..ff458e3d6c 100644
--- a/module/group/packagemanager.php
+++ b/module/group/packagemanager.php
@@ -65,6 +65,10 @@ $config->group->subset->project = new stdclass();
$config->group->subset->project->order = 170;
$config->group->subset->project->nav = 'project';
+$config->group->subset->projectDeliverable = new stdclass();
+$config->group->subset->projectDeliverable->order = 171;
+$config->group->subset->projectDeliverable->nav = 'project';
+
$config->group->subset->projectplan = new stdclass();
$config->group->subset->projectplan->order = 180;
$config->group->subset->projectplan->nav = 'project';
@@ -153,6 +157,10 @@ $config->group->subset->execution = new stdclass();
$config->group->subset->execution->order = 530;
$config->group->subset->execution->nav = 'execution';
+$config->group->subset->executionDeliverable = new stdclass();
+$config->group->subset->executionDeliverable->order = 531;
+$config->group->subset->executionDeliverable->nav = 'execution';
+
$config->group->subset->task = new stdclass();
$config->group->subset->task->order = 540;
$config->group->subset->task->nav = 'execution';
@@ -1048,6 +1056,12 @@ $config->group->package->projectWhitelist->privs['project-whitelist'] = ar
$config->group->package->projectWhitelist->privs['project-addWhitelist'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 110, 'depend' => array('project-whitelist'), 'recommend' => array('project-unbindWhitelist'));
$config->group->package->projectWhitelist->privs['project-unbindWhitelist'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 115, 'depend' => array('project-whitelist'), 'recommend' => array('project-addWhitelist'));
+$config->group->package->projectDeliverable = new stdclass();
+$config->group->package->projectDeliverable->order = 5;
+$config->group->package->projectDeliverable->subset = 'projectDeliverable';
+$config->group->package->projectDeliverable->privs = array();
+$config->group->package->projectDeliverable->privs['project-deliverable'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 10, 'depend' => array(), 'recommend' => array());
+
$config->group->package->browseExecution = new stdclass();
$config->group->package->browseExecution->order = 5;
$config->group->package->browseExecution->subset = 'execution';
@@ -1095,6 +1109,13 @@ $config->group->package->executionWhitelist->privs['execution-whitelist']
$config->group->package->executionWhitelist->privs['execution-addWhitelist'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 245, 'depend' => array('execution-whitelist'), 'recommend' => array('execution-unbindWhitelist'));
$config->group->package->executionWhitelist->privs['execution-unbindWhitelist'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 250, 'depend' => array('execution-whitelist'), 'recommend' => array('execution-addWhitelist'));
+$config->group->package->executionDeliverable = new stdclass();
+$config->group->package->executionDeliverable->order = 5;
+$config->group->package->executionDeliverable->subset = 'executionDeliverable';
+$config->group->package->executionDeliverable->nav = 'execution';
+$config->group->package->executionDeliverable->privs = array();
+$config->group->package->executionDeliverable->privs['execution-deliverable'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 10, 'depend' => array(), 'recommend' => array());
+
$config->group->package->gantt = new stdclass();
$config->group->package->gantt->order = 5;
$config->group->package->gantt->subset = 'executionview';
@@ -2568,7 +2589,7 @@ $config->group->package->workflowGroup = new stdclass();
$config->group->package->workflowGroup->order = 65;
$config->group->package->workflowGroup->subset = 'workflow';
$config->group->package->workflowGroup->privs = array();
-$config->group->package->workflowGroup->privs['workflowgroup-product'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 5, 'depend' => array('workflow-browseFlow'), 'recommend' => array());
+$config->group->package->workflowGroup->privs['workflowgroup-product'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 5, 'depend' => array('workflow-browseFlow'), 'recommend' => array());
$config->group->package->workflowGroup->privs['workflowgroup-project'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 10, 'depend' => array('workflow-browseFlow'), 'recommend' => array());
$config->group->package->workflowGroup->privs['workflowgroup-create'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 15, 'depend' => array('workflow-browseFlow'), 'recommend' => array());
$config->group->package->workflowGroup->privs['workflowgroup-edit'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 20, 'depend' => array('workflow-browseFlow'), 'recommend' => array());
@@ -2581,6 +2602,7 @@ $config->group->package->workflowGroup->privs['workflowgroup-setExclusive'] =
$config->group->package->workflowGroup->privs['workflowgroup-activateFlow'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 55, 'depend' => array('workflow-browseFlow'), 'recommend' => array());
$config->group->package->workflowGroup->privs['workflowgroup-deactivateFlow'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 60, 'depend' => array('workflow-browseFlow'), 'recommend' => array());
$config->group->package->workflowGroup->privs['workflowgroup-rule'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 65, 'depend' => array('workflow-browseFlow'), 'recommend' => array());
+$config->group->package->workflowGroup->privs['workflowgroup-deliverable'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 70, 'depend' => array('workflowgroup-project'), 'recommend' => array());
$config->group->package->workflow = new stdclass();
$config->group->package->workflow->order = 5;
@@ -2999,6 +3021,16 @@ $config->group->package->stageSetting->privs['stage-edit'] = array('editi
$config->group->package->stageSetting->privs['stage-delete'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 5, 'depend' => array(), 'recommend' => array('stage-create', 'stage-edit'));
$config->group->package->stageSetting->privs['stage-plusBrowse'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 1, 'depend' => array(), 'recommend' => array());
+$config->group->package->deliverable = new stdclass();
+$config->group->package->deliverable->order = 20;
+$config->group->package->deliverable->subset = 'modelconfig';
+$config->group->package->deliverable->privs = array();
+$config->group->package->deliverable->privs['deliverable-browse'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 0, 'depend' => array(), 'recommend' => array());
+$config->group->package->deliverable->privs['deliverable-create'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 1, 'depend' => array('deliverable-browse'), 'recommend' => array());
+$config->group->package->deliverable->privs['deliverable-edit'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 2, 'depend' => array('deliverable-browse'), 'recommend' => array('deliverable-view', 'deliverable-browse'));
+$config->group->package->deliverable->privs['deliverable-delete'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 3, 'depend' => array('deliverable-browse'), 'recommend' => array('deliverable-view', 'deliverable-browse'));
+$config->group->package->deliverable->privs['deliverable-view'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 4, 'depend' => array('deliverable-browse'), 'recommend' => array());
+
$config->group->package->classify = new stdclass();
$config->group->package->classify->order = 30;
$config->group->package->classify->subset = 'modelconfig';
diff --git a/module/programplan/js/create.ui.js b/module/programplan/js/create.ui.js
index 7b9e255475..6e61ed13b4 100644
--- a/module/programplan/js/create.ui.js
+++ b/module/programplan/js/create.ui.js
@@ -230,6 +230,12 @@ window.handleRenderRow = function($row, index, data)
$typePicker.render({items: typeItems});
}
+ /* 已关闭且有交付物的阶段无法变更阶段类型。 */
+ $row.find('[data-name="attribute"]').find('.picker-box').on('inited', function(e, info)
+ {
+ if(typeof data != 'undefined' && typeof data.hasDeliverable != 'undefined') info[0].render({disabled: true});
+ });
+
if(project.model == 'ipd')
{
if(planID == 0 && level == 0)
diff --git a/module/programplan/model.php b/module/programplan/model.php
index 25f0a28b56..56bd6ba96a 100755
--- a/module/programplan/model.php
+++ b/module/programplan/model.php
@@ -611,9 +611,9 @@ class programplanModel extends model
* @param string $action
* @param bool $isParent
* @access public
- * @return bool
+ * @return bool|array
*/
- public function computeProgress(int $stageID, string $action = '', bool $isParent = false): bool
+ public function computeProgress(int $stageID, string $action = '', bool $isParent = false): bool|array
{
$stage = $this->loadModel('execution')->fetchByID($stageID);
if(empty($stage) || empty($stage->path)) return false;
@@ -647,6 +647,17 @@ class programplanModel extends model
$newParent = $result['newParent'] ?? null;
$parentAction = $result['parentAction'] ?? '';
+ /* 如果当前是顶级阶段,并且由于交付物不能关闭,则跳转到顶级阶段的关闭页面。 */
+ if(isset($newParent->status) && $newParent->status == 'closed')
+ {
+ $isTopStage = $parent->grade == 1 && $parent->type != 'project' && $stageID != $id && $parent->status == 'doing';
+ if(in_array($this->config->edition, array('max', 'ipd')) && $isTopStage && !$this->execution->canCloseByDeliverable($parent))
+ {
+ $url = helper::createLink('execution', 'close', "executionID={$parent->id}");
+ return array('result' => 'fail', 'callback' => "zui.Modal.confirm('{$this->lang->execution->cannotAutoCloseParent}').then((res) => {if(res) {loadModal('$url', '.modal-dialog');} else {loadPage();}});");
+ }
+ }
+
/** 更新状态以及记录日志。 */
/** Update status and save log. */
if(isset($newParent) && $newParent)
diff --git a/module/programplan/tao.php b/module/programplan/tao.php
index aee10c40f6..42ae52ffbc 100644
--- a/module/programplan/tao.php
+++ b/module/programplan/tao.php
@@ -276,10 +276,12 @@ class programplanTao extends programplanModel
$taskTeams = $this->dao->select('task,account')->from(TABLE_TASKTEAM)->where('task')->in(array_keys($tasks))->fetchGroup('task', 'account');
$users = $this->loadModel('user')->getPairs('noletter');
+ $firstTask = reset($tasks);
+ $taskDateLimit = $this->dao->select('taskDateLimit')->from(TABLE_PROJECT)->where('id')->eq($firstTask->project)->fetch('taskDateLimit');
foreach($tasks as $task)
{
$plan = zget($plans, $task->execution, null);
- $dateLimit = $this->getTaskDateLimit($task, $plan);
+ $dateLimit = $this->getTaskDateLimit($task, $plan, $taskDateLimit == 'limit' ? zget($tasks, $task->parent, null) : null);
$data = $this->buildTaskDataForGantt($task, $dateLimit);
$data->id = $task->execution . '-' . $task->id;
$data->parent = $task->parent > 0 && isset($tasks[$task->parent]) ? $task->execution . '-' . $task->parent : $task->execution;
@@ -871,16 +873,23 @@ class programplanTao extends programplanModel
*
* @param object $task
* @param object|null $execution
+ * @param object|null $parent
* @access protected
* @return array
*/
- protected function getTaskDateLimit(object $task, object|null $execution = null): array
+ protected function getTaskDateLimit(object $task, object|null $execution = null, object|null $parent = null): array
{
$estStart = helper::isZeroDate($task->estStarted) ? '' : $task->estStarted;
$estEnd = helper::isZeroDate($task->deadline) ? '' : $task->deadline;
$realBegan = helper::isZeroDate($task->realStarted) ? '' : $task->realStarted;
$realEnd = (in_array($task->status, array('done', 'closed')) and !helper::isZeroDate($task->finishedDate)) ? $task->finishedDate : '';
+ if($parent)
+ {
+ if(empty($estStart) && !helper::isZeroDate($parent->estStarted)) $estStart = $parent->estStarted;
+ if(empty($estEnd) && !helper::isZeroDate($parent->deadline)) $estEnd = $parent->deadline;
+ }
+
$start = $estStart;
$end = $estEnd;
if(empty($start) and $execution) $start = $execution->begin;
diff --git a/module/programplan/ui/edit.html.php b/module/programplan/ui/edit.html.php
index 195582d4fa..aef5381878 100644
--- a/module/programplan/ui/edit.html.php
+++ b/module/programplan/ui/edit.html.php
@@ -45,6 +45,7 @@ formPanel
set::name('parent'),
set::items($parentStageList),
set::value($plan->parent),
+ set::disabled($hasUploadedDeliverable),
set::required(true),
on::change('changeParentStage')
)
@@ -95,7 +96,7 @@ formPanel
(
setID('attributeType'),
setClass('flex self-center w-full'),
- $enableOptionalAttr ? picker
+ $enableOptionalAttr && empty($plan->hasDeliverable) ? picker
(
setID('attribute'),
set::name('attribute'),
diff --git a/module/programplan/zen.php b/module/programplan/zen.php
index 9f0f4ace2d..4380e3e6f9 100644
--- a/module/programplan/zen.php
+++ b/module/programplan/zen.php
@@ -297,7 +297,7 @@ class programplanZen extends programplan
* @access protected
* @return void
*/
- protected function buildEditView(object $plan)
+ public function buildEditView(object $plan)
{
$this->loadModel('project');
$this->loadModel('execution');
@@ -305,17 +305,19 @@ class programplanZen extends programplan
$parentStage = $this->project->getByID($plan->parent, 'stage');
- $this->view->title = $this->lang->programplan->edit;
- $this->view->isCreateTask = $this->programplan->isCreateTask($plan->id);
- $this->view->plan = $plan;
- $this->view->project = $this->project->getByID($plan->project);
- $this->view->parentStageList = $this->programplan->getParentStageList($plan->project, $plan->id, $plan->product);
- $this->view->enableOptionalAttr = empty($parentStage) || (!empty($parentStage) && $parentStage->attribute == 'mix');
- $this->view->isTopStage = $this->programplan->isTopStage($plan->id);
- $this->view->isLeafStage = $this->programplan->checkLeafStage($plan->id);
- $this->view->PMUsers = $this->loadModel('user')->getPairs('noclosed|nodeleted|pmfirst', $plan->PM);
- $this->view->project = $this->project->getByID($plan->project);
- $this->view->requiredFields = $this->config->execution->edit->requiredFields;
+ $this->view->title = $this->lang->programplan->edit;
+ $this->view->isCreateTask = $this->programplan->isCreateTask($plan->id);
+ $this->view->plan = $plan;
+ $this->view->project = $this->project->getByID($plan->project);
+ $this->view->parentStageList = $this->programplan->getParentStageList($plan->project, $plan->id, $plan->product);
+ $this->view->enableOptionalAttr = empty($parentStage) || (!empty($parentStage) && $parentStage->attribute == 'mix');
+ $this->view->isTopStage = $this->programplan->isTopStage($plan->id);
+ $this->view->isLeafStage = $this->programplan->checkLeafStage($plan->id);
+ $this->view->PMUsers = $this->loadModel('user')->getPairs('noclosed|nodeleted|pmfirst', $plan->PM);
+ $this->view->project = $this->project->getByID($plan->project);
+ $this->view->requiredFields = $this->config->execution->edit->requiredFields;
+ $this->view->hasUploadedDeliverable = in_array($this->config->edition, array('max', 'ipd')) ? $this->execution->hasUploadedDeliverable($plan) : false;
+
$this->display();
}
diff --git a/module/project/config/dtable.php b/module/project/config/dtable.php
index 97aff15b75..044546eae1 100755
--- a/module/project/config/dtable.php
+++ b/module/project/config/dtable.php
@@ -97,6 +97,17 @@ $config->project->dtable->fieldList['invested']['group'] = 5;
$config->project->dtable->fieldList['invested']['show'] = true;
$config->project->dtable->fieldList['invested']['sortType'] = false;
+if(helper::hasFeature('deliverable') && in_array($config->edition, array('max', 'ipd')))
+{
+ $config->project->dtable->fieldList['deliverable']['title'] = $lang->project->deliverableAbbr;
+ $config->project->dtable->fieldList['deliverable']['name'] = 'deliverable';
+ $config->project->dtable->fieldList['deliverable']['type'] = 'html';
+ $config->project->dtable->fieldList['deliverable']['width'] = '120px';
+ $config->project->dtable->fieldList['deliverable']['group'] = 5;
+ $config->project->dtable->fieldList['deliverable']['show'] = true;
+ $config->project->dtable->fieldList['deliverable']['sortType'] = false;
+}
+
$config->project->dtable->fieldList['begin']['title'] = $lang->project->begin;
$config->project->dtable->fieldList['begin']['name'] = 'begin';
$config->project->dtable->fieldList['begin']['type'] = 'date';
@@ -188,6 +199,17 @@ $config->project->execution->dtable->fieldList['status']['width'] = '80';
$config->project->execution->dtable->fieldList['status']['group'] = '1';
$config->project->execution->dtable->fieldList['status']['show'] = true;
+if(helper::hasFeature('deliverable') && in_array($config->edition, array('max', 'ipd')))
+{
+ $config->project->execution->dtable->fieldList['deliverable']['title'] = $lang->project->deliverableAbbr;
+ $config->project->execution->dtable->fieldList['deliverable']['name'] = 'deliverable';
+ $config->project->execution->dtable->fieldList['deliverable']['type'] = 'html';
+ $config->project->execution->dtable->fieldList['deliverable']['width'] = '100px';
+ $config->project->execution->dtable->fieldList['deliverable']['group'] = '1';
+ $config->project->execution->dtable->fieldList['deliverable']['show'] = true;
+ $config->project->execution->dtable->fieldList['deliverable']['sortType'] = false;
+}
+
$config->project->execution->dtable->fieldList['PM']['title'] = $lang->project->PM;
$config->project->execution->dtable->fieldList['PM']['name'] = 'PM';
$config->project->execution->dtable->fieldList['PM']['type'] = 'avatarBtn';
diff --git a/module/project/config/form.php b/module/project/config/form.php
index 143cfdf679..56bcdab02a 100755
--- a/module/project/config/form.php
+++ b/module/project/config/form.php
@@ -36,6 +36,7 @@ $config->project->form->edit = $config->project->form->create;
$config->project->form->edit['products'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->project->form->edit['branch'] = array('type' => 'array', 'required' => false, 'default' => array());
$config->project->form->edit['plans'] = array('type' => 'array', 'required' => false, 'default' => array());
+$config->project->form->edit['taskDateLimit'] = array('type' => 'string', 'required' => false, 'default' => array());
unset($config->project->form->edit['hasProduct']);
unset($config->project->form->edit['stageBy']);
unset($config->project->form->edit['multiple']);
diff --git a/module/project/tao.php b/module/project/tao.php
index b941368228..3ba79f722f 100755
--- a/module/project/tao.php
+++ b/module/project/tao.php
@@ -1050,6 +1050,7 @@ class projectTao extends projectModel
include $this->app->getModulePath('', 'execution') . 'lang/' . $this->app->getClientLang() . '.php';
+ if($projectModel == 'ipd') unset($lang->waterfall->menu->other['dropMenu']->deliverable);
$lang->execution->typeList['sprint'] = $executionCommonLang;
}
elseif($projectModel == 'kanban')
diff --git a/module/project/test/model/addplans.php b/module/project/test/model/addplans.php
index 7cab569203..5f9e6c3418 100755
--- a/module/project/test/model/addplans.php
+++ b/module/project/test/model/addplans.php
@@ -8,7 +8,9 @@ zenData('product')->gen(100);
zenData('productplan')->gen(100);
$storyTable = zenData('story');
$storyTable->status->range('active');
+$storyTable->version->range('1');
$storyTable->gen(100);
+zenData('storyspec')->gen(100);
zenData('planstory')->gen(100);
zenData('projectstory')->gen(0);
@@ -20,6 +22,15 @@ title=测试 projectModel::addPlans();
timeout=0
cid=1
+- 将计划1,4,7下的需求关联到项目13,查看关联后的需求数 @16
+- 将计划1,4,7下的需求关联到项目13,查看关联后的需求ID/产品ID
+ - 第3条的story属性 @4
+ - 第3条的product属性 @1
+- 将计划2,5,10,13下的需求关联到项目11,查看关联后的需求数 @8
+- 将计划2,5,10,13下的需求关联到项目11,查看关联后的需求ID/产品ID
+ - 第0条的story属性 @13
+ - 第0条的product属性 @4
+
*/
$project = new Project();
@@ -32,4 +43,4 @@ r($project->addPlansTest(13, $plan)) && p('3:story,product') && e('4,1'); // 将
$plan = array();
$plan[1] = array(2, 5, 10, 13);
r(count($project->addPlansTest(11, $plan))) && p('') && e('8'); // 将计划2,5,10,13下的需求关联到项目11,查看关联后的需求数
-r($project->addPlansTest(11, $plan)) && p('0:story,product') && e('13,4'); // 将计划2,5,10,13下的需求关联到项目11,查看关联后的需求ID/产品ID
+r($project->addPlansTest(11, $plan)) && p('0:story,product') && e('13,4'); // 将计划2,5,10,13下的需求关联到项目11,查看关联后的需求ID/产品ID
\ No newline at end of file
diff --git a/module/project/test/model/batchupdate.php b/module/project/test/model/batchupdate.php
index 91824daaf1..4ad7533a3a 100755
--- a/module/project/test/model/batchupdate.php
+++ b/module/project/test/model/batchupdate.php
@@ -29,6 +29,27 @@ title=测试taskModel->batchUpdate();
timeout=0
cid=1
+- 查看被编辑了的项目数量 @3
+- 查看被编辑了的项目11详情
+ - 第1条的name属性 @批量修改项目11
+ - 第1条的parent属性 @1
+ - 第1条的PM属性 @user10
+ - 第1条的begin属性 @2022-02-08
+ - 第1条的acl属性 @open
+- 查看被编辑了的项目12详情
+ - 第2条的name属性 @批量修改项目12
+ - 第2条的parent属性 @2
+ - 第2条的PM属性 @user11
+ - 第2条的begin属性 @2022-03-05
+ - 第2条的acl属性 @private
+- 查看被编辑了的项目13详情
+ - 第3条的name属性 @批量修改项目13
+ - 第3条的parent属性 @3
+ - 第3条的PM属性 @user13
+ - 第3条的begin属性 @2022-02-19
+ - 第3条的acl属性 @program
+- 异常情况第message[end]条的0属性 @ID4『计划完成』应当大于『2023-02-19』。
+
*/
$project = new Project();
@@ -40,7 +61,7 @@ $data[1]->parent = 1;
$data[1]->PM = 'user10';
$data[1]->begin = '2022-02-08';
$data[1]->end = '2022-04-13';
-$data[1]->day = 10;
+$data[1]->days = 10;
$data[1]->acl = 'open';
$data[2] = new stdClass();
@@ -49,7 +70,7 @@ $data[2]->parent = 2;
$data[2]->PM = 'user11';
$data[2]->begin = '2022-03-05';
$data[2]->end = '2022-04-13';
-$data[2]->day = 10;
+$data[2]->days = 10;
$data[2]->acl = 'private';
$data[3] = new stdClass();
@@ -58,7 +79,7 @@ $data[3]->parent = 3;
$data[3]->PM = 'user13';
$data[3]->begin = '2022-02-19';
$data[3]->end = '2022-04-13';
-$data[3]->day = 14;
+$data[3]->days = 14;
$data[3]->acl = 'program';
$projects = $project->batchUpdate($data);
@@ -79,4 +100,4 @@ $data[4]->day = 14;
$data[4]->acl = 'program';
$projects = $project->batchUpdate($data);
-r($projects) && p('message[end]:0') && e('ID4『计划完成』应当大于『2023-02-19』。'); // 异常情况
+r($projects) && p('message[end]:0') && e('ID4『计划完成』应当大于『2023-02-19』。'); // 异常情况
\ No newline at end of file
diff --git a/module/project/test/model/getdisabledproducts.php b/module/project/test/model/getdisabledproducts.php
index 2fd5c5cb4a..aa45dc5eb8 100644
--- a/module/project/test/model/getdisabledproducts.php
+++ b/module/project/test/model/getdisabledproducts.php
@@ -9,6 +9,13 @@ title=测试 projectModel->getDisabledProducts();
timeout=0
cid=1
+- 测试获取敏捷项目不可修改产品 @0
+- 测试获取以产品创建的瀑布项目不可修改产品属性2 @项目已经关联了该产品中的研发需求,不能取消关联,您可以取消关联研发需求后再操作。
+- 测试获取以产品创建的瀑布项目不可修改产品属性3 @该产品已经创建了阶段,如需解除与项目的关联,请删除已创建的阶段后再操作。
+- 测试获取以产品创建的瀑布项目不可修改产品属性4 @该产品已经创建了阶段并关联了研发需求,如需解除与项目的关联,请先解除研发需求的关联关系,然后删除已创建的阶段后再操作。
+- 测试获取以项目创建的瀑布项目不可修改产品属性5 @该产品的研发需求已经关联到了项目和执行中,请先解除研发需求与项目和执行的关联后再操作。
+- 测试获取以项目创建的瀑布项目不可修改产品属性6 @项目已经关联了该产品中的研发需求,不能取消关联,您可以取消关联研发需求后再操作。
+
*/
zenData('project')->loadYaml('project')->gen(10);
@@ -24,4 +31,4 @@ r($projectTester->getDisabledProductsTest($projectIdList[1])) && p('2') && e('
r($projectTester->getDisabledProductsTest($projectIdList[2])) && p('3') && e('该产品已经创建了阶段,如需解除与项目的关联,请删除已创建的阶段后再操作。'); // 测试获取以产品创建的瀑布项目不可修改产品
r($projectTester->getDisabledProductsTest($projectIdList[3])) && p('4') && e('该产品已经创建了阶段并关联了研发需求,如需解除与项目的关联,请先解除研发需求的关联关系,然后删除已创建的阶段后再操作。'); // 测试获取以产品创建的瀑布项目不可修改产品
r($projectTester->getDisabledProductsTest($projectIdList[4])) && p('5') && e('该产品的研发需求已经关联到了项目和执行中,请先解除研发需求与项目和执行的关联后再操作。'); // 测试获取以项目创建的瀑布项目不可修改产品
-r($projectTester->getDisabledProductsTest($projectIdList[5])) && p('6') && e('项目已经关联了该产品中的研发需求,不能取消关联,您可以取消关联研发需求后再操作。'); // 测试获取以项目创建的瀑布项目不可修改产品
+r($projectTester->getDisabledProductsTest($projectIdList[5])) && p('6') && e('项目已经关联了该产品中的研发需求,不能取消关联,您可以取消关联研发需求后再操作。'); // 测试获取以项目创建的瀑布项目不可修改产品
\ No newline at end of file
diff --git a/module/project/test/model/yaml/getdisabledproducts/project.yaml b/module/project/test/model/yaml/getdisabledproducts/project.yaml
index 2cf2a1d34d..f1aaef5293 100644
--- a/module/project/test/model/yaml/getdisabledproducts/project.yaml
+++ b/module/project/test/model/yaml/getdisabledproducts/project.yaml
@@ -7,7 +7,7 @@ fields:
- field: type
range: project{6},stage{6}
- field: stageBy
- range: "product{4},project{2},[]{6}"
+ range: "product{4},project{2}"
- field: project
range: '0{6},3{2},4,5'
- field: model
diff --git a/module/project/test/tao/setmenubymodel.php b/module/project/test/tao/setmenubymodel.php
index 37648e686f..391d2b8b4d 100644
--- a/module/project/test/tao/setmenubymodel.php
+++ b/module/project/test/tao/setmenubymodel.php
@@ -9,8 +9,16 @@ su('admin');
/**
title=测试 projectModel::setMenuByModel();
+timeout=0
cid=1
-pid=1
+
+- 项目不存在的情况 @迭代
+- 项目不存在的情况 @迭代
+- 敏捷项目 @迭代
+- 融合敏捷项目 @迭代
+- 融合瀑布项目 @阶段
+- 瀑布项目 @阶段
+- 看板项目 @项目看板
*/
@@ -22,4 +30,4 @@ r($projectTester->setMenuByModelTest('scrum')) && p() && e('迭代');
r($projectTester->setMenuByModelTest('agileplus')) && p() && e('迭代'); // 融合敏捷项目
r($projectTester->setMenuByModelTest('waterfall')) && p() && e('阶段'); // 融合瀑布项目
r($projectTester->setMenuByModelTest('waterfallplus')) && p() && e('阶段'); // 瀑布项目
-r($projectTester->setMenuByModelTest('kanban')) && p() && e('项目看板'); // 看板项目
+r($projectTester->setMenuByModelTest('kanban')) && p() && e('项目看板'); // 看板项目
\ No newline at end of file
diff --git a/module/project/ui/browsebycard.html.php b/module/project/ui/browsebycard.html.php
index 67456d5ae4..e2b28d4efa 100644
--- a/module/project/ui/browsebycard.html.php
+++ b/module/project/ui/browsebycard.html.php
@@ -44,7 +44,7 @@ featureBar
set::text($lang->project->mine),
set::checked($this->cookie->involved ? 'checked' : '')
),
- li(searchToggle(set::module('project')))
+ li(searchToggle(set::module('project'), set::open($browseType == 'bysearch')))
);
/* zin: Define the toolbar on main menu. */
diff --git a/module/project/ui/browsebylist.html.php b/module/project/ui/browsebylist.html.php
index f2ea2480d0..5d64b2314e 100644
--- a/module/project/ui/browsebylist.html.php
+++ b/module/project/ui/browsebylist.html.php
@@ -25,7 +25,7 @@ featureBar
set::text($lang->project->mine),
set::checked($this->cookie->involved ? 'checked' : '')
),
- li(searchToggle(set::module('project')))
+ li(searchToggle(set::module('project'), set::open($browseType == 'bysearch')))
);
/* zin: Define the toolbar on main menu. */
diff --git a/module/project/ui/create.field.php b/module/project/ui/create.field.php
index 64f776a22a..3e761971fe 100644
--- a/module/project/ui/create.field.php
+++ b/module/project/ui/create.field.php
@@ -60,6 +60,8 @@ $fields->field('acl')
$fields->field('auth')->foldable()->value($copyProject ? data('copyProject.auth') : 'extend');
+$fields->field('taskDateLimit')->hidden(true)->value('auto');
+
$storyType = in_array($model, array('waterfall', 'waterfallplus', 'ipd')) ? 'story,requirement' : 'story';
if($copyProject) $storyType = data('copyProject.storyType');
$fields->field('taskDateLimit')->width('full')->foldable()->value('auto');
diff --git a/module/project/zen.php b/module/project/zen.php
index ec118dcc37..dc54beeaa7 100755
--- a/module/project/zen.php
+++ b/module/project/zen.php
@@ -484,6 +484,12 @@ class projectZen extends project
$this->view->programID = $programID;
$this->view->disableParent = $disableParent;
$this->view->groups = $this->loadModel('group')->getPairs();
+
+ if(in_array($this->config->edition, array('max', 'ipd')) && $this->project->checkUploadedDeliverable($project))
+ {
+ $this->view->disableModel = true;
+ }
+
$this->display();
}
@@ -1393,6 +1399,12 @@ class projectZen extends project
$project->estimate = helper::formatHours($project->estimate);
$project->consume = helper::formatHours($project->consume);
$project->left = helper::formatHours($project->left);
+
+ /* 交付物提交进度。 */
+ if(in_array($this->config->edition, array('max', 'ipd')))
+ {
+ $project->deliverable = $this->project->countDeliverable($project);
+ }
}
return array_values($projectList);
diff --git a/module/story/ui/edit.html.php b/module/story/ui/edit.html.php
index ccf24c8f39..09f33d0351 100644
--- a/module/story/ui/edit.html.php
+++ b/module/story/ui/edit.html.php
@@ -197,6 +197,7 @@ detailBody
),
section
(
+ setID('comment'),
set::title($lang->story->comment),
formGroup(editor(set::name('comment'), set::uid($uid)))
)
diff --git a/module/task/js/batchedit.ui.js b/module/task/js/batchedit.ui.js
index 8c08ad6d95..c4539c74f4 100644
--- a/module/task/js/batchedit.ui.js
+++ b/module/task/js/batchedit.ui.js
@@ -196,6 +196,7 @@ function checkBatchEstStartedAndDeadline(event)
$estStartedTd.find('.date-tip').remove();
const $childrenEstStarted = $(event.target).closest('tbody').find('tr[data-parent="' + taskID + '"]').find('[name^=estStarted]');
+
$childrenEstStarted.each(function()
{
let $childDatePicker = $(this).zui('datePicker');
@@ -204,6 +205,7 @@ function checkBatchEstStartedAndDeadline(event)
checkBatchEstStartedAndDeadline({target: this});
});
+
if(estStarted.length > 0)
{
let $datetip = $('');
diff --git a/module/transfer/ui/import.html.php b/module/transfer/ui/import.html.php
index cbf556678e..5a29070e0f 100644
--- a/module/transfer/ui/import.html.php
+++ b/module/transfer/ui/import.html.php
@@ -22,6 +22,6 @@ formPanel
set::items($typeList)
) : null,
input(set::type('file'), set::name('file')),
- span(setClass('label secondary'), $lang->transfer->importNotice)
+ span(setClass('label secondary h-auto'), $lang->transfer->importNotice)
);
h::js('$.cookie.set("maxImport", 0, {expires:config.cookieLife, path:config.webRoot});');
diff --git a/module/tutorial/model.php b/module/tutorial/model.php
index 0bacf79939..acb2d40b54 100644
--- a/module/tutorial/model.php
+++ b/module/tutorial/model.php
@@ -186,6 +186,7 @@ class tutorialModel extends model
$project->charter = 0;
$project->market = 1;
$project->budgetUnit = 'CNY';
+ $project->deliverable = '';
list($guide, $guideTask, $guideStepIndex) = empty($_SERVER['HTTP_X_ZIN_TUTORIAL']) ? array('', '', '') : explode('-', $_SERVER['HTTP_X_ZIN_TUTORIAL']);
if($guide && strpos($guide, 'scrumProjectManage') !== false)
@@ -306,6 +307,7 @@ class tutorialModel extends model
$execution->type = 'sprint';
$execution->projectName = '';
$execution->projectModel = 'scrum';
+ $execution->deliverable = '';
if($browseType && $browseType != 'all') $execution->name .= '-' . $browseType;
diff --git a/module/upgrade/config.php b/module/upgrade/config.php
index e0021b04e8..172c5af02d 100644
--- a/module/upgrade/config.php
+++ b/module/upgrade/config.php
@@ -1476,6 +1476,13 @@ $config->delete['21_6_beta'][] = 'extension/ipd/common/ext/lang/en/effort.php';
$config->delete['21_6_beta'][] = 'extension/ipd/common/ext/lang/fr/effort.php';
$config->delete['21_6_beta'][] = 'extension/ipd/common/ext/lang/zh-cn/effort.php';
+$config->delete['21_6_beta'][] = 'extension/ipd/project/ext/model/class/zentaoipd.class.php';
+$config->delete['21_6_beta'][] = 'extension/max/project/ext/model/class/zentaoipd.class.php';
+$config->delete['21_6_beta'][] = 'extension/biz/project/ext/model/class/zentaoipd.class.php';
+$config->delete['21_6_beta'][] = 'extension/ipd/project/ext/model/zentaoipd.php';
+$config->delete['21_6_beta'][] = 'extension/max/project/ext/model/zentaoipd.php';
+$config->delete['21_6_beta'][] = 'extension/biz/project/ext/model/zentaoipd.php';
+
$config->upgrade->openModules = array('action', 'admin', 'ai', 'bi', 'aiapp', 'api', 'automation', 'backup', 'block', 'branch', 'budget', 'bug', 'build', 'cache', 'caselib', 'chart', 'ci', 'client', 'common', 'company', 'compile', 'convert', 'cron', 'custom', 'datatable', 'dataview', 'dept', 'design', 'dev', 'dimension', 'doc', 'durationestimation', 'entry', 'execution', 'extension', 'file', 'git', 'gitlab', 'group', 'holiday', 'im', 'index', 'index.html', 'install', 'issue', 'jenkins', 'job', 'kanban', 'license', 'mail', 'message', 'metric', 'misc', 'mr', 'my', 'personnel', 'pipeline', 'product', 'productplan', 'productset', 'program', 'programplan', 'project', 'projectbuild', 'projectplan', 'projectrelease', 'projectstory', 'pivot', 'qa', 'release', 'repo', 'report', 'risk', 'score', 'screen', 'search', 'setting', 'sonarqube', 'sso', 'stage', 'stakeholder', 'story', 'subject', 'svn', 'task', 'testcase', 'testreport', 'testsuite', 'testtask', 'todo', 'tree', 'tutorial', 'upgrade', 'user', 'webhook', 'weekly', 'workestimation', 'gitea', 'gogs', 'transfer', 'zahost', 'zanode', 'editor', 'charter', 'roadmap', 'account', 'cne', 'host', 'instance', 'ops', 'serverroom', 'space', 'store', 'system', 'solution', 'demand', 'gitfox', 'epic', 'requirement', 'mark');
$config->upgrade->unsetModules = array('design', 'program', 'programplan', 'projectbuild', 'projectrelease', 'stage', 'stakeholder', 'product', 'branch', 'productplan', 'release', 'build', 'qa', 'bug', 'testcase', 'testtask', 'testreport', 'testsuite', 'caselib', 'automation', 'repo', 'ci', 'compile', 'jenkins', 'job', 'svn', 'gitlab', 'sonarqube', 'mr', 'git', 'report', 'sqlbuilder', 'feedback', 'faq', 'attend', 'holiday', 'leave', 'makeup', 'overtime', 'lieu', 'ops', 'host', 'serverroom', 'account', 'domain', 'service', 'deploy', 'conference', 'traincourse', 'pssp', 'baseline', 'classify', 'cm', 'cmcl', 'auditcl', 'reviewcl', 'process', 'activity', 'zoutput', 'auditplan', 'nc', 'subject', 'weekly', 'workestimation', 'issue', 'durationestimation', 'risk', 'opportunity', 'trainplan', 'gapanalysis', 'researchplan', 'researchreport', 'meeting', 'meetingroom', 'budget', 'reviewissue', 'reviewsetting', 'review', 'milestone', 'measurement', 'measrecord', 'assetlib', 'setting', 'im', 'client', 'ldap', 'dev', 'api', 'gitea', 'gogs', 'zanode', 'zahost');
diff --git a/module/user/model.php b/module/user/model.php
index 9893929b71..5b8161c6fc 100644
--- a/module/user/model.php
+++ b/module/user/model.php
@@ -1298,6 +1298,7 @@ class userModel extends model
*/
public function getProjects(string $account, string $status = 'all', string $orderBy = 'id_desc', object $pager = null): array
{
+ $this->loadModel('project');
$projects = $this->userTao->fetchProjects($account, $status, $orderBy, $pager);
if(!$projects) return array();
@@ -1317,6 +1318,8 @@ class userModel extends model
$project->storyPoints = $projectStory ? round($projectStory->estimate, 1) : 0;
$project->storyCount = $projectStory ? $projectStory->count : 0;
$project->executionCount = zget($projectExecutionCount, $project->id, 0);
+
+ if(in_array($this->config->edition, array('max', 'ipd'))) $project->deliverable = $this->project->countDeliverable($project);
}
return $projects;
diff --git a/test/data/deliverable.yaml b/test/data/deliverable.yaml
new file mode 100644
index 0000000000..ee40d120cc
--- /dev/null
+++ b/test/data/deliverable.yaml
@@ -0,0 +1,34 @@
+title: table zt_deliverable
+desc: "交付物"
+author: Sun Guangming
+version: "1.0"
+fields:
+ - field: id
+ note: "ID"
+ range: 1-1000
+ - field: name
+ fields:
+ - field: name1
+ range: '交付物'
+ - field: name2
+ range: 1-1000
+ - field: module
+ range: project,execution
+ - field: method
+ range: create,close
+ - field: model
+ fields:
+ - field: model1
+ range: product,project
+ postfix: "_"
+ - field: model2
+ range: waterfall,scrum
+ postfix: "_"
+ - field: model3
+ range: mix,request,design,dev,qa,release,review
+ - field: type
+ range: doc,file
+ - field: files
+ range: 1-10
+ - field: desc
+ range: '交付物描述'
\ No newline at end of file