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