Merge pull request #13972 from eileenmcnaughton/array_format_5
[civicrm-core.git] / CRM / Report / Form / Pledge / Detail.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28
29 /*
30 * !!!!!!!!!!!!!!!!!!!!
31 * NB: this is named detail but behaves like a summary report.
32 * It is also accessed through the Pledge Summary link in the UI
33 * This should presumably be changed.
34 * ~ Doten
35 * !!!!!!!!!!!!!!!!!!!!
36 *
37 */
38
39 /**
40 *
41 * @package CRM
42 * @copyright CiviCRM LLC (c) 2004-2019
43 */
44 class CRM_Report_Form_Pledge_Detail extends CRM_Report_Form {
45
46 protected $_summary = NULL;
47 protected $_totalPaid = FALSE;
48 protected $_pledgeStatuses = [];
49 protected $_customGroupExtends = [
50 'Pledge',
51 'Individual',
52 ];
53
54 /**
55 * This report has not been optimised for group filtering.
56 *
57 * The functionality for group filtering has been improved but not
58 * all reports have been adjusted to take care of it. This report has not
59 * and will run an inefficient query until fixed.
60 *
61 * CRM-19170
62 *
63 * @var bool
64 */
65 protected $groupFilterNotOptimised = TRUE;
66
67 /**
68 * Class constructor.
69 */
70 public function __construct() {
71 $this->_pledgeStatuses = CRM_Core_OptionGroup::values('pledge_status',
72 FALSE, FALSE, FALSE, NULL, 'label'
73 );
74
75 $this->_columns = [
76 'civicrm_contact' => [
77 'dao' => 'CRM_Contact_DAO_Contact',
78 'fields' => [
79 'sort_name' => [
80 'title' => ts('Contact Name'),
81 'required' => TRUE,
82 'no_repeat' => TRUE,
83 ],
84 ],
85 'filters' => [
86 'sort_name' => ['title' => ts('Contact Name')],
87 'id' => ['no_display' => TRUE],
88 ],
89 'grouping' => 'contact-fields',
90 ],
91 'civicrm_email' => [
92 'dao' => 'CRM_Core_DAO_Email',
93 'fields' => [
94 'email' => ['no_repeat' => TRUE],
95 ],
96 'grouping' => 'contact-fields',
97 ],
98 'civicrm_pledge' => [
99 'dao' => 'CRM_Pledge_DAO_Pledge',
100 'fields' => [
101 'id' => [
102 'no_display' => TRUE,
103 'required' => TRUE,
104 ],
105 'contact_id' => [
106 'no_display' => TRUE,
107 'required' => TRUE,
108 ],
109 'financial_type_id' => [
110 'title' => ts('Financial Type'),
111 ],
112 'amount' => [
113 'title' => ts('Pledge Amount'),
114 'required' => TRUE,
115 'type' => CRM_Utils_Type::T_MONEY,
116 ],
117 'currency' => [
118 'required' => TRUE,
119 'no_display' => TRUE,
120 ],
121 'frequency_unit' => [
122 'title' => ts('Frequency Unit'),
123 ],
124 'installments' => [
125 'title' => ts('Installments'),
126 ],
127 'pledge_create_date' => [
128 'title' => ts('Pledge Made Date'),
129 ],
130 'start_date' => [
131 'title' => ts('Pledge Start Date'),
132 'type' => CRM_Utils_Type::T_DATE,
133 ],
134 'end_date' => [
135 'title' => ts('Pledge End Date'),
136 'type' => CRM_Utils_Type::T_DATE,
137 ],
138 'status_id' => [
139 'title' => ts('Pledge Status'),
140 'required' => TRUE,
141 ],
142 ],
143 'filters' => [
144 'pledge_create_date' => [
145 'title' => ts('Pledge Made Date'),
146 'operatorType' => CRM_Report_Form::OP_DATE,
147 ],
148 'pledge_amount' => [
149 'title' => ts('Pledged Amount'),
150 'operatorType' => CRM_Report_Form::OP_INT,
151 ],
152 'currency' => [
153 'title' => ts('Currency'),
154 'operatorType' => CRM_Report_Form::OP_MULTISELECT,
155 'options' => CRM_Core_OptionGroup::values('currencies_enabled'),
156 'default' => NULL,
157 'type' => CRM_Utils_Type::T_STRING,
158 ],
159 'sid' => [
160 'name' => 'status_id',
161 'title' => ts('Pledge Status'),
162 'type' => CRM_Utils_Type::T_INT,
163 'operatorType' => CRM_Report_Form::OP_MULTISELECT,
164 'options' => CRM_Core_OptionGroup::values('pledge_status'),
165 ],
166 'financial_type_id' => [
167 'title' => ts('Financial Type'),
168 'type' => CRM_Utils_Type::T_INT,
169 'operatorType' => CRM_Report_Form::OP_MULTISELECT,
170 'options' => CRM_Contribute_PseudoConstant::financialType(),
171 ],
172
173 ],
174 ],
175 'civicrm_pledge_payment' => [
176 'dao' => 'CRM_Pledge_DAO_PledgePayment',
177 'fields' => [
178 'total_paid' => [
179 'title' => ts('Total Amount Paid'),
180 'type' => CRM_Utils_Type::T_MONEY,
181 ],
182 'balance_due' => [
183 'title' => ts('Balance Due'),
184 'default' => TRUE,
185 'type' => CRM_Utils_Type::T_MONEY,
186 ],
187 ],
188 ],
189 ];
190
191 $this->_columns += $this->getAddressColumns(['group_by' => FALSE]) + $this->getPhoneColumns();
192
193 // If we have a campaign, build out the relevant elements
194 $this->addCampaignFields('civicrm_pledge', TRUE);
195
196 $this->_groupFilter = TRUE;
197 $this->_tagFilter = TRUE;
198 $this->_currencyColumn = 'civicrm_pledge_currency';
199 parent::__construct();
200 }
201
202 public function preProcess() {
203 parent::preProcess();
204 }
205
206 public function select() {
207 parent::select();
208 }
209
210 /**
211 * If we are retrieving total paid we need to define the inclusion of pledge_payment.
212 *
213 * @param string $tableName
214 * @param $tableKey
215 * @param string $fieldName
216 * @param $field
217 *
218 * @return bool|string
219 */
220 public function selectClause(&$tableName, $tableKey, &$fieldName, &$field) {
221 if ($fieldName == 'total_paid') {
222 $this->_totalPaid = TRUE; // add pledge_payment join
223 $this->_columnHeaders["{$tableName}_{$fieldName}"] = [
224 'title' => $field['title'],
225 'type' => $field['type'],
226 ];
227 return "COALESCE(sum({$this->_aliases[$tableName]}.actual_amount), 0) as {$tableName}_{$fieldName}";
228 }
229 if ($fieldName == 'balance_due') {
230 $cancelledStatus = array_search('Cancelled', $this->_pledgeStatuses);
231 $completedStatus = array_search('Completed', $this->_pledgeStatuses);
232 $this->_totalPaid = TRUE; // add pledge_payment join
233 $this->_columnHeaders["{$tableName}_{$fieldName}"] = $field['title'];
234 $this->_columnHeaders["{$tableName}_{$fieldName}"] = [
235 'title' => $field['title'],
236 'type' => $field['type'],
237 ];
238 return "IF({$this->_aliases['civicrm_pledge']}.status_id IN({$cancelledStatus}, $completedStatus), 0, COALESCE({$this->_aliases['civicrm_pledge']}.amount, 0) - COALESCE(sum({$this->_aliases[$tableName]}.actual_amount),0)) as {$tableName}_{$fieldName}";
239 }
240 return FALSE;
241 }
242
243 public function groupBy() {
244 parent::groupBy();
245 if (empty($this->_groupBy) && $this->_totalPaid) {
246 $groupBy = ["{$this->_aliases['civicrm_pledge']}.id", "{$this->_aliases['civicrm_pledge']}.currency"];
247 $this->_groupBy = CRM_Contact_BAO_Query::getGroupByFromSelectColumns($this->_selectClauses, $groupBy);
248 }
249 }
250
251 public function from() {
252 $this->_from = "
253 FROM civicrm_pledge {$this->_aliases['civicrm_pledge']}
254 LEFT JOIN civicrm_contact {$this->_aliases['civicrm_contact']}
255 ON ({$this->_aliases['civicrm_contact']}.id =
256 {$this->_aliases['civicrm_pledge']}.contact_id )
257 {$this->_aclFrom} ";
258
259 if ($this->_totalPaid) {
260 $this->_from .= "
261 LEFT JOIN civicrm_pledge_payment {$this->_aliases['civicrm_pledge_payment']} ON
262 {$this->_aliases['civicrm_pledge']}.id = {$this->_aliases['civicrm_pledge_payment']}.pledge_id
263 AND {$this->_aliases['civicrm_pledge_payment']}.status_id = 1
264 ";
265 }
266
267 $this->joinPhoneFromContact();
268 $this->joinAddressFromContact();
269 $this->joinEmailFromContact();
270 }
271
272 /**
273 * @param $rows
274 *
275 * @return array
276 */
277 public function statistics(&$rows) {
278 $statistics = parent::statistics($rows);
279 //regenerate the from field without extra left join on pledge payments
280 $totalPaid = $this->_totalPaid;
281 $this->_totalPaid = FALSE;
282 $this->from();
283 $this->customDataFrom();
284 if (!$this->_having) {
285 $totalAmount = $average = [];
286 $count = 0;
287 $select = "
288 SELECT COUNT({$this->_aliases['civicrm_pledge']}.amount ) as count,
289 SUM({$this->_aliases['civicrm_pledge']}.amount ) as amount,
290 ROUND(AVG({$this->_aliases['civicrm_pledge']}.amount), 2) as avg,
291 {$this->_aliases['civicrm_pledge']}.currency as currency
292 ";
293
294 $group = "GROUP BY {$this->_aliases['civicrm_pledge']}.currency";
295 $sql = "{$select} {$this->_from} {$this->_where} {$group}";
296 $dao = CRM_Core_DAO::executeQuery($sql);
297 $count = $index = $totalCount = 0;
298 // this will run once per currency
299 while ($dao->fetch()) {
300 $totalAmount = CRM_Utils_Money::format($dao->amount, $dao->currency);
301 $average = CRM_Utils_Money::format($dao->avg, $dao->currency);
302 $count = $dao->count;
303 $totalCount .= $count;
304 $statistics['counts']['amount' . $index] = [
305 'title' => ts('Total Pledged') . ' (' . $dao->currency . ')',
306 'value' => $totalAmount,
307 'type' => CRM_Utils_Type::T_STRING,
308 ];
309 $statistics['counts']['avg' . $index] = [
310 'title' => ts('Average') . ' (' . $dao->currency . ')',
311 'value' => $average,
312 'type' => CRM_Utils_Type::T_STRING,
313 ];
314 $statistics['counts']['count' . $index] = [
315 'title' => ts('Total No Pledges') . ' (' . $dao->currency . ')',
316 'value' => $count,
317 'type' => CRM_Utils_Type::T_INT,
318 ];
319 $index++;
320 }
321 if ($totalCount > $count) {
322 $statistics['counts']['count' . $index] = [
323 'title' => ts('Total No Pledges'),
324 'value' => $totalCount,
325 'type' => CRM_Utils_Type::T_INT,
326 ];
327 }
328 }
329 // reset from clause
330 if ($totalPaid) {
331 $this->_totalPaid = TRUE;
332 $this->from();
333 }
334 return $statistics;
335 }
336
337 public function orderBy() {
338 $this->_orderBy = "ORDER BY {$this->_aliases['civicrm_contact']}.sort_name, {$this->_aliases['civicrm_contact']}.id";
339 }
340
341 public function where() {
342 $clauses = [];
343 foreach ($this->_columns as $tableName => $table) {
344 if (array_key_exists('filters', $table)) {
345 foreach ($table['filters'] as $fieldName => $field) {
346 $clause = NULL;
347 if (CRM_Utils_Array::value('type', $field) & CRM_Utils_Type::T_DATE) {
348 $relative = CRM_Utils_Array::value("{$fieldName}_relative", $this->_params);
349 $from = CRM_Utils_Array::value("{$fieldName}_from", $this->_params);
350 $to = CRM_Utils_Array::value("{$fieldName}_to", $this->_params);
351
352 if ($relative || $from || $to) {
353 $clause = $this->dateClause($field['name'], $relative, $from, $to, $field['type']);
354 }
355 }
356 else {
357 $op = CRM_Utils_Array::value("{$fieldName}_op", $this->_params);
358 if ($op) {
359 $clause = $this->whereClause($field,
360 $op,
361 CRM_Utils_Array::value("{$fieldName}_value",
362 $this->_params
363 ),
364 CRM_Utils_Array::value("{$fieldName}_min",
365 $this->_params
366 ),
367 CRM_Utils_Array::value("{$fieldName}_max",
368 $this->_params
369 )
370 );
371 }
372 }
373
374 if (!empty($clause)) {
375 $clauses[] = $clause;
376 }
377 }
378 }
379 }
380 if (empty($clauses)) {
381 $this->_where = "WHERE ({$this->_aliases['civicrm_pledge']}.is_test=0 ) ";
382 }
383 else {
384 $this->_where = "WHERE ({$this->_aliases['civicrm_pledge']}.is_test=0 ) AND
385 " . implode(' AND ', $clauses);
386 }
387
388 if ($this->_aclWhere) {
389 $this->_where .= " AND {$this->_aclWhere} ";
390 }
391 }
392
393 public function postProcess() {
394
395 $this->beginPostProcess();
396
397 // get the acl clauses built before we assemble the query
398 $this->buildACLClause($this->_aliases['civicrm_contact']);
399 $sql = $this->buildQuery();
400 $rows = $payment = [];
401
402 $dao = CRM_Core_DAO::executeQuery($sql);
403
404 // Set pager for the Main Query only which displays basic information
405 $this->setPager();
406 $this->assign('columnHeaders', $this->_columnHeaders);
407
408 while ($dao->fetch()) {
409 $pledgeID = $dao->civicrm_pledge_id;
410 foreach ($this->_columnHeaders as $columnHeadersKey => $columnHeadersValue) {
411 $row = [];
412 if (property_exists($dao, $columnHeadersKey)) {
413 $display[$pledgeID][$columnHeadersKey] = $dao->$columnHeadersKey;
414 }
415 }
416 $pledgeIDArray[] = $pledgeID;
417 }
418
419 // Add Special headers
420 $this->_columnHeaders['scheduled_date'] = [
421 'type' => CRM_Utils_Type::T_DATE,
422 'title' => ts('Next Payment Due'),
423 ];
424 $this->_columnHeaders['scheduled_amount'] = [
425 'type' => CRM_Utils_Type::T_MONEY,
426 'title' => ts('Next Payment Amount'),
427 ];
428 $this->_columnHeaders['status_id'] = NULL;
429
430 /*
431 * this is purely about ordering the total paid & balance due fields off to the end
432 * of the table in case custom or address fields cause them to fall in the middle
433 * (arguably the pledge amount should be moved to after these fields too)
434 *
435 */
436 $tableHeaders = [
437 'civicrm_pledge_payment_total_paid',
438 'civicrm_pledge_payment_balance_due',
439 ];
440
441 foreach ($tableHeaders as $header) {
442 //per above, unset & reset them so they move to the end
443 if (isset($this->_columnHeaders[$header])) {
444 $headervalue = $this->_columnHeaders[$header];
445 unset($this->_columnHeaders[$header]);
446 $this->_columnHeaders[$header] = $headervalue;
447 }
448 }
449
450 // To Display Payment Details of pledged amount
451 // for pledge payments In Progress
452 if (!empty($display)) {
453 $statusId = array_keys(CRM_Core_PseudoConstant::accountOptionValues("contribution_status", NULL, " AND v.name IN ('Pending', 'Overdue')"));
454 $statusId = implode(',', $statusId);
455 $select = "payment.pledge_id, payment.scheduled_amount, pledge.contact_id";
456 $sqlPayment = "
457 SELECT min(payment.scheduled_date) as scheduled_date,
458 {$select}
459
460 FROM civicrm_pledge_payment payment
461 LEFT JOIN civicrm_pledge pledge
462 ON pledge.id = payment.pledge_id
463
464 WHERE payment.status_id IN ({$statusId})
465
466 GROUP BY {$select}";
467
468 $daoPayment = CRM_Core_DAO::executeQuery($sqlPayment);
469
470 while ($daoPayment->fetch()) {
471 foreach ($pledgeIDArray as $key => $val) {
472 if ($val == $daoPayment->pledge_id) {
473
474 $display[$daoPayment->pledge_id]['scheduled_date'] = $daoPayment->scheduled_date;
475
476 $display[$daoPayment->pledge_id]['scheduled_amount'] = $daoPayment->scheduled_amount;
477 }
478 }
479 }
480 }
481
482 // Displaying entire data on the form
483 if (!empty($display)) {
484 foreach ($display as $key => $value) {
485 $row = [];
486 foreach ($this->_columnHeaders as $columnKey => $columnValue) {
487 if (array_key_exists($columnKey, $value)) {
488 $row[$columnKey] = !empty($value[$columnKey]) ? $value[$columnKey] : '';
489 }
490 }
491 $rows[] = $row;
492 }
493 }
494
495 unset($this->_columnHeaders['status_id']);
496 unset($this->_columnHeaders['civicrm_pledge_id']);
497 unset($this->_columnHeaders['civicrm_pledge_contact_id']);
498
499 $this->formatDisplay($rows, FALSE);
500 $this->doTemplateAssignment($rows);
501 $this->endPostProcess($rows);
502 }
503
504 /**
505 * Alter display of rows.
506 *
507 * Iterate through the rows retrieved via SQL and make changes for display purposes,
508 * such as rendering contacts as links.
509 *
510 * @param array $rows
511 * Rows generated by SQL, with an array for each row.
512 */
513 public function alterDisplay(&$rows) {
514 $entryFound = FALSE;
515 $checkList = [];
516 $display_flag = $prev_cid = $cid = 0;
517
518 foreach ($rows as $rowNum => $row) {
519 if (!empty($this->_noRepeats) && $this->_outputMode != 'csv') {
520 // don't repeat contact details if its same as the previous row
521 if (array_key_exists('civicrm_pledge_contact_id', $row)) {
522 if ($cid = $row['civicrm_pledge_contact_id']) {
523 if ($rowNum == 0) {
524 $prev_cid = $cid;
525 }
526 else {
527 if ($prev_cid == $cid) {
528 $display_flag = 1;
529 $prev_cid = $cid;
530 }
531 else {
532 $display_flag = 0;
533 $prev_cid = $cid;
534 }
535 }
536
537 if ($display_flag) {
538 foreach ($row as $colName => $colVal) {
539 if (in_array($colName, $this->_noRepeats)) {
540 unset($rows[$rowNum][$colName]);
541 }
542 }
543 }
544 $entryFound = TRUE;
545 }
546 }
547 }
548
549 // convert display name to links
550 if (array_key_exists('civicrm_contact_sort_name', $row) &&
551 array_key_exists('civicrm_pledge_contact_id', $row)
552 ) {
553 $url = CRM_Utils_System::url("civicrm/contact/view",
554 'reset=1&cid=' . $row['civicrm_pledge_contact_id'],
555 $this->_absoluteUrl
556 );
557 $rows[$rowNum]['civicrm_contact_sort_name_link'] = $url;
558 $rows[$rowNum]['civicrm_contact_sort_name_hover'] = ts("View Contact Summary for this Contact.");
559 $entryFound = TRUE;
560 }
561
562 if (array_key_exists('civicrm_pledge_financial_type_id', $row)) {
563 if ($value = $row['civicrm_pledge_financial_type_id']) {
564 $rows[$rowNum]['civicrm_pledge_financial_type_id'] = CRM_Contribute_PseudoConstant::financialType($value, FALSE);
565 }
566 $entryFound = TRUE;
567 }
568
569 //handle status id
570 if (array_key_exists('civicrm_pledge_status_id', $row)) {
571 if ($value = $row['civicrm_pledge_status_id']) {
572 $rows[$rowNum]['civicrm_pledge_status_id'] = CRM_Core_PseudoConstant::getLabel('CRM_Pledge_BAO_Pledge', 'status_id', $value);
573 }
574 $entryFound = TRUE;
575 }
576
577 // If using campaigns, convert campaign_id to campaign title
578 if (array_key_exists('civicrm_pledge_campaign_id', $row)) {
579 if ($value = $row['civicrm_pledge_campaign_id']) {
580 $rows[$rowNum]['civicrm_pledge_campaign_id'] = $this->campaigns[$value];
581 }
582 $entryFound = TRUE;
583 }
584
585 $entryFound = $this->alterDisplayAddressFields($row, $rows, $rowNum, 'pledge/detail', 'List all pledge(s) for this ') ? TRUE : $entryFound;
586
587 // skip looking further in rows, if first row itself doesn't
588 // have the column we need
589 if (!$entryFound) {
590 break;
591 }
592 }
593 }
594
595 }