LineItem.php 53.2 KB
Newer Older
totten's avatar
totten committed
1 2 3
<?php
/*
 +--------------------------------------------------------------------+
Kurund Jalmi's avatar
Kurund Jalmi committed
4
 | CiviCRM version 4.7                                                |
totten's avatar
totten committed
5
 +--------------------------------------------------------------------+
yashodha's avatar
yashodha committed
6
 | Copyright CiviCRM LLC (c) 2004-2017                                |
totten's avatar
totten committed
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
 +--------------------------------------------------------------------+
 | This file is a part of CiviCRM.                                    |
 |                                                                    |
 | CiviCRM is free software; you can copy, modify, and distribute it  |
 | under the terms of the GNU Affero General Public License           |
 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
 |                                                                    |
 | CiviCRM is distributed in the hope that it will be useful, but     |
 | WITHOUT ANY WARRANTY; without even the implied warranty of         |
 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
 | See the GNU Affero General Public License for more details.        |
 |                                                                    |
 | You should have received a copy of the GNU Affero General Public   |
 | License and the CiviCRM Licensing Exception along                  |
 | with this program; if not, contact CiviCRM LLC                     |
 | at info[AT]civicrm[DOT]org. If you have questions about the        |
 | GNU Affero General Public License or the licensing of CiviCRM,     |
 | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
 +--------------------------------------------------------------------+
26
 */
totten's avatar
totten committed
27 28 29 30

/**
 *
 * @package CRM
yashodha's avatar
yashodha committed
31
 * @copyright CiviCRM LLC (c) 2004-2017
totten's avatar
totten committed
32 33 34 35 36 37 38 39 40 41
 */

/**
 * Business objects for Line Items generated by monetary transactions
 */
class CRM_Price_BAO_LineItem extends CRM_Price_DAO_LineItem {

  /**
   * Creates a new entry in the database.
   *
42 43
   * @param array $params
   *   (reference) an assoc array of name/value pairs.
totten's avatar
totten committed
44
   *
45 46 47 48
   * @return \CRM_Price_DAO_LineItem
   *
   * @throws \CiviCRM_API3_Exception
   * @throws \Exception
totten's avatar
totten committed
49
   */
50
  public static function create(&$params) {
51 52 53
    $id = CRM_Utils_Array::value('id', $params);
    if ($id) {
      CRM_Utils_Hook::pre('edit', 'LineItem', $id, $params);
54
      $op = CRM_Core_Action::UPDATE;
55 56 57
    }
    else {
      CRM_Utils_Hook::pre('create', 'LineItem', $params['entity_id'], $params);
58
      $op = CRM_Core_Action::ADD;
59
    }
60

61 62 63
    // unset entity table and entity id in $params
    // we never update the entity table and entity id during update mode
    if ($id) {
cividesk's avatar
cividesk committed
64 65
      $entity_id = CRM_Utils_Array::value('entity_id', $params);
      $entity_table = CRM_Utils_Array::value('entity_table', $params);
66 67
      unset($params['entity_id'], $params['entity_table']);
    }
68 69 70 71 72
    else {
      if (!isset($params['unit_price'])) {
        $params['unit_price'] = 0;
      }
    }
73
    if (CRM_Financial_BAO_FinancialType::isACLFinancialTypeStatus() && CRM_Utils_Array::value('check_permissions', $params)) {
74 75 76
      if (empty($params['financial_type_id'])) {
        throw new Exception('Mandatory key(s) missing from params array: financial_type_id');
      }
77 78
      CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes($types, $op);
      if (!in_array($params['financial_type_id'], array_keys($types))) {
79
        throw new Exception('You do not have permission to create this line item');
80 81
      }
    }
eileen's avatar
eileen committed
82

totten's avatar
totten committed
83 84 85 86
    $lineItemBAO = new CRM_Price_BAO_LineItem();
    $lineItemBAO->copyValues($params);

    $return = $lineItemBAO->save();
87 88 89 90 91 92 93 94 95
    if ($lineItemBAO->entity_table == 'civicrm_membership' && $lineItemBAO->contribution_id && $lineItemBAO->entity_id) {
      $membershipPaymentParams = array(
        'membership_id' => $lineItemBAO->entity_id,
        'contribution_id' => $lineItemBAO->contribution_id,
      );
      if (!civicrm_api3('MembershipPayment', 'getcount', $membershipPaymentParams)) {
        civicrm_api3('MembershipPayment', 'create', $membershipPaymentParams);
      }
    }
totten's avatar
totten committed
96

97
    if ($id) {
98 99 100
      // CRM-21281: Restore entity reference in case the post hook needs it
      $lineItemBAO->entity_id = $entity_id;
      $lineItemBAO->entity_table = $entity_table;
101
      CRM_Utils_Hook::post('edit', 'LineItem', $id, $lineItemBAO);
102 103
    }
    else {
104
      CRM_Utils_Hook::post('create', 'LineItem', $lineItemBAO->id, $lineItemBAO);
105
    }
totten's avatar
totten committed
106 107 108 109 110

    return $return;
  }

  /**
111 112 113
   * Retrieve DB object based on input parameters.
   *
   * It also stores all the retrieved values in the default array.
totten's avatar
totten committed
114
   *
115 116 117 118
   * @param array $params
   *   (reference ) an assoc array of name/value pairs.
   * @param array $defaults
   *   (reference ) an assoc array to hold the flattened values.
totten's avatar
totten committed
119
   *
120
   * @return CRM_Price_BAO_LineItem
totten's avatar
totten committed
121
   */
122
  public static function retrieve(&$params, &$defaults) {
totten's avatar
totten committed
123 124 125 126 127 128 129 130 131
    $lineItem = new CRM_Price_BAO_LineItem();
    $lineItem->copyValues($params);
    if ($lineItem->find(TRUE)) {
      CRM_Core_DAO::storeValues($lineItem, $defaults);
      return $lineItem;
    }
    return NULL;
  }

132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
  /**
   * Modifies $params array for filtering financial types.
   *
   * @param array $params
   *   (reference ) an assoc array of name/value pairs.
   *
   */
  public static function getAPILineItemParams(&$params) {
    CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes($types);
    if ($types && empty($params['financial_type_id'])) {
      $params['financial_type_id'] = array('IN' => array_keys($types));
    }
    elseif ($types) {
      if (is_array($params['financial_type_id'])) {
        $invalidFts = array_diff($params['financial_type_id'], array_keys($types));
      }
      elseif (!in_array($params['financial_type_id'], array_keys($types))) {
        $invalidFts = $params['financial_type_id'];
      }
      if ($invalidFts) {
        $params['financial_type_id'] = array('NOT IN' => $invalidFts);
      }
    }
    else {
      $params['financial_type_id'] = 0;
    }
  }

160
  /**
161
   * @param int $contributionId
162 163 164
   *
   * @return null|string
   */
165
  public static function getLineTotal($contributionId) {
166
    $sqlLineItemTotal = "SELECT SUM(li.line_total + COALESCE(li.tax_amount,0))
167
FROM civicrm_line_item li
168 169 170
WHERE li.contribution_id = %1";
    $params = array(1 => array($contributionId, 'Integer'));
    $lineItemTotal = CRM_Core_DAO::singleValueQuery($sqlLineItemTotal, $params);
171 172 173
    return $lineItemTotal;
  }

174
  /**
175
   * Wrapper for line item retrieval when contribution ID is known.
colemanw's avatar
colemanw committed
176
   * @param int $contributionID
177 178 179
   *
   * @return array
   */
180
  public static function getLineItemsByContributionID($contributionID) {
totten's avatar
totten committed
181
    return self::getLineItems($contributionID, 'contribution', NULL, TRUE, TRUE, " WHERE contribution_id = " . (int) $contributionID);
182 183
  }

totten's avatar
totten committed
184 185 186 187
  /**
   * Given a participant id/contribution id,
   * return contribution/fee line items
   *
colemanw's avatar
colemanw committed
188 189 190 191
   * @param int $entityId
   *   participant/contribution id.
   * @param string $entity
   *   participant/contribution.
totten's avatar
totten committed
192
   *
193
   * @param bool $isQuick
Eileen McNaughton's avatar
Eileen McNaughton committed
194
   * @param bool $isQtyZero
195
   * @param bool $relatedEntity
Eileen McNaughton's avatar
Eileen McNaughton committed
196
   *
Web Access's avatar
Web Access committed
197
   * @param bool $invoice
198
   * @return array
199
   *   Array of line items
totten's avatar
totten committed
200
   */
201
  public static function getLineItems($entityId, $entity = 'participant', $isQuick = FALSE, $isQtyZero = TRUE, $relatedEntity = FALSE, $invoice = FALSE) {
202
    $whereClause = $fromClause = NULL;
totten's avatar
totten committed
203 204 205
    $selectClause = "
      SELECT    li.id,
      li.label,
206
      li.contribution_id,
totten's avatar
totten committed
207 208 209
      li.qty,
      li.unit_price,
      li.line_total,
210 211
      li.entity_table,
      li.entity_id,
totten's avatar
totten committed
212 213
      pf.label as field_title,
      pf.html_type,
214
      pf.price_set_id,
totten's avatar
totten committed
215
      pfv.membership_type_id,
216
      pfv.membership_num_terms,
totten's avatar
totten committed
217 218 219
      li.price_field_id,
      li.participant_count,
      li.price_field_value_id,
Pradeep Nayak's avatar
Pradeep Nayak committed
220
      li.financial_type_id,
221
      li.tax_amount,
totten's avatar
totten committed
222 223
      pfv.description";

224 225 226 227 228
    $condition = "li.entity_id = %2.id AND li.entity_table = 'civicrm_%2'";
    if ($relatedEntity) {
      $condition = "li.contribution_id = %2.id ";
    }

totten's avatar
totten committed
229 230
    $fromClause = "
      FROM      civicrm_%2 as %2
231
      LEFT JOIN civicrm_line_item li ON ({$condition})
totten's avatar
totten committed
232 233 234 235 236
      LEFT JOIN civicrm_price_field_value pfv ON ( pfv.id = li.price_field_value_id )
      LEFT JOIN civicrm_price_field pf ON (pf.id = li.price_field_id )";
    $whereClause = "
      WHERE     %2.id = %1";

Web Access's avatar
Web Access committed
237 238
    // CRM-16250 get additional participant's fee selection details only for invoice PDF (if any)
    if ($entity == 'participant' && $invoice) {
239 240
      $additionalParticipantIDs = CRM_Event_BAO_Participant::getAdditionalParticipantIds($entityId);
      if (!empty($additionalParticipantIDs)) {
jitendrapurohit's avatar
jitendrapurohit committed
241
        $whereClause = "WHERE %2.id IN (%1, " . implode(', ', $additionalParticipantIDs) . ")";
242 243 244
      }
    }

245 246
    $orderByClause = " ORDER BY pf.weight, pfv.weight";

totten's avatar
totten committed
247 248 249 250
    if ($isQuick) {
      $fromClause .= " LEFT JOIN civicrm_price_set cps on cps.id = pf.price_set_id ";
      $whereClause .= " and cps.is_quick_config = 0";
    }
251 252 253 254 255

    if (!$isQtyZero) {
      $whereClause .= " and li.qty != 0";
    }

totten's avatar
totten committed
256 257 258 259 260 261 262 263 264 265 266
    $lineItems = array();

    if (!$entityId || !$entity || !$fromClause) {
      return $lineItems;
    }

    $params = array(
      1 => array($entityId, 'Integer'),
      2 => array($entity, 'Text'),
    );

267
    $getTaxDetails = FALSE;
268
    $invoiceSettings = Civi::settings()->get('contribution_invoice_settings');
269
    $invoicing = CRM_Utils_Array::value('invoicing', $invoiceSettings);
270

271
    $dao = CRM_Core_DAO::executeQuery("$selectClause $fromClause $whereClause $orderByClause", $params);
totten's avatar
totten committed
272 273 274 275 276
    while ($dao->fetch()) {
      if (!$dao->id) {
        continue;
      }
      $lineItems[$dao->id] = array(
jitendrapurohit's avatar
jitendrapurohit committed
277
        'qty' => (float) $dao->qty,
totten's avatar
totten committed
278 279 280 281 282 283 284 285 286
        'label' => $dao->label,
        'unit_price' => $dao->unit_price,
        'line_total' => $dao->line_total,
        'price_field_id' => $dao->price_field_id,
        'participant_count' => $dao->participant_count,
        'price_field_value_id' => $dao->price_field_value_id,
        'field_title' => $dao->field_title,
        'html_type' => $dao->html_type,
        'description' => $dao->description,
287
        'entity_id' => $dao->entity_id,
288
        'entity_table' => $dao->entity_table,
289
        'contribution_id' => $dao->contribution_id,
Pradeep Nayak's avatar
Pradeep Nayak committed
290
        'financial_type_id' => $dao->financial_type_id,
291
        'financial_type' => CRM_Core_PseudoConstant::getLabel('CRM_Contribute_BAO_Contribution', 'financial_type_id', $dao->financial_type_id),
totten's avatar
totten committed
292
        'membership_type_id' => $dao->membership_type_id,
293
        'membership_num_terms' => $dao->membership_num_terms,
294
        'tax_amount' => $dao->tax_amount,
295
        'price_set_id' => $dao->price_set_id,
totten's avatar
totten committed
296
      );
297 298
      $taxRates = CRM_Core_PseudoConstant::getTaxRates();
      if (isset($lineItems[$dao->id]['financial_type_id']) && array_key_exists($lineItems[$dao->id]['financial_type_id'], $taxRates)) {
299 300
        // Cast to float so trailing zero decimals are removed for display.
        $lineItems[$dao->id]['tax_rate'] = (float) $taxRates[$lineItems[$dao->id]['financial_type_id']];
301 302 303 304 305
      }
      else {
        // There is no Tax Rate associated with this Financial Type
        $lineItems[$dao->id]['tax_rate'] = FALSE;
      }
306
      $lineItems[$dao->id]['subTotal'] = $lineItems[$dao->id]['qty'] * $lineItems[$dao->id]['unit_price'];
307 308 309
      if ($lineItems[$dao->id]['tax_amount'] != '') {
        $getTaxDetails = TRUE;
      }
totten's avatar
totten committed
310
    }
311
    if ($invoicing) {
eileen's avatar
eileen committed
312
      // @todo - this is an inappropriate place to be doing form level assignments.
313 314 315 316 317
      $taxTerm = CRM_Utils_Array::value('tax_term', $invoiceSettings);
      $smarty = CRM_Core_Smarty::singleton();
      $smarty->assign('taxTerm', $taxTerm);
      $smarty->assign('getTaxDetails', $getTaxDetails);
    }
totten's avatar
totten committed
318 319 320 321 322 323 324
    return $lineItems;
  }

  /**
   * This method will create the lineItem array required for
   * processAmount method
   *
325 326 327 328 329
   * @param int $fid
   *   Price set field id.
   * @param array $params
   *   Reference to form values.
   * @param array $fields
330
   *   Array of fields belonging to the price set used for particular event
331 332
   * @param array $values
   *   Reference to the values array(.
333
   *   this is
totten's avatar
totten committed
334
   *                          lineItem array)
335
   * @param string $amount_override
336 337 338 339 340
   *   Amount override must be in format 1000.00 - ie no thousand separator & if
   *   a decimal point is used it should be a decimal
   *
   * @todo - this parameter is only used for partial payments. It's unclear why a partial
   *  payment would change the line item price.
totten's avatar
totten committed
341
   */
342
  public static function format($fid, $params, $fields, &$values, $amount_override = NULL) {
totten's avatar
totten committed
343 344 345 346 347 348 349 350 351 352 353
    if (empty($params["price_{$fid}"])) {
      return;
    }

    //lets first check in fun parameter,
    //since user might modified w/ hooks.
    $options = array();
    if (array_key_exists('options', $fields)) {
      $options = $fields['options'];
    }
    else {
354
      CRM_Price_BAO_PriceFieldValue::getValues($fid, $options, 'weight', TRUE);
totten's avatar
totten committed
355 356 357
    }
    $fieldTitle = CRM_Utils_Array::value('label', $fields);
    if (!$fieldTitle) {
358
      $fieldTitle = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceField', $fid, 'label');
totten's avatar
totten committed
359 360 361
    }

    foreach ($params["price_{$fid}"] as $oid => $qty) {
362
      $price = $amount_override === NULL ? $options[$oid]['amount'] : $amount_override;
totten's avatar
totten committed
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380

      $participantsPerField = CRM_Utils_Array::value('count', $options[$oid], 0);

      $values[$oid] = array(
        'price_field_id' => $fid,
        'price_field_value_id' => $oid,
        'label' => CRM_Utils_Array::value('label', $options[$oid]),
        'field_title' => $fieldTitle,
        'description' => CRM_Utils_Array::value('description', $options[$oid]),
        'qty' => $qty,
        'unit_price' => $price,
        'line_total' => $qty * $price,
        'participant_count' => $qty * $participantsPerField,
        'max_value' => CRM_Utils_Array::value('max_value', $options[$oid]),
        'membership_type_id' => CRM_Utils_Array::value('membership_type_id', $options[$oid]),
        'membership_num_terms' => CRM_Utils_Array::value('membership_num_terms', $options[$oid]),
        'auto_renew' => CRM_Utils_Array::value('auto_renew', $options[$oid]),
        'html_type' => $fields['html_type'],
Pradeep Nayak's avatar
Pradeep Nayak committed
381
        'financial_type_id' => CRM_Utils_Array::value('financial_type_id', $options[$oid]),
382
        'tax_amount' => CRM_Utils_Array::value('tax_amount', $options[$oid]),
383
        'non_deductible_amount' => CRM_Utils_Array::value('non_deductible_amount', $options[$oid]),
totten's avatar
totten committed
384
      );
385

monishdeb's avatar
monishdeb committed
386
      if ($values[$oid]['membership_type_id'] && empty($values[$oid]['auto_renew'])) {
eileen's avatar
eileen committed
387
        $values[$oid]['auto_renew'] = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipType', $values[$oid]['membership_type_id'], 'auto_renew');
totten's avatar
totten committed
388 389 390 391 392 393 394 395 396 397
      }
    }
  }

  /**
   * Delete line items for given entity.
   *
   * @param int $entityId
   * @param int $entityTable
   *
398
   * @return bool
totten's avatar
totten committed
399
   */
eileen's avatar
eileen committed
400
  public static function deleteLineItems($entityId, $entityTable) {
totten's avatar
totten committed
401
    if (!$entityId || !$entityTable) {
402
      return FALSE;
totten's avatar
totten committed
403 404 405 406 407 408
    }

    if ($entityId && !is_array($entityId)) {
      $entityId = array($entityId);
    }

Pradeep Nayak's avatar
Pradeep Nayak committed
409
    $query = "DELETE FROM civicrm_line_item where entity_id IN ('" . implode("','", $entityId) . "') AND entity_table = '$entityTable'";
totten's avatar
totten committed
410
    $dao = CRM_Core_DAO::executeQuery($query);
411
    return TRUE;
totten's avatar
totten committed
412 413 414
  }

  /**
colemanw's avatar
colemanw committed
415
   * Process price set and line items.
Eileen McNaughton's avatar
Eileen McNaughton committed
416
   *
colemanw's avatar
colemanw committed
417
   * @param int $entityId
418 419
   * @param array $lineItem
   *   Line item array.
totten's avatar
totten committed
420
   * @param object $contributionDetails
421 422
   * @param string $entityTable
   *   Entity table.
totten's avatar
totten committed
423
   *
Eileen McNaughton's avatar
Eileen McNaughton committed
424 425
   * @param bool $update
   *
totten's avatar
totten committed
426 427
   * @return void
   */
428
  public static function processPriceSet($entityId, $lineItem, $contributionDetails = NULL, $entityTable = 'civicrm_contribution', $update = FALSE) {
totten's avatar
totten committed
429 430 431 432 433
    if (!$entityId || !is_array($lineItem)
      || CRM_Utils_system::isNull($lineItem)
    ) {
      return;
    }
eileen's avatar
eileen committed
434

435
    foreach ($lineItem as $priceSetId => &$values) {
totten's avatar
totten committed
436 437 438 439
      if (!$priceSetId) {
        continue;
      }

440
      foreach ($values as &$line) {
441 442 443
        if (empty($line['entity_table'])) {
          $line['entity_table'] = $entityTable;
        }
444 445 446
        if (empty($line['entity_id'])) {
          $line['entity_id'] = $entityId;
        }
447
        if (!empty($line['membership_type_id'])) {
448
          $line['entity_table'] = 'civicrm_membership';
449
        }
450 451 452 453
        if (!empty($contributionDetails->id)) {
          $line['contribution_id'] = $contributionDetails->id;
          if ($line['entity_table'] == 'civicrm_contribution') {
            $line['entity_id'] = $contributionDetails->id;
454
          }
455 456
          // CRM-19094: entity_table is set to civicrm_membership then ensure
          // the entityId is set to membership ID not contribution by default
deb.monish's avatar
deb.monish committed
457
          elseif ($line['entity_table'] == 'civicrm_membership' && !empty($line['entity_id']) && $line['entity_id'] == $contributionDetails->id) {
458 459 460
            $membershipId = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipPayment', 'contribution_id', $line['entity_id'], 'membership_id');
            $line['entity_id'] = $membershipId ? $membershipId : $line['entity_id'];
          }
461
        }
462

totten's avatar
totten committed
463 464
        // if financial type is not set and if price field value is NOT NULL
        // get financial type id of price field value
465
        if (!empty($line['price_field_value_id']) && empty($line['financial_type_id'])) {
466
          $line['financial_type_id'] = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceFieldValue', $line['price_field_value_id'], 'financial_type_id');
totten's avatar
totten committed
467 468 469
        }
        $lineItems = CRM_Price_BAO_LineItem::create($line);
        if (!$update && $contributionDetails) {
470 471
          $financialItem = CRM_Financial_BAO_FinancialItem::add($lineItems, $contributionDetails);
          $line['financial_item_id'] = $financialItem->id;
472
          if (!empty($line['tax_amount'])) {
473 474
            CRM_Financial_BAO_FinancialItem::add($lineItems, $contributionDetails, TRUE);
          }
totten's avatar
totten committed
475 476 477
        }
      }
    }
478 479 480
    if (!$update && $contributionDetails) {
      CRM_Core_BAO_FinancialTrxn::createDeferredTrxn($lineItem, $contributionDetails);
    }
eileen's avatar
eileen committed
481
  }
totten's avatar
totten committed
482

483
  /**
colemanw's avatar
colemanw committed
484
   * @param int $entityId
485 486
   * @param string $entityTable
   * @param $amount
colemanw's avatar
colemanw committed
487
   * @param array $otherParams
488
   */
totten's avatar
totten committed
489
  public static function syncLineItems($entityId, $entityTable = 'civicrm_contribution', $amount, $otherParams = NULL) {
totten's avatar
totten committed
490
    if (!$entityId || CRM_Utils_System::isNull($amount)) {
totten's avatar
totten committed
491
      return;
totten's avatar
totten committed
492
    }
totten's avatar
totten committed
493 494 495 496 497

    $from = " civicrm_line_item li
      LEFT JOIN   civicrm_price_field pf ON pf.id = li.price_field_id
      LEFT JOIN   civicrm_price_set ps ON ps.id = pf.price_set_id ";

eileen's avatar
eileen committed
498
    $set = " li.unit_price = %3,
totten's avatar
totten committed
499 500
      li.line_total = %3 ";

eileen's avatar
eileen committed
501
    $where = " li.entity_id = %1 AND
totten's avatar
totten committed
502 503 504 505 506 507 508 509 510 511 512
      li.entity_table = %2 ";

    $params = array(
      1 => array($entityId, 'Integer'),
      2 => array($entityTable, 'String'),
      3 => array($amount, 'Float'),
    );

    if ($entityTable == 'civicrm_contribution') {
      $entityName = 'default_contribution_amount';
      $where .= " AND ps.name = %4 ";
eileen's avatar
eileen committed
513 514
      $params[4] = array($entityName, 'String');
    }
totten's avatar
totten committed
515 516 517 518 519 520 521
    elseif ($entityTable == 'civicrm_participant') {
      $from .= "
        LEFT JOIN civicrm_price_set_entity cpse ON cpse.price_set_id = ps.id
        LEFT JOIN civicrm_price_field_value cpfv ON cpfv.price_field_id = pf.id and cpfv.label = %4 ";
      $set .= " ,li.label = %4,
        li.price_field_value_id = cpfv.id ";
      $where .= " AND cpse.entity_table = 'civicrm_event' AND cpse.entity_id = %5 ";
totten's avatar
totten committed
522
      $amount = empty($amount) ? 0 : $amount;
totten's avatar
totten committed
523 524 525 526 527 528
      $params += array(
        4 => array($otherParams['fee_label'], 'String'),
        5 => array($otherParams['event_id'], 'String'),
      );
    }

eileen's avatar
eileen committed
529
    $query = "
totten's avatar
totten committed
530 531
      UPDATE $from
      SET    $set
eileen's avatar
eileen committed
532
      WHERE  $where
totten's avatar
totten committed
533 534 535 536 537
      ";

    CRM_Core_DAO::executeQuery($query, $params);
  }

totten's avatar
totten committed
538
  /**
colemanw's avatar
colemanw committed
539
   * Build line items array.
eileenmcnaugton's avatar
eileenmcnaugton committed
540
   *
541 542
   * @param array $params
   *   Form values.
totten's avatar
totten committed
543
   *
544 545
   * @param string $entityId
   *   Entity id.
totten's avatar
totten committed
546
   *
547 548
   * @param string $entityTable
   *   Entity Table.
totten's avatar
totten committed
549
   *
eileenmcnaugton's avatar
eileenmcnaugton committed
550
   * @param bool $isRelatedID
totten's avatar
totten committed
551
   */
552
  public static function getLineItemArray(&$params, $entityId = NULL, $entityTable = 'contribution', $isRelatedID = FALSE) {
totten's avatar
totten committed
553
    if (!$entityId) {
554
      $priceSetDetails = CRM_Price_BAO_PriceSet::getDefaultPriceSet($entityTable);
555
      $totalAmount = CRM_Utils_Array::value('partial_payment_total', $params, CRM_Utils_Array::value('total_amount', $params));
556
      $financialType = CRM_Utils_Array::value('financial_type_id', $params);
totten's avatar
totten committed
557
      foreach ($priceSetDetails as $values) {
558
        if ($entityTable == 'membership') {
559 560 561 562 563 564 565 566
          if ($isRelatedID != $values['membership_type_id']) {
            continue;
          }
          if (!$totalAmount) {
            $totalAmount = $values['amount'];
          }
          $financialType = $values['financial_type_id'];
        }
totten's avatar
totten committed
567 568 569 570 571
        $params['line_item'][$values['setID']][$values['priceFieldID']] = array(
          'price_field_id' => $values['priceFieldID'],
          'price_field_value_id' => $values['priceFieldValueID'],
          'label' => $values['label'],
          'qty' => 1,
572 573 574
          'unit_price' => $totalAmount,
          'line_total' => $totalAmount,
          'financial_type_id' => $financialType,
575
          'membership_type_id' => $values['membership_type_id'],
totten's avatar
totten committed
576
        );
577
        break;
totten's avatar
totten committed
578
      }
eileen's avatar
eileen committed
579
    }
totten's avatar
totten committed
580 581
    else {
      $setID = NULL;
Pradeep Nayak's avatar
Pradeep Nayak committed
582
      $totalEntityId = count($entityId);
583 584 585
      if ($entityTable == 'contribution') {
        $isRelatedID = TRUE;
      }
Pradeep Nayak's avatar
Pradeep Nayak committed
586
      foreach ($entityId as $id) {
587
        $lineItems = CRM_Price_BAO_LineItem::getLineItems($id, $entityTable, FALSE, TRUE, $isRelatedID);
Pradeep Nayak's avatar
Pradeep Nayak committed
588
        foreach ($lineItems as $key => $values) {
589
          if (!$setID && $values['price_field_id']) {
590 591
            $setID = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceField', $values['price_field_id'], 'price_set_id');
            $params['is_quick_config'] = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceSet', $setID, 'is_quick_config');
Pradeep Nayak's avatar
Pradeep Nayak committed
592
          }
593
          if (!empty($params['is_quick_config']) && array_key_exists('total_amount', $params)
totten's avatar
totten committed
594 595
            && $totalEntityId == 1
          ) {
Pradeep Nayak's avatar
Pradeep Nayak committed
596 597 598 599
            $values['line_total'] = $values['unit_price'] = $params['total_amount'];
          }
          $values['id'] = $key;
          $params['line_item'][$setID][$key] = $values;
totten's avatar
totten committed
600 601 602 603
        }
      }
    }
  }
604

605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654
  /**
   * Build the line items for the submitted price field.
   *
   * This is when first building them - not an update where an entityId is already present
   * as this is intended as a subfunction of that. Ideally getLineItemArray would call this
   * (resolving to the same format regardless of what type of price set is being used first).
   *
   * @param array $priceParams
   *   These are per the way the form processes them - ie
   *   ['price_1' => 1, 'price_2' => 8]
   *   This would mean price field id 1, option 1 (or 1 unit if using is_enter_qty).
   * @param float|NULL $overrideAmount
   *   Optional override of the amount.
   *
   * @param int|NULL $financialTypeID
   *   Financial type ID is the type should be overridden.
   *
   * @return array
   *   Line items formatted for processing. These will look like
   *   [4] => ['price_field_id' => 4, 'price_field_value_id' => x, 'label....qty...unit_price...line_total...financial_type_id]
   *   [5] => ['price_field_id' => 5, 'price_field_value_id' => x, 'label....qty...unit_price...line_total...financial_type_id]
   *
   */
  public static function buildLineItemsForSubmittedPriceField($priceParams, $overrideAmount = NULL, $financialTypeID = NULL) {
    $lineItems = array();
    foreach ($priceParams as $key => $value) {
      $priceField = self::getPriceFieldMetaData($key);

      if ($priceField['html_type'] === 'Text') {
        $valueSpec = reset($priceField['values']);
      }
      else {
        $valueSpec = $priceField['values'][$value];
      }
      $qty = $priceField['is_enter_qty'] ? $value : 1;
      $lineItems[$priceField['id']] = [
        'price_field_id' => $priceField['id'],
        'price_field_value_id' => $valueSpec['id'],
        'label' => $valueSpec['label'],
        'qty' => $qty,
        'unit_price' => $overrideAmount ?: $valueSpec['amount'],
        'line_total' => $qty * ($overrideAmount ?: $valueSpec['amount']),
        'financial_type_id' => $financialTypeID ?: $valueSpec['financial_type_id'],
        'membership_type_id' => $valueSpec['membership_type_id'],
        'price_set_id' => $priceField['price_set_id'],
      ];
    }
    return $lineItems;
  }

655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672
  /**
   * Function to update related contribution of a entity and
   *  add/update/cancel financial records
   *
   * @param array $params
   * @param int $entityID
   * @param int $entity
   * @param int $contributionId
   * @param $feeBlock
   * @param array $lineItems
   *
   */
  public static function changeFeeSelections(
    $params,
    $entityID,
    $entity,
    $contributionId,
    $feeBlock,
673
    $lineItems
674 675 676 677 678 679 680 681 682
  ) {
    $entityTable = "civicrm_" . $entity;
    CRM_Price_BAO_PriceSet::processAmount($feeBlock,
      $params, $lineItems
    );
    // initialize empty Lineitem instance to call protected helper functions
    $lineItemObj = new CRM_Price_BAO_LineItem();

    // fetch submitted LineItems from input params and feeBlock information
683
    $submittedLineItems = $lineItemObj->getSubmittedLineItems($params, $feeBlock);
684

685
    $requiredChanges = $lineItemObj->getLineItemsToAlter($submittedLineItems, $entityID, $entity);
686 687

    // get financial information that need to be recorded on basis on submitted price field value IDs
688 689 690
    if (!empty($requiredChanges['line_items_to_cancel']) || !empty($requiredChanges['line_items_to_update'])) {
      // @todo - this IF is to get this through PR merge but I suspect that it should not
      // be necessary & is masking something else.
deb.monish's avatar
deb.monish committed
691
      $financialItemsArray = $lineItemObj->getAdjustedFinancialItemsToRecord(
692 693 694 695 696 697 698
        $entityID,
        $entityTable,
        $contributionId,
        array_keys($requiredChanges['line_items_to_cancel']),
        $requiredChanges['line_items_to_update']
      );
    }
699 700 701 702

    // update line item with changed line total and other information
    $totalParticipant = $participantCount = 0;
    $amountLevel = array();
703 704
    if (!empty($requiredChanges['line_items_to_update'])) {
      foreach ($requiredChanges['line_items_to_update'] as $priceFieldValueID => $value) {
705 706 707 708 709 710 711
        $amountLevel[] = $value['label'] . ' - ' . (float) $value['qty'];
        if ($entity == 'participant' && isset($value['participant_count'])) {
          $totalParticipant += $value['participant_count'];
        }
      }
    }

712 713 714 715
    foreach (array_merge($requiredChanges['line_items_to_resurrect'], $requiredChanges['line_items_to_cancel'], $requiredChanges['line_items_to_update']) as $lineItemToAlter) {
      // Must use BAO rather than api because a bad line it in the api which we want to avoid.
      CRM_Price_BAO_LineItem::create($lineItemToAlter);
    }
716

717 718
    $lineItemObj->addLineItemOnChangeFeeSelection($requiredChanges['line_items_to_add'], $entityID, $entityTable, $contributionId);

719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745
    $count = 0;
    if ($entity == 'participant') {
      $count = count(CRM_Event_BAO_Participant::getParticipantIds($contributionId));
    }
    else {
      $count = CRM_Utils_Array::value('count', civicrm_api3('MembershipPayment', 'getcount', array('contribution_id' => $contributionId)));
    }
    if ($count > 1) {
      $updatedAmount = CRM_Price_BAO_LineItem::getLineTotal($contributionId);
    }
    else {
      $updatedAmount = CRM_Utils_Array::value('amount', $params, CRM_Utils_Array::value('total_amount', $params));
    }
    if (strlen($params['tax_amount']) != 0) {
      $taxAmount = $params['tax_amount'];
    }
    else {
      $taxAmount = "NULL";
    }
    $displayParticipantCount = '';
    if ($totalParticipant > 0) {
      $displayParticipantCount = ' Participant Count -' . $totalParticipant;
    }
    $updateAmountLevel = NULL;
    if (!empty($amountLevel)) {
      $updateAmountLevel = CRM_Core_DAO::VALUE_SEPARATOR . implode(CRM_Core_DAO::VALUE_SEPARATOR, $amountLevel) . $displayParticipantCount . CRM_Core_DAO::VALUE_SEPARATOR;
    }
746
    $trxn = $lineItemObj->_recordAdjustedAmt($updatedAmount, $contributionId, $taxAmount, $updateAmountLevel);
eileen's avatar
eileen committed
747 748
    $contributionStatus = CRM_Core_PseudoConstant::getName('CRM_Contribute_DAO_Contribution', 'contribution_status_id', CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution', $contributionId, 'contribution_status_id'));

749 750
    if (!empty($financialItemsArray)) {
      foreach ($financialItemsArray as $updateFinancialItemInfoValues) {
deb.monish's avatar
deb.monish committed
751 752 753
        $newFinancialItem = CRM_Financial_BAO_FinancialItem::create($updateFinancialItemInfoValues);
        // record reverse transaction only if Contribution is Completed because for pending refund or
        //   partially paid we are already recording the surplus owed or refund amount
754
        if (!empty($updateFinancialItemInfoValues['financialTrxn']) && ($contributionStatus == 'Completed')) {
deb.monish's avatar
deb.monish committed
755
          $updateFinancialItemInfoValues = array_merge($updateFinancialItemInfoValues['financialTrxn'], array(
deb.monish's avatar
deb.monish committed
756 757
            'entity_id' => $newFinancialItem->id,
            'entity_table' => 'civicrm_financial_item',
deb.monish's avatar
deb.monish committed
758
          ));
deb.monish's avatar
deb.monish committed
759
          $reverseTrxn = CRM_Core_BAO_FinancialTrxn::create($updateFinancialItemInfoValues);
deb.monish's avatar
deb.monish committed
760 761 762 763 764 765 766 767
          // record reverse entity financial trxn linked to membership's related contribution
          civicrm_api3('EntityFinancialTrxn', 'create', array(
            'entity_table' => "civicrm_contribution",
            'entity_id' => $contributionId,
            'financial_trxn_id' => $reverseTrxn->id,
            'amount' => $reverseTrxn->total_amount,
          ));
          unset($updateFinancialItemInfoValues['financialTrxn']);
768
        }
769 770 771 772 773 774 775 776 777
        elseif (!empty($updateFinancialItemInfoValues['link-financial-trxn'])) {
          civicrm_api3('EntityFinancialTrxn', 'create', array(
            'entity_id' => $newFinancialItem->id,
            'entity_table' => 'civicrm_financial_item',
            'financial_trxn_id' => $trxn->id,
            'amount' => $newFinancialItem->amount,
          ));
          unset($updateFinancialItemInfoValues['link-financial-trxn']);
        }
778 779 780
      }
    }

781
    // @todo - it may be that trxn_id is always empty - flush out scenarios. Add tests.
eileen's avatar
eileen committed
782
    $trxnId = !empty($trxn->id) ? array('id' => $trxn->id) : array();
783
    $lineItemObj->addFinancialItemsOnLineItemsChange(array_merge($requiredChanges['line_items_to_add'], $requiredChanges['line_items_to_resurrect']), $entityID, $entityTable, $contributionId, $trxnId);
784 785

    // update participant fee_amount column
786
    $lineItemObj->updateEntityRecordOnChangeFeeSelection($params, $entityID, $entity);
787 788 789
  }

  /**
790
   * Function to retrieve financial items that need to be recorded as result of changed fee
791 792 793 794
   *
   * @param int $entityID
   * @param string $entityTable
   * @param int $contributionID
795 796
   * @param array $priceFieldValueIDsToCancel
   * @param array $lineItemsToUpdate
797 798
   *
   * @return array
799
   *      List of formatted reverse Financial Items to be recorded
800
   */
deb.monish's avatar
deb.monish committed
801
  protected function getAdjustedFinancialItemsToRecord($entityID, $entityTable, $contributionID, $priceFieldValueIDsToCancel, $lineItemsToUpdate) {
802 803 804
    $previousLineItems = CRM_Price_BAO_LineItem::getLineItems($entityID, str_replace('civicrm_', '', $entityTable));

    $financialItemsArray = array();
805
    $financialItemResult = $this->getNonCancelledFinancialItems($entityID, $entityTable);
806
    foreach ($financialItemResult as $updateFinancialItemInfoValues) {
807
      $updateFinancialItemInfoValues['transaction_date'] = date('YmdHis');
808 809

      // the below params are not needed as we are creating new financial item
810
      $previousFinancialItemID = $updateFinancialItemInfoValues['id'];
811
      $totalFinancialAmount = $this->checkFinancialItemTotalAmountByLineItemID($updateFinancialItemInfoValues['entity_id']);
812 813
      unset($updateFinancialItemInfoValues['id']);
      unset($updateFinancialItemInfoValues['created_date']);
814

815
      // if not submitted and difference is not 0 make it negative
816 817 818 819
      if ((empty($lineItemsToUpdate) || (in_array($updateFinancialItemInfoValues['price_field_value_id'], $priceFieldValueIDsToCancel) &&
          $totalFinancialAmount == $updateFinancialItemInfoValues['amount'])
        ) && $updateFinancialItemInfoValues['amount'] > 0
      ) {
820 821 822
        // INSERT negative financial_items
        $updateFinancialItemInfoValues['amount'] = -$updateFinancialItemInfoValues['amount'];
        // reverse the related financial trxn too
823
        $updateFinancialItemInfoValues['financialTrxn'] = $this->getRelatedCancelFinancialTrxn($previousFinancialItemID);
824 825
        if ($previousLineItems[$updateFinancialItemInfoValues['entity_id']]['tax_amount']) {
          $updateFinancialItemInfoValues['tax']['amount'] = -($previousLineItems[$updateFinancialItemInfoValues['entity_id']]['tax_amount']);
eileen's avatar
eileen committed
826
          $updateFinancialItemInfoValues['tax']['description'] = $this->getSalesTaxTerm();
827 828
        }
        // INSERT negative financial_items for tax amount
829
        $financialItemsArray[$updateFinancialItemInfoValues['entity_id']] = $updateFinancialItemInfoValues;
830
      }
deb.monish's avatar
deb.monish committed
831
      // INSERT a financial item to record surplus/lesser amount when a text price fee is changed
832 833 834 835
      elseif (!empty($lineItemsToUpdate) &&
      $lineItemsToUpdate[$updateFinancialItemInfoValues['price_field_value_id']]['html_type'] == 'Text' &&
      $updateFinancialItemInfoValues['amount'] > 0
      ) {
deb.monish's avatar
deb.monish committed
836 837 838
        // calculate the amount difference, considered as financial item amount
        $updateFinancialItemInfoValues['amount'] = $lineItemsToUpdate[$updateFinancialItemInfoValues['price_field_value_id']]['line_total'] - $totalFinancialAmount;
        // add a flag, later used to link financial trxn and this new financial item
839
        $updateFinancialItemInfoValues['link-financial-trxn'] = TRUE;
deb.monish's avatar
deb.monish committed
840 841 842 843
        if ($previousLineItems[$updateFinancialItemInfoValues['entity_id']]['tax_amount']) {
          $updateFinancialItemInfoValues['tax']['amount'] = $lineItemsToUpdate[$updateFinancialItemInfoValues['entity_id']]['tax_amount'] - $previousLineItems[$updateFinancialItemInfoValues['entity_id']]['tax_amount'];
          $updateFinancialItemInfoValues['tax']['description'] = $this->getSalesTaxTerm();
        }
844 845
        $financialItemsArray[$updateFinancialItemInfoValues['entity_id']] = $updateFinancialItemInfoValues;
      }
846 847 848 849 850
    }

    return $financialItemsArray;
  }

851 852 853 854 855 856 857 858 859 860 861 862 863 864
  /**
   * Helper function to return sum of financial item's amount related to a line-item
   * @param array $lineItemID
   *
   * @return float $financialItem
   */
  protected function checkFinancialItemTotalAmountByLineItemID($lineItemID) {
    return CRM_Core_DAO::singleValueQuery("
      SELECT SUM(amount)
      FROM civicrm_financial_item
      WHERE entity_table = 'civicrm_line_item' AND entity_id = {$lineItemID}
    ");
  }

865 866 867 868 869 870 871 872 873
  /**
   * Helper function to retrieve submitted line items from form values $inputParams and used $feeBlock
   *
   * @param array $inputParams
   * @param array $feeBlock
   *
   * @return array
   *     List of submitted line items
   */
874
  protected function getSubmittedLineItems($inputParams, $feeBlock) {
875 876 877 878 879 880 881 882 883
    $submittedLineItems = array();
    foreach ($feeBlock as $id => $values) {
      CRM_Price_BAO_LineItem::format($id, $inputParams, $values, $submittedLineItems);
    }

    return $submittedLineItems;
  }

  /**
884 885 886 887 888 889
   *  Helper function to retrieve line items that need to be altered.
   *
   * We iterate through the previous line items for the given entity to determine
   * what alterations to line items need to be made to reflect the new line items.
   *
   * There are 4 possible changes required - per the keys in the return array.
890 891 892 893 894 895
   *
   * @param array $submittedLineItems
   * @param int $entityID
   * @param string $entity
   *
   * @return array
896 897 898 899 900 901 902 903 904
   *    Array of line items to alter with the following keys
   *   - line_items_to_add. If the line items required are new radio options that
   *     have not previously been set then we should add line items for them
   *   - line_items_to_update. If we have already been an active option and a change has
   *     happened then it should be in this array.
   *   - line_items_to_cancel. Line items currently selected but not selected in the new selection.
   *     These need to be zero'd out.
   *   - line_items_to_resurrect. Line items previously selected and then deselected. These need to be
   *     re-enabled rather than a new one added.
905
   */
906
  protected function getLineItemsToAlter($submittedLineItems, $entityID, $entity) {
907 908 909 910 911
    $previousLineItems = CRM_Price_BAO_LineItem::getLineItems($entityID, $entity);

    $lineItemsToAdd = $submittedLineItems;
    $lineItemsToUpdate = array();
    $submittedPriceFieldValueIDs = array_keys($submittedLineItems);
912
    $lineItemsToCancel = $lineItemsToResurrect = array();
913 914 915

    foreach ($previousLineItems as $id => $previousLineItem) {
      if (in_array($previousLineItem['price_field_value_id'], $submittedPriceFieldValueIDs)) {
916
        $submittedLineItem = $submittedLineItems[$previousLineItem['price_field_value_id']];
917 918 919 920
        if (CRM_Utils_Array::value('html_type', $lineItemsToAdd[$previousLineItem['price_field_value_id']]) == 'Text') {
          // If a 'Text' price field was updated by changing qty value, then we are not adding new line-item but updating the existing one,
          //  because unlike other kind of price-field, it's related price-field-value-id isn't changed and thats why we need to make an
          //  exception here by adding financial item for updated line-item and will reverse any previous financial item entries.
921 922
          $lineItemsToUpdate[$previousLineItem['price_field_value_id']] = array_merge($submittedLineItem, array('id' => $id));
          unset($lineItemsToAdd[$previousLineItem['price_field_value_id']]);
923 924
        }
        else {
925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948
          $submittedLineItem = $submittedLineItems[$previousLineItem['price_field_value_id']];
          // for updating the line items i.e. use-case - once deselect-option selecting again
          if (($previousLineItem['line_total'] != $submittedLineItem['line_total'])
            || (
              // This would be a $0 line item - but why it should be catered to
              // other than when the above condition is unclear.
              $submittedLineItem['line_total'] == 0 && $submittedLineItem['qty'] == 1
            )
            || (
              $previousLineItem['qty'] != $submittedLineItem['qty']
            )
          ) {
            $lineItemsToUpdate[$previousLineItem['price_field_value_id']] = $submittedLineItem;
            $lineItemsToUpdate[$previousLineItem['price_field_value_id']]['id'] = $id;
            // Format is actually '0.00'
            if ($previousLineItem['line_total'] == 0) {
              $lineItemsToAdd[$previousLineItem['price_field_value_id']]['id'] = $id;
              $lineItemsToResurrect[] = $lineItemsToAdd[$previousLineItem['price_field_value_id']];
            }
          }
          // If there was previously a submitted line item for the same option value then there is
          // either no change or a qty adjustment. In either case we are not doing an add + reversal.
          unset($lineItemsToAdd[$previousLineItem['price_field_value_id']]);
          unset($lineItemsToCancel[$previousLineItem['price_field_value_id']]);
949
        }
950 951 952 953 954 955
      }
      else {
        if (!$this->isCancelled($previousLineItem)) {
          $cancelParams = array('qty' => 0, 'line_total' => 0, 'tax_amount' => 0, 'participant_count' => 0, 'non_deductible_amount' => 0, 'id' => $id);
          $lineItemsToCancel[$previousLineItem['price_field_value_id']] = array_merge($previousLineItem, $cancelParams);

956 957 958 959
        }
      }
    }

960 961 962
    return array(
      'line_items_to_add' => $lineItemsToAdd,
      'line_items_to_update' => $lineItemsToUpdate,
963 964
      'line_items_to_cancel' => $lineItemsToCancel,
      'line_items_to_resurrect' => $lineItemsToResurrect,
965
    );
966 967
  }

968 969 970 971 972 973 974 975 976 977 978 979
  /**
   * Check if a line item has already been cancelled.
   *
   * @param array $lineItem
   *
   * @return bool
   */
  protected function isCancelled($lineItem) {
    if ($lineItem['qty'] == 0 && $lineItem['line_total'] == 0) {
      return TRUE;
    }
  }
980
  /**
981
   * Add line Items as result of fee change.
982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998
   *
   * @param array $lineItemsToAdd
   * @param int $entityID
   * @param string $entityTable
   * @param int $contributionID
   */
  protected function addLineItemOnChangeFeeSelection(
    $lineItemsToAdd,
    $entityID,
    $entityTable,
    $contributionID
  ) {
    // if there is no line item to add, do not proceed
    if (empty($lineItemsToAdd)) {
      return;
    }

999 1000 1001 1002
    $changedFinancialTypeID = NULL;
    $updatedContribution = new CRM_Contribute_BAO_Contribution();
    $updatedContribution->id = (int) $contributionID;
    // insert financial items
1003 1004 1005 1006 1007 1008 1009 1010 1011 1012
    foreach ($lineItemsToAdd as $priceFieldValueID => $lineParams) {
      $lineParams = array_merge($lineParams, array(
        'entity_table' => $entityTable,
        'entity_id' => $entityID,
        'contribution_id' => $contributionID,
      ));
      if (!array_key_exists('skip', $lineParams)) {
        self::create($lineParams);
      }
    }
1013 1014 1015 1016 1017

    if ($changedFinancialTypeID) {
      $updatedContribution->financial_type_id = $changedFinancialTypeID;
      $updatedContribution->save();
    }
1018 1019
  }

1020
  /**
1021
   * Add financial transactions when an array of line items is changed.
1022 1023 1024 1025 1026
   *
   * @param array $lineItemsToAdd
   * @param int $entityID
   * @param string $entityTable
   * @param int $contributionID
1027 1028
   * @param bool $isCreateAdditionalFinancialTrxn
   *   Is there a change to the total balance requiring additional transactions to be created.
1029
   */
1030 1031 1032 1033
  protected function addFinancialItemsOnLineItemsChange($lineItemsToAdd, $entityID, $entityTable, $contributionID, $isCreateAdditionalFinancialTrxn) {
    $updatedContribution = new CRM_Contribute_BAO_Contribution();
    $updatedContribution->id = $contributionID;
    $updatedContribution->find(TRUE);
1034 1035 1036 1037 1038 1039 1040

    foreach ($lineItemsToAdd as $priceFieldValueID => $lineParams) {
      $lineParams = array_merge($lineParams, array(
        'entity_table' => $entityTable,
        'entity_id' => $entityID,
        'contribution_id' => $contributionID,
      ));
1041
      $this->addFinancialItemsOnLineItemChange($isCreateAdditionalFinancialTrxn, $lineParams, $updatedContribution);
1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052
    }
  }

  /**
   * Helper function to update entity record on change fee selection
   *
   * @param array $inputParams
   * @param int $entityID
   * @param string $entity
   *
   */
1053
  protected function updateEntityRecordOnChangeFeeSelection($inputParams, $entityID, $entity) {
1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075
    $entityTable = "civicrm_{$entity}";

    if ($entity == 'participant') {
      $partUpdateFeeAmt = array('id' => $entityID);
      $getUpdatedLineItems = "SELECT *
        FROM civicrm_line_item
        WHERE (entity_table = '{$entityTable}' AND entity_id = {$entityID} AND qty > 0)";
      $getUpdatedLineItemsDAO = CRM_Core_DAO::executeQuery($getUpdatedLineItems);
      $line = array();
      while ($getUpdatedLineItemsDAO->fetch()) {
        $line[$getUpdatedLineItemsDAO->price_field_value_id] = $getUpdatedLineItemsDAO->label . ' - ' . (float) $getUpdatedLineItemsDAO->qty;
      }

      $partUpdateFeeAmt['fee_level'] = implode(', ', $line);
      $partUpdateFeeAmt['fee_amount'] = $inputParams['amount'];
      CRM_Event_BAO_Participant::add($partUpdateFeeAmt);

      //activity creation
      CRM_Event_BAO_Participant::addActivityForSelection($entityID, 'Change Registration');
    }
  }

1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122
  /**
   * Get the metadata for a price field.
   *
   * @param string|int $key
   *   Price field id. Either as an integer or as 'price_4' where 4 is the id
   *
   * @return array
   *   Metadata for the price field with a values key for option values.
   */
  protected static function getPriceFieldMetaData($key) {
    $priceFieldID = str_replace('price_', '', $key);
    if (!isset(Civi::$statics[__CLASS__]['price_fields'][$priceFieldID])) {
      $values = civicrm_api3('PriceFieldValue', 'get', [
        'price_field_id' => $priceFieldID,
        'return' => [
          'id',
          'amount',
          'financial_type_id',
          'membership_type_id',
          'label',
          'price_field_id',
          'price_field_id.price_set_id',
          'price_field_id.html_type',
          'is_enter_qty',
        ],
      ]);
      $firstValue = reset($values['values']);
      $values = $values['values'];
      foreach ($values as $index => $value) {
        // Let's be nice to calling functions & ensure membership_type_id is set
        // so they don't have to handle notices on it. Handle one place not many.
        if (!isset($value['membership_type_id'])) {
          $values[$index]['membership_type_id'] = NULL;
        }
      }

      Civi::$statics[__CLASS__]['price_fields'][$priceFieldID] = [
        'price_set_id' => $firstValue['price_field_id.price_set_id'],
        'id' => $firstValue['price_field_id'],
        'html_type' => $firstValue['price_field_id.html_type'],
        'is_enter_qty' => !empty($firstValue['is_enter_qty']),
        'values' => $values,
      ];
    }
    return Civi::$statics[__CLASS__]['price_fields'][$priceFieldID];
  }

1123 1124 1125 1126 1127 1128 1129 1130 1131
  /**
   * Helper function to retrieve financial trxn parameters to reverse
   *  for given financial item identified by $financialItemID
   *
   * @param int $financialItemID
   *
   * @return array $financialTrxn
   *
   */
1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148
  protected function _getRelatedCancelFinancialTrxn($financialItemID) {
    try {
      $financialTrxn = civicrm_api3('EntityFinancialTrxn', 'getsingle', array(
        'entity_table' => 'civicrm_financial_item',
        'entity_id' => $financialItemID,
        'options' => array(
          'sort' => 'id DESC',
          'limit' => 1,
        ),
        'api.FinancialTrxn.getsingle' => array(
          'id' => "\$value.financial_trxn_id",
        ),
      ));
    }
    catch (CiviCRM_API3_Exception $e) {
      return array();
    }
deb.monish's avatar
deb.monish committed
1149 1150 1151 1152 1153 1154 1155 1156 1157

    $financialTrxn = array_merge($financialTrxn['api.FinancialTrxn.getsingle'], array(
      'trxn_date' => date('YmdHis'),
      'total_amount' => -$financialTrxn['api.FinancialTrxn.getsingle']['total_amount'],
      'net_amount' => -$financialTrxn['api.FinancialTrxn.getsingle']['net_amount'],
      'entity_table' => 'civicrm_financial_item',
      'entity_id' => $financialItemID,
    ));
    unset($financialTrxn['id']);
1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171

    return $financialTrxn;
  }

  /**
   * Record adjusted amount.
   *
   * @param int $updatedAmount
   * @param int $contributionId
   * @param int $taxAmount
   * @param bool $updateAmountLevel
   *
   * @return bool|\CRM_Core_BAO_FinancialTrxn
   */
1172 1173
  protected function _recordAdjustedAmt($updatedAmount, $contributionId, $taxAmount = NULL, $updateAmountLevel = NULL) {
    $paidAmount = CRM_Core_BAO_FinancialTrxn::getTotalPayments($contributionId);
1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228
    $balanceAmt = $updatedAmount - $paidAmount;

    $contributionStatuses = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name');
    $partiallyPaidStatusId = array_search('Partially paid', $contributionStatuses);
    $pendingRefundStatusId = array_search('Pending refund', $contributionStatuses);
    $completedStatusId = array_search('Completed', $contributionStatuses);

    $updatedContributionDAO = new CRM_Contribute_BAO_Contribution();
    $adjustedTrxn = $skip = FALSE;
    if ($balanceAmt) {
      if ($balanceAmt > 0 && $paidAmount != 0) {
        $contributionStatusVal = $partiallyPaidStatusId;
      }
      elseif ($balanceAmt < 0 && $paidAmount != 0) {
        $contributionStatusVal = $pendingRefundStatusId;
      }
      elseif ($paidAmount == 0) {
        //skip updating the contribution status if no payment is made
        $skip = TRUE;
        $updatedContributionDAO->cancel_date = 'null';
        $updatedContributionDAO->cancel_reason = NULL;
      }
      // update contribution status and total amount without trigger financial code
      // as this is handled in current BAO function used for change selection
      $updatedContributionDAO->id = $contributionId;
      if (!$skip) {
        $updatedContributionDAO->contribution_status_id = $contributionStatusVal;
      }
      $updatedContributionDAO->total_amount = $updatedContributionDAO->net_amount = $updatedAmount;
      $updatedContributionDAO->fee_amount = 0;
      $updatedContributionDAO->tax_amount = $taxAmount;
      if (!empty($updateAmountLevel)) {
        $updatedContributionDAO->amount_level = $updateAmountLevel;
      }
      $updatedContributionDAO->save();
      // adjusted amount financial_trxn creation
      $updatedContribution = CRM_Contribute_BAO_Contribution::getValues(
        array('id' => $contributionId),
        CRM_Core_DAO::$_nullArray,
        CRM_Core_DAO::$_nullArray
      );
      $toFinancialAccount = CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($updatedContribution->financial_type_id, 'Accounts Receivable Account is');
      $adjustedTrxnValues = array(
        'from_financial_account_id' => NULL,
        'to_financial_account_id' => $toFinancialAccount,
        'total_amount' => $balanceAmt,
        'net_amount' => $balanceAmt,
        'status_id' => $completedStatusId,
        'payment_instrument_id' => $updatedContribution->payment_instrument_id,
        'contribution_id' => $updatedContribution->id,
        'trxn_date' => date('YmdHis'),
        'currency' => $updatedContribution->currency,
      );
      $adjustedTrxn = CRM_Core_BAO_FinancialTrxn::create($adjustedTrxnValues);
    }
1229 1230 1231 1232 1233 1234
    // CRM-17151: Update the contribution status to completed if balance is zero,
    //  because due to sucessive fee change will leave the related contribution status incorrect
    else {
      CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_Contribution', $contributionId, 'contribution_status_id', $completedStatusId);
    }

1235 1236 1237
    return $adjustedTrxn;
  }

1238 1239 1240 1241 1242 1243 1244
  /**
   * Add financial items to reflect line item change.
   *
   * @param bool $isCreateAdditionalFinancialTrxn
   * @param array $lineParams
   * @param \CRM_Contribute_BAO_Contribution $updatedContribution
   */
1245 1246
  protected function addFinancialItemsOnLineItemChange($isCreateAdditionalFinancialTrxn, $lineParams, $updatedContribution) {
    $tempFinancialTrxnID = NULL;
1247 1248
    // don't add financial item for cancelled line item
    if ($lineParams['qty'] == 0) {
1249
      return;
1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286
    }
    elseif ($isCreateAdditionalFinancialTrxn) {
      // This routine & the return below is super uncomfortable.
      // I have refactored to here but don't understand how this would be hit
      // and it is how it would be a good thing, given the odd return below which
      // does not seem consistent with what is going on.
      // I'm tempted to add an e-deprecated into it to confirm my suspicion it only exists to
      // cause mental anguish.
      // original comment : add financial item if ONLY financial type is changed
      if ($lineParams['financial_type_id'] != $updatedContribution->financial_type_id) {
        $changedFinancialTypeID = (int) $lineParams['financial_type_id'];
        $adjustedTrxnValues = array(
          'from_financial_account_id' => NULL,
          'to_financial_account_id' => CRM_Financial_BAO_FinancialTypeAccount::getInstrumentFinancialAccount($updatedContribution->payment_instrument_id),
          'total_amount' => $lineParams['line_total'],
          'net_amount' => $lineParams['line_total'],
          'status_id' => $updatedContribution->contribution_status_id,
          'payment_instrument_id' => $updatedContribution->payment_instrument_id,
          'contribution_id' => $updatedContribution->id,
          'is_payment' => TRUE,
          // since balance is 0, which means contribution is completed
          'trxn_date' => date('YmdHis'),
          'currency' => $updatedContribution->currency,
        );
        $adjustedTrxn = CRM_Core_BAO_FinancialTrxn::create($adjustedTrxnValues);
        $tempFinancialTrxnID = array('id' => $adjustedTrxn->id);
      }
    }
    $lineObj = CRM_Price_BAO_LineItem::retrieve($lineParams, CRM_Core_DAO::$_nullArray);
    // insert financial items
    // ensure entity_financial_trxn table has a linking of it.
    CRM_Financial_BAO_FinancialItem::add($lineObj, $updatedContribution, NULL, $tempFinancialTrxnID);
    if (isset($lineObj->tax_amount)) {
      CRM_Financial_BAO_FinancialItem::add($lineObj, $updatedContribution, TRUE, $tempFinancialTrxnID);
    }
  }

1287 1288 1289 1290 1291 1292 1293 1294 1295 1296
  /**
   * Get Financial items, culling out any that have already been reversed.
   *
   * @param int $entityID
   * @param string $entityTable
   *
   * @return array
   *   Array of financial items that have not be reversed.
   */
  protected function getNonCancelledFinancialItems($entityID, $entityTable) {
1297
    // gathering necessary info to record negative (deselected) financial_item
1298
    $updateFinancialItem = "
1299
  SELECT fi.*, price_field_value_id, financial_type_id, tax_amount
1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327
    FROM civicrm_financial_item fi LEFT JOIN civicrm_line_item li ON (li.id = fi.entity_id AND fi.entity_table = 'civicrm_line_item')
  WHERE (li.entity_table = '{$entityTable}' AND li.entity_id = {$entityID})
  GROUP BY li.entity_table, li.entity_id, price_field_value_id, fi.id
    ";
    $updateFinancialItemInfoDAO = CRM_Core_DAO::executeQuery($updateFinancialItem);

    $financialItemResult = $updateFinancialItemInfoDAO->fetchAll();
    $items = array();
    foreach ($financialItemResult as $index => $financialItem) {
      $items[$financialItem['price_field_value_id']][$index] = $financialItem['amount'];

      if (!empty($items[$financialItem['price_field_value_id']])) {
        foreach ($items[$financialItem['price_field_value_id']] as $existingItemID => $existingAmount) {
          if ($financialItem['amount'] + $existingAmount == 0) {
            // Filter both rows as they cancel each other out.
            unset($financialItemResult[$index]);
            unset($financialItemResult[$existingItemID]);
            unset($items['price_field_value_id'][$existingItemID]);
            unset($items[$financialItem['price_field_value_id']][$index]);
          }
        }

      }

    }
    return $financialItemResult;
  }

eileen's avatar
eileen committed
1328 1329 1330 1331 1332 1333 1334 1335 1336
  /**
   * Get the string used to describe the sales tax (eg. VAT, GST).
   *
   * @return string
   */
  protected function getSalesTaxTerm() {
    return CRM_Contribute_BAO_Contribution::checkContributeSettings('tax_term');
  }

totten's avatar
totten committed