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/BAO/CaseSalesOrder.php b/CRM/Civicase/BAO/CaseSalesOrder.php
new file mode 100644
index 000000000..8da5376c0
--- /dev/null
+++ b/CRM/Civicase/BAO/CaseSalesOrder.php
@@ -0,0 +1,90 @@
+copyValues($params);
+ $instance->save();
+ CRM_Utils_Hook::post($hook, $entityName, $instance->id, $instance);
+
+ 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/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/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/DAO/CaseSalesOrder.php b/CRM/Civicase/DAO/CaseSalesOrder.php
new file mode 100644
index 000000000..9e2304e13
--- /dev/null
+++ b/CRM/Civicase/DAO/CaseSalesOrder.php
@@ -0,0 +1,561 @@
+ 'civicrm/case-features/quotations/view?reset=1&id=[id]',
+ 'update' => 'civicrm/case-features/a#/quotations/new?reset=1&id=[id]',
+ 'delete' => 'civicrm/case-features/quotations/delete?reset=1&id=[id]',
+ ];
+
+ /**
+ * Unique CaseSalesOrder ID
+ *
+ * @var int|string|null
+ * (SQL type: int unsigned)
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $id;
+
+ /**
+ * FK to Contact
+ *
+ * @var int|string|null
+ * (SQL type: int unsigned)
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $client_id;
+
+ /**
+ * FK to Contact
+ *
+ * @var int|string|null
+ * (SQL type: int unsigned)
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $owner_id;
+
+ /**
+ * FK to Case
+ *
+ * @var int|string|null
+ * (SQL type: int unsigned)
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $case_id;
+
+ /**
+ * 3 character string, value from config setting or input via user.
+ *
+ * @var string|null
+ * (SQL type: varchar(3))
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $currency;
+
+ /**
+ * One of the values of the case_sales_order_status option group
+ *
+ * @var int|string
+ * (SQL type: int unsigned)
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $status_id;
+
+ /**
+ * One of the values of the case_sales_order_invoicing_status option group
+ *
+ * @var int|string
+ * (SQL type: int unsigned)
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $invoicing_status_id;
+
+ /**
+ * One of the values of the case_sales_order_payment_status option group
+ *
+ * @var int|string
+ * (SQL type: int unsigned)
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $payment_status_id;
+
+ /**
+ * Sales order deesctiption
+ *
+ * @var string
+ * (SQL type: text)
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $description;
+
+ /**
+ * Sales order notes
+ *
+ * @var string
+ * (SQL type: text)
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $notes;
+
+ /**
+ * Total amount of the sales order line items before tax deduction.
+ *
+ * @var float|string
+ * (SQL type: decimal(20,2))
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $total_before_tax;
+
+ /**
+ * Total amount of the sales order line items after tax deduction.
+ *
+ * @var float|string
+ * (SQL type: decimal(20,2))
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $total_after_tax;
+
+ /**
+ * Quotation date
+ *
+ * @var string|null
+ * (SQL type: timestamp)
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $quotation_date;
+
+ /**
+ * Date the sales order is created
+ *
+ * @var string|null
+ * (SQL type: timestamp)
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $created_at;
+
+ /**
+ * Is this sales order deleted?
+ *
+ * @var bool|string|null
+ * (SQL type: tinyint)
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $is_deleted;
+
+ /**
+ * Class constructor.
+ */
+ public function __construct() {
+ $this->__table = 'civicase_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('Quotations') : E::ts('Quotation');
+ }
+
+ /**
+ * 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' => 'civicase_sales_order.id',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order.client_id',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order.owner_id',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order.case_id',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order.currency',
+ 'headerPattern' => '/cur(rency)?/i',
+ 'dataPattern' => '/^[A-Z]{3}$/',
+ 'default' => NULL,
+ 'table_name' => 'civicase_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' => 'civicase_sales_order.status_id',
+ 'table_name' => 'civicase_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,
+ ],
+ 'invoicing_status_id' => [
+ 'name' => 'invoicing_status_id',
+ 'type' => CRM_Utils_Type::T_INT,
+ 'description' => E::ts('One of the values of the case_sales_order_invoicing_status option group'),
+ 'required' => TRUE,
+ 'where' => 'civicase_sales_order.invoicing_status_id',
+ 'table_name' => 'civicase_sales_order',
+ 'entity' => 'CaseSalesOrder',
+ 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder',
+ 'localizable' => 0,
+ 'html' => [
+ 'type' => 'Select',
+ 'label' => E::ts("Invoicing"),
+ ],
+ 'pseudoconstant' => [
+ 'optionGroupName' => 'case_sales_order_invoicing_status',
+ 'optionEditPath' => 'civicrm/admin/options/case_sales_order_invoicing_status',
+ ],
+ 'add' => NULL,
+ ],
+ 'payment_status_id' => [
+ 'name' => 'payment_status_id',
+ 'type' => CRM_Utils_Type::T_INT,
+ 'description' => E::ts('One of the values of the case_sales_order_payment_status option group'),
+ 'required' => TRUE,
+ 'where' => 'civicase_sales_order.payment_status_id',
+ 'table_name' => 'civicase_sales_order',
+ 'entity' => 'CaseSalesOrder',
+ 'bao' => 'CRM_Civicase_DAO_CaseSalesOrder',
+ 'localizable' => 0,
+ 'html' => [
+ 'type' => 'Select',
+ 'label' => E::ts("Payments"),
+ ],
+ 'pseudoconstant' => [
+ 'optionGroupName' => 'case_sales_order_payment_status',
+ 'optionEditPath' => 'civicrm/admin/options/case_sales_order_payment_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' => 'civicase_sales_order.description',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order.notes',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order.total_before_tax',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order.total_after_tax',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order.quotation_date',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order.created_at',
+ 'default' => 'CURRENT_TIMESTAMP',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order.is_deleted',
+ 'default' => '0',
+ 'table_name' => 'civicase_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__, '_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__, '_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/CRM/Civicase/DAO/CaseSalesOrderLine.php b/CRM/Civicase/DAO/CaseSalesOrderLine.php
new file mode 100644
index 000000000..a70bf5f84
--- /dev/null
+++ b/CRM/Civicase/DAO/CaseSalesOrderLine.php
@@ -0,0 +1,418 @@
+__table = 'civicase_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('Quotation Lines') : E::ts('Quotation 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', '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']);
+ }
+ 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' => 'civicase_sales_order_line.id',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order_line.sales_order_id',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order_line.financial_type_id',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order_line.product_id',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order_line.item_description',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order_line.quantity',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order_line.unit_price',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order_line.tax_rate',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order_line.discounted_percentage',
+ 'table_name' => 'civicase_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' => 'civicase_sales_order_line.subtotal_amount',
+ 'table_name' => 'civicase_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__, '_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__, '_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/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php
new file mode 100644
index 000000000..6261a825b
--- /dev/null
+++ b/CRM/Civicase/Form/CaseSalesOrderContributionCreate.php
@@ -0,0 +1,237 @@
+id = CRM_Utils_Request::retrieve('id', 'Positive', $this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function buildQuickForm() {
+ $this->addElement('radio', 'to_be_invoiced', '', ts('Enter % to be invoiced ?'),
+ self::INVOICE_PERCENT, [
+ 'id' => 'invoice_percent',
+ ]);
+ $this->add('text', 'percent_value', '', [
+ 'id' => 'percent_value',
+ 'placeholder' => 'Percentage to be invoiced',
+ 'class' => 'form-control',
+ 'min' => 1,
+ ], FALSE);
+
+ 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')
+ ->addWhere('option_group_id:name', '=', 'case_sales_order_status')
+ ->execute()
+ ->getArrayCopy();
+
+ $this->add(
+ 'select',
+ 'status',
+ ts('Update status of quotation to'),
+ ['' => '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 setDefaultValues() {
+ $caseSalesOrder = CaseSalesOrder::get()
+ ->addWhere('id', '=', $this->id)
+ ->addSelect('status_id')
+ ->execute()
+ ->first();
+
+ return [
+ 'status' => $caseSalesOrder['status_id'] ?? NULL,
+ ];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function addRules() {
+ $this->addFormRule([$this, 'formRule']);
+ $this->addFormRule([$this, 'validateAmount']);
+ }
+
+ /**
+ * Form Validation rule.
+ *
+ * This enforces the rule whereby,
+ * user must supply an amount if the
+ * enter percentage amount 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'] == self::INVOICE_PERCENT && empty(floatval($values['percent_value']))) {
+ $errors['percent_value'] = 'Percentage value is required';
+ }
+
+ 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 = [];
+
+ if ($values['to_be_invoiced'] == self::INVOICE_PERCENT) {
+ 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)
+ ->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.
+ $contributions = Contribution::get()
+ ->addSelect('total_amount')
+ ->addWhere('Opportunity_Details.Quotation', '=', $this->id)
+ ->execute()
+ ->jsonSerialize();
+
+ $paidTotal = array_sum(array_column($contributions, 'total_amount'));
+ $remainBalance = $caseSalesOrder['total_after_tax'] - $paidTotal;
+ $remainBalance = round($remainBalance, 2);
+
+ if ($remainBalance <= 0) {
+ return FALSE;
+ }
+
+ return TRUE;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function postProcess() {
+ $values = $this->getSubmitValues();
+
+ if (!empty($this->id) && !empty($values['to_be_invoiced'])) {
+ $this->createContribution($values);
+ }
+ }
+
+ /**
+ * Redirects user to contribution add page.
+ *
+ * This contribution page will have the line items
+ * prefilled from the sales order line items.
+ */
+ public function createContribution(array $values) {
+ $query = [
+ 'action' => 'add',
+ 'reset' => 1,
+ 'context' => 'standalone',
+ 'sales_order' => $this->id,
+ 'sales_order_status_id' => $values['status'],
+ '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);
+ CRM_Utils_System::redirect($url);
+ }
+
+}
diff --git a/CRM/Civicase/Form/CaseSalesOrderDelete.php b/CRM/Civicase/Form/CaseSalesOrderDelete.php
new file mode 100755
index 000000000..7922e23db
--- /dev/null
+++ b/CRM/Civicase/Form/CaseSalesOrderDelete.php
@@ -0,0 +1,62 @@
+id = CRM_Utils_Request::retrieve('id', 'Positive', $this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function buildQuickForm() {
+ $this->assign('id', $this->id);
+
+ $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/CRM/Civicase/Form/CaseSalesOrderInvoice.php b/CRM/Civicase/Form/CaseSalesOrderInvoice.php
new file mode 100644
index 000000000..9315f99e6
--- /dev/null
+++ b/CRM/Civicase/Form/CaseSalesOrderInvoice.php
@@ -0,0 +1,191 @@
+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');
+
+ /** @var \CRM_Civicase_Service_CaseSalesOrderInvoice */
+ $invoiceService = new \CRM_Civicase_Service_CaseSalesOrderInvoice(new \CRM_Civicase_WorkflowMessage_SalesOrderInvoice());
+ return $invoiceService->render($salesOrderId);
+ }
+
+ /**
+ * 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/Hook/BuildForm/AddCaseCategoryFeaturesField.php b/CRM/Civicase/Hook/BuildForm/AddCaseCategoryFeaturesField.php
new file mode 100644
index 000000000..f77b33f36
--- /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['value'];
+ $form->add(
+ 'checkbox',
+ 'case_category_feature_' . $feature['value'],
+ $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/BuildForm/AddEntityReferenceToCustomField.php b/CRM/Civicase/Hook/BuildForm/AddEntityReferenceToCustomField.php
new file mode 100644
index 000000000..9e6b6c53d
--- /dev/null
+++ b/CRM/Civicase/Hook/BuildForm/AddEntityReferenceToCustomField.php
@@ -0,0 +1,85 @@
+shouldRun($form, $formName)) {
+ return;
+ }
+
+ $customFields['case'] = [
+ 'name' => CRM_Core_BAO_CustomField::getCustomFieldID('Case_Opportunity', 'Opportunity_Details', TRUE),
+ 'entity' => 'Case',
+ 'placeholder' => '- Select Case/Opportunity -',
+ ];
+
+ $customFields['salesOrder'] = [
+ 'name' => CRM_Core_BAO_CustomField::getCustomFieldID('Quotation', 'Opportunity_Details', TRUE),
+ 'entity' => 'CaseSalesOrder',
+ 'placeholder' => '- Select Quotation -',
+ ];
+
+ \Civi::resources()->add([
+ '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.
+ *
+ * @param \CRM_Core_Form $form
+ * Form object.
+ * @param string $formName
+ * Form Name.
+ *
+ * @return bool
+ * True if hook should run, otherwise false.
+ */
+ 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;
+ }
+
+}
diff --git a/CRM/Civicase/Hook/BuildForm/AddQuotationsNotesToContributionSettings.php b/CRM/Civicase/Hook/BuildForm/AddQuotationsNotesToContributionSettings.php
new file mode 100644
index 000000000..f7d243b98
--- /dev/null
+++ b/CRM/Civicase/Hook/BuildForm/AddQuotationsNotesToContributionSettings.php
@@ -0,0 +1,65 @@
+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]));
+
+ CRM_Core_Region::instance('form-buttons')->add([
+ 'template' => "CRM/Civicase/Form/CaseSalesOrderInvoiceNote.tpl",
+ ]);
+ }
+
+}
diff --git a/CRM/Civicase/Hook/BuildForm/AddSalesOrderLineItemsToContribution.php b/CRM/Civicase/Hook/BuildForm/AddSalesOrderLineItemsToContribution.php
new file mode 100644
index 000000000..26c5b1974
--- /dev/null
+++ b/CRM/Civicase/Hook/BuildForm/AddSalesOrderLineItemsToContribution.php
@@ -0,0 +1,64 @@
+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),
+ 'quotation_custom_field' => CRM_Core_BAO_CustomField::getCustomFieldID('Quotation', 'Opportunity_Details', TRUE),
+ 'case_custom_field' => CRM_Core_BAO_CustomField::getCustomFieldID('Case_Opportunity', 'Opportunity_Details', TRUE),
+ ]);
+ }
+
+ /**
+ * 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/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php b/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php
new file mode 100644
index 000000000..6150bc8e4
--- /dev/null
+++ b/CRM/Civicase/Hook/BuildForm/AttachQuotationToInvoiceMail.php
@@ -0,0 +1,72 @@
+shouldRun($form, $formName)) {
+ return;
+ }
+
+ $contributionId = CRM_Utils_Request::retrieve('id', 'Positive', $form, FALSE);
+ $salesOrder = Contribution::get()
+ ->addSelect('Opportunity_Details.Quotation')
+ ->addWhere('Opportunity_Details.Quotation', 'IS NOT EMPTY')
+ ->addWhere('id', 'IN', explode(',', $contributionId))
+ ->addChain('salesOrder', CaseSalesOrder::get()
+ ->addWhere('id', '=', '$Opportunity_Details.Quotation')
+ )
+ ->execute()
+ ->getArrayCopy();
+
+ if (!empty($salesOrder)) {
+ $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",
+ ]);
+ }
+
+ /**
+ * 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($form, $formName) {
+ if (!in_array($formName, [
+ 'CRM_Contribute_Form_Task_Invoice',
+ 'CRM_Invoicehelper_Contribute_Form_Task_Invoice',
+ ])) {
+ return FALSE;
+ }
+
+ $contributionId = CRM_Utils_Request::retrieve('id', 'Positive', $form, FALSE);
+ if (!$contributionId) {
+ return FALSE;
+ }
+
+ return TRUE;
+ }
+
+}
diff --git a/CRM/Civicase/Hook/BuildForm/RefreshInvoiceListOnUpdate.php b/CRM/Civicase/Hook/BuildForm/RefreshInvoiceListOnUpdate.php
new file mode 100644
index 000000000..8a799804b
--- /dev/null
+++ b/CRM/Civicase/Hook/BuildForm/RefreshInvoiceListOnUpdate.php
@@ -0,0 +1,49 @@
+shouldRun($form, $formName)) {
+ return;
+ }
+
+ CRM_Core_Resources::singleton()->addScript(
+ "CRM.$(function($) {
+ $(\"a[target='crm-popup']\").on('crmPopupFormSuccess', function (e) {
+ CRM.refreshParent(e);
+ });
+ });
+ ");
+ }
+
+ /**
+ * 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($form, $formName) {
+ if ($formName !== 'CRM_Contribute_Form_Contribution' || $form->getAction() !== CRM_Core_Action::UPDATE || !isset($_GET['snippet'])) {
+ return FALSE;
+ }
+
+ return TRUE;
+ }
+
+}
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/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 f71494176..00f914acc 100644
--- a/CRM/Civicase/Hook/NavigationMenu/AlterForCaseMenu.php
+++ b/CRM/Civicase/Hook/NavigationMenu/AlterForCaseMenu.php
@@ -1,12 +1,13 @@
caseCategorySetting = new CaseCategorySetting();
$this->rewriteCaseUrls($menu);
$this->addCaseWebformUrl($menu);
+ $this->addCiviCaseInstanceMenu($menu);
}
/**
@@ -116,4 +118,51 @@ 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];
+
+ $desiredWeight = $this->moveMenuDown($civicaseSettings['child'], 'Case Types');
+
+ $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;
+ }
+
}
diff --git a/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php b/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php
new file mode 100644
index 000000000..a3c4ec6e8
--- /dev/null
+++ b/CRM/Civicase/Hook/NavigationMenu/CaseInstanceFeaturesMenu.php
@@ -0,0 +1,76 @@
+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 {
+ $caseTypeCategoryFeatures = new CaseTypeCategoryFeatures();
+ $caseInstancesGroup = $caseTypeCategoryFeatures->retrieveCaseInstanceWithEnabledFeatures(self::FEATURES_WITH_MENU);
+
+ 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(),
+ ],
+ ]);
+ }
+ }
+
+}
diff --git a/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php b/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php
new file mode 100644
index 000000000..a729856e9
--- /dev/null
+++ b/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php
@@ -0,0 +1,134 @@
+shouldRun($op, $objectName, $objectRef)) {
+ return;
+ }
+
+ $financialTrnxId = $objectRef->financial_trxn_id;
+ if (empty($financialTrnxId)) {
+ return;
+ }
+
+ $contributionId = $this->getContributionId($financialTrnxId);
+ if (empty($contributionId)) {
+ return;
+ }
+
+ $contribution = Contribution::get()
+ ->addSelect('Opportunity_Details.Case_Opportunity', 'Opportunity_Details.Quotation')
+ ->addWhere('id', '=', $contributionId)
+ ->execute()
+ ->first();
+
+ $this->updateQuotationFinancialStatuses($contribution['Opportunity_Details.Quotation'] ?: NULL);
+ $this->updateCaseOpportunityFinancialDetails($contribution['Opportunity_Details.Case_Opportunity'] ?: NULL);
+ }
+
+ /**
+ * Updates CaseSalesOrder financial statuses.
+ *
+ * @param int $salesOrderID
+ * CaseSalesOrder ID.
+ */
+ private function updateQuotationFinancialStatuses(?int $salesOrderID): void {
+ if (empty($salesOrderID)) {
+ return;
+ }
+
+ $transaction = CRM_Core_Transaction::create();
+
+ try {
+ $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSalesOrderContributionCalculator($salesOrderID);
+ $paymentStatusID = $caseSaleOrderContributionService->calculatePaymentStatus();
+ $invoicingStatusID = $caseSaleOrderContributionService->calculateInvoicingStatus();
+
+ CaseSalesOrder::update()
+ ->addWhere('id', '=', $salesOrderID)
+ ->addValue('invoicing_status_id', $invoicingStatusID)
+ ->addValue('payment_status_id', $paymentStatusID)
+ ->execute();
+
+ $transaction->commit();
+ }
+ catch (\Throwable $th) {
+ $transaction->rollback();
+ CRM_Core_Error::statusBounce(ts('Error updating sales order statues'));
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @param string $op
+ * 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, $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
new file mode 100644
index 000000000..e93bf6078
--- /dev/null
+++ b/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php
@@ -0,0 +1,100 @@
+shouldRun($op, $objectName)) {
+ return;
+ }
+
+ $salesOrderId = CRM_Utils_Request::retrieve('sales_order', 'Integer');
+ if (empty($salesOrderId)) {
+ $salesOrderId = $this->getQuotationID($objectId)['Opportunity_Details.Quotation'];
+ }
+
+ if (empty($salesOrderId)) {
+ 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)) {
+ $salesOrder = $salesOrder['status_id'];
+ }
+
+ $transaction = CRM_Core_Transaction::create();
+ try {
+ $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSalesOrderContributionCalculator($salesOrderId);
+ $paymentStatusID = $caseSaleOrderContributionService->calculatePaymentStatus();
+ $invoicingStatusID = $caseSaleOrderContributionService->calculateInvoicingStatus();
+
+ CaseSalesOrder::update()
+ ->addWhere('id', '=', $salesOrderId)
+ ->addValue('status_id', $salesOrderStatusId)
+ ->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();
+ 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.
+ *
+ * @return bool
+ * returns a boolean to determine if hook will run or not.
+ */
+ private function shouldRun($op, $objectName) {
+ return strtolower($objectName) == 'contribution' && $op == 'create';
+ }
+
+ /**
+ * Gets quotation ID by contribution ID.
+ */
+ private function getQuotationId($id) {
+ return Contribution::get()
+ ->addSelect('Opportunity_Details.Quotation')
+ ->addWhere('id', '=', $id)
+ ->execute()
+ ->first();
+ }
+
+}
diff --git a/CRM/Civicase/Hook/PostProcess/SaveCaseCategoryFeature.php b/CRM/Civicase/Hook/PostProcess/SaveCaseCategoryFeature.php
new file mode 100644
index 000000000..acee44045
--- /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['value']])) {
+ CaseCategoryFeatures::create()
+ ->addValue('category_id', $categoryId)
+ ->addValue('feature_id', $feature['value'])
+ ->execute();
+ }
+ }
+
+ }
+
+}
diff --git a/CRM/Civicase/Hook/PostProcess/SaveQuotationsNotesSettings.php b/CRM/Civicase/Hook/PostProcess/SaveQuotationsNotesSettings.php
new file mode 100644
index 000000000..781375f28
--- /dev/null
+++ b/CRM/Civicase/Hook/PostProcess/SaveQuotationsNotesSettings.php
@@ -0,0 +1,50 @@
+shouldRun($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/CRM/Civicase/Hook/Pre/DeleteSalesOrderContribution.php b/CRM/Civicase/Hook/Pre/DeleteSalesOrderContribution.php
new file mode 100644
index 000000000..9dacd7a73
--- /dev/null
+++ b/CRM/Civicase/Hook/Pre/DeleteSalesOrderContribution.php
@@ -0,0 +1,85 @@
+shouldRun($op, $objectName)) {
+ return;
+ }
+
+ $salesOrderId = $this->getQuotationId($objectId);
+ if (empty($salesOrderId)) {
+ return;
+ }
+
+ $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSalesOrderContributionCalculator(
+ $salesOrderId,
+ (int) $objectId
+ );
+ $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'],
+ (int) $objectId
+ );
+ $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/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..0d66a4c6f
--- /dev/null
+++ b/CRM/Civicase/Hook/Tabset/CaseSalesOrderTabAdd.php
@@ -0,0 +1,55 @@
+retrieveCaseInstanceWithEnabledFeatures(['quotations']);
+
+ if (empty($caseInstances)) {
+ return;
+ }
+
+ $tabs[] = [
+ 'id' => '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/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'][''] = '';
}
diff --git a/CRM/Civicase/Hook/Tokens/SalesOrderTokens.php b/CRM/Civicase/Hook/Tokens/SalesOrderTokens.php
new file mode 100644
index 000000000..b6e4dc930
--- /dev/null
+++ b/CRM/Civicase/Hook/Tokens/SalesOrderTokens.php
@@ -0,0 +1,82 @@
+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) {
+ 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(),
+ ],
+ ]
+ );
+ }
+ }
+ }
+ }
+ }
+
+ }
+
+}
diff --git a/CRM/Civicase/Hook/alterMailParams/AttachQuotation.php b/CRM/Civicase/Hook/alterMailParams/AttachQuotation.php
new file mode 100644
index 000000000..d6a99919c
--- /dev/null
+++ b/CRM/Civicase/Hook/alterMailParams/AttachQuotation.php
@@ -0,0 +1,84 @@
+shouldRun($params, $context, $shouldAttachQuote)) {
+ return;
+ }
+
+ $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']['quotaition_invoice'] = $attachment;
+ }
+ }
+
+ /**
+ * Renders the Invoice for the quotation linked to the contribution.
+ *
+ * @param int $contributionId
+ * The contribution ID.
+ */
+ private function getContributionQuotationInvoice($contributionId) {
+ $salesOrder = Contribution::get()
+ ->addSelect('Opportunity_Details.Quotation')
+ ->addWhere('Opportunity_Details.Quotation', 'IS NOT EMPTY')
+ ->addWhere('id', '=', $contributionId)
+ ->addChain('salesOrder', CaseSalesOrder::get()
+ ->addWhere('id', '=', '$Opportunity_Details.Quotation')
+ )
+ ->execute()
+ ->first()['salesOrder'];
+
+ if (empty($salesOrder)) {
+ return;
+ }
+
+ /** @var \CRM_Civicase_Service_CaseSalesOrderInvoice */
+ $invoiceService = new \CRM_Civicase_Service_CaseSalesOrderInvoice(new CRM_Civicase_WorkflowMessage_SalesOrderInvoice());
+ return $invoiceService->render($salesOrder[0]['id']);
+ }
+
+ /**
+ * Determines if the hook will run.
+ *
+ * @param array $params
+ * Mail parameters.
+ * @param string $context
+ * Mail context.
+ * @param string $shouldAttachQuote
+ * If the Attach Quote is set.
+ *
+ * @return bool
+ * returns TRUE if hook should run, FALSE otherwise.
+ */
+ private function shouldRun(array $params, $context, $shouldAttachQuote) {
+ $component = $params['tplParams']['component'] ?? '';
+ if ($component !== 'contribute' || empty($shouldAttachQuote)) {
+ return FALSE;
+ }
+
+ return TRUE;
+ }
+
+}
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/CRM/Civicase/Page/CaseFeaturesAngular.php b/CRM/Civicase/Page/CaseFeaturesAngular.php
new file mode 100644
index 000000000..a6cad6eb1
--- /dev/null
+++ b/CRM/Civicase/Page/CaseFeaturesAngular.php
@@ -0,0 +1,42 @@
+setPageName('civicrm/case-features/a');
+ $loader->addModules(['crmApp', 'civicase-features']);
+ \Civi::resources()->addSetting([
+ 'crmApp' => [
+ 'defaultRoute' => '/quotations',
+ ],
+ ]);
+
+ return parent::run();
+ }
+
+ /**
+ * Get Template File Name.
+ *
+ * @inheritdoc
+ */
+ public function getTemplateFileName() {
+ return 'Civi/Angular/Page/Main.tpl';
+ }
+
+}
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/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/CRM/Civicase/Service/AbstractBaseSalesOrderCalculator.php b/CRM/Civicase/Service/AbstractBaseSalesOrderCalculator.php
new file mode 100644
index 000000000..ce7752b0f
--- /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
new file mode 100644
index 000000000..063447175
--- /dev/null
+++ b/CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php
@@ -0,0 +1,219 @@
+salesOrder = $this->getSalesOrder($salesOrderId);
+ $this->deletingContributionId = $deletingContributionId;
+ $this->contributions = $this->getContributions();
+ $this->totalInvoicedAmount = $this->getTotalInvoicedAmount();
+ $this->totalPaymentsAmount = $this->getTotalPaymentsAmount();
+
+ }
+
+ /**
+ * Calculates total invoiced amount.
+ *
+ * @return float
+ * Total invoiced amount.
+ */
+ public function calculateTotalInvoicedAmount(): float {
+
+ return $this->getTotalInvoicedAmount();
+ }
+
+ /**
+ * Calculates total paid amount.
+ *
+ * @return float
+ * Total paid amounts.
+ */
+ 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.
+ *
+ * @return string
+ * Invoicing status option value's value
+ */
+ public function calculateInvoicingStatus() {
+ if (empty($this->salesOrder) || empty($this->contributions)) {
+ return $this->getValueFromOptionValues(parent::INVOICING_STATUS_NO_INVOICES, $this->invoicingStatusOptionValues);
+ }
+
+ $quotationTotalAmount = $this->salesOrder['total_after_tax'];
+ if ($this->totalInvoicedAmount < $quotationTotalAmount) {
+ return $this->getValueFromOptionValues(parent::INVOICING_STATUS_PARTIALLY_INVOICED, $this->invoicingStatusOptionValues);
+ }
+
+ return $this->getValueFromOptionValues(parent::INVOICING_STATUS_FULLY_INVOICED, $this->invoicingStatusOptionValues);
+ }
+
+ /**
+ * Calculates payment status.
+ *
+ * @return string
+ * Payment status option value's value
+ */
+ public function calculatePaymentStatus() {
+ if (empty($this->salesOrder) || empty($this->contributions) || !($this->totalPaymentsAmount > 0)) {
+
+ return $this->getValueFromOptionValues(parent::PAYMENT_STATUS_NO_PAYMENTS, $this->paymentStatusOptionValues);
+ }
+
+ $quotationTotalAmount = $this->salesOrder['total_after_tax'];
+ if ($this->totalPaymentsAmount < $quotationTotalAmount) {
+ return $this->getValueFromOptionValues(parent::PAYMENT_STATUS_PARTIALLY_PAID, $this->paymentStatusOptionValues);
+ }
+
+ if ($this->totalPaymentsAmount > $quotationTotalAmount) {
+ return $this->getValueFromOptionValues(parent::PAYMENT_STATUS_OVERPAID, $this->paymentStatusOptionValues);
+ }
+
+ return $this->getValueFromOptionValues(parent::PAYMENT_STATUS_FULLY_PAID, $this->paymentStatusOptionValues);
+ }
+
+ /**
+ * Gets list of contributions from the sale order.
+ *
+ * @return array
+ * List of contributions.
+ *
+ * @throws API_Exception
+ * @throws \Civi\API\Exception\UnauthorizedException
+ */
+ private function getContributions() {
+ if (empty($this->salesOrder) || empty($this->salesOrder['id'])) {
+ return [];
+ }
+
+ $contributions = Contribution::get(FALSE)
+ ->addWhere('Opportunity_Details.Quotation', '=', $this->salesOrder['id']);
+
+ if ($this->deletingContributionId !== NULL) {
+ $contributions->addWhere('id', '!=', $this->deletingContributionId);
+ }
+
+ return $contributions->execute()->getArrayCopy();
+ }
+
+ /**
+ * Gets Sales Order by SaleOrder ID.
+ *
+ * @param string $saleOrderId
+ * Sales Order ID.
+ *
+ * @return array|null
+ * Sales Order array or null
+ *
+ * @throws API_Exception
+ * @throws \Civi\API\Exception\UnauthorizedException
+ */
+ private function getSalesOrder($saleOrderId) {
+ return CaseSalesOrder::get(FALSE)
+ ->addWhere('id', '=', $saleOrderId)
+ ->execute()
+ ->first();
+ }
+
+ /**
+ * Gets total invoiced amount.
+ *
+ * @return float
+ * Total invoiced amount.
+ */
+ private function getTotalInvoicedAmount(): float {
+ $totalInvoicedAmount = 0.0;
+ foreach ($this->contributions as $contribution) {
+ $totalInvoicedAmount += $contribution['total_amount'];
+ }
+
+ return $totalInvoicedAmount;
+ }
+
+ /**
+ * Gets total payments amount.
+ *
+ * @return float
+ * Total payment amount.
+ */
+ private function getTotalPaymentsAmount(): float {
+ $totalPaymentsAmount = 0.0;
+ foreach ($this->contributions as $contribution) {
+ $payments = civicrm_api3('Payment', 'get', [
+ 'sequential' => 1,
+ 'contribution_id' => $contribution['id'],
+ ])['values'];
+
+ foreach ($payments as $payment) {
+ $totalPaymentsAmount += $payment['total_amount'];
+ }
+ }
+
+ return $totalPaymentsAmount;
+ }
+
+}
diff --git a/CRM/Civicase/Service/CaseSalesOrderInvoice.php b/CRM/Civicase/Service/CaseSalesOrderInvoice.php
new file mode 100644
index 000000000..426b2352a
--- /dev/null
+++ b/CRM/Civicase/Service/CaseSalesOrderInvoice.php
@@ -0,0 +1,142 @@
+addWhere('id', '=', $id)
+ ->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($id);
+ $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,
+ ];
+ }
+
+ /**
+ * Returns the default format to use for Invoice.
+ */
+ private static function defaultInvoiceFormat() {
+ return [
+ 'margin_top' => 10,
+ 'margin_left' => 65,
+ 'metric' => 'px',
+ ];
+ }
+
+}
diff --git a/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php
new file mode 100644
index 000000000..63379b52d
--- /dev/null
+++ b/CRM/Civicase/Service/CaseSalesOrderLineItemsGenerator.php
@@ -0,0 +1,171 @@
+setSalesOrder();
+ }
+
+ /**
+ * Sets the sales order value.
+ */
+ private function setSalesOrder(): void {
+ $this->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['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);
+ }
+ }
+
+ 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 = [];
+
+ $contributions = Contribution::get()
+ ->addSelect('*',)
+ ->addWhere('Opportunity_Details.Quotation', '=', $this->salesOrderId)
+ ->addChain('items', LineItem::get()
+ ->addSelect('qty', 'unit_price', 'tax_amount', 'line_total', 'entity_table', 'label', 'financial_type_id')
+ ->addWhere('contribution_id', '=', '$id')
+ )
+ ->execute();
+
+ foreach ($contributions as $contribution) {
+ $items = $contribution['items'];
+
+ if (empty($items)) {
+ 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'];
+ $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' => round($item['tax'], 2),
+ 'label' => $item['item_description'],
+ 'entity_table' => 'civicrm_contribution',
+ 'financial_type_id' => $item['financial_type_id'],
+ 'line_total' => round($item['total'], 2),
+ 'unit_price' => round($item['unit_price'], 2),
+ ];
+ }
+
+ /**
+ * 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/CRM/Civicase/Service/CaseSalesOrderOpportunityCalculator.php b/CRM/Civicase/Service/CaseSalesOrderOpportunityCalculator.php
new file mode 100644
index 000000000..971d7da57
--- /dev/null
+++ b/CRM/Civicase/Service/CaseSalesOrderOpportunityCalculator.php
@@ -0,0 +1,184 @@
+caseId = $caseId;
+ $this->deletingContributionId = $deletingContributionId;
+ $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->totalQuotedAmount) {
+ return $this->getLabelFromOptionValues(parent::PAYMENT_STATUS_PARTIALLY_PAID, $this->paymentStatusOptionValues);
+ }
+
+ if ($this->totalPaidAmount > $this->totalQuotedAmount) {
+ 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->calculateTotalInvoicedAmount())
+ ->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) {
+ $contributions = Contribution::get(FALSE)
+ ->addSelect('*', 'Opportunity_Details.Quotation')
+ ->addWhere('Opportunity_Details.Case_Opportunity', '=', $caseId);
+
+ if ($this->deletingContributionId !== NULL) {
+ $contributions->addWhere('id', '!=', $this->deletingContributionId);
+ }
+
+ return $contributions->execute()->getArrayCopy();
+ }
+
+}
diff --git a/CRM/Civicase/Service/CaseTypeCategoryFeatures.php b/CRM/Civicase/Service/CaseTypeCategoryFeatures.php
new file mode 100644
index 000000000..94e557caf
--- /dev/null
+++ b/CRM/Civicase/Service/CaseTypeCategoryFeatures.php
@@ -0,0 +1,65 @@
+addSelect('id', 'label', 'value', 'name', 'option_group_id')
+ ->addWhere('option_group_id:name', '=', self::NAME)
+ ->execute();
+
+ return $optionValues;
+ }
+
+ /**
+ * Retrieves case instance that has the defined features enabled.
+ *
+ * @param array $features
+ * The features to retrieve case instances for.
+ *
+ * @return array
+ * Array of Key\Pair value grouped by case instance id.
+ */
+ public function retrieveCaseInstanceWithEnabledFeatures(array $features) {
+ $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', $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;
+ }
+
+}
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/CaseSalesOrderStatusManager.php b/CRM/Civicase/Setup/Manage/CaseSalesOrderStatusManager.php
new file mode 100644
index 000000000..e35d20efb
--- /dev/null
+++ b/CRM/Civicase/Setup/Manage/CaseSalesOrderStatusManager.php
@@ -0,0 +1,196 @@
+createSaleOrderStatus();
+ $this->createSaleOrderInvoicingStatus();
+ $this->createSaleOrderPaymentStatus();
+ }
+
+ /**
+ * Creates sale order status.
+ */
+ private function createSaleOrderStatus() {
+ CRM_Core_BAO_OptionGroup::ensureOptionGroupExists([
+ 'name' => self::SALE_ORDER_STATUS_NAME,
+ 'title' => ts('Sales Order Status'),
+ 'is_reserved' => 1,
+ ]);
+
+ CRM_Core_BAO_OptionValue::ensureOptionValueExists([
+ 'option_group_id' => self::SALE_ORDER_STATUS_NAME,
+ 'name' => 'new',
+ 'label' => 'New',
+ 'is_default' => TRUE,
+ 'is_active' => TRUE,
+ 'is_reserved' => TRUE,
+ ]);
+
+ CRM_Core_BAO_OptionValue::ensureOptionValueExists([
+ 'option_group_id' => self::SALE_ORDER_STATUS_NAME,
+ 'name' => 'sent_to_client',
+ 'label' => 'Sent to client',
+ 'is_active' => TRUE,
+ 'is_reserved' => TRUE,
+ ]);
+
+ CRM_Core_BAO_OptionValue::ensureOptionValueExists([
+ 'option_group_id' => self::SALE_ORDER_STATUS_NAME,
+ 'name' => 'accepted',
+ 'label' => 'Accepted',
+ 'is_active' => TRUE,
+ 'is_reserved' => TRUE,
+ ]);
+
+ CRM_Core_BAO_OptionValue::ensureOptionValueExists([
+ 'option_group_id' => self::SALE_ORDER_STATUS_NAME,
+ 'name' => 'deposit_paid',
+ 'label' => 'Deposit paid',
+ 'is_active' => TRUE,
+ 'is_reserved' => TRUE,
+ ]);
+
+ CRM_Core_BAO_OptionValue::ensureOptionValueExists([
+ 'option_group_id' => self::SALE_ORDER_STATUS_NAME,
+ 'name' => 'declined',
+ 'label' => 'Declined',
+ 'is_active' => TRUE,
+ 'is_reserved' => TRUE,
+ ]);
+ }
+
+ /**
+ * Creates sale order invoicing status.
+ */
+ private function createSaleOrderInvoicingStatus() {
+ CRM_Core_BAO_OptionGroup::ensureOptionGroupExists([
+ 'name' => self::SALE_ORDER_INVOICING_STATUS_NANE,
+ 'title' => ts('Invoicing'),
+ 'is_reserved' => 1,
+ ]);
+
+ CRM_Core_BAO_OptionValue::ensureOptionValueExists([
+ 'option_group_id' => self::SALE_ORDER_INVOICING_STATUS_NANE,
+ 'name' => 'no_invoices',
+ 'label' => 'No Invoices',
+ 'is_active' => TRUE,
+ 'is_reserved' => TRUE,
+ ]);
+
+ CRM_Core_BAO_OptionValue::ensureOptionValueExists([
+ 'option_group_id' => self::SALE_ORDER_INVOICING_STATUS_NANE,
+ 'name' => 'partially_invoiced',
+ 'label' => 'Partially Invoiced',
+ 'is_active' => TRUE,
+ 'is_reserved' => TRUE,
+ ]);
+
+ CRM_Core_BAO_OptionValue::ensureOptionValueExists([
+ 'option_group_id' => self::SALE_ORDER_INVOICING_STATUS_NANE,
+ 'name' => 'fully_invoiced',
+ 'label' => 'Fully Invoiced',
+ 'is_active' => TRUE,
+ 'is_reserved' => TRUE,
+ ]);
+ }
+
+ /**
+ * Creates sale order payments status.
+ */
+ private function createSaleOrderPaymentStatus() {
+ CRM_Core_BAO_OptionGroup::ensureOptionGroupExists([
+ 'name' => self::SALE_ORDER_PAYMENT_STATUS_NANE,
+ 'title' => ts('Payments'),
+ 'is_reserved' => 1,
+ ]);
+
+ CRM_Core_BAO_OptionValue::ensureOptionValueExists([
+ 'option_group_id' => self::SALE_ORDER_PAYMENT_STATUS_NANE,
+ 'name' => 'no_payments',
+ 'label' => 'No Payments',
+ 'is_active' => TRUE,
+ 'is_reserved' => TRUE,
+ ]);
+
+ CRM_Core_BAO_OptionValue::ensureOptionValueExists([
+ 'option_group_id' => self::SALE_ORDER_PAYMENT_STATUS_NANE,
+ 'name' => 'no_payments',
+ 'label' => 'No Payments',
+ 'is_active' => TRUE,
+ 'is_reserved' => TRUE,
+ ]);
+
+ CRM_Core_BAO_OptionValue::ensureOptionValueExists([
+ 'option_group_id' => self::SALE_ORDER_PAYMENT_STATUS_NANE,
+ 'name' => 'partially_paid',
+ 'label' => 'Partially Paid',
+ 'is_active' => TRUE,
+ 'is_reserved' => TRUE,
+ ]);
+
+ CRM_Core_BAO_OptionValue::ensureOptionValueExists([
+ 'option_group_id' => self::SALE_ORDER_PAYMENT_STATUS_NANE,
+ 'name' => 'fully_paid',
+ 'label' => 'Fully Paid',
+ 'is_active' => TRUE,
+ 'is_reserved' => TRUE,
+ ]);
+
+ CRM_Core_BAO_OptionValue::ensureOptionValueExists([
+ 'option_group_id' => self::SALE_ORDER_PAYMENT_STATUS_NANE,
+ 'name' => 'over_paid',
+ 'label' => 'Over Paid',
+ 'is_active' => TRUE,
+ 'is_reserved' => TRUE,
+ ]);
+ }
+
+ /**
+ * Removes the entity.
+ */
+ public function remove(): void {
+ civicrm_api3('OptionGroup', 'get', [
+ 'return' => ['id'],
+ 'name' => [
+ 'IN' => [
+ self::SALE_ORDER_STATUS_NAME,
+ self::SALE_ORDER_INVOICING_STATUS_NANE,
+ self::SALE_ORDER_PAYMENT_STATUS_NANE,
+ ],
+ ],
+ 'api.OptionGroup.delete' => ['id' => '$value.id'],
+ ]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function toggle($status): void {
+ civicrm_api3('OptionGroup', 'get', [
+ 'sequential' => 1,
+ 'name' => [
+ 'IN' => [
+ self::SALE_ORDER_STATUS_NAME,
+ self::SALE_ORDER_INVOICING_STATUS_NANE,
+ self::SALE_ORDER_PAYMENT_STATUS_NANE,
+ ],
+ ],
+ 'api.OptionGroup.create' => ['id' => '$value.id', 'is_active' => $status],
+ ]);
+ }
+
+}
diff --git a/CRM/Civicase/Setup/Manage/CaseTypeCategoryFeaturesManager.php b/CRM/Civicase/Setup/Manage/CaseTypeCategoryFeaturesManager.php
new file mode 100644
index 000000000..ae6f0e9fc
--- /dev/null
+++ b/CRM/Civicase/Setup/Manage/CaseTypeCategoryFeaturesManager.php
@@ -0,0 +1,62 @@
+ 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,
+ ]);
+ }
+
+ /**
+ * 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/Setup/Manage/MembershipTypeCustomFieldManager.php b/CRM/Civicase/Setup/Manage/MembershipTypeCustomFieldManager.php
new file mode 100644
index 000000000..710425708
--- /dev/null
+++ b/CRM/Civicase/Setup/Manage/MembershipTypeCustomFieldManager.php
@@ -0,0 +1,34 @@
+ self::OPTION_GROUP_NAME,
+ "label" => "Membership Type",
+ "value" => "MembershipType",
+ "name" => "civicrm_membership_type",
+ 'is_active' => TRUE,
+ 'is_reserved' => TRUE,
+ ]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function remove(): void {}
+
+ /**
+ * {@inheritDoc}
+ */
+ protected function toggle($status): void {}
+
+}
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/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/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/CRM/Civicase/Upgrader.php b/CRM/Civicase/Upgrader.php
index 762e85d6b..d47880eeb 100644
--- a/CRM/Civicase/Upgrader.php
+++ b/CRM/Civicase/Upgrader.php
@@ -1,25 +1,29 @@
createManageCasesMenuItem();
+ (new CaseTypeCategoryFeaturesManager())->create();
+ (new CaseSalesOrderStatusManager())->create();
+ (new QuotationTemplateManager())->create();
+ (new MembershipTypeCustomFieldManager())->create();
}
/**
@@ -243,6 +251,10 @@ public function uninstall() {
foreach ($steps as $step) {
$step->apply();
}
+
+ (new CaseTypeCategoryFeaturesManager())->remove();
+ (new CaseSalesOrderStatusManager())->remove();
+ (new QuotationTemplateManager())->remove();
}
/**
@@ -407,6 +419,10 @@ public function enable() {
$workflowMenu = new AddManageWorkflowMenu();
$workflowMenu->apply();
+
+ (new CaseTypeCategoryFeaturesManager())->enable();
+ (new CaseSalesOrderStatusManager())->enable();
+ (new QuotationTemplateManager())->enable();
}
/**
@@ -416,6 +432,9 @@ public function disable() {
$this->swapCaseMenuItems();
$this->toggleNav('Manage Cases', FALSE);
+ (new CaseTypeCategoryFeaturesManager())->disable();
+ (new CaseSalesOrderStatusManager())->disable();
+ (new QuotationTemplateManager())->disable();
}
/**
@@ -497,13 +516,13 @@ public function hasPendingRevisions() {
*
* @inheritdoc
*/
- public function enqueuePendingRevisions(CRM_Queue_Queue $queue) {
+ public function enqueuePendingRevisions() {
$currentRevisionNum = (int) $this->getCurrentRevision();
foreach ($this->getRevisions() as $revisionClass => $revisionNum) {
if ($revisionNum <= $currentRevisionNum) {
continue;
}
- $title = E::ts('Upgrade %1 to revision %2', [
+ $title = ts('Upgrade %1 to revision %2', [
1 => $this->extensionName,
2 => $revisionNum,
]);
@@ -512,13 +531,8 @@ public function enqueuePendingRevisions(CRM_Queue_Queue $queue) {
[(new $revisionClass())],
$title
);
- $queue->createItem($upgradeTask);
- $setRevisionTask = new CRM_Queue_Task(
- [get_class($this), '_queueAdapter'],
- ['setCurrentRevision', $revisionNum],
- $title
- );
- $queue->createItem($setRevisionTask);
+ $this->queue->createItem($upgradeTask);
+ $this->appendTask($title, 'setCurrentRevision', $revisionNum);
}
}
diff --git a/CRM/Civicase/Upgrader/Base.php b/CRM/Civicase/Upgrader/Base.php
deleted file mode 100644
index 1c8340902..000000000
--- a/CRM/Civicase/Upgrader/Base.php
+++ /dev/null
@@ -1,396 +0,0 @@
-ctx = array_shift($args);
- $instance->queue = $instance->ctx->queue;
- $method = array_shift($args);
- return call_user_func_array([$instance, $method], $args);
- }
-
- /**
- * CRM_Civicase_Upgrader_Base constructor.
- *
- * @param $extensionName
- * @param $extensionDir
- */
- public function __construct($extensionName, $extensionDir) {
- $this->extensionName = $extensionName;
- $this->extensionDir = $extensionDir;
- }
-
- // ******** Task helpers ********
-
- /**
- * Run a CustomData file.
- *
- * @param string $relativePath
- * the CustomData XML file path (relative to this extension's dir)
- * @return bool
- */
- public function executeCustomDataFile($relativePath) {
- $xml_file = $this->extensionDir . '/' . $relativePath;
- return $this->executeCustomDataFileByAbsPath($xml_file);
- }
-
- /**
- * Run a CustomData file
- *
- * @param string $xml_file
- * the CustomData XML file path (absolute path)
- *
- * @return bool
- */
- protected function executeCustomDataFileByAbsPath($xml_file) {
- $import = new CRM_Utils_Migrate_Import();
- $import->run($xml_file);
- return TRUE;
- }
-
- /**
- * Run a SQL file.
- *
- * @param string $relativePath
- * the SQL file path (relative to this extension's dir)
- *
- * @return bool
- */
- public function executeSqlFile($relativePath) {
- CRM_Utils_File::sourceSQLFile(
- CIVICRM_DSN,
- $this->extensionDir . DIRECTORY_SEPARATOR . $relativePath
- );
- return TRUE;
- }
-
- /**
- * Run the sql commands in the specified file.
- *
- * @param string $tplFile
- * The SQL file path (relative to this extension's dir).
- * Ex: "sql/mydata.mysql.tpl".
- *
- * @return bool
- * @throws \CRM_Core_Exception
- */
- public function executeSqlTemplate($tplFile) {
- // Assign multilingual variable to Smarty.
- $upgrade = new CRM_Upgrade_Form();
-
- $tplFile = CRM_Utils_File::isAbsolute($tplFile) ? $tplFile : $this->extensionDir . DIRECTORY_SEPARATOR . $tplFile;
- $smarty = CRM_Core_Smarty::singleton();
- $smarty->assign('domainID', CRM_Core_Config::domainID());
- CRM_Utils_File::sourceSQLFile(
- CIVICRM_DSN, $smarty->fetch($tplFile), NULL, TRUE
- );
- return TRUE;
- }
-
- /**
- * Run one SQL query.
- *
- * This is just a wrapper for CRM_Core_DAO::executeSql, but it
- * provides syntactic sugar for queueing several tasks that
- * run different queries
- *
- * @return bool
- */
- public function executeSql($query, $params = []) {
- // FIXME verify that we raise an exception on error
- CRM_Core_DAO::executeQuery($query, $params);
- return TRUE;
- }
-
- /**
- * Syntactic sugar for enqueuing a task which calls a function in this class.
- *
- * The task is weighted so that it is processed
- * as part of the currently-pending revision.
- *
- * After passing the $funcName, you can also pass parameters that will go to
- * the function. Note that all params must be serializable.
- */
- public function addTask($title) {
- $args = func_get_args();
- $title = array_shift($args);
- $task = new CRM_Queue_Task(
- [get_class($this), '_queueAdapter'],
- $args,
- $title
- );
- return $this->queue->createItem($task, ['weight' => -1]);
- }
-
- // ******** Revision-tracking helpers ********
-
- /**
- * Determine if there are any pending revisions.
- *
- * @return bool
- */
- public function hasPendingRevisions() {
- $revisions = $this->getRevisions();
- $currentRevision = $this->getCurrentRevision();
-
- if (empty($revisions)) {
- return FALSE;
- }
- if (empty($currentRevision)) {
- return TRUE;
- }
-
- return ($currentRevision < max($revisions));
- }
-
- /**
- * Add any pending revisions to the queue.
- *
- * @param CRM_Queue_Queue $queue
- */
- public function enqueuePendingRevisions(CRM_Queue_Queue $queue) {
- $this->queue = $queue;
-
- $currentRevision = $this->getCurrentRevision();
- foreach ($this->getRevisions() as $revision) {
- if ($revision > $currentRevision) {
- $title = E::ts('Upgrade %1 to revision %2', [
- 1 => $this->extensionName,
- 2 => $revision,
- ]);
-
- // note: don't use addTask() because it sets weight=-1
-
- $task = new CRM_Queue_Task(
- [get_class($this), '_queueAdapter'],
- ['upgrade_' . $revision],
- $title
- );
- $this->queue->createItem($task);
-
- $task = new CRM_Queue_Task(
- [get_class($this), '_queueAdapter'],
- ['setCurrentRevision', $revision],
- $title
- );
- $this->queue->createItem($task);
- }
- }
- }
-
- /**
- * Get a list of revisions.
- *
- * @return array
- * revisionNumbers sorted numerically
- */
- public function getRevisions() {
- if (!is_array($this->revisions)) {
- $this->revisions = [];
-
- $clazz = new ReflectionClass(get_class($this));
- $methods = $clazz->getMethods();
- foreach ($methods as $method) {
- if (preg_match('/^upgrade_(.*)/', $method->name, $matches)) {
- $this->revisions[] = $matches[1];
- }
- }
- sort($this->revisions, SORT_NUMERIC);
- }
-
- return $this->revisions;
- }
-
- public function getCurrentRevision() {
- $revision = CRM_Core_BAO_Extension::getSchemaVersion($this->extensionName);
- if (!$revision) {
- $revision = $this->getCurrentRevisionDeprecated();
- }
- return $revision;
- }
-
- private function getCurrentRevisionDeprecated() {
- $key = $this->extensionName . ':version';
- if ($revision = \Civi::settings()->get($key)) {
- $this->revisionStorageIsDeprecated = TRUE;
- }
- return $revision;
- }
-
- public function setCurrentRevision($revision) {
- CRM_Core_BAO_Extension::setSchemaVersion($this->extensionName, $revision);
- // clean up legacy schema version store (CRM-19252)
- $this->deleteDeprecatedRevision();
- return TRUE;
- }
-
- private function deleteDeprecatedRevision() {
- if ($this->revisionStorageIsDeprecated) {
- $setting = new CRM_Core_BAO_Setting();
- $setting->name = $this->extensionName . ':version';
- $setting->delete();
- CRM_Core_Error::debug_log_message("Migrated extension schema revision ID for {$this->extensionName} from civicrm_setting (deprecated) to civicrm_extension.\n");
- }
- }
-
- // ******** Hook delegates ********
-
- /**
- * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_install
- */
- public function onInstall() {
- $files = glob($this->extensionDir . '/sql/*_install.sql');
- if (is_array($files)) {
- foreach ($files as $file) {
- CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file);
- }
- }
- $files = glob($this->extensionDir . '/sql/*_install.mysql.tpl');
- if (is_array($files)) {
- foreach ($files as $file) {
- $this->executeSqlTemplate($file);
- }
- }
- $files = glob($this->extensionDir . '/xml/*_install.xml');
- if (is_array($files)) {
- foreach ($files as $file) {
- $this->executeCustomDataFileByAbsPath($file);
- }
- }
- if (is_callable([$this, 'install'])) {
- $this->install();
- }
- }
-
- /**
- * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_postInstall
- */
- public function onPostInstall() {
- $revisions = $this->getRevisions();
- if (!empty($revisions)) {
- $this->setCurrentRevision(max($revisions));
- }
- if (is_callable([$this, 'postInstall'])) {
- $this->postInstall();
- }
- }
-
- /**
- * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_uninstall
- */
- public function onUninstall() {
- $files = glob($this->extensionDir . '/sql/*_uninstall.mysql.tpl');
- if (is_array($files)) {
- foreach ($files as $file) {
- $this->executeSqlTemplate($file);
- }
- }
- if (is_callable([$this, 'uninstall'])) {
- $this->uninstall();
- }
- $files = glob($this->extensionDir . '/sql/*_uninstall.sql');
- if (is_array($files)) {
- foreach ($files as $file) {
- CRM_Utils_File::sourceSQLFile(CIVICRM_DSN, $file);
- }
- }
- }
-
- /**
- * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable
- */
- public function onEnable() {
- // stub for possible future use
- if (is_callable([$this, 'enable'])) {
- $this->enable();
- }
- }
-
- /**
- * @see https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_disable
- */
- public function onDisable() {
- // stub for possible future use
- if (is_callable([$this, 'disable'])) {
- $this->disable();
- }
- }
-
- public function onUpgrade($op, CRM_Queue_Queue $queue = NULL) {
- switch ($op) {
- case 'check':
- return [$this->hasPendingRevisions()];
-
- case 'enqueue':
- return $this->enqueuePendingRevisions($queue);
-
- default:
- }
- }
-
-}
diff --git a/CRM/Civicase/Upgrader/Steps/Step0019.php b/CRM/Civicase/Upgrader/Steps/Step0019.php
new file mode 100644
index 000000000..14cfd30cf
--- /dev/null
+++ b/CRM/Civicase/Upgrader/Steps/Step0019.php
@@ -0,0 +1,40 @@
+create();
+ (new CaseTypeCategoryManager())->create();
+ (new CaseSalesOrderStatusManager())->create();
+ (new MembershipTypeCustomFieldManager())->create();
+ }
+ catch (\Throwable $th) {
+ \Civi::log()->error('Error upgrading Civicase', [
+ 'context' => [
+ 'backtrace' => $th->getTraceAsString(),
+ 'message' => $th->getMessage(),
+ ],
+ ]);
+ }
+
+ return TRUE;
+ }
+
+}
diff --git a/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php b/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php
new file mode 100644
index 000000000..35eac01d0
--- /dev/null
+++ b/CRM/Civicase/WorkflowMessage/SalesOrderInvoice.php
@@ -0,0 +1,75 @@
+lineItems)) {
+ $result[] = CaseSalesOrderBAO::computeTotal($this->lineItems);
+ }
+ }
+
+}
diff --git a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountInvoicedAction.php b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountInvoicedAction.php
new file mode 100644
index 000000000..d35fa3298
--- /dev/null
+++ b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountInvoicedAction.php
@@ -0,0 +1,39 @@
+salesOrderID) {
+ return;
+ }
+ $service = new \CRM_Civicase_Service_CaseSalesOrderContributionCalculator($this->salesOrderID);
+ $result['amount'] = $service->calculateTotalInvoicedAmount();
+ }
+
+ /**
+ * Sets Sales Order ID.
+ */
+ public function setSalesOrderId(string $salesOrderId) {
+ $this->salesOrderID = $salesOrderId;
+ }
+
+}
diff --git a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountPaidAction.php b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountPaidAction.php
new file mode 100644
index 000000000..7b022ae53
--- /dev/null
+++ b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountPaidAction.php
@@ -0,0 +1,38 @@
+salesOrderID) {
+ return;
+ }
+ $service = new \CRM_Civicase_Service_CaseSalesOrderContributionCalculator($this->salesOrderID);
+ $result['amount'] = $service->calculateTotalPaidAmount();
+ }
+
+ /**
+ * Sets Sales Order ID.
+ */
+ public function setSalesOrderId(string $salesOrderId) {
+ $this->salesOrderID = $salesOrderId;
+ }
+
+}
diff --git a/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php
new file mode 100644
index 000000000..ce7c3e37a
--- /dev/null
+++ b/Civi/Api4/Action/CaseSalesOrder/ContributionCreateAction.php
@@ -0,0 +1,218 @@
+createContribution();
+
+ return $result->exchangeArray($resultArray);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ private function createContribution() {
+ $priceField = $this->getDefaultPriceSetFields();
+ $createdContributionsCount = 0;
+
+ foreach ($this->salesOrderIds as $id) {
+ $transaction = CRM_Core_Transaction::create();
+ try {
+ $contribution = $this->createContributionWithLineItems($id, $priceField);
+ $this->linkCaseSalesOrderToContribution($id, $contribution['id']);
+ $this->updateCaseSalesOrderStatus($id);
+ $createdContributionsCount++;
+ }
+ catch (\Exception $e) {
+ $transaction->rollback();
+ }
+
+ $transaction->commit();
+ }
+
+ return ['created_contributions_count' => $createdContributionsCount];
+ }
+
+ /**
+ * Creates sales order contribution with associated line items.
+ *
+ * @param int $salesOrderId
+ * Sales Order ID.
+ * @param array $priceField
+ * Array of price fields.
+ */
+ private function createContributionWithLineItems(int $salesOrderId, array $priceField): array {
+ $salesOrderContribution = new salesOrderlineItemGenerator($salesOrderId, $this->toBeInvoiced, $this->percentValue ?? 0);
+ $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;
+
+ if (round($totalAmount, 2) < 1) {
+ throw new \Exception("Contribution total amount must be greater than zero");
+ }
+
+ $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'],
+ 'contribution_status_id' => $this->getPendingContributionStatusId(),
+ ];
+
+ return Contribution::create($params)->toArray();
+ }
+
+ /**
+ * Returns default contribution price set fields.
+ *
+ * @return array
+ * Array of price fields
+ */
+ private function getDefaultPriceSetFields(): 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();
+ }
+
+ /**
+ * Links sales order with contirbution.
+ *
+ * @param int $salesOrderId
+ * Sales Order Id.
+ * @param int $contributionId
+ * Contribution ID.
+ */
+ private function linkCaseSalesOrderToContribution(int $salesOrderId, int $contributionId): void {
+ $salesOrder = CaseSalesOrder::get()
+ ->addWhere('id', '=', $salesOrderId)
+ ->execute()
+ ->first();
+
+ Api4Contribution::update()
+ ->addValue('Opportunity_Details.Case_Opportunity', $salesOrder['case_id'])
+ ->addValue('Opportunity_Details.Quotation', $salesOrderId)
+ ->addWhere('id', '=', $contributionId)
+ ->execute();
+ }
+
+ /**
+ * Updates Sales Order status.
+ *
+ * @param int $salesOrderId
+ * Sales Order Id.
+ */
+ private function updateCaseSalesOrderStatus(int $salesOrderId): void {
+ CaseSalesOrder::update()
+ ->addWhere('id', '=', $salesOrderId)
+ ->addValue('status_id', $this->statusId)
+ ->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;
+ }
+
+}
diff --git a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php
new file mode 100644
index 000000000..de525fec8
--- /dev/null
+++ b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php
@@ -0,0 +1,172 @@
+records as &$record) {
+ $record += $this->defaults;
+ $this->formatWriteValues($record);
+ $this->matchExisting($record);
+ if (empty($record['id'])) {
+ $this->fillDefaults($record);
+ $this->fillMandatoryFields($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'];
+ $this->validateLinItemProductPrice($lineItems);
+ $total = CaseSalesOrderBAO::computeTotal($lineItems);
+ $salesOrder['total_before_tax'] = $total['totalBeforeTax'];
+ $salesOrder['total_after_tax'] = $total['totalAfterTax'];
+
+ $saleOrderId = $salesOrder['id'] ?? NULL;
+ $caseSaleOrderContributionService = new \CRM_Civicase_Service_CaseSalesOrderContributionCalculator($saleOrderId);
+ $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);
+
+ $caseSalesOrderLineAPI = CaseSalesOrderLine::save(FALSE);
+ $this->removeStaleLineItems($salesOrder);
+ 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);
+ });
+
+ $result['items'] = $caseSalesOrderLineAPI->execute()->jsonSerialize();
+ }
+
+ $output[] = $result;
+ }
+
+ return $output;
+ }
+ 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');
+
+ if (empty($lineItemsInUse)) {
+ return;
+ }
+
+ CaseSalesOrderLine::delete(FALSE)
+ ->addWhere('sales_order_id', '=', $salesOrder['id'])
+ ->addWhere('id', 'NOT IN', $lineItemsInUse)
+ ->execute();
+ }
+
+ /**
+ * Updates sales order's case opportunity details.
+ *
+ * @param int $salesOrderId
+ * Sales Order Id.
+ */
+ private function updateOpportunityDetails($salesOrderId): void {
+ $caseSalesOrder = CaseSalesOrder::get(FALSE)
+ ->addSelect('case_id')
+ ->addWhere('id', '=', $salesOrderId)
+ ->execute()
+ ->first();
+
+ if (empty($caseSalesOrder) || empty($caseSalesOrder['case_id'])) {
+ return;
+ }
+
+ $caseSaleOrderContributionService = new \CRM_Civicase_Service_CaseSalesOrderOpportunityCalculator($caseSalesOrder['case_id']);
+ $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();
+ }
+
+ /**
+ * 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(FALSE)
+ ->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/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 @@
+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\ComputeTotalAction
+ * returns computed total action
+ */
+ public static function computeTotal($checkPermissions = FALSE) {
+ return (new ComputeTotalAction(__CLASS__, __FUNCTION__))
+ ->setCheckPermissions($checkPermissions);
+ }
+
+ /**
+ * Computes the sum of amount paid.
+ *
+ * @param bool $checkPermissions
+ * Should permission be checked for the user.
+ *
+ * @return ComputeAmountPaidAction
+ * returns computed amount paid action
+ */
+ public static function computeTotalAmountPaid(bool $checkPermissions = FALSE) {
+ return (new ComputeTotalAmountPaidAction(__CLASS__, __FUNCTION__))
+ ->setCheckPermissions($checkPermissions);
+ }
+
+ /**
+ * Computes the sum of amount invoiced.
+ *
+ * @param bool $checkPermissions
+ * Should permission be checked for the user.
+ *
+ * @return ComputeAmountInvoicedAction
+ * returns computed amount invoiced action
+ */
+ public static function computeTotalAmountInvoiced(bool $checkPermissions = FALSE) {
+ return (new ComputeTotalAmountInvoicedAction(__CLASS__, __FUNCTION__))
+ ->setCheckPermissions($checkPermissions);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public static function permissions() {
+ return [
+ 'meta' => ['access CiviCRM'],
+ 'default' => ['access CiviCRM'],
+ ];
+ }
+
+}
diff --git a/Civi/Api4/CaseSalesOrderLine.php b/Civi/Api4/CaseSalesOrderLine.php
new file mode 100644
index 000000000..806fc018b
--- /dev/null
+++ b/Civi/Api4/CaseSalesOrderLine.php
@@ -0,0 +1,26 @@
+ ['access CiviCRM'],
+ 'default' => ['access CiviCRM'],
+ ];
+ }
+
+}
diff --git a/Civi/Utils/CurrencyUtils.php b/Civi/Utils/CurrencyUtils.php
new file mode 100644
index 000000000..f2f15b1b0
--- /dev/null
+++ b/Civi/Utils/CurrencyUtils.php
@@ -0,0 +1,43 @@
+fetch()) {
+ self::$currencies[] = [
+ 'name' => $dao->name,
+ 'symbol' => $dao->symbol,
+ 'format' => CRM_Utils_Money::format(1234.56, $dao->name),
+ ];
+ }
+ }
+
+ return self::$currencies;
+ }
+
+}
diff --git a/ang/afsearchContactQuotations.aff.html b/ang/afsearchContactQuotations.aff.html
new file mode 100644
index 000000000..eda87620a
--- /dev/null
+++ b/ang/afsearchContactQuotations.aff.html
@@ -0,0 +1,25 @@
+
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/afsearchQuotationInvoices.aff.html b/ang/afsearchQuotationInvoices.aff.html
new file mode 100644
index 000000000..44a6bc143
--- /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/afsearchQuotations.aff.html b/ang/afsearchQuotations.aff.html
new file mode 100644
index 000000000..b6d80d2a5
--- /dev/null
+++ b/ang/afsearchQuotations.aff.html
@@ -0,0 +1,28 @@
+
+
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/civicase-features.ang.php b/ang/civicase-features.ang.php
new file mode 100644
index 000000000..6a397ae7b
--- /dev/null
+++ b/ang/civicase-features.ang.php
@@ -0,0 +1,94 @@
+retrieveCaseInstanceWithEnabledFeatures([$feature]);
+ $options['featureCaseTypes'][$feature] = array_keys($caseTypeCategories);
+ }, ['quotations', 'invoices']);
+}
+
+/**
+ * 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 = [
+ 'api4',
+ 'crmUi',
+ 'crmUtil',
+ 'civicase',
+ 'ui.sortable',
+ 'dialogService',
+ 'civicase-base',
+ 'afsearchQuotations',
+ 'afsearchContactQuotations',
+ 'afsearchQuotationInvoices',
+];
+
+return [
+ 'css' => [
+ 'css/*.css',
+ ],
+ 'js' => getFeaturesJsFiles(),
+ 'settings' => $options,
+ 'requires' => $requires,
+ 'partials' => [
+ '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/civicase-features/app.routes.js b/ang/civicase-features/app.routes.js
new file mode 100644
index 000000000..56645284a
--- /dev/null
+++ b/ang/civicase-features/app.routes.js
@@ -0,0 +1,30 @@
+(function (angular, $, _) {
+ var module = angular.module('civicase-features');
+
+ module.config(function ($routeProvider, UrlParametersProvider) {
+ $routeProvider.when('/quotations', {
+ template: function () {
+ var urlParams = UrlParametersProvider.parse(window.location.search);
+ return `
+
+ `;
+ }
+ });
+ $routeProvider.when('/quotations/new', {
+ template: function () {
+ var urlParams = UrlParametersProvider.parse(window.location.search);
+ return `
+
+ `;
+ }
+ });
+ $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-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/directives/invoices-list.directive.html b/ang/civicase-features/invoices/directives/invoices-list.directive.html
new file mode 100644
index 000000000..39c38d16a
--- /dev/null
+++ b/ang/civicase-features/invoices/directives/invoices-list.directive.html
@@ -0,0 +1,22 @@
+
+
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..4ce2265de
--- /dev/null
+++ b/ang/civicase-features/invoices/directives/invoices-list.directive.js
@@ -0,0 +1,31 @@
+(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) {
+ $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/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/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/directives/quotations-contribution-bulk.directive.html b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html
new file mode 100644
index 000000000..8cdb5982e
--- /dev/null
+++ b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html
@@ -0,0 +1,103 @@
+
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..3dd0a7dcf
--- /dev/null
+++ b/ang/civicase-features/quotations/directives/quotations-contribution-bulk.directive.js
@@ -0,0 +1,75 @@
+(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: null,
+ statusId: null,
+ financialTypeId: null,
+ date: $.datepicker.formatDate('yy-mm-dd', new Date())
+ };
+ ctrl.salesOrderStatus = SalesOrderStatus.getAll();
+ this.progress = null;
+ const BATCH_SIZE = 50;
+
+ (function () {
+ CaseUtils.getSalesOrderAndLineItems(ctrl.ids[0]).then((result) => {
+ ctrl.data.financialTypeId = result.items[0].financial_type_id ?? null;
+ ctrl.data.statusId = Number(result.status_id).toString();
+ });
+ })();
+
+ this.createBulkContribution = () => {
+ $q(async function (resolve, reject) {
+ ctrl.run = true;
+ ctrl.progress = 0;
+ let contributionCreated = 0;
+ let index = 0;
+ const chunkedIds = _.chunk(ctrl.ids, BATCH_SIZE);
+ for (const salesOrderIds of chunkedIds) {
+ try {
+ const result = await crmApi4('CaseSalesOrder', 'contributionCreateAction', { ...ctrl.data, salesOrderIds });
+ contributionCreated += result.created_contributions_count ?? 0;
+ } catch (error) {
+ console.log(error);
+ } finally {
+ index += BATCH_SIZE;
+ ctrl.progress = (index * 100) / ctrl.ids.length;
+ }
+ }
+
+ ctrl.run = false;
+ ctrl.close();
+ 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');
+ });
+ };
+ }
+})(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..ee144337f
--- /dev/null
+++ b/ang/civicase-features/quotations/directives/quotations-create.directive.html
@@ -0,0 +1,253 @@
+
+
{{ ts(isUpdate ? 'Edit Quotation':'Create Quotation') }}
+
+
+
+
+
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..b6721466e
--- /dev/null
+++ b/ang/civicase-features/quotations/directives/quotations-create.directive.js
@@ -0,0 +1,447 @@
+(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} $location the location service
+ * @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
+ * @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, 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;
+ $scope.roundTo = roundTo;
+ $scope.formatMoney = formatMoney;
+ $scope.submitInProgress = false;
+ $scope.caseApiParam = caseApiParam;
+ $scope.saveQuotation = saveQuotation;
+ $scope.calculateSubtotal = calculateSubtotal;
+ $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;
+ $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'),
+ clientId: null,
+ 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,
+ case_id: $scope.defaultCaseId
+ };
+ $scope.total = 0;
+ $scope.taxRates = [];
+
+ setDefaultClientID();
+ prefillSalesOrderForUpdate();
+ }
+
+ /**
+ * Pre-fill sales order when updating sales order.
+ */
+ function prefillSalesOrderForUpdate () {
+ 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.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');
+ });
+ }
+
+ /**
+ * Sets client ID to case client.
+ */
+ function setDefaultClientID () {
+ if (!$scope.defaultCaseId || $scope.isUpdate) {
+ return;
+ }
+
+ 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;
+ if ($scope.salesOrder.client_id) {
+ handleClientChange();
+ }
+ }
+ });
+ }
+
+ /**
+ * Removes a sales order line item
+ *
+ * @param {number} index element index to be removed
+ */
+ function removeSalesOrderItem (index) {
+ $scope.salesOrder.items.splice(index, 1);
+ $scope.$emit('totalChange');
+ }
+
+ /**
+ * 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: $scope.membeshipTypesProductDiscountPercentage,
+ 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) {
+ 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) {
+ $scope.salesOrder.items[index]['product_id.name'] = '';
+ 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);
+ const financialTypeId = productsCache.get(productId).financial_type_id ?? null;
+ if (financialTypeId) {
+ $scope.salesOrder.items[index].financial_type_id = financialTypeId;
+ handleFinancialTypeChange(index);
+ }
+ calculateSubtotal(index);
+ };
+
+ const productId = $scope.salesOrder.items[index].product_id;
+ getProduct(productId).then(function (result) {
+ if (result) {
+ updateProductDependentFields(productId);
+ }
+ });
+ }
+
+ /**
+ * Applies any membership type product discounts to
+ * each sale order line item if the selected client has any memberships.
+ */
+ function handleClientChange () {
+ const clientID = $scope.salesOrder.client_id;
+ crmApi4('Membership', 'get', {
+ select: ['membership_type_id.Product_Discounts.Product_Discount_Amount'],
+ where: [['contact_id', '=', clientID], ['status_id.is_current_member', '=', true]]
+ }).then(function (results) {
+ if (!results || results.length < 1) {
+ return;
+ }
+ let discountPercentage = 0;
+ results.forEach((membership) => {
+ discountPercentage += membership['membership_type_id.Product_Discounts.Product_Discount_Amount'];
+ });
+ // make sure that the discount percentage cannot be more than 100%
+ if (discountPercentage > 100) {
+ discountPercentage = 100;
+ }
+ $scope.membeshipTypesProductDiscountPercentage = discountPercentage;
+ applySaleOrderItemPencentageDiscount();
+ CRM.alert(ts('Automatic Members Discount Applied'), ts('Product Discount'), 'success');
+ });
+ }
+
+ /**
+ * Applies Membership Type discounted percentage to
+ * each sale order item discounted percentage.
+ */
+ function applySaleOrderItemPencentageDiscount () {
+ $scope.salesOrder.items.forEach((item) => {
+ item.discounted_percentage = item.discounted_percentage + $scope.membeshipTypesProductDiscountPercentage;
+ });
+ }
+
+ /**
+ * 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');
+
+ 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');
+ };
+
+ 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');
+ validateProductPrice(index);
+ }
+
+ /**
+ * Ensures the product price doesnt exceed the max price
+ *
+ * @param {number} index index of the sales order line item
+ */
+ async function validateProductPrice (index) {
+ const productId = $scope.salesOrder.items[index].product_id;
+ if (!productId) {
+ return;
+ }
+
+ const product = await getProduct(productId);
+ const shouldCompareCost = product && product.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);
+ }
+ }
+
+ /**
+ * 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 () {
+ const msg = !$scope.isUpdate ? 'Your Quotation has been generated successfully.' : 'Details updated successfully';
+ CRM.alert(msg, 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.isUpdate) {
+ $window.location.href = $window.document.referrer;
+ return;
+ }
+
+ if (!$scope.salesOrder.case_id) {
+ $window.location.href = 'a#/quotations';
+ }
+
+ CaseUtils.getDashboardLink($scope.salesOrder.case_id).then(link => {
+ $window.location.href = `${link}&tab=Quotations`;
+ });
+ }
+
+ /**
+ * @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
+ });
+ }
+
+ /**
+ * 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));
+ }
+
+ /**
+ * Get product by ID
+ *
+ * @param {number} productId id of product to fetch
+ *
+ * @returns {Promise} product
+ */
+ async function getProduct (productId) {
+ let product = null;
+ if (productsCache.has(productId)) {
+ return productsCache.get(productId);
+ }
+
+ try {
+ const result = await civicaseCrmApi('Product', 'get', { id: productId });
+ if (result.count > 0) {
+ product = result.values[productId];
+ productsCache.set(productId, product);
+ }
+ } catch (error) {
+ return null;
+ }
+
+ return product;
+ }
+ }
+})(angular, CRM.$, CRM._);
diff --git a/ang/civicase-features/quotations/directives/quotations-discount.directive.html b/ang/civicase-features/quotations/directives/quotations-discount.directive.html
new file mode 100644
index 000000000..596b1fa6e
--- /dev/null
+++ b/ang/civicase-features/quotations/directives/quotations-discount.directive.html
@@ -0,0 +1,51 @@
+
+
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/ang/civicase-features/quotations/directives/quotations-list.directive.html b/ang/civicase-features/quotations/directives/quotations-list.directive.html
new file mode 100644
index 000000000..e8327d827
--- /dev/null
+++ b/ang/civicase-features/quotations/directives/quotations-list.directive.html
@@ -0,0 +1,41 @@
+
+
diff --git a/ang/civicase-features/quotations/directives/quotations-list.directive.js b/ang/civicase-features/quotations/directives/quotations-list.directive.js
new file mode 100644
index 000000000..1d414ff7a
--- /dev/null
+++ b/ang/civicase-features/quotations/directives/quotations-list.directive.js
@@ -0,0 +1,67 @@
+(function (angular, _, $) {
+ var module = angular.module('civicase-features');
+
+ module.directive('quotationsList', function () {
+ return {
+ restrict: 'E',
+ controller: 'quotationsListController',
+ templateUrl: '~/civicase-features/quotations/directives/quotations-list.directive.html',
+ scope: {
+ view: '@',
+ contactId: '@'
+ }
+ };
+ });
+
+ module.controller('quotationsListController', quotationsListController);
+
+ /**
+ * @param {object} $scope the controller scope
+ * @param {object} $location the location service
+ * @param {object} $window window object of the browser
+ */
+ function quotationsListController ($scope, $location, $window) {
+ $scope.redirectToQuotationCreationScreen = redirectToQuotationCreationScreen;
+
+ (function init () {
+ addEventToElementsWhenInDOMTree();
+ }());
+
+ /**
+ * Redirects user to new quotation screen
+ */
+ function redirectToQuotationCreationScreen () {
+ let url = CRM.url('/case-features/a#/quotations/new');
+ const caseId = $location.search().caseId;
+ if (caseId) {
+ url += `?caseId=${caseId}`;
+ }
+
+ $window.location.href = url;
+ }
+
+ /**
+ * Add events to elements that are occasionally removed from DOM tree
+ */
+ 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, {
+ childList: true,
+ subtree: true
+ });
+ }
+ }
+})(angular, CRM._, CRM.$);
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..61ffba7a9
--- /dev/null
+++ b/ang/civicase-features/quotations/directives/quotations-view.directive.html
@@ -0,0 +1,177 @@
+
+
{{ ts('View Quotation') }}
+
+
+
+
+
+
+
+
+
+
+ Quotation 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}}
+
+
+ Invoicing
+ {{salesOrder['invoicing_status_id:label']}}
+
+
+ Payments
+ {{salesOrder['payment_status_id:label']}}
+
+
+
+
+
+
+
+
+
Items Overview
+
+
+
+
+ Product
+ Item Description
+ Financial Type
+ Unit Price
+ Quantity
+ Discount %
+ 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}}
+
+
+
+
+
+
+
+
+
+
+
+ Total Amount Invoiced
+ {{ currencySymbol }} {{ salesOrder.totalAmountInvoiced.amount }}
+
+
+ Total Amount Paid
+ {{ currencySymbol }} {{salesOrder.totalAmountPaid.amount}}
+
+
+
+
+
+
+
+
+ {{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..a285eaf23
--- /dev/null
+++ b/ang/civicase-features/quotations/directives/quotations-view.directive.js
@@ -0,0 +1,85 @@
+(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;
+ $scope.getContactLink = getContactLink;
+
+ (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', 'invoicing_status_id:label', 'payment_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' }],
+ totalAmountInvoiced: ['CaseSalesOrder', 'computeTotalAmountInvoiced', { salesOrderId: '$id' }],
+ totalAmountPaid: ['CaseSalesOrder', 'computeTotalAmountPaid', { salesOrderId: '$id' }]
+ }
+ }).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);
+ $scope.salesOrder.quotation_date = CRM.utils.formatDate($scope.salesOrder.quotation_date);
+ if (!$scope.salesOrder.case_id) {
+ return;
+ }
+ $scope.hasCase = true;
+ CaseUtils.getDashboardLink($scope.salesOrder.case_id).then(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._);
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-features/shared/configs/add-features-tab.config.js b/ang/civicase-features/shared/configs/add-features-tab.config.js
new file mode 100644
index 000000000..366fa75a4
--- /dev/null
+++ b/ang/civicase-features/shared/configs/add-features-tab.config.js
@@ -0,0 +1,58 @@
+(function (angular) {
+ const module = angular.module('civicase-features');
+
+ module.config(function ($windowProvider, tsProvider, CaseDetailsTabsProvider) {
+ const $window = $windowProvider.$get();
+ const ts = tsProvider.$get();
+ const featuresTab = [
+ {
+ name: 'Quotations',
+ label: ts('Quotations'),
+ weight: 100
+ }, {
+ name: 'Invoices',
+ label: ts('Invoices'),
+ weight: 110
+ }
+ ];
+
+ featuresTab.forEach(feature => {
+ if (caseTypeCategoryHasFeatureEnabled(feature.name)) {
+ CaseDetailsTabsProvider.addTabs([feature]);
+ }
+ });
+
+ /**
+ * 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 () {
+ const urlParamRegExp = /case_type_category=([^&]+)/i;
+ const currentSearch = decodeURIComponent($window.location.search);
+ const results = urlParamRegExp.exec(currentSearch);
+
+ return results && results[1];
+ }
+
+ /**
+ * 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 caseTypeCategoryHasFeatureEnabled (feature) {
+ const caseTypeCategory = parseInt(getCaseTypeCategory());
+ const quotationCaseTypeCategories = CRM['civicase-features'].featureCaseTypes[feature.toLocaleLowerCase()] || [];
+ if (Array.isArray(quotationCaseTypeCategories) && caseTypeCategory) {
+ return quotationCaseTypeCategories.includes(caseTypeCategory);
+ }
+
+ return false;
+ }
+ });
+})(angular);
diff --git a/ang/civicase-features/shared/services/case-utils.service.js b/ang/civicase-features/shared/services/case-utils.service.js
new file mode 100644
index 000000000..e8ed12dfc
--- /dev/null
+++ b/ang/civicase-features/shared/services/case-utils.service.js
@@ -0,0 +1,55 @@
+(function (angular, $, _, CRM) {
+ var module = angular.module('civicase-features');
+
+ module.service('CaseUtils', CaseUtils);
+
+ /**
+ * CaseUtils Service
+ *
+ * @param {object} $q ng promise object
+ * @param {object} crmApi4 api V4 service
+ * @param {Function} civicaseCrmApi civicrm api service
+ */
+ function CaseUtils ($q, crmApi4, civicaseCrmApi) {
+ /**
+ * Gets the link to a case dashboard
+ *
+ * @param {number} id ID of the case
+ * @returns {Promise} promise object
+ */
+ 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);
+ });
+ });
+ };
+
+ /**
+ * Retrieves the sales order and its line items from API
+ *
+ * @param {number} salesOrderId ID of the sales order to retrieve
+ * @returns {Promise} promise object
+ */
+ this.getSalesOrderAndLineItems = function (salesOrderId) {
+ return $q(function (resolve, reject) {
+ crmApi4('CaseSalesOrder', 'get', {
+ select: ['*', 'case_sales_order_line.*'],
+ where: [['id', '=', salesOrderId]],
+ limit: 1,
+ chain: { items: ['CaseSalesOrderLine', 'get', { where: [['sales_order_id', '=', '$id']], select: ['*', 'product_id.name', 'financial_type_id.name'] }] }
+ }).then(async function (caseSalesOrders) {
+ if (Array.isArray(caseSalesOrders) && caseSalesOrders.length > 0) {
+ const salesOrder = caseSalesOrders.pop();
+ resolve(salesOrder);
+ }
+ });
+ });
+ };
+ }
+})(angular, CRM.$, CRM._, CRM);
diff --git a/ang/civicase-features/shared/services/currency-codes.service.js b/ang/civicase-features/shared/services/currency-codes.service.js
new file mode 100644
index 000000000..fa467f2ab
--- /dev/null
+++ b/ang/civicase-features/shared/services/currency-codes.service.js
@@ -0,0 +1,28 @@
+(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 || '£';
+ };
+
+ this.getFormat = function (name) {
+ return CRM['civicase-features']
+ .currencyCodes
+ .filter(currency => currency.name === name)
+ .pop().format || null;
+ };
+ }
+})(angular, CRM.$, CRM._, CRM);
diff --git a/ang/civicase-features/shared/services/feature-case-types.service.js b/ang/civicase-features/shared/services/feature-case-types.service.js
new file mode 100644
index 000000000..d2135b229
--- /dev/null
+++ b/ang/civicase-features/shared/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/shared/services/sales-order-status.service.js b/ang/civicase-features/shared/services/sales-order-status.service.js
new file mode 100644
index 000000000..fa3d2983e
--- /dev/null
+++ b/ang/civicase-features/shared/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);
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..463bb1cc4
--- /dev/null
+++ b/ang/civicase/shared/directives/history-back.directive.js
@@ -0,0 +1,20 @@
+(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();
+ const currPage = window.location.href;
+ setTimeout(function () {
+ if ($window.location.href === currPage) {
+ $window.close();
+ }
+ }, 500);
+ });
+ }
+ };
+ });
+})(angular, window);
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']);
}
diff --git a/api/v3/CaseSalesOrder/Get.php b/api/v3/CaseSalesOrder/Get.php
new file mode 100644
index 000000000..f31813127
--- /dev/null
+++ b/api/v3/CaseSalesOrder/Get.php
@@ -0,0 +1,44 @@
+ $field) {
+ $spec[$fieldname] = $field;
+ }
+}
diff --git a/api/v3/CaseSalesOrder/Getlist.php b/api/v3/CaseSalesOrder/Getlist.php
new file mode 100644
index 000000000..612cc9499
--- /dev/null
+++ b/api/v3/CaseSalesOrder/Getlist.php
@@ -0,0 +1,88 @@
+ 3,
+ 'entity' => 'CaseSalesOrder',
+ 'action' => 'getlist',
+ 'params' => $params,
+ ];
+
+ return civicrm_api3_generic_getList($apiRequest);
+}
+
+/**
+ * Get case sales order list output.
+ *
+ * @param array $result
+ * API Result to format.
+ * @param array $request
+ * API Request.
+ *
+ * @return array
+ * API Result
+ *
+ * @see _civicrm_api3_generic_getlist_output
+ */
+function _civicrm_api3_case_sales_order_getlist_output($result, $request) {
+ $output = [];
+ if (!empty($result['values'])) {
+ foreach ($result['values'] as $row) {
+ $caseSalesOrder = CaseSalesOrder::get()
+ ->addSelect('contact.display_name', 'quotation_date')
+ ->addJoin('Contact AS contact', 'LEFT', ['contact.id', '=', 'client_id'])
+ ->addWhere('id', '=', $row['id'])
+ ->execute()
+ ->first();
+
+ $data = [
+ 'id' => $row[$request['id_field']],
+ 'label' => "ID: {$caseSalesOrder['id']}, Client: {$caseSalesOrder['contact.display_name']}",
+ 'description' => [
+ strip_tags($row['description']),
+ ],
+ ];
+ $data['description'][] = "Quotation Date: " . CRM_Utils_Date::customFormat($caseSalesOrder['quotation_date']);
+ $output[] = $data;
+ }
+ }
+ return $output;
+}
diff --git a/civicase.civix.php b/civicase.civix.php
index c1e8efe5b..5a4fb9350 100644
--- a/civicase.civix.php
+++ b/civicase.civix.php
@@ -24,7 +24,7 @@ class CRM_Civicase_ExtensionUtil {
* Translated text.
* @see ts
*/
- public static function ts($text, $params = []) {
+ public static function ts($text, $params = []): string {
if (!array_key_exists('domain', $params)) {
$params['domain'] = [self::LONG_NAME, NULL];
}
@@ -41,7 +41,7 @@ public static function ts($text, $params = []) {
* Ex: 'http://example.org/sites/default/ext/org.example.foo'.
* Ex: 'http://example.org/sites/default/ext/org.example.foo/css/foo.css'.
*/
- public static function url($file = NULL) {
+ public static function url($file = NULL): string {
if ($file === NULL) {
return rtrim(CRM_Core_Resources::singleton()->getUrl(self::LONG_NAME), '/');
}
@@ -84,40 +84,17 @@ public static function findClass($suffix) {
*
* @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_config
*/
-function _civicase_civix_civicrm_config(&$config = NULL) {
+function _civicase_civix_civicrm_config($config = NULL) {
static $configured = FALSE;
if ($configured) {
return;
}
$configured = TRUE;
- $template =& CRM_Core_Smarty::singleton();
-
- $extRoot = dirname(__FILE__) . DIRECTORY_SEPARATOR;
- $extDir = $extRoot . 'templates';
-
- if (is_array($template->template_dir)) {
- array_unshift($template->template_dir, $extDir);
- }
- else {
- $template->template_dir = [$extDir, $template->template_dir];
- }
-
+ $extRoot = __DIR__ . DIRECTORY_SEPARATOR;
$include_path = $extRoot . PATH_SEPARATOR . get_include_path();
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;
- }
+ // Based on , this does not currently require mixin/polyfill.php.
}
/**
@@ -127,35 +104,7 @@ function _civicase_civix_civicrm_xmlMenu(&$files) {
*/
function _civicase_civix_civicrm_install() {
_civicase_civix_civicrm_config();
- if ($upgrader = _civicase_civix_upgrader()) {
- $upgrader->onInstall();
- }
-}
-
-/**
- * Implements hook_civicrm_postInstall().
- *
- * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_postInstall
- */
-function _civicase_civix_civicrm_postInstall() {
- _civicase_civix_civicrm_config();
- if ($upgrader = _civicase_civix_upgrader()) {
- if (is_callable([$upgrader, 'onPostInstall'])) {
- $upgrader->onPostInstall();
- }
- }
-}
-
-/**
- * Implements hook_civicrm_uninstall().
- *
- * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_uninstall
- */
-function _civicase_civix_civicrm_uninstall() {
- _civicase_civix_civicrm_config();
- if ($upgrader = _civicase_civix_upgrader()) {
- $upgrader->onUninstall();
- }
+ // Based on , this does not currently require mixin/polyfill.php.
}
/**
@@ -163,212 +112,9 @@ function _civicase_civix_civicrm_uninstall() {
*
* @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_enable
*/
-function _civicase_civix_civicrm_enable() {
- _civicase_civix_civicrm_config();
- if ($upgrader = _civicase_civix_upgrader()) {
- if (is_callable([$upgrader, 'onEnable'])) {
- $upgrader->onEnable();
- }
- }
-}
-
-/**
- * (Delegated) Implements hook_civicrm_disable().
- *
- * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_disable
- * @return mixed
- */
-function _civicase_civix_civicrm_disable() {
+function _civicase_civix_civicrm_enable(): void {
_civicase_civix_civicrm_config();
- if ($upgrader = _civicase_civix_upgrader()) {
- if (is_callable([$upgrader, 'onDisable'])) {
- $upgrader->onDisable();
- }
- }
-}
-
-/**
- * (Delegated) Implements hook_civicrm_upgrade().
- *
- * @param $op string, the type of operation being performed; 'check' or 'enqueue'
- * @param $queue CRM_Queue_Queue, (for 'enqueue') the modifiable list of pending up upgrade tasks
- *
- * @return mixed
- * based on op. for 'check', returns array(boolean) (TRUE if upgrades are pending)
- * for 'enqueue', returns void
- *
- * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_upgrade
- */
-function _civicase_civix_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) {
- if ($upgrader = _civicase_civix_upgrader()) {
- return $upgrader->onUpgrade($op, $queue);
- }
-}
-
-/**
- * @return CRM_Civicase_Upgrader
- */
-function _civicase_civix_upgrader() {
- if (!file_exists(__DIR__ . '/CRM/Civicase/Upgrader.php')) {
- return NULL;
- }
- else {
- return CRM_Civicase_Upgrader_Base::instance();
- }
-}
-
-/**
- * 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 : [];
+ // Based on , this does not currently require mixin/polyfill.php.
}
/**
@@ -387,8 +133,8 @@ function _civicase_civix_insert_navigation_menu(&$menu, $path, $item) {
if (empty($path)) {
$menu[] = [
'attributes' => array_merge([
- 'label' => CRM_Utils_Array::value('name', $item),
- 'active' => 1,
+ 'label' => $item['name'] ?? NULL,
+ 'active' => 1,
], $item),
];
return TRUE;
@@ -452,37 +198,3 @@ 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().
- *
- * Find any *.entityType.php files, merge their content, and return.
- *
- * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes
- */
-function _civicase_civix_civicrm_entityTypes(&$entityTypes) {
- $entityTypes = array_merge($entityTypes, [
- 'CRM_Civicase_DAO_CaseCategoryInstance' => [
- 'name' => 'CaseCategoryInstance',
- 'class' => 'CRM_Civicase_DAO_CaseCategoryInstance',
- 'table' => 'civicrm_case_category_instance',
- ],
- 'CRM_Civicase_DAO_CaseContactLock' => [
- 'name' => 'CaseContactLock',
- 'class' => 'CRM_Civicase_DAO_CaseContactLock',
- 'table' => 'civicase_contactlock',
- ],
- ]);
-}
diff --git a/civicase.php b/civicase.php
index aa381a5b8..44e527c15 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']);
}
}
@@ -79,15 +79,16 @@ function civicase_civicrm_config(&$config) {
'hook_civicrm_buildAsset',
['CRM_Civicase_Event_Listener_AssetBuilder', 'addWordReplacements']
);
-}
-/**
- * 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);
+ Civi::dispatcher()->addListener(
+ 'civi.token.list',
+ ['CRM_Civicase_Hook_Tokens_SalesOrderTokens', 'listSalesOrderTokens']
+ );
+
+ Civi::dispatcher()->addListener(
+ 'civi.token.eval',
+ ['CRM_Civicase_Hook_Tokens_SalesOrderTokens', 'evaluateSalesOrderTokens']
+ );
}
/**
@@ -99,24 +100,6 @@ function civicase_civicrm_install() {
_civicase_civix_civicrm_install();
}
-/**
- * Implements hook_civicrm_postInstall().
- *
- * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_postInstall
- */
-function civicase_civicrm_postInstall() {
- _civicase_civix_civicrm_postInstall();
-}
-
-/**
- * Implements hook_civicrm_uninstall().
- *
- * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_uninstall
- */
-function civicase_civicrm_uninstall() {
- _civicase_civix_civicrm_uninstall();
-}
-
/**
* Implements hook_civicrm_enable().
*
@@ -126,61 +109,6 @@ function civicase_civicrm_enable() {
_civicase_civix_civicrm_enable();
}
-/**
- * Implements hook_civicrm_disable().
- *
- * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_disable
- */
-function civicase_civicrm_disable() {
- _civicase_civix_civicrm_disable();
-}
-
-/**
- * Implements hook_civicrm_upgrade().
- *
- * @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_upgrade
- */
-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 +124,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().
*/
@@ -230,6 +149,12 @@ 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(),
+ new CRM_Civicase_Hook_BuildForm_AddQuotationsNotesToContributionSettings(),
+ new CRM_Civicase_Hook_BuildForm_AddSalesOrderLineItemsToContribution(),
+ new CRM_Civicase_Hook_BuildForm_AddEntityReferenceToCustomField(),
+ new CRM_Civicase_Hook_BuildForm_AttachQuotationToInvoiceMail(),
+ new CRM_Civicase_Hook_BuildForm_RefreshInvoiceListOnUpdate(),
];
foreach ($hooks as $hook) {
@@ -300,6 +225,22 @@ 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_PDF' && $isSearchKit) {
+ $form->add('hidden', 'mail_task_from_sk', $isSearchKit);
+ }
+
+ 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)));
+ }
}
/**
@@ -323,6 +264,8 @@ function civicase_civicrm_validateForm($formName, &$fields, &$files, &$form, &$e
*/
function civicase_civicrm_post($op, $objectName, $objectId, &$objectRef) {
$hooks = [
+ new CRM_Civicase_Hook_Post_CaseSalesOrderPayment(),
+ new CRM_Civicase_Hook_Post_CreateSalesOrderContribution(),
new CRM_Civicase_Hook_Post_PopulateCaseCategoryForCaseType(),
new CRM_Civicase_Hook_Post_CaseCategoryCustomGroupSaver(),
new CRM_Civicase_Hook_Post_UpdateCaseTypeListForCaseCategoryCustomGroup(),
@@ -333,6 +276,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().
*/
@@ -346,6 +302,8 @@ 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(),
+ new CRM_Civicase_Hook_PostProcess_SaveQuotationsNotesSettings(),
];
foreach ($hooks as $hook) {
@@ -356,6 +314,15 @@ function civicase_civicrm_postProcess($formName, &$form) {
$api = civicrm_api3('Case', 'getdetails', ['check_permissions' => 1] + $form->civicase_reload);
$form->ajaxResponse['civicase_reload'] = $api['values'];
}
+
+ if (
+ in_array($formName, [
+ 'CRM_Contribute_Form_Task_Invoice', 'CRM_Contribute_Form_Task_PDF',
+ ])
+ && !empty($form->getVar('_submitValues')['mail_task_from_sk'])
+ ) {
+ CRM_Utils_System::redirect($_SERVER['HTTP_REFERER']);
+ }
}
/**
@@ -462,6 +429,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) {
@@ -490,7 +458,6 @@ function civicase_civicrm_selectWhereClause($entity, &$clauses) {
* Implements hook_civicrm_entityTypes().
*/
function civicase_civicrm_entityTypes(&$entityTypes) {
- _civicase_civix_civicrm_entityTypes($entityTypes);
_civicase_add_case_category_case_type_entity($entityTypes);
}
@@ -594,9 +561,64 @@ function _civicase_add_case_category_case_type_entity(array &$entityTypes) {
function civicase_civicrm_alterMailParams(&$params, $context) {
$hooks = [
new CRM_Civicase_Hook_alterMailParams_SubjectCaseTypeCategoryProcessor(),
+ new CRM_Civicase_Hook_alterMailParams_AttachQuotation(),
];
foreach ($hooks as &$hook) {
$hook->run($params, $context);
}
}
+
+/**
+ * Implements hook_civicrm_searchKitTasks().
+ */
+function civicase_civicrm_searchKitTasks(array &$tasks, bool $checkPermissions, ?int $userID) {
+ 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'),
+ 'uiDialog' => ['templateUrl' => '~/civicase-features/quotations/directives/quotations-discount.directive.html'],
+ ];
+
+ $actions['create_contribution'] = [
+ 'module' => 'civicase-features',
+ 'icon' => 'fa-credit-card',
+ 'title' => ts('Create Contribution(s)'),
+ 'uiDialog' => ['templateUrl' => '~/civicase-features/quotations/directives/quotations-contribution-bulk.directive.html'],
+ ];
+
+ $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',
+ ];
+
+ foreach ($tasks as &$task) {
+ if ($task['class'] === 'CRM_Contribute_Form_Task_PDF') {
+ $task['url'] .= '&sk=1';
+ }
+ }
+ }
+}
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/info.xml b/info.xml
index 731ac3434..fb74e2dbe 100644
--- a/info.xml
+++ b/info.xml
@@ -25,13 +25,25 @@
CiviCRM 5.51.1
-
+
+
CRM/Civicase
+ 23.02.1
org.civicrm.shoreditch
uk.co.compucorp.usermenu
+ org.civicrm.afform
+
+ ang-php@1.0.0
+ menu-xml@1.0.0
+ mgd-php@1.0.0
+ setting-php@1.0.0
+ smarty-v2@1.0.1
+ entity-types-php@1.0.0
+
+ CRM_Civicase_Upgrader
diff --git a/js/contribution-entityref-field.js b/js/contribution-entityref-field.js
new file mode 100644
index 000000000..8d0922353
--- /dev/null
+++ b/js/contribution-entityref-field.js
@@ -0,0 +1,30 @@
+(function ($, _) {
+ window.waitForElement = function ($, elementPath, callBack) {
+ (new window.MutationObserver(function () {
+ callBack($, $(elementPath));
+ })).observe(document.querySelector(elementPath), {
+ attributes: true
+ });
+ };
+
+ $(document).one('crmLoad', function () {
+ const entityRefCustomFields = Object.values(CRM.vars.civicase.entityRefCustomFields ?? {});
+
+ /* eslint-disable no-undef */
+ waitForElement($, '#customData', function ($, elem) {
+ entityRefCustomFields.forEach(field => {
+ $(`[name^=${field.name}_]`)
+ .attr('placeholder', field.placeholder)
+ .attr('disabled', false)
+ .crmEntityRef({
+ entity: field.entity,
+ create: false
+ });
+
+ if (field.value) {
+ $(`[name^=${field.name}_]`).val(field.value).trigger('change');
+ }
+ });
+ });
+ });
+})(CRM.$, CRM._);
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/js/sales-order-contribution.js b/js/sales-order-contribution.js
new file mode 100644
index 000000000..5de753872
--- /dev/null
+++ b/js/sales-order-contribution.js
@@ -0,0 +1,84 @@
+(function ($, _) {
+ const waitForElement = function ($, elementPath, callBack) {
+ (new window.MutationObserver(function () {
+ callBack($, $(elementPath));
+ })).observe(document.querySelector(elementPath), {
+ attributes: true
+ });
+ };
+
+ $(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);
+ const caseCustomField = params.case_custom_field;
+ const quotationCustomField = params.quotation_custom_field;
+ 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').hide();
+
+ waitForElement($, '#customData', function ($, elem) {
+ $(`[name^=${caseCustomField}_]`).val(caseSalesOrder.case_id).trigger('change');
+ $(`[name^=${quotationCustomField}_]`).val(caseSalesOrder.id).trigger('change');
+ });
+ });
+ }
+
+ /**
+ * @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');
+ 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);
+ $('input[id^="item_qty"]', row).val(quantity);
+
+ const total = quantity * parseFloat(unitPrice);
+
+ $('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(taxAmount);
+
+ count++;
+ }
+ });
+})(CRM.$, CRM._);
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,
+ ],
+ ],
+ ],
+];
diff --git a/managed/CustomGroup_Contribution_OpportunityDetails.mgd.php b/managed/CustomGroup_Contribution_OpportunityDetails.mgd.php
new file mode 100644
index 000000000..69534234a
--- /dev/null
+++ b/managed/CustomGroup_Contribution_OpportunityDetails.mgd.php
@@ -0,0 +1,117 @@
+ 'CustomGroup_Opportunity_Details',
+ 'entity' => 'CustomGroup',
+ 'cleanup' => 'always',
+ 'update' => 'unmodified',
+ 'params' => [
+ 'version' => 4,
+ 'values' => [
+ 'name' => 'Opportunity_Details',
+ 'title' => 'Opportunity Details',
+ 'extends' => 'Contribution',
+ 'extends_entity_column_value' => NULL,
+ 'style' => 'Inline',
+ 'collapse_display' => TRUE,
+ 'help_pre' => 'If using CiviProspect you can link this contribution to a prospect, quotation or both below.
',
+ 'help_post' => '',
+ 'weight' => 34,
+ 'is_active' => TRUE,
+ 'is_multiple' => FALSE,
+ 'min_multiple' => NULL,
+ 'max_multiple' => NULL,
+ 'collapse_adv_display' => TRUE,
+ 'created_date' => '2023-09-12 13:08:59',
+ 'is_reserved' => FALSE,
+ 'is_public' => FALSE,
+ 'icon' => '',
+ 'extends_entity_column_id' => NULL,
+ ],
+ ],
+ ],
+ [
+ 'name' => 'CustomGroup_Opportunity_Details_CustomField_Case_Opportunity',
+ 'entity' => 'CustomField',
+ 'cleanup' => 'always',
+ 'update' => 'unmodified',
+ 'params' => [
+ 'version' => 4,
+ 'values' => [
+ 'custom_group_id.name' => 'Opportunity_Details',
+ 'name' => 'Case_Opportunity',
+ 'label' => 'Case/Opportunity',
+ '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' => FALSE,
+ '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' => 'case_opportunity_51',
+ 'serialize' => 0,
+ 'filter' => NULL,
+ 'in_selector' => FALSE,
+ ],
+ ],
+ ],
+ [
+ 'name' => 'CustomGroup_Opportunity_Details_CustomField_Quotation',
+ 'entity' => 'CustomField',
+ 'cleanup' => 'always',
+ 'update' => 'unmodified',
+ 'params' => [
+ 'version' => 4,
+ 'values' => [
+ 'custom_group_id.name' => 'Opportunity_Details',
+ 'name' => 'Quotation',
+ 'label' => 'Quotation',
+ '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' => FALSE,
+ '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' => 'quotation_52',
+ 'serialize' => 0,
+ 'filter' => NULL,
+ 'in_selector' => FALSE,
+ ],
+ ],
+ ],
+];
diff --git a/managed/CustomGroup_Product_Discounts.mgd.php b/managed/CustomGroup_Product_Discounts.mgd.php
new file mode 100644
index 000000000..664d3fe72
--- /dev/null
+++ b/managed/CustomGroup_Product_Discounts.mgd.php
@@ -0,0 +1,94 @@
+ 'CustomGroup_Product_Discounts',
+ 'entity' => 'CustomGroup',
+ 'cleanup' => 'always',
+ 'update' => 'unmodified',
+ 'params' => [
+ 'version' => 4,
+ 'values' => [
+ 'name' => 'Product_Discounts',
+ 'title' => 'Product Discounts',
+ 'extends' => 'MembershipType',
+ 'extends_entity_column_value' => NULL,
+ 'style' => 'Inline',
+ 'collapse_display' => FALSE,
+ 'help_pre' => '',
+ 'help_post' => '',
+ 'weight' => 107,
+ 'is_active' => TRUE,
+ 'is_multiple' => FALSE,
+ 'min_multiple' => NULL,
+ 'max_multiple' => NULL,
+ 'collapse_adv_display' => TRUE,
+ 'created_date' => '2023-08-25 07:22:08',
+ 'is_reserved' => FALSE,
+ 'is_public' => TRUE,
+ 'icon' => '',
+ 'extends_entity_column_id' => NULL,
+ ],
+ ],
+ ],
+ [
+ 'name' => 'CustomGroup_Product_Discounts_CustomField_Product_Discount_Amount',
+ 'entity' => 'CustomField',
+ 'cleanup' => 'always',
+ 'update' => 'unmodified',
+ 'params' => [
+ 'version' => 4,
+ 'values' => [
+ 'custom_group_id.name' => 'Product_Discounts',
+ 'name' => 'Product_Discount_Amount',
+ 'label' => 'Product Discount Amount',
+ 'data_type' => 'Float',
+ 'html_type' => 'Text',
+ 'default_value' => NULL,
+ 'is_required' => FALSE,
+ 'is_searchable' => FALSE,
+ 'is_search_range' => FALSE,
+ 'help_pre' => NULL,
+ 'help_post' => 'Specify a discount that will automatically be applied when adding a product line item to a quotation if the contact is a member of this type.',
+ 'mask' => NULL,
+ 'attributes' => ' pattern="([0-9]*[.])?[0-9]+" ',
+ 'javascript' => NULL,
+ 'is_active' => TRUE,
+ 'is_view' => FALSE,
+ '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' => 'product_discount_amount',
+ 'serialize' => 0,
+ 'filter' => NULL,
+ 'in_selector' => FALSE,
+ ],
+ ],
+ ],
+];
+
+$rowCount = OptionValue::get(FALSE)
+ ->selectRowCount()
+ ->addSelect('*')
+ ->addWhere('option_group_id:name', '=', 'cg_extend_objects')
+ ->addWhere('name', '=', 'civicrm_membership_type')
+ ->execute()
+ ->count();
+
+if ($rowCount == 1) {
+ return $mgd;
+}
+
+return [];
diff --git a/managed/SavedSearch_Civicase_Quotations.mgd.php b/managed/SavedSearch_Civicase_Quotations.mgd.php
new file mode 100644
index 000000000..1000f31fc
--- /dev/null
+++ b/managed/SavedSearch_Civicase_Quotations.mgd.php
@@ -0,0 +1,609 @@
+ 'SavedSearch_Civicase_Quotations',
+ 'entity' => 'SavedSearch',
+ 'cleanup' => 'unused',
+ 'update' => 'unmodified',
+ 'params' => [
+ 'version' => 4,
+ 'values' => [
+ 'name' => 'Civicase_Quotations',
+ 'label' => 'Civicase Quotations',
+ 'form_values' => NULL,
+ 'search_custom_id' => NULL,
+ 'api_entity' => 'CaseSalesOrder',
+ 'api_params' => [
+ 'version' => 4,
+ 'select' => [
+ 'id',
+ 'client_id.display_name',
+ 'DATE(quotation_date) AS DATE_quotation_date',
+ 'description',
+ 'owner_id.display_name',
+ 'CONCAT_WS(" ", total_before_tax, currency:label) AS CONCAT_WS_total_before_tax_currency_label',
+ 'CONCAT_WS(" ", total_after_tax, currency:label) AS CONCAT_WS_total_after_tax_currency_label',
+ 'status_id:label',
+ 'invoicing_status_id:label',
+ 'payment_status_id:label',
+ ],
+ 'orderBy' => [
+ 'created_at' => 'DESC',
+ ],
+ 'where' => [],
+ 'groupBy' => [],
+ 'join' => [],
+ 'having' => [],
+ 'limit' => 10,
+ ],
+ 'expires_date' => NULL,
+ 'description' => NULL,
+ 'mapping_id' => NULL,
+ ],
+ ],
+ ],
+ [
+ 'name' => 'SavedSearch_Civicase_Quotations_SearchDisplay_Civicase_Contact_Quotations_Table',
+ 'entity' => 'SearchDisplay',
+ 'cleanup' => 'unused',
+ 'update' => 'unmodified',
+ 'params' => [
+ 'version' => 4,
+ 'values' => [
+ 'name' => 'Civicase_Contact_Quotations_Table',
+ 'label' => 'Contact Quotations List',
+ 'saved_search_id.name' => 'Civicase_Quotations',
+ 'type' => 'table',
+ 'settings' => [
+ 'actions' => TRUE,
+ 'limit' => 10,
+ 'classes' => [
+ 'table',
+ 'table-striped',
+ ],
+ 'pager' => [],
+ 'placeholder' => 5,
+ 'sort' => [
+ [
+ 'created_at',
+ 'DESC',
+ ],
+ ],
+ 'columns' => [
+ [
+ 'type' => 'field',
+ 'key' => 'id',
+ 'dataType' => 'Integer',
+ 'label' => 'ID',
+ 'sortable' => TRUE,
+ 'alignment' => '',
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'DATE_quotation_date',
+ 'dataType' => 'Date',
+ 'label' => 'Date',
+ 'sortable' => TRUE,
+ 'alignment' => '',
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'owner_id.display_name',
+ 'dataType' => 'String',
+ 'label' => 'Owner',
+ 'sortable' => TRUE,
+ 'link' => [
+ 'path' => '',
+ 'entity' => 'Contact',
+ 'action' => 'view',
+ 'join' => 'owner_id',
+ 'target' => '_blank',
+ ],
+ 'title' => 'View Owner',
+ 'alignment' => '',
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'CONCAT_WS_total_before_tax_currency_label',
+ 'dataType' => 'String',
+ 'label' => 'Total Before Tax',
+ 'sortable' => TRUE,
+ 'alignment' => '',
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'CONCAT_WS_total_after_tax_currency_label',
+ 'dataType' => 'String',
+ 'label' => 'Total After Tax',
+ 'sortable' => TRUE,
+ 'alignment' => '',
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'invoicing_status_id:label',
+ 'dataType' => 'Integer',
+ 'label' => 'Invoicing',
+ 'sortable' => TRUE,
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'payment_status_id:label',
+ 'dataType' => 'Integer',
+ 'label' => 'Payments',
+ 'sortable' => TRUE,
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'status_id:label',
+ 'dataType' => 'Integer',
+ 'label' => 'Status',
+ 'sortable' => TRUE,
+ 'alignment' => '',
+ ],
+ [
+ 'text' => '',
+ 'style' => 'default',
+ 'size' => 'btn-sm',
+ 'icon' => 'fa-bars',
+ 'links' => [
+ [
+ 'path' => '',
+ 'icon' => 'fa-external-link',
+ 'text' => 'View',
+ 'style' => 'default',
+ 'condition' => [],
+ 'entity' => 'CaseSalesOrder',
+ 'action' => 'view',
+ 'join' => '',
+ 'target' => '_blank',
+ ],
+ [
+ 'entity' => 'CaseSalesOrder',
+ 'action' => 'update',
+ 'join' => '',
+ 'target' => '_blank',
+ 'icon' => 'fa-pencil',
+ 'text' => 'Edit',
+ 'style' => 'default',
+ 'path' => '',
+ 'condition' => [],
+ ],
+ [
+ 'entity' => 'CaseSalesOrder',
+ 'action' => 'delete',
+ 'join' => '',
+ 'target' => 'crm-popup',
+ 'icon' => 'fa-trash',
+ 'text' => 'Delete',
+ 'style' => 'default',
+ 'path' => '',
+ 'condition' => [],
+ ],
+ [
+ 'path' => 'civicrm/case-features/quotations/download-pdf?id=[id]',
+ 'icon' => 'fa-file-pdf-o',
+ 'text' => 'Download PDF',
+ 'style' => 'default',
+ 'condition' => [],
+ 'entity' => '',
+ 'action' => '',
+ 'join' => '',
+ 'target' => '_blank',
+ ],
+ [
+ 'path' => 'civicrm/case-features/quotations/create-contribution?id=[id]',
+ 'icon' => 'fa-credit-card',
+ 'text' => 'Create Contribution',
+ 'style' => 'default',
+ 'condition' => [],
+ 'entity' => '',
+ 'action' => '',
+ 'join' => '',
+ 'target' => 'crm-popup',
+ ],
+ [
+ 'path' => 'civicrm/case-features/quotations/email?id=[id]',
+ 'icon' => 'fa-paper-plane-o',
+ 'text' => 'Send By Email',
+ 'style' => 'default',
+ 'condition' => [],
+ 'entity' => '',
+ 'action' => '',
+ 'join' => '',
+ 'target' => 'crm-popup',
+ ],
+ ],
+ 'type' => 'menu',
+ 'alignment' => '',
+ 'label' => 'Actions',
+ ],
+ ],
+ 'noResultsText' => 'No quotations found',
+ ],
+ 'acl_bypass' => FALSE,
+ ],
+ ],
+ ],
+ [
+ 'name' => 'SavedSearch_Civicase_Quotations_SearchDisplay_Civicase_Quotations_Table',
+ 'entity' => 'SearchDisplay',
+ 'cleanup' => 'unused',
+ 'update' => 'unmodified',
+ 'params' => [
+ 'version' => 4,
+ 'values' => [
+ 'name' => 'Civicase_Quotations_Table',
+ 'label' => 'Quotations List',
+ 'saved_search_id.name' => 'Civicase_Quotations',
+ 'type' => 'table',
+ 'settings' => [
+ 'actions' => TRUE,
+ 'limit' => 10,
+ 'classes' => [
+ 'table',
+ 'table-striped',
+ ],
+ 'pager' => [],
+ 'placeholder' => 5,
+ 'sort' => [
+ [
+ 'created_at',
+ 'DESC',
+ ],
+ ],
+ 'columns' => [
+ [
+ 'type' => 'field',
+ 'key' => 'id',
+ 'dataType' => 'Integer',
+ 'label' => 'ID',
+ 'sortable' => TRUE,
+ 'alignment' => '',
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'client_id.display_name',
+ 'dataType' => 'String',
+ 'label' => 'Client',
+ 'sortable' => TRUE,
+ 'link' => [
+ 'path' => '',
+ 'entity' => 'Contact',
+ 'action' => 'view',
+ 'join' => 'client_id',
+ 'target' => '_blank',
+ ],
+ 'title' => 'View Client',
+ 'alignment' => '',
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'DATE_quotation_date',
+ 'dataType' => 'Date',
+ 'label' => 'Date',
+ 'sortable' => TRUE,
+ 'alignment' => '',
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'owner_id.display_name',
+ 'dataType' => 'String',
+ 'label' => 'Owner',
+ 'sortable' => TRUE,
+ 'link' => [
+ 'path' => '',
+ 'entity' => 'Contact',
+ 'action' => 'view',
+ 'join' => 'owner_id',
+ 'target' => '_blank',
+ ],
+ 'title' => 'View Owner',
+ 'alignment' => '',
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'CONCAT_WS_total_before_tax_currency_label',
+ 'dataType' => 'String',
+ 'label' => 'Total Before Tax',
+ 'sortable' => TRUE,
+ 'alignment' => '',
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'CONCAT_WS_total_after_tax_currency_label',
+ 'dataType' => 'String',
+ 'label' => 'Total After Tax',
+ 'sortable' => TRUE,
+ 'alignment' => '',
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'status_id:label',
+ 'dataType' => 'Integer',
+ 'label' => 'Status',
+ 'sortable' => TRUE,
+ 'alignment' => '',
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'invoicing_status_id:label',
+ 'dataType' => 'Integer',
+ 'label' => 'Invoicing',
+ 'sortable' => TRUE,
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'payment_status_id:label',
+ 'dataType' => 'Integer',
+ 'label' => 'Payments',
+ 'sortable' => TRUE,
+ ],
+ [
+ 'text' => '',
+ 'style' => 'default',
+ 'size' => 'btn-sm',
+ 'icon' => 'fa-bars',
+ 'links' => [
+ [
+ 'path' => '',
+ 'icon' => 'fa-external-link',
+ 'text' => 'View',
+ 'style' => 'default',
+ 'condition' => [],
+ 'entity' => 'CaseSalesOrder',
+ 'action' => 'view',
+ 'join' => '',
+ 'target' => '_blank',
+ ],
+ [
+ 'entity' => 'CaseSalesOrder',
+ 'action' => 'update',
+ 'join' => '',
+ 'target' => '_blank',
+ 'icon' => 'fa-pencil',
+ 'text' => 'Edit',
+ 'style' => 'default',
+ 'path' => '',
+ 'condition' => [],
+ ],
+ [
+ 'entity' => 'CaseSalesOrder',
+ 'action' => 'delete',
+ 'join' => '',
+ 'target' => 'crm-popup',
+ 'icon' => 'fa-trash',
+ 'text' => 'Delete',
+ 'style' => 'default',
+ 'path' => '',
+ 'condition' => [],
+ ],
+ [
+ 'path' => 'civicrm/case-features/quotations/download-pdf?id=[id]',
+ 'icon' => 'fa-file-pdf-o',
+ 'text' => 'Download PDF',
+ 'style' => 'default',
+ 'condition' => [],
+ 'entity' => '',
+ 'action' => '',
+ 'join' => '',
+ 'target' => '_blank',
+ ],
+ [
+ 'path' => 'civicrm/case-features/quotations/create-contribution?id=[id]',
+ 'icon' => 'fa-credit-card',
+ 'text' => 'Create Contribution',
+ 'style' => 'default',
+ 'condition' => [],
+ 'entity' => '',
+ 'action' => '',
+ 'join' => '',
+ 'target' => 'crm-popup',
+ ],
+ [
+ 'path' => 'civicrm/case-features/quotations/email?id=[id]',
+ 'icon' => 'fa-paper-plane-o',
+ 'text' => 'Send By Email',
+ 'style' => 'default',
+ 'condition' => [],
+ 'entity' => '',
+ 'action' => '',
+ 'join' => '',
+ 'target' => 'crm-popup',
+ ],
+ ],
+ 'type' => 'menu',
+ 'alignment' => '',
+ 'label' => 'Actions',
+ ],
+ ],
+ 'noResultsText' => 'No quotations found',
+ ],
+ 'acl_bypass' => FALSE,
+ ],
+ ],
+ ],
+ [
+ 'name' => 'SavedSearch_Quotation_Contributions',
+ 'entity' => 'SavedSearch',
+ 'cleanup' => 'unused',
+ 'update' => 'unmodified',
+ 'params' => [
+ 'version' => 4,
+ 'values' => [
+ 'name' => 'Quotation_Contributions',
+ 'label' => 'Quotation Contributions',
+ 'form_values' => NULL,
+ 'search_custom_id' => NULL,
+ 'api_entity' => 'Contribution',
+ 'api_params' => [
+ 'version' => 4,
+ 'select' => [
+ 'total_amount',
+ 'financial_type_id:label',
+ 'source',
+ 'receive_date',
+ 'thankyou_date',
+ 'contribution_status_id:label',
+ 'Opportunity_Details.Case_Opportunity',
+ ],
+ 'orderBy' => [],
+ 'where' => [
+ [
+ 'id',
+ 'IS NOT EMPTY',
+ ],
+ [
+ 'OR',
+ [
+ [
+ 'Opportunity_Details.Case_Opportunity',
+ 'IS NOT EMPTY',
+ ],
+ [
+ 'Opportunity_Details.Quotation',
+ 'IS NOT EMPTY',
+ ],
+ ],
+ ],
+ ],
+ 'groupBy' => [],
+ 'join' => [],
+ 'having' => [],
+ ],
+ 'expires_date' => NULL,
+ 'description' => NULL,
+ 'mapping_id' => NULL,
+ ],
+ ],
+ ],
+ [
+ 'name' => 'SavedSearch_Quotation_Contributions_SearchDisplay_Contributions_Table_1',
+ 'entity' => 'SearchDisplay',
+ 'cleanup' => 'unused',
+ 'update' => 'unmodified',
+ 'params' => [
+ 'version' => 4,
+ 'values' => [
+ 'name' => 'Contributions_Table_1',
+ 'label' => 'Contributions Table 1',
+ 'saved_search_id.name' => 'Quotation_Contributions',
+ 'type' => 'table',
+ 'settings' => [
+ 'actions' => TRUE,
+ 'limit' => 10,
+ 'classes' => [
+ 'table',
+ 'table-striped',
+ ],
+ 'pager' => [],
+ 'placeholder' => 5,
+ 'sort' => [],
+ 'columns' => [
+ [
+ 'type' => 'field',
+ 'key' => 'total_amount',
+ 'dataType' => 'Money',
+ 'label' => 'Amount',
+ 'sortable' => TRUE,
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'financial_type_id:label',
+ 'dataType' => 'Integer',
+ 'label' => 'Type',
+ 'sortable' => TRUE,
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'source',
+ 'dataType' => 'String',
+ 'label' => 'Source',
+ 'sortable' => TRUE,
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'receive_date',
+ 'dataType' => 'Timestamp',
+ 'label' => 'Date',
+ 'sortable' => TRUE,
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'thankyou_date',
+ 'dataType' => 'Timestamp',
+ 'label' => 'Thank-you Sent',
+ 'sortable' => TRUE,
+ 'rewrite' => '{if "[thankyou_date]"} Yes {else} No {/if}',
+ ],
+ [
+ 'type' => 'field',
+ 'key' => 'contribution_status_id:label',
+ 'dataType' => 'Integer',
+ 'label' => 'Status',
+ 'sortable' => TRUE,
+ ],
+ [
+ 'text' => 'Actions',
+ 'style' => 'default',
+ 'size' => 'btn-sm',
+ 'icon' => 'fa-bars',
+ 'links' => [
+ [
+ 'entity' => 'Contribution',
+ 'action' => 'view',
+ 'join' => '',
+ 'target' => 'crm-popup',
+ 'icon' => 'fa-external-link',
+ 'text' => 'View',
+ 'style' => 'default',
+ 'path' => '',
+ 'condition' => [],
+ ],
+ [
+ 'entity' => 'Contribution',
+ 'action' => 'update',
+ 'join' => '',
+ 'target' => 'crm-popup',
+ 'icon' => 'fa-pencil',
+ 'text' => 'Edit',
+ 'style' => 'default',
+ 'path' => '',
+ 'condition' => [],
+ ],
+ [
+ 'entity' => '',
+ 'action' => '',
+ 'join' => '',
+ 'target' => 'crm-popup',
+ 'icon' => 'fa-paper-plane-o',
+ 'text' => 'Send By Email',
+ 'style' => 'default',
+ 'path' => 'civicrm/contribute/invoice/email/?reset=1&id=[id]&select=email&cid=[contact_id]',
+ 'condition' => [],
+ ],
+ ],
+ 'type' => 'menu',
+ 'alignment' => 'text-right',
+ ],
+ ],
+ 'noResultsText' => 'No Invoices found',
+ ],
+ 'acl_bypass' => FALSE,
+ ],
+ ],
+ ],
+];
+
+$searchKitIsInstalled = 'installed' ===
+CRM_Extension_System::singleton()->getManager()->getStatus('org.civicrm.search_kit');
+if ($searchKitIsInstalled) {
+ return $mgd;
+}
+
+return [];
diff --git a/mixin/entity-types-php@1.0.0.mixin.php b/mixin/entity-types-php@1.0.0.mixin.php
new file mode 100644
index 000000000..8ec4203df
--- /dev/null
+++ b/mixin/entity-types-php@1.0.0.mixin.php
@@ -0,0 +1,36 @@
+addListener('hook_civicrm_entityTypes', function ($e) use ($mixInfo) {
+ // When deactivating on a polyfill/pre-mixin system, listeners may not cleanup automatically.
+ if (!$mixInfo->isActive() || !is_dir($mixInfo->getPath('xml/schema/CRM'))) {
+ return;
+ }
+
+ $files = (array) glob($mixInfo->getPath('xml/schema/CRM/*/*.entityType.php'));
+ foreach ($files as $file) {
+ $entities = include $file;
+ foreach ($entities as $entity) {
+ $e->entityTypes[$entity['class']] = $entity;
+ }
+ }
+ });
+
+};
diff --git a/mixin/smarty-v2@1.0.1.mixin.php b/mixin/smarty-v2@1.0.1.mixin.php
new file mode 100644
index 000000000..5972dbdc5
--- /dev/null
+++ b/mixin/smarty-v2@1.0.1.mixin.php
@@ -0,0 +1,51 @@
+getPath('templates');
+ if (!file_exists($dir)) {
+ return;
+ }
+
+ $register = function() use ($dir) {
+ // This implementation has a theoretical edge-case bug on older versions of CiviCRM where a template could
+ // be registered more than once.
+ CRM_Core_Smarty::singleton()->addTemplateDir($dir);
+ };
+
+ // Let's figure out what environment we're in -- so that we know the best way to call $register().
+
+ if (!empty($GLOBALS['_CIVIX_MIXIN_POLYFILL'])) {
+ // Polyfill Loader (v<=5.45): We're already in the middle of firing `hook_config`.
+ if ($mixInfo->isActive()) {
+ $register();
+ }
+ return;
+ }
+
+ if (CRM_Extension_System::singleton()->getManager()->extensionIsBeingInstalledOrEnabled($mixInfo->longName)) {
+ // New Install, Standard Loader: The extension has just been enabled, and we're now setting it up.
+ // System has already booted. New templates may be needed for upcoming installation steps.
+ $register();
+ return;
+ }
+
+ // Typical Pageview, Standard Loader: Defer the actual registration for a moment -- to ensure that Smarty is online.
+ \Civi::dispatcher()->addListener('hook_civicrm_config', function() use ($mixInfo, $register) {
+ if ($mixInfo->isActive()) {
+ $register();
+ }
+ });
+
+};
diff --git a/phpcs-ruleset.xml b/phpcs-ruleset.xml
index 0524d7a5d..7a721e9d4 100644
--- a/phpcs-ruleset.xml
+++ b/phpcs-ruleset.xml
@@ -15,12 +15,16 @@
tests/phpunit/bootstrap.php
+ mixin/*
CRM/Civicase/Form/Report/ExtendedReport.php
civicase.civix.php
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/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/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;
+}
diff --git a/sql/auto_install.sql b/sql/auto_install.sql
index 9a3244980..c85f5274a 100644
--- a/sql/auto_install.sql
+++ b/sql/auto_install.sql
@@ -29,3 +29,74 @@ 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;
+
+
+-- /*******************************************************
+-- *
+-- * civicase_sales_order
+-- *
+-- * Sales order that represents quotations
+-- *
+-- *******************************************************/
+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',
+ `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',
+ `invoicing_status_id` int unsigned NOT NULL COMMENT 'One of the values of the case_sales_order_invoicing_status option group',
+ `payment_status_id` int unsigned NOT NULL COMMENT 'One of the values of the case_sales_order_payment_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_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;
+
+-- /*******************************************************
+-- *
+-- * civicase_sales_order_line
+-- *
+-- * Sales order line items
+-- *
+-- *******************************************************/
+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',
+ `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_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 af9bba66c..80fb0b568 100644
--- a/sql/auto_uninstall.sql
+++ b/sql/auto_uninstall.sql
@@ -1,7 +1,24 @@
+-- +--------------------------------------------------------------------+
+-- | 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_sales_order_line`;
+DROP TABLE IF EXISTS `civicase_sales_order`;
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/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"
/>
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}
+
+ {$form.$row.html} {$form.$row.label}
+
+ {/foreach}
+
+
+
+
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 @@
+
+
+
+
+
+
+ {include file="CRM/common/formButtons.tpl" location="bottom"}
+
+
+
+{literal}
+
+{/literal}
diff --git a/templates/CRM/Civicase/Form/CaseSalesOrderDelete.tpl b/templates/CRM/Civicase/Form/CaseSalesOrderDelete.tpl
new file mode 100755
index 000000000..68af99c89
--- /dev/null
+++ b/templates/CRM/Civicase/Form/CaseSalesOrderDelete.tpl
@@ -0,0 +1,24 @@
+
+
+
+ {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/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 @@
+
+{include file="CRM/Contact/Form/Task/Email.tpl"}
+
+{literal}
+
+{/literal}
\ No newline at end of file
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}
diff --git a/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl b/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl
new file mode 100644
index 000000000..6770a4286
--- /dev/null
+++ b/templates/CRM/Civicase/Form/Contribute/AttachQuotation.tpl
@@ -0,0 +1,21 @@
+
+
+ {$form.attach_quote.label}
+ {$form.attach_quote.html} {if isset($form.attach_quote.html)}Yes {/if}
+
+
+
+
+{literal}
+
+{/literal}
diff --git a/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl b/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl
new file mode 100644
index 000000000..4e4918993
--- /dev/null
+++ b/templates/CRM/Civicase/MessageTemplate/QuotationInvoice.tpl
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {ts}Client Name: {$sales_order.client.display_name}<{/ts}
+ {ts}Date:{/ts}
+ {$domain_name}
+
+
+
+ {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}
+
+
+
+
+
+
+
+ {ts}Description{/ts}
+ {ts}Quantity{/ts}
+ {ts}Unit Price{/ts}
+ {ts}Discount{/ts}
+ {ts}VAT{/ts}
+ {ts}Amount {$sales_order.currency} (without tax){/ts}
+
+
+
+ {foreach from=$sales_order.items key=k item=item}
+
+ {$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}
+
+
+ {ts}SubTotal (inclusive of discount){/ts}
+ {$sales_order.total_before_tax|crmMoney:$sales_order.currency}
+
+ {foreach from=$sales_order.taxRates item=tax}
+
+
+ {ts}Total VAT ({$tax.rate}%){/ts}
+ {$tax.value|crmMoney:$sales_order.currency}
+
+ {/foreach}
+
+
+
+
+
+
+ {ts}Total Amount{/ts}
+ {$sales_order.total_after_tax|crmMoney:$sales_order.currency}
+
+
+
+
+
+ {if !empty($terms) }
+
+ {/if}
+
+
+
+
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/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/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}
diff --git a/tests/phpunit/BaseHeadlessTest.php b/tests/phpunit/BaseHeadlessTest.php
index 2eda63621..d3ad2d3bd 100644
--- a/tests/phpunit/BaseHeadlessTest.php
+++ b/tests/phpunit/BaseHeadlessTest.php
@@ -3,11 +3,12 @@
use Civi\Test;
use Civi\Test\HeadlessInterface;
use Civi\Test\TransactionalInterface;
+use PHPUnit\Framework\TestCase;
/**
* Base test class.
*/
-abstract class BaseHeadlessTest extends PHPUnit_Framework_TestCase implements HeadlessInterface, TransactionalInterface {
+abstract class BaseHeadlessTest extends 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..7807aee85 100644
--- a/tests/phpunit/CRM/Civicase/BAO/CaseContactLockTest.php
+++ b/tests/phpunit/CRM/Civicase/BAO/CaseContactLockTest.php
@@ -1,18 +1,19 @@
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/CRM/Civicase/Service/CaseSalesOrderLineItemsGeneratorTest.php b/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderLineItemsGeneratorTest.php
new file mode 100644
index 000000000..ba092253e
--- /dev/null
+++ b/tests/phpunit/CRM/Civicase/Service/CaseSalesOrderLineItemsGeneratorTest.php
@@ -0,0 +1,121 @@
+generatePriceField();
+ }
+
+ /**
+ * Ensures the correct number of line item is generated.
+ *
+ * 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 generated.
+ *
+ * When there's previous contribution.
+ */
+ public function testCorrectNumberOfLineItemsIsGeneratedWithPreviousContribution() {
+ $salesOrder = $this->createCaseSalesOrder();
+
+ $previousContributionCount = rand(1, 4);
+ for ($i = 0; $i < $previousContributionCount; $i++) {
+ CaseSalesOrder::contributionCreateAction()
+ ->setSalesOrderIds([$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);
+ }
+
+ /**
+ * 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']);
+ }
+
+}
diff --git a/tests/phpunit/Civi/Api4/CaseSalesOrder/ComputeTotalActionTest.php b/tests/phpunit/Civi/Api4/CaseSalesOrder/ComputeTotalActionTest.php
new file mode 100644
index 000000000..441c7d12b
--- /dev/null
+++ b/tests/phpunit/Civi/Api4/CaseSalesOrder/ComputeTotalActionTest.php
@@ -0,0 +1,114 @@
+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);
+ }
+
+ /**
+ * 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']);
+ }
+
+}
diff --git a/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php b/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php
new file mode 100644
index 000000000..41a8d5351
--- /dev/null
+++ b/tests/phpunit/Civi/Api4/CaseSalesOrder/ContributionCreateActionTest.php
@@ -0,0 +1,473 @@
+generatePriceField();
+ $contact = ContactFabricator::fabricate();
+ $this->registerCurrentLoggedInContactInSession($contact['id']);
+ }
+
+ /**
+ * Ensures contribution create action updates status successfully.
+ */
+ public function testContributionCreateActionWillUpdateSalesOrderStatus() {
+ $ids = [];
+
+ for ($i = 0; $i < rand(5, 11); $i++) {
+ $ids[] = $this->createCaseSalesOrder()['id'];
+ }
+
+ $newStatus = $this->getCaseSalesOrderStatus()[1]['value'];
+ CaseSalesOrder::contributionCreateAction()
+ ->setSalesOrderIds($ids)
+ ->setStatusId($newStatus)
+ ->setToBeInvoiced('percent')
+ ->setPercentValue('100')
+ ->setDate('2020-2-12')
+ ->setFinancialTypeId('1')
+ ->execute();
+
+ $results = CaseSalesOrder::get()
+ ->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 update contribution custom field.
+ */
+ public function testContributionCreateActionWillUpdateCustomFields() {
+ $ids = [];
+
+ for ($i = 0; $i < rand(5, 11); $i++) {
+ $ids[] = $this->createCaseSalesOrder()['id'];
+ }
+
+ $newStatus = $this->getCaseSalesOrderStatus()[1]['value'];
+ CaseSalesOrder::contributionCreateAction()
+ ->setSalesOrderIds($ids)
+ ->setStatusId($newStatus)
+ ->setToBeInvoiced('percent')
+ ->setPercentValue('100')
+ ->setDate('2020-2-12')
+ ->setFinancialTypeId('1')
+ ->execute();
+
+ $contributions = Contribution::get()
+ ->addSelect('Opportunity_Details.Quotation')
+ ->addWhere('Opportunity_Details.Quotation', 'IN', $ids)
+ ->execute()
+ ->jsonSerialize();
+
+ $this->assertCount(count($ids), $contributions);
+ $this->assertEmpty(array_diff(array_column($contributions, 'Opportunity_Details.Quotation'), $ids));
+ }
+
+ /**
+ * 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()
+ ->setSalesOrderIds([$salesOrder['id']])
+ ->setStatusId($data['statusId'])
+ ->setToBeInvoiced($data['toBeInvoiced'])
+ ->setPercentValue($data['percentValue'])
+ ->setDate($data['date'])
+ ->setFinancialTypeId($data['financialTypeId'])
+ ->execute();
+ }
+
+ $contributionAmounts = Contribution::get()
+ ->addSelect('total_amount')
+ ->addWhere('Opportunity_Details.Quotation', '=', $salesOrder['id'])
+ ->execute()
+ ->jsonSerialize();
+
+ $paidTotal = array_sum(array_column($contributionAmounts, '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));
+ }
+
+ /**
+ * 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.
+ *
+ * @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',
+ ],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * 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,
+ ],
+ ],
+ ],
+ ];
+ }
+
+}
diff --git a/tests/phpunit/Civi/Api4/CaseSalesOrder/SalesOrderSaveActionTest.php b/tests/phpunit/Civi/Api4/CaseSalesOrder/SalesOrderSaveActionTest.php
new file mode 100644
index 000000000..6244d58ed
--- /dev/null
+++ b/tests/phpunit/Civi/Api4/CaseSalesOrder/SalesOrderSaveActionTest.php
@@ -0,0 +1,192 @@
+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);
+ }
+
+ /**
+ * Test Case Sales Order save action updates sales order as expected.
+ */
+ public function testCaseSalesOrderIsUpdatedWithSaveAction() {
+ $salesOrderData = $this->getCaseSalesOrderData();
+ $salesOrderData['items'][] = $this->getCaseSalesOrderLineData();
+ $salesOrderData['items'][] = $this->getCaseSalesOrderLineData();
+
+ // Create sales order.
+ $salesOrder = CaseSalesOrder::save()
+ ->addRecord($salesOrderData)
+ ->execute()
+ ->jsonSerialize()[0];
+
+ // Update Sales order.
+ $salesOrderData['id'] = $salesOrder['id'];
+ $salesOrderData['items'] = $salesOrder['items'];
+ $salesOrderData['notes'] = substr(md5(mt_rand()), 0, 7);
+ $salesOrderData['description'] = substr(md5(mt_rand()), 0, 7);
+ CaseSalesOrder::save()
+ ->addRecord($salesOrderData)
+ ->execute()
+ ->jsonSerialize()[0];
+
+ // Assert that sales order was updated.
+ $updatedSalesOrder = CaseSalesOrder::get()
+ ->addWhere('id', '=', $salesOrder['id'])
+ ->execute()
+ ->jsonSerialize()[0];
+
+ $this->assertEquals($salesOrderData['id'], $updatedSalesOrder['id']);
+ $this->assertEquals($salesOrderData['notes'], $updatedSalesOrder['notes']);
+ $this->assertEquals($salesOrderData['description'], $updatedSalesOrder['description']);
+ }
+
+ /**
+ * Test line items not included in case sales order update data are removed.
+ */
+ public function testDetachedLineItemsAreRemovedFromSalesOrderOnUpdate() {
+ $salesOrderData = $this->getCaseSalesOrderData();
+ $salesOrderData['items'][] = $this->getCaseSalesOrderLineData();
+ $salesOrderData['items'][] = $this->getCaseSalesOrderLineData();
+
+ $salesOrder = CaseSalesOrder::save()
+ ->addRecord($salesOrderData)
+ ->execute()
+ ->jsonSerialize()[0];
+
+ $this->assertCount(2, $salesOrder['items']);
+
+ // Perform update with single line item.
+ $salesOrderData['id'] = $salesOrder['id'];
+ $salesOrderData['items'] = [$salesOrder['items'][0]];
+ CaseSalesOrder::save()
+ ->addRecord($salesOrderData)
+ ->execute()
+ ->jsonSerialize()[0];
+
+ // Assert that sales order has only one line item.
+ $salesOrderLine = CaseSalesOrderLine::get()
+ ->addWhere('sales_order_id', '=', $salesOrder['id'])
+ ->execute()
+ ->jsonSerialize();
+
+ $this->assertCount(1, $salesOrderLine);
+ $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']);
+ }
+
+}
diff --git a/tests/phpunit/Helpers/CaseSalesOrderTrait.php b/tests/phpunit/Helpers/CaseSalesOrderTrait.php
new file mode 100644
index 000000000..355d91363
--- /dev/null
+++ b/tests/phpunit/Helpers/CaseSalesOrderTrait.php
@@ -0,0 +1,151 @@
+addSelect('id', 'value', 'name', 'label')
+ ->addWhere('option_group_id:name', '=', 'case_sales_order_status')
+ ->execute();
+
+ return $salesOrderStatus;
+ }
+
+ /**
+ * Returns list of available statuses.
+ *
+ * @return array
+ * Array of sales order invoicing statuses
+ */
+ public function getCaseSalesOrderInvoicingStatus() {
+ return OptionValue::get()
+ ->addSelect('id', 'value', 'name', 'label')
+ ->addWhere('option_group_id:name', '=', 'case_sales_order_invoicing_status')
+ ->execute()
+ ->getArrayCopy();
+ }
+
+ /**
+ * Returns list of available statuses.
+ *
+ * @return array
+ * Array of sales order payment statuses
+ */
+ public function getCaseSalesOrderPaymentStatus() {
+ return OptionValue::get()
+ ->addSelect('id', 'value', 'name', 'label')
+ ->addWhere('option_group_id:name', '=', 'case_sales_order_payment_status')
+ ->execute()
+ ->getArrayCopy();
+ }
+
+ /**
+ * 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'],
+ 'invoicing_status_id' => $this->getCaseSalesOrderInvoicingStatus()[0]['value'],
+ 'payment_status_id' => $this->getCaseSalesOrderPaymentStatus()[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();
+ $quantity = rand(2, 9);
+ $unitPrice = rand(50, 1000);
+
+ return array_merge([
+ 'financial_type_id' => 1,
+ 'product_id' => $product['id'],
+ 'item_description' => 'test',
+ 'quantity' => $quantity,
+ 'unit_price' => $unitPrice,
+ 'tax_rate' => NULL,
+ 'discounted_percentage' => NULL,
+ 'subtotal_amount' => $quantity * $unitPrice,
+ ], $default);
+ }
+
+ /**
+ * Creates case sales order.
+ *
+ * @param array $params
+ * Extra paramters.
+ *
+ * @return array
+ * Created case sales order
+ */
+ public function createCaseSalesOrder(array $params = []): array {
+ $salesOrder = $this->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,
+ ]);
+ }
+ }
+ }
+
+}
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;
diff --git a/xml/Menu/civicase.xml b/xml/Menu/civicase.xml
index 7c194eb52..3776d45af 100644
--- a/xml/Menu/civicase.xml
+++ b/xml/Menu/civicase.xml
@@ -28,6 +28,41 @@
CRM_Civicase_Page_WorkflowAngular
administer CiviCase
+
-
+
civicrm/case-features/a
+ CRM_Civicase_Page_CaseFeaturesAngular
+ administer CiviCase
+
+
-
+
civicrm/case-features/quotations/view
+ CRM_Civicase_Page_CaseSalesOrder
+ administer CiviCase
+
+
-
+
civicrm/case-features/quotations/delete
+ CRM_Civicase_Form_CaseSalesOrderDelete
+ administer CiviCase
+
+
-
+
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
+ 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
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
+
+
+
diff --git a/xml/schema/CRM/Civicase/CaseSalesOrder.entityType.php b/xml/schema/CRM/Civicase/CaseSalesOrder.entityType.php
new file mode 100644
index 000000000..55b2d44de
--- /dev/null
+++ b/xml/schema/CRM/Civicase/CaseSalesOrder.entityType.php
@@ -0,0 +1,17 @@
+ 'CaseSalesOrder',
+ 'class' => 'CRM_Civicase_DAO_CaseSalesOrder',
+ 'table' => 'civicase_sales_order',
+ ],
+];
diff --git a/xml/schema/CRM/Civicase/CaseSalesOrder.xml b/xml/schema/CRM/Civicase/CaseSalesOrder.xml
new file mode 100644
index 000000000..e7a4630bb
--- /dev/null
+++ b/xml/schema/CRM/Civicase/CaseSalesOrder.xml
@@ -0,0 +1,205 @@
+
+
+
+ CRM/Civicase
+ CaseSalesOrder
+ civicase_sales_order
+ Sales order that represents quotations
+ true
+
+
+ civicrm/case-features/quotations/view?reset=1&id=[id]
+ civicrm/case-features/a#/quotations/new?reset=1&id=[id]
+ civicrm/case-features/quotations/delete?reset=1&id=[id]
+
+
+
+ id
+ int unsigned
+ true
+ Unique CaseSalesOrder ID
+
+ Number
+
+
+
+ id
+ true
+
+
+
+ client_id
+ int unsigned
+ FK to Contact
+
+ Client
+ EntityRef
+
+
+
+ client_id
+
+ id
+ CASCADE
+
+
+
+ owner_id
+ int unsigned
+ FK to Contact
+
+ Owner
+ EntityRef
+
+
+
+ owner_id
+
+ id
+ CASCADE
+
+
+
+ case_id
+ int unsigned
+ FK to Case
+
+ Case/Opportunity
+ EntityRef
+
+
+
+ case_id
+
+ 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.
+
+
+ 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
+
+
+ Status
+ Select
+
+
+
+
+ invoicing_status_id
+ int unsigned
+ One of the values of the case_sales_order_invoicing_status option group
+ true
+
+ case_sales_order_invoicing_status
+
+
+ Invoicing
+ Select
+
+
+
+
+ payment_status_id
+ int unsigned
+ One of the values of the case_sales_order_payment_status option group
+ true
+
+ case_sales_order_payment_status
+
+
+ Payments
+ Select
+
+
+
+
+ description
+ text
+ false
+ Sales order deesctiption
+
+ Description
+ TextArea
+
+
+
+
+ notes
+ text
+ false
+ Sales order notes
+
+ 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?
+
+
diff --git a/xml/schema/CRM/Civicase/CaseSalesOrderLine.entityType.php b/xml/schema/CRM/Civicase/CaseSalesOrderLine.entityType.php
new file mode 100644
index 000000000..4547d665b
--- /dev/null
+++ b/xml/schema/CRM/Civicase/CaseSalesOrderLine.entityType.php
@@ -0,0 +1,17 @@
+ 'CaseSalesOrderLine',
+ 'class' => 'CRM_Civicase_DAO_CaseSalesOrderLine',
+ 'table' => 'civicase_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..8a7bccdd0
--- /dev/null
+++ b/xml/schema/CRM/Civicase/CaseSalesOrderLine.xml
@@ -0,0 +1,136 @@
+
+
+
+ CRM/Civicase
+ CaseSalesOrderLine
+ civicase_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
+
+ id
+ CASCADE
+
+
+
+ financial_type_id
+ int unsigned
+ FK to CiviCRM Financial Type
+
+ Financial Type
+ EntityRef
+
+
+
+
+ id
+ name
+
+
+ financial_type_id
+
+ id
+ SET NULL
+
+
+
+ product_id
+ Product ID
+ int unsigned
+
+ Product
+ EntityRef
+
+
+
+ product_id
+
+ id
+ SET NULL
+
+
+
+ item_description
+ text
+ false
+ line item deesctiption
+
+ Item Description
+ TextArea
+
+
+
+
+ quantity
+ decimal
+ Quantity
+
+ Quantity
+ Text
+
+
+
+
+ unit_price
+ decimal
+ Unit Price
+
+ Unit Price
+ Text
+
+
+
+
+ tax_rate
+ decimal
+ Tax rate for the line item
+
+ Tax
+ Text
+
+
+
+
+ discounted_percentage
+ decimal
+ Discount applied to the line item
+
+ Discount
+ Text
+
+
+
+
+ subtotal_amount
+ decimal
+ Quantity x Unit Price x (100-Discount)%
+
+ Subtotal
+ Text
+
+
+