From 3c8b2534ebae612ea24ac768203b09d9891cbe14 Mon Sep 17 00:00:00 2001 From: yuri Date: Mon, 11 Jun 2018 14:46:21 +0300 Subject: [PATCH] opp last stage field --- .../Modules/Crm/Controllers/Opportunity.php | 3 +- .../Modules/Crm/Repositories/Opportunity.php | 47 +++++++++++++++++ .../Resources/i18n/en_US/DashletOptions.json | 3 +- .../Crm/Resources/i18n/en_US/Opportunity.json | 3 +- .../metadata/clientDefs/Opportunity.json | 15 ++++++ .../metadata/dashlets/SalesPipeline.json | 5 +- .../metadata/entityDefs/Opportunity.json | 9 ++++ .../Espo/Modules/Crm/Services/Opportunity.php | 27 +++++++--- .../crm/src/views/dashlets/sales-pipeline.js | 4 ++ .../views/opportunity/fields/last-stage.js | 52 +++++++++++++++++++ client/src/dynamic-handler.js | 6 ++- client/src/views/record/detail.js | 4 +- 12 files changed, 165 insertions(+), 13 deletions(-) create mode 100644 client/modules/crm/src/views/opportunity/fields/last-stage.js diff --git a/application/Espo/Modules/Crm/Controllers/Opportunity.php b/application/Espo/Modules/Crm/Controllers/Opportunity.php index 9add38aa34..79f682a642 100644 --- a/application/Espo/Modules/Crm/Controllers/Opportunity.php +++ b/application/Espo/Modules/Crm/Controllers/Opportunity.php @@ -87,8 +87,9 @@ class Opportunity extends \Espo\Core\Controllers\Record $dateFrom = $request->get('dateFrom'); $dateTo = $request->get('dateTo'); $dateFilter = $request->get('dateFilter'); + $useLastStage = $request->get('useLastStage') === 'true'; - return $this->getService('Opportunity')->reportSalesPipeline($dateFilter, $dateFrom, $dateTo); + return $this->getService('Opportunity')->reportSalesPipeline($dateFilter, $dateFrom, $dateTo, $useLastStage); } public function postActionMassConvertCurrency($params, $data, $request) diff --git a/application/Espo/Modules/Crm/Repositories/Opportunity.php b/application/Espo/Modules/Crm/Repositories/Opportunity.php index c24ffc5a44..5de75b4d9f 100644 --- a/application/Espo/Modules/Crm/Repositories/Opportunity.php +++ b/application/Espo/Modules/Crm/Repositories/Opportunity.php @@ -42,6 +42,53 @@ class Opportunity extends \Espo\Core\ORM\Repositories\RDB } } + if (!$entity->isAttributeChanged('lastStage') && $entity->isAttributeChanged('stage')) { + $probability = $this->getMetadata()->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap', $entity->get('stage')], 0); + $probabilityMap = $this->getMetadata()->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap'], []); + + if (!$probability) { + $stageList = $this->getMetadata()->get('entityDefs.Opportunity.fields.stage.options', []); + if ($entity->isNew()) { + if (count($stageList)) { + $min = 100; + $minStage = null; + foreach ($stageList as $stage) { + if (!empty($probabilityMap[$stage]) && $probabilityMap[$stage] !== 100) { + if ($probabilityMap[$stage] < $min) { + $min = $probabilityMap[$stage]; + $minStage = $stage; + } + } + } + if ($minStage) { + $entity->set('lastStage', $minStage); + } + } + } else { + $lastStageProbability = $this->getMetadata()->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap', $entity->get('lastStage')], 0); + if ($lastStageProbability === 100) { + if (count($stageList)) { + $max = 0; + $maxStage = null; + foreach ($stageList as $stage) { + if (!empty($probabilityMap[$stage]) && $probabilityMap[$stage] !== 100) { + if ($probabilityMap[$stage] > $max) { + $max = $probabilityMap[$stage]; + $maxStage = $stage; + } + } + } + if ($maxStage) { + $entity->set('lastStage', $maxStage); + } + } + } + } + } else { + $entity->set('lastStage', $entity->get('stage')); + } + } + parent::beforeSave($entity, $options); } diff --git a/application/Espo/Modules/Crm/Resources/i18n/en_US/DashletOptions.json b/application/Espo/Modules/Crm/Resources/i18n/en_US/DashletOptions.json index 2d51f2fb5e..3277c22ccc 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/en_US/DashletOptions.json +++ b/application/Espo/Modules/Crm/Resources/i18n/en_US/DashletOptions.json @@ -1,5 +1,6 @@ { "fields": { - "futureDays": "Next X Days" + "futureDays": "Next X Days", + "useLastStage": "Group by last reached stage" } } diff --git a/application/Espo/Modules/Crm/Resources/i18n/en_US/Opportunity.json b/application/Espo/Modules/Crm/Resources/i18n/en_US/Opportunity.json index 2fc844ff03..5fd694cd84 100644 --- a/application/Espo/Modules/Crm/Resources/i18n/en_US/Opportunity.json +++ b/application/Espo/Modules/Crm/Resources/i18n/en_US/Opportunity.json @@ -15,7 +15,8 @@ "campaign": "Campaign", "originalLead": "Original Lead", "amountCurrency": "Amount Currency", - "contactRole": "Contact Role" + "contactRole": "Contact Role", + "lastStage": "Last Stage" }, "links": { "contacts": "Contacts", diff --git a/application/Espo/Modules/Crm/Resources/metadata/clientDefs/Opportunity.json b/application/Espo/Modules/Crm/Resources/metadata/clientDefs/Opportunity.json index 3498966211..937cc57f65 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/clientDefs/Opportunity.json +++ b/application/Espo/Modules/Crm/Resources/metadata/clientDefs/Opportunity.json @@ -73,6 +73,21 @@ } }, "kanbanViewMode": true, + "dynamicLogic": { + "fields": { + "lastStage": { + "visible": { + "conditionGroup": [ + { + "type": "equals", + "attribute": "stage", + "value": "Closed Lost" + } + ] + } + } + } + }, "color": "#9fc77e", "iconClass": "fas fa-dollar-sign" } diff --git a/application/Espo/Modules/Crm/Resources/metadata/dashlets/SalesPipeline.json b/application/Espo/Modules/Crm/Resources/metadata/dashlets/SalesPipeline.json index 6c6a2642fd..449c2dac22 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/dashlets/SalesPipeline.json +++ b/application/Espo/Modules/Crm/Resources/metadata/dashlets/SalesPipeline.json @@ -21,6 +21,9 @@ "options": ["currentYear", "currentQuarter", "currentMonth", "ever", "between"], "default": "currentYear", "translation": "Global.options.dateSearchRanges" + }, + "useLastStage": { + "type": "bool" } }, "layout": [ @@ -31,7 +34,7 @@ ], [ {"name": "dateFilter"}, - false + {"name": "useLastStage"} ], [ {"name": "dateFrom"}, diff --git a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Opportunity.json b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Opportunity.json index 34193ef713..c224a5bf3f 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Opportunity.json +++ b/application/Espo/Modules/Crm/Resources/metadata/entityDefs/Opportunity.json @@ -62,6 +62,12 @@ } ] }, + "lastStage": { + "type": "enum", + "view": "crm:views/opportunity/fields/last-stage", + "customizationOptionsDisabled": true, + "customizationDefaultDisabled": true + }, "probability": { "type": "int", "required": true, @@ -247,6 +253,9 @@ "stage": { "columns": ["stage", "deleted"] }, + "lastStage": { + "columns": ["lastStage"] + }, "assignedUser": { "columns": ["assignedUserId", "deleted"] }, diff --git a/application/Espo/Modules/Crm/Services/Opportunity.php b/application/Espo/Modules/Crm/Services/Opportunity.php index b44c014140..6e5c77a8dc 100644 --- a/application/Espo/Modules/Crm/Services/Opportunity.php +++ b/application/Espo/Modules/Crm/Services/Opportunity.php @@ -36,7 +36,7 @@ use \Espo\Core\Exceptions\Forbidden; class Opportunity extends \Espo\Services\Record { - public function reportSalesPipeline($dateFilter, $dateFrom = null, $dateTo = null) + public function reportSalesPipeline($dateFilter, $dateFrom = null, $dateTo = null, $useLastStage = false) { if (in_array('amount', $this->getAcl()->getScopeForbiddenAttributeList('Opportunity'))) { throw new Forbidden(); @@ -46,19 +46,34 @@ class Opportunity extends \Espo\Services\Record list($dateFrom, $dateTo) = $this->getDateRangeByFilter($dateFilter); } + $lostStageList = []; + $probabilityMap = $this->getMetadata()->get(['entityDefs', 'Opportunity', 'fields', 'stage', 'probabilityMap'], []); + $stageList = $this->getMetadata()->get('entityDefs.Opportunity.fields.stage.options', []); + foreach ($stageList as $stage) { + if (empty($probabilityMap[$stage])) { + $lostStageList[] = $stage; + } + } + $pdo = $this->getEntityManager()->getPDO(); $options = $this->getMetadata()->get('entityDefs.Opportunity.fields.stage.options', []); $selectManager = $this->getSelectManagerFactory()->create('Opportunity'); + $stageField = 'stage'; + if ($useLastStage) { + $stageField = 'lastStage'; + } + $selectParams = [ - 'select' => ['stage', ['SUM:amountConverted', 'amount']], + 'select' => [$stageField, ['SUM:amountConverted', 'amount']], 'whereClause' => [ - 'stage!=' => 'Closed Lost' + [$stageField . '!=' => $lostStageList], + [$stageField . '!=' => null] ], - 'orderBy' => 'LIST:stage:' . implode(',', $options), - 'groupBy' => ['stage'] + 'orderBy' => 'LIST:'.$stageField.':' . implode(',', $options), + 'groupBy' => [$stageField] ]; if ($dateFilter !== 'ever') { @@ -81,7 +96,7 @@ class Opportunity extends \Espo\Services\Record $result = array(); foreach ($rows as $row) { - $result[$row['stage']] = floatval($row['amount']); + $result[$row[$stageField]] = floatval($row['amount']); } return $result; diff --git a/client/modules/crm/src/views/dashlets/sales-pipeline.js b/client/modules/crm/src/views/dashlets/sales-pipeline.js index 6de80d793a..3f771f61d5 100644 --- a/client/modules/crm/src/views/dashlets/sales-pipeline.js +++ b/client/modules/crm/src/views/dashlets/sales-pipeline.js @@ -43,6 +43,10 @@ Espo.define('crm:views/dashlets/sales-pipeline', 'crm:views/dashlets/abstract/ch if (this.getDateFilter() === 'between') { url += '&dateFrom=' + this.getOption('dateFrom') + '&dateTo=' + this.getOption('dateTo'); } + + if (this.getOption('useLastStage')) { + url += '&useLastStage=true'; + } return url; }, diff --git a/client/modules/crm/src/views/opportunity/fields/last-stage.js b/client/modules/crm/src/views/opportunity/fields/last-stage.js new file mode 100644 index 0000000000..4bc7668210 --- /dev/null +++ b/client/modules/crm/src/views/opportunity/fields/last-stage.js @@ -0,0 +1,52 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM - Open Source CRM application. + * Copyright (C) 2014-2018 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko + * Website: http://www.espocrm.com + * + * EspoCRM is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EspoCRM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EspoCRM. If not, see http://www.gnu.org/licenses/. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU General Public License version 3. + * + * In accordance with Section 7(b) of the GNU General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +Espo.define('crm:views/opportunity/fields/last-stage', 'views/fields/enum', function (Dep) { + + return Dep.extend({ + + setup: function () { + var optionList = this.getMetadata().get('entityDefs.Opportunity.fields.stage.options', []); + var probabilityMap = this.getMetadata().get('entityDefs.Opportunity.fields.stage.probabilityMap', {}); + + this.params.options = []; + + optionList.forEach(function (item) { + if (!probabilityMap[item]) return; + if (probabilityMap[item] === 100) return; + this.params.options.push(item); + }, this); + + this.params.translation = 'Opportunity.options.stage'; + + Dep.prototype.setup.call(this); + } + + }); + +}); diff --git a/client/src/dynamic-handler.js b/client/src/dynamic-handler.js index 1e7cc927a9..b8568838aa 100644 --- a/client/src/dynamic-handler.js +++ b/client/src/dynamic-handler.js @@ -37,7 +37,11 @@ define('dynamic-handler', [], function () { init: function () {}, - onChange: function (model, o) {} + onChange: function (model, o) {}, + + getMetadata: function () { + return this.recordView.getMetadata() + } }); DynamicHandler.extend = Backbone.Router.extend; diff --git a/client/src/views/record/detail.js b/client/src/views/record/detail.js index b397e72c04..02915e076e 100644 --- a/client/src/views/record/detail.js +++ b/client/src/views/record/detail.js @@ -789,14 +789,14 @@ Espo.define('views/record/detail', ['views/record/base', 'view-record-helper'], this.listenTo(this.model, 'change', function (model, o) { if ('onChange' in this.dynamicHandler) { - this.dynamicHandler.onChange(model, o); + this.dynamicHandler.onChange.call(this.dynamicHandler, model, o); } var changedAttributes = model.changedAttributes(); for (var attribute in changedAttributes) { var methodName = 'onChange' + Espo.Utils.upperCaseFirst(attribute); if (methodName in this.dynamicHandler) { - this.dynamicHandler[methodName](model, changedAttributes[attribute], o); + this.dynamicHandler[methodName].call(this.dynamicHandler, model, changedAttributes[attribute], o); } } }, this);