Merge pull request #13967 from eileenmcnaughton/activity_token
[civicrm-core.git] / CRM / Export / BAO / ExportProcessor.php
CommitLineData
6003a964 1<?php
2/*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 5 |
5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
6003a964 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 * @package CRM
6b83d5bd 31 * @copyright CiviCRM LLC (c) 2004-2019
6003a964 32 */
33
34/**
35 * Class CRM_Export_BAO_ExportProcessor
36 *
37 * Class to handle logic of export.
38 */
39class CRM_Export_BAO_ExportProcessor {
40
41 /**
42 * @var int
43 */
44 protected $queryMode;
45
46 /**
47 * @var int
48 */
49 protected $exportMode;
50
adabfa40 51 /**
52 * Array of fields in the main query.
53 *
54 * @var array
55 */
56 protected $queryFields = [];
57
71464b73 58 /**
59 * Either AND or OR.
60 *
61 * @var string
62 */
63 protected $queryOperator;
64
d41ab886 65 /**
66 * Requested output fields.
67 *
68 * If set to NULL then it is 'primary fields only'
69 * which actually means pretty close to all fields!
70 *
71 * @var array|null
72 */
73 protected $requestedFields;
74
b7db6051 75 /**
76 * Is the contact being merged into a single household.
77 *
78 * @var bool
79 */
80 protected $isMergeSameHousehold;
81
6d52bfe5 82 /**
83 * Only export contacts that can receive postal mail.
84 *
85 * Includes being alive, having an address & not having do_not_mail.
86 *
87 * @var bool
88 */
89 protected $isPostalableOnly;
90
944ed388 91 /**
92 * Key representing the head of household in the relationship array.
93 *
94 * e.g. ['8_b_a' => 'Household Member Is', '8_a_b = 'Household Member Of'.....]
95 *
96 * @var
97 */
98 protected $relationshipTypes = [];
99
704e3e9a 100 /**
101 * Array of properties to retrieve for relationships.
102 *
103 * @var array
104 */
105 protected $relationshipReturnProperties = [];
106
136f69a8 107 /**
108 * IDs of households that have already been exported.
109 *
110 * @var array
111 */
112 protected $exportedHouseholds = [];
113
ce12a9e0 114 /**
115 * Get return properties by relationship.
116 * @return array
117 */
118 public function getRelationshipReturnProperties() {
119 return $this->relationshipReturnProperties;
120 }
121
122 /**
123 * Export values for related contacts.
124 *
125 * @var array
126 */
127 protected $relatedContactValues = [];
128
c66a5741 129 /**
130 * @var array
131 */
132 protected $returnProperties = [];
133
28254dcb 134 /**
135 * @var array
136 */
137 protected $outputSpecification = [];
138
71464b73 139 /**
140 * CRM_Export_BAO_ExportProcessor constructor.
141 *
142 * @param int $exportMode
d41ab886 143 * @param array|NULL $requestedFields
71464b73 144 * @param string $queryOperator
b7db6051 145 * @param bool $isMergeSameHousehold
6d52bfe5 146 * @param bool $isPostalableOnly
71464b73 147 */
6d52bfe5 148 public function __construct($exportMode, $requestedFields, $queryOperator, $isMergeSameHousehold = FALSE, $isPostalableOnly = FALSE) {
71464b73 149 $this->setExportMode($exportMode);
150 $this->setQueryMode();
151 $this->setQueryOperator($queryOperator);
d41ab886 152 $this->setRequestedFields($requestedFields);
944ed388 153 $this->setRelationshipTypes();
b7db6051 154 $this->setIsMergeSameHousehold($isMergeSameHousehold);
6d52bfe5 155 $this->setisPostalableOnly($isPostalableOnly);
d41ab886 156 }
157
6d52bfe5 158 /**
159 * @return bool
160 */
161 public function isPostalableOnly() {
162 return $this->isPostalableOnly;
163 }
164
165 /**
166 * @param bool $isPostalableOnly
167 */
168 public function setIsPostalableOnly($isPostalableOnly) {
169 $this->isPostalableOnly = $isPostalableOnly;
170 }
d41ab886 171 /**
172 * @return array|null
173 */
174 public function getRequestedFields() {
175 return $this->requestedFields;
176 }
177
178 /**
179 * @param array|null $requestedFields
180 */
181 public function setRequestedFields($requestedFields) {
182 $this->requestedFields = $requestedFields;
71464b73 183 }
184
c66a5741 185 /**
186 * @return array
187 */
188 public function getReturnProperties() {
189 return $this->returnProperties;
190 }
191
192 /**
193 * @param array $returnProperties
194 */
195 public function setReturnProperties($returnProperties) {
196 $this->returnProperties = $returnProperties;
197 }
198
944ed388 199 /**
200 * @return array
201 */
202 public function getRelationshipTypes() {
203 return $this->relationshipTypes;
204 }
205
206 /**
207 */
208 public function setRelationshipTypes() {
209 $this->relationshipTypes = CRM_Contact_BAO_Relationship::getContactRelationshipType(
210 NULL,
211 NULL,
212 NULL,
213 NULL,
214 TRUE,
215 'name',
216 FALSE
217 );
218 }
219
ce12a9e0 220 /**
221 * Set the value for a relationship type field.
222 *
223 * In this case we are building up an array of properties for a related contact.
224 *
225 * These may be used for direct exporting or for merge to household depending on the
226 * options selected.
227 *
228 * @param string $relationshipType
229 * @param int $contactID
230 * @param string $field
231 * @param string $value
232 */
233 public function setRelationshipValue($relationshipType, $contactID, $field, $value) {
234 $this->relatedContactValues[$relationshipType][$contactID][$field] = $value;
235 }
236
237 /**
238 * Get the value for a relationship type field.
239 *
240 * In this case we are building up an array of properties for a related contact.
241 *
242 * These may be used for direct exporting or for merge to household depending on the
243 * options selected.
244 *
245 * @param string $relationshipType
246 * @param int $contactID
247 * @param string $field
248 *
249 * @return string
250 */
251 public function getRelationshipValue($relationshipType, $contactID, $field) {
252 return isset($this->relatedContactValues[$relationshipType][$contactID][$field]) ? $this->relatedContactValues[$relationshipType][$contactID][$field] : '';
253 }
254
136f69a8 255 /**
256 * Get the id of the related household.
257 *
258 * @param int $contactID
259 * @param string $relationshipType
260 *
261 * @return int
262 */
263 public function getRelatedHouseholdID($contactID, $relationshipType) {
264 return $this->relatedContactValues[$relationshipType][$contactID]['id'];
265 }
266
267 /**
268 * Has the household already been exported.
269 *
270 * @param int $housholdContactID
271 *
272 * @return bool
273 */
274 public function isHouseholdExported($housholdContactID) {
275 return isset($this->exportedHouseholds[$housholdContactID]);
276
277 }
278
b7db6051 279 /**
280 * @return bool
281 */
282 public function isMergeSameHousehold() {
283 return $this->isMergeSameHousehold;
284 }
285
286 /**
287 * @param bool $isMergeSameHousehold
288 */
289 public function setIsMergeSameHousehold($isMergeSameHousehold) {
290 $this->isMergeSameHousehold = $isMergeSameHousehold;
291 }
292
293 /**
294 * Return relationship types for household merge.
295 *
296 * @return mixed
297 */
298 public function getHouseholdRelationshipTypes() {
299 if (!$this->isMergeSameHousehold()) {
300 return [];
301 }
302 return [
303 CRM_Utils_Array::key('Household Member of', $this->getRelationshipTypes()),
304 CRM_Utils_Array::key('Head of Household for', $this->getRelationshipTypes()),
305 ];
306 }
944ed388 307
308 /**
309 * @param $fieldName
310 * @return bool
311 */
312 public function isRelationshipTypeKey($fieldName) {
313 return array_key_exists($fieldName, $this->relationshipTypes);
314 }
315
b7db6051 316
317 /**
318 * @param $fieldName
319 * @return bool
320 */
321 public function isHouseholdMergeRelationshipTypeKey($fieldName) {
322 return in_array($fieldName, $this->getHouseholdRelationshipTypes());
323 }
324
71464b73 325 /**
326 * @return string
327 */
328 public function getQueryOperator() {
329 return $this->queryOperator;
330 }
331
332 /**
333 * @param string $queryOperator
334 */
335 public function setQueryOperator($queryOperator) {
336 $this->queryOperator = $queryOperator;
337 }
338
adabfa40 339 /**
340 * @return array
341 */
342 public function getQueryFields() {
343 return $this->queryFields;
344 }
345
346 /**
347 * @param array $queryFields
348 */
349 public function setQueryFields($queryFields) {
5ebd7d09 350 // legacy hacks - we add these to queryFields because this
351 // pseudometadata is currently required.
352 $queryFields['im_provider']['pseudoconstant']['var'] = 'imProviders';
353 $queryFields['country']['context'] = 'country';
354 $queryFields['world_region']['context'] = 'country';
355 $queryFields['state_province']['context'] = 'province';
adabfa40 356 $this->queryFields = $queryFields;
357 }
358
6003a964 359 /**
360 * @return int
361 */
362 public function getQueryMode() {
363 return $this->queryMode;
364 }
365
366 /**
367 * Set the query mode based on the export mode.
368 */
369 public function setQueryMode() {
370
371 switch ($this->getExportMode()) {
372 case CRM_Export_Form_Select::CONTRIBUTE_EXPORT:
373 $this->queryMode = CRM_Contact_BAO_Query::MODE_CONTRIBUTE;
374 break;
375
376 case CRM_Export_Form_Select::EVENT_EXPORT:
377 $this->queryMode = CRM_Contact_BAO_Query::MODE_EVENT;
378 break;
379
380 case CRM_Export_Form_Select::MEMBER_EXPORT:
381 $this->queryMode = CRM_Contact_BAO_Query::MODE_MEMBER;
382 break;
383
384 case CRM_Export_Form_Select::PLEDGE_EXPORT:
385 $this->queryMode = CRM_Contact_BAO_Query::MODE_PLEDGE;
386 break;
387
388 case CRM_Export_Form_Select::CASE_EXPORT:
389 $this->queryMode = CRM_Contact_BAO_Query::MODE_CASE;
390 break;
391
392 case CRM_Export_Form_Select::GRANT_EXPORT:
393 $this->queryMode = CRM_Contact_BAO_Query::MODE_GRANT;
394 break;
395
396 case CRM_Export_Form_Select::ACTIVITY_EXPORT:
397 $this->queryMode = CRM_Contact_BAO_Query::MODE_ACTIVITY;
398 break;
399
400 default:
401 $this->queryMode = CRM_Contact_BAO_Query::MODE_CONTACTS;
402 }
403 }
404
405 /**
406 * @return int
407 */
408 public function getExportMode() {
409 return $this->exportMode;
410 }
411
412 /**
413 * @param int $exportMode
414 */
415 public function setExportMode($exportMode) {
416 $this->exportMode = $exportMode;
417 }
418
29034a98 419 /**
420 * Get the name for the export file.
421 *
422 * @return string
423 */
424 public function getExportFileName() {
425 switch ($this->getExportMode()) {
426 case CRM_Export_Form_Select::CONTACT_EXPORT:
427 return ts('CiviCRM Contact Search');
428
429 case CRM_Export_Form_Select::CONTRIBUTE_EXPORT:
430 return ts('CiviCRM Contribution Search');
431
432 case CRM_Export_Form_Select::MEMBER_EXPORT:
433 return ts('CiviCRM Member Search');
434
435 case CRM_Export_Form_Select::EVENT_EXPORT:
436 return ts('CiviCRM Participant Search');
437
438 case CRM_Export_Form_Select::PLEDGE_EXPORT:
439 return ts('CiviCRM Pledge Search');
440
441 case CRM_Export_Form_Select::CASE_EXPORT:
442 return ts('CiviCRM Case Search');
443
444 case CRM_Export_Form_Select::GRANT_EXPORT:
445 return ts('CiviCRM Grant Search');
446
447 case CRM_Export_Form_Select::ACTIVITY_EXPORT:
448 return ts('CiviCRM Activity Search');
449
450 default:
451 // Legacy code suggests the value could be 'financial' - ie. something
452 // other than what should be accepted. However, I suspect that this line is
453 // never hit.
454 return ts('CiviCRM Search');
455 }
456 }
457
af17bedf 458 /**
459 * Get the label for the header row based on the field to output.
460 *
461 * @param string $field
462 *
463 * @return string
464 */
465 public function getHeaderForRow($field) {
466 if (substr($field, -11) == 'campaign_id') {
467 // @todo - set this correctly in the xml rather than here.
468 // This will require a generalised handling cleanup
469 return ts('Campaign ID');
470 }
471 if ($this->isMergeSameHousehold() && $field === 'id') {
472 return ts('Household ID');
473 }
474 elseif (isset($this->getQueryFields()[$field]['title'])) {
475 return $this->getQueryFields()[$field]['title'];
476 }
477 elseif ($this->isExportPaymentFields() && array_key_exists($field, $this->getcomponentPaymentFields())) {
478 return CRM_Utils_Array::value($field, $this->getcomponentPaymentFields());
479 }
480 else {
481 return $field;
482 }
483 }
484
adabfa40 485 /**
486 * @param $params
487 * @param $order
adabfa40 488 * @param $returnProperties
489 * @return array
490 */
71464b73 491 public function runQuery($params, $order, $returnProperties) {
6d52bfe5 492 $addressWhere = '';
493 $params = array_merge($params, $this->getWhereParams());
494 if ($this->isPostalableOnly) {
495 if (array_key_exists('street_address', $returnProperties)) {
496 $addressWhere = " civicrm_address.street_address <> ''";
497 if (array_key_exists('supplemental_address_1', $returnProperties)) {
498 // We need this to be an OR rather than AND on the street_address so, hack it in.
499 $addressOptions = CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
500 'address_options', TRUE, NULL, TRUE
501 );
502 if (!empty($addressOptions['supplemental_address_1'])) {
503 $addressWhere .= " OR civicrm_address.supplemental_address_1 <> ''";
504 }
505 }
506 $addressWhere = ' AND (' . $addressWhere . ')';
507 }
508 }
adabfa40 509 $query = new CRM_Contact_BAO_Query($params, $returnProperties, NULL,
510 FALSE, FALSE, $this->getQueryMode(),
71464b73 511 FALSE, TRUE, TRUE, NULL, $this->getQueryOperator()
adabfa40 512 );
513
514 //sort by state
515 //CRM-15301
516 $query->_sort = $order;
517 list($select, $from, $where, $having) = $query->query();
518 $this->setQueryFields($query->_fields);
6d52bfe5 519 return array($query, $select, $from, $where . $addressWhere, $having);
adabfa40 520 }
521
28254dcb 522 /**
523 * Add a row to the specification for how to output data.
ae5d4caf 524 *
28254dcb 525 * @param string $key
28254dcb 526 * @param string $relationshipType
ae5d4caf 527 * @param string $locationType
528 * @param int $entityTypeID phone_type_id or provider_id for phone or im fields.
28254dcb 529 */
ae5d4caf 530 public function addOutputSpecification($key, $relationshipType = NULL, $locationType = NULL, $entityTypeID = NULL) {
5ebd7d09 531 $entityLabel = '';
ae5d4caf 532 if ($entityTypeID) {
533 if ($key === 'phone') {
5ebd7d09 534 $entityLabel = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_Phone', 'phone_type_id', $entityTypeID);
ae5d4caf 535 }
536 if ($key === 'im') {
5ebd7d09 537 $entityLabel = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_IM', 'provider_id', $entityTypeID);
ae5d4caf 538 }
539 }
ae5d4caf 540
5ebd7d09 541 // These oddly constructed keys are for legacy reasons. Altering them will affect test success
542 // but in time it may be good to rationalise them.
543 $label = $this->getOutputSpecificationLabel($key, $relationshipType, $locationType, $entityLabel);
544 $index = $this->getOutputSpecificationIndex($key, $relationshipType, $locationType, $entityLabel);
545 $fieldKey = $this->getOutputSpecificationFieldKey($key, $relationshipType, $locationType, $entityLabel);
546
547 $this->outputSpecification[$index]['header'] = $label;
548 $this->outputSpecification[$index]['sql_columns'] = $this->getSqlColumnDefinition($fieldKey, $key);
549
550 if ($relationshipType && $this->isHouseholdMergeRelationshipTypeKey($relationshipType)) {
551 $this->setColumnAsCalculationOnly($index);
552 }
553 $this->outputSpecification[$index]['metadata'] = $this->getMetaDataForField($key);
554 }
555
556 /**
557 * Get the metadata for the given field.
558 *
559 * @param $key
560 *
561 * @return array
562 */
563 public function getMetaDataForField($key) {
564 $mappings = ['contact_id' => 'id'];
565 if (isset($this->getQueryFields()[$key])) {
566 return $this->getQueryFields()[$key];
567 }
568 if (isset($mappings[$key])) {
569 return $this->getQueryFields()[$mappings[$key]];
570 }
571 return [];
2cd3d767 572 }
ae5d4caf 573
2cd3d767 574 /**
575 * @param $key
576 */
577 public function setSqlColumnDefn($key) {
5ebd7d09 578 $this->outputSpecification[$this->getMungedFieldName($key)]['sql_columns'] = $this->getSqlColumnDefinition($key, $this->getMungedFieldName($key));
de6ff509 579 }
580
581 /**
582 * Mark a column as only required for calculations.
583 *
584 * Do not include the row with headers.
585 *
586 * @param string $column
587 */
588 public function setColumnAsCalculationOnly($column) {
589 $this->outputSpecification[$column]['do_not_output_to_csv'] = TRUE;
28254dcb 590 }
591
592 /**
593 * @return array
594 */
595 public function getHeaderRows() {
596 $headerRows = [];
597 foreach ($this->outputSpecification as $key => $spec) {
de6ff509 598 if (empty($spec['do_not_output_to_csv'])) {
599 $headerRows[] = $spec['header'];
600 }
28254dcb 601 }
602 return $headerRows;
603 }
604
2cd3d767 605 /**
606 * @return array
607 */
608 public function getSQLColumns() {
609 $sqlColumns = [];
610 foreach ($this->outputSpecification as $key => $spec) {
611 if (empty($spec['do_not_output_to_sql'])) {
612 $sqlColumns[$key] = $spec['sql_columns'];
613 }
614 }
615 return $sqlColumns;
616 }
617
5ebd7d09 618 /**
619 * @return array
620 */
621 public function getMetadata() {
622 $metadata = [];
623 foreach ($this->outputSpecification as $key => $spec) {
624 $metadata[$key] = $spec['metadata'];
625 }
626 return $metadata;
627 }
2cd3d767 628
6e2de55d 629 /**
630 * Build the row for output.
631 *
632 * @param \CRM_Contact_BAO_Query $query
633 * @param CRM_Core_DAO $iterationDAO
634 * @param array $outputColumns
635 * @param $metadata
636 * @param $paymentDetails
637 * @param $addPaymentHeader
638 * @param $paymentTableId
639 *
136f69a8 640 * @return array|bool
6e2de55d 641 */
642 public function buildRow($query, $iterationDAO, $outputColumns, $metadata, $paymentDetails, $addPaymentHeader, $paymentTableId) {
643 $phoneTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Phone', 'phone_type_id');
644 $imProviders = CRM_Core_PseudoConstant::get('CRM_Core_DAO_IM', 'provider_id');
645
646 $row = [];
136f69a8 647 $householdMergeRelationshipType = $this->getHouseholdMergeTypeForRow($iterationDAO->contact_id);
648 if ($householdMergeRelationshipType) {
649 $householdID = $this->getRelatedHouseholdID($iterationDAO->contact_id, $householdMergeRelationshipType);
650 if ($this->isHouseholdExported($householdID)) {
651 return FALSE;
652 }
653 foreach (array_keys($outputColumns) as $column) {
654 $row[$column] = $this->getRelationshipValue($householdMergeRelationshipType, $iterationDAO->contact_id, $column);
655 }
656 $this->markHouseholdExported($householdID);
657 return $row;
658 }
659
6e2de55d 660 $query->convertToPseudoNames($iterationDAO);
661
662 //first loop through output columns so that we return what is required, and in same order.
663 foreach ($outputColumns as $field => $value) {
6e2de55d 664 // add im_provider to $dao object
665 if ($field == 'im_provider' && property_exists($iterationDAO, 'provider_id')) {
666 $iterationDAO->im_provider = $iterationDAO->provider_id;
667 }
668
669 //build row values (data)
670 $fieldValue = NULL;
671 if (property_exists($iterationDAO, $field)) {
672 $fieldValue = $iterationDAO->$field;
673 // to get phone type from phone type id
674 if ($field == 'phone_type_id' && isset($phoneTypes[$fieldValue])) {
675 $fieldValue = $phoneTypes[$fieldValue];
676 }
677 elseif ($field == 'provider_id' || $field == 'im_provider') {
678 $fieldValue = CRM_Utils_Array::value($fieldValue, $imProviders);
679 }
680 elseif (strstr($field, 'master_id')) {
681 $masterAddressId = NULL;
682 if (isset($iterationDAO->$field)) {
683 $masterAddressId = $iterationDAO->$field;
684 }
685 // get display name of contact that address is shared.
686 $fieldValue = CRM_Contact_BAO_Contact::getMasterDisplayName($masterAddressId);
687 }
688 }
689
690 if ($this->isRelationshipTypeKey($field)) {
136f69a8 691 $this->buildRelationshipFieldsForRow($row, $iterationDAO->contact_id, $value, $field);
6e2de55d 692 }
693 else {
694 $row[$field] = $this->getTransformedFieldValue($field, $iterationDAO, $fieldValue, $metadata, $paymentDetails);
695 }
696 }
697
698 // If specific payment fields have been selected for export, payment
699 // data will already be in $row. Otherwise, add payment related
700 // information, if appropriate.
701 if ($addPaymentHeader) {
702 if (!$this->isExportSpecifiedPaymentFields()) {
703 $nullContributionDetails = array_fill_keys(array_keys($this->getPaymentHeaders()), NULL);
704 if ($this->isExportPaymentFields()) {
705 $paymentData = CRM_Utils_Array::value($row[$paymentTableId], $paymentDetails);
706 if (!is_array($paymentData) || empty($paymentData)) {
707 $paymentData = $nullContributionDetails;
708 }
709 $row = array_merge($row, $paymentData);
710 }
711 elseif (!empty($paymentDetails)) {
712 $row = array_merge($row, $nullContributionDetails);
713 }
714 }
715 }
716 //remove organization name for individuals if it is set for current employer
717 if (!empty($row['contact_type']) &&
718 $row['contact_type'] == 'Individual' && array_key_exists('organization_name', $row)
719 ) {
720 $row['organization_name'] = '';
721 }
722 return $row;
723 }
724
136f69a8 725 /**
726 * If this row has a household whose details we should use get the relationship type key.
727 *
728 * @param $contactID
729 *
730 * @return bool
731 */
732 public function getHouseholdMergeTypeForRow($contactID) {
733 if (!$this->isMergeSameHousehold()) {
734 return FALSE;
735 }
736 foreach ($this->getHouseholdRelationshipTypes() as $relationshipType) {
737 if (isset($this->relatedContactValues[$relationshipType][$contactID])) {
738 return $relationshipType;
739 }
740 }
741 }
742
743 /**
744 * Mark the given household as already exported.
745 *
746 * @param $householdID
747 */
748 public function markHouseholdExported($householdID) {
749 $this->exportedHouseholds[$householdID] = $householdID;
750 }
751
8bd67f47 752 /**
753 * @param $field
754 * @param $iterationDAO
755 * @param $fieldValue
756 * @param $metadata
757 * @param $paymentDetails
758 *
759 * @return string
760 */
761 public function getTransformedFieldValue($field, $iterationDAO, $fieldValue, $metadata, $paymentDetails) {
762
763 $i18n = CRM_Core_I18n::singleton();
764 if ($field == 'id') {
765 return $iterationDAO->contact_id;
766 // special case for calculated field
767 }
768 elseif ($field == 'source_contact_id') {
769 return $iterationDAO->contact_id;
770 }
771 elseif ($field == 'pledge_balance_amount') {
772 return $iterationDAO->pledge_amount - $iterationDAO->pledge_total_paid;
773 // special case for calculated field
774 }
775 elseif ($field == 'pledge_next_pay_amount') {
776 return $iterationDAO->pledge_next_pay_amount + $iterationDAO->pledge_outstanding_amount;
777 }
778 elseif (isset($fieldValue) &&
779 $fieldValue != ''
780 ) {
781 //check for custom data
782 if ($cfID = CRM_Core_BAO_CustomField::getKeyID($field)) {
783 return CRM_Core_BAO_CustomField::displayValue($fieldValue, $cfID);
784 }
785
786 elseif (in_array($field, array(
787 'email_greeting',
788 'postal_greeting',
789 'addressee',
790 ))) {
791 //special case for greeting replacement
792 $fldValue = "{$field}_display";
793 return $iterationDAO->$fldValue;
794 }
795 else {
796 //normal fields with a touch of CRM-3157
797 switch ($field) {
798 case 'country':
799 case 'world_region':
800 return $i18n->crm_translate($fieldValue, array('context' => 'country'));
801
802 case 'state_province':
803 return $i18n->crm_translate($fieldValue, array('context' => 'province'));
804
805 case 'gender':
806 case 'preferred_communication_method':
807 case 'preferred_mail_format':
808 case 'communication_style':
809 return $i18n->crm_translate($fieldValue);
810
811 default:
812 if (isset($metadata[$field])) {
813 // No I don't know why we do it this way & whether we could
814 // make better use of pseudoConstants.
815 if (!empty($metadata[$field]['context'])) {
816 return $i18n->crm_translate($fieldValue, $metadata[$field]);
817 }
818 if (!empty($metadata[$field]['pseudoconstant'])) {
001a515b 819 if (!empty($metadata[$field]['bao'])) {
820 return CRM_Core_PseudoConstant::getLabel($metadata[$field]['bao'], $metadata[$field]['name'], $fieldValue);
821 }
8bd67f47 822 // This is not our normal syntax for pseudoconstants but I am a bit loath to
823 // call an external function until sure it is not increasing php processing given this
824 // may be iterated 100,000 times & we already have the $imProvider var loaded.
825 // That can be next refactor...
826 // Yes - definitely feeling hatred for this bit of code - I know you will beat me up over it's awfulness
827 // but I have to reach a stable point....
828 $varName = $metadata[$field]['pseudoconstant']['var'];
829 if ($varName === 'imProviders') {
830 return CRM_Core_PseudoConstant::getLabel('CRM_Core_DAO_IM', 'provider_id', $fieldValue);
831 }
832 if ($varName === 'phoneTypes') {
833 return CRM_Core_PseudoConstant::getLabel('CRM_Core_DAO_Phone', 'phone_type_id', $fieldValue);
834 }
835 }
836
837 }
838 return $fieldValue;
839 }
840 }
841 }
842 elseif ($this->isExportSpecifiedPaymentFields() && array_key_exists($field, $this->getcomponentPaymentFields())) {
843 $paymentTableId = $this->getPaymentTableID();
844 $paymentData = CRM_Utils_Array::value($iterationDAO->$paymentTableId, $paymentDetails);
845 $payFieldMapper = array(
846 'componentPaymentField_total_amount' => 'total_amount',
847 'componentPaymentField_contribution_status' => 'contribution_status',
848 'componentPaymentField_payment_instrument' => 'pay_instru',
849 'componentPaymentField_transaction_id' => 'trxn_id',
850 'componentPaymentField_received_date' => 'receive_date',
851 );
852 return CRM_Utils_Array::value($payFieldMapper[$field], $paymentData, '');
853 }
854 else {
855 // if field is empty or null
856 return '';
857 }
858 }
859
ce14544c 860 /**
861 * Get array of fields to return, over & above those defined in the main contact exportable fields.
862 *
863 * These include export mode specific fields & some fields apparently required as 'exportableFields'
864 * but not returned by the function of the same name.
865 *
866 * @return array
867 * Array of fields to return in the format ['field_name' => 1,...]
868 */
869 public function getAdditionalReturnProperties() {
ce14544c 870 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTACTS) {
871 $componentSpecificFields = [];
872 }
873 else {
874 $componentSpecificFields = CRM_Contact_BAO_Query::defaultReturnProperties($this->getQueryMode());
875 }
876 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_PLEDGE) {
877 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Pledge_BAO_Query::extraReturnProperties($this->getQueryMode()));
d28b6cf2 878 unset($componentSpecificFields['contribution_status_id']);
879 unset($componentSpecificFields['pledge_status_id']);
880 unset($componentSpecificFields['pledge_payment_status_id']);
ce14544c 881 }
882 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CASE) {
883 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Case_BAO_Query::extraReturnProperties($this->getQueryMode()));
884 }
885 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTRIBUTE) {
886 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Contribute_BAO_Query::softCreditReturnProperties(TRUE));
d28b6cf2 887 unset($componentSpecificFields['contribution_status_id']);
ce14544c 888 }
7626754f 889 return $componentSpecificFields;
ce14544c 890 }
891
d41ab886 892 /**
893 * Should payment fields be appended to the export.
894 *
895 * (This is pretty hacky so hopefully this function won't last long - notice
896 * how obviously it should be part of the above function!).
897 */
898 public function isExportPaymentFields() {
899 if ($this->getRequestedFields() === NULL
f065c170 900 && in_array($this->getQueryMode(), [
d41ab886 901 CRM_Contact_BAO_Query::MODE_EVENT,
902 CRM_Contact_BAO_Query::MODE_MEMBER,
903 CRM_Contact_BAO_Query::MODE_PLEDGE,
904 ])) {
905 return TRUE;
906 }
c66a5741 907 elseif ($this->isExportSpecifiedPaymentFields()) {
908 return TRUE;
909 }
d41ab886 910 return FALSE;
911 }
912
c66a5741 913 /**
914 * Has specific payment fields been requested (as opposed to via all fields).
915 *
916 * If specific fields have been requested then they get added at various points.
917 *
918 * @return bool
919 */
920 public function isExportSpecifiedPaymentFields() {
921 if ($this->getRequestedFields() !== NULL && $this->hasRequestedComponentPaymentFields()) {
922 return TRUE;
923 }
924 }
925
d41ab886 926 /**
927 * Get the name of the id field in the table that connects contributions to the export entity.
928 */
929 public function getPaymentTableID() {
930 if ($this->getRequestedFields() === NULL) {
931 $mapping = [
932 CRM_Contact_BAO_Query::MODE_EVENT => 'participant_id',
933 CRM_Contact_BAO_Query::MODE_MEMBER => 'membership_id',
934 CRM_Contact_BAO_Query::MODE_PLEDGE => 'pledge_payment_id',
935 ];
936 return isset($mapping[$this->getQueryMode()]) ? $mapping[$this->getQueryMode()] : '';
937 }
c66a5741 938 elseif ($this->hasRequestedComponentPaymentFields()) {
939 return 'participant_id';
940 }
d41ab886 941 return FALSE;
942 }
943
c66a5741 944 /**
945 * Have component payment fields been requested.
946 *
947 * @return bool
948 */
949 protected function hasRequestedComponentPaymentFields() {
950 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_EVENT) {
951 $participantPaymentFields = array_intersect_key($this->getComponentPaymentFields(), $this->getReturnProperties());
952 if (!empty($participantPaymentFields)) {
953 return TRUE;
954 }
955 }
d41ab886 956 return FALSE;
957 }
958
c66a5741 959 /**
960 * Get fields that indicate payment fields have been requested for a component.
961 *
0e32ed68 962 * Ideally this should be protected but making it temporarily public helps refactoring..
963 *
c66a5741 964 * @return array
965 */
0e32ed68 966 public function getComponentPaymentFields() {
c66a5741 967 return [
968 'componentPaymentField_total_amount' => ts('Total Amount'),
969 'componentPaymentField_contribution_status' => ts('Contribution Status'),
970 'componentPaymentField_received_date' => ts('Date Received'),
971 'componentPaymentField_payment_instrument' => ts('Payment Method'),
972 'componentPaymentField_transaction_id' => ts('Transaction ID'),
973 ];
974 }
975
05ad310f 976 /**
977 * Get headers for payment fields.
978 *
979 * Returns an array of contribution fields when the entity supports payment fields and specific fields
980 * are not specified. This is a transitional function for refactoring legacy code.
981 */
982 public function getPaymentHeaders() {
983 if ($this->isExportPaymentFields() && !$this->isExportSpecifiedPaymentFields()) {
984 return $this->getcomponentPaymentFields();
985 }
986 return [];
987 }
988
d41ab886 989 /**
990 * Get the default properties when not specified.
991 *
992 * In the UI this appears as 'Primary fields only' but in practice it's
993 * most of the kitchen sink and the hallway closet thrown in.
994 *
995 * Since CRM-952 custom fields are excluded, but no other form of mercy is shown.
996 *
997 * @return array
998 */
999 public function getDefaultReturnProperties() {
1000 $returnProperties = [];
1001 $fields = CRM_Contact_BAO_Contact::exportableFields('All', TRUE, TRUE);
704e3e9a 1002 $skippedFields = ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTACTS) ? [] : [
1003 'groups',
1004 'tags',
1005 'notes'
1006 ];
d41ab886 1007
1008 foreach ($fields as $key => $var) {
1009 if ($key && (substr($key, 0, 6) != 'custom') && !in_array($key, $skippedFields)) {
1010 $returnProperties[$key] = 1;
1011 }
1012 }
1013 $returnProperties = array_merge($returnProperties, $this->getAdditionalReturnProperties());
1014 return $returnProperties;
1015 }
1016
704e3e9a 1017 /**
1018 * Add the field to relationship return properties & return it.
1019 *
1020 * This function is doing both setting & getting which is yuck but it is an interim
1021 * refactor.
1022 *
1023 * @param array $value
1024 * @param string $relationshipKey
1025 *
1026 * @return array
1027 */
1028 public function setRelationshipReturnProperties($value, $relationshipKey) {
704e3e9a 1029 $relPhoneTypeId = $relIMProviderId = NULL;
1030 if (!empty($value[2])) {
1031 $relationField = CRM_Utils_Array::value(2, $value);
1032 if (trim(CRM_Utils_Array::value(3, $value))) {
1033 $relLocTypeId = CRM_Utils_Array::value(3, $value);
1034 }
1035 else {
1036 $relLocTypeId = 'Primary';
1037 }
1038
1039 if ($relationField == 'phone') {
1040 $relPhoneTypeId = CRM_Utils_Array::value(4, $value);
1041 }
1042 elseif ($relationField == 'im') {
1043 $relIMProviderId = CRM_Utils_Array::value(4, $value);
1044 }
1045 }
1046 elseif (!empty($value[4])) {
1047 $relationField = CRM_Utils_Array::value(4, $value);
1048 $relLocTypeId = CRM_Utils_Array::value(5, $value);
1049 if ($relationField == 'phone') {
1050 $relPhoneTypeId = CRM_Utils_Array::value(6, $value);
1051 }
1052 elseif ($relationField == 'im') {
1053 $relIMProviderId = CRM_Utils_Array::value(6, $value);
1054 }
1055 }
1056 if (in_array($relationField, $this->getValidLocationFields()) && is_numeric($relLocTypeId)) {
a16a432a 1057 $locationName = CRM_Core_PseudoConstant::getName('CRM_Core_BAO_Address', 'location_type_id', $relLocTypeId);
704e3e9a 1058 if ($relPhoneTypeId) {
a16a432a 1059 $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName]['phone-' . $relPhoneTypeId] = 1;
704e3e9a 1060 }
1061 elseif ($relIMProviderId) {
a16a432a 1062 $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName]['im-' . $relIMProviderId] = 1;
704e3e9a 1063 }
1064 else {
a16a432a 1065 $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName][$relationField] = 1;
704e3e9a 1066 }
1067 }
1068 else {
1069 $this->relationshipReturnProperties[$relationshipKey][$relationField] = 1;
1070 }
1071 return $this->relationshipReturnProperties[$relationshipKey];
1072 }
1073
ce12a9e0 1074 /**
1075 * Add the main return properties to the household merge properties if needed for merging.
1076 *
1077 * If we are using household merge we need to add these to the relationship properties to
1078 * be retrieved.
1079 *
1080 * @param $returnProperties
1081 */
1082 public function setHouseholdMergeReturnProperties($returnProperties) {
1083 foreach ($this->getHouseholdRelationshipTypes() as $householdRelationshipType) {
1084 $this->relationshipReturnProperties[$householdRelationshipType] = $returnProperties;
1085 }
1086 }
1087
704e3e9a 1088 /**
1089 * Get the default location fields to request.
1090 *
1091 * @return array
1092 */
1093 public function getValidLocationFields() {
1094 return [
1095 'street_address',
1096 'supplemental_address_1',
1097 'supplemental_address_2',
1098 'supplemental_address_3',
1099 'city',
1100 'postal_code',
1101 'postal_code_suffix',
1102 'geo_code_1',
1103 'geo_code_2',
1104 'state_province',
1105 'country',
1106 'phone',
1107 'email',
1108 'im',
1109 ];
1110 }
1111
aa3a113b 1112 /**
c8adad81 1113 * Get the sql column definition for the given field.
1114 *
5ebd7d09 1115 * @param string $fieldName
1116 * @param string $columnName
aa3a113b 1117 *
1118 * @return mixed
1119 */
5ebd7d09 1120 public function getSqlColumnDefinition($fieldName, $columnName) {
aa3a113b 1121
1122 // early exit for master_id, CRM-12100
1123 // in the DB it is an ID, but in the export, we retrive the display_name of the master record
1124 // also for current_employer, CRM-16939
5ebd7d09 1125 if ($columnName == 'master_id' || $columnName == 'current_employer') {
aa3a113b 1126 return "$fieldName varchar(128)";
1127 }
1128
1129 if (substr($fieldName, -11) == 'campaign_id') {
1130 // CRM-14398
1131 return "$fieldName varchar(128)";
1132 }
1133
1134 $queryFields = $this->getQueryFields();
1135 $lookUp = ['prefix_id', 'suffix_id'];
1136 // set the sql columns
5ebd7d09 1137 if (isset($queryFields[$columnName]['type'])) {
1138 switch ($queryFields[$columnName]['type']) {
aa3a113b 1139 case CRM_Utils_Type::T_INT:
1140 case CRM_Utils_Type::T_BOOLEAN:
5ebd7d09 1141 if (in_array($columnName, $lookUp)) {
aa3a113b 1142 return "$fieldName varchar(255)";
1143 }
1144 else {
1145 return "$fieldName varchar(16)";
1146 }
1147
1148 case CRM_Utils_Type::T_STRING:
5ebd7d09 1149 if (isset($queryFields[$columnName]['maxlength'])) {
1150 return "$fieldName varchar({$queryFields[$columnName]['maxlength']})";
aa3a113b 1151 }
1152 else {
1153 return "$fieldName varchar(255)";
1154 }
1155
1156 case CRM_Utils_Type::T_TEXT:
1157 case CRM_Utils_Type::T_LONGTEXT:
1158 case CRM_Utils_Type::T_BLOB:
1159 case CRM_Utils_Type::T_MEDIUMBLOB:
1160 return "$fieldName longtext";
1161
1162 case CRM_Utils_Type::T_FLOAT:
1163 case CRM_Utils_Type::T_ENUM:
1164 case CRM_Utils_Type::T_DATE:
1165 case CRM_Utils_Type::T_TIME:
1166 case CRM_Utils_Type::T_TIMESTAMP:
1167 case CRM_Utils_Type::T_MONEY:
1168 case CRM_Utils_Type::T_EMAIL:
1169 case CRM_Utils_Type::T_URL:
1170 case CRM_Utils_Type::T_CCNUM:
1171 default:
1172 return "$fieldName varchar(32)";
1173 }
1174 }
1175 else {
1176 if (substr($fieldName, -3, 3) == '_id') {
1177 return "$fieldName varchar(255)";
1178 }
1179 elseif (substr($fieldName, -5, 5) == '_note') {
1180 return "$fieldName text";
1181 }
1182 else {
1183 $changeFields = [
1184 'groups',
1185 'tags',
1186 'notes',
1187 ];
1188
1189 if (in_array($fieldName, $changeFields)) {
1190 return "$fieldName text";
1191 }
1192 else {
1193 // set the sql columns for custom data
5ebd7d09 1194 if (isset($queryFields[$columnName]['data_type'])) {
aa3a113b 1195
5ebd7d09 1196 switch ($queryFields[$columnName]['data_type']) {
aa3a113b 1197 case 'String':
1198 // May be option labels, which could be up to 512 characters
5ebd7d09 1199 $length = max(512, CRM_Utils_Array::value('text_length', $queryFields[$columnName]));
aa3a113b 1200 return "$fieldName varchar($length)";
1201
1202 case 'Country':
1203 case 'StateProvince':
1204 case 'Link':
1205 return "$fieldName varchar(255)";
1206
1207 case 'Memo':
1208 return "$fieldName text";
1209
1210 default:
1211 return "$fieldName varchar(255)";
1212 }
1213 }
1214 else {
1215 return "$fieldName text";
1216 }
1217 }
1218 }
1219 }
1220 }
1221
c8adad81 1222 /**
1223 * Get the munged field name.
1224 *
1225 * @param string $field
1226 * @return string
1227 */
1228 public function getMungedFieldName($field) {
1229 $fieldName = CRM_Utils_String::munge(strtolower($field), '_', 64);
1230 if ($fieldName == 'id') {
1231 $fieldName = 'civicrm_primary_id';
1232 }
1233 return $fieldName;
1234 }
1235
5ebd7d09 1236 /**
1237 * In order to respect the history of this class we need to index kinda illogically.
1238 *
1239 * On the bright side - this stuff is tested within a nano-byte of it's life.
1240 *
1241 * e.g '2-a-b_Home-City'
1242 *
1243 * @param string $key
1244 * @param string $relationshipType
1245 * @param string $locationType
1246 * @param $entityLabel
1247 *
1248 * @return string
1249 */
1250 protected function getOutputSpecificationIndex($key, $relationshipType, $locationType, $entityLabel) {
001a515b 1251 if ($entityLabel || $key === 'im') {
5ebd7d09 1252 // Just cos that's the history...
1253 if ($key !== 'master_id') {
1254 $key = $this->getHeaderForRow($key);
1255 }
1256 }
1257 if (!$relationshipType || $key !== 'id') {
1258 $key = $this->getMungedFieldName($key);
1259 }
1260 return $this->getMungedFieldName(
1261 ($relationshipType ? ($relationshipType . '_') : '')
1262 . ($locationType ? ($locationType . '_') : '')
1263 . $key
1264 . ($entityLabel ? ('_' . $entityLabel) : '')
1265 );
1266 }
1267
1268 /**
1269 * Get the compiled label for the column.
1270 *
1271 * e.g 'Gender', 'Employee Of-Home-city'
1272 *
1273 * @param string $key
1274 * @param string $relationshipType
1275 * @param string $locationType
1276 * @param string $entityLabel
1277 *
1278 * @return string
1279 */
1280 protected function getOutputSpecificationLabel($key, $relationshipType, $locationType, $entityLabel) {
1281 return ($relationshipType ? $this->getRelationshipTypes()[$relationshipType] . '-' : '')
1282 . ($locationType ? $locationType . '-' : '')
1283 . $this->getHeaderForRow($key)
1284 . ($entityLabel ? '-' . $entityLabel : '');
1285 }
1286
1287 /**
1288 * Get the mysql field name key.
1289 *
1290 * This key is locked in by tests but the reasons for the specific conventions -
1291 * ie. headings are used for keying fields in some cases, are likely
1292 * accidental rather than deliberate.
1293 *
1294 * This key is used for the output sql array.
1295 *
1296 * @param string $key
1297 * @param $relationshipType
1298 * @param $locationType
1299 * @param $entityLabel
1300 *
1301 * @return string
1302 */
1303 protected function getOutputSpecificationFieldKey($key, $relationshipType, $locationType, $entityLabel) {
001a515b 1304 if ($entityLabel || $key === 'im') {
5ebd7d09 1305 if ($key !== 'state_province' && $key !== 'id') {
001a515b 1306 // @todo - test removing this - indexing by $key should be fine...
5ebd7d09 1307 $key = $this->getHeaderForRow($key);
1308 }
1309 }
1310 if (!$relationshipType || $key !== 'id') {
1311 $key = $this->getMungedFieldName($key);
1312 }
1313 $fieldKey = $this->getMungedFieldName(
1314 ($relationshipType ? ($relationshipType . '_') : '')
1315 . ($locationType ? ($locationType . '_') : '')
1316 . $key
1317 . ($entityLabel ? ('_' . $entityLabel) : '')
1318 );
1319 return $fieldKey;
1320 }
1321
6d52bfe5 1322 /**
1323 * Get params for the where criteria.
1324 *
1325 * @return mixed
1326 */
1327 public function getWhereParams() {
1328 if (!$this->isPostalableOnly()) {
1329 return [];
1330 }
1331 $params['is_deceased'] = ['is_deceased', '=', 0, CRM_Contact_BAO_Query::MODE_CONTACTS];
1332 $params['do_not_mail'] = ['do_not_mail', '=', 0, CRM_Contact_BAO_Query::MODE_CONTACTS];
1333 return $params;
1334 }
1335
136f69a8 1336 /**
1337 * @param $row
1338 * @param $contactID
1339 * @param $value
1340 * @param $field
1341 */
1342 protected function buildRelationshipFieldsForRow(&$row, $contactID, $value, $field) {
1343 foreach (array_keys($value) as $property) {
1344 if ($property === 'location') {
1345 // @todo just undo all this nasty location wrangling!
1346 foreach ($value['location'] as $locationKey => $locationFields) {
1347 foreach (array_keys($locationFields) as $locationField) {
1348 $fieldKey = str_replace(' ', '_', $locationKey . '-' . $locationField);
1349 $row[$field . '_' . $fieldKey] = $this->getRelationshipValue($field, $contactID, $fieldKey);
1350 }
1351 }
1352 }
1353 else {
1354 $row[$field . '_' . $property] = $this->getRelationshipValue($field, $contactID, $property);
1355 }
1356 }
1357 }
1358
6003a964 1359}