From a1603eb9821b1651ca9d4e689289d4aefab2dd64 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 1 Feb 2023 16:42:14 +0100 Subject: [PATCH 001/199] BTHAB-37: Add direct link to CiviCase Instances --- .../Hook/NavigationMenu/AlterForCaseMenu.php | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/CRM/Civicase/Hook/NavigationMenu/AlterForCaseMenu.php b/CRM/Civicase/Hook/NavigationMenu/AlterForCaseMenu.php index f71494176..22dae9dad 100644 --- a/CRM/Civicase/Hook/NavigationMenu/AlterForCaseMenu.php +++ b/CRM/Civicase/Hook/NavigationMenu/AlterForCaseMenu.php @@ -1,5 +1,6 @@ caseCategorySetting = new CaseCategorySetting(); $this->rewriteCaseUrls($menu); $this->addCaseWebformUrl($menu); + $this->addCiviCaseInstanceMenu($menu); } /** @@ -116,4 +118,62 @@ private function menuWalk(array &$menu, callable $callback) { } } + /** + * Adds the civicase instance menu to the Adminsiter Civicase Menu. + * + * @param array $menu + * Tree of menu items, per hook_civicrm_navigationMenu. + */ + private function addCiviCaseInstanceMenu(array &$menu) { + $groupId = $this->getCaseTypeCategoryGroupId(); + if (empty($groupId)) { + return; + } + + // Find the Civicase menu. + $caseID = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Navigation', 'CiviCase', 'id', 'name'); + $administerID = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Navigation', 'Administer', 'id', 'name'); + $civicaseSettings = &$menu[$administerID]['child'][$caseID]; + + $weight = $desiredWeight = 0; + $moveDown = FALSE; + foreach ($civicaseSettings['child'] as $key => &$value) { + if ($value['attributes']['name'] === 'Case Types') { + $weight = $desiredWeight = (int) $value['attributes']['weight']; + $moveDown = TRUE; + } + + if ($moveDown) { + $value['attributes']['weight'] = ++$weight; + } + } + + $menu[$administerID]['child'][$caseID]['child'][] = [ + 'attributes' => [ + 'label' => ts('CiviCase Instances'), + 'name' => 'CiviCase Instances', + 'url' => "civicrm/admin/options?gid=$groupId&reset=1", + 'permission' => 'access all cases and activities', + 'operator' => 'OR', + 'separator' => 1, + 'parentID' => $caseID, + 'active' => 1, + 'weight' => $desiredWeight, + ], + ]; + } + + /** + * Returnd the ID of the case type category option group. + */ + private function getCaseTypeCategoryGroupId() { + $optionGroups = OptionGroup::get() + ->addSelect('id') + ->addWhere('name', '=', 'case_type_categories') + ->setLimit(25) + ->execute(); + + return $optionGroups[0]["id"] ?? NULL; + } + } From b783b66a2347503f0a5cf1962b3b6d960adac714 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 8 Feb 2023 16:00:12 +0100 Subject: [PATCH 002/199] BTHAB-45: Show case instances without searching --- templates/CRM/Civicase/ChangeSet/CaseTypeCategory.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/CRM/Civicase/ChangeSet/CaseTypeCategory.html b/templates/CRM/Civicase/ChangeSet/CaseTypeCategory.html index 1d510af43..4798c4dc5 100644 --- a/templates/CRM/Civicase/ChangeSet/CaseTypeCategory.html +++ b/templates/CRM/Civicase/ChangeSet/CaseTypeCategory.html @@ -9,7 +9,7 @@ api: { params: {option_group_id: 'case_type_categories'} }, - select: {allowClear: true, multiple: false, placeholder: ts('Select Instance')}, + select: {allowClear: true, multiple: false, placeholder: ts('Select Instance'), 'minimumInputLength': 0}, }" class="big crm-form-text" /> From 7860cfb4b8c7110f3e690f73299d12cdbc9ffc51 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 2 Feb 2023 15:18:21 +0100 Subject: [PATCH 003/199] BTHAB-38: Regenerate civix file --- civicase.civix.php | 183 +----------------- civicase.php | 55 ------ info.xml | 9 +- phpunit.xml.dist | 2 +- tests/phpunit/BaseHeadlessTest.php | 2 +- .../CRM/Civicase/BAO/CaseContactLockTest.php | 2 +- tests/phpunit/api/v3/Case/BaseTestCase.php | 2 +- 7 files changed, 14 insertions(+), 241 deletions(-) diff --git a/civicase.civix.php b/civicase.civix.php index c1e8efe5b..c479c0456 100644 --- a/civicase.civix.php +++ b/civicase.civix.php @@ -91,9 +91,9 @@ function _civicase_civix_civicrm_config(&$config = NULL) { } $configured = TRUE; - $template =& CRM_Core_Smarty::singleton(); + $template = CRM_Core_Smarty::singleton(); - $extRoot = dirname(__FILE__) . DIRECTORY_SEPARATOR; + $extRoot = __DIR__ . DIRECTORY_SEPARATOR; $extDir = $extRoot . 'templates'; if (is_array($template->template_dir)) { @@ -107,19 +107,6 @@ function _civicase_civix_civicrm_config(&$config = NULL) { set_include_path($include_path); } -/** - * (Delegated) Implements hook_civicrm_xmlMenu(). - * - * @param $files array(string) - * - * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_xmlMenu - */ -function _civicase_civix_civicrm_xmlMenu(&$files) { - foreach (_civicase_civix_glob(__DIR__ . '/xml/Menu/*.xml') as $file) { - $files[] = $file; - } -} - /** * Implements hook_civicrm_install(). * @@ -217,160 +204,6 @@ function _civicase_civix_upgrader() { } } -/** - * Search directory tree for files which match a glob pattern. - * - * Note: Dot-directories (like "..", ".git", or ".svn") will be ignored. - * Note: In Civi 4.3+, delegate to CRM_Utils_File::findFiles() - * - * @param string $dir base dir - * @param string $pattern , glob pattern, eg "*.txt" - * - * @return array - */ -function _civicase_civix_find_files($dir, $pattern) { - if (is_callable(['CRM_Utils_File', 'findFiles'])) { - return CRM_Utils_File::findFiles($dir, $pattern); - } - - $todos = [$dir]; - $result = []; - while (!empty($todos)) { - $subdir = array_shift($todos); - foreach (_civicase_civix_glob("$subdir/$pattern") as $match) { - if (!is_dir($match)) { - $result[] = $match; - } - } - if ($dh = opendir($subdir)) { - while (FALSE !== ($entry = readdir($dh))) { - $path = $subdir . DIRECTORY_SEPARATOR . $entry; - if ($entry[0] == '.') { - } - elseif (is_dir($path)) { - $todos[] = $path; - } - } - closedir($dh); - } - } - return $result; -} - -/** - * (Delegated) Implements hook_civicrm_managed(). - * - * Find any *.mgd.php files, merge their content, and return. - * - * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_managed - */ -function _civicase_civix_civicrm_managed(&$entities) { - $mgdFiles = _civicase_civix_find_files(__DIR__, '*.mgd.php'); - sort($mgdFiles); - foreach ($mgdFiles as $file) { - $es = include $file; - foreach ($es as $e) { - if (empty($e['module'])) { - $e['module'] = E::LONG_NAME; - } - if (empty($e['params']['version'])) { - $e['params']['version'] = '3'; - } - $entities[] = $e; - } - } -} - -/** - * (Delegated) Implements hook_civicrm_caseTypes(). - * - * Find any and return any files matching "xml/case/*.xml" - * - * Note: This hook only runs in CiviCRM 4.4+. - * - * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_caseTypes - */ -function _civicase_civix_civicrm_caseTypes(&$caseTypes) { - if (!is_dir(__DIR__ . '/xml/case')) { - return; - } - - foreach (_civicase_civix_glob(__DIR__ . '/xml/case/*.xml') as $file) { - $name = preg_replace('/\.xml$/', '', basename($file)); - if ($name != CRM_Case_XMLProcessor::mungeCaseType($name)) { - $errorMessage = sprintf("Case-type file name is malformed (%s vs %s)", $name, CRM_Case_XMLProcessor::mungeCaseType($name)); - throw new CRM_Core_Exception($errorMessage); - } - $caseTypes[$name] = [ - 'module' => E::LONG_NAME, - 'name' => $name, - 'file' => $file, - ]; - } -} - -/** - * (Delegated) Implements hook_civicrm_angularModules(). - * - * Find any and return any files matching "ang/*.ang.php" - * - * Note: This hook only runs in CiviCRM 4.5+. - * - * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_angularModules - */ -function _civicase_civix_civicrm_angularModules(&$angularModules) { - if (!is_dir(__DIR__ . '/ang')) { - return; - } - - $files = _civicase_civix_glob(__DIR__ . '/ang/*.ang.php'); - foreach ($files as $file) { - $name = preg_replace(':\.ang\.php$:', '', basename($file)); - $module = include $file; - if (empty($module['ext'])) { - $module['ext'] = E::LONG_NAME; - } - $angularModules[$name] = $module; - } -} - -/** - * (Delegated) Implements hook_civicrm_themes(). - * - * Find any and return any files matching "*.theme.php" - */ -function _civicase_civix_civicrm_themes(&$themes) { - $files = _civicase_civix_glob(__DIR__ . '/*.theme.php'); - foreach ($files as $file) { - $themeMeta = include $file; - if (empty($themeMeta['name'])) { - $themeMeta['name'] = preg_replace(':\.theme\.php$:', '', basename($file)); - } - if (empty($themeMeta['ext'])) { - $themeMeta['ext'] = E::LONG_NAME; - } - $themes[$themeMeta['name']] = $themeMeta; - } -} - -/** - * Glob wrapper which is guaranteed to return an array. - * - * The documentation for glob() says, "On some systems it is impossible to - * distinguish between empty match and an error." Anecdotally, the return - * result for an empty match is sometimes array() and sometimes FALSE. - * This wrapper provides consistency. - * - * @link http://php.net/glob - * @param string $pattern - * - * @return array - */ -function _civicase_civix_glob($pattern) { - $result = glob($pattern); - return is_array($result) ? $result : []; -} - /** * Inserts a navigation menu item at a given place in the hierarchy. * @@ -453,18 +286,6 @@ function _civicase_civix_fixNavigationMenuItems(&$nodes, &$maxNavID, $parentID) } } -/** - * (Delegated) Implements hook_civicrm_alterSettingsFolders(). - * - * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_alterSettingsFolders - */ -function _civicase_civix_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) { - $settingsDir = __DIR__ . DIRECTORY_SEPARATOR . 'settings'; - if (!in_array($settingsDir, $metaDataFolders) && is_dir($settingsDir)) { - $metaDataFolders[] = $settingsDir; - } -} - /** * (Delegated) Implements hook_civicrm_entityTypes(). * diff --git a/civicase.php b/civicase.php index aa381a5b8..b8a923b9a 100644 --- a/civicase.php +++ b/civicase.php @@ -81,15 +81,6 @@ function civicase_civicrm_config(&$config) { ); } -/** - * Implements hook_civicrm_xmlMenu(). - * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_xmlMenu - */ -function civicase_civicrm_xmlMenu(&$files) { - _civicase_civix_civicrm_xmlMenu($files); -} - /** * Implements hook_civicrm_install(). * @@ -144,43 +135,6 @@ function civicase_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) { return _civicase_civix_civicrm_upgrade($op, $queue); } -/** - * Implements hook_civicrm_managed(). - * - * Generate a list of entities to create/deactivate/delete when this module - * is installed, disabled, uninstalled. - * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_managed - */ -function civicase_civicrm_managed(&$entities) { - _civicase_civix_civicrm_managed($entities); -} - -/** - * Implements hook_civicrm_caseTypes(). - * - * Generate a list of case-types. - * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_caseTypes - */ -function civicase_civicrm_caseTypes(&$caseTypes) { - _civicase_civix_civicrm_caseTypes($caseTypes); -} - -/** - * Implements hook_civicrm_angularModules(). - * - * Generate a list of Angular modules. - * - * Note: This hook only runs in CiviCRM 4.5+. It may - * use features only available in v4.6+. - * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_caseTypes - */ -function civicase_civicrm_angularModules(&$angularModules) { - _civicase_civix_civicrm_angularModules($angularModules); -} - /** * Implements hook_civicrm_alterMenu(). * @@ -196,15 +150,6 @@ function civicase_civicrm_alterMenu(&$items) { $items['civicrm/export/standalone']['ids_arguments']['json'][] = 'civicase_reload'; } -/** - * Implements hook_civicrm_alterSettingsFolders(). - * - * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_alterSettingsFolders - */ -function civicase_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) { - _civicase_civix_civicrm_alterSettingsFolders($metaDataFolders); -} - /** * Implements hook_civicrm_buildForm(). */ diff --git a/info.xml b/info.xml index 9d6d1852e..0a98f4df9 100644 --- a/info.xml +++ b/info.xml @@ -25,13 +25,20 @@ CiviCRM 5.51.1 - + CRM/Civicase + 22.05.2 org.civicrm.shoreditch uk.co.compucorp.usermenu + + ang-php@1.0.0 + menu-xml@1.0.0 + mgd-php@1.0.0 + setting-php@1.0.0 + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0f9f25d30..fc8f870b7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,5 @@ - + ./tests/phpunit diff --git a/tests/phpunit/BaseHeadlessTest.php b/tests/phpunit/BaseHeadlessTest.php index 2eda63621..5f0a6db5e 100644 --- a/tests/phpunit/BaseHeadlessTest.php +++ b/tests/phpunit/BaseHeadlessTest.php @@ -7,7 +7,7 @@ /** * Base test class. */ -abstract class BaseHeadlessTest extends PHPUnit_Framework_TestCase implements HeadlessInterface, TransactionalInterface { +abstract class BaseHeadlessTest extends PHPUnit\Framework\TestCase implements HeadlessInterface, TransactionalInterface { /** * {@inheritDoc} diff --git a/tests/phpunit/CRM/Civicase/BAO/CaseContactLockTest.php b/tests/phpunit/CRM/Civicase/BAO/CaseContactLockTest.php index 96bf4b95f..ec14124cc 100644 --- a/tests/phpunit/CRM/Civicase/BAO/CaseContactLockTest.php +++ b/tests/phpunit/CRM/Civicase/BAO/CaseContactLockTest.php @@ -12,7 +12,7 @@ * * @group headless */ -class CRM_Civicase_BAO_CaseContactLockTest extends PHPUnit_Framework_TestCase implements HeadlessInterface, TransactionalInterface { +class CRM_Civicase_BAO_CaseContactLockTest extends PHPUnit\Framework\TestCase implements HeadlessInterface, TransactionalInterface { /** * {@inheritdoc} diff --git a/tests/phpunit/api/v3/Case/BaseTestCase.php b/tests/phpunit/api/v3/Case/BaseTestCase.php index 3082b0008..e23477d2a 100644 --- a/tests/phpunit/api/v3/Case/BaseTestCase.php +++ b/tests/phpunit/api/v3/Case/BaseTestCase.php @@ -3,7 +3,7 @@ /** * Test the "Case.getfiles" API. */ -class api_v3_Case_BaseTestCase extends \PHPUnit_Framework_TestCase { +class api_v3_Case_BaseTestCase extends \PHPUnit\Framework\TestCase { protected $_apiversion = 3; protected static $filePrefix = NULL; From 3a21a79c23d3e17864e0fdd3c319a8e2b2c8319c Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 2 Feb 2023 16:05:18 +0100 Subject: [PATCH 004/199] BTHAB-38: Apply PHPCS lints and ignore basetestcases --- phpcs-ruleset.xml | 2 ++ tests/phpunit/BaseHeadlessTest.php | 3 ++- tests/phpunit/CRM/Civicase/BAO/CaseContactLockTest.php | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/phpcs-ruleset.xml b/phpcs-ruleset.xml index 0524d7a5d..c96b72908 100644 --- a/phpcs-ruleset.xml +++ b/phpcs-ruleset.xml @@ -21,6 +21,8 @@ CRM/Civicase/DAO/* CRM/Civicase/Upgrader/Base.php CRM/Civicase/Form/Report/Case/CaseWithActivityPivot.php + tests/phpunit/api/v3/Case/BaseTestCase.php + tests/phpunit/BaseHeadlessTest.phpp diff --git a/tests/phpunit/BaseHeadlessTest.php b/tests/phpunit/BaseHeadlessTest.php index 5f0a6db5e..ea2509aa2 100644 --- a/tests/phpunit/BaseHeadlessTest.php +++ b/tests/phpunit/BaseHeadlessTest.php @@ -1,5 +1,6 @@ Date: Thu, 2 Feb 2023 16:24:41 +0100 Subject: [PATCH 005/199] BTHAB-38: Manage optiongroup used to store case instance features --- CRM/Civicase/Setup/Manage/AbstractManager.php | 44 ++++++++++++ .../CaseTypeCategoryFeaturesManager.php | 71 +++++++++++++++++++ CRM/Civicase/Upgrader.php | 7 ++ CRM/Civicase/Upgrader/Steps/Step0019.php | 22 ++++++ 4 files changed, 144 insertions(+) create mode 100644 CRM/Civicase/Setup/Manage/AbstractManager.php create mode 100644 CRM/Civicase/Setup/Manage/CaseTypeCategoryFeaturesManager.php create mode 100644 CRM/Civicase/Upgrader/Steps/Step0019.php diff --git a/CRM/Civicase/Setup/Manage/AbstractManager.php b/CRM/Civicase/Setup/Manage/AbstractManager.php new file mode 100644 index 000000000..cdc918fb3 --- /dev/null +++ b/CRM/Civicase/Setup/Manage/AbstractManager.php @@ -0,0 +1,44 @@ +toggle(FALSE); + } + + /** + * Enables the entity. + */ + public function enable() { + $this->toggle(TRUE); + } + + /** + * Enables/Disables the entity based on the passed status. + * + * @params boolean $status + * True to enable the entity, False to disable the entity. + */ + abstract protected function toggle($status): void; + +} diff --git a/CRM/Civicase/Setup/Manage/CaseTypeCategoryFeaturesManager.php b/CRM/Civicase/Setup/Manage/CaseTypeCategoryFeaturesManager.php new file mode 100644 index 000000000..ca0ecb936 --- /dev/null +++ b/CRM/Civicase/Setup/Manage/CaseTypeCategoryFeaturesManager.php @@ -0,0 +1,71 @@ + CRM_Civicase_Service_CaseTypeCategoryFeatures::NAME, + 'title' => ts('Case Type Category Additional Features'), + 'is_reserved' => 1, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => CRM_Civicase_Service_CaseTypeCategoryFeatures::NAME, + 'name' => 'quotations', + 'label' => 'Quotations', + 'is_default' => TRUE, + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => CRM_Civicase_Service_CaseTypeCategoryFeatures::NAME, + 'name' => 'invoices', + 'label' => 'Invoices', + 'is_default' => TRUE, + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => CRM_Civicase_Service_CaseTypeCategoryFeatures::NAME, + 'name' => 'pledges', + 'label' => 'Pledges', + 'is_default' => TRUE, + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + } + + /** + * Removes the entity. + */ + public function remove(): void { + civicrm_api3('OptionGroup', 'get', [ + 'return' => ['id'], + 'name' => CRM_Civicase_Service_CaseTypeCategoryFeatures::NAME, + 'api.OptionGroup.delete' => ['id' => '$value.id'], + ]); + } + + /** + * {@inheritDoc} + */ + protected function toggle($status): void { + civicrm_api3('OptionGroup', 'get', [ + 'sequential' => 1, + 'name' => CRM_Civicase_Service_CaseTypeCategoryFeatures::NAME, + 'api.OptionGroup.create' => ['id' => '$value.id', 'is_active' => $status], + ]); + } + +} diff --git a/CRM/Civicase/Upgrader.php b/CRM/Civicase/Upgrader.php index 762e85d6b..e34ae0214 100644 --- a/CRM/Civicase/Upgrader.php +++ b/CRM/Civicase/Upgrader.php @@ -15,6 +15,7 @@ use CRM_Civicase_Setup_AddSingularLabels as AddSingularLabels; use CRM_Civicase_ExtensionUtil as E; use CRM_Civicase_Setup_AddMyActivitiesMenu as AddMyActivitiesMenu; +use CRM_Civicase_Setup_Manage_CaseTypeCategoryFeaturesManager as CaseTypeCategoryFeaturesManager; /** * Collection of upgrade steps. @@ -148,6 +149,7 @@ public function install() { } $this->createManageCasesMenuItem(); + (new CaseTypeCategoryFeaturesManager())->create(); } /** @@ -243,6 +245,8 @@ public function uninstall() { foreach ($steps as $step) { $step->apply(); } + + (new CaseTypeCategoryFeaturesManager())->remove(); } /** @@ -407,6 +411,8 @@ public function enable() { $workflowMenu = new AddManageWorkflowMenu(); $workflowMenu->apply(); + + (new CaseTypeCategoryFeaturesManager())->enable(); } /** @@ -416,6 +422,7 @@ public function disable() { $this->swapCaseMenuItems(); $this->toggleNav('Manage Cases', FALSE); + (new CaseTypeCategoryFeaturesManager())->disable(); } /** diff --git a/CRM/Civicase/Upgrader/Steps/Step0019.php b/CRM/Civicase/Upgrader/Steps/Step0019.php new file mode 100644 index 000000000..a29add7f5 --- /dev/null +++ b/CRM/Civicase/Upgrader/Steps/Step0019.php @@ -0,0 +1,22 @@ +create(); + + return TRUE; + } + +} From 15f5a71fb0023924ef8dc4d41e6e7261ccc93d46 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 6 Feb 2023 09:06:35 +0100 Subject: [PATCH 006/199] BTHAB-38: Create new entity to link case category with features --- CRM/Civicase/BAO/CaseCategoryFeatures.php | 31 +++ CRM/Civicase/DAO/CaseCategoryFeatures.php | 208 ++++++++++++++++++ CRM/Civicase/DAO/CaseCategoryInstance.php | 39 +++- CRM/Civicase/DAO/CaseContactLock.php | 50 ++++- CRM/Civicase/Upgrader/Steps/Step0019.php | 3 + Civi/Api4/CaseCategoryFeatures.php | 16 ++ civicase.civix.php | 5 + sql/auto_install.sql | 15 ++ sql/auto_uninstall.sql | 17 +- .../CaseCategoryFeatures.entityType.php | 17 ++ .../CRM/Civicase/CaseCategoryFeatures.xml | 43 ++++ 11 files changed, 423 insertions(+), 21 deletions(-) create mode 100644 CRM/Civicase/BAO/CaseCategoryFeatures.php create mode 100644 CRM/Civicase/DAO/CaseCategoryFeatures.php create mode 100644 Civi/Api4/CaseCategoryFeatures.php create mode 100644 xml/schema/CRM/Civicase/CaseCategoryFeatures.entityType.php create mode 100644 xml/schema/CRM/Civicase/CaseCategoryFeatures.xml diff --git a/CRM/Civicase/BAO/CaseCategoryFeatures.php b/CRM/Civicase/BAO/CaseCategoryFeatures.php new file mode 100644 index 000000000..a23f7cc32 --- /dev/null +++ b/CRM/Civicase/BAO/CaseCategoryFeatures.php @@ -0,0 +1,31 @@ +copyValues($params); + $instance->save(); + CRM_Utils_Hook::post($hook, $entityName, $instance->id, $instance); + + return $instance; + } + +} diff --git a/CRM/Civicase/DAO/CaseCategoryFeatures.php b/CRM/Civicase/DAO/CaseCategoryFeatures.php new file mode 100644 index 000000000..35e33a992 --- /dev/null +++ b/CRM/Civicase/DAO/CaseCategoryFeatures.php @@ -0,0 +1,208 @@ +__table = 'civicrm_case_category_features'; + parent::__construct(); + } + + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Case Category Featureses') : E::ts('Case Category Features'); + } + + /** + * Returns all the column names of this table + * + * @return array + */ + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ + 'name' => 'id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('Unique CaseCategoryFeatures ID'), + 'required' => TRUE, + 'where' => 'civicrm_case_category_features.id', + 'table_name' => 'civicrm_case_category_features', + 'entity' => 'CaseCategoryFeatures', + 'bao' => 'CRM_Civicase_DAO_CaseCategoryFeatures', + 'localizable' => 0, + 'html' => [ + 'type' => 'Number', + ], + 'readonly' => TRUE, + 'add' => NULL, + ], + 'category_id' => [ + 'name' => 'category_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('One of the values of the case_type_categories option group'), + 'required' => TRUE, + 'where' => 'civicrm_case_category_features.category_id', + 'table_name' => 'civicrm_case_category_features', + 'entity' => 'CaseCategoryFeatures', + 'bao' => 'CRM_Civicase_DAO_CaseCategoryFeatures', + 'localizable' => 0, + 'pseudoconstant' => [ + 'optionGroupName' => 'case_type_categories', + 'optionEditPath' => 'civicrm/admin/options/case_type_categories', + ], + 'add' => NULL, + ], + 'feature_id' => [ + 'name' => 'feature_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('One of the values of the case_type_category_features option group'), + 'required' => TRUE, + 'where' => 'civicrm_case_category_features.feature_id', + 'table_name' => 'civicrm_case_category_features', + 'entity' => 'CaseCategoryFeatures', + 'bao' => 'CRM_Civicase_DAO_CaseCategoryFeatures', + 'localizable' => 0, + 'pseudoconstant' => [ + 'optionGroupName' => 'case_type_category_features', + 'optionEditPath' => 'civicrm/admin/options/case_type_category_features', + ], + 'add' => NULL, + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); + } + return Civi::$statics[__CLASS__]['fields']; + } + + /** + * Return a mapping from field-name to the corresponding key (as used in fields()). + * + * @return array + * Array(string $name => string $uniqueName). + */ + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); + } + return Civi::$statics[__CLASS__]['fieldKeys']; + } + + /** + * Returns the names of this table + * + * @return string + */ + public static function getTableName() { + return self::$_tableName; + } + + /** + * Returns if this table needs to be logged + * + * @return bool + */ + public function getLog() { + return self::$_log; + } + + /** + * Returns the list of fields that can be imported + * + * @param bool $prefix + * + * @return array + */ + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'case_category_features', $prefix, []); + return $r; + } + + /** + * Returns the list of fields that can be exported + * + * @param bool $prefix + * + * @return array + */ + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'case_category_features', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = []; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; + } + +} diff --git a/CRM/Civicase/DAO/CaseCategoryInstance.php b/CRM/Civicase/DAO/CaseCategoryInstance.php index c087599b0..39cecd4f5 100644 --- a/CRM/Civicase/DAO/CaseCategoryInstance.php +++ b/CRM/Civicase/DAO/CaseCategoryInstance.php @@ -4,15 +4,18 @@ * @package CRM * @copyright CiviCRM LLC https://civicrm.org/licensing * - * Generated from /var/www/site2/profiles/compuclient/modules/contrib/civicrm/ext/uk.co.compucorp.civicase/xml/schema/CRM/Civicase/CaseCategoryInstance.xml + * Generated from uk.co.compucorp.civicase/xml/schema/CRM/Civicase/CaseCategoryInstance.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:b15b25c6edb87d09a297cc7861c72b27) + * (GenCodeChecksum:c54be94451d9f9473afcc991619787f0) */ +use CRM_Civicase_ExtensionUtil as E; /** * Database access object for the CaseCategoryInstance entity. */ class CRM_Civicase_DAO_CaseCategoryInstance extends CRM_Core_DAO { + const EXT = E::LONG_NAME; + const TABLE_ADDED = ''; /** * Static instance to hold the table name. @@ -31,21 +34,27 @@ class CRM_Civicase_DAO_CaseCategoryInstance extends CRM_Core_DAO { /** * Unique CaseCategoryInstance Id * - * @var int + * @var int|string|null + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. */ public $id; /** * One of the values of the case_type_categories option group * - * @var int + * @var int|string + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. */ public $category_id; /** * One of the values of the case_category_instance_type option group * - * @var int + * @var int|string + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. */ public $instance_id; @@ -57,6 +66,16 @@ public function __construct() { parent::__construct(); } + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Case Category Instances') : E::ts('Case Category Instance'); + } + /** * Returns all the column names of this table * @@ -68,18 +87,20 @@ public static function &fields() { 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'description' => CRM_Civicase_ExtensionUtil::ts('Unique CaseCategoryInstance Id'), + 'description' => E::ts('Unique CaseCategoryInstance Id'), 'required' => TRUE, 'where' => 'civicrm_case_category_instance.id', 'table_name' => 'civicrm_case_category_instance', 'entity' => 'CaseCategoryInstance', 'bao' => 'CRM_Civicase_DAO_CaseCategoryInstance', 'localizable' => 0, + 'readonly' => TRUE, + 'add' => NULL, ], 'category_id' => [ 'name' => 'category_id', 'type' => CRM_Utils_Type::T_INT, - 'description' => CRM_Civicase_ExtensionUtil::ts('One of the values of the case_type_categories option group'), + 'description' => E::ts('One of the values of the case_type_categories option group'), 'required' => TRUE, 'where' => 'civicrm_case_category_instance.category_id', 'table_name' => 'civicrm_case_category_instance', @@ -90,11 +111,12 @@ public static function &fields() { 'optionGroupName' => 'case_type_categories', 'optionEditPath' => 'civicrm/admin/options/case_type_categories', ], + 'add' => NULL, ], 'instance_id' => [ 'name' => 'instance_id', 'type' => CRM_Utils_Type::T_INT, - 'description' => CRM_Civicase_ExtensionUtil::ts('One of the values of the case_category_instance_type option group'), + 'description' => E::ts('One of the values of the case_category_instance_type option group'), 'required' => TRUE, 'where' => 'civicrm_case_category_instance.instance_id', 'table_name' => 'civicrm_case_category_instance', @@ -105,6 +127,7 @@ public static function &fields() { 'optionGroupName' => 'case_category_instance_type', 'optionEditPath' => 'civicrm/admin/options/case_category_instance_type', ], + 'add' => NULL, ], ]; CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); diff --git a/CRM/Civicase/DAO/CaseContactLock.php b/CRM/Civicase/DAO/CaseContactLock.php index 12e441640..057de4e57 100644 --- a/CRM/Civicase/DAO/CaseContactLock.php +++ b/CRM/Civicase/DAO/CaseContactLock.php @@ -2,50 +2,59 @@ /** * @package CRM - * @copyright CiviCRM LLC (c) 2004-2017 + * @copyright CiviCRM LLC https://civicrm.org/licensing * - * Generated from xml/schema/CRM/Civicase/CaseContactLock.xml + * Generated from uk.co.compucorp.civicase/xml/schema/CRM/Civicase/CaseContactLock.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:db5bf4a479b00ab2f957675c14fbabcc) + * (GenCodeChecksum:df5738a7e2ec72d54c3a5834697213f6) */ +use CRM_Civicase_ExtensionUtil as E; /** * Database access object for the CaseContactLock entity. */ class CRM_Civicase_DAO_CaseContactLock extends CRM_Core_DAO { + const EXT = E::LONG_NAME; + const TABLE_ADDED = '4.7'; /** * Static instance to hold the table name. * * @var string */ - static $_tableName = 'civicase_contactlock'; + public static $_tableName = 'civicase_contactlock'; /** * Should CiviCRM log any modifications to this table in the civicrm_log table. * * @var bool */ - static $_log = TRUE; + public static $_log = TRUE; /** * Unique CaseContactLock ID * - * @var int unsigned + * @var int|string|null + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. */ public $id; /** * Case ID that is locked. * - * @var int unsigned + * @var int|string|null + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. */ public $case_id; /** * Contact for which the case is locked. * - * @var int unsigned + * @var int|string|null + * (SQL type: int unsigned) + * Note that values will be retrieved from the database as a string. */ public $contact_id; @@ -57,6 +66,16 @@ public function __construct() { parent::__construct(); } + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Case Contact Locks') : E::ts('Case Contact Lock'); + } + /** * Returns foreign keys and entity references. * @@ -65,7 +84,7 @@ public function __construct() { */ public static function getReferenceColumns() { if (!isset(Civi::$statics[__CLASS__]['links'])) { - Civi::$statics[__CLASS__]['links'] = static ::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'case_id', 'civicrm_case', 'id'); Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'contact_id', 'civicrm_contact', 'id'); CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); @@ -84,32 +103,39 @@ public static function &fields() { 'id' => [ 'name' => 'id', 'type' => CRM_Utils_Type::T_INT, - 'description' => 'Unique CaseContactLock ID', + 'description' => E::ts('Unique CaseContactLock ID'), 'required' => TRUE, + 'where' => 'civicase_contactlock.id', 'table_name' => 'civicase_contactlock', 'entity' => 'CaseContactLock', 'bao' => 'CRM_Civicase_DAO_CaseContactLock', 'localizable' => 0, + 'readonly' => TRUE, + 'add' => '4.4', ], 'case_id' => [ 'name' => 'case_id', 'type' => CRM_Utils_Type::T_INT, - 'description' => 'Case ID that is locked.', + 'description' => E::ts('Case ID that is locked.'), + 'where' => 'civicase_contactlock.case_id', 'table_name' => 'civicase_contactlock', 'entity' => 'CaseContactLock', 'bao' => 'CRM_Civicase_DAO_CaseContactLock', 'localizable' => 0, 'FKClassName' => 'CRM_Case_DAO_Case', + 'add' => '4.7', ], 'contact_id' => [ 'name' => 'contact_id', 'type' => CRM_Utils_Type::T_INT, - 'description' => 'Contact for which the case is locked.', + 'description' => E::ts('Contact for which the case is locked.'), + 'where' => 'civicase_contactlock.contact_id', 'table_name' => 'civicase_contactlock', 'entity' => 'CaseContactLock', 'bao' => 'CRM_Civicase_DAO_CaseContactLock', 'localizable' => 0, 'FKClassName' => 'CRM_Contact_DAO_Contact', + 'add' => '4.7', ], ]; CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); diff --git a/CRM/Civicase/Upgrader/Steps/Step0019.php b/CRM/Civicase/Upgrader/Steps/Step0019.php index a29add7f5..8d23e4dd4 100644 --- a/CRM/Civicase/Upgrader/Steps/Step0019.php +++ b/CRM/Civicase/Upgrader/Steps/Step0019.php @@ -14,6 +14,9 @@ class CRM_Civicase_Upgrader_Steps_Step0019 { * Return value in boolean. */ public function apply() { + $upgrader = CRM_Civicase_Upgrader_Base::instance(); + $upgrader->executeSqlFile('sql/auto_install.sql'); + (new CaseTypeCategoryManager())->create(); return TRUE; diff --git a/Civi/Api4/CaseCategoryFeatures.php b/Civi/Api4/CaseCategoryFeatures.php new file mode 100644 index 000000000..f8cf59b46 --- /dev/null +++ b/Civi/Api4/CaseCategoryFeatures.php @@ -0,0 +1,16 @@ + [ + 'name' => 'CaseCategoryFeatures', + 'class' => 'CRM_Civicase_DAO_CaseCategoryFeatures', + 'table' => 'civicrm_case_category_features', + ], 'CRM_Civicase_DAO_CaseCategoryInstance' => [ 'name' => 'CaseCategoryInstance', 'class' => 'CRM_Civicase_DAO_CaseCategoryInstance', diff --git a/sql/auto_install.sql b/sql/auto_install.sql index 9a3244980..cca12b36c 100644 --- a/sql/auto_install.sql +++ b/sql/auto_install.sql @@ -29,3 +29,18 @@ CREATE TABLE IF NOT EXISTS `civicrm_case_category_instance` ( PRIMARY KEY (`id`), UNIQUE INDEX `unique_category`(category_id) ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci; + +-- /******************************************************* +-- * +-- * civicrm_case_category_features +-- * +-- * Stores additional features enabled for a case category +-- * +-- *******************************************************/ +CREATE TABLE IF NOT EXISTS `civicrm_case_category_features` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique CaseCategoryFeatures ID', + `category_id` int unsigned NOT NULL COMMENT 'One of the values of the case_type_categories option group', + `feature_id` int unsigned NOT NULL COMMENT 'One of the values of the case_type_category_features option group', + PRIMARY KEY (`id`), + UNIQUE INDEX `unique_category_feature` (category_id, feature_id) +) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci; diff --git a/sql/auto_uninstall.sql b/sql/auto_uninstall.sql index af9bba66c..9f06a7ab9 100644 --- a/sql/auto_uninstall.sql +++ b/sql/auto_uninstall.sql @@ -1,7 +1,22 @@ +-- +--------------------------------------------------------------------+ +-- | Copyright CiviCRM LLC. All rights reserved. | +-- | | +-- | This work is published under the GNU AGPLv3 license with some | +-- | permitted exceptions and without any warranty. For full license | +-- | and copyright information, see https://civicrm.org/licensing | +-- +--------------------------------------------------------------------+ +-- +-- Generated from drop.tpl +-- DO NOT EDIT. Generated by CRM_Core_CodeGen +---- /******************************************************* +-- * +-- * Clean up the existing tables-- * +-- *******************************************************/ + SET FOREIGN_KEY_CHECKS=0; DROP TABLE IF EXISTS `civicase_contactlock`; DROP TABLE IF EXISTS `civicrm_case_category_instance`; -ALTER TABLE `civicrm_case_type` DROP COLUMN `case_type_category`; +DROP TABLE IF EXISTS `civicrm_case_category_features`; SET FOREIGN_KEY_CHECKS=1; \ No newline at end of file diff --git a/xml/schema/CRM/Civicase/CaseCategoryFeatures.entityType.php b/xml/schema/CRM/Civicase/CaseCategoryFeatures.entityType.php new file mode 100644 index 000000000..c9030d554 --- /dev/null +++ b/xml/schema/CRM/Civicase/CaseCategoryFeatures.entityType.php @@ -0,0 +1,17 @@ + 'CaseCategoryFeatures', + 'class' => 'CRM_Civicase_DAO_CaseCategoryFeatures', + 'table' => 'civicrm_case_category_features', + ], +]; diff --git a/xml/schema/CRM/Civicase/CaseCategoryFeatures.xml b/xml/schema/CRM/Civicase/CaseCategoryFeatures.xml new file mode 100644 index 000000000..80e13d60a --- /dev/null +++ b/xml/schema/CRM/Civicase/CaseCategoryFeatures.xml @@ -0,0 +1,43 @@ + + + + CRM/Civicase + CaseCategoryFeatures + civicrm_case_category_features + Stores additional features enabled for a case category + true + + + id + int unsigned + true + Unique CaseCategoryFeatures ID + + Number + + + + id + true + + + + category_id + int unsigned + One of the values of the case_type_categories option group + true + + case_type_categories + + + + + feature_id + int unsigned + One of the values of the case_type_category_features option group + true + + case_type_category_features + + +
From 00644b10f8bbfbbe163b4c79072ea09f771f7e91 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 6 Feb 2023 09:13:15 +0100 Subject: [PATCH 007/199] BTHAB-38: Allow users to en/disable feature for a case category --- .../AddCaseCategoryFeaturesField.php | 99 +++++++++++++++++++ .../Hook/CaseCategoryFormHookBase.php | 5 + .../PostProcess/SaveCaseCategoryFeature.php | 60 +++++++++++ .../Service/CaseTypeCategoryFeatures.php | 25 +++++ civicase.php | 2 + .../Civicase/Form/CaseCategoryFeatures.tpl | 22 +++++ 6 files changed, 213 insertions(+) create mode 100644 CRM/Civicase/Hook/BuildForm/AddCaseCategoryFeaturesField.php create mode 100644 CRM/Civicase/Hook/PostProcess/SaveCaseCategoryFeature.php create mode 100644 CRM/Civicase/Service/CaseTypeCategoryFeatures.php create mode 100644 templates/CRM/Civicase/Form/CaseCategoryFeatures.tpl diff --git a/CRM/Civicase/Hook/BuildForm/AddCaseCategoryFeaturesField.php b/CRM/Civicase/Hook/BuildForm/AddCaseCategoryFeaturesField.php new file mode 100644 index 000000000..69ab32073 --- /dev/null +++ b/CRM/Civicase/Hook/BuildForm/AddCaseCategoryFeaturesField.php @@ -0,0 +1,99 @@ +shouldRun($form, $formName)) { + return; + } + + $this->addCategoryFeaturesFormField($form); + $this->addCategoryFeaturesTemplate(); + } + + /** + * Adds the Case Category Features Form field. + * + * @param CRM_Core_Form $form + * Form Class object. + */ + private function addCategoryFeaturesFormField(CRM_Core_Form &$form) { + $caseCategoryFeatures = new CRM_Civicase_Service_CaseTypeCategoryFeatures(); + $features = []; + + foreach ($caseCategoryFeatures->getFeatures() as $feature) { + $features[] = 'case_category_feature_' . $feature['id']; + $form->add( + 'checkbox', + 'case_category_feature_' . $feature['id'], + $feature['label'] + ); + } + + $form->assign('features', $features); + $this->setDefaultValues($form); + } + + /** + * Adds the template for case category features field template. + */ + private function addCategoryFeaturesTemplate() { + $templatePath = CRM_Civicase_ExtensionUtil::path() . '/templates'; + CRM_Core_Region::instance('page-body')->add( + [ + 'template' => "{$templatePath}/CRM/Civicase/Form/CaseCategoryFeatures.tpl", + ] + ); + } + + /** + * Sets default values. + */ + private function setDefaultValues(CRM_Core_Form &$form) { + if (empty($form->getVar('_id'))) { + return; + } + + $defaults = $form->_defaultValues; + $defaultFeatures = $this->getDefaultFeatures($form); + $form->setDefaults(array_merge($defaults, $defaultFeatures)); + } + + /** + * Returns the default value for the category instance fields. + * + * @param CRM_Core_Form $form + * Form Class object. + * + * @return mixed|null + * Default value. + */ + private function getDefaultFeatures(CRM_Core_Form $form) { + $caseCategory = $form->getVar('_values')['value']; + $enabledFeatures = []; + + $caseCategoryFeatures = CaseCategoryFeatures::get() + ->addWhere('category_id', '=', $caseCategory) + ->execute(); + + foreach ($caseCategoryFeatures as $caseCategoryFeature) { + $enabledFeatures['case_category_feature_' . $caseCategoryFeature['feature_id']] = 1; + } + + return $enabledFeatures; + } + +} diff --git a/CRM/Civicase/Hook/CaseCategoryFormHookBase.php b/CRM/Civicase/Hook/CaseCategoryFormHookBase.php index 7294bcb20..155558345 100644 --- a/CRM/Civicase/Hook/CaseCategoryFormHookBase.php +++ b/CRM/Civicase/Hook/CaseCategoryFormHookBase.php @@ -14,6 +14,11 @@ class CRM_Civicase_Hook_CaseCategoryFormHookBase { */ const INSTANCE_TYPE_FIELD_NAME = 'case_category_instance_type'; + /** + * Instance field name. + */ + const FEATURES_FIELD_NAME = 'case_category_features'; + /** * Determines if the given form is a case type categories form. * diff --git a/CRM/Civicase/Hook/PostProcess/SaveCaseCategoryFeature.php b/CRM/Civicase/Hook/PostProcess/SaveCaseCategoryFeature.php new file mode 100644 index 000000000..52129a679 --- /dev/null +++ b/CRM/Civicase/Hook/PostProcess/SaveCaseCategoryFeature.php @@ -0,0 +1,60 @@ +shouldRun($form, $formName)) { + return; + } + + $caseCategoryValues = $form->getVar('_submitValues'); + $caseCategory = $caseCategoryValues['value']; + + $this->saveCaseCategoryFeature($caseCategory, $caseCategoryValues); + } + + /** + * Saves the case category instance values. + * + * @param int $categoryId + * Case category id. + * @param array $submittedValues + * The key-value pair of submitted values. + */ + private function saveCaseCategoryFeature($categoryId, array $submittedValues) { + // Delete old features link. + CaseCategoryFeatures::delete() + ->addWhere('category_id', '=', $categoryId) + ->execute(); + + // Create new features link. + $caseCategoryFeatures = new CRM_Civicase_Service_CaseTypeCategoryFeatures(); + foreach ($caseCategoryFeatures->getFeatures() as $feature) { + if (!empty($submittedValues['case_category_feature_' . $feature['id']])) { + CaseCategoryFeatures::create() + ->addValue('category_id', $categoryId) + ->addValue('feature_id', $feature['id']) + ->execute(); + } + } + + } + +} diff --git a/CRM/Civicase/Service/CaseTypeCategoryFeatures.php b/CRM/Civicase/Service/CaseTypeCategoryFeatures.php new file mode 100644 index 000000000..7dd022240 --- /dev/null +++ b/CRM/Civicase/Service/CaseTypeCategoryFeatures.php @@ -0,0 +1,25 @@ +addSelect('id', 'label', 'value', 'name', 'option_group_id') + ->addWhere('option_group_id:name', '=', self::NAME) + ->setLimit(25) + ->execute(); + + return $optionValues; + } + +} diff --git a/civicase.php b/civicase.php index b8a923b9a..bc7232337 100644 --- a/civicase.php +++ b/civicase.php @@ -175,6 +175,7 @@ function civicase_civicrm_buildForm($formName, &$form) { new CRM_Civicase_Hook_BuildForm_MakePdfFormSubjectRequired(), new CRM_Civicase_Hook_BuildForm_PdfFormButtonsLabelChange(), new CRM_Civicase_Hook_BuildForm_AddScriptToCreatePdfForm(), + new CRM_Civicase_Hook_BuildForm_AddCaseCategoryFeaturesField(), ]; foreach ($hooks as $hook) { @@ -291,6 +292,7 @@ function civicase_civicrm_postProcess($formName, &$form) { new CRM_Civicase_Hook_PostProcess_AttachEmailActivityToAllCases(), new CRM_Civicase_Hook_PostProcess_HandleDraftActivity(), new CRM_Civicase_Hook_PostProcess_SaveCaseCategoryCustomFields(), + new CRM_Civicase_Hook_PostProcess_SaveCaseCategoryFeature(), ]; foreach ($hooks as $hook) { diff --git a/templates/CRM/Civicase/Form/CaseCategoryFeatures.tpl b/templates/CRM/Civicase/Form/CaseCategoryFeatures.tpl new file mode 100644 index 000000000..be04a172b --- /dev/null +++ b/templates/CRM/Civicase/Form/CaseCategoryFeatures.tpl @@ -0,0 +1,22 @@ + + + + + + + +
Additional Features + + {foreach from=$features item=row} + + + + {/foreach} +
{$form.$row.html} {$form.$row.label}
+
From 7a821f44fe1aae4288ecc8c89364db2e2793b493 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 9 Feb 2023 15:07:46 +0100 Subject: [PATCH 008/199] BTHAB-18: Add upgrader to create quotation status option group --- .../Manage/CaseSalesOrderStatusManager.php | 79 +++++++++++++++++++ CRM/Civicase/Upgrader.php | 5 ++ CRM/Civicase/Upgrader/Steps/Step0019.php | 2 + 3 files changed, 86 insertions(+) create mode 100644 CRM/Civicase/Setup/Manage/CaseSalesOrderStatusManager.php diff --git a/CRM/Civicase/Setup/Manage/CaseSalesOrderStatusManager.php b/CRM/Civicase/Setup/Manage/CaseSalesOrderStatusManager.php new file mode 100644 index 000000000..71d9e6f5c --- /dev/null +++ b/CRM/Civicase/Setup/Manage/CaseSalesOrderStatusManager.php @@ -0,0 +1,79 @@ + self::NAME, + 'title' => ts('Sales Order Status'), + 'is_reserved' => 1, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::NAME, + 'name' => 'new', + 'label' => 'New', + 'is_default' => TRUE, + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::NAME, + 'name' => 'sent_to_client', + 'label' => 'Sent to client', + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::NAME, + 'name' => 'accepted', + 'label' => 'Accepted', + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + + CRM_Core_BAO_OptionValue::ensureOptionValueExists([ + 'option_group_id' => self::NAME, + 'name' => 'declined', + 'label' => 'Declined', + 'is_active' => TRUE, + 'is_reserved' => TRUE, + ]); + } + + /** + * Removes the entity. + */ + public function remove(): void { + civicrm_api3('OptionGroup', 'get', [ + 'return' => ['id'], + 'name' => CRM_Civicase_Service_CaseTypeCategoryFeatures::NAME, + 'api.OptionGroup.delete' => ['id' => '$value.id'], + ]); + } + + /** + * {@inheritDoc} + */ + protected function toggle($status): void { + civicrm_api3('OptionGroup', 'get', [ + 'sequential' => 1, + 'name' => CRM_Civicase_Service_CaseTypeCategoryFeatures::NAME, + 'api.OptionGroup.create' => ['id' => '$value.id', 'is_active' => $status], + ]); + } + +} diff --git a/CRM/Civicase/Upgrader.php b/CRM/Civicase/Upgrader.php index e34ae0214..2e683909b 100644 --- a/CRM/Civicase/Upgrader.php +++ b/CRM/Civicase/Upgrader.php @@ -16,6 +16,7 @@ use CRM_Civicase_ExtensionUtil as E; use CRM_Civicase_Setup_AddMyActivitiesMenu as AddMyActivitiesMenu; use CRM_Civicase_Setup_Manage_CaseTypeCategoryFeaturesManager as CaseTypeCategoryFeaturesManager; +use CRM_Civicase_Setup_Manage_CaseSalesOrderStatusManager as CaseSalesOrderStatusManager; /** * Collection of upgrade steps. @@ -150,6 +151,7 @@ public function install() { $this->createManageCasesMenuItem(); (new CaseTypeCategoryFeaturesManager())->create(); + (new CaseSalesOrderStatusManager())->create(); } /** @@ -247,6 +249,7 @@ public function uninstall() { } (new CaseTypeCategoryFeaturesManager())->remove(); + (new CaseSalesOrderStatusManager())->remove(); } /** @@ -413,6 +416,7 @@ public function enable() { $workflowMenu->apply(); (new CaseTypeCategoryFeaturesManager())->enable(); + (new CaseSalesOrderStatusManager())->enable(); } /** @@ -423,6 +427,7 @@ public function disable() { $this->toggleNav('Manage Cases', FALSE); (new CaseTypeCategoryFeaturesManager())->disable(); + (new CaseSalesOrderStatusManager())->disable(); } /** diff --git a/CRM/Civicase/Upgrader/Steps/Step0019.php b/CRM/Civicase/Upgrader/Steps/Step0019.php index 8d23e4dd4..704ee0e16 100644 --- a/CRM/Civicase/Upgrader/Steps/Step0019.php +++ b/CRM/Civicase/Upgrader/Steps/Step0019.php @@ -1,6 +1,7 @@ executeSqlFile('sql/auto_install.sql'); (new CaseTypeCategoryManager())->create(); + (new CaseSalesOrderStatusManager())->create(); return TRUE; } From 40f65e2f53322c810e02b0553b91f221471d41ec Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 9 Feb 2023 15:12:40 +0100 Subject: [PATCH 009/199] BTHAB-18: Add new entity case_sales_order to store quotations --- CRM/Civicase/BAO/CaseSalesOrder.php | 31 ++ CRM/Civicase/DAO/CaseSalesOrder.php | 492 ++++++++++++++++++ Civi/Api4/CaseSalesOrder.php | 16 + civicase.civix.php | 5 + sql/auto_install.sql | 29 ++ sql/auto_uninstall.sql | 1 + .../Civicase/CaseSalesOrder.entityType.php | 17 + xml/schema/CRM/Civicase/CaseSalesOrder.xml | 171 ++++++ 8 files changed, 762 insertions(+) create mode 100644 CRM/Civicase/BAO/CaseSalesOrder.php create mode 100644 CRM/Civicase/DAO/CaseSalesOrder.php create mode 100644 Civi/Api4/CaseSalesOrder.php create mode 100644 xml/schema/CRM/Civicase/CaseSalesOrder.entityType.php create mode 100644 xml/schema/CRM/Civicase/CaseSalesOrder.xml diff --git a/CRM/Civicase/BAO/CaseSalesOrder.php b/CRM/Civicase/BAO/CaseSalesOrder.php new file mode 100644 index 000000000..69614b82a --- /dev/null +++ b/CRM/Civicase/BAO/CaseSalesOrder.php @@ -0,0 +1,31 @@ +copyValues($params); + $instance->save(); + CRM_Utils_Hook::post($hook, $entityName, $instance->id, $instance); + + return $instance; + } + +} diff --git a/CRM/Civicase/DAO/CaseSalesOrder.php b/CRM/Civicase/DAO/CaseSalesOrder.php new file mode 100644 index 000000000..26aced7de --- /dev/null +++ b/CRM/Civicase/DAO/CaseSalesOrder.php @@ -0,0 +1,492 @@ +__table = 'civicrm_case_sales_order'; + parent::__construct(); + } + + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Case Sales Orders') : E::ts('Case Sales Order'); + } + + /** + * Returns foreign keys and entity references. + * + * @return array + * [CRM_Core_Reference_Interface] + */ + public static function getReferenceColumns() { + if (!isset(Civi::$statics[__CLASS__]['links'])) { + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'client_id', 'civicrm_contact', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'owner_id', 'civicrm_contact', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'case_id', 'civicrm_case', 'id'); + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); + } + return Civi::$statics[__CLASS__]['links']; + } + + /** + * Returns all the column names of this table + * + * @return array + */ + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ + 'name' => 'id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('Unique CaseSalesOrder ID'), + 'required' => TRUE, + 'where' => 'civicrm_case_sales_order.id', + 'table_name' => 'civicrm_case_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'Number', + ], + 'readonly' => TRUE, + 'add' => NULL, + ], + 'client_id' => [ + 'name' => 'client_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to Contact'), + 'where' => 'civicrm_case_sales_order.client_id', + 'table_name' => 'civicrm_case_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'FKClassName' => 'CRM_Contact_DAO_Contact', + 'html' => [ + 'type' => 'EntityRef', + 'label' => E::ts("Client"), + ], + 'add' => NULL, + ], + 'owner_id' => [ + 'name' => 'owner_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to Contact'), + 'where' => 'civicrm_case_sales_order.owner_id', + 'table_name' => 'civicrm_case_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'FKClassName' => 'CRM_Contact_DAO_Contact', + 'html' => [ + 'type' => 'EntityRef', + 'label' => E::ts("Owner"), + ], + 'add' => NULL, + ], + 'case_id' => [ + 'name' => 'case_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to Case'), + 'where' => 'civicrm_case_sales_order.case_id', + 'table_name' => 'civicrm_case_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'FKClassName' => 'CRM_Case_DAO_Case', + 'html' => [ + 'type' => 'EntityRef', + 'label' => E::ts("Case/Opportunity"), + ], + 'add' => NULL, + ], + 'currency' => [ + 'name' => 'currency', + 'type' => CRM_Utils_Type::T_STRING, + 'title' => E::ts('Financial Currency'), + 'description' => E::ts('3 character string, value from config setting or input via user.'), + 'maxlength' => 3, + 'size' => CRM_Utils_Type::FOUR, + 'where' => 'civicrm_case_sales_order.currency', + 'headerPattern' => '/cur(rency)?/i', + 'dataPattern' => '/^[A-Z]{3}$/', + 'default' => NULL, + 'table_name' => 'civicrm_case_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'Select', + ], + 'pseudoconstant' => [ + 'table' => 'civicrm_currency', + 'keyColumn' => 'name', + 'labelColumn' => 'full_name', + 'nameColumn' => 'name', + 'abbrColumn' => 'symbol', + ], + 'add' => NULL, + ], + 'status_id' => [ + 'name' => 'status_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('One of the values of the case_sales_order_status option group'), + 'required' => TRUE, + 'where' => 'civicrm_case_sales_order.status_id', + 'table_name' => 'civicrm_case_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'Select', + 'label' => E::ts("Status"), + ], + 'pseudoconstant' => [ + 'optionGroupName' => 'case_sales_order_status', + 'optionEditPath' => 'civicrm/admin/options/case_sales_order_status', + ], + 'add' => NULL, + ], + 'description' => [ + 'name' => 'description', + 'type' => CRM_Utils_Type::T_TEXT, + 'title' => E::ts('Description'), + 'description' => E::ts('Sales order deesctiption'), + 'required' => FALSE, + 'where' => 'civicrm_case_sales_order.description', + 'table_name' => 'civicrm_case_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'TextArea', + 'label' => E::ts("Description"), + ], + 'add' => NULL, + ], + 'notes' => [ + 'name' => 'notes', + 'type' => CRM_Utils_Type::T_TEXT, + 'title' => E::ts('Notes'), + 'description' => E::ts('Sales order notes'), + 'required' => FALSE, + 'where' => 'civicrm_case_sales_order.notes', + 'table_name' => 'civicrm_case_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'RichTextEditor', + 'label' => E::ts("Notes"), + ], + 'add' => NULL, + ], + 'total_before_tax' => [ + 'name' => 'total_before_tax', + 'type' => CRM_Utils_Type::T_MONEY, + 'title' => E::ts('Total Before Tax'), + 'description' => E::ts('Total amount of the sales order line items before tax deduction.'), + 'required' => FALSE, + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicrm_case_sales_order.total_before_tax', + 'table_name' => 'civicrm_case_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'Text', + ], + 'add' => NULL, + ], + 'total_after_tax' => [ + 'name' => 'total_after_tax', + 'type' => CRM_Utils_Type::T_MONEY, + 'title' => E::ts('Total After Tax'), + 'description' => E::ts('Total amount of the sales order line items after tax deduction.'), + 'required' => FALSE, + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicrm_case_sales_order.total_after_tax', + 'table_name' => 'civicrm_case_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'Text', + ], + 'add' => NULL, + ], + 'quotation_date' => [ + 'name' => 'quotation_date', + 'type' => CRM_Utils_Type::T_TIMESTAMP, + 'title' => E::ts('Quotation Date'), + 'description' => E::ts('Quotation date'), + 'where' => 'civicrm_case_sales_order.quotation_date', + 'table_name' => 'civicrm_case_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'html' => [ + 'type' => 'Select Date', + ], + 'add' => NULL, + ], + 'created_at' => [ + 'name' => 'created_at', + 'type' => CRM_Utils_Type::T_TIMESTAMP, + 'title' => E::ts('Created At'), + 'description' => E::ts('Date the sales order is created'), + 'where' => 'civicrm_case_sales_order.created_at', + 'default' => 'CURRENT_TIMESTAMP', + 'table_name' => 'civicrm_case_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'add' => NULL, + ], + 'is_deleted' => [ + 'name' => 'is_deleted', + 'type' => CRM_Utils_Type::T_BOOLEAN, + 'description' => E::ts('Is this sales order deleted?'), + 'where' => 'civicrm_case_sales_order.is_deleted', + 'default' => '0', + 'table_name' => 'civicrm_case_sales_order', + 'entity' => 'CaseSalesOrder', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'localizable' => 0, + 'add' => NULL, + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); + } + return Civi::$statics[__CLASS__]['fields']; + } + + /** + * Return a mapping from field-name to the corresponding key (as used in fields()). + * + * @return array + * Array(string $name => string $uniqueName). + */ + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); + } + return Civi::$statics[__CLASS__]['fieldKeys']; + } + + /** + * Returns the names of this table + * + * @return string + */ + public static function getTableName() { + return self::$_tableName; + } + + /** + * Returns if this table needs to be logged + * + * @return bool + */ + public function getLog() { + return self::$_log; + } + + /** + * Returns the list of fields that can be imported + * + * @param bool $prefix + * + * @return array + */ + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'case_sales_order', $prefix, []); + return $r; + } + + /** + * Returns the list of fields that can be exported + * + * @param bool $prefix + * + * @return array + */ + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'case_sales_order', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = []; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; + } + +} diff --git a/Civi/Api4/CaseSalesOrder.php b/Civi/Api4/CaseSalesOrder.php new file mode 100644 index 000000000..bebff5e2d --- /dev/null +++ b/Civi/Api4/CaseSalesOrder.php @@ -0,0 +1,16 @@ + 'CRM_Civicase_DAO_CaseContactLock', 'table' => 'civicase_contactlock', ], + 'CRM_Civicase_DAO_CaseSalesOrder' => [ + 'name' => 'CaseSalesOrder', + 'class' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'table' => 'civicrm_case_sales_order', + ], ]); } diff --git a/sql/auto_install.sql b/sql/auto_install.sql index cca12b36c..4e9b7a7e5 100644 --- a/sql/auto_install.sql +++ b/sql/auto_install.sql @@ -44,3 +44,32 @@ CREATE TABLE IF NOT EXISTS `civicrm_case_category_features` ( PRIMARY KEY (`id`), UNIQUE INDEX `unique_category_feature` (category_id, feature_id) ) ENGINE=InnoDB DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci; + + +-- /******************************************************* +-- * +-- * civicrm_case_sales_order +-- * +-- * Sales order that represents quotations +-- * +-- *******************************************************/ +CREATE TABLE `civicrm_case_sales_order` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique CaseSalesOrder ID', + `client_id` int unsigned COMMENT 'FK to Contact', + `owner_id` int unsigned COMMENT 'FK to Contact', + `case_id` int unsigned COMMENT 'FK to Case', + `currency` varchar(3) DEFAULT NULL COMMENT '3 character string, value from config setting or input via user.', + `status_id` int unsigned NOT NULL COMMENT 'One of the values of the case_sales_order_status option group', + `description` text NULL COMMENT 'Sales order deesctiption', + `notes` text NULL COMMENT 'Sales order notes', + `total_before_tax` decimal(20,2) NULL COMMENT 'Total amount of the sales order line items before tax deduction.', + `total_after_tax` decimal(20,2) NULL COMMENT 'Total amount of the sales order line items after tax deduction.', + `quotation_date` timestamp COMMENT 'Quotation date', + `created_at` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT 'Date the sales order is created', + `is_deleted` tinyint DEFAULT 0 COMMENT 'Is this sales order deleted?', + PRIMARY KEY (`id`), + CONSTRAINT FK_civicrm_case_sales_order_client_id FOREIGN KEY (`client_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE, + CONSTRAINT FK_civicrm_case_sales_order_owner_id FOREIGN KEY (`owner_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE, + CONSTRAINT FK_civicrm_case_sales_order_case_id FOREIGN KEY (`case_id`) REFERENCES `civicrm_case`(`id`) ON DELETE CASCADE +) +ENGINE=InnoDB; diff --git a/sql/auto_uninstall.sql b/sql/auto_uninstall.sql index 9f06a7ab9..50227090c 100644 --- a/sql/auto_uninstall.sql +++ b/sql/auto_uninstall.sql @@ -15,6 +15,7 @@ SET FOREIGN_KEY_CHECKS=0; +DROP TABLE IF EXISTS `civicrm_case_sales_order_line`; DROP TABLE IF EXISTS `civicase_contactlock`; DROP TABLE IF EXISTS `civicrm_case_category_instance`; DROP TABLE IF EXISTS `civicrm_case_category_features`; diff --git a/xml/schema/CRM/Civicase/CaseSalesOrder.entityType.php b/xml/schema/CRM/Civicase/CaseSalesOrder.entityType.php new file mode 100644 index 000000000..f4d758a7f --- /dev/null +++ b/xml/schema/CRM/Civicase/CaseSalesOrder.entityType.php @@ -0,0 +1,17 @@ + 'CaseSalesOrder', + 'class' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'table' => 'civicrm_case_sales_order', + ], +]; diff --git a/xml/schema/CRM/Civicase/CaseSalesOrder.xml b/xml/schema/CRM/Civicase/CaseSalesOrder.xml new file mode 100644 index 000000000..343d00148 --- /dev/null +++ b/xml/schema/CRM/Civicase/CaseSalesOrder.xml @@ -0,0 +1,171 @@ + + + + CRM/Civicase + CaseSalesOrder + civicrm_case_sales_order + Sales order that represents quotations + true + + + id + int unsigned + true + Unique CaseSalesOrder ID + + Number + + + + id + true + + + + client_id + int unsigned + FK to Contact + + + EntityRef + + + + client_id +
civicrm_contact
+ id + CASCADE + + + + owner_id + int unsigned + FK to Contact + + + EntityRef + + + + owner_id + civicrm_contact
+ id + CASCADE +
+ + + case_id + int unsigned + FK to Case + + + EntityRef + + + + case_id + civicrm_case
+ id + CASCADE +
+ + + currency + Financial Currency + varchar + 3 + NULL + /cur(rency)?/i + /^[A-Z]{3}$/ + 3 character string, value from config setting or input via user. + + civicrm_currency
+ name + full_name + name + symbol +
+ + Select + +
+ + + status_id + int unsigned + One of the values of the case_sales_order_status option group + true + + case_sales_order_status + + + + Select + + + + + description + text + false + Sales order deesctiption + + + TextArea + + + + + notes + text + false + Sales order notes + + + RichTextEditor + + + + + total_before_tax + decimal + false + Total amount of the sales order line items before tax deduction. + + Text + + + + + total_after_tax + decimal + false + Total amount of the sales order line items after tax deduction. + + Text + + + + + quotation_date + timestamp + Quotation date + + Select Date + + + + + created_at + timestamp + Date the sales order is created + CURRENT_TIMESTAMP + + + + is_deleted + boolean + 0 + Is this sales order deleted? + + From 5702ce13ea2705b988978df1454dc1965a1ae3c1 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 9 Feb 2023 15:23:37 +0100 Subject: [PATCH 010/199] BTHAB-18: Add new entity to store quotation line items --- CRM/Civicase/BAO/CaseSalesOrderLine.php | 31 ++ CRM/Civicase/DAO/CaseSalesOrderLine.php | 418 ++++++++++++++++++ Civi/Api4/CaseSalesOrderLine.php | 16 + civicase.civix.php | 5 + sql/auto_install.sql | 25 ++ sql/auto_uninstall.sql | 1 + .../CaseSalesOrderLine.entityType.php | 17 + .../CRM/Civicase/CaseSalesOrderLine.xml | 136 ++++++ 8 files changed, 649 insertions(+) create mode 100644 CRM/Civicase/BAO/CaseSalesOrderLine.php create mode 100644 CRM/Civicase/DAO/CaseSalesOrderLine.php create mode 100644 Civi/Api4/CaseSalesOrderLine.php create mode 100644 xml/schema/CRM/Civicase/CaseSalesOrderLine.entityType.php create mode 100644 xml/schema/CRM/Civicase/CaseSalesOrderLine.xml diff --git a/CRM/Civicase/BAO/CaseSalesOrderLine.php b/CRM/Civicase/BAO/CaseSalesOrderLine.php new file mode 100644 index 000000000..c48676d53 --- /dev/null +++ b/CRM/Civicase/BAO/CaseSalesOrderLine.php @@ -0,0 +1,31 @@ +copyValues($params); + $instance->save(); + CRM_Utils_Hook::post($hook, $entityName, $instance->id, $instance); + + return $instance; + } + +} diff --git a/CRM/Civicase/DAO/CaseSalesOrderLine.php b/CRM/Civicase/DAO/CaseSalesOrderLine.php new file mode 100644 index 000000000..7f7bf67d7 --- /dev/null +++ b/CRM/Civicase/DAO/CaseSalesOrderLine.php @@ -0,0 +1,418 @@ +__table = 'civicrm_case_sales_order_line'; + parent::__construct(); + } + + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Case Sales Order Lines') : E::ts('Case Sales Order Line'); + } + + /** + * Returns foreign keys and entity references. + * + * @return array + * [CRM_Core_Reference_Interface] + */ + public static function getReferenceColumns() { + if (!isset(Civi::$statics[__CLASS__]['links'])) { + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'sales_order_id', 'civicrm_case_sales_order', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'financial_type_id', 'civicrm_financial_type', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'product_id', 'civicrm_product', 'id'); + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); + } + return Civi::$statics[__CLASS__]['links']; + } + + /** + * Returns all the column names of this table + * + * @return array + */ + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ + 'name' => 'id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('Unique CaseSalesOrderLine ID'), + 'required' => TRUE, + 'where' => 'civicrm_case_sales_order_line.id', + 'table_name' => 'civicrm_case_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'html' => [ + 'type' => 'Number', + ], + 'readonly' => TRUE, + 'add' => NULL, + ], + 'sales_order_id' => [ + 'name' => 'sales_order_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to CaseSalesOrder'), + 'where' => 'civicrm_case_sales_order_line.sales_order_id', + 'table_name' => 'civicrm_case_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'FKClassName' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'html' => [ + 'type' => 'EntityRef', + ], + 'add' => NULL, + ], + 'financial_type_id' => [ + 'name' => 'financial_type_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to CiviCRM Financial Type'), + 'where' => 'civicrm_case_sales_order_line.financial_type_id', + 'table_name' => 'civicrm_case_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'FKClassName' => 'CRM_Financial_DAO_FinancialType', + 'html' => [ + 'type' => 'EntityRef', + 'label' => E::ts("Financial Type"), + ], + 'add' => NULL, + ], + 'product_id' => [ + 'name' => 'product_id', + 'type' => CRM_Utils_Type::T_INT, + 'title' => E::ts('Product ID'), + 'where' => 'civicrm_case_sales_order_line.product_id', + 'table_name' => 'civicrm_case_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'FKClassName' => 'CRM_Contribute_DAO_Product', + 'html' => [ + 'type' => 'EntityRef', + 'label' => E::ts("Product"), + ], + 'add' => NULL, + ], + 'item_description' => [ + 'name' => 'item_description', + 'type' => CRM_Utils_Type::T_TEXT, + 'title' => E::ts('Item Description'), + 'description' => E::ts('line item deesctiption'), + 'required' => FALSE, + 'where' => 'civicrm_case_sales_order_line.item_description', + 'table_name' => 'civicrm_case_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'html' => [ + 'type' => 'TextArea', + 'label' => E::ts("Item Description"), + ], + 'add' => NULL, + ], + 'quantity' => [ + 'name' => 'quantity', + 'type' => CRM_Utils_Type::T_MONEY, + 'title' => E::ts('Quantity'), + 'description' => E::ts('Quantity'), + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicrm_case_sales_order_line.quantity', + 'table_name' => 'civicrm_case_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'html' => [ + 'type' => 'Text', + 'label' => E::ts("Quantity"), + ], + 'add' => NULL, + ], + 'unit_price' => [ + 'name' => 'unit_price', + 'type' => CRM_Utils_Type::T_MONEY, + 'title' => E::ts('Unit Price'), + 'description' => E::ts('Unit Price'), + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicrm_case_sales_order_line.unit_price', + 'table_name' => 'civicrm_case_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'html' => [ + 'type' => 'Text', + 'label' => E::ts("Unit Price"), + ], + 'add' => NULL, + ], + 'tax_rate' => [ + 'name' => 'tax_rate', + 'type' => CRM_Utils_Type::T_MONEY, + 'title' => E::ts('Tax Rate'), + 'description' => E::ts('Tax rate for the line item'), + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicrm_case_sales_order_line.tax_rate', + 'table_name' => 'civicrm_case_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'html' => [ + 'type' => 'Text', + 'label' => E::ts("Tax"), + ], + 'add' => NULL, + ], + 'discounted_percentage' => [ + 'name' => 'discounted_percentage', + 'type' => CRM_Utils_Type::T_MONEY, + 'title' => E::ts('Discounted Percentage'), + 'description' => E::ts('Discount applied to the line item'), + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicrm_case_sales_order_line.discounted_percentage', + 'table_name' => 'civicrm_case_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'html' => [ + 'type' => 'Text', + 'label' => E::ts("Discount"), + ], + 'add' => NULL, + ], + 'subtotal_amount' => [ + 'name' => 'subtotal_amount', + 'type' => CRM_Utils_Type::T_MONEY, + 'title' => E::ts('Subtotal Amount'), + 'description' => E::ts('Quantity x Unit Price x (100-Discount)%'), + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicrm_case_sales_order_line.subtotal_amount', + 'table_name' => 'civicrm_case_sales_order_line', + 'entity' => 'CaseSalesOrderLine', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'localizable' => 0, + 'html' => [ + 'type' => 'Text', + 'label' => E::ts("Subtotal"), + ], + 'add' => NULL, + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); + } + return Civi::$statics[__CLASS__]['fields']; + } + + /** + * Return a mapping from field-name to the corresponding key (as used in fields()). + * + * @return array + * Array(string $name => string $uniqueName). + */ + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); + } + return Civi::$statics[__CLASS__]['fieldKeys']; + } + + /** + * Returns the names of this table + * + * @return string + */ + public static function getTableName() { + return self::$_tableName; + } + + /** + * Returns if this table needs to be logged + * + * @return bool + */ + public function getLog() { + return self::$_log; + } + + /** + * Returns the list of fields that can be imported + * + * @param bool $prefix + * + * @return array + */ + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'case_sales_order_line', $prefix, []); + return $r; + } + + /** + * Returns the list of fields that can be exported + * + * @param bool $prefix + * + * @return array + */ + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'case_sales_order_line', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = []; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; + } + +} diff --git a/Civi/Api4/CaseSalesOrderLine.php b/Civi/Api4/CaseSalesOrderLine.php new file mode 100644 index 000000000..d1f999001 --- /dev/null +++ b/Civi/Api4/CaseSalesOrderLine.php @@ -0,0 +1,16 @@ + 'CRM_Civicase_DAO_CaseSalesOrder', 'table' => 'civicrm_case_sales_order', ], + 'CRM_Civicase_DAO_CaseSalesOrderLine' => [ + 'name' => 'CaseSalesOrderLine', + 'class' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'table' => 'civicrm_case_sales_order_line', + ], ]); } diff --git a/sql/auto_install.sql b/sql/auto_install.sql index 4e9b7a7e5..f60284e6f 100644 --- a/sql/auto_install.sql +++ b/sql/auto_install.sql @@ -73,3 +73,28 @@ CREATE TABLE `civicrm_case_sales_order` ( CONSTRAINT FK_civicrm_case_sales_order_case_id FOREIGN KEY (`case_id`) REFERENCES `civicrm_case`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB; + +-- /******************************************************* +-- * +-- * civicrm_case_sales_order_line +-- * +-- * Sales order line items +-- * +-- *******************************************************/ +CREATE TABLE `civicrm_case_sales_order_line` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique CaseSalesOrderLine ID', + `sales_order_id` int unsigned COMMENT 'FK to CaseSalesOrder', + `financial_type_id` int unsigned COMMENT 'FK to CiviCRM Financial Type', + `product_id` int unsigned, + `item_description` text NULL COMMENT 'line item deesctiption', + `quantity` decimal(20,2) COMMENT 'Quantity', + `unit_price` decimal(20,2) COMMENT 'Unit Price', + `tax_rate` decimal(20,2) COMMENT 'Tax rate for the line item', + `discounted_percentage` decimal(20,2) COMMENT 'Discount applied to the line item', + `subtotal_amount` decimal(20,2) COMMENT 'Quantity x Unit Price x (100-Discount)%', + PRIMARY KEY (`id`), + CONSTRAINT FK_civicrm_case_sales_order_line_sales_order_id FOREIGN KEY (`sales_order_id`) REFERENCES `civicrm_case_sales_order`(`id`) ON DELETE CASCADE, + CONSTRAINT FK_civicrm_case_sales_order_line_financial_type_id FOREIGN KEY (`financial_type_id`) REFERENCES `civicrm_financial_type`(`id`) ON DELETE SET NULL, + CONSTRAINT FK_civicrm_case_sales_order_line_product_id FOREIGN KEY (`product_id`) REFERENCES `civicrm_product`(`id`) ON DELETE SET NULL +) +ENGINE=InnoDB; diff --git a/sql/auto_uninstall.sql b/sql/auto_uninstall.sql index 50227090c..7eca6f5b2 100644 --- a/sql/auto_uninstall.sql +++ b/sql/auto_uninstall.sql @@ -16,6 +16,7 @@ SET FOREIGN_KEY_CHECKS=0; DROP TABLE IF EXISTS `civicrm_case_sales_order_line`; +DROP TABLE IF EXISTS `civicrm_case_sales_order`; DROP TABLE IF EXISTS `civicase_contactlock`; DROP TABLE IF EXISTS `civicrm_case_category_instance`; DROP TABLE IF EXISTS `civicrm_case_category_features`; diff --git a/xml/schema/CRM/Civicase/CaseSalesOrderLine.entityType.php b/xml/schema/CRM/Civicase/CaseSalesOrderLine.entityType.php new file mode 100644 index 000000000..40f068780 --- /dev/null +++ b/xml/schema/CRM/Civicase/CaseSalesOrderLine.entityType.php @@ -0,0 +1,17 @@ + 'CaseSalesOrderLine', + 'class' => 'CRM_Civicase_DAO_CaseSalesOrderLine', + 'table' => 'civicrm_case_sales_order_line', + ], +]; diff --git a/xml/schema/CRM/Civicase/CaseSalesOrderLine.xml b/xml/schema/CRM/Civicase/CaseSalesOrderLine.xml new file mode 100644 index 000000000..628431df6 --- /dev/null +++ b/xml/schema/CRM/Civicase/CaseSalesOrderLine.xml @@ -0,0 +1,136 @@ + + + + CRM/Civicase + CaseSalesOrderLine + civicrm_case_sales_order_line + Sales order line items + true + + + id + int unsigned + true + Unique CaseSalesOrderLine ID + + Number + + + + id + true + + + + sales_order_id + int unsigned + FK to CaseSalesOrder + + EntityRef + + + + sales_order_id +
civicrm_case_sales_order
+ id + CASCADE + + + + financial_type_id + int unsigned + FK to CiviCRM Financial Type + + + EntityRef + + + + civicrm_financial_type
+ id + name +
+ + financial_type_id + civicrm_financial_type
+ id + SET NULL +
+ + + product_id + Product ID + int unsigned + + + EntityRef + + + + product_id + civicrm_product
+ id + SET NULL +
+ + + item_description + text + false + line item deesctiption + + + TextArea + + + + + quantity + decimal + Quantity + + + Text + + + + + unit_price + decimal + Unit Price + + + Text + + + + + tax_rate + decimal + Tax rate for the line item + + + Text + + + + + discounted_percentage + decimal + Discount applied to the line item + + + Text + + + + + subtotal_amount + decimal + Quantity x Unit Price x (100-Discount)% + + + Text + + + From cbd85f894c9e078969f8f68952c18b60e33bbc81 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 13 Feb 2023 11:58:22 +0100 Subject: [PATCH 011/199] BTHAB-18: Prefix table mame with extension name --- CRM/Civicase/DAO/CaseSalesOrder.php | 62 +++++++++---------- CRM/Civicase/DAO/CaseSalesOrderLine.php | 52 ++++++++-------- civicase.civix.php | 4 +- sql/auto_install.sql | 20 +++--- sql/auto_uninstall.sql | 4 +- .../Civicase/CaseSalesOrder.entityType.php | 2 +- xml/schema/CRM/Civicase/CaseSalesOrder.xml | 2 +- .../CaseSalesOrderLine.entityType.php | 2 +- .../CRM/Civicase/CaseSalesOrderLine.xml | 4 +- 9 files changed, 76 insertions(+), 76 deletions(-) diff --git a/CRM/Civicase/DAO/CaseSalesOrder.php b/CRM/Civicase/DAO/CaseSalesOrder.php index 26aced7de..d1aa64033 100644 --- a/CRM/Civicase/DAO/CaseSalesOrder.php +++ b/CRM/Civicase/DAO/CaseSalesOrder.php @@ -6,7 +6,7 @@ * * Generated from uk.co.compucorp.civicase/xml/schema/CRM/Civicase/CaseSalesOrder.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:34ab136dd14fa518532eaecfb09112c6) + * (GenCodeChecksum:363627d2e3cd7f868f0cc2c5b89fb95b) */ use CRM_Civicase_ExtensionUtil as E; @@ -22,7 +22,7 @@ class CRM_Civicase_DAO_CaseSalesOrder extends CRM_Core_DAO { * * @var string */ - public static $_tableName = 'civicrm_case_sales_order'; + public static $_tableName = 'civicase_sales_order'; /** * Should CiviCRM log any modifications to this table in the civicrm_log table. @@ -152,7 +152,7 @@ class CRM_Civicase_DAO_CaseSalesOrder extends CRM_Core_DAO { * Class constructor. */ public function __construct() { - $this->__table = 'civicrm_case_sales_order'; + $this->__table = 'civicase_sales_order'; parent::__construct(); } @@ -196,8 +196,8 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'description' => E::ts('Unique CaseSalesOrder ID'), 'required' => TRUE, - 'where' => 'civicrm_case_sales_order.id', - 'table_name' => 'civicrm_case_sales_order', + 'where' => 'civicase_sales_order.id', + 'table_name' => 'civicase_sales_order', 'entity' => 'CaseSalesOrder', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', 'localizable' => 0, @@ -211,8 +211,8 @@ public static function &fields() { 'name' => 'client_id', 'type' => CRM_Utils_Type::T_INT, 'description' => E::ts('FK to Contact'), - 'where' => 'civicrm_case_sales_order.client_id', - 'table_name' => 'civicrm_case_sales_order', + 'where' => 'civicase_sales_order.client_id', + 'table_name' => 'civicase_sales_order', 'entity' => 'CaseSalesOrder', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', 'localizable' => 0, @@ -227,8 +227,8 @@ public static function &fields() { 'name' => 'owner_id', 'type' => CRM_Utils_Type::T_INT, 'description' => E::ts('FK to Contact'), - 'where' => 'civicrm_case_sales_order.owner_id', - 'table_name' => 'civicrm_case_sales_order', + 'where' => 'civicase_sales_order.owner_id', + 'table_name' => 'civicase_sales_order', 'entity' => 'CaseSalesOrder', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', 'localizable' => 0, @@ -243,8 +243,8 @@ public static function &fields() { 'name' => 'case_id', 'type' => CRM_Utils_Type::T_INT, 'description' => E::ts('FK to Case'), - 'where' => 'civicrm_case_sales_order.case_id', - 'table_name' => 'civicrm_case_sales_order', + 'where' => 'civicase_sales_order.case_id', + 'table_name' => 'civicase_sales_order', 'entity' => 'CaseSalesOrder', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', 'localizable' => 0, @@ -262,11 +262,11 @@ public static function &fields() { 'description' => E::ts('3 character string, value from config setting or input via user.'), 'maxlength' => 3, 'size' => CRM_Utils_Type::FOUR, - 'where' => 'civicrm_case_sales_order.currency', + 'where' => 'civicase_sales_order.currency', 'headerPattern' => '/cur(rency)?/i', 'dataPattern' => '/^[A-Z]{3}$/', 'default' => NULL, - 'table_name' => 'civicrm_case_sales_order', + 'table_name' => 'civicase_sales_order', 'entity' => 'CaseSalesOrder', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', 'localizable' => 0, @@ -287,8 +287,8 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'description' => E::ts('One of the values of the case_sales_order_status option group'), 'required' => TRUE, - 'where' => 'civicrm_case_sales_order.status_id', - 'table_name' => 'civicrm_case_sales_order', + 'where' => 'civicase_sales_order.status_id', + 'table_name' => 'civicase_sales_order', 'entity' => 'CaseSalesOrder', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', 'localizable' => 0, @@ -308,8 +308,8 @@ public static function &fields() { 'title' => E::ts('Description'), 'description' => E::ts('Sales order deesctiption'), 'required' => FALSE, - 'where' => 'civicrm_case_sales_order.description', - 'table_name' => 'civicrm_case_sales_order', + 'where' => 'civicase_sales_order.description', + 'table_name' => 'civicase_sales_order', 'entity' => 'CaseSalesOrder', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', 'localizable' => 0, @@ -325,8 +325,8 @@ public static function &fields() { 'title' => E::ts('Notes'), 'description' => E::ts('Sales order notes'), 'required' => FALSE, - 'where' => 'civicrm_case_sales_order.notes', - 'table_name' => 'civicrm_case_sales_order', + 'where' => 'civicase_sales_order.notes', + 'table_name' => 'civicase_sales_order', 'entity' => 'CaseSalesOrder', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', 'localizable' => 0, @@ -346,8 +346,8 @@ public static function &fields() { 20, 2, ], - 'where' => 'civicrm_case_sales_order.total_before_tax', - 'table_name' => 'civicrm_case_sales_order', + 'where' => 'civicase_sales_order.total_before_tax', + 'table_name' => 'civicase_sales_order', 'entity' => 'CaseSalesOrder', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', 'localizable' => 0, @@ -366,8 +366,8 @@ public static function &fields() { 20, 2, ], - 'where' => 'civicrm_case_sales_order.total_after_tax', - 'table_name' => 'civicrm_case_sales_order', + 'where' => 'civicase_sales_order.total_after_tax', + 'table_name' => 'civicase_sales_order', 'entity' => 'CaseSalesOrder', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', 'localizable' => 0, @@ -381,8 +381,8 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_TIMESTAMP, 'title' => E::ts('Quotation Date'), 'description' => E::ts('Quotation date'), - 'where' => 'civicrm_case_sales_order.quotation_date', - 'table_name' => 'civicrm_case_sales_order', + 'where' => 'civicase_sales_order.quotation_date', + 'table_name' => 'civicase_sales_order', 'entity' => 'CaseSalesOrder', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', 'localizable' => 0, @@ -396,9 +396,9 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_TIMESTAMP, 'title' => E::ts('Created At'), 'description' => E::ts('Date the sales order is created'), - 'where' => 'civicrm_case_sales_order.created_at', + 'where' => 'civicase_sales_order.created_at', 'default' => 'CURRENT_TIMESTAMP', - 'table_name' => 'civicrm_case_sales_order', + 'table_name' => 'civicase_sales_order', 'entity' => 'CaseSalesOrder', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', 'localizable' => 0, @@ -408,9 +408,9 @@ public static function &fields() { 'name' => 'is_deleted', 'type' => CRM_Utils_Type::T_BOOLEAN, 'description' => E::ts('Is this sales order deleted?'), - 'where' => 'civicrm_case_sales_order.is_deleted', + 'where' => 'civicase_sales_order.is_deleted', 'default' => '0', - 'table_name' => 'civicrm_case_sales_order', + 'table_name' => 'civicase_sales_order', 'entity' => 'CaseSalesOrder', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder', 'localizable' => 0, @@ -461,7 +461,7 @@ public function getLog() { * @return array */ public static function &import($prefix = FALSE) { - $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'case_sales_order', $prefix, []); + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, '_sales_order', $prefix, []); return $r; } @@ -473,7 +473,7 @@ public static function &import($prefix = FALSE) { * @return array */ public static function &export($prefix = FALSE) { - $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'case_sales_order', $prefix, []); + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, '_sales_order', $prefix, []); return $r; } diff --git a/CRM/Civicase/DAO/CaseSalesOrderLine.php b/CRM/Civicase/DAO/CaseSalesOrderLine.php index 7f7bf67d7..d855e713b 100644 --- a/CRM/Civicase/DAO/CaseSalesOrderLine.php +++ b/CRM/Civicase/DAO/CaseSalesOrderLine.php @@ -6,7 +6,7 @@ * * Generated from uk.co.compucorp.civicase/xml/schema/CRM/Civicase/CaseSalesOrderLine.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:68a85389455a42d654dbb49ce271ada4) + * (GenCodeChecksum:4de7e55e8e6d9992c60cc1dd9035336f) */ use CRM_Civicase_ExtensionUtil as E; @@ -22,7 +22,7 @@ class CRM_Civicase_DAO_CaseSalesOrderLine extends CRM_Core_DAO { * * @var string */ - public static $_tableName = 'civicrm_case_sales_order_line'; + public static $_tableName = 'civicase_sales_order_line'; /** * Should CiviCRM log any modifications to this table in the civicrm_log table. @@ -123,7 +123,7 @@ class CRM_Civicase_DAO_CaseSalesOrderLine extends CRM_Core_DAO { * Class constructor. */ public function __construct() { - $this->__table = 'civicrm_case_sales_order_line'; + $this->__table = 'civicase_sales_order_line'; parent::__construct(); } @@ -146,7 +146,7 @@ public static function getEntityTitle($plural = FALSE) { public static function getReferenceColumns() { if (!isset(Civi::$statics[__CLASS__]['links'])) { Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); - Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'sales_order_id', 'civicrm_case_sales_order', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'sales_order_id', 'civicase_sales_order', 'id'); Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'financial_type_id', 'civicrm_financial_type', 'id'); Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'product_id', 'civicrm_product', 'id'); CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); @@ -167,8 +167,8 @@ public static function &fields() { 'type' => CRM_Utils_Type::T_INT, 'description' => E::ts('Unique CaseSalesOrderLine ID'), 'required' => TRUE, - 'where' => 'civicrm_case_sales_order_line.id', - 'table_name' => 'civicrm_case_sales_order_line', + 'where' => 'civicase_sales_order_line.id', + 'table_name' => 'civicase_sales_order_line', 'entity' => 'CaseSalesOrderLine', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', 'localizable' => 0, @@ -182,8 +182,8 @@ public static function &fields() { 'name' => 'sales_order_id', 'type' => CRM_Utils_Type::T_INT, 'description' => E::ts('FK to CaseSalesOrder'), - 'where' => 'civicrm_case_sales_order_line.sales_order_id', - 'table_name' => 'civicrm_case_sales_order_line', + 'where' => 'civicase_sales_order_line.sales_order_id', + 'table_name' => 'civicase_sales_order_line', 'entity' => 'CaseSalesOrderLine', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', 'localizable' => 0, @@ -197,8 +197,8 @@ public static function &fields() { 'name' => 'financial_type_id', 'type' => CRM_Utils_Type::T_INT, 'description' => E::ts('FK to CiviCRM Financial Type'), - 'where' => 'civicrm_case_sales_order_line.financial_type_id', - 'table_name' => 'civicrm_case_sales_order_line', + 'where' => 'civicase_sales_order_line.financial_type_id', + 'table_name' => 'civicase_sales_order_line', 'entity' => 'CaseSalesOrderLine', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', 'localizable' => 0, @@ -213,8 +213,8 @@ public static function &fields() { 'name' => 'product_id', 'type' => CRM_Utils_Type::T_INT, 'title' => E::ts('Product ID'), - 'where' => 'civicrm_case_sales_order_line.product_id', - 'table_name' => 'civicrm_case_sales_order_line', + 'where' => 'civicase_sales_order_line.product_id', + 'table_name' => 'civicase_sales_order_line', 'entity' => 'CaseSalesOrderLine', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', 'localizable' => 0, @@ -231,8 +231,8 @@ public static function &fields() { 'title' => E::ts('Item Description'), 'description' => E::ts('line item deesctiption'), 'required' => FALSE, - 'where' => 'civicrm_case_sales_order_line.item_description', - 'table_name' => 'civicrm_case_sales_order_line', + 'where' => 'civicase_sales_order_line.item_description', + 'table_name' => 'civicase_sales_order_line', 'entity' => 'CaseSalesOrderLine', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', 'localizable' => 0, @@ -251,8 +251,8 @@ public static function &fields() { 20, 2, ], - 'where' => 'civicrm_case_sales_order_line.quantity', - 'table_name' => 'civicrm_case_sales_order_line', + 'where' => 'civicase_sales_order_line.quantity', + 'table_name' => 'civicase_sales_order_line', 'entity' => 'CaseSalesOrderLine', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', 'localizable' => 0, @@ -271,8 +271,8 @@ public static function &fields() { 20, 2, ], - 'where' => 'civicrm_case_sales_order_line.unit_price', - 'table_name' => 'civicrm_case_sales_order_line', + 'where' => 'civicase_sales_order_line.unit_price', + 'table_name' => 'civicase_sales_order_line', 'entity' => 'CaseSalesOrderLine', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', 'localizable' => 0, @@ -291,8 +291,8 @@ public static function &fields() { 20, 2, ], - 'where' => 'civicrm_case_sales_order_line.tax_rate', - 'table_name' => 'civicrm_case_sales_order_line', + 'where' => 'civicase_sales_order_line.tax_rate', + 'table_name' => 'civicase_sales_order_line', 'entity' => 'CaseSalesOrderLine', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', 'localizable' => 0, @@ -311,8 +311,8 @@ public static function &fields() { 20, 2, ], - 'where' => 'civicrm_case_sales_order_line.discounted_percentage', - 'table_name' => 'civicrm_case_sales_order_line', + 'where' => 'civicase_sales_order_line.discounted_percentage', + 'table_name' => 'civicase_sales_order_line', 'entity' => 'CaseSalesOrderLine', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', 'localizable' => 0, @@ -331,8 +331,8 @@ public static function &fields() { 20, 2, ], - 'where' => 'civicrm_case_sales_order_line.subtotal_amount', - 'table_name' => 'civicrm_case_sales_order_line', + 'where' => 'civicase_sales_order_line.subtotal_amount', + 'table_name' => 'civicase_sales_order_line', 'entity' => 'CaseSalesOrderLine', 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderLine', 'localizable' => 0, @@ -387,7 +387,7 @@ public function getLog() { * @return array */ public static function &import($prefix = FALSE) { - $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'case_sales_order_line', $prefix, []); + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, '_sales_order_line', $prefix, []); return $r; } @@ -399,7 +399,7 @@ public static function &import($prefix = FALSE) { * @return array */ public static function &export($prefix = FALSE) { - $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'case_sales_order_line', $prefix, []); + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, '_sales_order_line', $prefix, []); return $r; } diff --git a/civicase.civix.php b/civicase.civix.php index 902500e2a..e3932e11e 100644 --- a/civicase.civix.php +++ b/civicase.civix.php @@ -313,12 +313,12 @@ function _civicase_civix_civicrm_entityTypes(&$entityTypes) { 'CRM_Civicase_DAO_CaseSalesOrder' => [ 'name' => 'CaseSalesOrder', 'class' => 'CRM_Civicase_DAO_CaseSalesOrder', - 'table' => 'civicrm_case_sales_order', + 'table' => 'civicase_sales_order', ], 'CRM_Civicase_DAO_CaseSalesOrderLine' => [ 'name' => 'CaseSalesOrderLine', 'class' => 'CRM_Civicase_DAO_CaseSalesOrderLine', - 'table' => 'civicrm_case_sales_order_line', + 'table' => 'civicase_sales_order_line', ], ]); } diff --git a/sql/auto_install.sql b/sql/auto_install.sql index f60284e6f..dc80f323f 100644 --- a/sql/auto_install.sql +++ b/sql/auto_install.sql @@ -48,12 +48,12 @@ CREATE TABLE IF NOT EXISTS `civicrm_case_category_features` ( -- /******************************************************* -- * --- * civicrm_case_sales_order +-- * civicase_sales_order -- * -- * Sales order that represents quotations -- * -- *******************************************************/ -CREATE TABLE `civicrm_case_sales_order` ( +CREATE TABLE `civicase_sales_order` ( `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique CaseSalesOrder ID', `client_id` int unsigned COMMENT 'FK to Contact', `owner_id` int unsigned COMMENT 'FK to Contact', @@ -68,20 +68,20 @@ CREATE TABLE `civicrm_case_sales_order` ( `created_at` timestamp DEFAULT CURRENT_TIMESTAMP COMMENT 'Date the sales order is created', `is_deleted` tinyint DEFAULT 0 COMMENT 'Is this sales order deleted?', PRIMARY KEY (`id`), - CONSTRAINT FK_civicrm_case_sales_order_client_id FOREIGN KEY (`client_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE, - CONSTRAINT FK_civicrm_case_sales_order_owner_id FOREIGN KEY (`owner_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE, - CONSTRAINT FK_civicrm_case_sales_order_case_id FOREIGN KEY (`case_id`) REFERENCES `civicrm_case`(`id`) ON DELETE CASCADE + CONSTRAINT FK_civicase_sales_order_client_id FOREIGN KEY (`client_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE, + CONSTRAINT FK_civicase_sales_order_owner_id FOREIGN KEY (`owner_id`) REFERENCES `civicrm_contact`(`id`) ON DELETE CASCADE, + CONSTRAINT FK_civicase_sales_order_case_id FOREIGN KEY (`case_id`) REFERENCES `civicrm_case`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB; -- /******************************************************* -- * --- * civicrm_case_sales_order_line +-- * civicase_sales_order_line -- * -- * Sales order line items -- * -- *******************************************************/ -CREATE TABLE `civicrm_case_sales_order_line` ( +CREATE TABLE `civicase_sales_order_line` ( `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique CaseSalesOrderLine ID', `sales_order_id` int unsigned COMMENT 'FK to CaseSalesOrder', `financial_type_id` int unsigned COMMENT 'FK to CiviCRM Financial Type', @@ -93,8 +93,8 @@ CREATE TABLE `civicrm_case_sales_order_line` ( `discounted_percentage` decimal(20,2) COMMENT 'Discount applied to the line item', `subtotal_amount` decimal(20,2) COMMENT 'Quantity x Unit Price x (100-Discount)%', PRIMARY KEY (`id`), - CONSTRAINT FK_civicrm_case_sales_order_line_sales_order_id FOREIGN KEY (`sales_order_id`) REFERENCES `civicrm_case_sales_order`(`id`) ON DELETE CASCADE, - CONSTRAINT FK_civicrm_case_sales_order_line_financial_type_id FOREIGN KEY (`financial_type_id`) REFERENCES `civicrm_financial_type`(`id`) ON DELETE SET NULL, - CONSTRAINT FK_civicrm_case_sales_order_line_product_id FOREIGN KEY (`product_id`) REFERENCES `civicrm_product`(`id`) ON DELETE SET NULL + CONSTRAINT FK_civicase_sales_order_line_sales_order_id FOREIGN KEY (`sales_order_id`) REFERENCES `civicase_sales_order`(`id`) ON DELETE CASCADE, + CONSTRAINT FK_civicase_sales_order_line_financial_type_id FOREIGN KEY (`financial_type_id`) REFERENCES `civicrm_financial_type`(`id`) ON DELETE SET NULL, + CONSTRAINT FK_civicase_sales_order_line_product_id FOREIGN KEY (`product_id`) REFERENCES `civicrm_product`(`id`) ON DELETE SET NULL ) ENGINE=InnoDB; diff --git a/sql/auto_uninstall.sql b/sql/auto_uninstall.sql index 7eca6f5b2..80fb0b568 100644 --- a/sql/auto_uninstall.sql +++ b/sql/auto_uninstall.sql @@ -15,8 +15,8 @@ SET FOREIGN_KEY_CHECKS=0; -DROP TABLE IF EXISTS `civicrm_case_sales_order_line`; -DROP TABLE IF EXISTS `civicrm_case_sales_order`; +DROP TABLE IF EXISTS `civicase_sales_order_line`; +DROP TABLE IF EXISTS `civicase_sales_order`; DROP TABLE IF EXISTS `civicase_contactlock`; DROP TABLE IF EXISTS `civicrm_case_category_instance`; DROP TABLE IF EXISTS `civicrm_case_category_features`; diff --git a/xml/schema/CRM/Civicase/CaseSalesOrder.entityType.php b/xml/schema/CRM/Civicase/CaseSalesOrder.entityType.php index f4d758a7f..55b2d44de 100644 --- a/xml/schema/CRM/Civicase/CaseSalesOrder.entityType.php +++ b/xml/schema/CRM/Civicase/CaseSalesOrder.entityType.php @@ -12,6 +12,6 @@ [ 'name' => 'CaseSalesOrder', 'class' => 'CRM_Civicase_DAO_CaseSalesOrder', - 'table' => 'civicrm_case_sales_order', + 'table' => 'civicase_sales_order', ], ]; diff --git a/xml/schema/CRM/Civicase/CaseSalesOrder.xml b/xml/schema/CRM/Civicase/CaseSalesOrder.xml index 343d00148..6fecdd8ed 100644 --- a/xml/schema/CRM/Civicase/CaseSalesOrder.xml +++ b/xml/schema/CRM/Civicase/CaseSalesOrder.xml @@ -3,7 +3,7 @@ CRM/CivicaseCaseSalesOrder - civicrm_case_sales_order + civicase_sales_orderSales order that represents quotationstrue diff --git a/xml/schema/CRM/Civicase/CaseSalesOrderLine.entityType.php b/xml/schema/CRM/Civicase/CaseSalesOrderLine.entityType.php index 40f068780..4547d665b 100644 --- a/xml/schema/CRM/Civicase/CaseSalesOrderLine.entityType.php +++ b/xml/schema/CRM/Civicase/CaseSalesOrderLine.entityType.php @@ -12,6 +12,6 @@ [ 'name' => 'CaseSalesOrderLine', 'class' => 'CRM_Civicase_DAO_CaseSalesOrderLine', - 'table' => 'civicrm_case_sales_order_line', + 'table' => 'civicase_sales_order_line', ], ]; diff --git a/xml/schema/CRM/Civicase/CaseSalesOrderLine.xml b/xml/schema/CRM/Civicase/CaseSalesOrderLine.xml index 628431df6..8a7bccdd0 100644 --- a/xml/schema/CRM/Civicase/CaseSalesOrderLine.xml +++ b/xml/schema/CRM/Civicase/CaseSalesOrderLine.xml @@ -3,7 +3,7 @@
CRM/CivicaseCaseSalesOrderLine - civicrm_case_sales_order_line + civicase_sales_order_lineSales order line itemstrue @@ -31,7 +31,7 @@ sales_order_id -
civicrm_case_sales_order
+ civicase_sales_order
id CASCADE From 6eff805cf76281cd24f0a47725af7193b83de90d Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 15 Feb 2023 10:22:35 +0100 Subject: [PATCH 012/199] BTHAB-20: Use features option_value value instead of ID (cherry picked from commit 402dccd02cf58bb5cc02d826e3132b967c21cede) --- CRM/Civicase/Hook/BuildForm/AddCaseCategoryFeaturesField.php | 4 ++-- CRM/Civicase/Hook/PostProcess/SaveCaseCategoryFeature.php | 4 ++-- CRM/Civicase/Service/CaseTypeCategoryFeatures.php | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CRM/Civicase/Hook/BuildForm/AddCaseCategoryFeaturesField.php b/CRM/Civicase/Hook/BuildForm/AddCaseCategoryFeaturesField.php index 69ab32073..f77b33f36 100644 --- a/CRM/Civicase/Hook/BuildForm/AddCaseCategoryFeaturesField.php +++ b/CRM/Civicase/Hook/BuildForm/AddCaseCategoryFeaturesField.php @@ -35,10 +35,10 @@ private function addCategoryFeaturesFormField(CRM_Core_Form &$form) { $features = []; foreach ($caseCategoryFeatures->getFeatures() as $feature) { - $features[] = 'case_category_feature_' . $feature['id']; + $features[] = 'case_category_feature_' . $feature['value']; $form->add( 'checkbox', - 'case_category_feature_' . $feature['id'], + 'case_category_feature_' . $feature['value'], $feature['label'] ); } diff --git a/CRM/Civicase/Hook/PostProcess/SaveCaseCategoryFeature.php b/CRM/Civicase/Hook/PostProcess/SaveCaseCategoryFeature.php index 52129a679..acee44045 100644 --- a/CRM/Civicase/Hook/PostProcess/SaveCaseCategoryFeature.php +++ b/CRM/Civicase/Hook/PostProcess/SaveCaseCategoryFeature.php @@ -47,10 +47,10 @@ private function saveCaseCategoryFeature($categoryId, array $submittedValues) { // Create new features link. $caseCategoryFeatures = new CRM_Civicase_Service_CaseTypeCategoryFeatures(); foreach ($caseCategoryFeatures->getFeatures() as $feature) { - if (!empty($submittedValues['case_category_feature_' . $feature['id']])) { + if (!empty($submittedValues['case_category_feature_' . $feature['value']])) { CaseCategoryFeatures::create() ->addValue('category_id', $categoryId) - ->addValue('feature_id', $feature['id']) + ->addValue('feature_id', $feature['value']) ->execute(); } } diff --git a/CRM/Civicase/Service/CaseTypeCategoryFeatures.php b/CRM/Civicase/Service/CaseTypeCategoryFeatures.php index 7dd022240..59e8e055a 100644 --- a/CRM/Civicase/Service/CaseTypeCategoryFeatures.php +++ b/CRM/Civicase/Service/CaseTypeCategoryFeatures.php @@ -16,7 +16,6 @@ public function getFeatures() { $optionValues = OptionValue::get() ->addSelect('id', 'label', 'value', 'name', 'option_group_id') ->addWhere('option_group_id:name', '=', self::NAME) - ->setLimit(25) ->execute(); return $optionValues; From c4cd3b01bb5c2a01a5987d6711142c160c2db0cc Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 16 Feb 2023 08:56:29 +0100 Subject: [PATCH 013/199] BTHAB-20: Move shared function to abstract class (cherry picked from commit dcf8852fedea801ba7b3ce365b0d734ae2934c27) --- .../Hook/NavigationMenu/AbstractMenuAlter.php | 45 +++++++++++++++++++ .../Hook/NavigationMenu/AlterForCaseMenu.php | 15 +------ 2 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 CRM/Civicase/Hook/NavigationMenu/AbstractMenuAlter.php diff --git a/CRM/Civicase/Hook/NavigationMenu/AbstractMenuAlter.php b/CRM/Civicase/Hook/NavigationMenu/AbstractMenuAlter.php new file mode 100644 index 000000000..01cdbf58c --- /dev/null +++ b/CRM/Civicase/Hook/NavigationMenu/AbstractMenuAlter.php @@ -0,0 +1,45 @@ + &$value) { + if ($value['attributes']['name'] === $menuBefore) { + $weight = $desiredWeight = (int) $value['attributes']['weight']; + $moveDown = TRUE; + } + + if ($moveDown) { + $value['attributes']['weight'] = ++$weight; + } + } + + return $desiredWeight; + } + +} diff --git a/CRM/Civicase/Hook/NavigationMenu/AlterForCaseMenu.php b/CRM/Civicase/Hook/NavigationMenu/AlterForCaseMenu.php index 22dae9dad..94d285ba9 100644 --- a/CRM/Civicase/Hook/NavigationMenu/AlterForCaseMenu.php +++ b/CRM/Civicase/Hook/NavigationMenu/AlterForCaseMenu.php @@ -7,7 +7,7 @@ /** * Class CRM_Civicase_Hook_Navigation_AlterForCaseMenu. */ -class CRM_Civicase_Hook_NavigationMenu_AlterForCaseMenu { +class CRM_Civicase_Hook_NavigationMenu_AlterForCaseMenu extends CRM_Civicase_Hook_NavigationMenu_AbstractMenuAlter { /** * Case category Setting. @@ -135,18 +135,7 @@ private function addCiviCaseInstanceMenu(array &$menu) { $administerID = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_Navigation', 'Administer', 'id', 'name'); $civicaseSettings = &$menu[$administerID]['child'][$caseID]; - $weight = $desiredWeight = 0; - $moveDown = FALSE; - foreach ($civicaseSettings['child'] as $key => &$value) { - if ($value['attributes']['name'] === 'Case Types') { - $weight = $desiredWeight = (int) $value['attributes']['weight']; - $moveDown = TRUE; - } - - if ($moveDown) { - $value['attributes']['weight'] = ++$weight; - } - } + $desiredWeight = $this->moveMenuDown($civicaseSettings['child'], 'Case Types'); $menu[$administerID]['child'][$caseID]['child'][] = [ 'attributes' => [ From 8da64e1c7cf1af286375d7919ccd34d151f40076 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 16 Feb 2023 09:36:32 +0100 Subject: [PATCH 014/199] BTHAB-20: Display enabled features menu (cherry picked from commit e00d9c1ba9194160b2c0c39e9f26c8157143d9b8) --- .../CaseInstanceFeaturesMenu.php | 112 ++++++++++++++++++ civicase.php | 1 + 2 files changed, 113 insertions(+) create mode 100644 CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php diff --git a/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php b/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php new file mode 100644 index 000000000..ea9dd2654 --- /dev/null +++ b/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php @@ -0,0 +1,112 @@ +addFeaturesMenu($menu); + } + + /** + * Adds enabled features menu to menu. + * + * @param array $menu + * Tree of menu items, per hook_civicrm_navigationMenu. + */ + private function addFeaturesMenu(array &$menu) { + try { + + $caseInstancesGroup = $this->retrieveCaseInstanceWithEnabledFeatures(); + + foreach ($caseInstancesGroup as $caseInstances) { + $separator = 0; + $caseInstanceMenu = &$menu[$caseInstances['navigation_id']]; + $caseInstanceName = $caseInstances['name']; + $caseInstanceName = ($caseInstanceName === 'Prospecting') ? 'prospect' : $caseInstanceName; + $desiredWeight = $this->moveMenuDown($caseInstanceMenu['child'], "manage_{$caseInstanceName}_workflows"); + + foreach ($caseInstances['items'] as $caseInstance) { + $caseInstanceMenu['child'][] = [ + 'attributes' => [ + 'label' => ts('Manage ' . $caseInstance['feature_id:label']), + 'name' => 'Manage ' . $caseInstance['feature_id:label'], + 'url' => "civicrm/case-features/a?case_type_category={$caseInstance['category_id']}#/{$caseInstance['feature_id:name']}", + 'permission' => $caseInstanceMenu['attributes']['permission'], + 'operator' => 'OR', + 'parentID' => $caseInstanceMenu['attributes']['navID'], + 'active' => 1, + 'separator' => $separator++, + 'weight' => $desiredWeight, + ], + ]; + } + } + } + catch (\Throwable $th) { + \Civi::log()->error(E::ts("Error adding case instance features menu"), [ + 'context' => [ + 'backtrace' => $th->getTraceAsString(), + 'message' => $th->getMessage(), + ], + ]); + } + } + + /** + * Retrieves case instance that has the defined features enabled. + * + * @return array + * Array of Key\Pair value grouped by case instance id. + */ + private function retrieveCaseInstanceWithEnabledFeatures() { + $caseInstanceGroup = OptionGroup::get()->addWhere('name', '=', 'case_type_categories')->execute()[0] ?? NULL; + + if (empty($caseInstanceGroup)) { + return []; + } + + $result = CaseCategoryFeatures::get() + ->addSelect('*', 'option_value.label', 'option_value.name', 'feature_id:name', 'feature_id:label', 'navigation.id') + ->addJoin('OptionValue AS option_value', 'LEFT', + ['option_value.value', '=', 'category_id'] + ) + ->addJoin('Navigation AS navigation', 'LEFT', + ['navigation.name', '=', 'option_value.name'] + ) + ->addWhere('option_value.option_group_id', '=', $caseInstanceGroup['id']) + ->addWhere('feature_id:name', 'IN', self::FEATURES_WITH_MENU) + ->execute(); + + $caseCategoriesGroup = array_reduce((array) $result, function (array $accumulator, array $element) { + $accumulator[$element['category_id']]['items'][] = $element; + $accumulator[$element['category_id']]['navigation_id'] = $element['navigation.id']; + $accumulator[$element['category_id']]['name'] = $element['option_value.name']; + + return $accumulator; + }, []); + + return $caseCategoriesGroup; + } + +} diff --git a/civicase.php b/civicase.php index bc7232337..b929068e4 100644 --- a/civicase.php +++ b/civicase.php @@ -409,6 +409,7 @@ function civicase_civicrm_check(&$messages) { function civicase_civicrm_navigationMenu(&$menu) { $hooks = [ new CRM_Civicase_Hook_NavigationMenu_AlterForCaseMenu(), + new CRM_Civicase_Hook_NavigationMenu_CaseInstanceFeaturesMenu(), ]; foreach ($hooks as $hook) { From 300cad11039e4755f24d0e99466c627e046310a0 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 20 Feb 2023 10:01:30 +0100 Subject: [PATCH 015/199] BRHAB-20: Avoid creating sales_order table if exist --- sql/auto_install.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sql/auto_install.sql b/sql/auto_install.sql index dc80f323f..aa06792c5 100644 --- a/sql/auto_install.sql +++ b/sql/auto_install.sql @@ -53,7 +53,7 @@ CREATE TABLE IF NOT EXISTS `civicrm_case_category_features` ( -- * Sales order that represents quotations -- * -- *******************************************************/ -CREATE TABLE `civicase_sales_order` ( +CREATE TABLE IF NOT EXISTS `civicase_sales_order` ( `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique CaseSalesOrder ID', `client_id` int unsigned COMMENT 'FK to Contact', `owner_id` int unsigned COMMENT 'FK to Contact', @@ -81,7 +81,7 @@ ENGINE=InnoDB; -- * Sales order line items -- * -- *******************************************************/ -CREATE TABLE `civicase_sales_order_line` ( +CREATE TABLE IF NOT EXISTS `civicase_sales_order_line` ( `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique CaseSalesOrderLine ID', `sales_order_id` int unsigned COMMENT 'FK to CaseSalesOrder', `financial_type_id` int unsigned COMMENT 'FK to CiviCRM Financial Type', From 39a4a9eaedf0ef273dcc81f2627638fadcf6e1d5 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 16 Feb 2023 09:43:11 +0100 Subject: [PATCH 016/199] BTHAB-20: Add base angular module for case features --- CRM/Civicase/Page/CaseFeaturesAngular.php | 42 +++++++++++++++++++ ang/case-features.ang.php | 51 +++++++++++++++++++++++ ang/case-features.js | 3 ++ ang/case-features/app.routes.js | 7 ++++ xml/Menu/civicase.xml | 5 +++ 5 files changed, 108 insertions(+) create mode 100644 CRM/Civicase/Page/CaseFeaturesAngular.php create mode 100644 ang/case-features.ang.php create mode 100644 ang/case-features.js create mode 100644 ang/case-features/app.routes.js diff --git a/CRM/Civicase/Page/CaseFeaturesAngular.php b/CRM/Civicase/Page/CaseFeaturesAngular.php new file mode 100644 index 000000000..1482145e8 --- /dev/null +++ b/CRM/Civicase/Page/CaseFeaturesAngular.php @@ -0,0 +1,42 @@ +setPageName('civicrm/case-features/a'); + $loader->addModules(['crmApp', 'case-features']); + \Civi::resources()->addSetting([ + 'crmApp' => [ + 'defaultRoute' => '/', + ], + ]); + + return parent::run(); + } + + /** + * Get Template File Name. + * + * @inheritdoc + */ + public function getTemplateFileName() { + return 'Civi/Angular/Page/Main.tpl'; + } + +} diff --git a/ang/case-features.ang.php b/ang/case-features.ang.php new file mode 100644 index 000000000..3eaabfad2 --- /dev/null +++ b/ang/case-features.ang.php @@ -0,0 +1,51 @@ + [ + 'css/*.css', + ], + 'js' => getFeaturesJsFiles(), + 'requires' => $requires, + 'partials' => [ + 'ang/case-features', + ], +]; diff --git a/ang/case-features.js b/ang/case-features.js new file mode 100644 index 000000000..7a120c16d --- /dev/null +++ b/ang/case-features.js @@ -0,0 +1,3 @@ +(function (angular, $, _) { + angular.module('case-features', CRM.angRequires('case-features')); +})(angular, CRM.$, CRM._); diff --git a/ang/case-features/app.routes.js b/ang/case-features/app.routes.js new file mode 100644 index 000000000..7ae5bb7e5 --- /dev/null +++ b/ang/case-features/app.routes.js @@ -0,0 +1,7 @@ +(function (angular, $, _) { + var module = angular.module('case-features'); + + module.config(function ($routeProvider, UrlParametersProvider) { + + }); +})(angular, CRM.$, CRM._); diff --git a/xml/Menu/civicase.xml b/xml/Menu/civicase.xml index 7c194eb52..b66f7a741 100644 --- a/xml/Menu/civicase.xml +++ b/xml/Menu/civicase.xml @@ -28,6 +28,11 @@ CRM_Civicase_Page_WorkflowAngular administer CiviCase + + civicrm/case-features/a + CRM_Civicase_Page_CaseFeaturesAngular + administer CiviCase + civicrm/case/webforms CRM_Civicase_Form_CaseWebforms From 8f65d441e6f2d64d5092327a6dc06b397b5a0c7d Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 16 Feb 2023 11:08:34 +0100 Subject: [PATCH 017/199] BTHAB-20: Display quotations that do not belong to any case --- ang/afsearchQuotations.aff.html | 6 +++ ang/afsearchQuotations.aff.json | 16 +++++++ ang/case-features/app.routes.js | 9 +++- .../directives/quotations-list.directive.html | 16 +++++++ .../directives/quotations-list.directive.js | 21 +++++++++ managed/SavedSearch_Quotations.mgd.php | 45 +++++++++++++++++++ 6 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 ang/afsearchQuotations.aff.html create mode 100644 ang/afsearchQuotations.aff.json create mode 100644 ang/case-features/quotations/directives/quotations-list.directive.html create mode 100644 ang/case-features/quotations/directives/quotations-list.directive.js create mode 100644 managed/SavedSearch_Quotations.mgd.php diff --git a/ang/afsearchQuotations.aff.html b/ang/afsearchQuotations.aff.html new file mode 100644 index 000000000..2502b1181 --- /dev/null +++ b/ang/afsearchQuotations.aff.html @@ -0,0 +1,6 @@ +
+
+ +
+ +
diff --git a/ang/afsearchQuotations.aff.json b/ang/afsearchQuotations.aff.json new file mode 100644 index 000000000..4d37e2e0c --- /dev/null +++ b/ang/afsearchQuotations.aff.json @@ -0,0 +1,16 @@ +{ + "type": "search", + "requires": [], + "title": "Quotations", + "description": "", + "is_dashlet": false, + "is_public": false, + "is_token": false, + "server_route": "", + "permission": "access CiviCRM", + "entity_type": null, + "join_entity": null, + "contact_summary": null, + "redirect": null, + "create_submission": null +} diff --git a/ang/case-features/app.routes.js b/ang/case-features/app.routes.js index 7ae5bb7e5..4a529e884 100644 --- a/ang/case-features/app.routes.js +++ b/ang/case-features/app.routes.js @@ -2,6 +2,13 @@ var module = angular.module('case-features'); module.config(function ($routeProvider, UrlParametersProvider) { - + $routeProvider.when('/quotations', { + template: function () { + var urlParams = UrlParametersProvider.parse(window.location.search); + return ` + + `; + } + }); }); })(angular, CRM.$, CRM._); diff --git a/ang/case-features/quotations/directives/quotations-list.directive.html b/ang/case-features/quotations/directives/quotations-list.directive.html new file mode 100644 index 000000000..d7d2e2028 --- /dev/null +++ b/ang/case-features/quotations/directives/quotations-list.directive.html @@ -0,0 +1,16 @@ +
+

{{ ts('Manage Quotations') }}

+ + + add_circle + {{:: ts('Create Quotation') }} + + +
+
+ +
+
+
diff --git a/ang/case-features/quotations/directives/quotations-list.directive.js b/ang/case-features/quotations/directives/quotations-list.directive.js new file mode 100644 index 000000000..10f62592b --- /dev/null +++ b/ang/case-features/quotations/directives/quotations-list.directive.js @@ -0,0 +1,21 @@ +(function (angular, _) { + var module = angular.module('case-features'); + + module.directive('quotationsList', function () { + return { + restrict: 'E', + controller: 'quotationsListController', + templateUrl: '~/case-features/quotations/directives/quotations-list.directive.html', + scope: {} + }; + }); + + module.controller('quotationsListController', quotationsListController); + + /** + * @param {object} $scope the controller scope + */ + function quotationsListController ($scope, $rootScope) { + + } +})(angular, CRM._); diff --git a/managed/SavedSearch_Quotations.mgd.php b/managed/SavedSearch_Quotations.mgd.php new file mode 100644 index 000000000..8581110db --- /dev/null +++ b/managed/SavedSearch_Quotations.mgd.php @@ -0,0 +1,45 @@ + 'SavedSearch_Quotations', + 'entity' => 'SavedSearch', + 'cleanup' => 'unused', + 'update' => 'unmodified', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'Quotations', + 'label' => 'Quotations', + 'form_values' => NULL, + 'search_custom_id' => NULL, + 'api_entity' => 'CaseSalesOrder', + 'api_params' => [ + 'version' => 4, + 'select' => [ + 'id', + 'client_id.display_name', + 'owner_id.display_name', + 'currency:label', + 'total_before_tax', + 'total_after_tax', + ], + 'orderBy' => [], + 'where' => [ + [ + 'case_id.subject', + 'IS EMPTY', + ], + ], + 'groupBy' => [], + 'join' => [], + 'having' => [], + ], + 'expires_date' => NULL, + 'description' => NULL, + 'mapping_id' => NULL, + ], + ], + 'match' => ['name', 'api_entity'] + ], +]; From 6c28918b91e8e75f057b06fa7c7586156f4de248 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 17 Feb 2023 11:24:46 +0100 Subject: [PATCH 018/199] BTHAB-20: Prefix angular resources with extension name --- CRM/Civicase/Page/CaseFeaturesAngular.php | 2 +- ang/afsearchQuotations.aff.html | 4 +- ang/case-features.js | 3 -- ...ures.ang.php => civicase-features.ang.php} | 6 +-- ang/civicase-features.js | 3 ++ .../app.routes.js | 2 +- .../directives/quotations-list.directive.html | 0 .../directives/quotations-list.directive.js | 4 +- managed/SavedSearch_Quotations.mgd.php | 45 ------------------- 9 files changed, 12 insertions(+), 57 deletions(-) delete mode 100644 ang/case-features.js rename ang/{case-features.ang.php => civicase-features.ang.php} (89%) create mode 100644 ang/civicase-features.js rename ang/{case-features => civicase-features}/app.routes.js (89%) rename ang/{case-features => civicase-features}/quotations/directives/quotations-list.directive.html (100%) rename ang/{case-features => civicase-features}/quotations/directives/quotations-list.directive.js (73%) delete mode 100644 managed/SavedSearch_Quotations.mgd.php diff --git a/CRM/Civicase/Page/CaseFeaturesAngular.php b/CRM/Civicase/Page/CaseFeaturesAngular.php index 1482145e8..8440ff91e 100644 --- a/CRM/Civicase/Page/CaseFeaturesAngular.php +++ b/CRM/Civicase/Page/CaseFeaturesAngular.php @@ -20,7 +20,7 @@ class CRM_Civicase_Page_CaseFeaturesAngular extends \CRM_Core_Page { public function run() { $loader = Civi::service('angularjs.loader'); $loader->setPageName('civicrm/case-features/a'); - $loader->addModules(['crmApp', 'case-features']); + $loader->addModules(['crmApp', 'civicase-features']); \Civi::resources()->addSetting([ 'crmApp' => [ 'defaultRoute' => '/', diff --git a/ang/afsearchQuotations.aff.html b/ang/afsearchQuotations.aff.html index 2502b1181..4c50be2c4 100644 --- a/ang/afsearchQuotations.aff.html +++ b/ang/afsearchQuotations.aff.html @@ -1,6 +1,6 @@
- +
- +
diff --git a/ang/case-features.js b/ang/case-features.js deleted file mode 100644 index 7a120c16d..000000000 --- a/ang/case-features.js +++ /dev/null @@ -1,3 +0,0 @@ -(function (angular, $, _) { - angular.module('case-features', CRM.angRequires('case-features')); -})(angular, CRM.$, CRM._); diff --git a/ang/case-features.ang.php b/ang/civicase-features.ang.php similarity index 89% rename from ang/case-features.ang.php rename to ang/civicase-features.ang.php index 3eaabfad2..5b326137b 100644 --- a/ang/case-features.ang.php +++ b/ang/civicase-features.ang.php @@ -19,11 +19,11 @@ function getFeaturesJsFiles() { return array_merge( [ - 'ang/case-features.js', + 'ang/civicase-features.js', ], GlobRecursive::getRelativeToExtension( 'uk.co.compucorp.civicase', - 'ang/case-features/*.js' + 'ang/civicase-features/*.js' ) ); } @@ -46,6 +46,6 @@ function getFeaturesJsFiles() { 'js' => getFeaturesJsFiles(), 'requires' => $requires, 'partials' => [ - 'ang/case-features', + 'ang/civicase-features', ], ]; diff --git a/ang/civicase-features.js b/ang/civicase-features.js new file mode 100644 index 000000000..540167ef4 --- /dev/null +++ b/ang/civicase-features.js @@ -0,0 +1,3 @@ +(function (angular, $, _) { + angular.module('civicase-features', CRM.angRequires('civicase-features')); +})(angular, CRM.$, CRM._); diff --git a/ang/case-features/app.routes.js b/ang/civicase-features/app.routes.js similarity index 89% rename from ang/case-features/app.routes.js rename to ang/civicase-features/app.routes.js index 4a529e884..e37ae2d05 100644 --- a/ang/case-features/app.routes.js +++ b/ang/civicase-features/app.routes.js @@ -1,5 +1,5 @@ (function (angular, $, _) { - var module = angular.module('case-features'); + var module = angular.module('civicase-features'); module.config(function ($routeProvider, UrlParametersProvider) { $routeProvider.when('/quotations', { diff --git a/ang/case-features/quotations/directives/quotations-list.directive.html b/ang/civicase-features/quotations/directives/quotations-list.directive.html similarity index 100% rename from ang/case-features/quotations/directives/quotations-list.directive.html rename to ang/civicase-features/quotations/directives/quotations-list.directive.html diff --git a/ang/case-features/quotations/directives/quotations-list.directive.js b/ang/civicase-features/quotations/directives/quotations-list.directive.js similarity index 73% rename from ang/case-features/quotations/directives/quotations-list.directive.js rename to ang/civicase-features/quotations/directives/quotations-list.directive.js index 10f62592b..e7a3f87c0 100644 --- a/ang/case-features/quotations/directives/quotations-list.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-list.directive.js @@ -1,11 +1,11 @@ (function (angular, _) { - var module = angular.module('case-features'); + var module = angular.module('civicase-features'); module.directive('quotationsList', function () { return { restrict: 'E', controller: 'quotationsListController', - templateUrl: '~/case-features/quotations/directives/quotations-list.directive.html', + templateUrl: '~/civicase-features/quotations/directives/quotations-list.directive.html', scope: {} }; }); diff --git a/managed/SavedSearch_Quotations.mgd.php b/managed/SavedSearch_Quotations.mgd.php deleted file mode 100644 index 8581110db..000000000 --- a/managed/SavedSearch_Quotations.mgd.php +++ /dev/null @@ -1,45 +0,0 @@ - 'SavedSearch_Quotations', - 'entity' => 'SavedSearch', - 'cleanup' => 'unused', - 'update' => 'unmodified', - 'params' => [ - 'version' => 4, - 'values' => [ - 'name' => 'Quotations', - 'label' => 'Quotations', - 'form_values' => NULL, - 'search_custom_id' => NULL, - 'api_entity' => 'CaseSalesOrder', - 'api_params' => [ - 'version' => 4, - 'select' => [ - 'id', - 'client_id.display_name', - 'owner_id.display_name', - 'currency:label', - 'total_before_tax', - 'total_after_tax', - ], - 'orderBy' => [], - 'where' => [ - [ - 'case_id.subject', - 'IS EMPTY', - ], - ], - 'groupBy' => [], - 'join' => [], - 'having' => [], - ], - 'expires_date' => NULL, - 'description' => NULL, - 'mapping_id' => NULL, - ], - ], - 'match' => ['name', 'api_entity'] - ], -]; From 68aebb66684bcb0e916538e157b274563cd458c8 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 17 Feb 2023 11:25:32 +0100 Subject: [PATCH 019/199] BTHAB-20: Add default entity links --- CRM/Civicase/DAO/CaseSalesOrder.php | 13 ++++++++++++- .../directives/quotations-list.directive.js | 4 ++-- xml/schema/CRM/Civicase/CaseSalesOrder.xml | 6 ++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CRM/Civicase/DAO/CaseSalesOrder.php b/CRM/Civicase/DAO/CaseSalesOrder.php index d1aa64033..52c0b5bc5 100644 --- a/CRM/Civicase/DAO/CaseSalesOrder.php +++ b/CRM/Civicase/DAO/CaseSalesOrder.php @@ -6,7 +6,7 @@ * * Generated from uk.co.compucorp.civicase/xml/schema/CRM/Civicase/CaseSalesOrder.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:363627d2e3cd7f868f0cc2c5b89fb95b) + * (GenCodeChecksum:12c8b8325efddd813eeaa69be2c655fb) */ use CRM_Civicase_ExtensionUtil as E; @@ -31,6 +31,17 @@ class CRM_Civicase_DAO_CaseSalesOrder extends CRM_Core_DAO { */ public static $_log = TRUE; + /** + * Paths for accessing this entity in the UI. + * + * @var string[] + */ + protected static $_paths = [ + 'view' => 'civicrm/case-features?reset=1&action=view&lid=[id]', + 'update' => 'civicrm/case-features?reset=1&action=update&lid=[id]', + 'delete' => 'civicrm/case-features/delete?reset=1&lid=[id]', + ]; + /** * Unique CaseSalesOrder ID * diff --git a/ang/civicase-features/quotations/directives/quotations-list.directive.js b/ang/civicase-features/quotations/directives/quotations-list.directive.js index e7a3f87c0..389b2e213 100644 --- a/ang/civicase-features/quotations/directives/quotations-list.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-list.directive.js @@ -15,7 +15,7 @@ /** * @param {object} $scope the controller scope */ - function quotationsListController ($scope, $rootScope) { - + function quotationsListController ($scope) { + } })(angular, CRM._); diff --git a/xml/schema/CRM/Civicase/CaseSalesOrder.xml b/xml/schema/CRM/Civicase/CaseSalesOrder.xml index 6fecdd8ed..e8dcb0427 100644 --- a/xml/schema/CRM/Civicase/CaseSalesOrder.xml +++ b/xml/schema/CRM/Civicase/CaseSalesOrder.xml @@ -7,6 +7,12 @@ Sales order that represents quotations true + + civicrm/case-features?reset=1&action=view&lid=[id] + civicrm/case-features?reset=1&action=update&lid=[id] + civicrm/case-features/delete?reset=1&lid=[id] + + id int unsigned From 5e4434bed1746b07d25bf09bc80a168661955c14 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 27 Feb 2023 17:42:24 +0100 Subject: [PATCH 020/199] BTHAB-23: Refactor method to retrieve feature case instance --- .../CaseInstanceFeaturesMenu.php | 43 ++----------------- .../Service/CaseTypeCategoryFeatures.php | 41 ++++++++++++++++++ 2 files changed, 44 insertions(+), 40 deletions(-) diff --git a/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php b/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php index ea9dd2654..b835384c1 100644 --- a/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php +++ b/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php @@ -1,8 +1,7 @@ retrieveCaseInstanceWithEnabledFeatures(); + $caseTypeCategoryFeatures = new CaseTypeCategoryFeatures(); + $caseInstancesGroup = $caseTypeCategoryFeatures->retrieveCaseInstanceWithEnabledFeatures(self::FEATURES_WITH_MENU); foreach ($caseInstancesGroup as $caseInstances) { $separator = 0; @@ -73,40 +72,4 @@ private function addFeaturesMenu(array &$menu) { } } - /** - * Retrieves case instance that has the defined features enabled. - * - * @return array - * Array of Key\Pair value grouped by case instance id. - */ - private function retrieveCaseInstanceWithEnabledFeatures() { - $caseInstanceGroup = OptionGroup::get()->addWhere('name', '=', 'case_type_categories')->execute()[0] ?? NULL; - - if (empty($caseInstanceGroup)) { - return []; - } - - $result = CaseCategoryFeatures::get() - ->addSelect('*', 'option_value.label', 'option_value.name', 'feature_id:name', 'feature_id:label', 'navigation.id') - ->addJoin('OptionValue AS option_value', 'LEFT', - ['option_value.value', '=', 'category_id'] - ) - ->addJoin('Navigation AS navigation', 'LEFT', - ['navigation.name', '=', 'option_value.name'] - ) - ->addWhere('option_value.option_group_id', '=', $caseInstanceGroup['id']) - ->addWhere('feature_id:name', 'IN', self::FEATURES_WITH_MENU) - ->execute(); - - $caseCategoriesGroup = array_reduce((array) $result, function (array $accumulator, array $element) { - $accumulator[$element['category_id']]['items'][] = $element; - $accumulator[$element['category_id']]['navigation_id'] = $element['navigation.id']; - $accumulator[$element['category_id']]['name'] = $element['option_value.name']; - - return $accumulator; - }, []); - - return $caseCategoriesGroup; - } - } diff --git a/CRM/Civicase/Service/CaseTypeCategoryFeatures.php b/CRM/Civicase/Service/CaseTypeCategoryFeatures.php index 59e8e055a..23365c327 100644 --- a/CRM/Civicase/Service/CaseTypeCategoryFeatures.php +++ b/CRM/Civicase/Service/CaseTypeCategoryFeatures.php @@ -1,5 +1,7 @@ addWhere('name', '=', 'case_type_categories')->execute()[0] ?? NULL; + + if (empty($caseInstanceGroup)) { + return []; + } + + $result = CaseCategoryFeatures::get() + ->addSelect('*', 'option_value.label', 'option_value.name', 'feature_id:name', 'feature_id:label', 'navigation.id') + ->addJoin('OptionValue AS option_value', 'LEFT', + ['option_value.value', '=', 'category_id'] + ) + ->addJoin('Navigation AS navigation', 'LEFT', + ['navigation.name', '=', 'option_value.name'] + ) + ->addWhere('option_value.option_group_id', '=', $caseInstanceGroup['id']) + ->addWhere('feature_id:name', 'IN', $features) + ->execute(); + + $caseCategoriesGroup = array_reduce((array) $result, function (array $accumulator, array $element) { + $accumulator[$element['category_id']]['items'][] = $element; + $accumulator[$element['category_id']]['navigation_id'] = $element['navigation.id']; + $accumulator[$element['category_id']]['name'] = $element['option_value.name']; + + return $accumulator; + }, []); + + return $caseCategoriesGroup; + } + } From 85ab7a862d3d806fe700a1cc813d9564d6df5c48 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 27 Feb 2023 18:59:36 +0100 Subject: [PATCH 021/199] BTHAB-23: Expose extra options required for quotations create page --- ang/civicase-features.ang.php | 47 ++++++++++++++++--- .../services/currency-codes.service.js | 21 +++++++++ .../services/feature-case-types.service.js | 14 ++++++ .../services/sales-order-status.service.js | 21 +++++++++ 4 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 ang/civicase-features/services/currency-codes.service.js create mode 100644 ang/civicase-features/services/feature-case-types.service.js create mode 100644 ang/civicase-features/services/sales-order-status.service.js diff --git a/ang/civicase-features.ang.php b/ang/civicase-features.ang.php index 5b326137b..858904c7d 100644 --- a/ang/civicase-features.ang.php +++ b/ang/civicase-features.ang.php @@ -8,7 +8,16 @@ * http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_angularModules. */ +use Civi\Api4\OptionValue; +use Civi\Utils\CurrencyUtils; use CRM_Civicase_Helper_GlobRecursive as GlobRecursive; +use CRM_Civicase_Service_CaseTypeCategoryFeatures as CaseTypeCategoryFeatures; + +$options = []; + +set_currency_codes($options); +set_case_sales_order_status($options); +set_case_types_with_quotations_enabled($options); /** * Get a list of JS files. @@ -28,22 +37,48 @@ function getFeaturesJsFiles() { ); } +/** + * Exposes currency codes to Angular. + */ +function set_currency_codes(&$options) { + $options['currencyCodes'] = CurrencyUtils::getCurrencies(); +} + +/** + * Exposes Case types that have quotations enabled to Angular. + */ +function set_case_types_with_quotations_enabled(&$options) { + $caseTypeCategoryFeatures = new CaseTypeCategoryFeatures(); + $caseTypeCategories = $caseTypeCategoryFeatures->retrieveCaseInstanceWithEnabledFeatures(['quotations']); + $options['featureCaseTypes']['quotations'] = array_keys($caseTypeCategories); +} + +/** + * Exposes case sales order statuses to Angular. + */ +function set_case_sales_order_status(&$options) { + $optionValues = OptionValue::get() + ->addSelect('id', 'value', 'name', 'label') + ->addWhere('option_group_id:name', '=', 'case_sales_order_status') + ->execute(); + + $options['salesOrderStatus'] = $optionValues->getArrayCopy(); +} + $requires = [ - 'crmUi', - 'crmCaseType', - 'ngRoute', - 'dialogService', + 'api4', + 'crmUtil', + 'civicase', 'civicase-base', 'afsearchQuotations', ]; -$requires = CRM_Workflow_Hook_addDependentAngularModules::invoke($requires); - return [ 'css' => [ 'css/*.css', ], 'js' => getFeaturesJsFiles(), + 'settings' => $options, 'requires' => $requires, 'partials' => [ 'ang/civicase-features', diff --git a/ang/civicase-features/services/currency-codes.service.js b/ang/civicase-features/services/currency-codes.service.js new file mode 100644 index 000000000..a2126c8bb --- /dev/null +++ b/ang/civicase-features/services/currency-codes.service.js @@ -0,0 +1,21 @@ +(function (angular, $, _, CRM) { + var module = angular.module('civicase-features'); + + module.service('CurrencyCodes', CurrencyCodes); + + /** + * CurrencyCodes Service + */ + function CurrencyCodes () { + this.getAll = function () { + return CRM['civicase-features'].currencyCodes; + }; + + this.getSymbol = function (name) { + return CRM['civicase-features'] + .currencyCodes + .filter(currency => currency.name === name) + .pop().symbol || '£'; + }; + } +})(angular, CRM.$, CRM._, CRM); diff --git a/ang/civicase-features/services/feature-case-types.service.js b/ang/civicase-features/services/feature-case-types.service.js new file mode 100644 index 000000000..d2135b229 --- /dev/null +++ b/ang/civicase-features/services/feature-case-types.service.js @@ -0,0 +1,14 @@ +(function (angular, $, _, CRM) { + var module = angular.module('civicase-features'); + + module.service('FeatureCaseTypes', FeatureCaseTypes); + + /** + * FeatureCaseTypes Service + */ + function FeatureCaseTypes () { + this.getCaseTypes = function ($feature) { + return CRM['civicase-features'].featureCaseTypes[$feature] || []; + }; + } +})(angular, CRM.$, CRM._, CRM); diff --git a/ang/civicase-features/services/sales-order-status.service.js b/ang/civicase-features/services/sales-order-status.service.js new file mode 100644 index 000000000..fa3d2983e --- /dev/null +++ b/ang/civicase-features/services/sales-order-status.service.js @@ -0,0 +1,21 @@ +(function (angular, $, _, CRM) { + var module = angular.module('civicase-features'); + + module.service('SalesOrderStatus', SalesOrderStatus); + + /** + * SalesOrderStatus Service + */ + function SalesOrderStatus () { + this.getAll = function () { + return CRM['civicase-features'].salesOrderStatus; + }; + + this.getValueByName = function (name) { + return CRM['civicase-features'] + .salesOrderStatus + .filter(status => status.name === name) + .pop().value || ''; + }; + } +})(angular, CRM.$, CRM._, CRM); From b0a53faae36535fcc7abe26c97fd1dbfe54938be Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 27 Feb 2023 19:01:36 +0100 Subject: [PATCH 022/199] BTHAB-23: Add a new directive to redirect user to previous page --- .../shared/directives/history-back.directive.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 ang/civicase/shared/directives/history-back.directive.js diff --git a/ang/civicase/shared/directives/history-back.directive.js b/ang/civicase/shared/directives/history-back.directive.js new file mode 100644 index 000000000..8419633ca --- /dev/null +++ b/ang/civicase/shared/directives/history-back.directive.js @@ -0,0 +1,14 @@ +(function (angular, $window) { + var module = angular.module('civicase'); + + module.directive('historyBack', function () { + return { + restrict: 'A', + link: function (scope, elem, attrs) { + elem.bind('click', function () { + $window.history.back(); + }); + } + }; + }); +})(angular, window); From ca69f0bb3064e47316c24970f1e5f8716f11af49 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 27 Feb 2023 19:02:07 +0100 Subject: [PATCH 023/199] BTHAB-23: Add Util class to manage currency table --- Civi/Utils/CurrencyUtils.php | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 Civi/Utils/CurrencyUtils.php diff --git a/Civi/Utils/CurrencyUtils.php b/Civi/Utils/CurrencyUtils.php new file mode 100644 index 000000000..907f1b88f --- /dev/null +++ b/Civi/Utils/CurrencyUtils.php @@ -0,0 +1,38 @@ +fetch()) { + self::$currencies[] = ['name' => $dao->name, 'symbol' => $dao->symbol]; + } + } + + return self::$currencies; + } + +} From 3a9730c94ad031cdd86ef32d10174315a394bb5f Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 27 Feb 2023 19:04:33 +0100 Subject: [PATCH 024/199] BTHAB-23: Add endpoint to upsert sales order --- .../CaseSalesOrder/SalesOrderSaveAction.php | 74 +++++++++++++++++++ Civi/Api4/CaseSalesOrder.php | 15 ++++ 2 files changed, 89 insertions(+) create mode 100644 Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php diff --git a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php new file mode 100644 index 000000000..19b3ab949 --- /dev/null +++ b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php @@ -0,0 +1,74 @@ +records as &$record) { + $record += $this->defaults; + $this->formatWriteValues($record); + $this->matchExisting($record); + if (empty($record['id'])) { + $this->fillDefaults($record); + } + } + $this->validateValues(); + + $resultArray = $this->writeRecord($this->records); + + $result->exchangeArray($resultArray); + } + + /** + * {@inheritDoc} + */ + protected function writeRecord($items) { + $transaction = CRM_Core_Transaction::create(); + + try { + $output = []; + foreach ($items as $salesOrder) { + $lineItems = $salesOrder['items']; + $total = CaseSalesOrderBAO::computeTotal($lineItems); + $salesOrder['total_before_tax'] = $total['totalBeforeTax']; + $salesOrder['total_after_tax'] = $total['totalAfterTax']; + $result = array_pop($this->writeObjects([$salesOrder])); + + $caseSalesOrderLineAPI = CaseSalesOrderLine::save(); + if (!empty($result) && !empty($lineItems)) { + array_walk($lineItems, function (&$lineItem) use ($result, $caseSalesOrderLineAPI) { + $lineItem['sales_order_id'] = $result['id']; + $caseSalesOrderLineAPI->addRecord($lineItem); + }); + + $result['items'] = $caseSalesOrderLineAPI->execute()->jsonSerialize(); + } + + $output[] = $result; + } + + return $output; + } + catch (\Exception $e) { + $transaction->rollback(); + + throw $e; + } + } + +} diff --git a/Civi/Api4/CaseSalesOrder.php b/Civi/Api4/CaseSalesOrder.php index bebff5e2d..c01ae9944 100644 --- a/Civi/Api4/CaseSalesOrder.php +++ b/Civi/Api4/CaseSalesOrder.php @@ -3,6 +3,7 @@ namespace Civi\Api4; use Civi\Api4\Generic\DAOEntity; +use Civi\Api4\Action\CaseSalesOrder\SalesOrderSaveAction; /** * CaseSalesOrder entity. @@ -13,4 +14,18 @@ */ class CaseSalesOrder extends DAOEntity { + /** + * Creates or Updates a SalesOrder with the line items. + * + * @param bool $checkPermissions + * Should permission be checked for the user. + * + * @return Civi\Api4\Action\CaseSalesOrder\SalesOrderSaveAction + * returns save order action + */ + public static function save($checkPermissions = TRUE) { + return (new SalesOrderSaveAction(__CLASS__, __FUNCTION__)) + ->setCheckPermissions($checkPermissions); + } + } From 13d911b9141c1ca3e52e6dcbfceb7eebe4c28c49 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 27 Feb 2023 19:05:49 +0100 Subject: [PATCH 025/199] BTHAB-23: Add endpoint to compute sales order total amount --- CRM/Civicase/BAO/CaseSalesOrder.php | 59 +++++++++++++++++++ .../CaseSalesOrder/ComputeTotalAction.php | 33 +++++++++++ Civi/Api4/CaseSalesOrder.php | 15 +++++ 3 files changed, 107 insertions(+) create mode 100644 Civi/Api4/Action/CaseSalesOrder/ComputeTotalAction.php diff --git a/CRM/Civicase/BAO/CaseSalesOrder.php b/CRM/Civicase/BAO/CaseSalesOrder.php index 69614b82a..8da5376c0 100644 --- a/CRM/Civicase/BAO/CaseSalesOrder.php +++ b/CRM/Civicase/BAO/CaseSalesOrder.php @@ -28,4 +28,63 @@ public static function create(array $params) { return $instance; } + /** + * Computes the sales order line item total. + * + * @param array $items + * Array of sales order line items. + * + * @return array + * ['totalAfterTax' => , 'totalBeforeTax' => ] + */ + public static function computeTotal(array $items) { + $totalBeforeTax = round(array_reduce($items, fn ($a, $b) => $a + self::getSubTotal($b), 0), 2); + $totalAfterTax = round(array_reduce($items, + fn ($a, $b) => $a + (($b['tax_rate'] * self::getSubTotal($b)) / 100), + 0 + ) + $totalBeforeTax, 2); + + return [ + 'taxRates' => self::computeTaxRates($items), + 'totalAfterTax' => $totalAfterTax, + 'totalBeforeTax' => $totalBeforeTax, + ]; + } + + /** + * Computes the sub total of a single line item. + * + * @param array $item + * Single sales order line item. + * + * @return int + * The line item subtotal. + */ + public static function getSubTotal(array $item) { + return $item['unit_price'] * $item['quantity'] * ((100 - ($item['discounted_percentage'] ?? 0)) / 100) ?? 0; + } + + /** + * Computes the tax rates of each line item. + * + * @param array $items + * Single sales order line item. + * + * @return array + * Returned sorted array of line items tax rates. + */ + public static function computeTaxRates(array $items) { + $items = array_filter($items, fn ($a) => $a['tax_rate'] > 0); + usort($items, fn ($a, $b) => $a['tax_rate'] <=> $b['tax_rate']); + + return array_map( + fn ($a) => + [ + 'rate' => round($a['tax_rate'], 2), + 'value' => round(($a['tax_rate'] * self::getSubTotal($a)) / 100, 2), + ], + $items + ); + } + } diff --git a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAction.php b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAction.php new file mode 100644 index 000000000..a0397c24c --- /dev/null +++ b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAction.php @@ -0,0 +1,33 @@ +lineItems)) { + $result[] = CaseSalesOrderBAO::computeTotal($this->lineItems); + } + } + +} diff --git a/Civi/Api4/CaseSalesOrder.php b/Civi/Api4/CaseSalesOrder.php index c01ae9944..2ba0cc624 100644 --- a/Civi/Api4/CaseSalesOrder.php +++ b/Civi/Api4/CaseSalesOrder.php @@ -3,6 +3,7 @@ namespace Civi\Api4; use Civi\Api4\Generic\DAOEntity; +use Civi\Api4\Action\CaseSalesOrder\ComputeTotalAction; use Civi\Api4\Action\CaseSalesOrder\SalesOrderSaveAction; /** @@ -28,4 +29,18 @@ public static function save($checkPermissions = TRUE) { ->setCheckPermissions($checkPermissions); } + /** + * Compute the sum of the line items value. + * + * @param bool $checkPermissions + * Should permission be checked for the user. + * + * @return Civi\Api4\Action\CaseSalesOrder\SalesOrderSaveAction + * returns save order action + */ + public static function computeTotal($checkPermissions = FALSE) { + return (new ComputeTotalAction(__CLASS__, __FUNCTION__)) + ->setCheckPermissions($checkPermissions); + } + } From d0074ede71a670c14f86a04a4ed5a4d5cfbb42d6 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 27 Feb 2023 19:06:23 +0100 Subject: [PATCH 026/199] BTHAB-23: Return case_type_category from case getdetails API action --- api/v3/Case/Getdetails.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/api/v3/Case/Getdetails.php b/api/v3/Case/Getdetails.php index 6bcd2739e..9fc88c8a2 100644 --- a/api/v3/Case/Getdetails.php +++ b/api/v3/Case/Getdetails.php @@ -6,6 +6,7 @@ */ require_once 'api/v3/Case.php'; +require_once 'api/v3/CaseType.php'; use CRM_Civicase_APIHelpers_CaseDetails as CaseDetailsQuery; /** @@ -217,6 +218,16 @@ function civicrm_api3_case_getdetails(array $params) { $case['related_case_ids'] = CRM_Case_BAO_Case::getRelatedCaseIds($case['id']); } } + + // Get case type category. + if (in_array('case_type_category', $toReturn)) { + foreach ($result['values'] as $id => &$case) { + $caseType = civicrm_api3_case_type_get(['id' => $case['case_type_id']]); + if (!empty($caseType['values']) && is_array($caseType['values'])) { + $case['case_type_category'] = $caseType['values'][$case['case_type_id']]['case_type_category']; + } + } + } if (!empty($params['sequential'])) { $result['values'] = array_values($result['values']); } From 6008a3d08496b22c232e5deac062dedf1cf6ba24 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 27 Feb 2023 19:07:12 +0100 Subject: [PATCH 027/199] BTHAB-23: Add controller and page to create new sales order --- ang/civicase-features/app.routes.js | 8 + .../quotations-create.directive.html | 247 +++++++++++++++ .../directives/quotations-create.directive.js | 287 ++++++++++++++++++ .../directives/quotations-list.directive.html | 2 +- .../directives/quotations-list.directive.js | 10 +- 5 files changed, 552 insertions(+), 2 deletions(-) create mode 100644 ang/civicase-features/quotations/directives/quotations-create.directive.html create mode 100644 ang/civicase-features/quotations/directives/quotations-create.directive.js diff --git a/ang/civicase-features/app.routes.js b/ang/civicase-features/app.routes.js index e37ae2d05..5612883fc 100644 --- a/ang/civicase-features/app.routes.js +++ b/ang/civicase-features/app.routes.js @@ -10,5 +10,13 @@ `; } }); + $routeProvider.when('/new', { + template: function () { + var urlParams = UrlParametersProvider.parse(window.location.search); + return ` + + `; + } + }); }); })(angular, CRM.$, CRM._); diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.html b/ang/civicase-features/quotations/directives/quotations-create.directive.html new file mode 100644 index 000000000..91d0566a9 --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.html @@ -0,0 +1,247 @@ +
+

{{ ts('Create Quotations') }}

+ +
+
+
+
+ +
+ + Client is required +
+
+
+ +
+ + Date is required +
+
+
+ +
+ + Description is required +
+
+
+ +
+ +
+
+
+ +
+ + Owner is required +
+
+
+ +
+ + Status is required +
+
+
+ +
+ + Currency is required +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
ProductItem Description Financial TypeUnit PriceQuantityDiscount %Tax %Subtotal
+ + + +
+ Description is required +
+ +
+ Financial Type is required +
+
+ {{ currencySymbol }} + +
+
+ Unit price is required +
+
+ +
+
+ Quantity is required +
+
+ +
+
+ {{ roundTo(salesOrder.items[$index].tax_rate, 2) }} + {{ roundTo(salesOrder.items[$index].subtotal_amount, 2) }}
+
+
+
+ +
+ +
+
+ +
+ +
+ + + + + + + +
Total{{ currencySymbol }} {{ salesOrder.total }}
Tax @ {{ i.rate }}%{{ currencySymbol }} {{ i.value }}
Grand Total{{ currencySymbol }} {{salesOrder.grandTotal}}
+
+
+ +
+ +
+ +
+
+ + +
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+
diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js new file mode 100644 index 000000000..f73ae665c --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -0,0 +1,287 @@ +(function (angular, $, _) { + var module = angular.module('civicase-features'); + + module.directive('quotationsCreate', function () { + return { + restrict: 'E', + controller: 'quotationsCreateController', + templateUrl: '~/civicase-features/quotations/directives/quotations-create.directive.html', + scope: {} + }; + }); + + module.controller('quotationsCreateController', quotationsCreateController); + + /** + * @param {object} $scope the controller scope + * @param {object} $window window object of the browser + * @param {object} CurrencyCodes CurrencyCodes service + * @param {Function} civicaseCrmApi crm api service + * @param {object} Contact contact service + * @param {object} crmApi4 api V4 service + * @param {object} FeatureCaseTypes FeatureCaseTypes service + * @param {object} SalesOrderStatus SalesOrderStatus service + */ + function quotationsCreateController ($scope, $window, CurrencyCodes, civicaseCrmApi, Contact, crmApi4, FeatureCaseTypes, SalesOrderStatus) { + const defaultCurrency = 'GBP'; + const productsCache = new Map(); + const financialTypesCache = new Map(); + + $scope.formValid = true; + $scope.roundTo = roundTo; + $scope.submitInProgress = false; + $scope.caseApiParam = caseApiParam; + $scope.saveQuotation = saveQuotation; + $scope.calculateSubtotal = calculateSubtotal; + $scope.currencyCodes = CurrencyCodes.getAll(); + $scope.handleProductChange = handleProductChange; + $scope.handleCurrencyChange = handleCurrencyChange; + $scope.salesOrderStatus = SalesOrderStatus.getAll(); + $scope.handleFinancialTypeChange = handleFinancialTypeChange; + $scope.currencySymbol = CurrencyCodes.getSymbol(defaultCurrency); + + (function init () { + initializeSalesOrder(); + $scope.newSalesOrderItem = newSalesOrderItem; + CRM.wysiwyg.create('#sales-order-description'); + $scope.removeSalesOrderItem = removeSalesOrderItem; + + $scope.$on('totalChange', _.debounce(handleTotalChange, 250)); + }()); + + /** + * Initializess the sales order object + */ + function initializeSalesOrder () { + $scope.salesOrder = { + currency: defaultCurrency, + status_id: SalesOrderStatus.getValueByName('new'), + owner_id: Contact.getCurrentContactID(), + quotation_date: $.datepicker.formatDate('yy-mm-dd', new Date()), + items: [{ + product_id: null, + item_description: null, + financial_type_id: null, + unit_price: null, + quantity: null, + discounted_percentage: null, + tax_rate: 0, + subtotal_amount: 0 + }], + total: 0, + grandTotal: 0 + }; + $scope.total = 0; + $scope.taxRates = []; + } + + /** + * Removes a sales order line item + * + * @param {number} index element index to be removed + */ + function removeSalesOrderItem (index) { + $scope.salesOrder.items.splice(index, 1); + } + + /** + * Initializes empty sales order line item + */ + function newSalesOrderItem () { + $scope.salesOrder.items.push({ + product: null, + description: null, + financial_type_id: null, + unit_price: null, + quantity: null, + discounted_percentage: null, + tax_rate: 0, + subtotal_amount: 0 + }); + } + + /** + * Persists quotaiton and redirects on success + */ + function saveQuotation () { + if (!validateForm()) { + return; + } + + $scope.submitInProgress = true; + crmApi4('CaseSalesOrder', 'save', { records: [$scope.salesOrder] }) + .then(function (results) { + $scope.submitInProgress = false; + showSucessNotification(); + redirectToAppropraitePage(); + }, function (failure) { + $scope.submitInProgress = false; + CRM.alert('Unable to generate quotations', ts('Error'), 'error'); + }); + } + + /** + * Validates form before saving + * + * @returns {boolean} true if form is valid, otherwise false + */ + function validateForm () { + angular.forEach($scope.quotationsForm.$$controls, function (control) { + control.$setDirty(); + control.$validate(); + }); + + return $scope.quotationsForm.$valid; + } + + /** + * Updates description and unit price if user selects a product + * + * @param {*} index index of the sales order line item + */ + function handleProductChange (index) { + if (!$scope.salesOrder.items[index].product_id) { + return; + } + const updateProductDependentFields = (productId) => { + $scope.salesOrder.items[index].item_description = productsCache.get(productId).description; + $scope.salesOrder.items[index].unit_price = parseFloat(productsCache.get(productId).price); + calculateSubtotal(index); + }; + + const productId = $scope.salesOrder.items[index].product_id; + if (productsCache.has(productId)) { + updateProductDependentFields(productId); + return; + } + + civicaseCrmApi('Product', 'get', { id: productId }) + .then(function (result) { + if (result.count > 0) { + productsCache.set(productId, result.values[productId]); + updateProductDependentFields(productId); + } + }); + } + + /** + * Update currency symbol if currecny field is upddated + */ + function handleCurrencyChange () { + $scope.currencySymbol = CurrencyCodes.getSymbol($scope.salesOrder.currency); + } + + /** + * Update tax filed and regenrate line item tax rates for line itme financial types + * + * @param {number} index index of the sales order line item + */ + function handleFinancialTypeChange (index) { + $scope.salesOrder.items[index].tax_rate = 0; + $scope.$emit('totalChange'); + + const updateFinancialTypeDependentFields = (financialTypeId) => { + $scope.salesOrder.items[index].tax_rate = financialTypesCache.get(financialTypeId).tax_rate; + $scope.$emit('totalChange'); + }; + + const financialTypeId = $scope.salesOrder.items[index].financial_type_id; + if (financialTypeId && financialTypesCache.has(financialTypeId)) { + updateFinancialTypeDependentFields(financialTypeId); + return; + } + + if (financialTypeId) { + civicaseCrmApi('EntityFinancialAccount', 'get', { + account_relationship: 'Sales Tax Account is', + entity_table: 'civicrm_financial_type', + entity_id: financialTypeId, + 'api.FinancialAccount.get': { id: '$value.financial_account_id' } + }) + .then(function (result) { + if (result.count > 0) { + financialTypesCache.set(financialTypeId, Object.values(result.values)[0]['api.FinancialAccount.get'].values[0]); + updateFinancialTypeDependentFields(financialTypeId); + } + }); + } + } + + /** + * Sums sales order line item without tax, and computes tax rates separately + * + * @param {number} index index of the sales order line item + */ + function calculateSubtotal (index) { + const item = $scope.salesOrder.items[index]; + if (!item) { + return; + } + + item.subtotal_amount = item.unit_price * item.quantity * ((100 - item.discounted_percentage) / 100) || 0; + $scope.$emit('totalChange'); + } + + /** + * Rounds floating ponumber n to specified number of places + * + * @param {*} n number to round + * @param {*} place decimal places to round to + * @returns {number} the rounded off number + */ + function roundTo (n, place) { + return +(Math.round(n + 'e+' + place) + 'e-' + place); + } + + /** + * Show Quotation success create notification + */ + function showSucessNotification () { + CRM.alert('Your Quotation has been generated successfully.', ts('Saved'), 'success'); + } + + /** + * Handles page rediection after successfully creating quotation. + * + * redirects to main quotation list page if no case is selected + * else redirects to the case view of the selected case. + */ + function redirectToAppropraitePage () { + if (!$scope.salesOrder.case_id) { + $window.location.href = 'a#/quotations'; + } + + const params = { id: $scope.salesOrder.case_id, return: ['case_type_category', 'case_type_id'] }; + civicaseCrmApi('Case', 'getdetails', params) + .then(function (result) { + const categoryId = result.values[$scope.salesOrder.case_id].case_type_category; + $window.location.href = `../case/a/case_type_category=${categoryId}` + + `#/case/list?caseId=${$scope.salesOrder.case_id}&` + + `cf=%7B"case_type_category":"${categoryId}"%7D`; + }); + } + + /** + * @returns {object} api parameters for Case.getlist + */ + function caseApiParam () { + const caseTypeCategoryId = FeatureCaseTypes.getCaseTypes('quotations'); + return { params: { 'case_id.case_type_id.case_type_category': { IN: caseTypeCategoryId } } }; + } + + /** + * Computes total and tax rates from API + */ + function handleTotalChange () { + crmApi4('CaseSalesOrder', 'computeTotal', { + lineItems: $scope.salesOrder.items + }).then(function (results) { + $scope.taxRates = results[0].taxRates; + $scope.salesOrder.total = results[0].totalBeforeTax; + $scope.salesOrder.grandTotal = results[0].totalAfterTax; + }, function (failure) { + // handle failure + }); + } + } +})(angular, CRM.$, CRM._); diff --git a/ang/civicase-features/quotations/directives/quotations-list.directive.html b/ang/civicase-features/quotations/directives/quotations-list.directive.html index d7d2e2028..5eb5ff503 100644 --- a/ang/civicase-features/quotations/directives/quotations-list.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-list.directive.html @@ -2,7 +2,7 @@

{{ ts('Manage Quotations') }}

add_circle {{:: ts('Create Quotation') }} diff --git a/ang/civicase-features/quotations/directives/quotations-list.directive.js b/ang/civicase-features/quotations/directives/quotations-list.directive.js index 389b2e213..c2622de1e 100644 --- a/ang/civicase-features/quotations/directives/quotations-list.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-list.directive.js @@ -14,8 +14,16 @@ /** * @param {object} $scope the controller scope + * @param {object} $window window object of the browser */ - function quotationsListController ($scope) { + function quotationsListController ($scope, $window) { + $scope.redirectToQuotationCreationScreen = redirectToQuotationCreationScreen; + /** + * Redirect user to new quotation screen + */ + function redirectToQuotationCreationScreen () { + $window.location.href = '/civicrm/case-features/a#/new'; + } } })(angular, CRM._); From 49acca0577ef6454540059f1021bde3af580ebef Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 28 Feb 2023 07:24:15 +0100 Subject: [PATCH 028/199] BTHAB-23: Apply PHPCS linter fix --- Civi/Api4/Action/CaseSalesOrder/ComputeTotalAction.php | 10 +++++----- .../Action/CaseSalesOrder/SalesOrderSaveAction.php | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAction.php b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAction.php index a0397c24c..7fea54f59 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAction.php @@ -8,13 +8,13 @@ use CRM_Civicase_BAO_CaseSalesOrder as CaseSalesOrderBAO; /** - * Computes the total of a sales order + * Computes the total of a sales order. */ class ComputeTotalAction extends AbstractAction { use DAOActionTrait; /** - * Sales order line items + * Sales order line items. * * @var array * @required @@ -25,9 +25,9 @@ class ComputeTotalAction extends AbstractAction { * {@inheritDoc} */ public function _run(Result $result) { // phpcs:ignore - if (is_array($this->lineItems)) { - $result[] = CaseSalesOrderBAO::computeTotal($this->lineItems); - } + if (is_array($this->lineItems)) { + $result[] = CaseSalesOrderBAO::computeTotal($this->lineItems); + } } } diff --git a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php index 19b3ab949..373617bbc 100644 --- a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php @@ -54,7 +54,7 @@ protected function writeRecord($items) { array_walk($lineItems, function (&$lineItem) use ($result, $caseSalesOrderLineAPI) { $lineItem['sales_order_id'] = $result['id']; $caseSalesOrderLineAPI->addRecord($lineItem); - }); + }); $result['items'] = $caseSalesOrderLineAPI->execute()->jsonSerialize(); } From a88d97481366cfef05692df9b28467f5618703ca Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 1 Mar 2023 17:26:42 +0100 Subject: [PATCH 029/199] BTHAB-78: Add test to custom sales order API actions --- CRM/Civicase/Test/Fabricator/Product.php | 38 ++++++++ .../CaseSalesOrder/SalesOrderSaveAction.php | 3 +- .../CaseSalesOrder/ComputeTotalActionTest.php | 94 ++++++++++++++++++ .../SalesOrderSaveActionTest.php | 97 +++++++++++++++++++ tests/phpunit/Helpers/CaseSalesOrderTrait.php | 87 +++++++++++++++++ 5 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 CRM/Civicase/Test/Fabricator/Product.php create mode 100644 tests/phpunit/Civi/Api4/CaseSalesOrder/ComputeTotalActionTest.php create mode 100644 tests/phpunit/Civi/Api4/CaseSalesOrder/SalesOrderSaveActionTest.php create mode 100644 tests/phpunit/Helpers/CaseSalesOrderTrait.php diff --git a/CRM/Civicase/Test/Fabricator/Product.php b/CRM/Civicase/Test/Fabricator/Product.php new file mode 100644 index 000000000..7ced2b477 --- /dev/null +++ b/CRM/Civicase/Test/Fabricator/Product.php @@ -0,0 +1,38 @@ + 'test', + 'description' => 'test', + 'sku' => 'test', + 'price' => 20, + ]; + + /** + * Fabricate Case. + * + * @param array $params + * Parameters. + * + * @return mixed + * Api result. + */ + public static function fabricate(array $params = []) { + $params = array_merge(self::$defaultParams, $params); + $result = civicrm_api4('Product', 'create', [ + 'values' => $params, + ])->jsonSerialize(); + + return array_shift($result); + } + +} diff --git a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php index 373617bbc..c792172de 100644 --- a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php @@ -47,7 +47,8 @@ protected function writeRecord($items) { $total = CaseSalesOrderBAO::computeTotal($lineItems); $salesOrder['total_before_tax'] = $total['totalBeforeTax']; $salesOrder['total_after_tax'] = $total['totalAfterTax']; - $result = array_pop($this->writeObjects([$salesOrder])); + $salesOrders = $this->writeObjects([$salesOrder]); + $result = array_pop($salesOrders); $caseSalesOrderLineAPI = CaseSalesOrderLine::save(); if (!empty($result) && !empty($lineItems)) { diff --git a/tests/phpunit/Civi/Api4/CaseSalesOrder/ComputeTotalActionTest.php b/tests/phpunit/Civi/Api4/CaseSalesOrder/ComputeTotalActionTest.php new file mode 100644 index 000000000..78eb11d33 --- /dev/null +++ b/tests/phpunit/Civi/Api4/CaseSalesOrder/ComputeTotalActionTest.php @@ -0,0 +1,94 @@ +registerCurrentLoggedInContactInSession($contact['id']); + } + + /** + * Test case sales order compute action returns expected fields. + */ + public function testComputeTotalActionReturnsExpectedFields() { + $items = []; + $items[] = $this->getCaseSalesOrderLineData( + ['quantity' => 10, 'unit_price' => 10, 'tax_rate' => 10] + ); + $items[] = $this->getCaseSalesOrderLineData( + ['quantity' => 5, 'unit_price' => 10] + ); + + $computedTotal = CaseSalesOrder::computeTotal() + ->setLineItems($items) + ->execute() + ->jsonSerialize()[0]; + + $this->assertArrayHasKey('taxRates', $computedTotal); + $this->assertArrayHasKey('totalBeforeTax', $computedTotal); + $this->assertArrayHasKey('totalAfterTax', $computedTotal); + } + + /** + * Test case sales order total is calculated appropraitely. + */ + public function testComputeTotalActionReturnsExpectedTotal() { + $items = []; + $items[] = $this->getCaseSalesOrderLineData( + ['quantity' => 10, 'unit_price' => 10, 'tax_rate' => 10] + ); + $items[] = $this->getCaseSalesOrderLineData( + ['quantity' => 5, 'unit_price' => 10] + ); + + $computedTotal = CaseSalesOrder::computeTotal() + ->setLineItems($items) + ->execute() + ->jsonSerialize()[0]; + + $this->assertEquals($computedTotal['totalBeforeTax'], 150); + $this->assertEquals($computedTotal['totalAfterTax'], 160); + } + + /** + * Test case sales order tax rates is computed as epxected. + */ + public function testComputeTotalActionReturnsExpectedTaxRates() { + $items = []; + $items[] = $this->getCaseSalesOrderLineData( + ['quantity' => 10, 'unit_price' => 10, 'tax_rate' => 10] + ); + $items[] = $this->getCaseSalesOrderLineData( + ['quantity' => 5, 'unit_price' => 10, 'tax_rate' => 2] + ); + + $computedTotal = CaseSalesOrder::computeTotal() + ->setLineItems($items) + ->execute() + ->jsonSerialize()[0]; + + $this->assertNotEmpty($computedTotal['taxRates']); + $this->assertCount(2, $computedTotal['taxRates']); + + // Ensure the tax rates are sorted in ascending order of rate. + $this->assertEquals($computedTotal['taxRates'][0]['rate'], 2); + $this->assertEquals($computedTotal['taxRates'][0]['value'], 1); + $this->assertEquals($computedTotal['taxRates'][1]['rate'], 10); + $this->assertEquals($computedTotal['taxRates'][1]['value'], 10); + } + +} diff --git a/tests/phpunit/Civi/Api4/CaseSalesOrder/SalesOrderSaveActionTest.php b/tests/phpunit/Civi/Api4/CaseSalesOrder/SalesOrderSaveActionTest.php new file mode 100644 index 000000000..6cb322369 --- /dev/null +++ b/tests/phpunit/Civi/Api4/CaseSalesOrder/SalesOrderSaveActionTest.php @@ -0,0 +1,97 @@ +registerCurrentLoggedInContactInSession($contact['id']); + } + + /** + * Test case sales order and line item can be saved with the save action. + */ + public function testCanSaveCaseSalesOrder() { + $salesOrder = $this->getCaseSalesOrderData(); + + $salesOrderId = CaseSalesOrder::save() + ->addRecord($salesOrder) + ->execute() + ->jsonSerialize()[0]['id']; + + $results = CaseSalesOrder::get() + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->jsonSerialize(); + + $this->assertNotEmpty($results); + foreach (['client_id', 'owner_id', 'notes', 'total_before_tax'] as $key) { + $this->assertEquals($salesOrder[$key], $results[0][$key]); + } + } + + /** + * Test case sales order and line item can be saved with the save action. + */ + public function testCanSaveCaseSalesOrderAndLineItems() { + $salesOrder = $this->getCaseSalesOrderData(); + $salesOrder['items'][] = $this->getCaseSalesOrderLineData(); + $salesOrder['items'][] = $this->getCaseSalesOrderLineData(); + + $salesOrderId = CaseSalesOrder::save() + ->addRecord($salesOrder) + ->execute() + ->jsonSerialize()[0]['id']; + + $results = CaseSalesOrderLine::get() + ->addWhere('sales_order_id', '=', $salesOrderId) + ->execute() + ->jsonSerialize(); + + $this->assertCount(2, $results); + foreach ($results as $result) { + $this->assertEquals($result['sales_order_id'], $salesOrderId); + } + } + + /** + * Test case sales order total is calculated appropraitely. + */ + public function testSaveCaseSalesOrderTotalIsCorrect() { + $salesOrderData = $this->getCaseSalesOrderData(); + $salesOrderData['items'][] = $this->getCaseSalesOrderLineData( + ['quantity' => 10, 'unit_price' => 10, 'tax_rate' => 10] + ); + $salesOrderData['items'][] = $this->getCaseSalesOrderLineData( + ['quantity' => 5, 'unit_price' => 10] + ); + + $salesOrderId = CaseSalesOrder::save() + ->addRecord($salesOrderData) + ->execute() + ->jsonSerialize()[0]['id']; + + $salesOrder = CaseSalesOrder::get() + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->jsonSerialize()[0]; + + $this->assertEquals($salesOrder['total_before_tax'], 150); + $this->assertEquals($salesOrder['total_after_tax'], 160); + } + +} diff --git a/tests/phpunit/Helpers/CaseSalesOrderTrait.php b/tests/phpunit/Helpers/CaseSalesOrderTrait.php new file mode 100644 index 000000000..f80919f67 --- /dev/null +++ b/tests/phpunit/Helpers/CaseSalesOrderTrait.php @@ -0,0 +1,87 @@ +addSelect('id', 'value', 'name', 'label') + ->addWhere('option_group_id:name', '=', 'case_sales_order_status') + ->execute(); + + return $salesOrderStatus; + } + + /** + * Returns fabricated case sales order data. + * + * @param array $default + * Default value. + * + * @return array + * Key-Value pair of a case sales order fields and values + */ + public function getCaseSalesOrderData(array $default = []) { + $client = ContactFabricator::fabricate(); + $caseType = CaseTypeFabricator::fabricate(); + $case = CaseFabricator::fabricate( + [ + 'case_type_id' => $caseType['id'], + 'contact_id' => $client['id'], + 'creator_id' => $client['id'], + ] + ); + + return array_merge([ + 'client_id' => $client['id'], + 'owner_id' => $client['id'], + 'case_id' => $case['id'], + 'currency' => 'GBP', + 'status_id' => $this->getCaseSalesOrderStatus()[0]['value'], + 'description' => 'test', + 'notes' => 'test', + 'total_before_tax' => 0, + 'total_after_tax' => 0, + 'quotation_date' => '2022-08-09', + 'items' => [], + ], $default); + } + + /** + * Returns fabricated case sales order line data. + * + * @param array $default + * Default value. + * + * @return array + * Key-Value pair of a case sales order line item fields and values + */ + public function getCaseSalesOrderLineData(array $default = []) { + $product = ProductFabricator::fabricate(); + return array_merge([ + 'financial_type_id' => 1, + 'product_id' => $product['id'], + 'item_description' => 'test', + 'quantity' => 1, + 'unit_price' => 50, + 'tax_rate' => NULL, + 'discounted_percentage' => NULL, + 'subtotal_amount' => 50, + ], $default); + } + +} From 91b3e5c4c727fec487b098b2c56508e26391879c Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 2 Mar 2023 10:38:34 +0100 Subject: [PATCH 030/199] BTHAB-78: ComputeTotalAction should support empty line items --- .../CaseSalesOrder/ComputeTotalAction.php | 1 - .../CaseSalesOrder/ComputeTotalActionTest.php | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAction.php b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAction.php index 7fea54f59..ca2c7093a 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAction.php @@ -17,7 +17,6 @@ class ComputeTotalAction extends AbstractAction { * Sales order line items. * * @var array - * @required */ protected $lineItems; diff --git a/tests/phpunit/Civi/Api4/CaseSalesOrder/ComputeTotalActionTest.php b/tests/phpunit/Civi/Api4/CaseSalesOrder/ComputeTotalActionTest.php index 78eb11d33..441c7d12b 100644 --- a/tests/phpunit/Civi/Api4/CaseSalesOrder/ComputeTotalActionTest.php +++ b/tests/phpunit/Civi/Api4/CaseSalesOrder/ComputeTotalActionTest.php @@ -91,4 +91,24 @@ public function testComputeTotalActionReturnsExpectedTaxRates() { $this->assertEquals($computedTotal['taxRates'][1]['value'], 10); } + /** + * Test compute action doesn't throw error for empty line items. + */ + public function testComputeTotalActionReturnsEmptyResultForEmptyLineItems() { + $items = []; + + $computedTotal = CaseSalesOrder::computeTotal() + ->setLineItems($items) + ->execute() + ->jsonSerialize()[0]; + + $this->assertArrayHasKey('taxRates', $computedTotal); + $this->assertArrayHasKey('totalBeforeTax', $computedTotal); + $this->assertArrayHasKey('totalAfterTax', $computedTotal); + + $this->assertEmpty($computedTotal['taxRates']); + $this->assertEmpty($computedTotal['totalAfterTax']); + $this->assertEmpty($computedTotal['totalBeforeTax']); + } + } From 5d434db11299cae120a5124a2ae1efcced65a0b2 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 22 Feb 2023 10:30:47 +0100 Subject: [PATCH 031/199] BTHAB-21: Add filter to quotation list view --- ang/afsearchQuotations.aff.html | 24 ++++++++++++++++++++++-- css/civicase.min.css | 2 +- scss/components/_case-features.scss | 4 ++++ 3 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 scss/components/_case-features.scss diff --git a/ang/afsearchQuotations.aff.html b/ang/afsearchQuotations.aff.html index 4c50be2c4..d81b789bc 100644 --- a/ang/afsearchQuotations.aff.html +++ b/ang/afsearchQuotations.aff.html @@ -1,6 +1,26 @@
-
- +
+ + +
+
+ + +
+
+ +
diff --git a/css/civicase.min.css b/css/civicase.min.css index afda796c1..2fb2b752d 100644 --- a/css/civicase.min.css +++ b/css/civicase.min.css @@ -1,2 +1,2 @@ -@keyframes civicase__infinite-rotation{from{transform:rotate(0)}to{transform:rotate(360deg)}}.page-civicrm-case-a .page-title,.page-civicrm-dashboard .page-title,.page-civicrm:not([class*=' page-civicrm-']) .page-title{clip:rect(1px,1px,1px,1px);height:1px;overflow:hidden;position:absolute!important}@font-face{font-family:"Material Icons";font-style:normal;font-weight:400;src:local("Material Icons"),local("MaterialIcons-Regular"),url(../resources/fonts/material-design-icons/MaterialIcons-Regular.woff2) format("woff2"),url(../resources/fonts/material-design-icons/MaterialIcons-Regular.woff) format("woff")}#bootstrap-theme .material-icons{direction:ltr;display:inline-block;font-family:'Material Icons';font-feature-settings:'liga';-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-style:normal;font-weight:400;letter-spacing:normal;line-height:1;text-rendering:optimizeLegibility;text-transform:none;white-space:nowrap;word-wrap:normal}#bootstrap-theme .badge{font-size:13px;line-height:18px;margin-right:8px;padding-bottom:0;padding-top:0}#bootstrap-theme .badge:last-child{margin-right:0}#bootstrap-theme .btn{font-size:13px;line-height:1.384615em}#bootstrap-theme .crm-clear-link{color:#0071bd}#bootstrap-theme .crm-clear-link .fa-times::before{content:'\f057'}.select2-container.crm-token-selector{width:360px!important}#bootstrap-theme .dropdown-menu{padding:10px 0}#bootstrap-theme .dropdown-menu>li>a{line-height:18px;padding:6px 16px}#bootstrap-theme .dropdown-menu>li .material-icons{color:#9494a5;font-size:13px;margin-right:3px;position:relative;top:2px}#bootstrap-theme .crm_notification-badge{line-height:18px;padding:0 10px}#bootstrap-theme .progress{background:#d3dee2;border-radius:2px;box-shadow:none;height:4px}#bootstrap-theme .select2-container-multi .select2-choices{background:#fff}#bootstrap-theme .select2-container-disabled .select2-choice{background:#f3f6f7;cursor:no-drop;opacity:.8}#bootstrap-theme .simplebar-track{background:#e8eef0;overflow:hidden}#bootstrap-theme .simplebar-track.horizontal{position:relative;height:11px}#bootstrap-theme .simplebar-track.horizontal[style="visibility: hidden;"]{height:0}#bootstrap-theme .simplebar-track.horizontal .simplebar-scrollbar{height:5px;top:3px}#bootstrap-theme .simplebar-scrollbar::before{background:#c2cfd8;border-radius:2.5;opacity:1}#bootstrap-theme .civicase__accordion .panel-heading{padding:0 0 15px}#bootstrap-theme .civicase__accordion .panel-title{font-size:16px}#bootstrap-theme .civicase__accordion .panel-title a,#bootstrap-theme .civicase__accordion .panel-title a:hover{text-decoration:none}#bootstrap-theme .civicase__accordion .panel-title a::before{content:'\f105'}#bootstrap-theme .civicase__accordion.panel-open .panel-title a::before{content:'\f107';margin-left:-4px}#bootstrap-theme .civicase__accordion .panel-body{padding:0 0 15px}#bootstrap-theme .civicase__activities-calendar{transition:opacity .2s linear}#bootstrap-theme .civicase__activities-calendar.is-loading-days{opacity:.7}#bootstrap-theme .civicase__activities-calendar .btn-default,#bootstrap-theme .civicase__activities-calendar table,#bootstrap-theme .civicase__activities-calendar th{background:0 0;color:#464354}#bootstrap-theme .civicase__activities-calendar thead th{position:relative;z-index:1}#bootstrap-theme .civicase__activities-calendar thead tr:nth-child(1){background-color:transparent}#bootstrap-theme .civicase__activities-calendar thead .btn{font-size:16px;text-transform:none}#bootstrap-theme .civicase__activities-calendar thead .btn.uib-title{font-weight:600;margin-top:-3px;padding:3px 0 8px}#bootstrap-theme .civicase__activities-calendar thead .btn.uib-left,#bootstrap-theme .civicase__activities-calendar thead .btn.uib-right{margin-top:-3px;max-width:24px;padding:3px 0 8px}#bootstrap-theme .civicase__activities-calendar thead .material-icons{font-size:24px;line-height:24px}#bootstrap-theme .civicase__activities-calendar .civicase__activities-calendar__title-word,#bootstrap-theme .civicase__activities-calendar .uib-title strong{color:#464354;font-size:16px;font-weight:600;line-height:22px}#bootstrap-theme .civicase__activities-calendar .uib-title span:nth-last-child(1){color:#9494a5}#bootstrap-theme .civicase__activities-calendar [uib-daypicker] .uib-title strong{display:none}#bootstrap-theme .civicase__activities-calendar [uib-monthpicker] .civicase__activities-calendar__title-word,#bootstrap-theme .civicase__activities-calendar [uib-yearpicker] .civicase__activities-calendar__title-word{display:none}#bootstrap-theme .civicase__activities-calendar tr{background-color:#fff;padding:0 5px}#bootstrap-theme .civicase__activities-calendar tbody,#bootstrap-theme .civicase__activities-calendar tr:nth-child(0n+2) th{background:#fff}#bootstrap-theme .civicase__activities-calendar tr:nth-child(2n){border-top-left-radius:5px;border-top-right-radius:5px;margin-top:-3px}#bootstrap-theme .civicase__activities-calendar tr:nth-child(2n) th{color:#9494a5;font-size:10px;padding:21px 0;text-transform:uppercase}#bootstrap-theme .civicase__activities-calendar tr:nth-child(2n) .current-week-day{color:#0071bd}#bootstrap-theme .civicase__activities-calendar thead th:nth-child(1){border-top-left-radius:2px}#bootstrap-theme .civicase__activities-calendar thead th:nth-last-child(1){border-top-right-radius:2px}#bootstrap-theme .civicase__activities-calendar tr:nth-last-child(1) td:nth-child(1){border-bottom-left-radius:2px}#bootstrap-theme .civicase__activities-calendar tr:nth-last-child(1) td:nth-last-child(1){border-bottom-right-radius:2px}#bootstrap-theme .civicase__activities-calendar tbody{border-bottom-left-radius:5px;border-bottom-right-radius:5px;box-shadow:0 3px 8px 0 rgba(49,40,40,.15);min-height:205px}#bootstrap-theme .civicase__activities-calendar [uib-monthpicker] thead,#bootstrap-theme .civicase__activities-calendar [uib-yearpicker] thead{padding-bottom:7px}#bootstrap-theme .civicase__activities-calendar [uib-monthpicker] tbody,#bootstrap-theme .civicase__activities-calendar [uib-yearpicker] tbody{margin-top:-3px;min-height:262px}#bootstrap-theme .civicase__activities-calendar tbody .btn{font-weight:600;padding:10px 0;position:relative;width:100%}#bootstrap-theme .civicase__activities-calendar .btn.active{background-color:#cde1ed;color:#0071bd}#bootstrap-theme .civicase__activities-calendar .uib-day .btn.active{height:28px;margin-top:2px;padding:0;width:28px}#bootstrap-theme .civicase__activities-calendar .uib-day .material-icons{display:none}#bootstrap-theme .civicase__activities-calendar__day-status.uib-day .material-icons{color:#9494a5;display:block;font-size:6px;left:50%;position:absolute;transform:translateX(-50%) translateY(3px);width:6px}#bootstrap-theme .civicase__activities-calendar__day-status--completed.uib-day .material-icons{color:#44cb7e}#bootstrap-theme .civicase__activities-calendar__day-status--overdue.uib-day .material-icons{color:#cf3458}#bootstrap-theme .civicase__activities-calendar__day-status--scheduled.uib-day .material-icons{color:#0071bd}#bootstrap-theme .activities-calendar-popover{border-color:#e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);margin-top:15px;max-width:280px;padding:0}#bootstrap-theme .activities-calendar-popover>.arrow{border-bottom-color:#e8eef0}#bootstrap-theme .activities-calendar-popover .popover-content{max-height:330px;overflow-x:hidden;overflow-y:auto;padding:0}#bootstrap-theme .activities-calendar-popover__footer{border-top:1px solid #e8eef0}#bootstrap-theme .activities-calendar-popover__see-all{padding:10px}#bootstrap-theme .civicase__activities-calendar__dropdown{transform:translateX(calc(-100% + 18px))}#bootstrap-theme .civicase__activity-card--big{display:flex;flex-direction:column;height:auto;min-height:264px;width:100%}#bootstrap-theme .civicase__activity-card--big .panel{flex-grow:1}#bootstrap-theme .civicase__activity-card--big .panel .panel-body{padding:16px 24px 24px}#bootstrap-theme .civicase__activity-card--big .civicase__tooltip{flex:1;min-width:0}#bootstrap-theme .civicase__activity-card--big .material-icons{vertical-align:middle}#bootstrap-theme .civicase__activity-card--big .civicase__activity-card-menu{top:-2px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-type{flex:1 0 0;font-size:16px;line-height:22px;margin-bottom:12px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-date{color:#4d4d69}#bootstrap-theme .civicase__activity-card--big .civicase__activity-date .material-icons{font-size:22px;margin-right:5px;position:relative;top:-2px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-card .civicase__checkbox{margin-left:2px;margin-right:10px}#bootstrap-theme .civicase__activity-card--big .civicase__contact-additional__container--avatar{margin-left:5px;margin-top:1px}#bootstrap-theme .civicase__activity-card--big .civicase__contact-avatar{margin-top:0}#bootstrap-theme .civicase__activity-card--big .civicase__contact-icon{margin-top:-3px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-card-row{align-items:flex-start}#bootstrap-theme .civicase__activity-card--big .civicase__activity-card-row.civicase__activity-card-row--first{border-bottom:1px solid #e8eef0;margin:0 -24px 15px;padding:1px 16px 16px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-icon-container{align-items:center;display:flex;justify-content:center;width:auto}#bootstrap-theme .civicase__activity-card--big .civicase__activity-icon-container span:not(.civicase__activity-icon-ribbon){margin:0 8px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-icon-container .civicase__activity-icon-ribbon{border-bottom-width:10px;border-left-width:20px;border-right-width:20px;height:62px;left:16px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-icon-container .civicase__activity-icon{font-size:22px;left:2px;vertical-align:middle}#bootstrap-theme .civicase__activity-card--big .civicase__tags-container{margin-bottom:10px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-subject{color:#9494a5;font-size:13px;font-weight:400;line-height:18px;margin:5px 0 17px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-attachment__container,#bootstrap-theme .civicase__activity-card--big .civicase__activity-star__container{position:relative}#bootstrap-theme .civicase__activity-card--big .civicase__activity-star__container{margin-left:4px}#bootstrap-theme .civicase__activity-card--big--empty{align-items:center;display:flex;flex-direction:column;justify-content:center;min-height:265px;text-align:center;width:100%}#bootstrap-theme .civicase__activity-card--big--empty.civicase__activity-card--big--empty--list-view{border:1px solid #d3dee2;border-radius:2px;min-height:265px}#bootstrap-theme .civicase__activity-card--big--empty-title{font-size:20px;font-weight:600;line-height:27px;margin:15px 0 5px}#bootstrap-theme .civicase__activity-card--big--empty-description{color:#9494a5;margin-bottom:20px;padding:0 5px}#bootstrap-theme .civicase__activity-card--big--empty-button{border-color:#0071bd!important;font-size:13px;font-weight:600;line-height:18px;padding:10px 16px}#bootstrap-theme .civicase__activity-card--big--empty-button,#bootstrap-theme .civicase__activity-card--big--empty-button:active,#bootstrap-theme .civicase__activity-card--big--empty-button:focus{background-color:inherit}#bootstrap-theme .civicase__activity-card--big--empty-button .material-icons{color:#0071bd;margin-right:8px;position:relative;top:-1px}#bootstrap-theme .civicase__activity-card--big--empty-button i:nth-last-child(1){margin-left:8px}#bootstrap-theme .civicase__activity-card--big--empty-button.btn-default:active,#bootstrap-theme .civicase__activity-card--big--empty-button:active,#bootstrap-theme .civicase__activity-card--big--empty-button:hover,#bootstrap-theme .civicase__activity-card--big--empty-button:hover .material-icons,#bootstrap-theme .civicase__activity-card--big--empty-button[disabled]:hover{background-color:#0071bd;color:#fff}#bootstrap-theme .civicase__activity-card--long{box-shadow:0 3px 8px 0 rgba(49,40,40,.15);position:relative}#bootstrap-theme .civicase__activity-card--long .civicase__activity-icon-container .civicase__activity-icon-ribbon{border-bottom-width:10px;border-left-width:20px;border-right-width:20px;height:63px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-icon-container .civicase__activity-icon{font-size:22px;left:12px;top:0}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card-row--first{margin-bottom:0}#bootstrap-theme .civicase__activity-card--long .civicase__activity-icon-container--ribbon{width:50px}#bootstrap-theme .civicase__activity-card--long .civicase__checkbox{margin-left:10px;margin-right:8px}#bootstrap-theme .civicase__activity-card--long .civicase__tooltip{flex:1;max-width:300px;min-width:0}#bootstrap-theme .civicase__activity-card--long .civicase__activity-type{display:block;font-size:16px;margin-right:12px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-attachment__icon{position:relative;top:2px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-attachment__file-options{display:inline-block}#bootstrap-theme .civicase__activity-card--long .civicase__activity-attachment__file-options .civicase__activity-card-menu.btn-group>.dropdown-menu{transform:translateX(0)}#bootstrap-theme .civicase__activity-card--long .civicase__tags-container{margin-right:5px;margin-top:-3px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-star{position:relative;top:3px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-date{margin-left:5px;margin-top:-1px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-date__with-year,#bootstrap-theme .civicase__activity-card--long .civicase__activity-date__without-year{vertical-align:middle}#bootstrap-theme .civicase__activity-card--long .civicase__activity-date__without-year{display:none}#bootstrap-theme .civicase__activity-card--long .civicase__contact-additional__container--avatar{margin-top:0}#bootstrap-theme .civicase__activity-card--long .civicase__activity-subject{color:#9494a5;font-weight:400;margin-left:30px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card-menu.btn-group .btn{margin-left:0;top:-2px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--ribbon{min-height:75px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--ribbon .panel-body{min-height:70px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--ribbon .civicase__activity-subject{margin-left:52px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--with-checkbox .civicase__activity-subject{margin-left:64px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--draft{background:0 0;box-shadow:none}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--draft .panel-footer{border-top:1px dashed #c2cfd8}#bootstrap-theme .civicase__activity-card--long .civicase__activity-icon-arrow{left:22px;top:8px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card__case-type{overflow:hidden;text-overflow:ellipsis}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card-row--communication>div{align-items:center;display:flex}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card-row--communication .civicase__contact-card{position:relative;top:2px}#bootstrap-theme .civicase__activity-card--short{box-shadow:0 1px 4px 0 rgba(49,40,40,.2);min-height:100px;position:relative;width:280px}#bootstrap-theme .civicase__activity-card--short .panel-body{min-height:100px}#bootstrap-theme .civicase__activity-card--short .civicase__contact-avatar{margin-top:-10px}#bootstrap-theme .civicase__activity-card--short .civicase__activity-subject{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__activity-card--short .civicase__tooltip{flex:1;min-width:0}#bootstrap-theme .civicase__activity-card--short .civicase__activity-card__case-type{max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__activity-card{background:#fff;border-radius:5px;cursor:pointer}#bootstrap-theme .civicase__activity-card:hover{background:#f3f6f7}#bootstrap-theme .civicase__activity-card .panel{box-shadow:none;height:100%;margin-bottom:0}#bootstrap-theme .civicase__activity-card .panel-body{background:0 0;border-top:0!important;height:100%;padding:15px}#bootstrap-theme .civicase__activity-card .panel-footer{background:0 0;padding:5px 16px}#bootstrap-theme .civicase__activity-card .civicase__contact-avatar{margin-left:5px}#bootstrap-theme .civicase__activity-card .civicase__checkbox{margin-right:5px;margin-top:2px}#bootstrap-theme .civicase__activity-card .panel-footer:hover{background:#e8eef0}#bootstrap-theme .civicase__activity-card .panel-footer:hover>a:hover{text-decoration:none}#bootstrap-theme .civicase__activity-card-inner{position:relative;width:100%}#bootstrap-theme .civicase__activity-card--empty .panel-body{align-items:center;display:flex;justify-content:center}#bootstrap-theme .civicase__activity-card--draft{border:1px dashed #c2cfd8}#bootstrap-theme .civicase__activity-card--alert{background:#fbf0e2;border:1px solid #e6ab5e}#bootstrap-theme .civicase__activity-card--alert:hover{background:#fbf0e2;border-color:#c2cfd8}#bootstrap-theme .civicase__activity-card--alert .civicase__activity-subject{color:#4d4d69;white-space:initial}#bootstrap-theme .civicase__activity-card--alert .civicase__tags-container{margin-left:30px;margin-top:2px}#bootstrap-theme .civicase__activity-card--file .civicase__activity-subject{color:#464354;font-size:16px;font-weight:600;margin-left:0}#bootstrap-theme .civicase__activity-card__case-id__label,#bootstrap-theme .civicase__activity-card__case-id__value,#bootstrap-theme .civicase__activity-card__case-type{color:#9494a5}#bootstrap-theme .civicase__activity-card__case-id__value{font-weight:600}#bootstrap-theme .civicase__activity-icon-container{color:#0071bd;font-size:18px;width:30px}#bootstrap-theme .civicase__activity-icon-container--ribbon{width:38px}#bootstrap-theme .civicase__activity-icon-container--ribbon .civicase__activity-icon{color:#fff;font-size:16px;left:8px;position:relative;top:-6px;z-index:1}#bootstrap-theme .civicase__activity-icon-arrow{font-size:10px;left:24px;position:absolute;top:11px}#bootstrap-theme .civicase__activity-icon-ribbon{border-bottom:6px solid transparent;border-left:14px solid #0071bd;border-radius:2px;border-right:14px solid #0071bd;height:42px;left:17px;position:absolute;top:-3px;width:0}#bootstrap-theme .civicase__activity-icon-ribbon.text-danger{border-left:14px solid #cf3458;border-right:14px solid #cf3458}#bootstrap-theme .civicase__activity-icon-ribbon.civicase__text-success{border-left:14px solid #44cb7e;border-right:14px solid #44cb7e}#bootstrap-theme .civicase__activity-date{color:#9494a5}#bootstrap-theme .civicase__activity__right-container{margin-left:auto;white-space:nowrap}#bootstrap-theme .civicase__activity__right-container>*{display:inline-block!important;vertical-align:middle}#bootstrap-theme .civicase__activity-type{color:#464354;font-weight:600}#bootstrap-theme .civicase__activity-type--completed{text-decoration:line-through}#bootstrap-theme .civicase__activity-subject{color:#464354;font-weight:600}#bootstrap-theme .civicase__activity-card-row{align-items:flex-start;display:flex;line-height:1.8em;vertical-align:middle}#bootstrap-theme .civicase__activity-card-row--first{margin-bottom:5px}#bootstrap-theme .civicase__activity-card-row--file{display:block;margin-left:30px}#bootstrap-theme .civicase__activity-card-row--case-info{line-height:2em;white-space:nowrap}#bootstrap-theme .civicase__activity-card-row--case-info .civicase__contact-card{margin-right:5px}#bootstrap-theme .civicase__activity-card-row--case-info .civicase__contact-icon{position:relative;top:2px}#bootstrap-theme .civicase__activity-card-row--case-info .civicase__pipe{margin:0 5px}#bootstrap-theme .civicase__activity-with{color:#9494a5}#bootstrap-theme .civicase__activity-star{color:#c2cfd8;font-size:18px}#bootstrap-theme .civicase__activity-star.active{color:#e6ab5e}#bootstrap-theme .civicase__activity-attachment__container a{display:flex!important}#bootstrap-theme .civicase__activity-attachment__container.open .dropdown-toggle{box-shadow:none}#bootstrap-theme .civicase__activity-attachment__container:hover .dropdown-menu{display:block}#bootstrap-theme .civicase__activity-attachment__dropdown-menu{z-index:1061}#bootstrap-theme .civicase__activity-attachment__file-name{color:#0071bd!important}#bootstrap-theme .civicase__activity-attachment__file-description{color:#9494a5}#bootstrap-theme .civicase__activity-attachment__icon{color:#c2cfd8;font-size:18px}#bootstrap-theme .civicase__activity-attachment__icon:hover{color:#0071bd}#bootstrap-theme .civicase__activity-card-menu .material-icons{vertical-align:initial}#bootstrap-theme .civicase__activity-card-menu.btn-group .btn{border:0;height:18px;margin-left:6px;padding:0;top:0;width:18px}#bootstrap-theme .civicase__activity-card-menu.btn-group .btn .material-icons{font-size:18px}#bootstrap-theme .civicase__activity-card-menu.btn-group .btn.dropdown-toggle{background:0 0;box-shadow:none}#bootstrap-theme .civicase__activity-card-menu.btn-group>.dropdown-menu{left:50%!important;top:150%!important;transform:translateX(-100%)}#bootstrap-theme .civicase__activity-attachment__file-icon{color:#9494a5}#bootstrap-theme .civicase__activity-attachment-load{padding:10px 20px!important}#bootstrap-theme .civicase__activity-attachment-load-icon{animation:civicase__infinite-rotation 2s linear reverse;font-size:16px;margin-top:3px;position:relative;top:3px}#bootstrap-theme .civicase__activity-empty-message{color:#9494a5;font-size:16px;text-align:center}#bootstrap-theme .civicase__activity-empty-link{display:block;text-align:center}#bootstrap-theme .civicase__activity-no-result-icon{background-position:center center;background-repeat:no-repeat;background-size:contain;height:48px;width:48px}#bootstrap-theme .civicase__activity-no-result-icon--milestone{background-image:url(../resources/icons/milestone.svg)}#bootstrap-theme .civicase__activity-no-result-icon--activity{background-image:url(../resources/icons/activities.svg)}#bootstrap-theme .civicase__activity-no-result-icon--case{background-image:url(../resources/icons/cases.svg)}#bootstrap-theme .civicase__activity-no-result-icon--communications{background-image:url(../resources/icons/comms.svg);width:66px}#bootstrap-theme .civicase__activity-no-result-icon--tasks{background-image:url(../resources/icons/tasks.svg)}#bootstrap-theme .civicase__activity-feed{box-shadow:none;margin-bottom:0}#bootstrap-theme .civicase__activity-feed .panel-body{background:0 0;border-top:0!important;padding:15px}#bootstrap-theme .civicase__activity-feed>.panel-body{padding-bottom:0;padding-top:8px}#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header{margin:auto}#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header>.panel-body{background:0 0;border-top:0;box-shadow:none;padding:0}#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header .panel-title.civicase__overdue-activity-icon--before{color:#464354!important;padding-left:35px}#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header .panel-title.civicase__overdue-activity-icon--before::after,#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header .panel-title.civicase__overdue-activity-icon--before::before{font-size:14px;height:20px;line-height:20px;top:8px;width:20px}#bootstrap-theme .civicase__activity-feed .civicase__bulkactions-message{margin:0 85px}#bootstrap-theme .civicase__activity-feed .civicase__bulkactions-message .alert{border-bottom:1px solid #d3dee2!important;box-shadow:none}#bootstrap-theme .civicase__activity-feed__activity-container{display:inline-block;margin-left:5px;width:calc(100% - 50px)}#bootstrap-theme .civicase__activity-feed__list{padding-left:10px;padding-right:10px;padding-top:6px;position:relative}#bootstrap-theme .civicase__activity-feed__list.active{background:#b3d5ec;border-radius:5px}#bootstrap-theme .civicase__activity-feed__list.civicase__animated-checkbox-card--expanded{padding-left:50px}#bootstrap-theme .civicase__activity-feed__list__vertical_bar::before{background-color:#c2cfd8;bottom:1px;content:'';left:17px;position:absolute;top:50px;width:8px;z-index:1}#bootstrap-theme .civicase__activity-feed__list-item{display:inline-block;position:relative;width:100%}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card{display:inline-block;margin-top:2px;position:relative;vertical-align:top;z-index:2}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card .civicase__contact-avatar{height:40px;line-height:30px;width:40px}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card .civicase__contact-avatar--image img{height:40px;width:40px}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card .civicase__contact-avatar__full-name{height:40px}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card .civicase__contact-avatar--image:hover .civicase__contact-avatar__full-name{left:39px}#bootstrap-theme .civicase__activity-feed__list-item .civicase__activity-card{margin-bottom:5px;margin-left:auto;margin-right:auto;width:100%}#bootstrap-theme .civicase__checkbox--bulk-action{display:inline-block;margin-right:20px;top:13px;vertical-align:top}#bootstrap-theme .civicase__activity-feed-pager .material-icons{font-size:28px}#bootstrap-theme .civicase__activity-feed-pager--down .civicase__activity-feed-pager__more>.btn,#bootstrap-theme .civicase__activity-feed-pager--down .civicase__activity-feed-pager__no-more>.btn{margin-top:40px}#bootstrap-theme .civicase__activity-feed-pager--down .civicase__spinner{margin-top:40px}#bootstrap-theme .civicase__activity-feed-pager__no-more>.btn{background:0 0;border:0;font-weight:600}#bootstrap-theme .civicase__activity-feed__body{display:flex;justify-content:center;margin:auto;max-width:1330px}#bootstrap-theme .civicase__activity-feed__body__list{flex-grow:1;max-width:630px;min-height:400px;overflow:auto}#bootstrap-theme .civicase__activity-feed__body__details{box-sizing:content-box;min-height:400px;min-width:550px;overflow-y:auto;padding-left:15px;padding-right:10px;padding-top:8px;width:550px}#bootstrap-theme .civicase__activity-feed__body__month-nav{margin-left:15px;min-height:400px;overflow-x:hidden;overflow-y:auto;width:125px}#bootstrap-theme .civicase__activity-feed__placeholder{margin-left:auto;margin-right:auto;width:50%}#bootstrap-theme .civicase__activity-feed__placeholder .civicase__panel-transparent-header{width:100%}@media (max-width:1300px){#bootstrap-theme .civicase__activity-feed .civicase__activity-card--long .civicase__activity-date__with-year{display:none}#bootstrap-theme .civicase__activity-feed .civicase__activity-card--long .civicase__activity-date__without-year{display:inline-block}#bootstrap-theme .civicase__activity-feed .civicase__activity-card--long.civicase__activity-card--ribbon .panel-body{padding:15px 10px 15px 15px}#bootstrap-theme .civicase__activity-feed .civicase__activity-card--long .civicase__tags-container .badge{max-width:35px}#bootstrap-theme .civicase__activity-feed__body__list--details-visible .civicase__contact-name{max-width:40px}}#bootstrap-theme .civicase__activity-filter{background:#e8eef0;padding:16px 40px;width:100%;z-index:11}#bootstrap-theme .civicase__activity-filter__settings .dropdown-toggle{background:0 0!important;box-shadow:none!important;padding:0}#bootstrap-theme .civicase__activity-filter__settings .dropdown-menu{width:250px}#bootstrap-theme .civicase__activity-filter__add .dropdown-menu li,#bootstrap-theme .civicase__activity-filter__settings .dropdown-menu li{padding:0 20px}#bootstrap-theme .civicase__activity-filter__add .dropdown-menu li label,#bootstrap-theme .civicase__activity-filter__settings .dropdown-menu li label{font-weight:400;margin-left:5px;position:relative;top:3px}#bootstrap-theme .civicase__activity-filter__add .material-icons,#bootstrap-theme .civicase__activity-filter__settings .material-icons{color:#9494a5;font-size:20px;position:relative;top:2px}#bootstrap-theme .civicase__activity-filter__add .caret,#bootstrap-theme .civicase__activity-filter__settings .caret{line-height:20px;margin-left:4px;position:relative;top:-5px}#bootstrap-theme .civicase__activity-filter__add,#bootstrap-theme .civicase__activity-filter__others{min-width:150px}#bootstrap-theme .civicase__activity-filter__add .select2-container,#bootstrap-theme .civicase__activity-filter__others .select2-container{height:auto!important;max-width:170px;min-width:170px}#bootstrap-theme .civicase__activity-filter__contact{margin-left:8px}#bootstrap-theme .civicase__activity-filter__contact .btn{border:1px solid #c2cfd8!important}#bootstrap-theme .civicase__activity-filter__contact .btn.active{background:#f3f6f7;box-shadow:inset 0 0 5px 0 rgba(0,0,0,.1);color:#0071bd}#bootstrap-theme .civicase__activity-filter__timeline{width:auto}#bootstrap-theme .civicase__activity-filter__case-type-categories{display:inline-block;margin-left:5px;width:175px}#bootstrap-theme .civicase__activity-filter__category{vertical-align:top;width:175px}#bootstrap-theme .civicase__activity-filter__category .crm-i{color:#4d4d69}#bootstrap-theme .civicase__activity-filter__category .select2-chosen{max-width:130px}#bootstrap-theme .civicase__activity-filter__category,#bootstrap-theme .civicase__activity-filter__timeline{display:inline-block;margin-left:8px}#bootstrap-theme .civicase__activity-filter__category .select2-choice .select2-arrow,#bootstrap-theme .civicase__activity-filter__timeline .select2-choice .select2-arrow{top:0;width:24px}#bootstrap-theme .civicase__activity-filter__attachment,#bootstrap-theme .civicase__activity-filter__more,#bootstrap-theme .civicase__activity-filter__star{background:0 0!important;padding:5px;text-transform:initial}#bootstrap-theme .civicase__activity-filter__attachment .material-icons,#bootstrap-theme .civicase__activity-filter__more .material-icons,#bootstrap-theme .civicase__activity-filter__star .material-icons{color:#9494a5;font-size:18px;vertical-align:middle}#bootstrap-theme .civicase__activity-filter__attachment.btn-active .material-icons,#bootstrap-theme .civicase__activity-filter__more.btn-active .material-icons,#bootstrap-theme .civicase__activity-filter__star.btn-active .material-icons{color:#0071bd}#bootstrap-theme .civicase__activity-filter__more,#bootstrap-theme .civicase__activity-filter__star{padding-left:0}#bootstrap-theme .civicase__activity-filter__star.btn-active .material-icons{color:#e6ab5e}#bootstrap-theme .civicase__activity-filter__more span{color:#4d4d69;position:relative;top:2px}#bootstrap-theme .civicase__activity-filter__more-container{margin-top:15px}#bootstrap-theme .civicase__activity-filter__more-container>*{display:inline-block;margin-bottom:15px;margin-left:5px;margin-right:5px;vertical-align:top}#bootstrap-theme .civicase__activity-filter__custom .civicase__activity-filter__header{border-bottom:1px solid #e8eef0;display:block;margin-top:5px}@media (max-width:1420px){#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter{padding:16px 10px}#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__timeline{width:120px!important}#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__category{width:165px!important}#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__more__text{display:none}#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__attachment,#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__more,#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__star{padding:5px 2px}}#bootstrap-theme .civicase__activity-month-nav{overflow:hidden;width:105px}#bootstrap-theme .civicase__activity-month-nav.affix{position:fixed!important}#bootstrap-theme .civicase__activity-month-nav__group{border-left:2px solid #d9e1e6}#bootstrap-theme .civicase__activity-month-nav__group-month,#bootstrap-theme .civicase__activity-month-nav__group-title,#bootstrap-theme .civicase__activity-month-nav__group-year{padding-left:15px}#bootstrap-theme .civicase__activity-month-nav__group-title{color:#464354;font-weight:700;text-transform:uppercase}#bootstrap-theme .civicase__activity-month-nav__group-year{color:#464354}#bootstrap-theme .civicase__activity-month-nav__group-gap{height:10px}#bootstrap-theme .civicase__activity-month-nav__group-month{color:#9494a5;cursor:pointer;font-weight:600}#bootstrap-theme .civicase__activity-month-nav__group-month.active{border-left:2px solid #0071bd;color:#0071bd;margin-left:-2px}#bootstrap-theme .civicase__activity-month-nav__group-month:hover{color:#0071bd}#bootstrap-theme .civicase__overdue-activity-icon{color:#cf3458!important;display:inline-block;font-weight:600;padding-right:20px;position:relative}#bootstrap-theme .civicase__overdue-activity-icon::before{background-color:#cf3458;content:''}#bootstrap-theme .civicase__overdue-activity-icon::after{color:#fff;content:'!';top:1px}#bootstrap-theme .civicase__overdue-activity-icon::after,#bootstrap-theme .civicase__overdue-activity-icon::before{border-radius:50%!important;font-size:11px;height:13px;line-height:1em;position:absolute;right:0;text-align:center;top:50%;transform:translateY(-50%);width:13px;z-index:0!important}#bootstrap-theme .civicase__overdue-activity-icon.civicase__overdue-activity-icon--before{padding-left:20px;padding-right:0}#bootstrap-theme .civicase__overdue-activity-icon.civicase__overdue-activity-icon--before::after,#bootstrap-theme .civicase__overdue-activity-icon.civicase__overdue-activity-icon--before::before{left:2px;right:auto}#bootstrap-theme .civicase__activity-panel.affix{position:fixed!important}#bootstrap-theme .civicase__activity-panel .panel{overflow:auto;position:relative}#bootstrap-theme .civicase__activity-panel .panel-heading,#bootstrap-theme .civicase__activity-panel .panel-subheading{position:absolute;width:100%}#bootstrap-theme .civicase__activity-panel .panel-subheading{border-bottom:1px solid #e8eef0;top:63px}#bootstrap-theme .civicase__activity-panel .panel-body{margin-bottom:76px;margin-top:120px;overflow:auto;padding:0}#bootstrap-theme .civicase__activity-panel .panel-subtitle{color:#464354;display:flex;font-size:16px;font-weight:600;line-height:25px}#bootstrap-theme .civicase__activity-panel .civicase__tooltip{flex:1;min-width:0}#bootstrap-theme .civicase__activity-panel .civicase__activity__right-container .civicase__activity-date{font-size:13px;font-weight:400;position:relative;top:2px}#bootstrap-theme .civicase__activity-panel .civicase__activity__right-container .civicase__activity-star{position:relative;top:2px}#bootstrap-theme .civicase__activity-panel .civicase__activity__right-container .civicase__contact-additional__container--avatar{margin-top:0}#bootstrap-theme .civicase__activity-panel__close,#bootstrap-theme .civicase__activity-panel__maximise{color:#464354;font-size:18px;padding:0}#bootstrap-theme .civicase__activity-panel__maximise{margin-right:5px;transform:rotate(45deg)}#bootstrap-theme .civicase__activity-panel__status-dropdown{margin-right:5px}#bootstrap-theme .civicase__activity-panel__status-dropdown .list-group-item-info{color:#9494a5;display:block;padding:7px 19px 7px 24px}#bootstrap-theme .civicase__activity-panel__priority-dropdown{margin-right:10px}#bootstrap-theme .civicase__activity-panel__priority-dropdown .list-group-item-info{color:#9494a5;display:block;padding:7px 19px 7px 24px}#bootstrap-theme .civicase__activity-panel__id{line-height:33px}#bootstrap-theme .civicase__activity-panel__resume-draft{bottom:20px;height:36px;position:absolute;right:20px}#bootstrap-theme .civicase__activity-panel__core_container{min-height:200px;position:static!important}#bootstrap-theme .civicase__activity-panel__core_container .help{display:none}#bootstrap-theme .civicase__activity-panel__core_container .crm-activity-form-block-separation{display:none}#bootstrap-theme .civicase__activity-panel__core_container .crm-form-block{overflow:auto;padding-top:20px}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody td:nth-child(2)>*,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody td:nth-child(2)>*{margin-bottom:10px!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody td:nth-child(2) .crm-form-checkbox,#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody td:nth-child(2) .crm-form-radio,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody td:nth-child(2) .crm-form-checkbox,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody td:nth-child(2) .crm-form-radio{margin-right:5px;position:relative;top:2px}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel,#bootstrap-theme .civicase__activity-panel__core_container .form-layout{box-shadow:none}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody>tr,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel tbody>tr,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody>tr{border:0}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody>tr .label,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel tbody>tr .label,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody>tr .label{padding-left:15px!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody>tr .view-value,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel tbody>tr .view-value,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody>tr .view-value{padding-right:15px!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body .label,#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body .label label,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel .label,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel .label label,#bootstrap-theme .civicase__activity-panel__core_container .form-layout .label,#bootstrap-theme .civicase__activity-panel__core_container .form-layout .label label{color:#9494a5!important;font-weight:400!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body .section-shown,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel .section-shown,#bootstrap-theme .civicase__activity-panel__core_container .form-layout .section-shown{padding:0}#bootstrap-theme .civicase__activity-panel__core_container .crm-button_qf_Activity_cancel{display:none}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons{border-bottom:0!important;border-top:1px solid #e8eef0;bottom:0;height:auto!important;margin:0;padding:20px!important;position:absolute;width:100%}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit{color:#fff!important;background-color:#0071bd;border-color:#0062a4}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:focus{color:#fff!important;background-color:#00538a;border-color:#001624}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:hover{color:#fff!important;background-color:#00538a;border-color:#003d66}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.active,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:active,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.active,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:active,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .cancel.dropdown-toggle,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .edit.dropdown-toggle{color:#fff!important;background-color:#00538a;background-image:none;border-color:#003d66}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.active:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:active:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.active:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:active:hover,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .cancel.dropdown-toggle.focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .cancel.dropdown-toggle:focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .cancel.dropdown-toggle:hover,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .edit.dropdown-toggle.focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .edit.dropdown-toggle:focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .edit.dropdown-toggle:hover{color:#fff!important;background-color:#003d66;border-color:#001624}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.disabled.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.disabled:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.disabled:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel[disabled].focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel[disabled]:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel[disabled]:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.disabled.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.disabled:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.disabled:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit[disabled].focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit[disabled]:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit[disabled]:hover,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .cancel.focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .cancel:focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .cancel:hover,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .edit.focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .edit:focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .edit:hover{background-color:#0071bd;border-color:#0062a4}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel .badge,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit .badge{color:#0071bd;background-color:#fff!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete{color:#fff;background-color:#cf3458;border-color:#bd2d4e;background-color:#cf3458!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:focus{color:#fff;background-color:#a82846;border-color:#561423}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:hover{color:#fff;background-color:#a82846;border-color:#8b213a}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.active,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:active,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .delete.dropdown-toggle{color:#fff;background-color:#a82846;background-image:none;border-color:#8b213a}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.active:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:active:hover,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .delete.dropdown-toggle.focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .delete.dropdown-toggle:focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .delete.dropdown-toggle:hover{color:#fff;background-color:#8b213a;border-color:#561423}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.disabled.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.disabled:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.disabled:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete[disabled].focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete[disabled]:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete[disabled]:hover,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .delete.focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .delete:focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .delete:hover{background-color:#cf3458;border-color:#bd2d4e}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete .badge{color:#cf3458;background-color:#fff}#bootstrap-theme .civicase__activity-panel__core_container .crm-form-date-wrapper{display:inline-block}#bootstrap-theme .civicase__activity-panel__core_container .crm-form-date{width:153px}#bootstrap-theme .civicase__activity-panel__core_container .crm-form-time{margin-left:10px;width:75px}#bootstrap-theme .civicase__activity-panel__core_container .crm-ajax-select{width:230px}#bootstrap-theme .civicase__activity-panel__core_container input[name=followup_activity_subject]{width:230px}#bootstrap-theme .civicase__activity-panel__core_container .view-value>input,#bootstrap-theme .civicase__activity-panel__core_container .view-value>select{width:230px}#bootstrap-theme .civicase__activity-panel__core_container .view-value .crm-form-date-wrapper{margin-bottom:10px}.civicase__badge{border-radius:10px;color:#fff;display:inline-block;font-size:13px;line-height:18px;padding:0 7px}.civicase__badge.text-dark{color:#000}.civicase__badge--default{background:#fff;box-shadow:0 0 0 1px #d3dee2 inset;color:#000}#bootstrap-theme .civicase__bulkactions-checkbox{background:#fff;border:1px solid #c2cfd8;border-radius:2px;display:inline-block;padding:0 3px;position:relative}#bootstrap-theme .civicase__bulkactions-checkbox-toggle{color:#e8eef0;cursor:pointer;font-size:18px;margin-left:3px;transition:.3s color cubic-bezier(0,0,0,.4);vertical-align:middle}#bootstrap-theme .civicase__bulkactions-checkbox-toggle.civicase__checkbox{display:inline-block}#bootstrap-theme .civicase__bulkactions-checkbox-toggle .civicase__checkbox--checked{color:#0071bd;transition-property:color}#bootstrap-theme .civicase__bulkactions-checkbox-toggle .civicase__checkbox--checked--hide{color:#c2cfd8;font-size:19.5px;left:3px;top:2px}#bootstrap-theme .civicase__bulkactions-select-mode-dropdown{background:#fff;padding:4px 5px;vertical-align:middle}#bootstrap-theme .civicase__bulkactions-actions-dropdown{margin-left:10px;position:relative}#bootstrap-theme .civicase__bulkactions-actions-dropdown .btn{border:1px solid #c2cfd8;line-height:18px;padding:5px 25px 5px 10px;text-transform:unset}#bootstrap-theme .civicase__bulkactions-actions-dropdown .btn:hover{border-color:#c2cfd8!important}#bootstrap-theme .civicase__bulkactions-actions-dropdown .btn+.dropdown-toggle{padding:5px}#bootstrap-theme .civicase__bulkactions-message .alert{background-color:#f3f6f7;border:1px solid #d3dee2;box-shadow:0 3px 8px 0 rgba(49,40,40,.15);line-height:18px;margin-bottom:0;padding:15px;text-align:center}#bootstrap-theme .civicase__checkbox--bulk-action .civicase__checkbox--checked{color:#0071bd}#bootstrap-theme .civicase__button--with-shadow{box-shadow:0 3px 18px 0 rgba(48,40,40,.25)}#bootstrap-theme .civicase__case-activity-count__popover{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);padding:0;white-space:nowrap;z-index:11}#bootstrap-theme .civicase__case-activity-count__popover .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__case-activity-count__popover .arrow.left{left:20px}#bootstrap-theme .civicase__case-body{padding:0}#bootstrap-theme .civicase__case-body .tab-content{background:0 0;position:relative;z-index:0}#bootstrap-theme .civicase__case-body_tab{position:relative;z-index:1}#bootstrap-theme .civicase__case-body_tab.affix{position:fixed;top:0;z-index:11}#bootstrap-theme .civicase__case-body_tab.affix+.tab-content{padding-top:50px}#bootstrap-theme .civicase__case-body_tab>[civicase-dropdown]{opacity:1}#bootstrap-theme .civicase__case-details-panel--summary .civicase__activity-filter.affix,#bootstrap-theme .civicase__case-details-panel--summary .civicase__case-body_tab.affix{width:calc(100% - 300px)}#bootstrap-theme .civicase__case-details-panel--focused .civicase__activity-filter.affix,#bootstrap-theme .civicase__case-details-panel--focused .civicase__case-body_tab.affix{width:100%}#bootstrap-theme .civicase__case-card{border-radius:0!important;box-shadow:none;cursor:pointer;height:100%;margin-bottom:0}#bootstrap-theme .civicase__case-card:hover{background:#d9edf7}#bootstrap-theme .civicase__case-card .panel-body{background-color:transparent;border:0!important}#bootstrap-theme .civicase__case-card .civicase__contact-card{font-size:14px;font-weight:600;line-height:18px}#bootstrap-theme .civicase__case-card .civicase__contact-card>span{display:flex}#bootstrap-theme .civicase__case-card .civicase__contact-icon{color:#c2cfd8;font-size:24px;margin-top:-3px}#bootstrap-theme .civicase__case-card .civicase__checkbox{left:20px;top:15px}#bootstrap-theme .civicase__case-card .civicase__tags-container .badge{max-width:195px}#bootstrap-theme .civicase__case-card--closed{background-image:repeating-linear-gradient(60deg,#e8eef0,#e8eef0 2px,#f3f6f7 2px,#f3f6f7 20px);min-height:149px}#bootstrap-theme .civicase__case-card--closed .civicase__case-card-subject,#bootstrap-theme .civicase__case-card--closed .civicase__case-card__type,#bootstrap-theme .civicase__case-card--closed .civicase__contact-additional__container,#bootstrap-theme .civicase__case-card--closed .civicase__contact-name{text-decoration:line-through}#bootstrap-theme .civicase__case-card--closed .civicase__case-card__activity-count,#bootstrap-theme .civicase__case-card--closed .civicase__case-card__next-milestone-date{color:inherit;font-weight:400}#bootstrap-theme .civicase__case-card--case-list .civicase__case-card__activity-info{overflow:hidden;white-space:nowrap}#bootstrap-theme .civicase__case-card--case-list .civicase__contact-name,#bootstrap-theme .civicase__case-card--case-list .civicase__contact-name-additional{max-width:100px}#bootstrap-theme .civicase__case-card--other{border-bottom:1px solid #e8eef0;min-height:auto}#bootstrap-theme .civicase__case-card--other .civicase__contact-additional__container,#bootstrap-theme .civicase__case-card--other .civicase__contact-name{color:#464354;font-size:16px}#bootstrap-theme .civicase__case-card--other .civicase__contact-name{max-width:none}#bootstrap-theme .civicase__case-card--other .civicase__case-card__activity-info{display:inline-flex}#bootstrap-theme .civicase__case-card--other .civicase__case-card__next-milestone{margin-right:30px}#bootstrap-theme .civicase__case-card__right_container{color:#9494a5}#bootstrap-theme .civicase__case-card__dates{margin-right:16px;vertical-align:text-top}#bootstrap-theme .civicase__case-card__link-type{font-size:16px;margin-left:16px;vertical-align:middle}#bootstrap-theme .civicase__case-card--active{border-bottom:1px solid #0071bd!important;border-right:1px solid #0071bd!important;border-top:1px solid #0071bd!important}#bootstrap-theme .civicase__case-card__additional-information{line-height:normal;position:absolute;right:20px;top:15px}#bootstrap-theme .civicase__case-card__case-id{color:#9494a5}#bootstrap-theme .civicase__case-card__lock{color:#c2cfd8;font-size:24px;line-height:0;position:relative;top:5px}#bootstrap-theme .civicase__case-card__type{color:#4d4d69}#bootstrap-theme .civicase__case-card__activity-info,#bootstrap-theme .civicase__case-card__contact,#bootstrap-theme .civicase__case-card__next-milestone,#bootstrap-theme .civicase__case-card__type{margin-bottom:3px}#bootstrap-theme .civicase__case-card__activity-info,#bootstrap-theme .civicase__case-card__next-milestone{color:#9494a5}#bootstrap-theme .civicase__case-card__activity-count,#bootstrap-theme .civicase__case-card__next-milestone-date{color:#0071bd;font-weight:600}#bootstrap-theme .civicase__case-card__activity-count:hover,#bootstrap-theme .civicase__case-card__next-milestone-date:hover{text-decoration:none}#bootstrap-theme .civicase__case-card__activity-count--zero{color:#9494a5}#bootstrap-theme .civicase__case-card__activity-count-container{display:inline-block;margin-right:8px}#bootstrap-theme .civicase__case-card--detached{background:#fff;border-radius:2px!important;box-shadow:0 3px 8px 0 rgba(49,40,40,.15);color:#9494a5;margin-bottom:10px}#bootstrap-theme .civicase__case-card--detached>.panel-body{padding:0}#bootstrap-theme .civicase__case-card--detached .civicase__case-card__date{color:#4d4d69}#bootstrap-theme .civicase__case-card--detached .civicase__contact-icon{font-size:18px;vertical-align:middle}#bootstrap-theme .civicase__case-card--detached .crm_notification-badge{vertical-align:unset}#bootstrap-theme .civicase__case-card--detached .civicase__case-card-subject{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:calc(100% - 300px)}#bootstrap-theme .civicase__case-card--detached.civicase__case-card--closed{background:#f3f6f7;min-height:auto}#bootstrap-theme .civicase__case-card--detached.civicase__case-card--closed .civicase__case-card-role,#bootstrap-theme .civicase__case-card--detached.civicase__case-card--closed .civicase__case-card-subject{color:inherit}#bootstrap-theme .civicase__case-card--detached.civicase__case-card--active{box-shadow:0 0 0 5px #b3d5ec}#bootstrap-theme .civicase__case-card-role-container>*{vertical-align:middle}#bootstrap-theme .civicase__case-card-role-container .material-icons{margin-right:5px}#bootstrap-theme .civicase__case-card-role,#bootstrap-theme .civicase__case-card-subject{color:#4d4d69;line-height:18px}#bootstrap-theme .civicase__case-card__row{border-bottom:1px solid #e8eef0;clear:both}#bootstrap-theme .civicase__case-card__row:last-child{border-bottom:0}#bootstrap-theme .civicase__case-card__row--primary{padding:15px}#bootstrap-theme .civicase__case-card__row--secondary{padding:10px 15px}#bootstrap-theme .civicase__case-custom-fields__container.civicase__summary-tab-tile{padding-top:0}#bootstrap-theme .civicase__case-custom-fields__container civicase-masonry-grid-item:not(:first-child){margin-top:30px}#bootstrap-theme .civicase__case-custom-fields__container .panel-body{border-top:0!important}#bootstrap-theme .civicase__case-custom-fields__container .crm-editable-enabled:not(.crm-editable-editing):hover{border:2px dashed transparent;padding:24px}#bootstrap-theme .civicase__case-custom-fields__container .crm-case-custom-form-block table{width:100%}#bootstrap-theme .civicase__case-custom-fields__container .crm-submit-buttons{border:0;padding:15px 8px 0;text-align:right}#bootstrap-theme .civicase__case-custom-fields__container .crm-form-submit{min-width:auto;padding:7px 19px}#bootstrap-theme .civicase__case-custom-fields__container .crm-form-submit.cancel{padding:6px 19px}#bootstrap-theme .civicase__case-custom-fields__container .crm-form-submit.cancel:hover{color:#fff!important}#bootstrap-theme .civicase__case-custom-fields__container .crm-ajax-container{background:#fff;padding:20px}#bootstrap-theme .civicase__case-custom-fields__container .crm-ajax-container .crm-block{box-shadow:none}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row label{color:#9494a5;font-weight:400!important}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row:not(:first-child){display:block;margin-top:16px}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:first-child{display:block;text-align:left}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2){display:block;margin-left:7px}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) .cke,#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) textarea{width:calc(100% - 4px)}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) .select2-arrow{padding-right:20px}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) .crm-form-checkbox,#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) .crm-form-radio{margin-right:8px;margin-top:-4px}#bootstrap-theme .civicase__case-details__add-new-dropdown{left:-20px;position:relative;top:6px}#bootstrap-theme .civicase__case-details__add-new-dropdown .btn-primary{border:1px solid #fff;line-height:20px;padding:7px 16px}#bootstrap-theme .civicase__case-details__add-new-dropdown .btn-primary i:nth-child(1){margin-right:8px;position:relative;top:1px}#bootstrap-theme .civicase__case-details__add-new-dropdown .btn-primary i:nth-last-child(1){margin-left:8px}#bootstrap-theme .civicase__case-details__add-new-dropdown [civicase-dropdown]{position:relative}#bootstrap-theme .civicase__case-details__add-new-dropdown .dropdown-menu{left:auto;right:0;width:180px}#bootstrap-theme .civicase__case-details__add-new-dropdown [civicase-dropdown] .dropdown-menu{top:0}#bootstrap-theme .civicase__case-details__add-new-dropdown .dropdown-menu .fa-fw,#bootstrap-theme .civicase__case-details__add-new-dropdown a .material-icons{color:#0071bd}#bootstrap-theme .civicase__dropdown-menu--filters.dropdown-menu{max-height:260px;overflow-x:hidden;overflow-y:scroll;padding:8px 0 0;right:170px;top:0;width:220px}#bootstrap-theme .civicase__dropdown-menu--filters.dropdown-menu .form-control{margin:0 auto;width:204px}#bootstrap-theme .civicase__dropdown-menu--filters.dropdown-menu .form-control-feedback{color:#464354;font-size:14px;margin-right:8px;margin-top:7px;position:absolute}#bootstrap-theme .civicase__dropdown-menu--filters.dropdown-menu a{padding:9px 17px;white-space:normal}#bootstrap-theme .civicase__activity-dropdown .civicase__dropdown-menu--filters a{padding:9px 17px 9px 38px}#bootstrap-theme .civicase__activity-dropdown .civicase__dropdown-menu--filters .fa-fw{margin-left:-21px;margin-right:4px}#bootstrap-theme [civicase-dropdown]{position:relative}#bootstrap-theme [civicase-dropdown] .dropdown-menu{top:calc(100% + 8px)}#bootstrap-theme .civicase__case-tab--files .civicase__activity-feed__list{margin-left:auto;margin-right:auto;width:685px}#bootstrap-theme .civicase__case-tab--files .civicase__activity-feed__list::before{content:none}#bootstrap-theme .civicase__case-tab--files .civicase__bulkactions-message{margin:0 85px 10px}#bootstrap-theme .civicase__case-tab--files .civicase__bulkactions-message .alert{border-bottom:1px solid #d3dee2!important;box-shadow:none}#bootstrap-theme .civicase__file-tab-filters{display:inline-block;float:right}#bootstrap-theme .civicase__file-filters-container{display:flex}#bootstrap-theme .civicase__file-filters{margin-left:16px;width:180px}#bootstrap-theme .civicase__file-filters .select2-container{height:auto!important}#bootstrap-theme .civicase__file-filters:not(:nth-child(2)) .form-control:not(.select2-container){width:180px}#bootstrap-theme .civicase__file-filters .input-group-addon{font-size:14px;padding:0;width:30px!important}#bootstrap-theme .civicase__file-filters .input-group-addon .material-icons{position:relative;top:2px}#bootstrap-theme .civicase__case-header{background:#fff;position:relative}#bootstrap-theme .civicase__case-header__expand_button{background:0 0;border-left:1px solid #e8eef0;border-right:1px solid #e8eef0;bottom:0;color:#c2cfd8;font-size:30px;left:0;padding:0;position:absolute;top:0;width:56px}#bootstrap-theme .civicase__case-header__expand_button>.material-icons{vertical-align:middle}#bootstrap-theme .civicase__case-header__content{border-top:1px solid #d3dee2;padding:15px 15px 15px 80px}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--client{color:#464354;font-size:24px;font-weight:600}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--client .civicase__contact-icon{font-size:30px}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--client .material-icons{line-height:1}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--client .civicase__contact-additional__arrow{top:-18px}#bootstrap-theme .civicase__case-header__content .civicase__contact-name{margin-top:1px;max-width:300px}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--manager{display:inline-block;position:relative;top:4px}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--manager .material-icons{line-height:1}#bootstrap-theme .civicase__case-header__content .civicase__case-header__case-type+.civicase__pipe{margin-right:5px}#bootstrap-theme .civicase__case-header__content .civicase__tags-container{position:relative;top:-1px}#bootstrap-theme .civicase__case-header__webform-dropdown+.dropdown-menu>li a{max-width:500px!important}#bootstrap-theme .civicase__case-header__content__first-row{display:flex;min-height:42px}#bootstrap-theme .civicase__case-header__content__trash{font-size:30px;margin-right:5px;position:relative;top:2px;width:20px}#bootstrap-theme .civicase__case-header__case-info,#bootstrap-theme .civicase__case-header__dates{color:#9494a5}#bootstrap-theme .civicase__case-header__case-info{margin-top:5px}#bootstrap-theme .civicase__case-header__case-id,#bootstrap-theme .civicase__case-header__case-source,#bootstrap-theme .civicase__case-header__case-type{color:#464354}#bootstrap-theme .civicase__case-header__case-type a{display:inline}#bootstrap-theme .civicase__case-header__action-menu{position:absolute;right:20px;top:20px}#bootstrap-theme .civicase__case-header__action-menu .list-group-item-info{color:#9494a5;display:block;padding:7px 19px 7px 24px}#bootstrap-theme .civicase__case-header__action-menu .dropdown-menu>li a{max-width:200px;overflow:hidden;position:relative;text-overflow:ellipsis}#bootstrap-theme .civicase__case-header__action-menu .dropdown-menu>li>.dropdown-menu.sub-menu{left:auto;margin-top:-40px;position:absolute;right:100%}#bootstrap-theme .civicase__case-header__action-menu .dropdown-menu>li>.dropdown-menu.sub-menu a{max-width:500px}#bootstrap-theme .civicase__case-header__action-menu .dropdown-menu>li:hover>.dropdown-menu.sub-menu{display:block;opacity:1;visibility:visible}#bootstrap-theme .civicase__case-header__action-icon{font-size:20px;padding:2px 10px}#bootstrap-theme .civicase__case-header__action-icon .material-icons{position:relative;top:3px}#bootstrap-theme .civicase__case-tab--linked-cases .civicase__summary-tab__other-cases{margin-left:0;margin-right:0}#bootstrap-theme .civicase__panel-empty{margin-bottom:110px;margin-top:110px;padding:5px;text-align:center}#bootstrap-theme .civicase__panel-empty .fa.fa-big,#bootstrap-theme .civicase__panel-empty .material-icons{color:#9494a5;font-size:64px}#bootstrap-theme .civicase__panel-empty .empty-label{color:#9494a5;font-size:14px;font-weight:600;line-height:19px;padding:18px 0;text-align:center}#bootstrap-theme .civicase__case-list-table-container{border-left:1px solid #e8eef0;margin-left:300px;overflow-x:auto;overflow-y:visible}#bootstrap-theme .civicase__case-list-table{table-layout:inherit}#bootstrap-theme .civicase__case-list-table td:first-child,#bootstrap-theme .civicase__case-list-table th:first-child{width:300px}#bootstrap-theme .civicase__case-list-table th{line-height:18px;min-width:142px;padding:22px 15px!important}#bootstrap-theme .civicase__case-list-table .civicase__bulkactions-checkbox{top:2px}#bootstrap-theme .civicase__case-list-table .civicase__bulkactions-actions-dropdown{top:2px}#bootstrap-theme .civicase__case-list-table .civicase__bulkactions-actions-dropdown .civicase__bulkactions-actions-dropdown__text{width:60px}#bootstrap-theme .civicase__case-list-table .civicase__case-list-column--first{padding:16px 14px!important}#bootstrap-theme .civicase__case-list-table th:first-child{background:#f3f6f7;left:0;position:absolute;width:300px}#bootstrap-theme .civicase__case-list-table th:nth-child(2){max-width:320px;min-width:320px;position:relative}#bootstrap-theme .civicase__case-list-table tr{height:150px}#bootstrap-theme .civicase__case-list-table thead tr{height:63px}#bootstrap-theme .civicase__case-list-table td{height:150px;min-width:142px;padding:20px}#bootstrap-theme .civicase__case-list-table td:first-child{background:#fff;left:0;padding:0;position:absolute;width:300px;z-index:1}#bootstrap-theme .civicase__case-list-table td:nth-child(2){vertical-align:middle}#bootstrap-theme .civicase__case-list-table .case-activity-card-wrapper{max-width:320px;min-width:320px;position:relative}#bootstrap-theme .civicase__case-list-table__column--status_badge{max-width:200px;min-width:200px!important}#bootstrap-theme .civicase__case-list-table__column--status_badge .crm_notification-badge{display:block;max-width:fit-content;overflow:hidden;text-overflow:ellipsis}#bootstrap-theme .civicase__case-list-table__header.affix{display:block;left:0;margin-left:300px;overflow-x:hidden;overflow-y:visible;right:0;top:60px;z-index:10}#bootstrap-theme .civicase__case-list-table__header.affix tr{display:table;width:100%}#bootstrap-theme .civicase__case-list-table__header.affix th{border-bottom:1px solid #e8eef0;display:table-cell}#bootstrap-theme .civicase__case-list-table__header.affix th:nth-child(1){left:0;position:fixed}#bootstrap-theme .civicase__case-list{margin:0;overflow:hidden;position:relative}#bootstrap-theme .civicase__case-list .civicase__bulkactions-message .alert{border-bottom:0}#bootstrap-theme .civicase__case-list .civicase__pager--fixed{position:fixed}#bootstrap-theme .civicase__case-list .civicase__pager--viewing-case{width:300px}#bootstrap-theme .civicase__case-list .civicase__pager--viewing-case.civicase__pager--fixed{position:absolute}#bootstrap-theme .civicase__case-list .civicase__pager--case-focused{display:none}#bootstrap-theme .civicase__case-list--summary>.civicase__pager{bottom:0;height:60px;position:absolute}#bootstrap-theme .civicase__case-list--summary .civicase__case-list-table-container{overflow-x:hidden}#bootstrap-theme .civicase__case-list-panel{border-top:1px solid #d3dee2;box-shadow:none;margin-bottom:0;overflow:auto;padding:0;position:relative;transition:width .3s linear}#bootstrap-theme .civicase__case-list-panel--summary{border-top:0;bottom:60px;overflow-x:hidden;position:absolute;top:65px;width:300px}#bootstrap-theme .civicase__case-list-panel--summary thead{display:none}#bootstrap-theme .civicase__case-list-panel--summary .civicase__case-list-column--first{background:#fff!important}#bootstrap-theme .civicase__case-list-panel--summary .civicase__case-list-table td:first-child{width:100%}#bootstrap-theme .civicase__case-list-column--first--detached{background:#fff;border-bottom:1px solid #d3dee2;border-top:1px solid #d3dee2;height:65px;left:0;padding:16px 14px;position:absolute;width:300px}#bootstrap-theme .civicase__case-list-panel--focused{width:0}#bootstrap-theme .civicase__case-list-panel--focused .civicase__pager{display:none}#bootstrap-theme .civicase__case-details-panel{box-shadow:none;display:none;float:right;height:0;overflow:hidden;transition:width .3s;width:0}#bootstrap-theme .civicase__case-details-panel>.panel-body{background:0 0}#bootstrap-theme .civicase__case-details-panel .civicase__panel-empty{background:#fff;height:100%;margin:0;padding:15px 20px}#bootstrap-theme .civicase__case-details-panel--summary{display:block;height:100%;overflow-y:auto;width:calc(100% - 300px)}#bootstrap-theme .civicase__case-details-panel--focused{width:100%}#bootstrap-theme .civicase__case-list-sortable-header{cursor:pointer}#bootstrap-theme .civicase__case-list-sortable-header:hover{background-color:#e8eef0}#bootstrap-theme .civicase__case-list-sortable-header.active{background-color:#e8eef0!important}#bootstrap-theme .civicase__case-list__toggle-sort{color:#9494a5;cursor:pointer;font-size:20px;position:relative;top:7px}#bootstrap-theme .civicase__case-list__header-toggle-sort{float:right;position:relative;top:3px}#bootstrap-theme .civicase__case-sort-dropdown{box-shadow:none;display:inline-block;width:90px!important}#bootstrap-theme .civicase__case-overview .panel-body{background-color:#fafafb;padding:0!important}#bootstrap-theme .civicase__case-overview paging{padding-right:20px}#bootstrap-theme .civicase__case-overview-container a{color:inherit;text-decoration:none}#bootstrap-theme .civicase__case-overview-container .civicase__case-overview__flow,#bootstrap-theme .civicase__case-overview-container .simplebar-content{position:static}#bootstrap-theme .civicase__case-overview-container .simplebar-content{padding-right:0!important}#bootstrap-theme .civicase__case-overview__breakdown,#bootstrap-theme .civicase__case-overview__flow{display:flex;margin-left:200px}#bootstrap-theme .civicase__case-overview__breakdown-field,#bootstrap-theme .civicase__case-overview__flow-status{display:inline-flex;flex-basis:200px;flex-grow:1;flex-shrink:0}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child,#bootstrap-theme .civicase__case-overview__flow-status:first-child{align-items:flex-start;align-items:center;flex-direction:row;justify-content:flex-start;left:0;position:absolute;top:auto;width:200px;z-index:5}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child .civicase__case-overview__flow-status__icon,#bootstrap-theme .civicase__case-overview__flow-status:first-child .civicase__case-overview__flow-status__icon{color:#0071bd;cursor:pointer;margin-left:8px;position:relative;top:1px}#bootstrap-theme .civicase__case-overview__flow-status{align-items:flex-start;background-color:#fff;flex-direction:column;height:80px;justify-content:center;position:relative}#bootstrap-theme .civicase__case-overview__flow-status::after{background-color:#fff;border-bottom-right-radius:5px;box-shadow:1px 1px 0 0 #e8eef0;content:'';height:57px;position:absolute;right:-25px;top:12px;transform:rotateZ(-45deg);transform-origin:50% 50%;width:57px;z-index:1}#bootstrap-theme .civicase__case-overview__flow-status:first-child{font-size:16px;font-weight:600;line-height:22px;padding-left:24px;z-index:10}#bootstrap-theme .civicase__case-overview__flow-status:last-child{overflow:hidden}#bootstrap-theme .civicase__case-overview__flow-status:last-child::after{content:none}#bootstrap-theme .civicase__case-overview__flow-status-settings{position:relative}#bootstrap-theme .civicase__case-overview__flow-status-settings .btn{color:inherit;margin-right:10px;padding:3px 0;text-decoration:none!important}#bootstrap-theme .civicase__case-overview__flow-status-settings .civicase__case-overview__flow-status__icon--settings{color:inherit!important;margin:0!important;vertical-align:middle}#bootstrap-theme .civicase__case-overview__flow-status-settings .civicase__case-overview__flow-status__icon--settings.material-icons{color:#9494a5!important;font-size:18px}#bootstrap-theme .civicase__case-overview__flow-status__border{bottom:0;height:4px;position:absolute;transform:skewx(-45deg);width:calc(100% - 1px);z-index:2}#bootstrap-theme .civicase__case-overview__flow-status__count{color:#464354;font-size:24px;font-weight:600;line-height:33px;text-align:center;width:100%}#bootstrap-theme .civicase__case-overview__flow-status__empty-state{text-align:center;width:100%}#bootstrap-theme .civicase__case-overview__flow-status__description{color:#9494a5;margin:0 auto;overflow:hidden;text-align:center;text-overflow:ellipsis;white-space:nowrap;width:140px}#bootstrap-theme .civicase__case-overview__flow-status__description span{text-overflow:ellipsis}#bootstrap-theme .civicase__case-overview__breakdown:last-child .civicase__case-overview__breakdown-field{border:0}#bootstrap-theme .civicase__case-overview__breakdown-field{align-items:center;border-bottom:1px solid #e8eef0;justify-content:center;padding:16px 24px;position:relative}#bootstrap-theme .civicase__case-overview__breakdown-field:not(:first-child){color:#9494a5}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child::after,#bootstrap-theme .civicase__case-overview__breakdown-field:last-child::after{background-color:#fafafb;bottom:-1px;content:'';height:1px;position:absolute;width:24px}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child{background-color:#fafafb;font-weight:600}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child a{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:100%}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child::after{left:0}#bootstrap-theme .civicase__case-overview__breakdown-field:last-child::after{right:0}#bootstrap-theme .civicase__case-overview__breakdown-field--hoverable:hover,#bootstrap-theme .civicase__case-overview__flow-status--hoverable:hover{background-color:#f3f6f7}#bootstrap-theme .civicase__case-overview__breakdown-field--hoverable:hover::after,#bootstrap-theme .civicase__case-overview__flow-status--hoverable:hover::after{background-color:#f3f6f7}#bootstrap-theme .civicase__case-overview__popup{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);color:#464354;padding:0;z-index:1}#bootstrap-theme .civicase__case-overview__popup.dropdown-menu{margin-top:15px;padding:8px 0}#bootstrap-theme .civicase__case-overview__popup .popover-content{padding:0}#bootstrap-theme .civicase__case-overview__popup .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__case-overview__popup .arrow.left{left:20px}#bootstrap-theme .civicase__case-overview__popup .dropdown-menu{box-shadow:none;display:inherit;position:inherit}#bootstrap-theme .civicase__case-overview__popup .civicase__checkbox{display:inline-block;margin-right:5px}#bootstrap-theme .civicase__case-overview__popup .civicase__checkbox--checked{color:#0071bd!important;font-size:24px!important;top:0!important}#bootstrap-theme .civicase__case-overview__popup .civicase__checkbox-container{line-height:18px}#bootstrap-theme .civicase__case-overview__popup .civicase__checkbox-container>*{vertical-align:middle}#bootstrap-theme .civicase__case-tab--people .nav-tabs{border-top-left-radius:2px;border-top-right-radius:2px;box-shadow:0 6px 20px 0 rgba(49,40,40,.03)}#bootstrap-theme .civicase__case-tab--people .civicase__checkbox{margin-right:16px}#bootstrap-theme .civicase__case-tab--people .civicase__checkbox .civicase__people-tab__table-checkbox{cursor:pointer;height:100%;left:0;opacity:0;position:absolute;right:0;top:0;width:100%;z-index:11}#bootstrap-theme .civicase__case-tab--people .civicase__people-tab-link{line-height:18px;padding-bottom:15px;padding-top:12px}#bootstrap-theme .civicase__case-tab--people .civicase__people-tab__table-header .civicase__people-tab__table-column{line-height:18px;padding:16px 24px}#bootstrap-theme .civicase__case-tab--people .civicase__people-tab__table-body .civicase__people-tab__table-column{padding:16px 20px}#bootstrap-theme .civicase__people-tab{background:#fff;border-radius:2px;box-shadow:0 3px 8px 0 rgba(49,40,40,.15)}#bootstrap-theme .civicase__people-tab__sub-tab .civicase__add-btn{margin-right:-6px;margin-top:-4px}#bootstrap-theme .civicase__people-tab__add-role-dropdown [disabled],#bootstrap-theme .civicase__people-tab__add-role-dropdown [disabled]:hover{color:#9494a5;cursor:not-allowed}#bootstrap-theme .civicase__people-tab__search{background:#fff;padding:20px 30px}#bootstrap-theme .civicase__people-tab__search h3{margin:0}#bootstrap-theme .civicase__people-tab__search .btn .material-icons{margin-right:5px;position:relative;top:2px}#bootstrap-theme .civicase__people-tab__search .dropdown-menu{left:auto!important;right:0;top:100%!important}#bootstrap-theme .civicase__people-tab__search .civicase__bulkactions-actions-dropdown .dropdown-menu{right:auto}#bootstrap-theme .civicase__people-tab__selection{align-items:center;display:flex;padding:16px 0}#bootstrap-theme .civicase__people-tab__selection>input{margin:0 5px 0 9px}#bootstrap-theme .civicase__people-tab__selection label{line-height:18px;margin-bottom:0;position:relative;top:1px}#bootstrap-theme .civicase__people-tab__select-box .form-control{width:240px}#bootstrap-theme .civicase__people-tab__filter{align-items:center;border-bottom:1px solid #e8eef0;border-top:1px solid #e8eef0;display:flex;justify-content:space-between;padding:10px 24px}#bootstrap-theme .civicase__people-tab__filter--role .form-control{width:160px}#bootstrap-theme .civicase__people-tab__filter--relations{justify-content:unset}#bootstrap-theme .civicase__people-tab__filter-alpha-pager{margin-left:20px}#bootstrap-theme .civicase__people-tab__filter-alpha-pager .civicase__people-tab__filter-alpha-pager__link{color:#464354;margin:0 3px;padding:2px}#bootstrap-theme .civicase__people-tab__filter-alpha-pager .civicase__people-tab__filter-alpha-pager__link.active,#bootstrap-theme .civicase__people-tab__filter-alpha-pager .civicase__people-tab__filter-alpha-pager__link.all{color:#0071bd;text-decoration:none}#bootstrap-theme .civicase__people-tab__filter-alpha-pager .civicase__people-tab__filter-alpha-pager__link:first-child{margin-left:0;padding-left:0}#bootstrap-theme .civicase__people-tab__table-column--first{display:flex}#bootstrap-theme .civicase__people-tab__table-column--first em{font-weight:400}#bootstrap-theme .civicase__people-tab__table-column--first input{margin:0}#bootstrap-theme .civicase__people-tab__table-column--last{padding:20px 0!important}#bootstrap-theme .civicase__people-tab__table-column--last .dropdown-menu{left:auto!important;right:20px;top:100%!important}#bootstrap-theme .civicase__people-tab__table-column--last .btn{padding:0}#bootstrap-theme .civicase__people-tab__table-column--last .open{position:relative}#bootstrap-theme .civicase__people-tab__table-column--last .open .dropdown-toggle{background:0 0!important;box-shadow:none}#bootstrap-theme .civicase__people-tab__table-column--last .material-icons{font-size:18px;padding:0}#bootstrap-theme .civicase__people-tab__inactive-filter{margin-left:auto;margin-right:30px}#bootstrap-theme .civicase__people-tab__inactive-filter .civicase__checkbox{display:inline-block;margin-right:5px;top:5px}#bootstrap-theme .civicase__people-tab__table-assign-icon{cursor:pointer}#bootstrap-theme .civicase__people-tab__table-assign-icon:hover{color:#0071bd}#bootstrap-theme .civicase__people-tab-counter{border-top:1px solid #e8eef0;line-height:18px;padding:16px 24px}#bootstrap-theme .civicase__case-filter-panel{background-color:#f3f6f7;box-shadow:none;margin-bottom:0}#bootstrap-theme .civicase__case-filter-panel .panel-header{position:relative}#bootstrap-theme .civicase__case-filter-panel__title{font-size:18px;left:20px;line-height:24px;margin:0;max-width:calc(((100% - 950px)/ 2) - 20px);overflow:hidden;position:absolute;text-overflow:ellipsis;top:50%;transform:translateY(-50%);white-space:nowrap}#bootstrap-theme .civicase__case-filters-container{display:flex;justify-content:center;left:0;padding:13.5px 0;top:0}#bootstrap-theme .civicase__case-filter__input.form-control{margin:0 8px}#bootstrap-theme .civicase__case-filter__input.form-control:not(.select2-container){width:240px}#bootstrap-theme .civicase__case-filter-panel__button{margin:0 8px;width:158px}#bootstrap-theme .civicase__case-filter-panel__button:first-child{margin-left:0}#bootstrap-theme .civicase__case-filter-panel__button .fa{font-size:18px;margin-right:7px;position:relative;top:2px}#bootstrap-theme .civicase__case-filter-form-elements-container{margin:0 auto;width:926px}#bootstrap-theme .civicase__case-filter-form-elements{clear:both;margin-bottom:10px}#bootstrap-theme .civicase__case-filter-form-elements .select2-choices{padding-right:30px}#bootstrap-theme .civicase__case-filter-form-elements .form-control{max-width:385px}#bootstrap-theme .civicase__case-filter-form-elements.civicase__case-filter-form-elements--case-id .form-control{max-width:160px}#bootstrap-theme .civicase__case-filter-form-elements label,#bootstrap-theme .civicase__case-filter-form-elements-container .civicase__checkbox__container label{color:#9494a5;font-weight:400}#bootstrap-theme .civicase__case-filter-panel__description{align-items:center;display:flex;flex-direction:row}#bootstrap-theme .civicase__filter-search-description-list-container{flex:1 0 0;margin-bottom:0}#bootstrap-theme .civicase__case-filter-form-legend{border-color:#e8eef0;color:#464354;font-size:16px;font-weight:600;line-height:22px;margin-bottom:20px;padding:0 0 8px}#bootstrap-theme .civicase__case-filter-fieldset{margin:15px 0}#bootstrap-theme .civicase__case-summary-fields:not(:first-child){margin-top:16px}#bootstrap-theme .civicase__case-summary-fields__label{color:#9494a5}#bootstrap-theme .civicase__case-summary-fields__value{color:#464354;word-break:break-all}#bootstrap-theme .civicase__case-tab__container{padding:24px 30px}#bootstrap-theme .civicase__case-tab__actions{margin-bottom:16px}#bootstrap-theme .civicase__case-tab__empty{color:#464354;font-weight:600;margin-top:40px;opacity:.65}#bootstrap-theme .civicase__checkbox{background-color:#fff;border:1px solid #c2cfd8;border-radius:2px;box-shadow:0 6px 20px 0 rgba(49,40,40,.03);box-sizing:border-box;cursor:pointer;display:inline-block;height:18px;margin-right:5px;position:relative;width:18px}#bootstrap-theme .civicase__checkbox__container .control-label{position:relative;top:-4px}#bootstrap-theme .civicase__checkbox--checked{color:#c2cfd8;font-size:24px;left:0;margin-left:-4px;margin-top:-4px;position:absolute;top:0;transition:.2s all cubic-bezier(0,0,0,.4);z-index:10}#bootstrap-theme .civicase__animated-checkbox-card{position:relative;transition:.1s padding-left cubic-bezier(0,0,0,.4);transition-delay:.1s}#bootstrap-theme .civicase__animated-checkbox-card .civicase__checkbox--bulk-action{cursor:pointer;left:14px;opacity:0;outline:0;position:absolute;top:15px;transform:scale(.3);transition:.1s all cubic-bezier(0,0,0,.4);transition-delay:unset}#bootstrap-theme .civicase__animated-checkbox-card--expanded{padding-left:30px;transition-delay:0}#bootstrap-theme .civicase__animated-checkbox-card--expanded .civicase__checkbox--bulk-action{opacity:1;transform:scale(1);transition-delay:.1s}.civicase__contact-activity-tab__add .select2-container .select2-choice{background:#4d4d69;border:0;box-shadow:none;height:auto;line-height:initial;padding:7px 19px;width:155px!important}.civicase__contact-activity-tab__add .select2-container .select2-chosen{color:#fff!important;margin:0;text-transform:uppercase}.civicase__contact-activity-tab__add .select2-container .select2-arrow{background:0 0!important;border:0;line-height:34px}.civicase__contact-activity-tab__add .select2-container .select2-arrow::before{color:#fff!important}#bootstrap-theme .civicase__contact-card{color:#0071bd;display:flex}#bootstrap-theme .civicase__contact-name-container{display:flex}#bootstrap-theme .civicase__contact-name,#bootstrap-theme .civicase__contact-name-additional{color:inherit;margin-left:5px;max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__contact-icon,#bootstrap-theme .civicase__contact-icon-additional{color:#c2cfd8;font-size:18px;margin-top:2px}#bootstrap-theme .civicase__contact-icon-additional.civicase__contact-icon--highlighted,#bootstrap-theme .civicase__contact-icon-additional:hover,#bootstrap-theme .civicase__contact-icon.civicase__contact-icon--highlighted,#bootstrap-theme .civicase__contact-icon:hover{color:#0071bd;cursor:pointer}#bootstrap-theme .civicase__contact-icon .material-icons,#bootstrap-theme .civicase__contact-icon-additional .material-icons{line-height:inherit}#bootstrap-theme .civicase__contact-additional__container{margin-left:8px}#bootstrap-theme .civicase__contact-additional__container,#bootstrap-theme .civicase__tooltip-popup-list--additional-contacts{color:#0071bd;padding-left:0!important;padding-right:0!important}#bootstrap-theme .civicase__contact-additional__container .civicase__contact-icon,#bootstrap-theme .civicase__tooltip-popup-list--additional-contacts .civicase__contact-icon{font-size:18px}#bootstrap-theme .civicase__contact-additional__container .civicase__contact-name-additional,#bootstrap-theme .civicase__tooltip-popup-list--additional-contacts .civicase__contact-name-additional{color:#0071bd}#bootstrap-theme .civicase__contact-additional__popover{border:1px solid #e8eef0;border-radius:0;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);cursor:pointer}#bootstrap-theme .civicase__contact-additional__popover a{display:flex!important;line-height:22px}#bootstrap-theme .civicase__contact-additional__popover .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__contact-additional__popover .popover-content{padding:0}#bootstrap-theme .civicase__contact-additional__list{margin:0;padding:0}#bootstrap-theme .civicase__contact-additional__list li{height:34px;list-style:none;padding:7px 20px}#bootstrap-theme .civicase__contact-additional__list li:hover{background:#f3f6f7}#bootstrap-theme .civicase__contact-additional__list li .civicase__contact-icon{vertical-align:middle}#bootstrap-theme .civicase__contact-additional__list li a{text-decoration:none}#bootstrap-theme .civicase__contact-additional__hidden_contacts_info{color:#9494a5;font-size:12px}#bootstrap-theme .civicase__contact-avatar{background:#99c6e5;min-width:30px;padding:5px;position:relative}#bootstrap-theme .civicase__contact-additional__container--avatar{background:#99c6e5;margin-left:0;margin-top:-10px;min-width:30px;padding:5px}#bootstrap-theme .civicase__contact-avatar--image{background:0 0;padding:0;position:relative;z-index:1}#bootstrap-theme .civicase__contact-avatar--image img{border-radius:2px;height:25px;width:25px}#bootstrap-theme .civicase__contact-avatar__full-name{background:#99c6e5;border-radius:1px;display:none;height:30px;left:0;padding:5px 10px;position:absolute;top:0;width:auto}#bootstrap-theme .civicase__contact-avatar--image .civicase__contact-avatar__full-name{border-bottom-left-radius:0;border-top-left-radius:0}#bootstrap-theme .civicase__contact-avatar--has-full-name:hover{opacity:1}#bootstrap-theme .civicase__contact-avatar--has-full-name:hover .civicase__contact-avatar__full-name{display:block}#bootstrap-theme .civicase__contact-avatar--has-full-name:hover.civicase__contact-avatar--image .civicase__contact-avatar__full-name{left:29px;padding-left:11px;z-index:0}#bootstrap-theme .civicase__contact-card__with-more-fields{flex-wrap:wrap}#bootstrap-theme .civicase__contact-card__with-more-fields .civicase__contact__more-field{color:#464354}#bootstrap-theme .civicase__contact-card__with-more-fields .civicase__contact__break{flex-basis:100%;height:0}#bootstrap-theme .civicase__contact-card__with-more-fields .civicase__contact-name{max-width:initial}#bootstrap-theme .civicase__contact-additional__list__with-more-fields{max-height:300px;overflow-y:auto}#bootstrap-theme .civicase__contact-additional__list__with-more-fields li{height:auto}#bootstrap-theme .civicase__contact-additional__list__with-more-fields li:not(:first-of-type){border-top:1px solid #e8eef0}#bootstrap-theme .civicase__contact-additional__list__with-more-fields .civicase__contact-name-additional{max-width:initial}#bootstrap-theme .civicase__contact-additional__list__with-more-fields .civicase__contact__more-field{margin-top:5px}#bootstrap-theme .civicase__contact-cases-tab{margin-left:-5px}#bootstrap-theme .civicase__contact-cases-tab-container{padding-left:15px;padding-right:15px}#bootstrap-theme .civicase__contact-cases-tab-container .civicase__panel-transparent-header>.panel-heading .panel-title{font-size:18px;margin:0;padding:15px 0}#bootstrap-theme .civicase__contact-cases-tab-container .civicase__panel-transparent-header>.panel-body{background:0 0;box-shadow:none;padding:0}#bootstrap-theme .civicase__contact-case-tab__case-list__footer{margin-top:24px}#bootstrap-theme .civicase__contact-case-tab__case-list__footer .btn{box-shadow:0 3px 8px 0 rgba(49,40,40,.15)}#bootstrap-theme .civicase__contact-cases-tab-empty{align-items:center;display:flex;flex-direction:column;padding:50px 0}#bootstrap-theme .civicase__contact-cases-tab-empty a{color:#4d4d69}#bootstrap-theme .civicase__contact-cases-tab-add{background:#4d4d69;margin-bottom:10px}#bootstrap-theme .civicase__contact-cases-tab-add .material-icons{margin-right:5px;position:relative;top:2px}#bootstrap-theme .civicase__contact-cases-tab-details{box-shadow:0 3px 8px 0 rgba(49,40,40,.15);margin-top:calc((1.1 * 18px) + 2 * 15px)}#bootstrap-theme .civicase__contact-cases-tab-details>.panel-body{padding:0}#bootstrap-theme .civicase__contact-cases-tab-details .btn-group{margin-right:5px}#bootstrap-theme .civicase__contact-cases-tab-details .btn-group:last-child{margin-right:0}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__tags-container{max-width:80%}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__activity-card{width:100%}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__summary-tab__subject{margin-bottom:3px;margin-left:-2px;margin-top:20px}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__summary-tab__description{margin-bottom:5px}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-card--client{position:relative;top:8px}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-card--client .material-icons,#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-card--manager .material-icons{line-height:1}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-card--manager{position:relative;top:2px}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-cases-tab__status-label{display:inline-block;max-width:250px;overflow:hidden;position:relative;text-overflow:ellipsis;top:3px;white-space:nowrap}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-cases-tab__case-link .fa{font-size:17px;margin-right:5px;position:relative;top:1px}#bootstrap-theme .civicase__contact-cases-tab-details .list-group-item-info{color:#9494a5;display:block;padding:7px 19px 7px 24px}#bootstrap-theme .civicase__contact-cases-tab-details__title{margin:6.5px 0}#bootstrap-theme .civicase__contact-cases-tab__panel-row{border-bottom:1px solid #e8eef0;padding:15px 24px}#bootstrap-theme .civicase__contact-cases-tab__panel-row:last-child{border-bottom:0}#bootstrap-theme .civicase__contact-cases-tab__panel-row .civicase__summary-tab__subject textarea{min-height:65px}#bootstrap-theme .civicase__contact-cases-tab__panel-row .civicase__summary-tab__description textarea{min-height:85px}#bootstrap-theme .civicase__contact-cases-tab__panel-actions{padding:20px}#bootstrap-theme .civicase__contact-cases-tab__panel-row--dark{background-color:#f3f6f7}#bootstrap-theme .civicase__contact-cases-tab__panel-row--dark .civicase__pipe{color:#e8eef0;margin:0 8px}#bootstrap-theme .civicase__contact-cases-tab__panel-fields{padding-bottom:15px}#bootstrap-theme .civicase__contact-cases-tab__panel-fields--inline{align-items:center;display:flex}#bootstrap-theme .civicase__contact-cases-tab__panel-field-emphasis{color:#9494a5}#bootstrap-theme .civicase__contact-cases-tab__panel-field-title{color:#9494a5;margin-bottom:5px}.crm-contact-page #ui-id-5{padding:30px;width:calc(100% - 200px)}@media (max-width:1400px){#bootstrap-theme .civicase__contact-card--client{clear:both}}.contact-popover-container{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);color:#464354;max-width:90vw;padding:20px;width:708px}.contact-popover-container .popover-content{padding:0}.contact-popover-container.bottom>.arrow{border-bottom-color:#e8eef0}.contact-popover-container.top>.arrow{border-top-color:#e8eef0}.civicase__contact-popover__header h2{line-height:24px;margin:0}.civicase__contact-popover__header hr{background-color:#e8eef0;margin:16px 0}.civicase__contact-popover__column{float:left;width:46%}.civicase__contact-popover__column+.civicase__contact-popover__column{width:54%}.civicase__contact-popover__detail-group{float:left;margin-bottom:10px;width:100%}.civicase__contact-popover__detail-header,.civicase__contact-popover__detail-value{color:#4d4d69;float:left;line-height:18px;overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap;width:55%}.civicase__contact-popover__detail-header strong,.civicase__contact-popover__detail-value strong{color:#464354;font-weight:600}.civicase__contact-popover__detail-header{clear:both;width:45%}#bootstrap-theme .civicrm__contact-prompt-dialog textarea{width:100%}#bootstrap-theme .civicrm__contact-prompt-dialog__date-error.crm-error{background:#fbe3e4}#bootstrap-theme .civicase__crm-dashboard__tabs{position:relative;width:100%;z-index:1}#bootstrap-theme .civicase__crm-dashboard__tabs.affix{position:fixed;z-index:11}#bootstrap-theme .civicase__crm-dashboard__myactivities-tab{padding:0}#bootstrap-theme .civicase__dashboard__tab{background:#e8eef0}#bootstrap-theme .civicase__dashboard__tab>.civicase__dashboard__tab__top{margin-bottom:30px}#bootstrap-theme .civicase__dashboard__tab .panel-secondary{margin-bottom:30px}#bootstrap-theme .civicase__dashboard__tab__col{padding:0 15px}#bootstrap-theme .civicase__dashboard__tab__col-wrapper{display:flex;flex-direction:column;margin:0 -15px}#bootstrap-theme .civicase__dashboard__tab__col-wrapper>.civicase__dashboard__tab__col{margin-bottom:30px}#bootstrap-theme .civicase__dashboard__tab__main{margin:auto;max-width:1081px;padding:0 30px}@media (min-width:992px){#bootstrap-theme .civicase__dashboard__tab__col--left{flex-basis:330px;flex-grow:0;flex-shrink:0}#bootstrap-theme .civicase__dashboard__tab__col--right{flex-grow:1}#bootstrap-theme .civicase__dashboard__tab__col-wrapper{flex-direction:row}#bootstrap-theme .civicase__dashboard__tab__col-wrapper>.civicase__dashboard__tab__col{margin-bottom:0}}@media (min-width:1200px){#bootstrap-theme .civicase__dashboard__tab__main{padding:0}}#bootstrap-theme .civicase__dashboard .tab-pane{padding:0}#bootstrap-theme .civicase__dashboard__tab-container .nav{width:100%;z-index:10}#bootstrap-theme .civicase__dashboard__tab-container .nav.affix-top{position:relative}#bootstrap-theme .civicase__dashboard__tab-container .nav.affix{position:fixed}#bootstrap-theme .civicase__dashboard-activites-feed{background:#e8eef0}#bootstrap-theme .civicase__dashboard__action-btn{background:#0071bd;border:1px solid #fff;border-radius:2px;color:#fff;line-height:20px;margin-right:15px;margin-top:7px;padding:6px 16px}#bootstrap-theme .civicase__dashboard__action-btn .material-icons{font-size:16px;margin-right:5px;position:relative;top:2px}#bootstrap-theme .civicase__dashboard__action-btn--light{background:#fff;color:#0071bd}#bootstrap-theme .civicase__dashboard__relation-filter{display:inline-block;margin-right:10px;margin-top:7px;width:190px}#bootstrap-theme .civicase__dashboard__relation-filter .select2-choice{color:#9494a5}#bootstrap-theme .civicase__ui-range>span{display:inline-block;margin-right:16px}#bootstrap-theme .civicase__ui-range .crm-form-date{display:inline-block;width:134px}#bootstrap-theme .civicase__ui-range .crm-form-date-wrapper{display:inline-block;position:relative}#bootstrap-theme .civicase__ui-range .crm-clear-link{position:absolute;right:-20px;top:50%;transform:translateY(-50%)}#bootstrap-theme .civicase__activity-panel__core_container--draft [title='File On Case']{display:none}.crm-container.ui-dialog .ui-dialog-content.civicase__email-role-selector{height:220px!important}#bootstrap-theme .civicase__file-upload-container{margin:0 -12px}#bootstrap-theme .civicase__file-upload-item{padding:0 12px}#bootstrap-theme .civicase__file-upload-dropzone{align-items:center;border:1px dashed #c2cfd8;border-radius:3px;display:flex;flex-direction:column;height:330px;justify-content:center;padding:20px;width:100%}#bootstrap-theme .civicase__file-upload-dropzone:hover{background-color:#fff}#bootstrap-theme .civicase__file-upload-dropzone .material-icons{color:#bfcfd9;font-size:48px}#bootstrap-theme .civicase__file-upload-dropzone h3{font-size:14px;line-height:18px;margin:10px 0 0}#bootstrap-theme .civicase__file-upload-dropzone label{color:#0071bd;cursor:pointer;font-weight:400}#bootstrap-theme .civicase__file-upload-button{display:none!important}#bootstrap-theme .civicase__file-upload-box{transition:.25s width cubic-bezier(0,0,0,.4);width:100%}#bootstrap-theme .civicase__file-upload-details{opacity:0;overflow:hidden;padding:0;transition:.1s opacity cubic-bezier(0 0,0,.4);transition-delay:.3s;width:0}#bootstrap-theme .civicase__file-upload-details label{color:#9494a5;font-weight:400;line-height:18px;margin-bottom:5px}#bootstrap-theme .civicase__file-upload-details .btn{margin-right:8px;padding:8px 16px}#bootstrap-theme .civicase__file-upload-details .btn:last-child{margin-right:0}#bootstrap-theme .civicase__file-upload-details .btn-default{border-color:inherit}#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector{margin-bottom:25px;margin-top:-10px}#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .col-sm-5,#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .col-xs-7{float:none}#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .col-sm-5,#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .col-xs-7,#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .select2-container{margin-bottom:5px;padding:0;width:100%!important}#bootstrap-theme .civicase__file-upload-container--upload-active .civicase__file-upload-box{width:50%}#bootstrap-theme .civicase__file-upload-container--upload-active .civicase__file-upload-details{opacity:1;overflow:visible;padding:0 12px;width:50%}#bootstrap-theme .civicase__file-upload-name,#bootstrap-theme .civicase__file-upload-remove,#bootstrap-theme .civicase__file-upload-size{font-size:13px;line-height:18px;margin:0}#bootstrap-theme .civicase__file-upload-name{color:#0071bd}#bootstrap-theme .civicase__file-upload-description{margin-bottom:24px}#bootstrap-theme .civicase__file-upload-description textarea{min-height:90px}#bootstrap-theme .civicase__file-upload-remove{text-align:right}#bootstrap-theme .civicase__file-upload-remove .btn{border:0;color:#cf3458;padding:0;text-transform:capitalize}#bootstrap-theme .civicase__file-upload-remove .btn:hover{background-color:transparent}#bootstrap-theme .civicase__file-upload-progress{margin:12px 0 16px}#bootstrap-theme .civicase__icon{transform:translateY(14%) rotate(.03deg)}#bootstrap-theme .civicase-inline-datepicker__wrapper{margin-left:-11px;margin-top:-5px}#bootstrap-theme .civicase-inline-datepicker__wrapper [civicase-inline-datepicker]{border:1px solid transparent;border-radius:2px;height:30px;padding:4px 10px;width:140px!important}#bootstrap-theme .civicase-inline-datepicker__wrapper .form-control{box-shadow:none;display:inline-block}#bootstrap-theme .civicase-inline-datepicker__wrapper .form-control.ng-invalid{background-color:#fbe3e4}#bootstrap-theme .civicase-inline-datepicker__wrapper .addon{margin-top:-3px!important;opacity:0;transition:opacity .15s}#bootstrap-theme .civicase-inline-datepicker__wrapper:active [civicase-inline-datepicker],#bootstrap-theme .civicase-inline-datepicker__wrapper:focus [civicase-inline-datepicker],#bootstrap-theme .civicase-inline-datepicker__wrapper:hover [civicase-inline-datepicker]{border:1px solid #c2cfd8}#bootstrap-theme .civicase-inline-datepicker__wrapper:active .form-control,#bootstrap-theme .civicase-inline-datepicker__wrapper:focus .form-control,#bootstrap-theme .civicase-inline-datepicker__wrapper:hover .form-control{box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}#bootstrap-theme .civicase-inline-datepicker__wrapper:active .addon,#bootstrap-theme .civicase-inline-datepicker__wrapper:focus .addon,#bootstrap-theme .civicase-inline-datepicker__wrapper:hover .addon{opacity:1}#bootstrap-theme .civicase-inline-datepicker__wrapper .civicase__inline-datepicker--open{border:1px solid #c2cfd8;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}#bootstrap-theme .civicase-inline-datepicker__wrapper [civicase-inline-datepicker]:active+.addon,#bootstrap-theme .civicase-inline-datepicker__wrapper [civicase-inline-datepicker]:focus+.addon{opacity:1}#bootstrap-theme .civicase__loading-placeholder__icon{color:#edf3f5;display:inline-block;font-size:10px;left:-14px;position:relative;top:-1px;width:10px}#bootstrap-theme .civicase__loading-placeholder__activity-card{border:1px solid #edf3f5;border-radius:4px;height:90px;margin-bottom:10px;position:relative;width:280px}#bootstrap-theme .civicase__loading-placeholder__activity-card::before{background-color:#edf3f5;border:.4em solid #fff;border-radius:1.5em;content:' ';font-size:1.5em;height:2.7em;left:1em;padding-top:.2em;position:absolute;text-align:center;top:.4em;width:2.7em}#bootstrap-theme .civicase__loading-placeholder__activity-card::after{background-color:#edf3f5;content:' ';height:30px;position:absolute;right:20px;top:20px;width:12px}#bootstrap-theme .civicase__loading-placeholder__activity-card div{margin-left:90px;margin-right:90px}#bootstrap-theme .civicase__loading-placeholder__activity-card div::before{background-color:#edf3f5;content:' ';display:block;height:10px;margin-top:23px}#bootstrap-theme .civicase__loading-placeholder__activity-card div::after{background-color:#edf3f5;content:' ';display:block;height:10px;margin-top:23px}#bootstrap-theme .civicase__loading-placeholder__oneline::before{background-color:#edf3f5;content:' ';display:block;height:1em}#bootstrap-theme .civicase__loading-placeholder__date{background-color:#edf3f5}#bootstrap-theme .civicase__loading-placeholder__date::before{border-left-color:#d6e4e8;border-top-color:#d6e4e8}#bootstrap-theme .civicase__loading-placeholder--big::before{height:1.5em}#bootstrap-theme .panel-header .civicase__loading-placeholder__oneline::before{background-color:#edf3f5}#bootstrap-theme .civicase__loading-placeholder__oneline-strip{border-left-color:#edf3f5!important}#bootstrap-theme civicase-masonry-grid{width:100%}#bootstrap-theme civicase-masonry-grid .civicase__masonry-grid__column{float:left;width:50%}#bootstrap-theme civicase-masonry-grid-item{display:block}#bootstrap-theme .civicase__pager{background:#fafafb;border-radius:0 0 3px 3px;border-top:1px solid #e8eef0;padding:18px 15px;position:relative;z-index:10}#bootstrap-theme .civicase__pager .disabled{display:none}#bootstrap-theme .civicase__pager [title='First Page'] a,#bootstrap-theme .civicase__pager [title='Last Page'] a,#bootstrap-theme .civicase__pager [title='Next Page'] a,#bootstrap-theme .civicase__pager [title='Previous Page'] a{font-size:16px;font-weight:400;top:-3px}#bootstrap-theme .civicase__pager--fixed{bottom:0;left:0;position:fixed;width:100%}#bootstrap-theme .panel-query>.panel-body{transition:opacity .2s linear}#bootstrap-theme .panel-query.is-loading-page>.panel-body{opacity:.7}#bootstrap-theme .panel-query .civicase__activity-card--empty{text-align:center}#bootstrap-theme .panel-query .civicase__activity-card--big--empty-description{margin-bottom:0}#bootstrap-theme .panel-query .civicase__activity-no-result-icon{display:inline-block}#bootstrap-theme .panel-secondary{box-shadow:0 3px 8px 0 rgba(49,40,40,.15)}#bootstrap-theme .panel-secondary>.panel-body{padding:24px}#bootstrap-theme .panel-secondary>.panel-footer{padding:16px 24px}#bootstrap-theme .panel-secondary>.panel-heading{background:#fff;line-height:1;padding:24px;position:relative}#bootstrap-theme .panel-secondary>.panel-heading::after{border-bottom:1px solid #e8eef0;bottom:0;content:'';display:block;height:0;left:16px;position:absolute;width:calc(100% - 32px)}#bootstrap-theme .panel-secondary>.panel-heading .panel-title{font-size:16px}#bootstrap-theme .panel-secondary .panel-heading-control{display:block;margin-left:0;margin-top:-13px;position:relative;top:6px}#bootstrap-theme+.panel-secondary .panel-heading-control{margin-left:10px}#bootstrap-theme .panel-secondary a.panel-heading-control{line-height:30px}#bootstrap-theme .panel-secondary .panel-title+.panel-heading-control{margin-left:10px}#bootstrap-theme .panel-secondary .civicase__activity-card--long,#bootstrap-theme .panel-secondary .civicase__case-card--detached{box-shadow:0 3px 12px 0 rgba(49,40,40,.14);margin-bottom:0}#bootstrap-theme .panel-secondary .civicase__activity-card--long:not(:last-child),#bootstrap-theme .panel-secondary .civicase__case-card--detached:not(:last-child){margin-bottom:15px}#bootstrap-theme .civicase__panel-transparent-header{box-shadow:none}#bootstrap-theme .civicase__panel-transparent-header>.panel-heading{background:0 0;padding:0}#bootstrap-theme .civicase__panel-transparent-header>.panel-heading .panel-title{font-size:16px;margin:3px 0 12px}#bootstrap-theme .civicase__panel-transparent-header>.panel-heading h3::before{box-shadow:none}#bootstrap-theme .civicase__panel-transparent-header>.panel-heading .civicase__pipe{color:#c2cfd8;font-weight:400;margin:0 5px}#bootstrap-theme .civicase__panel-transparent-header>.panel-body{background:#fff;border-radius:2px;box-shadow:0 3px 8px 0 rgba(49,40,40,.15);padding:24px;position:relative}#bootstrap-theme civicase-popover{display:inline-block}#bootstrap-theme .civicase__popover-box{display:block}#bootstrap-theme civicase-popover-toggle-button{cursor:pointer}.civicase__tooltip-popup-list{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);color:#464354;padding:8px 12px}.civicase__tooltip-popup-list .popover-content{padding:0}.civicase__tooltip-popup-list .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__responsive-calendar tbody,#bootstrap-theme .civicase__responsive-calendar thead{display:block}#bootstrap-theme .civicase__responsive-calendar tr{display:flex}#bootstrap-theme .civicase__responsive-calendar td,#bootstrap-theme .civicase__responsive-calendar th{padding:0;width:100%}#bootstrap-theme .civicase__show-more-button{cursor:pointer}#bootstrap-theme .civicase__summary-tab__basic-details{background:#fff}#bootstrap-theme .civicase__summary-tab__basic-details .panel-body{display:flex;flex-direction:row;padding:20px}#bootstrap-theme .civicase__summary-tab__subject-container{flex-basis:56%;padding-right:15px}#bootstrap-theme .civicase__summary-tab__subject{font-size:20px;font-weight:600;line-height:27px;margin:0 0 13px}#bootstrap-theme .civicase__summary-tab__description{color:#9494a5;line-height:18px}#bootstrap-theme .civicase__summary-tab__last-updated{margin:13px 0 0;padding:2px 4px}#bootstrap-theme .civicase__summary-tab__last-updated__label{color:#9494a5;line-height:18px}#bootstrap-theme .civicase__summary-activity-count{align-items:center;border-left:1px solid #e8eef0;color:#464354;display:table-cell;font-weight:600;justify-content:center;text-align:center;vertical-align:top;width:20%}#bootstrap-theme .civicase__summary-activity-count a,#bootstrap-theme .civicase__summary-activity-count a:hover{color:#464354;text-decoration:none}#bootstrap-theme .civicase__summary-activity-count a{display:block;height:100%;margin:0 10px}#bootstrap-theme .civicase__summary-activity-count a:hover{background:#f3f6f7;border-radius:5px}#bootstrap-theme .civicase__summary-activity-count .civicase__summary-activity-count__number{font-size:50px;line-height:76px}#bootstrap-theme .civicase__summary-activity-count .civicase__summary-activity-count__description{color:#9494a5;font-size:15px;line-height:22px}#bootstrap-theme .civicase__summary-overdue-count{line-height:18px}#bootstrap-theme .civicase__summary-tab-tile{margin-bottom:15px;padding:15px}#bootstrap-theme .civicase__summary-tab-tile>.panel{margin-bottom:0}#bootstrap-theme .civicase__summary-tab-tile>.panel-body{border-radius:5px;border-top:0!important;padding:0}#bootstrap-theme .civicase__summary-tab-tile .civicase__panel-transparent-header>.panel-body{border-radius:5px}#bootstrap-theme .civicase__summary-tab-tile-container{display:flex;padding:0 15px;width:100%}#bootstrap-theme .civicase__summary-tab-tile--fixed{width:350px}#bootstrap-theme .civicase__summary-tab-tile--responsive{flex-basis:calc(50% - 150px);flex-grow:1;min-width:0}#bootstrap-theme .civicase__summary-tab__activity-list>.panel-heading .civicase__case-card__activity-count:nth-child(2){margin-left:10px}#bootstrap-theme .civicase__summary-tab__activity-list>.panel-body{background:0 0;box-shadow:none}#bootstrap-theme .civicase__summary-tab__activity-list .civicase__activity-card{margin-bottom:15px}#bootstrap-theme .civicase__summary-tab__other-cases{margin-left:25px;margin-right:25px}#bootstrap-theme .civicase__summary-tab__other-cases>.panel-collapse>.panel-body{padding:0}#bootstrap-theme .civicase__summary-tab__other-cases .panel-footer{border-top:0;padding:0}#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager{border-top:0}#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager .pagination>li>a{color:#9494a5;font-weight:400}#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager .pagination>li>a::after,#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager .pagination>li>a::before{display:none}#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager .pagination>li[title^=Page].active>a{color:#464354}#crm-container .civicase__tabs{background-color:#fff;padding:0 0 0 20px}#crm-container .civicase__tabs .ui-tab{background:0 0;border:0;padding:0}#crm-container .civicase__tabs .ui-tab .ui-tabs-anchor{height:auto;padding:15px 20px!important}#crm-container .civicase__tabs__panel{padding:15px 20px}#bootstrap-theme .civicase__tags-container .badge,#bootstrap-theme .civicase__tags-container__additional__list .badge{margin-right:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__tags-container{display:inline-block}#bootstrap-theme .civicase__tags-container .badge{max-width:36ch}#bootstrap-theme .civicase__tags-container__additional__list{margin:0;padding:0}#bootstrap-theme .civicase__tags-container__additional__list li{list-style:none;padding:5px 10px}#bootstrap-theme .civicase__tags-container__additional__list li:hover{background:#f3f6f7}#bootstrap-theme .civicase__tags-container__additional__list li a{text-decoration:none}#bootstrap-theme .civicase__tags-container__additional__list .badge{max-width:36ch}#bootstrap-theme .civicase__tags-container__additional-tags{background-color:#9494a5;display:inline;padding:0 8px}#bootstrap-theme .civicase__tags-container__additional__popover{border:1px solid #e8eef0;border-radius:0;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);cursor:pointer;max-width:calc(36ch + 20px + 4px);padding:0}#bootstrap-theme .civicase__tags-container__additional__popover a{display:flex!important;line-height:22px}#bootstrap-theme .civicase__tags-container__additional__popover .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__tags-container__additional__popover .popover-content{background:#fff;padding:0}.civicase__tags-modal{background-color:#fff!important}#bootstrap-theme .civicase__tags-modal__generic-tags-container{border:1px solid #d3dee2;border-radius:2px;padding:5px}#bootstrap-theme .civicase__tags-modal__generic-tags-container input{margin-top:0!important}#bootstrap-theme .civicase__tags-modal__generic-tags-container label{margin-bottom:0}#bootstrap-theme .civicase__tags-modal__tags-container label{margin-top:4px}#bootstrap-theme .civicase__tags-modal__tags-container .col-sm-9{width:75%!important}.civicase__tags-selector__item-color{margin-right:2px;position:relative;top:1px}#bootstrap-theme .civicase__tooltip__popup{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);padding:0;z-index:5}#bootstrap-theme .civicase__tooltip__popup .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__tooltip__popup .arrow.left{left:20px}#bootstrap-theme .civicase__tooltip__ellipsis{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__pipe{position:relative;top:-1px}#bootstrap-theme .civicase__text-warning{color:#e6ab5e}#bootstrap-theme .civicase__text-success{color:#44cb7e}#bootstrap-theme .civicase__link-disabled{cursor:no-drop;pointer-events:none}#bootstrap-theme .civicase__spinner{animation:spin 1.5s linear infinite;background:url(../resources/icons/spinner.svg) no-repeat center center!important;display:block;height:32px;margin:auto;width:32px}#bootstrap-theme .civicase__tooltip-popup-list{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);color:#464354;padding:8px 12px}#bootstrap-theme .civicase__tooltip-popup-list .popover-content{padding:0}#bootstrap-theme .civicase__tooltip-popup-list .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase-workflow-list>.panel-body{padding:0}#bootstrap-theme .civicase-workflow-list__new-button{margin-bottom:20px}#bootstrap-theme .civicase-workflow-list__new-button .material-icons{position:relative;top:2px}#bootstrap-theme .civicase-workflow-list_duplicate-form .ng-invalid:not(.ng-untouched){border-color:#cf3458}#bootstrap-theme .civicase-workflow-list_duplicate-form textarea{width:100%}#bootstrap-theme .civicase-workflow-list__filters .form-group{margin-bottom:0}#bootstrap-theme .civicase-workflow-list__filters .form-group [type=checkbox]{margin:0}#bootstrap-theme .civicase-workflow-list__filters .form-group>div{display:inline-block;margin-bottom:20px}#bootstrap-theme .civicase-workflow-list__filters .form-group>div:nth-child(odd){margin-right:10px;width:calc(50% - 10px)}#bootstrap-theme .civicase-workflow-list__filters .form-group>div:nth-child(even){margin-left:10px;width:calc(50% - 10px)}#bootstrap-theme .civicase-workflow-list__filters .form-group .select2-container{width:240px!important}#civicaseActivitiesTab{margin-left:-10px;margin-right:-10px}#civicaseActivitiesTab .civicase__activity-feed>.panel-body{padding-left:0;padding-right:0}#civicaseActivitiesTab .civicase__activity-filter{padding:16px 0}#civicaseActivitiesTab .civicase__activity-filter.affix{width:calc(100% - 240px)}.page-civicrm-contact-view:not([class*=page-civicrm-contact-view-]) #crm-container .civicase__activity-panel__core_container .crm-submit-buttons{margin-bottom:0!important}.page-civicrm-case-a #page{margin:0;padding-top:0}.page-civicrm-case-a .block-civicrm>h2{margin:0}.page-civicrm-case-a #branding{padding:16px 0!important}.page-civicrm-case-a #branding .breadcrumb{left:0;line-height:18px;padding:0 20px;top:0}.page-civicrm-case-a #branding .breadcrumb>a{left:0}@media (max-width:1455px){.page-civicrm-case-a .crm-contactEmail-form-block-subject .crm-token-selector{margin-top:5px}}.page-civicrm-dashboard #page,.page-civicrm:not([class*=' page-civicrm-']) #page{margin:0;padding-top:0}.page-civicrm-dashboard .block-civicrm>h2,.page-civicrm:not([class*=' page-civicrm-']) .block-civicrm>h2{margin:0}.page-civicrm-dashboard #branding,.page-civicrm:not([class*=' page-civicrm-']) #branding{padding:16px 0!important}.page-civicrm-dashboard #branding .breadcrumb,.page-civicrm:not([class*=' page-civicrm-']) #branding .breadcrumb{left:0;line-height:18px;padding:0 20px;top:0}.page-civicrm-dashboard #branding .breadcrumb>a,.page-civicrm:not([class*=' page-civicrm-']) #branding .breadcrumb>a{left:0}.page-civicrm-dashboard .civicase__tabs.affix,.page-civicrm:not([class*=' page-civicrm-']) .civicase__tabs.affix{position:fixed;top:0;width:100%;z-index:9999}.civicase__crm-dashboard .tab-content,.civicase__crm-dashboard.ui-tabs{background:0 0}#civicaseMyActivitiesTab{margin-left:-10px;margin-right:-10px}#civicaseMyActivitiesTab [crm-page-title]{display:none}#civicaseMyActivitiesTab .civicase__activity-feed>.panel-body{padding-left:0;padding-right:0}#civicaseMyActivitiesTab .civicase__activity-filter{padding:16px 0}#civicaseMyActivitiesTab .civicase__activity-filter.affix{width:calc(100% - 240px)} +@keyframes civicase__infinite-rotation{from{transform:rotate(0)}to{transform:rotate(360deg)}}.page-civicrm-case-a .page-title,.page-civicrm-dashboard .page-title,.page-civicrm:not([class*=' page-civicrm-']) .page-title{clip:rect(1px,1px,1px,1px);height:1px;overflow:hidden;position:absolute!important}@font-face{font-family:"Material Icons";font-style:normal;font-weight:400;src:local("Material Icons"),local("MaterialIcons-Regular"),url(../resources/fonts/material-design-icons/MaterialIcons-Regular.woff2) format("woff2"),url(../resources/fonts/material-design-icons/MaterialIcons-Regular.woff) format("woff")}#bootstrap-theme .material-icons{direction:ltr;display:inline-block;font-family:'Material Icons';font-feature-settings:'liga';-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-style:normal;font-weight:400;letter-spacing:normal;line-height:1;text-rendering:optimizeLegibility;text-transform:none;white-space:nowrap;word-wrap:normal}#bootstrap-theme .badge{font-size:13px;line-height:18px;margin-right:8px;padding-bottom:0;padding-top:0}#bootstrap-theme .badge:last-child{margin-right:0}#bootstrap-theme .btn{font-size:13px;line-height:1.384615em}#bootstrap-theme .crm-clear-link{color:#0071bd}#bootstrap-theme .crm-clear-link .fa-times::before{content:'\f057'}.select2-container.crm-token-selector{width:360px!important}#bootstrap-theme .dropdown-menu{padding:10px 0}#bootstrap-theme .dropdown-menu>li>a{line-height:18px;padding:6px 16px}#bootstrap-theme .dropdown-menu>li .material-icons{color:#9494a5;font-size:13px;margin-right:3px;position:relative;top:2px}#bootstrap-theme .crm_notification-badge{line-height:18px;padding:0 10px}#bootstrap-theme .progress{background:#d3dee2;border-radius:2px;box-shadow:none;height:4px}#bootstrap-theme .select2-container-multi .select2-choices{background:#fff}#bootstrap-theme .select2-container-disabled .select2-choice{background:#f3f6f7;cursor:no-drop;opacity:.8}#bootstrap-theme .simplebar-track{background:#e8eef0;overflow:hidden}#bootstrap-theme .simplebar-track.horizontal{position:relative;height:11px}#bootstrap-theme .simplebar-track.horizontal[style="visibility: hidden;"]{height:0}#bootstrap-theme .simplebar-track.horizontal .simplebar-scrollbar{height:5px;top:3px}#bootstrap-theme .simplebar-scrollbar::before{background:#c2cfd8;border-radius:2.5;opacity:1}#bootstrap-theme .civicase__accordion .panel-heading{padding:0 0 15px}#bootstrap-theme .civicase__accordion .panel-title{font-size:16px}#bootstrap-theme .civicase__accordion .panel-title a,#bootstrap-theme .civicase__accordion .panel-title a:hover{text-decoration:none}#bootstrap-theme .civicase__accordion .panel-title a::before{content:'\f105'}#bootstrap-theme .civicase__accordion.panel-open .panel-title a::before{content:'\f107';margin-left:-4px}#bootstrap-theme .civicase__accordion .panel-body{padding:0 0 15px}#bootstrap-theme .civicase__activities-calendar{transition:opacity .2s linear}#bootstrap-theme .civicase__activities-calendar.is-loading-days{opacity:.7}#bootstrap-theme .civicase__activities-calendar .btn-default,#bootstrap-theme .civicase__activities-calendar table,#bootstrap-theme .civicase__activities-calendar th{background:0 0;color:#464354}#bootstrap-theme .civicase__activities-calendar thead th{position:relative;z-index:1}#bootstrap-theme .civicase__activities-calendar thead tr:nth-child(1){background-color:transparent}#bootstrap-theme .civicase__activities-calendar thead .btn{font-size:16px;text-transform:none}#bootstrap-theme .civicase__activities-calendar thead .btn.uib-title{font-weight:600;margin-top:-3px;padding:3px 0 8px}#bootstrap-theme .civicase__activities-calendar thead .btn.uib-left,#bootstrap-theme .civicase__activities-calendar thead .btn.uib-right{margin-top:-3px;max-width:24px;padding:3px 0 8px}#bootstrap-theme .civicase__activities-calendar thead .material-icons{font-size:24px;line-height:24px}#bootstrap-theme .civicase__activities-calendar .civicase__activities-calendar__title-word,#bootstrap-theme .civicase__activities-calendar .uib-title strong{color:#464354;font-size:16px;font-weight:600;line-height:22px}#bootstrap-theme .civicase__activities-calendar .uib-title span:nth-last-child(1){color:#9494a5}#bootstrap-theme .civicase__activities-calendar [uib-daypicker] .uib-title strong{display:none}#bootstrap-theme .civicase__activities-calendar [uib-monthpicker] .civicase__activities-calendar__title-word,#bootstrap-theme .civicase__activities-calendar [uib-yearpicker] .civicase__activities-calendar__title-word{display:none}#bootstrap-theme .civicase__activities-calendar tr{background-color:#fff;padding:0 5px}#bootstrap-theme .civicase__activities-calendar tbody,#bootstrap-theme .civicase__activities-calendar tr:nth-child(0n+2) th{background:#fff}#bootstrap-theme .civicase__activities-calendar tr:nth-child(2n){border-top-left-radius:5px;border-top-right-radius:5px;margin-top:-3px}#bootstrap-theme .civicase__activities-calendar tr:nth-child(2n) th{color:#9494a5;font-size:10px;padding:21px 0;text-transform:uppercase}#bootstrap-theme .civicase__activities-calendar tr:nth-child(2n) .current-week-day{color:#0071bd}#bootstrap-theme .civicase__activities-calendar thead th:nth-child(1){border-top-left-radius:2px}#bootstrap-theme .civicase__activities-calendar thead th:nth-last-child(1){border-top-right-radius:2px}#bootstrap-theme .civicase__activities-calendar tr:nth-last-child(1) td:nth-child(1){border-bottom-left-radius:2px}#bootstrap-theme .civicase__activities-calendar tr:nth-last-child(1) td:nth-last-child(1){border-bottom-right-radius:2px}#bootstrap-theme .civicase__activities-calendar tbody{border-bottom-left-radius:5px;border-bottom-right-radius:5px;box-shadow:0 3px 8px 0 rgba(49,40,40,.15);min-height:205px}#bootstrap-theme .civicase__activities-calendar [uib-monthpicker] thead,#bootstrap-theme .civicase__activities-calendar [uib-yearpicker] thead{padding-bottom:7px}#bootstrap-theme .civicase__activities-calendar [uib-monthpicker] tbody,#bootstrap-theme .civicase__activities-calendar [uib-yearpicker] tbody{margin-top:-3px;min-height:262px}#bootstrap-theme .civicase__activities-calendar tbody .btn{font-weight:600;padding:10px 0;position:relative;width:100%}#bootstrap-theme .civicase__activities-calendar .btn.active{background-color:#cde1ed;color:#0071bd}#bootstrap-theme .civicase__activities-calendar .uib-day .btn.active{height:28px;margin-top:2px;padding:0;width:28px}#bootstrap-theme .civicase__activities-calendar .uib-day .material-icons{display:none}#bootstrap-theme .civicase__activities-calendar__day-status.uib-day .material-icons{color:#9494a5;display:block;font-size:6px;left:50%;position:absolute;transform:translateX(-50%) translateY(3px);width:6px}#bootstrap-theme .civicase__activities-calendar__day-status--completed.uib-day .material-icons{color:#44cb7e}#bootstrap-theme .civicase__activities-calendar__day-status--overdue.uib-day .material-icons{color:#cf3458}#bootstrap-theme .civicase__activities-calendar__day-status--scheduled.uib-day .material-icons{color:#0071bd}#bootstrap-theme .activities-calendar-popover{border-color:#e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);margin-top:15px;max-width:280px;padding:0}#bootstrap-theme .activities-calendar-popover>.arrow{border-bottom-color:#e8eef0}#bootstrap-theme .activities-calendar-popover .popover-content{max-height:330px;overflow-x:hidden;overflow-y:auto;padding:0}#bootstrap-theme .activities-calendar-popover__footer{border-top:1px solid #e8eef0}#bootstrap-theme .activities-calendar-popover__see-all{padding:10px}#bootstrap-theme .civicase__activities-calendar__dropdown{transform:translateX(calc(-100% + 18px))}#bootstrap-theme .civicase__activity-card--big{display:flex;flex-direction:column;height:auto;min-height:264px;width:100%}#bootstrap-theme .civicase__activity-card--big .panel{flex-grow:1}#bootstrap-theme .civicase__activity-card--big .panel .panel-body{padding:16px 24px 24px}#bootstrap-theme .civicase__activity-card--big .civicase__tooltip{flex:1;min-width:0}#bootstrap-theme .civicase__activity-card--big .material-icons{vertical-align:middle}#bootstrap-theme .civicase__activity-card--big .civicase__activity-card-menu{top:-2px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-type{flex:1 0 0;font-size:16px;line-height:22px;margin-bottom:12px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-date{color:#4d4d69}#bootstrap-theme .civicase__activity-card--big .civicase__activity-date .material-icons{font-size:22px;margin-right:5px;position:relative;top:-2px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-card .civicase__checkbox{margin-left:2px;margin-right:10px}#bootstrap-theme .civicase__activity-card--big .civicase__contact-additional__container--avatar{margin-left:5px;margin-top:1px}#bootstrap-theme .civicase__activity-card--big .civicase__contact-avatar{margin-top:0}#bootstrap-theme .civicase__activity-card--big .civicase__contact-icon{margin-top:-3px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-card-row{align-items:flex-start}#bootstrap-theme .civicase__activity-card--big .civicase__activity-card-row.civicase__activity-card-row--first{border-bottom:1px solid #e8eef0;margin:0 -24px 15px;padding:1px 16px 16px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-icon-container{align-items:center;display:flex;justify-content:center;width:auto}#bootstrap-theme .civicase__activity-card--big .civicase__activity-icon-container span:not(.civicase__activity-icon-ribbon){margin:0 8px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-icon-container .civicase__activity-icon-ribbon{border-bottom-width:10px;border-left-width:20px;border-right-width:20px;height:62px;left:16px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-icon-container .civicase__activity-icon{font-size:22px;left:2px;vertical-align:middle}#bootstrap-theme .civicase__activity-card--big .civicase__tags-container{margin-bottom:10px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-subject{color:#9494a5;font-size:13px;font-weight:400;line-height:18px;margin:5px 0 17px}#bootstrap-theme .civicase__activity-card--big .civicase__activity-attachment__container,#bootstrap-theme .civicase__activity-card--big .civicase__activity-star__container{position:relative}#bootstrap-theme .civicase__activity-card--big .civicase__activity-star__container{margin-left:4px}#bootstrap-theme .civicase__activity-card--big--empty{align-items:center;display:flex;flex-direction:column;justify-content:center;min-height:265px;text-align:center;width:100%}#bootstrap-theme .civicase__activity-card--big--empty.civicase__activity-card--big--empty--list-view{border:1px solid #d3dee2;border-radius:2px;min-height:265px}#bootstrap-theme .civicase__activity-card--big--empty-title{font-size:20px;font-weight:600;line-height:27px;margin:15px 0 5px}#bootstrap-theme .civicase__activity-card--big--empty-description{color:#9494a5;margin-bottom:20px;padding:0 5px}#bootstrap-theme .civicase__activity-card--big--empty-button{border-color:#0071bd!important;font-size:13px;font-weight:600;line-height:18px;padding:10px 16px}#bootstrap-theme .civicase__activity-card--big--empty-button,#bootstrap-theme .civicase__activity-card--big--empty-button:active,#bootstrap-theme .civicase__activity-card--big--empty-button:focus{background-color:inherit}#bootstrap-theme .civicase__activity-card--big--empty-button .material-icons{color:#0071bd;margin-right:8px;position:relative;top:-1px}#bootstrap-theme .civicase__activity-card--big--empty-button i:nth-last-child(1){margin-left:8px}#bootstrap-theme .civicase__activity-card--big--empty-button.btn-default:active,#bootstrap-theme .civicase__activity-card--big--empty-button:active,#bootstrap-theme .civicase__activity-card--big--empty-button:hover,#bootstrap-theme .civicase__activity-card--big--empty-button:hover .material-icons,#bootstrap-theme .civicase__activity-card--big--empty-button[disabled]:hover{background-color:#0071bd;color:#fff}#bootstrap-theme .civicase__activity-card--long{box-shadow:0 3px 8px 0 rgba(49,40,40,.15);position:relative}#bootstrap-theme .civicase__activity-card--long .civicase__activity-icon-container .civicase__activity-icon-ribbon{border-bottom-width:10px;border-left-width:20px;border-right-width:20px;height:63px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-icon-container .civicase__activity-icon{font-size:22px;left:12px;top:0}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card-row--first{margin-bottom:0}#bootstrap-theme .civicase__activity-card--long .civicase__activity-icon-container--ribbon{width:50px}#bootstrap-theme .civicase__activity-card--long .civicase__checkbox{margin-left:10px;margin-right:8px}#bootstrap-theme .civicase__activity-card--long .civicase__tooltip{flex:1;max-width:300px;min-width:0}#bootstrap-theme .civicase__activity-card--long .civicase__activity-type{display:block;font-size:16px;margin-right:12px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-attachment__icon{position:relative;top:2px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-attachment__file-options{display:inline-block}#bootstrap-theme .civicase__activity-card--long .civicase__activity-attachment__file-options .civicase__activity-card-menu.btn-group>.dropdown-menu{transform:translateX(0)}#bootstrap-theme .civicase__activity-card--long .civicase__tags-container{margin-right:5px;margin-top:-3px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-star{position:relative;top:3px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-date{margin-left:5px;margin-top:-1px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-date__with-year,#bootstrap-theme .civicase__activity-card--long .civicase__activity-date__without-year{vertical-align:middle}#bootstrap-theme .civicase__activity-card--long .civicase__activity-date__without-year{display:none}#bootstrap-theme .civicase__activity-card--long .civicase__contact-additional__container--avatar{margin-top:0}#bootstrap-theme .civicase__activity-card--long .civicase__activity-subject{color:#9494a5;font-weight:400;margin-left:30px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card-menu.btn-group .btn{margin-left:0;top:-2px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--ribbon{min-height:75px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--ribbon .panel-body{min-height:70px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--ribbon .civicase__activity-subject{margin-left:52px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--with-checkbox .civicase__activity-subject{margin-left:64px}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--draft{background:0 0;box-shadow:none}#bootstrap-theme .civicase__activity-card--long.civicase__activity-card--draft .panel-footer{border-top:1px dashed #c2cfd8}#bootstrap-theme .civicase__activity-card--long .civicase__activity-icon-arrow{left:22px;top:8px}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card__case-type{overflow:hidden;text-overflow:ellipsis}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card-row--communication>div{align-items:center;display:flex}#bootstrap-theme .civicase__activity-card--long .civicase__activity-card-row--communication .civicase__contact-card{position:relative;top:2px}#bootstrap-theme .civicase__activity-card--short{box-shadow:0 1px 4px 0 rgba(49,40,40,.2);min-height:100px;position:relative;width:280px}#bootstrap-theme .civicase__activity-card--short .panel-body{min-height:100px}#bootstrap-theme .civicase__activity-card--short .civicase__contact-avatar{margin-top:-10px}#bootstrap-theme .civicase__activity-card--short .civicase__activity-subject{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__activity-card--short .civicase__tooltip{flex:1;min-width:0}#bootstrap-theme .civicase__activity-card--short .civicase__activity-card__case-type{max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__activity-card{background:#fff;border-radius:5px;cursor:pointer}#bootstrap-theme .civicase__activity-card:hover{background:#f3f6f7}#bootstrap-theme .civicase__activity-card .panel{box-shadow:none;height:100%;margin-bottom:0}#bootstrap-theme .civicase__activity-card .panel-body{background:0 0;border-top:0!important;height:100%;padding:15px}#bootstrap-theme .civicase__activity-card .panel-footer{background:0 0;padding:5px 16px}#bootstrap-theme .civicase__activity-card .civicase__contact-avatar{margin-left:5px}#bootstrap-theme .civicase__activity-card .civicase__checkbox{margin-right:5px;margin-top:2px}#bootstrap-theme .civicase__activity-card .panel-footer:hover{background:#e8eef0}#bootstrap-theme .civicase__activity-card .panel-footer:hover>a:hover{text-decoration:none}#bootstrap-theme .civicase__activity-card-inner{position:relative;width:100%}#bootstrap-theme .civicase__activity-card--empty .panel-body{align-items:center;display:flex;justify-content:center}#bootstrap-theme .civicase__activity-card--draft{border:1px dashed #c2cfd8}#bootstrap-theme .civicase__activity-card--alert{background:#fbf0e2;border:1px solid #e6ab5e}#bootstrap-theme .civicase__activity-card--alert:hover{background:#fbf0e2;border-color:#c2cfd8}#bootstrap-theme .civicase__activity-card--alert .civicase__activity-subject{color:#4d4d69;white-space:initial}#bootstrap-theme .civicase__activity-card--alert .civicase__tags-container{margin-left:30px;margin-top:2px}#bootstrap-theme .civicase__activity-card--file .civicase__activity-subject{color:#464354;font-size:16px;font-weight:600;margin-left:0}#bootstrap-theme .civicase__activity-card__case-id__label,#bootstrap-theme .civicase__activity-card__case-id__value,#bootstrap-theme .civicase__activity-card__case-type{color:#9494a5}#bootstrap-theme .civicase__activity-card__case-id__value{font-weight:600}#bootstrap-theme .civicase__activity-icon-container{color:#0071bd;font-size:18px;width:30px}#bootstrap-theme .civicase__activity-icon-container--ribbon{width:38px}#bootstrap-theme .civicase__activity-icon-container--ribbon .civicase__activity-icon{color:#fff;font-size:16px;left:8px;position:relative;top:-6px;z-index:1}#bootstrap-theme .civicase__activity-icon-arrow{font-size:10px;left:24px;position:absolute;top:11px}#bootstrap-theme .civicase__activity-icon-ribbon{border-bottom:6px solid transparent;border-left:14px solid #0071bd;border-radius:2px;border-right:14px solid #0071bd;height:42px;left:17px;position:absolute;top:-3px;width:0}#bootstrap-theme .civicase__activity-icon-ribbon.text-danger{border-left:14px solid #cf3458;border-right:14px solid #cf3458}#bootstrap-theme .civicase__activity-icon-ribbon.civicase__text-success{border-left:14px solid #44cb7e;border-right:14px solid #44cb7e}#bootstrap-theme .civicase__activity-date{color:#9494a5}#bootstrap-theme .civicase__activity__right-container{margin-left:auto;white-space:nowrap}#bootstrap-theme .civicase__activity__right-container>*{display:inline-block!important;vertical-align:middle}#bootstrap-theme .civicase__activity-type{color:#464354;font-weight:600}#bootstrap-theme .civicase__activity-type--completed{text-decoration:line-through}#bootstrap-theme .civicase__activity-subject{color:#464354;font-weight:600}#bootstrap-theme .civicase__activity-card-row{align-items:flex-start;display:flex;line-height:1.8em;vertical-align:middle}#bootstrap-theme .civicase__activity-card-row--first{margin-bottom:5px}#bootstrap-theme .civicase__activity-card-row--file{display:block;margin-left:30px}#bootstrap-theme .civicase__activity-card-row--case-info{line-height:2em;white-space:nowrap}#bootstrap-theme .civicase__activity-card-row--case-info .civicase__contact-card{margin-right:5px}#bootstrap-theme .civicase__activity-card-row--case-info .civicase__contact-icon{position:relative;top:2px}#bootstrap-theme .civicase__activity-card-row--case-info .civicase__pipe{margin:0 5px}#bootstrap-theme .civicase__activity-with{color:#9494a5}#bootstrap-theme .civicase__activity-star{color:#c2cfd8;font-size:18px}#bootstrap-theme .civicase__activity-star.active{color:#e6ab5e}#bootstrap-theme .civicase__activity-attachment__container a{display:flex!important}#bootstrap-theme .civicase__activity-attachment__container.open .dropdown-toggle{box-shadow:none}#bootstrap-theme .civicase__activity-attachment__container:hover .dropdown-menu{display:block}#bootstrap-theme .civicase__activity-attachment__dropdown-menu{z-index:1061}#bootstrap-theme .civicase__activity-attachment__file-name{color:#0071bd!important}#bootstrap-theme .civicase__activity-attachment__file-description{color:#9494a5}#bootstrap-theme .civicase__activity-attachment__icon{color:#c2cfd8;font-size:18px}#bootstrap-theme .civicase__activity-attachment__icon:hover{color:#0071bd}#bootstrap-theme .civicase__activity-card-menu .material-icons{vertical-align:initial}#bootstrap-theme .civicase__activity-card-menu.btn-group .btn{border:0;height:18px;margin-left:6px;padding:0;top:0;width:18px}#bootstrap-theme .civicase__activity-card-menu.btn-group .btn .material-icons{font-size:18px}#bootstrap-theme .civicase__activity-card-menu.btn-group .btn.dropdown-toggle{background:0 0;box-shadow:none}#bootstrap-theme .civicase__activity-card-menu.btn-group>.dropdown-menu{left:50%!important;top:150%!important;transform:translateX(-100%)}#bootstrap-theme .civicase__activity-attachment__file-icon{color:#9494a5}#bootstrap-theme .civicase__activity-attachment-load{padding:10px 20px!important}#bootstrap-theme .civicase__activity-attachment-load-icon{animation:civicase__infinite-rotation 2s linear reverse;font-size:16px;margin-top:3px;position:relative;top:3px}#bootstrap-theme .civicase__activity-empty-message{color:#9494a5;font-size:16px;text-align:center}#bootstrap-theme .civicase__activity-empty-link{display:block;text-align:center}#bootstrap-theme .civicase__activity-no-result-icon{background-position:center center;background-repeat:no-repeat;background-size:contain;height:48px;width:48px}#bootstrap-theme .civicase__activity-no-result-icon--milestone{background-image:url(../resources/icons/milestone.svg)}#bootstrap-theme .civicase__activity-no-result-icon--activity{background-image:url(../resources/icons/activities.svg)}#bootstrap-theme .civicase__activity-no-result-icon--case{background-image:url(../resources/icons/cases.svg)}#bootstrap-theme .civicase__activity-no-result-icon--communications{background-image:url(../resources/icons/comms.svg);width:66px}#bootstrap-theme .civicase__activity-no-result-icon--tasks{background-image:url(../resources/icons/tasks.svg)}#bootstrap-theme .civicase__activity-feed{box-shadow:none;margin-bottom:0}#bootstrap-theme .civicase__activity-feed .panel-body{background:0 0;border-top:0!important;padding:15px}#bootstrap-theme .civicase__activity-feed>.panel-body{padding-bottom:0;padding-top:8px}#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header{margin:auto}#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header>.panel-body{background:0 0;border-top:0;box-shadow:none;padding:0}#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header .panel-title.civicase__overdue-activity-icon--before{color:#464354!important;padding-left:35px}#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header .panel-title.civicase__overdue-activity-icon--before::after,#bootstrap-theme .civicase__activity-feed .civicase__panel-transparent-header .panel-title.civicase__overdue-activity-icon--before::before{font-size:14px;height:20px;line-height:20px;top:8px;width:20px}#bootstrap-theme .civicase__activity-feed .civicase__bulkactions-message{margin:0 85px}#bootstrap-theme .civicase__activity-feed .civicase__bulkactions-message .alert{border-bottom:1px solid #d3dee2!important;box-shadow:none}#bootstrap-theme .civicase__activity-feed__activity-container{display:inline-block;margin-left:5px;width:calc(100% - 50px)}#bootstrap-theme .civicase__activity-feed__list{padding-left:10px;padding-right:10px;padding-top:6px;position:relative}#bootstrap-theme .civicase__activity-feed__list.active{background:#b3d5ec;border-radius:5px}#bootstrap-theme .civicase__activity-feed__list.civicase__animated-checkbox-card--expanded{padding-left:50px}#bootstrap-theme .civicase__activity-feed__list__vertical_bar::before{background-color:#c2cfd8;bottom:1px;content:'';left:17px;position:absolute;top:50px;width:8px;z-index:1}#bootstrap-theme .civicase__activity-feed__list-item{display:inline-block;position:relative;width:100%}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card{display:inline-block;margin-top:2px;position:relative;vertical-align:top;z-index:2}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card .civicase__contact-avatar{height:40px;line-height:30px;width:40px}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card .civicase__contact-avatar--image img{height:40px;width:40px}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card .civicase__contact-avatar__full-name{height:40px}#bootstrap-theme .civicase__activity-feed__list-item>.civicase__contact-card .civicase__contact-avatar--image:hover .civicase__contact-avatar__full-name{left:39px}#bootstrap-theme .civicase__activity-feed__list-item .civicase__activity-card{margin-bottom:5px;margin-left:auto;margin-right:auto;width:100%}#bootstrap-theme .civicase__checkbox--bulk-action{display:inline-block;margin-right:20px;top:13px;vertical-align:top}#bootstrap-theme .civicase__activity-feed-pager .material-icons{font-size:28px}#bootstrap-theme .civicase__activity-feed-pager--down .civicase__activity-feed-pager__more>.btn,#bootstrap-theme .civicase__activity-feed-pager--down .civicase__activity-feed-pager__no-more>.btn{margin-top:40px}#bootstrap-theme .civicase__activity-feed-pager--down .civicase__spinner{margin-top:40px}#bootstrap-theme .civicase__activity-feed-pager__no-more>.btn{background:0 0;border:0;font-weight:600}#bootstrap-theme .civicase__activity-feed__body{display:flex;justify-content:center;margin:auto;max-width:1330px}#bootstrap-theme .civicase__activity-feed__body__list{flex-grow:1;max-width:630px;min-height:400px;overflow:auto}#bootstrap-theme .civicase__activity-feed__body__details{box-sizing:content-box;min-height:400px;min-width:550px;overflow-y:auto;padding-left:15px;padding-right:10px;padding-top:8px;width:550px}#bootstrap-theme .civicase__activity-feed__body__month-nav{margin-left:15px;min-height:400px;overflow-x:hidden;overflow-y:auto;width:125px}#bootstrap-theme .civicase__activity-feed__placeholder{margin-left:auto;margin-right:auto;width:50%}#bootstrap-theme .civicase__activity-feed__placeholder .civicase__panel-transparent-header{width:100%}@media (max-width:1300px){#bootstrap-theme .civicase__activity-feed .civicase__activity-card--long .civicase__activity-date__with-year{display:none}#bootstrap-theme .civicase__activity-feed .civicase__activity-card--long .civicase__activity-date__without-year{display:inline-block}#bootstrap-theme .civicase__activity-feed .civicase__activity-card--long.civicase__activity-card--ribbon .panel-body{padding:15px 10px 15px 15px}#bootstrap-theme .civicase__activity-feed .civicase__activity-card--long .civicase__tags-container .badge{max-width:35px}#bootstrap-theme .civicase__activity-feed__body__list--details-visible .civicase__contact-name{max-width:40px}}#bootstrap-theme .civicase__activity-filter{background:#e8eef0;padding:16px 40px;width:100%;z-index:11}#bootstrap-theme .civicase__activity-filter__settings .dropdown-toggle{background:0 0!important;box-shadow:none!important;padding:0}#bootstrap-theme .civicase__activity-filter__settings .dropdown-menu{width:250px}#bootstrap-theme .civicase__activity-filter__add .dropdown-menu li,#bootstrap-theme .civicase__activity-filter__settings .dropdown-menu li{padding:0 20px}#bootstrap-theme .civicase__activity-filter__add .dropdown-menu li label,#bootstrap-theme .civicase__activity-filter__settings .dropdown-menu li label{font-weight:400;margin-left:5px;position:relative;top:3px}#bootstrap-theme .civicase__activity-filter__add .material-icons,#bootstrap-theme .civicase__activity-filter__settings .material-icons{color:#9494a5;font-size:20px;position:relative;top:2px}#bootstrap-theme .civicase__activity-filter__add .caret,#bootstrap-theme .civicase__activity-filter__settings .caret{line-height:20px;margin-left:4px;position:relative;top:-5px}#bootstrap-theme .civicase__activity-filter__add,#bootstrap-theme .civicase__activity-filter__others{min-width:150px}#bootstrap-theme .civicase__activity-filter__add .select2-container,#bootstrap-theme .civicase__activity-filter__others .select2-container{height:auto!important;max-width:170px;min-width:170px}#bootstrap-theme .civicase__activity-filter__contact{margin-left:8px}#bootstrap-theme .civicase__activity-filter__contact .btn{border:1px solid #c2cfd8!important}#bootstrap-theme .civicase__activity-filter__contact .btn.active{background:#f3f6f7;box-shadow:inset 0 0 5px 0 rgba(0,0,0,.1);color:#0071bd}#bootstrap-theme .civicase__activity-filter__timeline{width:auto}#bootstrap-theme .civicase__activity-filter__case-type-categories{display:inline-block;margin-left:5px;width:175px}#bootstrap-theme .civicase__activity-filter__category{vertical-align:top;width:175px}#bootstrap-theme .civicase__activity-filter__category .crm-i{color:#4d4d69}#bootstrap-theme .civicase__activity-filter__category .select2-chosen{max-width:130px}#bootstrap-theme .civicase__activity-filter__category,#bootstrap-theme .civicase__activity-filter__timeline{display:inline-block;margin-left:8px}#bootstrap-theme .civicase__activity-filter__category .select2-choice .select2-arrow,#bootstrap-theme .civicase__activity-filter__timeline .select2-choice .select2-arrow{top:0;width:24px}#bootstrap-theme .civicase__activity-filter__attachment,#bootstrap-theme .civicase__activity-filter__more,#bootstrap-theme .civicase__activity-filter__star{background:0 0!important;padding:5px;text-transform:initial}#bootstrap-theme .civicase__activity-filter__attachment .material-icons,#bootstrap-theme .civicase__activity-filter__more .material-icons,#bootstrap-theme .civicase__activity-filter__star .material-icons{color:#9494a5;font-size:18px;vertical-align:middle}#bootstrap-theme .civicase__activity-filter__attachment.btn-active .material-icons,#bootstrap-theme .civicase__activity-filter__more.btn-active .material-icons,#bootstrap-theme .civicase__activity-filter__star.btn-active .material-icons{color:#0071bd}#bootstrap-theme .civicase__activity-filter__more,#bootstrap-theme .civicase__activity-filter__star{padding-left:0}#bootstrap-theme .civicase__activity-filter__star.btn-active .material-icons{color:#e6ab5e}#bootstrap-theme .civicase__activity-filter__more span{color:#4d4d69;position:relative;top:2px}#bootstrap-theme .civicase__activity-filter__more-container{margin-top:15px}#bootstrap-theme .civicase__activity-filter__more-container>*{display:inline-block;margin-bottom:15px;margin-left:5px;margin-right:5px;vertical-align:top}#bootstrap-theme .civicase__activity-filter__custom .civicase__activity-filter__header{border-bottom:1px solid #e8eef0;display:block;margin-top:5px}@media (max-width:1420px){#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter{padding:16px 10px}#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__timeline{width:120px!important}#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__category{width:165px!important}#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__more__text{display:none}#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__attachment,#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__more,#bootstrap-theme .civicase__case-details-panel:not(.civicase__case-details-panel--focused) .civicase__activity-filter__star{padding:5px 2px}}#bootstrap-theme .civicase__activity-month-nav{overflow:hidden;width:105px}#bootstrap-theme .civicase__activity-month-nav.affix{position:fixed!important}#bootstrap-theme .civicase__activity-month-nav__group{border-left:2px solid #d9e1e6}#bootstrap-theme .civicase__activity-month-nav__group-month,#bootstrap-theme .civicase__activity-month-nav__group-title,#bootstrap-theme .civicase__activity-month-nav__group-year{padding-left:15px}#bootstrap-theme .civicase__activity-month-nav__group-title{color:#464354;font-weight:700;text-transform:uppercase}#bootstrap-theme .civicase__activity-month-nav__group-year{color:#464354}#bootstrap-theme .civicase__activity-month-nav__group-gap{height:10px}#bootstrap-theme .civicase__activity-month-nav__group-month{color:#9494a5;cursor:pointer;font-weight:600}#bootstrap-theme .civicase__activity-month-nav__group-month.active{border-left:2px solid #0071bd;color:#0071bd;margin-left:-2px}#bootstrap-theme .civicase__activity-month-nav__group-month:hover{color:#0071bd}#bootstrap-theme .civicase__overdue-activity-icon{color:#cf3458!important;display:inline-block;font-weight:600;padding-right:20px;position:relative}#bootstrap-theme .civicase__overdue-activity-icon::before{background-color:#cf3458;content:''}#bootstrap-theme .civicase__overdue-activity-icon::after{color:#fff;content:'!';top:1px}#bootstrap-theme .civicase__overdue-activity-icon::after,#bootstrap-theme .civicase__overdue-activity-icon::before{border-radius:50%!important;font-size:11px;height:13px;line-height:1em;position:absolute;right:0;text-align:center;top:50%;transform:translateY(-50%);width:13px;z-index:0!important}#bootstrap-theme .civicase__overdue-activity-icon.civicase__overdue-activity-icon--before{padding-left:20px;padding-right:0}#bootstrap-theme .civicase__overdue-activity-icon.civicase__overdue-activity-icon--before::after,#bootstrap-theme .civicase__overdue-activity-icon.civicase__overdue-activity-icon--before::before{left:2px;right:auto}#bootstrap-theme .civicase__activity-panel.affix{position:fixed!important}#bootstrap-theme .civicase__activity-panel .panel{overflow:auto;position:relative}#bootstrap-theme .civicase__activity-panel .panel-heading,#bootstrap-theme .civicase__activity-panel .panel-subheading{position:absolute;width:100%}#bootstrap-theme .civicase__activity-panel .panel-subheading{border-bottom:1px solid #e8eef0;top:63px}#bootstrap-theme .civicase__activity-panel .panel-body{margin-bottom:76px;margin-top:120px;overflow:auto;padding:0}#bootstrap-theme .civicase__activity-panel .panel-subtitle{color:#464354;display:flex;font-size:16px;font-weight:600;line-height:25px}#bootstrap-theme .civicase__activity-panel .civicase__tooltip{flex:1;min-width:0}#bootstrap-theme .civicase__activity-panel .civicase__activity__right-container .civicase__activity-date{font-size:13px;font-weight:400;position:relative;top:2px}#bootstrap-theme .civicase__activity-panel .civicase__activity__right-container .civicase__activity-star{position:relative;top:2px}#bootstrap-theme .civicase__activity-panel .civicase__activity__right-container .civicase__contact-additional__container--avatar{margin-top:0}#bootstrap-theme .civicase__activity-panel__close,#bootstrap-theme .civicase__activity-panel__maximise{color:#464354;font-size:18px;padding:0}#bootstrap-theme .civicase__activity-panel__maximise{margin-right:5px;transform:rotate(45deg)}#bootstrap-theme .civicase__activity-panel__status-dropdown{margin-right:5px}#bootstrap-theme .civicase__activity-panel__status-dropdown .list-group-item-info{color:#9494a5;display:block;padding:7px 19px 7px 24px}#bootstrap-theme .civicase__activity-panel__priority-dropdown{margin-right:10px}#bootstrap-theme .civicase__activity-panel__priority-dropdown .list-group-item-info{color:#9494a5;display:block;padding:7px 19px 7px 24px}#bootstrap-theme .civicase__activity-panel__id{line-height:33px}#bootstrap-theme .civicase__activity-panel__resume-draft{bottom:20px;height:36px;position:absolute;right:20px}#bootstrap-theme .civicase__activity-panel__core_container{min-height:200px;position:static!important}#bootstrap-theme .civicase__activity-panel__core_container .help{display:none}#bootstrap-theme .civicase__activity-panel__core_container .crm-activity-form-block-separation{display:none}#bootstrap-theme .civicase__activity-panel__core_container .crm-form-block{overflow:auto;padding-top:20px}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody td:nth-child(2)>*,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody td:nth-child(2)>*{margin-bottom:10px!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody td:nth-child(2) .crm-form-checkbox,#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody td:nth-child(2) .crm-form-radio,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody td:nth-child(2) .crm-form-checkbox,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody td:nth-child(2) .crm-form-radio{margin-right:5px;position:relative;top:2px}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel,#bootstrap-theme .civicase__activity-panel__core_container .form-layout{box-shadow:none}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody>tr,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel tbody>tr,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody>tr{border:0}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody>tr .label,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel tbody>tr .label,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody>tr .label{padding-left:15px!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body tbody>tr .view-value,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel tbody>tr .view-value,#bootstrap-theme .civicase__activity-panel__core_container .form-layout tbody>tr .view-value{padding-right:15px!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body .label,#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body .label label,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel .label,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel .label label,#bootstrap-theme .civicase__activity-panel__core_container .form-layout .label,#bootstrap-theme .civicase__activity-panel__core_container .form-layout .label label{color:#9494a5!important;font-weight:400!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-accordion-body .section-shown,#bootstrap-theme .civicase__activity-panel__core_container .crm-info-panel .section-shown,#bootstrap-theme .civicase__activity-panel__core_container .form-layout .section-shown{padding:0}#bootstrap-theme .civicase__activity-panel__core_container .crm-button_qf_Activity_cancel{display:none}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons{border-bottom:0!important;border-top:1px solid #e8eef0;bottom:0;height:auto!important;margin:0;padding:20px!important;position:absolute;width:100%}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit{color:#fff!important;background-color:#0071bd;border-color:#0062a4}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:focus{color:#fff!important;background-color:#00538a;border-color:#001624}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:hover{color:#fff!important;background-color:#00538a;border-color:#003d66}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.active,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:active,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.active,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:active,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .cancel.dropdown-toggle,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .edit.dropdown-toggle{color:#fff!important;background-color:#00538a;background-image:none;border-color:#003d66}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.active:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel:active:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.active:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit:active:hover,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .cancel.dropdown-toggle.focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .cancel.dropdown-toggle:focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .cancel.dropdown-toggle:hover,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .edit.dropdown-toggle.focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .edit.dropdown-toggle:focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .edit.dropdown-toggle:hover{color:#fff!important;background-color:#003d66;border-color:#001624}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.disabled.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.disabled:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel.disabled:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel[disabled].focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel[disabled]:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel[disabled]:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.disabled.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.disabled:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit.disabled:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit[disabled].focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit[disabled]:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit[disabled]:hover,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .cancel.focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .cancel:focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .cancel:hover,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .edit.focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .edit:focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .edit:hover{background-color:#0071bd;border-color:#0062a4}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .cancel .badge,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .edit .badge{color:#0071bd;background-color:#fff!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete{color:#fff;background-color:#cf3458;border-color:#bd2d4e;background-color:#cf3458!important}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:focus{color:#fff;background-color:#a82846;border-color:#561423}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:hover{color:#fff;background-color:#a82846;border-color:#8b213a}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.active,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:active,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .delete.dropdown-toggle{color:#fff;background-color:#a82846;background-image:none;border-color:#8b213a}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.active:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:active.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:active:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete:active:hover,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .delete.dropdown-toggle.focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .delete.dropdown-toggle:focus,#bootstrap-theme .open>.civicase__activity-panel__core_container .crm-submit-buttons .delete.dropdown-toggle:hover{color:#fff;background-color:#8b213a;border-color:#561423}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.disabled.focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.disabled:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete.disabled:hover,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete[disabled].focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete[disabled]:focus,#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete[disabled]:hover,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .delete.focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .delete:focus,#bootstrap-theme fieldset[disabled] .civicase__activity-panel__core_container .crm-submit-buttons .delete:hover{background-color:#cf3458;border-color:#bd2d4e}#bootstrap-theme .civicase__activity-panel__core_container .crm-submit-buttons .delete .badge{color:#cf3458;background-color:#fff}#bootstrap-theme .civicase__activity-panel__core_container .crm-form-date-wrapper{display:inline-block}#bootstrap-theme .civicase__activity-panel__core_container .crm-form-date{width:153px}#bootstrap-theme .civicase__activity-panel__core_container .crm-form-time{margin-left:10px;width:75px}#bootstrap-theme .civicase__activity-panel__core_container .crm-ajax-select{width:230px}#bootstrap-theme .civicase__activity-panel__core_container input[name=followup_activity_subject]{width:230px}#bootstrap-theme .civicase__activity-panel__core_container .view-value>input,#bootstrap-theme .civicase__activity-panel__core_container .view-value>select{width:230px}#bootstrap-theme .civicase__activity-panel__core_container .view-value .crm-form-date-wrapper{margin-bottom:10px}.civicase__badge{border-radius:10px;color:#fff;display:inline-block;font-size:13px;line-height:18px;padding:0 7px}.civicase__badge.text-dark{color:#000}.civicase__badge--default{background:#fff;box-shadow:0 0 0 1px #d3dee2 inset;color:#000}#bootstrap-theme .civicase__bulkactions-checkbox{background:#fff;border:1px solid #c2cfd8;border-radius:2px;display:inline-block;padding:0 3px;position:relative}#bootstrap-theme .civicase__bulkactions-checkbox-toggle{color:#e8eef0;cursor:pointer;font-size:18px;margin-left:3px;transition:.3s color cubic-bezier(0,0,0,.4);vertical-align:middle}#bootstrap-theme .civicase__bulkactions-checkbox-toggle.civicase__checkbox{display:inline-block}#bootstrap-theme .civicase__bulkactions-checkbox-toggle .civicase__checkbox--checked{color:#0071bd;transition-property:color}#bootstrap-theme .civicase__bulkactions-checkbox-toggle .civicase__checkbox--checked--hide{color:#c2cfd8;font-size:19.5px;left:3px;top:2px}#bootstrap-theme .civicase__bulkactions-select-mode-dropdown{background:#fff;padding:4px 5px;vertical-align:middle}#bootstrap-theme .civicase__bulkactions-actions-dropdown{margin-left:10px;position:relative}#bootstrap-theme .civicase__bulkactions-actions-dropdown .btn{border:1px solid #c2cfd8;line-height:18px;padding:5px 25px 5px 10px;text-transform:unset}#bootstrap-theme .civicase__bulkactions-actions-dropdown .btn:hover{border-color:#c2cfd8!important}#bootstrap-theme .civicase__bulkactions-actions-dropdown .btn+.dropdown-toggle{padding:5px}#bootstrap-theme .civicase__bulkactions-message .alert{background-color:#f3f6f7;border:1px solid #d3dee2;box-shadow:0 3px 8px 0 rgba(49,40,40,.15);line-height:18px;margin-bottom:0;padding:15px;text-align:center}#bootstrap-theme .civicase__checkbox--bulk-action .civicase__checkbox--checked{color:#0071bd}#bootstrap-theme .civicase__button--with-shadow{box-shadow:0 3px 18px 0 rgba(48,40,40,.25)}#bootstrap-theme .civicase__case-activity-count__popover{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);padding:0;white-space:nowrap;z-index:11}#bootstrap-theme .civicase__case-activity-count__popover .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__case-activity-count__popover .arrow.left{left:20px}#bootstrap-theme .civicase__case-body{padding:0}#bootstrap-theme .civicase__case-body .tab-content{background:0 0;position:relative;z-index:0}#bootstrap-theme .civicase__case-body_tab{position:relative;z-index:1}#bootstrap-theme .civicase__case-body_tab.affix{position:fixed;top:0;z-index:11}#bootstrap-theme .civicase__case-body_tab.affix+.tab-content{padding-top:50px}#bootstrap-theme .civicase__case-body_tab>[civicase-dropdown]{opacity:1}#bootstrap-theme .civicase__case-details-panel--summary .civicase__activity-filter.affix,#bootstrap-theme .civicase__case-details-panel--summary .civicase__case-body_tab.affix{width:calc(100% - 300px)}#bootstrap-theme .civicase__case-details-panel--focused .civicase__activity-filter.affix,#bootstrap-theme .civicase__case-details-panel--focused .civicase__case-body_tab.affix{width:100%}#bootstrap-theme .civicase__case-card{border-radius:0!important;box-shadow:none;cursor:pointer;height:100%;margin-bottom:0}#bootstrap-theme .civicase__case-card:hover{background:#d9edf7}#bootstrap-theme .civicase__case-card .panel-body{background-color:transparent;border:0!important}#bootstrap-theme .civicase__case-card .civicase__contact-card{font-size:14px;font-weight:600;line-height:18px}#bootstrap-theme .civicase__case-card .civicase__contact-card>span{display:flex}#bootstrap-theme .civicase__case-card .civicase__contact-icon{color:#c2cfd8;font-size:24px;margin-top:-3px}#bootstrap-theme .civicase__case-card .civicase__checkbox{left:20px;top:15px}#bootstrap-theme .civicase__case-card .civicase__tags-container .badge{max-width:195px}#bootstrap-theme .civicase__case-card--closed{background-image:repeating-linear-gradient(60deg,#e8eef0,#e8eef0 2px,#f3f6f7 2px,#f3f6f7 20px);min-height:149px}#bootstrap-theme .civicase__case-card--closed .civicase__case-card-subject,#bootstrap-theme .civicase__case-card--closed .civicase__case-card__type,#bootstrap-theme .civicase__case-card--closed .civicase__contact-additional__container,#bootstrap-theme .civicase__case-card--closed .civicase__contact-name{text-decoration:line-through}#bootstrap-theme .civicase__case-card--closed .civicase__case-card__activity-count,#bootstrap-theme .civicase__case-card--closed .civicase__case-card__next-milestone-date{color:inherit;font-weight:400}#bootstrap-theme .civicase__case-card--case-list .civicase__case-card__activity-info{overflow:hidden;white-space:nowrap}#bootstrap-theme .civicase__case-card--case-list .civicase__contact-name,#bootstrap-theme .civicase__case-card--case-list .civicase__contact-name-additional{max-width:100px}#bootstrap-theme .civicase__case-card--other{border-bottom:1px solid #e8eef0;min-height:auto}#bootstrap-theme .civicase__case-card--other .civicase__contact-additional__container,#bootstrap-theme .civicase__case-card--other .civicase__contact-name{color:#464354;font-size:16px}#bootstrap-theme .civicase__case-card--other .civicase__contact-name{max-width:none}#bootstrap-theme .civicase__case-card--other .civicase__case-card__activity-info{display:inline-flex}#bootstrap-theme .civicase__case-card--other .civicase__case-card__next-milestone{margin-right:30px}#bootstrap-theme .civicase__case-card__right_container{color:#9494a5}#bootstrap-theme .civicase__case-card__dates{margin-right:16px;vertical-align:text-top}#bootstrap-theme .civicase__case-card__link-type{font-size:16px;margin-left:16px;vertical-align:middle}#bootstrap-theme .civicase__case-card--active{border-bottom:1px solid #0071bd!important;border-right:1px solid #0071bd!important;border-top:1px solid #0071bd!important}#bootstrap-theme .civicase__case-card__additional-information{line-height:normal;position:absolute;right:20px;top:15px}#bootstrap-theme .civicase__case-card__case-id{color:#9494a5}#bootstrap-theme .civicase__case-card__lock{color:#c2cfd8;font-size:24px;line-height:0;position:relative;top:5px}#bootstrap-theme .civicase__case-card__type{color:#4d4d69}#bootstrap-theme .civicase__case-card__activity-info,#bootstrap-theme .civicase__case-card__contact,#bootstrap-theme .civicase__case-card__next-milestone,#bootstrap-theme .civicase__case-card__type{margin-bottom:3px}#bootstrap-theme .civicase__case-card__activity-info,#bootstrap-theme .civicase__case-card__next-milestone{color:#9494a5}#bootstrap-theme .civicase__case-card__activity-count,#bootstrap-theme .civicase__case-card__next-milestone-date{color:#0071bd;font-weight:600}#bootstrap-theme .civicase__case-card__activity-count:hover,#bootstrap-theme .civicase__case-card__next-milestone-date:hover{text-decoration:none}#bootstrap-theme .civicase__case-card__activity-count--zero{color:#9494a5}#bootstrap-theme .civicase__case-card__activity-count-container{display:inline-block;margin-right:8px}#bootstrap-theme .civicase__case-card--detached{background:#fff;border-radius:2px!important;box-shadow:0 3px 8px 0 rgba(49,40,40,.15);color:#9494a5;margin-bottom:10px}#bootstrap-theme .civicase__case-card--detached>.panel-body{padding:0}#bootstrap-theme .civicase__case-card--detached .civicase__case-card__date{color:#4d4d69}#bootstrap-theme .civicase__case-card--detached .civicase__contact-icon{font-size:18px;vertical-align:middle}#bootstrap-theme .civicase__case-card--detached .crm_notification-badge{vertical-align:unset}#bootstrap-theme .civicase__case-card--detached .civicase__case-card-subject{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:calc(100% - 300px)}#bootstrap-theme .civicase__case-card--detached.civicase__case-card--closed{background:#f3f6f7;min-height:auto}#bootstrap-theme .civicase__case-card--detached.civicase__case-card--closed .civicase__case-card-role,#bootstrap-theme .civicase__case-card--detached.civicase__case-card--closed .civicase__case-card-subject{color:inherit}#bootstrap-theme .civicase__case-card--detached.civicase__case-card--active{box-shadow:0 0 0 5px #b3d5ec}#bootstrap-theme .civicase__case-card-role-container>*{vertical-align:middle}#bootstrap-theme .civicase__case-card-role-container .material-icons{margin-right:5px}#bootstrap-theme .civicase__case-card-role,#bootstrap-theme .civicase__case-card-subject{color:#4d4d69;line-height:18px}#bootstrap-theme .civicase__case-card__row{border-bottom:1px solid #e8eef0;clear:both}#bootstrap-theme .civicase__case-card__row:last-child{border-bottom:0}#bootstrap-theme .civicase__case-card__row--primary{padding:15px}#bootstrap-theme .civicase__case-card__row--secondary{padding:10px 15px}#bootstrap-theme .civicase__case-custom-fields__container.civicase__summary-tab-tile{padding-top:0}#bootstrap-theme .civicase__case-custom-fields__container civicase-masonry-grid-item:not(:first-child){margin-top:30px}#bootstrap-theme .civicase__case-custom-fields__container .panel-body{border-top:0!important}#bootstrap-theme .civicase__case-custom-fields__container .crm-editable-enabled:not(.crm-editable-editing):hover{border:2px dashed transparent;padding:24px}#bootstrap-theme .civicase__case-custom-fields__container .crm-case-custom-form-block table{width:100%}#bootstrap-theme .civicase__case-custom-fields__container .crm-submit-buttons{border:0;padding:15px 8px 0;text-align:right}#bootstrap-theme .civicase__case-custom-fields__container .crm-form-submit{min-width:auto;padding:7px 19px}#bootstrap-theme .civicase__case-custom-fields__container .crm-form-submit.cancel{padding:6px 19px}#bootstrap-theme .civicase__case-custom-fields__container .crm-form-submit.cancel:hover{color:#fff!important}#bootstrap-theme .civicase__case-custom-fields__container .crm-ajax-container{background:#fff;padding:20px}#bootstrap-theme .civicase__case-custom-fields__container .crm-ajax-container .crm-block{box-shadow:none}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row label{color:#9494a5;font-weight:400!important}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row:not(:first-child){display:block;margin-top:16px}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:first-child{display:block;text-align:left}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2){display:block;margin-left:7px}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) .cke,#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) textarea{width:calc(100% - 4px)}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) .select2-arrow{padding-right:20px}#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) .crm-form-checkbox,#bootstrap-theme .civicase__case-custom-fields__container .custom_field-row td:nth-child(2) .crm-form-radio{margin-right:8px;margin-top:-4px}#bootstrap-theme .civicase__case-details__add-new-dropdown{left:-20px;position:relative;top:6px}#bootstrap-theme .civicase__case-details__add-new-dropdown .btn-primary{border:1px solid #fff;line-height:20px;padding:7px 16px}#bootstrap-theme .civicase__case-details__add-new-dropdown .btn-primary i:nth-child(1){margin-right:8px;position:relative;top:1px}#bootstrap-theme .civicase__case-details__add-new-dropdown .btn-primary i:nth-last-child(1){margin-left:8px}#bootstrap-theme .civicase__case-details__add-new-dropdown [civicase-dropdown]{position:relative}#bootstrap-theme .civicase__case-details__add-new-dropdown .dropdown-menu{left:auto;right:0;width:180px}#bootstrap-theme .civicase__case-details__add-new-dropdown [civicase-dropdown] .dropdown-menu{top:0}#bootstrap-theme .civicase__case-details__add-new-dropdown .dropdown-menu .fa-fw,#bootstrap-theme .civicase__case-details__add-new-dropdown a .material-icons{color:#0071bd}#bootstrap-theme .civicase__dropdown-menu--filters.dropdown-menu{max-height:260px;overflow-x:hidden;overflow-y:scroll;padding:8px 0 0;right:170px;top:0;width:220px}#bootstrap-theme .civicase__dropdown-menu--filters.dropdown-menu .form-control{margin:0 auto;width:204px}#bootstrap-theme .civicase__dropdown-menu--filters.dropdown-menu .form-control-feedback{color:#464354;font-size:14px;margin-right:8px;margin-top:7px;position:absolute}#bootstrap-theme .civicase__dropdown-menu--filters.dropdown-menu a{padding:9px 17px;white-space:normal}#bootstrap-theme .civicase__activity-dropdown .civicase__dropdown-menu--filters a{padding:9px 17px 9px 38px}#bootstrap-theme .civicase__activity-dropdown .civicase__dropdown-menu--filters .fa-fw{margin-left:-21px;margin-right:4px}#bootstrap-theme [civicase-dropdown]{position:relative}#bootstrap-theme [civicase-dropdown] .dropdown-menu{top:calc(100% + 8px)}#bootstrap-theme .civicase__case-tab--files .civicase__activity-feed__list{margin-left:auto;margin-right:auto;width:685px}#bootstrap-theme .civicase__case-tab--files .civicase__activity-feed__list::before{content:none}#bootstrap-theme .civicase__case-tab--files .civicase__bulkactions-message{margin:0 85px 10px}#bootstrap-theme .civicase__case-tab--files .civicase__bulkactions-message .alert{border-bottom:1px solid #d3dee2!important;box-shadow:none}#bootstrap-theme .civicase__file-tab-filters{display:inline-block;float:right}#bootstrap-theme .civicase__file-filters-container{display:flex}#bootstrap-theme .civicase__file-filters{margin-left:16px;width:180px}#bootstrap-theme .civicase__file-filters .select2-container{height:auto!important}#bootstrap-theme .civicase__file-filters:not(:nth-child(2)) .form-control:not(.select2-container){width:180px}#bootstrap-theme .civicase__file-filters .input-group-addon{font-size:14px;padding:0;width:30px!important}#bootstrap-theme .civicase__file-filters .input-group-addon .material-icons{position:relative;top:2px}#bootstrap-theme .civicase__case-header{background:#fff;position:relative}#bootstrap-theme .civicase__case-header__expand_button{background:0 0;border-left:1px solid #e8eef0;border-right:1px solid #e8eef0;bottom:0;color:#c2cfd8;font-size:30px;left:0;padding:0;position:absolute;top:0;width:56px}#bootstrap-theme .civicase__case-header__expand_button>.material-icons{vertical-align:middle}#bootstrap-theme .civicase__case-header__content{border-top:1px solid #d3dee2;padding:15px 15px 15px 80px}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--client{color:#464354;font-size:24px;font-weight:600}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--client .civicase__contact-icon{font-size:30px}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--client .material-icons{line-height:1}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--client .civicase__contact-additional__arrow{top:-18px}#bootstrap-theme .civicase__case-header__content .civicase__contact-name{margin-top:1px;max-width:300px}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--manager{display:inline-block;position:relative;top:4px}#bootstrap-theme .civicase__case-header__content .civicase__contact-card--manager .material-icons{line-height:1}#bootstrap-theme .civicase__case-header__content .civicase__case-header__case-type+.civicase__pipe{margin-right:5px}#bootstrap-theme .civicase__case-header__content .civicase__tags-container{position:relative;top:-1px}#bootstrap-theme .civicase__case-header__webform-dropdown+.dropdown-menu>li a{max-width:500px!important}#bootstrap-theme .civicase__case-header__content__first-row{display:flex;min-height:42px}#bootstrap-theme .civicase__case-header__content__trash{font-size:30px;margin-right:5px;position:relative;top:2px;width:20px}#bootstrap-theme .civicase__case-header__case-info,#bootstrap-theme .civicase__case-header__dates{color:#9494a5}#bootstrap-theme .civicase__case-header__case-info{margin-top:5px}#bootstrap-theme .civicase__case-header__case-id,#bootstrap-theme .civicase__case-header__case-source,#bootstrap-theme .civicase__case-header__case-type{color:#464354}#bootstrap-theme .civicase__case-header__case-type a{display:inline}#bootstrap-theme .civicase__case-header__action-menu{position:absolute;right:20px;top:20px}#bootstrap-theme .civicase__case-header__action-menu .list-group-item-info{color:#9494a5;display:block;padding:7px 19px 7px 24px}#bootstrap-theme .civicase__case-header__action-menu .dropdown-menu>li a{max-width:200px;overflow:hidden;position:relative;text-overflow:ellipsis}#bootstrap-theme .civicase__case-header__action-menu .dropdown-menu>li>.dropdown-menu.sub-menu{left:auto;margin-top:-40px;position:absolute;right:100%}#bootstrap-theme .civicase__case-header__action-menu .dropdown-menu>li>.dropdown-menu.sub-menu a{max-width:500px}#bootstrap-theme .civicase__case-header__action-menu .dropdown-menu>li:hover>.dropdown-menu.sub-menu{display:block;opacity:1;visibility:visible}#bootstrap-theme .civicase__case-header__action-icon{font-size:20px;padding:2px 10px}#bootstrap-theme .civicase__case-header__action-icon .material-icons{position:relative;top:3px}#bootstrap-theme .civicase__case-tab--linked-cases .civicase__summary-tab__other-cases{margin-left:0;margin-right:0}#bootstrap-theme .civicase__panel-empty{margin-bottom:110px;margin-top:110px;padding:5px;text-align:center}#bootstrap-theme .civicase__panel-empty .fa.fa-big,#bootstrap-theme .civicase__panel-empty .material-icons{color:#9494a5;font-size:64px}#bootstrap-theme .civicase__panel-empty .empty-label{color:#9494a5;font-size:14px;font-weight:600;line-height:19px;padding:18px 0;text-align:center}#bootstrap-theme .civicase__case-list-table-container{border-left:1px solid #e8eef0;margin-left:300px;overflow-x:auto;overflow-y:visible}#bootstrap-theme .civicase__case-list-table{table-layout:inherit}#bootstrap-theme .civicase__case-list-table td:first-child,#bootstrap-theme .civicase__case-list-table th:first-child{width:300px}#bootstrap-theme .civicase__case-list-table th{line-height:18px;min-width:142px;padding:22px 15px!important}#bootstrap-theme .civicase__case-list-table .civicase__bulkactions-checkbox{top:2px}#bootstrap-theme .civicase__case-list-table .civicase__bulkactions-actions-dropdown{top:2px}#bootstrap-theme .civicase__case-list-table .civicase__bulkactions-actions-dropdown .civicase__bulkactions-actions-dropdown__text{width:60px}#bootstrap-theme .civicase__case-list-table .civicase__case-list-column--first{padding:16px 14px!important}#bootstrap-theme .civicase__case-list-table th:first-child{background:#f3f6f7;left:0;position:absolute;width:300px}#bootstrap-theme .civicase__case-list-table th:nth-child(2){max-width:320px;min-width:320px;position:relative}#bootstrap-theme .civicase__case-list-table tr{height:150px}#bootstrap-theme .civicase__case-list-table thead tr{height:63px}#bootstrap-theme .civicase__case-list-table td{height:150px;min-width:142px;padding:20px}#bootstrap-theme .civicase__case-list-table td:first-child{background:#fff;left:0;padding:0;position:absolute;width:300px;z-index:1}#bootstrap-theme .civicase__case-list-table td:nth-child(2){vertical-align:middle}#bootstrap-theme .civicase__case-list-table .case-activity-card-wrapper{max-width:320px;min-width:320px;position:relative}#bootstrap-theme .civicase__case-list-table__column--status_badge{max-width:200px;min-width:200px!important}#bootstrap-theme .civicase__case-list-table__column--status_badge .crm_notification-badge{display:block;max-width:fit-content;overflow:hidden;text-overflow:ellipsis}#bootstrap-theme .civicase__case-list-table__header.affix{display:block;left:0;margin-left:300px;overflow-x:hidden;overflow-y:visible;right:0;top:60px;z-index:10}#bootstrap-theme .civicase__case-list-table__header.affix tr{display:table;width:100%}#bootstrap-theme .civicase__case-list-table__header.affix th{border-bottom:1px solid #e8eef0;display:table-cell}#bootstrap-theme .civicase__case-list-table__header.affix th:nth-child(1){left:0;position:fixed}#bootstrap-theme .civicase__case-list{margin:0;overflow:hidden;position:relative}#bootstrap-theme .civicase__case-list .civicase__bulkactions-message .alert{border-bottom:0}#bootstrap-theme .civicase__case-list .civicase__pager--fixed{position:fixed}#bootstrap-theme .civicase__case-list .civicase__pager--viewing-case{width:300px}#bootstrap-theme .civicase__case-list .civicase__pager--viewing-case.civicase__pager--fixed{position:absolute}#bootstrap-theme .civicase__case-list .civicase__pager--case-focused{display:none}#bootstrap-theme .civicase__case-list--summary>.civicase__pager{bottom:0;height:60px;position:absolute}#bootstrap-theme .civicase__case-list--summary .civicase__case-list-table-container{overflow-x:hidden}#bootstrap-theme .civicase__case-list-panel{border-top:1px solid #d3dee2;box-shadow:none;margin-bottom:0;overflow:auto;padding:0;position:relative;transition:width .3s linear}#bootstrap-theme .civicase__case-list-panel--summary{border-top:0;bottom:60px;overflow-x:hidden;position:absolute;top:65px;width:300px}#bootstrap-theme .civicase__case-list-panel--summary thead{display:none}#bootstrap-theme .civicase__case-list-panel--summary .civicase__case-list-column--first{background:#fff!important}#bootstrap-theme .civicase__case-list-panel--summary .civicase__case-list-table td:first-child{width:100%}#bootstrap-theme .civicase__case-list-column--first--detached{background:#fff;border-bottom:1px solid #d3dee2;border-top:1px solid #d3dee2;height:65px;left:0;padding:16px 14px;position:absolute;width:300px}#bootstrap-theme .civicase__case-list-panel--focused{width:0}#bootstrap-theme .civicase__case-list-panel--focused .civicase__pager{display:none}#bootstrap-theme .civicase__case-details-panel{box-shadow:none;display:none;float:right;height:0;overflow:hidden;transition:width .3s;width:0}#bootstrap-theme .civicase__case-details-panel>.panel-body{background:0 0}#bootstrap-theme .civicase__case-details-panel .civicase__panel-empty{background:#fff;height:100%;margin:0;padding:15px 20px}#bootstrap-theme .civicase__case-details-panel--summary{display:block;height:100%;overflow-y:auto;width:calc(100% - 300px)}#bootstrap-theme .civicase__case-details-panel--focused{width:100%}#bootstrap-theme .civicase__case-list-sortable-header{cursor:pointer}#bootstrap-theme .civicase__case-list-sortable-header:hover{background-color:#e8eef0}#bootstrap-theme .civicase__case-list-sortable-header.active{background-color:#e8eef0!important}#bootstrap-theme .civicase__case-list__toggle-sort{color:#9494a5;cursor:pointer;font-size:20px;position:relative;top:7px}#bootstrap-theme .civicase__case-list__header-toggle-sort{float:right;position:relative;top:3px}#bootstrap-theme .civicase__case-sort-dropdown{box-shadow:none;display:inline-block;width:90px!important}#bootstrap-theme .civicase__case-overview .panel-body{background-color:#fafafb;padding:0!important}#bootstrap-theme .civicase__case-overview paging{padding-right:20px}#bootstrap-theme .civicase__case-overview-container a{color:inherit;text-decoration:none}#bootstrap-theme .civicase__case-overview-container .civicase__case-overview__flow,#bootstrap-theme .civicase__case-overview-container .simplebar-content{position:static}#bootstrap-theme .civicase__case-overview-container .simplebar-content{padding-right:0!important}#bootstrap-theme .civicase__case-overview__breakdown,#bootstrap-theme .civicase__case-overview__flow{display:flex;margin-left:200px}#bootstrap-theme .civicase__case-overview__breakdown-field,#bootstrap-theme .civicase__case-overview__flow-status{display:inline-flex;flex-basis:200px;flex-grow:1;flex-shrink:0}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child,#bootstrap-theme .civicase__case-overview__flow-status:first-child{align-items:flex-start;align-items:center;flex-direction:row;justify-content:flex-start;left:0;position:absolute;top:auto;width:200px;z-index:5}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child .civicase__case-overview__flow-status__icon,#bootstrap-theme .civicase__case-overview__flow-status:first-child .civicase__case-overview__flow-status__icon{color:#0071bd;cursor:pointer;margin-left:8px;position:relative;top:1px}#bootstrap-theme .civicase__case-overview__flow-status{align-items:flex-start;background-color:#fff;flex-direction:column;height:80px;justify-content:center;position:relative}#bootstrap-theme .civicase__case-overview__flow-status::after{background-color:#fff;border-bottom-right-radius:5px;box-shadow:1px 1px 0 0 #e8eef0;content:'';height:57px;position:absolute;right:-25px;top:12px;transform:rotateZ(-45deg);transform-origin:50% 50%;width:57px;z-index:1}#bootstrap-theme .civicase__case-overview__flow-status:first-child{font-size:16px;font-weight:600;line-height:22px;padding-left:24px;z-index:10}#bootstrap-theme .civicase__case-overview__flow-status:last-child{overflow:hidden}#bootstrap-theme .civicase__case-overview__flow-status:last-child::after{content:none}#bootstrap-theme .civicase__case-overview__flow-status-settings{position:relative}#bootstrap-theme .civicase__case-overview__flow-status-settings .btn{color:inherit;margin-right:10px;padding:3px 0;text-decoration:none!important}#bootstrap-theme .civicase__case-overview__flow-status-settings .civicase__case-overview__flow-status__icon--settings{color:inherit!important;margin:0!important;vertical-align:middle}#bootstrap-theme .civicase__case-overview__flow-status-settings .civicase__case-overview__flow-status__icon--settings.material-icons{color:#9494a5!important;font-size:18px}#bootstrap-theme .civicase__case-overview__flow-status__border{bottom:0;height:4px;position:absolute;transform:skewx(-45deg);width:calc(100% - 1px);z-index:2}#bootstrap-theme .civicase__case-overview__flow-status__count{color:#464354;font-size:24px;font-weight:600;line-height:33px;text-align:center;width:100%}#bootstrap-theme .civicase__case-overview__flow-status__empty-state{text-align:center;width:100%}#bootstrap-theme .civicase__case-overview__flow-status__description{color:#9494a5;margin:0 auto;overflow:hidden;text-align:center;text-overflow:ellipsis;white-space:nowrap;width:140px}#bootstrap-theme .civicase__case-overview__flow-status__description span{text-overflow:ellipsis}#bootstrap-theme .civicase__case-overview__breakdown:last-child .civicase__case-overview__breakdown-field{border:0}#bootstrap-theme .civicase__case-overview__breakdown-field{align-items:center;border-bottom:1px solid #e8eef0;justify-content:center;padding:16px 24px;position:relative}#bootstrap-theme .civicase__case-overview__breakdown-field:not(:first-child){color:#9494a5}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child::after,#bootstrap-theme .civicase__case-overview__breakdown-field:last-child::after{background-color:#fafafb;bottom:-1px;content:'';height:1px;position:absolute;width:24px}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child{background-color:#fafafb;font-weight:600}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child a{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:100%}#bootstrap-theme .civicase__case-overview__breakdown-field:first-child::after{left:0}#bootstrap-theme .civicase__case-overview__breakdown-field:last-child::after{right:0}#bootstrap-theme .civicase__case-overview__breakdown-field--hoverable:hover,#bootstrap-theme .civicase__case-overview__flow-status--hoverable:hover{background-color:#f3f6f7}#bootstrap-theme .civicase__case-overview__breakdown-field--hoverable:hover::after,#bootstrap-theme .civicase__case-overview__flow-status--hoverable:hover::after{background-color:#f3f6f7}#bootstrap-theme .civicase__case-overview__popup{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);color:#464354;padding:0;z-index:1}#bootstrap-theme .civicase__case-overview__popup.dropdown-menu{margin-top:15px;padding:8px 0}#bootstrap-theme .civicase__case-overview__popup .popover-content{padding:0}#bootstrap-theme .civicase__case-overview__popup .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__case-overview__popup .arrow.left{left:20px}#bootstrap-theme .civicase__case-overview__popup .dropdown-menu{box-shadow:none;display:inherit;position:inherit}#bootstrap-theme .civicase__case-overview__popup .civicase__checkbox{display:inline-block;margin-right:5px}#bootstrap-theme .civicase__case-overview__popup .civicase__checkbox--checked{color:#0071bd!important;font-size:24px!important;top:0!important}#bootstrap-theme .civicase__case-overview__popup .civicase__checkbox-container{line-height:18px}#bootstrap-theme .civicase__case-overview__popup .civicase__checkbox-container>*{vertical-align:middle}#bootstrap-theme .civicase__case-tab--people .nav-tabs{border-top-left-radius:2px;border-top-right-radius:2px;box-shadow:0 6px 20px 0 rgba(49,40,40,.03)}#bootstrap-theme .civicase__case-tab--people .civicase__checkbox{margin-right:16px}#bootstrap-theme .civicase__case-tab--people .civicase__checkbox .civicase__people-tab__table-checkbox{cursor:pointer;height:100%;left:0;opacity:0;position:absolute;right:0;top:0;width:100%;z-index:11}#bootstrap-theme .civicase__case-tab--people .civicase__people-tab-link{line-height:18px;padding-bottom:15px;padding-top:12px}#bootstrap-theme .civicase__case-tab--people .civicase__people-tab__table-header .civicase__people-tab__table-column{line-height:18px;padding:16px 24px}#bootstrap-theme .civicase__case-tab--people .civicase__people-tab__table-body .civicase__people-tab__table-column{padding:16px 20px}#bootstrap-theme .civicase__people-tab{background:#fff;border-radius:2px;box-shadow:0 3px 8px 0 rgba(49,40,40,.15)}#bootstrap-theme .civicase__people-tab__sub-tab .civicase__add-btn{margin-right:-6px;margin-top:-4px}#bootstrap-theme .civicase__people-tab__add-role-dropdown [disabled],#bootstrap-theme .civicase__people-tab__add-role-dropdown [disabled]:hover{color:#9494a5;cursor:not-allowed}#bootstrap-theme .civicase__people-tab__search{background:#fff;padding:20px 30px}#bootstrap-theme .civicase__people-tab__search h3{margin:0}#bootstrap-theme .civicase__people-tab__search .btn .material-icons{margin-right:5px;position:relative;top:2px}#bootstrap-theme .civicase__people-tab__search .dropdown-menu{left:auto!important;right:0;top:100%!important}#bootstrap-theme .civicase__people-tab__search .civicase__bulkactions-actions-dropdown .dropdown-menu{right:auto}#bootstrap-theme .civicase__people-tab__selection{align-items:center;display:flex;padding:16px 0}#bootstrap-theme .civicase__people-tab__selection>input{margin:0 5px 0 9px}#bootstrap-theme .civicase__people-tab__selection label{line-height:18px;margin-bottom:0;position:relative;top:1px}#bootstrap-theme .civicase__people-tab__select-box .form-control{width:240px}#bootstrap-theme .civicase__people-tab__filter{align-items:center;border-bottom:1px solid #e8eef0;border-top:1px solid #e8eef0;display:flex;justify-content:space-between;padding:10px 24px}#bootstrap-theme .civicase__people-tab__filter--role .form-control{width:160px}#bootstrap-theme .civicase__features-filters{display: flex;justify-content: space-between;}#bootstrap-theme .civicase__people-tab__filter--relations{justify-content:unset}#bootstrap-theme .civicase__people-tab__filter-alpha-pager{margin-left:20px}#bootstrap-theme .civicase__people-tab__filter-alpha-pager .civicase__people-tab__filter-alpha-pager__link{color:#464354;margin:0 3px;padding:2px}#bootstrap-theme .civicase__people-tab__filter-alpha-pager .civicase__people-tab__filter-alpha-pager__link.active,#bootstrap-theme .civicase__people-tab__filter-alpha-pager .civicase__people-tab__filter-alpha-pager__link.all{color:#0071bd;text-decoration:none}#bootstrap-theme .civicase__people-tab__filter-alpha-pager .civicase__people-tab__filter-alpha-pager__link:first-child{margin-left:0;padding-left:0}#bootstrap-theme .civicase__people-tab__table-column--first{display:flex}#bootstrap-theme .civicase__people-tab__table-column--first em{font-weight:400}#bootstrap-theme .civicase__people-tab__table-column--first input{margin:0}#bootstrap-theme .civicase__people-tab__table-column--last{padding:20px 0!important}#bootstrap-theme .civicase__people-tab__table-column--last .dropdown-menu{left:auto!important;right:20px;top:100%!important}#bootstrap-theme .civicase__people-tab__table-column--last .btn{padding:0}#bootstrap-theme .civicase__people-tab__table-column--last .open{position:relative}#bootstrap-theme .civicase__people-tab__table-column--last .open .dropdown-toggle{background:0 0!important;box-shadow:none}#bootstrap-theme .civicase__people-tab__table-column--last .material-icons{font-size:18px;padding:0}#bootstrap-theme .civicase__people-tab__inactive-filter{margin-left:auto;margin-right:30px}#bootstrap-theme .civicase__people-tab__inactive-filter .civicase__checkbox{display:inline-block;margin-right:5px;top:5px}#bootstrap-theme .civicase__people-tab__table-assign-icon{cursor:pointer}#bootstrap-theme .civicase__people-tab__table-assign-icon:hover{color:#0071bd}#bootstrap-theme .civicase__people-tab-counter{border-top:1px solid #e8eef0;line-height:18px;padding:16px 24px}#bootstrap-theme .civicase__case-filter-panel{background-color:#f3f6f7;box-shadow:none;margin-bottom:0}#bootstrap-theme .civicase__case-filter-panel .panel-header{position:relative}#bootstrap-theme .civicase__case-filter-panel__title{font-size:18px;left:20px;line-height:24px;margin:0;max-width:calc(((100% - 950px)/ 2) - 20px);overflow:hidden;position:absolute;text-overflow:ellipsis;top:50%;transform:translateY(-50%);white-space:nowrap}#bootstrap-theme .civicase__case-filters-container{display:flex;justify-content:center;left:0;padding:13.5px 0;top:0}#bootstrap-theme .civicase__case-filter__input.form-control{margin:0 8px}#bootstrap-theme .civicase__case-filter__input.form-control:not(.select2-container){width:240px}#bootstrap-theme .civicase__case-filter-panel__button{margin:0 8px;width:158px}#bootstrap-theme .civicase__case-filter-panel__button:first-child{margin-left:0}#bootstrap-theme .civicase__case-filter-panel__button .fa{font-size:18px;margin-right:7px;position:relative;top:2px}#bootstrap-theme .civicase__case-filter-form-elements-container{margin:0 auto;width:926px}#bootstrap-theme .civicase__case-filter-form-elements{clear:both;margin-bottom:10px}#bootstrap-theme .civicase__case-filter-form-elements .select2-choices{padding-right:30px}#bootstrap-theme .civicase__case-filter-form-elements .form-control{max-width:385px}#bootstrap-theme .civicase__case-filter-form-elements.civicase__case-filter-form-elements--case-id .form-control{max-width:160px}#bootstrap-theme .civicase__case-filter-form-elements label,#bootstrap-theme .civicase__case-filter-form-elements-container .civicase__checkbox__container label{color:#9494a5;font-weight:400}#bootstrap-theme .civicase__case-filter-panel__description{align-items:center;display:flex;flex-direction:row}#bootstrap-theme .civicase__filter-search-description-list-container{flex:1 0 0;margin-bottom:0}#bootstrap-theme .civicase__case-filter-form-legend{border-color:#e8eef0;color:#464354;font-size:16px;font-weight:600;line-height:22px;margin-bottom:20px;padding:0 0 8px}#bootstrap-theme .civicase__case-filter-fieldset{margin:15px 0}#bootstrap-theme .civicase__case-summary-fields:not(:first-child){margin-top:16px}#bootstrap-theme .civicase__case-summary-fields__label{color:#9494a5}#bootstrap-theme .civicase__case-summary-fields__value{color:#464354;word-break:break-all}#bootstrap-theme .civicase__case-tab__container{padding:24px 30px}#bootstrap-theme .civicase__case-tab__actions{margin-bottom:16px}#bootstrap-theme .civicase__case-tab__empty{color:#464354;font-weight:600;margin-top:40px;opacity:.65}#bootstrap-theme .civicase__checkbox{background-color:#fff;border:1px solid #c2cfd8;border-radius:2px;box-shadow:0 6px 20px 0 rgba(49,40,40,.03);box-sizing:border-box;cursor:pointer;display:inline-block;height:18px;margin-right:5px;position:relative;width:18px}#bootstrap-theme .civicase__checkbox__container .control-label{position:relative;top:-4px}#bootstrap-theme .civicase__checkbox--checked{color:#c2cfd8;font-size:24px;left:0;margin-left:-4px;margin-top:-4px;position:absolute;top:0;transition:.2s all cubic-bezier(0,0,0,.4);z-index:10}#bootstrap-theme .civicase__animated-checkbox-card{position:relative;transition:.1s padding-left cubic-bezier(0,0,0,.4);transition-delay:.1s}#bootstrap-theme .civicase__animated-checkbox-card .civicase__checkbox--bulk-action{cursor:pointer;left:14px;opacity:0;outline:0;position:absolute;top:15px;transform:scale(.3);transition:.1s all cubic-bezier(0,0,0,.4);transition-delay:unset}#bootstrap-theme .civicase__animated-checkbox-card--expanded{padding-left:30px;transition-delay:0}#bootstrap-theme .civicase__animated-checkbox-card--expanded .civicase__checkbox--bulk-action{opacity:1;transform:scale(1);transition-delay:.1s}.civicase__contact-activity-tab__add .select2-container .select2-choice{background:#4d4d69;border:0;box-shadow:none;height:auto;line-height:initial;padding:7px 19px;width:155px!important}.civicase__contact-activity-tab__add .select2-container .select2-chosen{color:#fff!important;margin:0;text-transform:uppercase}.civicase__contact-activity-tab__add .select2-container .select2-arrow{background:0 0!important;border:0;line-height:34px}.civicase__contact-activity-tab__add .select2-container .select2-arrow::before{color:#fff!important}#bootstrap-theme .civicase__contact-card{color:#0071bd;display:flex}#bootstrap-theme .civicase__contact-name-container{display:flex}#bootstrap-theme .civicase__contact-name,#bootstrap-theme .civicase__contact-name-additional{color:inherit;margin-left:5px;max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__contact-icon,#bootstrap-theme .civicase__contact-icon-additional{color:#c2cfd8;font-size:18px;margin-top:2px}#bootstrap-theme .civicase__contact-icon-additional.civicase__contact-icon--highlighted,#bootstrap-theme .civicase__contact-icon-additional:hover,#bootstrap-theme .civicase__contact-icon.civicase__contact-icon--highlighted,#bootstrap-theme .civicase__contact-icon:hover{color:#0071bd;cursor:pointer}#bootstrap-theme .civicase__contact-icon .material-icons,#bootstrap-theme .civicase__contact-icon-additional .material-icons{line-height:inherit}#bootstrap-theme .civicase__contact-additional__container{margin-left:8px}#bootstrap-theme .civicase__contact-additional__container,#bootstrap-theme .civicase__tooltip-popup-list--additional-contacts{color:#0071bd;padding-left:0!important;padding-right:0!important}#bootstrap-theme .civicase__contact-additional__container .civicase__contact-icon,#bootstrap-theme .civicase__tooltip-popup-list--additional-contacts .civicase__contact-icon{font-size:18px}#bootstrap-theme .civicase__contact-additional__container .civicase__contact-name-additional,#bootstrap-theme .civicase__tooltip-popup-list--additional-contacts .civicase__contact-name-additional{color:#0071bd}#bootstrap-theme .civicase__contact-additional__popover{border:1px solid #e8eef0;border-radius:0;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);cursor:pointer}#bootstrap-theme .civicase__contact-additional__popover a{display:flex!important;line-height:22px}#bootstrap-theme .civicase__contact-additional__popover .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__contact-additional__popover .popover-content{padding:0}#bootstrap-theme .civicase__contact-additional__list{margin:0;padding:0}#bootstrap-theme .civicase__contact-additional__list li{height:34px;list-style:none;padding:7px 20px}#bootstrap-theme .civicase__contact-additional__list li:hover{background:#f3f6f7}#bootstrap-theme .civicase__contact-additional__list li .civicase__contact-icon{vertical-align:middle}#bootstrap-theme .civicase__contact-additional__list li a{text-decoration:none}#bootstrap-theme .civicase__contact-additional__hidden_contacts_info{color:#9494a5;font-size:12px}#bootstrap-theme .civicase__contact-avatar{background:#99c6e5;min-width:30px;padding:5px;position:relative}#bootstrap-theme .civicase__contact-additional__container--avatar{background:#99c6e5;margin-left:0;margin-top:-10px;min-width:30px;padding:5px}#bootstrap-theme .civicase__contact-avatar--image{background:0 0;padding:0;position:relative;z-index:1}#bootstrap-theme .civicase__contact-avatar--image img{border-radius:2px;height:25px;width:25px}#bootstrap-theme .civicase__contact-avatar__full-name{background:#99c6e5;border-radius:1px;display:none;height:30px;left:0;padding:5px 10px;position:absolute;top:0;width:auto}#bootstrap-theme .civicase__contact-avatar--image .civicase__contact-avatar__full-name{border-bottom-left-radius:0;border-top-left-radius:0}#bootstrap-theme .civicase__contact-avatar--has-full-name:hover{opacity:1}#bootstrap-theme .civicase__contact-avatar--has-full-name:hover .civicase__contact-avatar__full-name{display:block}#bootstrap-theme .civicase__contact-avatar--has-full-name:hover.civicase__contact-avatar--image .civicase__contact-avatar__full-name{left:29px;padding-left:11px;z-index:0}#bootstrap-theme .civicase__contact-card__with-more-fields{flex-wrap:wrap}#bootstrap-theme .civicase__contact-card__with-more-fields .civicase__contact__more-field{color:#464354}#bootstrap-theme .civicase__contact-card__with-more-fields .civicase__contact__break{flex-basis:100%;height:0}#bootstrap-theme .civicase__contact-card__with-more-fields .civicase__contact-name{max-width:initial}#bootstrap-theme .civicase__contact-additional__list__with-more-fields{max-height:300px;overflow-y:auto}#bootstrap-theme .civicase__contact-additional__list__with-more-fields li{height:auto}#bootstrap-theme .civicase__contact-additional__list__with-more-fields li:not(:first-of-type){border-top:1px solid #e8eef0}#bootstrap-theme .civicase__contact-additional__list__with-more-fields .civicase__contact-name-additional{max-width:initial}#bootstrap-theme .civicase__contact-additional__list__with-more-fields .civicase__contact__more-field{margin-top:5px}#bootstrap-theme .civicase__contact-cases-tab{margin-left:-5px}#bootstrap-theme .civicase__contact-cases-tab-container{padding-left:15px;padding-right:15px}#bootstrap-theme .civicase__contact-cases-tab-container .civicase__panel-transparent-header>.panel-heading .panel-title{font-size:18px;margin:0;padding:15px 0}#bootstrap-theme .civicase__contact-cases-tab-container .civicase__panel-transparent-header>.panel-body{background:0 0;box-shadow:none;padding:0}#bootstrap-theme .civicase__contact-case-tab__case-list__footer{margin-top:24px}#bootstrap-theme .civicase__contact-case-tab__case-list__footer .btn{box-shadow:0 3px 8px 0 rgba(49,40,40,.15)}#bootstrap-theme .civicase__contact-cases-tab-empty{align-items:center;display:flex;flex-direction:column;padding:50px 0}#bootstrap-theme .civicase__contact-cases-tab-empty a{color:#4d4d69}#bootstrap-theme .civicase__contact-cases-tab-add{background:#4d4d69;margin-bottom:10px}#bootstrap-theme .civicase__contact-cases-tab-add .material-icons{margin-right:5px;position:relative;top:2px}#bootstrap-theme .civicase__contact-cases-tab-details{box-shadow:0 3px 8px 0 rgba(49,40,40,.15);margin-top:calc((1.1 * 18px) + 2 * 15px)}#bootstrap-theme .civicase__contact-cases-tab-details>.panel-body{padding:0}#bootstrap-theme .civicase__contact-cases-tab-details .btn-group{margin-right:5px}#bootstrap-theme .civicase__contact-cases-tab-details .btn-group:last-child{margin-right:0}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__tags-container{max-width:80%}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__activity-card{width:100%}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__summary-tab__subject{margin-bottom:3px;margin-left:-2px;margin-top:20px}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__summary-tab__description{margin-bottom:5px}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-card--client{position:relative;top:8px}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-card--client .material-icons,#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-card--manager .material-icons{line-height:1}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-card--manager{position:relative;top:2px}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-cases-tab__status-label{display:inline-block;max-width:250px;overflow:hidden;position:relative;text-overflow:ellipsis;top:3px;white-space:nowrap}#bootstrap-theme .civicase__contact-cases-tab-details .civicase__contact-cases-tab__case-link .fa{font-size:17px;margin-right:5px;position:relative;top:1px}#bootstrap-theme .civicase__contact-cases-tab-details .list-group-item-info{color:#9494a5;display:block;padding:7px 19px 7px 24px}#bootstrap-theme .civicase__contact-cases-tab-details__title{margin:6.5px 0}#bootstrap-theme .civicase__contact-cases-tab__panel-row{border-bottom:1px solid #e8eef0;padding:15px 24px}#bootstrap-theme .civicase__contact-cases-tab__panel-row:last-child{border-bottom:0}#bootstrap-theme .civicase__contact-cases-tab__panel-row .civicase__summary-tab__subject textarea{min-height:65px}#bootstrap-theme .civicase__contact-cases-tab__panel-row .civicase__summary-tab__description textarea{min-height:85px}#bootstrap-theme .civicase__contact-cases-tab__panel-actions{padding:20px}#bootstrap-theme .civicase__contact-cases-tab__panel-row--dark{background-color:#f3f6f7}#bootstrap-theme .civicase__contact-cases-tab__panel-row--dark .civicase__pipe{color:#e8eef0;margin:0 8px}#bootstrap-theme .civicase__contact-cases-tab__panel-fields{padding-bottom:15px}#bootstrap-theme .civicase__contact-cases-tab__panel-fields--inline{align-items:center;display:flex}#bootstrap-theme .civicase__contact-cases-tab__panel-field-emphasis{color:#9494a5}#bootstrap-theme .civicase__contact-cases-tab__panel-field-title{color:#9494a5;margin-bottom:5px}.crm-contact-page #ui-id-5{padding:30px;width:calc(100% - 200px)}@media (max-width:1400px){#bootstrap-theme .civicase__contact-card--client{clear:both}}.contact-popover-container{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);color:#464354;max-width:90vw;padding:20px;width:708px}.contact-popover-container .popover-content{padding:0}.contact-popover-container.bottom>.arrow{border-bottom-color:#e8eef0}.contact-popover-container.top>.arrow{border-top-color:#e8eef0}.civicase__contact-popover__header h2{line-height:24px;margin:0}.civicase__contact-popover__header hr{background-color:#e8eef0;margin:16px 0}.civicase__contact-popover__column{float:left;width:46%}.civicase__contact-popover__column+.civicase__contact-popover__column{width:54%}.civicase__contact-popover__detail-group{float:left;margin-bottom:10px;width:100%}.civicase__contact-popover__detail-header,.civicase__contact-popover__detail-value{color:#4d4d69;float:left;line-height:18px;overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap;width:55%}.civicase__contact-popover__detail-header strong,.civicase__contact-popover__detail-value strong{color:#464354;font-weight:600}.civicase__contact-popover__detail-header{clear:both;width:45%}#bootstrap-theme .civicrm__contact-prompt-dialog textarea{width:100%}#bootstrap-theme .civicrm__contact-prompt-dialog__date-error.crm-error{background:#fbe3e4}#bootstrap-theme .civicase__crm-dashboard__tabs{position:relative;width:100%;z-index:1}#bootstrap-theme .civicase__crm-dashboard__tabs.affix{position:fixed;z-index:11}#bootstrap-theme .civicase__crm-dashboard__myactivities-tab{padding:0}#bootstrap-theme .civicase__dashboard__tab{background:#e8eef0}#bootstrap-theme .civicase__dashboard__tab>.civicase__dashboard__tab__top{margin-bottom:30px}#bootstrap-theme .civicase__dashboard__tab .panel-secondary{margin-bottom:30px}#bootstrap-theme .civicase__dashboard__tab__col{padding:0 15px}#bootstrap-theme .civicase__dashboard__tab__col-wrapper{display:flex;flex-direction:column;margin:0 -15px}#bootstrap-theme .civicase__dashboard__tab__col-wrapper>.civicase__dashboard__tab__col{margin-bottom:30px}#bootstrap-theme .civicase__dashboard__tab__main{margin:auto;max-width:1081px;padding:0 30px}@media (min-width:992px){#bootstrap-theme .civicase__dashboard__tab__col--left{flex-basis:330px;flex-grow:0;flex-shrink:0}#bootstrap-theme .civicase__dashboard__tab__col--right{flex-grow:1}#bootstrap-theme .civicase__dashboard__tab__col-wrapper{flex-direction:row}#bootstrap-theme .civicase__dashboard__tab__col-wrapper>.civicase__dashboard__tab__col{margin-bottom:0}}@media (min-width:1200px){#bootstrap-theme .civicase__dashboard__tab__main{padding:0}}#bootstrap-theme .civicase__dashboard .tab-pane{padding:0}#bootstrap-theme .civicase__dashboard__tab-container .nav{width:100%;z-index:10}#bootstrap-theme .civicase__dashboard__tab-container .nav.affix-top{position:relative}#bootstrap-theme .civicase__dashboard__tab-container .nav.affix{position:fixed}#bootstrap-theme .civicase__dashboard-activites-feed{background:#e8eef0}#bootstrap-theme .civicase__dashboard__action-btn{background:#0071bd;border:1px solid #fff;border-radius:2px;color:#fff;line-height:20px;margin-right:15px;margin-top:7px;padding:6px 16px}#bootstrap-theme .civicase__dashboard__action-btn .material-icons{font-size:16px;margin-right:5px;position:relative;top:2px}#bootstrap-theme .civicase__dashboard__action-btn--light{background:#fff;color:#0071bd}#bootstrap-theme .civicase__dashboard__relation-filter{display:inline-block;margin-right:10px;margin-top:7px;width:190px}#bootstrap-theme .civicase__dashboard__relation-filter .select2-choice{color:#9494a5}#bootstrap-theme .civicase__ui-range>span{display:inline-block;margin-right:16px}#bootstrap-theme .civicase__ui-range .crm-form-date{display:inline-block;width:134px}#bootstrap-theme .civicase__ui-range .crm-form-date-wrapper{display:inline-block;position:relative}#bootstrap-theme .civicase__ui-range .crm-clear-link{position:absolute;right:-20px;top:50%;transform:translateY(-50%)}#bootstrap-theme .civicase__activity-panel__core_container--draft [title='File On Case']{display:none}.crm-container.ui-dialog .ui-dialog-content.civicase__email-role-selector{height:220px!important}#bootstrap-theme .civicase__file-upload-container{margin:0 -12px}#bootstrap-theme .civicase__file-upload-item{padding:0 12px}#bootstrap-theme .civicase__file-upload-dropzone{align-items:center;border:1px dashed #c2cfd8;border-radius:3px;display:flex;flex-direction:column;height:330px;justify-content:center;padding:20px;width:100%}#bootstrap-theme .civicase__file-upload-dropzone:hover{background-color:#fff}#bootstrap-theme .civicase__file-upload-dropzone .material-icons{color:#bfcfd9;font-size:48px}#bootstrap-theme .civicase__file-upload-dropzone h3{font-size:14px;line-height:18px;margin:10px 0 0}#bootstrap-theme .civicase__file-upload-dropzone label{color:#0071bd;cursor:pointer;font-weight:400}#bootstrap-theme .civicase__file-upload-button{display:none!important}#bootstrap-theme .civicase__file-upload-box{transition:.25s width cubic-bezier(0,0,0,.4);width:100%}#bootstrap-theme .civicase__file-upload-details{opacity:0;overflow:hidden;padding:0;transition:.1s opacity cubic-bezier(0 0,0,.4);transition-delay:.3s;width:0}#bootstrap-theme .civicase__file-upload-details label{color:#9494a5;font-weight:400;line-height:18px;margin-bottom:5px}#bootstrap-theme .civicase__file-upload-details .btn{margin-right:8px;padding:8px 16px}#bootstrap-theme .civicase__file-upload-details .btn:last-child{margin-right:0}#bootstrap-theme .civicase__file-upload-details .btn-default{border-color:inherit}#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector{margin-bottom:25px;margin-top:-10px}#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .col-sm-5,#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .col-xs-7{float:none}#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .col-sm-5,#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .col-xs-7,#bootstrap-theme .civicase__file-upload-details .civicase__tags-selector .select2-container{margin-bottom:5px;padding:0;width:100%!important}#bootstrap-theme .civicase__file-upload-container--upload-active .civicase__file-upload-box{width:50%}#bootstrap-theme .civicase__file-upload-container--upload-active .civicase__file-upload-details{opacity:1;overflow:visible;padding:0 12px;width:50%}#bootstrap-theme .civicase__file-upload-name,#bootstrap-theme .civicase__file-upload-remove,#bootstrap-theme .civicase__file-upload-size{font-size:13px;line-height:18px;margin:0}#bootstrap-theme .civicase__file-upload-name{color:#0071bd}#bootstrap-theme .civicase__file-upload-description{margin-bottom:24px}#bootstrap-theme .civicase__file-upload-description textarea{min-height:90px}#bootstrap-theme .civicase__file-upload-remove{text-align:right}#bootstrap-theme .civicase__file-upload-remove .btn{border:0;color:#cf3458;padding:0;text-transform:capitalize}#bootstrap-theme .civicase__file-upload-remove .btn:hover{background-color:transparent}#bootstrap-theme .civicase__file-upload-progress{margin:12px 0 16px}#bootstrap-theme .civicase__icon{transform:translateY(14%) rotate(.03deg)}#bootstrap-theme .civicase-inline-datepicker__wrapper{margin-left:-11px;margin-top:-5px}#bootstrap-theme .civicase-inline-datepicker__wrapper [civicase-inline-datepicker]{border:1px solid transparent;border-radius:2px;height:30px;padding:4px 10px;width:140px!important}#bootstrap-theme .civicase-inline-datepicker__wrapper .form-control{box-shadow:none;display:inline-block}#bootstrap-theme .civicase-inline-datepicker__wrapper .form-control.ng-invalid{background-color:#fbe3e4}#bootstrap-theme .civicase-inline-datepicker__wrapper .addon{margin-top:-3px!important;opacity:0;transition:opacity .15s}#bootstrap-theme .civicase-inline-datepicker__wrapper:active [civicase-inline-datepicker],#bootstrap-theme .civicase-inline-datepicker__wrapper:focus [civicase-inline-datepicker],#bootstrap-theme .civicase-inline-datepicker__wrapper:hover [civicase-inline-datepicker]{border:1px solid #c2cfd8}#bootstrap-theme .civicase-inline-datepicker__wrapper:active .form-control,#bootstrap-theme .civicase-inline-datepicker__wrapper:focus .form-control,#bootstrap-theme .civicase-inline-datepicker__wrapper:hover .form-control{box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}#bootstrap-theme .civicase-inline-datepicker__wrapper:active .addon,#bootstrap-theme .civicase-inline-datepicker__wrapper:focus .addon,#bootstrap-theme .civicase-inline-datepicker__wrapper:hover .addon{opacity:1}#bootstrap-theme .civicase-inline-datepicker__wrapper .civicase__inline-datepicker--open{border:1px solid #c2cfd8;box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}#bootstrap-theme .civicase-inline-datepicker__wrapper [civicase-inline-datepicker]:active+.addon,#bootstrap-theme .civicase-inline-datepicker__wrapper [civicase-inline-datepicker]:focus+.addon{opacity:1}#bootstrap-theme .civicase__loading-placeholder__icon{color:#edf3f5;display:inline-block;font-size:10px;left:-14px;position:relative;top:-1px;width:10px}#bootstrap-theme .civicase__loading-placeholder__activity-card{border:1px solid #edf3f5;border-radius:4px;height:90px;margin-bottom:10px;position:relative;width:280px}#bootstrap-theme .civicase__loading-placeholder__activity-card::before{background-color:#edf3f5;border:.4em solid #fff;border-radius:1.5em;content:' ';font-size:1.5em;height:2.7em;left:1em;padding-top:.2em;position:absolute;text-align:center;top:.4em;width:2.7em}#bootstrap-theme .civicase__loading-placeholder__activity-card::after{background-color:#edf3f5;content:' ';height:30px;position:absolute;right:20px;top:20px;width:12px}#bootstrap-theme .civicase__loading-placeholder__activity-card div{margin-left:90px;margin-right:90px}#bootstrap-theme .civicase__loading-placeholder__activity-card div::before{background-color:#edf3f5;content:' ';display:block;height:10px;margin-top:23px}#bootstrap-theme .civicase__loading-placeholder__activity-card div::after{background-color:#edf3f5;content:' ';display:block;height:10px;margin-top:23px}#bootstrap-theme .civicase__loading-placeholder__oneline::before{background-color:#edf3f5;content:' ';display:block;height:1em}#bootstrap-theme .civicase__loading-placeholder__date{background-color:#edf3f5}#bootstrap-theme .civicase__loading-placeholder__date::before{border-left-color:#d6e4e8;border-top-color:#d6e4e8}#bootstrap-theme .civicase__loading-placeholder--big::before{height:1.5em}#bootstrap-theme .panel-header .civicase__loading-placeholder__oneline::before{background-color:#edf3f5}#bootstrap-theme .civicase__loading-placeholder__oneline-strip{border-left-color:#edf3f5!important}#bootstrap-theme civicase-masonry-grid{width:100%}#bootstrap-theme civicase-masonry-grid .civicase__masonry-grid__column{float:left;width:50%}#bootstrap-theme civicase-masonry-grid-item{display:block}#bootstrap-theme .civicase__pager{background:#fafafb;border-radius:0 0 3px 3px;border-top:1px solid #e8eef0;padding:18px 15px;position:relative;z-index:10}#bootstrap-theme .civicase__pager .disabled{display:none}#bootstrap-theme .civicase__pager [title='First Page'] a,#bootstrap-theme .civicase__pager [title='Last Page'] a,#bootstrap-theme .civicase__pager [title='Next Page'] a,#bootstrap-theme .civicase__pager [title='Previous Page'] a{font-size:16px;font-weight:400;top:-3px}#bootstrap-theme .civicase__pager--fixed{bottom:0;left:0;position:fixed;width:100%}#bootstrap-theme .panel-query>.panel-body{transition:opacity .2s linear}#bootstrap-theme .panel-query.is-loading-page>.panel-body{opacity:.7}#bootstrap-theme .panel-query .civicase__activity-card--empty{text-align:center}#bootstrap-theme .panel-query .civicase__activity-card--big--empty-description{margin-bottom:0}#bootstrap-theme .panel-query .civicase__activity-no-result-icon{display:inline-block}#bootstrap-theme .panel-secondary{box-shadow:0 3px 8px 0 rgba(49,40,40,.15)}#bootstrap-theme .panel-secondary>.panel-body{padding:24px}#bootstrap-theme .panel-secondary>.panel-footer{padding:16px 24px}#bootstrap-theme .panel-secondary>.panel-heading{background:#fff;line-height:1;padding:24px;position:relative}#bootstrap-theme .panel-secondary>.panel-heading::after{border-bottom:1px solid #e8eef0;bottom:0;content:'';display:block;height:0;left:16px;position:absolute;width:calc(100% - 32px)}#bootstrap-theme .panel-secondary>.panel-heading .panel-title{font-size:16px}#bootstrap-theme .panel-secondary .panel-heading-control{display:block;margin-left:0;margin-top:-13px;position:relative;top:6px}#bootstrap-theme+.panel-secondary .panel-heading-control{margin-left:10px}#bootstrap-theme .panel-secondary a.panel-heading-control{line-height:30px}#bootstrap-theme .panel-secondary .panel-title+.panel-heading-control{margin-left:10px}#bootstrap-theme .panel-secondary .civicase__activity-card--long,#bootstrap-theme .panel-secondary .civicase__case-card--detached{box-shadow:0 3px 12px 0 rgba(49,40,40,.14);margin-bottom:0}#bootstrap-theme .panel-secondary .civicase__activity-card--long:not(:last-child),#bootstrap-theme .panel-secondary .civicase__case-card--detached:not(:last-child){margin-bottom:15px}#bootstrap-theme .civicase__panel-transparent-header{box-shadow:none}#bootstrap-theme .civicase__panel-transparent-header>.panel-heading{background:0 0;padding:0}#bootstrap-theme .civicase__panel-transparent-header>.panel-heading .panel-title{font-size:16px;margin:3px 0 12px}#bootstrap-theme .civicase__panel-transparent-header>.panel-heading h3::before{box-shadow:none}#bootstrap-theme .civicase__panel-transparent-header>.panel-heading .civicase__pipe{color:#c2cfd8;font-weight:400;margin:0 5px}#bootstrap-theme .civicase__panel-transparent-header>.panel-body{background:#fff;border-radius:2px;box-shadow:0 3px 8px 0 rgba(49,40,40,.15);padding:24px;position:relative}#bootstrap-theme civicase-popover{display:inline-block}#bootstrap-theme .civicase__popover-box{display:block}#bootstrap-theme civicase-popover-toggle-button{cursor:pointer}.civicase__tooltip-popup-list{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);color:#464354;padding:8px 12px}.civicase__tooltip-popup-list .popover-content{padding:0}.civicase__tooltip-popup-list .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__responsive-calendar tbody,#bootstrap-theme .civicase__responsive-calendar thead{display:block}#bootstrap-theme .civicase__responsive-calendar tr{display:flex}#bootstrap-theme .civicase__responsive-calendar td,#bootstrap-theme .civicase__responsive-calendar th{padding:0;width:100%}#bootstrap-theme .civicase__show-more-button{cursor:pointer}#bootstrap-theme .civicase__summary-tab__basic-details{background:#fff}#bootstrap-theme .civicase__summary-tab__basic-details .panel-body{display:flex;flex-direction:row;padding:20px}#bootstrap-theme .civicase__summary-tab__subject-container{flex-basis:56%;padding-right:15px}#bootstrap-theme .civicase__summary-tab__subject{font-size:20px;font-weight:600;line-height:27px;margin:0 0 13px}#bootstrap-theme .civicase__summary-tab__description{color:#9494a5;line-height:18px}#bootstrap-theme .civicase__summary-tab__last-updated{margin:13px 0 0;padding:2px 4px}#bootstrap-theme .civicase__summary-tab__last-updated__label{color:#9494a5;line-height:18px}#bootstrap-theme .civicase__summary-activity-count{align-items:center;border-left:1px solid #e8eef0;color:#464354;display:table-cell;font-weight:600;justify-content:center;text-align:center;vertical-align:top;width:20%}#bootstrap-theme .civicase__summary-activity-count a,#bootstrap-theme .civicase__summary-activity-count a:hover{color:#464354;text-decoration:none}#bootstrap-theme .civicase__summary-activity-count a{display:block;height:100%;margin:0 10px}#bootstrap-theme .civicase__summary-activity-count a:hover{background:#f3f6f7;border-radius:5px}#bootstrap-theme .civicase__summary-activity-count .civicase__summary-activity-count__number{font-size:50px;line-height:76px}#bootstrap-theme .civicase__summary-activity-count .civicase__summary-activity-count__description{color:#9494a5;font-size:15px;line-height:22px}#bootstrap-theme .civicase__summary-overdue-count{line-height:18px}#bootstrap-theme .civicase__summary-tab-tile{margin-bottom:15px;padding:15px}#bootstrap-theme .civicase__summary-tab-tile>.panel{margin-bottom:0}#bootstrap-theme .civicase__summary-tab-tile>.panel-body{border-radius:5px;border-top:0!important;padding:0}#bootstrap-theme .civicase__summary-tab-tile .civicase__panel-transparent-header>.panel-body{border-radius:5px}#bootstrap-theme .civicase__summary-tab-tile-container{display:flex;padding:0 15px;width:100%}#bootstrap-theme .civicase__summary-tab-tile--fixed{width:350px}#bootstrap-theme .civicase__summary-tab-tile--responsive{flex-basis:calc(50% - 150px);flex-grow:1;min-width:0}#bootstrap-theme .civicase__summary-tab__activity-list>.panel-heading .civicase__case-card__activity-count:nth-child(2){margin-left:10px}#bootstrap-theme .civicase__summary-tab__activity-list>.panel-body{background:0 0;box-shadow:none}#bootstrap-theme .civicase__summary-tab__activity-list .civicase__activity-card{margin-bottom:15px}#bootstrap-theme .civicase__summary-tab__other-cases{margin-left:25px;margin-right:25px}#bootstrap-theme .civicase__summary-tab__other-cases>.panel-collapse>.panel-body{padding:0}#bootstrap-theme .civicase__summary-tab__other-cases .panel-footer{border-top:0;padding:0}#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager{border-top:0}#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager .pagination>li>a{color:#9494a5;font-weight:400}#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager .pagination>li>a::after,#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager .pagination>li>a::before{display:none}#bootstrap-theme .civicase__summary-tab__other-cases .civicase__pager .pagination>li[title^=Page].active>a{color:#464354}#crm-container .civicase__tabs{background-color:#fff;padding:0 0 0 20px}#crm-container .civicase__tabs .ui-tab{background:0 0;border:0;padding:0}#crm-container .civicase__tabs .ui-tab .ui-tabs-anchor{height:auto;padding:15px 20px!important}#crm-container .civicase__tabs__panel{padding:15px 20px}#bootstrap-theme .civicase__tags-container .badge,#bootstrap-theme .civicase__tags-container__additional__list .badge{margin-right:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__tags-container{display:inline-block}#bootstrap-theme .civicase__tags-container .badge{max-width:36ch}#bootstrap-theme .civicase__tags-container__additional__list{margin:0;padding:0}#bootstrap-theme .civicase__tags-container__additional__list li{list-style:none;padding:5px 10px}#bootstrap-theme .civicase__tags-container__additional__list li:hover{background:#f3f6f7}#bootstrap-theme .civicase__tags-container__additional__list li a{text-decoration:none}#bootstrap-theme .civicase__tags-container__additional__list .badge{max-width:36ch}#bootstrap-theme .civicase__tags-container__additional-tags{background-color:#9494a5;display:inline;padding:0 8px}#bootstrap-theme .civicase__tags-container__additional__popover{border:1px solid #e8eef0;border-radius:0;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);cursor:pointer;max-width:calc(36ch + 20px + 4px);padding:0}#bootstrap-theme .civicase__tags-container__additional__popover a{display:flex!important;line-height:22px}#bootstrap-theme .civicase__tags-container__additional__popover .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__tags-container__additional__popover .popover-content{background:#fff;padding:0}.civicase__tags-modal{background-color:#fff!important}#bootstrap-theme .civicase__tags-modal__generic-tags-container{border:1px solid #d3dee2;border-radius:2px;padding:5px}#bootstrap-theme .civicase__tags-modal__generic-tags-container input{margin-top:0!important}#bootstrap-theme .civicase__tags-modal__generic-tags-container label{margin-bottom:0}#bootstrap-theme .civicase__tags-modal__tags-container label{margin-top:4px}#bootstrap-theme .civicase__tags-modal__tags-container .col-sm-9{width:75%!important}.civicase__tags-selector__item-color{margin-right:2px;position:relative;top:1px}#bootstrap-theme .civicase__tooltip__popup{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);padding:0;z-index:5}#bootstrap-theme .civicase__tooltip__popup .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase__tooltip__popup .arrow.left{left:20px}#bootstrap-theme .civicase__tooltip__ellipsis{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#bootstrap-theme .civicase__pipe{position:relative;top:-1px}#bootstrap-theme .civicase__text-warning{color:#e6ab5e}#bootstrap-theme .civicase__text-success{color:#44cb7e}#bootstrap-theme .civicase__link-disabled{cursor:no-drop;pointer-events:none}#bootstrap-theme .civicase__spinner{animation:spin 1.5s linear infinite;background:url(../resources/icons/spinner.svg) no-repeat center center!important;display:block;height:32px;margin:auto;width:32px}#bootstrap-theme .civicase__tooltip-popup-list{border:1px solid #e8eef0;border-radius:2px;box-shadow:0 2px 4px 0 rgba(49,40,40,.13);color:#464354;padding:8px 12px}#bootstrap-theme .civicase__tooltip-popup-list .popover-content{padding:0}#bootstrap-theme .civicase__tooltip-popup-list .arrow{border-bottom-color:#e8eef0}#bootstrap-theme .civicase-workflow-list>.panel-body{padding:0}#bootstrap-theme .civicase-workflow-list__new-button{margin-bottom:20px}#bootstrap-theme .civicase-workflow-list__new-button .material-icons{position:relative;top:2px}#bootstrap-theme .civicase-workflow-list_duplicate-form .ng-invalid:not(.ng-untouched){border-color:#cf3458}#bootstrap-theme .civicase-workflow-list_duplicate-form textarea{width:100%}#bootstrap-theme .civicase-workflow-list__filters .form-group{margin-bottom:0}#bootstrap-theme .civicase-workflow-list__filters .form-group [type=checkbox]{margin:0}#bootstrap-theme .civicase-workflow-list__filters .form-group>div{display:inline-block;margin-bottom:20px}#bootstrap-theme .civicase-workflow-list__filters .form-group>div:nth-child(odd){margin-right:10px;width:calc(50% - 10px)}#bootstrap-theme .civicase-workflow-list__filters .form-group>div:nth-child(even){margin-left:10px;width:calc(50% - 10px)}#bootstrap-theme .civicase-workflow-list__filters .form-group .select2-container{width:240px!important}#civicaseActivitiesTab{margin-left:-10px;margin-right:-10px}#civicaseActivitiesTab .civicase__activity-feed>.panel-body{padding-left:0;padding-right:0}#civicaseActivitiesTab .civicase__activity-filter{padding:16px 0}#civicaseActivitiesTab .civicase__activity-filter.affix{width:calc(100% - 240px)}.page-civicrm-contact-view:not([class*=page-civicrm-contact-view-]) #crm-container .civicase__activity-panel__core_container .crm-submit-buttons{margin-bottom:0!important}.page-civicrm-case-a #page{margin:0;padding-top:0}.page-civicrm-case-a .block-civicrm>h2{margin:0}.page-civicrm-case-a #branding{padding:16px 0!important}.page-civicrm-case-a #branding .breadcrumb{left:0;line-height:18px;padding:0 20px;top:0}.page-civicrm-case-a #branding .breadcrumb>a{left:0}@media (max-width:1455px){.page-civicrm-case-a .crm-contactEmail-form-block-subject .crm-token-selector{margin-top:5px}}.page-civicrm-dashboard #page,.page-civicrm:not([class*=' page-civicrm-']) #page{margin:0;padding-top:0}.page-civicrm-dashboard .block-civicrm>h2,.page-civicrm:not([class*=' page-civicrm-']) .block-civicrm>h2{margin:0}.page-civicrm-dashboard #branding,.page-civicrm:not([class*=' page-civicrm-']) #branding{padding:16px 0!important}.page-civicrm-dashboard #branding .breadcrumb,.page-civicrm:not([class*=' page-civicrm-']) #branding .breadcrumb{left:0;line-height:18px;padding:0 20px;top:0}.page-civicrm-dashboard #branding .breadcrumb>a,.page-civicrm:not([class*=' page-civicrm-']) #branding .breadcrumb>a{left:0}.page-civicrm-dashboard .civicase__tabs.affix,.page-civicrm:not([class*=' page-civicrm-']) .civicase__tabs.affix{position:fixed;top:0;width:100%;z-index:9999}.civicase__crm-dashboard .tab-content,.civicase__crm-dashboard.ui-tabs{background:0 0}#civicaseMyActivitiesTab{margin-left:-10px;margin-right:-10px}#civicaseMyActivitiesTab [crm-page-title]{display:none}#civicaseMyActivitiesTab .civicase__activity-feed>.panel-body{padding-left:0;padding-right:0}#civicaseMyActivitiesTab .civicase__activity-filter{padding:16px 0}#civicaseMyActivitiesTab .civicase__activity-filter.affix{width:calc(100% - 240px)} /*# sourceMappingURL=civicase.min.css.map */ diff --git a/scss/components/_case-features.scss b/scss/components/_case-features.scss new file mode 100644 index 000000000..5690ee6c4 --- /dev/null +++ b/scss/components/_case-features.scss @@ -0,0 +1,4 @@ +.civicase__features-filters { + display: flex; + justify-content: space-between; +} From aad803f27570277bd1f2a9fe9ad2c4285a3bf118 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 2 Mar 2023 10:40:47 +0100 Subject: [PATCH 032/199] BTHAB-24: Add method to get case dashboard link --- .../services/case-utils.service.js | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 ang/civicase-features/services/case-utils.service.js diff --git a/ang/civicase-features/services/case-utils.service.js b/ang/civicase-features/services/case-utils.service.js new file mode 100644 index 000000000..bc058240e --- /dev/null +++ b/ang/civicase-features/services/case-utils.service.js @@ -0,0 +1,26 @@ +(function (angular, $, _, CRM) { + var module = angular.module('civicase-features'); + + module.service('CaseUtils', CaseUtils); + + /** + * CaseUtils Service + * + * @param {object} $q ng promise object + * @param {Function} civicaseCrmApi civicrm api service + */ + function CaseUtils ($q, civicaseCrmApi) { + this.getDashboardLink = function (id) { + return $q(function (resolve, reject) { + const params = { id, return: ['case_type_category', 'case_type_id'] }; + civicaseCrmApi('Case', 'getdetails', params) + .then(function (result) { + const categoryId = result.values[id].case_type_category; + const link = CRM.url(`/case/a/?case_type_category=${categoryId}#/case/list?cf={"case_type_category":"${categoryId}"}&caseId=${id}`); + + resolve(link); + }); + }); + }; + } +})(angular, CRM.$, CRM._, CRM); From 45ebb40a82f09eec51bd11158ccd09a3811147de Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 2 Mar 2023 10:41:46 +0100 Subject: [PATCH 033/199] BTHAB-24: Add page to view single quotation --- CRM/Civicase/DAO/CaseSalesOrder.php | 4 +- CRM/Civicase/Page/CaseSalesOrder.php | 28 ++++ .../directives/quotations-view.directive.html | 135 ++++++++++++++++++ .../directives/quotations-view.directive.js | 65 +++++++++ .../CRM/Civicase/Page/CaseSalesOrder.tpl | 24 ++++ xml/Menu/civicase.xml | 5 + xml/schema/CRM/Civicase/CaseSalesOrder.xml | 2 +- 7 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 CRM/Civicase/Page/CaseSalesOrder.php create mode 100644 ang/civicase-features/quotations/directives/quotations-view.directive.html create mode 100644 ang/civicase-features/quotations/directives/quotations-view.directive.js create mode 100644 templates/CRM/Civicase/Page/CaseSalesOrder.tpl diff --git a/CRM/Civicase/DAO/CaseSalesOrder.php b/CRM/Civicase/DAO/CaseSalesOrder.php index 52c0b5bc5..92d942fb4 100644 --- a/CRM/Civicase/DAO/CaseSalesOrder.php +++ b/CRM/Civicase/DAO/CaseSalesOrder.php @@ -6,7 +6,7 @@ * * Generated from uk.co.compucorp.civicase/xml/schema/CRM/Civicase/CaseSalesOrder.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:12c8b8325efddd813eeaa69be2c655fb) + * (GenCodeChecksum:363627d2e3cd7f868f0cc2c5b89fb95b) */ use CRM_Civicase_ExtensionUtil as E; @@ -37,7 +37,7 @@ class CRM_Civicase_DAO_CaseSalesOrder extends CRM_Core_DAO { * @var string[] */ protected static $_paths = [ - 'view' => 'civicrm/case-features?reset=1&action=view&lid=[id]', + 'view' => 'civicrm/case-features/quotations/view?reset=1&id=[id]', 'update' => 'civicrm/case-features?reset=1&action=update&lid=[id]', 'delete' => 'civicrm/case-features/delete?reset=1&lid=[id]', ]; diff --git a/CRM/Civicase/Page/CaseSalesOrder.php b/CRM/Civicase/Page/CaseSalesOrder.php new file mode 100644 index 000000000..e010417b2 --- /dev/null +++ b/CRM/Civicase/Page/CaseSalesOrder.php @@ -0,0 +1,28 @@ +addModules(['crmApp', 'civicase-features']); + $salesOrderId = CRM_Utils_Request::retrieveValue('id', 'Positive'); + $this->assign('sales_order_id', $salesOrderId); + + return parent::run(); + } + +} diff --git a/ang/civicase-features/quotations/directives/quotations-view.directive.html b/ang/civicase-features/quotations/directives/quotations-view.directive.html new file mode 100644 index 000000000..1249179f5 --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-view.directive.html @@ -0,0 +1,135 @@ +
+

{{ ts('Quotations') }}

+ +
+
+

View Quotation

+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Quotatioin Id{{salesOrder.id}}
Client{{salesOrder['client_id.display_name']}}
Date{{salesOrder.quotation_date}}
Description
Case/Opportunity
Case Id{{salesOrder.case_id}}
Case Type{{salesOrder['case_id.case_type_id:label']}}
Case Subject{{salesOrder['case_id.subject']}}
Owner{{salesOrder['owner_id.display_name']}}
Status{{salesOrder['status_id:label']}}
Currency{{salesOrder.currency}}
+
+
+ +
+
+

Items Overview

+
+
+ + + + + + + + + + + + + + + + + + + + + +
ProductItem Description Financial TypeUnit PriceQuantityDiscount %Tax %Subtotal
{{ item["product_id.name"] }}{{ item["item_description"] }}{{ item["financial_type_id.name"] }}{{ item["unit_price"] || 0 }}{{ item["quantity"] || 0 }}{{ item["discounted_percentage"] || 0 }}{{ item["tax_rate"] || 0 }}{{ item["subtotal_amount"] }}
+
+
+ +
+
+

Amount Summary

+
+
+ + + + + + + + + + + + + +
Total{{ currencySymbol }} {{ salesOrder.total_before_tax }}
Tax @ {{ i.rate }}%{{ currencySymbol }} {{ i.value }}
Grand Total{{ currencySymbol }} {{salesOrder.total_after_tax}}
+
+
+ +
+
+

Notes

+
+
+
+

+ {{salesOrder.notes}} +

+
+ +
+
+
+ +
diff --git a/ang/civicase-features/quotations/directives/quotations-view.directive.js b/ang/civicase-features/quotations/directives/quotations-view.directive.js new file mode 100644 index 000000000..33e47a676 --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-view.directive.js @@ -0,0 +1,65 @@ +(function (angular, _) { + var module = angular.module('civicase-features'); + + module.directive('quotationsView', function () { + return { + restrict: 'E', + controller: 'quotationsViewController', + templateUrl: '~/civicase-features/quotations/directives/quotations-view.directive.html', + scope: { + salesOrderId: '=' + } + }; + }); + + module.controller('quotationsViewController', quotationsViewController); + + /** + * @param {object} crmApi4 api V4 service + * @param {object} $scope the controller scope + * @param {object} CaseUtils case utility service + * @param {object} CurrencyCodes CurrencyCodes service + */ + function quotationsViewController (crmApi4, $scope, CaseUtils, CurrencyCodes) { + $scope.taxRates = []; + $scope.currencySymbol = ''; + $scope.dashboardLink = '#'; + $scope.salesOrder = { + total_before_tax: 0, + total_after_tax: 0 + }; + $scope.hasCase = false; + + (function init () { + if ($scope.salesOrderId) { + getSalesOrderAndLineItems(); + } + })(); + + /** + * Retrieves the sales order and its line items from API + */ + function getSalesOrderAndLineItems () { + crmApi4('CaseSalesOrder', 'get', { + select: ['*', 'case_sales_order_line.*', 'client_id.display_name', 'owner_id.display_name', 'case_id.case_type_id:label', 'case_id.subject', 'status_id:label'], + where: [['id', '=', $scope.salesOrderId]], + limit: 1, + chain: { items: ['CaseSalesOrderLine', 'get', { where: [['sales_order_id', '=', '$id']], select: ['*', 'product_id.name', 'financial_type_id.name'] }], computedRates: ['CaseSalesOrder', 'computeTotal', { lineItems: '$items' }] } + }).then(async function (caseSalesOrders) { + if (Array.isArray(caseSalesOrders) && caseSalesOrders.length > 0) { + $scope.salesOrder = caseSalesOrders.pop(); + $scope.salesOrder.taxRates = $scope.salesOrder.computedRates[0].taxRates; + $scope.currencySymbol = CurrencyCodes.getSymbol($scope.salesOrder.currency); + + if (!$scope.salesOrder.case_id) { + return; + } + $scope.hasCase = true; + CaseUtils.getDashboardLink($scope.salesOrder.case_id).then(link => { + $scope.dashboardLink = link; + }); + } + }); + } + } +})(angular, CRM._); diff --git a/templates/CRM/Civicase/Page/CaseSalesOrder.tpl b/templates/CRM/Civicase/Page/CaseSalesOrder.tpl new file mode 100644 index 000000000..fe97dd9a5 --- /dev/null +++ b/templates/CRM/Civicase/Page/CaseSalesOrder.tpl @@ -0,0 +1,24 @@ +
+
+ +
+
+ diff --git a/xml/Menu/civicase.xml b/xml/Menu/civicase.xml index b66f7a741..6888a530f 100644 --- a/xml/Menu/civicase.xml +++ b/xml/Menu/civicase.xml @@ -33,6 +33,11 @@ CRM_Civicase_Page_CaseFeaturesAngular administer CiviCase + + civicrm/case-features/quotations/view + CRM_Civicase_Page_CaseSalesOrder + administer CiviCase + civicrm/case/webforms CRM_Civicase_Form_CaseWebforms diff --git a/xml/schema/CRM/Civicase/CaseSalesOrder.xml b/xml/schema/CRM/Civicase/CaseSalesOrder.xml index e8dcb0427..54eeefebf 100644 --- a/xml/schema/CRM/Civicase/CaseSalesOrder.xml +++ b/xml/schema/CRM/Civicase/CaseSalesOrder.xml @@ -8,7 +8,7 @@ true - civicrm/case-features?reset=1&action=view&lid=[id] + civicrm/case-features/quotations/view?reset=1&id=[id] civicrm/case-features?reset=1&action=update&lid=[id] civicrm/case-features/delete?reset=1&lid=[id] From 5552bf213a0ca5cc67f22f330b6ee598479d2c0d Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 3 Mar 2023 07:22:51 +0100 Subject: [PATCH 034/199] BTHAB-24: Use caseutil to direct user to case dashboard --- .../directives/quotations-create.directive.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index f73ae665c..e10fa5874 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -21,8 +21,9 @@ * @param {object} crmApi4 api V4 service * @param {object} FeatureCaseTypes FeatureCaseTypes service * @param {object} SalesOrderStatus SalesOrderStatus service + * @param {object} CaseUtils case utility service */ - function quotationsCreateController ($scope, $window, CurrencyCodes, civicaseCrmApi, Contact, crmApi4, FeatureCaseTypes, SalesOrderStatus) { + function quotationsCreateController ($scope, $window, CurrencyCodes, civicaseCrmApi, Contact, crmApi4, FeatureCaseTypes, SalesOrderStatus, CaseUtils) { const defaultCurrency = 'GBP'; const productsCache = new Map(); const financialTypesCache = new Map(); @@ -251,14 +252,9 @@ $window.location.href = 'a#/quotations'; } - const params = { id: $scope.salesOrder.case_id, return: ['case_type_category', 'case_type_id'] }; - civicaseCrmApi('Case', 'getdetails', params) - .then(function (result) { - const categoryId = result.values[$scope.salesOrder.case_id].case_type_category; - $window.location.href = `../case/a/case_type_category=${categoryId}` + - `#/case/list?caseId=${$scope.salesOrder.case_id}&` + - `cf=%7B"case_type_category":"${categoryId}"%7D`; - }); + CaseUtils.getDashboardLink($scope.salesOrder.case_id).then(link => { + $window.location.href = link; + }); } /** From dfb68c938c6dcb79d33c0653599dd9c6270aa811 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 3 Mar 2023 12:29:47 +0100 Subject: [PATCH 035/199] BTHAB-29: Refactor shared service into own folder --- ang/civicase-features/{ => shared}/services/case-utils.service.js | 0 .../{ => shared}/services/currency-codes.service.js | 0 .../{ => shared}/services/feature-case-types.service.js | 0 .../{ => shared}/services/sales-order-status.service.js | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename ang/civicase-features/{ => shared}/services/case-utils.service.js (100%) rename ang/civicase-features/{ => shared}/services/currency-codes.service.js (100%) rename ang/civicase-features/{ => shared}/services/feature-case-types.service.js (100%) rename ang/civicase-features/{ => shared}/services/sales-order-status.service.js (100%) diff --git a/ang/civicase-features/services/case-utils.service.js b/ang/civicase-features/shared/services/case-utils.service.js similarity index 100% rename from ang/civicase-features/services/case-utils.service.js rename to ang/civicase-features/shared/services/case-utils.service.js diff --git a/ang/civicase-features/services/currency-codes.service.js b/ang/civicase-features/shared/services/currency-codes.service.js similarity index 100% rename from ang/civicase-features/services/currency-codes.service.js rename to ang/civicase-features/shared/services/currency-codes.service.js diff --git a/ang/civicase-features/services/feature-case-types.service.js b/ang/civicase-features/shared/services/feature-case-types.service.js similarity index 100% rename from ang/civicase-features/services/feature-case-types.service.js rename to ang/civicase-features/shared/services/feature-case-types.service.js diff --git a/ang/civicase-features/services/sales-order-status.service.js b/ang/civicase-features/shared/services/sales-order-status.service.js similarity index 100% rename from ang/civicase-features/services/sales-order-status.service.js rename to ang/civicase-features/shared/services/sales-order-status.service.js From 94a36f6c16631903c2e84624a2b5ac574e93edaf Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 3 Mar 2023 14:08:31 +0100 Subject: [PATCH 036/199] BTHAB-29: Filter quotations by case id --- ang/afsearchQuotations.aff.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ang/afsearchQuotations.aff.html b/ang/afsearchQuotations.aff.html index d81b789bc..d413ae665 100644 --- a/ang/afsearchQuotations.aff.html +++ b/ang/afsearchQuotations.aff.html @@ -22,5 +22,5 @@
- + From 40fb3a5c951cdd9426336a3f47074d0e28c92069 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 3 Mar 2023 14:43:17 +0100 Subject: [PATCH 037/199] BTHAB-29: Display quotations belonging to a case on case dashboard --- .../configs/add-quotations-tab.config.js | 51 +++++++++++++++++++ .../quotations-case-tab-content.html | 3 ++ .../services/quotations-case-tab.service.js | 17 +++++++ ang/civicase.ang.php | 1 + 4 files changed, 72 insertions(+) create mode 100644 ang/civicase-features/quotations/configs/add-quotations-tab.config.js create mode 100644 ang/civicase-features/quotations/directives/quotations-case-tab-content.html create mode 100644 ang/civicase-features/quotations/services/quotations-case-tab.service.js diff --git a/ang/civicase-features/quotations/configs/add-quotations-tab.config.js b/ang/civicase-features/quotations/configs/add-quotations-tab.config.js new file mode 100644 index 000000000..1a38bea4b --- /dev/null +++ b/ang/civicase-features/quotations/configs/add-quotations-tab.config.js @@ -0,0 +1,51 @@ +(function (angular) { + const module = angular.module('civicase-features'); + const FEATURE_NAME = 'quotations'; + + module.config(function ($windowProvider, tsProvider, CaseDetailsTabsProvider) { + var $window = $windowProvider.$get(); + var ts = tsProvider.$get(); + var quotationsTab = { + name: 'Quotations', + label: ts('Quotations'), + weight: 100 + }; + + if (caseTypeCategoryHasQuotationEnabled()) { + CaseDetailsTabsProvider.addTabs([ + quotationsTab + ]); + } + + /** + * Returns the current case type category parameter. This is used instead of + * the $location service because the later is not available at configuration + * time. + * + * @returns {string|null} the name of the case type category, or null. + */ + function getCaseTypeCategory () { + var urlParamRegExp = /case_type_category=([^&]+)/i; + var currentSearch = decodeURIComponent($window.location.search); + var results = urlParamRegExp.exec(currentSearch); + + return results && results[1]; + } + + /** + * Returns true if the current case type category has quotations + * features enabled + * + * @returns {boolean} true if quotations is enabled, otherwise false + */ + function caseTypeCategoryHasQuotationEnabled () { + const caseTypeCategory = parseInt(getCaseTypeCategory()); + const quotationCaseTypeCategories = CRM['civicase-features'].featureCaseTypes[FEATURE_NAME] || []; + if (Array.isArray(quotationCaseTypeCategories) && caseTypeCategory) { + return quotationCaseTypeCategories.includes(caseTypeCategory); + } + + return false; + } + }); +})(angular); diff --git a/ang/civicase-features/quotations/directives/quotations-case-tab-content.html b/ang/civicase-features/quotations/directives/quotations-case-tab-content.html new file mode 100644 index 000000000..ba39d3404 --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-case-tab-content.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/ang/civicase-features/quotations/services/quotations-case-tab.service.js b/ang/civicase-features/quotations/services/quotations-case-tab.service.js new file mode 100644 index 000000000..2e22f7a6e --- /dev/null +++ b/ang/civicase-features/quotations/services/quotations-case-tab.service.js @@ -0,0 +1,17 @@ +(function (angular, $, _) { + var module = angular.module('civicase'); + + module.service('QuotationsCaseTab', QuotationsCaseTab); + + /** + * Quotations Case Tab service. + */ + function QuotationsCaseTab () { + /** + * @returns {string} Returns tab content HTMl template url. + */ + this.activeTabContentUrl = function () { + return '~/civicase-features/quotations/directives/quotations-case-tab-content.html'; + }; + } +})(angular, CRM.$, CRM._); diff --git a/ang/civicase.ang.php b/ang/civicase.ang.php index 2d3188100..0e387072a 100644 --- a/ang/civicase.ang.php +++ b/ang/civicase.ang.php @@ -53,6 +53,7 @@ 'uibTabsetClass', 'dialogService', 'civicase-base', + 'civicase-features', ]; $requires = CRM_Civicase_Hook_addDependentAngularModules::invoke($requires); From b1619a7035270a8464d14c5c8597ede6094a1706 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 6 Mar 2023 17:41:35 +0100 Subject: [PATCH 038/199] BTHAB-28: Prefill case/opportunity field for new quotations --- .../directives/quotations-create.directive.html | 1 + .../directives/quotations-create.directive.js | 9 ++++++--- .../directives/quotations-list.directive.js | 11 +++++++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.html b/ang/civicase-features/quotations/directives/quotations-create.directive.html index 91d0566a9..7fd9f28fb 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.html @@ -57,6 +57,7 @@

{{ ts('Create Quotations') }}

select: { multiple: false, allowClear: true }, api: caseApiParam(), }" + ng-disabled="defaultCaseId !== null" /> diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index e10fa5874..fde83de73 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -14,6 +14,7 @@ /** * @param {object} $scope the controller scope + * @param {object} $location the location service * @param {object} $window window object of the browser * @param {object} CurrencyCodes CurrencyCodes service * @param {Function} civicaseCrmApi crm api service @@ -23,7 +24,7 @@ * @param {object} SalesOrderStatus SalesOrderStatus service * @param {object} CaseUtils case utility service */ - function quotationsCreateController ($scope, $window, CurrencyCodes, civicaseCrmApi, Contact, crmApi4, FeatureCaseTypes, SalesOrderStatus, CaseUtils) { + function quotationsCreateController ($scope, $location, $window, CurrencyCodes, civicaseCrmApi, Contact, crmApi4, FeatureCaseTypes, SalesOrderStatus, CaseUtils) { const defaultCurrency = 'GBP'; const productsCache = new Map(); const financialTypesCache = new Map(); @@ -38,6 +39,7 @@ $scope.handleProductChange = handleProductChange; $scope.handleCurrencyChange = handleCurrencyChange; $scope.salesOrderStatus = SalesOrderStatus.getAll(); + $scope.defaultCaseId = $location.search().caseId || null; $scope.handleFinancialTypeChange = handleFinancialTypeChange; $scope.currencySymbol = CurrencyCodes.getSymbol(defaultCurrency); @@ -70,7 +72,8 @@ subtotal_amount: 0 }], total: 0, - grandTotal: 0 + grandTotal: 0, + case_id: $scope.defaultCaseId }; $scope.total = 0; $scope.taxRates = []; @@ -253,7 +256,7 @@ } CaseUtils.getDashboardLink($scope.salesOrder.case_id).then(link => { - $window.location.href = link; + $window.location.href = `${link}&tab=Quotations`; }); } diff --git a/ang/civicase-features/quotations/directives/quotations-list.directive.js b/ang/civicase-features/quotations/directives/quotations-list.directive.js index c2622de1e..41802a79c 100644 --- a/ang/civicase-features/quotations/directives/quotations-list.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-list.directive.js @@ -14,16 +14,23 @@ /** * @param {object} $scope the controller scope + * @param {object} $location the location service * @param {object} $window window object of the browser */ - function quotationsListController ($scope, $window) { + function quotationsListController ($scope, $location, $window) { $scope.redirectToQuotationCreationScreen = redirectToQuotationCreationScreen; /** * Redirect user to new quotation screen */ function redirectToQuotationCreationScreen () { - $window.location.href = '/civicrm/case-features/a#/new'; + let url = '/civicrm/case-features/a#/new'; + const caseId = $location.search().caseId; + if (caseId) { + url += `?caseId=${caseId}`; + } + + $window.location.href = url; } } })(angular, CRM._); From f31ba234bc820fff78e9e9e70d9f78b78c8b95e2 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 7 Mar 2023 11:38:28 +0100 Subject: [PATCH 039/199] BTHAB-26: Allow delete of quotations --- CRM/Civicase/DAO/CaseSalesOrder.php | 4 +- CRM/Civicase/Form/CaseSalesOrderDelete.php | 60 +++++++++++++++++++ .../Civicase/Form/CaseSalesOrderDelete.tpl | 21 +++++++ xml/Menu/civicase.xml | 5 ++ xml/schema/CRM/Civicase/CaseSalesOrder.xml | 2 +- 5 files changed, 89 insertions(+), 3 deletions(-) create mode 100755 CRM/Civicase/Form/CaseSalesOrderDelete.php create mode 100755 templates/CRM/Civicase/Form/CaseSalesOrderDelete.tpl diff --git a/CRM/Civicase/DAO/CaseSalesOrder.php b/CRM/Civicase/DAO/CaseSalesOrder.php index 92d942fb4..fb42b4ccb 100644 --- a/CRM/Civicase/DAO/CaseSalesOrder.php +++ b/CRM/Civicase/DAO/CaseSalesOrder.php @@ -6,7 +6,7 @@ * * Generated from uk.co.compucorp.civicase/xml/schema/CRM/Civicase/CaseSalesOrder.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:363627d2e3cd7f868f0cc2c5b89fb95b) + * (GenCodeChecksum:55874b05cb7ad3e78f9b0dae8f02161d) */ use CRM_Civicase_ExtensionUtil as E; @@ -39,7 +39,7 @@ class CRM_Civicase_DAO_CaseSalesOrder extends CRM_Core_DAO { protected static $_paths = [ 'view' => 'civicrm/case-features/quotations/view?reset=1&id=[id]', 'update' => 'civicrm/case-features?reset=1&action=update&lid=[id]', - 'delete' => 'civicrm/case-features/delete?reset=1&lid=[id]', + 'delete' => 'civicrm/case-features/quotations/delete?reset=1&id=[id]', ]; /** diff --git a/CRM/Civicase/Form/CaseSalesOrderDelete.php b/CRM/Civicase/Form/CaseSalesOrderDelete.php new file mode 100755 index 000000000..b64834667 --- /dev/null +++ b/CRM/Civicase/Form/CaseSalesOrderDelete.php @@ -0,0 +1,60 @@ +id = CRM_Utils_Request::retrieve('id', 'Positive', $this); + } + + /** + * {@inheritDoc} + */ + public function buildQuickForm() { + $this->addButtons([ + [ + 'type' => 'submit', + 'name' => E::ts('Delete'), + ], + [ + 'type' => 'cancel', + 'name' => E::ts('Cancel'), + 'isDefault' => TRUE, + ], + ]); + + parent::buildQuickForm(); + } + + /** + * {@inheritDoc} + */ + public function postProcess() { + if (!empty($this->id)) { + CaseSalesOrder::delete() + ->addWhere('id', '=', $this->id) + ->execute(); + CRM_Core_Session::setStatus(E::ts('Quotation is deleted successfully.'), ts('Quotation deleted'), 'success'); + } + } + +} diff --git a/templates/CRM/Civicase/Form/CaseSalesOrderDelete.tpl b/templates/CRM/Civicase/Form/CaseSalesOrderDelete.tpl new file mode 100755 index 000000000..8cb19585d --- /dev/null +++ b/templates/CRM/Civicase/Form/CaseSalesOrderDelete.tpl @@ -0,0 +1,21 @@ +
+
+

+

{ts}Are you sure, you want to delete the record?{/ts}

+ +

{ts}Any existing contributions will not be deleted and will still be visible on the relevant contact records{/ts}

+
+
+ {include file="CRM/common/formButtons.tpl" location="bottom"} +
+
+ + diff --git a/xml/Menu/civicase.xml b/xml/Menu/civicase.xml index 6888a530f..bb8637f98 100644 --- a/xml/Menu/civicase.xml +++ b/xml/Menu/civicase.xml @@ -38,6 +38,11 @@ CRM_Civicase_Page_CaseSalesOrder administer CiviCase
+ + civicrm/case-features/quotations/delete + CRM_Civicase_Form_CaseSalesOrderDelete + administer CiviCase + civicrm/case/webforms CRM_Civicase_Form_CaseWebforms diff --git a/xml/schema/CRM/Civicase/CaseSalesOrder.xml b/xml/schema/CRM/Civicase/CaseSalesOrder.xml index 54eeefebf..4f183a37a 100644 --- a/xml/schema/CRM/Civicase/CaseSalesOrder.xml +++ b/xml/schema/CRM/Civicase/CaseSalesOrder.xml @@ -10,7 +10,7 @@ civicrm/case-features/quotations/view?reset=1&id=[id] civicrm/case-features?reset=1&action=update&lid=[id] - civicrm/case-features/delete?reset=1&lid=[id] + civicrm/case-features/quotations/delete?reset=1&id=[id] From 995e48bb4e943e21467ce293a88478b54ee1f523 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 7 Mar 2023 08:54:54 +0100 Subject: [PATCH 040/199] BTHAB-25: Allow edit of quotations --- CRM/Civicase/DAO/CaseSalesOrder.php | 2 +- .../CaseSalesOrder/SalesOrderSaveAction.php | 20 ++++++ ang/civicase-features/app.routes.js | 2 +- .../quotations-create.directive.html | 2 +- .../directives/quotations-create.directive.js | 21 ++++++ .../directives/quotations-list.directive.js | 2 +- .../shared/services/case-utils.service.js | 31 +++++++- .../SalesOrderSaveActionTest.php | 70 ++++++++++++++++++- xml/schema/CRM/Civicase/CaseSalesOrder.xml | 2 +- 9 files changed, 145 insertions(+), 7 deletions(-) diff --git a/CRM/Civicase/DAO/CaseSalesOrder.php b/CRM/Civicase/DAO/CaseSalesOrder.php index fb42b4ccb..b6a7f2cc8 100644 --- a/CRM/Civicase/DAO/CaseSalesOrder.php +++ b/CRM/Civicase/DAO/CaseSalesOrder.php @@ -38,7 +38,7 @@ class CRM_Civicase_DAO_CaseSalesOrder extends CRM_Core_DAO { */ protected static $_paths = [ 'view' => 'civicrm/case-features/quotations/view?reset=1&id=[id]', - 'update' => 'civicrm/case-features?reset=1&action=update&lid=[id]', + 'update' => 'civicrm/case-features/a#/quotations/new?reset=1&id=[id]', 'delete' => 'civicrm/case-features/quotations/delete?reset=1&id=[id]', ]; diff --git a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php index c792172de..32a4b1baa 100644 --- a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php @@ -51,6 +51,7 @@ protected function writeRecord($items) { $result = array_pop($salesOrders); $caseSalesOrderLineAPI = CaseSalesOrderLine::save(); + $this->removeStaleLineItems($salesOrder); if (!empty($result) && !empty($lineItems)) { array_walk($lineItems, function (&$lineItem) use ($result, $caseSalesOrderLineAPI) { $lineItem['sales_order_id'] = $result['id']; @@ -72,4 +73,23 @@ protected function writeRecord($items) { } } + /** + * Delete line items that have been detached. + * + * @param array $salesOrder + * Array of the salesorder to remove stale line items for. + */ + public function removeStaleLineItems(array $salesOrder) { + if (empty($salesOrder['id'])) { + return; + } + + $lineItemsInUse = array_column($salesOrder['items'], 'id'); + + CaseSalesOrderLine::delete() + ->addWhere('sales_order_id', '=', $salesOrder['id']) + ->addWhere('id', 'NOT IN', $lineItemsInUse) + ->execute(); + } + } diff --git a/ang/civicase-features/app.routes.js b/ang/civicase-features/app.routes.js index 5612883fc..739a6b1d0 100644 --- a/ang/civicase-features/app.routes.js +++ b/ang/civicase-features/app.routes.js @@ -10,7 +10,7 @@ `; } }); - $routeProvider.when('/new', { + $routeProvider.when('/quotations/new', { template: function () { var urlParams = UrlParametersProvider.parse(window.location.search); return ` diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.html b/ang/civicase-features/quotations/directives/quotations-create.directive.html index 7fd9f28fb..c2cc7a661 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.html @@ -67,7 +67,7 @@

{{ ts('Create Quotations') }}

+

Apply Discount

+ +
+ +
+
{{$ctrl.completedMessage}}
+
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ +
+
diff --git a/ang/civicase-features/quotations/directives/quotations-discount.directive.js b/ang/civicase-features/quotations/directives/quotations-discount.directive.js new file mode 100644 index 000000000..395631fb0 --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-discount.directive.js @@ -0,0 +1,51 @@ +(function (angular, _) { + var module = angular.module('civicase-features'); + + module.directive('quotationsDiscount', function () { + return { + restrict: 'E', + controller: 'quotationsDiscountController', + templateUrl: '~/civicase-features/quotations/directives/quotations-discount.directive.html', + scope: {} + }; + }); + + module.controller('quotationsDiscountController', quotationsDiscountController); + + /** + * @param {object} $q ng-promise object + * @param {object} $scope the controller scope + * @param {object} crmApi4 api V4 service + * @param {object} searchTaskBaseTrait searchkit trait + * @param {object} CaseUtils case utility service + */ + function quotationsDiscountController ($q, $scope, crmApi4, searchTaskBaseTrait, CaseUtils) { + $scope.ts = CRM.ts('civicase'); + const ctrl = angular.extend(this, $scope.model, searchTaskBaseTrait); + ctrl.stage = 'form'; + $scope.submitInProgress = false; + + this.applyDiscount = () => { + $q(async function (resolve, reject) { + const updatedSalesOrder = {}; + + for (const salesOrderId of ctrl.ids) { + const result = await CaseUtils.getSalesOrderAndLineItems(salesOrderId); + const selectedProducts = ctrl.products.split(','); + result.items.forEach((lineItem, index) => { + // The line item is part of the product user desires to apply discount to + if (lineItem.product_id && selectedProducts.includes((lineItem.product_id).toString())) { + const newDiscount = result.items[index].discounted_percentage + ctrl.discount; + result.items[index].discounted_percentage = Math.min(100, newDiscount); + updatedSalesOrder[salesOrderId] = result; + } + }); + } + + await crmApi4('CaseSalesOrder', 'save', { records: Object.values(updatedSalesOrder) }); + ctrl.close(); + CRM.alert(`Discount applied to ${Object.values(updatedSalesOrder).length} Quotation(s) successfully`, ts('Success'), 'success'); + }); + }; + } +})(angular, CRM._); diff --git a/civicase.php b/civicase.php index b929068e4..a94060994 100644 --- a/civicase.php +++ b/civicase.php @@ -548,3 +548,15 @@ function civicase_civicrm_alterMailParams(&$params, $context) { $hook->run($params, $context); } } + +/** + * Implements hook_civicrm_searchKitTasks(). + */ +function civicase_civicrm_searchKitTasks(array &$tasks, bool $checkPermissions, ?int $userID) { + $tasks['CaseSalesOrder']['add_discount'] = [ + 'module' => 'civicase-features', + 'icon' => 'fa-percent', + 'title' => ts('Add Discount'), + 'uiDialog' => ['templateUrl' => '~/civicase-features/quotations/directives/quotations-discount.directive.html'], + ]; +} From 6180ad266cbcab47a2f82be1c6cd505d38741c67 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 8 Mar 2023 15:58:00 +0100 Subject: [PATCH 043/199] BTHAB-44: Add quotations field to contribution settings --- ...dQuotationsNotesToContributionSettings.php | 61 +++++++++++++++++++ .../SaveQuotationsNotesSettings.php | 50 +++++++++++++++ civicase.php | 2 + 3 files changed, 113 insertions(+) create mode 100644 CRM/Civicase/Hook/BuildForm/AddQuotationsNotesToContributionSettings.php create mode 100644 CRM/Civicase/Hook/PostProcess/SaveQuotationsNotesSettings.php diff --git a/CRM/Civicase/Hook/BuildForm/AddQuotationsNotesToContributionSettings.php b/CRM/Civicase/Hook/BuildForm/AddQuotationsNotesToContributionSettings.php new file mode 100644 index 000000000..2f375b52d --- /dev/null +++ b/CRM/Civicase/Hook/BuildForm/AddQuotationsNotesToContributionSettings.php @@ -0,0 +1,61 @@ +shouldRun($formName)) { + return; + } + + $this->addQuotationsNoteField($form); + } + + /** + * Checks if this shook should run. + * + * @param string $formName + * Form Name. + * + * @return bool + * True if the hook should run. + */ + public function shouldRun($formName) { + return $formName == CRM_Admin_Form_Preferences_Contribute::class; + } + + /** + * Add Quotations note fields. + * + * @param CRM_Core_Form $form + * Form Class object. + */ + public function addQuotationsNoteField(CRM_Core_Form &$form) { + $fieldName = 'quotations_notes'; + $field = [ + $fieldName => [ + 'html_type' => 'wysiwyg', + 'title' => ts('Terms/Notes for Quotations'), + 'weight' => 5, + 'description' => ts('Enter note or message to be displyaed on quotations'), + 'attributes' => ['rows' => 2, 'cols' => 40], + ], + ]; + + $form->add('wysiwyg', $fieldName, $field[$fieldName]['title'], $field[$fieldName]['attributes']); + $form->assign('htmlFields', array_merge($form->get_template_vars('htmlFields'), $field)); + $value = Civi::settings()->get($fieldName) ?? NULL; + $form->setDefaults(array_merge($form->_defaultValues, [$fieldName => $value])); + } + +} diff --git a/CRM/Civicase/Hook/PostProcess/SaveQuotationsNotesSettings.php b/CRM/Civicase/Hook/PostProcess/SaveQuotationsNotesSettings.php new file mode 100644 index 000000000..a1d3d8a1f --- /dev/null +++ b/CRM/Civicase/Hook/PostProcess/SaveQuotationsNotesSettings.php @@ -0,0 +1,50 @@ +shouldRun($form, $formName)) { + return; + } + + $this->saveQuotationsNoteField($form); + } + + /** + * Checks if this shook should run. + * + * @param string $formName + * Form Name. + * + * @return bool + * True if the hook should run. + */ + public function shouldRun($formName) { + return $formName == CRM_Admin_Form_Preferences_Contribute::class; + } + + /** + * Saves the Quotations Note Form field. + * + * @param CRM_Core_Form $form + * Form Class object. + */ + public function saveQuotationsNoteField(CRM_Core_Form &$form) { + $values = $form->getVar('_submitValues'); + if (!empty($values['quotations_notes'])) { + Civi::settings()->set('quotations_notes', $values['quotations_notes']); + } + } + +} diff --git a/civicase.php b/civicase.php index a94060994..86c24e379 100644 --- a/civicase.php +++ b/civicase.php @@ -176,6 +176,7 @@ function civicase_civicrm_buildForm($formName, &$form) { new CRM_Civicase_Hook_BuildForm_PdfFormButtonsLabelChange(), new CRM_Civicase_Hook_BuildForm_AddScriptToCreatePdfForm(), new CRM_Civicase_Hook_BuildForm_AddCaseCategoryFeaturesField(), + new CRM_Civicase_Hook_BuildForm_AddQuotationsNotesToContributionSettings(), ]; foreach ($hooks as $hook) { @@ -293,6 +294,7 @@ function civicase_civicrm_postProcess($formName, &$form) { new CRM_Civicase_Hook_PostProcess_HandleDraftActivity(), new CRM_Civicase_Hook_PostProcess_SaveCaseCategoryCustomFields(), new CRM_Civicase_Hook_PostProcess_SaveCaseCategoryFeature(), + new CRM_Civicase_Hook_PostProcess_SaveQuotationsNotesSettings(), ]; foreach ($hooks as $hook) { From af646cbed9b609cb21ac1e10013a4f80d4be390a Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 13 Mar 2023 12:40:00 +0100 Subject: [PATCH 044/199] BTHAB-47: Remove title from quotation view --- .../quotations/directives/quotations-view.directive.html | 1 - 1 file changed, 1 deletion(-) diff --git a/ang/civicase-features/quotations/directives/quotations-view.directive.html b/ang/civicase-features/quotations/directives/quotations-view.directive.html index 1249179f5..ebb02087e 100644 --- a/ang/civicase-features/quotations/directives/quotations-view.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-view.directive.html @@ -1,5 +1,4 @@
+ From 35dc2d0b1e7f022d489b343ff139f829688569c8 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 22 Mar 2023 09:06:17 +0100 Subject: [PATCH 049/199] BTHAB-43: Create Sales Order template workflow --- .../WorkflowMessage/SalesOrderInvoice.php | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php diff --git a/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php b/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php new file mode 100644 index 000000000..0afad867a --- /dev/null +++ b/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php @@ -0,0 +1,55 @@ + Date: Wed, 22 Mar 2023 09:07:15 +0100 Subject: [PATCH 050/199] BTHAB-43: Create salesorder invoice template on update/install --- .../Setup/Manage/QuotationTemplateManager.php | 84 +++++++++++ CRM/Civicase/Upgrader.php | 5 + CRM/Civicase/Upgrader/Steps/Step0019.php | 20 ++- .../MessageTemplate/QuotationInvoice.tpl | 134 ++++++++++++++++++ 4 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 CRM/Civicase/Setup/Manage/QuotationTemplateManager.php create mode 100644 templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl diff --git a/CRM/Civicase/Setup/Manage/QuotationTemplateManager.php b/CRM/Civicase/Setup/Manage/QuotationTemplateManager.php new file mode 100644 index 000000000..bed8ba95f --- /dev/null +++ b/CRM/Civicase/Setup/Manage/QuotationTemplateManager.php @@ -0,0 +1,84 @@ +addSelect('id') + ->addWhere('workflow_name', '=', SalesOrderInvoice::WORKFLOW) + ->execute() + ->first(); + + $templatePath = E::path('/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl'); + $templateBodyHtml = file_get_contents($templatePath); + + $params = [ + 'workflow_name' => SalesOrderInvoice::WORKFLOW, + 'msg_title' => 'Quotation Invoice', + 'msg_subject' => 'Quotation Invoice', + 'msg_html' => $templateBodyHtml, + 'is_reserved' => 0, + 'is_default' => 1, + ]; + + if (!empty($messageTemplate)) { + $params = array_merge(['id' => $messageTemplate['id']], $params); + } + + $optionValue = OptionValue::get(FALSE) + ->addWhere('option_group_id:name', '=', 'msg_tpl_workflow_case') + ->addWhere('name', '=', SalesOrderInvoice::WORKFLOW) + ->execute() + ->first(); + + if (empty($optionValue)) { + $optionValue = OptionValue::create(FALSE) + ->addValue('option_group_id.name', 'msg_tpl_workflow_case') + ->addValue('label', 'Quotation Invoice') + ->addValue('name', SalesOrderInvoice::WORKFLOW) + ->execute() + ->first(); + } + + $params['workflow_id'] = $optionValue['id']; + + MessageTemplate::save(FALSE)->addRecord($params)->execute(); + } + + /** + * {@inheritDoc} + */ + public function remove(): void { + MessageTemplate::delete(FALSE) + ->addWhere('workflow_name', '=', SalesOrderInvoice::WORKFLOW) + ->execute(); + + OptionValue::delete(FALSE) + ->addWhere('option_group_id:name', '=', 'msg_tpl_workflow_case') + ->addWhere('name', '=', SalesOrderInvoice::WORKFLOW) + ->execute() + ->first(); + } + + /** + * {@inheritDoc} + */ + protected function toggle($status): void { + MessageTemplate::update(FALSE) + ->addValue('is_active', $status) + ->addWhere('workflow_name', '=', SalesOrderInvoice::WORKFLOW) + ->execute(); + } + +} diff --git a/CRM/Civicase/Upgrader.php b/CRM/Civicase/Upgrader.php index 2e683909b..f188ebf40 100644 --- a/CRM/Civicase/Upgrader.php +++ b/CRM/Civicase/Upgrader.php @@ -17,6 +17,7 @@ use CRM_Civicase_Setup_AddMyActivitiesMenu as AddMyActivitiesMenu; use CRM_Civicase_Setup_Manage_CaseTypeCategoryFeaturesManager as CaseTypeCategoryFeaturesManager; use CRM_Civicase_Setup_Manage_CaseSalesOrderStatusManager as CaseSalesOrderStatusManager; +use CRM_Civicase_Setup_Manage_QuotationTemplateManager as QuotationTemplateManager; /** * Collection of upgrade steps. @@ -152,6 +153,7 @@ public function install() { $this->createManageCasesMenuItem(); (new CaseTypeCategoryFeaturesManager())->create(); (new CaseSalesOrderStatusManager())->create(); + (new QuotationTemplateManager())->create(); } /** @@ -250,6 +252,7 @@ public function uninstall() { (new CaseTypeCategoryFeaturesManager())->remove(); (new CaseSalesOrderStatusManager())->remove(); + (new QuotationTemplateManager())->remove(); } /** @@ -417,6 +420,7 @@ public function enable() { (new CaseTypeCategoryFeaturesManager())->enable(); (new CaseSalesOrderStatusManager())->enable(); + (new QuotationTemplateManager())->enable(); } /** @@ -428,6 +432,7 @@ public function disable() { $this->toggleNav('Manage Cases', FALSE); (new CaseTypeCategoryFeaturesManager())->disable(); (new CaseSalesOrderStatusManager())->disable(); + (new QuotationTemplateManager())->disable(); } /** diff --git a/CRM/Civicase/Upgrader/Steps/Step0019.php b/CRM/Civicase/Upgrader/Steps/Step0019.php index 704ee0e16..8139aefa2 100644 --- a/CRM/Civicase/Upgrader/Steps/Step0019.php +++ b/CRM/Civicase/Upgrader/Steps/Step0019.php @@ -1,5 +1,6 @@ executeSqlFile('sql/auto_install.sql'); + try { + $upgrader = CRM_Civicase_Upgrader_Base::instance(); + $upgrader->executeSqlFile('sql/auto_install.sql'); - (new CaseTypeCategoryManager())->create(); - (new CaseSalesOrderStatusManager())->create(); + (new QuotationTemplateManager())->create(); + (new CaseTypeCategoryManager())->create(); + (new CaseSalesOrderStatusManager())->create(); + } + catch (\Throwable $th) { + \Civi::log()->error('Error upgrading Civicase', [ + 'context' => [ + 'backtrace' => $th->getTraceAsString(), + 'message' => $th->getMessage(), + ], + ]); + } return TRUE; } diff --git a/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl b/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl new file mode 100644 index 000000000..b291fd12d --- /dev/null +++ b/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl @@ -0,0 +1,134 @@ + + + + + + + + +
+ + + + + + +
+ logo + +
+ +
+ + + + + + + + + + + + + + + +
Client Name: {$sales_order.client.display_name}DateAddress
+ {if $sales_order.clientAddress.street_address }{$sales_order.clientAddress.street_address}{/if} +
+ {if $sales_order.clientAddress.supplemental_address_1 }{$sales_order.clientAddress.supplemental_address_1}{/if} +
+ {if $sales_order.clientAddress.supplemental_address_2 }{$sales_order.clientAddress.supplemental_address_2}{/if} +
+ + {if $sales_order.clientAddress.city} + {$sales_order.clientAddress.city} {$sales_order.clientAddress.postal_code}{if $sales_order.clientAddress.postal_code_suffix} - {$sales_order.clientAddress.postal_code_suffix}{/if}
+ {/if} +
+
+ {$sales_order.quotation_date|crmDate} +

Quote Number

+

{$sales_order.id}

+
+ {domain.address} +
+
+ +
+

Description

+ +

{$sales_order.description}

+ + +
+
+ +
+ + + + + + + + + + + + + {foreach from=$sales_order.items key=k item=item} + + + + + + + + + {/foreach} + +
DescriptionQuantityUnit PriceDiscountVATAmount {$sales_order.currency} (without tax)
{$item.item_description}{$item.quantity}{$item.unit_price|crmMoney:$sales_order.currency}{if empty($item.discounted_percentage) } 0 {else}{$item.discounted_percentage}{/if}%{if empty($item.tax_rate) } 0 {else}{$item.tax_rate}{/if}%{$item.subtotal_amount|crmMoney:$sales_order.currency}
+
+ +
+
+ + + + + + + + {foreach from=$sales_order.taxRates item=tax} + + + + + + + {/foreach} + + + + + + +
SubTotal (inclusive of discount){$sales_order.total_before_tax|crmMoney:$sales_order.currency}
Total VAT ({$tax.rate}%){$tax.value|crmMoney:$sales_order.currency}
Total Amount{$sales_order.total_after_tax|crmMoney:$sales_order.currency}
+
+
+ +
+

Terms

+ +

{if $terms }{$terms}{/if}

+ + +
+
+
+ + + From 02b625df1f5d0659848f50bde20b6e11c2de8b45 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 22 Mar 2023 09:09:03 +0100 Subject: [PATCH 051/199] BTHAB-43: Allow users to use sales_order fields as tokens --- CRM/Civicase/Hook/Tokens/SalesOrderTokens.php | 69 +++++++++++++++++++ civicase.php | 10 +++ 2 files changed, 79 insertions(+) create mode 100644 CRM/Civicase/Hook/Tokens/SalesOrderTokens.php diff --git a/CRM/Civicase/Hook/Tokens/SalesOrderTokens.php b/CRM/Civicase/Hook/Tokens/SalesOrderTokens.php new file mode 100644 index 000000000..5eeb11432 --- /dev/null +++ b/CRM/Civicase/Hook/Tokens/SalesOrderTokens.php @@ -0,0 +1,69 @@ +execute()->jsonSerialize(); + foreach ($fields as $field) { + $label = E::ts('Quotation ' . ucwords(str_replace("_", " ", $field['name']))); + $e->entity(self::TOKEN)->register($field['name'], $label); + } + } + + /** + * Evaluates Token values. + * + * @param \Civi\Token\Event\TokenValueEvent $e + * TokenValue Event. + */ + public static function evaluateSalesOrderTokens(TokenValueEvent $e) { + $context = $e->getTokenProcessor()->context; + + if (array_key_exists('schema', $context) && in_array('salesOrderId', $context['schema'])) { + foreach ($e->getRows() as $row) { + if (!empty($row->context['salesOrderId'])) { + $salesOrderId = $row->context['salesOrderId']; + + $caseSalesOrder = CaseSalesOrder::get() + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->first(); + foreach ($caseSalesOrder as $key => $value) { + $row->tokens(self::TOKEN, $key, $value); + } + } + } + } + + } + +} diff --git a/civicase.php b/civicase.php index ad655bbfa..2a324116b 100644 --- a/civicase.php +++ b/civicase.php @@ -79,6 +79,16 @@ function civicase_civicrm_config(&$config) { 'hook_civicrm_buildAsset', ['CRM_Civicase_Event_Listener_AssetBuilder', 'addWordReplacements'] ); + + Civi::dispatcher()->addListener( + 'civi.token.list', + ['CRM_Civicase_Hook_Tokens_SalesOrderTokens', 'listSalesOrderTokens'] + ); + + Civi::dispatcher()->addListener( + 'civi.token.eval', + ['CRM_Civicase_Hook_Tokens_SalesOrderTokens', 'evaluateSalesOrderTokens'] + ); } /** From d3370d9d6ec572348520286466ab28813a361154 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 22 Mar 2023 09:10:37 +0100 Subject: [PATCH 052/199] BTHAB-43: Fix quotation notes form hook --- CRM/Civicase/Hook/PostProcess/SaveQuotationsNotesSettings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CRM/Civicase/Hook/PostProcess/SaveQuotationsNotesSettings.php b/CRM/Civicase/Hook/PostProcess/SaveQuotationsNotesSettings.php index a1d3d8a1f..781375f28 100644 --- a/CRM/Civicase/Hook/PostProcess/SaveQuotationsNotesSettings.php +++ b/CRM/Civicase/Hook/PostProcess/SaveQuotationsNotesSettings.php @@ -14,7 +14,7 @@ class CRM_Civicase_Hook_PostProcess_SaveQuotationsNotesSettings { * Form Class object. */ public function run($formName, CRM_Core_Form $form) { - if (!$this->shouldRun($form, $formName)) { + if (!$this->shouldRun($formName)) { return; } From a5dff6a1eabc96b02916a14c808f5ba300d2c9aa Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 22 Mar 2023 09:12:10 +0100 Subject: [PATCH 053/199] BTHAB-43: Allow user to send or download salesorder Invoice --- CRM/Civicase/Form/CaseSalesOrderInvoice.php | 231 ++++++++++++++++++ .../WorkflowMessage/SalesOrderInvoice.php | 10 + .../Civicase/Form/CaseSalesOrderInvoice.tpl | 15 ++ .../MessageTemplate/QuotationInvoice.tpl | 2 +- 4 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 CRM/Civicase/Form/CaseSalesOrderInvoice.php create mode 100644 templates/CRM/Civicase/Form/CaseSalesOrderInvoice.tpl diff --git a/CRM/Civicase/Form/CaseSalesOrderInvoice.php b/CRM/Civicase/Form/CaseSalesOrderInvoice.php new file mode 100644 index 000000000..c5d2280ea --- /dev/null +++ b/CRM/Civicase/Form/CaseSalesOrderInvoice.php @@ -0,0 +1,231 @@ +setTitle('Email Quotation'); + $this->salesOrderId = CRM_Utils_Request::retrieveValue('id', 'Positive'); + + $this->setContactIDs(); + $this->setIsSearchContext(FALSE); + $this->traitPreProcess(); + } + + /** + * List available tokens for this form. + * + * Presently all tokens are returned. + * + * @return array + * List of Available tokens + * + * @throws \CRM_Core_Exception + */ + public function listTokens() { + $tokenProcessor = new TokenProcessor(Civi::dispatcher(), ['schema' => ['contactId']]); + $tokens = $tokenProcessor->listTokens(); + + return $tokens; + } + + /** + * Submit the form values. + * + * This is also accessible for testing. + * + * @param array $formValues + * Submitted values. + * + * @throws \CRM_Core_Exception + * @throws \CiviCRM_API3_Exception + * @throws \Civi\API\Exception\UnauthorizedException + * @throws \API_Exception + */ + public function submit(array $formValues): void { + $this->saveMessageTemplate($formValues); + $sents = 0; + $from = $formValues['from_email_address']; + $text = $this->getSubmittedValue('text_message'); + $html = $this->getSubmittedValue('html_message'); + $from = CRM_Utils_Mail::formatFromAddress($from); + + $cc = $this->getCc(); + $additionalDetails = empty($cc) ? '' : "\ncc : " . $this->getEmailUrlString($this->getCcArray()); + + $bcc = $this->getBcc(); + $additionalDetails .= empty($bcc) ? '' : "\nbcc : " . $this->getEmailUrlString($this->getBccArray()); + + $quotationInvoice = self::getQuotationInvoice(); + + foreach ($this->getRowsForEmails() as $values) { + $mailParams = []; + $mailParams['messageTemplate'] = [ + 'msg_text' => $text, + 'msg_html' => $html, + 'msg_subject' => $this->getSubject(), + ]; + $mailParams['tokenContext'] = [ + 'contactId' => $values['contact_id'], + 'salesOrderId' => $this->salesOrderId, + ]; + $mailParams['tplParams'] = []; + $mailParams['from'] = $from; + $mailParams['toEmail'] = $values['email']; + $mailParams['cc'] = $cc ?? NULL; + $mailParams['bcc'] = $bcc ?? NULL; + $mailParams['attachments'][] = CRM_Utils_Mail::appendPDF('quotation_invoice.pdf', $quotationInvoice['html'], $quotationInvoice['format']); + // Send the mail. + [$sent, $subject, $message, $html] = CRM_Core_BAO_MessageTemplate::sendTemplate($mailParams); + $sents += ($sent ? 1 : 0); + } + + CRM_Core_Session::setStatus(ts('One email has been sent successfully. ', [ + 'plural' => '%count emails were sent successfully. ', + 'count' => $sents, + ]), ts('Message Sent', ['plural' => 'Messages Sent', 'count' => $sents]), 'success'); + + } + + /** + * {@inheritDoc} + */ + public function setContactIDs() { // phpcs:ignore + $this->_contactIds = $this->getContactIds(); + } + + /** + * Returns Sales Order Client Contact ID. + * + * @return array + * Client Contact ID as an array + */ + protected function getContactIds(): array { + if (isset($this->_contactIds)) { + return $this->_contactIds; + } + + $salesOrderId = CRM_Utils_Request::retrieveValue('id', 'Positive'); + + $caseSalesOrder = CaseSalesOrder::get() + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->first(); + + $this->_contactIds = [$caseSalesOrder['client_id']]; + + return $this->_contactIds; + } + + /** + * Renders the quotatioin invoice message template. + * + * @return array + * Rendered message, consistent of 'subject', 'text', 'html' + */ + public static function getQuotationInvoice(): array { + $salesOrderId = CRM_Utils_Request::retrieveValue('id', 'Positive'); + $caseSalesOrder = CaseSalesOrder::get() + ->addWhere('id', '=', $salesOrderId) + ->addChain('items', CaseSalesOrderLine::get() + ->addWhere('sales_order_id', '=', '$id') + ->addSelect('*', 'product_id.name', 'financial_type_id.name') + ) + ->addChain('computedRates', CaseSalesOrder::computeTotal() + ->setLineItems('$items') + ) + ->addChain('client', Contact::get() + ->addWhere('id', '=', '$client_id'), 0 + ) + ->execute() + ->first(); + + if (!empty($caseSalesOrder['client']['addressee_id'])) { + $caseSalesOrder['clientAddress'] = Address::get() + ->addWhere('contact_id', '=', $caseSalesOrder['client_id']) + ->execute() + ->first(); + } + + $caseSalesOrder['taxRates'] = $caseSalesOrder['computedRates'][0]['taxRates'] ?? []; + $caseSalesOrder['quotation_date'] = date('Y-m-d', strtotime($caseSalesOrder['quotation_date'])); + + $domain = CRM_Core_BAO_Domain::getDomain(); + $organisation = Contact::get() + ->addSelect('image_URL') + ->addWhere('id', '=', $domain->contact_id) + ->execute() + ->first(); + + $model = new CRM_Civicase_WorkflowMessage_SalesOrderInvoice(); + $terms = Civi::settings()->get('quotations_notes'); + $model->setDomainLogo($organisation['image_URL']); + $model->setSalesOrder($caseSalesOrder); + $model->setTerms($terms); + $model->setSalesOrderId($salesOrderId); + $rendered = $model->renderTemplate(); + + return $rendered; + } + + /** + * Get the rows for each contactID. + * + * @return array + * Array if contact IDs. + */ + protected function getRows(): array { + $rows = []; + foreach ($this->_contactIds as $index => $contactID) { + $rows[] = [ + 'contact_id' => $contactID, + 'schema' => ['contactId' => $contactID], + ]; + } + return $rows; + } + + /** + * Renders and return the generated PDF to the browser. + */ + public static function download(): void { + $rendered = self::getQuotationInvoice(); + ob_end_clean(); + CRM_Utils_PDF_Utils::html2pdf($rendered['html'], 'quotation_invoice.pdf', FALSE, $rendered['format'] ?? []); + CRM_Utils_System::civiExit(); + } + +} diff --git a/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php b/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php index 0afad867a..3b439bc3d 100644 --- a/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php +++ b/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php @@ -15,6 +15,8 @@ * @method $this setDomainLocation(?array $domainLocation) * @method ?string getDomainLogo() * @method $this setDomainLogo(?string $logo) + * @method ?int getSalesOrderId() + * @method $this setSalesOrderId(?int $salesOrderId) */ class CRM_Civicase_WorkflowMessage_SalesOrderInvoice extends GenericWorkflowMessage { @@ -52,4 +54,12 @@ class CRM_Civicase_WorkflowMessage_SalesOrderInvoice extends GenericWorkflowMess */ protected $domainLogo; + /** + * Sales Order ID. + * + * @var array + * @scope tokenContext + */ + protected $salesOrderId; + } diff --git a/templates/CRM/Civicase/Form/CaseSalesOrderInvoice.tpl b/templates/CRM/Civicase/Form/CaseSalesOrderInvoice.tpl new file mode 100644 index 000000000..4fb2b9cd7 --- /dev/null +++ b/templates/CRM/Civicase/Form/CaseSalesOrderInvoice.tpl @@ -0,0 +1,15 @@ +
+ {icon icon="fa-info-circle"}{/icon} + {ts}Your quotation will be emailed to the contact below as a PDF attachment.{/ts} +
+{include file="CRM/Contact/Form/Task/Email.tpl"} + +{literal} + +{/literal} \ No newline at end of file diff --git a/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl b/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl index b291fd12d..d9d5e31f3 100644 --- a/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl +++ b/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl @@ -7,7 +7,7 @@ -
+
From 2371e9e31059cacb4525b6e8a5fac498d2505efd Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 22 Mar 2023 09:18:05 +0100 Subject: [PATCH 054/199] BTHAB-43: Prevents error when token is being resolved for unittest --- CRM/Civicase/Hook/Tokens/AddCaseTokenCategory.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CRM/Civicase/Hook/Tokens/AddCaseTokenCategory.php b/CRM/Civicase/Hook/Tokens/AddCaseTokenCategory.php index 09d51b8e7..e746edc25 100644 --- a/CRM/Civicase/Hook/Tokens/AddCaseTokenCategory.php +++ b/CRM/Civicase/Hook/Tokens/AddCaseTokenCategory.php @@ -34,6 +34,12 @@ public function run(array &$tokens) { * Available tokens. */ private function setCaseTokenCategory(array &$tokens) { + if (CIVICRM_UF === 'UnitTests') { + // For unit tests where AddCaseCustomFieldsTokenValues might not be called + // using an empty key breaks the code. + return $tokens['case_cf'] = []; + } + $tokens['case_cf'][''] = ''; } From 91094a010979460a87f4857d06109f31dd3c0f0c Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 22 Mar 2023 09:25:48 +0100 Subject: [PATCH 055/199] BTHAB-43: Add test to ensure invoice download/mail works as expected --- CRM/Civicase/Form/CaseSalesOrderInvoice.php | 2 +- .../Form/CaseSalesOrderInvoiceTest.php | 172 ++++++++++++++++++ tests/phpunit/Helpers/CaseSalesOrderTrait.php | 9 +- 3 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 tests/phpunit/CRM/Civicase/Form/CaseSalesOrderInvoiceTest.php diff --git a/CRM/Civicase/Form/CaseSalesOrderInvoice.php b/CRM/Civicase/Form/CaseSalesOrderInvoice.php index c5d2280ea..287e49a01 100644 --- a/CRM/Civicase/Form/CaseSalesOrderInvoice.php +++ b/CRM/Civicase/Form/CaseSalesOrderInvoice.php @@ -173,7 +173,7 @@ public static function getQuotationInvoice(): array { ->execute() ->first(); - if (!empty($caseSalesOrder['client']['addressee_id'])) { + if (!empty($caseSalesOrder['client_id'])) { $caseSalesOrder['clientAddress'] = Address::get() ->addWhere('contact_id', '=', $caseSalesOrder['client_id']) ->execute() diff --git a/tests/phpunit/CRM/Civicase/Form/CaseSalesOrderInvoiceTest.php b/tests/phpunit/CRM/Civicase/Form/CaseSalesOrderInvoiceTest.php new file mode 100644 index 000000000..85bc63b1c --- /dev/null +++ b/tests/phpunit/CRM/Civicase/Form/CaseSalesOrderInvoiceTest.php @@ -0,0 +1,172 @@ +registerCurrentLoggedInContactInSession($contact['id']); + } + + /** + * Ensures user sees the email form when the Email URL is accessed. + */ + public function testEmailFormWillDisplayAsExpected() { + $salesOrder = $this->getCaseSalesOrderData(); + $salesOrder = (object) (CaseSalesOrder::save(FALSE) + ->addRecord($salesOrder) + ->execute() + ->first()); + + Email::save(FALSE) + ->addRecord([ + "contact_id" => $salesOrder->client_id, + "location_type_id" => 2, + "email" => "junwe@mail.com", + "is_primary" => TRUE, + "on_hold" => 0, + ]) + ->execute(); + + $link = "civicrm/case-features/quotations/email?id={$salesOrder->id}"; + $page = $this->imitateLinkVisit($link); + + $this->assertRegExp('/name="subject"/', $page); + $this->assertRegExp('/name="from_email_address"/', $page); + } + + /** + * Ensures the To Email is set to client email on email form. + */ + public function testToEmailIsSetByDefaulToSalesOrderClientEmail() { + $expectedToEmail = "junwe@mail.com"; + $salesOrder = $this->getCaseSalesOrderData(); + $salesOrder = (object) (CaseSalesOrder::save(FALSE) + ->addRecord($salesOrder) + ->execute() + ->first()); + + Email::save(FALSE) + ->addRecord([ + "contact_id" => $salesOrder->client_id, + "location_type_id" => 2, + "email" => $expectedToEmail, + "is_primary" => TRUE, + "on_hold" => 0, + ]) + ->execute(); + + $link = "civicrm/case-features/quotations/email?id={$salesOrder->id}"; + $page = $this->imitateLinkVisit($link); + + $this->assertRegExp('/<' . $expectedToEmail . '>/', $page); + } + + /** + * Ensures invoice will render expected tokens & tplParams. + * + * We only cheeck for some fields, that is enough to show that + * the right case sales order entity value is passed to + * the invoice. + */ + public function testInvoiceRendersAsExpected() { + $salesOrder = $this->getCaseSalesOrderData(); + $salesOrder['items'][] = $lineItem1 = $this->getCaseSalesOrderLineData(); + $salesOrder['items'][] = $lineItem2 = $this->getCaseSalesOrderLineData(); + + $salesOrder = (object) (CaseSalesOrder::save(FALSE) + ->addRecord($salesOrder) + ->execute() + ->first()); + + $address = Address::save(FALSE) + ->addRecord([ + "contact_id" => $salesOrder->client_id, + "location_type_id" => 5, + "is_primary" => TRUE, + "is_billing" => TRUE, + "street_address" => "Coldharbour Ln", + "street_number" => "42", + "supplemental_address_1" => "Supplementary Address 1", + "supplemental_address_2" => "Supplementary Address 2", + "supplemental_address_3" => "Supplementary Address 3", + "city" => "Hayes", + "postal_code" => "UB3 3EA", + "country_id" => 1226, + "manual_geo_code" => FALSE, + "timezone" => NULL, + "name" => NULL, + "master_id" => NULL, + ]) + ->execute() + ->first(); + + $contact = (object) (Contact::get(FALSE) + ->addWhere('id', '=', $salesOrder->client_id) + ->execute() + ->first()); + + $_REQUEST['id'] = $_GET['id'] = $salesOrder->id; + + $invoice = CRM_Civicase_Form_CaseSalesOrderInvoice::getQuotationInvoice(); + + $totalBeforeTax = CRM_Utils_Money::format($salesOrder->total_before_tax, $salesOrder->currency); + $totalAfterTax = CRM_Utils_Money::format($salesOrder->total_after_tax, $salesOrder->currency); + $this->assertArrayHasKey("html", $invoice); + $this->assertRegExp('/' . $contact->display_name . '/', $invoice['html']); + $this->assertRegExp('/Supplementary Address 1/', $invoice['html']); + $this->assertRegExp('/Supplementary Address 2/', $invoice['html']); + $this->assertRegExp('/' . $salesOrder->description . '/', $invoice['html']); + $this->assertRegExp('/' . str_replace(' ', '', $totalBeforeTax) . '/', $invoice['html']); + $this->assertRegExp('/' . str_replace(' ', '', $totalAfterTax) . '/', $invoice['html']); + $this->assertRegExp('/' . $lineItem1['item_description'] . '/', $invoice['html']); + $this->assertRegExp('/' . $lineItem2['item_description'] . '/', $invoice['html']); + $this->assertRegExp('/' . $lineItem1['quantity'] . '/', $invoice['html']); + $this->assertRegExp('/' . $lineItem2['quantity'] . '/', $invoice['html']); + } + + /** + * Visits a CiviCRM link and returns the page content. + * + * @param string $url + * URL to the page. + * + * @return string + * Content of the page. + */ + public function imitateLinkVisit(string $url) { + $_SERVER['REQUEST_URI'] = $url; + $urlParts = explode('?', $url); + $_GET['q'] = $urlParts[0]; + + if (!empty($urlParts[1])) { + $parsed = []; + parse_str($urlParts[1], $parsed); + foreach ($parsed as $param => $value) { + $_REQUEST[$param] = $value; + } + } + + $item = CRM_Core_Invoke::getItem([$_GET['q']]); + ob_start(); + CRM_Core_Invoke::runItem($item); + return ob_get_clean(); + } + +} diff --git a/tests/phpunit/Helpers/CaseSalesOrderTrait.php b/tests/phpunit/Helpers/CaseSalesOrderTrait.php index f80919f67..cd8495ac5 100644 --- a/tests/phpunit/Helpers/CaseSalesOrderTrait.php +++ b/tests/phpunit/Helpers/CaseSalesOrderTrait.php @@ -72,15 +72,18 @@ public function getCaseSalesOrderData(array $default = []) { */ public function getCaseSalesOrderLineData(array $default = []) { $product = ProductFabricator::fabricate(); + $quantity = rand(2, 9); + $unitPrice = rand(50, 1000); + return array_merge([ 'financial_type_id' => 1, 'product_id' => $product['id'], 'item_description' => 'test', - 'quantity' => 1, - 'unit_price' => 50, + 'quantity' => $quantity, + 'unit_price' => $unitPrice, 'tax_rate' => NULL, 'discounted_percentage' => NULL, - 'subtotal_amount' => 50, + 'subtotal_amount' => $quantity * $unitPrice, ], $default); } From bc1b0fd77f1139ff90f26d54e0c900e460c6d0d4 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 22 Mar 2023 09:26:58 +0100 Subject: [PATCH 056/199] BTHAB-43: Add salesorder invoice Download and Email menu --- xml/Menu/civicase.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/xml/Menu/civicase.xml b/xml/Menu/civicase.xml index 2c96e4703..e443095fc 100644 --- a/xml/Menu/civicase.xml +++ b/xml/Menu/civicase.xml @@ -48,6 +48,16 @@ CRM_Civicase_Page_ContactCaseSalesOrderTab administer CiviCase + + civicrm/case-features/quotations/download-pdf + CRM_Civicase_Form_CaseSalesOrderInvoice::download + administer CiviCase + + + civicrm/case-features/quotations/email + CRM_Civicase_Form_CaseSalesOrderInvoice + administer CiviCase + civicrm/case/webforms CRM_Civicase_Form_CaseWebforms From f83d97cbf302a95c9fa6e31141fc249012b25fc4 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 28 Mar 2023 07:22:18 +0100 Subject: [PATCH 057/199] BTHAB-25: Allow user to save quotation edit with default date --- .../quotations/directives/quotations-create.directive.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index 73b4b64e9..bd730bba3 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -93,6 +93,7 @@ CaseUtils.getSalesOrderAndLineItems(salesOrderId).then((result) => { $scope.salesOrder = result; + $scope.salesOrder.quotation_date = $.datepicker.formatDate('yy-mm-dd', new Date(result.quotation_date)); $scope.salesOrder.status_id = (result.status_id).toString(); CRM.wysiwyg.setVal('#sales-order-description', $scope.salesOrder.description); $scope.$emit('totalChange'); From 47449d08503da0fd53619834133c24fcd61df04f Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 28 Mar 2023 08:29:40 +0100 Subject: [PATCH 058/199] BTHAB-25: Correct message shown to user after succesful update of quotation --- .../quotations/directives/quotations-create.directive.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index bd730bba3..2016a92c8 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -29,6 +29,7 @@ const productsCache = new Map(); const financialTypesCache = new Map(); + $scope.isUpdate = false; $scope.formValid = true; $scope.roundTo = roundTo; $scope.submitInProgress = false; @@ -92,6 +93,7 @@ } CaseUtils.getSalesOrderAndLineItems(salesOrderId).then((result) => { + $scope.isUpdate = true; $scope.salesOrder = result; $scope.salesOrder.quotation_date = $.datepicker.formatDate('yy-mm-dd', new Date(result.quotation_date)); $scope.salesOrder.status_id = (result.status_id).toString(); @@ -263,7 +265,8 @@ * Show Quotation success create notification */ function showSucessNotification () { - CRM.alert('Your Quotation has been generated successfully.', ts('Saved'), 'success'); + const msg = !$scope.isUpdate ? 'Your Quotation has been generated successfully.' : 'Details updated successfully'; + CRM.alert(msg, ts('Saved'), 'success'); } /** From 7456d0b387b84aa2f7b3e1ff899d4d1d27c75598 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 14 Mar 2023 12:08:33 +0100 Subject: [PATCH 059/199] BTHAB-33: Add new case sales order contribution entity --- .../BAO/CaseSalesOrderContribution.php | 31 ++ CRM/Civicase/DAO/CaseSalesOrder.php | 2 +- .../DAO/CaseSalesOrderContribution.php | 269 ++++++++++++++++++ Civi/Api4/CaseSalesOrderContribution.php | 16 ++ civicase.civix.php | 5 + sql/auto_install.sql | 19 ++ sql/auto_uninstall.sql | 1 + .../CaseSalesOrderContribution.entityType.php | 16 ++ .../Civicase/CaseSalesOrderContribution.xml | 65 +++++ 9 files changed, 423 insertions(+), 1 deletion(-) create mode 100644 CRM/Civicase/BAO/CaseSalesOrderContribution.php create mode 100644 CRM/Civicase/DAO/CaseSalesOrderContribution.php create mode 100644 Civi/Api4/CaseSalesOrderContribution.php create mode 100644 xml/schema/CRM/Civicase/CaseSalesOrderContribution.entityType.php create mode 100644 xml/schema/CRM/Civicase/CaseSalesOrderContribution.xml diff --git a/CRM/Civicase/BAO/CaseSalesOrderContribution.php b/CRM/Civicase/BAO/CaseSalesOrderContribution.php new file mode 100644 index 000000000..8f1484f9a --- /dev/null +++ b/CRM/Civicase/BAO/CaseSalesOrderContribution.php @@ -0,0 +1,31 @@ +copyValues($params); + $instance->save(); + CRM_Utils_Hook::post($hook, $entityName, $instance->id, $instance); + + return $instance; + } + +} diff --git a/CRM/Civicase/DAO/CaseSalesOrder.php b/CRM/Civicase/DAO/CaseSalesOrder.php index b6a7f2cc8..1cf4746c5 100644 --- a/CRM/Civicase/DAO/CaseSalesOrder.php +++ b/CRM/Civicase/DAO/CaseSalesOrder.php @@ -6,7 +6,7 @@ * * Generated from uk.co.compucorp.civicase/xml/schema/CRM/Civicase/CaseSalesOrder.xml * DO NOT EDIT. Generated by CRM_Core_CodeGen - * (GenCodeChecksum:55874b05cb7ad3e78f9b0dae8f02161d) + * (GenCodeChecksum:555941e9f2d2bb4c81224d13c42ac043) */ use CRM_Civicase_ExtensionUtil as E; diff --git a/CRM/Civicase/DAO/CaseSalesOrderContribution.php b/CRM/Civicase/DAO/CaseSalesOrderContribution.php new file mode 100644 index 000000000..4a29c39ea --- /dev/null +++ b/CRM/Civicase/DAO/CaseSalesOrderContribution.php @@ -0,0 +1,269 @@ +__table = 'civicase_sales_order_contribution'; + parent::__construct(); + } + + /** + * Returns localized title of this entity. + * + * @param bool $plural + * Whether to return the plural version of the title. + */ + public static function getEntityTitle($plural = FALSE) { + return $plural ? E::ts('Case Sales Order Contributions') : E::ts('Case Sales Order Contribution'); + } + + /** + * Returns foreign keys and entity references. + * + * @return array + * [CRM_Core_Reference_Interface] + */ + public static function getReferenceColumns() { + if (!isset(Civi::$statics[__CLASS__]['links'])) { + Civi::$statics[__CLASS__]['links'] = static::createReferenceColumns(__CLASS__); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'case_sales_order_id', 'civicase_sales_order', 'id'); + Civi::$statics[__CLASS__]['links'][] = new CRM_Core_Reference_Basic(self::getTableName(), 'contribution_id', 'civicrm_contribution', 'id'); + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'links_callback', Civi::$statics[__CLASS__]['links']); + } + return Civi::$statics[__CLASS__]['links']; + } + + /** + * Returns all the column names of this table + * + * @return array + */ + public static function &fields() { + if (!isset(Civi::$statics[__CLASS__]['fields'])) { + Civi::$statics[__CLASS__]['fields'] = [ + 'id' => [ + 'name' => 'id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('Unique CaseSalesOrderContribution ID'), + 'required' => TRUE, + 'where' => 'civicase_sales_order_contribution.id', + 'table_name' => 'civicase_sales_order_contribution', + 'entity' => 'CaseSalesOrderContribution', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderContribution', + 'localizable' => 0, + 'html' => [ + 'type' => 'Number', + ], + 'readonly' => TRUE, + 'add' => NULL, + ], + 'case_sales_order_id' => [ + 'name' => 'case_sales_order_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('FK to Case Sales Order'), + 'required' => TRUE, + 'where' => 'civicase_sales_order_contribution.case_sales_order_id', + 'table_name' => 'civicase_sales_order_contribution', + 'entity' => 'CaseSalesOrderContribution', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderContribution', + 'localizable' => 0, + 'FKClassName' => 'CRM_Civicase_DAO_CaseSalesOrder', + 'add' => NULL, + ], + 'contribution_id' => [ + 'name' => 'contribution_id', + 'type' => CRM_Utils_Type::T_INT, + 'description' => E::ts('ID of Contribution'), + 'required' => TRUE, + 'where' => 'civicase_sales_order_contribution.contribution_id', + 'table_name' => 'civicase_sales_order_contribution', + 'entity' => 'CaseSalesOrderContribution', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderContribution', + 'localizable' => 0, + 'FKClassName' => 'CRM_Contribute_DAO_Contribution', + 'add' => NULL, + ], + 'to_be_invoiced' => [ + 'name' => 'to_be_invoiced', + 'type' => CRM_Utils_Type::T_STRING, + 'title' => E::ts('To Be Invoiced'), + 'description' => E::ts('Either percent ,i.e. certain percentage of the total_amount or remain, i.e. the remaining amount to be paid'), + 'required' => TRUE, + 'maxlength' => 20, + 'size' => CRM_Utils_Type::MEDIUM, + 'where' => 'civicase_sales_order_contribution.to_be_invoiced', + 'table_name' => 'civicase_sales_order_contribution', + 'entity' => 'CaseSalesOrderContribution', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderContribution', + 'localizable' => 0, + 'add' => NULL, + ], + 'percent_value' => [ + 'name' => 'percent_value', + 'type' => CRM_Utils_Type::T_MONEY, + 'title' => E::ts('Percent Value'), + 'description' => E::ts('The percentage value if to_be_invoiced is percent'), + 'required' => FALSE, + 'precision' => [ + 20, + 2, + ], + 'where' => 'civicase_sales_order_contribution.percent_value', + 'default' => '0', + 'table_name' => 'civicase_sales_order_contribution', + 'entity' => 'CaseSalesOrderContribution', + 'bao' => 'CRM_Civicase_DAO_CaseSalesOrderContribution', + 'localizable' => 0, + 'add' => NULL, + ], + ]; + CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']); + } + return Civi::$statics[__CLASS__]['fields']; + } + + /** + * Return a mapping from field-name to the corresponding key (as used in fields()). + * + * @return array + * Array(string $name => string $uniqueName). + */ + public static function &fieldKeys() { + if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) { + Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields())); + } + return Civi::$statics[__CLASS__]['fieldKeys']; + } + + /** + * Returns the names of this table + * + * @return string + */ + public static function getTableName() { + return self::$_tableName; + } + + /** + * Returns if this table needs to be logged + * + * @return bool + */ + public function getLog() { + return self::$_log; + } + + /** + * Returns the list of fields that can be imported + * + * @param bool $prefix + * + * @return array + */ + public static function &import($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, '_sales_order_contribution', $prefix, []); + return $r; + } + + /** + * Returns the list of fields that can be exported + * + * @param bool $prefix + * + * @return array + */ + public static function &export($prefix = FALSE) { + $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, '_sales_order_contribution', $prefix, []); + return $r; + } + + /** + * Returns the list of indices + * + * @param bool $localize + * + * @return array + */ + public static function indices($localize = TRUE) { + $indices = []; + return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices; + } + +} diff --git a/Civi/Api4/CaseSalesOrderContribution.php b/Civi/Api4/CaseSalesOrderContribution.php new file mode 100644 index 000000000..55d6ccfdc --- /dev/null +++ b/Civi/Api4/CaseSalesOrderContribution.php @@ -0,0 +1,16 @@ + 'CRM_Civicase_DAO_CaseSalesOrder', 'table' => 'civicase_sales_order', ], + 'CRM_Civicase_DAO_CaseSalesOrderContribution' => [ + 'name' => 'CaseSalesOrderContribution', + 'class' => 'CRM_Civicase_DAO_CaseSalesOrderContribution', + 'table' => 'civicase_sales_order_contribution', + ], 'CRM_Civicase_DAO_CaseSalesOrderLine' => [ 'name' => 'CaseSalesOrderLine', 'class' => 'CRM_Civicase_DAO_CaseSalesOrderLine', diff --git a/sql/auto_install.sql b/sql/auto_install.sql index aa06792c5..f75c23b31 100644 --- a/sql/auto_install.sql +++ b/sql/auto_install.sql @@ -98,3 +98,22 @@ CREATE TABLE IF NOT EXISTS `civicase_sales_order_line` ( CONSTRAINT FK_civicase_sales_order_line_product_id FOREIGN KEY (`product_id`) REFERENCES `civicrm_product`(`id`) ON DELETE SET NULL ) ENGINE=InnoDB; + +-- /******************************************************* +-- * +-- * civicase_sales_order_contribution +-- * +-- * Relationship between Case Sales Order and Contribution entity. +-- * +-- *******************************************************/ +CREATE TABLE IF NOT EXISTS `civicase_sales_order_contribution` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'Unique CaseSalesOrderContribution ID', + `case_sales_order_id` int unsigned NOT NULL COMMENT 'FK to Case Sales Order', + `contribution_id` int unsigned NOT NULL COMMENT 'ID of Contribution', + `to_be_invoiced` varchar(20) NOT NULL COMMENT 'Either percent ,i.e. certain percentage of the total_amount or remain, i.e. the remaining amount to be paid', + `percent_value` decimal(20,2) NULL DEFAULT 0 COMMENT 'The percentage value if to_be_invoiced is percent', + PRIMARY KEY (`id`), + CONSTRAINT FK_civicase_sales_order_contribution_case_sales_order_id FOREIGN KEY (`case_sales_order_id`) REFERENCES `civicase_sales_order`(`id`) ON DELETE CASCADE, + CONSTRAINT FK_civicase_sales_order_contribution_contribution_id FOREIGN KEY (`contribution_id`) REFERENCES `civicrm_contribution`(`id`) ON DELETE CASCADE +) +ENGINE=InnoDB; diff --git a/sql/auto_uninstall.sql b/sql/auto_uninstall.sql index 80fb0b568..18e844bc9 100644 --- a/sql/auto_uninstall.sql +++ b/sql/auto_uninstall.sql @@ -16,6 +16,7 @@ SET FOREIGN_KEY_CHECKS=0; DROP TABLE IF EXISTS `civicase_sales_order_line`; +DROP TABLE IF EXISTS `civicase_sales_order_contribution`; DROP TABLE IF EXISTS `civicase_sales_order`; DROP TABLE IF EXISTS `civicase_contactlock`; DROP TABLE IF EXISTS `civicrm_case_category_instance`; diff --git a/xml/schema/CRM/Civicase/CaseSalesOrderContribution.entityType.php b/xml/schema/CRM/Civicase/CaseSalesOrderContribution.entityType.php new file mode 100644 index 000000000..016d22b68 --- /dev/null +++ b/xml/schema/CRM/Civicase/CaseSalesOrderContribution.entityType.php @@ -0,0 +1,16 @@ + 'CaseSalesOrderContribution', + 'class' => 'CRM_Civicase_DAO_CaseSalesOrderContribution', + 'table' => 'civicase_sales_order_contribution', + ], +]; diff --git a/xml/schema/CRM/Civicase/CaseSalesOrderContribution.xml b/xml/schema/CRM/Civicase/CaseSalesOrderContribution.xml new file mode 100644 index 000000000..f9848fd3a --- /dev/null +++ b/xml/schema/CRM/Civicase/CaseSalesOrderContribution.xml @@ -0,0 +1,65 @@ + + +
+ CRM/Civicase + CaseSalesOrderContribution + civicase_sales_order_contribution + Relationship between Case Sales Order and Contribution entity. + true + + + id + int unsigned + true + Unique CaseSalesOrderContribution ID + + Number + + + + id + true + + + + case_sales_order_id + int unsigned + true + FK to Case Sales Order + + + case_sales_order_id +
civicase_sales_order
+ id + CASCADE + + + + contribution_id + int unsigned + true + ID of Contribution + + + contribution_id + civicrm_contribution
+ id + CASCADE +
+ + + to_be_invoiced + varchar + 20 + true + Either percent ,i.e. certain percentage of the total_amount or remain, i.e. the remaining amount to be paid + + + + percent_value + decimal + false + 0 + The percentage value if to_be_invoiced is percent + + From dfd3dfe2835fad4132a11b67e1856216c3b8d5c0 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 27 Mar 2023 18:21:14 +0100 Subject: [PATCH 060/199] BTHAB-33: Add menu item for creating sales order contribution --- xml/Menu/civicase.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/xml/Menu/civicase.xml b/xml/Menu/civicase.xml index e443095fc..3776d45af 100644 --- a/xml/Menu/civicase.xml +++ b/xml/Menu/civicase.xml @@ -47,6 +47,11 @@ civicrm/case-features/quotations/contact-tab CRM_Civicase_Page_ContactCaseSalesOrderTab administer CiviCase + + + civicrm/case-features/quotations/create-contribution + CRM_Civicase_Form_CaseSalesOrderContributionCreate + administer CiviCase civicrm/case-features/quotations/download-pdf From 5d416aefca8cfabdb88d352d8a9a6cf5f2bb1c0b Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 27 Mar 2023 18:23:11 +0100 Subject: [PATCH 061/199] BTHAB-33: Add page for creating sales order contribution --- .../Form/CaseSalesOrderContributionCreate.php | 148 ++++++++++++++++++ .../Form/CaseSalesOrderContributionCreate.tpl | 63 ++++++++ 2 files changed, 211 insertions(+) create mode 100644 CRM/Civicase/Form/CaseSalesOrderContributionCreate.php create mode 100644 templates/CRM/Civicase/Form/CaseSalesOrderContributionCreate.tpl diff --git a/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php new file mode 100644 index 000000000..ebef9d1c5 --- /dev/null +++ b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php @@ -0,0 +1,148 @@ +id = CRM_Utils_Request::retrieve('id', 'Positive', $this); + } + + /** + * {@inheritDoc} + */ + public function buildQuickForm() { + $this->addElement('radio', 'to_be_invoiced', '', ts('Enter % to be invoiced ?'), + 'percent', [ + 'id' => 'invoice_percent', + ]); + $this->add('text', 'percent_value', '', [ + 'id' => 'percent_value', + 'placeholder' => 'Percentage to be invoiced', + 'class' => 'form-control', + 'min' => 1, + 'max' => 10, + ], FALSE); + $this->addElement('radio', 'to_be_invoiced', '', ts('Remaining Balance'), + 'remain', + ['id' => 'invoice_remain'] + ); + $this->addRule('to_be_invoiced', ts('Invoice value is required'), 'required'); + + $statusOptions = OptionValue::get() + ->addSelect('value', 'label') + ->addWhere('option_group_id:name', '=', 'case_sales_order_status') + ->execute() + ->getArrayCopy(); + array_combine(array_column($statusOptions, 'label'), array_column($statusOptions, 'value')); + + $this->add( + 'select', + 'status', + ts('Status'), + array_merge( + ['' => 'Select'], + array_combine( + array_column($statusOptions, 'value'), + array_column($statusOptions, 'label') + ), + ), + TRUE, + ['class' => 'form-control'] + ); + + $this->addButtons([ + [ + 'type' => 'submit', + 'name' => E::ts('Create Contribution'), + ], + [ + 'type' => 'cancel', + 'name' => E::ts('Cancel'), + // 'isDefault' => TRUE, + ], + ]); + + parent::buildQuickForm(); + } + + /** + * {@inheritDoc} + */ + public function addRules() { + $this->addFormRule([$this, 'formRule']); + } + + /** + * Form Validation rule. + * + * This enforces the rule whereby, + * user must supply an amount if the + * enter percentage smount radio is selected. + * + * @param array $values + * Array of submitted values. + * + * @return array|bool + * Returns the form errors if form is invalid + */ + public function formRule(array $values) { + $errors = []; + + if ($values['to_be_invoiced'] == 'percent' && empty(floatval($values['percent_value']))) { + $errors['percent_value'] = 'Percentage value is required'; + } + + return $errors ?: TRUE; + } + + /** + * {@inheritDoc} + */ + public function postProcess() { + $values = $this->getSubmitValues(); + + if (!empty($this->id) && $values['to_be_invoiced'] == 'percent') { + $this->createPercentageContribution($values); + } + } + + /** + * Redirects user to contribution add page. + * + * This contribution page will have the line items + * prefilled from the sales order line items. + */ + public function createPercentageContribution(array $values) { + $query = [ + 'action' => 'add', + 'reset' => 1, + 'context' => 'standalone', + 'sales_order' => $this->id, + 'sales_order_status_id' => $values['status'], + 'percent_amount' => floatval($values['percent_value']), + ]; + + $url = CRM_Utils_System::url('civicrm/contribute/add', $query); + CRM_Utils_System::redirect($url); + } + +} diff --git a/templates/CRM/Civicase/Form/CaseSalesOrderContributionCreate.tpl b/templates/CRM/Civicase/Form/CaseSalesOrderContributionCreate.tpl new file mode 100644 index 000000000..d575b8bae --- /dev/null +++ b/templates/CRM/Civicase/Form/CaseSalesOrderContributionCreate.tpl @@ -0,0 +1,63 @@ +
+ +
+
+
+
+
+ {$form.to_be_invoiced.percent.html} +
+
+ {$form.percent_value.html} +
+
+
+ +
+
+
+ {$form.to_be_invoiced.remain.html} +
+
+
+ +
+
+ +
+ {$form.status.html} +
+
+
+
+
+ + +
+ {include file="CRM/common/formButtons.tpl" location="bottom"} +
+
+ +{literal} + +{/literal} From 6f683744edc6d7e034f5af67e1add74d475542cb Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 30 Mar 2023 21:24:50 +0100 Subject: [PATCH 062/199] BTHAB-34: Add popup screen to create bulk sales order contribution --- ...uotations-contribution-bulk.directive.html | 85 +++++++++++++++++++ .../quotations-contribution-bulk.directive.js | 58 +++++++++++++ civicase.php | 7 ++ 3 files changed, 150 insertions(+) create mode 100644 ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html create mode 100644 ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js diff --git a/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html new file mode 100644 index 000000000..b1dbceb71 --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html @@ -0,0 +1,85 @@ +
+ +
+ +
+
{{$ctrl.completedMessage}}
+
+ +
+
+ + +
+
+ + Amount is required +
+
+ +
+
+ + +
+
+ +
+ +
+ + Financial type is required +
+
+ +
+ +
+ + Date is required +
+
+ +
+ +
+ + Status is required +
+
+ +
+
+
+ + +
+
+
+
+
diff --git a/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js new file mode 100644 index 000000000..bd71b43e9 --- /dev/null +++ b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js @@ -0,0 +1,58 @@ +(function (angular, $, _) { + var module = angular.module('civicase-features'); + + module.directive('quotationContributionBulk', function () { + return { + restrict: 'E', + controller: 'quotationContributionBulkController', + templateUrl: '~/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html', + scope: {} + }; + }); + + module.controller('quotationContributionBulkController', quotationContributionBulkController); + + /** + * @param {object} $q ng-promise object + * @param {object} $scope the controller scope + * @param {object} crmApi4 api V4 service + * @param {object} searchTaskBaseTrait searchkit trait + * @param {object} CaseUtils case utility service + * @param {object} SalesOrderStatus SalesOrderStatus service + */ + function quotationContributionBulkController ($q, $scope, crmApi4, searchTaskBaseTrait, CaseUtils, SalesOrderStatus) { + $scope.ts = CRM.ts('civicase'); + + const ctrl = angular.extend(this, $scope.model, searchTaskBaseTrait); + ctrl.stage = 'form'; + $scope.submitInProgress = false; + ctrl.data = { + toBeInvoiced: 'percent', + percentValue: 0, + statusId: null, + financialTypeId: null, + date: $.datepicker.formatDate('yy-mm-dd', new Date()) + }; + ctrl.salesOrderStatus = SalesOrderStatus.getAll(); + + this.createBulkContribution = () => { + $q(async function (resolve, reject) { + ctrl.run = true; + let contributionCreated = 0; + + for (const id of ctrl.ids) { + try { + await crmApi4('CaseSalesOrder', 'contributionCreateAction', { ...ctrl.data, id }); + contributionCreated++; + } catch (error) { + console.log(error); + } + } + + ctrl.run = false; + ctrl.close(); + CRM.alert(`${contributionCreated} Invoices have been generated.`, ts('Success'), 'success'); + }); + }; + } +})(angular, CRM.$, CRM._); diff --git a/civicase.php b/civicase.php index 2a324116b..2d4828d6a 100644 --- a/civicase.php +++ b/civicase.php @@ -571,4 +571,11 @@ function civicase_civicrm_searchKitTasks(array &$tasks, bool $checkPermissions, 'title' => ts('Add Discount'), 'uiDialog' => ['templateUrl' => '~/civicase-features/quotations/directives/quotations-discount.directive.html'], ]; + + $tasks['CaseSalesOrder']['create_contribution'] = [ + 'module' => 'civicase-features', + 'icon' => 'fa-credit-card', + 'title' => ts('Create Contribution(Bulk)'), + 'uiDialog' => ['templateUrl' => '~/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html'], + ]; } From 934992ecc849e0930b9867404af96219bdf3e11f Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 30 Mar 2023 21:25:51 +0100 Subject: [PATCH 063/199] BTHAB-34: Add Service and API action to create sales order contribution --- .../Service/CaseSalesOrderContribution.php | 160 ++++++++++++++++ .../ContributionCreateAction.php | 171 ++++++++++++++++++ 2 files changed, 331 insertions(+) create mode 100644 CRM/Civicase/Service/CaseSalesOrderContribution.php create mode 100644 Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php diff --git a/CRM/Civicase/Service/CaseSalesOrderContribution.php b/CRM/Civicase/Service/CaseSalesOrderContribution.php new file mode 100644 index 000000000..19f78e9bd --- /dev/null +++ b/CRM/Civicase/Service/CaseSalesOrderContribution.php @@ -0,0 +1,160 @@ +salesOrder = CaseSalesOrder::get() + ->addSelect('*') + ->addWhere('id', '=', $this->salesOrderId) + ->addChain('items', CaseSalesOrderLine::get() + ->addWhere('sales_order_id', '=', '$id') + ->addSelect('*', 'product_id.name', 'financial_type_id.name') + ) + ->execute() + ->first() ?? []; + } + + /** + * Generates Line Items for the sales order entity. + */ + public function generateLineItems() { + $lineItems = []; + + if (empty($this->salesOrder['items'])) { + return []; + } + + $lineItems = $this->getLineItemForSalesOrder(); + + if ($this->type === self::INVOICE_REMAIN) { + $lineItems = [...$lineItems, ...$this->getPreviousContributionLineItem()]; + } + + return $lineItems; + } + + /** + * Returns the line items for a sales order. + * + * @return array + * Array of values keyed by contribution line item fields. + */ + private function getLineItemForSalesOrder() { + $items = []; + foreach ($this->salesOrder['items'] as $item) { + $item['quantity'] = ($this->type === self::INVOICE_PERCENT) ? + ($this->percentValue / 100) * $item['quantity'] : + $item['quantity']; + $item['total'] = $item['quantity'] * floatval($item['unit_price']); + $item['tax'] = empty($item['tax_rate']) ? 0 : $this->percent($item['tax_rate'], $item['total']); + + $items[] = $this->lineItemToContributionLineItem($item); + + if ($item['discounted_percentage'] > 0) { + $item['tax'] = 0; + $item['item_description'] = "{$item['item_description']} Discount {$item['discounted_percentage']}%"; + $item['unit_price'] = $this->percent($item['discounted_percentage'], -$item['unit_price']); + $item['total'] = $item['quantity'] * floatval($item['unit_price']); + $items[] = $this->lineItemToContributionLineItem($item); + } + } + + return $items; + } + + /** + * Returns the line items for a sales order. + * + * This is from the previously created contributions. + * + * @return array + * Array of values keyed by contribution line item fields. + */ + private function getPreviousContributionLineItem() { + $previousItems = []; + + $caseSalesOrderContributions = CaseSalesOrderContribution::get() + ->addSelect('contribution_id') + ->addWhere('case_sales_order_id.id', '=', $this->salesOrderId) + ->addChain('items', LineItem::get() + ->addWhere('contribution_id', '=', '$contribution_id') + ) + ->execute(); + + foreach ($caseSalesOrderContributions as $contribution) { + $items = $contribution['items']; + + if (empty($items)) { + return []; + } + + foreach ($items as $item) { + $item['qty'] = -1 * $item['qty']; + $item['line_total'] = $item['qty'] * floatval($item['unit_price']); + $previousItems[] = $item; + } + } + + return $previousItems; + } + + /** + * Converts a sales order line item to a contribution line item. + * + * @param array $item + * Sales Order line item. + * + * @return array + * Contribution line item + */ + private function lineItemToContributionLineItem(array $item) { + return [ + 'qty' => $item['quantity'], + 'tax_amount' => $item['tax'], + 'label' => $item['item_description'], + 'entity_table' => 'civicrm_contribution', + 'financial_type_id' => $item['financial_type_id'], + 'line_total' => $item['total'], + 'unit_price' => $item['unit_price'], + ]; + } + + /** + * Returns percentage% of value. + * + * E.g. 5% of 10. + * + * @param float $percentage + * Percentage to calculate. + * @param float $value + * The value to get percentage of. + * + * @return float + * Calculated Percentage in float + */ + public function percent(float $percentage, float $value) { + return (floatval($percentage) / 100) * floatval($value); + } + +} diff --git a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php new file mode 100644 index 000000000..5f5239867 --- /dev/null +++ b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php @@ -0,0 +1,171 @@ +createContribution(); + + $result->exchangeArray($resultArray); + } + + /** + * {@inheritDoc} + */ + protected function createContribution() { + $transaction = CRM_Core_Transaction::create(); + + try { + $salesOrderContribution = new CaseSalesOrderContribution($this->id, $this->toBeInvoiced, $this->percentValue); + $lineItems = $salesOrderContribution->generateLineItems(); + + $priceSet = PriceSet::get() + ->addWhere('name', '=', 'default_contribution_amount') + ->addWhere('is_quick_config', '=', 1) + ->execute() + ->first(); + $priceField = PriceField::get() + ->addWhere('price_set_id', '=', $priceSet['id']) + ->addChain('price_field_value', PriceFieldValue::get() + ->addWhere('price_field_id', '=', '$id') + )->execute(); + + $taxAmount = $lineTotal = 0; + $allLineItems = []; + foreach ($lineItems as $index => &$lineItem) { + $lineItem['price_field_id'] = $priceField[$index]['id']; + $lineItem['price_field_value_id'] = $priceField[$index]['price_field_value'][0]['id']; + $priceSetID = \CRM_Core_DAO::getFieldValue('CRM_Price_BAO_PriceField', $priceField[$index]['id'], 'price_set_id'); + $allLineItems[$priceSetID][$priceField[$index]['id']] = $lineItem; + $taxAmount += (float) ($lineItem['tax_amount'] ?? 0); + $lineTotal += (float) ($lineItem['line_total'] ?? 0); + } + $totalAmount = $lineTotal + $taxAmount; + + $params = [ + 'source' => "Quotation {$this->id}", + 'line_item' => $allLineItems, + 'total_amount' => $totalAmount, + 'tax_amount' => $taxAmount, + 'financial_type_id' => $this->financialTypeId, + 'receive_date' => $this->date, + 'contact_id' => $salesOrderContribution->salesOrder['client_id'], + ]; + + $contribution = Contribution::create($params)->toArray(); + $this->postCreateAction($contribution['id']); + return $contribution; + } + catch (\Exception $e) { + $transaction->rollback(); + + throw $e; + } + + } + + /** + * Delete line items that have been detached. + * + * @param array $salesOrder + * Array of the salesorder to remove stale line items for. + */ + public function removeStaleLineItems(array $salesOrder) { + if (empty($salesOrder['id'])) { + return; + } + + $lineItemsInUse = array_column($salesOrder['items'], 'id'); + + CaseSalesOrderLine::delete() + ->addWhere('sales_order_id', '=', $salesOrder['id']) + ->addWhere('id', 'NOT IN', $lineItemsInUse) + ->execute(); + } + + /** + * Updates Sales Order status. + * + * Also creates SalesOrdeContribution. + * + * @param int $contributionId + * New contribution ID. + */ + public function postCreateAction($contributionId) { + Api4CaseSalesOrderContribution::create() + ->addValue('case_sales_order_id', $this->id) + ->addValue('to_be_invoiced', $this->toBeInvoiced) + ->addValue('percent_value', $this->percentValue) + ->addValue('contribution_id', $contributionId) + ->execute(); + + CaseSalesOrder::update() + ->addWhere('id', '=', $this->id) + ->addValue('status_id', $this->statusId) + ->execute(); + } + +} From 865ff57d12697a1a15022a0e64513c229b0c22d9 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 30 Mar 2023 21:27:48 +0100 Subject: [PATCH 064/199] BTHAB-34: Add test to ensure the right contribution value is created for sales order --- .../CaseSalesOrderContributionTest.php | 62 ++++ .../ContributionCreateActionTest.php | 283 ++++++++++++++++++ tests/phpunit/Helpers/CaseSalesOrderTrait.php | 31 ++ tests/phpunit/Helpers/PriceFieldTrait.php | 62 ++++ 4 files changed, 438 insertions(+) create mode 100644 tests/phpunit/CRM/Civicase/Service/CaseSalesOrderContributionTest.php create mode 100644 tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php create mode 100644 tests/phpunit/Helpers/PriceFieldTrait.php diff --git a/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderContributionTest.php b/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderContributionTest.php new file mode 100644 index 000000000..ae7c8cb09 --- /dev/null +++ b/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderContributionTest.php @@ -0,0 +1,62 @@ +generatePriceField(); + } + + /** + * Ensures the correct number of line item is created. + * + * When there's no previous contribution. + */ + public function testCorrectNumberOfLineItemsIsGeneratedWithoutPreviousContribution() { + $salesOrder = $this->createCaseSalesOrder(); + + $salesOrderService = new SalesOrderService($salesOrder['id'], SalesOrderService::INVOICE_PERCENT, 25); + $lineItems = $salesOrderService->generateLineItems(); + + $this->assertCount(2, $lineItems); + } + + /** + * Ensures the correct number of line item is created. + * + * When there's previous contribution. + */ + public function testCorrectNumberOfLineItemsIsGeneratedWithPreviousContribution() { + $salesOrder = $this->createCaseSalesOrder(); + + $previousContributionCount = rand(1, 4); + for ($i = 0; $i < $previousContributionCount; $i++) { + CaseSalesOrder::contributionCreateAction() + ->setId($salesOrder['id']) + ->setStatusId(1) + ->setToBeInvoiced(SalesOrderService::INVOICE_PERCENT) + ->setPercentValue(20) + ->setDate(date('Y-m-d')) + ->setFinancialTypeId('1') + ->execute(); + } + + $salesOrderService = new SalesOrderService($salesOrder['id'], SalesOrderService::INVOICE_REMAIN, 0); + $lineItems = $salesOrderService->generateLineItems(); + + $this->assertCount(($previousContributionCount * 2) + 2, $lineItems); + } + +} diff --git a/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php b/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php new file mode 100644 index 000000000..235731324 --- /dev/null +++ b/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php @@ -0,0 +1,283 @@ +generatePriceField(); + $contact = ContactFabricator::fabricate(); + $this->registerCurrentLoggedInContactInSession($contact['id']); + } + + /** + * Ensures contribution create action updates status successfully. + */ + public function testContributionCreateActionWillUpdateSalesOrderStatus() { + ['id' => $id] = $this->createCaseSalesOrder(); + + $newStatus = $this->getCaseSalesOrderStatus()[1]['value']; + CaseSalesOrder::contributionCreateAction() + ->setId($id) + ->setStatusId($newStatus) + ->setToBeInvoiced('percent') + ->setPercentValue('100') + ->setDate('2020-2-12') + ->setFinancialTypeId('1') + ->execute(); + + $results = CaseSalesOrder::get() + ->addWhere('id', '=', $id) + ->execute() + ->first(); + + $this->assertEquals($newStatus, $results['status_id']); + } + + /** + * Ensures The total amount of contribution will be the expected amount. + * + * @dataProvider provideContributionCreateData + */ + public function testAppropriateContributionAmountIsCreated($expectedPercent, $contributionCreateData, $withDiscount = FALSE, $withTax = FALSE) { + $params = []; + + if ($withDiscount) { + $params['items']['discounted_percentage'] = rand(1, 50); + } + + if ($withTax) { + $params['items']['tax_rate'] = rand(1, 50); + } + + $salesOrder = $this->createCaseSalesOrder($params); + $computedTotal = CaseSalesOrder::computeTotal() + ->setLineItems($salesOrder['items']) + ->execute() + ->jsonSerialize()[0]; + + foreach ($contributionCreateData as $data) { + CaseSalesOrder::contributionCreateAction() + ->setId($salesOrder['id']) + ->setStatusId($data['statusId']) + ->setToBeInvoiced($data['toBeInvoiced']) + ->setPercentValue($data['percentValue']) + ->setDate($data['date']) + ->setFinancialTypeId($data['financialTypeId']) + ->execute(); + } + + $contributionAmounts = Api4CaseSalesOrderContribution::get() + ->addSelect('contribution_id', 'contribution_id.total_amount') + ->addWhere('case_sales_order_id.id', '=', $salesOrder['id']) + ->execute() + ->jsonSerialize(); + + $paidTotal = array_sum(array_column($contributionAmounts, 'contribution_id.total_amount')); + + // We can only guarantee that the value will be equal to 1 decimal place. + $this->assertEquals(round(($expectedPercent * $computedTotal['totalAfterTax']) / 100, 1), round($paidTotal, 1)); + } + + /** + * Provides data to test contribution create action. + * + * @return array + * Array of different scenarios + */ + public function provideContributionCreateData(): array { + return [ + '100% value will be total of the sales order value' => [ + 'expectedPercent' => 100, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 100, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + ], + '100% value will be total of the sales order value with discount applied' => [ + 'expectedPercent' => 100, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 100, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + 'withDiscount' => TRUE, + ], + '100% value will be total of the sales order value with tax_rate applied' => [ + 'expectedPercent' => 100, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 100, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + 'withTax' => TRUE, + ], + '100% value will be total of the sales order value with tax_rate applied and paid twice' => [ + 'expectedPercent' => 100, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 50, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 50, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + 'withTax' => TRUE, + ], + '100% value will be total of the sales order value when paid in 4 instalment of 25%' => [ + 'expectedPercent' => 100, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + ], + '75% value will be total of the sales order value when paid in 3 instalment of 25%' => [ + 'expectedPercent' => 75, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + ], + '100% value will be total of the sales order value when paid at once using remain option' => [ + 'expectedPercent' => 100, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_REMAIN, + // Expects this value to be ignored. + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + ], + '100% value will be total of the sales order value when paid with 25% and remain option' => [ + 'expectedPercent' => 100, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 25, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_REMAIN, + 'percentValue' => 0, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + ], + '100% value will be total of the sales order value when paid with 30%, 30% and remain option' => [ + 'expectedPercent' => 100, + 'contributionCreateData' => [ + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 30, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 30, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_REMAIN, + 'percentValue' => 0, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + ], + ], + ]; + } + +} diff --git a/tests/phpunit/Helpers/CaseSalesOrderTrait.php b/tests/phpunit/Helpers/CaseSalesOrderTrait.php index cd8495ac5..b66a8d870 100644 --- a/tests/phpunit/Helpers/CaseSalesOrderTrait.php +++ b/tests/phpunit/Helpers/CaseSalesOrderTrait.php @@ -1,5 +1,6 @@ getCaseSalesOrderData(); + $salesOrder['items'][] = $this->getCaseSalesOrderLineData(); + $salesOrder['items'][] = $this->getCaseSalesOrderLineData(); + + if (!empty($params['items']['discounted_percentage'])) { + $salesOrder['items'][0]['discounted_percentage'] = $params['items']['discounted_percentage']; + } + + if (!empty($params['items']['tax_rate'])) { + $salesOrder['items'][0]['tax_rate'] = $params['items']['tax_rate']; + } + + $salesOrder['id'] = CaseSalesOrder::save() + ->addRecord($salesOrder) + ->execute() + ->jsonSerialize()[0]['id']; + + return $salesOrder; + } + } diff --git a/tests/phpunit/Helpers/PriceFieldTrait.php b/tests/phpunit/Helpers/PriceFieldTrait.php new file mode 100644 index 000000000..07c1ab481 --- /dev/null +++ b/tests/phpunit/Helpers/PriceFieldTrait.php @@ -0,0 +1,62 @@ + civicrm_api3('PriceSet', 'getvalue', [ + 'name' => 'default_contribution_amount', + 'return' => 'id', + ]), + 'options' => ['limit' => 1], + ] + ); + $priceFieldParams = $priceField; + unset( + $priceFieldParams['id'], + $priceFieldParams['name'], + $priceFieldParams['weight'], + $priceFieldParams['is_required'] + ); + $priceFieldValue = civicrm_api3('PriceFieldValue', + 'getsingle', + [ + 'price_field_id' => $priceField['id'], + 'options' => ['limit' => 1], + ] + ); + $priceFieldValueParams = $priceFieldValue; + unset($priceFieldValueParams['id'], $priceFieldValueParams['name'], $priceFieldValueParams['weight']); + for ($i = 1; $i <= 30; ++$i) { + $params = array_merge($priceFieldParams, ['label' => ts('Additional Line Item') . " $i"]); + $priceField = civicrm_api3('PriceField', 'get', $params)['values']; + if (empty($priceField)) { + $p = civicrm_api3('PriceField', 'create', $params); + civicrm_api3('PriceFieldValue', 'create', array_merge( + $priceFieldValueParams, + [ + 'label' => ts('Additional Item') . " $i", + 'price_field_id' => $p['id'], + ] + )); + } + else { + civicrm_api3('PriceField', 'create', [ + 'id' => key($priceField), + 'is_active' => TRUE, + ]); + } + } + } + +} From 27e800dc1a9553ae4f3a005852de016cb9a7298a Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 30 Mar 2023 21:29:21 +0100 Subject: [PATCH 065/199] BTHAB-34: Only one client per case sales order --- .../quotations/directives/quotations-create.directive.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.html b/ang/civicase-features/quotations/directives/quotations-create.directive.html index c2cc7a661..efa36d7a4 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.html @@ -15,7 +15,7 @@

{{ ts('Create Quotations') }}

name="client" crm-entityref="{ entity: 'Contact', - select: { multiple: true, allowClear: true } + select: { multiple: false, allowClear: true } }" required ng-minlength="1" From eac8cbf2126f04b51188282bc3cdc7fe5141810a Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 3 Apr 2023 08:40:33 +0100 Subject: [PATCH 066/199] BTHAB-34: Create Bulk contributions in batches --- CRM/Civicase/Test/Fabricator/CaseType.php | 11 +++ .../ContributionCreateAction.php | 91 ++++++++++--------- ...uotations-contribution-bulk.directive.html | 20 +++- .../quotations-contribution-bulk.directive.js | 15 ++- .../CaseSalesOrderContributionTest.php | 2 +- .../ContributionCreateActionTest.php | 52 +++++++++-- 6 files changed, 136 insertions(+), 55 deletions(-) diff --git a/CRM/Civicase/Test/Fabricator/CaseType.php b/CRM/Civicase/Test/Fabricator/CaseType.php index 555539375..156047cff 100644 --- a/CRM/Civicase/Test/Fabricator/CaseType.php +++ b/CRM/Civicase/Test/Fabricator/CaseType.php @@ -44,6 +44,17 @@ class CRM_Civicase_Test_Fabricator_CaseType { */ public static function fabricate(array $params = []) { $params = array_merge(self::$defaultParams, $params); + + $result = civicrm_api3( + 'CaseType', + 'get', + ['name' => $params['name']] + ); + + if (!empty($result['values'])) { + $params['id'] = array_pop($result['values'])['id']; + } + $result = civicrm_api3( 'CaseType', 'create', diff --git a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php index 5f5239867..38d3122a9 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php @@ -3,7 +3,6 @@ namespace Civi\Api4\Action\CaseSalesOrder; use Civi\Api4\CaseSalesOrder; -use Civi\Api4\CaseSalesOrderContribution as Api4CaseSalesOrderContribution; use Civi\Api4\PriceFieldValue; use Civi\Api4\PriceField; use Civi\Api4\PriceSet; @@ -14,6 +13,7 @@ use Civi\Api4\Generic\Traits\DAOActionTrait; use CRM_Contribute_BAO_Contribution as Contribution; use CRM_Civicase_Service_CaseSalesOrderContribution as CaseSalesOrderContribution; +use CRM_Utils_SQL_Insert; /** * Creates contribution for multiple sales orders. @@ -22,11 +22,11 @@ class ContributionCreateAction extends AbstractAction { use DAOActionTrait; /** - * Sales order ID. + * Sales order IDs. * - * @var int + * @var array */ - protected $id; + protected $ids; /** * Sales order Status ID. @@ -77,11 +77,9 @@ public function _run(Result $result) { // phpcs:ignore */ protected function createContribution() { $transaction = CRM_Core_Transaction::create(); + $salesOrderContributions = []; try { - $salesOrderContribution = new CaseSalesOrderContribution($this->id, $this->toBeInvoiced, $this->percentValue); - $lineItems = $salesOrderContribution->generateLineItems(); - $priceSet = PriceSet::get() ->addWhere('name', '=', 'default_contribution_amount') ->addWhere('is_quick_config', '=', 1) @@ -92,39 +90,49 @@ protected function createContribution() { ->addChain('price_field_value', PriceFieldValue::get() ->addWhere('price_field_id', '=', '$id') )->execute(); - - $taxAmount = $lineTotal = 0; - $allLineItems = []; - foreach ($lineItems as $index => &$lineItem) { - $lineItem['price_field_id'] = $priceField[$index]['id']; - $lineItem['price_field_value_id'] = $priceField[$index]['price_field_value'][0]['id']; - $priceSetID = \CRM_Core_DAO::getFieldValue('CRM_Price_BAO_PriceField', $priceField[$index]['id'], 'price_set_id'); - $allLineItems[$priceSetID][$priceField[$index]['id']] = $lineItem; - $taxAmount += (float) ($lineItem['tax_amount'] ?? 0); - $lineTotal += (float) ($lineItem['line_total'] ?? 0); + foreach ($this->ids as $id) { + $salesOrderContribution = new CaseSalesOrderContribution($id, $this->toBeInvoiced, $this->percentValue); + $lineItems = $salesOrderContribution->generateLineItems(); + + $taxAmount = $lineTotal = 0; + $allLineItems = []; + foreach ($lineItems as $index => &$lineItem) { + $lineItem['price_field_id'] = $priceField[$index]['id']; + $lineItem['price_field_value_id'] = $priceField[$index]['price_field_value'][0]['id']; + $priceSetID = \CRM_Core_DAO::getFieldValue('CRM_Price_BAO_PriceField', $priceField[$index]['id'], 'price_set_id'); + $allLineItems[$priceSetID][$priceField[$index]['id']] = $lineItem; + $taxAmount += (float) ($lineItem['tax_amount'] ?? 0); + $lineTotal += (float) ($lineItem['line_total'] ?? 0); + } + $totalAmount = $lineTotal + $taxAmount; + + $params = [ + 'source' => "Quotation {$id}", + 'line_item' => $allLineItems, + 'total_amount' => $totalAmount, + 'tax_amount' => $taxAmount, + 'financial_type_id' => $this->financialTypeId, + 'receive_date' => $this->date, + 'contact_id' => $salesOrderContribution->salesOrder['client_id'], + ]; + + $contribution = Contribution::create($params)->toArray(); + $salesOrderContributions[] = [ + 'case_sales_order_id' => $id, + 'to_be_invoiced' => $this->toBeInvoiced, + 'percent_value' => $this->percentValue, + 'contribution_id' => $contribution['id'], + ]; } - $totalAmount = $lineTotal + $taxAmount; - - $params = [ - 'source' => "Quotation {$this->id}", - 'line_item' => $allLineItems, - 'total_amount' => $totalAmount, - 'tax_amount' => $taxAmount, - 'financial_type_id' => $this->financialTypeId, - 'receive_date' => $this->date, - 'contact_id' => $salesOrderContribution->salesOrder['client_id'], - ]; - - $contribution = Contribution::create($params)->toArray(); - $this->postCreateAction($contribution['id']); - return $contribution; + + $this->postCreateAction($salesOrderContributions); } catch (\Exception $e) { $transaction->rollback(); throw $e; } - + return []; } /** @@ -151,19 +159,16 @@ public function removeStaleLineItems(array $salesOrder) { * * Also creates SalesOrdeContribution. * - * @param int $contributionId - * New contribution ID. + * @param array $salesOrderContributions + * Sales Order COntribution values. */ - public function postCreateAction($contributionId) { - Api4CaseSalesOrderContribution::create() - ->addValue('case_sales_order_id', $this->id) - ->addValue('to_be_invoiced', $this->toBeInvoiced) - ->addValue('percent_value', $this->percentValue) - ->addValue('contribution_id', $contributionId) - ->execute(); + public function postCreateAction(array $salesOrderContributions) { + $insert = CRM_Utils_SQL_Insert::into(\CRM_Civicase_BAO_CaseSalesOrderContribution::$_tableName) + ->rows($salesOrderContributions); + \CRM_Core_DAO::executeQuery($insert->toSQL()); CaseSalesOrder::update() - ->addWhere('id', '=', $this->id) + ->addWhere('id', 'IN', $this->ids) ->addValue('status_id', $this->statusId) ->execute(); } diff --git a/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html index b1dbceb71..c63fc9e8e 100644 --- a/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html @@ -1,7 +1,13 @@
- +
+
+

+ {{:: ts('Create Bulk Contribution for %1 Quotations', {1: $ctrl.ids.length}) }} +

+
+
{{$ctrl.completedMessage}}
@@ -72,6 +78,18 @@ Status is required
+ +
+
+ +
+
{{:: ts('Creating Contributions..') }}
+
+
+
+
+
+
diff --git a/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js index bd71b43e9..a1792ba76 100644 --- a/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js @@ -34,18 +34,25 @@ date: $.datepicker.formatDate('yy-mm-dd', new Date()) }; ctrl.salesOrderStatus = SalesOrderStatus.getAll(); + this.progress = null; + const BATCH_SIZE = 50; this.createBulkContribution = () => { $q(async function (resolve, reject) { ctrl.run = true; + ctrl.progress = 0; let contributionCreated = 0; - - for (const id of ctrl.ids) { + let index = 0; + const chunkedIds = _.chunk(ctrl.ids, BATCH_SIZE); + for (const ids of chunkedIds) { try { - await crmApi4('CaseSalesOrder', 'contributionCreateAction', { ...ctrl.data, id }); - contributionCreated++; + await crmApi4('CaseSalesOrder', 'contributionCreateAction', { ...ctrl.data, ids }); + contributionCreated += ids.length; } catch (error) { console.log(error); + } finally { + index += BATCH_SIZE; + ctrl.progress = (index * 100) / ctrl.ids.length; } } diff --git a/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderContributionTest.php b/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderContributionTest.php index ae7c8cb09..dfe7a1ed0 100644 --- a/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderContributionTest.php +++ b/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderContributionTest.php @@ -44,7 +44,7 @@ public function testCorrectNumberOfLineItemsIsGeneratedWithPreviousContribution( $previousContributionCount = rand(1, 4); for ($i = 0; $i < $previousContributionCount; $i++) { CaseSalesOrder::contributionCreateAction() - ->setId($salesOrder['id']) + ->setIds([$salesOrder['id']]) ->setStatusId(1) ->setToBeInvoiced(SalesOrderService::INVOICE_PERCENT) ->setPercentValue(20) diff --git a/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php b/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php index 235731324..ddafa22f1 100644 --- a/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php +++ b/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php @@ -29,11 +29,15 @@ public function setUp() { * Ensures contribution create action updates status successfully. */ public function testContributionCreateActionWillUpdateSalesOrderStatus() { - ['id' => $id] = $this->createCaseSalesOrder(); + $ids = []; + + for ($i = 0; $i < rand(5, 11); $i++) { + $ids[] = $this->createCaseSalesOrder()['id']; + } $newStatus = $this->getCaseSalesOrderStatus()[1]['value']; CaseSalesOrder::contributionCreateAction() - ->setId($id) + ->setIds($ids) ->setStatusId($newStatus) ->setToBeInvoiced('percent') ->setPercentValue('100') @@ -42,11 +46,47 @@ public function testContributionCreateActionWillUpdateSalesOrderStatus() { ->execute(); $results = CaseSalesOrder::get() - ->addWhere('id', '=', $id) + ->addWhere('id', 'IN', $ids) + ->execute(); + + $iterator = $results->getIterator(); + while ($iterator->valid()) { + $this->assertEquals($newStatus, $iterator->current()['status_id']); + $iterator->next(); + } + } + + /** + * Ensures contribution create action will create expeted pivot row. + * + * I.e. + * Inserts a new row for each of the case sales order contribution. + */ + public function testContributionCreateActionWillInsertPivotRow() { + $ids = []; + + for ($i = 0; $i < rand(5, 11); $i++) { + $ids[] = $this->createCaseSalesOrder()['id']; + } + + $newStatus = $this->getCaseSalesOrderStatus()[1]['value']; + CaseSalesOrder::contributionCreateAction() + ->setIds($ids) + ->setStatusId($newStatus) + ->setToBeInvoiced('percent') + ->setPercentValue('100') + ->setDate('2020-2-12') + ->setFinancialTypeId('1') + ->execute(); + + $salesOrderContributions = Api4CaseSalesOrderContribution::get() + ->addSelect('case_sales_order_id') + ->addWhere('case_sales_order_id.id', 'IN', $ids) ->execute() - ->first(); + ->jsonSerialize(); - $this->assertEquals($newStatus, $results['status_id']); + $this->assertCount(count($ids), $salesOrderContributions); + $this->assertEmpty(array_diff(array_column($salesOrderContributions, 'case_sales_order_id'), $ids)); } /** @@ -73,7 +113,7 @@ public function testAppropriateContributionAmountIsCreated($expectedPercent, $co foreach ($contributionCreateData as $data) { CaseSalesOrder::contributionCreateAction() - ->setId($salesOrder['id']) + ->setIds([$salesOrder['id']]) ->setStatusId($data['statusId']) ->setToBeInvoiced($data['toBeInvoiced']) ->setPercentValue($data['percentValue']) From 3964d4c091a6d95069b25297c971363fe8885812 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 3 Apr 2023 18:02:02 +0100 Subject: [PATCH 067/199] BTHAB-34: Refactor CreateAction API Separate the create action method into separate distinct methods --- .../ContributionCreateAction.php | 158 ++++++++++-------- 1 file changed, 84 insertions(+), 74 deletions(-) diff --git a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php index 38d3122a9..db651627f 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php @@ -8,12 +8,11 @@ use Civi\Api4\PriceSet; use CRM_Core_Transaction; use Civi\Api4\Generic\Result; -use Civi\Api4\CaseSalesOrderLine; use Civi\Api4\Generic\AbstractAction; use Civi\Api4\Generic\Traits\DAOActionTrait; use CRM_Contribute_BAO_Contribution as Contribution; use CRM_Civicase_Service_CaseSalesOrderContribution as CaseSalesOrderContribution; -use CRM_Utils_SQL_Insert; +use Civi\Api4\CaseSalesOrderContribution as Api4CaseSalesOrderContribution; /** * Creates contribution for multiple sales orders. @@ -77,98 +76,109 @@ public function _run(Result $result) { // phpcs:ignore */ protected function createContribution() { $transaction = CRM_Core_Transaction::create(); - $salesOrderContributions = []; - - try { - $priceSet = PriceSet::get() - ->addWhere('name', '=', 'default_contribution_amount') - ->addWhere('is_quick_config', '=', 1) - ->execute() - ->first(); - $priceField = PriceField::get() - ->addWhere('price_set_id', '=', $priceSet['id']) - ->addChain('price_field_value', PriceFieldValue::get() - ->addWhere('price_field_id', '=', '$id') - )->execute(); - foreach ($this->ids as $id) { - $salesOrderContribution = new CaseSalesOrderContribution($id, $this->toBeInvoiced, $this->percentValue); - $lineItems = $salesOrderContribution->generateLineItems(); - - $taxAmount = $lineTotal = 0; - $allLineItems = []; - foreach ($lineItems as $index => &$lineItem) { - $lineItem['price_field_id'] = $priceField[$index]['id']; - $lineItem['price_field_value_id'] = $priceField[$index]['price_field_value'][0]['id']; - $priceSetID = \CRM_Core_DAO::getFieldValue('CRM_Price_BAO_PriceField', $priceField[$index]['id'], 'price_set_id'); - $allLineItems[$priceSetID][$priceField[$index]['id']] = $lineItem; - $taxAmount += (float) ($lineItem['tax_amount'] ?? 0); - $lineTotal += (float) ($lineItem['line_total'] ?? 0); - } - $totalAmount = $lineTotal + $taxAmount; - - $params = [ - 'source' => "Quotation {$id}", - 'line_item' => $allLineItems, - 'total_amount' => $totalAmount, - 'tax_amount' => $taxAmount, - 'financial_type_id' => $this->financialTypeId, - 'receive_date' => $this->date, - 'contact_id' => $salesOrderContribution->salesOrder['client_id'], - ]; - - $contribution = Contribution::create($params)->toArray(); - $salesOrderContributions[] = [ - 'case_sales_order_id' => $id, - 'to_be_invoiced' => $this->toBeInvoiced, - 'percent_value' => $this->percentValue, - 'contribution_id' => $contribution['id'], - ]; - } + $priceField = $this->getDefaultContributionPriceField(); - $this->postCreateAction($salesOrderContributions); + foreach ($this->ids as $id) { + try { + $contribution = $this->createContributionWithLineItems($id, $priceField); + $this->linkCaseSalesOrderToContribution($id, $contribution['id']); + $this->updateCaseSalesOrderStatus($id); + } + catch (\Exception $e) { + $transaction->rollback(); + } } - catch (\Exception $e) { - $transaction->rollback(); - throw $e; - } return []; } /** - * Delete line items that have been detached. + * Creates sales order contribution with associated line items. * - * @param array $salesOrder - * Array of the salesorder to remove stale line items for. + * @param int $salesOrderId + * Sales Order ID. + * @param array $priceField + * Array of price fields. */ - public function removeStaleLineItems(array $salesOrder) { - if (empty($salesOrder['id'])) { - return; + public function createContributionWithLineItems(int $salesOrderId, array $priceField): array { + $salesOrderContribution = new CaseSalesOrderContribution($salesOrderId, $this->toBeInvoiced, $this->percentValue); + $lineItems = $salesOrderContribution->generateLineItems(); + + $taxAmount = $lineTotal = 0; + $allLineItems = []; + foreach ($lineItems as $index => &$lineItem) { + $lineItem['price_field_id'] = $priceField[$index]['id']; + $lineItem['price_field_value_id'] = $priceField[$index]['price_field_value'][0]['id']; + $priceSetID = \CRM_Core_DAO::getFieldValue('CRM_Price_BAO_PriceField', $priceField[$index]['id'], 'price_set_id'); + $allLineItems[$priceSetID][$priceField[$index]['id']] = $lineItem; + $taxAmount += (float) ($lineItem['tax_amount'] ?? 0); + $lineTotal += (float) ($lineItem['line_total'] ?? 0); } + $totalAmount = $lineTotal + $taxAmount; + + $params = [ + 'source' => "Quotation {$salesOrderId}", + 'line_item' => $allLineItems, + 'total_amount' => $totalAmount, + 'tax_amount' => $taxAmount, + 'financial_type_id' => $this->financialTypeId, + 'receive_date' => $this->date, + 'contact_id' => $salesOrderContribution->salesOrder['client_id'], + ]; + + return Contribution::create($params)->toArray(); + } - $lineItemsInUse = array_column($salesOrder['items'], 'id'); + /** + * Returns default contribution price set fields. + * + * @return array + * Array of price fields + */ + public function getDefaultContributionPriceField(): array { + $priceSet = PriceSet::get() + ->addWhere('name', '=', 'default_contribution_amount') + ->addWhere('is_quick_config', '=', 1) + ->execute() + ->first(); + + return PriceField::get() + ->addWhere('price_set_id', '=', $priceSet['id']) + ->addChain('price_field_value', PriceFieldValue::get() + ->addWhere('price_field_id', '=', '$id') + )->execute() + ->getArrayCopy(); + } - CaseSalesOrderLine::delete() - ->addWhere('sales_order_id', '=', $salesOrder['id']) - ->addWhere('id', 'NOT IN', $lineItemsInUse) + /** + * Links sales order with contirbution. + * + * This is done by inserting new row into the + * pivot table CaseSalesOrderContribution. + * + * @param int $salesOrderId + * Sales Order Id. + * @param int $contributionId + * Contribution ID. + */ + public function linkCaseSalesOrderToContribution(int $salesOrderId, int $contributionId): void { + Api4CaseSalesOrderContribution::create() + ->addValue('case_sales_order_id', $salesOrderId) + ->addValue('to_be_invoiced', $this->toBeInvoiced) + ->addValue('percent_value', $this->percentValue) + ->addValue('contribution_id', $contributionId) ->execute(); } /** * Updates Sales Order status. * - * Also creates SalesOrdeContribution. - * - * @param array $salesOrderContributions - * Sales Order COntribution values. + * @param int $salesOrderId + * Sales Order Id. */ - public function postCreateAction(array $salesOrderContributions) { - $insert = CRM_Utils_SQL_Insert::into(\CRM_Civicase_BAO_CaseSalesOrderContribution::$_tableName) - ->rows($salesOrderContributions); - \CRM_Core_DAO::executeQuery($insert->toSQL()); - + public function updateCaseSalesOrderStatus(int $salesOrderId): void { CaseSalesOrder::update() - ->addWhere('id', 'IN', $this->ids) + ->addWhere('id', '=', $salesOrderId) ->addValue('status_id', $this->statusId) ->execute(); } From ee708c8cf16f8f08e4c2c36351771ac0605caf0d Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 3 Apr 2023 18:18:29 +0100 Subject: [PATCH 068/199] BTHAB-34: Refactor CaseSalesOrderContribution service --- ...ribution.php => CaseSalesOrderLineItemsGenerator.php} | 9 ++++++++- .../Action/CaseSalesOrder/ContributionCreateAction.php | 4 ++-- ...Test.php => CaseSalesOrderLineItemsGeneratorTest.php} | 6 +++--- .../Api4/CaseSalesOrder/ContributionCreateActionTest.php | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) rename CRM/Civicase/Service/{CaseSalesOrderContribution.php => CaseSalesOrderLineItemsGenerator.php} (95%) rename tests/phpunit/CRM/Civicase/Service/{CaseSalesOrderContributionTest.php => CaseSalesOrderLineItemsGeneratorTest.php} (86%) diff --git a/CRM/Civicase/Service/CaseSalesOrderContribution.php b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php similarity index 95% rename from CRM/Civicase/Service/CaseSalesOrderContribution.php rename to CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php index 19f78e9bd..f49baf46b 100644 --- a/CRM/Civicase/Service/CaseSalesOrderContribution.php +++ b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php @@ -8,7 +8,7 @@ /** * Service class to generate sales order line items. */ -class CRM_Civicase_Service_CaseSalesOrderContribution { +class CRM_Civicase_Service_CaseSalesOrderLineItemsGenerator { const INVOICE_PERCENT = 'percent'; const INVOICE_REMAIN = 'remain'; @@ -24,6 +24,13 @@ class CRM_Civicase_Service_CaseSalesOrderContribution { * Constructs CaseSalesOrderContribution service. */ public function __construct(private int $salesOrderId, private string $type, private string $percentValue) { + $this->setSalesOrder(); + } + + /** + * Sets the sales order value. + */ + private function setSalesOrder(): void { $this->salesOrder = CaseSalesOrder::get() ->addSelect('*') ->addWhere('id', '=', $this->salesOrderId) diff --git a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php index db651627f..b8766e7c3 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php @@ -11,7 +11,7 @@ use Civi\Api4\Generic\AbstractAction; use Civi\Api4\Generic\Traits\DAOActionTrait; use CRM_Contribute_BAO_Contribution as Contribution; -use CRM_Civicase_Service_CaseSalesOrderContribution as CaseSalesOrderContribution; +use CRM_Civicase_Service_CaseSalesOrderLineItemsGenerator as salesOrderlineItemGenerator; use Civi\Api4\CaseSalesOrderContribution as Api4CaseSalesOrderContribution; /** @@ -101,7 +101,7 @@ protected function createContribution() { * Array of price fields. */ public function createContributionWithLineItems(int $salesOrderId, array $priceField): array { - $salesOrderContribution = new CaseSalesOrderContribution($salesOrderId, $this->toBeInvoiced, $this->percentValue); + $salesOrderContribution = new salesOrderlineItemGenerator($salesOrderId, $this->toBeInvoiced, $this->percentValue); $lineItems = $salesOrderContribution->generateLineItems(); $taxAmount = $lineTotal = 0; diff --git a/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderContributionTest.php b/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderLineItemsGeneratorTest.php similarity index 86% rename from tests/phpunit/CRM/Civicase/Service/CaseSalesOrderContributionTest.php rename to tests/phpunit/CRM/Civicase/Service/CaseSalesOrderLineItemsGeneratorTest.php index dfe7a1ed0..e63b960c3 100644 --- a/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderContributionTest.php +++ b/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderLineItemsGeneratorTest.php @@ -1,14 +1,14 @@ Date: Mon, 3 Apr 2023 19:50:19 +0100 Subject: [PATCH 069/199] BTHAB-34: pass ids to salesorder create action API as salesOrderIds --- .../Api4/Action/CaseSalesOrder/ContributionCreateAction.php | 4 ++-- .../directives/quotations-contribution-bulk.directive.js | 6 +++--- .../Api4/CaseSalesOrder/ContributionCreateActionTest.php | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php index b8766e7c3..a43c816cb 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php @@ -25,7 +25,7 @@ class ContributionCreateAction extends AbstractAction { * * @var array */ - protected $ids; + protected $salesOrderIds; /** * Sales order Status ID. @@ -78,7 +78,7 @@ protected function createContribution() { $transaction = CRM_Core_Transaction::create(); $priceField = $this->getDefaultContributionPriceField(); - foreach ($this->ids as $id) { + foreach ($this->salesOrderIds as $id) { try { $contribution = $this->createContributionWithLineItems($id, $priceField); $this->linkCaseSalesOrderToContribution($id, $contribution['id']); diff --git a/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js index a1792ba76..50d2064b6 100644 --- a/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js @@ -44,10 +44,10 @@ let contributionCreated = 0; let index = 0; const chunkedIds = _.chunk(ctrl.ids, BATCH_SIZE); - for (const ids of chunkedIds) { + for (const salesOrderIds of chunkedIds) { try { - await crmApi4('CaseSalesOrder', 'contributionCreateAction', { ...ctrl.data, ids }); - contributionCreated += ids.length; + await crmApi4('CaseSalesOrder', 'contributionCreateAction', { ...ctrl.data, salesOrderIds }); + contributionCreated += salesOrderIds.length; } catch (error) { console.log(error); } finally { diff --git a/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php b/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php index 1e4c597fa..dfac2154c 100644 --- a/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php +++ b/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php @@ -37,7 +37,7 @@ public function testContributionCreateActionWillUpdateSalesOrderStatus() { $newStatus = $this->getCaseSalesOrderStatus()[1]['value']; CaseSalesOrder::contributionCreateAction() - ->setIds($ids) + ->setSalesOrderIds($ids) ->setStatusId($newStatus) ->setToBeInvoiced('percent') ->setPercentValue('100') @@ -71,7 +71,7 @@ public function testContributionCreateActionWillInsertPivotRow() { $newStatus = $this->getCaseSalesOrderStatus()[1]['value']; CaseSalesOrder::contributionCreateAction() - ->setIds($ids) + ->setSalesOrderIds($ids) ->setStatusId($newStatus) ->setToBeInvoiced('percent') ->setPercentValue('100') @@ -113,7 +113,7 @@ public function testAppropriateContributionAmountIsCreated($expectedPercent, $co foreach ($contributionCreateData as $data) { CaseSalesOrder::contributionCreateAction() - ->setIds([$salesOrder['id']]) + ->setSalesOrderIds([$salesOrder['id']]) ->setStatusId($data['statusId']) ->setToBeInvoiced($data['toBeInvoiced']) ->setPercentValue($data['percentValue']) From a480f08bfb5fd614207432c29c773f00b52bd984 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 3 Apr 2023 19:52:20 +0100 Subject: [PATCH 070/199] BTHAB-34: Add more test case scenarios to the salesOrder lin item generator test Here we ensure the appropraite number of line items with the expected values are generated --- .../CaseSalesOrderLineItemsGeneratorTest.php | 67 +++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderLineItemsGeneratorTest.php b/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderLineItemsGeneratorTest.php index e63b960c3..ba092253e 100644 --- a/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderLineItemsGeneratorTest.php +++ b/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderLineItemsGeneratorTest.php @@ -4,7 +4,7 @@ use CRM_Civicase_Service_CaseSalesOrderLineItemsGenerator as SalesOrderService; /** - * Runs tests on CRM_Civicase_Service_CaseSalesOrderLineItemsGenerator Service tests. + * Runs tests on CRM_Civicase_Service_CaseSalesOrderLineItemsGenerator tests. * * @group headless */ @@ -20,7 +20,7 @@ public function setUp() { } /** - * Ensures the correct number of line item is created. + * Ensures the correct number of line item is generated. * * When there's no previous contribution. */ @@ -34,7 +34,7 @@ public function testCorrectNumberOfLineItemsIsGeneratedWithoutPreviousContributi } /** - * Ensures the correct number of line item is created. + * Ensures the correct number of line item is generated. * * When there's previous contribution. */ @@ -44,7 +44,7 @@ public function testCorrectNumberOfLineItemsIsGeneratedWithPreviousContribution( $previousContributionCount = rand(1, 4); for ($i = 0; $i < $previousContributionCount; $i++) { CaseSalesOrder::contributionCreateAction() - ->setIds([$salesOrder['id']]) + ->setSalesOrderIds([$salesOrder['id']]) ->setStatusId(1) ->setToBeInvoiced(SalesOrderService::INVOICE_PERCENT) ->setPercentValue(20) @@ -59,4 +59,63 @@ public function testCorrectNumberOfLineItemsIsGeneratedWithPreviousContribution( $this->assertCount(($previousContributionCount * 2) + 2, $lineItems); } + /** + * Ensures the correct number of line item is generated. + * + * When there's discount with the right value. + */ + public function testCorrectNumberOfLineItemsIsGeneratedWithDiscount() { + $percent = 20; + $salesOrder = $this->getCaseSalesOrderData(); + $salesOrder['items'][] = $this->getCaseSalesOrderLineData(['discounted_percentage' => $percent]); + $salesOrder['id'] = CaseSalesOrder::save() + ->addRecord($salesOrder) + ->execute() + ->jsonSerialize()[0]['id']; + + $salesOrderService = new SalesOrderService($salesOrder['id'], SalesOrderService::INVOICE_REMAIN, 0); + $lineItems = $salesOrderService->generateLineItems(); + + usort($lineItems, fn($a, $b) => $a['line_total'] <=> $b['line_total']); + + $this->assertCount(2, $lineItems); + $this->assertEquals(-1 * $salesOrder['items'][0]['subtotal_amount'] * $percent / 100, $lineItems[0]['line_total']); + } + + /** + * Ensures the correct number of line item is generated. + * + * When the discount value is zero and the value is as expected. + */ + public function testCorrectNumberOfLineItemsIsGeneratedWhenDiscountIsZero() { + $percent = 0; + $salesOrder = $this->getCaseSalesOrderData(); + $salesOrder['items'][] = $this->getCaseSalesOrderLineData(['discounted_percentage' => $percent]); + $salesOrder['id'] = CaseSalesOrder::save() + ->addRecord($salesOrder) + ->execute() + ->jsonSerialize()[0]['id']; + + $salesOrderService = new SalesOrderService($salesOrder['id'], SalesOrderService::INVOICE_REMAIN, 0); + $lineItems = $salesOrderService->generateLineItems(); + + $this->assertCount(1, $lineItems); + $this->assertEquals($salesOrder['items'][0]['subtotal_amount'], $lineItems[0]['line_total']); + } + + /** + * Ensures the value of the generated line item is correct. + */ + public function testGeneratedPercentLineItemHasTheAppropraiteValue() { + $percent = rand(20, 40); + $salesOrder = $this->createCaseSalesOrder(); + + $salesOrderService = new SalesOrderService($salesOrder['id'], SalesOrderService::INVOICE_PERCENT, $percent); + $lineItems = $salesOrderService->generateLineItems(); + + $this->assertCount(2, $lineItems); + $this->assertEquals($salesOrder['items'][0]['subtotal_amount'] * $percent / 100, $lineItems[0]['line_total']); + $this->assertEquals($salesOrder['items'][1]['subtotal_amount'] * $percent / 100, $lineItems[1]['line_total']); + } + } From e70d813bd62baf82a84c051acc6f76fb5dc6fb0b Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 4 Apr 2023 13:28:06 +0100 Subject: [PATCH 071/199] BTHAB-34: Refactor method name and also encapsulate non public methods --- .../CaseSalesOrder/ContributionCreateAction.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php index a43c816cb..9acd23f15 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php @@ -74,9 +74,9 @@ public function _run(Result $result) { // phpcs:ignore /** * {@inheritDoc} */ - protected function createContribution() { + private function createContribution() { $transaction = CRM_Core_Transaction::create(); - $priceField = $this->getDefaultContributionPriceField(); + $priceField = $this->getDefaultPriceSetFields(); foreach ($this->salesOrderIds as $id) { try { @@ -100,7 +100,7 @@ protected function createContribution() { * @param array $priceField * Array of price fields. */ - public function createContributionWithLineItems(int $salesOrderId, array $priceField): array { + private function createContributionWithLineItems(int $salesOrderId, array $priceField): array { $salesOrderContribution = new salesOrderlineItemGenerator($salesOrderId, $this->toBeInvoiced, $this->percentValue); $lineItems = $salesOrderContribution->generateLineItems(); @@ -135,7 +135,7 @@ public function createContributionWithLineItems(int $salesOrderId, array $priceF * @return array * Array of price fields */ - public function getDefaultContributionPriceField(): array { + private function getDefaultPriceSetFields(): array { $priceSet = PriceSet::get() ->addWhere('name', '=', 'default_contribution_amount') ->addWhere('is_quick_config', '=', 1) @@ -161,7 +161,7 @@ public function getDefaultContributionPriceField(): array { * @param int $contributionId * Contribution ID. */ - public function linkCaseSalesOrderToContribution(int $salesOrderId, int $contributionId): void { + private function linkCaseSalesOrderToContribution(int $salesOrderId, int $contributionId): void { Api4CaseSalesOrderContribution::create() ->addValue('case_sales_order_id', $salesOrderId) ->addValue('to_be_invoiced', $this->toBeInvoiced) @@ -176,7 +176,7 @@ public function linkCaseSalesOrderToContribution(int $salesOrderId, int $contrib * @param int $salesOrderId * Sales Order Id. */ - public function updateCaseSalesOrderStatus(int $salesOrderId): void { + private function updateCaseSalesOrderStatus(int $salesOrderId): void { CaseSalesOrder::update() ->addWhere('id', '=', $salesOrderId) ->addValue('status_id', $this->statusId) From a1a1ba695692cd2109f1200cbf2474fb4c5253a1 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 4 Apr 2023 13:28:56 +0100 Subject: [PATCH 072/199] BTHAB-34: Only rollback transaction for individual sales order contribution --- Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php index 9acd23f15..61dd4674c 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php @@ -75,10 +75,10 @@ public function _run(Result $result) { // phpcs:ignore * {@inheritDoc} */ private function createContribution() { - $transaction = CRM_Core_Transaction::create(); $priceField = $this->getDefaultPriceSetFields(); foreach ($this->salesOrderIds as $id) { + $transaction = CRM_Core_Transaction::create(); try { $contribution = $this->createContributionWithLineItems($id, $priceField); $this->linkCaseSalesOrderToContribution($id, $contribution['id']); From bcd7c1643935349acb22f128df9626c9604897c4 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 3 Apr 2023 15:18:07 +0100 Subject: [PATCH 073/199] BTHAB-48: Add page to list quotation invoices --- ang/afsearchQuotationInvoices.aff.html | 3 +++ ang/afsearchQuotationInvoices.aff.json | 16 +++++++++++++ ang/civicase-features/app.routes.js | 8 +++++++ .../directives/invoices-list.directive.html | 8 +++++++ .../directives/invoices-list.directive.js | 23 +++++++++++++++++++ 5 files changed, 58 insertions(+) create mode 100644 ang/afsearchQuotationInvoices.aff.html create mode 100644 ang/afsearchQuotationInvoices.aff.json create mode 100644 ang/civicase-features/invoices/directives/invoices-list.directive.html create mode 100644 ang/civicase-features/invoices/directives/invoices-list.directive.js diff --git a/ang/afsearchQuotationInvoices.aff.html b/ang/afsearchQuotationInvoices.aff.html new file mode 100644 index 000000000..818179830 --- /dev/null +++ b/ang/afsearchQuotationInvoices.aff.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/ang/afsearchQuotationInvoices.aff.json b/ang/afsearchQuotationInvoices.aff.json new file mode 100644 index 000000000..494c1f075 --- /dev/null +++ b/ang/afsearchQuotationInvoices.aff.json @@ -0,0 +1,16 @@ +{ + "type": "search", + "requires": [], + "title": "Quotation Invoice", + "description": "", + "is_dashlet": false, + "is_public": false, + "is_token": false, + "server_route": "", + "permission": "access CiviCRM", + "entity_type": null, + "join_entity": null, + "contact_summary": null, + "redirect": null, + "create_submission": null +} diff --git a/ang/civicase-features/app.routes.js b/ang/civicase-features/app.routes.js index 739a6b1d0..56645284a 100644 --- a/ang/civicase-features/app.routes.js +++ b/ang/civicase-features/app.routes.js @@ -18,5 +18,13 @@ `; } }); + $routeProvider.when('/invoices', { + template: function () { + var urlParams = UrlParametersProvider.parse(window.location.search); + return ` + + `; + } + }); }); })(angular, CRM.$, CRM._); diff --git a/ang/civicase-features/invoices/directives/invoices-list.directive.html b/ang/civicase-features/invoices/directives/invoices-list.directive.html new file mode 100644 index 000000000..c34641c6d --- /dev/null +++ b/ang/civicase-features/invoices/directives/invoices-list.directive.html @@ -0,0 +1,8 @@ +
+

{{ ts('Quotation Invoices') }}

+
+
+ +
+
+
diff --git a/ang/civicase-features/invoices/directives/invoices-list.directive.js b/ang/civicase-features/invoices/directives/invoices-list.directive.js new file mode 100644 index 000000000..41751d97a --- /dev/null +++ b/ang/civicase-features/invoices/directives/invoices-list.directive.js @@ -0,0 +1,23 @@ +(function (angular, $, _) { + var module = angular.module('civicase-features'); + + module.directive('invoicesList', function () { + return { + restrict: 'E', + controller: 'invoicesListController', + templateUrl: '~/civicase-features/invoices/directives/invoices-list.directive.html', + scope: {} + }; + }); + + module.controller('invoicesListController', invoicesListController); + + /** + * @param {object} $scope the controller scope + * @param {object} $location the location service + * @param {object} $window window object of the browser + */ + function invoicesListController ($scope, $location, $window) { + + } +})(angular, CRM._); From b7306cd959e0f5311a8f5c2f9016f29ec489dd1e Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 3 Apr 2023 15:22:37 +0100 Subject: [PATCH 074/199] BTHAB-48: Display invoice on civicase instance tab when enabled --- CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php | 1 + 1 file changed, 1 insertion(+) diff --git a/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php b/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php index b835384c1..a3c4ec6e8 100644 --- a/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php +++ b/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php @@ -15,6 +15,7 @@ class CRM_Civicase_Hook_NavigationMenu_CaseInstanceFeaturesMenu extends CRM_Civi */ const FEATURES_WITH_MENU = [ 'quotations', + 'invoices', ]; /** From ac0b8f1ec5a14300f1460946ed44d97f1943da62 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 3 Apr 2023 15:27:05 +0100 Subject: [PATCH 075/199] BTHAB-48: Add Invoice tab to case instance dashboard --- ang/civicase-features.ang.php | 14 +++--- .../directives/invoices-case-tab-content.html | 3 ++ .../services/quotations-case-tab.service.js | 17 ++++++++ .../configs/add-features-tab.config.js} | 43 +++++++++++-------- 4 files changed, 54 insertions(+), 23 deletions(-) create mode 100644 ang/civicase-features/invoices/directives/invoices-case-tab-content.html create mode 100644 ang/civicase-features/invoices/services/quotations-case-tab.service.js rename ang/civicase-features/{quotations/configs/add-quotations-tab.config.js => shared/configs/add-features-tab.config.js} (55%) diff --git a/ang/civicase-features.ang.php b/ang/civicase-features.ang.php index e638009d0..13ab14294 100644 --- a/ang/civicase-features.ang.php +++ b/ang/civicase-features.ang.php @@ -17,7 +17,7 @@ set_currency_codes($options); set_case_sales_order_status($options); -set_case_types_with_quotations_enabled($options); +set_case_types_with_features_enabled($options); /** * Get a list of JS files. @@ -45,12 +45,15 @@ function set_currency_codes(&$options) { } /** - * Exposes Case types that have quotations enabled to Angular. + * Exposes Case types that have features enabled to Angular. */ -function set_case_types_with_quotations_enabled(&$options) { +function set_case_types_with_features_enabled(&$options) { $caseTypeCategoryFeatures = new CaseTypeCategoryFeatures(); - $caseTypeCategories = $caseTypeCategoryFeatures->retrieveCaseInstanceWithEnabledFeatures(['quotations']); - $options['featureCaseTypes']['quotations'] = array_keys($caseTypeCategories); + + array_map(function ($feature) use ($caseTypeCategoryFeatures, &$options) { + $caseTypeCategories = $caseTypeCategoryFeatures->retrieveCaseInstanceWithEnabledFeatures([$feature]); + $options['featureCaseTypes'][$feature] = array_keys($caseTypeCategories); + }, ['quotations', 'invoices']); } /** @@ -73,6 +76,7 @@ function set_case_sales_order_status(&$options) { 'civicase-base', 'afsearchQuotations', 'afsearchContactQuotations', + 'afsearchQuotationInvoices', ]; return [ diff --git a/ang/civicase-features/invoices/directives/invoices-case-tab-content.html b/ang/civicase-features/invoices/directives/invoices-case-tab-content.html new file mode 100644 index 000000000..2b392f829 --- /dev/null +++ b/ang/civicase-features/invoices/directives/invoices-case-tab-content.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/ang/civicase-features/invoices/services/quotations-case-tab.service.js b/ang/civicase-features/invoices/services/quotations-case-tab.service.js new file mode 100644 index 000000000..1773d6030 --- /dev/null +++ b/ang/civicase-features/invoices/services/quotations-case-tab.service.js @@ -0,0 +1,17 @@ +(function (angular, $, _) { + var module = angular.module('civicase'); + + module.service('InvoicesCaseTab', InvoicesCaseTab); + + /** + * Invoices Case Tab service. + */ + function InvoicesCaseTab () { + /** + * @returns {string} Returns tab content HTMl template url. + */ + this.activeTabContentUrl = function () { + return '~/civicase-features/invoices/directives/invoices-case-tab-content.html'; + }; + } +})(angular, CRM.$, CRM._); diff --git a/ang/civicase-features/quotations/configs/add-quotations-tab.config.js b/ang/civicase-features/shared/configs/add-features-tab.config.js similarity index 55% rename from ang/civicase-features/quotations/configs/add-quotations-tab.config.js rename to ang/civicase-features/shared/configs/add-features-tab.config.js index 1a38bea4b..366fa75a4 100644 --- a/ang/civicase-features/quotations/configs/add-quotations-tab.config.js +++ b/ang/civicase-features/shared/configs/add-features-tab.config.js @@ -1,21 +1,26 @@ (function (angular) { const module = angular.module('civicase-features'); - const FEATURE_NAME = 'quotations'; module.config(function ($windowProvider, tsProvider, CaseDetailsTabsProvider) { - var $window = $windowProvider.$get(); - var ts = tsProvider.$get(); - var quotationsTab = { - name: 'Quotations', - label: ts('Quotations'), - weight: 100 - }; + const $window = $windowProvider.$get(); + const ts = tsProvider.$get(); + const featuresTab = [ + { + name: 'Quotations', + label: ts('Quotations'), + weight: 100 + }, { + name: 'Invoices', + label: ts('Invoices'), + weight: 110 + } + ]; - if (caseTypeCategoryHasQuotationEnabled()) { - CaseDetailsTabsProvider.addTabs([ - quotationsTab - ]); - } + featuresTab.forEach(feature => { + if (caseTypeCategoryHasFeatureEnabled(feature.name)) { + CaseDetailsTabsProvider.addTabs([feature]); + } + }); /** * Returns the current case type category parameter. This is used instead of @@ -25,9 +30,9 @@ * @returns {string|null} the name of the case type category, or null. */ function getCaseTypeCategory () { - var urlParamRegExp = /case_type_category=([^&]+)/i; - var currentSearch = decodeURIComponent($window.location.search); - var results = urlParamRegExp.exec(currentSearch); + const urlParamRegExp = /case_type_category=([^&]+)/i; + const currentSearch = decodeURIComponent($window.location.search); + const results = urlParamRegExp.exec(currentSearch); return results && results[1]; } @@ -36,11 +41,13 @@ * Returns true if the current case type category has quotations * features enabled * + * @param {string} feature THe name of the feature + * * @returns {boolean} true if quotations is enabled, otherwise false */ - function caseTypeCategoryHasQuotationEnabled () { + function caseTypeCategoryHasFeatureEnabled (feature) { const caseTypeCategory = parseInt(getCaseTypeCategory()); - const quotationCaseTypeCategories = CRM['civicase-features'].featureCaseTypes[FEATURE_NAME] || []; + const quotationCaseTypeCategories = CRM['civicase-features'].featureCaseTypes[feature.toLocaleLowerCase()] || []; if (Array.isArray(quotationCaseTypeCategories) && caseTypeCategory) { return quotationCaseTypeCategories.includes(caseTypeCategory); } From 3b5fbaf2eebbe43a1cf8d02fd2e2ebd2020e6373 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 5 Apr 2023 09:54:49 +0100 Subject: [PATCH 076/199] BTHAB-33: Initiliaze new Contibution page with sales order line items --- .../Form/CaseSalesOrderContributionCreate.php | 91 ++++++++++++++++--- .../AddSalesOrderLineItemsToContribution.php | 62 +++++++++++++ civicase.php | 1 + js/sales-order-contribution.js | 72 +++++++++++++++ 4 files changed, 213 insertions(+), 13 deletions(-) create mode 100644 CRM/Civicase/Hook/BuildForm/AddSalesOrderLineItemsToContribution.php create mode 100644 js/sales-order-contribution.js diff --git a/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php index ebef9d1c5..493ed7934 100644 --- a/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php +++ b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php @@ -1,5 +1,7 @@ addElement('radio', 'to_be_invoiced', '', ts('Enter % to be invoiced ?'), - 'percent', [ + self::INVOICE_PERCENT, [ 'id' => 'invoice_percent', ]); $this->add('text', 'percent_value', '', [ @@ -39,10 +44,10 @@ public function buildQuickForm() { 'placeholder' => 'Percentage to be invoiced', 'class' => 'form-control', 'min' => 1, - 'max' => 10, + 'max' => 100, ], FALSE); $this->addElement('radio', 'to_be_invoiced', '', ts('Remaining Balance'), - 'remain', + self::INVOICE_REMAIN, ['id' => 'invoice_remain'] ); $this->addRule('to_be_invoiced', ts('Invoice value is required'), 'required'); @@ -52,19 +57,16 @@ public function buildQuickForm() { ->addWhere('option_group_id:name', '=', 'case_sales_order_status') ->execute() ->getArrayCopy(); - array_combine(array_column($statusOptions, 'label'), array_column($statusOptions, 'value')); $this->add( 'select', 'status', ts('Status'), - array_merge( - ['' => 'Select'], + ['' => 'Select'] + array_combine( array_column($statusOptions, 'value'), array_column($statusOptions, 'label') ), - ), TRUE, ['class' => 'form-control'] ); @@ -89,6 +91,7 @@ public function buildQuickForm() { */ public function addRules() { $this->addFormRule([$this, 'formRule']); + $this->addFormRule([$this, 'validateAmount']); } /** @@ -96,7 +99,7 @@ public function addRules() { * * This enforces the rule whereby, * user must supply an amount if the - * enter percentage smount radio is selected. + * enter percentage amount radio is selected. * * @param array $values * Array of submitted values. @@ -107,10 +110,70 @@ public function addRules() { public function formRule(array $values) { $errors = []; - if ($values['to_be_invoiced'] == 'percent' && empty(floatval($values['percent_value']))) { + if ($values['to_be_invoiced'] == self::INVOICE_PERCENT && empty(floatval($values['percent_value']))) { $errors['percent_value'] = 'Percentage value is required'; } + if ($values['to_be_invoiced'] == self::INVOICE_PERCENT && $values['percent_value'] > 100) { + $errors['percent_value'] = 'Percentage value cannot exceed 100 '; + } + + return $errors ?: TRUE; + } + + /** + * Validate Invoice value. + * + * Ensures that percent amount entered by user or + * calculated as part of other remaining balance + * selection is correct and not exceeding the + * balance amount. + * + * e.g. If a sales_order total_amount is 1000, + * and has the following contributions + * contribution 1 with value - 500 + * contribution 2 with value - 250 + * the amount owed is 250, so any new contribution + * that will exceed this amount should return an error. + * + * @param array $values + * Array of submitted values. + * + * @return array|bool + * Returns the form errors if form is invalid + */ + public function validateAmount(array $values) { + $errors = []; + + $caseSalesOrder = CaseSalesOrder::get() + ->addSelect('total_after_tax') + ->addWhere('id', '=', $this->id) + ->setLimit(1) + ->execute() + ->first(); + if (empty($caseSalesOrder)) { + throw new CRM_Core_Exception("The specified case sales order doesn't exist"); + } + + // Get all the previous contributions. + $caseSalesOrderContributions = CaseSalesOrderContribution::get() + ->addSelect('contribution_id', 'contribution_id.total_amount') + ->addWhere('case_sales_order_id.id', '=', $this->id) + ->execute() + ->jsonSerialize(); + + $paidTotal = array_sum(array_column($caseSalesOrderContributions, 'contribution_id.total_amount')); + $paidPercent = ($paidTotal * 100) / $caseSalesOrder['total_after_tax']; + $remainPercent = 100 - $paidPercent; + + if ($remainPercent <= 0) { + $errors['to_be_invoiced'] = 'Contribution amount cannot exceed the total sales order amount'; + } + + if ($values['to_be_invoiced'] == self::INVOICE_PERCENT && $values['percent_value'] > $remainPercent) { + $errors['percent_value'] = 'Percentage value cannot exceed ' . round($remainPercent, 2); + } + return $errors ?: TRUE; } @@ -120,8 +183,8 @@ public function formRule(array $values) { public function postProcess() { $values = $this->getSubmitValues(); - if (!empty($this->id) && $values['to_be_invoiced'] == 'percent') { - $this->createPercentageContribution($values); + if (!empty($this->id) && !empty($values['to_be_invoiced'])) { + $this->createContribution($values); } } @@ -131,14 +194,16 @@ public function postProcess() { * This contribution page will have the line items * prefilled from the sales order line items. */ - public function createPercentageContribution(array $values) { + public function createContribution(array $values) { $query = [ 'action' => 'add', 'reset' => 1, 'context' => 'standalone', 'sales_order' => $this->id, 'sales_order_status_id' => $values['status'], - 'percent_amount' => floatval($values['percent_value']), + 'to_be_invoiced' => $values['to_be_invoiced'], + 'percent_value' => $values['to_be_invoiced'] == + self::INVOICE_PERCENT ? floatval($values['percent_value']) : 0, ]; $url = CRM_Utils_System::url('civicrm/contribute/add', $query); diff --git a/CRM/Civicase/Hook/BuildForm/AddSalesOrderLineItemsToContribution.php b/CRM/Civicase/Hook/BuildForm/AddSalesOrderLineItemsToContribution.php new file mode 100644 index 000000000..50c5c8bbd --- /dev/null +++ b/CRM/Civicase/Hook/BuildForm/AddSalesOrderLineItemsToContribution.php @@ -0,0 +1,62 @@ +shouldRun($form, $formName, $salesOrderId)) { + return; + } + $lineItemGenerator = new salesOrderlineItemGenerator($salesOrderId, $toBeInvoiced, $percentValue); + $lineItems = $lineItemGenerator->generateLineItems(); + + CRM_Core_Resources::singleton() + ->addScriptFile(E::LONG_NAME, 'js/sales-order-contribution.js') + ->addVars(E::LONG_NAME, [ + 'sales_order' => $salesOrderId, + 'sales_order_status_id' => $status, + 'to_be_invoiced' => $toBeInvoiced, + 'percent_value' => $percentValue, + 'line_items' => json_encode($lineItems), + ]); + } + + /** + * Determines if the hook will run. + * + * This hook is only valid for the Case form. + * + * The civicase client id parameter must be defined. + * + * @param CRM_Core_Form $form + * Form class. + * @param string $formName + * Form Name. + * @param int|null $salesOrderId + * Sales Order ID. + */ + public function shouldRun(CRM_Core_Form $form, string $formName, ?int $salesOrderId) { + return $formName === 'CRM_Contribute_Form_Contribution' + && $form->_action == CRM_Core_Action::ADD + && !empty($salesOrderId); + } + +} diff --git a/civicase.php b/civicase.php index 2d4828d6a..743014918 100644 --- a/civicase.php +++ b/civicase.php @@ -187,6 +187,7 @@ function civicase_civicrm_buildForm($formName, &$form) { new CRM_Civicase_Hook_BuildForm_AddScriptToCreatePdfForm(), new CRM_Civicase_Hook_BuildForm_AddCaseCategoryFeaturesField(), new CRM_Civicase_Hook_BuildForm_AddQuotationsNotesToContributionSettings(), + new CRM_Civicase_Hook_BuildForm_AddSalesOrderLineItemsToContribution(), ]; foreach ($hooks as $hook) { diff --git a/js/sales-order-contribution.js b/js/sales-order-contribution.js new file mode 100644 index 000000000..338b1ccc2 --- /dev/null +++ b/js/sales-order-contribution.js @@ -0,0 +1,72 @@ +(function ($, _) { + $(document).one('crmLoad', function () { + const params = CRM.vars['uk.co.compucorp.civicase']; + const salesOrderId = params.sales_order; + const salesOrderStatusId = params.sales_order_status_id; + const percentValue = params.percent_value; + const toBeInvoiced = params.to_be_invoiced; + const lineItems = JSON.parse(params.line_items); + let count = 0; + + const apiRequest = {}; + apiRequest.caseSalesOrders = ['CaseSalesOrder', 'get', { + select: ['*'], + where: [['id', '=', salesOrderId]] + }]; + apiRequest.optionValues = ['OptionValue', 'get', { + select: ['value'], + where: [['option_group_id:name', '=', 'contribution_status'], ['name', '=', 'pending']] + }]; + + if (Array.isArray(lineItems)) { + CRM.api4(apiRequest).then(function (batch) { + const caseSalesOrder = batch.caseSalesOrders[0]; + + lineItems.forEach(lineItem => { + addLineItem(lineItem.qty, lineItem.unit_price, lineItem.label, lineItem.financial_type_id, lineItem.tax_amount); + }); + + $('#total_amount').val(0); + $('#lineitem-add-block').show().removeClass('hiddenElement'); + $('#contribution_status_id').val(batch.optionValues[0].value); + $('#source').val(`Quotation ${caseSalesOrder.id}`).trigger('change'); + $('#contact_id').select2('val', caseSalesOrder.client_id).trigger('change'); + $('input[id="total_amount"]', 'form.CRM_Contribute_Form_Contribution').trigger('change'); + $(``).insertBefore('#source'); + $(``).insertBefore('#source'); + $(``).insertBefore('#source'); + $(``).insertBefore('#source'); + $('#totalAmount, #totalAmountORaddLineitem, #totalAmountORPriceSet, #price_set_id, #choose-manual, .remove_item, #add-another-item').hide(); + }); + } + + $("a[target='crm-popup']").on('crmPopupFormSuccess', function (e) { + CRM.refreshParent(e); + }); + + /** + * @param {number} quantity Item quantity + * @param {number} unitPrice Item unit price + * @param {string} description Item description + * @param {number} financialTypeId Item financial type + * @param {number|object} taxAmount Item tax amount + */ + function addLineItem (quantity, unitPrice, description, financialTypeId, taxAmount) { + const row = $($(`tr#add-item-row-${count}`)); + row.show().removeClass('hiddenElement'); + + $('input[id^="item_label"]', row).val(ts(description)); + $('select[id^="item_financial_type_id"]', row).select2('val', financialTypeId); + $('input[id^="item_qty"]', row).val(quantity); + + const total = quantity * parseFloat(unitPrice); + + $('input[id^="item_unit_price"]', row).val(CRM.formatMoney(unitPrice, true)); + $('input[id^="item_line_total"]', row).val(CRM.formatMoney(total, true)); + + $('input[id^="item_tax_amount"]', row).val(CRM.formatMoney(taxAmount, true)); + + count++; + } + }); +})(CRM.$, CRM._); From 6c0f4b3edcdeeb0202b5c39c177440afba4827f4 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 5 Apr 2023 09:56:49 +0100 Subject: [PATCH 077/199] BTHAB-33: Link sales order with contribution and update sales order i.e. After creating contribution for sales order --- .../Post/CreateSalesOrderContribution.php | 70 +++++++++++++++++++ civicase.php | 1 + 2 files changed, 71 insertions(+) create mode 100644 CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php diff --git a/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php b/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php new file mode 100644 index 000000000..e0bfdbcda --- /dev/null +++ b/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php @@ -0,0 +1,70 @@ +shouldRun($op, $objectName, $salesOrderId)) { + return; + } + + $transaction = CRM_Core_Transaction::create(); + try { + CaseSalesOrderContribution::create() + ->addValue('case_sales_order_id', $salesOrderId) + ->addValue('to_be_invoiced', $toBeInvoiced) + ->addValue('percent_value', $percentValue) + ->addValue('contribution_id', $objectId) + ->execute(); + + CaseSalesOrder::update() + ->addWhere('id', '=', $salesOrderId) + ->addValue('status_id', $salesOrderStatusId) + ->execute(); + } + catch (\Throwable $th) { + $transaction->rollback(); + CRM_Core_Error::statusBounce(ts('Error creating sales order contribution')); + } + } + + /** + * Determines if the hook should run or not. + * + * @param string $op + * The operation being performed. + * @param string $objectName + * Object name. + * @param string $salesOrderId + * The sales order that triggered the contribution (if any). + * + * @return bool + * returns a boolean to determine if hook will run or not. + */ + private function shouldRun($op, $objectName, $salesOrderId) { + return strtolower($objectName) == 'contribution' && !empty($salesOrderId) && $op == 'create'; + } + +} diff --git a/civicase.php b/civicase.php index 743014918..cb12a929e 100644 --- a/civicase.php +++ b/civicase.php @@ -281,6 +281,7 @@ function civicase_civicrm_validateForm($formName, &$fields, &$files, &$form, &$e */ function civicase_civicrm_post($op, $objectName, $objectId, &$objectRef) { $hooks = [ + new CRM_Civicase_Hook_Post_CreateSalesOrderContribution(), new CRM_Civicase_Hook_Post_PopulateCaseCategoryForCaseType(), new CRM_Civicase_Hook_Post_CaseCategoryCustomGroupSaver(), new CRM_Civicase_Hook_Post_UpdateCaseTypeListForCaseCategoryCustomGroup(), From cad67ccf5cfc557ad1b79c23cd47a4bc45b7badb Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 14 Apr 2023 09:07:30 +0100 Subject: [PATCH 078/199] BTHAB-101: Fix browser navigating to case dashboard on datepicker day click --- .../directives/quotations-list.directive.js | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/ang/civicase-features/quotations/directives/quotations-list.directive.js b/ang/civicase-features/quotations/directives/quotations-list.directive.js index 65d9bac4f..949aa6238 100644 --- a/ang/civicase-features/quotations/directives/quotations-list.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-list.directive.js @@ -1,4 +1,4 @@ -(function (angular, _) { +(function (angular, _, $) { var module = angular.module('civicase-features'); module.directive('quotationsList', function () { @@ -27,9 +27,12 @@ if ($scope.contactId) { $location.search().cid = $scope.contactId; } + + preventDatePickerNavigation(); }()); + /** - * Redirect user to new quotation screen + * Redirects user to new quotation screen */ function redirectToQuotationCreationScreen () { let url = '/civicrm/case-features/a#/quotations/new'; @@ -40,5 +43,21 @@ $window.location.href = url; } + + /** + * Prevents date picker from triggering route navigation. + */ + function preventDatePickerNavigation () { + const observer = new window.MutationObserver(function (mutations) { + if ($('#ui-datepicker-div:visible a').length) { + $('#ui-datepicker-div:visible a').click((event) => { event.preventDefault(); }); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + } } -})(angular, CRM._); +})(angular, CRM._, CRM.$); From 7227a4df99f42a52d21a2ee75cdbff0081a3172c Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 14 Apr 2023 17:06:06 +0100 Subject: [PATCH 079/199] BTHAB-101: Reduce field field sizes and reduce show filter button toggle --- ang/afsearchContactQuotations.aff.html | 38 ++++++---------- ang/afsearchQuotations.aff.html | 44 +++++++------------ .../directives/quotations-list.directive.html | 14 ++++++ 3 files changed, 45 insertions(+), 51 deletions(-) diff --git a/ang/afsearchContactQuotations.aff.html b/ang/afsearchContactQuotations.aff.html index 82538824a..53afb9ef9 100644 --- a/ang/afsearchContactQuotations.aff.html +++ b/ang/afsearchContactQuotations.aff.html @@ -1,30 +1,20 @@
-
- - +
+ + + +
+
+ +
- - -
-
- - + +
- diff --git a/ang/afsearchQuotations.aff.html b/ang/afsearchQuotations.aff.html index fdf746f7a..206158c76 100644 --- a/ang/afsearchQuotations.aff.html +++ b/ang/afsearchQuotations.aff.html @@ -1,33 +1,23 @@
-
- - +
+ + + + +
+
+ +
+
- - - -
-
- - + +
- + diff --git a/ang/civicase-features/quotations/directives/quotations-list.directive.html b/ang/civicase-features/quotations/directives/quotations-list.directive.html index cb3e2e95c..276490d8b 100644 --- a/ang/civicase-features/quotations/directives/quotations-list.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-list.directive.html @@ -18,3 +18,17 @@

{{ ts('Manage Quotations') }}

+ From 2c6f53f4aa675de0c99a9ca32ce8ea1f5bc620a6 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 14 Apr 2023 18:55:23 +0100 Subject: [PATCH 080/199] BTHAB-101: Allow user to clear all filter fileds --- ang/afsearchContactQuotations.aff.html | 35 ++++++++++-------- ang/afsearchQuotations.aff.html | 37 +++++++++++-------- .../directives/quotations-list.directive.js | 14 +++++-- 3 files changed, 52 insertions(+), 34 deletions(-) diff --git a/ang/afsearchContactQuotations.aff.html b/ang/afsearchContactQuotations.aff.html index 53afb9ef9..4ea5d1cfe 100644 --- a/ang/afsearchContactQuotations.aff.html +++ b/ang/afsearchContactQuotations.aff.html @@ -1,20 +1,25 @@
-
- - - -
-
- +
+
+ + + +
+
+ +
+
+
+ + +
+ +
-
-
- -
diff --git a/ang/afsearchQuotations.aff.html b/ang/afsearchQuotations.aff.html index 206158c76..a5bd3e57c 100644 --- a/ang/afsearchQuotations.aff.html +++ b/ang/afsearchQuotations.aff.html @@ -1,22 +1,27 @@
-
- - - - -
-
- +
+
+ + + + +
+
+ +
-
-
- - +
+ + +
+ +
+
diff --git a/ang/civicase-features/quotations/directives/quotations-list.directive.js b/ang/civicase-features/quotations/directives/quotations-list.directive.js index 949aa6238..414c1ea81 100644 --- a/ang/civicase-features/quotations/directives/quotations-list.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-list.directive.js @@ -28,7 +28,7 @@ $location.search().cid = $scope.contactId; } - preventDatePickerNavigation(); + addEventToElementsWhenInDOMTree(); }()); /** @@ -45,13 +45,21 @@ } /** - * Prevents date picker from triggering route navigation. + * Add events to elements that are occasionally removed from DOM tree */ - function preventDatePickerNavigation () { + function addEventToElementsWhenInDOMTree () { const observer = new window.MutationObserver(function (mutations) { if ($('#ui-datepicker-div:visible a').length) { + // Prevents date picker from triggering route navigation. $('#ui-datepicker-div:visible a').click((event) => { event.preventDefault(); }); } + + if ($('.civicase__features-filters-clear').length) { + // Handle clear filter button. + $('.civicase__features-filters-clear').click(event => { + CRM.$('.civicase__features input, .civicase__features textarea').val('').change(); + }); + } }); observer.observe(document.body, { From 03b31b0c1edeba7c85868cae27bcd65ba5829496 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 18 Apr 2023 09:04:06 +0100 Subject: [PATCH 081/199] BTHAB-102: Allow creating contact on the client field --- .../quotations/directives/quotations-create.directive.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.html b/ang/civicase-features/quotations/directives/quotations-create.directive.html index efa36d7a4..116b0876c 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.html @@ -1,5 +1,5 @@
-

{{ ts('Create Quotations') }}

+

{{ ts('Create Quotation') }}

@@ -14,6 +14,7 @@

{{ ts('Create Quotations') }}

placeholder="Client" name="client" crm-entityref="{ + create: true, entity: 'Contact', select: { multiple: false, allowClear: true } }" From 197eb164f3370abb3b232d68618a9d525a13525b Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 19 Apr 2023 06:00:13 +0100 Subject: [PATCH 082/199] BRHAB-102: Update financial type when product is selected --- .../quotations/directives/quotations-create.directive.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index 2016a92c8..0b6cf958b 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -174,6 +174,11 @@ const updateProductDependentFields = (productId) => { $scope.salesOrder.items[index].item_description = productsCache.get(productId).description; $scope.salesOrder.items[index].unit_price = parseFloat(productsCache.get(productId).price); + const financialTypeId = productsCache.get(productId).financial_type_id ?? null; + if (financialTypeId) { + $scope.salesOrder.items[index].financial_type_id = financialTypeId; + handleFinancialTypeChange(index); + } calculateSubtotal(index); }; From 803ffaefa94a2f7ca4c5364d85d45b5f79942f22 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 19 Apr 2023 06:01:44 +0100 Subject: [PATCH 083/199] BTHAB-102: Increase element sizes and update validation messages --- .../quotations-create.directive.html | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.html b/ang/civicase-features/quotations/directives/quotations-create.directive.html index 116b0876c..119a1b0bd 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.html @@ -1,7 +1,7 @@

{{ ts('Create Quotation') }}

-
+
@@ -137,6 +137,7 @@

{{ ts('Create Quotation') }}

{{ ts('Create Quotation') }} /> - +
Description is required {{ ts('Create Quotation') }} Financial Type is required -
+
{{ currencySymbol }} - +

- Unit price is required + Unit price is invalid -
- -
+
- Quantity is required + Quantity is invalid -
- -
+ +
+ Discount is invalid {{ roundTo(salesOrder.items[$index].tax_rate, 2) }} @@ -247,3 +247,9 @@

{{ ts('Create Quotation') }}

+ + From d8e0c28b43e45d51338c03bf58f9c6de07e42f9e Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 19 Apr 2023 07:27:09 +0100 Subject: [PATCH 084/199] BTHAB-102: Limit sales order owner field to a contact --- .../quotations/directives/quotations-create.directive.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.html b/ang/civicase-features/quotations/directives/quotations-create.directive.html index 119a1b0bd..aee2acf56 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.html @@ -74,7 +74,7 @@

{{ ts('Create Quotation') }}

crm-entityref="{ create: true, entity: 'Contact', - select: { multiple: true, allowClear: true } + select: { multiple: false, allowClear: true } }" required ng-minlength="1" From 653161ef8cbc5077e60d8d0dc475da0f087beca3 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 19 Apr 2023 11:35:14 +0100 Subject: [PATCH 085/199] BTHAB-102: Clear product name when product ID is empty --- .../quotations/directives/quotations-create.directive.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index 0b6cf958b..23ff926ca 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -169,6 +169,7 @@ */ function handleProductChange (index) { if (!$scope.salesOrder.items[index].product_id) { + $scope.salesOrder.items[index]['product_id.name'] = ''; return; } const updateProductDependentFields = (productId) => { From 8164cd594a30c2d3a36d68c49f9155c795c84f55 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 19 Apr 2023 08:15:08 +0100 Subject: [PATCH 086/199] BTHAB-113: Display terms on invoice only if tax and invoicing is enabled --- CRM/Civicase/Form/CaseSalesOrderInvoice.php | 20 ++++++++++++++++++- .../MessageTemplate/QuotationInvoice.tpl | 4 +++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CRM/Civicase/Form/CaseSalesOrderInvoice.php b/CRM/Civicase/Form/CaseSalesOrderInvoice.php index 287e49a01..b83df347e 100644 --- a/CRM/Civicase/Form/CaseSalesOrderInvoice.php +++ b/CRM/Civicase/Form/CaseSalesOrderInvoice.php @@ -1,5 +1,6 @@ first(); $model = new CRM_Civicase_WorkflowMessage_SalesOrderInvoice(); - $terms = Civi::settings()->get('quotations_notes'); + $terms = self::getTerms(); $model->setDomainLogo($organisation['image_URL']); $model->setSalesOrder($caseSalesOrder); $model->setTerms($terms); @@ -201,6 +202,23 @@ public static function getQuotationInvoice(): array { return $rendered; } + /** + * Returns the Quotation invoice terms. + */ + private static function getTerms() { + $terms = NULL; + $invoicing = Setting::get() + ->addSelect('invoicing') + ->execute() + ->first(); + + if (!empty($invoicing['value'])) { + $terms = Civi::settings()->get('quotations_notes'); + } + + return $terms; + } + /** * Get the rows for each contactID. * diff --git a/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl b/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl index d9d5e31f3..059826667 100644 --- a/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl +++ b/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl @@ -120,14 +120,16 @@
+ {if !empty($terms) }

Terms

-

{if $terms }{$terms}{/if}

+

{$terms}

+ {/if}
From b9619faa562c0450c0ff68f76e5da75559b508f9 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 19 Apr 2023 11:48:04 +0100 Subject: [PATCH 087/199] BTHAB-113: Prevent sales_order tokens from being assigned null vallues --- CRM/Civicase/Hook/Tokens/SalesOrderTokens.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CRM/Civicase/Hook/Tokens/SalesOrderTokens.php b/CRM/Civicase/Hook/Tokens/SalesOrderTokens.php index 5eeb11432..5b3179bce 100644 --- a/CRM/Civicase/Hook/Tokens/SalesOrderTokens.php +++ b/CRM/Civicase/Hook/Tokens/SalesOrderTokens.php @@ -58,7 +58,20 @@ public static function evaluateSalesOrderTokens(TokenValueEvent $e) { ->execute() ->first(); foreach ($caseSalesOrder as $key => $value) { - $row->tokens(self::TOKEN, $key, $value); + try { + $row->tokens(self::TOKEN, $key, $value ?? ''); + } + catch (\Throwable $th) { + \Civi::log()->error( + 'Error resolving token: ' . self::TOKEN . '.' . $key, + [ + 'context' => [ + 'backtrace' => $th->getTraceAsString(), + 'message' => $th->getMessage(), + ], + ] + ); + } } } } From a0e5835fe9f1b6f2b27d67be0e71fb1bf52fbd16 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 17 Apr 2023 10:51:27 +0100 Subject: [PATCH 088/199] BTHAB-105: Remove table borders on quotations view --- .../directives/quotations-view.directive.html | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/ang/civicase-features/quotations/directives/quotations-view.directive.html b/ang/civicase-features/quotations/directives/quotations-view.directive.html index ebb02087e..9670e3294 100644 --- a/ang/civicase-features/quotations/directives/quotations-view.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-view.directive.html @@ -1,15 +1,13 @@
+

{{ ts('View Quotation') }}

-
-

View Quotation

-
-
+
-
- +
+
@@ -33,16 +31,16 @@

View Quotation

Case/Opportunity - - + + - - + + - - + + @@ -68,7 +66,7 @@

View Quotation

Items Overview

-
+
Quotatioin Id
Case Id{{salesOrder.case_id}}Case Id{{salesOrder.case_id}}
Case Type{{salesOrder['case_id.case_type_id:label']}}Case Type{{salesOrder['case_id.case_type_id:label']}}
Case Subject{{salesOrder['case_id.subject']}}Case Subject{{salesOrder['case_id.subject']}}
@@ -98,8 +96,8 @@

Items Overview

Amount Summary

-
-
Product
+
+
@@ -121,7 +119,7 @@

Amount Summary

Notes

-
+

{{salesOrder.notes}}

@@ -132,3 +130,22 @@

Notes

+ + From 8012afb75b9cb4ef9dcec2fcb1ffc5cd75f7d2ab Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 17 Apr 2023 10:52:50 +0100 Subject: [PATCH 089/199] BTHAB-105: Link contact name to dashbard on quotation view --- .../directives/quotations-view.directive.html | 4 ++-- .../directives/quotations-view.directive.js | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/ang/civicase-features/quotations/directives/quotations-view.directive.html b/ang/civicase-features/quotations/directives/quotations-view.directive.html index 9670e3294..effd7d481 100644 --- a/ang/civicase-features/quotations/directives/quotations-view.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-view.directive.html @@ -15,7 +15,7 @@

{{ ts('View Quotation') }}

- + @@ -47,7 +47,7 @@

{{ ts('View Quotation') }}

- + diff --git a/ang/civicase-features/quotations/directives/quotations-view.directive.js b/ang/civicase-features/quotations/directives/quotations-view.directive.js index 33e47a676..c0017f76e 100644 --- a/ang/civicase-features/quotations/directives/quotations-view.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-view.directive.js @@ -29,6 +29,7 @@ total_after_tax: 0 }; $scope.hasCase = false; + $scope.getContactLink = getContactLink; (function init () { if ($scope.salesOrderId) { @@ -56,10 +57,21 @@ } $scope.hasCase = true; CaseUtils.getDashboardLink($scope.salesOrder.case_id).then(link => { - $scope.dashboardLink = link; + $scope.dashboardLink = `${link}&focus=1&tab=Quotations`; }); } }); } + + /** + * Returns link to the contact dashboard + * + * @param {number} id the contact ID + * + * @returns {string} dashboard link + */ + function getContactLink (id) { + return CRM.url(`/contact/view?reset=1&cid=${id}`); + } } })(angular, CRM._); From e07ad3cab8eca39e04ecf7ab4e89dc5a1a930810 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 20 Apr 2023 15:02:22 +0100 Subject: [PATCH 090/199] BTHAB-105: Link owner name to owner contact dashboard --- .../quotations/directives/quotations-view.directive.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ang/civicase-features/quotations/directives/quotations-view.directive.html b/ang/civicase-features/quotations/directives/quotations-view.directive.html index effd7d481..ca24c280c 100644 --- a/ang/civicase-features/quotations/directives/quotations-view.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-view.directive.html @@ -47,7 +47,7 @@

{{ ts('View Quotation') }}

- + From 4649268e671d8826f8fb934ba0065b8a52d7654c Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 20 Apr 2023 09:46:28 +0100 Subject: [PATCH 091/199] BTHAB-119: Include tax rate in discount line item --- CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php index f49baf46b..86269bbeb 100644 --- a/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php +++ b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php @@ -79,10 +79,10 @@ private function getLineItemForSalesOrder() { $items[] = $this->lineItemToContributionLineItem($item); if ($item['discounted_percentage'] > 0) { - $item['tax'] = 0; $item['item_description'] = "{$item['item_description']} Discount {$item['discounted_percentage']}%"; $item['unit_price'] = $this->percent($item['discounted_percentage'], -$item['unit_price']); $item['total'] = $item['quantity'] * floatval($item['unit_price']); + $item['tax'] = empty($item['tax_rate']) ? 0 : $this->percent($item['tax_rate'], $item['total']); $items[] = $this->lineItemToContributionLineItem($item); } } From 50dc6bfb24f800e67f1175ebd47032644e49962d Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 20 Apr 2023 14:45:20 +0100 Subject: [PATCH 092/199] BTHAB-119: Ensure input is of same type before comparing --- CRM/Civicase/Form/CaseSalesOrderContributionCreate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php index 493ed7934..8b9cac695 100644 --- a/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php +++ b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php @@ -170,7 +170,7 @@ public function validateAmount(array $values) { $errors['to_be_invoiced'] = 'Contribution amount cannot exceed the total sales order amount'; } - if ($values['to_be_invoiced'] == self::INVOICE_PERCENT && $values['percent_value'] > $remainPercent) { + if ($values['to_be_invoiced'] == self::INVOICE_PERCENT && floatval($values['percent_value']) > $remainPercent) { $errors['percent_value'] = 'Percentage value cannot exceed ' . round($remainPercent, 2); } From a19ccc31fb2d7b5405b1524e95daa1e1d1911b89 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 20 Apr 2023 14:46:36 +0100 Subject: [PATCH 093/199] BTHAB-119: Set sales order bulk contribution status to pending --- .../ContributionCreateAction.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php index 61dd4674c..8cf59dda7 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php @@ -2,6 +2,7 @@ namespace Civi\Api4\Action\CaseSalesOrder; +use Civi\Api4\OptionValue; use Civi\Api4\CaseSalesOrder; use Civi\Api4\PriceFieldValue; use Civi\Api4\PriceField; @@ -124,6 +125,7 @@ private function createContributionWithLineItems(int $salesOrderId, array $price 'financial_type_id' => $this->financialTypeId, 'receive_date' => $this->date, 'contact_id' => $salesOrderContribution->salesOrder['client_id'], + 'contribution_status_id' => $this->getPendingContributionStatusId(), ]; return Contribution::create($params)->toArray(); @@ -183,4 +185,25 @@ private function updateCaseSalesOrderStatus(int $salesOrderId): void { ->execute(); } + /** + * Returns ID for pending contribution status. + * + * @return int + * pending status ID + */ + private function getPendingContributionStatusId(): ?int { + $pendingStatus = OptionValue::get() + ->addSelect('value') + ->addWhere('option_group_id:name', '=', 'contribution_status') + ->addWhere('name', '=', 'pending') + ->execute() + ->first(); + + if (!empty($pendingStatus)) { + return $pendingStatus['value']; + } + + return NULL; + } + } From f464ba7ab49d053b3c28d94881d99f62e3f815ce Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 20 Apr 2023 14:48:31 +0100 Subject: [PATCH 094/199] BTHAB-119: Display correct lineitem total for previous contribution --- CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php index 86269bbeb..9fa744697 100644 --- a/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php +++ b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php @@ -117,7 +117,9 @@ private function getPreviousContributionLineItem() { } foreach ($items as $item) { - $item['qty'] = -1 * $item['qty']; + $item['qty'] = $item['qty']; + $item['unit_price'] = -1 * $item['unit_price']; + $item['tax_amount'] = -1 * $item['tax_amount']; $item['line_total'] = $item['qty'] * floatval($item['unit_price']); $previousItems[] = $item; } From 4f6c7aee86a8510ff9e5dc70462eafdbcd6a307c Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 25 Apr 2023 15:32:53 +0100 Subject: [PATCH 095/199] BTHAB-129: Ensure quotation financial type can be updated --- .../quotations/directives/quotations-create.directive.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index 23ff926ca..9bdd6139b 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -214,6 +214,10 @@ $scope.salesOrder.items[index].tax_rate = 0; $scope.$emit('totalChange'); + if ($scope.salesOrder.items[index]['financial_type_id.name']) { + $scope.salesOrder.items[index]['financial_type_id.name'] = ''; + } + const updateFinancialTypeDependentFields = (financialTypeId) => { $scope.salesOrder.items[index].tax_rate = financialTypesCache.get(financialTypeId).tax_rate; $scope.$emit('totalChange'); From c2dd98200f8197ec885a17d37249ae797185559c Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 25 Apr 2023 14:37:34 +0100 Subject: [PATCH 096/199] BTHAB-38: Remove pledges from supported case instance features --- .../Setup/Manage/CaseTypeCategoryFeaturesManager.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/CRM/Civicase/Setup/Manage/CaseTypeCategoryFeaturesManager.php b/CRM/Civicase/Setup/Manage/CaseTypeCategoryFeaturesManager.php index ca0ecb936..ae6f0e9fc 100644 --- a/CRM/Civicase/Setup/Manage/CaseTypeCategoryFeaturesManager.php +++ b/CRM/Civicase/Setup/Manage/CaseTypeCategoryFeaturesManager.php @@ -35,15 +35,6 @@ public function create(): void { 'is_active' => TRUE, 'is_reserved' => TRUE, ]); - - CRM_Core_BAO_OptionValue::ensureOptionValueExists([ - 'option_group_id' => CRM_Civicase_Service_CaseTypeCategoryFeatures::NAME, - 'name' => 'pledges', - 'label' => 'Pledges', - 'is_default' => TRUE, - 'is_active' => TRUE, - 'is_reserved' => TRUE, - ]); } /** From acc7c9a4f057658bd79904df739b1aa922590ac2 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 25 Apr 2023 13:28:37 +0100 Subject: [PATCH 097/199] BTHAB-109: Close tab if there's no previous page to navigate to --- ang/civicase/shared/directives/history-back.directive.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ang/civicase/shared/directives/history-back.directive.js b/ang/civicase/shared/directives/history-back.directive.js index 8419633ca..463bb1cc4 100644 --- a/ang/civicase/shared/directives/history-back.directive.js +++ b/ang/civicase/shared/directives/history-back.directive.js @@ -7,6 +7,12 @@ link: function (scope, elem, attrs) { elem.bind('click', function () { $window.history.back(); + const currPage = window.location.href; + setTimeout(function () { + if ($window.location.href === currPage) { + $window.close(); + } + }, 500); }); } }; From 7a98c4fb5acbe631464c761ad98d77e0577a9359 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 25 Apr 2023 14:13:41 +0100 Subject: [PATCH 098/199] BTHAB-109: Prevent financial_type and product input field from hiding other fields --- .../directives/quotations-create.directive.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.html b/ang/civicase-features/quotations/directives/quotations-create.directive.html index aee2acf56..59c07c103 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.html @@ -121,7 +121,7 @@

{{ ts('Create Quotation') }}

-
+
Total {{ currencySymbol }} {{ salesOrder.total_before_tax }}
Client{{salesOrder['client_id.display_name']}}{{salesOrder['client_id.display_name']}}
Date
Owner{{salesOrder['owner_id.display_name']}}{{salesOrder['owner_id.display_name']}}
Status
Owner{{salesOrder['owner_id.display_name']}}{{salesOrder['owner_id.display_name']}}
Status
@@ -135,7 +135,7 @@

{{ ts('Create Quotation') }}

- - +
Product
+ {{ ts('Create Quotation') }}
Description is required
+ {{ ts('Create Quotation') }} Financial Type is required -
+
{{ currencySymbol }} - +

Unit price is invalid From 0847597e9884f00e0f5f44d4aa87d94aa0c857fb Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 25 Apr 2023 14:14:38 +0100 Subject: [PATCH 099/199] BTHAB-109: Space date range input fields --- ang/afsearchContactQuotations.aff.html | 2 +- ang/afsearchQuotations.aff.html | 2 +- .../quotations/directives/quotations-list.directive.html | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ang/afsearchContactQuotations.aff.html b/ang/afsearchContactQuotations.aff.html index 4ea5d1cfe..ea5ffde78 100644 --- a/ang/afsearchContactQuotations.aff.html +++ b/ang/afsearchContactQuotations.aff.html @@ -15,7 +15,7 @@
- +
diff --git a/ang/afsearchQuotations.aff.html b/ang/afsearchQuotations.aff.html index a5bd3e57c..b6d80d2a5 100644 --- a/ang/afsearchQuotations.aff.html +++ b/ang/afsearchQuotations.aff.html @@ -17,7 +17,7 @@
- +
diff --git a/ang/civicase-features/quotations/directives/quotations-list.directive.html b/ang/civicase-features/quotations/directives/quotations-list.directive.html index 276490d8b..1d472f036 100644 --- a/ang/civicase-features/quotations/directives/quotations-list.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-list.directive.html @@ -31,4 +31,7 @@

{{ ts('Manage Quotations') }}

background-color: transparent; border: none; } + .af-field-range-sep { + margin: 0em 1em; + } From da9df7b6093de0445a8171d0fe08fddc7a5f3aa7 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 27 Apr 2023 09:12:06 +0100 Subject: [PATCH 100/199] BTHAB-124: Redirect back to previous page on quotation edit --- .../quotations/directives/quotations-create.directive.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index 9bdd6139b..430d3b61f 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -286,6 +286,11 @@ * else redirects to the case view of the selected case. */ function redirectToAppropraitePage () { + if ($scope.isUpdate) { + $window.location.href = $window.document.referrer; + return; + } + if (!$scope.salesOrder.case_id) { $window.location.href = 'a#/quotations'; } From 9798c2aea70c1203650bd0e4304dbc2e3845c358 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 27 Apr 2023 09:12:54 +0100 Subject: [PATCH 101/199] BTHAB-124: Keep submit button disabled when save is successful --- .../quotations/directives/quotations-create.directive.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index 430d3b61f..d17f5a617 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -139,7 +139,6 @@ $scope.submitInProgress = true; crmApi4('CaseSalesOrder', 'save', { records: [$scope.salesOrder] }) .then(function (results) { - $scope.submitInProgress = false; showSucessNotification(); redirectToAppropraitePage(); }, function (failure) { From a5d2588b351e135bcec62e2ef83820105e83c38d Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 27 Apr 2023 09:13:50 +0100 Subject: [PATCH 102/199] BTHAB-124: Fix typo in Quotation view page --- .../quotations/directives/quotations-view.directive.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ang/civicase-features/quotations/directives/quotations-view.directive.html b/ang/civicase-features/quotations/directives/quotations-view.directive.html index ca24c280c..3140e3ed6 100644 --- a/ang/civicase-features/quotations/directives/quotations-view.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-view.directive.html @@ -10,7 +10,7 @@

{{ ts('View Quotation') }}

- + From d475019c1d49d70f7567e4c5ed3c3ea869dd3a8d Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 26 Apr 2023 12:06:21 +0100 Subject: [PATCH 103/199] BTHAB-130: Prevent quantity field from throwing error due to excess decimal places --- js/sales-order-contribution.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/sales-order-contribution.js b/js/sales-order-contribution.js index 338b1ccc2..1b553dc3c 100644 --- a/js/sales-order-contribution.js +++ b/js/sales-order-contribution.js @@ -54,6 +54,7 @@ function addLineItem (quantity, unitPrice, description, financialTypeId, taxAmount) { const row = $($(`tr#add-item-row-${count}`)); row.show().removeClass('hiddenElement'); + quantity = +parseFloat(quantity).toFixed(10); // limit to 10 decimal places $('input[id^="item_label"]', row).val(ts(description)); $('select[id^="item_financial_type_id"]', row).select2('val', financialTypeId); From 74501a2ea15dc1b9d2601a26cdae76829509aa8f Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 27 Apr 2023 12:24:44 +0100 Subject: [PATCH 104/199] BTHAB-130: Prevent incorrect quotation contribution total Contribution line items with tax_amount in thousands are not handled appropratiely by CiviCRM when contribution is created, to resolve this we remove the formatter, and have the tax_amount as number, did the same thing for unit_price. --- js/sales-order-contribution.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/sales-order-contribution.js b/js/sales-order-contribution.js index 1b553dc3c..448080584 100644 --- a/js/sales-order-contribution.js +++ b/js/sales-order-contribution.js @@ -62,10 +62,10 @@ const total = quantity * parseFloat(unitPrice); - $('input[id^="item_unit_price"]', row).val(CRM.formatMoney(unitPrice, true)); + $('input[id^="item_unit_price"]', row).val(unitPrice); $('input[id^="item_line_total"]', row).val(CRM.formatMoney(total, true)); - $('input[id^="item_tax_amount"]', row).val(CRM.formatMoney(taxAmount, true)); + $('input[id^="item_tax_amount"]', row).val(taxAmount); count++; } From 798bda4544f474f6cd2a4bb2600e6305318fca2f Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 1 May 2023 16:21:37 +0100 Subject: [PATCH 105/199] BTHAB-126: Display contact Quotation tab if Quotation feature is enabled on atleast one case instances --- CRM/Civicase/Hook/Tabset/CaseSalesOrderTabAdd.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CRM/Civicase/Hook/Tabset/CaseSalesOrderTabAdd.php b/CRM/Civicase/Hook/Tabset/CaseSalesOrderTabAdd.php index b75de3319..0d66a4c6f 100644 --- a/CRM/Civicase/Hook/Tabset/CaseSalesOrderTabAdd.php +++ b/CRM/Civicase/Hook/Tabset/CaseSalesOrderTabAdd.php @@ -1,6 +1,7 @@ retrieveCaseInstanceWithEnabledFeatures(['quotations']); + + if (empty($caseInstances)) { + return; + } + $tabs[] = [ 'id' => 'quotations', 'url' => CRM_Utils_System::url("civicrm/case-features/quotations/contact-tab?cid=$contactID"), From d7fe0ea6c85d31ab66da39b361087341edd508a8 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 1 May 2023 13:37:33 +0100 Subject: [PATCH 106/199] BTHAB-155: Update quotation contribution status field label --- CRM/Civicase/Form/CaseSalesOrderContributionCreate.php | 2 +- .../directives/quotations-contribution-bulk.directive.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php index 8b9cac695..d6aaff67c 100644 --- a/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php +++ b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php @@ -61,7 +61,7 @@ public function buildQuickForm() { $this->add( 'select', 'status', - ts('Status'), + ts('Update status of quotation to'), ['' => 'Select'] + array_combine( array_column($statusOptions, 'value'), diff --git a/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html index c63fc9e8e..d82f4a163 100644 --- a/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html @@ -64,7 +64,7 @@
Quotatioin IdQuotation Id {{salesOrder.id}}
- - - - - +
+
- logo - -
+ + +
- - - - - - - - - - - - - - - +
Client Name: {$sales_order.client.display_name}DateAddress
- {if $sales_order.clientAddress.street_address }{$sales_order.clientAddress.street_address}{/if} -
- {if $sales_order.clientAddress.supplemental_address_1 }{$sales_order.clientAddress.supplemental_address_1}{/if} -
- {if $sales_order.clientAddress.supplemental_address_2 }{$sales_order.clientAddress.supplemental_address_2}{/if} -
- - {if $sales_order.clientAddress.city} - {$sales_order.clientAddress.city} {$sales_order.clientAddress.postal_code}{if $sales_order.clientAddress.postal_code_suffix} - {$sales_order.clientAddress.postal_code_suffix}{/if}
- {/if} -
-
- {$sales_order.quotation_date|crmDate} -

Quote Number

-

{$sales_order.id}

-
- {domain.address} -
+ + + + + + + + + + + + + + + + + + + + + + + + +
{ts}Client Name: {$sales_order.client.display_name}<{/ts}{ts}Date:{/ts}Address
+ {if $sales_order.clientAddress.street_address }{$sales_order.clientAddress.street_address}{/if} + {if $sales_order.clientAddress.supplemental_address_1 }{$sales_order.clientAddress.supplemental_address_1}{/if} + {$sales_order.quotation_date|crmDate} + {if !empty($domain_location.street_address) }{$domain_location.street_address}{/if} + {if !empty($domain_location.supplemental_address_1) }{$domain_location.supplemental_address_1}{/if} +
+ {if $sales_order.clientAddress.supplemental_address_2 }{$sales_order.clientAddress.supplemental_address_2 }{/if} + {if $sales_order.clientAddress.state }{$sales_order.clientAddress.state}{/if} + Quote Number + {if !empty($domain_location.supplemental_address_2) }{$domain_location.supplemental_address_2 }{/if} + {if !empty($domain_location.state) }{$domain_location.state}{/if} +
+ {if $sales_order.clientAddress.city }{$sales_order.clientAddress.city }{/if} + {if $sales_order.clientAddress.postal_code }{$sales_order.clientAddress.postal_code}{/if} + {$sales_order.id} + {if !empty($domain_location.city) }{$domain_location.city }{/if} + {if !empty($domain_location.postal_code) }{$domain_location.postal_code}{/if} +
+ {if $sales_order.clientAddress.country }{$sales_order.clientAddress.country}{/if} + + {if !empty($domain_location.country) }{$domain_location.country }{/if} +
-
- -
-

Description

-

{$sales_order.description}

- - +
+
+ +
Description
{$sales_order.description}
- +
- - - - - - - + + + + + + + {foreach from=$sales_order.items key=k item=item} - - - - - - - + + + + + + + {/foreach} - -
DescriptionQuantityUnit PriceDiscountVATAmount {$sales_order.currency} (without tax)
{ts}Description{/ts}{ts}Quantity{/ts}{ts}Unit Price{/ts}{ts}Discount{/ts}{ts}VAT{/ts}{ts}Amount {$sales_order.currency} (without tax){/ts}
{$item.item_description}{$item.quantity}{$item.unit_price|crmMoney:$sales_order.currency}{if empty($item.discounted_percentage) } 0 {else}{$item.discounted_percentage}{/if}%{if empty($item.tax_rate) } 0 {else}{$item.tax_rate}{/if}%{$item.subtotal_amount|crmMoney:$sales_order.currency}
{$item.item_description|truncate:30:"..."}{$item.quantity}{$item.unit_price|crmMoney:$sales_order.currency}{if empty($item.discounted_percentage) } 0 {else}{$item.discounted_percentage}{/if}%{if empty($item.tax_rate) } 0 {else}{$item.tax_rate}{/if}%{$item.subtotal_amount|crmMoney:$sales_order.currency}
-
- -
-
- - - - - - - + + + + + {foreach from=$sales_order.taxRates item=tax} - - - + + + - {/foreach} - - - - + + + + + + + + - -
SubTotal (inclusive of discount){$sales_order.total_before_tax|crmMoney:$sales_order.currency}
{ts}SubTotal (inclusive of discount){/ts}{$sales_order.total_before_tax|crmMoney:$sales_order.currency}
Total VAT ({$tax.rate}%){$tax.value|crmMoney:$sales_order.currency}{ts}Total VAT ({$tax.rate}%){/ts}{$tax.value|crmMoney:$sales_order.currency}
Total Amount{$sales_order.total_after_tax|crmMoney:$sales_order.currency}

{ts}Total Amount{/ts}{$sales_order.total_after_tax|crmMoney:$sales_order.currency}
-
+
{if !empty($terms) } -
-

Terms

- -

{$terms}

- - +
+
+ +
Terms
{$terms}
{/if} From 5d89771b8b7a891ebf72ce610c1ee50f5011f41f Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 2 May 2023 07:57:02 +0100 Subject: [PATCH 111/199] BTHAB-43: Specify default tepmlate format and set domain locatoin field --- CRM/Civicase/Form/CaseSalesOrderInvoice.php | 51 ++++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/CRM/Civicase/Form/CaseSalesOrderInvoice.php b/CRM/Civicase/Form/CaseSalesOrderInvoice.php index b83df347e..61ba72914 100644 --- a/CRM/Civicase/Form/CaseSalesOrderInvoice.php +++ b/CRM/Civicase/Form/CaseSalesOrderInvoice.php @@ -1,10 +1,10 @@ addSelect('*', 'country_id:label', 'state_province_id:label') ->addWhere('contact_id', '=', $caseSalesOrder['client_id']) ->execute() ->first(); + $caseSalesOrder['clientAddress']['country'] = $caseSalesOrder['clientAddress']['country_id:label']; + $caseSalesOrder['clientAddress']['state'] = $caseSalesOrder['clientAddress']['state_province_id:label']; } $caseSalesOrder['taxRates'] = $caseSalesOrder['computedRates'][0]['taxRates'] ?? []; @@ -197,8 +200,11 @@ public static function getQuotationInvoice(): array { $model->setSalesOrder($caseSalesOrder); $model->setTerms($terms); $model->setSalesOrderId($salesOrderId); + $model->setDomainLocation(self::getDomainLocation()); $rendered = $model->renderTemplate(); + $rendered['format'] = $rendered['format'] ?? self::defaultInvoiceFormat(); + return $rendered; } @@ -219,6 +225,36 @@ private static function getTerms() { return $terms; } + /** + * Gets domain location. + * + * @return array + * An array of address lines. + */ + private static function getDomainLocation() { + $domain = CRM_Core_BAO_Domain::getDomain(); + $locParams = ['contact_id' => $domain->contact_id]; + $locationDefaults = CRM_Core_BAO_Location::getValues($locParams); + if (empty($locationDefaults['address'][1])) { + return []; + } + $stateProvinceId = $locationDefaults['address'][1]['state_province_id'] ?? NULL; + $stateProvinceAbbreviationDomain = !empty($stateProvinceId) ? CRM_Core_PseudoConstant::stateProvinceAbbreviation($stateProvinceId) : ''; + $countryId = $locationDefaults['address'][1]['country_id']; + $countryDomain = !empty($countryId) ? CRM_Core_PseudoConstant::country($countryId) : ''; + + return [ + 'street_address' => CRM_Utils_Array::value('street_address', CRM_Utils_Array::value('1', $locationDefaults['address'])), + 'supplemental_address_1' => CRM_Utils_Array::value('supplemental_address_1', CRM_Utils_Array::value('1', $locationDefaults['address'])), + 'supplemental_address_2' => CRM_Utils_Array::value('supplemental_address_2', CRM_Utils_Array::value('1', $locationDefaults['address'])), + 'supplemental_address_3' => CRM_Utils_Array::value('supplemental_address_3', CRM_Utils_Array::value('1', $locationDefaults['address'])), + 'city' => CRM_Utils_Array::value('city', CRM_Utils_Array::value('1', $locationDefaults['address'])), + 'postal_code' => CRM_Utils_Array::value('postal_code', CRM_Utils_Array::value('1', $locationDefaults['address'])), + 'state' => $stateProvinceAbbreviationDomain, + 'country' => $countryDomain, + ]; + } + /** * Get the rows for each contactID. * @@ -236,13 +272,24 @@ protected function getRows(): array { return $rows; } + /** + * Returns the default format to use for Invoice. + */ + private static function defaultInvoiceFormat() { + return [ + 'margin_top' => 10, + 'margin_left' => 65, + 'metric' => 'px', + ]; + } + /** * Renders and return the generated PDF to the browser. */ public static function download(): void { $rendered = self::getQuotationInvoice(); ob_end_clean(); - CRM_Utils_PDF_Utils::html2pdf($rendered['html'], 'quotation_invoice.pdf', FALSE, $rendered['format'] ?? []); + CRM_Utils_PDF_Utils::html2pdf($rendered['html'], 'quotation_invoice.pdf', FALSE, $rendered['format']); CRM_Utils_System::civiExit(); } From 4db63b2e238f426df1e78d5654d3c4fa50fc699b Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Sat, 29 Apr 2023 15:06:24 +0100 Subject: [PATCH 112/199] BTHAB-153: Add Helper text to quotation notes field --- .../BuildForm/AddQuotationsNotesToContributionSettings.php | 4 ++++ templates/CRM/Civicase/Form/CaseSalesOrderInvoiceNote.hlp | 3 +++ templates/CRM/Civicase/Form/CaseSalesOrderInvoiceNote.tpl | 5 +++++ 3 files changed, 12 insertions(+) create mode 100644 templates/CRM/Civicase/Form/CaseSalesOrderInvoiceNote.hlp create mode 100644 templates/CRM/Civicase/Form/CaseSalesOrderInvoiceNote.tpl diff --git a/CRM/Civicase/Hook/BuildForm/AddQuotationsNotesToContributionSettings.php b/CRM/Civicase/Hook/BuildForm/AddQuotationsNotesToContributionSettings.php index 2f375b52d..f7d243b98 100644 --- a/CRM/Civicase/Hook/BuildForm/AddQuotationsNotesToContributionSettings.php +++ b/CRM/Civicase/Hook/BuildForm/AddQuotationsNotesToContributionSettings.php @@ -56,6 +56,10 @@ public function addQuotationsNoteField(CRM_Core_Form &$form) { $form->assign('htmlFields', array_merge($form->get_template_vars('htmlFields'), $field)); $value = Civi::settings()->get($fieldName) ?? NULL; $form->setDefaults(array_merge($form->_defaultValues, [$fieldName => $value])); + + CRM_Core_Region::instance('form-buttons')->add([ + 'template' => "CRM/Civicase/Form/CaseSalesOrderInvoiceNote.tpl", + ]); } } diff --git a/templates/CRM/Civicase/Form/CaseSalesOrderInvoiceNote.hlp b/templates/CRM/Civicase/Form/CaseSalesOrderInvoiceNote.hlp new file mode 100644 index 000000000..12166036c --- /dev/null +++ b/templates/CRM/Civicase/Form/CaseSalesOrderInvoiceNote.hlp @@ -0,0 +1,3 @@ +{htxt id="sales_order_invoce_note"} +{ts}Notes will be displayed on the Quotation PDF.{/ts}

+{/htxt} diff --git a/templates/CRM/Civicase/Form/CaseSalesOrderInvoiceNote.tpl b/templates/CRM/Civicase/Form/CaseSalesOrderInvoiceNote.tpl new file mode 100644 index 000000000..0e5b4a26c --- /dev/null +++ b/templates/CRM/Civicase/Form/CaseSalesOrderInvoiceNote.tpl @@ -0,0 +1,5 @@ +{literal} + +{/literal} From 46beca098ba9b881b8c7b7356aea33d02a651bcb Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Sat, 29 Apr 2023 08:16:23 +0100 Subject: [PATCH 113/199] BTHAB-152: Display only essesntial bulk actions --- civicase.php | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/civicase.php b/civicase.php index cb12a929e..69703d472 100644 --- a/civicase.php +++ b/civicase.php @@ -567,17 +567,29 @@ function civicase_civicrm_alterMailParams(&$params, $context) { * Implements hook_civicrm_searchKitTasks(). */ function civicase_civicrm_searchKitTasks(array &$tasks, bool $checkPermissions, ?int $userID) { - $tasks['CaseSalesOrder']['add_discount'] = [ + if (empty($tasks['CaseSalesOrder'])) { + return; + } + + $actions = []; + + if (!empty($tasks['CaseSalesOrder']['delete'])) { + $actions['delete'] = $tasks['CaseSalesOrder']['delete']; + $actions['delete']['title'] = 'Delete Quotation(s)'; + } + + $actions['add_discount'] = [ 'module' => 'civicase-features', - 'icon' => 'fa-percent', - 'title' => ts('Add Discount'), + 'title' => ts('Add Discount %'), 'uiDialog' => ['templateUrl' => '~/civicase-features/quotations/directives/quotations-discount.directive.html'], ]; - $tasks['CaseSalesOrder']['create_contribution'] = [ + $actions['create_contribution'] = [ 'module' => 'civicase-features', 'icon' => 'fa-credit-card', - 'title' => ts('Create Contribution(Bulk)'), + 'title' => ts('Create Contribution(s)'), 'uiDialog' => ['templateUrl' => '~/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html'], ]; + + $tasks['CaseSalesOrder'] = $actions; } From d5b9f8f641592234ec10ba7bced730199c0326ee Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Sat, 29 Apr 2023 08:17:06 +0100 Subject: [PATCH 114/199] BTHAB-152: Recalculate lineitem total before saving --- Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php | 1 + 1 file changed, 1 insertion(+) diff --git a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php index 32a4b1baa..df4dadade 100644 --- a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php @@ -55,6 +55,7 @@ protected function writeRecord($items) { if (!empty($result) && !empty($lineItems)) { array_walk($lineItems, function (&$lineItem) use ($result, $caseSalesOrderLineAPI) { $lineItem['sales_order_id'] = $result['id']; + $lineItem['subtotal_amount'] = $lineItem['unit_price'] * $lineItem['quantity'] * ((100 - $lineItem['discounted_percentage']) / 100); $caseSalesOrderLineAPI->addRecord($lineItem); }); From 29216aeb3c1b5201c8dbcb77b44ba6c5d81cb3c1 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Sat, 29 Apr 2023 07:25:22 +0100 Subject: [PATCH 115/199] BTHAB-147: Format displayed total in selected currency format --- Civi/Utils/CurrencyUtils.php | 7 ++++++- .../directives/quotations-create.directive.html | 12 ++++++------ .../directives/quotations-create.directive.js | 13 +++++++++++++ .../shared/services/currency-codes.service.js | 7 +++++++ 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/Civi/Utils/CurrencyUtils.php b/Civi/Utils/CurrencyUtils.php index 907f1b88f..f2f15b1b0 100644 --- a/Civi/Utils/CurrencyUtils.php +++ b/Civi/Utils/CurrencyUtils.php @@ -3,6 +3,7 @@ namespace Civi\Utils; use CRM_Core_DAO; +use CRM_Utils_Money; /** * Utility class to manage CiviCRM currency table. @@ -28,7 +29,11 @@ public static function getCurrencies() { $dao = CRM_Core_DAO::executeQuery($query); self::$currencies = []; while ($dao->fetch()) { - self::$currencies[] = ['name' => $dao->name, 'symbol' => $dao->symbol]; + self::$currencies[] = [ + 'name' => $dao->name, + 'symbol' => $dao->symbol, + 'format' => CRM_Utils_Money::format(1234.56, $dao->name), + ]; } } diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.html b/ang/civicase-features/quotations/directives/quotations-create.directive.html index 59c07c103..ecb0eb112 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.html @@ -121,7 +121,7 @@

{{ ts('Create Quotation') }}

-
+
@@ -150,7 +150,7 @@

{{ ts('Create Quotation') }}

/> @@ -192,7 +192,7 @@

{{ ts('Create Quotation') }}

- +
Product - +
Description is required
{{ roundTo(salesOrder.items[$index].tax_rate, 2) }} {{ roundTo(salesOrder.items[$index].subtotal_amount, 2) }}{{ formatMoney(salesOrder.items[$index].subtotal_amount, salesOrder.currency) }}
@@ -209,12 +209,12 @@

{{ ts('Create Quotation') }}

- + - + - +
Total{{ currencySymbol }} {{ salesOrder.total }}
Total{{ currencySymbol }} {{ formatMoney(salesOrder.total, salesOrder.currency) }}
Tax @ {{ i.rate }}%{{ currencySymbol }} {{ i.value }}{{ currencySymbol }} {{ formatMoney(i.value, salesOrder.currency) }}
Grand Total{{ currencySymbol }} {{salesOrder.grandTotal}}
Grand Total{{ currencySymbol }} {{formatMoney(salesOrder.grandTotal, salesOrder.currency)}}
diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index d17f5a617..2d4bbd5f8 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -32,6 +32,7 @@ $scope.isUpdate = false; $scope.formValid = true; $scope.roundTo = roundTo; + $scope.formatMoney = formatMoney; $scope.submitInProgress = false; $scope.caseApiParam = caseApiParam; $scope.saveQuotation = saveQuotation; @@ -60,6 +61,7 @@ $scope.salesOrder = { currency: defaultCurrency, status_id: SalesOrderStatus.getValueByName('new'), + clientId: null, owner_id: Contact.getCurrentContactID(), quotation_date: $.datepicker.formatDate('yy-mm-dd', new Date()), items: [{ @@ -321,5 +323,16 @@ // handle failure }); } + + /** + * Formats a number into the number format of the currently selected currency + * + * @param {number} value the number to be formatted + * @param {string } currency the selected currency + * @returns {number} the formatted number + */ + function formatMoney (value, currency) { + return CRM.formatMoney(value, true, CurrencyCodes.getFormat(currency)); + } } })(angular, CRM.$, CRM._); diff --git a/ang/civicase-features/shared/services/currency-codes.service.js b/ang/civicase-features/shared/services/currency-codes.service.js index a2126c8bb..fa467f2ab 100644 --- a/ang/civicase-features/shared/services/currency-codes.service.js +++ b/ang/civicase-features/shared/services/currency-codes.service.js @@ -17,5 +17,12 @@ .filter(currency => currency.name === name) .pop().symbol || '£'; }; + + this.getFormat = function (name) { + return CRM['civicase-features'] + .currencyCodes + .filter(currency => currency.name === name) + .pop().format || null; + }; } })(angular, CRM.$, CRM._, CRM); From 0d03a2824cfba286a1bec96cf0890347b90e35ec Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Sat, 29 Apr 2023 07:27:07 +0100 Subject: [PATCH 116/199] BTHAB-147: Add help text to notes and descrition field --- .../directives/quotations-create.directive.html | 2 ++ .../directives/quotations-create.directive.js | 4 +++- templates/CRM/Civicase/SalesOrderCtrl.hlp | 11 +++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 templates/CRM/Civicase/SalesOrderCtrl.hlp diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.html b/ang/civicase-features/quotations/directives/quotations-create.directive.html index ecb0eb112..f2391f764 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.html @@ -38,6 +38,7 @@

{{ ts('Create Quotation') }}

@@ -222,6 +223,7 @@

{{ ts('Create Quotation') }}

diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index 2d4bbd5f8..7f2e572ac 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -23,11 +23,13 @@ * @param {object} FeatureCaseTypes FeatureCaseTypes service * @param {object} SalesOrderStatus SalesOrderStatus service * @param {object} CaseUtils case utility service + * @param {object} crmUiHelp crm ui help service */ - function quotationsCreateController ($scope, $location, $window, CurrencyCodes, civicaseCrmApi, Contact, crmApi4, FeatureCaseTypes, SalesOrderStatus, CaseUtils) { + function quotationsCreateController ($scope, $location, $window, CurrencyCodes, civicaseCrmApi, Contact, crmApi4, FeatureCaseTypes, SalesOrderStatus, CaseUtils, crmUiHelp) { const defaultCurrency = 'GBP'; const productsCache = new Map(); const financialTypesCache = new Map(); + $scope.hs = crmUiHelp({ file: 'CRM/Civicase/SalesOrderCtrl' }); $scope.isUpdate = false; $scope.formValid = true; diff --git a/templates/CRM/Civicase/SalesOrderCtrl.hlp b/templates/CRM/Civicase/SalesOrderCtrl.hlp new file mode 100644 index 000000000..6cd80b07d --- /dev/null +++ b/templates/CRM/Civicase/SalesOrderCtrl.hlp @@ -0,0 +1,11 @@ +{htxt id="sales_order_notes"} +

+ {ts}Add any notes related to the quotation here. They will be visible on the quotation PDF.{/ts} +

+{/htxt} + +{htxt id="sales_order_description} +

+ {ts}This is an internal field for adding a description for the quotation. It will not be displayed anywhere.{/ts} +

+{/htxt} From 625c8985caa8f91748f1d9dedcd1a046db4adae3 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Sat, 29 Apr 2023 07:34:05 +0100 Subject: [PATCH 117/199] BTHAB-147: Use case client as default quotation client When they are multiple case client, the first client is use; the first here implies first to be returned by the API not neccessarily --- .../directives/quotations-create.directive.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index 7f2e572ac..8a9adef15 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -83,6 +83,7 @@ $scope.total = 0; $scope.taxRates = []; + setDefaultClientID(); prefillSalesOrderForUpdate(); } @@ -106,6 +107,25 @@ }); } + /** + * Sets client ID to case client. + */ + function setDefaultClientID () { + if (!$scope.defaultCaseId || $scope.isUpdate) { + return; + } + + crmApi4('Relationship', 'get', { + select: ['contact_id_a'], + where: [['case_id', '=', $scope.defaultCaseId], ['relationship_type_id:name', '=', 'Case Coordinator is'], ['is_current', '=', true]], + limit: 1 + }).then(function (relationships) { + if (Array.isArray(relationships) && relationships.length > 0) { + $scope.salesOrder.client_id = relationships[0].contact_id_a ?? null; + } + }); + } + /** * Removes a sales order line item * From 9d11ea04d05680e855aba11acc5bafb4baee7e89 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 27 Apr 2023 15:20:00 +0100 Subject: [PATCH 118/199] BTHAB-128: Allow creation of single contribution without restricing the percenrtage value --- .../Form/CaseSalesOrderContributionCreate.php | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php index 9c9625484..dd6714e86 100644 --- a/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php +++ b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php @@ -44,7 +44,6 @@ public function buildQuickForm() { 'placeholder' => 'Percentage to be invoiced', 'class' => 'form-control', 'min' => 1, - 'max' => 100, ], FALSE); $this->addElement('radio', 'to_be_invoiced', '', ts('Remaining Balance'), self::INVOICE_REMAIN, @@ -129,10 +128,6 @@ public function formRule(array $values) { $errors['percent_value'] = 'Percentage value is required'; } - if ($values['to_be_invoiced'] == self::INVOICE_PERCENT && $values['percent_value'] > 100) { - $errors['percent_value'] = 'Percentage value cannot exceed 100 '; - } - return $errors ?: TRUE; } @@ -160,6 +155,10 @@ public function formRule(array $values) { public function validateAmount(array $values) { $errors = []; + if ($values['to_be_invoiced'] == self::INVOICE_PERCENT) { + return TRUE; + } + $caseSalesOrder = CaseSalesOrder::get() ->addSelect('total_after_tax') ->addWhere('id', '=', $this->id) @@ -178,15 +177,10 @@ public function validateAmount(array $values) { ->jsonSerialize(); $paidTotal = array_sum(array_column($caseSalesOrderContributions, 'contribution_id.total_amount')); - $paidPercent = ($paidTotal * 100) / $caseSalesOrder['total_after_tax']; - $remainPercent = 100 - $paidPercent; - - if ($remainPercent <= 0) { - $errors['to_be_invoiced'] = 'Contribution amount cannot exceed the total sales order amount'; - } + $remainBalance = $caseSalesOrder['total_after_tax'] - $paidTotal; - if ($values['to_be_invoiced'] == self::INVOICE_PERCENT && floatval($values['percent_value']) > $remainPercent) { - $errors['percent_value'] = 'Percentage value cannot exceed ' . round($remainPercent, 2); + if ($remainBalance <= 0) { + $errors['to_be_invoiced'] = 'Unable to create a contribution due to insufficient balance.'; } return $errors ?: TRUE; From d3db578004f76257585d6f9d4c6747fff71c4af3 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 27 Apr 2023 17:17:52 +0100 Subject: [PATCH 119/199] BTHAB-128: Use numbers of bulk contribution created from API result --- .../Api4/Action/CaseSalesOrder/ContributionCreateAction.php | 6 ++++-- .../directives/quotations-contribution-bulk.directive.js | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php index 8cf59dda7..5d3d23916 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php @@ -69,7 +69,7 @@ class ContributionCreateAction extends AbstractAction { public function _run(Result $result) { // phpcs:ignore $resultArray = $this->createContribution(); - $result->exchangeArray($resultArray); + return $result->exchangeArray($resultArray); } /** @@ -77,6 +77,7 @@ public function _run(Result $result) { // phpcs:ignore */ private function createContribution() { $priceField = $this->getDefaultPriceSetFields(); + $createdContributionsCount = 0; foreach ($this->salesOrderIds as $id) { $transaction = CRM_Core_Transaction::create(); @@ -84,13 +85,14 @@ private function createContribution() { $contribution = $this->createContributionWithLineItems($id, $priceField); $this->linkCaseSalesOrderToContribution($id, $contribution['id']); $this->updateCaseSalesOrderStatus($id); + $createdContributionsCount++; } catch (\Exception $e) { $transaction->rollback(); } } - return []; + return ['created_contributions_count' => $createdContributionsCount]; } /** diff --git a/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js index 1e0482f8b..3e13f0264 100644 --- a/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js @@ -28,7 +28,7 @@ $scope.submitInProgress = false; ctrl.data = { toBeInvoiced: 'percent', - percentValue: 0, + percentValue: null, statusId: null, financialTypeId: null, date: $.datepicker.formatDate('yy-mm-dd', new Date()) @@ -53,8 +53,8 @@ const chunkedIds = _.chunk(ctrl.ids, BATCH_SIZE); for (const salesOrderIds of chunkedIds) { try { - await crmApi4('CaseSalesOrder', 'contributionCreateAction', { ...ctrl.data, salesOrderIds }); - contributionCreated += salesOrderIds.length; + const result = await crmApi4('CaseSalesOrder', 'contributionCreateAction', { ...ctrl.data, salesOrderIds }); + contributionCreated += result.created_contributions_count ?? 0; } catch (error) { console.log(error); } finally { From ee6000ff2c1aaf81985d6670c100efefc04facc4 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 27 Apr 2023 18:14:49 +0100 Subject: [PATCH 120/199] BTHAB-128: Ensure Quotation Contribution amount will be greater than 0 --- CRM/Civicase/Form/CaseSalesOrderContributionCreate.php | 1 + CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php | 2 +- .../Action/CaseSalesOrder/ContributionCreateAction.php | 8 ++++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php index dd6714e86..7a9ad74cc 100644 --- a/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php +++ b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php @@ -178,6 +178,7 @@ public function validateAmount(array $values) { $paidTotal = array_sum(array_column($caseSalesOrderContributions, 'contribution_id.total_amount')); $remainBalance = $caseSalesOrder['total_after_tax'] - $paidTotal; + $remainBalance = round($remainBalance, 2); if ($remainBalance <= 0) { $errors['to_be_invoiced'] = 'Unable to create a contribution due to insufficient balance.'; diff --git a/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php index 9fa744697..0fe2af3a2 100644 --- a/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php +++ b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php @@ -23,7 +23,7 @@ class CRM_Civicase_Service_CaseSalesOrderLineItemsGenerator { /** * Constructs CaseSalesOrderContribution service. */ - public function __construct(private int $salesOrderId, private string $type, private string $percentValue) { + public function __construct(private int $salesOrderId, private string $type, private ?string $percentValue) { $this->setSalesOrder(); } diff --git a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php index 5d3d23916..07c698afb 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php @@ -114,11 +114,15 @@ private function createContributionWithLineItems(int $salesOrderId, array $price $lineItem['price_field_value_id'] = $priceField[$index]['price_field_value'][0]['id']; $priceSetID = \CRM_Core_DAO::getFieldValue('CRM_Price_BAO_PriceField', $priceField[$index]['id'], 'price_set_id'); $allLineItems[$priceSetID][$priceField[$index]['id']] = $lineItem; - $taxAmount += (float) ($lineItem['tax_amount'] ?? 0); - $lineTotal += (float) ($lineItem['line_total'] ?? 0); + $taxAmount += (float) $lineItem['tax_amount'] ?? 0; + $lineTotal += (float) $lineItem['line_total'] ?? 0; } $totalAmount = $lineTotal + $taxAmount; + if (round($totalAmount, 2) < 1) { + throw new \Exception("Contribution total amount must be greater than zero"); + } + $params = [ 'source' => "Quotation {$salesOrderId}", 'line_item' => $allLineItems, From 674c727234c23472742918732c3b4ccd3eae57d6 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 28 Apr 2023 07:28:45 +0100 Subject: [PATCH 121/199] BTHAB-128: Allow creation of bulk contribution without restricing the percenrtage value --- .../Service/CaseSalesOrderLineItemsGenerator.php | 4 ++-- .../CaseSalesOrder/ContributionCreateAction.php | 16 ++++++++-------- .../quotations-contribution-bulk.directive.html | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php index 0fe2af3a2..99a24bfaa 100644 --- a/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php +++ b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php @@ -1,9 +1,9 @@ Enter % to be invoiced ?
- + Amount is required
From 4966b72d05c6b1bfcd73feb983cf404ea1f9cf01 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 3 May 2023 13:59:52 +0100 Subject: [PATCH 122/199] BTHAB-152: Use Quotation as case_sales_order title --- CRM/Civicase/DAO/CaseSalesOrder.php | 2 +- CRM/Civicase/DAO/CaseSalesOrderContribution.php | 2 +- CRM/Civicase/DAO/CaseSalesOrderLine.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CRM/Civicase/DAO/CaseSalesOrder.php b/CRM/Civicase/DAO/CaseSalesOrder.php index 1cf4746c5..bb2718a65 100644 --- a/CRM/Civicase/DAO/CaseSalesOrder.php +++ b/CRM/Civicase/DAO/CaseSalesOrder.php @@ -174,7 +174,7 @@ public function __construct() { * Whether to return the plural version of the title. */ public static function getEntityTitle($plural = FALSE) { - return $plural ? E::ts('Case Sales Orders') : E::ts('Case Sales Order'); + return $plural ? E::ts('Quotation') : E::ts('Quotations'); } /** diff --git a/CRM/Civicase/DAO/CaseSalesOrderContribution.php b/CRM/Civicase/DAO/CaseSalesOrderContribution.php index 4a29c39ea..c7be46e85 100644 --- a/CRM/Civicase/DAO/CaseSalesOrderContribution.php +++ b/CRM/Civicase/DAO/CaseSalesOrderContribution.php @@ -91,7 +91,7 @@ public function __construct() { * Whether to return the plural version of the title. */ public static function getEntityTitle($plural = FALSE) { - return $plural ? E::ts('Case Sales Order Contributions') : E::ts('Case Sales Order Contribution'); + return $plural ? E::ts('Quotation Contributions') : E::ts('Quotation Contribution'); } /** diff --git a/CRM/Civicase/DAO/CaseSalesOrderLine.php b/CRM/Civicase/DAO/CaseSalesOrderLine.php index d855e713b..a70bf5f84 100644 --- a/CRM/Civicase/DAO/CaseSalesOrderLine.php +++ b/CRM/Civicase/DAO/CaseSalesOrderLine.php @@ -134,7 +134,7 @@ public function __construct() { * Whether to return the plural version of the title. */ public static function getEntityTitle($plural = FALSE) { - return $plural ? E::ts('Case Sales Order Lines') : E::ts('Case Sales Order Line'); + return $plural ? E::ts('Quotation Lines') : E::ts('Quotation Line'); } /** From bbec84dc1615333f1fb2f18007296efc53f7c782 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 3 May 2023 14:01:36 +0100 Subject: [PATCH 123/199] BTHAB-152: Ensure logo_image on quotaion invoice is responsive --- templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl b/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl index e056a6d55..231cacced 100644 --- a/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl +++ b/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl @@ -8,10 +8,10 @@ -
- +
+
- +
From 19e83cbf5ecea78fc9b641b189e01944b62912b2 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 3 May 2023 14:59:10 +0100 Subject: [PATCH 124/199] BTHAB-152: Add domain name to quotation invoice --- CRM/Civicase/Form/CaseSalesOrderInvoice.php | 1 + CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php | 12 +++++++++++- .../Civicase/MessageTemplate/QuotationInvoice.tpl | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CRM/Civicase/Form/CaseSalesOrderInvoice.php b/CRM/Civicase/Form/CaseSalesOrderInvoice.php index 61ba72914..bf8d48c34 100644 --- a/CRM/Civicase/Form/CaseSalesOrderInvoice.php +++ b/CRM/Civicase/Form/CaseSalesOrderInvoice.php @@ -201,6 +201,7 @@ public static function getQuotationInvoice(): array { $model->setTerms($terms); $model->setSalesOrderId($salesOrderId); $model->setDomainLocation(self::getDomainLocation()); + $model->setDomainName($domain->name ?? ''); $rendered = $model->renderTemplate(); $rendered['format'] = $rendered['format'] ?? self::defaultInvoiceFormat(); diff --git a/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php b/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php index 3b439bc3d..35eac01d0 100644 --- a/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php +++ b/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php @@ -17,6 +17,8 @@ * @method $this setDomainLogo(?string $logo) * @method ?int getSalesOrderId() * @method $this setSalesOrderId(?int $salesOrderId) + * @method ?string getDomainName() + * @method $this setDomainName(?string $domainName) */ class CRM_Civicase_WorkflowMessage_SalesOrderInvoice extends GenericWorkflowMessage { @@ -57,9 +59,17 @@ class CRM_Civicase_WorkflowMessage_SalesOrderInvoice extends GenericWorkflowMess /** * Sales Order ID. * - * @var array + * @var int * @scope tokenContext */ protected $salesOrderId; + /** + * Domain Name. + * + * @var string + * @scope tplParams as domain_name + */ + protected $domainName; + } diff --git a/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl b/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl index 231cacced..6ca206f05 100644 --- a/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl +++ b/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl @@ -20,7 +20,7 @@ {ts}Client Name: {$sales_order.client.display_name}<{/ts} {ts}Date:{/ts} - Address + {$domain_name} From 805bbe7b47b40201f1c9887b9c0b9d8a25101d06 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 3 May 2023 14:40:05 +0100 Subject: [PATCH 125/199] BTHAB-119: Round off quotation lineitem money fields to 2 decimal places Due to division when creating quotaion invoice with percentage value, some of the fields can have values that exceed 14 decimal places, to resolve this we keep all the moeny fields ie. tax_amount, line_total and unit_price to 2 decimal places --- CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php index 99a24bfaa..647e91072 100644 --- a/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php +++ b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php @@ -140,12 +140,12 @@ private function getPreviousContributionLineItem() { private function lineItemToContributionLineItem(array $item) { return [ 'qty' => $item['quantity'], - 'tax_amount' => $item['tax'], + 'tax_amount' => round($item['tax'], 2), 'label' => $item['item_description'], 'entity_table' => 'civicrm_contribution', 'financial_type_id' => $item['financial_type_id'], - 'line_total' => $item['total'], - 'unit_price' => $item['unit_price'], + 'line_total' => round($item['total'], 2), + 'unit_price' => round($item['unit_price'], 2), ]; } From f8779667199436310496b4465a86ab90145ce728 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 4 May 2023 07:26:30 +0100 Subject: [PATCH 126/199] BTHAB-119: Prefix discount action link with percent icon --- civicase.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/civicase.php b/civicase.php index 69703d472..93c784c2d 100644 --- a/civicase.php +++ b/civicase.php @@ -580,7 +580,8 @@ function civicase_civicrm_searchKitTasks(array &$tasks, bool $checkPermissions, $actions['add_discount'] = [ 'module' => 'civicase-features', - 'title' => ts('Add Discount %'), + 'icon' => 'fa-percent', + 'title' => ts('Add Discount'), 'uiDialog' => ['templateUrl' => '~/civicase-features/quotations/directives/quotations-discount.directive.html'], ]; From 59f093fce37243a26c960ee1ee73116cadbabd68 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 12 May 2023 09:38:20 +0100 Subject: [PATCH 127/199] BTHAB-146: Move quotation create screen action button to footer of panel --- .../quotations-create.directive.html | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.html b/ang/civicase-features/quotations/directives/quotations-create.directive.html index f2391f764..f16e4a7c4 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.html @@ -229,24 +229,15 @@

{{ ts('Create Quotation') }}

- - -
-
-
-
- -
-
- -
-
-
-
+ +
@@ -254,4 +245,8 @@

{{ ts('Create Quotation') }}

#quotation__create .table>tbody>tr>td { padding: 10px 10px; } + .flex-between { + display: flex; + justify-content: space-between; + } From bf89e2f33b71e27c135cd5b3aa554dce46e22057 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 12 May 2023 09:39:21 +0100 Subject: [PATCH 128/199] BTHAB-146: Update sales_order entity title --- CRM/Civicase/DAO/CaseSalesOrder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CRM/Civicase/DAO/CaseSalesOrder.php b/CRM/Civicase/DAO/CaseSalesOrder.php index bb2718a65..5a85f9e39 100644 --- a/CRM/Civicase/DAO/CaseSalesOrder.php +++ b/CRM/Civicase/DAO/CaseSalesOrder.php @@ -174,7 +174,7 @@ public function __construct() { * Whether to return the plural version of the title. */ public static function getEntityTitle($plural = FALSE) { - return $plural ? E::ts('Quotation') : E::ts('Quotations'); + return $plural ? E::ts('Quotations') : E::ts('Quotation'); } /** From 572f0dd7db0d0957dca6ef84f673535a4a23e9db Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 12 May 2023 09:05:28 +0100 Subject: [PATCH 129/199] BTHAB-146: Hide remain_balance radio if theres no left over balance --- .../Form/CaseSalesOrderContributionCreate.php | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php index 7a9ad74cc..de4154a96 100644 --- a/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php +++ b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php @@ -45,11 +45,14 @@ public function buildQuickForm() { 'class' => 'form-control', 'min' => 1, ], FALSE); - $this->addElement('radio', 'to_be_invoiced', '', ts('Remaining Balance'), - self::INVOICE_REMAIN, - ['id' => 'invoice_remain'] - ); - $this->addRule('to_be_invoiced', ts('Invoice value is required'), 'required'); + + if ($this->hasRemainingBalance()) { + $this->addElement('radio', 'to_be_invoiced', '', ts('Remaining Balance'), + self::INVOICE_REMAIN, + ['id' => 'invoice_remain'] + ); + $this->addRule('to_be_invoiced', ts('Invoice value is required'), 'required'); + } $statusOptions = OptionValue::get() ->addSelect('value', 'label') @@ -159,6 +162,17 @@ public function validateAmount(array $values) { return TRUE; } + if (!$this->hasRemainingBalance()) { + $errors['to_be_invoiced'] = 'Unable to create a contribution due to insufficient balance.'; + } + + return $errors ?: TRUE; + } + + /** + * Checks if the sales order has left over balance to be invoiced. + */ + public function hasRemainingBalance() { $caseSalesOrder = CaseSalesOrder::get() ->addSelect('total_after_tax') ->addWhere('id', '=', $this->id) @@ -181,10 +195,10 @@ public function validateAmount(array $values) { $remainBalance = round($remainBalance, 2); if ($remainBalance <= 0) { - $errors['to_be_invoiced'] = 'Unable to create a contribution due to insufficient balance.'; + return FALSE; } - return $errors ?: TRUE; + return TRUE; } /** From 0a59c3603942fb31b65cdbca5b6599db20d43fc5 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 12 May 2023 09:20:40 +0100 Subject: [PATCH 130/199] BTHAB-146: Report numbers of contribution not created to user --- .../directives/quotations-contribution-bulk.directive.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js index 3e13f0264..3dd0a7dcf 100644 --- a/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js @@ -65,7 +65,10 @@ ctrl.run = false; ctrl.close(); - CRM.alert(`${contributionCreated} Invoices have been generated.`, ts('Success'), 'success'); + const contributionNotCreated = ctrl.ids.length - contributionCreated; + let message = `${contributionCreated} contributions have been generated`; + message += contributionNotCreated > 0 ? ` and no contributions were created for ${contributionNotCreated} quotes as there was no remaining amount to be invoiced` : ''; + CRM.alert(message, ts('Success'), 'success'); }); }; } From 69dac6b3dfd5ee45c060c484c7002652e6ddb398 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 12 May 2023 09:22:08 +0100 Subject: [PATCH 131/199] BTHAB-146: Allow user to add/remove lineitem on quotation contribution screen --- js/sales-order-contribution.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/sales-order-contribution.js b/js/sales-order-contribution.js index 448080584..5e3f229cf 100644 --- a/js/sales-order-contribution.js +++ b/js/sales-order-contribution.js @@ -36,7 +36,7 @@ $(``).insertBefore('#source'); $(``).insertBefore('#source'); $(``).insertBefore('#source'); - $('#totalAmount, #totalAmountORaddLineitem, #totalAmountORPriceSet, #price_set_id, #choose-manual, .remove_item, #add-another-item').hide(); + $('#totalAmount, #totalAmountORaddLineitem, #totalAmountORPriceSet, #price_set_id, #choose-manual').hide(); }); } From 01edc81fd5f9e3bf503689022623c2288571a25d Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 19 May 2023 13:38:17 +0100 Subject: [PATCH 132/199] BTHAB-106: Contribution Create action should support financial type ID of string or int --- Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php index b28f5a128..ece306df4 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php @@ -59,7 +59,7 @@ class ContributionCreateAction extends AbstractAction { /** * Contribution Financial Type ID. * - * @var string + * @var string|int */ protected $financialTypeId; From ff99ed946379e9cd251638452fbe4609000fa66f Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 19 May 2023 13:38:59 +0100 Subject: [PATCH 133/199] BTHAB-106: Update quotation edit page title --- .../quotations/directives/quotations-create.directive.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.html b/ang/civicase-features/quotations/directives/quotations-create.directive.html index f16e4a7c4..c3dc754ff 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.html @@ -1,5 +1,5 @@
-

{{ ts('Create Quotation') }}

+

{{ ts(isUpdate ? 'Edit Quotation':'Create Quotation') }}

From 24beb1e56985d41a4e4bea85f4bb690fec8203d9 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 19 May 2023 13:39:45 +0100 Subject: [PATCH 134/199] BTHAB-106: Set Owner field to current quotation owner --- .../quotations/directives/quotations-create.directive.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index 8a9adef15..7e6f0624d 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -100,6 +100,7 @@ CaseUtils.getSalesOrderAndLineItems(salesOrderId).then((result) => { $scope.isUpdate = true; $scope.salesOrder = result; + $scope.salesOrder.owner_id = parseInt(result.owner_id); $scope.salesOrder.quotation_date = $.datepicker.formatDate('yy-mm-dd', new Date(result.quotation_date)); $scope.salesOrder.status_id = (result.status_id).toString(); CRM.wysiwyg.setVal('#sales-order-description', $scope.salesOrder.description); From 24dbc95b6589890eab90b8712fc1cc7991833d60 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 19 May 2023 13:40:15 +0100 Subject: [PATCH 135/199] BTHAB-106: Allow quotation description to be multiline --- templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl b/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl index 6ca206f05..4e4918993 100644 --- a/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl +++ b/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl @@ -88,7 +88,7 @@ {foreach from=$sales_order.items key=k item=item} - {$item.item_description|truncate:30:"..."} + {$item.item_description} {$item.quantity} {$item.unit_price|crmMoney:$sales_order.currency} {if empty($item.discounted_percentage) } 0 {else}{$item.discounted_percentage}{/if}% From a675023ce8f8eaec5b4d8096435d0e7878fdf668 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 22 May 2023 17:41:29 +0100 Subject: [PATCH 136/199] BTHAB-170: Ensure each contribution create is persisted --- Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php index ece306df4..d3d134586 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php @@ -90,6 +90,8 @@ private function createContribution() { catch (\Exception $e) { $transaction->rollback(); } + + $transaction->commit(); } return ['created_contributions_count' => $createdContributionsCount]; @@ -104,7 +106,7 @@ private function createContribution() { * Array of price fields. */ private function createContributionWithLineItems(int $salesOrderId, array $priceField): array { - $salesOrderContribution = new salesOrderlineItemGenerator($salesOrderId, $this->toBeInvoiced, $this->percentValue); + $salesOrderContribution = new salesOrderlineItemGenerator($salesOrderId, $this->toBeInvoiced, $this->percentValue ?? 0); $lineItems = $salesOrderContribution->generateLineItems(); $taxAmount = $lineTotal = 0; From 5ae684210979b042ddf106ce306c1a93f7bfe2f8 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 22 May 2023 17:44:20 +0100 Subject: [PATCH 137/199] BTHAB-170: Prevent previous line_item from colliding with new line_item --- CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php index 647e91072..495073583 100644 --- a/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php +++ b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php @@ -105,6 +105,7 @@ private function getPreviousContributionLineItem() { ->addSelect('contribution_id') ->addWhere('case_sales_order_id.id', '=', $this->salesOrderId) ->addChain('items', LineItem::get() + ->addSelect('qty', 'unit_price', 'tax_amount', 'line_total', 'entity_table', 'label', 'financial_type_id') ->addWhere('contribution_id', '=', '$contribution_id') ) ->execute(); @@ -113,10 +114,11 @@ private function getPreviousContributionLineItem() { $items = $contribution['items']; if (empty($items)) { - return []; + continue; } foreach ($items as $item) { + unset($item['id']); $item['qty'] = $item['qty']; $item['unit_price'] = -1 * $item['unit_price']; $item['tax_amount'] = -1 * $item['tax_amount']; From 10b84814d0648e96859663b2c3a7c900982a19ef Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 23 May 2023 16:31:08 +0100 Subject: [PATCH 138/199] BTHAB-170: Add test to enusre bulk contribtuion create action works as expected --- .../ContributionCreateActionTest.php | 157 +++++++++++++++++- 1 file changed, 155 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php b/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php index dfac2154c..cc513ef69 100644 --- a/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php +++ b/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php @@ -2,8 +2,8 @@ use Civi\Api4\CaseSalesOrder; use Civi\Api4\CaseSalesOrderContribution as Api4CaseSalesOrderContribution; -use CRM_Civicase_Test_Fabricator_Contact as ContactFabricator; use CRM_Civicase_Service_CaseSalesOrderLineItemsGenerator as CaseSalesOrderContribution; +use CRM_Civicase_Test_Fabricator_Contact as ContactFabricator; /** * CaseSalesOrder.ContributionCreateAction API Test Case. @@ -19,7 +19,7 @@ class Civi_Api4_CaseSalesOrder_ContributionCreateActionTest extends BaseHeadless /** * Setup data before tests run. */ - public function setUp() { + public function setUp(): void { $this->generatePriceField(); $contact = ContactFabricator::fabricate(); $this->registerCurrentLoggedInContactInSession($contact['id']); @@ -134,6 +134,43 @@ public function testAppropriateContributionAmountIsCreated($expectedPercent, $co $this->assertEquals(round(($expectedPercent * $computedTotal['totalAfterTax']) / 100, 1), round($paidTotal, 1)); } + /** + * Ensures The expected numbers of contributions are created for bulk action. + * + * @dataProvider provideBulkContributionCreateData + */ + public function testExpectedContributionCountIsCreated($expectedCount, $contributionCreateData, $salesOrderData) { + $salesOrders = []; + + foreach ($salesOrderData as $data) { + $salesOrder = $this->createCaseSalesOrder(); + + if ($data['previouslyInvoiced'] > 0) { + CaseSalesOrder::contributionCreateAction() + ->setSalesOrderIds([$salesOrder['id']]) + ->setStatusId(1) + ->setToBeInvoiced(CaseSalesOrderContribution::INVOICE_PERCENT) + ->setPercentValue($data['previouslyInvoiced']) + ->setDate(date("Y-m-d")) + ->setFinancialTypeId(1) + ->execute(); + } + + $salesOrders[] = $salesOrder; + } + + $result = CaseSalesOrder::contributionCreateAction() + ->setSalesOrderIds(array_column($salesOrders, 'id')) + ->setStatusId($contributionCreateData['statusId']) + ->setToBeInvoiced($contributionCreateData['toBeInvoiced']) + ->setPercentValue($contributionCreateData['percentValue']) + ->setDate($contributionCreateData['date']) + ->setFinancialTypeId($contributionCreateData['financialTypeId']) + ->execute(); + + $this->assertEquals($expectedCount, $result['created_contributions_count']); + } + /** * Provides data to test contribution create action. * @@ -320,4 +357,120 @@ public function provideContributionCreateData(): array { ]; } + /** + * Provides data to test bulk contribution create action. + * + * @return array + * Array of different scenarios + */ + public function provideBulkContributionCreateData(): array { + return [ + '1 percentvalue contribution is created for 1 quotaition' => [ + 'expectedCount' => 1, + 'contributionCreateData' => [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 100, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + 'salesOrderData' => [ + [ + 'previouslyInvoiced' => 60, + ], + ], + ], + '2 percentvalue contributions are created for 2 quotations' => [ + 'expectedCount' => 2, + 'contributionCreateData' => [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 100, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + 'salesOrderData' => [ + [ + 'previouslyInvoiced' => 60, + ], + [ + 'previouslyInvoiced' => 100, + ], + ], + ], + '2 percentvalues contribution are created for 2 quotations with 100% already invoiced' => [ + 'expectedCount' => 2, + 'contributionCreateData' => [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_PERCENT, + 'percentValue' => 100, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + 'salesOrderData' => [ + [ + 'previouslyInvoiced' => 100, + ], + [ + 'previouslyInvoiced' => 100, + ], + ], + ], + 'No remain value contribution is created for 2 quotaions with 100% already invoiced' => [ + 'expectedCount' => 0, + 'contributionCreateData' => [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_REMAIN, + 'percentValue' => 0, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + 'salesOrderData' => [ + [ + 'previouslyInvoiced' => 100, + ], + [ + 'previouslyInvoiced' => 100, + ], + ], + ], + '1 remain value contribution is created for 2 quotaions, where only one has remain value' => [ + 'expectedCount' => 1, + 'contributionCreateData' => [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_REMAIN, + 'percentValue' => 0, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + 'salesOrderData' => [ + [ + 'previouslyInvoiced' => 80, + ], + [ + 'previouslyInvoiced' => 100, + ], + ], + ], + '2 remain value contribution is created for 2 quotaions, where the two has remain value' => [ + 'expectedCount' => 2, + 'contributionCreateData' => [ + 'statusId' => 1, + 'toBeInvoiced' => CaseSalesOrderContribution::INVOICE_REMAIN, + 'percentValue' => 0, + 'date' => date("Y-m-d"), + 'financialTypeId' => '1', + ], + 'salesOrderData' => [ + [ + 'previouslyInvoiced' => 0, + ], + [ + 'previouslyInvoiced' => 0, + ], + ], + ], + ]; + } + } From bdcfa4f8dca69ec85ad26796cd8d7b6f97496d74 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 23 May 2023 13:39:57 +0100 Subject: [PATCH 139/199] BTHAB-106: Prevent setting owner to current_user on edit screen --- .../quotations/directives/quotations-create.directive.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index 7e6f0624d..6208262a5 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -64,7 +64,6 @@ currency: defaultCurrency, status_id: SalesOrderStatus.getValueByName('new'), clientId: null, - owner_id: Contact.getCurrentContactID(), quotation_date: $.datepicker.formatDate('yy-mm-dd', new Date()), items: [{ product_id: null, @@ -94,13 +93,13 @@ const salesOrderId = $location.search().id; if (!salesOrderId) { + $scope.salesOrder.owner_id = Contact.getCurrentContactID(); return; } CaseUtils.getSalesOrderAndLineItems(salesOrderId).then((result) => { $scope.isUpdate = true; $scope.salesOrder = result; - $scope.salesOrder.owner_id = parseInt(result.owner_id); $scope.salesOrder.quotation_date = $.datepicker.formatDate('yy-mm-dd', new Date(result.quotation_date)); $scope.salesOrder.status_id = (result.status_id).toString(); CRM.wysiwyg.setVal('#sales-order-description', $scope.salesOrder.description); From 34d63c726b61ba92a910971e12e5e8d63f4ce5f4 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 13 Jul 2023 12:15:45 +0100 Subject: [PATCH 140/199] BTHAB-200: Display only relevant quotations for the viewed contact --- ang/afsearchContactQuotations.aff.html | 2 +- .../quotations/directives/quotations-list.directive.html | 2 +- .../quotations/directives/quotations-list.directive.js | 4 ---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/ang/afsearchContactQuotations.aff.html b/ang/afsearchContactQuotations.aff.html index ea5ffde78..eda87620a 100644 --- a/ang/afsearchContactQuotations.aff.html +++ b/ang/afsearchContactQuotations.aff.html @@ -21,5 +21,5 @@
- +
diff --git a/ang/civicase-features/quotations/directives/quotations-list.directive.html b/ang/civicase-features/quotations/directives/quotations-list.directive.html index 1d472f036..495a03c7d 100644 --- a/ang/civicase-features/quotations/directives/quotations-list.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-list.directive.html @@ -11,7 +11,7 @@

{{ ts('Manage Quotations') }}

- +
diff --git a/ang/civicase-features/quotations/directives/quotations-list.directive.js b/ang/civicase-features/quotations/directives/quotations-list.directive.js index 414c1ea81..d527176bc 100644 --- a/ang/civicase-features/quotations/directives/quotations-list.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-list.directive.js @@ -24,10 +24,6 @@ $scope.redirectToQuotationCreationScreen = redirectToQuotationCreationScreen; (function init () { - if ($scope.contactId) { - $location.search().cid = $scope.contactId; - } - addEventToElementsWhenInDOMTree(); }()); From e53cbd9c84fd8219b9142782bed7d4d6cf01047a Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 17 Jul 2023 06:00:57 +0100 Subject: [PATCH 141/199] BTHAB-200: Refresh Case order table post delete --- CRM/Civicase/Form/CaseSalesOrderDelete.php | 2 ++ ang/civicase-features.ang.php | 2 ++ js/sales-order-contribution.js | 4 ---- templates/CRM/Civicase/Form/CaseSalesOrderDelete.tpl | 5 ++++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CRM/Civicase/Form/CaseSalesOrderDelete.php b/CRM/Civicase/Form/CaseSalesOrderDelete.php index b64834667..7922e23db 100755 --- a/CRM/Civicase/Form/CaseSalesOrderDelete.php +++ b/CRM/Civicase/Form/CaseSalesOrderDelete.php @@ -30,6 +30,8 @@ public function preProcess() { * {@inheritDoc} */ public function buildQuickForm() { + $this->assign('id', $this->id); + $this->addButtons([ [ 'type' => 'submit', diff --git a/ang/civicase-features.ang.php b/ang/civicase-features.ang.php index 13ab14294..6a397ae7b 100644 --- a/ang/civicase-features.ang.php +++ b/ang/civicase-features.ang.php @@ -73,6 +73,8 @@ function set_case_sales_order_status(&$options) { 'crmUi', 'crmUtil', 'civicase', + 'ui.sortable', + 'dialogService', 'civicase-base', 'afsearchQuotations', 'afsearchContactQuotations', diff --git a/js/sales-order-contribution.js b/js/sales-order-contribution.js index 5e3f229cf..1a74e349f 100644 --- a/js/sales-order-contribution.js +++ b/js/sales-order-contribution.js @@ -40,10 +40,6 @@ }); } - $("a[target='crm-popup']").on('crmPopupFormSuccess', function (e) { - CRM.refreshParent(e); - }); - /** * @param {number} quantity Item quantity * @param {number} unitPrice Item unit price diff --git a/templates/CRM/Civicase/Form/CaseSalesOrderDelete.tpl b/templates/CRM/Civicase/Form/CaseSalesOrderDelete.tpl index 8cb19585d..68af99c89 100755 --- a/templates/CRM/Civicase/Form/CaseSalesOrderDelete.tpl +++ b/templates/CRM/Civicase/Form/CaseSalesOrderDelete.tpl @@ -11,10 +11,13 @@
+{/literal} From 7534b0b33307024c3e5b2e211cf11396a76439fd Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Thu, 21 Sep 2023 10:05:52 +0100 Subject: [PATCH 173/199] BTHAB-182: Use Invoice render service --- CRM/Civicase/Form/CaseSalesOrderInvoice.php | 112 +------------------- 1 file changed, 3 insertions(+), 109 deletions(-) diff --git a/CRM/Civicase/Form/CaseSalesOrderInvoice.php b/CRM/Civicase/Form/CaseSalesOrderInvoice.php index bf8d48c34..9315f99e6 100644 --- a/CRM/Civicase/Form/CaseSalesOrderInvoice.php +++ b/CRM/Civicase/Form/CaseSalesOrderInvoice.php @@ -1,10 +1,6 @@ addWhere('id', '=', $salesOrderId) - ->addChain('items', CaseSalesOrderLine::get() - ->addWhere('sales_order_id', '=', '$id') - ->addSelect('*', 'product_id.name', 'financial_type_id.name') - ) - ->addChain('computedRates', CaseSalesOrder::computeTotal() - ->setLineItems('$items') - ) - ->addChain('client', Contact::get() - ->addWhere('id', '=', '$client_id'), 0 - ) - ->execute() - ->first(); - if (!empty($caseSalesOrder['client_id'])) { - $caseSalesOrder['clientAddress'] = Address::get() - ->addSelect('*', 'country_id:label', 'state_province_id:label') - ->addWhere('contact_id', '=', $caseSalesOrder['client_id']) - ->execute() - ->first(); - $caseSalesOrder['clientAddress']['country'] = $caseSalesOrder['clientAddress']['country_id:label']; - $caseSalesOrder['clientAddress']['state'] = $caseSalesOrder['clientAddress']['state_province_id:label']; - } - - $caseSalesOrder['taxRates'] = $caseSalesOrder['computedRates'][0]['taxRates'] ?? []; - $caseSalesOrder['quotation_date'] = date('Y-m-d', strtotime($caseSalesOrder['quotation_date'])); - - $domain = CRM_Core_BAO_Domain::getDomain(); - $organisation = Contact::get() - ->addSelect('image_URL') - ->addWhere('id', '=', $domain->contact_id) - ->execute() - ->first(); - - $model = new CRM_Civicase_WorkflowMessage_SalesOrderInvoice(); - $terms = self::getTerms(); - $model->setDomainLogo($organisation['image_URL']); - $model->setSalesOrder($caseSalesOrder); - $model->setTerms($terms); - $model->setSalesOrderId($salesOrderId); - $model->setDomainLocation(self::getDomainLocation()); - $model->setDomainName($domain->name ?? ''); - $rendered = $model->renderTemplate(); - - $rendered['format'] = $rendered['format'] ?? self::defaultInvoiceFormat(); - - return $rendered; - } - - /** - * Returns the Quotation invoice terms. - */ - private static function getTerms() { - $terms = NULL; - $invoicing = Setting::get() - ->addSelect('invoicing') - ->execute() - ->first(); - - if (!empty($invoicing['value'])) { - $terms = Civi::settings()->get('quotations_notes'); - } - - return $terms; - } - - /** - * Gets domain location. - * - * @return array - * An array of address lines. - */ - private static function getDomainLocation() { - $domain = CRM_Core_BAO_Domain::getDomain(); - $locParams = ['contact_id' => $domain->contact_id]; - $locationDefaults = CRM_Core_BAO_Location::getValues($locParams); - if (empty($locationDefaults['address'][1])) { - return []; - } - $stateProvinceId = $locationDefaults['address'][1]['state_province_id'] ?? NULL; - $stateProvinceAbbreviationDomain = !empty($stateProvinceId) ? CRM_Core_PseudoConstant::stateProvinceAbbreviation($stateProvinceId) : ''; - $countryId = $locationDefaults['address'][1]['country_id']; - $countryDomain = !empty($countryId) ? CRM_Core_PseudoConstant::country($countryId) : ''; - - return [ - 'street_address' => CRM_Utils_Array::value('street_address', CRM_Utils_Array::value('1', $locationDefaults['address'])), - 'supplemental_address_1' => CRM_Utils_Array::value('supplemental_address_1', CRM_Utils_Array::value('1', $locationDefaults['address'])), - 'supplemental_address_2' => CRM_Utils_Array::value('supplemental_address_2', CRM_Utils_Array::value('1', $locationDefaults['address'])), - 'supplemental_address_3' => CRM_Utils_Array::value('supplemental_address_3', CRM_Utils_Array::value('1', $locationDefaults['address'])), - 'city' => CRM_Utils_Array::value('city', CRM_Utils_Array::value('1', $locationDefaults['address'])), - 'postal_code' => CRM_Utils_Array::value('postal_code', CRM_Utils_Array::value('1', $locationDefaults['address'])), - 'state' => $stateProvinceAbbreviationDomain, - 'country' => $countryDomain, - ]; + /** @var \CRM_Civicase_Service_CaseSalesOrderInvoice */ + $invoiceService = new \CRM_Civicase_Service_CaseSalesOrderInvoice(new \CRM_Civicase_WorkflowMessage_SalesOrderInvoice()); + return $invoiceService->render($salesOrderId); } /** @@ -273,17 +178,6 @@ protected function getRows(): array { return $rows; } - /** - * Returns the default format to use for Invoice. - */ - private static function defaultInvoiceFormat() { - return [ - 'margin_top' => 10, - 'margin_left' => 65, - 'metric' => 'px', - ]; - } - /** * Renders and return the generated PDF to the browser. */ From 23f17ddac56573f44863dd276bd837d46519bd35 Mon Sep 17 00:00:00 2001 From: Erawat Chamanont Date: Thu, 21 Sep 2023 22:51:07 +0100 Subject: [PATCH 174/199] BTHAB-186: Rename service class The renaming is to align with other objects and provides more meaningful --- CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php | 4 ++-- CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php | 2 +- ...tribution.php => CaseSalesOrderContributionCalculator.php} | 2 +- .../CaseSalesOrder/ComputeTotalAmountInvoicedAction.php | 2 +- .../Action/CaseSalesOrder/ComputeTotalAmountPaidAction.php | 2 +- Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) rename CRM/Civicase/Service/{CaseSaleOrderContribution.php => CaseSalesOrderContributionCalculator.php} (98%) diff --git a/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php b/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php index 412859ec4..ead823b27 100644 --- a/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php +++ b/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php @@ -9,7 +9,7 @@ class CRM_Civicase_Hook_Post_CaseSalesOrderPayment { /** - * Updates CaseSaleOrder statuses when creating a payment transcation. + * Updates CaseSalesOrder statuses when creating a payment transaction. * * @param string $op * The operation being performed. @@ -50,7 +50,7 @@ public function run($op, $objectName, $objectId, &$objectRef) { $transaction = CRM_Core_Transaction::create(); try { - $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSaleOrderContribution($salesOrderID); + $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSalesOrderContributionCalculator($salesOrderID); $paymentStatusID = $caseSaleOrderContributionService->calculatePaymentStatus(); $invoicingStatusID = $caseSaleOrderContributionService->calculateInvoicingStatus(); diff --git a/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php b/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php index c4b8a08b9..86285be05 100644 --- a/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php +++ b/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php @@ -45,7 +45,7 @@ public function run($op, $objectName, $objectId, &$objectRef) { $transaction = CRM_Core_Transaction::create(); try { - $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSaleOrderContribution($salesOrderId); + $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSalesOrderContributionCalculator($salesOrderId); $paymentStatusID = $caseSaleOrderContributionService->calculatePaymentStatus(); $invoicingStatusID = $caseSaleOrderContributionService->calculateInvoicingStatus(); diff --git a/CRM/Civicase/Service/CaseSaleOrderContribution.php b/CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php similarity index 98% rename from CRM/Civicase/Service/CaseSaleOrderContribution.php rename to CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php index 09a8af022..1e3a99ea8 100644 --- a/CRM/Civicase/Service/CaseSaleOrderContribution.php +++ b/CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php @@ -10,7 +10,7 @@ * This class provides calculations for payment and invoices that * attached to the sale order. */ -class CRM_Civicase_Service_CaseSaleOrderContribution { +class CRM_Civicase_Service_CaseSalesOrderContributionCalculator { /** * Case Sales Order object. diff --git a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountInvoicedAction.php b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountInvoicedAction.php index a7be9c48c..d35fa3298 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountInvoicedAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountInvoicedAction.php @@ -25,7 +25,7 @@ public function _run(Result $result) { // phpcs:ignore if (!$this->salesOrderID) { return; } - $service = new \CRM_Civicase_Service_CaseSaleOrderContribution($this->salesOrderID); + $service = new \CRM_Civicase_Service_CaseSalesOrderContributionCalculator($this->salesOrderID); $result['amount'] = $service->calculateTotalInvoicedAmount(); } diff --git a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountPaidAction.php b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountPaidAction.php index 9fcf1b8a2..7b022ae53 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountPaidAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountPaidAction.php @@ -24,7 +24,7 @@ public function _run(Result $result) { // phpcs:ignore if (!$this->salesOrderID) { return; } - $service = new \CRM_Civicase_Service_CaseSaleOrderContribution($this->salesOrderID); + $service = new \CRM_Civicase_Service_CaseSalesOrderContributionCalculator($this->salesOrderID); $result['amount'] = $service->calculateTotalPaidAmount(); } diff --git a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php index ce7401a9f..1f766af03 100644 --- a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php @@ -49,7 +49,7 @@ protected function writeRecord($items) { $salesOrder['total_after_tax'] = $total['totalAfterTax']; $saleOrderId = $salesOrder['id'] ?? NULL; - $caseSaleOrderContributionService = new \CRM_Civicase_Service_CaseSaleOrderContribution($saleOrderId); + $caseSaleOrderContributionService = new \CRM_Civicase_Service_CaseSalesOrderContributionCalculator($saleOrderId); $salesOrder['payment_status_id'] = $caseSaleOrderContributionService->calculateInvoicingStatus(); $salesOrder['invoicing_status_id'] = $caseSaleOrderContributionService->calculatePaymentStatus(); From 43e45487cfadc906060aa3e70c4e087f14c14c10 Mon Sep 17 00:00:00 2001 From: Erawat Chamanont Date: Fri, 22 Sep 2023 11:26:38 +0100 Subject: [PATCH 175/199] BTHAB-186: Add case opportunity custom fields --- ...stomGroup_Cases_OpportunityDetails.mgd.php | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 managed/CustomGroup_Cases_OpportunityDetails.mgd.php diff --git a/managed/CustomGroup_Cases_OpportunityDetails.mgd.php b/managed/CustomGroup_Cases_OpportunityDetails.mgd.php new file mode 100644 index 000000000..2241110ea --- /dev/null +++ b/managed/CustomGroup_Cases_OpportunityDetails.mgd.php @@ -0,0 +1,234 @@ + 'CustomGroup_Case_Opportunity_Details', + 'entity' => 'CustomGroup', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'Case_Opportunity_Details', + 'title' => 'Opportunity Details', + 'extends' => 'Case', + 'extends_entity_column_value' => NULL, + 'style' => 'Inline', + 'collapse_display' => FALSE, + 'help_pre' => '', + 'help_post' => '', + 'weight' => 70, + 'is_active' => TRUE, + 'is_multiple' => FALSE, + 'min_multiple' => NULL, + 'max_multiple' => NULL, + 'collapse_adv_display' => TRUE, + 'created_date' => '2023-09-21 14:46:02', + 'is_reserved' => FALSE, + 'is_public' => FALSE, + 'icon' => '', + 'extends_entity_column_id' => NULL, + ], + ], + ], + [ + 'name' => 'CustomGroup_Case_Opportunity_Details_CustomField_Total_Amount_Quoted', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Case_Opportunity_Details', + 'name' => 'Total_Amount_Quoted', + 'label' => 'Total Amount Quoted', + 'data_type' => 'Money', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => TRUE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'total_amount_quoted', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], + [ + 'name' => 'CustomGroup_Case_Opportunity_Details_CustomField_Total_Amount_Invoiced', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Case_Opportunity_Details', + 'name' => 'Total_Amount_Invoiced', + 'label' => 'Total Amount Invoiced', + 'data_type' => 'Money', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => TRUE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'total_amount_invoiced', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], + [ + 'name' => 'CustomGroup_Case_Opportunity_Details_CustomField_Invoicing_Status', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Case_Opportunity_Details', + 'name' => 'Invoicing_Status', + 'label' => 'Invoicing Status', + 'data_type' => 'String', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => TRUE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'invoicing_status', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], + [ + 'name' => 'CustomGroup_Case_Opportunity_Details_CustomField_Total_amounts_paid', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Case_Opportunity_Details', + 'name' => 'Total_Amounts_Paid', + 'label' => 'Total Amounts Paid', + 'data_type' => 'Money', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => TRUE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'total_amounts_paid', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], + [ + 'name' => 'CustomGroup_Case_Opportunity_Details_CustomField_Payments_Status', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Case_Opportunity_Details', + 'name' => 'Payments_Status', + 'label' => 'Payments Status', + 'data_type' => 'String', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => TRUE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'payments_status', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], +]; From 29c02fc66c51b632747da9d099a3e60720304f71 Mon Sep 17 00:00:00 2001 From: Erawat Chamanont Date: Fri, 22 Sep 2023 12:27:20 +0100 Subject: [PATCH 176/199] BTHAB-186: Update case opportunity details Update case opportunity statuses, and total amounts when: * Create or update sales order contribution * Create or update sales order * Create a payment --- .../Hook/Post/CaseSalesOrderPayment.php | 76 ++++++-- .../Post/CreateSalesOrderContribution.php | 19 +- .../AbstractBaseSalesOrderCalculator.php | 94 ++++++++++ .../CaseSalesOrderContributionCalculator.php | 80 +++----- .../CaseSalesOrderOpportunityCalculator.php | 171 ++++++++++++++++++ .../CaseSalesOrder/SalesOrderSaveAction.php | 26 +++ 6 files changed, 388 insertions(+), 78 deletions(-) create mode 100644 CRM/Civicase/Service/AbstractBaseSalesOrderCalculator.php create mode 100644 CRM/Civicase/Service/CaseSalesOrderOpportunityCalculator.php diff --git a/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php b/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php index ead823b27..894a0d81a 100644 --- a/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php +++ b/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php @@ -21,28 +21,37 @@ class CRM_Civicase_Hook_Post_CaseSalesOrderPayment { * Object reference. */ public function run($op, $objectName, $objectId, &$objectRef) { - if (!$this->shouldRun($op, $objectName)) { + if (!$this->shouldRun($op, $objectName, $objectRef)) { return; } - $entityFinancialTrxn = civicrm_api3('EntityFinancialTrxn', 'get', [ - 'sequential' => 1, - 'entity_table' => 'civicrm_contribution', - 'financial_trxn_id' => $objectRef->financial_trxn_id, - ]); - - if (empty($entityFinancialTrxn['values'][0])) { + $financialTrnxId = $objectRef->financial_trxn_id; + if (empty($financialTrnxId)) { return; } - $contributionId = $entityFinancialTrxn['values'][0]['entity_id']; + $contributionId = $this->getContributionId($financialTrnxId); + if (empty($contributionId)) { + return; + } - $salesOrderID = Contribution::get() - ->addSelect('Opportunity_Details.Quotation') + $contribution = Contribution::get() + ->addSelect('Opportunity_Details.Case_Opportunity', 'Opportunity_Details.Quotation') ->addWhere('id', '=', $contributionId) ->execute() - ->first()['Opportunity_Details.Quotation']; + ->first(); + + $this->updateQuotationFinancialStatuses($contribution['Opportunity_Details.Quotation']); + $this->updateCaseOpportunityFinancialDetails($contribution['Opportunity_Details.Case_Opportunity']); + } + /** + * Updates CaseSalesOrder financial statuses. + * + * @param int $salesOrderID + * CaseSalesOrder ID. + */ + private function updateQuotationFinancialStatuses(int $salesOrderID): void { if (empty($salesOrderID)) { return; } @@ -68,6 +77,43 @@ public function run($op, $objectName, $objectId, &$objectRef) { } } + /** + * Updates Case financial statuses. + * + * @param int? $caseId + * CaseSalesOrder ID. + */ + private function updateCaseOpportunityFinancialDetails(?int $caseId) { + if (empty($caseId)) { + return; + } + + try { + $calculator = new CRM_Civicase_Service_CaseSalesOrderOpportunityCalculator($caseId); + $calculator->updateOpportunityFinancialDetails(); + } + catch (\Throwable $th) { + CRM_Core_Error::statusBounce(ts('Error updating opportunity details')); + } + } + + /** + * Gets Contribution ID by Financial Transaction ID. + */ + private function getContributionId($financialTrxnId) { + $entityFinancialTrxn = civicrm_api3('EntityFinancialTrxn', 'get', [ + 'sequential' => 1, + 'entity_table' => 'civicrm_contribution', + 'financial_trxn_id' => $financialTrxnId, + ]); + + if (empty($entityFinancialTrxn['values'][0])) { + return NULL; + } + + return $entityFinancialTrxn['values'][0]['entity_id']; + } + /** * Determines if the hook should run or not. * @@ -75,12 +121,14 @@ public function run($op, $objectName, $objectId, &$objectRef) { * The operation being performed. * @param string $objectName * Object name. + * @param string $objectRef + * The hook object reference. * * @return bool * returns a boolean to determine if hook will run or not. */ - private function shouldRun($op, $objectName) { - return $objectName == 'EntityFinancialTrxn' && $op == 'create'; + private function shouldRun($op, $objectName, $objectRef) { + return $objectName == 'EntityFinancialTrxn' && $op == 'create' && property_exists($objectRef, 'financial_trxn_id'); } } diff --git a/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php b/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php index 86285be05..e93bf6078 100644 --- a/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php +++ b/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php @@ -34,13 +34,15 @@ public function run($op, $objectName, $objectId, &$objectRef) { return; } + $salesOrder = CaseSalesOrder::get() + ->addSelect('status_id', 'case_id') + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->first(); + $salesOrderStatusId = CRM_Utils_Request::retrieve('sales_order_status_id', 'Integer'); if (empty($salesOrderStatusId)) { - $salesOrderStatusId = CaseSalesOrder::get() - ->addSelect('status_id') - ->addWhere('id', '=', $salesOrderId) - ->execute() - ->first()['status_id']; + $salesOrder = $salesOrder['status_id']; } $transaction = CRM_Core_Transaction::create(); @@ -55,6 +57,13 @@ public function run($op, $objectName, $objectId, &$objectRef) { ->addValue('invoicing_status_id', $invoicingStatusID) ->addValue('payment_status_id', $paymentStatusID) ->execute(); + + $caseId = $salesOrder['case_id']; + if (empty($caseId)) { + return; + } + $calculator = new CRM_Civicase_Service_CaseSalesOrderOpportunityCalculator($caseId); + $calculator->updateOpportunityFinancialDetails(); } catch (\Throwable $th) { $transaction->rollback(); diff --git a/CRM/Civicase/Service/AbstractBaseSalesOrderCalculator.php b/CRM/Civicase/Service/AbstractBaseSalesOrderCalculator.php new file mode 100644 index 000000000..1e9b74b2c --- /dev/null +++ b/CRM/Civicase/Service/AbstractBaseSalesOrderCalculator.php @@ -0,0 +1,94 @@ +paymentStatusOptionValues = $this->getOptionValues('case_sales_order_payment_status'); + $this->invoicingStatusOptionValues = $this->getOptionValues('case_sales_order_invoicing_status'); + } + + /** + * Gets option values by option group name. + * + * @param string $name + * Option group name. + * + * @return array + * Option values. + * + * @throws API_Exception + * @throws \Civi\API\Exception\UnauthorizedException + */ + protected function getOptionValues($name) { + return OptionValue::get(FALSE) + ->addSelect('*') + ->addWhere('option_group_id:name', '=', $name) + ->execute() + ->getArrayCopy(); + } + + /** + * Gets status (option values' value) from the given options. + * + * @param string $needle + * Search value. + * @param array $options + * Option value. + * + * @return string + * Option values' value. + */ + protected function getValueFromOptionValues($needle, $options) { + $key = array_search($needle, array_column($options, 'name')); + + return $options[$key]['value']; + } + + /** + * Gets status (option values' label) from the given options. + * + * @param string $needle + * Search value. + * @param array $options + * Option value. + * + * @return string + * Option values' value. + */ + protected function getLabelFromOptionValues($needle, $options) { + $key = array_search($needle, array_column($options, 'name')); + + return $options[$key]['label']; + } + +} diff --git a/CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php b/CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php index 1e3a99ea8..2e44c8470 100644 --- a/CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php +++ b/CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php @@ -2,7 +2,6 @@ use Civi\Api4\CaseSalesOrder; use Civi\Api4\Contribution; -use Civi\Api4\OptionValue; /** * Case Sale Order Contribution Service. @@ -10,7 +9,7 @@ * This class provides calculations for payment and invoices that * attached to the sale order. */ -class CRM_Civicase_Service_CaseSalesOrderContributionCalculator { +class CRM_Civicase_Service_CaseSalesOrderContributionCalculator extends CRM_Civicase_Service_AbstractBaseSalesOrderCalculator { /** * Case Sales Order object. @@ -18,18 +17,7 @@ class CRM_Civicase_Service_CaseSalesOrderContributionCalculator { * @var array|null */ private ?array $salesOrder; - /** - * Case Sales Order payment status option values. - * - * @var array - */ - private array $paymentStatusOptionValues; - /** - * Case Sales Order Invoicing status option values. - * - * @var array - */ - private array $invoicingStatusOptionValues; + /** * List of contributions that links to the sales order. * @@ -60,12 +48,12 @@ class CRM_Civicase_Service_CaseSalesOrderContributionCalculator { * @throws \Civi\API\Exception\UnauthorizedException */ public function __construct($salesOrderId) { + parent::__construct(); $this->salesOrder = $this->getSalesOrder($salesOrderId); - $this->paymentStatusOptionValues = $this->getOptionValues('case_sales_order_payment_status'); - $this->invoicingStatusOptionValues = $this->getOptionValues('case_sales_order_invoicing_status'); $this->contributions = $this->getContributions(); $this->totalInvoicedAmount = $this->getTotalInvoicedAmount(); $this->totalPaymentsAmount = $this->getTotalPaymentsAmount(); + } /** @@ -89,6 +77,13 @@ public function calculateTotalPaidAmount(): float { return $this->getTotalPaymentsAmount(); } + /** + * Gets SalesOrder Total amount after tax. + */ + public function getQuotedAmount(): float { + return $this->salesOrder['total_after_tax']; + } + /** * Calculates invoicing status. * @@ -97,15 +92,15 @@ public function calculateTotalPaidAmount(): float { */ public function calculateInvoicingStatus() { if (empty($this->salesOrder) || empty($this->contributions)) { - return $this->getStatus('no_invoices', $this->invoicingStatusOptionValues); + return $this->getValueFromOptionValues(parent::INVOICING_STATUS_NO_INVOICES, $this->invoicingStatusOptionValues); } $quotationTotalAmount = $this->salesOrder['total_after_tax']; if ($this->totalInvoicedAmount < $quotationTotalAmount) { - return $this->getStatus('partially_invoiced', $this->invoicingStatusOptionValues); + return $this->getValueFromOptionValues(parent::INVOICING_STATUS_PARTIALLY_INVOICED, $this->invoicingStatusOptionValues); } - return $this->getStatus('fully_invoiced', $this->invoicingStatusOptionValues); + return $this->getValueFromOptionValues(parent::INVOICING_STATUS_FULLY_INVOICED, $this->invoicingStatusOptionValues); } /** @@ -117,14 +112,18 @@ public function calculateInvoicingStatus() { public function calculatePaymentStatus() { if (empty($this->salesOrder) || empty($this->contributions) || !($this->totalPaymentsAmount > 0)) { - return $this->getStatus('no_payments', $this->paymentStatusOptionValues); + return $this->getValueFromOptionValues(parent::PAYMENT_STATUS_NO_PAYMENTS, $this->paymentStatusOptionValues); } if ($this->totalPaymentsAmount < $this->totalInvoicedAmount) { - return $this->getStatus('partially_paid', $this->paymentStatusOptionValues); + return $this->getValueFromOptionValues(parent::PAYMENT_STATUS_PARTIALLY_PAID, $this->paymentStatusOptionValues); } - return $this->getStatus('fully_paid', $this->paymentStatusOptionValues); + if ($this->totalPaymentsAmount > $this->totalInvoicedAmount) { + return $this->getValueFromOptionValues(parent::PAYMENT_STATUS_OVERPAID, $this->paymentStatusOptionValues); + } + + return $this->getValueFromOptionValues(parent::PAYMENT_STATUS_FULLY_PAID, $this->paymentStatusOptionValues); } /** @@ -166,26 +165,6 @@ private function getSalesOrder($saleOrderId) { ->first(); } - /** - * Gets option values by option group name. - * - * @param string $name - * Option group name. - * - * @return array - * Option values. - * - * @throws API_Exception - * @throws \Civi\API\Exception\UnauthorizedException - */ - private function getOptionValues($name) { - return OptionValue::get() - ->addSelect('*') - ->addWhere('option_group_id:name', '=', $name) - ->execute() - ->getArrayCopy(); - } - /** * Gets total invoiced amount. * @@ -223,21 +202,4 @@ private function getTotalPaymentsAmount(): float { return $totalPaymentsAmount; } - /** - * Gets status (option values' value) from the given options. - * - * @param string $needle - * Search value. - * @param array $options - * Option value. - * - * @return string - * Option values' value. - */ - private function getStatus($needle, $options) { - $key = array_search($needle, array_column($options, 'name')); - - return $options[$key]['value']; - } - } diff --git a/CRM/Civicase/Service/CaseSalesOrderOpportunityCalculator.php b/CRM/Civicase/Service/CaseSalesOrderOpportunityCalculator.php new file mode 100644 index 000000000..b73923908 --- /dev/null +++ b/CRM/Civicase/Service/CaseSalesOrderOpportunityCalculator.php @@ -0,0 +1,171 @@ +caseId = $caseId; + $contributions = $this->getContributions($caseId); + $this->calculateOpportunityFinancialAmount($contributions); + } + + /** + * Calculates total invoiced amount. + * + * @return float + * Total invoiced amount. + */ + public function calculateTotalInvoicedAmount(): float { + return $this->totalInvoicedAmount; + } + + /** + * Calculates total paid amount. + * + * @return float + * Total paid amounts. + */ + public function calculateTotalPaidAmount(): float { + return $this->totalPaidAmount; + } + + /** + * Calculates total quoted amount. + * + * @return float + * Total paid amounts. + */ + public function calculateTotalQuotedAmount(): float { + return $this->totalQuotedAmount; + } + + /** + * Calculates opportunity invoicing status. + * + * @return string + * Invoicing status option value's value + */ + public function calculateInvoicingStatus() { + if (!($this->totalInvoicedAmount > 0)) { + return $this->getLabelFromOptionValues(parent::INVOICING_STATUS_NO_INVOICES, $this->invoicingStatusOptionValues); + } + + if ($this->totalInvoicedAmount < $this->totalQuotedAmount) { + return $this->getLabelFromOptionValues(parent::INVOICING_STATUS_PARTIALLY_INVOICED, $this->invoicingStatusOptionValues); + } + + return $this->getLabelFromOptionValues(parent::INVOICING_STATUS_FULLY_INVOICED, $this->invoicingStatusOptionValues); + } + + /** + * Calculates opportunity payment status. + * + * @return string + * Payment status option value's value + */ + public function calculatePaymentStatus() { + if (!($this->totalPaidAmount > 0)) { + return $this->getLabelFromOptionValues(parent::PAYMENT_STATUS_NO_PAYMENTS, $this->paymentStatusOptionValues); + } + + if ($this->totalPaidAmount < $this->totalInvoicedAmount) { + return $this->getLabelFromOptionValues(parent::PAYMENT_STATUS_PARTIALLY_PAID, $this->paymentStatusOptionValues); + } + + if ($this->totalPaidAmount > $this->totalInvoicedAmount) { + return $this->getLabelFromOptionValues(parent::PAYMENT_STATUS_OVERPAID, $this->paymentStatusOptionValues); + + } + + return $this->getLabelFromOptionValues(parent::PAYMENT_STATUS_FULLY_PAID, $this->paymentStatusOptionValues); + } + + /** + * Updates opportunity financial details. + */ + public function updateOpportunityFinancialDetails(): void { + CiviCase::update() + ->addValue('Case_Opportunity_Details.Total_Amount_Quoted', $this->calculateTotalQuotedAmount()) + ->addValue('Case_Opportunity_Details.Total_Amount_Invoiced', $this->calculateTotalQuotedAmount()) + ->addValue('Case_Opportunity_Details.Invoicing_Status', $this->calculateInvoicingStatus()) + ->addValue('Case_Opportunity_Details.Total_Amounts_Paid', $this->calculateTotalPaidAmount()) + ->addValue('Case_Opportunity_Details.Payments_Status', $this->calculatePaymentStatus()) + ->addWhere('id', '=', $this->caseId) + ->execute(); + } + + /** + * Calculates opportunity financial amounts. + * + * @param array $contributions + * List of contributions that link to the opportunity. + */ + private function calculateOpportunityFinancialAmount($contributions) { + $totalQuotedAmount = 0; + $totalInvoicedAmount = 0; + $totalPaidAmount = 0; + foreach ($contributions as $contribution) { + $salesOrderId = $contribution['Opportunity_Details.Quotation']; + $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSalesOrderContributionCalculator($salesOrderId); + + $totalQuotedAmount += $caseSaleOrderContributionService->getQuotedAmount(); + $totalPaidAmount += $caseSaleOrderContributionService->calculateTotalPaidAmount(); + $totalInvoicedAmount += $caseSaleOrderContributionService->calculateTotalInvoicedAmount(); + } + + $this->totalQuotedAmount = $totalQuotedAmount; + $this->totalPaidAmount = $totalPaidAmount; + $this->totalInvoicedAmount = $totalInvoicedAmount; + } + + /** + * Gets Contributions by case Id. + * + * @param int $caseId + * List of contributions that link to the opportunity. + */ + private function getContributions($caseId) { + return Contribution::get(FALSE) + ->addSelect('*', 'Opportunity_Details.Quotation') + ->addWhere('Opportunity_Details.Case_Opportunity', '=', $caseId) + ->execute() + ->getArrayCopy(); + } + +} diff --git a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php index 1f766af03..c5918c126 100644 --- a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php @@ -2,6 +2,7 @@ namespace Civi\Api4\Action\CaseSalesOrder; +use Civi\Api4\CaseSalesOrder; use Civi\Api4\CaseSalesOrderLine; use Civi\Api4\Generic\AbstractSaveAction; use Civi\Api4\Generic\Result; @@ -53,6 +54,10 @@ protected function writeRecord($items) { $salesOrder['payment_status_id'] = $caseSaleOrderContributionService->calculateInvoicingStatus(); $salesOrder['invoicing_status_id'] = $caseSaleOrderContributionService->calculatePaymentStatus(); + if (!is_null($saleOrderId)) { + $this->updateOpportunityDetails($saleOrderId); + } + $salesOrders = $this->writeObjects([$salesOrder]); $result = array_pop($salesOrders); @@ -103,4 +108,25 @@ public function removeStaleLineItems(array $salesOrder) { ->execute(); } + /** + * Updates sales order's case opportunity details. + * + * @param int $salesOrderId + * Sales Order Id. + */ + private function updateOpportunityDetails($salesOrderId): void { + $caseSalesOrder = CaseSalesOrder::get() + ->addSelect('case_id') + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->first(); + + if (empty($caseSalesOrder)) { + return; + } + + $caseSaleOrderContributionService = new \CRM_Civicase_Service_CaseSalesOrderOpportunityCalculator($caseSalesOrder['case_id']); + $caseSaleOrderContributionService->updateOpportunityFinancialDetails(); + } + } From b454abfb1414dac900fc87c9ec3c81a116e4f06f Mon Sep 17 00:00:00 2001 From: Erawat Chamanont Date: Fri, 22 Sep 2023 13:09:21 +0100 Subject: [PATCH 177/199] BTHAB-185: Update statuses on contribution deletion --- .../Hook/Pre/DeleteSalesOrderContribution.php | 79 +++++++++++++++++++ civicase.php | 13 +++ 2 files changed, 92 insertions(+) create mode 100644 CRM/Civicase/Hook/Pre/DeleteSalesOrderContribution.php diff --git a/CRM/Civicase/Hook/Pre/DeleteSalesOrderContribution.php b/CRM/Civicase/Hook/Pre/DeleteSalesOrderContribution.php new file mode 100644 index 000000000..e8d5c9f98 --- /dev/null +++ b/CRM/Civicase/Hook/Pre/DeleteSalesOrderContribution.php @@ -0,0 +1,79 @@ +shouldRun($op, $objectName)) { + return; + } + + $salesOrderId = $this->getQuotationId($objectId); + if (empty($salesOrderId)) { + return; + } + + $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSalesOrderContributionCalculator($salesOrderId); + $invoicingStatusId = $caseSaleOrderContributionService->calculateInvoicingStatus(); + $paymentStatusId = $caseSaleOrderContributionService->calculatePaymentStatus(); + + CaseSalesOrder::update() + ->addWhere('id', '=', $salesOrderId) + ->addValue('invoicing_status_id', $invoicingStatusId) + ->addValue('payment_status_id', $paymentStatusId) + ->execute(); + + $caseSalesOrder = CaseSalesOrder::get() + ->addSelect('case_id') + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->first(); + + $caseSaleOrderContributionService = new \CRM_Civicase_Service_CaseSalesOrderOpportunityCalculator($caseSalesOrder['case_id']); + $caseSaleOrderContributionService->updateOpportunityFinancialDetails(); + } + + /** + * Determines if the hook should run or not. + * + * @param string $op + * The operation being performed. + * @param string $objectName + * Object name. + * + * @return bool + * returns a boolean to determine if hook will run or not. + */ + private function shouldRun($op, $objectName) { + return strtolower($objectName) == 'contribution' && $op == 'delete'; + } + + /** + * Gets quotation ID by contribution ID. + */ + private function getQuotationId($id) { + return Contribution::get() + ->addSelect('Opportunity_Details.Quotation') + ->addWhere('id', '=', $id) + ->execute() + ->first()['Opportunity_Details.Quotation']; + } + +} diff --git a/civicase.php b/civicase.php index c0d185230..cd497f01c 100644 --- a/civicase.php +++ b/civicase.php @@ -259,6 +259,19 @@ function civicase_civicrm_post($op, $objectName, $objectId, &$objectRef) { } } +/** + * Implements hook_civicrm_pre(). + */ +function civicase_civicrm_pre($op, $objectName, $id, &$params) { + $hooks = [ + new CRM_Civicase_Hook_Pre_DeleteSalesOrderContribution(), + ]; + + foreach ($hooks as $hook) { + $hook->run($op, $objectName, $id, $params); + } +} + /** * Implements hook_civicrm_postProcess(). */ From e4f2ba5a5fa2749c362cc1b99bc9c8f429009827 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 25 Sep 2023 15:11:58 +0100 Subject: [PATCH 178/199] BTHAB-206: Fix failing upgrader --- CRM/Civicase/Upgrader/Steps/Step0019.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CRM/Civicase/Upgrader/Steps/Step0019.php b/CRM/Civicase/Upgrader/Steps/Step0019.php index c89186996..14cfd30cf 100644 --- a/CRM/Civicase/Upgrader/Steps/Step0019.php +++ b/CRM/Civicase/Upgrader/Steps/Step0019.php @@ -18,8 +18,7 @@ class CRM_Civicase_Upgrader_Steps_Step0019 { */ public function apply() { try { - $upgrader = CRM_Civicase_Upgrader_Base::instance(); - $upgrader->executeSqlFile('sql/auto_install.sql'); + CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, CRM_Civicase_ExtensionUtil::path('sql/auto_install.sql')); (new QuotationTemplateManager())->create(); (new CaseTypeCategoryManager())->create(); From b962ce8e6275aca61ce6f2fc0bab30cfbfb225be Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 25 Sep 2023 15:13:16 +0100 Subject: [PATCH 179/199] BTHAB-206: Set payment and invoice status for new sales order --- CRM/Civicase/Service/CaseSalesOrderInvoice.php | 2 +- .../Action/CaseSalesOrder/SalesOrderSaveAction.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CRM/Civicase/Service/CaseSalesOrderInvoice.php b/CRM/Civicase/Service/CaseSalesOrderInvoice.php index 57dfa46a2..426b2352a 100644 --- a/CRM/Civicase/Service/CaseSalesOrderInvoice.php +++ b/CRM/Civicase/Service/CaseSalesOrderInvoice.php @@ -13,7 +13,7 @@ class CRM_Civicase_Service_CaseSalesOrderInvoice { /** - * CreditNoteInvoiceService constructor. + * CaseSalesOrderInvoice constructor. * * @param \CRM_Civicase_WorkflowMessage_SalesOrderInvoice $template * Workflow template. diff --git a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php index c5918c126..9529b80e8 100644 --- a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php @@ -26,6 +26,7 @@ public function _run(Result $result) { // phpcs:ignore $this->matchExisting($record); if (empty($record['id'])) { $this->fillDefaults($record); + $this->fillMandatoryFields($record); } } $this->validateValues(); @@ -129,4 +130,17 @@ private function updateOpportunityDetails($salesOrderId): void { $caseSaleOrderContributionService->updateOpportunityFinancialDetails(); } + /** + * Fill mandatory fields. + * + * @param array $params + * Single Sales Order Record. + */ + protected function fillMandatoryFields(&$params) { + $saleOrderId = $params['id'] ?? NULL; + $caseSaleOrderContributionService = new \CRM_Civicase_Service_CaseSalesOrderContributionCalculator($saleOrderId); + $params['payment_status_id'] = $caseSaleOrderContributionService->calculateInvoicingStatus(); + $params['invoicing_status_id'] = $caseSaleOrderContributionService->calculatePaymentStatus(); + } + } From 3138d5a0e751b59013660e5957fa1268469a6d92 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 25 Sep 2023 15:22:35 +0100 Subject: [PATCH 180/199] BTHAB-206: Add searchtask to send bulk invoice with quotation --- .../AttachQuotationToInvoiceMail.php | 12 +++++---- .../Hook/alterMailParams/AttachQuotation.php | 6 ++++- civicase.php | 25 +++++++++++++++++++ .../Form/Contribute/AttachQuotation.tpl | 2 +- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php b/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php index 9c36c431b..ab64014c2 100644 --- a/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php +++ b/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php @@ -17,7 +17,7 @@ class CRM_Civicase_Hook_BuildForm_AttachQuotationToInvoiceMail { * Form Name. */ public function run(CRM_Core_Form &$form, $formName) { - if (!$this->shouldRun($formName)) { + if (!$this->shouldRun($form, $formName)) { return; } @@ -31,18 +31,20 @@ public function run(CRM_Core_Form &$form, $formName) { /** * Determines if the hook will run. * + * @param CRM_Core_Form $form + * Form Class object. * @param string $formName * Form Name. * * @return bool * TRUE if the hook should run, FALSE otherwise. */ - private function shouldRun($formName) { + private function shouldRun($form, $formName) { if ($formName != 'CRM_Contribute_Form_Task_Invoice') { return FALSE; } - $contributionId = CRM_Utils_Request::retrieve('id', 'Positive'); + $contributionId = CRM_Utils_Request::retrieve('id', 'Positive', $form, FALSE); if (!$contributionId) { return FALSE; } @@ -50,12 +52,12 @@ private function shouldRun($formName) { $salesOrder = Contribution::get() ->addSelect('Opportunity_Details.Quotation') ->addWhere('Opportunity_Details.Quotation', 'IS NOT EMPTY') - ->addWhere('id', '=', $contributionId) + ->addWhere('id', 'IN', explode(',', $contributionId)) ->addChain('salesOrder', CaseSalesOrder::get() ->addWhere('id', '=', '$Opportunity_Details.Quotation') ) ->execute() - ->first()['salesOrder']; + ->getArrayCopy(); return !empty($salesOrder); } diff --git a/CRM/Civicase/Hook/alterMailParams/AttachQuotation.php b/CRM/Civicase/Hook/alterMailParams/AttachQuotation.php index dcf2d8ecf..e368a33ef 100644 --- a/CRM/Civicase/Hook/alterMailParams/AttachQuotation.php +++ b/CRM/Civicase/Hook/alterMailParams/AttachQuotation.php @@ -25,7 +25,11 @@ public function run(array &$params, $context) { $rendered = $this->getContributionQuotationInvoice($params['tokenContext']['contributionId']); - $params['attachments'][] = CRM_Utils_Mail::appendPDF('quotation_invoice.pdf', $rendered['html'], $rendered['format']); + $attachment = CRM_Utils_Mail::appendPDF('quotation_invoice.pdf', $rendered['html'], $rendered['format']); + + if ($attachment) { + $params['attachments'][] = $attachment; + } } /** diff --git a/civicase.php b/civicase.php index cd497f01c..ec2a095ed 100644 --- a/civicase.php +++ b/civicase.php @@ -224,6 +224,11 @@ function civicase_civicrm_buildForm($formName, &$form) { if (!empty($_REQUEST['civicase_reload'])) { $form->civicase_reload = json_decode($_REQUEST['civicase_reload'], TRUE); } + + $isSearchKit = CRM_Utils_Request::retrieve('sk', 'Positive'); + if ($formName == 'CRM_Contribute_Form_Task_Invoice' && $isSearchKit) { + $form->add('hidden', 'mail_task_from_sk', $isSearchKit); + } } /** @@ -297,6 +302,10 @@ function civicase_civicrm_postProcess($formName, &$form) { $api = civicrm_api3('Case', 'getdetails', ['check_permissions' => 1] + $form->civicase_reload); $form->ajaxResponse['civicase_reload'] = $api['values']; } + + if ($formName == 'CRM_Contribute_Form_Task_Invoice' && !empty($form->getVar('_submitValues')['mail_task_from_sk'])) { + CRM_Utils_System::redirect($_SERVER['HTTP_REFERER']); + } } /** @@ -573,4 +582,20 @@ function civicase_civicrm_searchKitTasks(array &$tasks, bool $checkPermissions, ]; $tasks['CaseSalesOrder'] = $actions; + +} + +/** + * Implements hook_civicrm_searchTasks(). + */ +function civicase_civicrm_searchTasks(string $objectName, array &$tasks) { + if ($objectName === 'contribution') { + $tasks['bulk_invoice'] = [ + 'title' => ts('Send Invoice by email'), + 'class' => 'CRM_Contribute_Form_Task_Invoice', + 'icon' => 'fa-paper-plane-o', + 'url' => 'civicrm/contribute/task?reset=1&task_item=invoice&sk=1', + 'key' => 'invoice', + ]; + } } diff --git a/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl b/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl index 82a949091..76c82d7b8 100644 --- a/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl +++ b/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl @@ -8,7 +8,7 @@ {literal} {/literal} From daf29c6e3622857e424c330df48cf30cee3ddf0d Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 26 Sep 2023 07:30:14 +0100 Subject: [PATCH 181/199] BTHAB-206: Hide PDF selector and suport invoicehelper customization --- .../Hook/BuildForm/AttachQuotationToInvoiceMail.php | 5 ++++- civicase.php | 7 +++++++ js/invoice-bulk-mail.js | 5 +++++ managed/SavedSearch_Civicase_Quotations.mgd.php | 2 +- templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl | 6 ++++++ 5 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 js/invoice-bulk-mail.js diff --git a/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php b/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php index ab64014c2..ea9dd080a 100644 --- a/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php +++ b/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php @@ -40,7 +40,10 @@ public function run(CRM_Core_Form &$form, $formName) { * TRUE if the hook should run, FALSE otherwise. */ private function shouldRun($form, $formName) { - if ($formName != 'CRM_Contribute_Form_Task_Invoice') { + if (!in_array($formName, [ + 'CRM_Contribute_Form_Task_Invoice', + 'CRM_Invoicehelper_Contribute_Form_Task_Invoice', + ])) { return FALSE; } diff --git a/civicase.php b/civicase.php index ec2a095ed..e226931d5 100644 --- a/civicase.php +++ b/civicase.php @@ -228,6 +228,13 @@ function civicase_civicrm_buildForm($formName, &$form) { $isSearchKit = CRM_Utils_Request::retrieve('sk', 'Positive'); if ($formName == 'CRM_Contribute_Form_Task_Invoice' && $isSearchKit) { $form->add('hidden', 'mail_task_from_sk', $isSearchKit); + CRM_Core_Resources::singleton()->addScriptFile( + CRM_Civicase_ExtensionUtil::LONG_NAME, + 'js/invoice-bulk-mail.js', + ); + $form->setTitle(ts('Email Contribution Invoice')); + $ids = CRM_Utils_Request::retrieve('id', 'Positive', $form, FALSE); + $form->assign('totalSelectedContributions', count(explode(',', $ids))); } } diff --git a/js/invoice-bulk-mail.js b/js/invoice-bulk-mail.js new file mode 100644 index 000000000..d63bb1bac --- /dev/null +++ b/js/invoice-bulk-mail.js @@ -0,0 +1,5 @@ +(function ($, _) { + $('input[value=email_invoice]').prop('checked', true).click(); + $('div.help').hide(); + $('input[name=output]').parent().parent().hide(); +})(CRM.$, CRM._); diff --git a/managed/SavedSearch_Civicase_Quotations.mgd.php b/managed/SavedSearch_Civicase_Quotations.mgd.php index df80a6b6c..1000f31fc 100644 --- a/managed/SavedSearch_Civicase_Quotations.mgd.php +++ b/managed/SavedSearch_Civicase_Quotations.mgd.php @@ -584,7 +584,7 @@ 'icon' => 'fa-paper-plane-o', 'text' => 'Send By Email', 'style' => 'default', - 'path' => 'civicrm/contribute/invoice/email/?reset=1&id=[id]&select=email', + 'path' => 'civicrm/contribute/invoice/email/?reset=1&id=[id]&select=email&cid=[contact_id]', 'condition' => [], ], ], diff --git a/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl b/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl index 76c82d7b8..b84493606 100644 --- a/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl +++ b/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl @@ -4,10 +4,16 @@ {$form.attach_quote.html} Yes +
{literal} From 4ac5d724421d7c4ebc898288c2b6b3a9cc8cf12b Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 26 Sep 2023 22:56:22 +0100 Subject: [PATCH 182/199] BTHAB-145: Add button to create invoice --- .../AddEntityReferenceToCustomField.php | 31 +++++++++++++++++-- .../directives/invoices-list.directive.html | 8 +++++ .../directives/invoices-list.directive.js | 8 +++++ .../directives/quotations-list.directive.js | 2 +- js/contribution-entityref-field.js | 6 +++- 5 files changed, 51 insertions(+), 4 deletions(-) diff --git a/CRM/Civicase/Hook/BuildForm/AddEntityReferenceToCustomField.php b/CRM/Civicase/Hook/BuildForm/AddEntityReferenceToCustomField.php index 0e2a9d1b5..dac5e0698 100644 --- a/CRM/Civicase/Hook/BuildForm/AddEntityReferenceToCustomField.php +++ b/CRM/Civicase/Hook/BuildForm/AddEntityReferenceToCustomField.php @@ -1,5 +1,6 @@ CRM_Core_BAO_CustomField::getCustomFieldID('Case_Opportunity', 'Opportunity_Details', TRUE), 'entity' => 'Case', 'placeholder' => '- Select Case/Opportunity -', ]; - $customFields[] = [ + $customFields['salesOrder'] = [ 'name' => CRM_Core_BAO_CustomField::getCustomFieldID('Quotation', 'Opportunity_Details', TRUE), 'entity' => 'CaseSalesOrder', 'placeholder' => '- Select Quotation -', @@ -36,9 +37,35 @@ public function run(CRM_Core_Form &$form, $formName) { 'scriptFile' => [E::LONG_NAME, 'js/contribution-entityref-field.js'], 'region' => 'page-header', ]); + + $this->populateDefaultFields($form, $customFields); \Civi::resources()->addVars('civicase', ['entityRefCustomFields' => $customFields]); } + /** + * Populates default fields. + * + * @param \CRM_Core_Form &$form + * Form Class object. + * @param array &$customFields + * Custom fields to set default value. + */ + private function populateDefaultFields(CRM_Core_Form &$form, array &$customFields) { + $caseId = CRM_Utils_Request::retrieve('caseId', 'Positive', $form); + if (!$caseId) { + return; + } + + $customFields['case']['value'] = $caseId; + $caseClient = CaseContact::get() + ->addSelect('contact_id') + ->addWhere('case_id', '=', $caseId) + ->execute() + ->first()['contact_id'] ?? NULL; + + $form->setDefaults(array_merge($form->_defaultValues, ['contact_id' => $caseClient])); + } + /** * Checks if the hook should run. * diff --git a/ang/civicase-features/invoices/directives/invoices-list.directive.html b/ang/civicase-features/invoices/directives/invoices-list.directive.html index c34641c6d..233d52c1c 100644 --- a/ang/civicase-features/invoices/directives/invoices-list.directive.html +++ b/ang/civicase-features/invoices/directives/invoices-list.directive.html @@ -1,5 +1,13 @@

{{ ts('Quotation Invoices') }}

+ +
+ add_circle + {{:: ts('Create Invoice') }} + +
diff --git a/ang/civicase-features/invoices/directives/invoices-list.directive.js b/ang/civicase-features/invoices/directives/invoices-list.directive.js index 41751d97a..4ce2265de 100644 --- a/ang/civicase-features/invoices/directives/invoices-list.directive.js +++ b/ang/civicase-features/invoices/directives/invoices-list.directive.js @@ -18,6 +18,14 @@ * @param {object} $window window object of the browser */ function invoicesListController ($scope, $location, $window) { + $scope.contributionURL = async () => { + let url = CRM.url('/contribute/add?reset=1&action=add&context=standalone'); + const caseId = $location.search().caseId; + if (caseId) { + url += `&caseId=${caseId}`; + } + $window.location.href = url; + }; } })(angular, CRM._); diff --git a/ang/civicase-features/quotations/directives/quotations-list.directive.js b/ang/civicase-features/quotations/directives/quotations-list.directive.js index d527176bc..1d414ff7a 100644 --- a/ang/civicase-features/quotations/directives/quotations-list.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-list.directive.js @@ -31,7 +31,7 @@ * Redirects user to new quotation screen */ function redirectToQuotationCreationScreen () { - let url = '/civicrm/case-features/a#/quotations/new'; + let url = CRM.url('/case-features/a#/quotations/new'); const caseId = $location.search().caseId; if (caseId) { url += `?caseId=${caseId}`; diff --git a/js/contribution-entityref-field.js b/js/contribution-entityref-field.js index cd424a04a..8d0922353 100644 --- a/js/contribution-entityref-field.js +++ b/js/contribution-entityref-field.js @@ -8,7 +8,7 @@ }; $(document).one('crmLoad', function () { - const entityRefCustomFields = CRM.vars.civicase.entityRefCustomFields ?? []; + const entityRefCustomFields = Object.values(CRM.vars.civicase.entityRefCustomFields ?? {}); /* eslint-disable no-undef */ waitForElement($, '#customData', function ($, elem) { @@ -20,6 +20,10 @@ entity: field.entity, create: false }); + + if (field.value) { + $(`[name^=${field.name}_]`).val(field.value).trigger('change'); + } }); }); }); From adc92438e2f0eb1dfdbd7fef3236e61488e5046d Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Wed, 27 Sep 2023 07:18:20 +0100 Subject: [PATCH 183/199] BTHAB-145: Refactor how client_id is populated from case --- .../directives/quotations-create.directive.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index 5f4d16e02..10008049a 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -117,13 +117,12 @@ return; } - crmApi4('Relationship', 'get', { - select: ['contact_id_a'], - where: [['case_id', '=', $scope.defaultCaseId], ['relationship_type_id:name', '=', 'Case Coordinator is'], ['is_current', '=', true]], - limit: 1 - }).then(function (relationships) { - if (Array.isArray(relationships) && relationships.length > 0) { - $scope.salesOrder.client_id = relationships[0].contact_id_a ?? null; + crmApi4('CaseContact', 'get', { + select: ['contact_id'], + where: [['case_id', '=', $scope.defaultCaseId]] + }).then(function (caseContacts) { + if (Array.isArray(caseContacts) && caseContacts.length > 0) { + $scope.salesOrder.client_id = caseContacts[0].contact_id ?? null; } }); } From 48d22e494646e6c0cefd411806a6f0261a41aba3 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 29 Sep 2023 06:58:42 +0100 Subject: [PATCH 184/199] CIWEMB-514: Fix not able to create non-salesorder contribution --- CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php b/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php index 894a0d81a..a729856e9 100644 --- a/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php +++ b/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php @@ -41,8 +41,8 @@ public function run($op, $objectName, $objectId, &$objectRef) { ->execute() ->first(); - $this->updateQuotationFinancialStatuses($contribution['Opportunity_Details.Quotation']); - $this->updateCaseOpportunityFinancialDetails($contribution['Opportunity_Details.Case_Opportunity']); + $this->updateQuotationFinancialStatuses($contribution['Opportunity_Details.Quotation'] ?: NULL); + $this->updateCaseOpportunityFinancialDetails($contribution['Opportunity_Details.Case_Opportunity'] ?: NULL); } /** @@ -51,7 +51,7 @@ public function run($op, $objectName, $objectId, &$objectRef) { * @param int $salesOrderID * CaseSalesOrder ID. */ - private function updateQuotationFinancialStatuses(int $salesOrderID): void { + private function updateQuotationFinancialStatuses(?int $salesOrderID): void { if (empty($salesOrderID)) { return; } From e8dd0b0467724b4b2e742077a330197affc913eb Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Fri, 6 Oct 2023 08:51:09 +0100 Subject: [PATCH 185/199] CIWEMB-525: Ensure hook will only run where expected --- .../Hook/BuildForm/AddEntityReferenceToCustomField.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CRM/Civicase/Hook/BuildForm/AddEntityReferenceToCustomField.php b/CRM/Civicase/Hook/BuildForm/AddEntityReferenceToCustomField.php index dac5e0698..9e6b6c53d 100644 --- a/CRM/Civicase/Hook/BuildForm/AddEntityReferenceToCustomField.php +++ b/CRM/Civicase/Hook/BuildForm/AddEntityReferenceToCustomField.php @@ -17,7 +17,7 @@ class CRM_Civicase_Hook_BuildForm_AddEntityReferenceToCustomField { * Form Name. */ public function run(CRM_Core_Form &$form, $formName) { - if (!$this->shouldRun($formName)) { + if (!$this->shouldRun($form, $formName)) { return; } @@ -69,14 +69,17 @@ private function populateDefaultFields(CRM_Core_Form &$form, array &$customField /** * Checks if the hook should run. * + * @param \CRM_Core_Form $form + * Form object. * @param string $formName * Form Name. * * @return bool * True if hook should run, otherwise false. */ - public function shouldRun($formName) { - return $formName === "CRM_Contribute_Form_Contribution"; + public function shouldRun($form, $formName) { + $addOrUpdate = ($form->getAction() & CRM_Core_Action::ADD) || ($form->getAction() & CRM_Core_Action::UPDATE); + return $formName === "CRM_Contribute_Form_Contribution" && $addOrUpdate; } } From ca4ee2ca06668e5ce9ef793ebb82fb63fb48bd29 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 10 Oct 2023 09:50:24 +0100 Subject: [PATCH 186/199] BTHAB-202: Ensure the product price doesnt exceed the max price --- .../CaseSalesOrder/SalesOrderSaveAction.php | 28 +++++++++++++++++- .../directives/quotations-create.directive.js | 24 ++++++++++++++- .../SalesOrderSaveActionTest.php | 29 ++++++++++++++++++- 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php index 9529b80e8..2af4afe87 100644 --- a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php @@ -7,6 +7,7 @@ use Civi\Api4\Generic\AbstractSaveAction; use Civi\Api4\Generic\Result; use Civi\Api4\Generic\Traits\DAOActionTrait; +use Civi\Api4\Product; use CRM_Civicase_BAO_CaseSalesOrder as CaseSalesOrderBAO; use CRM_Core_Transaction; @@ -46,6 +47,7 @@ protected function writeRecord($items) { $output = []; foreach ($items as $salesOrder) { $lineItems = $salesOrder['items']; + $this->validateLinItemProductPrice($lineItems); $total = CaseSalesOrderBAO::computeTotal($lineItems); $salesOrder['total_before_tax'] = $total['totalBeforeTax']; $salesOrder['total_after_tax'] = $total['totalAfterTax']; @@ -122,7 +124,7 @@ private function updateOpportunityDetails($salesOrderId): void { ->execute() ->first(); - if (empty($caseSalesOrder)) { + if (empty($caseSalesOrder) || empty($caseSalesOrder['case_id'])) { return; } @@ -143,4 +145,28 @@ protected function fillMandatoryFields(&$params) { $params['invoicing_status_id'] = $caseSaleOrderContributionService->calculatePaymentStatus(); } + /** + * Ensures the product price doesnt exceed the max price. + * + * @param array $lineItems + * Sales Order line items. + */ + protected function validateLinItemProductPrice(array &$lineItems) { + array_walk($lineItems, function (&$lineItem) { + if (!empty($lineItem['product_id'])) { + $product = Product::get() + ->addSelect('cost') + ->addWhere('id', '=', $lineItem['product_id']) + ->execute() + ->first(); + + if ($product && !empty($product['cost']) && $product['cost'] < $lineItem['subtotal_amount']) { + $lineItem['unit_price'] = $product['cost']; + $lineItem['quantity'] = 1; + $lineItem['subtotal_amount'] = CaseSalesOrderBAO::getSubTotal($lineItem); + } + } + }); + } + } diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index 10008049a..83591af02 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -33,7 +33,6 @@ $scope.isUpdate = false; $scope.formValid = true; - $scope.membeshipTypesProductDiscountPercentage = 0; $scope.roundTo = roundTo; $scope.formatMoney = formatMoney; $scope.submitInProgress = false; @@ -43,6 +42,7 @@ $scope.currencyCodes = CurrencyCodes.getAll(); $scope.handleClientChange = handleClientChange; $scope.handleProductChange = handleProductChange; + $scope.membeshipTypesProductDiscountPercentage = 0; $scope.handleCurrencyChange = handleCurrencyChange; $scope.salesOrderStatus = SalesOrderStatus.getAll(); $scope.defaultCaseId = $location.search().caseId || null; @@ -316,6 +316,28 @@ item.subtotal_amount = item.unit_price * item.quantity * ((100 - item.discounted_percentage) / 100) || 0; $scope.$emit('totalChange'); + validateProductPrice(index); + } + + /** + * Ensures the product price doesnt exceed the max price + * + * @param {number} index index of the sales order line item + */ + function validateProductPrice (index) { + const productId = $scope.salesOrder.items[index].product_id; + const shouldCompareCost = productId && productsCache.has(productId) && parseFloat(productsCache.get(productId).cost) > 0; + if (!shouldCompareCost) { + return; + } + + const cost = productsCache.get(productId).cost; + if ($scope.salesOrder.items[index].subtotal_amount > cost) { + $scope.salesOrder.items[index].quantity = 1; + $scope.salesOrder.items[index].unit_price = parseFloat(cost); + CRM.alert('The quotation line item(s) have been set to the maximum premium price', ts('info'), 'info'); + calculateSubtotal(index); + } } /** diff --git a/tests/phpunit/Civi/Api4/CaseSalesOrder/SalesOrderSaveActionTest.php b/tests/phpunit/Civi/Api4/CaseSalesOrder/SalesOrderSaveActionTest.php index 787c34c50..6244d58ed 100644 --- a/tests/phpunit/Civi/Api4/CaseSalesOrder/SalesOrderSaveActionTest.php +++ b/tests/phpunit/Civi/Api4/CaseSalesOrder/SalesOrderSaveActionTest.php @@ -3,6 +3,7 @@ use Civi\Api4\CaseSalesOrder; use Civi\Api4\CaseSalesOrderLine; use CRM_Civicase_Test_Fabricator_Contact as ContactFabricator; +use CRM_Civicase_Test_Fabricator_Product as ProductFabricator; /** * CaseSalesOrder.SalesOrderSaveAction API Test Case. @@ -17,7 +18,7 @@ class Civi_Api4_CaseSalesOrder_SalesOrderSaveActionTest extends BaseHeadlessTest /** * Setup data before tests run. */ - public function setUp() { + public function setUp(): void { $contact = ContactFabricator::fabricate(); $this->registerCurrentLoggedInContactInSession($contact['id']); } @@ -162,4 +163,30 @@ public function testDetachedLineItemsAreRemovedFromSalesOrderOnUpdate() { $this->assertEquals($salesOrder['items'][0]['id'], $salesOrderLine[0]['id']); } + /** + * Test line item subtotal doesnt exceed product maximum cost. + */ + public function testSubTotalDoesntExceedProductCost() { + $productPrice = 200; + $salesOrder = $this->getCaseSalesOrderData(); + $salesOrder['items'][] = $this->getCaseSalesOrderLineData( + ['unit_price' => 200, 'quantity' => 2] + ); + + $salesOrder['items'][0]['product_id'] = ProductFabricator::fabricate(['cost' => 200])['id']; + + $salesOrderId = CaseSalesOrder::save() + ->addRecord($salesOrder) + ->execute() + ->jsonSerialize()[0]['id']; + + $salesOrderLine = CaseSalesOrderLine::get() + ->addWhere('sales_order_id', '=', $salesOrderId) + ->execute() + ->jsonSerialize(); + + $this->assertNotEmpty($salesOrderLine); + $this->assertEquals($productPrice, $salesOrderLine[0]['subtotal_amount']); + } + } From 4d43982d77a478b3f0982b99ef3d8374065d846e Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 16 Oct 2023 05:40:48 +0100 Subject: [PATCH 187/199] BTHAB-257: Fetch discount information for case client --- .../quotations/directives/quotations-create.directive.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ang/civicase-features/quotations/directives/quotations-create.directive.js b/ang/civicase-features/quotations/directives/quotations-create.directive.js index 83591af02..5efdac61b 100644 --- a/ang/civicase-features/quotations/directives/quotations-create.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js @@ -123,6 +123,9 @@ }).then(function (caseContacts) { if (Array.isArray(caseContacts) && caseContacts.length > 0) { $scope.salesOrder.client_id = caseContacts[0].contact_id ?? null; + if ($scope.salesOrder.client_id) { + handleClientChange(); + } } }); } @@ -230,7 +233,7 @@ const clientID = $scope.salesOrder.client_id; crmApi4('Membership', 'get', { select: ['membership_type_id.Product_Discounts.Product_Discount_Amount'], - where: [['contact_id', '=', clientID]] + where: [['contact_id', '=', clientID], ['status_id.is_current_member', '=', true]] }).then(function (results) { let discountPercentage = 0; results.forEach((membership) => { From 7e62cab264e96039d38cccbd2f1db4fc07bb2b31 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 17 Oct 2023 05:02:09 +0100 Subject: [PATCH 188/199] BTHAB-257: Ensure quotation is attached to mail by default --- .../Hook/BuildForm/AttachQuotationToInvoiceMail.php | 1 + CRM/Civicase/Hook/alterMailParams/AttachQuotation.php | 7 ++++--- templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php b/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php index ea9dd080a..74a660cb0 100644 --- a/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php +++ b/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php @@ -22,6 +22,7 @@ public function run(CRM_Core_Form &$form, $formName) { } $form->add('checkbox', 'attach_quote', ts('Attach Quotation')); + $form->setDefaults(array_merge($form->_defaultValues, ['attach_quote' => TRUE])); CRM_Core_Region::instance('page-body')->add([ 'template' => "CRM/Civicase/Form/Contribute/AttachQuotation.tpl", diff --git a/CRM/Civicase/Hook/alterMailParams/AttachQuotation.php b/CRM/Civicase/Hook/alterMailParams/AttachQuotation.php index e368a33ef..d6a99919c 100644 --- a/CRM/Civicase/Hook/alterMailParams/AttachQuotation.php +++ b/CRM/Civicase/Hook/alterMailParams/AttachQuotation.php @@ -23,12 +23,13 @@ public function run(array &$params, $context) { return; } - $rendered = $this->getContributionQuotationInvoice($params['tokenContext']['contributionId']); + $contributionId = $params['tokenContext']['contributionId'] ?? $params['tplParams']['id']; + $rendered = $this->getContributionQuotationInvoice($contributionId); $attachment = CRM_Utils_Mail::appendPDF('quotation_invoice.pdf', $rendered['html'], $rendered['format']); if ($attachment) { - $params['attachments'][] = $attachment; + $params['attachments']['quotaition_invoice'] = $attachment; } } @@ -73,7 +74,7 @@ private function getContributionQuotationInvoice($contributionId) { */ private function shouldRun(array $params, $context, $shouldAttachQuote) { $component = $params['tplParams']['component'] ?? ''; - if ($component !== 'contribute' || $context !== 'messageTemplate' || empty($shouldAttachQuote)) { + if ($component !== 'contribute' || empty($shouldAttachQuote)) { return FALSE; } diff --git a/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl b/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl index b84493606..68e374fef 100644 --- a/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl +++ b/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl @@ -9,6 +9,7 @@ {literal}
-

{{ ts('Quotations') }}

From 960f5884e7313ebd53cb37a07fc38b1044331b35 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 13 Mar 2023 12:48:21 +0100 Subject: [PATCH 045/199] BTHAB-47: Resolve prospect contact tab not loading --- CRM/Civicase/Page/CaseAngular.php | 2 +- ang/civicase.ang.php | 1 - civicase.php | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CRM/Civicase/Page/CaseAngular.php b/CRM/Civicase/Page/CaseAngular.php index 0ea8ad317..a871565f1 100644 --- a/CRM/Civicase/Page/CaseAngular.php +++ b/CRM/Civicase/Page/CaseAngular.php @@ -20,7 +20,7 @@ class CRM_Civicase_Page_CaseAngular extends \CRM_Core_Page { public function run() { $loader = Civi::service('angularjs.loader'); $loader->setPageName('civicrm/case/a'); - $loader->addModules(['crmApp', 'civicase']); + $loader->addModules(['crmApp', 'civicase', 'civicase-features']); \Civi::resources()->addSetting([ 'crmApp' => [ 'defaultRoute' => '/case/list', diff --git a/ang/civicase.ang.php b/ang/civicase.ang.php index 0e387072a..2d3188100 100644 --- a/ang/civicase.ang.php +++ b/ang/civicase.ang.php @@ -53,7 +53,6 @@ 'uibTabsetClass', 'dialogService', 'civicase-base', - 'civicase-features', ]; $requires = CRM_Civicase_Hook_addDependentAngularModules::invoke($requires); diff --git a/civicase.php b/civicase.php index 86c24e379..ad655bbfa 100644 --- a/civicase.php +++ b/civicase.php @@ -28,7 +28,7 @@ function civicase_civicrm_tabset($tabsetName, &$tabs, $context) { if ($useAng) { $loader = Civi::service('angularjs.loader'); - $loader->addModules('civicase'); + $loader->addModules(['civicase', 'civicase-features']); } } From 3e3cfebe9da89e199a77683a1f3b762b1a51830a Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 13 Mar 2023 12:53:56 +0100 Subject: [PATCH 046/199] BTHAB-47: Create view to list contact quotations --- ang/afsearchContactQuotations.aff.html | 23 +++++++++++++++++++ ang/afsearchContactQuotations.aff.json | 16 +++++++++++++ ang/civicase-features.ang.php | 1 + .../directives/quotations-list.directive.html | 8 +++++-- .../directives/quotations-list.directive.js | 10 +++++++- 5 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 ang/afsearchContactQuotations.aff.html create mode 100644 ang/afsearchContactQuotations.aff.json diff --git a/ang/afsearchContactQuotations.aff.html b/ang/afsearchContactQuotations.aff.html new file mode 100644 index 000000000..0a5239b33 --- /dev/null +++ b/ang/afsearchContactQuotations.aff.html @@ -0,0 +1,23 @@ +
+
+ + +
+
+ +
+
+ + +
+ +
diff --git a/ang/afsearchContactQuotations.aff.json b/ang/afsearchContactQuotations.aff.json new file mode 100644 index 000000000..3236e4480 --- /dev/null +++ b/ang/afsearchContactQuotations.aff.json @@ -0,0 +1,16 @@ +{ + "type": "search", + "requires": [], + "title": "Contact Quotations", + "description": "", + "is_dashlet": false, + "is_public": false, + "is_token": false, + "server_route": "", + "permission": "access CiviCRM", + "entity_type": null, + "join_entity": null, + "contact_summary": null, + "redirect": null, + "create_submission": null +} diff --git a/ang/civicase-features.ang.php b/ang/civicase-features.ang.php index 7ff99777d..e638009d0 100644 --- a/ang/civicase-features.ang.php +++ b/ang/civicase-features.ang.php @@ -72,6 +72,7 @@ function set_case_sales_order_status(&$options) { 'civicase', 'civicase-base', 'afsearchQuotations', + 'afsearchContactQuotations', ]; return [ diff --git a/ang/civicase-features/quotations/directives/quotations-list.directive.html b/ang/civicase-features/quotations/directives/quotations-list.directive.html index 5eb5ff503..cb3e2e95c 100644 --- a/ang/civicase-features/quotations/directives/quotations-list.directive.html +++ b/ang/civicase-features/quotations/directives/quotations-list.directive.html @@ -1,5 +1,5 @@
diff --git a/ang/civicase-features/quotations/directives/quotations-list.directive.js b/ang/civicase-features/quotations/directives/quotations-list.directive.js index d6f55eb4c..65d9bac4f 100644 --- a/ang/civicase-features/quotations/directives/quotations-list.directive.js +++ b/ang/civicase-features/quotations/directives/quotations-list.directive.js @@ -6,7 +6,10 @@ restrict: 'E', controller: 'quotationsListController', templateUrl: '~/civicase-features/quotations/directives/quotations-list.directive.html', - scope: {} + scope: { + view: '@', + contactId: '@' + } }; }); @@ -20,6 +23,11 @@ function quotationsListController ($scope, $location, $window) { $scope.redirectToQuotationCreationScreen = redirectToQuotationCreationScreen; + (function init () { + if ($scope.contactId) { + $location.search().cid = $scope.contactId; + } + }()); /** * Redirect user to new quotation screen */ From ad42e0e4069012ba8906a584fce4f36959801052 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 13 Mar 2023 12:57:04 +0100 Subject: [PATCH 047/199] BTHAB-47: Display contact quotations on contact tab --- .../Hook/Tabset/CaseCategoryTabAdd.php | 15 ++++-- .../Hook/Tabset/CaseSalesOrderTabAdd.php | 47 +++++++++++++++++++ .../Page/ContactCaseSalesOrderTab.php | 28 +++++++++++ .../Page/ContactCaseSalesOrderTab.tpl | 16 +++++++ xml/Menu/civicase.xml | 5 ++ 5 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 CRM/Civicase/Hook/Tabset/CaseSalesOrderTabAdd.php create mode 100644 CRM/Civicase/Page/ContactCaseSalesOrderTab.php create mode 100644 templates/CRM/Civicase/Page/ContactCaseSalesOrderTab.tpl diff --git a/CRM/Civicase/Hook/Tabset/CaseCategoryTabAdd.php b/CRM/Civicase/Hook/Tabset/CaseCategoryTabAdd.php index 2f87fd3de..6e53bcfc6 100644 --- a/CRM/Civicase/Hook/Tabset/CaseCategoryTabAdd.php +++ b/CRM/Civicase/Hook/Tabset/CaseCategoryTabAdd.php @@ -8,6 +8,13 @@ */ class CRM_Civicase_Hook_Tabset_CaseCategoryTabAdd { + /** + * Case tab last weight. + * + * @var int + */ + public $caseTabWeight; + /** * Determines what happens if the hook is handled. * @@ -26,6 +33,8 @@ public function run($tabsetName, array &$tabs, array $context, &$useAng) { } $this->addCaseCategoryContactTabs($tabs, $context['contact_id'], $useAng); + $caseSalesOrdertab = new CRM_Civicase_Hook_Tabset_CaseSalesOrderTabAdd(); + $caseSalesOrdertab->addCaseSalesOrderTab($tabs, $context['contact_id'], $this->caseTabWeight++); } /** @@ -49,7 +58,7 @@ private function addCaseCategoryContactTabs(array &$tabs, $contactID, &$useAng) } $permissionService = new CaseCategoryPermission(); - $caseTabWeight = $this->getCaseTabWeight($tabs); + $this->caseTabWeight = $this->getCaseTabWeight($tabs); foreach ($result['values'] as $caseCategory) { $caseCategoryPermissions = $permissionService->get($caseCategory['name']); $permissionsToCheck = $this->getBasicCaseCategoryPermissions($caseCategoryPermissions); @@ -57,7 +66,7 @@ private function addCaseCategoryContactTabs(array &$tabs, $contactID, &$useAng) continue; } - $caseTabWeight++; + $this->caseTabWeight++; $useAng = TRUE; $icon = !empty($caseCategory['icon']) ? "crm-i {$caseCategory['icon']}" : ''; $tabs[] = [ @@ -67,7 +76,7 @@ private function addCaseCategoryContactTabs(array &$tabs, $contactID, &$useAng) 'case_type_category' => $caseCategory['value'], ]), 'title' => ucfirst(strtolower($caseCategory['label'])), - 'weight' => $caseTabWeight, + 'weight' => $this->caseTabWeight, 'count' => CaseCategoryHelper::getCaseCount($caseCategory['name'], $contactID), 'class' => 'livePage', 'icon' => $icon, diff --git a/CRM/Civicase/Hook/Tabset/CaseSalesOrderTabAdd.php b/CRM/Civicase/Hook/Tabset/CaseSalesOrderTabAdd.php new file mode 100644 index 000000000..b75de3319 --- /dev/null +++ b/CRM/Civicase/Hook/Tabset/CaseSalesOrderTabAdd.php @@ -0,0 +1,47 @@ + 'quotations', + 'url' => CRM_Utils_System::url("civicrm/case-features/quotations/contact-tab?cid=$contactID"), + 'title' => 'Quotations', + 'weight' => $weight, + 'count' => $this->getContactSalesOrderCount($contactID), + 'icon' => '', + ]; + } + + /** + * Returns the number of sales order owned by a contact. + * + * @param int $contactID + * Contact ID to retrieve count for. + */ + public function getContactSalesOrderCount(int $contactID) { + $result = CaseSalesOrder::get() + ->addSelect('COUNT(id) AS count') + ->addWhere('client_id', '=', $contactID) + ->execute() + ->jsonSerialize(); + + return $result[0]['count']; + } + +} diff --git a/CRM/Civicase/Page/ContactCaseSalesOrderTab.php b/CRM/Civicase/Page/ContactCaseSalesOrderTab.php new file mode 100644 index 000000000..678b9e65f --- /dev/null +++ b/CRM/Civicase/Page/ContactCaseSalesOrderTab.php @@ -0,0 +1,28 @@ +addModules(['crmApp', 'civicase-features']); + $contactId = CRM_Utils_Request::retrieveValue('cid', 'Positive'); + $this->assign('contactId', $contactId); + + return parent::run(); + } + +} diff --git a/templates/CRM/Civicase/Page/ContactCaseSalesOrderTab.tpl b/templates/CRM/Civicase/Page/ContactCaseSalesOrderTab.tpl new file mode 100644 index 000000000..b7ec9b85e --- /dev/null +++ b/templates/CRM/Civicase/Page/ContactCaseSalesOrderTab.tpl @@ -0,0 +1,16 @@ +
+
+ +
+
+ diff --git a/xml/Menu/civicase.xml b/xml/Menu/civicase.xml index bb8637f98..2c96e4703 100644 --- a/xml/Menu/civicase.xml +++ b/xml/Menu/civicase.xml @@ -43,6 +43,11 @@ CRM_Civicase_Form_CaseSalesOrderDelete administer CiviCase + + civicrm/case-features/quotations/contact-tab + CRM_Civicase_Page_ContactCaseSalesOrderTab + administer CiviCase + civicrm/case/webforms CRM_Civicase_Form_CaseWebforms From cd48c8a980bfeb8c06fd1abbefedeed862a62f1d Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Mon, 27 Mar 2023 18:19:00 +0100 Subject: [PATCH 048/199] BTHAB-21: Allow quotation to be filtered by description --- ang/afsearchContactQuotations.aff.html | 7 +++++++ ang/afsearchQuotations.aff.html | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/ang/afsearchContactQuotations.aff.html b/ang/afsearchContactQuotations.aff.html index 0a5239b33..82538824a 100644 --- a/ang/afsearchContactQuotations.aff.html +++ b/ang/afsearchContactQuotations.aff.html @@ -14,6 +14,7 @@
+
@@ -21,3 +22,9 @@
+ diff --git a/ang/afsearchQuotations.aff.html b/ang/afsearchQuotations.aff.html index d413ae665..fdf746f7a 100644 --- a/ang/afsearchQuotations.aff.html +++ b/ang/afsearchQuotations.aff.html @@ -17,6 +17,7 @@
+
@@ -24,3 +25,9 @@