3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
17 class CRM_Report_Form_Member_Summary
extends CRM_Report_Form
{
19 protected $_summary = NULL;
20 protected $_interval = NULL;
21 protected $_charts = [
23 'barChart' => 'Bar Chart',
24 'pieChart' => 'Pie Chart',
26 protected $_add2groupSupported = FALSE;
28 protected $_customGroupExtends = ['Membership'];
29 protected $_customGroupGroupBy = FALSE;
30 public $_drilldownReport = ['member/detail' => 'Link to Detail Report'];
33 * This report has not been optimised for group filtering.
35 * The functionality for group filtering has been improved but not
36 * all reports have been adjusted to take care of it. This report has not
37 * and will run an inefficient query until fixed.
43 protected $groupFilterNotOptimised = TRUE;
48 public function __construct() {
50 'civicrm_membership' => [
51 'dao' => 'CRM_Member_DAO_Membership',
52 'grouping' => 'member-fields',
54 'membership_type_id' => [
55 'title' => ts('Membership Type'),
60 'membership_join_date' => [
61 'title' => ts('Member Since'),
62 'type' => CRM_Utils_Type
::T_DATE
,
63 'operatorType' => CRM_Report_Form
::OP_DATE
,
65 'membership_start_date' => [
66 'name' => 'start_date',
67 'title' => ts('Membership Start Date'),
68 'type' => CRM_Utils_Type
::T_DATE
,
69 'operatorType' => CRM_Report_Form
::OP_DATE
,
71 'membership_end_date' => [
73 'title' => ts('Membership End Date'),
74 'type' => CRM_Utils_Type
::T_DATE
,
75 'operatorType' => CRM_Report_Form
::OP_DATE
,
77 'owner_membership_id' => [
78 'title' => ts('Primary Membership'),
79 'type' => CRM_Utils_Type
::T_INT
,
80 'operatorType' => CRM_Report_Form
::OP_INT
,
82 'membership_type_id' => [
83 'title' => ts('Membership Type'),
84 'type' => CRM_Utils_Type
::T_INT
,
85 'operatorType' => CRM_Report_Form
::OP_MULTISELECT
,
86 'options' => CRM_Member_PseudoConstant
::membershipType(),
89 'title' => ts('Membership Status'),
90 'type' => CRM_Utils_Type
::T_INT
,
91 'operatorType' => CRM_Report_Form
::OP_MULTISELECT
,
92 'options' => CRM_Member_PseudoConstant
::membershipStatus(NULL, NULL, 'label'),
97 'title' => ts('Member Since'),
103 'membership_type_id' => [
104 'title' => ts('Membership Type'),
110 'civicrm_contact' => [
111 'dao' => 'CRM_Contact_DAO_Contact',
114 'no_display' => TRUE,
117 'title' => ts('Contact Type'),
119 'contact_sub_type' => [
120 'title' => ts('Contact Subtype'),
124 'civicrm_contribution' => [
125 'dao' => 'CRM_Contribute_DAO_Contribution',
129 'no_display' => TRUE,
132 'title' => ts('Amount Statistics'),
135 'sum' => ts('Total Payments Made'),
136 'count' => ts('Contribution Count'),
137 'avg' => ts('Average'),
143 'title' => ts('Currency'),
144 'operatorType' => CRM_Report_Form
::OP_MULTISELECT
,
145 'options' => CRM_Core_OptionGroup
::values('currencies_enabled'),
147 'type' => CRM_Utils_Type
::T_STRING
,
149 'contribution_status_id' => [
150 'title' => ts('Contribution Status'),
151 'operatorType' => CRM_Report_Form
::OP_MULTISELECT
,
152 'options' => CRM_Contribute_BAO_Contribution
::buildOptions('contribution_status_id', 'search'),
155 'grouping' => 'member-fields',
158 $this->_tagFilter
= TRUE;
160 // If we have campaigns enabled, add those elements to both the fields, filters and group by
161 $this->addCampaignFields('civicrm_membership', TRUE);
163 $this->_groupFilter
= TRUE;
164 $this->_currencyColumn
= 'civicrm_contribution_currency';
165 parent
::__construct();
168 public function select() {
171 $this->_columnHeaders
= [];
172 $select[] = " COUNT( DISTINCT {$this->_aliases['civicrm_membership']}.id ) as civicrm_membership_member_count";
173 $select['joinDate'] = " {$this->_aliases['civicrm_membership']}.join_date as civicrm_membership_member_join_date";
174 $this->_columnHeaders
["civicrm_membership_member_join_date"] = [
175 'title' => ts('Member Since'),
176 'type' => CRM_Utils_Type
::T_DATE
,
178 foreach ($this->_columns
as $tableName => $table) {
179 if (array_key_exists('group_bys', $table)) {
180 foreach ($table['group_bys'] as $fieldName => $field) {
181 if (!empty($this->_params
['group_bys'][$fieldName])) {
183 switch (CRM_Utils_Array
::value($fieldName, $this->_params
['group_bys_freq'])) {
185 $select[] = "DATE_SUB({$field['dbAlias']}, INTERVAL WEEKDAY({$field['dbAlias']}) DAY) AS {$tableName}_{$fieldName}_start";
187 $select[] = "YEARWEEK({$field['dbAlias']}) AS {$tableName}_{$fieldName}_subtotal";
188 $select[] = "WEEKOFYEAR({$field['dbAlias']}) AS {$tableName}_{$fieldName}_interval";
189 $field['title'] = 'Week';
193 $select[] = "MAKEDATE(YEAR({$field['dbAlias']}), 1) AS {$tableName}_{$fieldName}_start";
194 $select[] = "YEAR({$field['dbAlias']}) AS {$tableName}_{$fieldName}_subtotal";
195 $select[] = "YEAR({$field['dbAlias']}) AS {$tableName}_{$fieldName}_interval";
196 $field['title'] = 'Year';
200 $select[] = "DATE_SUB({$field['dbAlias']}, INTERVAL (DAYOFMONTH({$field['dbAlias']})-1) DAY) as {$tableName}_{$fieldName}_start";
201 $select[] = "MONTH({$field['dbAlias']}) AS {$tableName}_{$fieldName}_subtotal";
202 $select[] = "MONTHNAME({$field['dbAlias']}) AS {$tableName}_{$fieldName}_interval";
203 $field['title'] = 'Month';
207 $select[] = "STR_TO_DATE(CONCAT( 3 * QUARTER( {$field['dbAlias']} ) -2 , '/', '1', '/', YEAR( {$field['dbAlias']} ) ), '%m/%d/%Y') AS {$tableName}_{$fieldName}_start";
208 $select[] = "QUARTER({$field['dbAlias']}) AS {$tableName}_{$fieldName}_subtotal";
209 $select[] = "QUARTER({$field['dbAlias']}) AS {$tableName}_{$fieldName}_interval";
210 $field['title'] = 'Quarter';
213 if (!empty($this->_params
['group_bys_freq'][$fieldName])) {
214 $this->_interval
= $field['title'];
215 $this->_columnHeaders
["{$tableName}_{$fieldName}_start"]['title'] = $field['title'] . ' Beginning';
216 $this->_columnHeaders
["{$tableName}_{$fieldName}_start"]['type'] = $field['type'];
217 $this->_columnHeaders
["{$tableName}_{$fieldName}_start"]['group_by'] = $this->_params
['group_bys_freq'][$fieldName];
219 // just to make sure these values are transferred to rows.
220 // since we need that for calculation purpose,
221 // e.g making subtotals look nicer or graphs
222 $this->_columnHeaders
["{$tableName}_{$fieldName}_interval"] = ['no_display' => TRUE];
223 $this->_columnHeaders
["{$tableName}_{$fieldName}_subtotal"] = ['no_display' => TRUE];
231 if (array_key_exists('fields', $table)) {
232 foreach ($table['fields'] as $fieldName => $field) {
233 if (!empty($field['required']) ||
234 !empty($this->_params
['fields'][$fieldName])
237 // only include statistics columns if set
238 if (!empty($field['statistics'])) {
239 $this->_statFields
[] = 'civicrm_membership_member_count';
240 foreach ($field['statistics'] as $stat => $label) {
241 switch (strtolower($stat)) {
243 $select[] = "IFNULL(SUM({$field['dbAlias']}), 0) as {$tableName}_{$fieldName}_{$stat}";
244 $this->_columnHeaders
["{$tableName}_{$fieldName}_{$stat}"]['title'] = $label;
245 $this->_columnHeaders
["{$tableName}_{$fieldName}_{$stat}"]['type'] = $field['type'];
246 $this->_statFields
[] = "{$tableName}_{$fieldName}_{$stat}";
250 $select[] = "COUNT({$field['dbAlias']}) as {$tableName}_{$fieldName}_{$stat}";
251 $this->_columnHeaders
["{$tableName}_{$fieldName}_{$stat}"]['type'] = CRM_Utils_Type
::T_INT
;
252 $this->_columnHeaders
["{$tableName}_{$fieldName}_{$stat}"]['title'] = $label;
253 $this->_statFields
[] = "{$tableName}_{$fieldName}_{$stat}";
257 $select[] = "IFNULL(ROUND(AVG({$field['dbAlias']}),2), 0) as {$tableName}_{$fieldName}_{$stat}";
258 $this->_columnHeaders
["{$tableName}_{$fieldName}_{$stat}"]['type'] = $field['type'];
259 $this->_columnHeaders
["{$tableName}_{$fieldName}_{$stat}"]['title'] = $label;
260 $this->_statFields
[] = "{$tableName}_{$fieldName}_{$stat}";
265 elseif ($fieldName == 'membership_type_id') {
266 if (empty($this->_params
['group_bys']['membership_type_id']) &&
267 !empty($this->_params
['group_bys']['join_date'])
269 $select[] = "GROUP_CONCAT(DISTINCT {$field['dbAlias']} ORDER BY {$field['dbAlias']} ) as {$tableName}_{$fieldName}";
272 $select[] = "{$field['dbAlias']} as {$tableName}_{$fieldName}";
274 $this->_columnHeaders
["{$tableName}_{$fieldName}"]['title'] = $field['title'];
275 $this->_columnHeaders
["{$tableName}_{$fieldName}"]['operatorType'] = CRM_Utils_Array
::value('operatorType', $field);
278 $select[] = "{$field['dbAlias']} as {$tableName}_{$fieldName}";
279 $this->_columnHeaders
["{$tableName}_{$fieldName}"]['title'] = $field['title'];
280 $this->_columnHeaders
["{$tableName}_{$fieldName}"]['operatorType'] = CRM_Utils_Array
::value('operatorType', $field);
285 $this->_columnHeaders
["civicrm_membership_member_count"] = [
286 'title' => ts('Member Count'),
287 'type' => CRM_Utils_Type
::T_INT
,
290 //If grouping is availabled then remove join date from field
292 unset($select['joinDate']);
293 unset($this->_columnHeaders
["civicrm_membership_member_join_date"]);
295 $this->_selectClauses
= $select;
296 $this->_select
= "SELECT " . implode(', ', $select) . " ";
299 public function from() {
301 FROM civicrm_membership {$this->_aliases['civicrm_membership']}
303 LEFT JOIN civicrm_contact {$this->_aliases['civicrm_contact']} ON ( {$this->_aliases['civicrm_membership']}.contact_id = {$this->_aliases['civicrm_contact']}.id )
305 LEFT JOIN civicrm_membership_status
306 ON ({$this->_aliases['civicrm_membership']}.status_id = civicrm_membership_status.id )
307 LEFT JOIN civicrm_membership_payment payment
308 ON ( {$this->_aliases['civicrm_membership']}.id = payment.membership_id )
309 LEFT JOIN civicrm_contribution {$this->_aliases['civicrm_contribution']}
310 ON payment.contribution_id = {$this->_aliases['civicrm_contribution']}.id";
313 public function where() {
314 $this->_whereClauses
[] = "{$this->_aliases['civicrm_membership']}.is_test = 0 AND
315 {$this->_aliases['civicrm_contact']}.is_deleted = 0";
319 public function groupBy() {
320 $this->_groupBy
= "";
321 if (is_array($this->_params
['group_bys']) &&
322 !empty($this->_params
['group_bys'])
324 foreach ($this->_columns
as $table) {
325 if (array_key_exists('group_bys', $table)) {
326 foreach ($table['group_bys'] as $fieldName => $field) {
327 if (!empty($this->_params
['group_bys'][$fieldName])) {
328 if (!empty($field['chart'])) {
329 $this->assign('chartSupported', TRUE);
331 if (!empty($table['group_bys'][$fieldName]['frequency']) &&
332 !empty($this->_params
['group_bys_freq'][$fieldName])
335 $append = "YEAR({$field['dbAlias']})";
336 if (in_array(strtolower($this->_params
['group_bys_freq'][$fieldName]),
341 $this->_groupByArray
[] = $append;
342 $this->_groupByArray
[] = "{$this->_params['group_bys_freq'][$fieldName]}({$field['dbAlias']})";
346 $this->_groupByArray
[] = $field['dbAlias'];
353 $this->_rollup
= ' WITH ROLLUP';
354 $this->_select
= CRM_Contact_BAO_Query
::appendAnyValueToSelect($this->_selectClauses
, array_filter($this->_groupByArray
));
355 $this->_groupBy
= 'GROUP BY ' . implode(', ', array_filter($this->_groupByArray
)) .
356 " {$this->_rollup} ";
359 $this->_groupBy
= CRM_Contact_BAO_Query
::getGroupByFromSelectColumns($this->_selectClauses
, "{$this->_aliases['civicrm_membership']}.join_date");
368 public function statistics(&$rows) {
369 $statistics = parent
::statistics($rows);
371 SELECT COUNT({$this->_aliases['civicrm_contribution']}.total_amount ) as count,
372 IFNULL(SUM({$this->_aliases['civicrm_contribution']}.total_amount ), 0) as amount,
373 IFNULL(ROUND(AVG({$this->_aliases['civicrm_contribution']}.total_amount), 2),0) as avg,
374 COUNT( DISTINCT {$this->_aliases['civicrm_membership']}.id ) as memberCount,
375 {$this->_aliases['civicrm_contribution']}.currency as currency
378 $sql = "{$select} {$this->_from} {$this->_where}
379 GROUP BY {$this->_aliases['civicrm_contribution']}.currency
382 $dao = CRM_Core_DAO
::executeQuery($sql);
384 $totalAmount = $average = [];
385 $count = $memberCount = 0;
386 while ($dao->fetch()) {
387 $totalAmount[] = CRM_Utils_Money
::format($dao->amount
, $dao->currency
) . "(" . $dao->count
. ")";
388 $average[] = CRM_Utils_Money
::format($dao->avg
, $dao->currency
);
389 $count +
= $dao->count
;
390 $memberCount +
= $dao->memberCount
;
392 $statistics['counts']['amount'] = [
393 'title' => ts('Total Amount'),
394 'value' => implode(', ', $totalAmount),
395 'type' => CRM_Utils_Type
::T_STRING
,
397 $statistics['counts']['count'] = [
398 'title' => ts('Total Contributions'),
401 $statistics['counts']['memberCount'] = [
402 'title' => ts('Total Members'),
403 'value' => $memberCount,
405 $statistics['counts']['avg'] = [
406 'title' => ts('Average'),
407 'value' => implode(', ', $average),
408 'type' => CRM_Utils_Type
::T_STRING
,
411 if (!(int) $statistics['counts']['amount']['value']) {
412 //if total amount is zero then hide Chart Options
413 $this->assign('chartSupported', FALSE);
419 public function postProcess() {
420 parent
::postProcess();
423 public function getOperationPair($type = "string", $fieldName = NULL) {
424 //re-name IS NULL/IS NOT NULL for clarity
425 if ($fieldName == 'owner_membership_id') {
427 $result['nll'] = ts('Primary members only');
428 $result['nnll'] = ts('Non-primary members only');
429 $options = parent
::getOperationPair($type, $fieldName);
430 foreach ($options as $key => $label) {
431 if (!array_key_exists($key, $result)) {
432 $result[$key] = $label;
437 $result = parent
::getOperationPair($type, $fieldName);
445 public function buildChart(&$rows) {
448 $membershipTypeValues = CRM_Member_PseudoConstant
::membershipType();
449 $isMembershipType = CRM_Utils_Array
::value('membership_type_id', $this->_params
['group_bys']);
450 $isJoiningDate = CRM_Utils_Array
::value('join_date', $this->_params
['group_bys']);
451 if (!empty($this->_params
['charts'])) {
452 foreach ($rows as $key => $row) {
453 if (!($row['civicrm_membership_join_date_subtotal'] &&
454 $row['civicrm_membership_membership_type_id']
459 if ($isMembershipType) {
460 $join_date = CRM_Utils_Array
::value('civicrm_membership_join_date_start', $row);
461 $displayInterval = CRM_Utils_Array
::value('civicrm_membership_join_date_interval', $row);
463 list($year, $month) = explode('-', $join_date);
465 if (!empty($row['civicrm_membership_join_date_subtotal'])) {
467 switch ($this->_interval
) {
469 $displayRange = $displayInterval . ' ' . $year;
473 $displayRange = 'Quarter ' . $displayInterval . ' of ' . $year;
477 $displayRange = 'Week ' . $displayInterval . ' of ' . $year;
481 $displayRange = $year;
484 $membershipType = $displayRange . "-" .
485 $membershipTypeValues[$row['civicrm_membership_membership_type_id']];
489 $membershipType = $membershipTypeValues[$row['civicrm_membership_membership_type_id']];
492 $interval[$membershipType] = $membershipType;
493 $display[$membershipType] = $row['civicrm_contribution_total_amount_sum'];
496 $graphRows['receive_date'][] = CRM_Utils_Array
::value('civicrm_membership_join_date_start', $row);
497 $graphRows[$this->_interval
][] = CRM_Utils_Array
::value('civicrm_membership_join_date_interval', $row);
498 $graphRows['value'][] = $row['civicrm_contribution_total_amount_sum'];
504 if ($isMembershipType) {
505 $graphRows['value'] = $display;
507 'legend' => ts('Membership Summary'),
508 'xname' => ts('Member Since / Member Type'),
509 'yname' => ts('Fees'),
511 CRM_Utils_Chart
::reportChart($graphRows, $this->_params
['charts'], $interval, $chartInfo);
514 CRM_Utils_Chart
::chart($graphRows, $this->_params
['charts'], $this->_interval
);
517 $this->assign('chartType', $this->_params
['charts']);
521 * Alter display of rows.
523 * Iterate through the rows retrieved via SQL and make changes for display purposes,
524 * such as rendering contacts as links.
527 * Rows generated by SQL, with an array for each row.
529 public function alterDisplay(&$rows) {
531 foreach ($rows as $rowNum => $row) {
532 // make count columns point to detail report
533 if (!empty($this->_params
['group_bys']['join_date']) &&
534 !empty($row['civicrm_membership_join_date_start']) &&
535 $row['civicrm_membership_join_date_start'] &&
536 $row['civicrm_membership_join_date_subtotal']
539 $dateStart = CRM_Utils_Date
::customFormat($row['civicrm_membership_join_date_start'], '%Y%m%d');
540 $endDate = new DateTime($dateStart);
543 list($dateEnd['Y'], $dateEnd['M'], $dateEnd['d']) = explode(':', $endDate->format('Y:m:d'));
545 switch (strtolower($this->_params
['group_bys_freq']['join_date'])) {
547 $dateEnd = date("Ymd", mktime(0, 0, 0, $dateEnd['M'] +
1,
548 $dateEnd['d'] - 1, $dateEnd['Y']
553 $dateEnd = date("Ymd", mktime(0, 0, 0, $dateEnd['M'],
554 $dateEnd['d'] - 1, $dateEnd['Y'] +
1
559 $dateEnd = date("Ymd", mktime(0, 0, 0, $dateEnd['M'],
560 $dateEnd['d'] +
6, $dateEnd['Y']
565 $dateEnd = date("Ymd", mktime(0, 0, 0, $dateEnd['M'] +
3,
566 $dateEnd['d'] - 1, $dateEnd['Y']
571 if (!empty($this->_params
['group_bys']['membership_type_id']) &&
572 $typeID = $row['civicrm_membership_membership_type_id']
574 $typeUrl = "&tid_op=in&tid_value={$typeID}";
577 if (!empty($this->_params
['status_id_value'])) {
578 $statusUrl = "&sid_op=in&sid_value=" .
579 implode(",", $this->_params
['status_id_value']);
581 $url = CRM_Report_Utils_Report
::getNextUrl('member/detail',
582 "reset=1&force=1&membership_join_date_from={$dateStart}&membership_join_date_to={$dateEnd}{$typeUrl}{$statusUrl}",
583 $this->_absoluteUrl
, $this->_id
, $this->_drilldownReport
585 $row['civicrm_membership_join_date_start'] = CRM_Utils_Date
::format($row['civicrm_membership_join_date_start']);
586 $rows[$rowNum]['civicrm_membership_join_date_start_link'] = $url;
587 $rows[$rowNum]['civicrm_membership_join_date_start_hover'] = ts("Lists Summary of Memberships for this date unit.");
592 // handle Membership Types
593 if (array_key_exists('civicrm_membership_membership_type_id', $row)) {
594 if ($value = $row['civicrm_membership_membership_type_id']) {
595 $value = explode(',', $value);
596 foreach ($value as $key => $id) {
597 $value[$key] = CRM_Member_PseudoConstant
::membershipType($id, FALSE);
599 $rows[$rowNum]['civicrm_membership_membership_type_id'] = implode(' , ', $value);
604 // make subtotals look nicer
605 if (array_key_exists('civicrm_membership_join_date_subtotal', $row) &&
606 !$row['civicrm_membership_join_date_subtotal']
608 $this->fixSubTotalDisplay($rows[$rowNum], $this->_statFields
);
611 elseif (array_key_exists('civicrm_membership_join_date_subtotal', $row) &&
612 $row['civicrm_membership_join_date_subtotal'] &&
613 !$row['civicrm_membership_membership_type_id']
615 $this->fixSubTotalDisplay($rows[$rowNum], $this->_statFields
, FALSE);
616 $rows[$rowNum]['civicrm_membership_membership_type_id'] = '<b>' . ts('Subtotal') . '</b>';
620 // If using campaigns, convert campaign_id to campaign title
621 if (array_key_exists('civicrm_membership_campaign_id', $row)) {
622 if ($value = $row['civicrm_membership_campaign_id']) {
623 $rows[$rowNum]['civicrm_membership_campaign_id'] = $this->campaigns
[$value];
628 // skip looking further in rows, if first row itself doesn't
629 // have the column we need