Merge pull request #23727 from demeritcowboy/squigglefiles
[civicrm-core.git] / CRM / Contribute / Import / Form / MapField.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 *
14 * @package CRM
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
16 */
17
18 /**
19 * This class gets the name of the file to upload.
20 */
21 class CRM_Contribute_Import_Form_MapField extends CRM_Import_Form_MapField {
22
23 /**
24 * Check if required fields are present.
25 *
26 * @param CRM_Contribute_Import_Form_MapField $self
27 * @param string $contactORContributionId
28 * @param array $importKeys
29 * @param array $errors
30 * @param int $weightSum
31 * @param int $threshold
32 * @param string $fieldMessage
33 *
34 * @return array
35 */
36 protected static function checkRequiredFields($self, string $contactORContributionId, array $importKeys, array $errors, int $weightSum, $threshold, string $fieldMessage): array {
37 // FIXME: should use the schema titles, not redeclare them
38 $requiredFields = [
39 $contactORContributionId == 'contribution_id' ? 'contribution_id' : 'contribution_contact_id' => $contactORContributionId == 'contribution_id' ? ts('Contribution ID') : ts('Contact ID'),
40 'total_amount' => ts('Total Amount'),
41 'financial_type_id' => ts('Financial Type'),
42 ];
43
44 foreach ($requiredFields as $field => $title) {
45 if (!in_array($field, $importKeys)) {
46 if (empty($errors['_qf_default'])) {
47 $errors['_qf_default'] = '';
48 }
49 if ($field == $contactORContributionId) {
50 if (!($weightSum >= $threshold || in_array('external_identifier', $importKeys)) &&
51 $self->_onDuplicate != CRM_Import_Parser::DUPLICATE_UPDATE
52 ) {
53 $errors['_qf_default'] .= ts('Missing required contact matching fields.') . " $fieldMessage " . ts('(Sum of all weights should be greater than or equal to threshold: %1).', [1 => $threshold]) . '<br />';
54 }
55 elseif ($self->_onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE &&
56 !(in_array('invoice_id', $importKeys) || in_array('trxn_id', $importKeys) ||
57 in_array('contribution_id', $importKeys)
58 )
59 ) {
60 $errors['_qf_default'] .= ts('Invoice ID or Transaction ID or Contribution ID are required to match to the existing contribution records in Update mode.') . '<br />';
61 }
62 }
63 else {
64 $errors['_qf_default'] .= ts('Missing required field: %1', [1 => $title]) . '<br />';
65 }
66 }
67 }
68 return $errors;
69 }
70
71 /**
72 * Set variables up before form is built.
73 */
74 public function preProcess() {
75 parent::preProcess();
76
77 $this->_columnCount = $this->get('columnCount');
78 $skipColumnHeader = $this->getSubmittedValue('skipColumnHeader');
79 $this->_onDuplicate = $this->getSubmittedValue('onDuplicate');
80 $this->assign('skipColumnHeader', $skipColumnHeader);
81
82 $highlightedFields = ['financial_type_id', 'total_amount'];
83 //CRM-2219 removing other required fields since for updation only
84 //invoice id or trxn id or contribution id is required.
85 if ($this->_onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE) {
86 $remove = [
87 'contribution_contact_id',
88 'email',
89 'first_name',
90 'last_name',
91 'external_identifier',
92 ];
93 foreach ($remove as $value) {
94 unset($this->_mapperFields[$value]);
95 }
96
97 //modify field title only for update mode. CRM-3245
98 foreach ([
99 'contribution_id',
100 'invoice_id',
101 'trxn_id',
102 ] as $key) {
103 $this->_mapperFields[$key] .= ' (match to contribution record)';
104 $highlightedFields[] = $key;
105 }
106 }
107 elseif ($this->_onDuplicate == CRM_Import_Parser::DUPLICATE_SKIP) {
108 unset($this->_mapperFields['contribution_id']);
109 $highlightedFieldsArray = [
110 'contribution_contact_id',
111 'email',
112 'first_name',
113 'last_name',
114 'external_identifier',
115 ];
116 foreach ($highlightedFieldsArray as $name) {
117 $highlightedFields[] = $name;
118 }
119 }
120
121 // modify field title for contribution status
122 $this->_mapperFields['contribution_status_id'] = ts('Contribution Status');
123
124 $this->assign('highlightedFields', $highlightedFields);
125 }
126
127 /**
128 * Build the form object.
129 *
130 * @throws \CiviCRM_API3_Exception
131 */
132 public function buildQuickForm() {
133 $savedMappingID = $this->getSubmittedValue('savedMapping');
134
135 $this->buildSavedMappingFields($savedMappingID);
136
137 $this->addFormRule([
138 'CRM_Contribute_Import_Form_MapField',
139 'formRule',
140 ], $this);
141
142 //-------- end of saved mapping stuff ---------
143
144 $defaults = [];
145 $mapperKeys = array_keys($this->_mapperFields);
146 $hasHeaders = $this->getSubmittedValue('skipColumnHeader');
147 $headerPatterns = $this->getHeaderPatterns();
148 $dataPatterns = $this->getDataPatterns();
149 $mapperKeysValues = $this->getSubmittedValue('mapper');
150 $columnHeaders = $this->getColumnHeaders();
151
152 /* Initialize all field usages to false */
153 foreach ($mapperKeys as $key) {
154 $this->_fieldUsed[$key] = FALSE;
155 }
156 $sel1 = $this->_mapperFields;
157
158 if (!$this->get('onDuplicate')) {
159 unset($sel1['id']);
160 unset($sel1['contribution_id']);
161 }
162
163 $softCreditFields['contact_id'] = ts('Contact ID');
164 $softCreditFields['external_identifier'] = ts('External ID');
165 $softCreditFields['email'] = ts('Email');
166
167 $sel2['soft_credit'] = $softCreditFields;
168 $sel3['soft_credit']['contact_id'] = $sel3['soft_credit']['external_identifier'] = $sel3['soft_credit']['email'] = CRM_Core_OptionGroup::values('soft_credit_type');
169 $sel4 = NULL;
170
171 // end of soft credit section
172 $js = "<script type='text/javascript'>\n";
173 $formName = 'document.forms.' . $this->_name;
174
175 //used to warn for mismatch column count or mismatch mapping
176 $warning = 0;
177
178 foreach ($columnHeaders as $i => $columnHeader) {
179 $sel = &$this->addElement('hierselect', "mapper[$i]", ts('Mapper for Field %1', [1 => $i]), NULL);
180 $jsSet = FALSE;
181 if ($this->get('savedMapping')) {
182 [$mappingName, $mappingContactType] = CRM_Core_BAO_Mapping::getMappingFields($savedMappingID);
183
184 $mappingName = $mappingName[1];
185 $mappingContactType = $mappingContactType[1];
186 if (isset($mappingName[$i])) {
187 if ($mappingName[$i] != ts('do_not_import')) {
188 $softField = $mappingContactType[$i] ?? '';
189
190 if (!$softField) {
191 $js .= "{$formName}['mapper[$i][1]'].style.display = 'none';\n";
192 }
193
194 $js .= "{$formName}['mapper[$i][2]'].style.display = 'none';\n";
195 $js .= "{$formName}['mapper[$i][3]'].style.display = 'none';\n";
196 $defaults["mapper[$i]"] = [
197 $mappingName[$i],
198 $softField,
199 // Since the soft credit type id is not stored we can't load it here.
200 '',
201 ];
202 $jsSet = TRUE;
203 }
204 else {
205 $defaults["mapper[$i]"] = [];
206 }
207 if (!$jsSet) {
208 for ($k = 1; $k < 4; $k++) {
209 $js .= "{$formName}['mapper[$i][$k]'].style.display = 'none';\n";
210 }
211 }
212 }
213 else {
214 // this load section to help mapping if we ran out of saved columns when doing Load Mapping
215 $js .= "swapOptions($formName, 'mapper[$i]', 0, 3, 'hs_mapper_0_');\n";
216
217 if ($hasHeaders) {
218 $defaults["mapper[$i]"] = [$this->defaultFromHeader($columnHeader, $headerPatterns)];
219 }
220 else {
221 $defaults["mapper[$i]"] = [$this->defaultFromData($dataPatterns, $i)];
222 }
223 }
224 //end of load mapping
225 }
226 else {
227 $js .= "swapOptions($formName, 'mapper[$i]', 0, 3, 'hs_mapper_0_');\n";
228 if ($hasHeaders) {
229 // do array search first to see if has mapped key
230 $columnKey = array_search($columnHeader, $this->_mapperFields);
231 if (isset($this->_fieldUsed[$columnKey])) {
232 $defaults["mapper[$i]"] = $columnKey;
233 $this->_fieldUsed[$key] = TRUE;
234 }
235 else {
236 // Infer the default from the column names if we have them
237 $defaults["mapper[$i]"] = [
238 $this->defaultFromHeader($columnHeader, $headerPatterns),
239 0,
240 ];
241 }
242 }
243 else {
244 // Otherwise guess the default from the form of the data
245 $defaults["mapper[$i]"] = [
246 $this->defaultFromData($dataPatterns, $i),
247 0,
248 ];
249 }
250 if (!empty($mapperKeysValues) && ($mapperKeysValues[$i][0] ?? NULL) === 'soft_credit') {
251 $softCreditField = $mapperKeysValues[$i][1];
252 $softCreditTypeID = $mapperKeysValues[$i][2];
253 $js .= "cj('#mapper_" . $i . "_1').val($softCreditField);\n";
254 $js .= "cj('#mapper_" . $i . "_2').val($softCreditTypeID);\n";
255 }
256 }
257 $sel->setOptions([$sel1, $sel2, $sel3, $sel4]);
258 }
259 $js .= "</script>\n";
260 $this->assign('initHideBoxes', $js);
261
262 //set warning if mismatch in more than
263 if (isset($mappingName)) {
264 if (($this->_columnCount != count($mappingName))) {
265 $warning++;
266 }
267 }
268 if ($warning != 0 && $this->get('savedMapping')) {
269 $session = CRM_Core_Session::singleton();
270 $session->setStatus(ts('The data columns in this import file appear to be different from the saved mapping. Please verify that you have selected the correct saved mapping before continuing.'));
271 }
272 else {
273 $session = CRM_Core_Session::singleton();
274 $session->setStatus(NULL);
275 }
276
277 $this->setDefaults($defaults);
278
279 $this->addFormButtons();
280 }
281
282 /**
283 * Global validation rules for the form.
284 *
285 * @param array $fields
286 * Posted values of the form.
287 *
288 * @param $files
289 * @param self $self
290 *
291 * @return array
292 * list of errors to be posted back to the form
293 */
294 public static function formRule($fields, $files, $self) {
295 $errors = [];
296 $fieldMessage = NULL;
297 $contactORContributionId = $self->_onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE ? 'contribution_id' : 'contribution_contact_id';
298 if (!array_key_exists('savedMapping', $fields)) {
299 $importKeys = [];
300 foreach ($fields['mapper'] as $mapperPart) {
301 $importKeys[] = $mapperPart[0];
302 }
303
304 $params = [
305 'used' => 'Unsupervised',
306 'contact_type' => $self->getContactType(),
307 ];
308 [$ruleFields, $threshold] = CRM_Dedupe_BAO_DedupeRuleGroup::dedupeRuleFieldsWeight($params);
309 $weightSum = 0;
310 foreach ($importKeys as $key => $val) {
311 if (array_key_exists($val, $ruleFields)) {
312 $weightSum += $ruleFields[$val];
313 }
314 if ($val == "soft_credit") {
315 $mapperKey = CRM_Utils_Array::key('soft_credit', $importKeys);
316 if (empty($fields['mapper'][$mapperKey][1])) {
317 if (empty($errors['_qf_default'])) {
318 $errors['_qf_default'] = '';
319 }
320 $errors['_qf_default'] .= ts('Missing required fields: Soft Credit') . '<br />';
321 }
322 }
323 }
324 foreach ($ruleFields as $field => $weight) {
325 $fieldMessage .= ' ' . $field . '(weight ' . $weight . ')';
326 }
327 $errors = self::checkRequiredFields($self, $contactORContributionId, $importKeys, $errors, $weightSum, $threshold, $fieldMessage);
328
329 //at least one field should be mapped during update.
330 if ($self->_onDuplicate == CRM_Import_Parser::DUPLICATE_UPDATE) {
331 $atleastOne = FALSE;
332 foreach ($self->_mapperFields as $key => $field) {
333 if (in_array($key, $importKeys) &&
334 !in_array($key, [
335 'doNotImport',
336 'contribution_id',
337 'invoice_id',
338 'trxn_id',
339 ])
340 ) {
341 $atleastOne = TRUE;
342 break;
343 }
344 }
345 if (!$atleastOne) {
346 $errors['_qf_default'] .= ts('At least one contribution field needs to be mapped for update during update mode.') . '<br />';
347 }
348 }
349 }
350
351 if (!empty($fields['saveMapping'])) {
352 $nameField = $fields['saveMappingName'] ?? NULL;
353 if (empty($nameField)) {
354 $errors['saveMappingName'] = ts('Name is required to save Import Mapping');
355 }
356 else {
357 if (CRM_Core_BAO_Mapping::checkMapping($nameField, CRM_Core_PseudoConstant::getKey('CRM_Core_BAO_Mapping', 'mapping_type_id', 'Import Contribution'))) {
358 $errors['saveMappingName'] = ts('Duplicate Import Contribution Mapping Name');
359 }
360 }
361 }
362
363 if (!empty($errors)) {
364 if (!empty($errors['saveMappingName'])) {
365 $_flag = 1;
366 $assignError = new CRM_Core_Page();
367 $assignError->assign('mappingDetailsError', $_flag);
368 }
369 if (!empty($errors['_qf_default'])) {
370 CRM_Core_Session::setStatus($errors['_qf_default'], ts("Error"), "error");
371 return $errors;
372 }
373 }
374
375 return TRUE;
376 }
377
378 /**
379 * Get the mapping name per the civicrm_mapping_field.type_id option group.
380 *
381 * @return string
382 */
383 public function getMappingTypeName(): string {
384 return 'Import Contribution';
385 }
386
387 /**
388 * @return \CRM_Contribute_Import_Parser_Contribution
389 */
390 protected function getParser(): CRM_Contribute_Import_Parser_Contribution {
391 if (!$this->parser) {
392 $this->parser = new CRM_Contribute_Import_Parser_Contribution();
393 $this->parser->setUserJobID($this->getUserJobID());
394 $this->parser->init();
395 }
396 return $this->parser;
397 }
398
399 }