| 1 | <?php |
| 2 | /* |
| 3 | +--------------------------------------------------------------------+ |
| 4 | | CiviCRM version 5 | |
| 5 | +--------------------------------------------------------------------+ |
| 6 | | Copyright CiviCRM LLC (c) 2004-2019 | |
| 7 | +--------------------------------------------------------------------+ |
| 8 | | This file is a part of CiviCRM. | |
| 9 | | | |
| 10 | | CiviCRM is free software; you can copy, modify, and distribute it | |
| 11 | | under the terms of the GNU Affero General Public License | |
| 12 | | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. | |
| 13 | | | |
| 14 | | CiviCRM is distributed in the hope that it will be useful, but | |
| 15 | | WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 16 | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | |
| 17 | | See the GNU Affero General Public License for more details. | |
| 18 | | | |
| 19 | | You should have received a copy of the GNU Affero General Public | |
| 20 | | License and the CiviCRM Licensing Exception along | |
| 21 | | with this program; if not, contact CiviCRM LLC | |
| 22 | | at info[AT]civicrm[DOT]org. If you have questions about the | |
| 23 | | GNU Affero General Public License or the licensing of CiviCRM, | |
| 24 | | see the CiviCRM license FAQ at http://civicrm.org/licensing | |
| 25 | +--------------------------------------------------------------------+ |
| 26 | */ |
| 27 | |
| 28 | /** |
| 29 | * |
| 30 | * @package CRM |
| 31 | * @copyright CiviCRM LLC (c) 2004-2019 |
| 32 | */ |
| 33 | |
| 34 | /** |
| 35 | * Business objects for Line Items generated by monetary transactions |
| 36 | */ |
| 37 | class CRM_Price_BAO_LineItem extends CRM_Price_DAO_LineItem { |
| 38 | |
| 39 | /** |
| 40 | * Creates a new entry in the database. |
| 41 | * |
| 42 | * @param array $params |
| 43 | * (reference) an assoc array of name/value pairs. |
| 44 | * |
| 45 | * @return \CRM_Price_DAO_LineItem |
| 46 | * |
| 47 | * @throws \CiviCRM_API3_Exception |
| 48 | * @throws \Exception |
| 49 | */ |
| 50 | public static function create(&$params) { |
| 51 | $id = CRM_Utils_Array::value('id', $params); |
| 52 | if ($id) { |
| 53 | CRM_Utils_Hook::pre('edit', 'LineItem', $id, $params); |
| 54 | $op = CRM_Core_Action::UPDATE; |
| 55 | } |
| 56 | else { |
| 57 | CRM_Utils_Hook::pre('create', 'LineItem', $params['entity_id'], $params); |
| 58 | $op = CRM_Core_Action::ADD; |
| 59 | } |
| 60 | |
| 61 | // unset entity table and entity id in $params |
| 62 | // we never update the entity table and entity id during update mode |
| 63 | if ($id) { |
| 64 | $entity_id = CRM_Utils_Array::value('entity_id', $params); |
| 65 | $entity_table = CRM_Utils_Array::value('entity_table', $params); |
| 66 | unset($params['entity_id'], $params['entity_table']); |
| 67 | } |
| 68 | else { |
| 69 | if (!isset($params['unit_price'])) { |
| 70 | $params['unit_price'] = 0; |
| 71 | } |
| 72 | } |
| 73 | if (CRM_Financial_BAO_FinancialType::isACLFinancialTypeStatus() && CRM_Utils_Array::value('check_permissions', $params)) { |
| 74 | if (empty($params['financial_type_id'])) { |
| 75 | throw new Exception('Mandatory key(s) missing from params array: financial_type_id'); |
| 76 | } |
| 77 | CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes($types, $op); |
| 78 | 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 | } |
| 82 | |
| 83 | $lineItemBAO = new CRM_Price_BAO_LineItem(); |
| 84 | $lineItemBAO->copyValues($params); |
| 85 | |
| 86 | $return = $lineItemBAO->save(); |
| 87 | if ($lineItemBAO->entity_table == 'civicrm_membership' && $lineItemBAO->contribution_id && $lineItemBAO->entity_id) { |
| 88 | $membershipPaymentParams = [ |
| 89 | 'membership_id' => $lineItemBAO->entity_id, |
| 90 | 'contribution_id' => $lineItemBAO->contribution_id, |
| 91 | ]; |
| 92 | if (!civicrm_api3('MembershipPayment', 'getcount', $membershipPaymentParams)) { |
| 93 | civicrm_api3('MembershipPayment', 'create', $membershipPaymentParams); |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | if ($id) { |
| 98 | // CRM-21281: Restore entity reference in case the post hook needs it |
| 99 | $lineItemBAO->entity_id = $entity_id; |
| 100 | $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 | } |
| 106 | |
| 107 | return $return; |
| 108 | } |
| 109 | |
| 110 | /** |
| 111 | * Retrieve DB object based on input parameters. |
| 112 | * |
| 113 | * It also stores all the retrieved values in the default array. |
| 114 | * |
| 115 | * @param array $params |
| 116 | * (reference ) an assoc array of name/value pairs. |
| 117 | * @param array $defaults |
| 118 | * (reference ) an assoc array to hold the flattened values. |
| 119 | * |
| 120 | * @return CRM_Price_BAO_LineItem |
| 121 | */ |
| 122 | public static function retrieve(&$params, &$defaults) { |
| 123 | $lineItem = new CRM_Price_BAO_LineItem(); |
| 124 | $lineItem->copyValues($params); |
| 125 | if ($lineItem->find(TRUE)) { |
| 126 | CRM_Core_DAO::storeValues($lineItem, $defaults); |
| 127 | return $lineItem; |
| 128 | } |
| 129 | return NULL; |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * Modifies $params array for filtering financial types. |
| 134 | * |
| 135 | * @param array $params |
| 136 | * (reference ) an assoc array of name/value pairs. |
| 137 | * |
| 138 | */ |
| 139 | public static function getAPILineItemParams(&$params) { |
| 140 | CRM_Financial_BAO_FinancialType::getAvailableFinancialTypes($types); |
| 141 | if ($types && empty($params['financial_type_id'])) { |
| 142 | $params['financial_type_id'] = ['IN' => array_keys($types)]; |
| 143 | } |
| 144 | elseif ($types) { |
| 145 | if (is_array($params['financial_type_id'])) { |
| 146 | $invalidFts = array_diff($params['financial_type_id'], array_keys($types)); |
| 147 | } |
| 148 | elseif (!in_array($params['financial_type_id'], array_keys($types))) { |
| 149 | $invalidFts = $params['financial_type_id']; |
| 150 | } |
| 151 | if ($invalidFts) { |
| 152 | $params['financial_type_id'] = ['NOT IN' => $invalidFts]; |
| 153 | } |
| 154 | } |
| 155 | else { |
| 156 | $params['financial_type_id'] = 0; |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | /** |
| 161 | * @param int $contributionId |
| 162 | * |
| 163 | * @return null|string |
| 164 | */ |
| 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 | WHERE li.contribution_id = %1"; |
| 169 | $params = [1 => [$contributionId, 'Integer']]; |
| 170 | $lineItemTotal = CRM_Core_DAO::singleValueQuery($sqlLineItemTotal, $params); |
| 171 | return $lineItemTotal; |
| 172 | } |
| 173 | |
| 174 | /** |
| 175 | * Wrapper for line item retrieval when contribution ID is known. |
| 176 | * @param int $contributionID |
| 177 | * |
| 178 | * @return array |
| 179 | */ |
| 180 | public static function getLineItemsByContributionID($contributionID) { |
| 181 | return self::getLineItems($contributionID, 'contribution', NULL, TRUE, TRUE); |
| 182 | } |
| 183 | |
| 184 | /** |
| 185 | * Given a participant id/contribution id, |
| 186 | * return contribution/fee line items |
| 187 | * |
| 188 | * @param int $entityId |
| 189 | * participant/contribution id. |
| 190 | * @param string $entity |
| 191 | * participant/contribution. |
| 192 | * |
| 193 | * @param bool $isQuick |
| 194 | * @param bool $isQtyZero |
| 195 | * @param bool $relatedEntity |
| 196 | * |
| 197 | * @param bool $invoice |
| 198 | * @return array |
| 199 | * Array of line items |
| 200 | */ |
| 201 | public static function getLineItems($entityId, $entity = 'participant', $isQuick = FALSE, $isQtyZero = TRUE, $relatedEntity = FALSE, $invoice = FALSE) { |
| 202 | $whereClause = $fromClause = NULL; |
| 203 | $selectClause = " |
| 204 | SELECT li.id, |
| 205 | li.label, |
| 206 | li.contribution_id, |
| 207 | li.qty, |
| 208 | li.unit_price, |
| 209 | li.line_total, |
| 210 | li.entity_table, |
| 211 | li.entity_id, |
| 212 | pf.label as field_title, |
| 213 | pf.html_type, |
| 214 | pf.price_set_id, |
| 215 | pfv.membership_type_id, |
| 216 | pfv.membership_num_terms, |
| 217 | li.price_field_id, |
| 218 | li.participant_count, |
| 219 | li.price_field_value_id, |
| 220 | li.financial_type_id, |
| 221 | li.tax_amount, |
| 222 | pfv.description"; |
| 223 | |
| 224 | $condition = "li.entity_id = %2.id AND li.entity_table = 'civicrm_%2'"; |
| 225 | if ($relatedEntity) { |
| 226 | $condition = "li.contribution_id = %2.id "; |
| 227 | } |
| 228 | |
| 229 | $fromClause = " |
| 230 | FROM civicrm_%2 as %2 |
| 231 | LEFT JOIN civicrm_line_item li ON ({$condition}) |
| 232 | LEFT JOIN civicrm_price_field_value pfv ON ( pfv.id = li.price_field_value_id ) |
| 233 | LEFT JOIN civicrm_price_field pf ON (pf.id = li.price_field_id )"; |
| 234 | $whereClause = " |
| 235 | WHERE %2.id = %1"; |
| 236 | |
| 237 | // CRM-16250 get additional participant's fee selection details only for invoice PDF (if any) |
| 238 | if ($entity == 'participant' && $invoice) { |
| 239 | $additionalParticipantIDs = CRM_Event_BAO_Participant::getAdditionalParticipantIds($entityId); |
| 240 | if (!empty($additionalParticipantIDs)) { |
| 241 | $whereClause = "WHERE %2.id IN (%1, " . implode(', ', $additionalParticipantIDs) . ")"; |
| 242 | } |
| 243 | } |
| 244 | |
| 245 | $orderByClause = " ORDER BY pf.weight, pfv.weight"; |
| 246 | |
| 247 | if ($isQuick) { |
| 248 | $fromClause .= " LEFT JOIN civicrm_price_set cps on cps.id = pf.price_set_id "; |
| 249 | $whereClause .= " and cps.is_quick_config = 0"; |
| 250 | } |
| 251 | |
| 252 | if (!$isQtyZero) { |
| 253 | $whereClause .= " and li.qty != 0"; |
| 254 | } |
| 255 | |
| 256 | $lineItems = []; |
| 257 | |
| 258 | if (!$entityId || !$entity || !$fromClause) { |
| 259 | return $lineItems; |
| 260 | } |
| 261 | |
| 262 | $params = [ |
| 263 | 1 => [$entityId, 'Integer'], |
| 264 | 2 => [$entity, 'Text'], |
| 265 | ]; |
| 266 | |
| 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); |
| 272 | while ($dao->fetch()) { |
| 273 | if (!$dao->id) { |
| 274 | continue; |
| 275 | } |
| 276 | $lineItems[$dao->id] = [ |
| 277 | 'qty' => (float) $dao->qty, |
| 278 | 'label' => $dao->label, |
| 279 | 'unit_price' => $dao->unit_price, |
| 280 | 'line_total' => $dao->line_total, |
| 281 | 'price_field_id' => $dao->price_field_id, |
| 282 | 'participant_count' => $dao->participant_count, |
| 283 | 'price_field_value_id' => $dao->price_field_value_id, |
| 284 | 'field_title' => $dao->field_title, |
| 285 | 'html_type' => $dao->html_type, |
| 286 | 'description' => $dao->description, |
| 287 | 'entity_id' => $dao->entity_id, |
| 288 | 'entity_table' => $dao->entity_table, |
| 289 | 'contribution_id' => $dao->contribution_id, |
| 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), |
| 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, |
| 296 | ]; |
| 297 | $taxRates = CRM_Core_PseudoConstant::getTaxRates(); |
| 298 | if (isset($lineItems[$dao->id]['financial_type_id']) && array_key_exists($lineItems[$dao->id]['financial_type_id'], $taxRates)) { |
| 299 | // Cast to float so trailing zero decimals are removed for display. |
| 300 | $lineItems[$dao->id]['tax_rate'] = (float) $taxRates[$lineItems[$dao->id]['financial_type_id']]; |
| 301 | } |
| 302 | else { |
| 303 | // There is no Tax Rate associated with this Financial Type |
| 304 | $lineItems[$dao->id]['tax_rate'] = FALSE; |
| 305 | } |
| 306 | $lineItems[$dao->id]['subTotal'] = $lineItems[$dao->id]['qty'] * $lineItems[$dao->id]['unit_price']; |
| 307 | if ($lineItems[$dao->id]['tax_amount'] != '') { |
| 308 | $getTaxDetails = TRUE; |
| 309 | } |
| 310 | } |
| 311 | if ($invoicing) { |
| 312 | // @todo - this is an inappropriate place to be doing form level assignments. |
| 313 | $taxTerm = CRM_Utils_Array::value('tax_term', $invoiceSettings); |
| 314 | $smarty = CRM_Core_Smarty::singleton(); |
| 315 | $smarty->assign('taxTerm', $taxTerm); |
| 316 | $smarty->assign('getTaxDetails', $getTaxDetails); |
| 317 | } |
| 318 | return $lineItems; |
| 319 | } |
| 320 | |
| 321 | /** |
| 322 | * This method will create the lineItem array required for |
| 323 | * processAmount method |
| 324 | * |
| 325 | * @param int $fid |
| 326 | * Price set field id. |
| 327 | * @param array $params |
| 328 | * Reference to form values. |
| 329 | * @param array $fields |
| 330 | * Array of fields belonging to the price set used for particular event |
| 331 | * @param array $values |
| 332 | * Reference to the values array(. |
| 333 | * this is |
| 334 | * lineItem array) |
| 335 | * @param string $amount_override |
| 336 | * Amount override must be in format 1000.00 - ie no thousand separator & if |
| 337 | * a decimal point is used it should be a decimal |
| 338 | * |
| 339 | * @todo - this parameter is only used for partial payments. It's unclear why a partial |
| 340 | * payment would change the line item price. |
| 341 | */ |
| 342 | public static function format($fid, $params, $fields, &$values, $amount_override = NULL) { |
| 343 | if (empty($params["price_{$fid}"])) { |
| 344 | return; |
| 345 | } |
| 346 | |
| 347 | //lets first check in fun parameter, |
| 348 | //since user might modified w/ hooks. |
| 349 | $options = []; |
| 350 | if (array_key_exists('options', $fields)) { |
| 351 | $options = $fields['options']; |
| 352 | } |
| 353 | else { |
| 354 | CRM_Price_BAO_PriceFieldValue::getValues($fid, $options, 'weight', TRUE); |
| 355 | } |
| 356 | $fieldTitle = CRM_Utils_Array::value('label', $fields); |
| 357 | if (!$fieldTitle) { |
| 358 | $fieldTitle = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceField', $fid, 'label'); |
| 359 | } |
| 360 | |
| 361 | foreach ($params["price_{$fid}"] as $oid => $qty) { |
| 362 | $price = $amount_override === NULL ? $options[$oid]['amount'] : $amount_override; |
| 363 | |
| 364 | $participantsPerField = CRM_Utils_Array::value('count', $options[$oid], 0); |
| 365 | |
| 366 | $values[$oid] = [ |
| 367 | 'price_field_id' => $fid, |
| 368 | 'price_field_value_id' => $oid, |
| 369 | 'label' => CRM_Utils_Array::value('label', $options[$oid]), |
| 370 | 'field_title' => $fieldTitle, |
| 371 | 'description' => CRM_Utils_Array::value('description', $options[$oid]), |
| 372 | 'qty' => $qty, |
| 373 | 'unit_price' => $price, |
| 374 | 'line_total' => $qty * $price, |
| 375 | 'participant_count' => $qty * $participantsPerField, |
| 376 | 'max_value' => CRM_Utils_Array::value('max_value', $options[$oid]), |
| 377 | 'membership_type_id' => CRM_Utils_Array::value('membership_type_id', $options[$oid]), |
| 378 | 'membership_num_terms' => CRM_Utils_Array::value('membership_num_terms', $options[$oid]), |
| 379 | 'auto_renew' => CRM_Utils_Array::value('auto_renew', $options[$oid]), |
| 380 | 'html_type' => $fields['html_type'], |
| 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]), |
| 384 | ]; |
| 385 | |
| 386 | if ($values[$oid]['membership_type_id'] && empty($values[$oid]['auto_renew'])) { |
| 387 | $values[$oid]['auto_renew'] = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipType', $values[$oid]['membership_type_id'], 'auto_renew'); |
| 388 | } |
| 389 | } |
| 390 | } |
| 391 | |
| 392 | /** |
| 393 | * Delete line items for given entity. |
| 394 | * |
| 395 | * @param int $entityId |
| 396 | * @param int $entityTable |
| 397 | * |
| 398 | * @return bool |
| 399 | */ |
| 400 | public static function deleteLineItems($entityId, $entityTable) { |
| 401 | if (!$entityId || !$entityTable) { |
| 402 | return FALSE; |
| 403 | } |
| 404 | |
| 405 | if ($entityId && !is_array($entityId)) { |
| 406 | $entityId = [$entityId]; |
| 407 | } |
| 408 | |
| 409 | $query = "DELETE FROM civicrm_line_item where entity_id IN ('" . implode("','", $entityId) . "') AND entity_table = '$entityTable'"; |
| 410 | $dao = CRM_Core_DAO::executeQuery($query); |
| 411 | return TRUE; |
| 412 | } |
| 413 | |
| 414 | /** |
| 415 | * Process price set and line items. |
| 416 | * |
| 417 | * @param int $entityId |
| 418 | * @param array $lineItem |
| 419 | * Line item array. |
| 420 | * @param object $contributionDetails |
| 421 | * @param string $entityTable |
| 422 | * Entity table. |
| 423 | * |
| 424 | * @param bool $update |
| 425 | * |
| 426 | * @return void |
| 427 | */ |
| 428 | public static function processPriceSet($entityId, $lineItem, $contributionDetails = NULL, $entityTable = 'civicrm_contribution', $update = FALSE) { |
| 429 | if (!$entityId || !is_array($lineItem) |
| 430 | || CRM_Utils_system::isNull($lineItem) |
| 431 | ) { |
| 432 | return; |
| 433 | } |
| 434 | |
| 435 | foreach ($lineItem as $priceSetId => &$values) { |
| 436 | if (!$priceSetId) { |
| 437 | continue; |
| 438 | } |
| 439 | |
| 440 | foreach ($values as &$line) { |
| 441 | if (empty($line['entity_table'])) { |
| 442 | $line['entity_table'] = $entityTable; |
| 443 | } |
| 444 | if (empty($line['entity_id'])) { |
| 445 | $line['entity_id'] = $entityId; |
| 446 | } |
| 447 | if (!empty($line['membership_type_id'])) { |
| 448 | $line['entity_table'] = 'civicrm_membership'; |
| 449 | } |
| 450 | if (!empty($contributionDetails->id)) { |
| 451 | $line['contribution_id'] = $contributionDetails->id; |
| 452 | if ($line['entity_table'] == 'civicrm_contribution') { |
| 453 | $line['entity_id'] = $contributionDetails->id; |
| 454 | } |
| 455 | // CRM-19094: entity_table is set to civicrm_membership then ensure |
| 456 | // the entityId is set to membership ID not contribution by default |
| 457 | elseif ($line['entity_table'] == 'civicrm_membership' && !empty($line['entity_id']) && $line['entity_id'] == $contributionDetails->id) { |
| 458 | $membershipId = CRM_Core_DAO::getFieldValue('CRM_Member_DAO_MembershipPayment', $contributionDetails->id, 'membership_id', 'contribution_id'); |
| 459 | if ($membershipId && (int) $membershipId !== (int) $line['entity_id']) { |
| 460 | $line['entity_id'] = $membershipId; |
| 461 | Civi::log()->warning('Per https://lab.civicrm.org/dev/core/issues/15 this data fix should not be required. Please log a ticket at https://lab.civicrm.org/dev/core with steps to get this.', ['civi.tag' => 'deprecated']); |
| 462 | } |
| 463 | } |
| 464 | } |
| 465 | |
| 466 | // if financial type is not set and if price field value is NOT NULL |
| 467 | // get financial type id of price field value |
| 468 | if (!empty($line['price_field_value_id']) && empty($line['financial_type_id'])) { |
| 469 | $line['financial_type_id'] = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceFieldValue', $line['price_field_value_id'], 'financial_type_id'); |
| 470 | } |
| 471 | $lineItems = CRM_Price_BAO_LineItem::create($line); |
| 472 | if (!$update && $contributionDetails) { |
| 473 | $financialItem = CRM_Financial_BAO_FinancialItem::add($lineItems, $contributionDetails); |
| 474 | $line['financial_item_id'] = $financialItem->id; |
| 475 | if (!empty($line['tax_amount'])) { |
| 476 | CRM_Financial_BAO_FinancialItem::add($lineItems, $contributionDetails, TRUE); |
| 477 | } |
| 478 | } |
| 479 | } |
| 480 | } |
| 481 | if (!$update && $contributionDetails) { |
| 482 | CRM_Core_BAO_FinancialTrxn::createDeferredTrxn($lineItem, $contributionDetails); |
| 483 | } |
| 484 | } |
| 485 | |
| 486 | /** |
| 487 | * @param int $entityId |
| 488 | * @param string $entityTable |
| 489 | * @param $amount |
| 490 | * @param array $otherParams |
| 491 | */ |
| 492 | public static function syncLineItems($entityId, $entityTable = 'civicrm_contribution', $amount, $otherParams = NULL) { |
| 493 | if (!$entityId || CRM_Utils_System::isNull($amount)) { |
| 494 | return; |
| 495 | } |
| 496 | |
| 497 | $from = " civicrm_line_item li |
| 498 | LEFT JOIN civicrm_price_field pf ON pf.id = li.price_field_id |
| 499 | LEFT JOIN civicrm_price_set ps ON ps.id = pf.price_set_id "; |
| 500 | |
| 501 | $set = " li.unit_price = %3, |
| 502 | li.line_total = %3 "; |
| 503 | |
| 504 | $where = " li.entity_id = %1 AND |
| 505 | li.entity_table = %2 "; |
| 506 | |
| 507 | $params = [ |
| 508 | 1 => [$entityId, 'Integer'], |
| 509 | 2 => [$entityTable, 'String'], |
| 510 | 3 => [$amount, 'Float'], |
| 511 | ]; |
| 512 | |
| 513 | if ($entityTable == 'civicrm_contribution') { |
| 514 | $entityName = 'default_contribution_amount'; |
| 515 | $where .= " AND ps.name = %4 "; |
| 516 | $params[4] = [$entityName, 'String']; |
| 517 | } |
| 518 | elseif ($entityTable == 'civicrm_participant') { |
| 519 | $from .= " |
| 520 | LEFT JOIN civicrm_price_set_entity cpse ON cpse.price_set_id = ps.id |
| 521 | LEFT JOIN civicrm_price_field_value cpfv ON cpfv.price_field_id = pf.id and cpfv.label = %4 "; |
| 522 | $set .= " ,li.label = %4, |
| 523 | li.price_field_value_id = cpfv.id "; |
| 524 | $where .= " AND cpse.entity_table = 'civicrm_event' AND cpse.entity_id = %5 "; |
| 525 | $amount = empty($amount) ? 0 : $amount; |
| 526 | $params += [ |
| 527 | 4 => [$otherParams['fee_label'], 'String'], |
| 528 | 5 => [$otherParams['event_id'], 'String'], |
| 529 | ]; |
| 530 | } |
| 531 | |
| 532 | $query = " |
| 533 | UPDATE $from |
| 534 | SET $set |
| 535 | WHERE $where |
| 536 | "; |
| 537 | |
| 538 | CRM_Core_DAO::executeQuery($query, $params); |
| 539 | } |
| 540 | |
| 541 | /** |
| 542 | * Build line items array. |
| 543 | * |
| 544 | * @param array $params |
| 545 | * Form values. |
| 546 | * |
| 547 | * @param string $entityId |
| 548 | * Entity id. |
| 549 | * |
| 550 | * @param string $entityTable |
| 551 | * Entity Table. |
| 552 | * |
| 553 | * @param bool $isRelatedID |
| 554 | */ |
| 555 | public static function getLineItemArray(&$params, $entityId = NULL, $entityTable = 'contribution', $isRelatedID = FALSE) { |
| 556 | if (!$entityId) { |
| 557 | $priceSetDetails = CRM_Price_BAO_PriceSet::getDefaultPriceSet($entityTable); |
| 558 | $totalAmount = CRM_Utils_Array::value('partial_payment_total', $params, CRM_Utils_Array::value('total_amount', $params)); |
| 559 | $financialType = CRM_Utils_Array::value('financial_type_id', $params); |
| 560 | foreach ($priceSetDetails as $values) { |
| 561 | if ($entityTable == 'membership') { |
| 562 | if ($isRelatedID != $values['membership_type_id']) { |
| 563 | continue; |
| 564 | } |
| 565 | if (!$totalAmount) { |
| 566 | $totalAmount = $values['amount']; |
| 567 | } |
| 568 | $financialType = $values['financial_type_id']; |
| 569 | } |
| 570 | $params['line_item'][$values['setID']][$values['priceFieldID']] = [ |
| 571 | 'price_field_id' => $values['priceFieldID'], |
| 572 | 'price_field_value_id' => $values['priceFieldValueID'], |
| 573 | 'label' => $values['label'], |
| 574 | 'qty' => 1, |
| 575 | 'unit_price' => $totalAmount, |
| 576 | 'line_total' => $totalAmount, |
| 577 | 'financial_type_id' => $financialType, |
| 578 | 'membership_type_id' => $values['membership_type_id'], |
| 579 | ]; |
| 580 | break; |
| 581 | } |
| 582 | } |
| 583 | else { |
| 584 | $setID = NULL; |
| 585 | $totalEntityId = count($entityId); |
| 586 | if ($entityTable == 'contribution') { |
| 587 | $isRelatedID = TRUE; |
| 588 | } |
| 589 | foreach ($entityId as $id) { |
| 590 | $lineItems = CRM_Price_BAO_LineItem::getLineItems($id, $entityTable, FALSE, TRUE, $isRelatedID); |
| 591 | foreach ($lineItems as $key => $values) { |
| 592 | if (!$setID && $values['price_field_id']) { |
| 593 | $setID = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceField', $values['price_field_id'], 'price_set_id'); |
| 594 | $params['is_quick_config'] = CRM_Core_DAO::getFieldValue('CRM_Price_DAO_PriceSet', $setID, 'is_quick_config'); |
| 595 | } |
| 596 | if (!empty($params['is_quick_config']) && array_key_exists('total_amount', $params) |
| 597 | && $totalEntityId == 1 |
| 598 | ) { |
| 599 | $values['line_total'] = $values['unit_price'] = $params['total_amount']; |
| 600 | } |
| 601 | $values['id'] = $key; |
| 602 | $params['line_item'][$setID][$key] = $values; |
| 603 | } |
| 604 | } |
| 605 | } |
| 606 | } |
| 607 | |
| 608 | /** |
| 609 | * Build the line items for the submitted price field. |
| 610 | * |
| 611 | * This is when first building them - not an update where an entityId is already present |
| 612 | * as this is intended as a subfunction of that. Ideally getLineItemArray would call this |
| 613 | * (resolving to the same format regardless of what type of price set is being used first). |
| 614 | * |
| 615 | * @param array $priceParams |
| 616 | * These are per the way the form processes them - ie |
| 617 | * ['price_1' => 1, 'price_2' => 8] |
| 618 | * This would mean price field id 1, option 1 (or 1 unit if using is_enter_qty). |
| 619 | * @param float|NULL $overrideAmount |
| 620 | * Optional override of the amount. |
| 621 | * |
| 622 | * @param int|NULL $financialTypeID |
| 623 | * Financial type ID is the type should be overridden. |
| 624 | * |
| 625 | * @return array |
| 626 | * Line items formatted for processing. These will look like |
| 627 | * [4] => ['price_field_id' => 4, 'price_field_value_id' => x, 'label....qty...unit_price...line_total...financial_type_id] |
| 628 | * [5] => ['price_field_id' => 5, 'price_field_value_id' => x, 'label....qty...unit_price...line_total...financial_type_id] |
| 629 | * |
| 630 | */ |
| 631 | public static function buildLineItemsForSubmittedPriceField($priceParams, $overrideAmount = NULL, $financialTypeID = NULL) { |
| 632 | $lineItems = []; |
| 633 | foreach ($priceParams as $key => $value) { |
| 634 | $priceField = self::getPriceFieldMetaData($key); |
| 635 | |
| 636 | if ($priceField['html_type'] === 'Text') { |
| 637 | $valueSpec = reset($priceField['values']); |
| 638 | } |
| 639 | else { |
| 640 | $valueSpec = $priceField['values'][$value]; |
| 641 | } |
| 642 | $qty = $priceField['is_enter_qty'] ? $value : 1; |
| 643 | $lineItems[$priceField['id']] = [ |
| 644 | 'price_field_id' => $priceField['id'], |
| 645 | 'price_field_value_id' => $valueSpec['id'], |
| 646 | 'label' => $valueSpec['label'], |
| 647 | 'qty' => $qty, |
| 648 | 'unit_price' => $overrideAmount ?: $valueSpec['amount'], |
| 649 | 'line_total' => $qty * ($overrideAmount ?: $valueSpec['amount']), |
| 650 | 'financial_type_id' => $financialTypeID ?: $valueSpec['financial_type_id'], |
| 651 | 'membership_type_id' => $valueSpec['membership_type_id'], |
| 652 | 'price_set_id' => $priceField['price_set_id'], |
| 653 | ]; |
| 654 | } |
| 655 | return $lineItems; |
| 656 | } |
| 657 | |
| 658 | /** |
| 659 | * Function to update related contribution of a entity and |
| 660 | * add/update/cancel financial records |
| 661 | * |
| 662 | * @param array $params |
| 663 | * @param int $entityID |
| 664 | * @param int $entity |
| 665 | * @param int $contributionId |
| 666 | * @param $feeBlock |
| 667 | * @param array $lineItems |
| 668 | * |
| 669 | */ |
| 670 | public static function changeFeeSelections( |
| 671 | $params, |
| 672 | $entityID, |
| 673 | $entity, |
| 674 | $contributionId, |
| 675 | $feeBlock, |
| 676 | $lineItems |
| 677 | ) { |
| 678 | $entityTable = "civicrm_" . $entity; |
| 679 | CRM_Price_BAO_PriceSet::processAmount($feeBlock, |
| 680 | $params, $lineItems |
| 681 | ); |
| 682 | // initialize empty Lineitem instance to call protected helper functions |
| 683 | $lineItemObj = new CRM_Price_BAO_LineItem(); |
| 684 | |
| 685 | // fetch submitted LineItems from input params and feeBlock information |
| 686 | $submittedLineItems = $lineItemObj->getSubmittedLineItems($params, $feeBlock); |
| 687 | |
| 688 | $requiredChanges = $lineItemObj->getLineItemsToAlter($submittedLineItems, $entityID, $entity); |
| 689 | |
| 690 | // get financial information that need to be recorded on basis on submitted price field value IDs |
| 691 | if (!empty($requiredChanges['line_items_to_cancel']) || !empty($requiredChanges['line_items_to_update'])) { |
| 692 | // @todo - this IF is to get this through PR merge but I suspect that it should not |
| 693 | // be necessary & is masking something else. |
| 694 | $financialItemsArray = $lineItemObj->getAdjustedFinancialItemsToRecord( |
| 695 | $entityID, |
| 696 | $entityTable, |
| 697 | $contributionId, |
| 698 | array_keys($requiredChanges['line_items_to_cancel']), |
| 699 | $requiredChanges['line_items_to_update'] |
| 700 | ); |
| 701 | } |
| 702 | |
| 703 | // update line item with changed line total and other information |
| 704 | $totalParticipant = $participantCount = 0; |
| 705 | $amountLevel = []; |
| 706 | if (!empty($requiredChanges['line_items_to_update'])) { |
| 707 | foreach ($requiredChanges['line_items_to_update'] as $priceFieldValueID => $value) { |
| 708 | $amountLevel[] = $value['label'] . ' - ' . (float) $value['qty']; |
| 709 | if ($entity == 'participant' && isset($value['participant_count'])) { |
| 710 | $totalParticipant += $value['participant_count']; |
| 711 | } |
| 712 | } |
| 713 | } |
| 714 | |
| 715 | foreach (array_merge($requiredChanges['line_items_to_resurrect'], $requiredChanges['line_items_to_cancel'], $requiredChanges['line_items_to_update']) as $lineItemToAlter) { |
| 716 | // Must use BAO rather than api because a bad line it in the api which we want to avoid. |
| 717 | CRM_Price_BAO_LineItem::create($lineItemToAlter); |
| 718 | } |
| 719 | |
| 720 | $lineItemObj->addLineItemOnChangeFeeSelection($requiredChanges['line_items_to_add'], $entityID, $entityTable, $contributionId); |
| 721 | |
| 722 | $count = 0; |
| 723 | if ($entity == 'participant') { |
| 724 | $count = count(CRM_Event_BAO_Participant::getParticipantIds($contributionId)); |
| 725 | } |
| 726 | else { |
| 727 | $count = CRM_Utils_Array::value('count', civicrm_api3('MembershipPayment', 'getcount', ['contribution_id' => $contributionId])); |
| 728 | } |
| 729 | if ($count > 1) { |
| 730 | $updatedAmount = CRM_Price_BAO_LineItem::getLineTotal($contributionId); |
| 731 | } |
| 732 | else { |
| 733 | $updatedAmount = CRM_Utils_Array::value('amount', $params, CRM_Utils_Array::value('total_amount', $params)); |
| 734 | } |
| 735 | if (strlen($params['tax_amount']) != 0) { |
| 736 | $taxAmount = $params['tax_amount']; |
| 737 | } |
| 738 | else { |
| 739 | $taxAmount = "NULL"; |
| 740 | } |
| 741 | $displayParticipantCount = ''; |
| 742 | if ($totalParticipant > 0) { |
| 743 | $displayParticipantCount = ' Participant Count -' . $totalParticipant; |
| 744 | } |
| 745 | $updateAmountLevel = NULL; |
| 746 | if (!empty($amountLevel)) { |
| 747 | $updateAmountLevel = CRM_Core_DAO::VALUE_SEPARATOR . implode(CRM_Core_DAO::VALUE_SEPARATOR, $amountLevel) . $displayParticipantCount . CRM_Core_DAO::VALUE_SEPARATOR; |
| 748 | } |
| 749 | $trxn = $lineItemObj->_recordAdjustedAmt($updatedAmount, $contributionId, $taxAmount, $updateAmountLevel); |
| 750 | $contributionStatus = CRM_Core_PseudoConstant::getName('CRM_Contribute_DAO_Contribution', 'contribution_status_id', CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_Contribution', $contributionId, 'contribution_status_id')); |
| 751 | |
| 752 | if (!empty($financialItemsArray)) { |
| 753 | foreach ($financialItemsArray as $updateFinancialItemInfoValues) { |
| 754 | $newFinancialItem = CRM_Financial_BAO_FinancialItem::create($updateFinancialItemInfoValues); |
| 755 | // record reverse transaction only if Contribution is Completed because for pending refund or |
| 756 | // partially paid we are already recording the surplus owed or refund amount |
| 757 | if (!empty($updateFinancialItemInfoValues['financialTrxn']) && ($contributionStatus == 'Completed')) { |
| 758 | $updateFinancialItemInfoValues = array_merge($updateFinancialItemInfoValues['financialTrxn'], [ |
| 759 | 'entity_id' => $newFinancialItem->id, |
| 760 | 'entity_table' => 'civicrm_financial_item', |
| 761 | ]); |
| 762 | $reverseTrxn = CRM_Core_BAO_FinancialTrxn::create($updateFinancialItemInfoValues); |
| 763 | // record reverse entity financial trxn linked to membership's related contribution |
| 764 | civicrm_api3('EntityFinancialTrxn', 'create', [ |
| 765 | 'entity_table' => "civicrm_contribution", |
| 766 | 'entity_id' => $contributionId, |
| 767 | 'financial_trxn_id' => $reverseTrxn->id, |
| 768 | 'amount' => $reverseTrxn->total_amount, |
| 769 | ]); |
| 770 | unset($updateFinancialItemInfoValues['financialTrxn']); |
| 771 | } |
| 772 | elseif (!empty($updateFinancialItemInfoValues['link-financial-trxn']) && $newFinancialItem->amount != 0) { |
| 773 | civicrm_api3('EntityFinancialTrxn', 'create', [ |
| 774 | 'entity_id' => $newFinancialItem->id, |
| 775 | 'entity_table' => 'civicrm_financial_item', |
| 776 | 'financial_trxn_id' => $trxn->id, |
| 777 | 'amount' => $newFinancialItem->amount, |
| 778 | ]); |
| 779 | unset($updateFinancialItemInfoValues['link-financial-trxn']); |
| 780 | } |
| 781 | } |
| 782 | } |
| 783 | |
| 784 | // @todo - it may be that trxn_id is always empty - flush out scenarios. Add tests. |
| 785 | $trxnId = !empty($trxn->id) ? ['id' => $trxn->id] : []; |
| 786 | $lineItemObj->addFinancialItemsOnLineItemsChange(array_merge($requiredChanges['line_items_to_add'], $requiredChanges['line_items_to_resurrect']), $entityID, $entityTable, $contributionId, $trxnId); |
| 787 | |
| 788 | // update participant fee_amount column |
| 789 | $lineItemObj->updateEntityRecordOnChangeFeeSelection($params, $entityID, $entity); |
| 790 | } |
| 791 | |
| 792 | /** |
| 793 | * Function to retrieve financial items that need to be recorded as result of changed fee |
| 794 | * |
| 795 | * @param int $entityID |
| 796 | * @param string $entityTable |
| 797 | * @param int $contributionID |
| 798 | * @param array $priceFieldValueIDsToCancel |
| 799 | * @param array $lineItemsToUpdate |
| 800 | * |
| 801 | * @return array |
| 802 | * List of formatted reverse Financial Items to be recorded |
| 803 | */ |
| 804 | protected function getAdjustedFinancialItemsToRecord($entityID, $entityTable, $contributionID, $priceFieldValueIDsToCancel, $lineItemsToUpdate) { |
| 805 | $previousLineItems = CRM_Price_BAO_LineItem::getLineItems($entityID, str_replace('civicrm_', '', $entityTable)); |
| 806 | |
| 807 | $financialItemsArray = []; |
| 808 | $financialItemResult = $this->getNonCancelledFinancialItems($entityID, $entityTable); |
| 809 | foreach ($financialItemResult as $updateFinancialItemInfoValues) { |
| 810 | $updateFinancialItemInfoValues['transaction_date'] = date('YmdHis'); |
| 811 | |
| 812 | // the below params are not needed as we are creating new financial item |
| 813 | $previousFinancialItemID = $updateFinancialItemInfoValues['id']; |
| 814 | $totalFinancialAmount = $this->checkFinancialItemTotalAmountByLineItemID($updateFinancialItemInfoValues['entity_id']); |
| 815 | unset($updateFinancialItemInfoValues['id']); |
| 816 | unset($updateFinancialItemInfoValues['created_date']); |
| 817 | |
| 818 | // if not submitted and difference is not 0 make it negative |
| 819 | if ((empty($lineItemsToUpdate) || (in_array($updateFinancialItemInfoValues['price_field_value_id'], $priceFieldValueIDsToCancel) && |
| 820 | $totalFinancialAmount == $updateFinancialItemInfoValues['amount']) |
| 821 | ) && $updateFinancialItemInfoValues['amount'] > 0 |
| 822 | ) { |
| 823 | // INSERT negative financial_items |
| 824 | $updateFinancialItemInfoValues['amount'] = -$updateFinancialItemInfoValues['amount']; |
| 825 | // reverse the related financial trxn too |
| 826 | $updateFinancialItemInfoValues['financialTrxn'] = $this->getRelatedCancelFinancialTrxn($previousFinancialItemID); |
| 827 | if ($previousLineItems[$updateFinancialItemInfoValues['entity_id']]['tax_amount']) { |
| 828 | $updateFinancialItemInfoValues['tax']['amount'] = -($previousLineItems[$updateFinancialItemInfoValues['entity_id']]['tax_amount']); |
| 829 | $updateFinancialItemInfoValues['tax']['description'] = $this->getSalesTaxTerm(); |
| 830 | } |
| 831 | // INSERT negative financial_items for tax amount |
| 832 | $financialItemsArray[$updateFinancialItemInfoValues['entity_id']] = $updateFinancialItemInfoValues; |
| 833 | } |
| 834 | // INSERT a financial item to record surplus/lesser amount when a text price fee is changed |
| 835 | elseif (!empty($lineItemsToUpdate) && |
| 836 | $lineItemsToUpdate[$updateFinancialItemInfoValues['price_field_value_id']]['html_type'] == 'Text' && |
| 837 | $updateFinancialItemInfoValues['amount'] > 0 |
| 838 | ) { |
| 839 | // calculate the amount difference, considered as financial item amount |
| 840 | $updateFinancialItemInfoValues['amount'] = $lineItemsToUpdate[$updateFinancialItemInfoValues['price_field_value_id']]['line_total'] - $totalFinancialAmount; |
| 841 | // add a flag, later used to link financial trxn and this new financial item |
| 842 | $updateFinancialItemInfoValues['link-financial-trxn'] = TRUE; |
| 843 | if ($previousLineItems[$updateFinancialItemInfoValues['entity_id']]['tax_amount']) { |
| 844 | $updateFinancialItemInfoValues['tax']['amount'] = $lineItemsToUpdate[$updateFinancialItemInfoValues['entity_id']]['tax_amount'] - $previousLineItems[$updateFinancialItemInfoValues['entity_id']]['tax_amount']; |
| 845 | $updateFinancialItemInfoValues['tax']['description'] = $this->getSalesTaxTerm(); |
| 846 | } |
| 847 | $financialItemsArray[$updateFinancialItemInfoValues['entity_id']] = $updateFinancialItemInfoValues; |
| 848 | } |
| 849 | } |
| 850 | |
| 851 | return $financialItemsArray; |
| 852 | } |
| 853 | |
| 854 | /** |
| 855 | * Helper function to return sum of financial item's amount related to a line-item |
| 856 | * @param array $lineItemID |
| 857 | * |
| 858 | * @return float $financialItem |
| 859 | */ |
| 860 | protected function checkFinancialItemTotalAmountByLineItemID($lineItemID) { |
| 861 | return CRM_Core_DAO::singleValueQuery(" |
| 862 | SELECT SUM(amount) |
| 863 | FROM civicrm_financial_item |
| 864 | WHERE entity_table = 'civicrm_line_item' AND entity_id = {$lineItemID} |
| 865 | "); |
| 866 | } |
| 867 | |
| 868 | /** |
| 869 | * Helper function to retrieve submitted line items from form values $inputParams and used $feeBlock |
| 870 | * |
| 871 | * @param array $inputParams |
| 872 | * @param array $feeBlock |
| 873 | * |
| 874 | * @return array |
| 875 | * List of submitted line items |
| 876 | */ |
| 877 | protected function getSubmittedLineItems($inputParams, $feeBlock) { |
| 878 | $submittedLineItems = []; |
| 879 | foreach ($feeBlock as $id => $values) { |
| 880 | CRM_Price_BAO_LineItem::format($id, $inputParams, $values, $submittedLineItems); |
| 881 | } |
| 882 | |
| 883 | return $submittedLineItems; |
| 884 | } |
| 885 | |
| 886 | /** |
| 887 | * Helper function to retrieve line items that need to be altered. |
| 888 | * |
| 889 | * We iterate through the previous line items for the given entity to determine |
| 890 | * what alterations to line items need to be made to reflect the new line items. |
| 891 | * |
| 892 | * There are 4 possible changes required - per the keys in the return array. |
| 893 | * |
| 894 | * @param array $submittedLineItems |
| 895 | * @param int $entityID |
| 896 | * @param string $entity |
| 897 | * |
| 898 | * @return array |
| 899 | * Array of line items to alter with the following keys |
| 900 | * - line_items_to_add. If the line items required are new radio options that |
| 901 | * have not previously been set then we should add line items for them |
| 902 | * - line_items_to_update. If we have already been an active option and a change has |
| 903 | * happened then it should be in this array. |
| 904 | * - line_items_to_cancel. Line items currently selected but not selected in the new selection. |
| 905 | * These need to be zero'd out. |
| 906 | * - line_items_to_resurrect. Line items previously selected and then deselected. These need to be |
| 907 | * re-enabled rather than a new one added. |
| 908 | */ |
| 909 | protected function getLineItemsToAlter($submittedLineItems, $entityID, $entity) { |
| 910 | $previousLineItems = CRM_Price_BAO_LineItem::getLineItems($entityID, $entity); |
| 911 | |
| 912 | $lineItemsToAdd = $submittedLineItems; |
| 913 | $lineItemsToUpdate = []; |
| 914 | $submittedPriceFieldValueIDs = array_keys($submittedLineItems); |
| 915 | $lineItemsToCancel = $lineItemsToResurrect = []; |
| 916 | |
| 917 | foreach ($previousLineItems as $id => $previousLineItem) { |
| 918 | if (in_array($previousLineItem['price_field_value_id'], $submittedPriceFieldValueIDs)) { |
| 919 | $submittedLineItem = $submittedLineItems[$previousLineItem['price_field_value_id']]; |
| 920 | if (CRM_Utils_Array::value('html_type', $lineItemsToAdd[$previousLineItem['price_field_value_id']]) == 'Text') { |
| 921 | // If a 'Text' price field was updated by changing qty value, then we are not adding new line-item but updating the existing one, |
| 922 | // 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 |
| 923 | // exception here by adding financial item for updated line-item and will reverse any previous financial item entries. |
| 924 | $lineItemsToUpdate[$previousLineItem['price_field_value_id']] = array_merge($submittedLineItem, ['id' => $id]); |
| 925 | unset($lineItemsToAdd[$previousLineItem['price_field_value_id']]); |
| 926 | } |
| 927 | else { |
| 928 | $submittedLineItem = $submittedLineItems[$previousLineItem['price_field_value_id']]; |
| 929 | // for updating the line items i.e. use-case - once deselect-option selecting again |
| 930 | if (($previousLineItem['line_total'] != $submittedLineItem['line_total']) |
| 931 | || ( |
| 932 | // This would be a $0 line item - but why it should be catered to |
| 933 | // other than when the above condition is unclear. |
| 934 | $submittedLineItem['line_total'] == 0 && $submittedLineItem['qty'] == 1 |
| 935 | ) |
| 936 | || ( |
| 937 | $previousLineItem['qty'] != $submittedLineItem['qty'] |
| 938 | ) |
| 939 | ) { |
| 940 | $lineItemsToUpdate[$previousLineItem['price_field_value_id']] = $submittedLineItem; |
| 941 | $lineItemsToUpdate[$previousLineItem['price_field_value_id']]['id'] = $id; |
| 942 | // Format is actually '0.00' |
| 943 | if ($previousLineItem['line_total'] == 0) { |
| 944 | $lineItemsToAdd[$previousLineItem['price_field_value_id']]['id'] = $id; |
| 945 | $lineItemsToResurrect[] = $lineItemsToAdd[$previousLineItem['price_field_value_id']]; |
| 946 | } |
| 947 | } |
| 948 | // If there was previously a submitted line item for the same option value then there is |
| 949 | // either no change or a qty adjustment. In either case we are not doing an add + reversal. |
| 950 | unset($lineItemsToAdd[$previousLineItem['price_field_value_id']]); |
| 951 | unset($lineItemsToCancel[$previousLineItem['price_field_value_id']]); |
| 952 | } |
| 953 | } |
| 954 | else { |
| 955 | if (!$this->isCancelled($previousLineItem)) { |
| 956 | $cancelParams = ['qty' => 0, 'line_total' => 0, 'tax_amount' => 0, 'participant_count' => 0, 'non_deductible_amount' => 0, 'id' => $id]; |
| 957 | $lineItemsToCancel[$previousLineItem['price_field_value_id']] = array_merge($previousLineItem, $cancelParams); |
| 958 | |
| 959 | } |
| 960 | } |
| 961 | } |
| 962 | |
| 963 | return [ |
| 964 | 'line_items_to_add' => $lineItemsToAdd, |
| 965 | 'line_items_to_update' => $lineItemsToUpdate, |
| 966 | 'line_items_to_cancel' => $lineItemsToCancel, |
| 967 | 'line_items_to_resurrect' => $lineItemsToResurrect, |
| 968 | ]; |
| 969 | } |
| 970 | |
| 971 | /** |
| 972 | * Check if a line item has already been cancelled. |
| 973 | * |
| 974 | * @param array $lineItem |
| 975 | * |
| 976 | * @return bool |
| 977 | */ |
| 978 | protected function isCancelled($lineItem) { |
| 979 | if ($lineItem['qty'] == 0 && $lineItem['line_total'] == 0) { |
| 980 | return TRUE; |
| 981 | } |
| 982 | } |
| 983 | |
| 984 | /** |
| 985 | * Add line Items as result of fee change. |
| 986 | * |
| 987 | * @param array $lineItemsToAdd |
| 988 | * @param int $entityID |
| 989 | * @param string $entityTable |
| 990 | * @param int $contributionID |
| 991 | */ |
| 992 | protected function addLineItemOnChangeFeeSelection( |
| 993 | $lineItemsToAdd, |
| 994 | $entityID, |
| 995 | $entityTable, |
| 996 | $contributionID |
| 997 | ) { |
| 998 | // if there is no line item to add, do not proceed |
| 999 | if (empty($lineItemsToAdd)) { |
| 1000 | return; |
| 1001 | } |
| 1002 | |
| 1003 | $changedFinancialTypeID = NULL; |
| 1004 | $updatedContribution = new CRM_Contribute_BAO_Contribution(); |
| 1005 | $updatedContribution->id = (int) $contributionID; |
| 1006 | // insert financial items |
| 1007 | foreach ($lineItemsToAdd as $priceFieldValueID => $lineParams) { |
| 1008 | $lineParams = array_merge($lineParams, [ |
| 1009 | 'entity_table' => $entityTable, |
| 1010 | 'entity_id' => $entityID, |
| 1011 | 'contribution_id' => $contributionID, |
| 1012 | ]); |
| 1013 | if (!array_key_exists('skip', $lineParams)) { |
| 1014 | self::create($lineParams); |
| 1015 | } |
| 1016 | } |
| 1017 | |
| 1018 | if ($changedFinancialTypeID) { |
| 1019 | $updatedContribution->financial_type_id = $changedFinancialTypeID; |
| 1020 | $updatedContribution->save(); |
| 1021 | } |
| 1022 | } |
| 1023 | |
| 1024 | /** |
| 1025 | * Add financial transactions when an array of line items is changed. |
| 1026 | * |
| 1027 | * @param array $lineItemsToAdd |
| 1028 | * @param int $entityID |
| 1029 | * @param string $entityTable |
| 1030 | * @param int $contributionID |
| 1031 | * @param bool $isCreateAdditionalFinancialTrxn |
| 1032 | * Is there a change to the total balance requiring additional transactions to be created. |
| 1033 | */ |
| 1034 | protected function addFinancialItemsOnLineItemsChange($lineItemsToAdd, $entityID, $entityTable, $contributionID, $isCreateAdditionalFinancialTrxn) { |
| 1035 | $updatedContribution = new CRM_Contribute_BAO_Contribution(); |
| 1036 | $updatedContribution->id = $contributionID; |
| 1037 | $updatedContribution->find(TRUE); |
| 1038 | |
| 1039 | foreach ($lineItemsToAdd as $priceFieldValueID => $lineParams) { |
| 1040 | $lineParams = array_merge($lineParams, [ |
| 1041 | 'entity_table' => $entityTable, |
| 1042 | 'entity_id' => $entityID, |
| 1043 | 'contribution_id' => $contributionID, |
| 1044 | ]); |
| 1045 | $this->addFinancialItemsOnLineItemChange($isCreateAdditionalFinancialTrxn, $lineParams, $updatedContribution); |
| 1046 | } |
| 1047 | } |
| 1048 | |
| 1049 | /** |
| 1050 | * Helper function to update entity record on change fee selection |
| 1051 | * |
| 1052 | * @param array $inputParams |
| 1053 | * @param int $entityID |
| 1054 | * @param string $entity |
| 1055 | * |
| 1056 | */ |
| 1057 | protected function updateEntityRecordOnChangeFeeSelection($inputParams, $entityID, $entity) { |
| 1058 | $entityTable = "civicrm_{$entity}"; |
| 1059 | |
| 1060 | if ($entity == 'participant') { |
| 1061 | $partUpdateFeeAmt = ['id' => $entityID]; |
| 1062 | $getUpdatedLineItems = "SELECT * |
| 1063 | FROM civicrm_line_item |
| 1064 | WHERE (entity_table = '{$entityTable}' AND entity_id = {$entityID} AND qty > 0)"; |
| 1065 | $getUpdatedLineItemsDAO = CRM_Core_DAO::executeQuery($getUpdatedLineItems); |
| 1066 | $line = []; |
| 1067 | while ($getUpdatedLineItemsDAO->fetch()) { |
| 1068 | $line[$getUpdatedLineItemsDAO->price_field_value_id] = $getUpdatedLineItemsDAO->label . ' - ' . (float) $getUpdatedLineItemsDAO->qty; |
| 1069 | } |
| 1070 | |
| 1071 | $partUpdateFeeAmt['fee_level'] = implode(', ', $line); |
| 1072 | $partUpdateFeeAmt['fee_amount'] = $inputParams['amount']; |
| 1073 | CRM_Event_BAO_Participant::add($partUpdateFeeAmt); |
| 1074 | |
| 1075 | //activity creation |
| 1076 | CRM_Event_BAO_Participant::addActivityForSelection($entityID, 'Change Registration'); |
| 1077 | } |
| 1078 | } |
| 1079 | |
| 1080 | /** |
| 1081 | * Get the metadata for a price field. |
| 1082 | * |
| 1083 | * @param string|int $key |
| 1084 | * Price field id. Either as an integer or as 'price_4' where 4 is the id |
| 1085 | * |
| 1086 | * @return array |
| 1087 | * Metadata for the price field with a values key for option values. |
| 1088 | */ |
| 1089 | protected static function getPriceFieldMetaData($key) { |
| 1090 | $priceFieldID = str_replace('price_', '', $key); |
| 1091 | if (!isset(Civi::$statics[__CLASS__]['price_fields'][$priceFieldID])) { |
| 1092 | $values = civicrm_api3('PriceFieldValue', 'get', [ |
| 1093 | 'price_field_id' => $priceFieldID, |
| 1094 | 'return' => [ |
| 1095 | 'id', |
| 1096 | 'amount', |
| 1097 | 'financial_type_id', |
| 1098 | 'membership_type_id', |
| 1099 | 'label', |
| 1100 | 'price_field_id', |
| 1101 | 'price_field_id.price_set_id', |
| 1102 | 'price_field_id.html_type', |
| 1103 | 'is_enter_qty', |
| 1104 | ], |
| 1105 | ]); |
| 1106 | $firstValue = reset($values['values']); |
| 1107 | $values = $values['values']; |
| 1108 | foreach ($values as $index => $value) { |
| 1109 | // Let's be nice to calling functions & ensure membership_type_id is set |
| 1110 | // so they don't have to handle notices on it. Handle one place not many. |
| 1111 | if (!isset($value['membership_type_id'])) { |
| 1112 | $values[$index]['membership_type_id'] = NULL; |
| 1113 | } |
| 1114 | } |
| 1115 | |
| 1116 | Civi::$statics[__CLASS__]['price_fields'][$priceFieldID] = [ |
| 1117 | 'price_set_id' => $firstValue['price_field_id.price_set_id'], |
| 1118 | 'id' => $firstValue['price_field_id'], |
| 1119 | 'html_type' => $firstValue['price_field_id.html_type'], |
| 1120 | 'is_enter_qty' => !empty($firstValue['is_enter_qty']), |
| 1121 | 'values' => $values, |
| 1122 | ]; |
| 1123 | } |
| 1124 | return Civi::$statics[__CLASS__]['price_fields'][$priceFieldID]; |
| 1125 | } |
| 1126 | |
| 1127 | /** |
| 1128 | * Helper function to retrieve financial trxn parameters to reverse |
| 1129 | * for given financial item identified by $financialItemID |
| 1130 | * |
| 1131 | * @param int $financialItemID |
| 1132 | * |
| 1133 | * @return array $financialTrxn |
| 1134 | * |
| 1135 | */ |
| 1136 | protected function _getRelatedCancelFinancialTrxn($financialItemID) { |
| 1137 | try { |
| 1138 | $financialTrxn = civicrm_api3('EntityFinancialTrxn', 'getsingle', [ |
| 1139 | 'entity_table' => 'civicrm_financial_item', |
| 1140 | 'entity_id' => $financialItemID, |
| 1141 | 'options' => [ |
| 1142 | 'sort' => 'id DESC', |
| 1143 | 'limit' => 1, |
| 1144 | ], |
| 1145 | 'api.FinancialTrxn.getsingle' => [ |
| 1146 | 'id' => "\$value.financial_trxn_id", |
| 1147 | ], |
| 1148 | ]); |
| 1149 | } |
| 1150 | catch (CiviCRM_API3_Exception $e) { |
| 1151 | return []; |
| 1152 | } |
| 1153 | |
| 1154 | $financialTrxn = array_merge($financialTrxn['api.FinancialTrxn.getsingle'], [ |
| 1155 | 'trxn_date' => date('YmdHis'), |
| 1156 | 'total_amount' => -$financialTrxn['api.FinancialTrxn.getsingle']['total_amount'], |
| 1157 | 'net_amount' => -$financialTrxn['api.FinancialTrxn.getsingle']['net_amount'], |
| 1158 | 'entity_table' => 'civicrm_financial_item', |
| 1159 | 'entity_id' => $financialItemID, |
| 1160 | ]); |
| 1161 | unset($financialTrxn['id']); |
| 1162 | |
| 1163 | return $financialTrxn; |
| 1164 | } |
| 1165 | |
| 1166 | /** |
| 1167 | * Record adjusted amount. |
| 1168 | * |
| 1169 | * @param int $updatedAmount |
| 1170 | * @param int $contributionId |
| 1171 | * @param int $taxAmount |
| 1172 | * @param bool $updateAmountLevel |
| 1173 | * |
| 1174 | * @return bool|\CRM_Core_BAO_FinancialTrxn |
| 1175 | */ |
| 1176 | protected function _recordAdjustedAmt($updatedAmount, $contributionId, $taxAmount = NULL, $updateAmountLevel = NULL) { |
| 1177 | $paidAmount = CRM_Core_BAO_FinancialTrxn::getTotalPayments($contributionId); |
| 1178 | $balanceAmt = $updatedAmount - $paidAmount; |
| 1179 | |
| 1180 | $contributionStatuses = CRM_Contribute_PseudoConstant::contributionStatus(NULL, 'name'); |
| 1181 | $partiallyPaidStatusId = array_search('Partially paid', $contributionStatuses); |
| 1182 | $pendingRefundStatusId = array_search('Pending refund', $contributionStatuses); |
| 1183 | $completedStatusId = array_search('Completed', $contributionStatuses); |
| 1184 | |
| 1185 | $updatedContributionDAO = new CRM_Contribute_BAO_Contribution(); |
| 1186 | $adjustedTrxn = $skip = FALSE; |
| 1187 | if ($balanceAmt) { |
| 1188 | if ($balanceAmt > 0 && $paidAmount != 0) { |
| 1189 | $contributionStatusVal = $partiallyPaidStatusId; |
| 1190 | } |
| 1191 | elseif ($balanceAmt < 0 && $paidAmount != 0) { |
| 1192 | $contributionStatusVal = $pendingRefundStatusId; |
| 1193 | } |
| 1194 | elseif ($paidAmount == 0) { |
| 1195 | //skip updating the contribution status if no payment is made |
| 1196 | $skip = TRUE; |
| 1197 | $updatedContributionDAO->cancel_date = 'null'; |
| 1198 | $updatedContributionDAO->cancel_reason = NULL; |
| 1199 | } |
| 1200 | // update contribution status and total amount without trigger financial code |
| 1201 | // as this is handled in current BAO function used for change selection |
| 1202 | $updatedContributionDAO->id = $contributionId; |
| 1203 | if (!$skip) { |
| 1204 | $updatedContributionDAO->contribution_status_id = $contributionStatusVal; |
| 1205 | } |
| 1206 | $updatedContributionDAO->total_amount = $updatedContributionDAO->net_amount = $updatedAmount; |
| 1207 | $updatedContributionDAO->fee_amount = 0; |
| 1208 | $updatedContributionDAO->tax_amount = $taxAmount; |
| 1209 | if (!empty($updateAmountLevel)) { |
| 1210 | $updatedContributionDAO->amount_level = $updateAmountLevel; |
| 1211 | } |
| 1212 | $updatedContributionDAO->save(); |
| 1213 | // adjusted amount financial_trxn creation |
| 1214 | $updatedContribution = CRM_Contribute_BAO_Contribution::getValues( |
| 1215 | ['id' => $contributionId], |
| 1216 | CRM_Core_DAO::$_nullArray, |
| 1217 | CRM_Core_DAO::$_nullArray |
| 1218 | ); |
| 1219 | $toFinancialAccount = CRM_Contribute_PseudoConstant::getRelationalFinancialAccount($updatedContribution->financial_type_id, 'Accounts Receivable Account is'); |
| 1220 | $adjustedTrxnValues = [ |
| 1221 | 'from_financial_account_id' => NULL, |
| 1222 | 'to_financial_account_id' => $toFinancialAccount, |
| 1223 | 'total_amount' => $balanceAmt, |
| 1224 | 'net_amount' => $balanceAmt, |
| 1225 | 'status_id' => $completedStatusId, |
| 1226 | 'payment_instrument_id' => $updatedContribution->payment_instrument_id, |
| 1227 | 'contribution_id' => $updatedContribution->id, |
| 1228 | 'trxn_date' => date('YmdHis'), |
| 1229 | 'currency' => $updatedContribution->currency, |
| 1230 | ]; |
| 1231 | $adjustedTrxn = CRM_Core_BAO_FinancialTrxn::create($adjustedTrxnValues); |
| 1232 | } |
| 1233 | // CRM-17151: Update the contribution status to completed if balance is zero, |
| 1234 | // because due to sucessive fee change will leave the related contribution status incorrect |
| 1235 | else { |
| 1236 | CRM_Core_DAO::setFieldValue('CRM_Contribute_DAO_Contribution', $contributionId, 'contribution_status_id', $completedStatusId); |
| 1237 | } |
| 1238 | |
| 1239 | return $adjustedTrxn; |
| 1240 | } |
| 1241 | |
| 1242 | /** |
| 1243 | * Add financial items to reflect line item change. |
| 1244 | * |
| 1245 | * @param bool $isCreateAdditionalFinancialTrxn |
| 1246 | * @param array $lineParams |
| 1247 | * @param \CRM_Contribute_BAO_Contribution $updatedContribution |
| 1248 | */ |
| 1249 | protected function addFinancialItemsOnLineItemChange($isCreateAdditionalFinancialTrxn, $lineParams, $updatedContribution) { |
| 1250 | $tempFinancialTrxnID = NULL; |
| 1251 | // don't add financial item for cancelled line item |
| 1252 | if ($lineParams['qty'] == 0) { |
| 1253 | return; |
| 1254 | } |
| 1255 | elseif ($isCreateAdditionalFinancialTrxn) { |
| 1256 | // This routine & the return below is super uncomfortable. |
| 1257 | // I have refactored to here but don't understand how this would be hit |
| 1258 | // and it is how it would be a good thing, given the odd return below which |
| 1259 | // does not seem consistent with what is going on. |
| 1260 | // I'm tempted to add an e-deprecated into it to confirm my suspicion it only exists to |
| 1261 | // cause mental anguish. |
| 1262 | // original comment : add financial item if ONLY financial type is changed |
| 1263 | if ($lineParams['financial_type_id'] != $updatedContribution->financial_type_id) { |
| 1264 | $changedFinancialTypeID = (int) $lineParams['financial_type_id']; |
| 1265 | $adjustedTrxnValues = [ |
| 1266 | 'from_financial_account_id' => NULL, |
| 1267 | 'to_financial_account_id' => CRM_Financial_BAO_FinancialTypeAccount::getInstrumentFinancialAccount($updatedContribution->payment_instrument_id), |
| 1268 | 'total_amount' => $lineParams['line_total'], |
| 1269 | 'net_amount' => $lineParams['line_total'], |
| 1270 | 'status_id' => $updatedContribution->contribution_status_id, |
| 1271 | 'payment_instrument_id' => $updatedContribution->payment_instrument_id, |
| 1272 | 'contribution_id' => $updatedContribution->id, |
| 1273 | 'is_payment' => TRUE, |
| 1274 | // since balance is 0, which means contribution is completed |
| 1275 | 'trxn_date' => date('YmdHis'), |
| 1276 | 'currency' => $updatedContribution->currency, |
| 1277 | ]; |
| 1278 | $adjustedTrxn = CRM_Core_BAO_FinancialTrxn::create($adjustedTrxnValues); |
| 1279 | $tempFinancialTrxnID = ['id' => $adjustedTrxn->id]; |
| 1280 | } |
| 1281 | } |
| 1282 | $lineObj = CRM_Price_BAO_LineItem::retrieve($lineParams, CRM_Core_DAO::$_nullArray); |
| 1283 | // insert financial items |
| 1284 | // ensure entity_financial_trxn table has a linking of it. |
| 1285 | CRM_Financial_BAO_FinancialItem::add($lineObj, $updatedContribution, NULL, $tempFinancialTrxnID); |
| 1286 | if (isset($lineObj->tax_amount)) { |
| 1287 | CRM_Financial_BAO_FinancialItem::add($lineObj, $updatedContribution, TRUE, $tempFinancialTrxnID); |
| 1288 | } |
| 1289 | } |
| 1290 | |
| 1291 | /** |
| 1292 | * Get Financial items, culling out any that have already been reversed. |
| 1293 | * |
| 1294 | * @param int $entityID |
| 1295 | * @param string $entityTable |
| 1296 | * |
| 1297 | * @return array |
| 1298 | * Array of financial items that have not be reversed. |
| 1299 | */ |
| 1300 | protected function getNonCancelledFinancialItems($entityID, $entityTable) { |
| 1301 | // gathering necessary info to record negative (deselected) financial_item |
| 1302 | $updateFinancialItem = " |
| 1303 | SELECT fi.*, price_field_value_id, financial_type_id, tax_amount |
| 1304 | FROM civicrm_financial_item fi LEFT JOIN civicrm_line_item li ON (li.id = fi.entity_id AND fi.entity_table = 'civicrm_line_item') |
| 1305 | WHERE (li.entity_table = '{$entityTable}' AND li.entity_id = {$entityID}) |
| 1306 | GROUP BY li.entity_table, li.entity_id, price_field_value_id, fi.id |
| 1307 | "; |
| 1308 | $updateFinancialItemInfoDAO = CRM_Core_DAO::executeQuery($updateFinancialItem); |
| 1309 | |
| 1310 | $financialItemResult = $updateFinancialItemInfoDAO->fetchAll(); |
| 1311 | $items = []; |
| 1312 | foreach ($financialItemResult as $index => $financialItem) { |
| 1313 | $items[$financialItem['price_field_value_id']][$index] = $financialItem['amount']; |
| 1314 | |
| 1315 | if (!empty($items[$financialItem['price_field_value_id']])) { |
| 1316 | foreach ($items[$financialItem['price_field_value_id']] as $existingItemID => $existingAmount) { |
| 1317 | if ($financialItem['amount'] + $existingAmount == 0) { |
| 1318 | // Filter both rows as they cancel each other out. |
| 1319 | unset($financialItemResult[$index]); |
| 1320 | unset($financialItemResult[$existingItemID]); |
| 1321 | unset($items['price_field_value_id'][$existingItemID]); |
| 1322 | unset($items[$financialItem['price_field_value_id']][$index]); |
| 1323 | } |
| 1324 | } |
| 1325 | |
| 1326 | } |
| 1327 | |
| 1328 | } |
| 1329 | return $financialItemResult; |
| 1330 | } |
| 1331 | |
| 1332 | /** |
| 1333 | * Get the string used to describe the sales tax (eg. VAT, GST). |
| 1334 | * |
| 1335 | * @return string |
| 1336 | */ |
| 1337 | protected function getSalesTaxTerm() { |
| 1338 | return CRM_Contribute_BAO_Contribution::checkContributeSettings('tax_term'); |
| 1339 | } |
| 1340 | |
| 1341 | } |