From 4a7ad7a839ab556029cdcbfad9be81c2fe0b1763 Mon Sep 17 00:00:00 2001 From: Erawat Chamanont Date: Thu, 21 Sep 2023 22:51:07 +0100 Subject: [PATCH 1/4] BTHAB-186: Rename service class The renaming is to align with other objects and provides more meaningful --- CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php | 4 ++-- CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php | 2 +- ...tribution.php => CaseSalesOrderContributionCalculator.php} | 2 +- .../CaseSalesOrder/ComputeTotalAmountInvoicedAction.php | 2 +- .../Action/CaseSalesOrder/ComputeTotalAmountPaidAction.php | 2 +- Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) rename CRM/Civicase/Service/{CaseSaleOrderContribution.php => CaseSalesOrderContributionCalculator.php} (98%) diff --git a/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php b/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php index 412859ec4..ead823b27 100644 --- a/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php +++ b/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php @@ -9,7 +9,7 @@ class CRM_Civicase_Hook_Post_CaseSalesOrderPayment { /** - * Updates CaseSaleOrder statuses when creating a payment transcation. + * Updates CaseSalesOrder statuses when creating a payment transaction. * * @param string $op * The operation being performed. @@ -50,7 +50,7 @@ public function run($op, $objectName, $objectId, &$objectRef) { $transaction = CRM_Core_Transaction::create(); try { - $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSaleOrderContribution($salesOrderID); + $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSalesOrderContributionCalculator($salesOrderID); $paymentStatusID = $caseSaleOrderContributionService->calculatePaymentStatus(); $invoicingStatusID = $caseSaleOrderContributionService->calculateInvoicingStatus(); diff --git a/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php b/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php index c4b8a08b9..86285be05 100644 --- a/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php +++ b/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php @@ -45,7 +45,7 @@ public function run($op, $objectName, $objectId, &$objectRef) { $transaction = CRM_Core_Transaction::create(); try { - $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSaleOrderContribution($salesOrderId); + $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSalesOrderContributionCalculator($salesOrderId); $paymentStatusID = $caseSaleOrderContributionService->calculatePaymentStatus(); $invoicingStatusID = $caseSaleOrderContributionService->calculateInvoicingStatus(); diff --git a/CRM/Civicase/Service/CaseSaleOrderContribution.php b/CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php similarity index 98% rename from CRM/Civicase/Service/CaseSaleOrderContribution.php rename to CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php index 09a8af022..1e3a99ea8 100644 --- a/CRM/Civicase/Service/CaseSaleOrderContribution.php +++ b/CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php @@ -10,7 +10,7 @@ * This class provides calculations for payment and invoices that * attached to the sale order. */ -class CRM_Civicase_Service_CaseSaleOrderContribution { +class CRM_Civicase_Service_CaseSalesOrderContributionCalculator { /** * Case Sales Order object. diff --git a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountInvoicedAction.php b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountInvoicedAction.php index a7be9c48c..d35fa3298 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountInvoicedAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountInvoicedAction.php @@ -25,7 +25,7 @@ public function _run(Result $result) { // phpcs:ignore if (!$this->salesOrderID) { return; } - $service = new \CRM_Civicase_Service_CaseSaleOrderContribution($this->salesOrderID); + $service = new \CRM_Civicase_Service_CaseSalesOrderContributionCalculator($this->salesOrderID); $result['amount'] = $service->calculateTotalInvoicedAmount(); } diff --git a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountPaidAction.php b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountPaidAction.php index 9fcf1b8a2..7b022ae53 100644 --- a/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountPaidAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/ComputeTotalAmountPaidAction.php @@ -24,7 +24,7 @@ public function _run(Result $result) { // phpcs:ignore if (!$this->salesOrderID) { return; } - $service = new \CRM_Civicase_Service_CaseSaleOrderContribution($this->salesOrderID); + $service = new \CRM_Civicase_Service_CaseSalesOrderContributionCalculator($this->salesOrderID); $result['amount'] = $service->calculateTotalPaidAmount(); } diff --git a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php index ce7401a9f..1f766af03 100644 --- a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php @@ -49,7 +49,7 @@ protected function writeRecord($items) { $salesOrder['total_after_tax'] = $total['totalAfterTax']; $saleOrderId = $salesOrder['id'] ?? NULL; - $caseSaleOrderContributionService = new \CRM_Civicase_Service_CaseSaleOrderContribution($saleOrderId); + $caseSaleOrderContributionService = new \CRM_Civicase_Service_CaseSalesOrderContributionCalculator($saleOrderId); $salesOrder['payment_status_id'] = $caseSaleOrderContributionService->calculateInvoicingStatus(); $salesOrder['invoicing_status_id'] = $caseSaleOrderContributionService->calculatePaymentStatus(); From 8c46cbb94b607fe6fa6a208139004516a5870f1b Mon Sep 17 00:00:00 2001 From: Erawat Chamanont Date: Fri, 22 Sep 2023 11:26:38 +0100 Subject: [PATCH 2/4] BTHAB-186: Add case opportunity custom fields --- ...stomGroup_Cases_OpportunityDetails.mgd.php | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 managed/CustomGroup_Cases_OpportunityDetails.mgd.php diff --git a/managed/CustomGroup_Cases_OpportunityDetails.mgd.php b/managed/CustomGroup_Cases_OpportunityDetails.mgd.php new file mode 100644 index 000000000..2241110ea --- /dev/null +++ b/managed/CustomGroup_Cases_OpportunityDetails.mgd.php @@ -0,0 +1,234 @@ + 'CustomGroup_Case_Opportunity_Details', + 'entity' => 'CustomGroup', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'name' => 'Case_Opportunity_Details', + 'title' => 'Opportunity Details', + 'extends' => 'Case', + 'extends_entity_column_value' => NULL, + 'style' => 'Inline', + 'collapse_display' => FALSE, + 'help_pre' => '', + 'help_post' => '', + 'weight' => 70, + 'is_active' => TRUE, + 'is_multiple' => FALSE, + 'min_multiple' => NULL, + 'max_multiple' => NULL, + 'collapse_adv_display' => TRUE, + 'created_date' => '2023-09-21 14:46:02', + 'is_reserved' => FALSE, + 'is_public' => FALSE, + 'icon' => '', + 'extends_entity_column_id' => NULL, + ], + ], + ], + [ + 'name' => 'CustomGroup_Case_Opportunity_Details_CustomField_Total_Amount_Quoted', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Case_Opportunity_Details', + 'name' => 'Total_Amount_Quoted', + 'label' => 'Total Amount Quoted', + 'data_type' => 'Money', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => TRUE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'total_amount_quoted', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], + [ + 'name' => 'CustomGroup_Case_Opportunity_Details_CustomField_Total_Amount_Invoiced', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Case_Opportunity_Details', + 'name' => 'Total_Amount_Invoiced', + 'label' => 'Total Amount Invoiced', + 'data_type' => 'Money', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => TRUE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'total_amount_invoiced', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], + [ + 'name' => 'CustomGroup_Case_Opportunity_Details_CustomField_Invoicing_Status', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Case_Opportunity_Details', + 'name' => 'Invoicing_Status', + 'label' => 'Invoicing Status', + 'data_type' => 'String', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => TRUE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'invoicing_status', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], + [ + 'name' => 'CustomGroup_Case_Opportunity_Details_CustomField_Total_amounts_paid', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Case_Opportunity_Details', + 'name' => 'Total_Amounts_Paid', + 'label' => 'Total Amounts Paid', + 'data_type' => 'Money', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => TRUE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'total_amounts_paid', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], + [ + 'name' => 'CustomGroup_Case_Opportunity_Details_CustomField_Payments_Status', + 'entity' => 'CustomField', + 'cleanup' => 'unused', + 'update' => 'always', + 'params' => [ + 'version' => 4, + 'values' => [ + 'custom_group_id.name' => 'Case_Opportunity_Details', + 'name' => 'Payments_Status', + 'label' => 'Payments Status', + 'data_type' => 'String', + 'html_type' => 'Text', + 'default_value' => NULL, + 'is_required' => FALSE, + 'is_searchable' => FALSE, + 'is_search_range' => FALSE, + 'help_pre' => NULL, + 'help_post' => NULL, + 'mask' => NULL, + 'attributes' => NULL, + 'javascript' => NULL, + 'is_active' => TRUE, + 'is_view' => TRUE, + 'options_per_line' => NULL, + 'text_length' => 255, + 'start_date_years' => NULL, + 'end_date_years' => NULL, + 'date_format' => NULL, + 'time_format' => NULL, + 'note_columns' => 60, + 'note_rows' => 4, + 'column_name' => 'payments_status', + 'serialize' => 0, + 'filter' => NULL, + 'in_selector' => FALSE, + ], + ], + ], +]; From e94cc28a6153e954ea3848c77c7bc9d62bbe8282 Mon Sep 17 00:00:00 2001 From: Erawat Chamanont Date: Fri, 22 Sep 2023 12:27:20 +0100 Subject: [PATCH 3/4] BTHAB-186: Update case opportunity details Update case opportunity statuses, and total amounts when: * Create or update sales order contribution * Create or update sales order * Create a payment --- .../Hook/Post/CaseSalesOrderPayment.php | 76 ++++++-- .../Post/CreateSalesOrderContribution.php | 19 +- .../AbstractBaseSalesOrderCalculator.php | 94 ++++++++++ .../CaseSalesOrderContributionCalculator.php | 80 +++----- .../CaseSalesOrderOpportunityCalculator.php | 171 ++++++++++++++++++ .../CaseSalesOrder/SalesOrderSaveAction.php | 26 +++ 6 files changed, 388 insertions(+), 78 deletions(-) create mode 100644 CRM/Civicase/Service/AbstractBaseSalesOrderCalculator.php create mode 100644 CRM/Civicase/Service/CaseSalesOrderOpportunityCalculator.php diff --git a/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php b/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php index ead823b27..894a0d81a 100644 --- a/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php +++ b/CRM/Civicase/Hook/Post/CaseSalesOrderPayment.php @@ -21,28 +21,37 @@ class CRM_Civicase_Hook_Post_CaseSalesOrderPayment { * Object reference. */ public function run($op, $objectName, $objectId, &$objectRef) { - if (!$this->shouldRun($op, $objectName)) { + if (!$this->shouldRun($op, $objectName, $objectRef)) { return; } - $entityFinancialTrxn = civicrm_api3('EntityFinancialTrxn', 'get', [ - 'sequential' => 1, - 'entity_table' => 'civicrm_contribution', - 'financial_trxn_id' => $objectRef->financial_trxn_id, - ]); - - if (empty($entityFinancialTrxn['values'][0])) { + $financialTrnxId = $objectRef->financial_trxn_id; + if (empty($financialTrnxId)) { return; } - $contributionId = $entityFinancialTrxn['values'][0]['entity_id']; + $contributionId = $this->getContributionId($financialTrnxId); + if (empty($contributionId)) { + return; + } - $salesOrderID = Contribution::get() - ->addSelect('Opportunity_Details.Quotation') + $contribution = Contribution::get() + ->addSelect('Opportunity_Details.Case_Opportunity', 'Opportunity_Details.Quotation') ->addWhere('id', '=', $contributionId) ->execute() - ->first()['Opportunity_Details.Quotation']; + ->first(); + + $this->updateQuotationFinancialStatuses($contribution['Opportunity_Details.Quotation']); + $this->updateCaseOpportunityFinancialDetails($contribution['Opportunity_Details.Case_Opportunity']); + } + /** + * Updates CaseSalesOrder financial statuses. + * + * @param int $salesOrderID + * CaseSalesOrder ID. + */ + private function updateQuotationFinancialStatuses(int $salesOrderID): void { if (empty($salesOrderID)) { return; } @@ -68,6 +77,43 @@ public function run($op, $objectName, $objectId, &$objectRef) { } } + /** + * Updates Case financial statuses. + * + * @param int? $caseId + * CaseSalesOrder ID. + */ + private function updateCaseOpportunityFinancialDetails(?int $caseId) { + if (empty($caseId)) { + return; + } + + try { + $calculator = new CRM_Civicase_Service_CaseSalesOrderOpportunityCalculator($caseId); + $calculator->updateOpportunityFinancialDetails(); + } + catch (\Throwable $th) { + CRM_Core_Error::statusBounce(ts('Error updating opportunity details')); + } + } + + /** + * Gets Contribution ID by Financial Transaction ID. + */ + private function getContributionId($financialTrxnId) { + $entityFinancialTrxn = civicrm_api3('EntityFinancialTrxn', 'get', [ + 'sequential' => 1, + 'entity_table' => 'civicrm_contribution', + 'financial_trxn_id' => $financialTrxnId, + ]); + + if (empty($entityFinancialTrxn['values'][0])) { + return NULL; + } + + return $entityFinancialTrxn['values'][0]['entity_id']; + } + /** * Determines if the hook should run or not. * @@ -75,12 +121,14 @@ public function run($op, $objectName, $objectId, &$objectRef) { * The operation being performed. * @param string $objectName * Object name. + * @param string $objectRef + * The hook object reference. * * @return bool * returns a boolean to determine if hook will run or not. */ - private function shouldRun($op, $objectName) { - return $objectName == 'EntityFinancialTrxn' && $op == 'create'; + private function shouldRun($op, $objectName, $objectRef) { + return $objectName == 'EntityFinancialTrxn' && $op == 'create' && property_exists($objectRef, 'financial_trxn_id'); } } diff --git a/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php b/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php index 86285be05..e93bf6078 100644 --- a/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php +++ b/CRM/Civicase/Hook/Post/CreateSalesOrderContribution.php @@ -34,13 +34,15 @@ public function run($op, $objectName, $objectId, &$objectRef) { return; } + $salesOrder = CaseSalesOrder::get() + ->addSelect('status_id', 'case_id') + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->first(); + $salesOrderStatusId = CRM_Utils_Request::retrieve('sales_order_status_id', 'Integer'); if (empty($salesOrderStatusId)) { - $salesOrderStatusId = CaseSalesOrder::get() - ->addSelect('status_id') - ->addWhere('id', '=', $salesOrderId) - ->execute() - ->first()['status_id']; + $salesOrder = $salesOrder['status_id']; } $transaction = CRM_Core_Transaction::create(); @@ -55,6 +57,13 @@ public function run($op, $objectName, $objectId, &$objectRef) { ->addValue('invoicing_status_id', $invoicingStatusID) ->addValue('payment_status_id', $paymentStatusID) ->execute(); + + $caseId = $salesOrder['case_id']; + if (empty($caseId)) { + return; + } + $calculator = new CRM_Civicase_Service_CaseSalesOrderOpportunityCalculator($caseId); + $calculator->updateOpportunityFinancialDetails(); } catch (\Throwable $th) { $transaction->rollback(); diff --git a/CRM/Civicase/Service/AbstractBaseSalesOrderCalculator.php b/CRM/Civicase/Service/AbstractBaseSalesOrderCalculator.php new file mode 100644 index 000000000..1e9b74b2c --- /dev/null +++ b/CRM/Civicase/Service/AbstractBaseSalesOrderCalculator.php @@ -0,0 +1,94 @@ +paymentStatusOptionValues = $this->getOptionValues('case_sales_order_payment_status'); + $this->invoicingStatusOptionValues = $this->getOptionValues('case_sales_order_invoicing_status'); + } + + /** + * Gets option values by option group name. + * + * @param string $name + * Option group name. + * + * @return array + * Option values. + * + * @throws API_Exception + * @throws \Civi\API\Exception\UnauthorizedException + */ + protected function getOptionValues($name) { + return OptionValue::get(FALSE) + ->addSelect('*') + ->addWhere('option_group_id:name', '=', $name) + ->execute() + ->getArrayCopy(); + } + + /** + * Gets status (option values' value) from the given options. + * + * @param string $needle + * Search value. + * @param array $options + * Option value. + * + * @return string + * Option values' value. + */ + protected function getValueFromOptionValues($needle, $options) { + $key = array_search($needle, array_column($options, 'name')); + + return $options[$key]['value']; + } + + /** + * Gets status (option values' label) from the given options. + * + * @param string $needle + * Search value. + * @param array $options + * Option value. + * + * @return string + * Option values' value. + */ + protected function getLabelFromOptionValues($needle, $options) { + $key = array_search($needle, array_column($options, 'name')); + + return $options[$key]['label']; + } + +} diff --git a/CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php b/CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php index 1e3a99ea8..2e44c8470 100644 --- a/CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php +++ b/CRM/Civicase/Service/CaseSalesOrderContributionCalculator.php @@ -2,7 +2,6 @@ use Civi\Api4\CaseSalesOrder; use Civi\Api4\Contribution; -use Civi\Api4\OptionValue; /** * Case Sale Order Contribution Service. @@ -10,7 +9,7 @@ * This class provides calculations for payment and invoices that * attached to the sale order. */ -class CRM_Civicase_Service_CaseSalesOrderContributionCalculator { +class CRM_Civicase_Service_CaseSalesOrderContributionCalculator extends CRM_Civicase_Service_AbstractBaseSalesOrderCalculator { /** * Case Sales Order object. @@ -18,18 +17,7 @@ class CRM_Civicase_Service_CaseSalesOrderContributionCalculator { * @var array|null */ private ?array $salesOrder; - /** - * Case Sales Order payment status option values. - * - * @var array - */ - private array $paymentStatusOptionValues; - /** - * Case Sales Order Invoicing status option values. - * - * @var array - */ - private array $invoicingStatusOptionValues; + /** * List of contributions that links to the sales order. * @@ -60,12 +48,12 @@ class CRM_Civicase_Service_CaseSalesOrderContributionCalculator { * @throws \Civi\API\Exception\UnauthorizedException */ public function __construct($salesOrderId) { + parent::__construct(); $this->salesOrder = $this->getSalesOrder($salesOrderId); - $this->paymentStatusOptionValues = $this->getOptionValues('case_sales_order_payment_status'); - $this->invoicingStatusOptionValues = $this->getOptionValues('case_sales_order_invoicing_status'); $this->contributions = $this->getContributions(); $this->totalInvoicedAmount = $this->getTotalInvoicedAmount(); $this->totalPaymentsAmount = $this->getTotalPaymentsAmount(); + } /** @@ -89,6 +77,13 @@ public function calculateTotalPaidAmount(): float { return $this->getTotalPaymentsAmount(); } + /** + * Gets SalesOrder Total amount after tax. + */ + public function getQuotedAmount(): float { + return $this->salesOrder['total_after_tax']; + } + /** * Calculates invoicing status. * @@ -97,15 +92,15 @@ public function calculateTotalPaidAmount(): float { */ public function calculateInvoicingStatus() { if (empty($this->salesOrder) || empty($this->contributions)) { - return $this->getStatus('no_invoices', $this->invoicingStatusOptionValues); + return $this->getValueFromOptionValues(parent::INVOICING_STATUS_NO_INVOICES, $this->invoicingStatusOptionValues); } $quotationTotalAmount = $this->salesOrder['total_after_tax']; if ($this->totalInvoicedAmount < $quotationTotalAmount) { - return $this->getStatus('partially_invoiced', $this->invoicingStatusOptionValues); + return $this->getValueFromOptionValues(parent::INVOICING_STATUS_PARTIALLY_INVOICED, $this->invoicingStatusOptionValues); } - return $this->getStatus('fully_invoiced', $this->invoicingStatusOptionValues); + return $this->getValueFromOptionValues(parent::INVOICING_STATUS_FULLY_INVOICED, $this->invoicingStatusOptionValues); } /** @@ -117,14 +112,18 @@ public function calculateInvoicingStatus() { public function calculatePaymentStatus() { if (empty($this->salesOrder) || empty($this->contributions) || !($this->totalPaymentsAmount > 0)) { - return $this->getStatus('no_payments', $this->paymentStatusOptionValues); + return $this->getValueFromOptionValues(parent::PAYMENT_STATUS_NO_PAYMENTS, $this->paymentStatusOptionValues); } if ($this->totalPaymentsAmount < $this->totalInvoicedAmount) { - return $this->getStatus('partially_paid', $this->paymentStatusOptionValues); + return $this->getValueFromOptionValues(parent::PAYMENT_STATUS_PARTIALLY_PAID, $this->paymentStatusOptionValues); } - return $this->getStatus('fully_paid', $this->paymentStatusOptionValues); + if ($this->totalPaymentsAmount > $this->totalInvoicedAmount) { + return $this->getValueFromOptionValues(parent::PAYMENT_STATUS_OVERPAID, $this->paymentStatusOptionValues); + } + + return $this->getValueFromOptionValues(parent::PAYMENT_STATUS_FULLY_PAID, $this->paymentStatusOptionValues); } /** @@ -166,26 +165,6 @@ private function getSalesOrder($saleOrderId) { ->first(); } - /** - * Gets option values by option group name. - * - * @param string $name - * Option group name. - * - * @return array - * Option values. - * - * @throws API_Exception - * @throws \Civi\API\Exception\UnauthorizedException - */ - private function getOptionValues($name) { - return OptionValue::get() - ->addSelect('*') - ->addWhere('option_group_id:name', '=', $name) - ->execute() - ->getArrayCopy(); - } - /** * Gets total invoiced amount. * @@ -223,21 +202,4 @@ private function getTotalPaymentsAmount(): float { return $totalPaymentsAmount; } - /** - * Gets status (option values' value) from the given options. - * - * @param string $needle - * Search value. - * @param array $options - * Option value. - * - * @return string - * Option values' value. - */ - private function getStatus($needle, $options) { - $key = array_search($needle, array_column($options, 'name')); - - return $options[$key]['value']; - } - } diff --git a/CRM/Civicase/Service/CaseSalesOrderOpportunityCalculator.php b/CRM/Civicase/Service/CaseSalesOrderOpportunityCalculator.php new file mode 100644 index 000000000..b73923908 --- /dev/null +++ b/CRM/Civicase/Service/CaseSalesOrderOpportunityCalculator.php @@ -0,0 +1,171 @@ +caseId = $caseId; + $contributions = $this->getContributions($caseId); + $this->calculateOpportunityFinancialAmount($contributions); + } + + /** + * Calculates total invoiced amount. + * + * @return float + * Total invoiced amount. + */ + public function calculateTotalInvoicedAmount(): float { + return $this->totalInvoicedAmount; + } + + /** + * Calculates total paid amount. + * + * @return float + * Total paid amounts. + */ + public function calculateTotalPaidAmount(): float { + return $this->totalPaidAmount; + } + + /** + * Calculates total quoted amount. + * + * @return float + * Total paid amounts. + */ + public function calculateTotalQuotedAmount(): float { + return $this->totalQuotedAmount; + } + + /** + * Calculates opportunity invoicing status. + * + * @return string + * Invoicing status option value's value + */ + public function calculateInvoicingStatus() { + if (!($this->totalInvoicedAmount > 0)) { + return $this->getLabelFromOptionValues(parent::INVOICING_STATUS_NO_INVOICES, $this->invoicingStatusOptionValues); + } + + if ($this->totalInvoicedAmount < $this->totalQuotedAmount) { + return $this->getLabelFromOptionValues(parent::INVOICING_STATUS_PARTIALLY_INVOICED, $this->invoicingStatusOptionValues); + } + + return $this->getLabelFromOptionValues(parent::INVOICING_STATUS_FULLY_INVOICED, $this->invoicingStatusOptionValues); + } + + /** + * Calculates opportunity payment status. + * + * @return string + * Payment status option value's value + */ + public function calculatePaymentStatus() { + if (!($this->totalPaidAmount > 0)) { + return $this->getLabelFromOptionValues(parent::PAYMENT_STATUS_NO_PAYMENTS, $this->paymentStatusOptionValues); + } + + if ($this->totalPaidAmount < $this->totalInvoicedAmount) { + return $this->getLabelFromOptionValues(parent::PAYMENT_STATUS_PARTIALLY_PAID, $this->paymentStatusOptionValues); + } + + if ($this->totalPaidAmount > $this->totalInvoicedAmount) { + return $this->getLabelFromOptionValues(parent::PAYMENT_STATUS_OVERPAID, $this->paymentStatusOptionValues); + + } + + return $this->getLabelFromOptionValues(parent::PAYMENT_STATUS_FULLY_PAID, $this->paymentStatusOptionValues); + } + + /** + * Updates opportunity financial details. + */ + public function updateOpportunityFinancialDetails(): void { + CiviCase::update() + ->addValue('Case_Opportunity_Details.Total_Amount_Quoted', $this->calculateTotalQuotedAmount()) + ->addValue('Case_Opportunity_Details.Total_Amount_Invoiced', $this->calculateTotalQuotedAmount()) + ->addValue('Case_Opportunity_Details.Invoicing_Status', $this->calculateInvoicingStatus()) + ->addValue('Case_Opportunity_Details.Total_Amounts_Paid', $this->calculateTotalPaidAmount()) + ->addValue('Case_Opportunity_Details.Payments_Status', $this->calculatePaymentStatus()) + ->addWhere('id', '=', $this->caseId) + ->execute(); + } + + /** + * Calculates opportunity financial amounts. + * + * @param array $contributions + * List of contributions that link to the opportunity. + */ + private function calculateOpportunityFinancialAmount($contributions) { + $totalQuotedAmount = 0; + $totalInvoicedAmount = 0; + $totalPaidAmount = 0; + foreach ($contributions as $contribution) { + $salesOrderId = $contribution['Opportunity_Details.Quotation']; + $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSalesOrderContributionCalculator($salesOrderId); + + $totalQuotedAmount += $caseSaleOrderContributionService->getQuotedAmount(); + $totalPaidAmount += $caseSaleOrderContributionService->calculateTotalPaidAmount(); + $totalInvoicedAmount += $caseSaleOrderContributionService->calculateTotalInvoicedAmount(); + } + + $this->totalQuotedAmount = $totalQuotedAmount; + $this->totalPaidAmount = $totalPaidAmount; + $this->totalInvoicedAmount = $totalInvoicedAmount; + } + + /** + * Gets Contributions by case Id. + * + * @param int $caseId + * List of contributions that link to the opportunity. + */ + private function getContributions($caseId) { + return Contribution::get(FALSE) + ->addSelect('*', 'Opportunity_Details.Quotation') + ->addWhere('Opportunity_Details.Case_Opportunity', '=', $caseId) + ->execute() + ->getArrayCopy(); + } + +} diff --git a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php index 1f766af03..c5918c126 100644 --- a/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php +++ b/Civi/Api4/Action/CaseSalesOrder/SalesOrderSaveAction.php @@ -2,6 +2,7 @@ namespace Civi\Api4\Action\CaseSalesOrder; +use Civi\Api4\CaseSalesOrder; use Civi\Api4\CaseSalesOrderLine; use Civi\Api4\Generic\AbstractSaveAction; use Civi\Api4\Generic\Result; @@ -53,6 +54,10 @@ protected function writeRecord($items) { $salesOrder['payment_status_id'] = $caseSaleOrderContributionService->calculateInvoicingStatus(); $salesOrder['invoicing_status_id'] = $caseSaleOrderContributionService->calculatePaymentStatus(); + if (!is_null($saleOrderId)) { + $this->updateOpportunityDetails($saleOrderId); + } + $salesOrders = $this->writeObjects([$salesOrder]); $result = array_pop($salesOrders); @@ -103,4 +108,25 @@ public function removeStaleLineItems(array $salesOrder) { ->execute(); } + /** + * Updates sales order's case opportunity details. + * + * @param int $salesOrderId + * Sales Order Id. + */ + private function updateOpportunityDetails($salesOrderId): void { + $caseSalesOrder = CaseSalesOrder::get() + ->addSelect('case_id') + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->first(); + + if (empty($caseSalesOrder)) { + return; + } + + $caseSaleOrderContributionService = new \CRM_Civicase_Service_CaseSalesOrderOpportunityCalculator($caseSalesOrder['case_id']); + $caseSaleOrderContributionService->updateOpportunityFinancialDetails(); + } + } From 00cf2f17388de098b0e62f5505cf22802d538527 Mon Sep 17 00:00:00 2001 From: Erawat Chamanont Date: Fri, 22 Sep 2023 13:09:21 +0100 Subject: [PATCH 4/4] BTHAB-185: Update statuses on contribution deletion --- .../Hook/Pre/DeleteSalesOrderContribution.php | 79 +++++++++++++++++++ civicase.php | 13 +++ 2 files changed, 92 insertions(+) create mode 100644 CRM/Civicase/Hook/Pre/DeleteSalesOrderContribution.php diff --git a/CRM/Civicase/Hook/Pre/DeleteSalesOrderContribution.php b/CRM/Civicase/Hook/Pre/DeleteSalesOrderContribution.php new file mode 100644 index 000000000..e8d5c9f98 --- /dev/null +++ b/CRM/Civicase/Hook/Pre/DeleteSalesOrderContribution.php @@ -0,0 +1,79 @@ +shouldRun($op, $objectName)) { + return; + } + + $salesOrderId = $this->getQuotationId($objectId); + if (empty($salesOrderId)) { + return; + } + + $caseSaleOrderContributionService = new CRM_Civicase_Service_CaseSalesOrderContributionCalculator($salesOrderId); + $invoicingStatusId = $caseSaleOrderContributionService->calculateInvoicingStatus(); + $paymentStatusId = $caseSaleOrderContributionService->calculatePaymentStatus(); + + CaseSalesOrder::update() + ->addWhere('id', '=', $salesOrderId) + ->addValue('invoicing_status_id', $invoicingStatusId) + ->addValue('payment_status_id', $paymentStatusId) + ->execute(); + + $caseSalesOrder = CaseSalesOrder::get() + ->addSelect('case_id') + ->addWhere('id', '=', $salesOrderId) + ->execute() + ->first(); + + $caseSaleOrderContributionService = new \CRM_Civicase_Service_CaseSalesOrderOpportunityCalculator($caseSalesOrder['case_id']); + $caseSaleOrderContributionService->updateOpportunityFinancialDetails(); + } + + /** + * Determines if the hook should run or not. + * + * @param string $op + * The operation being performed. + * @param string $objectName + * Object name. + * + * @return bool + * returns a boolean to determine if hook will run or not. + */ + private function shouldRun($op, $objectName) { + return strtolower($objectName) == 'contribution' && $op == 'delete'; + } + + /** + * Gets quotation ID by contribution ID. + */ + private function getQuotationId($id) { + return Contribution::get() + ->addSelect('Opportunity_Details.Quotation') + ->addWhere('id', '=', $id) + ->execute() + ->first()['Opportunity_Details.Quotation']; + } + +} diff --git a/civicase.php b/civicase.php index 3cf5b82a7..a2eaae277 100644 --- a/civicase.php +++ b/civicase.php @@ -258,6 +258,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(). */