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