/** * Check a new set of line items against the existing set and update/delete as necessary * * Note: line numbers should be computed client-side and thus shouldn't need to be recalculated. * * @param array $items Each entry is an associative array of QuoteProduct [attribute]=>[value] * pairs * @param integer $quoteId ID of quote for which to update items * @param bool $save Whether or not to save changes in the database after finishing * @return array Array of QuoteProduct instances representing the item set after changes. * @throws CException */ public function setLineItems(array $items, $save = false, $skipProcessing = false) { if ($skipProcessing) { $this->_lineItems = $items; return; } $this->_deleteLineItems = array(); if (count($items) === 0) { QuoteProduct::model()->deleteAllByAttributes(array('quoteId' => $this->id)); return true; } // Check for valid input: $typeErrMsg = 'The setter of Quote.lineItems requires an array of QuoteProduct objects or ' . '[attribute]=>[value] arrays.'; $firstElt = reset($items); $type = gettype($firstElt); if ($type != 'object' && $type != 'array') { // Must be one or the other throw new Exception($typeErrMsg); } if ($type == 'object') { // If object, must be of the QuoteProduct class if (get_class($firstElt) != 'QuoteProduct') { throw new Exception($typeErrMsg); } } // Gather existing line items into an array indexed by ID. $existingItemIds = array(); $newItems = array(); $itemSet = array(); $existingItems = array(); foreach ($this->lineItems as $item) { if ($item->isNewRecord) { // this line might not be needed anymore. Used to be used for record duplication, // but now now skipProcessing is used instead, bypassing this line. $item->save(); } $existingItems[$item->id] = $item; $existingItemIds[] = (int) $item->id; } // Gather the new set of line items into arrays if (isset($items[''])) { unset($items['']); } if ($type == 'object') { foreach ($items as $item) { if (in_array($item->id, $existingItemIds)) { $itemSet[$item->id] = $existingItems[$item->id]; $itemSet[$item->id]->attributes = $item->attributes; } else { $newItems[] = $item; } } } else { if ($type == 'array') { foreach ($items as $item) { $new = false; if (isset($item['id'])) { $id = $item['id']; if (in_array($id, $existingItemIds)) { $itemSet[$id] = $existingItems[$item['id']]; $itemSet[$id]->attributes = $item; } else { $new = true; } } else { $new = true; } if ($new) { $itemObj = new QuoteProduct(); $itemObj->attributes = $item; $newItems[] = $itemObj; } } } } // Compute set changes: $itemIds = array_keys($itemSet); $deleteItemIds = array_diff($existingItemIds, $itemIds); $updateItemIds = array_intersect($existingItemIds, $itemIds); // Put all the items together into the same arrays $this->_lineItems = array_merge($newItems, array_values($itemSet)); usort($this->_lineItems, 'self::lineItemOrder'); $this->_deleteLineItems = array_map(function ($id) use($existingItems) { return $existingItems[$id]; }, $deleteItemIds); // Remove symbols from numerical input values and convert to numeric. // Behavior: // - Use the quote's currency if it isn't empty. // - Use the app's currency otherwise. $defaultCurrency = empty($this->currency) ? Yii::app()->settings->currency : $this->currency; $curSym = Yii::app()->locale->getCurrencySymbol($defaultCurrency); if (is_null($curSym)) { $curSym = $defaultCurrency; } foreach ($this->_lineItems as $lineItem) { $lineItem->quoteId = $this->id; $product = X2Model::model('Products')->findByAttributes(array('name' => $lineItem->name)); if (isset($product)) { $lineItem->productId = $product->id; } if (empty($lineItem->currency)) { $lineItem->currency = $defaultCurrency; } if ($lineItem->isPercentAdjustment) { $lineItem->adjustment = Fields::strToNumeric($lineItem->adjustment, 'percentage'); } else { $lineItem->adjustment = Fields::strToNumeric($lineItem->adjustment, 'currency', $curSym); } $lineItem->price = Fields::strToNumeric($lineItem->price, 'currency', $curSym); $lineItem->total = Fields::strToNumeric($lineItem->total, 'currency', $curSym); } // Validate $this->hasLineItemErrors = false; $this->lineItemErrors = array(); foreach ($this->_lineItems as $item) { $itemValid = $item->validate(); if (!$itemValid) { $this->hasLineItemErrors = true; foreach ($item->errors as $attribute => $errors) { foreach ($errors as $error) { $this->lineItemErrors[] = $error; } } } } $this->lineItemErrors = array_unique($this->lineItemErrors); // Reset derived properties: $this->_adjustmentLines = null; $this->_productLines = null; // Save if ($save && !$this->hasLineItemErrors) { $this->saveLineItems(); } }
/** * Sets attributes using X2Fields * @param array &$data array of attributes to be set (eg. $_POST['Contacts']) * @param bool $filter encode all HTML special characters in input * @param bool $bypassPermissions (optional) */ public function setX2Fields(&$data, $filter = false, $bypassPermissions = false) { $editableFieldsFieldNames = $this->getEditableFieldNames(); // loop through fields to deal with special types foreach (self::$_fields[$this->tableName()] as &$field) { $fieldName = $field->fieldName; // skip fields that are read-only or haven't been set if (!isset($data[$fieldName]) || !$bypassPermissions && !in_array($fieldName, $editableFieldsFieldNames)) { if (isset($data[$fieldName]) && !in_array($fieldName, $editableFieldsFieldNames)) { //if (YII_DEBUG) //printR('setX2Fields: Warning: ' . $fieldName . ' set'); } continue; } // eliminate placeholder values if ($data[$fieldName] === $this->getAttributeLabel($fieldName) && $field->type !== 'dropdown') { $data[$fieldName] = null; } if ($field->type === 'currency') { $defaultCurrency = Yii::app()->settings->currency; $curSym = Yii::app()->locale->getCurrencySymbol($defaultCurrency); if (is_null($curSym)) { $curSym = $defaultCurrency; } $data[$fieldName] = Fields::strToNumeric($data[$fieldName], 'currency', $curSym); } if ($field->type === 'link') { // Do a preliminary lookup for linkId in case there are // duplicates (similar name) and the user selects one of them, // in which case there is an ID from the form that was populated // via the auto-complete input widget: $linkId = null; if (isset($data[$fieldName . '_id'])) { // get the linked model's ID from the hidden autocomplete field $linkId = $data[$fieldName . '_id']; } if (ctype_digit((string) $linkId)) { $link = Yii::app()->db->createCommand()->select('name,nameId')->from(X2Model::model($field->linkType)->tableName())->where('id=?', array($linkId))->queryRow(true); // Make sure the linked model exists and that the name matches: if (isset($link['name']) && $link['name'] === $data[$fieldName]) { $data[$fieldName] = $link['nameId']; } } } $this->{$fieldName} = $field->parseValue($data[$fieldName], $filter); } // Set default values. // // This should only happen in the case that the field was not included // in the form submission data (with the exception of assignment fields), and the field is // empty, and the record is new. if ($this->getIsNewRecord() && $this->scenario == 'insert') { // Set default values foreach ($this->getFields(true) as $fieldName => $field) { if (!isset($data[$fieldName]) && $this->{$fieldName} == '' && $field->defaultValue != null && !$field->readOnly) { $this->{$fieldName} = $field->defaultValue; } else { if ($this->{$fieldName} === null && $field->defaultValue === null && $field->type === 'assignment') { $this->{$fieldName} = self::getDefaultAssignment(); } } } } }
public function testStrToNumeric() { $cur = Yii::app()->locale->getCurrencySymbol(Yii::app()->settings->currency); $input = " {$cur} 123.45 % "; $this->assertEquals(123.45, Fields::strToNumeric($input, 'currency')); $this->assertEquals(123, Fields::strToNumeric($input, 'int')); $this->assertEquals(123.45, Fields::strToNumeric($input, 'float')); $this->assertEquals(123.45, Fields::strToNumeric($input, 'percentage')); $this->assertEquals(0, Fields::strToNumeric(null, 'float')); $type = 'notanint'; $value = Fields::strToNumeric($input, $type); $this->assertEquals(123.45, $value); // Randumb string comes back as itself $input = 'cockadoodledoo'; $value = Fields::strToNumeric($input, 'int'); $this->assertEquals($input, $value); // Null always evaluates to zero $value = Fields::strToNumeric(''); $this->assertEquals(0, $value); // Parsing of parenthesized notation for negative currency values $value = Fields::strToNumeric('($45.82)', 'currency'); $this->assertEquals(-45.82, $value); // Negative percentage values: $value = Fields::strToNumeric('-12.5%', 'percentage'); $this->assertEquals(-12.5, $value); // Comma notation for thousands: $value = Fields::strToNumeric('$9,888.77', 'currency'); $this->assertEquals(9888.77, $value); // Comma plus parentheses notation $value = Fields::strToNumeric('($9,888.77)', 'currency'); $this->assertEquals(-9888.77, $value); // Comma and minus sign notation: $value = Fields::strToNumeric('-$9,888.77', 'currency'); $this->assertEquals(-9888.77, $value); // Rounded to integer, over 10^6: $value = Fields::strToNumeric('$10,000,000', 'currency'); $this->assertEquals(10000000, $value); // ...negative $value = Fields::strToNumeric('($10,000,000)', 'currency'); $this->assertEquals(-10000000, $value); // ...with decimal places $value = Fields::strToNumeric('($10,000,000.01)', 'currency'); $this->assertEquals(-10000000.01, $value); // Multibyte support: $curSym = Yii::app()->locale->getCurrencySymbol('INR'); $value = Fields::strToNumeric("({$curSym}" . "9,888.77)", 'currency', $curSym); $this->assertEquals(-9888.77, $value, 'Failed asserting proper conversion of multibyte strings to numbers.'); }