Merge pull request #15930 from seamuslee001/standardise_pledge_search
[civicrm-core.git] / CRM / Export / BAO / ExportProcessor.php
CommitLineData
6003a964 1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6003a964 5 | |
bc77d7c0
TO
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 |
6003a964 9 +--------------------------------------------------------------------+
10 */
11
12/**
13 *
14 * @package CRM
ca5cec67 15 * @copyright CiviCRM LLC https://civicrm.org/licensing
6003a964 16 */
17
18/**
19 * Class CRM_Export_BAO_ExportProcessor
20 *
21 * Class to handle logic of export.
22 */
23class CRM_Export_BAO_ExportProcessor {
24
25 /**
26 * @var int
27 */
28 protected $queryMode;
29
30 /**
31 * @var int
32 */
33 protected $exportMode;
34
adabfa40 35 /**
36 * Array of fields in the main query.
37 *
38 * @var array
39 */
40 protected $queryFields = [];
41
71464b73 42 /**
43 * Either AND or OR.
44 *
45 * @var string
46 */
47 protected $queryOperator;
48
d41ab886 49 /**
50 * Requested output fields.
51 *
52 * If set to NULL then it is 'primary fields only'
53 * which actually means pretty close to all fields!
54 *
55 * @var array|null
56 */
57 protected $requestedFields;
58
b7db6051 59 /**
60 * Is the contact being merged into a single household.
61 *
62 * @var bool
63 */
64 protected $isMergeSameHousehold;
65
2761a916 66 /**
67 * Should contacts with the same address be merged.
68 *
69 * @var bool
70 */
71 protected $isMergeSameAddress = FALSE;
72
73 /**
74 * Fields that need to be retrieved for address merge purposes but should not be in output.
75 *
76 * @var array
77 */
78 protected $additionalFieldsForSameAddressMerge = [];
79
d97cefdc 80 /**
81 * Fields used for merging same contacts.
82 *
83 * @var array
84 */
85 protected $contactGreetingFields = [];
86
246e571e
JG
87 /**
88 * An array of primary IDs of the entity being exported.
89 *
90 * @var array
91 */
92 protected $ids = [];
93
2761a916 94 /**
95 * Get additional non-visible fields for address merge purposes.
96 *
97 * @return array
98 */
99 public function getAdditionalFieldsForSameAddressMerge(): array {
100 return $this->additionalFieldsForSameAddressMerge;
101 }
102
103 /**
104 * Set additional non-visible fields for address merge purposes.
105 */
106 public function setAdditionalFieldsForSameAddressMerge() {
107 if ($this->isMergeSameAddress) {
108 $fields = ['id', 'master_id', 'state_province_id', 'postal_greeting_id', 'addressee_id'];
109 foreach ($fields as $index => $field) {
110 if (!empty($this->getReturnProperties()[$field])) {
111 unset($fields[$index]);
112 }
113 }
114 $this->additionalFieldsForSameAddressMerge = array_fill_keys($fields, 1);
115 }
116 }
117
118 /**
119 * Should contacts with the same address be merged.
120 *
121 * @return bool
122 */
123 public function isMergeSameAddress(): bool {
124 return $this->isMergeSameAddress;
125 }
126
127 /**
128 * Set same address is to be merged.
129 *
130 * @param bool $isMergeSameAddress
131 */
132 public function setIsMergeSameAddress(bool $isMergeSameAddress) {
133 $this->isMergeSameAddress = $isMergeSameAddress;
134 }
135
51d84eca 136 /**
137 * Additional fields required to export postal fields.
138 *
139 * @var array
140 */
141 protected $additionalFieldsForPostalExport = [];
142
143 /**
144 * Get additional fields required to do a postal export.
145 *
146 * @return array
147 */
148 public function getAdditionalFieldsForPostalExport() {
149 return $this->additionalFieldsForPostalExport;
150 }
151
152 /**
153 * Set additional fields required for a postal export.
154 */
155 public function setAdditionalFieldsForPostalExport() {
156 if ($this->getRequestedFields() && $this->isPostalableOnly()) {
157 $fields = ['is_deceased', 'do_not_mail', 'street_address', 'supplemental_address_1'];
158 foreach ($fields as $index => $field) {
159 if (!empty($this->getReturnProperties()[$field])) {
e8f0579a 160 unset($fields[$index]);
51d84eca 161 }
162 }
163 $this->additionalFieldsForPostalExport = array_fill_keys($fields, 1);
164 }
165 }
166
6d52bfe5 167 /**
168 * Only export contacts that can receive postal mail.
169 *
170 * Includes being alive, having an address & not having do_not_mail.
171 *
172 * @var bool
173 */
174 protected $isPostalableOnly;
175
944ed388 176 /**
177 * Key representing the head of household in the relationship array.
178 *
179 * e.g. ['8_b_a' => 'Household Member Is', '8_a_b = 'Household Member Of'.....]
180 *
7b966967 181 * @var array
944ed388 182 */
183 protected $relationshipTypes = [];
184
704e3e9a 185 /**
186 * Array of properties to retrieve for relationships.
187 *
188 * @var array
189 */
190 protected $relationshipReturnProperties = [];
191
136f69a8 192 /**
193 * IDs of households that have already been exported.
194 *
195 * @var array
196 */
197 protected $exportedHouseholds = [];
198
ebebd629 199 /**
200 * Households to skip during export as they will be exported via their relationships anyway.
201 *
202 * @var array
203 */
204 protected $householdsToSkip = [];
205
e530cef4 206 /**
207 * Additional fields to return.
208 *
209 * This doesn't make much sense when we have a fields set but search build add it's own onto
210 * the 'Primary fields' (all) option.
211 *
212 * @var array
213 */
214 protected $additionalRequestedReturnProperties = [];
215
216 /**
217 * Get additional return properties.
218 *
219 * @return array
220 */
221 public function getAdditionalRequestedReturnProperties() {
222 return $this->additionalRequestedReturnProperties;
223 }
224
225 /**
226 * Set additional return properties.
227 *
228 * @param array $value
229 */
230 public function setAdditionalRequestedReturnProperties($value) {
231 // fix for CRM-7066
232 if (!empty($value['group'])) {
233 unset($value['group']);
234 $value['groups'] = 1;
235 }
236 $this->additionalRequestedReturnProperties = $value;
237 }
238
ce12a9e0 239 /**
240 * Get return properties by relationship.
241 * @return array
242 */
243 public function getRelationshipReturnProperties() {
244 return $this->relationshipReturnProperties;
245 }
246
247 /**
248 * Export values for related contacts.
249 *
250 * @var array
251 */
252 protected $relatedContactValues = [];
253
c66a5741 254 /**
255 * @var array
256 */
257 protected $returnProperties = [];
258
28254dcb 259 /**
260 * @var array
261 */
262 protected $outputSpecification = [];
263
e034ee91 264 /**
265 * @var string
266 */
267 protected $componentTable = '';
268
269 /**
270 * @return string
271 */
272 public function getComponentTable() {
273 return $this->componentTable;
274 }
275
276 /**
277 * Set the component table (if any).
278 *
279 * @param string $componentTable
280 */
281 public function setComponentTable($componentTable) {
282 $this->componentTable = $componentTable;
283 }
284
285 /**
286 * Clause from component search.
287 *
288 * @var string
289 */
290 protected $componentClause = '';
291
292 /**
293 * @return string
294 */
295 public function getComponentClause() {
296 return $this->componentClause;
297 }
298
299 /**
300 * @param string $componentClause
301 */
302 public function setComponentClause($componentClause) {
303 $this->componentClause = $componentClause;
304 }
305
68989e71 306 /**
307 * Name of a temporary table created to hold the results.
308 *
309 * Current decision making on when to create a temp table is kinda bad so this might change
310 * a bit as it is reviewed but basically we need a temp table or similar to calculate merging
311 * addresses. Merging households is handled in php. We create a temp table even when we don't need them.
312 *
313 * @var string
314 */
315 protected $temporaryTable;
316
317 /**
318 * @return string
319 */
320 public function getTemporaryTable(): string {
321 return $this->temporaryTable;
322 }
323
324 /**
325 * @param string $temporaryTable
326 */
327 public function setTemporaryTable(string $temporaryTable) {
328 $this->temporaryTable = $temporaryTable;
329 }
330
a2b21ae3 331 protected $postalGreetingTemplate;
332
333 /**
334 * @return mixed
335 */
336 public function getPostalGreetingTemplate() {
337 return $this->postalGreetingTemplate;
338 }
339
340 /**
341 * @param mixed $postalGreetingTemplate
342 */
343 public function setPostalGreetingTemplate($postalGreetingTemplate) {
344 $this->postalGreetingTemplate = $postalGreetingTemplate;
345 }
346
347 /**
348 * @return mixed
349 */
350 public function getAddresseeGreetingTemplate() {
351 return $this->addresseeGreetingTemplate;
352 }
353
354 /**
355 * @param mixed $addresseeGreetingTemplate
356 */
357 public function setAddresseeGreetingTemplate($addresseeGreetingTemplate) {
358 $this->addresseeGreetingTemplate = $addresseeGreetingTemplate;
359 }
360
361 protected $addresseeGreetingTemplate;
362
71464b73 363 /**
364 * CRM_Export_BAO_ExportProcessor constructor.
365 *
366 * @param int $exportMode
041ecc95 367 * @param array|null $requestedFields
71464b73 368 * @param string $queryOperator
b7db6051 369 * @param bool $isMergeSameHousehold
6d52bfe5 370 * @param bool $isPostalableOnly
2761a916 371 * @param bool $isMergeSameAddress
1e9e062d 372 * @param array $formValues
373 * Values from the export options form on contact export. We currently support these keys
374 * - postal_greeting
375 * - postal_other
376 * - addresee_greeting
377 * - addressee_other
378 */
379 public function __construct($exportMode, $requestedFields, $queryOperator, $isMergeSameHousehold = FALSE, $isPostalableOnly = FALSE, $isMergeSameAddress = FALSE, $formValues = []) {
71464b73 380 $this->setExportMode($exportMode);
381 $this->setQueryMode();
382 $this->setQueryOperator($queryOperator);
d41ab886 383 $this->setRequestedFields($requestedFields);
944ed388 384 $this->setRelationshipTypes();
b7db6051 385 $this->setIsMergeSameHousehold($isMergeSameHousehold);
806c5e1e 386 $this->setIsPostalableOnly($isPostalableOnly);
2761a916 387 $this->setIsMergeSameAddress($isMergeSameAddress);
806c5e1e 388 $this->setReturnProperties($this->determineReturnProperties());
2761a916 389 $this->setAdditionalFieldsForSameAddressMerge();
51d84eca 390 $this->setAdditionalFieldsForPostalExport();
3611a532 391 $this->setHouseholdMergeReturnProperties();
1e9e062d 392 $this->setGreetingStringsForSameAddressMerge($formValues);
d41ab886 393 }
394
6d52bfe5 395 /**
396 * @return bool
397 */
398 public function isPostalableOnly() {
399 return $this->isPostalableOnly;
400 }
401
402 /**
403 * @param bool $isPostalableOnly
404 */
405 public function setIsPostalableOnly($isPostalableOnly) {
406 $this->isPostalableOnly = $isPostalableOnly;
407 }
7b966967 408
d41ab886 409 /**
410 * @return array|null
411 */
412 public function getRequestedFields() {
04f8f758 413 return empty($this->requestedFields) ? NULL : $this->requestedFields;
d41ab886 414 }
415
416 /**
417 * @param array|null $requestedFields
418 */
419 public function setRequestedFields($requestedFields) {
420 $this->requestedFields = $requestedFields;
71464b73 421 }
422
c66a5741 423 /**
424 * @return array
425 */
426 public function getReturnProperties() {
51d84eca 427 return array_merge($this->returnProperties, $this->getAdditionalRequestedReturnProperties(), $this->getAdditionalFieldsForSameAddressMerge(), $this->getAdditionalFieldsForPostalExport());
c66a5741 428 }
429
430 /**
431 * @param array $returnProperties
432 */
433 public function setReturnProperties($returnProperties) {
434 $this->returnProperties = $returnProperties;
435 }
436
944ed388 437 /**
438 * @return array
439 */
440 public function getRelationshipTypes() {
441 return $this->relationshipTypes;
442 }
443
444 /**
445 */
446 public function setRelationshipTypes() {
447 $this->relationshipTypes = CRM_Contact_BAO_Relationship::getContactRelationshipType(
448 NULL,
449 NULL,
450 NULL,
451 NULL,
452 TRUE,
453 'name',
454 FALSE
455 );
456 }
457
ce12a9e0 458 /**
459 * Set the value for a relationship type field.
460 *
461 * In this case we are building up an array of properties for a related contact.
462 *
463 * These may be used for direct exporting or for merge to household depending on the
464 * options selected.
465 *
466 * @param string $relationshipType
467 * @param int $contactID
468 * @param string $field
469 * @param string $value
470 */
471 public function setRelationshipValue($relationshipType, $contactID, $field, $value) {
472 $this->relatedContactValues[$relationshipType][$contactID][$field] = $value;
ebebd629 473 if ($field === 'id') {
474 $this->householdsToSkip[] = $value;
475 }
ce12a9e0 476 }
477
478 /**
479 * Get the value for a relationship type field.
480 *
481 * In this case we are building up an array of properties for a related contact.
482 *
483 * These may be used for direct exporting or for merge to household depending on the
484 * options selected.
485 *
486 * @param string $relationshipType
487 * @param int $contactID
488 * @param string $field
489 *
490 * @return string
491 */
492 public function getRelationshipValue($relationshipType, $contactID, $field) {
493 return isset($this->relatedContactValues[$relationshipType][$contactID][$field]) ? $this->relatedContactValues[$relationshipType][$contactID][$field] : '';
494 }
495
136f69a8 496 /**
497 * Get the id of the related household.
498 *
499 * @param int $contactID
500 * @param string $relationshipType
501 *
502 * @return int
503 */
504 public function getRelatedHouseholdID($contactID, $relationshipType) {
505 return $this->relatedContactValues[$relationshipType][$contactID]['id'];
506 }
507
508 /**
509 * Has the household already been exported.
510 *
511 * @param int $housholdContactID
512 *
513 * @return bool
514 */
515 public function isHouseholdExported($housholdContactID) {
516 return isset($this->exportedHouseholds[$housholdContactID]);
517
518 }
519
b7db6051 520 /**
521 * @return bool
522 */
523 public function isMergeSameHousehold() {
524 return $this->isMergeSameHousehold;
525 }
526
527 /**
528 * @param bool $isMergeSameHousehold
529 */
530 public function setIsMergeSameHousehold($isMergeSameHousehold) {
531 $this->isMergeSameHousehold = $isMergeSameHousehold;
532 }
533
534 /**
535 * Return relationship types for household merge.
536 *
537 * @return mixed
538 */
539 public function getHouseholdRelationshipTypes() {
540 if (!$this->isMergeSameHousehold()) {
541 return [];
542 }
543 return [
544 CRM_Utils_Array::key('Household Member of', $this->getRelationshipTypes()),
545 CRM_Utils_Array::key('Head of Household for', $this->getRelationshipTypes()),
546 ];
547 }
944ed388 548
549 /**
550 * @param $fieldName
551 * @return bool
552 */
553 public function isRelationshipTypeKey($fieldName) {
554 return array_key_exists($fieldName, $this->relationshipTypes);
555 }
556
b7db6051 557 /**
558 * @param $fieldName
559 * @return bool
560 */
561 public function isHouseholdMergeRelationshipTypeKey($fieldName) {
562 return in_array($fieldName, $this->getHouseholdRelationshipTypes());
563 }
564
71464b73 565 /**
566 * @return string
567 */
568 public function getQueryOperator() {
569 return $this->queryOperator;
570 }
571
572 /**
573 * @param string $queryOperator
574 */
575 public function setQueryOperator($queryOperator) {
576 $this->queryOperator = $queryOperator;
577 }
578
246e571e
JG
579 /**
580 * @return array
581 */
582 public function getIds() {
583 return $this->ids;
584 }
585
586 /**
587 * @param array $ids
588 */
589 public function setIds($ids) {
590 $this->ids = $ids;
591 }
592
adabfa40 593 /**
594 * @return array
595 */
596 public function getQueryFields() {
749e60f4 597 return array_merge(
598 $this->queryFields,
599 $this->getComponentPaymentFields()
600 );
adabfa40 601 }
602
603 /**
604 * @param array $queryFields
605 */
606 public function setQueryFields($queryFields) {
5ebd7d09 607 // legacy hacks - we add these to queryFields because this
608 // pseudometadata is currently required.
609 $queryFields['im_provider']['pseudoconstant']['var'] = 'imProviders';
610 $queryFields['country']['context'] = 'country';
611 $queryFields['world_region']['context'] = 'country';
612 $queryFields['state_province']['context'] = 'province';
808a80ba 613 $queryFields['contact_id'] = ['title' => ts('Contact ID'), 'type' => CRM_Utils_Type::T_INT];
adabfa40 614 $this->queryFields = $queryFields;
615 }
616
6003a964 617 /**
618 * @return int
619 */
620 public function getQueryMode() {
621 return $this->queryMode;
622 }
623
624 /**
625 * Set the query mode based on the export mode.
626 */
627 public function setQueryMode() {
628
629 switch ($this->getExportMode()) {
630 case CRM_Export_Form_Select::CONTRIBUTE_EXPORT:
631 $this->queryMode = CRM_Contact_BAO_Query::MODE_CONTRIBUTE;
632 break;
633
634 case CRM_Export_Form_Select::EVENT_EXPORT:
635 $this->queryMode = CRM_Contact_BAO_Query::MODE_EVENT;
636 break;
637
638 case CRM_Export_Form_Select::MEMBER_EXPORT:
639 $this->queryMode = CRM_Contact_BAO_Query::MODE_MEMBER;
640 break;
641
642 case CRM_Export_Form_Select::PLEDGE_EXPORT:
643 $this->queryMode = CRM_Contact_BAO_Query::MODE_PLEDGE;
644 break;
645
646 case CRM_Export_Form_Select::CASE_EXPORT:
647 $this->queryMode = CRM_Contact_BAO_Query::MODE_CASE;
648 break;
649
650 case CRM_Export_Form_Select::GRANT_EXPORT:
651 $this->queryMode = CRM_Contact_BAO_Query::MODE_GRANT;
652 break;
653
654 case CRM_Export_Form_Select::ACTIVITY_EXPORT:
655 $this->queryMode = CRM_Contact_BAO_Query::MODE_ACTIVITY;
656 break;
657
658 default:
659 $this->queryMode = CRM_Contact_BAO_Query::MODE_CONTACTS;
660 }
661 }
662
663 /**
664 * @return int
665 */
666 public function getExportMode() {
667 return $this->exportMode;
668 }
669
670 /**
671 * @param int $exportMode
672 */
673 public function setExportMode($exportMode) {
674 $this->exportMode = $exportMode;
675 }
676
29034a98 677 /**
678 * Get the name for the export file.
679 *
680 * @return string
681 */
682 public function getExportFileName() {
683 switch ($this->getExportMode()) {
684 case CRM_Export_Form_Select::CONTACT_EXPORT:
685 return ts('CiviCRM Contact Search');
686
687 case CRM_Export_Form_Select::CONTRIBUTE_EXPORT:
688 return ts('CiviCRM Contribution Search');
689
690 case CRM_Export_Form_Select::MEMBER_EXPORT:
691 return ts('CiviCRM Member Search');
692
693 case CRM_Export_Form_Select::EVENT_EXPORT:
694 return ts('CiviCRM Participant Search');
695
696 case CRM_Export_Form_Select::PLEDGE_EXPORT:
697 return ts('CiviCRM Pledge Search');
698
699 case CRM_Export_Form_Select::CASE_EXPORT:
700 return ts('CiviCRM Case Search');
701
702 case CRM_Export_Form_Select::GRANT_EXPORT:
703 return ts('CiviCRM Grant Search');
704
705 case CRM_Export_Form_Select::ACTIVITY_EXPORT:
706 return ts('CiviCRM Activity Search');
707
708 default:
709 // Legacy code suggests the value could be 'financial' - ie. something
710 // other than what should be accepted. However, I suspect that this line is
711 // never hit.
712 return ts('CiviCRM Search');
713 }
714 }
715
af17bedf 716 /**
717 * Get the label for the header row based on the field to output.
718 *
719 * @param string $field
720 *
721 * @return string
722 */
723 public function getHeaderForRow($field) {
724 if (substr($field, -11) == 'campaign_id') {
725 // @todo - set this correctly in the xml rather than here.
726 // This will require a generalised handling cleanup
727 return ts('Campaign ID');
728 }
729 if ($this->isMergeSameHousehold() && $field === 'id') {
730 return ts('Household ID');
731 }
732 elseif (isset($this->getQueryFields()[$field]['title'])) {
733 return $this->getQueryFields()[$field]['title'];
734 }
735 elseif ($this->isExportPaymentFields() && array_key_exists($field, $this->getcomponentPaymentFields())) {
749e60f4 736 return CRM_Utils_Array::value($field, $this->getcomponentPaymentFields())['title'];
af17bedf 737 }
738 else {
739 return $field;
740 }
741 }
742
adabfa40 743 /**
744 * @param $params
745 * @param $order
6df77635 746 *
adabfa40 747 * @return array
748 */
6df77635 749 public function runQuery($params, $order) {
750 $returnProperties = $this->getReturnProperties();
6d52bfe5 751 $params = array_merge($params, $this->getWhereParams());
efaa3367 752
adabfa40 753 $query = new CRM_Contact_BAO_Query($params, $returnProperties, NULL,
754 FALSE, FALSE, $this->getQueryMode(),
71464b73 755 FALSE, TRUE, TRUE, NULL, $this->getQueryOperator()
adabfa40 756 );
757
758 //sort by state
759 //CRM-15301
760 $query->_sort = $order;
761 list($select, $from, $where, $having) = $query->query();
762 $this->setQueryFields($query->_fields);
e034ee91 763 $whereClauses = ['trash_clause' => "contact_a.is_deleted != 1"];
9858dc0b 764 if ($this->getRequestedFields() && ($this->getComponentTable())) {
e034ee91 765 $from .= " INNER JOIN " . $this->getComponentTable() . " ctTable ON ctTable.contact_id = contact_a.id ";
766 }
767 elseif ($this->getComponentClause()) {
768 $whereClauses[] = $this->getComponentClause();
769 }
770
771 // CRM-13982 - check if is deleted
772 foreach ($params as $value) {
773 if ($value[0] == 'contact_is_deleted') {
774 unset($whereClauses['trash_clause']);
775 }
776 }
777
efaa3367 778 if ($this->isPostalableOnly) {
779 if (array_key_exists('street_address', $returnProperties)) {
780 $addressWhere = " civicrm_address.street_address <> ''";
781 if (array_key_exists('supplemental_address_1', $returnProperties)) {
782 // We need this to be an OR rather than AND on the street_address so, hack it in.
783 $addressOptions = CRM_Core_BAO_Setting::valueOptions(CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME,
784 'address_options', TRUE, NULL, TRUE
785 );
786 if (!empty($addressOptions['supplemental_address_1'])) {
787 $addressWhere .= " OR civicrm_address.supplemental_address_1 <> ''";
788 }
789 }
62a39786 790 $whereClauses['address'] = '(' . $addressWhere . ')';
efaa3367 791 }
792 }
793
e034ee91 794 if (empty($where)) {
795 $where = "WHERE " . implode(' AND ', $whereClauses);
796 }
797 else {
798 $where .= " AND " . implode(' AND ', $whereClauses);
799 }
7628764a 800
801 $groupBy = $this->getGroupBy($query);
43767da2 802 $queryString = "$select $from $where $having $groupBy";
803 if ($order) {
804 // always add contact_a.id to the ORDER clause
805 // so the order is deterministic
806 //CRM-15301
807 if (strpos('contact_a.id', $order) === FALSE) {
808 $order .= ", contact_a.id";
809 }
810
811 list($field, $dir) = explode(' ', $order, 2);
812 $field = trim($field);
813 if (!empty($this->getReturnProperties()[$field])) {
814 //CRM-15301
815 $queryString .= " ORDER BY $order";
816 }
817 }
818 return [$query, $queryString];
adabfa40 819 }
820
28254dcb 821 /**
822 * Add a row to the specification for how to output data.
ae5d4caf 823 *
28254dcb 824 * @param string $key
28254dcb 825 * @param string $relationshipType
ae5d4caf 826 * @param string $locationType
827 * @param int $entityTypeID phone_type_id or provider_id for phone or im fields.
28254dcb 828 */
ae5d4caf 829 public function addOutputSpecification($key, $relationshipType = NULL, $locationType = NULL, $entityTypeID = NULL) {
5ebd7d09 830 $entityLabel = '';
ae5d4caf 831 if ($entityTypeID) {
832 if ($key === 'phone') {
5ebd7d09 833 $entityLabel = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_Phone', 'phone_type_id', $entityTypeID);
ae5d4caf 834 }
835 if ($key === 'im') {
5ebd7d09 836 $entityLabel = CRM_Core_PseudoConstant::getLabel('CRM_Core_BAO_IM', 'provider_id', $entityTypeID);
ae5d4caf 837 }
838 }
ae5d4caf 839
5ebd7d09 840 // These oddly constructed keys are for legacy reasons. Altering them will affect test success
841 // but in time it may be good to rationalise them.
842 $label = $this->getOutputSpecificationLabel($key, $relationshipType, $locationType, $entityLabel);
843 $index = $this->getOutputSpecificationIndex($key, $relationshipType, $locationType, $entityLabel);
844 $fieldKey = $this->getOutputSpecificationFieldKey($key, $relationshipType, $locationType, $entityLabel);
845
846 $this->outputSpecification[$index]['header'] = $label;
847 $this->outputSpecification[$index]['sql_columns'] = $this->getSqlColumnDefinition($fieldKey, $key);
848
849 if ($relationshipType && $this->isHouseholdMergeRelationshipTypeKey($relationshipType)) {
850 $this->setColumnAsCalculationOnly($index);
851 }
852 $this->outputSpecification[$index]['metadata'] = $this->getMetaDataForField($key);
853 }
854
855 /**
856 * Get the metadata for the given field.
857 *
858 * @param $key
859 *
860 * @return array
861 */
862 public function getMetaDataForField($key) {
863 $mappings = ['contact_id' => 'id'];
864 if (isset($this->getQueryFields()[$key])) {
865 return $this->getQueryFields()[$key];
866 }
867 if (isset($mappings[$key])) {
868 return $this->getQueryFields()[$mappings[$key]];
869 }
870 return [];
2cd3d767 871 }
ae5d4caf 872
2cd3d767 873 /**
874 * @param $key
875 */
876 public function setSqlColumnDefn($key) {
5ebd7d09 877 $this->outputSpecification[$this->getMungedFieldName($key)]['sql_columns'] = $this->getSqlColumnDefinition($key, $this->getMungedFieldName($key));
de6ff509 878 }
879
880 /**
881 * Mark a column as only required for calculations.
882 *
883 * Do not include the row with headers.
884 *
885 * @param string $column
886 */
887 public function setColumnAsCalculationOnly($column) {
888 $this->outputSpecification[$column]['do_not_output_to_csv'] = TRUE;
28254dcb 889 }
890
891 /**
892 * @return array
893 */
894 public function getHeaderRows() {
895 $headerRows = [];
896 foreach ($this->outputSpecification as $key => $spec) {
de6ff509 897 if (empty($spec['do_not_output_to_csv'])) {
898 $headerRows[] = $spec['header'];
899 }
28254dcb 900 }
901 return $headerRows;
902 }
903
2cd3d767 904 /**
905 * @return array
906 */
907 public function getSQLColumns() {
908 $sqlColumns = [];
909 foreach ($this->outputSpecification as $key => $spec) {
910 if (empty($spec['do_not_output_to_sql'])) {
911 $sqlColumns[$key] = $spec['sql_columns'];
912 }
913 }
914 return $sqlColumns;
915 }
916
5ebd7d09 917 /**
918 * @return array
919 */
920 public function getMetadata() {
921 $metadata = [];
922 foreach ($this->outputSpecification as $key => $spec) {
923 $metadata[$key] = $spec['metadata'];
924 }
925 return $metadata;
926 }
2cd3d767 927
6e2de55d 928 /**
929 * Build the row for output.
930 *
931 * @param \CRM_Contact_BAO_Query $query
932 * @param CRM_Core_DAO $iterationDAO
933 * @param array $outputColumns
934 * @param $metadata
935 * @param $paymentDetails
936 * @param $addPaymentHeader
6e2de55d 937 *
136f69a8 938 * @return array|bool
6e2de55d 939 */
c58085f8 940 public function buildRow($query, $iterationDAO, $outputColumns, $metadata, $paymentDetails, $addPaymentHeader) {
941 $paymentTableId = $this->getPaymentTableID();
ebebd629 942 if ($this->isHouseholdToSkip($iterationDAO->contact_id)) {
943 return FALSE;
944 }
6e2de55d 945 $phoneTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Phone', 'phone_type_id');
946 $imProviders = CRM_Core_PseudoConstant::get('CRM_Core_DAO_IM', 'provider_id');
947
948 $row = [];
136f69a8 949 $householdMergeRelationshipType = $this->getHouseholdMergeTypeForRow($iterationDAO->contact_id);
950 if ($householdMergeRelationshipType) {
951 $householdID = $this->getRelatedHouseholdID($iterationDAO->contact_id, $householdMergeRelationshipType);
952 if ($this->isHouseholdExported($householdID)) {
953 return FALSE;
954 }
955 foreach (array_keys($outputColumns) as $column) {
956 $row[$column] = $this->getRelationshipValue($householdMergeRelationshipType, $iterationDAO->contact_id, $column);
957 }
958 $this->markHouseholdExported($householdID);
959 return $row;
960 }
961
6e2de55d 962 $query->convertToPseudoNames($iterationDAO);
963
964 //first loop through output columns so that we return what is required, and in same order.
965 foreach ($outputColumns as $field => $value) {
6e2de55d 966 // add im_provider to $dao object
967 if ($field == 'im_provider' && property_exists($iterationDAO, 'provider_id')) {
968 $iterationDAO->im_provider = $iterationDAO->provider_id;
969 }
970
971 //build row values (data)
972 $fieldValue = NULL;
973 if (property_exists($iterationDAO, $field)) {
974 $fieldValue = $iterationDAO->$field;
975 // to get phone type from phone type id
976 if ($field == 'phone_type_id' && isset($phoneTypes[$fieldValue])) {
977 $fieldValue = $phoneTypes[$fieldValue];
978 }
979 elseif ($field == 'provider_id' || $field == 'im_provider') {
980 $fieldValue = CRM_Utils_Array::value($fieldValue, $imProviders);
981 }
982 elseif (strstr($field, 'master_id')) {
ffd511ee 983 // @todo - why not just $field === 'master_id' - what else would it be?
984 $masterAddressId = $iterationDAO->$field ?? NULL;
6e2de55d 985 // get display name of contact that address is shared.
986 $fieldValue = CRM_Contact_BAO_Contact::getMasterDisplayName($masterAddressId);
987 }
988 }
989
990 if ($this->isRelationshipTypeKey($field)) {
136f69a8 991 $this->buildRelationshipFieldsForRow($row, $iterationDAO->contact_id, $value, $field);
6e2de55d 992 }
993 else {
994 $row[$field] = $this->getTransformedFieldValue($field, $iterationDAO, $fieldValue, $metadata, $paymentDetails);
995 }
996 }
997
998 // If specific payment fields have been selected for export, payment
999 // data will already be in $row. Otherwise, add payment related
1000 // information, if appropriate.
1001 if ($addPaymentHeader) {
1002 if (!$this->isExportSpecifiedPaymentFields()) {
1003 $nullContributionDetails = array_fill_keys(array_keys($this->getPaymentHeaders()), NULL);
1004 if ($this->isExportPaymentFields()) {
1005 $paymentData = CRM_Utils_Array::value($row[$paymentTableId], $paymentDetails);
1006 if (!is_array($paymentData) || empty($paymentData)) {
1007 $paymentData = $nullContributionDetails;
1008 }
1009 $row = array_merge($row, $paymentData);
1010 }
1011 elseif (!empty($paymentDetails)) {
1012 $row = array_merge($row, $nullContributionDetails);
1013 }
1014 }
1015 }
1016 //remove organization name for individuals if it is set for current employer
1017 if (!empty($row['contact_type']) &&
1018 $row['contact_type'] == 'Individual' && array_key_exists('organization_name', $row)
1019 ) {
1020 $row['organization_name'] = '';
1021 }
1022 return $row;
1023 }
1024
136f69a8 1025 /**
1026 * If this row has a household whose details we should use get the relationship type key.
1027 *
1028 * @param $contactID
1029 *
1030 * @return bool
1031 */
1032 public function getHouseholdMergeTypeForRow($contactID) {
1033 if (!$this->isMergeSameHousehold()) {
1034 return FALSE;
1035 }
1036 foreach ($this->getHouseholdRelationshipTypes() as $relationshipType) {
1037 if (isset($this->relatedContactValues[$relationshipType][$contactID])) {
1038 return $relationshipType;
1039 }
1040 }
1041 }
1042
1043 /**
1044 * Mark the given household as already exported.
1045 *
1046 * @param $householdID
1047 */
1048 public function markHouseholdExported($householdID) {
1049 $this->exportedHouseholds[$householdID] = $householdID;
1050 }
1051
8bd67f47 1052 /**
1053 * @param $field
1054 * @param $iterationDAO
1055 * @param $fieldValue
1056 * @param $metadata
1057 * @param $paymentDetails
1058 *
1059 * @return string
1060 */
1061 public function getTransformedFieldValue($field, $iterationDAO, $fieldValue, $metadata, $paymentDetails) {
1062
1063 $i18n = CRM_Core_I18n::singleton();
1064 if ($field == 'id') {
1065 return $iterationDAO->contact_id;
1066 // special case for calculated field
1067 }
1068 elseif ($field == 'source_contact_id') {
1069 return $iterationDAO->contact_id;
1070 }
1071 elseif ($field == 'pledge_balance_amount') {
1072 return $iterationDAO->pledge_amount - $iterationDAO->pledge_total_paid;
1073 // special case for calculated field
1074 }
1075 elseif ($field == 'pledge_next_pay_amount') {
1076 return $iterationDAO->pledge_next_pay_amount + $iterationDAO->pledge_outstanding_amount;
1077 }
1078 elseif (isset($fieldValue) &&
1079 $fieldValue != ''
1080 ) {
1081 //check for custom data
1082 if ($cfID = CRM_Core_BAO_CustomField::getKeyID($field)) {
1083 return CRM_Core_BAO_CustomField::displayValue($fieldValue, $cfID);
1084 }
1085
be2fb01f 1086 elseif (in_array($field, [
8bd67f47 1087 'email_greeting',
1088 'postal_greeting',
1089 'addressee',
be2fb01f 1090 ])) {
8bd67f47 1091 //special case for greeting replacement
1092 $fldValue = "{$field}_display";
1093 return $iterationDAO->$fldValue;
1094 }
1095 else {
1096 //normal fields with a touch of CRM-3157
1097 switch ($field) {
1098 case 'country':
1099 case 'world_region':
be2fb01f 1100 return $i18n->crm_translate($fieldValue, ['context' => 'country']);
8bd67f47 1101
1102 case 'state_province':
be2fb01f 1103 return $i18n->crm_translate($fieldValue, ['context' => 'province']);
8bd67f47 1104
1105 case 'gender':
1106 case 'preferred_communication_method':
1107 case 'preferred_mail_format':
1108 case 'communication_style':
1109 return $i18n->crm_translate($fieldValue);
1110
1111 default:
1112 if (isset($metadata[$field])) {
1113 // No I don't know why we do it this way & whether we could
1114 // make better use of pseudoConstants.
1115 if (!empty($metadata[$field]['context'])) {
1116 return $i18n->crm_translate($fieldValue, $metadata[$field]);
1117 }
1118 if (!empty($metadata[$field]['pseudoconstant'])) {
001a515b 1119 if (!empty($metadata[$field]['bao'])) {
1120 return CRM_Core_PseudoConstant::getLabel($metadata[$field]['bao'], $metadata[$field]['name'], $fieldValue);
1121 }
8bd67f47 1122 // This is not our normal syntax for pseudoconstants but I am a bit loath to
1123 // call an external function until sure it is not increasing php processing given this
1124 // may be iterated 100,000 times & we already have the $imProvider var loaded.
1125 // That can be next refactor...
1126 // Yes - definitely feeling hatred for this bit of code - I know you will beat me up over it's awfulness
1127 // but I have to reach a stable point....
1128 $varName = $metadata[$field]['pseudoconstant']['var'];
1129 if ($varName === 'imProviders') {
1130 return CRM_Core_PseudoConstant::getLabel('CRM_Core_DAO_IM', 'provider_id', $fieldValue);
1131 }
1132 if ($varName === 'phoneTypes') {
1133 return CRM_Core_PseudoConstant::getLabel('CRM_Core_DAO_Phone', 'phone_type_id', $fieldValue);
1134 }
1135 }
1136
1137 }
1138 return $fieldValue;
1139 }
1140 }
1141 }
1142 elseif ($this->isExportSpecifiedPaymentFields() && array_key_exists($field, $this->getcomponentPaymentFields())) {
1143 $paymentTableId = $this->getPaymentTableID();
1144 $paymentData = CRM_Utils_Array::value($iterationDAO->$paymentTableId, $paymentDetails);
be2fb01f 1145 $payFieldMapper = [
8bd67f47 1146 'componentPaymentField_total_amount' => 'total_amount',
1147 'componentPaymentField_contribution_status' => 'contribution_status',
1148 'componentPaymentField_payment_instrument' => 'pay_instru',
1149 'componentPaymentField_transaction_id' => 'trxn_id',
1150 'componentPaymentField_received_date' => 'receive_date',
be2fb01f 1151 ];
8bd67f47 1152 return CRM_Utils_Array::value($payFieldMapper[$field], $paymentData, '');
1153 }
1154 else {
1155 // if field is empty or null
1156 return '';
1157 }
1158 }
1159
ce14544c 1160 /**
1161 * Get array of fields to return, over & above those defined in the main contact exportable fields.
1162 *
1163 * These include export mode specific fields & some fields apparently required as 'exportableFields'
1164 * but not returned by the function of the same name.
1165 *
1166 * @return array
1167 * Array of fields to return in the format ['field_name' => 1,...]
1168 */
1169 public function getAdditionalReturnProperties() {
ce14544c 1170 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTACTS) {
1171 $componentSpecificFields = [];
1172 }
1173 else {
1174 $componentSpecificFields = CRM_Contact_BAO_Query::defaultReturnProperties($this->getQueryMode());
1175 }
1176 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_PLEDGE) {
1177 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Pledge_BAO_Query::extraReturnProperties($this->getQueryMode()));
d28b6cf2 1178 unset($componentSpecificFields['contribution_status_id']);
1179 unset($componentSpecificFields['pledge_status_id']);
1180 unset($componentSpecificFields['pledge_payment_status_id']);
ce14544c 1181 }
1182 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CASE) {
1183 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Case_BAO_Query::extraReturnProperties($this->getQueryMode()));
1184 }
1185 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTRIBUTE) {
1186 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Contribute_BAO_Query::softCreditReturnProperties(TRUE));
d28b6cf2 1187 unset($componentSpecificFields['contribution_status_id']);
ce14544c 1188 }
7626754f 1189 return $componentSpecificFields;
ce14544c 1190 }
1191
d41ab886 1192 /**
1193 * Should payment fields be appended to the export.
1194 *
1195 * (This is pretty hacky so hopefully this function won't last long - notice
1196 * how obviously it should be part of the above function!).
1197 */
1198 public function isExportPaymentFields() {
1199 if ($this->getRequestedFields() === NULL
f065c170 1200 && in_array($this->getQueryMode(), [
d41ab886 1201 CRM_Contact_BAO_Query::MODE_EVENT,
1202 CRM_Contact_BAO_Query::MODE_MEMBER,
1203 CRM_Contact_BAO_Query::MODE_PLEDGE,
1204 ])) {
1205 return TRUE;
1206 }
c66a5741 1207 elseif ($this->isExportSpecifiedPaymentFields()) {
1208 return TRUE;
1209 }
d41ab886 1210 return FALSE;
1211 }
1212
c66a5741 1213 /**
1214 * Has specific payment fields been requested (as opposed to via all fields).
1215 *
1216 * If specific fields have been requested then they get added at various points.
1217 *
1218 * @return bool
1219 */
1220 public function isExportSpecifiedPaymentFields() {
1221 if ($this->getRequestedFields() !== NULL && $this->hasRequestedComponentPaymentFields()) {
1222 return TRUE;
1223 }
1224 }
1225
d41ab886 1226 /**
1227 * Get the name of the id field in the table that connects contributions to the export entity.
1228 */
1229 public function getPaymentTableID() {
1230 if ($this->getRequestedFields() === NULL) {
1231 $mapping = [
1232 CRM_Contact_BAO_Query::MODE_EVENT => 'participant_id',
1233 CRM_Contact_BAO_Query::MODE_MEMBER => 'membership_id',
1234 CRM_Contact_BAO_Query::MODE_PLEDGE => 'pledge_payment_id',
1235 ];
1236 return isset($mapping[$this->getQueryMode()]) ? $mapping[$this->getQueryMode()] : '';
1237 }
c66a5741 1238 elseif ($this->hasRequestedComponentPaymentFields()) {
1239 return 'participant_id';
1240 }
d41ab886 1241 return FALSE;
1242 }
1243
c66a5741 1244 /**
1245 * Have component payment fields been requested.
1246 *
1247 * @return bool
1248 */
1249 protected function hasRequestedComponentPaymentFields() {
1250 if ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_EVENT) {
1251 $participantPaymentFields = array_intersect_key($this->getComponentPaymentFields(), $this->getReturnProperties());
1252 if (!empty($participantPaymentFields)) {
1253 return TRUE;
1254 }
1255 }
d41ab886 1256 return FALSE;
1257 }
1258
c66a5741 1259 /**
1260 * Get fields that indicate payment fields have been requested for a component.
1261 *
0e32ed68 1262 * Ideally this should be protected but making it temporarily public helps refactoring..
1263 *
c66a5741 1264 * @return array
1265 */
0e32ed68 1266 public function getComponentPaymentFields() {
c66a5741 1267 return [
749e60f4 1268 'componentPaymentField_total_amount' => ['title' => ts('Total Amount'), 'type' => CRM_Utils_Type::T_MONEY],
1269 'componentPaymentField_contribution_status' => ['title' => ts('Contribution Status'), 'type' => CRM_Utils_Type::T_STRING],
1270 'componentPaymentField_received_date' => ['title' => ts('Date Received'), 'type' => CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME],
1271 'componentPaymentField_payment_instrument' => ['title' => ts('Payment Method'), 'type' => CRM_Utils_Type::T_STRING],
1272 'componentPaymentField_transaction_id' => ['title' => ts('Transaction ID'), 'type' => CRM_Utils_Type::T_STRING],
c66a5741 1273 ];
1274 }
1275
05ad310f 1276 /**
1277 * Get headers for payment fields.
1278 *
1279 * Returns an array of contribution fields when the entity supports payment fields and specific fields
1280 * are not specified. This is a transitional function for refactoring legacy code.
1281 */
1282 public function getPaymentHeaders() {
1283 if ($this->isExportPaymentFields() && !$this->isExportSpecifiedPaymentFields()) {
749e60f4 1284 return CRM_Utils_Array::collect('title', $this->getcomponentPaymentFields());
05ad310f 1285 }
1286 return [];
1287 }
1288
d41ab886 1289 /**
1290 * Get the default properties when not specified.
1291 *
1292 * In the UI this appears as 'Primary fields only' but in practice it's
1293 * most of the kitchen sink and the hallway closet thrown in.
1294 *
1295 * Since CRM-952 custom fields are excluded, but no other form of mercy is shown.
1296 *
1297 * @return array
1298 */
1299 public function getDefaultReturnProperties() {
1300 $returnProperties = [];
1301 $fields = CRM_Contact_BAO_Contact::exportableFields('All', TRUE, TRUE);
704e3e9a 1302 $skippedFields = ($this->getQueryMode() === CRM_Contact_BAO_Query::MODE_CONTACTS) ? [] : [
1303 'groups',
1304 'tags',
7b966967 1305 'notes',
704e3e9a 1306 ];
d41ab886 1307
1308 foreach ($fields as $key => $var) {
1309 if ($key && (substr($key, 0, 6) != 'custom') && !in_array($key, $skippedFields)) {
1310 $returnProperties[$key] = 1;
1311 }
1312 }
1313 $returnProperties = array_merge($returnProperties, $this->getAdditionalReturnProperties());
1314 return $returnProperties;
1315 }
1316
704e3e9a 1317 /**
1318 * Add the field to relationship return properties & return it.
1319 *
1320 * This function is doing both setting & getting which is yuck but it is an interim
1321 * refactor.
1322 *
1323 * @param array $value
1324 * @param string $relationshipKey
1325 *
1326 * @return array
1327 */
1328 public function setRelationshipReturnProperties($value, $relationshipKey) {
04f8f758 1329 $relationField = $value['name'];
1330 $relIMProviderId = NULL;
1331 $relLocTypeId = CRM_Utils_Array::value('location_type_id', $value);
1332 $locationName = CRM_Core_PseudoConstant::getName('CRM_Core_BAO_Address', 'location_type_id', $relLocTypeId);
1333 $relPhoneTypeId = CRM_Utils_Array::value('phone_type_id', $value, ($locationName ? 'Primary' : NULL));
1334 $relIMProviderId = CRM_Utils_Array::value('im_provider_id', $value, ($locationName ? 'Primary' : NULL));
1335 if (in_array($relationField, $this->getValidLocationFields()) && $locationName) {
1336 if ($relationField === 'phone') {
a16a432a 1337 $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName]['phone-' . $relPhoneTypeId] = 1;
704e3e9a 1338 }
04f8f758 1339 elseif ($relationField === 'im') {
a16a432a 1340 $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName]['im-' . $relIMProviderId] = 1;
704e3e9a 1341 }
1342 else {
a16a432a 1343 $this->relationshipReturnProperties[$relationshipKey]['location'][$locationName][$relationField] = 1;
704e3e9a 1344 }
1345 }
1346 else {
1347 $this->relationshipReturnProperties[$relationshipKey][$relationField] = 1;
1348 }
1349 return $this->relationshipReturnProperties[$relationshipKey];
1350 }
1351
ce12a9e0 1352 /**
1353 * Add the main return properties to the household merge properties if needed for merging.
1354 *
1355 * If we are using household merge we need to add these to the relationship properties to
1356 * be retrieved.
ce12a9e0 1357 */
6df77635 1358 public function setHouseholdMergeReturnProperties() {
3611a532 1359 if ($this->isMergeSameHousehold()) {
1360 $returnProperties = $this->getReturnProperties();
1361 $returnProperties = array_diff_key($returnProperties, array_fill_keys(['location_type', 'im_provider'], 1));
1362 foreach ($this->getHouseholdRelationshipTypes() as $householdRelationshipType) {
1363 $this->relationshipReturnProperties[$householdRelationshipType] = $returnProperties;
1364 }
ce12a9e0 1365 }
1366 }
1367
704e3e9a 1368 /**
1369 * Get the default location fields to request.
1370 *
1371 * @return array
1372 */
1373 public function getValidLocationFields() {
1374 return [
1375 'street_address',
1376 'supplemental_address_1',
1377 'supplemental_address_2',
1378 'supplemental_address_3',
1379 'city',
1380 'postal_code',
1381 'postal_code_suffix',
1382 'geo_code_1',
1383 'geo_code_2',
1384 'state_province',
1385 'country',
1386 'phone',
1387 'email',
1388 'im',
1389 ];
1390 }
1391
aa3a113b 1392 /**
c8adad81 1393 * Get the sql column definition for the given field.
1394 *
5ebd7d09 1395 * @param string $fieldName
1396 * @param string $columnName
aa3a113b 1397 *
1398 * @return mixed
1399 */
5ebd7d09 1400 public function getSqlColumnDefinition($fieldName, $columnName) {
aa3a113b 1401
1402 // early exit for master_id, CRM-12100
1403 // in the DB it is an ID, but in the export, we retrive the display_name of the master record
1404 // also for current_employer, CRM-16939
5ebd7d09 1405 if ($columnName == 'master_id' || $columnName == 'current_employer') {
aa3a113b 1406 return "$fieldName varchar(128)";
1407 }
1408
aa3a113b 1409 $queryFields = $this->getQueryFields();
714ab070 1410 // @todo remove the enotice avoidance here, ensure all columns are declared.
1411 // tests will fail on the enotices until they all are & then all the 'else'
1412 // below can go.
1413 $fieldSpec = $queryFields[$columnName] ?? [];
1414
aa3a113b 1415 // set the sql columns
714ab070 1416 if (isset($fieldSpec['type'])) {
1417 switch ($fieldSpec['type']) {
aa3a113b 1418 case CRM_Utils_Type::T_INT:
1419 case CRM_Utils_Type::T_BOOLEAN:
d989f2a4 1420 if (in_array(CRM_Utils_Array::value('data_type', $fieldSpec), ['Country', 'StateProvince', 'ContactReference'])) {
1421 return "$fieldName varchar(255)";
1422 }
714ab070 1423 return "$fieldName varchar(16)";
aa3a113b 1424
1425 case CRM_Utils_Type::T_STRING:
5ebd7d09 1426 if (isset($queryFields[$columnName]['maxlength'])) {
1427 return "$fieldName varchar({$queryFields[$columnName]['maxlength']})";
aa3a113b 1428 }
1429 else {
1430 return "$fieldName varchar(255)";
1431 }
1432
1433 case CRM_Utils_Type::T_TEXT:
1434 case CRM_Utils_Type::T_LONGTEXT:
1435 case CRM_Utils_Type::T_BLOB:
1436 case CRM_Utils_Type::T_MEDIUMBLOB:
1437 return "$fieldName longtext";
1438
1439 case CRM_Utils_Type::T_FLOAT:
1440 case CRM_Utils_Type::T_ENUM:
1441 case CRM_Utils_Type::T_DATE:
1442 case CRM_Utils_Type::T_TIME:
1443 case CRM_Utils_Type::T_TIMESTAMP:
1444 case CRM_Utils_Type::T_MONEY:
1445 case CRM_Utils_Type::T_EMAIL:
1446 case CRM_Utils_Type::T_URL:
1447 case CRM_Utils_Type::T_CCNUM:
1448 default:
1449 return "$fieldName varchar(32)";
1450 }
1451 }
1452 else {
1453 if (substr($fieldName, -3, 3) == '_id') {
1454 return "$fieldName varchar(255)";
1455 }
1456 elseif (substr($fieldName, -5, 5) == '_note') {
1457 return "$fieldName text";
1458 }
1459 else {
1460 $changeFields = [
1461 'groups',
1462 'tags',
1463 'notes',
1464 ];
1465
1466 if (in_array($fieldName, $changeFields)) {
1467 return "$fieldName text";
1468 }
1469 else {
1470 // set the sql columns for custom data
5ebd7d09 1471 if (isset($queryFields[$columnName]['data_type'])) {
aa3a113b 1472
5ebd7d09 1473 switch ($queryFields[$columnName]['data_type']) {
aa3a113b 1474 case 'String':
1475 // May be option labels, which could be up to 512 characters
5ebd7d09 1476 $length = max(512, CRM_Utils_Array::value('text_length', $queryFields[$columnName]));
aa3a113b 1477 return "$fieldName varchar($length)";
1478
aa3a113b 1479 case 'Link':
1480 return "$fieldName varchar(255)";
1481
1482 case 'Memo':
1483 return "$fieldName text";
1484
1485 default:
1486 return "$fieldName varchar(255)";
1487 }
1488 }
1489 else {
1490 return "$fieldName text";
1491 }
1492 }
1493 }
1494 }
1495 }
1496
c8adad81 1497 /**
1498 * Get the munged field name.
1499 *
1500 * @param string $field
1501 * @return string
1502 */
1503 public function getMungedFieldName($field) {
1504 $fieldName = CRM_Utils_String::munge(strtolower($field), '_', 64);
1505 if ($fieldName == 'id') {
1506 $fieldName = 'civicrm_primary_id';
1507 }
1508 return $fieldName;
1509 }
1510
5ebd7d09 1511 /**
1512 * In order to respect the history of this class we need to index kinda illogically.
1513 *
1514 * On the bright side - this stuff is tested within a nano-byte of it's life.
1515 *
1516 * e.g '2-a-b_Home-City'
1517 *
1518 * @param string $key
1519 * @param string $relationshipType
1520 * @param string $locationType
1521 * @param $entityLabel
1522 *
1523 * @return string
1524 */
1525 protected function getOutputSpecificationIndex($key, $relationshipType, $locationType, $entityLabel) {
001a515b 1526 if ($entityLabel || $key === 'im') {
5ebd7d09 1527 // Just cos that's the history...
1528 if ($key !== 'master_id') {
1529 $key = $this->getHeaderForRow($key);
1530 }
1531 }
1532 if (!$relationshipType || $key !== 'id') {
1533 $key = $this->getMungedFieldName($key);
1534 }
1535 return $this->getMungedFieldName(
1536 ($relationshipType ? ($relationshipType . '_') : '')
1537 . ($locationType ? ($locationType . '_') : '')
1538 . $key
1539 . ($entityLabel ? ('_' . $entityLabel) : '')
1540 );
1541 }
1542
1543 /**
1544 * Get the compiled label for the column.
1545 *
1546 * e.g 'Gender', 'Employee Of-Home-city'
1547 *
1548 * @param string $key
1549 * @param string $relationshipType
1550 * @param string $locationType
1551 * @param string $entityLabel
1552 *
1553 * @return string
1554 */
1555 protected function getOutputSpecificationLabel($key, $relationshipType, $locationType, $entityLabel) {
1556 return ($relationshipType ? $this->getRelationshipTypes()[$relationshipType] . '-' : '')
1557 . ($locationType ? $locationType . '-' : '')
1558 . $this->getHeaderForRow($key)
1559 . ($entityLabel ? '-' . $entityLabel : '');
1560 }
1561
1562 /**
1563 * Get the mysql field name key.
1564 *
1565 * This key is locked in by tests but the reasons for the specific conventions -
1566 * ie. headings are used for keying fields in some cases, are likely
1567 * accidental rather than deliberate.
1568 *
1569 * This key is used for the output sql array.
1570 *
1571 * @param string $key
1572 * @param $relationshipType
1573 * @param $locationType
1574 * @param $entityLabel
1575 *
1576 * @return string
1577 */
1578 protected function getOutputSpecificationFieldKey($key, $relationshipType, $locationType, $entityLabel) {
001a515b 1579 if ($entityLabel || $key === 'im') {
5ebd7d09 1580 if ($key !== 'state_province' && $key !== 'id') {
001a515b 1581 // @todo - test removing this - indexing by $key should be fine...
5ebd7d09 1582 $key = $this->getHeaderForRow($key);
1583 }
1584 }
1585 if (!$relationshipType || $key !== 'id') {
1586 $key = $this->getMungedFieldName($key);
1587 }
1588 $fieldKey = $this->getMungedFieldName(
1589 ($relationshipType ? ($relationshipType . '_') : '')
1590 . ($locationType ? ($locationType . '_') : '')
1591 . $key
1592 . ($entityLabel ? ('_' . $entityLabel) : '')
1593 );
1594 return $fieldKey;
1595 }
1596
6d52bfe5 1597 /**
1598 * Get params for the where criteria.
1599 *
1600 * @return mixed
1601 */
1602 public function getWhereParams() {
1603 if (!$this->isPostalableOnly()) {
1604 return [];
1605 }
1606 $params['is_deceased'] = ['is_deceased', '=', 0, CRM_Contact_BAO_Query::MODE_CONTACTS];
1607 $params['do_not_mail'] = ['do_not_mail', '=', 0, CRM_Contact_BAO_Query::MODE_CONTACTS];
1608 return $params;
1609 }
1610
136f69a8 1611 /**
1612 * @param $row
1613 * @param $contactID
1614 * @param $value
1615 * @param $field
1616 */
1617 protected function buildRelationshipFieldsForRow(&$row, $contactID, $value, $field) {
1618 foreach (array_keys($value) as $property) {
1619 if ($property === 'location') {
1620 // @todo just undo all this nasty location wrangling!
1621 foreach ($value['location'] as $locationKey => $locationFields) {
1622 foreach (array_keys($locationFields) as $locationField) {
1623 $fieldKey = str_replace(' ', '_', $locationKey . '-' . $locationField);
1624 $row[$field . '_' . $fieldKey] = $this->getRelationshipValue($field, $contactID, $fieldKey);
1625 }
1626 }
1627 }
1628 else {
1629 $row[$field . '_' . $property] = $this->getRelationshipValue($field, $contactID, $property);
1630 }
1631 }
1632 }
1633
ebebd629 1634 /**
1635 * Is this contact a household that is already set to be exported by virtue of it's household members.
1636 *
1637 * @param int $contactID
1638 *
1639 * @return bool
1640 */
1641 protected function isHouseholdToSkip($contactID) {
1642 return in_array($contactID, $this->householdsToSkip);
1643 }
1644
44fc53ab 1645 /**
1646 * Get the various arrays that we use to structure our output.
1647 *
1648 * The extraction of these has been moved to a separate function for clarity and so that
1649 * tests can be added - in particular on the $outputHeaders array.
1650 *
1651 * However it still feels a bit like something that I'm too polite to write down and this should be seen
1652 * as a step on the refactoring path rather than how it should be.
1653 *
1654 * @return array
1655 * - outputColumns Array of columns to be exported. The values don't matter but the key must match the
1656 * alias for the field generated by BAO_Query object.
1657 * - headerRows Array of the column header strings to put in the csv header - non-associative.
1658 * - sqlColumns Array of column names for the temp table. Not too sure why outputColumns can't be used here.
1659 * - metadata Array of fields with specific parameters to pass to the translate function or another hacky nasty solution
1660 * I'm too embarassed to discuss here.
1661 * The keys need
1662 * - to match the outputColumns keys (yes, the fact we ignore the output columns values & then pass another array with values
1663 * we could use does suggest further refactors. However, you future improver, do remember that every check you do
1664 * in the main DAO loop is done once per row & that coule be 100,000 times.)
1665 * Finally a pop quiz: We need the translate context because we use a function other than ts() - is this because
1666 * - a) the function used is more efficient or
1667 * - b) this code is old & outdated. Submit your answers to circular bin or better
1668 * yet find a way to comment them for posterity.
1669 */
1670 public function getExportStructureArrays() {
1671 $outputColumns = $metadata = [];
1672 $queryFields = $this->getQueryFields();
1673 foreach ($this->getReturnProperties() as $key => $value) {
1674 if (($key != 'location' || !is_array($value)) && !$this->isRelationshipTypeKey($key)) {
1675 $outputColumns[$key] = $value;
1676 $this->addOutputSpecification($key);
1677 }
1678 elseif ($this->isRelationshipTypeKey($key)) {
1679 $outputColumns[$key] = $value;
1680 foreach ($value as $relationField => $relationValue) {
1681 // below block is same as primary block (duplicate)
1682 if (isset($queryFields[$relationField]['title'])) {
1683 $this->addOutputSpecification($relationField, $key);
1684 }
1685 elseif (is_array($relationValue) && $relationField == 'location') {
1686 // fix header for location type case
1687 foreach ($relationValue as $ltype => $val) {
1688 foreach (array_keys($val) as $fld) {
1689 $type = explode('-', $fld);
1690 $this->addOutputSpecification($type[0], $key, $ltype, CRM_Utils_Array::value(1, $type));
1691 }
1692 }
1693 }
1694 }
1695 }
1696 else {
1697 foreach ($value as $locationType => $locationFields) {
1698 foreach (array_keys($locationFields) as $locationFieldName) {
1699 $type = explode('-', $locationFieldName);
1700
1701 $actualDBFieldName = $type[0];
1702 $daoFieldName = CRM_Utils_String::munge($locationType) . '-' . $actualDBFieldName;
1703
1704 if (!empty($type[1])) {
1705 $daoFieldName .= "-" . $type[1];
1706 }
1707 $this->addOutputSpecification($actualDBFieldName, NULL, $locationType, CRM_Utils_Array::value(1, $type));
1708 $metadata[$daoFieldName] = $this->getMetaDataForField($actualDBFieldName);
1709 $outputColumns[$daoFieldName] = TRUE;
1710 }
1711 }
1712 }
1713 }
1714 return [$outputColumns, $metadata];
1715 }
1716
ccc3ac8a 1717 /**
1718 * Get default return property for export based on mode
1719 *
1720 * @return string
1721 * Default Return property
1722 */
1723 public function defaultReturnProperty() {
1724 // hack to add default return property based on export mode
1725 $property = NULL;
1726 $exportMode = $this->getExportMode();
1727 if ($exportMode == CRM_Export_Form_Select::CONTRIBUTE_EXPORT) {
1728 $property = 'contribution_id';
1729 }
1730 elseif ($exportMode == CRM_Export_Form_Select::EVENT_EXPORT) {
1731 $property = 'participant_id';
1732 }
1733 elseif ($exportMode == CRM_Export_Form_Select::MEMBER_EXPORT) {
1734 $property = 'membership_id';
1735 }
1736 elseif ($exportMode == CRM_Export_Form_Select::PLEDGE_EXPORT) {
1737 $property = 'pledge_id';
1738 }
1739 elseif ($exportMode == CRM_Export_Form_Select::CASE_EXPORT) {
1740 $property = 'case_id';
1741 }
1742 elseif ($exportMode == CRM_Export_Form_Select::GRANT_EXPORT) {
1743 $property = 'grant_id';
1744 }
1745 elseif ($exportMode == CRM_Export_Form_Select::ACTIVITY_EXPORT) {
1746 $property = 'activity_id';
1747 }
1748 return $property;
1749 }
1750
806c5e1e 1751 /**
1752 * Determine the required return properties from the input parameters.
1753 *
1754 * @return array
1755 */
1756 public function determineReturnProperties() {
1757 if ($this->getRequestedFields()) {
1758 $returnProperties = [];
1759 foreach ($this->getRequestedFields() as $key => $value) {
04f8f758 1760 $fieldName = $value['name'];
7d0e773c 1761 $locationName = !empty($value['location_type_id']) ? CRM_Core_PseudoConstant::getName('CRM_Core_BAO_Address', 'location_type_id', $value['location_type_id']) : NULL;
04f8f758 1762 $relationshipTypeKey = !empty($value['relationship_type_id']) ? $value['relationship_type_id'] . '_' . $value['relationship_direction'] : NULL;
1763 if (!$fieldName || $this->isHouseholdMergeRelationshipTypeKey($relationshipTypeKey)) {
806c5e1e 1764 continue;
1765 }
1766
04f8f758 1767 if ($this->isRelationshipTypeKey($relationshipTypeKey)) {
1768 $returnProperties[$relationshipTypeKey] = $this->setRelationshipReturnProperties($value, $relationshipTypeKey);
806c5e1e 1769 }
04f8f758 1770 elseif ($locationName) {
1771 if ($fieldName === 'phone') {
1772 $returnProperties['location'][$locationName]['phone-' . $value['phone_type_id'] ?? NULL] = 1;
806c5e1e 1773 }
04f8f758 1774 elseif ($fieldName === 'im') {
1775 $returnProperties['location'][$locationName]['im-' . $value['im_provider_id'] ?? NULL] = 1;
806c5e1e 1776 }
1777 else {
1778 $returnProperties['location'][$locationName][$fieldName] = 1;
1779 }
1780 }
1781 else {
1782 //hack to fix component fields
1783 //revert mix of event_id and title
1784 if ($fieldName == 'event_id') {
1785 $returnProperties['event_id'] = 1;
1786 }
1787 else {
1788 $returnProperties[$fieldName] = 1;
1789 }
1790 }
1791 }
1792 $defaultExportMode = $this->defaultReturnProperty();
1793 if ($defaultExportMode) {
1794 $returnProperties[$defaultExportMode] = 1;
1795 }
1796 }
1797 else {
1798 $returnProperties = $this->getDefaultReturnProperties();
1799 }
25e51a1c 1800 if ($this->isMergeSameHousehold()) {
1801 $returnProperties['id'] = 1;
1802 }
2761a916 1803 if ($this->isMergeSameAddress()) {
1804 $returnProperties['addressee'] = 1;
1805 $returnProperties['postal_greeting'] = 1;
1806 $returnProperties['email_greeting'] = 1;
1807 $returnProperties['street_name'] = 1;
1808 $returnProperties['household_name'] = 1;
1809 $returnProperties['street_address'] = 1;
1810 $returnProperties['city'] = 1;
1811 $returnProperties['state_province'] = 1;
1812
1813 }
806c5e1e 1814 return $returnProperties;
1815 }
1816
571f3b5b 1817 /**
7628764a 1818 * @param object $query
1819 * CRM_Contact_BAO_Query
1820 *
1821 * @return string
1822 * Group By Clause
1823 */
1824 public function getGroupBy($query) {
1825 $groupBy = NULL;
1826 $returnProperties = $this->getReturnProperties();
1827 $exportMode = $this->getExportMode();
1828 $queryMode = $this->getQueryMode();
1829 if (!empty($returnProperties['tags']) || !empty($returnProperties['groups']) ||
1830 CRM_Utils_Array::value('notes', $returnProperties) ||
1831 // CRM-9552
1832 ($queryMode & CRM_Contact_BAO_Query::MODE_CONTACTS && $query->_useGroupBy)
1833 ) {
1834 $groupBy = "contact_a.id";
1835 }
1836
1837 switch ($exportMode) {
1838 case CRM_Export_Form_Select::CONTRIBUTE_EXPORT:
1839 $groupBy = 'civicrm_contribution.id';
1840 if (CRM_Contribute_BAO_Query::isSoftCreditOptionEnabled()) {
1841 // especial group by when soft credit columns are included
1842 $groupBy = ['contribution_search_scredit_combined.id', 'contribution_search_scredit_combined.scredit_id'];
1843 }
1844 break;
1845
1846 case CRM_Export_Form_Select::EVENT_EXPORT:
1847 $groupBy = 'civicrm_participant.id';
1848 break;
1849
1850 case CRM_Export_Form_Select::MEMBER_EXPORT:
1851 $groupBy = "civicrm_membership.id";
1852 break;
1853 }
1854
1855 if ($queryMode & CRM_Contact_BAO_Query::MODE_ACTIVITY) {
1856 $groupBy = "civicrm_activity.id ";
1857 }
1858
1859 return $groupBy ? ' GROUP BY ' . implode(', ', (array) $groupBy) : '';
1860 }
806c5e1e 1861
571f3b5b 1862 /**
1863 * @param int $contactId
571f3b5b 1864 *
1865 * @return array
1866 */
423e3da6 1867 public function replaceMergeTokens($contactId) {
571f3b5b 1868 $greetings = [];
1869 $contact = NULL;
1870
1871 $greetingFields = [
a2b21ae3 1872 'postal_greeting' => $this->getPostalGreetingTemplate(),
1873 'addressee' => $this->getAddresseeGreetingTemplate(),
571f3b5b 1874 ];
a2b21ae3 1875 foreach ($greetingFields as $greeting => $greetingLabel) {
423e3da6 1876 $tokens = CRM_Utils_Token::getTokens($greetingLabel);
1877 if (!empty($tokens)) {
571f3b5b 1878 if (empty($contact)) {
1879 $values = [
1880 'id' => $contactId,
1881 'version' => 3,
1882 ];
1883 $contact = civicrm_api('contact', 'get', $values);
1884
1885 if (!empty($contact['is_error'])) {
1886 return $greetings;
1887 }
1888 $contact = $contact['values'][$contact['id']];
1889 }
1890
1891 $tokens = ['contact' => $greetingLabel];
1892 $greetings[$greeting] = CRM_Utils_Token::replaceContactTokens($greetingLabel, $contact, NULL, $tokens);
1893 }
1894 }
1895 return $greetings;
1896 }
1897
c8713fd4 1898 /**
1899 * Build array for merging same addresses.
1900 *
1901 * @param $sql
c8713fd4 1902 * @param bool $sharedAddress
1903 *
1904 * @return array
1905 */
ee769c00 1906 public function buildMasterCopyArray($sql, $sharedAddress = FALSE) {
c8713fd4 1907
1908 $addresseeOptions = CRM_Core_OptionGroup::values('addressee');
1909 $postalOptions = CRM_Core_OptionGroup::values('postal_greeting');
1910
1911 $merge = $parents = [];
1912 $dao = CRM_Core_DAO::executeQuery($sql);
1913
1914 while ($dao->fetch()) {
1915 $masterID = $dao->master_id;
1916 $copyID = $dao->copy_id;
1917 $masterPostalGreeting = $dao->master_postal_greeting;
1918 $masterAddressee = $dao->master_addressee;
1919 $copyAddressee = $dao->copy_addressee;
1920
1921 if (!$sharedAddress) {
d97cefdc 1922 if (!isset($this->contactGreetingFields[$dao->master_contact_id])) {
423e3da6 1923 $this->contactGreetingFields[$dao->master_contact_id] = $this->replaceMergeTokens($dao->master_contact_id);
c8713fd4 1924 }
1925 $masterPostalGreeting = CRM_Utils_Array::value('postal_greeting',
d97cefdc 1926 $this->contactGreetingFields[$dao->master_contact_id], $dao->master_postal_greeting
c8713fd4 1927 );
1928 $masterAddressee = CRM_Utils_Array::value('addressee',
d97cefdc 1929 $this->contactGreetingFields[$dao->master_contact_id], $dao->master_addressee
c8713fd4 1930 );
1931
1932 if (!isset($contactGreetingTokens[$dao->copy_contact_id])) {
423e3da6 1933 $this->contactGreetingFields[$dao->copy_contact_id] = $this->replaceMergeTokens($dao->copy_contact_id);
c8713fd4 1934 }
1935 $copyPostalGreeting = CRM_Utils_Array::value('postal_greeting',
d97cefdc 1936 $this->contactGreetingFields[$dao->copy_contact_id], $dao->copy_postal_greeting
c8713fd4 1937 );
1938 $copyAddressee = CRM_Utils_Array::value('addressee',
d97cefdc 1939 $this->contactGreetingFields[$dao->copy_contact_id], $dao->copy_addressee
c8713fd4 1940 );
1941 }
1942
1943 if (!isset($merge[$masterID])) {
1944 // check if this is an intermediate child
1945 // this happens if there are 3 or more matches a,b, c
1946 // the above query will return a, b / a, c / b, c
1947 // we might be doing a bit more work, but for now its ok, unless someone
1948 // knows how to fix the query above
1949 if (isset($parents[$masterID])) {
1950 $masterID = $parents[$masterID];
1951 }
1952 else {
1953 $merge[$masterID] = [
1954 'addressee' => $masterAddressee,
1955 'copy' => [],
1956 'postalGreeting' => $masterPostalGreeting,
1957 ];
1958 $merge[$masterID]['emailGreeting'] = &$merge[$masterID]['postalGreeting'];
1959 }
1960 }
1961 $parents[$copyID] = $masterID;
1962
1963 if (!$sharedAddress && !array_key_exists($copyID, $merge[$masterID]['copy'])) {
1964
ee769c00 1965 if ($copyPostalGreeting) {
c8713fd4 1966 $this->trimNonTokensFromAddressString($copyPostalGreeting,
1967 $postalOptions[$dao->copy_postal_greeting_id],
ca76de35 1968 $this->getPostalGreetingTemplate()
c8713fd4 1969 );
1970 $merge[$masterID]['postalGreeting'] = "{$merge[$masterID]['postalGreeting']}, {$copyPostalGreeting}";
1971 // if there happens to be a duplicate, remove it
1972 $merge[$masterID]['postalGreeting'] = str_replace(" {$copyPostalGreeting},", "", $merge[$masterID]['postalGreeting']);
1973 }
1974
ee769c00 1975 if ($copyAddressee) {
c8713fd4 1976 $this->trimNonTokensFromAddressString($copyAddressee,
1977 $addresseeOptions[$dao->copy_addressee_id],
ca76de35 1978 $this->getAddresseeGreetingTemplate()
c8713fd4 1979 );
1980 $merge[$masterID]['addressee'] = "{$merge[$masterID]['addressee']}, " . trim($copyAddressee);
1981 }
1982 }
1983 $merge[$masterID]['copy'][$copyID] = $copyAddressee;
1984 }
1985
1986 return $merge;
1987 }
1988
30237bf3 1989 /**
1990 * Merge contacts with the same address.
30237bf3 1991 */
f8fb0608 1992 public function mergeSameAddress() {
a2b21ae3 1993
30237bf3 1994 $tableName = $this->getTemporaryTable();
1995 // check if any records are present based on if they have used shared address feature,
1996 // and not based on if city / state .. matches.
1997 $sql = "
1998SELECT r1.id as copy_id,
1999 r1.civicrm_primary_id as copy_contact_id,
2000 r1.addressee as copy_addressee,
2001 r1.addressee_id as copy_addressee_id,
2002 r1.postal_greeting as copy_postal_greeting,
2003 r1.postal_greeting_id as copy_postal_greeting_id,
2004 r2.id as master_id,
2005 r2.civicrm_primary_id as master_contact_id,
2006 r2.postal_greeting as master_postal_greeting,
2007 r2.postal_greeting_id as master_postal_greeting_id,
2008 r2.addressee as master_addressee,
2009 r2.addressee_id as master_addressee_id
2010FROM $tableName r1
2011INNER JOIN civicrm_address adr ON r1.master_id = adr.id
2012INNER JOIN $tableName r2 ON adr.contact_id = r2.civicrm_primary_id
2013ORDER BY r1.id";
ee769c00 2014 $linkedMerge = $this->buildMasterCopyArray($sql, TRUE);
30237bf3 2015
2016 // find all the records that have the same street address BUT not in a household
2017 // require match on city and state as well
2018 $sql = "
2019SELECT r1.id as master_id,
2020 r1.civicrm_primary_id as master_contact_id,
2021 r1.postal_greeting as master_postal_greeting,
2022 r1.postal_greeting_id as master_postal_greeting_id,
2023 r1.addressee as master_addressee,
2024 r1.addressee_id as master_addressee_id,
2025 r2.id as copy_id,
2026 r2.civicrm_primary_id as copy_contact_id,
2027 r2.postal_greeting as copy_postal_greeting,
2028 r2.postal_greeting_id as copy_postal_greeting_id,
2029 r2.addressee as copy_addressee,
2030 r2.addressee_id as copy_addressee_id
2031FROM $tableName r1
2032LEFT JOIN $tableName r2 ON ( r1.street_address = r2.street_address AND
2033 r1.city = r2.city AND
2034 r1.state_province_id = r2.state_province_id )
2035WHERE ( r1.household_name IS NULL OR r1.household_name = '' )
2036AND ( r2.household_name IS NULL OR r2.household_name = '' )
2037AND ( r1.street_address != '' )
2038AND r2.id > r1.id
2039ORDER BY r1.id
2040";
ee769c00 2041 $merge = $this->buildMasterCopyArray($sql);
30237bf3 2042
2043 // unset ids from $merge already present in $linkedMerge
2044 foreach ($linkedMerge as $masterID => $values) {
2045 $keys = [$masterID];
2046 $keys = array_merge($keys, array_keys($values['copy']));
2047 foreach ($merge as $mid => $vals) {
2048 if (in_array($mid, $keys)) {
2049 unset($merge[$mid]);
2050 }
2051 else {
2052 foreach ($values['copy'] as $copyId) {
2053 if (in_array($copyId, $keys)) {
2054 unset($merge[$mid]['copy'][$copyId]);
2055 }
2056 }
2057 }
2058 }
2059 }
2060 $merge = $merge + $linkedMerge;
2061
2062 foreach ($merge as $masterID => $values) {
2063 $sql = "
2064UPDATE $tableName
2065SET addressee = %1, postal_greeting = %2, email_greeting = %3
2066WHERE id = %4
2067";
2068 $params = [
2069 1 => [$values['addressee'], 'String'],
2070 2 => [$values['postalGreeting'], 'String'],
2071 3 => [$values['emailGreeting'], 'String'],
2072 4 => [$masterID, 'Integer'],
2073 ];
2074 CRM_Core_DAO::executeQuery($sql, $params);
2075
2076 // delete all copies
2077 $deleteIDs = array_keys($values['copy']);
2078 $deleteIDString = implode(',', $deleteIDs);
2079 $sql = "
2080DELETE FROM $tableName
2081WHERE id IN ( $deleteIDString )
2082";
2083 CRM_Core_DAO::executeQuery($sql);
2084 }
30237bf3 2085 }
2086
e358ebd1 2087 /**
2088 * The function unsets static part of the string, if token is the dynamic part.
2089 *
2090 * Example: 'Hello {contact.first_name}' => converted to => '{contact.first_name}'
2091 * i.e 'Hello Alan' => converted to => 'Alan'
2092 *
2093 * @param string $parsedString
2094 * @param string $defaultGreeting
ca76de35 2095 * @param string $greetingLabel
e358ebd1 2096 *
2097 * @return mixed
2098 */
2099 public function trimNonTokensFromAddressString(
2100 &$parsedString, $defaultGreeting,
ca76de35 2101 $greetingLabel
e358ebd1 2102 ) {
e358ebd1 2103 $greetingLabel = empty($greetingLabel) ? $defaultGreeting : $greetingLabel;
2104
2105 $stringsToBeReplaced = preg_replace('/(\{[a-zA-Z._ ]+\})/', ';;', $greetingLabel);
2106 $stringsToBeReplaced = explode(';;', $stringsToBeReplaced);
2107 foreach ($stringsToBeReplaced as $key => $string) {
2108 // to keep one space
2109 $stringsToBeReplaced[$key] = ltrim($string);
2110 }
2111 $parsedString = str_replace($stringsToBeReplaced, "", $parsedString);
2112
2113 return $parsedString;
2114 }
2115
978f5005 2116 /**
2117 * Preview export output.
2118 *
2119 * @param int $limit
2120 * @return array
2121 */
2122 public function getPreview($limit) {
2123 $rows = [];
2124 list($outputColumns, $metadata) = $this->getExportStructureArrays();
2125 $query = $this->runQuery([], '');
2126 CRM_Core_DAO::disableFullGroupByMode();
2127 $result = CRM_Core_DAO::executeQuery($query[1] . ' LIMIT ' . (int) $limit);
2128 CRM_Core_DAO::reenableFullGroupByMode();
2129 while ($result->fetch()) {
2130 $rows[] = $this->buildRow($query[0], $result, $outputColumns, $metadata, [], []);
2131 }
2132 return $rows;
2133 }
2134
1e9e062d 2135 /**
2136 * Set the template strings to be used when merging two contacts with the same address.
2137 *
2138 * @param array $formValues
2139 * Values from first form. In this case we care about the keys
2140 * - postal_greeting
2141 * - postal_other
2142 * - address_greeting
2143 * - addressee_other
2144 *
2145 * @return mixed
2146 */
2147 protected function setGreetingStringsForSameAddressMerge($formValues) {
2148 $greetingOptions = CRM_Export_Form_Select::getGreetingOptions();
2149
2150 if (!empty($greetingOptions)) {
2151 // Greeting options is keyed by 'postal_greeting' or 'addressee'.
2152 foreach ($greetingOptions as $key => $value) {
2153 $option = CRM_Utils_Array::value($key, $formValues);
2154 if ($option) {
2155 if ($greetingOptions[$key][$option] == ts('Other')) {
2156 $formValues[$key] = $formValues["{$key}_other"];
2157 }
2158 elseif ($greetingOptions[$key][$option] == ts('List of names')) {
2159 $formValues[$key] = '';
2160 }
2161 else {
2162 $formValues[$key] = $greetingOptions[$key][$option];
2163 }
2164 }
2165 }
2166 }
2167 if (!empty($formValues['postal_greeting'])) {
2168 $this->setPostalGreetingTemplate($formValues['postal_greeting']);
2169 }
2170 if (!empty($formValues['addressee'])) {
2171 $this->setAddresseeGreetingTemplate($formValues['addressee']);
2172 }
2173 }
2174
3e644436 2175 /**
2176 * Create the temporary table for output.
2177 */
2178 public function createTempTable() {
2179 //creating a temporary table for the search result that need be exported
2180 $exportTempTable = CRM_Utils_SQL_TempTable::build()->setDurable()->setCategory('export');
2181 $sqlColumns = $this->getSQLColumns();
2182 // also create the sql table
2183 $exportTempTable->drop();
2184
2185 $sql = " id int unsigned NOT NULL AUTO_INCREMENT, ";
2186 if (!empty($sqlColumns)) {
2187 $sql .= implode(",\n", array_values($sqlColumns)) . ',';
2188 }
2189
2190 $sql .= "\n PRIMARY KEY ( id )";
2191
2192 // add indexes for street_address and household_name if present
2193 $addIndices = [
2194 'street_address',
2195 'household_name',
2196 'civicrm_primary_id',
2197 ];
2198
2199 foreach ($addIndices as $index) {
2200 if (isset($sqlColumns[$index])) {
2201 $sql .= ",
2202 INDEX index_{$index}( $index )
2203";
2204 }
2205 }
2206
2207 $exportTempTable->createWithColumns($sql);
2208 $this->setTemporaryTable($exportTempTable->getName());
2209 }
2210
b0b40cd7 2211 /**
2212 * Get the values of linked household contact.
2213 *
2214 * @param CRM_Core_DAO $relDAO
2215 * @param array $value
2216 * @param string $field
2217 * @param array $row
2218 *
2219 * @throws \Exception
2220 */
2221 public function fetchRelationshipDetails($relDAO, $value, $field, &$row) {
2222 $phoneTypes = CRM_Core_PseudoConstant::get('CRM_Core_DAO_Phone', 'phone_type_id');
2223 $imProviders = CRM_Core_PseudoConstant::get('CRM_Core_DAO_IM', 'provider_id');
2224 $i18n = CRM_Core_I18n::singleton();
2225 $field = $field . '_';
2226
2227 foreach ($value as $relationField => $relationValue) {
2228 if (is_object($relDAO) && property_exists($relDAO, $relationField)) {
2229 $fieldValue = $relDAO->$relationField;
2230 if ($relationField == 'phone_type_id') {
2231 $fieldValue = $phoneTypes[$relationValue];
2232 }
2233 elseif ($relationField == 'provider_id') {
2234 $fieldValue = CRM_Utils_Array::value($relationValue, $imProviders);
2235 }
2236 // CRM-13995
2237 elseif (is_object($relDAO) && in_array($relationField, [
2238 'email_greeting',
2239 'postal_greeting',
2240 'addressee',
2241 ])) {
2242 //special case for greeting replacement
2243 $fldValue = "{$relationField}_display";
2244 $fieldValue = $relDAO->$fldValue;
2245 }
2246 }
2247 elseif (is_object($relDAO) && $relationField == 'state_province') {
2248 $fieldValue = CRM_Core_PseudoConstant::stateProvince($relDAO->state_province_id);
2249 }
2250 elseif (is_object($relDAO) && $relationField == 'country') {
2251 $fieldValue = CRM_Core_PseudoConstant::country($relDAO->country_id);
2252 }
2253 else {
2254 $fieldValue = '';
2255 }
2256 $relPrefix = $field . $relationField;
2257
2258 if (is_object($relDAO) && $relationField == 'id') {
2259 $row[$relPrefix] = $relDAO->contact_id;
2260 }
2261 elseif (is_array($relationValue) && $relationField == 'location') {
2262 foreach ($relationValue as $ltype => $val) {
2263 // If the location name has a space in it the we need to handle that. This
2264 // is kinda hacky but specifically covered in the ExportTest so later efforts to
2265 // improve it should be secure in the knowled it will be caught.
2266 $ltype = str_replace(' ', '_', $ltype);
2267 foreach (array_keys($val) as $fld) {
2268 $type = explode('-', $fld);
2269 $fldValue = "{$ltype}-" . $type[0];
2270 if (!empty($type[1])) {
2271 $fldValue .= "-" . $type[1];
2272 }
2273 // CRM-3157: localise country, region (both have ‘country’ context)
2274 // and state_province (‘province’ context)
2275 switch (TRUE) {
2276 case (!is_object($relDAO)):
2277 $row[$field . '_' . $fldValue] = '';
2278 break;
2279
2280 case in_array('country', $type):
2281 case in_array('world_region', $type):
2282 $row[$field . '_' . $fldValue] = $i18n->crm_translate($relDAO->$fldValue,
2283 ['context' => 'country']
2284 );
2285 break;
2286
2287 case in_array('state_province', $type):
2288 $row[$field . '_' . $fldValue] = $i18n->crm_translate($relDAO->$fldValue,
2289 ['context' => 'province']
2290 );
2291 break;
2292
2293 default:
2294 $row[$field . '_' . $fldValue] = $relDAO->$fldValue;
2295 break;
2296 }
2297 }
2298 }
2299 }
2300 elseif (isset($fieldValue) && $fieldValue != '') {
2301 //check for custom data
2302 if ($cfID = CRM_Core_BAO_CustomField::getKeyID($relationField)) {
2303 $row[$relPrefix] = CRM_Core_BAO_CustomField::displayValue($fieldValue, $cfID);
2304 }
2305 else {
2306 //normal relationship fields
2307 // CRM-3157: localise country, region (both have ‘country’ context) and state_province (‘province’ context)
2308 switch ($relationField) {
2309 case 'country':
2310 case 'world_region':
2311 $row[$relPrefix] = $i18n->crm_translate($fieldValue, ['context' => 'country']);
2312 break;
2313
2314 case 'state_province':
2315 $row[$relPrefix] = $i18n->crm_translate($fieldValue, ['context' => 'province']);
2316 break;
2317
2318 default:
2319 $row[$relPrefix] = $fieldValue;
2320 break;
2321 }
2322 }
2323 }
2324 else {
2325 // if relation field is empty or null
2326 $row[$relPrefix] = '';
2327 }
2328 }
2329 }
2330
b6149515 2331 /**
2332 * Write to the csv from the temp table.
2333 */
2334 public function writeCSVFromTable() {
2335 // call export hook
2336 $headerRows = $this->getHeaderRows();
2337 $exportTempTable = $this->getTemporaryTable();
246e571e
JG
2338 $exportMode = $this->getExportMode();
2339 $sqlColumns = $this->getSQLColumns();
2340 $componentTable = $this->getComponentTable();
2341 $ids = $this->getIds();
b6149515 2342 CRM_Utils_Hook::export($exportTempTable, $headerRows, $sqlColumns, $exportMode, $componentTable, $ids);
246e571e
JG
2343 if ($exportMode !== $this->getExportMode() || $componentTable !== $this->getComponentTable()) {
2344 CRM_Core_Error::deprecatedFunctionWarning('altering the export mode and/or component table in the hook is no longer supported.');
2345 }
ca934663
SL
2346 if ($ids !== $this->getIds()) {
2347 CRM_Core_Error::deprecatedFunctionWarning('altering the ids in the hook is no longer supported.');
2348 }
b6149515 2349 if ($exportTempTable !== $this->getTemporaryTable()) {
2350 CRM_Core_Error::deprecatedFunctionWarning('altering the export table in the hook is deprecated (in some flows the table itself will be)');
2351 $this->setTemporaryTable($exportTempTable);
2352 }
2353 $exportTempTable = $this->getTemporaryTable();
2354 $writeHeader = TRUE;
2355 $offset = 0;
2356 // increase this number a lot to avoid making too many queries
2357 // LIMIT is not much faster than a no LIMIT query
2358 // CRM-7675
2359 $limit = 100000;
2360
2361 $query = "SELECT * FROM $exportTempTable";
2362
e2ea63f5 2363 $this->instantiateTempTable($headerRows);
b6149515 2364 while (1) {
2365 $limitQuery = $query . "
2366LIMIT $offset, $limit
2367";
2368 $dao = CRM_Core_DAO::executeQuery($limitQuery);
2369
2370 if ($dao->N <= 0) {
2371 break;
2372 }
2373
2374 $componentDetails = [];
2375 while ($dao->fetch()) {
2376 $row = [];
2377
246e571e 2378 foreach (array_keys($sqlColumns) as $column) {
b6149515 2379 $row[$column] = $dao->$column;
2380 }
2381 $componentDetails[] = $row;
2382 }
e2ea63f5 2383 $this->writeRows($headerRows, $componentDetails);
2384
b6149515 2385 $offset += $limit;
2386 }
2387 }
2388
e2ea63f5 2389 /**
2390 * Set up the temp table.
2391 *
2392 * @param array $headerRows
2393 */
2394 protected function instantiateTempTable(array $headerRows) {
2395 CRM_Utils_System::download(CRM_Utils_String::munge($this->getExportFileName()),
2396 'text/x-csv',
2397 CRM_Core_DAO::$_nullObject,
2398 'csv',
2399 FALSE
2400 );
2401 CRM_Core_Report_Excel::makeCSVTable($headerRows, [], NULL, TRUE, TRUE);
2402 }
2403
2404 /**
2405 * @param array $headerRows
2406 * @param array $componentDetails
2407 */
2408 protected function writeRows(array $headerRows, array $componentDetails) {
2409 CRM_Core_Report_Excel::writeCSVFile($this->getExportFileName(),
2410 $headerRows,
2411 $componentDetails,
2412 NULL,
2413 FALSE
2414 );
2415 }
2416
6003a964 2417}