INFRA-132 comments to end with full stops
[civicrm-core.git] / CRM / Contribute / Import / Parser.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
39de6fd5 4 | CiviCRM version 4.6 |
6a488035 5 +--------------------------------------------------------------------+
06b69b18 6 | Copyright CiviCRM LLC (c) 2004-2014 |
6a488035
TO
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 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
27
28/**
29 *
30 * @package CRM
06b69b18 31 * @copyright CiviCRM LLC (c) 2004-2014
6a488035
TO
32 * $Id$
33 *
34 */
ec3811b1 35abstract class CRM_Contribute_Import_Parser extends CRM_Import_Parser {
6a488035
TO
36
37 /**
ec3811b1
CW
38 * Contribution-specific result codes
39 * @see CRM_Import_Parser result code constants
6a488035 40 */
7da04cde 41 const SOFT_CREDIT = 512, SOFT_CREDIT_ERROR = 1024, PLEDGE_PAYMENT = 2048, PLEDGE_PAYMENT_ERROR = 4096;
6a488035
TO
42
43 protected $_fileName;
44
6a488035 45 /**
100fef9d 46 * Imported file size
6a488035
TO
47 */
48 protected $_fileSize;
49
50 /**
100fef9d 51 * Seperator being used
6a488035
TO
52 */
53 protected $_seperator;
54
55 /**
100fef9d 56 * Total number of lines in file
6a488035
TO
57 */
58 protected $_lineCount;
59
6a488035 60 /**
100fef9d 61 * Running total number of valid soft credit rows
6a488035
TO
62 */
63 protected $_validSoftCreditRowCount;
64
65 /**
100fef9d 66 * Running total number of invalid soft credit rows
6a488035
TO
67 */
68 protected $_invalidSoftCreditRowCount;
69
70 /**
100fef9d 71 * Running total number of valid pledge payment rows
6a488035
TO
72 */
73 protected $_validPledgePaymentRowCount;
74
75 /**
100fef9d 76 * Running total number of invalid pledge payment rows
6a488035
TO
77 */
78 protected $_invalidPledgePaymentRowCount;
79
6a488035 80 /**
100fef9d 81 * Array of pledge payment error lines, bounded by MAX_ERROR
6a488035
TO
82 */
83 protected $_pledgePaymentErrors;
84
85 /**
100fef9d 86 * Array of pledge payment error lines, bounded by MAX_ERROR
6a488035
TO
87 */
88 protected $_softCreditErrors;
89
6a488035 90 /**
100fef9d 91 * Filename of pledge payment error data
6a488035
TO
92 *
93 * @var string
94 */
95 protected $_pledgePaymentErrorsFileName;
96
97 /**
100fef9d 98 * Filename of soft credit error data
6a488035
TO
99 *
100 * @var string
101 */
102 protected $_softCreditErrorsFileName;
103
6a488035 104 /**
100fef9d 105 * Whether the file has a column header or not
6a488035
TO
106 *
107 * @var boolean
108 */
109 protected $_haveColumnHeader;
110
186c9c17 111 /**
100fef9d 112 * @param string $fileName
186c9c17
EM
113 * @param string $seperator
114 * @param $mapper
115 * @param bool $skipColumnHeader
116 * @param int $mode
117 * @param int $contactType
118 * @param int $onDuplicate
119 *
120 * @return mixed
121 * @throws Exception
122 */
317fceb4 123 public function run(
a13f3d8c 124 $fileName,
6a488035
TO
125 $seperator = ',',
126 &$mapper,
127 $skipColumnHeader = FALSE,
52892e8b
CW
128 $mode = self::MODE_PREVIEW,
129 $contactType = self::CONTACT_INDIVIDUAL,
130 $onDuplicate = self::DUPLICATE_SKIP
6a488035
TO
131 ) {
132 if (!is_array($fileName)) {
133 CRM_Core_Error::fatal();
134 }
135 $fileName = $fileName['name'];
136
137 switch ($contactType) {
138 case self::CONTACT_INDIVIDUAL:
139 $this->_contactType = 'Individual';
140 break;
141
142 case self::CONTACT_HOUSEHOLD:
143 $this->_contactType = 'Household';
144 break;
145
146 case self::CONTACT_ORGANIZATION:
147 $this->_contactType = 'Organization';
148 }
149
150 $this->init();
151
152 $this->_haveColumnHeader = $skipColumnHeader;
153
154 $this->_seperator = $seperator;
155
156 $fd = fopen($fileName, "r");
157 if (!$fd) {
158 return FALSE;
159 }
160
161 $this->_lineCount = $this->_warningCount = $this->_validSoftCreditRowCount = $this->_validPledgePaymentRowCount = 0;
162 $this->_invalidRowCount = $this->_validCount = $this->_invalidSoftCreditRowCount = $this->_invalidPledgePaymentRowCount = 0;
163 $this->_totalCount = $this->_conflictCount = 0;
164
165 $this->_errors = array();
166 $this->_warnings = array();
167 $this->_conflicts = array();
168 $this->_pledgePaymentErrors = array();
169 $this->_softCreditErrors = array();
170
171 $this->_fileSize = number_format(filesize($fileName) / 1024.0, 2);
172
173 if ($mode == self::MODE_MAPFIELD) {
174 $this->_rows = array();
175 }
176 else {
177 $this->_activeFieldCount = count($this->_activeFields);
178 }
179
180 while (!feof($fd)) {
181 $this->_lineCount++;
182
183 $values = fgetcsv($fd, 8192, $seperator);
184 if (!$values) {
185 continue;
186 }
187
188 self::encloseScrub($values);
189
190 // skip column header if we're not in mapfield mode
191 if ($mode != self::MODE_MAPFIELD && $skipColumnHeader) {
192 $skipColumnHeader = FALSE;
193 continue;
194 }
195
196 /* trim whitespace around the values */
197
198 $empty = TRUE;
199 foreach ($values as $k => $v) {
200 $values[$k] = trim($v, " \t\r\n");
201 }
202
203 if (CRM_Utils_System::isNull($values)) {
204 continue;
205 }
206
207 $this->_totalCount++;
208
209 if ($mode == self::MODE_MAPFIELD) {
210 $returnCode = $this->mapField($values);
211 }
212 elseif ($mode == self::MODE_PREVIEW) {
213 $returnCode = $this->preview($values);
214 }
215 elseif ($mode == self::MODE_SUMMARY) {
216 $returnCode = $this->summary($values);
217 }
218 elseif ($mode == self::MODE_IMPORT) {
219 $returnCode = $this->import($onDuplicate, $values);
220 }
221 else {
222 $returnCode = self::ERROR;
223 }
224
225 // note that a line could be valid but still produce a warning
226 if ($returnCode == self::VALID) {
227 $this->_validCount++;
228 if ($mode == self::MODE_MAPFIELD) {
229 $this->_rows[] = $values;
230 $this->_activeFieldCount = max($this->_activeFieldCount, count($values));
231 }
232 }
233
234 if ($returnCode == self::SOFT_CREDIT) {
235 $this->_validSoftCreditRowCount++;
236 $this->_validCount++;
237 if ($mode == self::MODE_MAPFIELD) {
238 $this->_rows[] = $values;
239 $this->_activeFieldCount = max($this->_activeFieldCount, count($values));
240 }
241 }
242
243 if ($returnCode == self::PLEDGE_PAYMENT) {
244 $this->_validPledgePaymentRowCount++;
245 $this->_validCount++;
246 if ($mode == self::MODE_MAPFIELD) {
247 $this->_rows[] = $values;
248 $this->_activeFieldCount = max($this->_activeFieldCount, count($values));
249 }
250 }
251
252 if ($returnCode == self::WARNING) {
253 $this->_warningCount++;
254 if ($this->_warningCount < $this->_maxWarningCount) {
255 $this->_warningCount[] = $line;
256 }
257 }
258
259 if ($returnCode == self::ERROR) {
260 $this->_invalidRowCount++;
261 if ($this->_invalidRowCount < $this->_maxErrorCount) {
262 $recordNumber = $this->_lineCount;
263 if ($this->_haveColumnHeader) {
264 $recordNumber--;
265 }
266 array_unshift($values, $recordNumber);
267 $this->_errors[] = $values;
268 }
269 }
270
271 if ($returnCode == self::PLEDGE_PAYMENT_ERROR) {
272 $this->_invalidPledgePaymentRowCount++;
273 if ($this->_invalidPledgePaymentRowCount < $this->_maxErrorCount) {
274 $recordNumber = $this->_lineCount;
275 if ($this->_haveColumnHeader) {
276 $recordNumber--;
277 }
278 array_unshift($values, $recordNumber);
279 $this->_pledgePaymentErrors[] = $values;
280 }
281 }
282
283 if ($returnCode == self::SOFT_CREDIT_ERROR) {
284 $this->_invalidSoftCreditRowCount++;
285 if ($this->_invalidSoftCreditRowCount < $this->_maxErrorCount) {
286 $recordNumber = $this->_lineCount;
287 if ($this->_haveColumnHeader) {
288 $recordNumber--;
289 }
290 array_unshift($values, $recordNumber);
291 $this->_softCreditErrors[] = $values;
292 }
293 }
294
295 if ($returnCode == self::CONFLICT) {
296 $this->_conflictCount++;
297 $recordNumber = $this->_lineCount;
298 if ($this->_haveColumnHeader) {
299 $recordNumber--;
300 }
301 array_unshift($values, $recordNumber);
302 $this->_conflicts[] = $values;
303 }
304
305 if ($returnCode == self::DUPLICATE) {
306 if ($returnCode == self::MULTIPLE_DUPE) {
307 /* TODO: multi-dupes should be counted apart from singles
006389de 308 * on non-skip action */
6a488035
TO
309 }
310 $this->_duplicateCount++;
311 $recordNumber = $this->_lineCount;
312 if ($this->_haveColumnHeader) {
313 $recordNumber--;
314 }
315 array_unshift($values, $recordNumber);
316 $this->_duplicates[] = $values;
317 if ($onDuplicate != self::DUPLICATE_SKIP) {
318 $this->_validCount++;
319 }
320 }
321
322 // we give the derived class a way of aborting the process
323 // note that the return code could be multiple code or'ed together
324 if ($returnCode == self::STOP) {
325 break;
326 }
327
328 // if we are done processing the maxNumber of lines, break
329 if ($this->_maxLinesToProcess > 0 && $this->_validCount >= $this->_maxLinesToProcess) {
330 break;
331 }
332 }
333
334 fclose($fd);
335
336 if ($mode == self::MODE_PREVIEW || $mode == self::MODE_IMPORT) {
337 $customHeaders = $mapper;
338
339 $customfields = CRM_Core_BAO_CustomField::getFields('Contribution');
340 foreach ($customHeaders as $key => $value) {
341 if ($id = CRM_Core_BAO_CustomField::getKeyID($value)) {
342 $customHeaders[$key] = $customfields[$id][0];
343 }
344 }
345 if ($this->_invalidRowCount) {
346 // removed view url for invlaid contacts
353ffa53
TO
347 $headers = array_merge(array(
348 ts('Line Number'),
6a488035
TO
349 ts('Reason'),
350 ),
351 $customHeaders
352 );
353 $this->_errorFileName = self::errorFileName(self::ERROR);
354 self::exportCSV($this->_errorFileName, $headers, $this->_errors);
355 }
356
357 if ($this->_invalidPledgePaymentRowCount) {
358 // removed view url for invlaid contacts
353ffa53
TO
359 $headers = array_merge(array(
360 ts('Line Number'),
6a488035
TO
361 ts('Reason'),
362 ),
363 $customHeaders
364 );
365 $this->_pledgePaymentErrorsFileName = self::errorFileName(self::PLEDGE_PAYMENT_ERROR);
366 self::exportCSV($this->_pledgePaymentErrorsFileName, $headers, $this->_pledgePaymentErrors);
367 }
368
369 if ($this->_invalidSoftCreditRowCount) {
370 // removed view url for invlaid contacts
353ffa53
TO
371 $headers = array_merge(array(
372 ts('Line Number'),
6a488035
TO
373 ts('Reason'),
374 ),
375 $customHeaders
376 );
377 $this->_softCreditErrorsFileName = self::errorFileName(self::SOFT_CREDIT_ERROR);
378 self::exportCSV($this->_softCreditErrorsFileName, $headers, $this->_softCreditErrors);
379 }
380
381 if ($this->_conflictCount) {
353ffa53
TO
382 $headers = array_merge(array(
383 ts('Line Number'),
6a488035
TO
384 ts('Reason'),
385 ),
386 $customHeaders
387 );
388 $this->_conflictFileName = self::errorFileName(self::CONFLICT);
389 self::exportCSV($this->_conflictFileName, $headers, $this->_conflicts);
390 }
391 if ($this->_duplicateCount) {
353ffa53
TO
392 $headers = array_merge(array(
393 ts('Line Number'),
6a488035
TO
394 ts('View Contribution URL'),
395 ),
396 $customHeaders
397 );
398
399 $this->_duplicateFileName = self::errorFileName(self::DUPLICATE);
400 self::exportCSV($this->_duplicateFileName, $headers, $this->_duplicates);
401 }
402 }
6a488035
TO
403 return $this->fini();
404 }
405
6a488035
TO
406 /**
407 * Given a list of the importable field keys that the user has selected
408 * set the active fields array to this list
409 *
317fceb4 410 * @param array $fieldKeys mapped array of values
6a488035 411 *
c490a46a 412 * @return void
6a488035 413 */
00be9182 414 public function setActiveFields($fieldKeys) {
6a488035
TO
415 $this->_activeFieldCount = count($fieldKeys);
416 foreach ($fieldKeys as $key) {
417 if (empty($this->_fields[$key])) {
418 $this->_activeFields[] = new CRM_Contribute_Import_Field('', ts('- do not import -'));
419 }
420 else {
421 $this->_activeFields[] = clone($this->_fields[$key]);
422 }
423 }
424 }
425
186c9c17 426 /**
c490a46a 427 * @param array $elements
186c9c17 428 */
00be9182 429 public function setActiveFieldSoftCredit($elements) {
6a488035
TO
430 for ($i = 0; $i < count($elements); $i++) {
431 $this->_activeFields[$i]->_softCreditField = $elements[$i];
432 }
433 }
434
186c9c17 435 /**
c490a46a 436 * @param array $elements
186c9c17 437 */
00be9182 438 public function setActiveFieldSoftCreditType($elements) {
1221efe9 439 for ($i = 0; $i < count($elements); $i++) {
440 $this->_activeFields[$i]->_softCreditType = $elements[$i];
441 }
442 }
c490a46a 443
6a488035 444 /**
fe482240 445 * Format the field values for input to the api.
6a488035 446 *
a6c01b45
CW
447 * @return array
448 * (reference ) associative array of name/value pairs
6a488035 449 */
00be9182 450 public function &getActiveFieldParams() {
6a488035
TO
451 $params = array();
452 for ($i = 0; $i < $this->_activeFieldCount; $i++) {
453 if (isset($this->_activeFields[$i]->_value)) {
454 if (isset($this->_activeFields[$i]->_softCreditField)) {
455 if (!isset($params[$this->_activeFields[$i]->_name])) {
456 $params[$this->_activeFields[$i]->_name] = array();
457 }
1221efe9 458 $params[$this->_activeFields[$i]->_name][$i][$this->_activeFields[$i]->_softCreditField] = $this->_activeFields[$i]->_value;
9b873358 459 if (isset($this->_activeFields[$i]->_softCreditType)) {
1221efe9 460 $params[$this->_activeFields[$i]->_name][$i]['soft_credit_type_id'] = $this->_activeFields[$i]->_softCreditType;
461 }
6a488035
TO
462 }
463
464 if (!isset($params[$this->_activeFields[$i]->_name])) {
465 if (!isset($this->_activeFields[$i]->_softCreditField)) {
466 $params[$this->_activeFields[$i]->_name] = $this->_activeFields[$i]->_value;
467 }
468 }
469 }
470 }
471 return $params;
472 }
473
186c9c17 474 /**
100fef9d 475 * @param string $name
186c9c17
EM
476 * @param $title
477 * @param int $type
478 * @param string $headerPattern
479 * @param string $dataPattern
480 */
00be9182 481 public function addField($name, $title, $type = CRM_Utils_Type::T_INT, $headerPattern = '//', $dataPattern = '//') {
6a488035
TO
482 if (empty($name)) {
483 $this->_fields['doNotImport'] = new CRM_Contribute_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
484 }
485 else {
486 $tempField = CRM_Contact_BAO_Contact::importableFields('All', NULL);
487 if (!array_key_exists($name, $tempField)) {
488 $this->_fields[$name] = new CRM_Contribute_Import_Field($name, $title, $type, $headerPattern, $dataPattern);
489 }
490 else {
719a6fec 491 $this->_fields[$name] = new CRM_Contact_Import_Field($name, $title, $type, $headerPattern, $dataPattern,
6a488035
TO
492 CRM_Utils_Array::value('hasLocationType', $tempField[$name])
493 );
494 }
495 }
496 }
497
6a488035 498 /**
fe482240 499 * Store parser values.
6a488035
TO
500 *
501 * @param CRM_Core_Session $store
502 *
2a6da8d7 503 * @param int $mode
6a488035 504 */
00be9182 505 public function set($store, $mode = self::MODE_SUMMARY) {
6a488035
TO
506 $store->set('fileSize', $this->_fileSize);
507 $store->set('lineCount', $this->_lineCount);
508 $store->set('seperator', $this->_seperator);
509 $store->set('fields', $this->getSelectValues());
510 $store->set('fieldTypes', $this->getSelectTypes());
511
512 $store->set('headerPatterns', $this->getHeaderPatterns());
513 $store->set('dataPatterns', $this->getDataPatterns());
514 $store->set('columnCount', $this->_activeFieldCount);
515
516 $store->set('totalRowCount', $this->_totalCount);
517 $store->set('validRowCount', $this->_validCount);
518 $store->set('invalidRowCount', $this->_invalidRowCount);
519 $store->set('invalidSoftCreditRowCount', $this->_invalidSoftCreditRowCount);
520 $store->set('validSoftCreditRowCount', $this->_validSoftCreditRowCount);
521 $store->set('invalidPledgePaymentRowCount', $this->_invalidPledgePaymentRowCount);
522 $store->set('validPledgePaymentRowCount', $this->_validPledgePaymentRowCount);
523 $store->set('conflictRowCount', $this->_conflictCount);
524
525 switch ($this->_contactType) {
526 case 'Individual':
a05662ef 527 $store->set('contactType', CRM_Import_Parser::CONTACT_INDIVIDUAL);
6a488035
TO
528 break;
529
530 case 'Household':
a05662ef 531 $store->set('contactType', CRM_Import_Parser::CONTACT_HOUSEHOLD);
6a488035
TO
532 break;
533
534 case 'Organization':
a05662ef 535 $store->set('contactType', CRM_Import_Parser::CONTACT_ORGANIZATION);
6a488035
TO
536 }
537
538 if ($this->_invalidRowCount) {
539 $store->set('errorsFileName', $this->_errorFileName);
540 }
541 if ($this->_conflictCount) {
542 $store->set('conflictsFileName', $this->_conflictFileName);
543 }
544 if (isset($this->_rows) && !empty($this->_rows)) {
545 $store->set('dataValues', $this->_rows);
546 }
547
548 if ($this->_invalidPledgePaymentRowCount) {
549 $store->set('pledgePaymentErrorsFileName', $this->_pledgePaymentErrorsFileName);
550 }
551
552 if ($this->_invalidSoftCreditRowCount) {
553 $store->set('softCreditErrorsFileName', $this->_softCreditErrorsFileName);
554 }
555
556 if ($mode == self::MODE_IMPORT) {
557 $store->set('duplicateRowCount', $this->_duplicateCount);
558 if ($this->_duplicateCount) {
559 $store->set('duplicatesFileName', $this->_duplicateFileName);
560 }
561 }
6a488035
TO
562 }
563
564 /**
fe482240 565 * Export data to a CSV file.
6a488035 566 *
c490a46a 567 * @param string $fileName
6a488035 568 * @param array $header
c490a46a 569 * @param array $data
6a488035
TO
570 *
571 * @return void
6a488035 572 */
00be9182 573 public static function exportCSV($fileName, $header, $data) {
6a488035
TO
574 $output = array();
575 $fd = fopen($fileName, 'w');
576
577 foreach ($header as $key => $value) {
578 $header[$key] = "\"$value\"";
579 }
580 $config = CRM_Core_Config::singleton();
581 $output[] = implode($config->fieldSeparator, $header);
582
583 foreach ($data as $datum) {
584 foreach ($datum as $key => $value) {
1221efe9 585 if (isset($value[0]) && is_array($value)) {
6a488035
TO
586 foreach ($value[0] as $k1 => $v1) {
587 if ($k1 == 'location_type_id') {
588 continue;
589 }
590 $datum[$k1] = $v1;
591 }
592 }
593 else {
594 $datum[$key] = "\"$value\"";
595 }
596 }
597 $output[] = implode($config->fieldSeparator, $datum);
598 }
599 fwrite($fd, implode("\n", $output));
600 fclose($fd);
601 }
602
186c9c17 603 /**
fe482240 604 * Determines the file extension based on error code.
186c9c17 605 *
014c4014
TO
606 * @param int $type
607 * Error code constant.
186c9c17
EM
608 *
609 * @return string
610 */
00be9182 611 public static function errorFileName($type) {
6a488035
TO
612 $fileName = NULL;
613 if (empty($type)) {
614 return $fileName;
615 }
616
617 $config = CRM_Core_Config::singleton();
618 $fileName = $config->uploadDir . "sqlImport";
619
620 switch ($type) {
6a488035
TO
621 case CRM_Contribute_Import_Parser::SOFT_CREDIT_ERROR:
622 $fileName .= '.softCreditErrors';
623 break;
624
625 case CRM_Contribute_Import_Parser::PLEDGE_PAYMENT_ERROR:
626 $fileName .= '.pledgePaymentErrors';
627 break;
69a4c20a
CW
628
629 default:
630 $fileName = parent::errorFileName($type);
631 break;
6a488035
TO
632 }
633
634 return $fileName;
635 }
636
186c9c17 637 /**
fe482240 638 * Determines the file name based on error code.
186c9c17 639 *
014c4014
TO
640 * @param int $type
641 * Error code constant.
186c9c17
EM
642 *
643 * @return string
644 */
00be9182 645 public static function saveFileName($type) {
6a488035
TO
646 $fileName = NULL;
647 if (empty($type)) {
648 return $fileName;
649 }
650
651 switch ($type) {
6a488035
TO
652 case CRM_Contribute_Import_Parser::SOFT_CREDIT_ERROR:
653 $fileName = 'Import_Soft_Credit_Errors.csv';
654 break;
655
656 case CRM_Contribute_Import_Parser::PLEDGE_PAYMENT_ERROR:
657 $fileName = 'Import_Pledge_Payment_Errors.csv';
658 break;
69a4c20a
CW
659
660 default:
661 $fileName = parent::saveFileName($type);
662 break;
6a488035
TO
663 }
664
665 return $fileName;
666 }
96025800 667
6a488035 668}