1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35:
36:
37:
38:
39: 40: 41: 42:
43: class Workflow extends CActiveRecord {
44:
45: const DEFAULT_ALL_MODULES = '-1';
46:
47: 48: 49: 50:
51: public static function model($className=__CLASS__) { return parent::model($className); }
52:
53: 54: 55:
56: public function tableName() { return 'x2_workflows'; }
57:
58: private static $_workflowOptions;
59: private $_stageNameAutoCompleteSource;
60:
61: public function behaviors() {
62: return array_merge(parent::behaviors(),array(
63: 'X2LinkableBehavior'=>array(
64: 'class'=>'X2LinkableBehavior',
65: 'module'=>'workflow'
66: ),
67: 'JSONFieldsDefaultValuesBehavior' => array(
68: 'class' => 'application.components.JSONFieldsDefaultValuesBehavior',
69: 'transformAttributes' => array(
70: 'colors' => array(
71: 'first'=>'c4f455',
72: 'last'=>'f18c1c',
73: ),
74: ),
75: 'maintainCurrentFieldsOrder' => true
76: ),
77: ));
78: }
79:
80: 81: 82:
83: public function rules() {
84:
85:
86: return array(
87: array('name', 'required'),
88: array('lastUpdated', 'numerical', 'integerOnly'=>true),
89: array('name, financialModel, financialField', 'length', 'max'=>250),
90: array('isDefault, financial', 'boolean'),
91: array('isDefaultFor', 'validateIsDefaultFor'),
92:
93:
94: array('id, name, lastUpdated, financial, financialModel, financialField', 'safe', 'on'=>'search'),
95: );
96: }
97:
98: 99: 100:
101: public function relations() {
102:
103:
104: return array(
105: 'stages'=>array(self::HAS_MANY, 'WorkflowStage', 'workflowId', 'order'=>'stageNumber ASC'),
106: );
107: }
108:
109: 110: 111:
112:
113:
114:
115:
116: 117: 118:
119: public function validateIsDefaultFor ($attr) {
120: $val = $this->$attr;
121: if (is_array ($val)) {
122: $moduleIds = Yii::app()->db->createCommand ()
123: ->select ('id')
124: ->from ('x2_modules')
125: ->queryColumn ();
126: if (array_diff ($val, array_merge (array (self::DEFAULT_ALL_MODULES), $moduleIds))) {
127: $this->addError ($attr, Yii::t('workflow', 'Invalid module'));
128: }
129: }
130: }
131:
132: 133: 134:
135: public function attributeLabels() {
136: return array(
137: 'id' => 'ID',
138: 'name' => Yii::t('workflow','Process Name'),
139: 'isDefault' => Yii::t('workflow','Default Process'),
140: 'isDefaultFor' => Yii::t('workflow','Default Process'),
141: 'lastUpdated' => Yii::t('workflow','Last Updated'),
142: 'financial' => Yii::t('workflow','Show Financial Data'),
143: 'financialModel' => Yii::t('workflow','Financial Data Model'),
144: 'financialField' => Yii::t('workflow','Financial Data Field'),
145: );
146: }
147:
148: private $_isDefaultFor;
149: public function setIsDefaultFor ($isDefaultFor) {
150: if (!is_array ($isDefaultFor)) $isDefaultFor = array ();
151: $this->_isDefaultFor = $isDefaultFor;
152: if (in_array (self::DEFAULT_ALL_MODULES, $this->_isDefaultFor)) {
153: $this->isDefault = true;
154: $this->_isDefaultFor = array (self::DEFAULT_ALL_MODULES);
155: } else {
156: $this->isDefault = false;
157: }
158: }
159:
160: public function getIsDefaultFor () {
161: if (!isset ($this->_isDefaultFor)) {
162: if ($this->isDefault) {
163: $this->_isDefaultFor = array (self::DEFAULT_ALL_MODULES);
164: } else {
165: $this->_isDefaultFor = Yii::app()->db->createCommand ("
166: select id
167: from x2_modules
168: where defaultWorkflow=:id
169: ")->queryColumn (array (':id' => $this->id));
170: }
171: }
172: return $this->_isDefaultFor;
173: }
174:
175: public function renderAttribute ($attr) {
176: switch ($attr) {
177: case 'isDefaultFor':
178: $isDefaultFor = $this->getIsDefaultFor ();
179: if (in_array (self::DEFAULT_ALL_MODULES, $isDefaultFor)) {
180: return Yii::t('workflow', 'All modules');
181: } elseif ($isDefaultFor) {
182: $qpg = new QueryParamGenerator;
183: $moduleNames = Yii::app()->db->createCommand ()
184: ->select ('name')
185: ->from ('x2_modules')
186: ->where ('id in '.$qpg->bindArray ($isDefaultFor, true))
187: ->queryColumn ($qpg->getParams ());
188: return implode (', ', ArrayUtil::asorti (array_map (function ($name) {
189: return Modules::displayName (true, $name);
190: }, $moduleNames)));
191: }
192: break;
193: default:
194: return $this->$attr;
195: }
196: }
197:
198: 199: 200:
201: public function afterSave() {
202: if (in_array (self::DEFAULT_ALL_MODULES, $this->isDefaultFor)) {
203:
204: Yii::app()->db->createCommand("
205: update x2_modules
206: set defaultWorkflow=NULL
207: where true
208: ")->execute (array (':id' => $this->id));
209:
210: Yii::app()->db->createCommand("
211: update x2_workflows
212: set isDefault=0
213: where id!=:id
214: ")->execute (array (':id' => $this->id));
215: } else {
216:
217:
218:
219: if ($this->isDefaultFor) {
220: $qpg = new QueryParamGenerator;
221: Yii::app()->db->createCommand("
222: update x2_modules
223: set defaultWorkflow=:id
224: where id in ".$qpg->bindArray ($this->isDefaultFor, true)."
225: ")->execute ($qpg->mergeParams (array (':id' => $this->id))->getParams ());
226: }
227:
228:
229: if ($this->isDefaultFor) {
230: $qpg = new QueryParamGenerator;
231: Yii::app()->db->createCommand("
232: update x2_modules
233: set defaultWorkflow=NULL
234: where id not in ".$qpg->bindArray ($this->isDefaultFor, true)." and
235: defaultWorkflow=:id
236: ")->execute ($qpg->mergeParams (array (':id' => $this->id))->getParams ());
237: } else {
238: Yii::app()->db->createCommand("
239: update x2_modules
240: set defaultWorkflow=NULL
241: where defaultWorkflow=:id
242: ")->execute (array (':id' => $this->id));
243: }
244:
245:
246: Yii::app()->db->createCommand("
247: update x2_workflows
248: set isDefault=0
249: where true
250: ")->execute ();
251: }
252:
253: parent::afterSave();
254: }
255:
256: 257: 258:
259: public static function getList($enableNone=true) {
260: $workflows = X2Model::model('Workflow')->findAll();
261: $list = array();
262: if($enableNone)
263: $list[0] = Yii::t('app','None');
264: foreach ($workflows as $model)
265: $list[$model->id] = $model->name;
266: return $list;
267: }
268:
269: public static function getWorkflowOptions () {
270: if (!isset (self::$_workflowOptions)) {
271: self::$_workflowOptions = self::getList (false);
272: }
273: return self::$_workflowOptions;
274: }
275:
276: 277: 278: 279: 280:
281: private static function canUncomplete ($workflowStatus, $stage) {
282: 283:
284: return Yii::app()->params->isAdmin ||
285: Yii::app()->settings->workflowBackdateWindow < 0 ||
286: $workflowStatus['stages'][$stage]['completeDate'] == 0 ||
287: (time() - $workflowStatus['stages'][$stage]['completeDate']) <
288: Yii::app()->settings->workflowBackdateWindow;
289: }
290:
291: public static function getStageUncompletionPermissions ($workflowStatus) {
292: $uncompletionPermissions = array ();
293: $stageCount = sizeof ($workflowStatus['stages']);
294: for ($stageNum = 1; $stageNum <= $stageCount; $stageNum++) {
295: $uncompletionPermissions[] = self::canUncomplete ($workflowStatus, $stageNum);
296: }
297: return $uncompletionPermissions;
298: }
299:
300: 301: 302: 303: 304: 305: 306:
307: private static function checkPermissions ($stageA, $stageB=null, $workflowStatus) {
308: $stagePermissions = Workflow::getStagePermissions ($workflowStatus);
309:
310: $hasPermission = true;
311: if ($stageB === null) {
312: return $stagePermissions[$stageA - 1];
313: }
314:
315: $stageRange = array ($stageA, $stageB);
316: sort ($stageRange);
317:
318: $hasPermission = array_reduce (array_slice (
319: $stagePermissions, $stageRange[0] - 1, ($stageRange[1] - $stageRange[0]) + 1),
320: function ($a, $b) { return $a & $b; }, true);
321:
322: return $hasPermission;
323: }
324:
325: 326: 327: 328: 329: 330: 331: 332:
333: private static function (
334: $stageA, $stageB=null, $workflowStatus, $comments) {
335:
336: $stagesWhichRequireComments = Workflow::getStageCommentRequirements ($workflowStatus);
337: $commentRequirementsMet = true;
338:
339: if ($stageB === null) {
340: return !$stagesWhichRequireComments[$stageA - 1] ||
341: (isset ($comments[$stageA]) && !empty ($comments[$stageA]));
342: }
343:
344: for ($i = $stageA - 1; $i < $stageB - 1; ++$i) {
345: $commentRequirementsMet &=
346: !$stagesWhichRequireComments[$i] ||
347: (isset ($comments[$i + 1]) && !empty ($comments[$i + 1]));
348: }
349:
350: return $commentRequirementsMet;
351: }
352:
353: 354: 355: 356:
357: private static function checkAllStageRequirements ($stageA, $stageB=null, $workflowStatus) {
358: $stageRequirementsMet = true;
359:
360:
361: if ($stageB === null) {
362: return self::checkStageRequirement ($stageA, $workflowStatus);
363: }
364:
365: $tmpWorfklowStatus = $workflowStatus;
366:
367: for ($i = $stageA; $i < $stageB; ++$i) {
368:
369: $stageRequirementsMet &=
370: self::checkStageRequirement ($i, $tmpWorfklowStatus);
371:
372: if ($stageRequirementsMet) {
373:
374: $tmpWorfklowStatus['stages'][$i]['complete'] = true;
375: } else {
376: break;
377: }
378: }
379:
380:
381:
382:
383: return $stageRequirementsMet;
384: }
385:
386:
387: 388: 389: 390: 391: 392:
393: private static function checkStageRequirement ($stageNumber, $workflowStatus) {
394: $requirementMet = true;
395:
396:
397:
398:
399: if($workflowStatus['stages'][$stageNumber]['requirePrevious'] ==
400: WorkflowStage::REQUIRE_ALL) {
401:
402: for($i=1; $i<$stageNumber; $i++) {
403: if(empty($workflowStatus['stages'][$i]['complete'])) {
404: $requirementMet = false;
405: break;
406: }
407: }
408: } else if($workflowStatus['stages'][$stageNumber]['requirePrevious'] < 0) {
409:
410:
411: if(empty($workflowStatus['stages'][ -1*$workflowStatus['stages'][$stageNumber]
412: ['requirePrevious'] ]['complete'])) {
413:
414: $requirementMet = false;
415: }
416: }
417: return $requirementMet;
418: }
419:
420: 421: 422: 423: 424: 425:
426: private static function checkAllBackdateWindows ($stageA, $stageB=null, $workflowStatus) {
427: if (Yii::app()->params->isAdmin) return true;
428:
429:
430: if ($stageB === null) $stageB = $stageA + 1;
431:
432: $noBackdateWindowViolations = true;
433: $stageRange = array ($stageA, $stageB);
434: sort ($stageRange);
435:
436:
437:
438:
439: for ($i = $stageRange[0]; $i < $stageRange[1]; ++$i) {
440:
441: $noBackdateWindowViolations &=
442: !(isset ($workflowStatus['stages'][$i]['complete']) &&
443: $workflowStatus['stages'][$i]['complete']) ||
444: self::canUncomplete ($workflowStatus, $i);
445: if (!$noBackdateWindowViolations) break;
446: }
447: return $noBackdateWindowViolations;
448: }
449:
450: 451: 452: 453: 454:
455: public static function isStarted ($workflowStatus, $stageNumber) {
456: return (self::isCompleted ($workflowStatus, $stageNumber) ||
457: $workflowStatus['stages'][$stageNumber]['createDate']);
458: }
459:
460: 461: 462: 463: 464:
465: public static function isCompleted ($workflowStatus, $stageNumber) {
466: return $workflowStatus['stages'][$stageNumber]['complete'];
467: }
468:
469: public static function isInProgress ($workflowStatus, $stageNumber) {
470: return self::isStarted ($workflowStatus, $stageNumber) &&
471: !self::isCompleted ($workflowStatus, $stageNumber);
472:
473: }
474:
475: 476: 477: 478: 479: 480:
481: public static function validateAction (
482: $action, $workflowStatus, $stage, $comment='', &$message='') {
483:
484: assert (in_array ($action, array ('complete', 'start', 'revert')));
485:
486: if (!isset ($workflowStatus['stages'][$stage])) {
487: $message = Yii::t(
488: 'workflow', 'Stage {stage} does not exist',
489: array ('{stage}' => $stage));
490: return false;
491: }
492:
493:
494: switch ($action) {
495: case 'complete':
496: if (self::isCompleted ($workflowStatus, $stage)) {
497: $message = Yii::t(
498: 'workflow', 'Stage {stage} has already been completed',
499: array ('{stage}' => $stage));
500: return false;
501: }
502: break;
503: case 'start':
504: if (self::isStarted ($workflowStatus, $stage)) {
505: $message = Yii::t(
506: 'workflow', 'Stage {stage} has already been started',
507: array ('{stage}' => $stage));
508: return false;
509: }
510: break;
511: case 'revert':
512: if (!self::isStarted ($workflowStatus, $stage)) {
513: $message = Yii::t(
514: 'workflow', 'Stage {stage} has not been started.',
515: array ('{stage}' => $stage));
516: return false;
517: }
518: break;
519: }
520:
521:
522: if (!self::checkPermissions (
523: $stage, null, $workflowStatus)) {
524:
525: $message = Yii::t('workflow', 'You do not have permission to perform that action.');
526: return false;
527: }
528: if ($action === 'complete' || $action === 'start') {
529: if (!self::checkStageRequirement ($stage, $workflowStatus)) {
530: $message = Yii::t('workflow', 'Stage requirements were not met.');
531: return false;
532: }
533: }
534: if ($action === 'complete') {
535: if (!self::checkCommentRequirements (
536: $stage, null, $workflowStatus, array ($stage => $comment))) {
537:
538: $message = Yii::t('workflow', 'Stage required a comment but was given none.');
539: return false;
540: }
541: } else if ($action === 'revert') {
542: if (!self::checkAllBackdateWindows ($stage, null, $workflowStatus)) {
543: $message = Yii::t('workflow', 'Stage could not be reverted because its '.
544: 'backdate window has expired.');
545: return false;
546: }
547: }
548: return true;
549: }
550:
551: 552: 553: 554: 555: 556: 557: 558: 559: 560: 561: 562:
563: private static function validateStageChange (
564: $workflowId, $stageA, $stageB, $modelId, $modelType, $comments=array()) {
565:
566: $workflowStatus = Workflow::getWorkflowStatus ($workflowId, $modelId, $modelType);
567:
568: $errors = array ();
569:
570:
571:
572:
573: if (!self::isInProgress ($workflowStatus, $stageA)) {
574: return array (
575: false,
576: Yii::t('workflow',
577: 'Stage change failed. This could be because your interface is displaying '.
578: 'out-of-date information. Please try refreshing the page.'));
579: }
580:
581: if ($stageA < $stageB) {
582: if (!self::checkAllStageRequirements ($stageA, $stageB, $workflowStatus)) {
583: return array (false, Yii::t('workflow', 'Stage requirements were not met.'));
584: } else if (!self::checkCommentRequirements (
585: $stageA, $stageB, $workflowStatus, $comments)) {
586:
587:
588: return array (false,
589: Yii::t('workflow', 'A stage required a comment but was given none.'));
590: }
591: } else {
592: if (!self::checkAllStageRequirements ($stageB, null, $workflowStatus)) {
593:
594:
595: return array (false, Yii::t('workflow', 'Stage requirements were not met.'));
596: } else if (!self::checkAllBackdateWindows ($stageA - 1, $stageB, $workflowStatus)) {
597:
598:
599:
600: return array (false,
601: Yii::t('workflow', 'At least one stage could not be reverted because its '.
602: 'backdate window has expired.'));
603: }
604: }
605:
606: if (!self::checkPermissions (
607: $stageA, $stageB, $workflowStatus)) {
608:
609: return array (false,
610: Yii::t('workflow', 'You do not have permission to perform that stage change.'));
611: }
612:
613: return array (true);
614: }
615:
616: 617: 618: 619: 620: 621: 622: 623: 624: 625: 626:
627: public static function moveFromStageAToStageB (
628: $workflowId, $stageA, $stageB, $model, $comments=array()) {
629:
630: if ($stageA === $stageB && YII_DEBUG) {
631: throw new CException ('Precondition violation: $stageA === $stageB');
632: }
633: $modelId = $model->id;
634: $type = lcfirst (X2Model::getModuleName (get_class ($model)));
635:
636: $retVal = self::validateStageChange (
637: $workflowId, $stageA, $stageB, $modelId, $type, $comments);
638:
639: if (!$retVal[0]) {
640: return $retVal;
641: }
642:
643:
644: if ($stageA < $stageB) {
645:
646: list ($success, $status) = Workflow::completeStage (
647: $workflowId, $stageA, $model,
648: isset ($comments[$stageA]) ? $comments[$stageA] : '', false);
649: for ($i = $stageA + 1; $i < $stageB; ++$i) {
650:
651: list ($success, $status) =
652: Workflow::startStage ($workflowId, $i, $model, $status);
653: list ($success, $status) = Workflow::completeStage (
654: $workflowId, $i, $model,
655: isset ($comments[$i]) ? $comments[$i] : '', false, $status);
656: }
657: list ($success, $status) =
658: Workflow::startStage ($workflowId, $stageB, $model, $status);
659:
660: list ($success, $status) =
661: Workflow::revertStage ($workflowId, $stageB, $model, false, $status);
662: } else {
663:
664: list ($success, $status) =
665: Workflow::revertStage ($workflowId, $stageA, $model);
666: for ($i = $stageA - 1; $i > $stageB; --$i) {
667:
668: list ($success, $status) =
669: Workflow::revertStage ($workflowId, $i, $model, $status);
670: list ($success, $status) =
671: Workflow::revertStage ($workflowId, $i, $model, $status);
672: }
673:
674: list ($success, $status) =
675: Workflow::revertStage ($workflowId, $stageB, $model, false, $status);
676: list ($success, $status) =
677: Workflow::startStage ($workflowId, $stageB, $model, false, $status);
678: }
679:
680: return array (true);
681: }
682:
683: 684: 685: 686: 687: 688: 689: 690: 691:
692: public static function getWorkflowStatus($workflowId,$modelId=0,$modelType='') {
693:
694: $workflowStatus = array(
695: 'id'=>$workflowId,
696: 'stages'=>array(),
697: 'started'=>false,
698: 'completed'=>true
699: );
700:
701: $workflow = Workflow::model()->findByPk($workflowId);
702: if($workflow){
703: $workflowStatus['financial'] = $workflow->financial;
704: $workflowStatus['financialModel'] = $workflow->financialModel;
705: $workflowStatus['financialField'] = $workflow->financialField;
706: }
707:
708: $workflowStages = X2Model::model('WorkflowStage')
709: ->findAllByAttributes(
710: array('workflowId'=>$workflowId),
711: new CDbCriteria(array('order'=>'id ASC')));
712:
713:
714: foreach($workflowStages as &$stage) {
715: $workflowStatus['stages'][$stage->stageNumber] = array(
716: 'id'=>$stage->id,
717: 'name'=>$stage->name,
718: 'requirePrevious'=>$stage->requirePrevious,
719: 'roles'=>$stage->roles,
720: 'complete' => false,
721: 'createDate' => null,
722: 'completeDate' => null,
723: 'requireComment'=>$stage->requireComment,
724: );
725: }
726: unset($stage);
727:
728: $workflowActions = array();
729:
730:
731: if($modelId !== 0) {
732: $workflowActions = X2Model::model('Actions')->findAllByAttributes(
733: array(
734: 'associationId'=>$modelId,
735: 'associationType'=>$modelType,
736: 'type'=>'workflow',
737: 'workflowId'=>$workflowId
738: ),
739: new CDbCriteria(array('order'=>'createDate ASC'))
740: );
741: }
742:
743: foreach($workflowActions as &$action) {
744:
745:
746: $workflowStatus['started'] = true;
747: if(isset($action->workflowStage)){
748: $stage = $action->workflowStage->stageNumber;
749:
750:
751:
752: $workflowStatus['stages'][$stage]['createDate'] = $action->createDate;
753: $workflowStatus['stages'][$stage]['completeDate'] = $action->completeDate;
754:
755: 756:
757: $workflowStatus['stages'][$stage]['complete'] =
758: ($action->complete == 'Yes') ||
759: (!empty($action->completeDate) && $action->completeDate < time());
760:
761: $workflowStatus['stages'][$stage]['description'] = $action->actionDescription;
762: }
763: }
764:
765:
766: foreach($workflowStatus['stages'] as &$stage) {
767: if(!isset($stage['completeDate'])) {
768: $workflowStatus['completed'] = false;
769: break;
770: }
771: }
772: return $workflowStatus;
773: }
774:
775: 776: 777: 778:
779: public static function getStages($id) {
780: return Yii::app()->db->createCommand()
781: ->select('name')
782: ->from('x2_workflow_stages')
783: ->where('workflowId=:id',array(':id'=>$id))
784: ->order('stageNumber ASC')
785: ->queryColumn();
786: }
787:
788: 789: 790:
791: public static function getStagesByNumber ($id) {
792: $stages = Yii::app()->db->createCommand()
793: ->select('name,stageNumber')
794: ->from('x2_workflow_stages')
795: ->where('workflowId=:id',array(':id'=>$id))
796: ->order('stageNumber ASC')
797: ->queryAll();
798:
799: $stageNamesIndexedByNumber = array ();
800: for ($i = 0; $i < sizeof ($stages); $i++) {
801: $stageNamesIndexedByNumber[$stages[$i]['stageNumber']] = $stages[$i]['name'];
802: }
803: return $stageNamesIndexedByNumber;
804: }
805:
806: 807: 808:
809: public function getStageName ($stageNumber) {
810: $stageName = Yii::app()->db->createCommand()
811: ->select('name')
812: ->from('x2_workflow_stages')
813: ->where('workflowId=:id AND stageNumber=:stageNumber',
814: array(
815: ':id'=>$this->id,
816: ':stageNumber'=>$stageNumber,
817: ))
818: ->queryScalar();
819: return $stageName;
820: }
821:
822: 823: 824: 825:
826: public static function getStageNames ($workflowStatus) {
827: $stageCount = count($workflowStatus['stages']);
828:
829: $stageNames = array ();
830: for($stage=1; $stage<=$stageCount;$stage++) {
831: $stageNames[] = $workflowStatus['stages'][$stage]['name'];
832: }
833:
834: return $stageNames;
835: }
836:
837: 838: 839: 840: 841:
842: public static function ($workflowStatus) {
843: $stageCount = count($workflowStatus['stages']);
844:
845: $commentRequirements = array ();
846:
847: for($stage=1; $stage<=$stageCount;$stage++) {
848: $commentRequirements[] = $workflowStatus['stages'][$stage]['requireComment'];
849: }
850:
851: return $commentRequirements;
852: }
853:
854: 855: 856: 857: 858:
859: public static function getStagePermissions ($workflowStatus) {
860: $stageCount = count($workflowStatus['stages']);
861:
862: $editPermissions = array ();
863:
864: for($stage=1; $stage<=$stageCount;$stage++) {
865:
866:
867: if(!empty($workflowStatus['stages'][$stage]['roles'])) {
868: $editPermissions[] = count(array_intersect(
869: Yii::app()->params->roles,$workflowStatus['stages'][$stage]['roles'])) > 0;
870: } else {
871: $editPermissions[] = true;
872: }
873:
874: if(Yii::app()->params->isAdmin)
875: $editPermissions[$stage - 1] = true;
876:
877: }
878:
879: return $editPermissions;
880: }
881:
882: 883: 884: 885: 886:
887: public function getWorkflowStageColors ($stageCount, $getShaded=false) {
888:
889: $color1 = $this->colors['first'];
890: $color2 = $this->colors['last'];
891:
892: $startingRgb = X2Color::hex2rgb2($color1);
893: $endingRgb = X2Color::hex2rgb2($color2);
894:
895: $rgbDifference = array(
896: $endingRgb[0] - $startingRgb[0],
897: $endingRgb[1] - $startingRgb[1],
898: $endingRgb[2] - $startingRgb[2],
899: );
900:
901: if ($stageCount === 1) {
902: $rgbSteps = array (0, 0, 0);
903: } else {
904: $steps = $stageCount - 1;
905:
906: $rgbSteps = array(
907: $rgbDifference[0] / $steps,
908: $rgbDifference[1] / $steps,
909: $rgbDifference[2] / $steps,
910: );
911: }
912:
913: $colors = array ();
914: for($i=0; $i<$stageCount;$i++) {
915: $colors[] = X2Color::rgb2hex2(
916: $startingRgb[0] + ($rgbSteps[0]*$i),
917: $startingRgb[1] + ($rgbSteps[1]*$i),
918: $startingRgb[2] + ($rgbSteps[2]*$i)
919: );
920: if ($getShaded) {
921: $colors[$i] = array ($colors[$i]);
922: $colors[$i][] = X2Color::rgb2hex2 (array_map (function ($a) {
923: return $a > 255 ? 255 : $a;
924: }, array (
925: 0.93 * ($startingRgb[0] + ($rgbSteps[0]*$i)),
926: 0.93 * ($startingRgb[1] + ($rgbSteps[1]*$i)),
927: 0.93 * ($startingRgb[2] + ($rgbSteps[2]*$i))
928: )));
929: }
930: }
931:
932: return $colors;
933: }
934:
935:
936: 937: 938: 939: 940: 941: 942: 943:
944: public static function getStageCounts (
945: &$workflowStatus, $dateRange, $users='', $modelType='contacts') {
946:
947: $stageCount = count($workflowStatus['stages']);
948:
949: if ($users !== '') {
950: $userString = " AND x2_actions.assignedTo='$users' ";
951: } else {
952: $userString = "";
953: }
954:
955: $params = array (
956: ':start' => $dateRange['start'],
957: ':end' => $dateRange['end'],
958: ':workflowId' => $workflowStatus['id'],
959: );
960:
961: $stageCounts = array ();
962: $modelName = X2Model::getModelName ($modelType);
963: if(!$modelName){
964: $modelType = 'contacts';
965: $modelName = 'Contacts';
966: }
967: $model = X2Model::model($modelName);
968: $tableName = $model->tableName();
969: list ($accessCondition, $accessConditionParams) =
970: $modelName::model ()->getAccessSQLCondition ($tableName);
971:
972: $allParams = array_merge ($params, $accessConditionParams);
973: $recordsAtStages = Yii::app()->db->createCommand()
974: ->select("stageNumber, COUNT(*)")
975: ->from($tableName)
976: ->join(
977: 'x2_actions',
978: 'x2_actions.associationId='.$tableName.'.id')
979: ->where(
980: "x2_actions.complete != 'Yes' $userString AND
981: (x2_actions.completeDate IS NULL OR x2_actions.completeDate = 0) AND
982: x2_actions.createDate BETWEEN :start AND :end AND
983: x2_actions.type='workflow' AND workflowId=:workflowId AND
984: associationType='".$modelType."' AND ".$accessCondition,
985: $allParams)
986: ->group('stageNumber')
987: ->queryAll();
988: foreach($recordsAtStages as $row){
989: $stage = WorkflowStage::model()->findByPk($row['stageNumber']);
990: if($stage){
991: $stageCounts[$stage->stageNumber - 1] = $row['COUNT(*)'];
992: }
993: }
994: for($i = 0; $i < $stageCount; $i++){
995: if(!isset($stageCounts[$i])){
996: $stageCounts[$i] = 0;
997: }
998: }
999: ksort($stageCounts);
1000: return $stageCounts;
1001: }
1002:
1003: public static function getStageValues(
1004: &$workflowStatus, $dateRange, $users = '', $modelType = 'contacts') {
1005: $stageValues = array();
1006: $stageCount = count($workflowStatus['stages']);
1007: if ($workflowStatus['financial'] && $modelType === $workflowStatus['financialModel']
1008: && !empty($workflowStatus['financialField'])) {
1009: $financialField = Fields::model()->findByAttributes(array(
1010: 'modelName' => X2Model::getModelName($workflowStatus['financialModel']),
1011: 'fieldName' => $workflowStatus['financialField']
1012: ));
1013: if($financialField){
1014: $params = array(
1015: ':start' => $dateRange['start'],
1016: ':end' => $dateRange['end'],
1017: ':workflowId' => $workflowStatus['id'],
1018: );
1019: if (!empty($users)) {
1020: $userString = " AND x2_actions.assignedTo=:user ";
1021: $params[':user'] = $users;
1022: } else {
1023: $userString = "";
1024: }
1025: $modelName = X2Model::getModelName($modelType);
1026: $model = X2Model::model($modelName);
1027: $tableName = $model->tableName();
1028: list ($accessCondition, $accessConditionParams) = $modelName::model()->getAccessSQLCondition($tableName);
1029:
1030: $allParams = array_merge($params, $accessConditionParams);
1031: $vals = Yii::app()->db->createCommand()
1032: ->select('stageNumber, SUM(' . $workflowStatus['financialField'] . ') as total')
1033: ->from($tableName)
1034: ->join(
1035: 'x2_actions',
1036: 'x2_actions.associationId='.$tableName.'.id')
1037: ->where(
1038: "x2_actions.complete != 'Yes' $userString AND
1039: (x2_actions.completeDate IS NULL OR x2_actions.completeDate = 0) AND
1040: x2_actions.createDate BETWEEN :start AND :end AND
1041: x2_actions.type='workflow' AND workflowId=:workflowId AND
1042: associationType='".$modelType."' AND ".$accessCondition,
1043: $allParams)
1044: ->group('stageNumber')
1045: ->queryAll();
1046: foreach($vals as $row){
1047: $stage = WorkflowStage::model()->findByPk($row['stageNumber']);
1048: if($stage){
1049: $stageValues[$stage->stageNumber - 1] = $row['total'];
1050: }
1051: }
1052: for($i = 0; $i < $stageCount; $i++){
1053: if(!isset($stageValues[$i])){
1054: $stageValues[$i] = 0;
1055: }
1056: }
1057: }
1058: }
1059: for($i = 0; $i < $stageCount; $i++){
1060: if(!isset($stageValues[$i])){
1061: $stageValues[$i] = null;
1062: }
1063: }
1064:
1065: ksort($stageValues);
1066: return $stageValues;
1067: }
1068:
1069: 1070: 1071: 1072: 1073:
1074: public static function getStageNameLinks (
1075: &$workflowStatus, $dateRange, $users) {
1076:
1077: $links = array ();
1078: $stageCount = count($workflowStatus['stages']);
1079:
1080: for($i=1; $i<=$stageCount;$i++) {
1081: $links[] = CHtml::link(
1082: $workflowStatus['stages'][$i]['name'],
1083: array(
1084: '/workflow/workflow/view',
1085: 'id'=>$workflowStatus['id'],
1086: 'stage'=>$i,
1087: 'start'=>Formatter::formatDate($dateRange['start']),
1088: 'end'=>Formatter::formatDate($dateRange['end']),
1089: 'range'=>$dateRange['range'],
1090: $users
1091: ),
1092: array(
1093: 'onclick'=>'x2.WorkflowViewManager.getStageMembers('.$i.'); return false;',
1094: 'title' => addslashes ($workflowStatus['stages'][$i]['name']),
1095: )
1096: );
1097: }
1098: return $links;
1099: }
1100:
1101: public function updateStages($newStages){
1102: $oldIds = array();
1103: foreach($this->stages as $stage){
1104: $oldIds[] = $stage->id;
1105: }
1106: $newIds = array();
1107: for ($i = 1; $i <= count($newStages); $i++) {
1108: if(isset($newStages[$i]['stageId'])){
1109: $newIds[$i] = $newStages[$i]['stageId'];
1110: }else{
1111: $newIds[$i] = '';
1112: }
1113: }
1114: $forDeletion = array_diff($oldIds, $newIds);
1115: $validStages = true;
1116: $returnStages = array();
1117: foreach($newIds as $number => $id){
1118: $stage = WorkflowStage::model()->findByPk($id);
1119: if(!$stage){
1120: $stage = new WorkflowStage;
1121: }
1122: $stage->workflowId = $this->id;
1123: $stage->stageNumber = $number;
1124: $stage->attributes = $newStages[$number];
1125: $stage->roles = $newStages[$number]['roles'];
1126: if(empty($stage->roles) || in_array('',$stage->roles)){
1127: $stage->roles = array();
1128: }
1129: $returnStages[] = $stage;
1130: if(!$stage->validate()){
1131: $validStages = false;
1132: }
1133: }
1134: return array($validStages, $returnStages, $forDeletion);
1135: }
1136:
1137: 1138: 1139: 1140:
1141: public function search() {
1142:
1143:
1144:
1145: $criteria=new CDbCriteria;
1146:
1147: $criteria->compare('id',$this->id);
1148: $criteria->compare('name',$this->name,true);
1149: $criteria->compare('isDefault',$this->isDefault,true);
1150: $criteria->compare('lastUpdated',$this->lastUpdated);
1151:
1152: return new CActiveDataProvider(get_class($this), array(
1153: 'criteria'=>$criteria,
1154: ));
1155: }
1156:
1157: 1158: 1159: 1160:
1161: public static function getStageRequirements ($workflowStatus) {
1162: $stageCount = count($workflowStatus['stages']);
1163:
1164: $stageRequirements = array ();
1165:
1166: for($stage=1; $stage<=$stageCount;$stage++) {
1167: $stageRequirements[] = $workflowStatus['stages'][$stage]['requirePrevious'];
1168: }
1169:
1170: return $stageRequirements;
1171: }
1172:
1173: 1174: 1175: 1176: 1177: 1178: 1179: 1180: 1181: 1182: 1183: 1184:
1185: public static function completeStage (
1186: $workflowId,$stageNumber,$model, $comment, $autoStart=true, $workflowStatus=null) {
1187:
1188: $comment = trim($comment);
1189:
1190: $modelId = $model->id;
1191: $type = lcfirst (X2Model::getModuleName (get_class ($model)));
1192:
1193: if (!$workflowStatus)
1194: $workflowStatus = Workflow::getWorkflowStatus($workflowId,$modelId,$type);
1195:
1196: $stageCount = count($workflowStatus['stages']);
1197:
1198: $stage = &$workflowStatus['stages'][$stageNumber];
1199:
1200: $completed = false;
1201:
1202:
1203:
1204: if($model !== null &&
1205: self::isStarted ($workflowStatus, $stageNumber) &&
1206: !self::isCompleted($workflowStatus, $stageNumber)) {
1207:
1208:
1209: if(self::checkStageRequirement ($stageNumber, $workflowStatus) &&
1210: (!$stage['requireComment'] || ($stage['requireComment'] && !empty($comment)))) {
1211:
1212: 1213: 1214:
1215:
1216:
1217: $action = X2Model::model('Actions')->findByAttributes(
1218: array(
1219: 'associationId'=>$modelId,'associationType'=>$type,'type'=>'workflow',
1220: 'workflowId'=>$workflowId,'stageNumber'=>$stage['id']
1221: )
1222: );
1223:
1224: $action->setScenario('workflow');
1225:
1226:
1227: $action->disableBehavior('changelog');
1228: $action->disableBehavior('TagBehavior');
1229: $action->completeDate = time();
1230: $action->dueDate=null;
1231: $action->complete = 'Yes';
1232: $action->completedBy = Yii::app()->user->getName();
1233: $action->actionDescription = $comment;
1234: $action->save();
1235:
1236: $model->updateLastActivity();
1237:
1238: self::updateWorkflowChangelog($action,'complete',$model);
1239:
1240: if ($autoStart) {
1241:
1242: 1243: 1244:
1245: for($i=1; $i<=$stageCount; $i++) {
1246:
1247: if($i != $stageNumber &&
1248: empty($workflowStatus['stages'][$i]['completeDate']) &&
1249: !empty($workflowStatus['stages'][$i]['createDate'])) {
1250:
1251: break;
1252: }
1253:
1254:
1255: if(empty($workflowStatus['stages'][$i]['createDate'])) {
1256: $nextAction = new Actions('workflow');
1257:
1258:
1259: $nextAction->disableBehavior('changelog');
1260: $nextAction->disableBehavior('TagBehavior');
1261: $nextAction->associationId = $modelId;
1262: $nextAction->associationType = $type;
1263: $nextAction->assignedTo = Yii::app()->user->getName();
1264: $nextAction->type = 'workflow';
1265: $nextAction->complete = 'No';
1266: $nextAction->visibility = 1;
1267: $nextAction->createDate = time();
1268: $nextAction->workflowId = $workflowId;
1269: $nextAction->stageNumber = $workflowStatus['stages'][$i]['id'];
1270:
1271: $nextAction->save();
1272:
1273: X2Flow::trigger('WorkflowStartStageTrigger',array(
1274: 'workflow'=>$nextAction->workflow,
1275: 'model'=>$model,
1276: 'workflowId'=>$nextAction->workflow->id,
1277: 'stageNumber'=>$i,
1278: ));
1279:
1280: self::updateWorkflowChangelog($nextAction,'start',$model);
1281:
1282:
1283:
1284:
1285: break;
1286: }
1287: }
1288:
1289: }
1290:
1291:
1292:
1293:
1294:
1295:
1296: $workflowStatus = Workflow::getWorkflowStatus($workflowId,$modelId,$type);
1297: $completed = true;
1298:
1299: X2Flow::trigger('WorkflowCompleteStageTrigger',array(
1300: 'workflow'=>$action->workflow,
1301: 'model'=>$model,
1302: 'workflowId'=>$action->workflow->id,
1303: 'stageNumber'=>$stageNumber,
1304: ));
1305:
1306:
1307: if($workflowStatus['completed'])
1308: X2Flow::trigger('WorkflowCompleteTrigger',array(
1309: 'workflow'=>$action->workflow,
1310: 'model'=>$model,
1311: 'workflowId'=>$action->workflow->id
1312: ));
1313:
1314: }
1315: }
1316:
1317:
1318: return array ($completed, $workflowStatus);
1319:
1320: }
1321:
1322: 1323: 1324: 1325: 1326: 1327:
1328: public static function startStage (
1329: $workflowId,$stageNumber,$model,$workflowStatus=null) {
1330:
1331:
1332: $modelId = $model->id;
1333: $type = lcfirst (X2Model::getModuleName (get_class ($model)));
1334:
1335: if (!$workflowStatus)
1336: $workflowStatus = Workflow::getWorkflowStatus($workflowId,$modelId,$type);
1337:
1338: $stage = $workflowStatus['stages'][$stageNumber];
1339:
1340:
1341:
1342: $started = false;
1343:
1344:
1345: if($model !== null &&
1346: self::checkStageRequirement ($stageNumber, $workflowStatus) &&
1347: !self::isStarted ($workflowStatus, $stageNumber)) {
1348:
1349: $action = new Actions('workflow');
1350:
1351:
1352: $action->disableBehavior('changelog');
1353: $action->disableBehavior('TagBehavior');
1354: $action->associationId = $modelId;
1355: $action->associationType = $type;
1356: $action->assignedTo = Yii::app()->user->getName();
1357: $action->updatedBy = Yii::app()->user->getName();
1358: $action->complete = 'No';
1359: $action->type = 'workflow';
1360: $action->visibility = 1;
1361: $action->createDate = time();
1362: $action->lastUpdated = time();
1363: $action->workflowId = (int)$workflowId;
1364: $action->stageNumber = (int)$stage['id'];
1365: $action->save();
1366:
1367: $model->updateLastActivity();
1368:
1369: X2Flow::trigger('WorkflowStartStageTrigger',array(
1370: 'workflow'=>$action->workflow,
1371: 'model'=>$model,
1372: 'workflowId'=>$action->workflow->id,
1373: 'stageNumber'=>$stageNumber,
1374: ));
1375:
1376: if(!$workflowStatus['started'])
1377: X2Flow::trigger('WorkflowStartTrigger',array(
1378: 'workflow'=>$action->workflow,
1379: 'model'=>$model,
1380: 'workflowId'=>$action->workflow->id,
1381: ));
1382:
1383: self::updateWorkflowChangelog($action,'start',$model);
1384: $workflowStatus = Workflow::getWorkflowStatus($workflowId,$modelId,$type);
1385: $started = true;
1386: }
1387:
1388:
1389: return array ($started, $workflowStatus);
1390: }
1391:
1392: 1393: 1394: 1395:
1396: public static function revertStage (
1397: $workflowId,$stageNumber,$model,$unstart=true,$workflowStatus=null) {
1398:
1399:
1400:
1401: $modelId = $model->id;
1402: $type = lcfirst (X2Model::getModuleName (get_class ($model)));
1403:
1404: if (!$workflowStatus)
1405: $workflowStatus = Workflow::getWorkflowStatus($workflowId,$modelId,$type);
1406:
1407: $stage = $workflowStatus['stages'][$stageNumber];
1408: $reverted = false;
1409:
1410:
1411: if($model !== null &&
1412: self::isStarted ($workflowStatus, $stageNumber)) {
1413:
1414: $action = X2Model::model('Actions')->findByAttributes(
1415: array(
1416: 'associationId'=>$modelId,'associationType'=>$type,'type'=>'workflow',
1417: 'workflowId'=>$workflowId,'stageNumber'=>$stage['id']
1418: )
1419: );
1420:
1421:
1422: if(self::isCompleted ($workflowStatus, $stageNumber) &&
1423: self::canUncomplete ($workflowStatus, $stageNumber)) {
1424:
1425:
1426: $action->setScenario('workflow');
1427:
1428:
1429: $action->disableBehavior('changelog');
1430: $action->disableBehavior('TagBehavior');
1431: $action->complete = 'No';
1432: $action->completeDate = null;
1433: $action->completedBy = '';
1434:
1435:
1436: $action->actionDescription = '';
1437: $action->save();
1438:
1439: self::updateWorkflowChangelog($action,'revert',$model);
1440:
1441: X2Flow::trigger('WorkflowRevertStageTrigger',array(
1442: 'workflow'=>$action->workflow,
1443: 'model'=>$model,
1444: 'workflowId'=>$action->workflow->id,
1445: 'stageNumber'=>$stageNumber,
1446: ));
1447:
1448:
1449:
1450:
1451:
1452:
1453:
1454: } else if ($unstart) {
1455:
1456:
1457: $subsequentActions = X2Model::model('Actions')->findAll(new CDbCriteria(
1458: array(
1459: 'condition' =>
1460: "associationId=$modelId AND associationType='$type' ".
1461: "AND type='workflow' AND workflowId=$workflowId ".
1462: "AND stageNumber >= {$stage['id']}"
1463: )
1464: ));
1465: foreach($subsequentActions as &$action) {
1466: self::updateWorkflowChangelog($action,'revert',$model);
1467: X2Flow::trigger('WorkflowRevertStageTrigger',array(
1468: 'workflow'=>$action->workflow,
1469: 'model'=>$model,
1470: 'workflowId'=>$action->workflow->id,
1471: 'stageNumber'=>$action->stageNumber,
1472: ));
1473: $action->delete();
1474: }
1475: }
1476: $workflowStatus = Workflow::getWorkflowStatus($workflowId,$modelId,$type);
1477: $reverted = true;
1478: }
1479:
1480: return array ($reverted, $workflowStatus);
1481: }
1482:
1483: public static function updateWorkflowChangelog(&$action,$changeType,&$model) {
1484: $changelog = new Changelog;
1485:
1486: $changelog->type = get_class($model);
1487: $changelog->itemId = $action->associationId;
1488:
1489:
1490:
1491:
1492:
1493:
1494: $changelog->recordName = $model->name;
1495: $changelog->changedBy = Yii::app()->user->getName();
1496: $changelog->timestamp = time();
1497: $changelog->oldValue = '';
1498:
1499: $workflowName = $action->workflow->name;
1500:
1501: $stageName = Yii::app()->db->createCommand()
1502: ->select('name')
1503: ->from('x2_workflow_stages')
1504: ->where(
1505: 'workflowId=:id AND stageNumber=:sn',
1506: array(
1507: ':sn'=>$action->stageNumber,
1508: ':id'=>$action->workflowId))
1509: ->queryScalar();
1510:
1511: $event = new Events;
1512: $event->associationType = 'Actions';
1513: $event->associationId = $action->id;
1514: $event->user = Yii::app()->user->getName();
1515:
1516: if($changeType === 'start') {
1517:
1518: $event->type = 'workflow_start';
1519: $changelog->newValue='Workflow Stage Started: '.$stageName;
1520:
1521: } elseif($changeType === 'complete') {
1522:
1523: $event->type = 'workflow_complete';
1524: $changelog->newValue = 'Workflow Stage Completed: '.$stageName;
1525:
1526: } elseif($changeType === 'revert') {
1527:
1528: $event->type = 'workflow_revert';
1529: $changelog->newValue = 'Workflow Stage Reverted: '.$stageName;
1530:
1531: } else {
1532: return;
1533: }
1534:
1535: 1536: 1537: 1538: 1539: 1540:
1541:
1542: $event->save();
1543: $changelog->save();
1544: }
1545:
1546: public function getStageNameAutoCompleteSource() {
1547: if (!isset ($this->_stageNameAutoCompleteSource)) {
1548: $this->_stageNameAutoCompleteSource = Yii::app()->controller->createUrl (
1549: '/workflow/workflow/getStageNameItems');
1550: }
1551: return $this->_stageNameAutoCompleteSource;
1552: }
1553:
1554:
1555: 1556: 1557: 1558:
1559: public static function getPipelineListItemColors ($colors) {
1560: $listItemColors = array ();
1561: for ($i = 1; $i <= count ($colors); ++$i) {
1562: list($r,$g,$b) = X2Color::hex2rgb2 ($colors[$i-1][0]);
1563: $listItemColors[$i - 1][] = "rgba($r, $g, $b, 0.20)";
1564: $listItemColors[$i - 1][] = "rgba($r, $g, $b, 0.12)";
1565: }
1566: return $listItemColors;
1567: }
1568:
1569: public function getDisplayName ($plural=true, $ofModule=true) {
1570: return Yii::t('workflow', '{process}', array(
1571: '{process}' => Modules::displayName($plural, 'Process'),
1572: ));
1573: }
1574:
1575: public static function getCurrencyFields($model='contacts'){
1576: $ret = array();
1577: if(X2Model::getModelName($model)){
1578: $financialFields = Fields::model()->findAllByAttributes(array(
1579: 'modelName'=>X2Model::getModelName($model),
1580: 'type' => 'currency'
1581: ));
1582: foreach($financialFields as $field){
1583: $ret[$field->fieldName] = $field->attributeLabel;
1584: }
1585: asort($ret);
1586: }
1587: return $ret;
1588: }
1589:
1590: }
1591: