Overview

Packages

  • application
    • commands
    • components
      • actions
      • filters
      • leftWidget
      • permissions
      • sortableWidget
      • util
      • webupdater
      • x2flow
        • actions
        • triggers
      • X2GridView
      • X2Settings
    • controllers
    • models
      • embedded
    • modules
      • accounts
        • controllers
        • models
      • actions
        • controllers
        • models
      • calendar
        • controllers
        • models
      • charts
        • models
      • contacts
        • controllers
        • models
      • docs
        • components
        • controllers
        • models
      • groups
        • controllers
        • models
      • marketing
        • components
        • controllers
        • models
      • media
        • controllers
        • models
      • mobile
        • components
      • opportunities
        • controllers
        • models
      • products
        • controllers
        • models
      • quotes
        • controllers
        • models
      • services
        • controllers
        • models
      • template
        • models
      • users
        • controllers
        • models
      • workflow
        • controllers
        • models
      • x2Leads
        • controllers
        • models
  • None
  • system
    • base
    • caching
    • console
    • db
      • ar
      • schema
    • validators
    • web
      • actions
      • auth
      • helpers
      • widgets
        • captcha
        • pagers
  • zii
    • widgets
      • grid

Classes

  • Workflow
  • WorkflowStage
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: /*****************************************************************************************
   3:  * X2Engine Open Source Edition is a customer relationship management program developed by
   4:  * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
   5:  * 
   6:  * This program is free software; you can redistribute it and/or modify it under
   7:  * the terms of the GNU Affero General Public License version 3 as published by the
   8:  * Free Software Foundation with the addition of the following permission added
   9:  * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
  10:  * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
  11:  * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
  12:  * 
  13:  * This program is distributed in the hope that it will be useful, but WITHOUT
  14:  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  15:  * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
  16:  * details.
  17:  * 
  18:  * You should have received a copy of the GNU Affero General Public License along with
  19:  * this program; if not, see http://www.gnu.org/licenses or write to the Free
  20:  * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  21:  * 02110-1301 USA.
  22:  * 
  23:  * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
  24:  * California 95067, USA. or at email address contact@x2engine.com.
  25:  * 
  26:  * The interactive user interfaces in modified source and object code versions
  27:  * of this program must display Appropriate Legal Notices, as required under
  28:  * Section 5 of the GNU Affero General Public License version 3.
  29:  * 
  30:  * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
  31:  * these Appropriate Legal Notices must retain the display of the "Powered by
  32:  * X2Engine" logo. If the display of the logo is not reasonably feasible for
  33:  * technical reasons, the Appropriate Legal Notices must display the words
  34:  * "Powered by X2Engine".
  35:  *****************************************************************************************/
  36: 
  37: // Yii::import('application.models.X2Model');
  38: 
  39: /**
  40:  * This is the model class for table "x2_workflows".
  41:  * @package application.modules.workflow.models
  42:  */
  43: class Workflow extends CActiveRecord {
  44: 
  45:     const DEFAULT_ALL_MODULES = '-1';
  46: 
  47:     /**
  48:      * Returns the static model of the specified AR class.
  49:      * @return Workflow the static model class
  50:      */
  51:     public static function model($className=__CLASS__) { return parent::model($className); }
  52: 
  53:     /**
  54:      * @return string the associated database table name
  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', // color of the first stage
  72:                         'last'=>'f18c1c', // color of the last stage
  73:                     ),
  74:                 ),
  75:                 'maintainCurrentFieldsOrder' => true
  76:             ),
  77:         ));
  78:     }
  79: 
  80:     /**
  81:      * @return array validation rules for model attributes.
  82:      */
  83:     public function rules() {
  84:         // NOTE: you should only define rules for those attributes that
  85:         // will receive user inputs.
  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:             // The following rule is used by search().
  93:             // Please remove those attributes that should not be searched.
  94:             array('id, name, lastUpdated, financial, financialModel, financialField', 'safe', 'on'=>'search'),
  95:         );
  96:     }
  97: 
  98:     /**
  99:      * @return array relational rules.
 100:      */
 101:     public function relations() {
 102:         // NOTE: you may need to adjust the relation name and the related
 103:         // class name for the relations automatically generated below.
 104:         return array(
 105:             'stages'=>array(self::HAS_MANY, 'WorkflowStage', 'workflowId', 'order'=>'stageNumber ASC'),
 106:         );
 107:     }
 108:     
 109:     /**
 110:      * @return array behaviors.
 111:      */
 112:     // public function behaviors(){
 113:         // return array('CSaveRelationsBehavior' => array('class' => 'application.components.CSaveRelationsBehavior'));
 114:     // }
 115: 
 116:     /**
 117:      * isDefault should either be a boolean value or an array of module ids 
 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:      * @return array customized attribute labels (name=>label)
 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:      * If this workflow is the default, unset isDefault flag on all other workflows
 200:      */
 201:     public function afterSave() {
 202:         if (in_array (self::DEFAULT_ALL_MODULES, $this->isDefaultFor)) {
 203:             // this workflow is default for all modules, so remove all defaults
 204:             Yii::app()->db->createCommand("
 205:                 update x2_modules
 206:                 set defaultWorkflow=NULL
 207:                 where true
 208:             ")->execute (array (':id' => $this->id));
 209:             // remove old global default
 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:             // set default on a per-module basis
 217: 
 218:             // add new values
 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:             // clear old values
 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:             // if there's a global default, remove it
 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:      * @return array workflow names indexed by id 
 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:      * @param array $workflowStatus
 278:      * @param int $stage
 279:      * @return bool true if stage can be uncompleted, false otherwise
 280:      */
 281:     private static function canUncomplete ($workflowStatus, $stage) {
 282:         /* can only uncomplete if there is no restriction on backdating, or we're 
 283:            still within the edit time window */
 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:      * This method is equivalent to the JS _checkPermissions method of DragAndDropViewManager
 302:      * @param int $stageA
 303:      * @param int $stageB
 304:      * @param array $workflowStatus
 305:      * @return bool true if user has permissions for all stages in range [$stageA, $stageB].
 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:      * @param int $stageA
 327:      * @param int $stageB
 328:      * @param array $workflowStatus
 329:      * @param array $comments Comments indexed by stage number
 330:      * @return bool true if comments array has a comment for each stage which requires a comment
 331:      *  in the range [$stageA, $stageB]
 332:      */
 333:     private static function checkCommentRequirements (
 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:      * @return bool true if stages in range [a, b) can be completed in order such that at each
 355:      *  stage completion, all stage requirements for that stage are met
 356:      */
 357:     private static function checkAllStageRequirements ($stageA, $stageB=null, $workflowStatus) {
 358:         $stageRequirementsMet = true;
 359:         //AuxLib::debugLogR ('checkAllStageRequirements: ' .$stageA.' '.$stageB);
 360: 
 361:         if ($stageB === null) {
 362:             return self::checkStageRequirement ($stageA, $workflowStatus); 
 363:         }
 364: 
 365:         $tmpWorfklowStatus = $workflowStatus; // clone array 
 366: 
 367:         for ($i = $stageA; $i < $stageB; ++$i) {
 368:             //AuxLib::debugLogR ('checking requirements for stage ' . $i);
 369:             $stageRequirementsMet &= 
 370:                 self::checkStageRequirement ($i, $tmpWorfklowStatus);
 371:             //AuxLib::debugLogR ((int) $stageRequirementsMet);
 372:             if ($stageRequirementsMet) {
 373:                 // mock stage completion since stages will be completed in order from a to b
 374:                 $tmpWorfklowStatus['stages'][$i]['complete'] = true;
 375:             } else {
 376:                 break;
 377:             }
 378:         }
 379: 
 380:        //AuxLib::debugLogR ('$requirementMet = ');
 381:        //AuxLib::debugLogR ($stageRequirementsMet);
 382: 
 383:         return $stageRequirementsMet;
 384:     }
 385: 
 386: 
 387:     /**
 388:      * Checks if all required stages are complete
 389:      * @param int $stageNumber
 390:      * @param object $workflowStatus
 391:      * @return bool true if stage dependencies are met, false otherwise
 392:      */
 393:     private static function checkStageRequirement ($stageNumber, $workflowStatus) {
 394:         $requirementMet = true;
 395:         //AuxLib::debugLogR ('checkStageRequirement');
 396:         //AuxLib::debugLogR ($workflowStatus['stages'][$stageNumber]['requirePrevious']);
 397: 
 398:         // check if all stages before this one are complete
 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:             // or just check if the specified stage is complete
 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:      * Used to determine if the user has permission to move record from stage a to b, subject to
 422:      * the backdate window restraint.
 423:      * @return bool true if user has permission to revert all stages which will be reverted in
 424:      *  the range [$stageA, $stageB], false otherwise
 425:      */
 426:     private static function checkAllBackdateWindows ($stageA, $stageB=null, $workflowStatus) {
 427:         if (Yii::app()->params->isAdmin) return true;
 428:         //AuxLib::debugLogR ('checkAllBackdateWindows');
 429: 
 430:         if ($stageB === null) $stageB = $stageA + 1;
 431: 
 432:         $noBackdateWindowViolations = true;
 433:         $stageRange = array ($stageA, $stageB);
 434:         sort ($stageRange);
 435: 
 436:         //AuxLib::debugLogR ($workflowStatus);
 437:         //AuxLib::debugLogR ($stageRange);
 438: 
 439:         for ($i = $stageRange[0]; $i < $stageRange[1]; ++$i) {
 440:             // valid if either stage will not be uncompleted or stage can be completed
 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:      * @param array $workflowStatus 
 452:      * @param int $stageNumber
 453:      * @return bool true if stage is started, false otherwise
 454:      */
 455:     public static function isStarted ($workflowStatus, $stageNumber) {
 456:         return (self::isCompleted ($workflowStatus, $stageNumber) ||
 457:             $workflowStatus['stages'][$stageNumber]['createDate']);
 458:     }
 459: 
 460:     /**
 461:      * @param array $workflowStatus 
 462:      * @param int $stageNumber
 463:      * @return bool true if stage is completed, false otherwise
 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:      * Validates a single workflow action. Like validateStageChange () except that only one
 477:      * stage change is validated.
 478:      * @param bool $strict If true, validation will fail in the case that the specified action
 479:      *  cannot be taken because it has already been taken before.
 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:         // ensure that the stage is in a valid state
 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:      * A helper method for moveFromStageAToStageB. Unlike validateAction, this method does
 553:      * not check whether or not intermediate stages are in valid states.
 554:      * Ensure that current user can move record from stage a to b with given comments. In
 555:      * addition to returning true/false, error flashes are added using X2Flashes.
 556:      * @param int $workflowId
 557:      * @param int $stageA Start stage (indexed by 1) 
 558:      * @param int $stageB End stage (indexed by 1) 
 559:      * @param array $comments comment strings indexed by workflow stage number
 560:      * @return bool true if the change from stage a to b is valid for the given workflow, false
 561:      *  otherwise
 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:         // ensure that the record is at the stage that the user thinks it is. It's possible that
 571:         // the date displayed in their interface has become out-of-date as the result of 
 572:         // users simultaenously updating workflow stages. 
 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:                 // comments only get added when stages are completed
 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:                 // only stage b is started, all other stages in range are reverted
 594: 
 595:                 return array (false, Yii::t('workflow', 'Stage requirements were not met.'));
 596:             } else if (!self::checkAllBackdateWindows ($stageA - 1, $stageB, $workflowStatus)) {
 597:                 // check backdate window of all but the first stage, since the first stage
 598:                 // never gets uncompleted 
 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:      * Moves a record up or down a workflow. Assumes that stageA is started but not completed.
 618:      * Intermediate stages and stageB can be in any state.
 619:      * @param int $workflowId
 620:      * @param int $stageA Start stage (indexed by 1) 
 621:      * @param int $stageB End stage (indexed by 1) 
 622:      * @param object $model model associated with workflow
 623:      * @param array $comments comment strings indexed by workflow stage number
 624:      * Precondition: $stageA !== $stageB
 625:      * @return array first element is success, the second is an optional message
 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:         // enact stage change
 644:         if ($stageA < $stageB) {
 645:             // complete first stage
 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:                 // start and complete intermediate stages
 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:             // uncomplete a completed final stage
 660:             list ($success, $status) = 
 661:                 Workflow::revertStage ($workflowId, $stageB, $model, false, $status);
 662:         } else { // $stageA > $stageB
 663:             // unstart first stage
 664:             list ($success, $status) = 
 665:                 Workflow::revertStage ($workflowId, $stageA, $model);
 666:             for ($i = $stageA - 1; $i > $stageB; --$i) {
 667:                 // uncomplete and unstart intermediate stages
 668:                 list ($success, $status) = 
 669:                     Workflow::revertStage ($workflowId, $i, $model, $status);
 670:                 list ($success, $status)  = 
 671:                     Workflow::revertStage ($workflowId, $i, $model, $status);
 672:             }
 673:             // uncomplete a completed final stage
 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:      * Retrieves information on all stages (their complete state, their stage dependencies,
 685:      * their stage permissions, and their comment requirements) and on the workflow itself
 686:      * (its complete and started state and its id)
 687:      * @param int $workflowId id of workflow
 688:      * @param int $modelId id of model to which the workflow is related (optional)
 689:      * @param mixed $modelType type of model to which the workflow is related. 
 690:      * @return array Contains information about the workflow and its stages
 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:         // load all WorkflowStage names into workflowStatus
 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; // clearly there's at least one stage up in here
 747:             if(isset($action->workflowStage)){
 748:                 $stage = $action->workflowStage->stageNumber;
 749: 
 750:                 // decode workflowActions into a funnel list
 751:                 // Note: multiple actions with the same stage will overwrite each other
 752:                 $workflowStatus['stages'][$stage]['createDate'] = $action->createDate;        
 753:                 $workflowStatus['stages'][$stage]['completeDate'] = $action->completeDate;
 754: 
 755:                  /* A stage is considered complete if either its complete attribute is true or if it 
 756:                  has a valid complete date. */
 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:         // now scan through and see if there are any incomplete stages
 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:      * @param int id workflow record id
 777:      * @return array all stage records associated with the workflow
 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:      * @return array names of stage records associated with the workflow indexed by stage number
 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:      * @return string Name of stage with given stage number 
 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:      * @param array return value of getWorkflowStatus 
 824:      * @return <array of strings> one for each stage
 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:      * @param array return value of getWorkflowStatus 
 839:      * @return <array of bools> One bool for each stage, true if the stage requires a comment,
 840:      *  false otherwise
 841:      */
 842:     public static function getStageCommentRequirements ($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:      * @param array return value of getWorkflowStatus 
 856:      * @return <array of bools> One bool for each stage, true if the current user has permission
 857:      *  for the stage, false otherwise
 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:             // if roles are specified, check if user has any of them
 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; // default is full permission for everybody
 872:             }
 873: 
 874:             if(Yii::app()->params->isAdmin)    // admin override
 875:                 $editPermissions[$stage - 1] = true;
 876:             
 877:         }
 878: 
 879:         return $editPermissions;
 880:     }
 881: 
 882:     /**
 883:       * get hex codes for each stage
 884:       * @param int $stageCount number of stages
 885:       * @return array array of hex codes, one for each stage
 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:             // 1 step for each stage other than the first
 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:      * Get number of records at each stage
 938:      * @param array $workflowStatus
 939:      * @param array $dateRange return value of WorkflowController::getDateRange ()
 940:      * @param string $users users filter
 941:      * @param string $modelType model type filter
 942:      * @return array number of records at each stage subject to specified filters
 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:      * Helper method for the workflow view.  
1071:      * @return array links for each of the workflow stages. When clicked, the details of a 
1072:      *  particular stage will be shown
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:      * Retrieves a list of models based on the current search/filter conditions.
1139:      * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions.
1140:      */
1141:     public function search() {
1142:         // Warning: Please modify the following code to remove attributes that
1143:         // should not be searched.
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:      * Returns stage requirements for each stage in the workflow
1159:      * @param array return value of getWorkflowStatus 
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:      * Completes a workflow stage 
1175:      * @param int $workflowId
1176:      * @param int $stageNumber
1177:      * @param object $model model associated with workflow
1178:      * @param string $comment comment to complete the stage with
1179:      * @param bool $autoStart if true, unless this action completes the workflow, an attempt will
1180:      *  be made to start the next unstarted stage in the case that no other stages have been
1181:      *  started
1182:      * @return array 
1183:      *  (<bool, true if the stage was completed and false otherwise>, <array, the workflow status>)
1184:      */
1185:     public static function completeStage (
1186:         $workflowId,$stageNumber,$model, $comment, $autoStart=true, $workflowStatus=null) {
1187:         //AuxLib::debugLogR ('completing stage '.$stageNumber.'with comment'.$comment);
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:         // if stage has been started but not completed. 
1203:         // TODO: verify the assumption that a set createDate indicates a started stage
1204:         if($model !== null && 
1205:             self::isStarted ($workflowStatus, $stageNumber) &&
1206:             !self::isCompleted($workflowStatus, $stageNumber)) {
1207:         
1208:             // is this stage OK to complete? if a comment is required, then is $comment empty?
1209:             if(self::checkStageRequirement ($stageNumber, $workflowStatus) && 
1210:                (!$stage['requireComment'] || ($stage['requireComment'] && !empty($comment)))) {
1211:             
1212:                 /*
1213:                 Find the action associated with the stage and complete it
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:                 // don't genererate normal action changelog/triggers/events
1227:                 $action->disableBehavior('changelog');    
1228:                 $action->disableBehavior('TagBehavior'); // no tags
1229:                 $action->completeDate = time(); // set completeDate and save model
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:                    Find the first stage which hasn't been started and start it
1244:                    */
1245:                    for($i=1; $i<=$stageCount; $i++) {
1246:                        // skip started but not completed stages
1247:                        if($i != $stageNumber && 
1248:                           empty($workflowStatus['stages'][$i]['completeDate']) && 
1249:                           !empty($workflowStatus['stages'][$i]['createDate'])) {
1250: 
1251:                            break;
1252:                        }
1253:                    
1254:                        // start the next one (unless there is already one)
1255:                        if(empty($workflowStatus['stages'][$i]['createDate'])) {
1256:                            $nextAction = new Actions('workflow');
1257:                            
1258:                            // don't genererate normal action changelog/triggers/events
1259:                            $nextAction->disableBehavior('changelog');    
1260:                            $nextAction->disableBehavior('TagBehavior'); // no tags
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:                            // $nextAction->actionDescription = $comment;
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:                            // $changes=$this->calculateChanges($oldAttributes, $model->attributes, 
1283:                            //   $model);
1284:                            // $this->updateChangelog($model,$changes);
1285:                            break;
1286:                        }
1287:                    }
1288:                
1289:                 }
1290: 
1291:                 // if($stageNumber < $stageCount && empty($workflowStatus[$stageNumber+1]['createDate'])) {    // if this isn't the final stage,
1292:                     
1293:                 // }
1294:                 
1295:                 // refresh the workflow status
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:         //AuxLib::debugLogR ((int) $completed);
1317: 
1318:         return array ($completed, $workflowStatus);
1319: 
1320:     }
1321: 
1322:     /**
1323:      * Starts a workflow stage 
1324:      * @param int $workflowId
1325:      * @param int $stageNumber the stage to start
1326:      * @param object $model model associated with workflow
1327:      */
1328:     public static function startStage (
1329:         $workflowId,$stageNumber,$model,$workflowStatus=null) {
1330: 
1331:         //AuxLib::debugLogR ('starting stage '.$stageNumber);
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:         //AuxLib::debugLogR ($workflowStatus);
1340:         //assert ($model !== null);
1341: 
1342:         $started = false;
1343:         
1344:         // if stage has not yet been started or completed
1345:         if($model !== null && 
1346:             self::checkStageRequirement ($stageNumber, $workflowStatus) && 
1347:            !self::isStarted ($workflowStatus, $stageNumber)) {
1348:             
1349:             $action = new Actions('workflow');
1350: 
1351:             // don't genererate normal action changelog/triggers/events
1352:             $action->disableBehavior('changelog');    
1353:             $action->disableBehavior('TagBehavior'); // no tags up in here
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:         //AuxLib::debugLogR ((int) $started);
1389:         return array ($started, $workflowStatus);
1390:     }
1391: 
1392:     /**
1393:      * Uncompletes a stage (if completed) or unstarts it (if started).
1394:      * @param $unstarts bool If false, will not attempt to unstart an ongoing stage
1395:      */
1396:     public static function revertStage (
1397:         $workflowId,$stageNumber,$model,$unstart=true,$workflowStatus=null) {
1398: 
1399:         //AuxLib::debugLogR ('reverting stage '.$stageNumber);
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:         // if stage has been started or completed
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:             // the stage is complete, so just set it to 'started'
1422:             if(self::isCompleted ($workflowStatus, $stageNumber) && 
1423:                self::canUncomplete ($workflowStatus, $stageNumber)) {
1424: 
1425:                 //AuxLib::debugLogR ('uncompleting stage '.$stageNumber);
1426:                 $action->setScenario('workflow');
1427:                 
1428:                 // don't genererate normal action changelog/triggers/events
1429:                 $action->disableBehavior('changelog');    
1430:                 $action->disableBehavior('TagBehavior'); // no tags up in here
1431:                 $action->complete = 'No';
1432:                 $action->completeDate = null;
1433:                 $action->completedBy = '';
1434: 
1435:                 // original completion note no longer applies
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:                 // delete all incomplete stages after this one
1449:                 // X2Model::model('Actions')->deleteAll(new CDbCriteria(
1450:                     // array('condition'=>"associationId=$modelId AND associationType='$type' AND type='workflow' AND workflowId=$workflowId AND stageNumber > $stageNumber AND (completeDate IS NULL OR completeDate=0)")
1451:                 // ));
1452:                 
1453:                 
1454:             } else if ($unstart) { 
1455:                 // the stage is already incomplete, so delete it and all subsequent stages
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:         //AuxLib::debugLogR ((int) $reverted);
1480:         return array ($reverted, $workflowStatus);
1481:     }
1482: 
1483:     public static function updateWorkflowChangelog(&$action,$changeType,&$model) {
1484:         $changelog = new Changelog;
1485:         // $type = $action->associationType=='opportunities'?"Opportunity":ucfirst($action->associationType);
1486:         $changelog->type = get_class($model);
1487:         $changelog->itemId = $action->associationId;
1488:         // $record=X2Model::model(ucfirst($type))->findByPk($action->associationId);
1489:         // if(isset($record) && $record->hasAttribute('name')){
1490:             // $changelog->recordName=$record->name;
1491:         // }else{
1492:             // $changelog->recordName=$type;
1493:         // }X2Flow::trigger('WorkflowStageCompleteTrigger',array('workflow'=>'model'=>$model));
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:         // $workflowName = Yii::app()->db->createCommand()->select('name')->from('x2_workflows')->where('id=:id',array(':id'=>$action->workflowId))->queryScalar();
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:             //$trigger = 'WorkflowStartStageTrigger';
1518:             $event->type = 'workflow_start';
1519:             $changelog->newValue='Workflow Stage Started: '.$stageName;
1520:             
1521:         } elseif($changeType === 'complete') {
1522:             //$trigger = 'WorkflowCompleteStageTrigger';
1523:             $event->type = 'workflow_complete';
1524:             $changelog->newValue = 'Workflow Stage Completed: '.$stageName;
1525:             
1526:         } elseif($changeType === 'revert') {
1527:             //$trigger = 'WorkflowRevertStageTrigger';
1528:             $event->type = 'workflow_revert';
1529:             $changelog->newValue = 'Workflow Stage Reverted: '.$stageName;
1530:             
1531:         } else {
1532:             return;
1533:         }
1534:         
1535:         /*X2Flow::trigger($trigger,array(
1536:             'workflow'=>$action->workflow,
1537:             'model'=>$model,
1538:             'stageNumber'=>$action->stageNumber,
1539:             'stageName'=>$stageName,
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:      * @param array $colors an array of color hex values, 1 for each stage 
1557:      * @return array css color strings to be used for pipeline list item backgrounds
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: 
X2CRM Documentation API documentation generated by ApiGen 2.8.0