3 +--------------------------------------------------------------------+
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2019 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
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. |
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. |
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 +--------------------------------------------------------------------+
31 * @copyright CiviCRM LLC (c) 2004-2019
35 * Class CRM_Export_BAO_ExportProcessor
37 * Class to handle logic of export.
39 class CRM_Export_BAO_ExportProcessor
{
49 protected $exportMode;
52 * Array of fields in the main query.
56 protected $queryFields = [];
63 protected $queryOperator;
66 * Requested output fields.
68 * If set to NULL then it is 'primary fields only'
69 * which actually means pretty close to all fields!
73 protected $requestedFields;
76 * Is the contact being merged into a single household.
80 protected $isMergeSameHousehold;
83 * Should contacts with the same address be merged.
87 protected $isMergeSameAddress = FALSE;
90 * Fields that need to be retrieved for address merge purposes but should not be in output.
94 protected $additionalFieldsForSameAddressMerge = [];
97 * Get additional non-visible fields for address merge purposes.
101 public function getAdditionalFieldsForSameAddressMerge(): array {
102 return $this->additionalFieldsForSameAddressMerge
;
106 * Set additional non-visible fields for address merge purposes.
108 public function setAdditionalFieldsForSameAddressMerge() {
109 if ($this->isMergeSameAddress
) {
110 $fields = ['id', 'master_id', 'state_province_id', 'postal_greeting_id', 'addressee_id'];
111 foreach ($fields as $index => $field) {
112 if (!empty($this->getReturnProperties()[$field])) {
113 unset($fields[$index]);
116 $this->additionalFieldsForSameAddressMerge
= array_fill_keys($fields, 1);
121 * Should contacts with the same address be merged.
125 public function isMergeSameAddress(): bool {
126 return $this->isMergeSameAddress
;
130 * Set same address is to be merged.
132 * @param bool $isMergeSameAddress
134 public function setIsMergeSameAddress(bool $isMergeSameAddress) {
135 $this->isMergeSameAddress
= $isMergeSameAddress;
139 * Additional fields required to export postal fields.
143 protected $additionalFieldsForPostalExport = [];
146 * Get additional fields required to do a postal export.
150 public function getAdditionalFieldsForPostalExport() {
151 return $this->additionalFieldsForPostalExport
;
155 * Set additional fields required for a postal export.
157 public function setAdditionalFieldsForPostalExport() {
158 if ($this->getRequestedFields() && $this->isPostalableOnly()) {
159 $fields = ['is_deceased', 'do_not_mail', 'street_address', 'supplemental_address_1'];
160 foreach ($fields as $index => $field) {
161 if (!empty($this->getReturnProperties()[$field])) {
162 unset($field[$index]);
165 $this->additionalFieldsForPostalExport
= array_fill_keys($fields, 1);
170 * Only export contacts that can receive postal mail.
172 * Includes being alive, having an address & not having do_not_mail.
176 protected $isPostalableOnly;
179 * Key representing the head of household in the relationship array.
181 * e.g. ['8_b_a' => 'Household Member Is', '8_a_b = 'Household Member Of'.....]
185 protected $relationshipTypes = [];
188 * Array of properties to retrieve for relationships.
192 protected $relationshipReturnProperties = [];
195 * IDs of households that have already been exported.
199 protected $exportedHouseholds = [];
202 * Households to skip during export as they will be exported via their relationships anyway.
206 protected $householdsToSkip = [];
209 * Additional fields to return.
211 * This doesn't make much sense when we have a fields set but search build add it's own onto
212 * the 'Primary fields' (all) option.
216 protected $additionalRequestedReturnProperties = [];
219 * Get additional return properties.
223 public function getAdditionalRequestedReturnProperties() {
224 return $this->additionalRequestedReturnProperties
;
228 * Set additional return properties.
230 * @param array $value
232 public function setAdditionalRequestedReturnProperties($value) {
234 if (!empty($value['group'])) {
235 unset($value['group']);
236 $value['groups'] = 1;
238 $this->additionalRequestedReturnProperties
= $value;
242 * Get return properties by relationship.
245 public function getRelationshipReturnProperties() {
246 return $this->relationshipReturnProperties
;
250 * Export values for related contacts.
254 protected $relatedContactValues = [];
259 protected $returnProperties = [];
264 protected $outputSpecification = [];
269 protected $componentTable = '';
274 public function getComponentTable() {
275 return $this->componentTable
;
279 * Set the component table (if any).
281 * @param string $componentTable
283 public function setComponentTable($componentTable) {
284 $this->componentTable
= $componentTable;
288 * Clause from component search.
292 protected $componentClause = '';
297 public function getComponentClause() {
298 return $this->componentClause
;
302 * @param string $componentClause
304 public function setComponentClause($componentClause) {
305 $this->componentClause
= $componentClause;
309 * Name of a temporary table created to hold the results.
311 * Current decision making on when to create a temp table is kinda bad so this might change
312 * a bit as it is reviewed but basically we need a temp table or similar to calculate merging
313 * addresses. Merging households is handled in php. We create a temp table even when we don't need them.
317 protected $temporaryTable;
322 public function getTemporaryTable(): string {
323 return $this->temporaryTable
;
327 * @param string $temporaryTable
329 public function setTemporaryTable(string $temporaryTable) {
330 $this->temporaryTable
= $temporaryTable;
334 * CRM_Export_BAO_ExportProcessor constructor.
336 * @param int $exportMode
337 * @param array|null $requestedFields
338 * @param string $queryOperator
339 * @param bool $isMergeSameHousehold
340 * @param bool $isPostalableOnly
341 * @param bool $isMergeSameAddress
343 public function __construct($exportMode, $requestedFields, $queryOperator, $isMergeSameHousehold = FALSE, $isPostalableOnly = FALSE, $isMergeSameAddress = FALSE) {
344 $this->setExportMode($exportMode);
345 $this->setQueryMode();
346 $this->setQueryOperator($queryOperator);
347 $this->setRequestedFields($requestedFields);
348 $this->setRelationshipTypes();
349 $this->setIsMergeSameHousehold($isMergeSameHousehold);
350 $this->setIsPostalableOnly($isPostalableOnly);
351 $this->setIsMergeSameAddress($isMergeSameAddress);
352 $this->setReturnProperties($this->determineReturnProperties());
353 $this->setAdditionalFieldsForSameAddressMerge();
354 $this->setAdditionalFieldsForPostalExport();
355 $this->setHouseholdMergeReturnProperties();
361 public function isPostalableOnly() {
362 return $this->isPostalableOnly
;
366 * @param bool $isPostalableOnly
368 public function setIsPostalableOnly($isPostalableOnly) {
369 $this->isPostalableOnly
= $isPostalableOnly;
375 public function getRequestedFields() {
376 return empty($this->requestedFields
) ?
NULL : $this->requestedFields
;
380 * @param array|null $requestedFields
382 public function setRequestedFields($requestedFields) {
383 $this->requestedFields
= $requestedFields;
389 public function getReturnProperties() {
390 return array_merge($this->returnProperties
, $this->getAdditionalRequestedReturnProperties(), $this->getAdditionalFieldsForSameAddressMerge(), $this->getAdditionalFieldsForPostalExport());
394 * @param array $returnProperties
396 public function setReturnProperties($returnProperties) {
397 $this->returnProperties
= $returnProperties;
403 public function getRelationshipTypes() {
404 return $this->relationshipTypes
;
409 public function setRelationshipTypes() {
410 $this->relationshipTypes
= CRM_Contact_BAO_Relationship
::getContactRelationshipType(
422 * Set the value for a relationship type field.
424 * In this case we are building up an array of properties for a related contact.
426 * These may be used for direct exporting or for merge to household depending on the
429 * @param string $relationshipType
430 * @param int $contactID
431 * @param string $field
432 * @param string $value
434 public function setRelationshipValue($relationshipType, $contactID, $field, $value) {
435 $this->relatedContactValues
[$relationshipType][$contactID][$field] = $value;
436 if ($field === 'id') {
437 $this->householdsToSkip
[] = $value;
442 * Get the value for a relationship type field.
444 * In this case we are building up an array of properties for a related contact.
446 * These may be used for direct exporting or for merge to household depending on the
449 * @param string $relationshipType
450 * @param int $contactID
451 * @param string $field
455 public function getRelationshipValue($relationshipType, $contactID, $field) {
456 return isset($this->relatedContactValues
[$relationshipType][$contactID][$field]) ?
$this->relatedContactValues
[$relationshipType][$contactID][$field] : '';
460 * Get the id of the related household.
462 * @param int $contactID
463 * @param string $relationshipType
467 public function getRelatedHouseholdID($contactID, $relationshipType) {
468 return $this->relatedContactValues
[$relationshipType][$contactID]['id'];
472 * Has the household already been exported.
474 * @param int $housholdContactID
478 public function isHouseholdExported($housholdContactID) {
479 return isset($this->exportedHouseholds
[$housholdContactID]);
486 public function isMergeSameHousehold() {
487 return $this->isMergeSameHousehold
;
491 * @param bool $isMergeSameHousehold
493 public function setIsMergeSameHousehold($isMergeSameHousehold) {
494 $this->isMergeSameHousehold
= $isMergeSameHousehold;
498 * Return relationship types for household merge.
502 public function getHouseholdRelationshipTypes() {
503 if (!$this->isMergeSameHousehold()) {
507 CRM_Utils_Array
::key('Household Member of', $this->getRelationshipTypes()),
508 CRM_Utils_Array
::key('Head of Household for', $this->getRelationshipTypes()),
516 public function isRelationshipTypeKey($fieldName) {
517 return array_key_exists($fieldName, $this->relationshipTypes
);
524 public function isHouseholdMergeRelationshipTypeKey($fieldName) {
525 return in_array($fieldName, $this->getHouseholdRelationshipTypes());
531 public function getQueryOperator() {
532 return $this->queryOperator
;
536 * @param string $queryOperator
538 public function setQueryOperator($queryOperator) {
539 $this->queryOperator
= $queryOperator;
545 public function getQueryFields() {
546 return $this->queryFields
;
550 * @param array $queryFields
552 public function setQueryFields($queryFields) {
553 // legacy hacks - we add these to queryFields because this
554 // pseudometadata is currently required.
555 $queryFields['im_provider']['pseudoconstant']['var'] = 'imProviders';
556 $queryFields['country']['context'] = 'country';
557 $queryFields['world_region']['context'] = 'country';
558 $queryFields['state_province']['context'] = 'province';
559 $this->queryFields
= $queryFields;
565 public function getQueryMode() {
566 return $this->queryMode
;
570 * Set the query mode based on the export mode.
572 public function setQueryMode() {
574 switch ($this->getExportMode()) {
575 case CRM_Export_Form_Select
::CONTRIBUTE_EXPORT
:
576 $this->queryMode
= CRM_Contact_BAO_Query
::MODE_CONTRIBUTE
;
579 case CRM_Export_Form_Select
::EVENT_EXPORT
:
580 $this->queryMode
= CRM_Contact_BAO_Query
::MODE_EVENT
;
583 case CRM_Export_Form_Select
::MEMBER_EXPORT
:
584 $this->queryMode
= CRM_Contact_BAO_Query
::MODE_MEMBER
;
587 case CRM_Export_Form_Select
::PLEDGE_EXPORT
:
588 $this->queryMode
= CRM_Contact_BAO_Query
::MODE_PLEDGE
;
591 case CRM_Export_Form_Select
::CASE_EXPORT
:
592 $this->queryMode
= CRM_Contact_BAO_Query
::MODE_CASE
;
595 case CRM_Export_Form_Select
::GRANT_EXPORT
:
596 $this->queryMode
= CRM_Contact_BAO_Query
::MODE_GRANT
;
599 case CRM_Export_Form_Select
::ACTIVITY_EXPORT
:
600 $this->queryMode
= CRM_Contact_BAO_Query
::MODE_ACTIVITY
;
604 $this->queryMode
= CRM_Contact_BAO_Query
::MODE_CONTACTS
;
611 public function getExportMode() {
612 return $this->exportMode
;
616 * @param int $exportMode
618 public function setExportMode($exportMode) {
619 $this->exportMode
= $exportMode;
623 * Get the name for the export file.
627 public function getExportFileName() {
628 switch ($this->getExportMode()) {
629 case CRM_Export_Form_Select
::CONTACT_EXPORT
:
630 return ts('CiviCRM Contact Search');
632 case CRM_Export_Form_Select
::CONTRIBUTE_EXPORT
:
633 return ts('CiviCRM Contribution Search');
635 case CRM_Export_Form_Select
::MEMBER_EXPORT
:
636 return ts('CiviCRM Member Search');
638 case CRM_Export_Form_Select
::EVENT_EXPORT
:
639 return ts('CiviCRM Participant Search');
641 case CRM_Export_Form_Select
::PLEDGE_EXPORT
:
642 return ts('CiviCRM Pledge Search');
644 case CRM_Export_Form_Select
::CASE_EXPORT
:
645 return ts('CiviCRM Case Search');
647 case CRM_Export_Form_Select
::GRANT_EXPORT
:
648 return ts('CiviCRM Grant Search');
650 case CRM_Export_Form_Select
::ACTIVITY_EXPORT
:
651 return ts('CiviCRM Activity Search');
654 // Legacy code suggests the value could be 'financial' - ie. something
655 // other than what should be accepted. However, I suspect that this line is
657 return ts('CiviCRM Search');
662 * Get the label for the header row based on the field to output.
664 * @param string $field
668 public function getHeaderForRow($field) {
669 if (substr($field, -11) == 'campaign_id') {
670 // @todo - set this correctly in the xml rather than here.
671 // This will require a generalised handling cleanup
672 return ts('Campaign ID');
674 if ($this->isMergeSameHousehold() && $field === 'id') {
675 return ts('Household ID');
677 elseif (isset($this->getQueryFields()[$field]['title'])) {
678 return $this->getQueryFields()[$field]['title'];
680 elseif ($this->isExportPaymentFields() && array_key_exists($field, $this->getcomponentPaymentFields())) {
681 return CRM_Utils_Array
::value($field, $this->getcomponentPaymentFields());
694 public function runQuery($params, $order) {
695 $returnProperties = $this->getReturnProperties();
697 $params = array_merge($params, $this->getWhereParams());
698 if ($this->isPostalableOnly
) {
699 if (array_key_exists('street_address', $returnProperties)) {
700 $addressWhere = " civicrm_address.street_address <> ''";
701 if (array_key_exists('supplemental_address_1', $returnProperties)) {
702 // We need this to be an OR rather than AND on the street_address so, hack it in.
703 $addressOptions = CRM_Core_BAO_Setting
::valueOptions(CRM_Core_BAO_Setting
::SYSTEM_PREFERENCES_NAME
,
704 'address_options', TRUE, NULL, TRUE
706 if (!empty($addressOptions['supplemental_address_1'])) {
707 $addressWhere .= " OR civicrm_address.supplemental_address_1 <> ''";
710 $addressWhere = ' AND (' . $addressWhere . ')';
713 $query = new CRM_Contact_BAO_Query($params, $returnProperties, NULL,
714 FALSE, FALSE, $this->getQueryMode(),
715 FALSE, TRUE, TRUE, NULL, $this->getQueryOperator()
720 $query->_sort
= $order;
721 list($select, $from, $where, $having) = $query->query();
722 $this->setQueryFields($query->_fields
);
723 $whereClauses = ['trash_clause' => "contact_a.is_deleted != 1"];
724 if ($this->getRequestedFields() && ($this->getComponentTable())) {
725 $from .= " INNER JOIN " . $this->getComponentTable() . " ctTable ON ctTable.contact_id = contact_a.id ";
727 elseif ($this->getComponentClause()) {
728 $whereClauses[] = $this->getComponentClause();
731 // CRM-13982 - check if is deleted
732 foreach ($params as $value) {
733 if ($value[0] == 'contact_is_deleted') {
734 unset($whereClauses['trash_clause']);
739 $where = "WHERE " . implode(' AND ', $whereClauses);
742 $where .= " AND " . implode(' AND ', $whereClauses);
745 $groupBy = $this->getGroupBy($query);
746 $queryString = "$select $from $where $having $groupBy";
748 // always add contact_a.id to the ORDER clause
749 // so the order is deterministic
751 if (strpos('contact_a.id', $order) === FALSE) {
752 $order .= ", contact_a.id";
755 list($field, $dir) = explode(' ', $order, 2);
756 $field = trim($field);
757 if (!empty($this->getReturnProperties()[$field])) {
759 $queryString .= " ORDER BY $order";
762 return [$query, $queryString];
766 * Add a row to the specification for how to output data.
769 * @param string $relationshipType
770 * @param string $locationType
771 * @param int $entityTypeID phone_type_id or provider_id for phone or im fields.
773 public function addOutputSpecification($key, $relationshipType = NULL, $locationType = NULL, $entityTypeID = NULL) {
776 if ($key === 'phone') {
777 $entityLabel = CRM_Core_PseudoConstant
::getLabel('CRM_Core_BAO_Phone', 'phone_type_id', $entityTypeID);
780 $entityLabel = CRM_Core_PseudoConstant
::getLabel('CRM_Core_BAO_IM', 'provider_id', $entityTypeID);
784 // These oddly constructed keys are for legacy reasons. Altering them will affect test success
785 // but in time it may be good to rationalise them.
786 $label = $this->getOutputSpecificationLabel($key, $relationshipType, $locationType, $entityLabel);
787 $index = $this->getOutputSpecificationIndex($key, $relationshipType, $locationType, $entityLabel);
788 $fieldKey = $this->getOutputSpecificationFieldKey($key, $relationshipType, $locationType, $entityLabel);
790 $this->outputSpecification
[$index]['header'] = $label;
791 $this->outputSpecification
[$index]['sql_columns'] = $this->getSqlColumnDefinition($fieldKey, $key);
793 if ($relationshipType && $this->isHouseholdMergeRelationshipTypeKey($relationshipType)) {
794 $this->setColumnAsCalculationOnly($index);
796 $this->outputSpecification
[$index]['metadata'] = $this->getMetaDataForField($key);
800 * Get the metadata for the given field.
806 public function getMetaDataForField($key) {
807 $mappings = ['contact_id' => 'id'];
808 if (isset($this->getQueryFields()[$key])) {
809 return $this->getQueryFields()[$key];
811 if (isset($mappings[$key])) {
812 return $this->getQueryFields()[$mappings[$key]];
820 public function setSqlColumnDefn($key) {
821 $this->outputSpecification
[$this->getMungedFieldName($key)]['sql_columns'] = $this->getSqlColumnDefinition($key, $this->getMungedFieldName($key));
825 * Mark a column as only required for calculations.
827 * Do not include the row with headers.
829 * @param string $column
831 public function setColumnAsCalculationOnly($column) {
832 $this->outputSpecification
[$column]['do_not_output_to_csv'] = TRUE;
838 public function getHeaderRows() {
840 foreach ($this->outputSpecification
as $key => $spec) {
841 if (empty($spec['do_not_output_to_csv'])) {
842 $headerRows[] = $spec['header'];
851 public function getSQLColumns() {
853 foreach ($this->outputSpecification
as $key => $spec) {
854 if (empty($spec['do_not_output_to_sql'])) {
855 $sqlColumns[$key] = $spec['sql_columns'];
864 public function getMetadata() {
866 foreach ($this->outputSpecification
as $key => $spec) {
867 $metadata[$key] = $spec['metadata'];
873 * Build the row for output.
875 * @param \CRM_Contact_BAO_Query $query
876 * @param CRM_Core_DAO $iterationDAO
877 * @param array $outputColumns
879 * @param $paymentDetails
880 * @param $addPaymentHeader
884 public function buildRow($query, $iterationDAO, $outputColumns, $metadata, $paymentDetails, $addPaymentHeader) {
885 $paymentTableId = $this->getPaymentTableID();
886 if ($this->isHouseholdToSkip($iterationDAO->contact_id
)) {
889 $phoneTypes = CRM_Core_PseudoConstant
::get('CRM_Core_DAO_Phone', 'phone_type_id');
890 $imProviders = CRM_Core_PseudoConstant
::get('CRM_Core_DAO_IM', 'provider_id');
893 $householdMergeRelationshipType = $this->getHouseholdMergeTypeForRow($iterationDAO->contact_id
);
894 if ($householdMergeRelationshipType) {
895 $householdID = $this->getRelatedHouseholdID($iterationDAO->contact_id
, $householdMergeRelationshipType);
896 if ($this->isHouseholdExported($householdID)) {
899 foreach (array_keys($outputColumns) as $column) {
900 $row[$column] = $this->getRelationshipValue($householdMergeRelationshipType, $iterationDAO->contact_id
, $column);
902 $this->markHouseholdExported($householdID);
906 $query->convertToPseudoNames($iterationDAO);
908 //first loop through output columns so that we return what is required, and in same order.
909 foreach ($outputColumns as $field => $value) {
910 // add im_provider to $dao object
911 if ($field == 'im_provider' && property_exists($iterationDAO, 'provider_id')) {
912 $iterationDAO->im_provider
= $iterationDAO->provider_id
;
915 //build row values (data)
917 if (property_exists($iterationDAO, $field)) {
918 $fieldValue = $iterationDAO->$field;
919 // to get phone type from phone type id
920 if ($field == 'phone_type_id' && isset($phoneTypes[$fieldValue])) {
921 $fieldValue = $phoneTypes[$fieldValue];
923 elseif ($field == 'provider_id' ||
$field == 'im_provider') {
924 $fieldValue = CRM_Utils_Array
::value($fieldValue, $imProviders);
926 elseif (strstr($field, 'master_id')) {
927 $masterAddressId = NULL;
928 if (isset($iterationDAO->$field)) {
929 $masterAddressId = $iterationDAO->$field;
931 // get display name of contact that address is shared.
932 $fieldValue = CRM_Contact_BAO_Contact
::getMasterDisplayName($masterAddressId);
936 if ($this->isRelationshipTypeKey($field)) {
937 $this->buildRelationshipFieldsForRow($row, $iterationDAO->contact_id
, $value, $field);
940 $row[$field] = $this->getTransformedFieldValue($field, $iterationDAO, $fieldValue, $metadata, $paymentDetails);
944 // If specific payment fields have been selected for export, payment
945 // data will already be in $row. Otherwise, add payment related
946 // information, if appropriate.
947 if ($addPaymentHeader) {
948 if (!$this->isExportSpecifiedPaymentFields()) {
949 $nullContributionDetails = array_fill_keys(array_keys($this->getPaymentHeaders()), NULL);
950 if ($this->isExportPaymentFields()) {
951 $paymentData = CRM_Utils_Array
::value($row[$paymentTableId], $paymentDetails);
952 if (!is_array($paymentData) ||
empty($paymentData)) {
953 $paymentData = $nullContributionDetails;
955 $row = array_merge($row, $paymentData);
957 elseif (!empty($paymentDetails)) {
958 $row = array_merge($row, $nullContributionDetails);
962 //remove organization name for individuals if it is set for current employer
963 if (!empty($row['contact_type']) &&
964 $row['contact_type'] == 'Individual' && array_key_exists('organization_name', $row)
966 $row['organization_name'] = '';
972 * If this row has a household whose details we should use get the relationship type key.
978 public function getHouseholdMergeTypeForRow($contactID) {
979 if (!$this->isMergeSameHousehold()) {
982 foreach ($this->getHouseholdRelationshipTypes() as $relationshipType) {
983 if (isset($this->relatedContactValues
[$relationshipType][$contactID])) {
984 return $relationshipType;
990 * Mark the given household as already exported.
992 * @param $householdID
994 public function markHouseholdExported($householdID) {
995 $this->exportedHouseholds
[$householdID] = $householdID;
1000 * @param $iterationDAO
1001 * @param $fieldValue
1003 * @param $paymentDetails
1007 public function getTransformedFieldValue($field, $iterationDAO, $fieldValue, $metadata, $paymentDetails) {
1009 $i18n = CRM_Core_I18n
::singleton();
1010 if ($field == 'id') {
1011 return $iterationDAO->contact_id
;
1012 // special case for calculated field
1014 elseif ($field == 'source_contact_id') {
1015 return $iterationDAO->contact_id
;
1017 elseif ($field == 'pledge_balance_amount') {
1018 return $iterationDAO->pledge_amount
- $iterationDAO->pledge_total_paid
;
1019 // special case for calculated field
1021 elseif ($field == 'pledge_next_pay_amount') {
1022 return $iterationDAO->pledge_next_pay_amount +
$iterationDAO->pledge_outstanding_amount
;
1024 elseif (isset($fieldValue) &&
1027 //check for custom data
1028 if ($cfID = CRM_Core_BAO_CustomField
::getKeyID($field)) {
1029 return CRM_Core_BAO_CustomField
::displayValue($fieldValue, $cfID);
1032 elseif (in_array($field, [
1037 //special case for greeting replacement
1038 $fldValue = "{$field}_display";
1039 return $iterationDAO->$fldValue;
1042 //normal fields with a touch of CRM-3157
1045 case 'world_region':
1046 return $i18n->crm_translate($fieldValue, ['context' => 'country']);
1048 case 'state_province':
1049 return $i18n->crm_translate($fieldValue, ['context' => 'province']);
1052 case 'preferred_communication_method':
1053 case 'preferred_mail_format':
1054 case 'communication_style':
1055 return $i18n->crm_translate($fieldValue);
1058 if (isset($metadata[$field])) {
1059 // No I don't know why we do it this way & whether we could
1060 // make better use of pseudoConstants.
1061 if (!empty($metadata[$field]['context'])) {
1062 return $i18n->crm_translate($fieldValue, $metadata[$field]);
1064 if (!empty($metadata[$field]['pseudoconstant'])) {
1065 if (!empty($metadata[$field]['bao'])) {
1066 return CRM_Core_PseudoConstant
::getLabel($metadata[$field]['bao'], $metadata[$field]['name'], $fieldValue);
1068 // This is not our normal syntax for pseudoconstants but I am a bit loath to
1069 // call an external function until sure it is not increasing php processing given this
1070 // may be iterated 100,000 times & we already have the $imProvider var loaded.
1071 // That can be next refactor...
1072 // Yes - definitely feeling hatred for this bit of code - I know you will beat me up over it's awfulness
1073 // but I have to reach a stable point....
1074 $varName = $metadata[$field]['pseudoconstant']['var'];
1075 if ($varName === 'imProviders') {
1076 return CRM_Core_PseudoConstant
::getLabel('CRM_Core_DAO_IM', 'provider_id', $fieldValue);
1078 if ($varName === 'phoneTypes') {
1079 return CRM_Core_PseudoConstant
::getLabel('CRM_Core_DAO_Phone', 'phone_type_id', $fieldValue);
1088 elseif ($this->isExportSpecifiedPaymentFields() && array_key_exists($field, $this->getcomponentPaymentFields())) {
1089 $paymentTableId = $this->getPaymentTableID();
1090 $paymentData = CRM_Utils_Array
::value($iterationDAO->$paymentTableId, $paymentDetails);
1092 'componentPaymentField_total_amount' => 'total_amount',
1093 'componentPaymentField_contribution_status' => 'contribution_status',
1094 'componentPaymentField_payment_instrument' => 'pay_instru',
1095 'componentPaymentField_transaction_id' => 'trxn_id',
1096 'componentPaymentField_received_date' => 'receive_date',
1098 return CRM_Utils_Array
::value($payFieldMapper[$field], $paymentData, '');
1101 // if field is empty or null
1107 * Get array of fields to return, over & above those defined in the main contact exportable fields.
1109 * These include export mode specific fields & some fields apparently required as 'exportableFields'
1110 * but not returned by the function of the same name.
1113 * Array of fields to return in the format ['field_name' => 1,...]
1115 public function getAdditionalReturnProperties() {
1116 if ($this->getQueryMode() === CRM_Contact_BAO_Query
::MODE_CONTACTS
) {
1117 $componentSpecificFields = [];
1120 $componentSpecificFields = CRM_Contact_BAO_Query
::defaultReturnProperties($this->getQueryMode());
1122 if ($this->getQueryMode() === CRM_Contact_BAO_Query
::MODE_PLEDGE
) {
1123 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Pledge_BAO_Query
::extraReturnProperties($this->getQueryMode()));
1124 unset($componentSpecificFields['contribution_status_id']);
1125 unset($componentSpecificFields['pledge_status_id']);
1126 unset($componentSpecificFields['pledge_payment_status_id']);
1128 if ($this->getQueryMode() === CRM_Contact_BAO_Query
::MODE_CASE
) {
1129 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Case_BAO_Query
::extraReturnProperties($this->getQueryMode()));
1131 if ($this->getQueryMode() === CRM_Contact_BAO_Query
::MODE_CONTRIBUTE
) {
1132 $componentSpecificFields = array_merge($componentSpecificFields, CRM_Contribute_BAO_Query
::softCreditReturnProperties(TRUE));
1133 unset($componentSpecificFields['contribution_status_id']);
1135 return $componentSpecificFields;
1139 * Should payment fields be appended to the export.
1141 * (This is pretty hacky so hopefully this function won't last long - notice
1142 * how obviously it should be part of the above function!).
1144 public function isExportPaymentFields() {
1145 if ($this->getRequestedFields() === NULL
1146 && in_array($this->getQueryMode(), [
1147 CRM_Contact_BAO_Query
::MODE_EVENT
,
1148 CRM_Contact_BAO_Query
::MODE_MEMBER
,
1149 CRM_Contact_BAO_Query
::MODE_PLEDGE
,
1153 elseif ($this->isExportSpecifiedPaymentFields()) {
1160 * Has specific payment fields been requested (as opposed to via all fields).
1162 * If specific fields have been requested then they get added at various points.
1166 public function isExportSpecifiedPaymentFields() {
1167 if ($this->getRequestedFields() !== NULL && $this->hasRequestedComponentPaymentFields()) {
1173 * Get the name of the id field in the table that connects contributions to the export entity.
1175 public function getPaymentTableID() {
1176 if ($this->getRequestedFields() === NULL) {
1178 CRM_Contact_BAO_Query
::MODE_EVENT
=> 'participant_id',
1179 CRM_Contact_BAO_Query
::MODE_MEMBER
=> 'membership_id',
1180 CRM_Contact_BAO_Query
::MODE_PLEDGE
=> 'pledge_payment_id',
1182 return isset($mapping[$this->getQueryMode()]) ?
$mapping[$this->getQueryMode()] : '';
1184 elseif ($this->hasRequestedComponentPaymentFields()) {
1185 return 'participant_id';
1191 * Have component payment fields been requested.
1195 protected function hasRequestedComponentPaymentFields() {
1196 if ($this->getQueryMode() === CRM_Contact_BAO_Query
::MODE_EVENT
) {
1197 $participantPaymentFields = array_intersect_key($this->getComponentPaymentFields(), $this->getReturnProperties());
1198 if (!empty($participantPaymentFields)) {
1206 * Get fields that indicate payment fields have been requested for a component.
1208 * Ideally this should be protected but making it temporarily public helps refactoring..
1212 public function getComponentPaymentFields() {
1214 'componentPaymentField_total_amount' => ts('Total Amount'),
1215 'componentPaymentField_contribution_status' => ts('Contribution Status'),
1216 'componentPaymentField_received_date' => ts('Date Received'),
1217 'componentPaymentField_payment_instrument' => ts('Payment Method'),
1218 'componentPaymentField_transaction_id' => ts('Transaction ID'),
1223 * Get headers for payment fields.
1225 * Returns an array of contribution fields when the entity supports payment fields and specific fields
1226 * are not specified. This is a transitional function for refactoring legacy code.
1228 public function getPaymentHeaders() {
1229 if ($this->isExportPaymentFields() && !$this->isExportSpecifiedPaymentFields()) {
1230 return $this->getcomponentPaymentFields();
1236 * Get the default properties when not specified.
1238 * In the UI this appears as 'Primary fields only' but in practice it's
1239 * most of the kitchen sink and the hallway closet thrown in.
1241 * Since CRM-952 custom fields are excluded, but no other form of mercy is shown.
1245 public function getDefaultReturnProperties() {
1246 $returnProperties = [];
1247 $fields = CRM_Contact_BAO_Contact
::exportableFields('All', TRUE, TRUE);
1248 $skippedFields = ($this->getQueryMode() === CRM_Contact_BAO_Query
::MODE_CONTACTS
) ?
[] : [
1254 foreach ($fields as $key => $var) {
1255 if ($key && (substr($key, 0, 6) != 'custom') && !in_array($key, $skippedFields)) {
1256 $returnProperties[$key] = 1;
1259 $returnProperties = array_merge($returnProperties, $this->getAdditionalReturnProperties());
1260 return $returnProperties;
1264 * Add the field to relationship return properties & return it.
1266 * This function is doing both setting & getting which is yuck but it is an interim
1269 * @param array $value
1270 * @param string $relationshipKey
1274 public function setRelationshipReturnProperties($value, $relationshipKey) {
1275 $relationField = $value['name'];
1276 $relIMProviderId = NULL;
1277 $relLocTypeId = CRM_Utils_Array
::value('location_type_id', $value);
1278 $locationName = CRM_Core_PseudoConstant
::getName('CRM_Core_BAO_Address', 'location_type_id', $relLocTypeId);
1279 $relPhoneTypeId = CRM_Utils_Array
::value('phone_type_id', $value, ($locationName ?
'Primary' : NULL));
1280 $relIMProviderId = CRM_Utils_Array
::value('im_provider_id', $value, ($locationName ?
'Primary' : NULL));
1281 if (in_array($relationField, $this->getValidLocationFields()) && $locationName) {
1282 if ($relationField === 'phone') {
1283 $this->relationshipReturnProperties
[$relationshipKey]['location'][$locationName]['phone-' . $relPhoneTypeId] = 1;
1285 elseif ($relationField === 'im') {
1286 $this->relationshipReturnProperties
[$relationshipKey]['location'][$locationName]['im-' . $relIMProviderId] = 1;
1289 $this->relationshipReturnProperties
[$relationshipKey]['location'][$locationName][$relationField] = 1;
1293 $this->relationshipReturnProperties
[$relationshipKey][$relationField] = 1;
1295 return $this->relationshipReturnProperties
[$relationshipKey];
1299 * Add the main return properties to the household merge properties if needed for merging.
1301 * If we are using household merge we need to add these to the relationship properties to
1304 public function setHouseholdMergeReturnProperties() {
1305 if ($this->isMergeSameHousehold()) {
1306 $returnProperties = $this->getReturnProperties();
1307 $returnProperties = array_diff_key($returnProperties, array_fill_keys(['location_type', 'im_provider'], 1));
1308 foreach ($this->getHouseholdRelationshipTypes() as $householdRelationshipType) {
1309 $this->relationshipReturnProperties
[$householdRelationshipType] = $returnProperties;
1315 * Get the default location fields to request.
1319 public function getValidLocationFields() {
1322 'supplemental_address_1',
1323 'supplemental_address_2',
1324 'supplemental_address_3',
1327 'postal_code_suffix',
1339 * Get the sql column definition for the given field.
1341 * @param string $fieldName
1342 * @param string $columnName
1346 public function getSqlColumnDefinition($fieldName, $columnName) {
1348 // early exit for master_id, CRM-12100
1349 // in the DB it is an ID, but in the export, we retrive the display_name of the master record
1350 // also for current_employer, CRM-16939
1351 if ($columnName == 'master_id' ||
$columnName == 'current_employer') {
1352 return "$fieldName varchar(128)";
1355 if (substr($fieldName, -11) == 'campaign_id') {
1357 return "$fieldName varchar(128)";
1360 $queryFields = $this->getQueryFields();
1361 $lookUp = ['prefix_id', 'suffix_id'];
1362 // set the sql columns
1363 if (isset($queryFields[$columnName]['type'])) {
1364 switch ($queryFields[$columnName]['type']) {
1365 case CRM_Utils_Type
::T_INT
:
1366 case CRM_Utils_Type
::T_BOOLEAN
:
1367 if (in_array($columnName, $lookUp)) {
1368 return "$fieldName varchar(255)";
1371 return "$fieldName varchar(16)";
1374 case CRM_Utils_Type
::T_STRING
:
1375 if (isset($queryFields[$columnName]['maxlength'])) {
1376 return "$fieldName varchar({$queryFields[$columnName]['maxlength']})";
1379 return "$fieldName varchar(255)";
1382 case CRM_Utils_Type
::T_TEXT
:
1383 case CRM_Utils_Type
::T_LONGTEXT
:
1384 case CRM_Utils_Type
::T_BLOB
:
1385 case CRM_Utils_Type
::T_MEDIUMBLOB
:
1386 return "$fieldName longtext";
1388 case CRM_Utils_Type
::T_FLOAT
:
1389 case CRM_Utils_Type
::T_ENUM
:
1390 case CRM_Utils_Type
::T_DATE
:
1391 case CRM_Utils_Type
::T_TIME
:
1392 case CRM_Utils_Type
::T_TIMESTAMP
:
1393 case CRM_Utils_Type
::T_MONEY
:
1394 case CRM_Utils_Type
::T_EMAIL
:
1395 case CRM_Utils_Type
::T_URL
:
1396 case CRM_Utils_Type
::T_CCNUM
:
1398 return "$fieldName varchar(32)";
1402 if (substr($fieldName, -3, 3) == '_id') {
1403 return "$fieldName varchar(255)";
1405 elseif (substr($fieldName, -5, 5) == '_note') {
1406 return "$fieldName text";
1415 if (in_array($fieldName, $changeFields)) {
1416 return "$fieldName text";
1419 // set the sql columns for custom data
1420 if (isset($queryFields[$columnName]['data_type'])) {
1422 switch ($queryFields[$columnName]['data_type']) {
1424 // May be option labels, which could be up to 512 characters
1425 $length = max(512, CRM_Utils_Array
::value('text_length', $queryFields[$columnName]));
1426 return "$fieldName varchar($length)";
1429 case 'StateProvince':
1431 return "$fieldName varchar(255)";
1434 return "$fieldName text";
1437 return "$fieldName varchar(255)";
1441 return "$fieldName text";
1449 * Get the munged field name.
1451 * @param string $field
1454 public function getMungedFieldName($field) {
1455 $fieldName = CRM_Utils_String
::munge(strtolower($field), '_', 64);
1456 if ($fieldName == 'id') {
1457 $fieldName = 'civicrm_primary_id';
1463 * In order to respect the history of this class we need to index kinda illogically.
1465 * On the bright side - this stuff is tested within a nano-byte of it's life.
1467 * e.g '2-a-b_Home-City'
1469 * @param string $key
1470 * @param string $relationshipType
1471 * @param string $locationType
1472 * @param $entityLabel
1476 protected function getOutputSpecificationIndex($key, $relationshipType, $locationType, $entityLabel) {
1477 if ($entityLabel ||
$key === 'im') {
1478 // Just cos that's the history...
1479 if ($key !== 'master_id') {
1480 $key = $this->getHeaderForRow($key);
1483 if (!$relationshipType ||
$key !== 'id') {
1484 $key = $this->getMungedFieldName($key);
1486 return $this->getMungedFieldName(
1487 ($relationshipType ?
($relationshipType . '_') : '')
1488 . ($locationType ?
($locationType . '_') : '')
1490 . ($entityLabel ?
('_' . $entityLabel) : '')
1495 * Get the compiled label for the column.
1497 * e.g 'Gender', 'Employee Of-Home-city'
1499 * @param string $key
1500 * @param string $relationshipType
1501 * @param string $locationType
1502 * @param string $entityLabel
1506 protected function getOutputSpecificationLabel($key, $relationshipType, $locationType, $entityLabel) {
1507 return ($relationshipType ?
$this->getRelationshipTypes()[$relationshipType] . '-' : '')
1508 . ($locationType ?
$locationType . '-' : '')
1509 . $this->getHeaderForRow($key)
1510 . ($entityLabel ?
'-' . $entityLabel : '');
1514 * Get the mysql field name key.
1516 * This key is locked in by tests but the reasons for the specific conventions -
1517 * ie. headings are used for keying fields in some cases, are likely
1518 * accidental rather than deliberate.
1520 * This key is used for the output sql array.
1522 * @param string $key
1523 * @param $relationshipType
1524 * @param $locationType
1525 * @param $entityLabel
1529 protected function getOutputSpecificationFieldKey($key, $relationshipType, $locationType, $entityLabel) {
1530 if ($entityLabel ||
$key === 'im') {
1531 if ($key !== 'state_province' && $key !== 'id') {
1532 // @todo - test removing this - indexing by $key should be fine...
1533 $key = $this->getHeaderForRow($key);
1536 if (!$relationshipType ||
$key !== 'id') {
1537 $key = $this->getMungedFieldName($key);
1539 $fieldKey = $this->getMungedFieldName(
1540 ($relationshipType ?
($relationshipType . '_') : '')
1541 . ($locationType ?
($locationType . '_') : '')
1543 . ($entityLabel ?
('_' . $entityLabel) : '')
1549 * Get params for the where criteria.
1553 public function getWhereParams() {
1554 if (!$this->isPostalableOnly()) {
1557 $params['is_deceased'] = ['is_deceased', '=', 0, CRM_Contact_BAO_Query
::MODE_CONTACTS
];
1558 $params['do_not_mail'] = ['do_not_mail', '=', 0, CRM_Contact_BAO_Query
::MODE_CONTACTS
];
1568 protected function buildRelationshipFieldsForRow(&$row, $contactID, $value, $field) {
1569 foreach (array_keys($value) as $property) {
1570 if ($property === 'location') {
1571 // @todo just undo all this nasty location wrangling!
1572 foreach ($value['location'] as $locationKey => $locationFields) {
1573 foreach (array_keys($locationFields) as $locationField) {
1574 $fieldKey = str_replace(' ', '_', $locationKey . '-' . $locationField);
1575 $row[$field . '_' . $fieldKey] = $this->getRelationshipValue($field, $contactID, $fieldKey);
1580 $row[$field . '_' . $property] = $this->getRelationshipValue($field, $contactID, $property);
1586 * Is this contact a household that is already set to be exported by virtue of it's household members.
1588 * @param int $contactID
1592 protected function isHouseholdToSkip($contactID) {
1593 return in_array($contactID, $this->householdsToSkip
);
1597 * Get the various arrays that we use to structure our output.
1599 * The extraction of these has been moved to a separate function for clarity and so that
1600 * tests can be added - in particular on the $outputHeaders array.
1602 * However it still feels a bit like something that I'm too polite to write down and this should be seen
1603 * as a step on the refactoring path rather than how it should be.
1606 * - outputColumns Array of columns to be exported. The values don't matter but the key must match the
1607 * alias for the field generated by BAO_Query object.
1608 * - headerRows Array of the column header strings to put in the csv header - non-associative.
1609 * - sqlColumns Array of column names for the temp table. Not too sure why outputColumns can't be used here.
1610 * - metadata Array of fields with specific parameters to pass to the translate function or another hacky nasty solution
1611 * I'm too embarassed to discuss here.
1613 * - to match the outputColumns keys (yes, the fact we ignore the output columns values & then pass another array with values
1614 * we could use does suggest further refactors. However, you future improver, do remember that every check you do
1615 * in the main DAO loop is done once per row & that coule be 100,000 times.)
1616 * Finally a pop quiz: We need the translate context because we use a function other than ts() - is this because
1617 * - a) the function used is more efficient or
1618 * - b) this code is old & outdated. Submit your answers to circular bin or better
1619 * yet find a way to comment them for posterity.
1621 public function getExportStructureArrays() {
1622 $outputColumns = $metadata = [];
1623 $queryFields = $this->getQueryFields();
1624 foreach ($this->getReturnProperties() as $key => $value) {
1625 if (($key != 'location' ||
!is_array($value)) && !$this->isRelationshipTypeKey($key)) {
1626 $outputColumns[$key] = $value;
1627 $this->addOutputSpecification($key);
1629 elseif ($this->isRelationshipTypeKey($key)) {
1630 $outputColumns[$key] = $value;
1631 foreach ($value as $relationField => $relationValue) {
1632 // below block is same as primary block (duplicate)
1633 if (isset($queryFields[$relationField]['title'])) {
1634 $this->addOutputSpecification($relationField, $key);
1636 elseif (is_array($relationValue) && $relationField == 'location') {
1637 // fix header for location type case
1638 foreach ($relationValue as $ltype => $val) {
1639 foreach (array_keys($val) as $fld) {
1640 $type = explode('-', $fld);
1641 $this->addOutputSpecification($type[0], $key, $ltype, CRM_Utils_Array
::value(1, $type));
1648 foreach ($value as $locationType => $locationFields) {
1649 foreach (array_keys($locationFields) as $locationFieldName) {
1650 $type = explode('-', $locationFieldName);
1652 $actualDBFieldName = $type[0];
1653 $daoFieldName = CRM_Utils_String
::munge($locationType) . '-' . $actualDBFieldName;
1655 if (!empty($type[1])) {
1656 $daoFieldName .= "-" . $type[1];
1658 $this->addOutputSpecification($actualDBFieldName, NULL, $locationType, CRM_Utils_Array
::value(1, $type));
1659 $metadata[$daoFieldName] = $this->getMetaDataForField($actualDBFieldName);
1660 $outputColumns[$daoFieldName] = TRUE;
1665 return [$outputColumns, $metadata];
1669 * Get default return property for export based on mode
1672 * Default Return property
1674 public function defaultReturnProperty() {
1675 // hack to add default return property based on export mode
1677 $exportMode = $this->getExportMode();
1678 if ($exportMode == CRM_Export_Form_Select
::CONTRIBUTE_EXPORT
) {
1679 $property = 'contribution_id';
1681 elseif ($exportMode == CRM_Export_Form_Select
::EVENT_EXPORT
) {
1682 $property = 'participant_id';
1684 elseif ($exportMode == CRM_Export_Form_Select
::MEMBER_EXPORT
) {
1685 $property = 'membership_id';
1687 elseif ($exportMode == CRM_Export_Form_Select
::PLEDGE_EXPORT
) {
1688 $property = 'pledge_id';
1690 elseif ($exportMode == CRM_Export_Form_Select
::CASE_EXPORT
) {
1691 $property = 'case_id';
1693 elseif ($exportMode == CRM_Export_Form_Select
::GRANT_EXPORT
) {
1694 $property = 'grant_id';
1696 elseif ($exportMode == CRM_Export_Form_Select
::ACTIVITY_EXPORT
) {
1697 $property = 'activity_id';
1703 * Determine the required return properties from the input parameters.
1707 public function determineReturnProperties() {
1708 if ($this->getRequestedFields()) {
1709 $returnProperties = [];
1710 foreach ($this->getRequestedFields() as $key => $value) {
1711 $fieldName = $value['name'];
1712 $locationName = !empty($value['location_type_id']) ? CRM_Core_PseudoConstant
::getName('CRM_Core_BAO_Address', 'location_type_id', $value['location_type_id']) : NULL;
1713 $relationshipTypeKey = !empty($value['relationship_type_id']) ?
$value['relationship_type_id'] . '_' . $value['relationship_direction'] : NULL;
1714 if (!$fieldName ||
$this->isHouseholdMergeRelationshipTypeKey($relationshipTypeKey)) {
1718 if ($this->isRelationshipTypeKey($relationshipTypeKey)) {
1719 $returnProperties[$relationshipTypeKey] = $this->setRelationshipReturnProperties($value, $relationshipTypeKey);
1721 elseif ($locationName) {
1722 if ($fieldName === 'phone') {
1723 $returnProperties['location'][$locationName]['phone-' . $value['phone_type_id'] ??
NULL] = 1;
1725 elseif ($fieldName === 'im') {
1726 $returnProperties['location'][$locationName]['im-' . $value['im_provider_id'] ??
NULL] = 1;
1729 $returnProperties['location'][$locationName][$fieldName] = 1;
1733 //hack to fix component fields
1734 //revert mix of event_id and title
1735 if ($fieldName == 'event_id') {
1736 $returnProperties['event_id'] = 1;
1739 $returnProperties[$fieldName] = 1;
1743 $defaultExportMode = $this->defaultReturnProperty();
1744 if ($defaultExportMode) {
1745 $returnProperties[$defaultExportMode] = 1;
1749 $returnProperties = $this->getDefaultReturnProperties();
1751 if ($this->isMergeSameHousehold()) {
1752 $returnProperties['id'] = 1;
1754 if ($this->isMergeSameAddress()) {
1755 $returnProperties['addressee'] = 1;
1756 $returnProperties['postal_greeting'] = 1;
1757 $returnProperties['email_greeting'] = 1;
1758 $returnProperties['street_name'] = 1;
1759 $returnProperties['household_name'] = 1;
1760 $returnProperties['street_address'] = 1;
1761 $returnProperties['city'] = 1;
1762 $returnProperties['state_province'] = 1;
1765 return $returnProperties;
1769 * @param object $query
1770 * CRM_Contact_BAO_Query
1775 public function getGroupBy($query) {
1777 $returnProperties = $this->getReturnProperties();
1778 $exportMode = $this->getExportMode();
1779 $queryMode = $this->getQueryMode();
1780 if (!empty($returnProperties['tags']) ||
!empty($returnProperties['groups']) ||
1781 CRM_Utils_Array
::value('notes', $returnProperties) ||
1783 ($queryMode & CRM_Contact_BAO_Query
::MODE_CONTACTS
&& $query->_useGroupBy
)
1785 $groupBy = "contact_a.id";
1788 switch ($exportMode) {
1789 case CRM_Export_Form_Select
::CONTRIBUTE_EXPORT
:
1790 $groupBy = 'civicrm_contribution.id';
1791 if (CRM_Contribute_BAO_Query
::isSoftCreditOptionEnabled()) {
1792 // especial group by when soft credit columns are included
1793 $groupBy = ['contribution_search_scredit_combined.id', 'contribution_search_scredit_combined.scredit_id'];
1797 case CRM_Export_Form_Select
::EVENT_EXPORT
:
1798 $groupBy = 'civicrm_participant.id';
1801 case CRM_Export_Form_Select
::MEMBER_EXPORT
:
1802 $groupBy = "civicrm_membership.id";
1806 if ($queryMode & CRM_Contact_BAO_Query
::MODE_ACTIVITY
) {
1807 $groupBy = "civicrm_activity.id ";
1810 return $groupBy ?
' GROUP BY ' . implode(', ', (array) $groupBy) : '';
1814 * @param int $contactId
1815 * @param array $exportParams
1819 public function replaceMergeTokens($contactId, $exportParams) {
1827 foreach ($greetingFields as $greeting) {
1828 if (!empty($exportParams[$greeting])) {
1829 $greetingLabel = $exportParams[$greeting];
1830 if (empty($contact)) {
1835 $contact = civicrm_api('contact', 'get', $values);
1837 if (!empty($contact['is_error'])) {
1840 $contact = $contact['values'][$contact['id']];
1843 $tokens = ['contact' => $greetingLabel];
1844 $greetings[$greeting] = CRM_Utils_Token
::replaceContactTokens($greetingLabel, $contact, NULL, $tokens);
1851 * Build array for merging same addresses.
1854 * @param array $exportParams
1855 * @param bool $sharedAddress
1859 public function buildMasterCopyArray($sql, $exportParams, $sharedAddress = FALSE) {
1860 static $contactGreetingTokens = [];
1862 $addresseeOptions = CRM_Core_OptionGroup
::values('addressee');
1863 $postalOptions = CRM_Core_OptionGroup
::values('postal_greeting');
1865 $merge = $parents = [];
1866 $dao = CRM_Core_DAO
::executeQuery($sql);
1868 while ($dao->fetch()) {
1869 $masterID = $dao->master_id
;
1870 $copyID = $dao->copy_id
;
1871 $masterPostalGreeting = $dao->master_postal_greeting
;
1872 $masterAddressee = $dao->master_addressee
;
1873 $copyAddressee = $dao->copy_addressee
;
1875 if (!$sharedAddress) {
1876 if (!isset($contactGreetingTokens[$dao->master_contact_id
])) {
1877 $contactGreetingTokens[$dao->master_contact_id
] = $this->replaceMergeTokens($dao->master_contact_id
, $exportParams);
1879 $masterPostalGreeting = CRM_Utils_Array
::value('postal_greeting',
1880 $contactGreetingTokens[$dao->master_contact_id
], $dao->master_postal_greeting
1882 $masterAddressee = CRM_Utils_Array
::value('addressee',
1883 $contactGreetingTokens[$dao->master_contact_id
], $dao->master_addressee
1886 if (!isset($contactGreetingTokens[$dao->copy_contact_id
])) {
1887 $contactGreetingTokens[$dao->copy_contact_id
] = $this->replaceMergeTokens($dao->copy_contact_id
, $exportParams);
1889 $copyPostalGreeting = CRM_Utils_Array
::value('postal_greeting',
1890 $contactGreetingTokens[$dao->copy_contact_id
], $dao->copy_postal_greeting
1892 $copyAddressee = CRM_Utils_Array
::value('addressee',
1893 $contactGreetingTokens[$dao->copy_contact_id
], $dao->copy_addressee
1897 if (!isset($merge[$masterID])) {
1898 // check if this is an intermediate child
1899 // this happens if there are 3 or more matches a,b, c
1900 // the above query will return a, b / a, c / b, c
1901 // we might be doing a bit more work, but for now its ok, unless someone
1902 // knows how to fix the query above
1903 if (isset($parents[$masterID])) {
1904 $masterID = $parents[$masterID];
1907 $merge[$masterID] = [
1908 'addressee' => $masterAddressee,
1910 'postalGreeting' => $masterPostalGreeting,
1912 $merge[$masterID]['emailGreeting'] = &$merge[$masterID]['postalGreeting'];
1915 $parents[$copyID] = $masterID;
1917 if (!$sharedAddress && !array_key_exists($copyID, $merge[$masterID]['copy'])) {
1919 if (!empty($exportParams['postal_greeting_other']) &&
1920 count($merge[$masterID]['copy']) >= 1
1922 // use static greetings specified if no of contacts > 2
1923 $merge[$masterID]['postalGreeting'] = $exportParams['postal_greeting_other'];
1925 elseif ($copyPostalGreeting) {
1926 $this->trimNonTokensFromAddressString($copyPostalGreeting,
1927 $postalOptions[$dao->copy_postal_greeting_id
],
1930 $merge[$masterID]['postalGreeting'] = "{$merge[$masterID]['postalGreeting']}, {$copyPostalGreeting}";
1931 // if there happens to be a duplicate, remove it
1932 $merge[$masterID]['postalGreeting'] = str_replace(" {$copyPostalGreeting},", "", $merge[$masterID]['postalGreeting']);
1935 if (!empty($exportParams['addressee_other']) &&
1936 count($merge[$masterID]['copy']) >= 1
1938 // use static greetings specified if no of contacts > 2
1939 $merge[$masterID]['addressee'] = $exportParams['addressee_other'];
1941 elseif ($copyAddressee) {
1942 $this->trimNonTokensFromAddressString($copyAddressee,
1943 $addresseeOptions[$dao->copy_addressee_id
],
1944 $exportParams, 'addressee'
1946 $merge[$masterID]['addressee'] = "{$merge[$masterID]['addressee']}, " . trim($copyAddressee);
1949 $merge[$masterID]['copy'][$copyID] = $copyAddressee;
1956 * Merge contacts with the same address.
1958 * @param $sqlColumns
1959 * @param array $exportParams
1961 public function mergeSameAddress(&$sqlColumns, $exportParams) {
1962 $greetingOptions = CRM_Export_Form_Select
::getGreetingOptions();
1964 if (!empty($greetingOptions)) {
1965 // Greeting options is keyed by 'postal_greeting' or 'addressee'.
1966 foreach ($greetingOptions as $key => $value) {
1967 if ($option = CRM_Utils_Array
::value($key, $exportParams)) {
1968 if ($greetingOptions[$key][$option] == ts('Other')) {
1969 $exportParams[$key] = $exportParams["{$key}_other"];
1971 elseif ($greetingOptions[$key][$option] == ts('List of names')) {
1972 $exportParams[$key] = '';
1975 $exportParams[$key] = $greetingOptions[$key][$option];
1980 $tableName = $this->getTemporaryTable();
1981 // check if any records are present based on if they have used shared address feature,
1982 // and not based on if city / state .. matches.
1984 SELECT r1.id as copy_id,
1985 r1.civicrm_primary_id as copy_contact_id,
1986 r1.addressee as copy_addressee,
1987 r1.addressee_id as copy_addressee_id,
1988 r1.postal_greeting as copy_postal_greeting,
1989 r1.postal_greeting_id as copy_postal_greeting_id,
1991 r2.civicrm_primary_id as master_contact_id,
1992 r2.postal_greeting as master_postal_greeting,
1993 r2.postal_greeting_id as master_postal_greeting_id,
1994 r2.addressee as master_addressee,
1995 r2.addressee_id as master_addressee_id
1997 INNER JOIN civicrm_address adr ON r1.master_id = adr.id
1998 INNER JOIN $tableName r2 ON adr.contact_id = r2.civicrm_primary_id
2000 $linkedMerge = $this->buildMasterCopyArray($sql, $exportParams, TRUE);
2002 // find all the records that have the same street address BUT not in a household
2003 // require match on city and state as well
2005 SELECT r1.id as master_id,
2006 r1.civicrm_primary_id as master_contact_id,
2007 r1.postal_greeting as master_postal_greeting,
2008 r1.postal_greeting_id as master_postal_greeting_id,
2009 r1.addressee as master_addressee,
2010 r1.addressee_id as master_addressee_id,
2012 r2.civicrm_primary_id as copy_contact_id,
2013 r2.postal_greeting as copy_postal_greeting,
2014 r2.postal_greeting_id as copy_postal_greeting_id,
2015 r2.addressee as copy_addressee,
2016 r2.addressee_id as copy_addressee_id
2018 LEFT JOIN $tableName r2 ON ( r1.street_address = r2.street_address AND
2019 r1.city = r2.city AND
2020 r1.state_province_id = r2.state_province_id )
2021 WHERE ( r1.household_name IS NULL OR r1.household_name = '' )
2022 AND ( r2.household_name IS NULL OR r2.household_name = '' )
2023 AND ( r1.street_address != '' )
2027 $merge = $this->buildMasterCopyArray($sql, $exportParams);
2029 // unset ids from $merge already present in $linkedMerge
2030 foreach ($linkedMerge as $masterID => $values) {
2031 $keys = [$masterID];
2032 $keys = array_merge($keys, array_keys($values['copy']));
2033 foreach ($merge as $mid => $vals) {
2034 if (in_array($mid, $keys)) {
2035 unset($merge[$mid]);
2038 foreach ($values['copy'] as $copyId) {
2039 if (in_array($copyId, $keys)) {
2040 unset($merge[$mid]['copy'][$copyId]);
2046 $merge = $merge +
$linkedMerge;
2048 foreach ($merge as $masterID => $values) {
2051 SET addressee = %1, postal_greeting = %2, email_greeting = %3
2055 1 => [$values['addressee'], 'String'],
2056 2 => [$values['postalGreeting'], 'String'],
2057 3 => [$values['emailGreeting'], 'String'],
2058 4 => [$masterID, 'Integer'],
2060 CRM_Core_DAO
::executeQuery($sql, $params);
2062 // delete all copies
2063 $deleteIDs = array_keys($values['copy']);
2064 $deleteIDString = implode(',', $deleteIDs);
2066 DELETE FROM $tableName
2067 WHERE id IN ( $deleteIDString )
2069 CRM_Core_DAO
::executeQuery($sql);
2072 // unset temporary columns that were added for postal mailing format
2073 // @todo - this part is pretty close to ready to be removed....
2074 if (!empty($exportParams['merge_same_address']['temp_columns'])) {
2075 $unsetKeys = array_keys($sqlColumns);
2076 foreach ($unsetKeys as $headerKey => $sqlColKey) {
2077 if (array_key_exists($sqlColKey, $exportParams['merge_same_address']['temp_columns'])) {
2078 unset($sqlColumns[$sqlColKey]);
2085 * The function unsets static part of the string, if token is the dynamic part.
2087 * Example: 'Hello {contact.first_name}' => converted to => '{contact.first_name}'
2088 * i.e 'Hello Alan' => converted to => 'Alan'
2090 * @param string $parsedString
2091 * @param string $defaultGreeting
2092 * @param bool $addressMergeGreetings
2093 * @param string $greetingType
2097 public function trimNonTokensFromAddressString(
2098 &$parsedString, $defaultGreeting,
2099 $addressMergeGreetings, $greetingType = 'postal_greeting'
2101 if (!empty($addressMergeGreetings[$greetingType])) {
2102 $greetingLabel = $addressMergeGreetings[$greetingType];
2104 $greetingLabel = empty($greetingLabel) ?
$defaultGreeting : $greetingLabel;
2106 $stringsToBeReplaced = preg_replace('/(\{[a-zA-Z._ ]+\})/', ';;', $greetingLabel);
2107 $stringsToBeReplaced = explode(';;', $stringsToBeReplaced);
2108 foreach ($stringsToBeReplaced as $key => $string) {
2109 // to keep one space
2110 $stringsToBeReplaced[$key] = ltrim($string);
2112 $parsedString = str_replace($stringsToBeReplaced, "", $parsedString);
2114 return $parsedString;
2118 * Preview export output.
2123 public function getPreview($limit) {
2125 list($outputColumns, $metadata) = $this->getExportStructureArrays();
2126 $query = $this->runQuery([], '');
2127 CRM_Core_DAO
::disableFullGroupByMode();
2128 $result = CRM_Core_DAO
::executeQuery($query[1] . ' LIMIT ' . (int) $limit);
2129 CRM_Core_DAO
::reenableFullGroupByMode();
2130 while ($result->fetch()) {
2131 $rows[] = $this->buildRow($query[0], $result, $outputColumns, $metadata, [], []);