diff --git a/CRM/Report/Form/Contribute/Detail.php b/CRM/Report/Form/Contribute/Detail.php new file mode 100644 index 0000000000000000000000000000000000000000..5774577239740eac872ecc2adb06eb78ce027ab6 --- /dev/null +++ b/CRM/Report/Form/Contribute/Detail.php @@ -0,0 +1,1104 @@ +<?php +/* + +--------------------------------------------------------------------+ + | 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 | + +--------------------------------------------------------------------+ + */ + +/** + * + * @package CRM + * @copyright CiviCRM LLC https://civicrm.org/licensing + */ +class CRM_Report_Form_Contribute_Detail extends CRM_Report_Form { + + protected $_summary = NULL; + + protected $_softFrom = NULL; + + protected $noDisplayContributionOrSoftColumn = FALSE; + + protected $_customGroupExtends = [ + 'Contact', + 'Individual', + 'Contribution', + ]; + + protected $groupConcatTested = TRUE; + + protected $isTempTableBuilt = FALSE; + + /** + * Query mode. + * + * This can be 'Main' or 'SoftCredit' to denote which query we are building. + * + * @var string + */ + protected $queryMode = 'Main'; + + /** + * Is this report being run on contributions as the base entity. + * + * The report structure is generally designed around a base entity but + * depending on input it can be run in a sort of hybrid way that causes a lot + * of complexity. + * + * If it is in isContributionsOnlyMode we can simplify. + * + * (arguably there should be 2 separate report templates, not one doing double duty.) + * + * @var bool + */ + protected $isContributionBaseMode = FALSE; + + /** + * This report has been optimised for group filtering. + * + * @var bool + * @see https://issues.civicrm.org/jira/browse/CRM-19170 + */ + protected $groupFilterNotOptimised = FALSE; + + /** + * Class constructor. + */ + public function __construct() { + $this->_autoIncludeIndexedFieldsAsOrderBys = 1; + $this->_columns = array_merge( + $this->getColumns('Contact', [ + 'order_bys_defaults' => ['sort_name' => 'ASC '], + 'fields_defaults' => ['sort_name'], + 'fields_excluded' => ['id'], + 'fields_required' => ['id'], + 'filters_defaults' => ['is_deleted' => 0], + 'no_field_disambiguation' => TRUE, + ]), + [ + 'civicrm_email' => [ + 'dao' => 'CRM_Core_DAO_Email', + 'fields' => [ + 'email' => [ + 'title' => ts('Donor Email'), + 'default' => TRUE, + ], + ], + 'grouping' => 'contact-fields', + ], + 'civicrm_line_item' => [ + 'dao' => 'CRM_Price_DAO_LineItem', + ], + 'civicrm_phone' => [ + 'dao' => 'CRM_Core_DAO_Phone', + 'fields' => [ + 'phone' => [ + 'title' => ts('Donor Phone'), + 'default' => TRUE, + 'no_repeat' => TRUE, + ], + ], + 'grouping' => 'contact-fields', + ], + 'civicrm_contribution' => [ + 'dao' => 'CRM_Contribute_DAO_Contribution', + 'fields' => [ + 'contribution_id' => [ + 'name' => 'id', + 'no_display' => TRUE, + 'required' => TRUE, + ], + 'list_contri_id' => [ + 'name' => 'id', + 'title' => ts('Contribution ID'), + ], + 'financial_type_id' => [ + 'title' => ts('Financial Type'), + 'default' => TRUE, + ], + 'contribution_status_id' => [ + 'title' => ts('Contribution Status'), + ], + 'contribution_page_id' => [ + 'title' => ts('Contribution Page'), + ], + 'source' => [ + 'title' => ts('Source'), + ], + 'payment_instrument_id' => [ + 'title' => ts('Payment Type'), + ], + 'check_number' => [ + 'title' => ts('Check Number'), + ], + 'trxn_id' => NULL, + 'receive_date' => ['default' => TRUE], + 'receipt_date' => NULL, + 'thankyou_date' => NULL, + 'non_deductible_amount' => [ + 'title' => ts('Non-deductible Amount'), + ], + 'fee_amount' => NULL, + 'net_amount' => NULL, + 'contribution_or_soft' => [ + 'title' => ts('Contribution OR Soft Credit?'), + 'dbAlias' => "'Contribution'", + ], + 'soft_credits' => [ + 'title' => ts('Soft Credits'), + 'dbAlias' => "NULL", + ], + 'soft_credit_for' => [ + 'title' => ts('Soft Credit For'), + 'dbAlias' => "NULL", + ], + 'cancel_date' => [ + 'title' => ts('Cancelled / Refunded Date'), + 'name' => 'contribution_cancel_date', + ], + 'cancel_reason' => [ + 'title' => ts('Cancellation / Refund Reason'), + ], + ], + 'filters' => [ + 'contribution_or_soft' => [ + 'title' => ts('Contribution OR Soft Credit?'), + 'clause' => "(1)", + 'operatorType' => CRM_Report_Form::OP_SELECT, + 'type' => CRM_Utils_Type::T_STRING, + 'options' => [ + 'contributions_only' => ts('Contributions Only'), + 'soft_credits_only' => ts('Soft Credits Only'), + 'both' => ts('Both'), + ], + 'default' => 'contributions_only', + ], + 'receive_date' => ['operatorType' => CRM_Report_Form::OP_DATE], + 'receipt_date' => ['operatorType' => CRM_Report_Form::OP_DATE], + 'thankyou_date' => ['operatorType' => CRM_Report_Form::OP_DATE], + 'contribution_source' => [ + 'title' => ts('Source'), + 'name' => 'source', + 'type' => CRM_Utils_Type::T_STRING, + ], + 'currency' => [ + 'title' => ts('Currency'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Core_OptionGroup::values('currencies_enabled'), + 'default' => NULL, + 'type' => CRM_Utils_Type::T_STRING, + ], + 'non_deductible_amount' => [ + 'title' => ts('Non-deductible Amount'), + ], + 'financial_type_id' => [ + 'title' => ts('Financial Type'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Contribute_BAO_Contribution::buildOptions('financial_type_id', 'search'), + 'type' => CRM_Utils_Type::T_INT, + ], + 'contribution_page_id' => [ + 'title' => ts('Contribution Page'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Contribute_PseudoConstant::contributionPage(), + 'type' => CRM_Utils_Type::T_INT, + ], + 'payment_instrument_id' => [ + 'title' => ts('Payment Type'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Contribute_PseudoConstant::paymentInstrument(), + 'type' => CRM_Utils_Type::T_INT, + ], + 'contribution_status_id' => [ + 'title' => ts('Contribution Status'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Contribute_BAO_Contribution::buildOptions('contribution_status_id', 'search'), + 'default' => [1], + 'type' => CRM_Utils_Type::T_INT, + ], + 'total_amount' => ['title' => ts('Contribution Amount')], + 'cancel_date' => [ + 'title' => ts('Cancelled / Refunded Date'), + 'operatorType' => CRM_Report_Form::OP_DATE, + 'name' => 'contribution_cancel_date', + ], + 'cancel_reason' => [ + 'title' => ts('Cancellation / Refund Reason'), + ], + ], + 'order_bys' => [ + 'financial_type_id' => ['title' => ts('Financial Type')], + 'contribution_status_id' => ['title' => ts('Contribution Status')], + 'payment_instrument_id' => ['title' => ts('Payment Method')], + 'receive_date' => ['title' => ts('Date Received')], + 'receipt_date' => ['title' => ts('Receipt Date')], + 'thankyou_date' => ['title' => ts('Thank-you Date')], + ], + 'group_bys' => [ + 'contribution_id' => [ + 'name' => 'id', + 'title' => ts('Contribution'), + ], + ], + 'grouping' => 'contri-fields', + ], + 'civicrm_contribution_soft' => [ + 'dao' => 'CRM_Contribute_DAO_ContributionSoft', + 'fields' => [ + 'soft_credit_type_id' => ['title' => ts('Soft Credit Type')], + 'soft_credit_amount' => ['title' => ts('Soft Credit amount'), 'name' => 'amount', 'type' => CRM_Utils_Type::T_MONEY], + ], + 'filters' => [ + 'soft_credit_type_id' => [ + 'title' => ts('Soft Credit Type'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Core_OptionGroup::values('soft_credit_type'), + 'default' => NULL, + 'type' => CRM_Utils_Type::T_STRING, + ], + ], + 'group_bys' => [ + 'soft_credit_id' => [ + 'name' => 'id', + 'title' => ts('Soft Credit'), + ], + ], + ], + 'civicrm_financial_trxn' => [ + 'dao' => 'CRM_Financial_DAO_FinancialTrxn', + 'fields' => [ + 'financial_trxn_id' => [ + 'name' => 'id', + 'no_display' => TRUE, + 'required' => TRUE, + ], + 'total_amount' => [ + 'title' => ts('Total Amount'), + 'required' => TRUE, + ], + 'currency' => [ + 'title' => 'Currency', + 'required' => TRUE, + 'no_display' => TRUE, + ], + 'trxn_date' => [ + 'title' => ts('Transaction Date'), + ], + 'status_id' => [ + 'title' => ts('Status'), + ], + 'card_type_id' => [ + 'title' => ts('Credit Card Type'), + ], + ], + 'filters' => [ + 'card_type_id' => [ + 'title' => ts('Credit Card Type'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Financial_DAO_FinancialTrxn::buildOptions('card_type_id'), + 'default' => NULL, + 'type' => CRM_Utils_Type::T_STRING, + ], + 'trxn_date' => [ + 'title' => ts('Transaction Date'), +'type' => CRM_Utils_Type::T_DATE, + 'operatorType' => CRM_Report_Form::OP_DATE, + ], + ], + 'group_bys' => [ + 'financial_trxn_id' => [ + 'name' => 'id', + 'required' => TRUE, + 'default' => TRUE, + 'title' => ts('Financial Trxn'), + ], + ], + ], + 'civicrm_batch' => [ + 'dao' => 'CRM_Batch_DAO_EntityBatch', + 'grouping' => 'contri-fields', + 'fields' => [ + 'batch_id' => [ + 'name' => 'batch_id', + 'title' => ts('Batch Name'), + ], + ], + 'filters' => [ + 'bid' => [ + 'title' => ts('Batch Name'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Batch_BAO_Batch::getBatches(), + 'type' => CRM_Utils_Type::T_INT, + 'dbAlias' => 'batch_civireport.batch_id', + ], + ], + ], + 'civicrm_contribution_ordinality' => [ + 'dao' => 'CRM_Contribute_DAO_Contribution', + 'alias' => 'cordinality', + 'filters' => [ + 'ordinality' => [ + 'title' => ts('Contribution Ordinality'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => [ + 0 => ts('First by Contributor'), + 1 => ts('Second or Later by Contributor'), + ], + 'type' => CRM_Utils_Type::T_INT, + ], + ], + ], + 'civicrm_note' => [ + 'dao' => 'CRM_Core_DAO_Note', + 'fields' => [ + 'contribution_note' => [ + 'name' => 'note', + 'title' => ts('Contribution Note'), + ], + ], + 'filters' => [ + 'note' => [ + 'name' => 'note', + 'title' => ts('Contribution Note'), + 'operator' => 'like', + 'type' => CRM_Utils_Type::T_STRING, + ], + ], + ], + 'civicrm_pledge_payment' => [ + 'dao' => 'CRM_Pledge_DAO_PledgePayment', + 'fields' => [ + 'pledge_id' => [ + 'title' => ts('Pledge ID'), + ], + ], + 'filters' => [ + 'pledge_id' => [ + 'title' => ts('Pledge ID'), + 'type' => CRM_Utils_Type::T_INT, + ], + ], + ], + ], + $this->getColumns('Address') + ); + // The tests test for this variation of the sort_name field. Don't argue with the tests :-). + $this->_columns['civicrm_contact']['fields']['sort_name']['title'] = ts('Donor Name'); + $this->_groupFilter = TRUE; + $this->_tagFilter = TRUE; + // If we have campaigns enabled, add those elements to both the fields, filters and sorting + $this->addCampaignFields('civicrm_contribution', FALSE, TRUE); + + $this->_currencyColumn = 'civicrm_financial_trxn_currency'; + parent::__construct(); + } + + /** + * Validate incompatible report settings. + * + * @return bool + * true if no error found + */ + public function validate() { + // If you're displaying Contributions Only, you can't group by soft credit. + $contributionOrSoftVal = $this->getElementValue('contribution_or_soft_value'); + if ($contributionOrSoftVal[0] == 'contributions_only') { + $groupBySoft = $this->getElementValue('group_bys'); + if (!empty($groupBySoft['soft_credit_id'])) { + $this->setElementError('group_bys', ts('You cannot group by soft credit when displaying contributions only. Please uncheck "Soft Credit" in the Grouping tab.')); + } + } + + return parent::validate(); + } + + /** + * Set the FROM clause for the report. + */ + public function from() { + $this->setFromBase('civicrm_contact'); + $this->_from .= " + INNER JOIN civicrm_contribution {$this->_aliases['civicrm_contribution']} + ON {$this->_aliases['civicrm_contact']}.id = {$this->_aliases['civicrm_contribution']}.contact_id + AND {$this->_aliases['civicrm_contribution']}.is_test = 0 + AND {$this->_aliases['civicrm_contribution']}.is_template = 0 + "; + + $this->joinContributionToSoftCredit(); + $this->appendAdditionalFromJoins(); + } + + /** + * @param array $rows + * + * @return array + * @throws \CRM_Core_Exception + */ + public function statistics(&$rows) { + $statistics = parent::statistics($rows); + + $totalAmount = $average = $fees = $net = []; + $count = 0; + $select = " + SELECT COUNT(civicrm_financial_trxn_total_amount ) as count, + SUM( civicrm_financial_trxn_total_amount ) as amount, + ROUND(AVG(civicrm_financial_trxn_total_amount), 2) as avg, + stats.currency as currency, + SUM( stats.fee_amount ) as fees, + SUM( stats.net_amount ) as net + "; + + $group = "\nGROUP BY civicrm_financial_trxn_currency"; + $from = " FROM {$this->temporaryTables['civireport_contribution_detail_temp3']['name']} " + . "JOIN civicrm_financial_trxn stats ON {$this->temporaryTables['civireport_contribution_detail_temp3']['name']}.civicrm_financial_trxn_financial_trxn_id = stats.id "; + $sql = "{$select} {$from} {$group} "; + CRM_Core_DAO::disableFullGroupByMode(); + $dao = CRM_Core_DAO::executeQuery($sql); + CRM_Core_DAO::reenableFullGroupByMode(); + $this->addToDeveloperTab($sql); + + while ($dao->fetch()) { + CRM_Core_Error::debug_var('a', $dao); + $totalAmount[] = CRM_Utils_Money::format($dao->amount, $dao->currency) . " (" . $dao->count . ")"; + $fees[] = CRM_Utils_Money::format($dao->fees, $dao->currency); + $net[] = CRM_Utils_Money::format($dao->net, $dao->currency); + $average[] = CRM_Utils_Money::format($dao->avg, $dao->currency); + $count += $dao->count; + } + $statistics['counts']['amount'] = [ + 'title' => ts('Total Amount (Contributions)'), + 'value' => implode(', ', $totalAmount), + 'type' => CRM_Utils_Type::T_STRING, + ]; + $statistics['counts']['count'] = [ + 'title' => ts('Total Contributions'), + 'value' => $count, + ]; + $statistics['counts']['fees'] = [ + 'title' => ts('Fees'), + 'value' => implode(', ', $fees), + 'type' => CRM_Utils_Type::T_STRING, + ]; + $statistics['counts']['net'] = [ + 'title' => ts('Net'), + 'value' => implode(', ', $net), + 'type' => CRM_Utils_Type::T_STRING, + ]; + $statistics['counts']['avg'] = [ + 'title' => ts('Average'), + 'value' => implode(', ', $average), + 'type' => CRM_Utils_Type::T_STRING, + ]; + + // Stats for soft credits + if ($this->_softFrom && + CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) != + 'contributions_only' + ) { + $totalAmount = $average = []; + $count = 0; + $select = " +SELECT COUNT(contribution_soft_civireport.amount ) as count, + SUM(contribution_soft_civireport.amount ) as amount, + ROUND(AVG(contribution_soft_civireport.amount), 2) as avg, + {$this->_aliases['civicrm_contribution']}.currency as currency"; + $sql = " +{$select} +{$this->_softFrom} +GROUP BY {$this->_aliases['civicrm_contribution']}.currency"; + $dao = CRM_Core_DAO::executeQuery($sql); + $this->addToDeveloperTab($sql); + while ($dao->fetch()) { + $totalAmount[] = CRM_Utils_Money::format($dao->amount, $dao->currency) . " (" . + $dao->count . ")"; + $average[] = CRM_Utils_Money::format($dao->avg, $dao->currency); + $count += $dao->count; + } + $statistics['counts']['softamount'] = [ + 'title' => ts('Total Amount (Soft Credits)'), + 'value' => implode(', ', $totalAmount), + 'type' => CRM_Utils_Type::T_STRING, + ]; + $statistics['counts']['softcount'] = [ + 'title' => ts('Total Soft Credits'), + 'value' => $count, + ]; + $statistics['counts']['softavg'] = [ + 'title' => ts('Average (Soft Credits)'), + 'value' => implode(', ', $average), + 'type' => CRM_Utils_Type::T_STRING, + ]; + } + + return $statistics; + } + + /** + * Build the report query. + * + * @param bool $applyLimit + * + * @return string + */ + public function buildQuery($applyLimit = FALSE) { + if ($this->isTempTableBuilt) { + $this->limit(); + return "SELECT SQL_CALC_FOUND_ROWS * FROM {$this->temporaryTables['civireport_contribution_detail_temp3']['name']} $this->_orderBy $this->_limit"; + } + return parent::buildQuery($applyLimit); + } + + /** + * Shared function for preliminary processing. + * + * This is called by the api / unit tests and the form layer and is + * the right place to do 'initial analysis of input'. + */ + public function beginPostProcessCommon() { + // CRM-18312 - display soft_credits and soft_credits_for column + // when 'Contribution or Soft Credit?' column is not selected + if (empty($this->_params['fields']['contribution_or_soft'])) { + $this->_params['fields']['contribution_or_soft'] = 1; + $this->noDisplayContributionOrSoftColumn = TRUE; + } + + if (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == 'contributions_only') { + $this->isContributionBaseMode = TRUE; + } + if ($this->isContributionBaseMode && + (!empty($this->_params['fields']['soft_credit_type_id']) + || !empty($this->_params['soft_credit_type_id_value'])) + ) { + unset($this->_params['fields']['soft_credit_type_id']); + if (!empty($this->_params['soft_credit_type_id_value'])) { + $this->_params['soft_credit_type_id_value'] = []; + CRM_Core_Session::setStatus(ts('Is it not possible to filter on soft contribution type when not including soft credits.')); + } + } + // 1. use main contribution query to build temp table 1 + $sql = $this->buildQuery(); + $this->createTemporaryTable('civireport_contribution_detail_temp1', $sql); + + // 2. customize main contribution query for soft credit, and build temp table 2 with soft credit contributions only + $this->queryMode = 'SoftCredit'; + // Rebuild select with no groupby. Do not let column headers change. + $headers = $this->_columnHeaders; + $this->select(); + $this->_columnHeaders = $headers; + $this->softCreditFrom(); + // also include custom group from if included + // since this might be included in select + $this->customDataFrom(); + + $select = str_ireplace('contribution_civireport.total_amount', 'contribution_soft_civireport.amount', $this->_select); + $select = str_ireplace("'Contribution' as", "'Soft Credit' as", $select); + + // we inner join with temp1 to restrict soft contributions to those in temp1 table. + // no group by here as we want to display as many soft credit rows as actually exist. + CRM_Utils_Hook::alterReportVar('sql', $this, $this); + $sql = "{$select} {$this->_from} {$this->_where} $this->_groupBy"; + $this->createTemporaryTable('civireport_contribution_detail_temp2', $sql); + + if (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == + 'soft_credits_only' + ) { + // revise pager : prev, next based on soft-credits only + $this->setPager(); + } + + // copy _from for later use of stats calculation for soft credits, and reset $this->_from to main query + $this->_softFrom = $this->_from; + + // simple reset of ->_from + $this->from(); + + // also include custom group from if included + // since this might be included in select + $this->customDataFrom(); + + // 3. Decide where to populate temp3 table from + if ($this->isContributionBaseMode + ) { + $this->createTemporaryTable('civireport_contribution_detail_temp3', + "(SELECT * FROM {$this->temporaryTables['civireport_contribution_detail_temp1']['name']})" + ); + } + elseif (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == + 'soft_credits_only' + ) { + $this->createTemporaryTable('civireport_contribution_detail_temp3', + "(SELECT * FROM {$this->temporaryTables['civireport_contribution_detail_temp2']['name']})" + ); + } + else { + $this->createTemporaryTable('civireport_contribution_detail_temp3', " +(SELECT * FROM {$this->temporaryTables['civireport_contribution_detail_temp1']['name']}) +UNION ALL +(SELECT * FROM {$this->temporaryTables['civireport_contribution_detail_temp2']['name']})"); + } + $this->isTempTableBuilt = TRUE; + } + + /** + * Store group bys into array - so we can check elsewhere what is grouped. + * + * If we are generating a table of soft credits we need to group by them. + */ + protected function storeGroupByArray() { + if ($this->queryMode === 'SoftCredit') { + $this->_groupByArray = [$this->_aliases['civicrm_contribution_soft'] . '.id']; + } + else { + parent::storeGroupByArray(); + } + } + + /** + * Alter display of rows. + * + * Iterate through the rows retrieved via SQL and make changes for display purposes, + * such as rendering contacts as links. + * + * @param array $rows + * Rows generated by SQL, with an array for each row. + */ + public function alterDisplay(&$rows) { + $entryFound = FALSE; + $display_flag = $prev_cid = $cid = 0; + $contributionTypes = CRM_Contribute_PseudoConstant::financialType(); + $contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'label'); + $paymentInstruments = CRM_Contribute_PseudoConstant::paymentInstrument(); + // We pass in TRUE as 2nd param so that even disabled contribution page titles are returned and replaced in the report + $contributionPages = CRM_Contribute_PseudoConstant::contributionPage(NULL, TRUE); + $batches = CRM_Batch_BAO_Batch::getBatches(); + foreach ($rows as $rowNum => $row) { + if (!empty($this->_noRepeats) && $this->_outputMode != 'csv') { + // don't repeat contact details if its same as the previous row + if (array_key_exists('civicrm_contact_id', $row)) { + if ($cid = $row['civicrm_contact_id']) { + if ($rowNum == 0) { + $prev_cid = $cid; + } + else { + if ($prev_cid == $cid) { + $display_flag = 1; + $prev_cid = $cid; + } + else { + $display_flag = 0; + $prev_cid = $cid; + } + } + + if ($display_flag) { + foreach ($row as $colName => $colVal) { + // Hide repeats in no-repeat columns, but not if the field's a section header + if (in_array($colName, $this->_noRepeats) && + !array_key_exists($colName, $this->_sections) + ) { + unset($rows[$rowNum][$colName]); + } + } + } + $entryFound = TRUE; + } + } + } + + if (CRM_Utils_Array::value('civicrm_contribution_contribution_or_soft', $rows[$rowNum]) == + 'Contribution' + ) { + unset($rows[$rowNum]['civicrm_contribution_soft_soft_credit_type_id']); + } + + $entryFound = $this->alterDisplayContactFields($row, $rows, $rowNum, 'contribution/detail', ts('View Contribution Details')) ? TRUE : $entryFound; + // convert donor sort name to link + if (array_key_exists('civicrm_contact_sort_name', $row) && + !empty($rows[$rowNum]['civicrm_contact_sort_name']) && + array_key_exists('civicrm_contact_id', $row) + ) { + $url = CRM_Utils_System::url("civicrm/contact/view", + 'reset=1&cid=' . $row['civicrm_contact_id'], + $this->_absoluteUrl + ); + $rows[$rowNum]['civicrm_contact_sort_name_link'] = $url; + $rows[$rowNum]['civicrm_contact_sort_name_hover'] = ts("View Contact Summary for this Contact."); + } + + if ($value = CRM_Utils_Array::value('civicrm_contribution_financial_type_id', $row)) { + $rows[$rowNum]['civicrm_contribution_financial_type_id'] = $contributionTypes[$value]; + $entryFound = TRUE; + } + if ($value = CRM_Utils_Array::value('civicrm_contribution_contribution_status_id', $row)) { + $rows[$rowNum]['civicrm_contribution_contribution_status_id'] = $contributionStatus[$value]; + $entryFound = TRUE; + } + if ($value = CRM_Utils_Array::value('civicrm_financial_trxn_status_id', $row)) { + $rows[$rowNum]['civicrm_financial_trxn_status_id'] = $contributionStatus[$value]; + $entryFound = TRUE; + } + if ($value = CRM_Utils_Array::value('civicrm_contribution_contribution_page_id', $row)) { + $rows[$rowNum]['civicrm_contribution_contribution_page_id'] = $contributionPages[$value]; + $entryFound = TRUE; + } + if ($value = CRM_Utils_Array::value('civicrm_contribution_payment_instrument_id', $row)) { + $rows[$rowNum]['civicrm_contribution_payment_instrument_id'] = $paymentInstruments[$value]; + $entryFound = TRUE; + } + if (!empty($row['civicrm_batch_batch_id'])) { + $rows[$rowNum]['civicrm_batch_batch_id'] = $batches[$row['civicrm_batch_batch_id']] ?? NULL; + $entryFound = TRUE; + } + if (!empty($row['civicrm_financial_trxn_card_type_id'])) { + $rows[$rowNum]['civicrm_financial_trxn_card_type_id'] = $this->getLabels($row['civicrm_financial_trxn_card_type_id'], 'CRM_Financial_DAO_FinancialTrxn', 'card_type_id'); + $entryFound = TRUE; + } + + // Contribution amount links to viewing contribution + + if ($value = CRM_Utils_Array::value('civicrm_contribution_total_amount', $row)) { + $rows[$rowNum]['civicrm_contribution_total_amount'] = CRM_Utils_Money::format($value, $row['civicrm_financial_trxn_currency']); + if (CRM_Core_Permission::check('access CiviContribute')) { + $url = CRM_Utils_System::url( + "civicrm/contact/view/contribution", + [ + 'reset' => 1, + 'id' => $row['civicrm_contribution_contribution_id'], + 'cid' => $row['civicrm_contact_id'], + 'action' => 'view', + 'context' => 'contribution', + 'selectedChild' => 'contribute', + ], + $this->_absoluteUrl + ); + $rows[$rowNum]['civicrm_contribution_total_amount_link'] = $url; + $rows[$rowNum]['civicrm_contribution_total_amount_hover'] = ts("View Details of this Contribution."); + } + $entryFound = TRUE; + } + + // Contribution amount links to viewing contribution + if ($value = CRM_Utils_Array::value('civicrm_financial_trxn_total_amount', $row)) { + $rows[$rowNum]['civicrm_financial_trxn_total_amount'] = CRM_Utils_Money::format($value, $row['civicrm_financial_trxn_currency']); + if (CRM_Core_Permission::check('access CiviContribute')) { + $url = CRM_Utils_System::url( + "civicrm/contact/view/contribution", + [ + 'reset' => 1, + 'id' => $row['civicrm_contribution_contribution_id'], + 'cid' => $row['civicrm_contact_id'], + 'action' => 'view', + 'context' => 'contribution', + 'selectedChild' => 'contribute', + ], + $this->_absoluteUrl + ); + $rows[$rowNum]['civicrm_financial_trxn_total_amount_link'] = $url; + $rows[$rowNum]['civicrm_financial_trxn_total_amount_hover'] = ts("View Details of this Contribution."); + } + $entryFound = TRUE; + } + + // convert campaign_id to campaign title + if (array_key_exists('civicrm_contribution_campaign_id', $row)) { + if ($value = $row['civicrm_contribution_campaign_id']) { + $rows[$rowNum]['civicrm_contribution_campaign_id'] = $this->campaigns[$value]; + $entryFound = TRUE; + } + } + + // soft credits + if (array_key_exists('civicrm_contribution_soft_credits', $row) && + 'Contribution' == + CRM_Utils_Array::value('civicrm_contribution_contribution_or_soft', $rows[$rowNum]) && + array_key_exists('civicrm_contribution_contribution_id', $row) + ) { + $query = " +SELECT civicrm_contact_id, civicrm_contact_sort_name, civicrm_contribution_total_amount, civicrm_financial_trxn_currency +FROM {$this->temporaryTables['civireport_contribution_detail_temp2']['name']} +WHERE civicrm_contribution_contribution_id={$row['civicrm_contribution_contribution_id']}"; + $dao = CRM_Core_DAO::executeQuery($query); + $string = ''; + $separator = ($this->_outputMode !== 'csv') ? "<br/>" : ' '; + while ($dao->fetch()) { + $url = CRM_Utils_System::url("civicrm/contact/view", 'reset=1&cid=' . + $dao->civicrm_contact_id); + $string = $string . ($string ? $separator : '') . + "<a href='{$url}'>{$dao->civicrm_contact_sort_name}</a> " . + CRM_Utils_Money::format($dao->civicrm_contribution_total_amount, $dao->civicrm_financial_trxn_currency); + } + $rows[$rowNum]['civicrm_contribution_soft_credits'] = $string; + } + + if (array_key_exists('civicrm_contribution_soft_credit_for', $row) && + 'Soft Credit' == + CRM_Utils_Array::value('civicrm_contribution_contribution_or_soft', $rows[$rowNum]) && + array_key_exists('civicrm_contribution_contribution_id', $row) + ) { + $query = " +SELECT civicrm_contact_id, civicrm_contact_sort_name +FROM {$this->temporaryTables['civireport_contribution_detail_temp1']['name']} +WHERE civicrm_contribution_contribution_id={$row['civicrm_contribution_contribution_id']}"; + $dao = CRM_Core_DAO::executeQuery($query); + $string = ''; + while ($dao->fetch()) { + $url = CRM_Utils_System::url("civicrm/contact/view", 'reset=1&cid=' . + $dao->civicrm_contact_id); + $string = $string . + "\n<a href='{$url}'>{$dao->civicrm_contact_sort_name}</a>"; + } + $rows[$rowNum]['civicrm_contribution_soft_credit_for'] = $string; + } + + // CRM-18312 - hide 'contribution_or_soft' column if unchecked. + if (!empty($this->noDisplayContributionOrSoftColumn)) { + unset($rows[$rowNum]['civicrm_contribution_contribution_or_soft']); + unset($this->_columnHeaders['civicrm_contribution_contribution_or_soft']); + } + + //convert soft_credit_type_id into label + if (array_key_exists('civicrm_contribution_soft_soft_credit_type_id', $rows[$rowNum])) { + $rows[$rowNum]['civicrm_contribution_soft_soft_credit_type_id'] = CRM_Core_PseudoConstant::getLabel( + 'CRM_Contribute_BAO_ContributionSoft', + 'soft_credit_type_id', + $row['civicrm_contribution_soft_soft_credit_type_id'] + ); + } + + // Contribution amount links to viewing contribution + if ($value = CRM_Utils_Array::value('civicrm_pledge_payment_pledge_id', $row)) { + if (CRM_Core_Permission::check('access CiviContribute')) { + $url = CRM_Utils_System::url( + "civicrm/contact/view/pledge", + [ + 'reset' => 1, + 'id' => $row['civicrm_pledge_payment_pledge_id'], + 'cid' => $row['civicrm_contact_id'], + 'action' => 'view', + 'context' => 'pledge', + 'selectedChild' => 'pledge', + ], + $this->_absoluteUrl + ); + $rows[$rowNum]['civicrm_pledge_payment_pledge_id_link'] = $url; + $rows[$rowNum]['civicrm_pledge_payment_pledge_id_hover'] = ts("View Details of this Pledge."); + } + $entryFound = TRUE; + } + + $entryFound = $this->alterDisplayAddressFields($row, $rows, $rowNum, 'contribute/detail', 'List all contribution(s) for this ') ? TRUE : $entryFound; + + // skip looking further in rows, if first row itself doesn't + // have the column we need + if (!$entryFound) { + break; + } + $lastKey = $rowNum; + } + } + + public function sectionTotals() { + + // Reports using order_bys with sections must populate $this->_selectAliases in select() method. + if (empty($this->_selectAliases)) { + return; + } + + if (!empty($this->_sections)) { + // build the query with no LIMIT clause + $select = str_ireplace('SELECT SQL_CALC_FOUND_ROWS ', 'SELECT ', $this->_select); + $sql = "{$select} {$this->_from} {$this->_where} {$this->_groupBy} {$this->_having} {$this->_orderBy}"; + + // pull section aliases out of $this->_sections + $sectionAliases = array_keys($this->_sections); + + $ifnulls = []; + foreach (array_merge($sectionAliases, $this->_selectAliases) as $alias) { + $ifnulls[] = "ifnull($alias, '') as $alias"; + } + $this->_select = "SELECT " . implode(", ", $ifnulls); + $this->_select = CRM_Contact_BAO_Query::appendAnyValueToSelect($ifnulls, $sectionAliases); + + /* Group (un-limited) report by all aliases and get counts. This might + * be done more efficiently when the contents of $sql are known, ie. by + * overriding this method in the report class. + */ + + $addtotals = ''; + + if (array_search("civicrm_contribution_total_amount", $this->_selectAliases) !== + FALSE + ) { + $addtotals = ", sum(civicrm_contribution_total_amount) as sumcontribs"; + $showsumcontribs = TRUE; + } + + $query = $this->_select . + "$addtotals, count(*) as ct from {$this->temporaryTables['civireport_contribution_detail_temp3']['name']} group by " . + implode(", ", $sectionAliases); + // initialize array of total counts + $sumcontribs = $totals = []; + $dao = CRM_Core_DAO::executeQuery($query); + $this->addToDeveloperTab($query); + while ($dao->fetch()) { + + // let $this->_alterDisplay translate any integer ids to human-readable values. + $rows[0] = $dao->toArray(); + $this->alterDisplay($rows); + $row = $rows[0]; + + // add totals for all permutations of section values + $values = []; + $i = 1; + $aliasCount = count($sectionAliases); + foreach ($sectionAliases as $alias) { + $values[] = $row[$alias]; + $key = implode(CRM_Core_DAO::VALUE_SEPARATOR, $values); + if ($i == $aliasCount) { + // the last alias is the lowest-level section header; use count as-is + $totals[$key] = $dao->ct; + if ($showsumcontribs) { + $sumcontribs[$key] = $dao->sumcontribs; + } + } + else { + // other aliases are higher level; roll count into their total + $totals[$key] = (array_key_exists($key, $totals)) ? $totals[$key] + $dao->ct : $dao->ct; + if ($showsumcontribs) { + $sumcontribs[$key] = array_key_exists($key, $sumcontribs) ? $sumcontribs[$key] + $dao->sumcontribs : $dao->sumcontribs; + } + } + } + } + if ($showsumcontribs) { + $totalandsum = []; + // ts exception to avoid having ts("%1 %2: %3") + $title = '%1 contributions / soft-credits: %2'; + + if (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == + 'contributions_only' + ) { + $title = '%1 contributions: %2'; + } + elseif (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == + 'soft_credits_only' + ) { + $title = '%1 soft-credits: %2'; + } + foreach ($totals as $key => $total) { + $totalandsum[$key] = ts($title, [ + 1 => $total, + 2 => CRM_Utils_Money::format($sumcontribs[$key]), + ]); + } + $this->assign('sectionTotals', $totalandsum); + } + else { + $this->assign('sectionTotals', $totals); + } + } + } + + /** + * Generate the from clause as it relates to the soft credits. + */ + public function softCreditFrom() { + + $this->_from = " + FROM {$this->temporaryTables['civireport_contribution_detail_temp1']['name']} temp1_civireport + INNER JOIN civicrm_contribution {$this->_aliases['civicrm_contribution']} + ON temp1_civireport.civicrm_contribution_contribution_id = {$this->_aliases['civicrm_contribution']}.id + INNER JOIN civicrm_contribution_soft contribution_soft_civireport + ON contribution_soft_civireport.contribution_id = {$this->_aliases['civicrm_contribution']}.id + INNER JOIN civicrm_contact {$this->_aliases['civicrm_contact']} + ON {$this->_aliases['civicrm_contact']}.id = contribution_soft_civireport.contact_id + {$this->_aclFrom} + "; + + //Join temp table if report is filtered by group. This is specific to 'notin' operator and covered in unit test(ref dev/core#212) + if (!empty($this->_params['gid_op']) && $this->_params['gid_op'] == 'notin') { + $this->joinGroupTempTable('civicrm_contact', 'id', $this->_aliases['civicrm_contact']); + } + $this->appendAdditionalFromJoins(); + } + + /** + * Append the joins that are required regardless of context. + */ + public function appendAdditionalFromJoins() { + if (!empty($this->_params['ordinality_value'])) { + $this->_from .= " + INNER JOIN (SELECT c.id, IF(COUNT(oc.id) = 0, 0, 1) AS ordinality FROM civicrm_contribution c LEFT JOIN civicrm_contribution oc ON c.contact_id = oc.contact_id AND oc.receive_date < c.receive_date GROUP BY c.id) {$this->_aliases['civicrm_contribution_ordinality']} + ON {$this->_aliases['civicrm_contribution_ordinality']}.id = {$this->_aliases['civicrm_contribution']}.id"; + } + $this->joinPhoneFromContact(); + $this->joinAddressFromContact(); + $this->joinEmailFromContact(); + + // include contribution note + if (!empty($this->_params['fields']['contribution_note']) || + !empty($this->_params['note_value']) + ) { + $this->_from .= " + LEFT JOIN civicrm_note {$this->_aliases['civicrm_note']} + ON ( {$this->_aliases['civicrm_note']}.entity_table = 'civicrm_contribution' AND + {$this->_aliases['civicrm_contribution']}.id = {$this->_aliases['civicrm_note']}.entity_id )"; + } + //for contribution batches + if (!empty($this->_params['fields']['batch_id']) || + !empty($this->_params['bid_value']) + ) { + $this->_from .= " + LEFT JOIN civicrm_entity_financial_trxn eft + ON eft.entity_id = {$this->_aliases['civicrm_contribution']}.id AND + eft.entity_table = 'civicrm_contribution' + LEFT JOIN civicrm_entity_batch {$this->_aliases['civicrm_batch']} + ON ({$this->_aliases['civicrm_batch']}.entity_id = eft.financial_trxn_id + AND {$this->_aliases['civicrm_batch']}.entity_table = 'civicrm_financial_trxn')"; + } + // for credit card type + $this->addFinancialTrxnFromClause(); + + if ($this->isTableSelected('civicrm_pledge_payment')) { + $this->_from .= " + LEFT JOIN civicrm_pledge_payment {$this->_aliases['civicrm_pledge_payment']} ON {$this->_aliases['civicrm_pledge_payment']}.contribution_id = {$this->_aliases['civicrm_contribution']}.id + "; + } + } + + /** + * Add join to the soft credit table. + */ + protected function joinContributionToSoftCredit() { + if (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == 'contributions_only' + && !$this->isTableSelected('civicrm_contribution_soft')) { + return; + } + $joinType = ' LEFT '; + if (CRM_Utils_Array::value('contribution_or_soft_value', $this->_params) == 'soft_credits_only') { + $joinType = ' INNER '; + } + $this->_from .= " + $joinType JOIN civicrm_contribution_soft {$this->_aliases['civicrm_contribution_soft']} + ON {$this->_aliases['civicrm_contribution_soft']}.contribution_id = {$this->_aliases['civicrm_contribution']}.id + "; + } + + /** + * Add Financial Transaction into From Table if required. + */ + public function addFinancialTrxnFromClause() { + if ($this->isTableSelected('civicrm_financial_trxn')) { + $this->_from .= " + INNER JOIN civicrm_entity_financial_trxn eftcc + ON ({$this->_aliases['civicrm_contribution']}.id = eftcc.entity_id AND + eftcc.entity_table = 'civicrm_contribution') + LEFT JOIN civicrm_financial_trxn {$this->_aliases['civicrm_financial_trxn']} + ON {$this->_aliases['civicrm_financial_trxn']}.id = eftcc.financial_trxn_id AND {$this->_aliases['civicrm_financial_trxn']}.is_payment = 1 \n"; + } + } + +} diff --git a/CRM/Report/Form/Contribute/Summary.php b/CRM/Report/Form/Contribute/Summary.php new file mode 100644 index 0000000000000000000000000000000000000000..5c16588c6c5156cfbf845802db806bccb7690931 --- /dev/null +++ b/CRM/Report/Form/Contribute/Summary.php @@ -0,0 +1,1093 @@ +<?php +/* + +--------------------------------------------------------------------+ + | 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 | + +--------------------------------------------------------------------+ + */ + +/** + * + * @package CRM + * @copyright CiviCRM LLC https://civicrm.org/licensing + */ +class CRM_Report_Form_Contribute_Summary extends CRM_Report_Form { + + protected $_customGroupExtends = ['Contribution', 'Contact', 'Individual']; + protected $_customGroupGroupBy = TRUE; + + public $_drilldownReport = ['contribute/detail' => 'Link to Detail Report']; + + /** + * To what frequency group-by a date column + * + * @var array + */ + protected $_groupByDateFreq = [ + 'MONTH' => 'Month', + 'YEARWEEK' => 'Week', + 'DATE' => 'Day', + 'QUARTER' => 'Quarter', + 'YEAR' => 'Year', + 'FISCALYEAR' => 'Fiscal Year', + ]; + + /** + * This report has been optimised for group filtering. + * + * @var bool + * @see https://issues.civicrm.org/jira/browse/CRM-19170 + */ + protected $groupFilterNotOptimised = FALSE; + + /** + * Use the generic (but flawed) handling to implement full group by. + * + * Note that because we are calling the parent group by function we set this to FALSE. + * The parent group by function adds things to the group by in order to make the mysql pass + * but can create incorrect results in the process. + * + * @var bool + */ + public $optimisedForOnlyFullGroupBy = FALSE; + + /** + * Class constructor. + */ + public function __construct() { + $this->_columns = [ + 'civicrm_contact' => [ + 'dao' => 'CRM_Contact_DAO_Contact', + 'fields' => array_merge( + $this->getBasicContactFields(), + [ + 'sort_name' => [ + 'title' => ts('Contact Name'), + 'no_repeat' => TRUE, + ], + ] + ), + 'filters' => $this->getBasicContactFilters(['deceased' => NULL]), + 'grouping' => 'contact-fields', + 'group_bys' => [ + 'id' => ['title' => ts('Contact ID')], + 'sort_name' => [ + 'title' => ts('Contact Name'), + ], + ], + ], + 'civicrm_email' => [ + 'dao' => 'CRM_Core_DAO_Email', + 'fields' => [ + 'email' => [ + 'title' => ts('Email'), + 'no_repeat' => TRUE, + ], + ], + 'grouping' => 'contact-fields', + ], + 'civicrm_line_item' => [ + 'dao' => 'CRM_Price_DAO_LineItem', + ], + 'civicrm_phone' => [ + 'dao' => 'CRM_Core_DAO_Phone', + 'fields' => [ + 'phone' => [ + 'title' => ts('Phone'), + 'no_repeat' => TRUE, + ], + ], + 'grouping' => 'contact-fields', + ], + 'civicrm_financial_type' => [ + 'dao' => 'CRM_Financial_DAO_FinancialType', + 'fields' => ['financial_type' => NULL], + 'grouping' => 'contri-fields', + 'group_bys' => [ + 'financial_type' => ['title' => ts('Financial Type')], + ], + ], + 'civicrm_contribution' => [ + 'dao' => 'CRM_Contribute_DAO_Contribution', + //'bao' => 'CRM_Contribute_BAO_Contribution', + 'fields' => [ + 'contribution_status_id' => [ + 'title' => ts('Contribution Status'), + ], + 'contribution_source' => ['title' => ts('Source')], + 'currency' => [ + 'required' => TRUE, + 'no_display' => TRUE, + ], + 'contribution_page_id' => [ + 'title' => ts('Contribution Page'), + ], + 'total_amount' => [ + 'title' => ts('Contribution Amount Stats'), + 'default' => TRUE, + 'statistics' => [ + 'count' => ts('Contributions'), + 'sum' => ts('Contribution Aggregate'), + 'avg' => ts('Contribution Avg'), + ], + ], + 'non_deductible_amount' => [ + 'title' => ts('Non-deductible Amount'), + ], + 'contribution_recur_id' => [ + 'title' => ts('Contribution Recurring'), + 'dbAlias' => '!ISNULL(contribution_civireport.contribution_recur_id)', + 'type' => CRM_Utils_Type::T_BOOLEAN, + ], + ], + 'grouping' => 'contri-fields', + 'filters' => [ + 'receive_date' => ['operatorType' => CRM_Report_Form::OP_DATE], + 'receipt_date' => ['operatorType' => CRM_Report_Form::OP_DATE], + 'thankyou_date' => ['operatorType' => CRM_Report_Form::OP_DATE], + 'contribution_status_id' => [ + 'title' => ts('Contribution Status'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Contribute_BAO_Contribution::buildOptions('contribution_status_id', 'search'), + 'default' => [1], + 'type' => CRM_Utils_Type::T_INT, + ], + 'contribution_page_id' => [ + 'title' => ts('Contribution Page'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Contribute_PseudoConstant::contributionPage(), + 'type' => CRM_Utils_Type::T_INT, + ], + 'currency' => [ + 'title' => ts('Currency'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Core_OptionGroup::values('currencies_enabled'), + 'default' => NULL, + 'type' => CRM_Utils_Type::T_STRING, + ], + 'financial_type_id' => [ + 'title' => ts('Financial Type'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Contribute_BAO_Contribution::buildOptions('financial_type_id', 'search'), + 'type' => CRM_Utils_Type::T_INT, + ], + 'contribution_page_id' => [ + 'title' => ts('Contribution Page'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Contribute_PseudoConstant::contributionPage(), + 'type' => CRM_Utils_Type::T_INT, + ], + 'contribution_recur_id' => [ + 'title' => ts('Contribution Recurring'), + 'operatorType' => CRM_Report_Form::OP_SELECT, + 'type' => CRM_Utils_Type::T_BOOLEAN, + 'options' => [ + '' => ts('Any'), + TRUE => ts('Yes'), + FALSE => ts('No'), + ], + 'dbAlias' => '!ISNULL(contribution_civireport.contribution_recur_id)', + ], + 'total_amount' => [ + 'title' => ts('Contribution Amount'), + ], + 'non_deductible_amount' => [ + 'title' => ts('Non-deductible Amount'), + ], + 'total_sum' => [ + 'title' => ts('Contribution Aggregate'), + 'type' => CRM_Report_Form::OP_INT, + 'dbAlias' => 'civicrm_contribution_total_amount_sum', + 'having' => TRUE, + ], + 'total_count' => [ + 'title' => ts('Contribution Count'), + 'type' => CRM_Report_Form::OP_INT, + 'dbAlias' => 'civicrm_contribution_total_amount_count', + 'having' => TRUE, + ], + 'total_avg' => [ + 'title' => ts('Contribution Avg'), + 'type' => CRM_Report_Form::OP_INT, + 'dbAlias' => 'civicrm_contribution_total_amount_avg', + 'having' => TRUE, + ], + ], + 'group_bys' => [ + 'receive_date' => [ + 'frequency' => TRUE, + 'default' => TRUE, + 'chart' => TRUE, + ], + 'contribution_source' => NULL, + 'contribution_status_id' => [ + 'title' => ts('Contribution Status'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Contribute_BAO_Contribution::buildOptions('contribution_status_id', 'search'), + 'default' => [1], + 'type' => CRM_Utils_Type::T_INT, + ], + 'contribution_page_id' => [ + 'title' => ts('Contribution Page'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Contribute_PseudoConstant::contributionPage(), + 'type' => CRM_Utils_Type::T_INT, + ], + 'contribution_recur_id' => [ + 'title' => ts('Contribution Recurring'), + 'type' => CRM_Utils_Type::T_BOOLEAN, + 'dbAlias' => '!ISNULL(contribution_civireport.contribution_recur_id)', + ], + ], + ], + 'civicrm_financial_trxn' => [ + 'dao' => 'CRM_Financial_DAO_FinancialTrxn', + 'fields' => [ + 'card_type_id' => [ + 'title' => ts('Credit Card Type'), + 'dbAlias' => 'GROUP_CONCAT(financial_trxn_civireport.card_type_id SEPARATOR ",")', + ], + 'trxn_date' => [ + 'title' => ts('Transaction Date'), + ], + ], + 'filters' => [ + 'card_type_id' => [ + 'title' => ts('Credit Card Type'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Financial_DAO_FinancialTrxn::buildOptions('card_type_id'), + 'default' => NULL, + 'type' => CRM_Utils_Type::T_STRING, + ], + 'trxn_date' => [ + 'title' => ts('Transaction Date'), + 'type' => CRM_Utils_Type::T_DATE, + 'operatorType' => CRM_Report_Form::OP_DATE, + ], + ], + ], + 'civicrm_batch' => [ + 'dao' => 'CRM_Batch_DAO_EntityBatch', + 'grouping' => 'contri-fields', + 'fields' => [ + 'batch_id' => [ + 'name' => 'batch_id', + 'title' => ts('Batch Title'), + 'dbAlias' => 'GROUP_CONCAT(DISTINCT batch_civireport.batch_id + ORDER BY batch_civireport.batch_id SEPARATOR ",")', + ], + ], + 'filters' => [ + 'batch_id' => [ + 'title' => ts('Batch Title'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Batch_BAO_Batch::getBatches(), + 'type' => CRM_Utils_Type::T_INT, + ], + ], + 'group_bys' => [ + 'batch_id' => ['title' => ts('Batch Title')], + ], + ], + 'civicrm_contribution_soft' => [ + 'dao' => 'CRM_Contribute_DAO_ContributionSoft', + 'fields' => [ + 'soft_amount' => [ + 'title' => ts('Soft Credit Amount Stats'), + 'name' => 'amount', + 'statistics' => [ + 'count' => ts('Soft Credits'), + 'sum' => ts('Soft Credit Aggregate'), + 'avg' => ts('Soft Credit Avg'), + ], + ], + ], + 'grouping' => 'contri-fields', + 'filters' => [ + 'amount' => [ + 'title' => ts('Soft Credit Amount'), + ], + 'soft_credit_type_id' => [ + 'title' => ts('Soft Credit Type'), + 'operatorType' => CRM_Report_Form::OP_MULTISELECT, + 'options' => CRM_Core_OptionGroup::values('soft_credit_type'), + 'default' => NULL, + 'type' => CRM_Utils_Type::T_STRING, + ], + 'soft_sum' => [ + 'title' => ts('Soft Credit Aggregate'), + 'type' => CRM_Report_Form::OP_INT, + 'dbAlias' => 'civicrm_contribution_soft_soft_amount_sum', + 'having' => TRUE, + ], + 'soft_count' => [ + 'title' => ts('Soft Credits Count'), + 'type' => CRM_Report_Form::OP_INT, + 'dbAlias' => 'civicrm_contribution_soft_soft_amount_count', + 'having' => TRUE, + ], + 'soft_avg' => [ + 'title' => ts('Soft Credit Avg'), + 'type' => CRM_Report_Form::OP_INT, + 'dbAlias' => 'civicrm_contribution_soft_soft_amount_avg', + 'having' => TRUE, + ], + ], + ], + ] + $this->addAddressFields(); + + $this->addCampaignFields('civicrm_contribution', TRUE); + + // Add charts support + $this->_charts = [ + '' => ts('Tabular'), + 'barChart' => ts('Bar Chart'), + 'pieChart' => ts('Pie Chart'), + ]; + + $this->_tagFilter = TRUE; + $this->_groupFilter = TRUE; + $this->_currencyColumn = 'civicrm_contribution_currency'; + parent::__construct(); + } + + /** + * Set select clause. + */ + public function select() { + $select = []; + $this->_columnHeaders = []; + foreach ($this->_columns as $tableName => $table) { + if (array_key_exists('group_bys', $table)) { + foreach ($table['group_bys'] as $fieldName => $field) { + if (!empty($this->_params['group_bys'][$fieldName])) { + switch (CRM_Utils_Array::value($fieldName, $this->_params['group_bys_freq'])) { + case 'YEARWEEK': + $select[] = "DATE_SUB({$field['dbAlias']}, INTERVAL WEEKDAY({$field['dbAlias']}) DAY) AS {$tableName}_{$fieldName}_start"; + $select[] = "YEARWEEK({$field['dbAlias']}) AS {$tableName}_{$fieldName}_subtotal"; + $select[] = "WEEKOFYEAR({$field['dbAlias']}) AS {$tableName}_{$fieldName}_interval"; + $field['title'] = ts('Week Beginning'); + break; + + case 'YEAR': + $select[] = "MAKEDATE(YEAR({$field['dbAlias']}), 1) AS {$tableName}_{$fieldName}_start"; + $select[] = "YEAR({$field['dbAlias']}) AS {$tableName}_{$fieldName}_subtotal"; + $select[] = "YEAR({$field['dbAlias']}) AS {$tableName}_{$fieldName}_interval"; + $field['title'] = ts('Year Beginning'); + break; + + case 'FISCALYEAR': + $config = CRM_Core_Config::singleton(); + $fy = $config->fiscalYearStart; + $fiscal = self::fiscalYearOffset($field['dbAlias']); + + $select[] = "DATE_ADD(MAKEDATE({$fiscal}, 1), INTERVAL ({$fy['M']})-1 MONTH) AS {$tableName}_{$fieldName}_start"; + $select[] = "{$fiscal} AS {$tableName}_{$fieldName}_subtotal"; + $select[] = "{$fiscal} AS {$tableName}_{$fieldName}_interval"; + $field['title'] = ts('Fiscal Year Beginning'); + break; + + case 'MONTH': + $select[] = "DATE_SUB({$field['dbAlias']}, INTERVAL (DAYOFMONTH({$field['dbAlias']})-1) DAY) as {$tableName}_{$fieldName}_start"; + $select[] = "MONTH({$field['dbAlias']}) AS {$tableName}_{$fieldName}_subtotal"; + $select[] = "MONTHNAME({$field['dbAlias']}) AS {$tableName}_{$fieldName}_interval"; + $field['title'] = ts('Month Beginning'); + break; + + case 'DATE': + $select[] = "DATE({$field['dbAlias']}) as {$tableName}_{$fieldName}_start"; + $field['title'] = ts('Date'); + break; + + case 'QUARTER': + $select[] = "STR_TO_DATE(CONCAT( 3 * QUARTER( {$field['dbAlias']} ) -2 , '/', '1', '/', YEAR( {$field['dbAlias']} ) ), '%m/%d/%Y') AS {$tableName}_{$fieldName}_start"; + $select[] = "QUARTER({$field['dbAlias']}) AS {$tableName}_{$fieldName}_subtotal"; + $select[] = "QUARTER({$field['dbAlias']}) AS {$tableName}_{$fieldName}_interval"; + $field['title'] = 'Quarter'; + break; + } + if (!empty($this->_params['group_bys_freq'][$fieldName])) { + $this->_interval = $this->_params['group_bys_freq'][$fieldName]; + $this->_columnHeaders["{$tableName}_{$fieldName}_start"]['title'] = $field['title']; + $this->_columnHeaders["{$tableName}_{$fieldName}_start"]['type'] = $field['type']; + $this->_columnHeaders["{$tableName}_{$fieldName}_start"]['group_by'] = $this->_params['group_bys_freq'][$fieldName]; + + // just to make sure these values are transferred to rows. + // since we need that for calculation purpose, + // e.g making subtotals look nicer or graphs + $this->_columnHeaders["{$tableName}_{$fieldName}_interval"] = ['no_display' => TRUE]; + $this->_columnHeaders["{$tableName}_{$fieldName}_subtotal"] = ['no_display' => TRUE]; + } + } + } + } + + if (array_key_exists('fields', $table)) { + foreach ($table['fields'] as $fieldName => $field) { + if (!empty($field['required']) || + !empty($this->_params['fields'][$fieldName]) + ) { + // only include statistics columns if set + if (!empty($field['statistics'])) { + foreach ($field['statistics'] as $stat => $label) { + $this->_columnHeaders["{$tableName}_{$fieldName}_{$stat}"]['title'] = $label; + $this->_columnHeaders["{$tableName}_{$fieldName}_{$stat}"]['type'] = $field['type']; + $this->_statFields[] = "{$tableName}_{$fieldName}_{$stat}"; + switch (strtolower($stat)) { + case 'sum': + $select[] = "SUM(IF (contribution_civireport.contribution_status_id IN (7, 3), -1 * {$field['dbAlias']}, {$field['dbAlias']})) as {$tableName}_{$fieldName}_{$stat}"; + break; + + case 'count': + $select[] = "COUNT({$field['dbAlias']}) as {$tableName}_{$fieldName}_{$stat}"; + $this->_columnHeaders["{$tableName}_{$fieldName}_{$stat}"]['type'] = CRM_Utils_Type::T_INT; + break; + + case 'avg': + $select[] = "ROUND(AVG({$field['dbAlias']}),2) as {$tableName}_{$fieldName}_{$stat}"; + break; + } + } + } + else { + $select[] = "{$field['dbAlias']} as {$tableName}_{$fieldName}"; + $this->_columnHeaders["{$tableName}_{$fieldName}"]['type'] = $field['type'] ?? NULL; + $this->_columnHeaders["{$tableName}_{$fieldName}"]['title'] = $field['title'] ?? NULL; + } + } + } + } + } + + $this->_selectClauses = $select; + $this->_select = "SELECT " . implode(', ', $select) . " "; + } + + /** + * Set form rules. + * + * @param array $fields + * @param array $files + * @param CRM_Report_Form_Contribute_Summary $self + * + * @return array + */ + public static function formRule($fields, $files, $self) { + // Check for searching combination of display columns and + // grouping criteria + $ignoreFields = ['total_amount', 'sort_name']; + $errors = $self->customDataFormRule($fields, $ignoreFields); + + if (empty($fields['fields']['total_amount'])) { + foreach ([ + 'total_count_value', + 'total_sum_value', + 'total_avg_value', + ] as $val) { + if (!empty($fields[$val])) { + $errors[$val] = ts("Please select the Amount Statistics"); + } + } + } + + return $errors; + } + + /** + * Set from clause. + * + * @param string $entity + * + * @todo fix function signature to match parent. Remove hacky passing of $entity + * to acheive unclear results. + */ + public function from($entity = NULL) { + $softCreditJoinType = "LEFT"; + if (!empty($this->_params['fields']['soft_amount']) && + empty($this->_params['fields']['total_amount']) + ) { + // if its only soft credit stats, use inner join + $softCreditJoinType = "INNER"; + } + + $softCreditJoin = "{$softCreditJoinType} JOIN civicrm_contribution_soft {$this->_aliases['civicrm_contribution_soft']} + ON {$this->_aliases['civicrm_contribution_soft']}.contribution_id = {$this->_aliases['civicrm_contribution']}.id"; + if ($entity == 'contribution' || empty($this->_params['fields']['soft_amount'])) { + $softCreditJoin .= " AND {$this->_aliases['civicrm_contribution_soft']}.id = (SELECT MIN(id) FROM civicrm_contribution_soft cs WHERE cs.contribution_id = {$this->_aliases['civicrm_contribution']}.id) "; + } + + $this->setFromBase('civicrm_contact'); + + $this->_from .= " + INNER JOIN civicrm_contribution {$this->_aliases['civicrm_contribution']} + ON {$this->_aliases['civicrm_contact']}.id = {$this->_aliases['civicrm_contribution']}.contact_id AND + {$this->_aliases['civicrm_contribution']}.is_test = 0 AND + {$this->_aliases['civicrm_contribution']}.is_template = 0 + {$softCreditJoin} + LEFT JOIN civicrm_financial_type {$this->_aliases['civicrm_financial_type']} + ON {$this->_aliases['civicrm_contribution']}.financial_type_id ={$this->_aliases['civicrm_financial_type']}.id + "; + + $this->joinAddressFromContact(); + $this->joinPhoneFromContact(); + $this->joinEmailFromContact(); + + //for contribution batches + if ($this->isTableSelected('civicrm_batch')) { + $this->_from .= " + LEFT JOIN civicrm_entity_financial_trxn eft + ON eft.entity_id = {$this->_aliases['civicrm_contribution']}.id AND + eft.entity_table = 'civicrm_contribution' + LEFT JOIN civicrm_entity_batch {$this->_aliases['civicrm_batch']} + ON ({$this->_aliases['civicrm_batch']}.entity_id = eft.financial_trxn_id + AND {$this->_aliases['civicrm_batch']}.entity_table = 'civicrm_financial_trxn')"; + } + + $this->addFinancialTrxnFromClause(); + } + + /** + * Set group by clause. + */ + public function groupBy() { + parent::groupBy(); + + $isGroupByFrequency = !empty($this->_params['group_bys_freq']); + + if (!empty($this->_params['group_bys']) && + is_array($this->_params['group_bys']) + ) { + + if (!empty($this->_statFields) && + (($isGroupByFrequency && count($this->_groupByArray) <= 1) || (!$isGroupByFrequency)) && + !$this->_having + ) { + $this->_rollup = " WITH ROLLUP"; + } + $groupBy = []; + foreach ($this->_groupByArray as $key => $val) { + if (strpos($val, ';;') !== FALSE) { + $groupBy = array_merge($groupBy, explode(';;', $val)); + } + else { + $groupBy[] = $this->_groupByArray[$key]; + } + } + $this->_groupBy = "GROUP BY " . implode(', ', $groupBy); + } + else { + $this->_groupBy = "GROUP BY {$this->_aliases['civicrm_contact']}.id"; + } + $this->_groupBy .= $this->_rollup; + } + + /** + * Store having clauses as an array. + */ + public function storeWhereHavingClauseArray() { + parent::storeWhereHavingClauseArray(); + if (empty($this->_params['fields']['soft_amount']) && + !empty($this->_havingClauses) + ) { + foreach ($this->_havingClauses as $key => $havingClause) { + if (stristr($havingClause, 'soft_soft')) { + unset($this->_havingClauses[$key]); + } + } + } + } + + /** + * Set statistics. + * + * @param array $rows + * + * @return array + * + * @throws \CRM_Core_Exception + */ + public function statistics(&$rows) { + $statistics = parent::statistics($rows); + + $softCredit = $this->_params['fields']['soft_amount'] ?? NULL; + $onlySoftCredit = $softCredit && !CRM_Utils_Array::value('total_amount', $this->_params['fields']); + if (!isset($this->_groupByArray['civicrm_contribution_currency'])) { + $this->_groupByArray['civicrm_contribution_currency'] = 'currency'; + } + $group = ' GROUP BY ' . implode(', ', $this->_groupByArray); + + $this->from('contribution'); + if ($softCredit) { + $this->from(); + } + $this->customDataFrom(); + + // Ensure that Extensions that modify the from statement in the sql also modify it in the statistics. + CRM_Utils_Hook::alterReportVar('sql', $this, $this); + + $contriQuery = " + COUNT( {$this->_aliases['civicrm_contribution']}.total_amount ) as civicrm_contribution_total_amount_count, + SUM( IF (contribution_civireport.contribution_status_id IN (7, 3), -1 * {$this->_aliases['civicrm_contribution']}.total_amount, {$this->_aliases['civicrm_contribution']}.total_amount ) ) as civicrm_contribution_total_amount_sum, + ROUND((IF (contribution_civireport.contribution_status_id IN (7, 3), -1 * {$this->_aliases['civicrm_contribution']}.total_amount, {$this->_aliases['civicrm_contribution']}.total_amount)/ COUNT( {$this->_aliases['civicrm_contribution']}.total_amount )), 2) as civicrm_contribution_total_amount_avg, + {$this->_aliases['civicrm_contribution']}.currency as currency + {$this->_from} {$this->_where} + "; + + if ($softCredit) { + $selectOnlySoftCredit = " + COUNT({$this->_aliases['civicrm_contribution_soft']}.amount ) as civicrm_contribution_soft_soft_amount_count, + SUM({$this->_aliases['civicrm_contribution_soft']}.amount ) as civicrm_contribution_soft_soft_amount_sum, + ROUND(AVG({$this->_aliases['civicrm_contribution_soft']}.amount), 2) as civicrm_contribution_soft_soft_amount_avg, + "; + + $selectWithSoftCredit = " + COUNT({$this->_aliases['civicrm_contribution_soft']}.amount ) as civicrm_contribution_soft_soft_amount_count, + SUM({$this->_aliases['civicrm_contribution_soft']}.amount ) as civicrm_contribution_soft_soft_amount_sum, + ROUND(AVG({$this->_aliases['civicrm_contribution_soft']}.amount), 2) as civicrm_contribution_soft_soft_amount_avg, + COUNT({$this->_aliases['civicrm_contribution']}.total_amount ) as civicrm_contribution_total_amount_count, + SUM({$this->_aliases['civicrm_contribution']}.total_amount ) as civicrm_contribution_total_amount_sum, + ROUND(AVG({$this->_aliases['civicrm_contribution']}.total_amount), 2) as civicrm_contribution_total_amount_avg, + "; + + if ($softCredit && $onlySoftCredit) { + $contriQuery = "{$selectOnlySoftCredit} {$contriQuery}"; + $softSQL = "SELECT {$selectOnlySoftCredit} {$this->_aliases['civicrm_contribution']}.currency as currency + {$this->_from} {$this->_where} {$group} {$this->_having}"; + } + elseif ($softCredit && !$onlySoftCredit) { + $contriQuery = "{$selectOnlySoftCredit} {$contriQuery}"; + $softSQL = "SELECT {$selectWithSoftCredit} {$this->_aliases['civicrm_contribution']}.currency as currency + {$this->_from} {$this->_where} {$group} {$this->_having}"; + } + } + + $contriSQL = "SELECT {$contriQuery} {$group} {$this->_having}"; + $contriDAO = CRM_Core_DAO::executeQuery($contriSQL); + $this->addToDeveloperTab($contriSQL); + $currencies = $currAmount = $currAverage = $currCount = []; + $totalAmount = $average = $mode = $median = []; + $softTotalAmount = $softAverage = $averageCount = $averageSoftCount = []; + $softCount = $count = 0; + while ($contriDAO->fetch()) { + if (!isset($currAmount[$contriDAO->currency])) { + $currAmount[$contriDAO->currency] = 0; + } + if (!isset($currCount[$contriDAO->currency])) { + $currCount[$contriDAO->currency] = 0; + } + if (!isset($currAverage[$contriDAO->currency])) { + $currAverage[$contriDAO->currency] = 0; + } + if (!isset($averageCount[$contriDAO->currency])) { + $averageCount[$contriDAO->currency] = 0; + } + $currAmount[$contriDAO->currency] += $contriDAO->civicrm_contribution_total_amount_sum; + $currCount[$contriDAO->currency] += $contriDAO->civicrm_contribution_total_amount_count; + $currAverage[$contriDAO->currency] += $contriDAO->civicrm_contribution_total_amount_avg; + $averageCount[$contriDAO->currency]++; + $count += $contriDAO->civicrm_contribution_total_amount_count; + + if (!in_array($contriDAO->currency, $currencies)) { + $currencies[] = $contriDAO->currency; + } + } + + foreach ($currencies as $currency) { + $totalAmount[] = CRM_Utils_Money::format($currAmount[$currency], $currency) . + " (" . $currCount[$currency] . ")"; + $average[] = CRM_Utils_Money::format(round(($currAmount[$currency] / $currCount[$currency]), 2), $currency); + // $average[] = CRM_Utils_Money::format(($currAverage[$currency] / $averageCount[$currency]), $currency); + } + + $groupBy = "\n{$group}, {$this->_aliases['civicrm_contribution']}.total_amount"; + $orderBy = "\nORDER BY civicrm_contribution_total_amount_count DESC"; + $modeSQL = "SELECT MAX(civicrm_contribution_total_amount_count) as civicrm_contribution_total_amount_count, + SUBSTRING_INDEX(GROUP_CONCAT(amount ORDER BY mode.civicrm_contribution_total_amount_count DESC SEPARATOR ';'), ';', 1) as amount, + currency + FROM (SELECT {$this->_aliases['civicrm_contribution']}.total_amount as amount, + {$contriQuery} {$groupBy} {$orderBy}) as mode GROUP BY currency"; + + $mode = $this->calculateMode($modeSQL); + $median = $this->calculateMedian(); + + $currencies = $currSoftAmount = $currSoftAverage = $currSoftCount = []; + if ($softCredit) { + $softDAO = CRM_Core_DAO::executeQuery($softSQL); + $this->addToDeveloperTab($softSQL); + while ($softDAO->fetch()) { + if (!isset($currSoftAmount[$softDAO->currency])) { + $currSoftAmount[$softDAO->currency] = 0; + } + if (!isset($currSoftCount[$softDAO->currency])) { + $currSoftCount[$softDAO->currency] = 0; + } + if (!isset($currSoftAverage[$softDAO->currency])) { + $currSoftAverage[$softDAO->currency] = 0; + } + if (!isset($averageSoftCount[$softDAO->currency])) { + $averageSoftCount[$softDAO->currency] = 0; + } + $currSoftAmount[$softDAO->currency] += $softDAO->civicrm_contribution_soft_soft_amount_sum; + $currSoftCount[$softDAO->currency] += $softDAO->civicrm_contribution_soft_soft_amount_count; + $currSoftAverage[$softDAO->currency] += $softDAO->civicrm_contribution_soft_soft_amount_avg; + $averageSoftCount[$softDAO->currency]++; + $softCount += $softDAO->civicrm_contribution_soft_soft_amount_count; + + if (!in_array($softDAO->currency, $currencies)) { + $currencies[] = $softDAO->currency; + } + } + + foreach ($currencies as $currency) { + $softTotalAmount[] = CRM_Utils_Money::format($currSoftAmount[$currency], $currency) . + " (" . $currSoftCount[$currency] . ")"; + $softAverage[] = CRM_Utils_Money::format(($currSoftAverage[$currency] / $averageSoftCount[$currency]), $currency); + } + } + + if (!$onlySoftCredit) { + $statistics['counts']['amount'] = [ + 'title' => ts('Total Amount'), + 'value' => implode(', ', $totalAmount), + 'type' => CRM_Utils_Type::T_STRING, + ]; + $statistics['counts']['count'] = [ + 'title' => ts('Total Contributions'), + 'value' => $count, + ]; + $statistics['counts']['avg'] = [ + 'title' => ts('Average'), + 'value' => implode(', ', $average), + 'type' => CRM_Utils_Type::T_STRING, + ]; + $statistics['counts']['mode'] = [ + 'title' => ts('Mode'), + 'value' => implode(', ', $mode), + 'type' => CRM_Utils_Type::T_STRING, + ]; + $statistics['counts']['median'] = [ + 'title' => ts('Median'), + 'value' => implode(', ', $median), + 'type' => CRM_Utils_Type::T_STRING, + ]; + } + if ($softCredit) { + $statistics['counts']['soft_amount'] = [ + 'title' => ts('Total Soft Credit Amount'), + 'value' => implode(', ', $softTotalAmount), + 'type' => CRM_Utils_Type::T_STRING, + ]; + $statistics['counts']['soft_count'] = [ + 'title' => ts('Total Soft Credits'), + 'value' => $softCount, + ]; + $statistics['counts']['soft_avg'] = [ + 'title' => ts('Average Soft Credit'), + 'value' => implode(', ', $softAverage), + 'type' => CRM_Utils_Type::T_STRING, + ]; + } + return $statistics; + } + + /** + * Build chart. + * + * @param array $original_rows + */ + public function buildChart(&$original_rows) { + $graphRows = []; + + if (!empty($this->_params['charts'])) { + if (!empty($this->_params['group_bys']['receive_date'])) { + + $contrib = !empty($this->_params['fields']['total_amount']); + $softContrib = !empty($this->_params['fields']['soft_amount']); + + // Make a copy so that we don't affect what gets passed later to hooks etc. + $rows = $original_rows; + if ($this->_rollup) { + // Remove the total row otherwise it overwrites the real last month's data since it has the + // same date. + array_pop($rows); + } + + foreach ($rows as $key => $row) { + if ($row['civicrm_contribution_receive_date_subtotal']) { + $graphRows['receive_date'][] = $row['civicrm_contribution_receive_date_start']; + $graphRows[$this->_interval][] = $row['civicrm_contribution_receive_date_interval']; + if ($softContrib && $contrib) { + // both contri & soft contri stats are present + $graphRows['multiValue'][0][] = $row['civicrm_contribution_total_amount_sum']; + $graphRows['multiValue'][1][] = $row['civicrm_contribution_soft_soft_amount_sum']; + } + elseif ($softContrib) { + // only soft contributions + $graphRows['multiValue'][0][] = $row['civicrm_contribution_soft_soft_amount_sum']; + } + else { + // only contributions + $graphRows['multiValue'][0][] = $row['civicrm_contribution_total_amount_sum']; + } + } + } + + if ($softContrib && $contrib) { + $graphRows['barKeys'][0] = ts('Contributions'); + $graphRows['barKeys'][1] = ts('Soft Credits'); + $graphRows['legend'] = ts('Contributions and Soft Credits'); + } + elseif ($softContrib) { + $graphRows['legend'] = ts('Soft Credits'); + } + + // build the chart. + $config = CRM_Core_Config::Singleton(); + $graphRows['xname'] = $this->_interval; + $graphRows['yname'] = ts('Amount (%1)', [1 => $config->defaultCurrency]); + CRM_Utils_Chart::chart($graphRows, $this->_params['charts'], $this->_interval); + $this->assign('chartType', $this->_params['charts']); + } + } + } + + /** + * Alter display of rows. + * + * Iterate through the rows retrieved via SQL and make changes for display purposes, + * such as rendering contacts as links. + * + * @param array $rows + * Rows generated by SQL, with an array for each row. + */ + public function alterDisplay(&$rows) { + $entryFound = FALSE; + $contributionStatus = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'label'); + $contributionPages = CRM_Contribute_PseudoConstant::contributionPage(); + //CRM-16338 if both soft-credit and contribution are enabled then process the contribution's + //total amount's average, count and sum separately and add it to the respective result list + $softCredit = (!empty($this->_params['fields']['soft_amount']) && !empty($this->_params['fields']['total_amount'])); + if ($softCredit) { + $this->from('contribution'); + $this->customDataFrom(); + $contriSQL = "{$this->_select} {$this->_from} {$this->_where} {$this->_groupBy} {$this->_having} {$this->_orderBy} {$this->_limit}"; + CRM_Core_DAO::disableFullGroupByMode(); + $contriDAO = CRM_Core_DAO::executeQuery($contriSQL); + CRM_Core_DAO::reenableFullGroupByMode(); + $this->addToDeveloperTab($contriSQL); + $contriFields = [ + 'civicrm_contribution_total_amount_sum', + 'civicrm_contribution_total_amount_avg', + 'civicrm_contribution_total_amount_count', + ]; + $count = 0; + while ($contriDAO->fetch()) { + foreach ($contriFields as $column) { + $rows[$count][$column] = $contriDAO->$column; + } + $count++; + } + } + foreach ($rows as $rowNum => $row) { + // make count columns point to detail report + if (!empty($this->_params['group_bys']['receive_date']) && + !empty($row['civicrm_contribution_receive_date_start']) && + CRM_Utils_Array::value('civicrm_contribution_receive_date_start', $row) && + !empty($row['civicrm_contribution_receive_date_subtotal']) + ) { + + $dateStart = CRM_Utils_Date::customFormat($row['civicrm_contribution_receive_date_start'], '%Y%m%d'); + $endDate = new DateTime($dateStart); + $dateEnd = []; + + list($dateEnd['Y'], $dateEnd['M'], $dateEnd['d']) = explode(':', $endDate->format('Y:m:d')); + + switch (strtolower($this->_params['group_bys_freq']['receive_date'])) { + case 'month': + $dateEnd = date("Ymd", mktime(0, 0, 0, $dateEnd['M'] + 1, + $dateEnd['d'] - 1, $dateEnd['Y'] + )); + break; + + case 'year': + $dateEnd = date("Ymd", mktime(0, 0, 0, $dateEnd['M'], + $dateEnd['d'] - 1, $dateEnd['Y'] + 1 + )); + break; + + case 'fiscalyear': + $dateEnd = date("Ymd", mktime(0, 0, 0, $dateEnd['M'], + $dateEnd['d'] - 1, $dateEnd['Y'] + 1 + )); + break; + + case 'yearweek': + $dateEnd = date("Ymd", mktime(0, 0, 0, $dateEnd['M'], + $dateEnd['d'] + 6, $dateEnd['Y'] + )); + break; + + case 'quarter': + $dateEnd = date("Ymd", mktime(0, 0, 0, $dateEnd['M'] + 3, + $dateEnd['d'] - 1, $dateEnd['Y'] + )); + break; + } + $url = CRM_Report_Utils_Report::getNextUrl('contribute/detail', + "reset=1&force=1&receive_date_from={$dateStart}&receive_date_to={$dateEnd}", + $this->_absoluteUrl, + $this->_id, + $this->_drilldownReport + ); + $rows[$rowNum]['civicrm_contribution_receive_date_start_link'] = $url; + $rows[$rowNum]['civicrm_contribution_receive_date_start_hover'] = ts('List all contribution(s) for this date unit.'); + $entryFound = TRUE; + } + + // make subtotals look nicer + if (array_key_exists('civicrm_contribution_receive_date_subtotal', $row) && + !$row['civicrm_contribution_receive_date_subtotal'] + ) { + $this->fixSubTotalDisplay($rows[$rowNum], $this->_statFields); + $entryFound = TRUE; + } + + // convert display name to links + if (array_key_exists('civicrm_contact_sort_name', $row) && + array_key_exists('civicrm_contact_id', $row) + ) { + $url = CRM_Report_Utils_Report::getNextUrl('contribute/detail', + 'reset=1&force=1&id_op=eq&id_value=' . $row['civicrm_contact_id'], + $this->_absoluteUrl, $this->_id, $this->_drilldownReport + ); + $rows[$rowNum]['civicrm_contact_sort_name_link'] = $url; + $rows[$rowNum]['civicrm_contact_sort_name_hover'] = ts("Lists detailed contribution(s) for this record."); + $entryFound = TRUE; + } + + // convert contribution status id to status name + if ($value = CRM_Utils_Array::value('civicrm_contribution_contribution_status_id', $row)) { + $rows[$rowNum]['civicrm_contribution_contribution_status_id'] = $contributionStatus[$value]; + $entryFound = TRUE; + } + + if (!empty($row['civicrm_financial_trxn_card_type_id'])) { + $rows[$rowNum]['civicrm_financial_trxn_card_type_id'] = $this->getLabels($row['civicrm_financial_trxn_card_type_id'], 'CRM_Financial_DAO_FinancialTrxn', 'card_type_id'); + $entryFound = TRUE; + } + + if ($value = CRM_Utils_Array::value('civicrm_contribution_contribution_page_id', $row)) { + $rows[$rowNum]['civicrm_contribution_contribution_page_id'] = $contributionPages[$value]; + $entryFound = TRUE; + } + + // If using campaigns, convert campaign_id to campaign title + if (array_key_exists('civicrm_contribution_campaign_id', $row)) { + if ($value = $row['civicrm_contribution_campaign_id']) { + $rows[$rowNum]['civicrm_contribution_campaign_id'] = $this->campaigns[$value]; + } + $entryFound = TRUE; + } + + // convert batch id to batch title + if (!empty($row['civicrm_batch_batch_id']) && !in_array('Subtotal', $rows[$rowNum])) { + $rows[$rowNum]['civicrm_batch_batch_id'] = $this->getLabels($row['civicrm_batch_batch_id'], 'CRM_Batch_BAO_EntityBatch', 'batch_id'); + $entryFound = TRUE; + } + + $entryFound = $this->alterDisplayAddressFields($row, $rows, $rowNum, 'contribute/detail', 'List all contribution(s) for this ') ? TRUE : $entryFound; + $entryFound = $this->alterDisplayContactFields($row, $rows, $rowNum, 'contribute/detail', 'List all contribution(s) for this ') ? TRUE : $entryFound; + + // skip looking further in rows, if first row itself doesn't + // have the column we need + if (!$entryFound) { + break; + } + } + } + + /** + * Calculate mode. + * + * Note this is a slow query. Alternative is extended reports. + * + * @param string $sql + * @return array|null + */ + protected function calculateMode($sql) { + $mode = []; + $modeDAO = CRM_Core_DAO::executeQuery($sql); + while ($modeDAO->fetch()) { + if ($modeDAO->civicrm_contribution_total_amount_count > 1) { + $mode[] = CRM_Utils_Money::format($modeDAO->amount, $modeDAO->currency); + } + else { + $mode[] = ts('N/A'); + } + } + return $mode; + } + + /** + * Calculate mode. + * + * Note this is a slow query. Alternative is extended reports. + * + * @return array|null + */ + protected function calculateMedian() { + $sql = "{$this->_from} {$this->_where}"; + $currencies = CRM_Core_OptionGroup::values('currencies_enabled'); + $median = []; + foreach ($currencies as $currency => $val) { + $midValue = 0; + $where = "AND {$this->_aliases['civicrm_contribution']}.currency = '{$currency}'"; + $rowCount = CRM_Core_DAO::singleValueQuery("SELECT count(*) as count {$sql} {$where}"); + + $even = FALSE; + $offset = 1; + $medianRow = floor($rowCount / 2); + if ($rowCount % 2 == 0 && !empty($medianRow)) { + $even = TRUE; + $offset++; + $medianRow--; + } + + $medianValue = "SELECT {$this->_aliases['civicrm_contribution']}.total_amount as median + {$sql} {$where} + ORDER BY median LIMIT {$medianRow},{$offset}"; + $medianValDAO = CRM_Core_DAO::executeQuery($medianValue); + while ($medianValDAO->fetch()) { + if ($even) { + $midValue = $midValue + $medianValDAO->median; + } + else { + $median[] = CRM_Utils_Money::format($medianValDAO->median, $currency); + } + } + if ($even) { + $midValue = $midValue / 2; + $median[] = CRM_Utils_Money::format($midValue, $currency); + } + } + return $median; + } + +/** + * Add Financial Transaction into From Table if required. + */ +public function addFinancialTrxnFromClause() { + if ($this->isTableSelected('civicrm_financial_trxn')) { + $this->_from .= " + INNER JOIN civicrm_entity_financial_trxn eftcc + ON ({$this->_aliases['civicrm_contribution']}.id = eftcc.entity_id AND + eftcc.entity_table = 'civicrm_contribution') + INNER JOIN civicrm_financial_trxn {$this->_aliases['civicrm_financial_trxn']} + ON {$this->_aliases['civicrm_financial_trxn']}.id = eftcc.financial_trxn_id AND {$this->_aliases['civicrm_financial_trxn']}.is_payment = 1 AND status_id = {$this->_aliases['civicrm_contribution']}.contribution_status_id \n"; + } +} + +} diff --git a/acop.php b/acop.php index 59c35d0e14769515616b49c89235ffee567ac751..d3b59fb9f37ad37f3e4cf25163421f978674b07d 100644 --- a/acop.php +++ b/acop.php @@ -144,22 +144,23 @@ function acop_civicrm_themes(&$themes) { } function acop_civicrm_alterReportVar($type, &$columns, &$form) { - if ('CRM_Report_Form_Contribute_Summary' == get_class($form) && $type == 'sql') { +if ('CRM_Report_Form_Contribute_Summary' == get_class($form) && $type == 'sql') { $selects = $columns->_selectClauses; foreach ($selects as &$clause) { if ($clause == "SUM(contribution_civireport.total_amount) as civicrm_contribution_total_amount_sum") { - $clause = "SUM(IF (contribution_civireport.contribution_status_id = 7, -1 * contribution_civireport.total_amount, contribution_civireport.total_amount)) as civicrm_contribution_total_amount_sum"; + $clause = "SUM(IF (contribution_civireport.contribution_status_id IN (7, 3), -1 * contribution_civireport.total_amount, contribution_civireport.total_amount)) as civicrm_contribution_total_amount_sum"; } if ($clause == "ROUND(AVG(contribution_civireport.total_amount),2) as civicrm_contribution_total_amount_avg") { - $clause = "ROUND(AVG(IF (contribution_civireport.contribution_status_id = 7, -1 * contribution_civireport.total_amount, contribution_civireport.total_amount)),2) as civicrm_contribution_total_amount_avg"; + $clause = "ROUND(AVG(IF (contribution_civireport.contribution_status_id IN (7, 3), -1 * contribution_civireport.total_amount, contribution_civireport.total_amount)),2) as civicrm_contribution_total_amount_avg"; } if ($clause == "contribution_civireport.non_deductible_amount as civicrm_contribution_non_deductible_amount") { - $clause = "IF (contribution_civireport.contribution_status_id = 7, -1 * contribution_civireport.non_deductible_amount, contribution_civireport.non_deductible_amount) as civicrm_contribution_non_deductible_amount"; + $clause = "IF (contribution_civireport.contribution_status_id IN (7, 3), -1 * contribution_civireport.non_deductible_amount, contribution_civireport.non_deductible_amount) as civicrm_contribution_non_deductible_amount"; } } $columns->_selectClauses = $selects; $columns->_select = "SELECT SQL_CALC_FOUND_ROWS " . implode(', ', $selects); } + if ('CRM_Report_Form_Contribute_Detail' == get_class($form) && $type == 'sql') { $selects = $columns->_selectClauses; foreach ($selects as &$clause) { @@ -174,6 +175,7 @@ function acop_civicrm_alterReportVar($type, &$columns, &$form) { } } if ('CRM_Report_Form_Contribute_Summary' == get_class($form) && $type == 'columns') { + $columns['civicrm_contribution']['fields']['total_amount']['type'] = CRM_Utils_Type::T_INT; $columns['civicrm_contribution']['filters']['payment_instrument_id'] = [ 'title' => ts('Payment Method'), 'operatorType' => CRM_Report_Form::OP_MULTISELECT, @@ -235,6 +237,25 @@ function acop_civicrm_validateForm($formName, &$fields, &$files, &$form, &$error * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_buildForm/ */ function acop_civicrm_buildForm($formName, &$form) { + if ($formName === 'CRM_Contribute_Form_Contribution_Main') { + /*CRM_Core_Resources::singleton()->addScript( + "CRM.$(function($) { + $('#_qf_Main_upload-bottom, #is_for_organization').on('click', function(e) { + if ($('#is_for_organization').length && $('#is_for_organization').is(':checked') === false ) { + $('#on-behalf-block input').not('input[type=checkbox], input[type=radio], #onbehalfof_id').val(''); + // clear checkboxes and radio + $('#on-behalf-block') + .find('input[type=checkbox], input[type=radio]') + .not('input[name=org_option]') + .attr('checked', false); + } + }); + }); + ");*/ + /*if (!empty($form->_submitValues)) { + CRM_Core_Error::debug_var('formsubmission', $form->_submitValues); + }*/ + } if ($formName === 'CRM_Contribute_Form_Contribution_Main' && $form->getVar('_id') == '2') { Civi::resources()->addScriptFile('biz.jmaconsulting.acop', 'js/acop_membership_form.js'); // Set future date for renewals. @@ -268,7 +289,7 @@ function acop_civicrm_buildForm($formName, &$form) { if ($form->_action & CRM_Core_Action::ADD) { $totalItems = $form->getVar('_batchInfo')['item_count']; for ($i = 1; $i <= $totalItems; $i++) { - $form->setDefaults(["field[$i][custom_83]" => 0]); + $form->setDefaults(["field[$i][custom_83]" => 0]); } CRM_Core_Resources::singleton()->addScript( "CRM.$(function($) { @@ -306,12 +327,57 @@ function acop_cdntaxreceipts_alter_receipt(&$receipt) { 'contact_id_a' => $receipt['contact_id'], 'relationship_type_id' => "Spouse of", 'is_active' => 1, + 'options' => ['limit' => 1], ])['values']; + + if (empty($relationships)) { + $relationships = civicrm_api3('Relationship', 'get', [ + 'sequential' => 1, + 'contact_id_b' => $receipt['contact_id'], + 'relationship_type_id' => "Spouse of", + 'is_active' => 1, + 'options' => ['limit' => 1], + ])['values']; + $relContact = $relationships[0]['contact_id_a']; + } + else { + $relContact = $relationships[0]['contact_id_b']; + } + $spouseRecords[] = CRM_Contact_BAO_Contact::displayName($receipt['contact_id']); - foreach ($relationships as $relationship) { - $spouseRecords[] = CRM_Contact_BAO_Contact::displayName($relationship['contact_id_b']); + if (!empty($relContact)) { + $spouseRecords[] = CRM_Contact_BAO_Contact::displayName($relContact); + $receipt['display_name'] = implode(" and ", $spouseRecords); } - $receipt['display_name'] = implode(" and ", $spouseRecords); + } +} + +/** + * Implements hook_civicrm_postProcess(). + * + * @link https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_postProcess + */ +function acop_civicrm_postProcess($formName, $form) { + if ($formName === 'CRM_Contribute_Form_Contribution_Confirm' || $formName === 'CRM_Contribute_Form_Contribution') { + return; + if ($formName === 'CRM_Contribute_Form_Contribution' && $form->getAction() !== CRM_Core_Action::ADD) { + return; + } + $params = $form->getVar('_params'); + $logValues = [ + 'first_name' => $params['billing_first_name'], + 'last_name' => $params['billing_last_name'], + 'pan_truncation' => $params['pan_truncation'], + 'street_address' => $params['street_address-5'], + 'city' => $params['city-5'], + 'postcode' => $params['postal_code-5'], + 'logged_in_user_id' => CRM_Core_Session::getLoggedInContactID(), + 'trigger_log_id' => CRM_Core_DAO::singleValueQuery('SELECT @civicrm_user_id'), + 'amount' => $params['total_amount'], + 'trxn_id' => $params['trxn_id'], + 'invoice_id' => $params['invoice_id'], + ]; + Civi::log()->debug('{formName} was submitted and the following logged values {logValues} were captured', ['formName' => $formName, 'logValues' => $logValues]); } }