diff --git a/commerce.module b/commerce.module index d1402f6..8b4910a 100644 --- a/commerce.module +++ b/commerce.module @@ -323,35 +323,43 @@ function commerce_currency_get_symbol($currency_code = NULL) { * Formats a price for a particular currency. * * @param $price - * A numeric price value. + * A numeric price amount value. * @param $currency_code * The three character code of the currency. * @param $object * When present, the object to which the price is attached. + * @param $convert + * Boolean indicating whether or not the amount needs to be converted to a + * decimal price amount when formatting. * * @return * A fully formatted currency. */ -function commerce_currency_format($price, $currency_code, $object = NULL) { +function commerce_currency_format($amount, $currency_code, $object = NULL, $convert = TRUE) { // First load the currency array. $currency = commerce_currency_load($currency_code); + // Then convert the price amount to the currency's major unit decimal value. + if ($convert = TRUE) { + $amount = commerce_currency_amount_to_decimal($amount, $currency_code); + } + // Invoke the custom format callback if specified. if (!empty($currency['format_callback'])) { - return $currency['format_callback']($price, $currency, $object); + return $currency['format_callback']($amount, $currency, $object); } // Separate the negative symbol from the number itself. - if ($price < 0) { + if ($amount < 0) { $negative = TRUE; - $price = abs($price); + $price = abs($amount); } else { $negative = FALSE; } // Format the price as a number. - $price = number_format(commerce_currency_round($price, $currency), $currency['decimals'], $currency['decimal_separator'], $currency['thousands_separator']); + $price = number_format(commerce_currency_round($amount, $currency), $currency['decimals'], $currency['decimal_separator'], $currency['thousands_separator']); // Establish the replacement values to format this price for its currency. $replacements = array( @@ -363,7 +371,7 @@ function commerce_currency_format($price, $currency_code, $object = NULL) { '@negative' => $negative ? '-' : '', ); - return t('@code_before @negative@symbol_before@price @symbol_after @code_after', $replacements); + return trim(t('@code_before @negative@symbol_before@price @symbol_after @code_after', $replacements)); } /** @@ -431,16 +439,19 @@ function commerce_currency_convert($amount, $currency_code, $target_currency_cod /** * Converts a price amount to an integer value for storage in the database. * - * @param $amount - * The price amount to convert to an integer. + * @param $decimal + * The decimal amount to convert to a price amount. * @param $currency_code * The currency code of the price whose decimals value will be used to - * multiply by the proper factor when converting the amount. + * multiply by the proper factor when converting the decimal amount. + * @param $round + * Whether or not the return value should be rounded and cast to an integer; + * defaults to TRUE as necessary for standard price amount column storage. * * @return - * The integer value of the price amount. + * The appropriate price amount based on the currency's decimals value. */ -function commerce_currency_amount_to_integer($amount, $currency_code) { +function commerce_currency_decimal_to_amount($decimal, $currency_code, $round = TRUE) { static $factors; // If the divisor for this currency hasn't been calculated yet... @@ -451,24 +462,28 @@ function commerce_currency_amount_to_integer($amount, $currency_code) { } // Ensure the amount has the proper number of decimal places for the currency. - $amount = commerce_currency_round($amount, commerce_currency_load($currency_code)); - - return (int) ($amount * $factors[$currency_code]); + if ($round) { + $decimal = commerce_currency_round($decimal, commerce_currency_load($currency_code)); + return (int) ($decimal * $factors[$currency_code]); + } + else { + return $decimal * $factors[$currency_code]; + } } /** - * Converts an integer value to a price amount when loading from the database. + * Converts a price amount to a decimal value based on the currency. * - * @param $integer - * The integer to convert to a price amount. + * @param $amount + * The price amount to convert to a decimal value. * @param $currency_code * The currency code of the price whose decimals value will be used to - * divide by the proper divisor when converting the integer. + * divide by the proper divisor when converting the amount. * * @return - * The price amount depending on the number of decimals the currency uses. + * The decimal amount depending on the number of places the currency uses. */ -function commerce_currency_integer_to_amount($integer, $currency_code) { +function commerce_currency_amount_to_decimal($amount, $currency_code) { static $divisors; // If the divisor for this currency hasn't been calculated yet... @@ -478,7 +493,7 @@ function commerce_currency_integer_to_amount($integer, $currency_code) { $divisors[$currency_code] = pow(10, $currency['decimals']); } - return $integer / $divisors[$currency_code]; + return $amount / $divisors[$currency_code]; } /** diff --git a/modules/cart/commerce_cart.module b/modules/cart/commerce_cart.module index 014786c..4972c83 100644 --- a/modules/cart/commerce_cart.module +++ b/modules/cart/commerce_cart.module @@ -597,10 +597,8 @@ function commerce_cart_order_refresh($order) { $data['components'] = $current_data['components']; $line_item_wrapper->commerce_unit_price->data = $data; - // Save the updated line item and clear the entity cache. Use a clone so - // the save doesn't interfere with price values in a live line item object - // that would be modified on presave. - commerce_line_item_save(clone($line_item_wrapper->value())); + // Save the updated line item and clear the entity cache. + commerce_line_item_save($line_item_wrapper->value()); entity_get_controller('commerce_line_item')->resetCache(array($line_item_wrapper->line_item_id->value())); $line_item_changed = TRUE; @@ -617,7 +615,7 @@ function commerce_cart_order_refresh($order) { if ($order_wrapper->value() != $original_order || $line_item_changed) { // Use a clone so the save doesn't interfere with price values in a live // order object that would be modified on presave. - commerce_order_save(clone($order_wrapper->value())); + commerce_order_save($order_wrapper->value()); } return $order_wrapper; diff --git a/modules/cart/tests/commerce_cart.test b/modules/cart/tests/commerce_cart.test index 8120224..f2c868a 100644 --- a/modules/cart/tests/commerce_cart.test +++ b/modules/cart/tests/commerce_cart.test @@ -515,7 +515,7 @@ class CommerceCartTestCaseAnonymousToAuthenticated extends CommerceCartTestCase // Change the price to check if the amount gets updated when the user logs // in. $new_price = $this->product->commerce_price[LANGUAGE_NONE][0]['amount'] + rand(2, 500); - $this->product->commerce_price[LANGUAGE_NONE][0]['amount'] = commerce_currency_integer_to_amount($new_price, $this->product->commerce_price[LANGUAGE_NONE][0]['currency_code']); + $this->product->commerce_price[LANGUAGE_NONE][0]['amount'] = commerce_currency_amount_to_decimal($new_price, $this->product->commerce_price[LANGUAGE_NONE][0]['currency_code']); $this->product->is_new = FALSE; $this->product = commerce_product_save($this->product); @@ -529,7 +529,7 @@ class CommerceCartTestCaseAnonymousToAuthenticated extends CommerceCartTestCase $this->drupalGet($this->getCommerceUrl('cart')); $this->assertNoText(t('Your shopping cart is empty.'), t('Cart is not empty')); $this->assertText($this->product->title, t('Product is still in the cart')); - $this->assertText(trim(commerce_currency_integer_to_amount($this->product->commerce_price[LANGUAGE_NONE][0]['amount'], $this->product->commerce_price[LANGUAGE_NONE][0]['currency_code'])), t('Product price has been updated')); + $this->assertText(trim(commerce_currency_amount_to_decimal($this->product->commerce_price[LANGUAGE_NONE][0]['amount'], $this->product->commerce_price[LANGUAGE_NONE][0]['currency_code'])), t('Product price has been updated')); // Check if the order is the same. $this->assertTrue($order_anonymous->order_id == $order_authenticated->order_id, t('Cart has been converted successfully')); diff --git a/modules/order/commerce_order.install b/modules/order/commerce_order.install index f09ce57..d1a2e70 100644 --- a/modules/order/commerce_order.install +++ b/modules/order/commerce_order.install @@ -180,3 +180,171 @@ function commerce_order_schema() { return $schema; } + +/** + * Between 7.x-1.0-beta2 and 7.x-1.0-beta3 we determined we needed to revise the + * way we handled price amounts, preferring to preserve integer amounts as + * loaded from the database until formatting them as decimal values upon display + * instead of converting them to decimals upon loading. The initial reasons and + * related issues are outlined in http://drupal.org/node/1124416. + * + * While the fix did not involve changing the database schema at all, it did + * change the way price amounts were stored in the components array of a price's + * data array. Therefore, the following update functions are responsible for + * loading and resaving entities the change will affect, primarily to result in + * a recalculated order total components array./ + */ + +/** + * Loads and resaves all the products on the site, updating the default price + * field to have proper component price amount values. + */ +function commerce_order_update_7100(&$sandbox) { + // Ensure there are no stale prices in the field cache. + field_cache_clear(); + + // Establish the progress variables. + if (!isset($sandbox['progress'])) { + $sandbox['progress'] = 0; + $sandbox['current_product_id'] = 0; + $sandbox['max'] = db_query("SELECT COUNT(DISTINCT product_id) FROM {commerce_product}")->fetchField(); + } + + // Load the next 50 products. + $products = db_select('commerce_product', 'cp') + ->fields('cp', array('product_id')) + ->condition('product_id', $sandbox['current_product_id'], '>') + ->range(0, 50) + ->orderBy('product_id', 'ASC') + ->execute(); + + // Loop over the products, loading, adjusting, and resaving each one. + foreach ($products as $product) { + $product = commerce_product_load($product->product_id); + + // If the commerce_price field has a components array, multiply its price + // amounts by the proper value for its currency. + if (!empty($product->commerce_price)) { + foreach ($product->commerce_price as $langcode => &$data) { + foreach ($data as $delta => &$item) { + if (!empty($item['data']['components'])) { + foreach ($item['data']['components'] as $key => &$component) { + $component['price']['amount'] = commerce_currency_decimal_to_amount($component['price']['amount'], $component['price']['currency_code'], FALSE); + } + } + } + } + } + + commerce_product_save($product); + + $sandbox['progress']++; + $sandbox['current_product_id'] = $product->product_id; + } + + $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']); + + return t('All products have been loaded and saved with updated price component arrays.'); +} + +/** + * Loads and resaves all the line items on the site, updating the unit price + * field to have proper component price amount values. + */ +function commerce_order_update_7101(&$sandbox) { + // Ensure there are no stale prices in the field cache. + field_cache_clear(); + + // Establish the progress variables. + if (!isset($sandbox['progress'])) { + $sandbox['progress'] = 0; + $sandbox['current_line_item_id'] = 0; + $sandbox['max'] = db_query("SELECT COUNT(DISTINCT line_item_id) FROM {commerce_line_item}")->fetchField(); + } + + // Load the next 50 line items. + $line_items = db_select('commerce_line_item', 'cli') + ->fields('cli', array('line_item_id')) + ->condition('line_item_id', $sandbox['current_line_item_id'], '>') + ->range(0, 50) + ->orderBy('line_item_id', 'ASC') + ->execute(); + + // Loop over the line items, loading, adjusting, and resaving each one. + foreach ($line_items as $line_item) { + $line_item = commerce_line_item_load($line_item->line_item_id); + + // If the commerce_unit_price field has a components array, multiply its + // amounts by the proper value for its currency. + if (!empty($line_item->commerce_unit_price)) { + foreach ($line_item->commerce_unit_price as $langcode => &$data) { + foreach ($data as $delta => &$item) { + if (!empty($item['data']['components'])) { + foreach ($item['data']['components'] as $key => &$component) { + $component['price']['amount'] = commerce_currency_decimal_to_amount($component['price']['amount'], $component['price']['currency_code'], FALSE); + } + } + } + } + } + + commerce_line_item_save($line_item); + + $sandbox['progress']++; + $sandbox['current_line_item_id'] = $line_item->line_item_id; + } + + $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']); + + return t('All line items have been loaded and saved with updated price component arrays.'); +} + +/** + * Loads and resaves all the orders on the site to rebuild the order total price + * component arrays. + */ +function commerce_order_update_7102(&$sandbox) { + // Ensure there are no stale prices in the field cache. + field_cache_clear(); + + // Establish the progress variables. + if (!isset($sandbox['progress'])) { + $sandbox['progress'] = 0; + $sandbox['current_order_id'] = 0; + $sandbox['max'] = db_query("SELECT COUNT(DISTINCT order_id) FROM {commerce_order}")->fetchField(); + } + + // Load the next 50 orders. + $orders = db_select('commerce_order', 'co') + ->fields('co', array('order_id')) + ->condition('order_id', $sandbox['current_order_id'], '>') + ->range(0, 50) + ->orderBy('order_id', 'ASC') + ->execute(); + + // Loop over the orders, loading and resaving each one. + foreach ($orders as $order) { + $order = commerce_order_load($order->order_id); + + // Save the order as a new revision with an update log message. + $order->revision = TRUE; + $order->log = t('Order updated for 7.0-1.0-beta3 price component changes.'); + + commerce_order_save($order); + + $sandbox['progress']++; + $sandbox['current_order_id'] = $order->order_id; + } + + $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']); + + return t('All orders have been loaded and saved with updated price component arrays.'); +} + +/** + * Truncates the pre-calculated price table. + */ +function commerce_order_update_7103() { + db_query("TRUNCATE TABLE {commerce_calculated_price}"); + return t('The calculated price table has been cleared. If your site uses product sell price pre-calculation, you will need to recalculate these prices.'); +} diff --git a/modules/order/tests/commerce_order_ui.test b/modules/order/tests/commerce_order_ui.test index 48f5e2f..682a56b 100644 --- a/modules/order/tests/commerce_order_ui.test +++ b/modules/order/tests/commerce_order_ui.test @@ -188,7 +188,7 @@ class CommerceOrderUIAdminTest extends CommerceBaseTestCase { $line_item_id = $order_wrapper->commerce_line_items->get(0)->value()->line_item_id; // Format the price based in the currency. - $price = commerce_currency_integer_to_amount($product_wrapper->commerce_price->amount->value(), $product_wrapper->commerce_price->currency_code->value()); + $price = commerce_currency_amount_to_decimal($product_wrapper->commerce_price->amount->value(), $product_wrapper->commerce_price->currency_code->value()); // Check the existance of the fields for quantity and price of the line // item just created. @@ -200,7 +200,7 @@ class CommerceOrderUIAdminTest extends CommerceBaseTestCase { // Generate new quantity and prices and save them. $new_qty = rand(0,99); $new_currency_code = 'EUR'; - $new_price = commerce_currency_integer_to_amount(rand(2, 500), $new_currency_code); + $new_price = commerce_currency_amount_to_decimal(rand(2, 500), $new_currency_code); $edit = array( 'commerce_line_items[und][line_items][' . $line_item_id . '][quantity]' => $new_qty, 'commerce_line_items[und][line_items][' . $line_item_id . '][commerce_unit_price][und][0][amount]' => $new_price, diff --git a/modules/payment/commerce_payment.module b/modules/payment/commerce_payment.module index c17cd90..1f25a3d 100644 --- a/modules/payment/commerce_payment.module +++ b/modules/payment/commerce_payment.module @@ -603,15 +603,7 @@ function _commerce_payment_transaction_create($values = array()) { * The saved transaction object. */ function commerce_payment_transaction_save($transaction) { - // Convert the transaction amount to an integer. - $transaction->amount = commerce_currency_amount_to_integer($transaction->amount, $transaction->currency_code); - - $transaction = entity_get_controller('commerce_payment_transaction')->save($transaction); - - // Convert the amount back from an integer. - $transaction->amount = commerce_currency_integer_to_amount($transaction->amount, $transaction->currency_code); - - return $transaction; + return entity_get_controller('commerce_payment_transaction')->save($transaction); } /** @@ -639,18 +631,7 @@ function commerce_payment_transaction_load($transaction_id) { * An array of transaction objects indexed by transaction_id. */ function commerce_payment_transaction_load_multiple($transaction_ids = array(), $conditions = array(), $reset = FALSE) { - $transactions = entity_load('commerce_payment_transaction', $transaction_ids, $conditions, $reset); - - // Convert the loaded amount integers to price decimal amounts. - foreach ($transactions as $transaction_id => &$transaction) { - // Ensure it happens only once per entity. - if (empty($transaction->amount_converted)) { - $transaction->amount = commerce_currency_integer_to_amount($transaction->amount, $transaction->currency_code); - $transaction->amount_converted = TRUE; - } - } - - return $transactions; + return entity_load('commerce_payment_transaction', $transaction_ids, $conditions, $reset); } /** @@ -855,7 +836,7 @@ function commerce_payment_transaction_access($op, $order, $transaction = NULL, $ */ function commerce_payment_order_balance($order, $totals = array()) { $wrapper = entity_metadata_wrapper('commerce_order', $order); - $order_total = commerce_line_items_total($wrapper->commerce_line_items); + $order_total = $wrapper->commerce_order_total->value(); // Calculate the transaction totals if not supplied. if (empty($totals)) { diff --git a/modules/payment/includes/commerce_payment.forms.inc b/modules/payment/includes/commerce_payment.forms.inc index 08b3f71..0a14df5 100644 --- a/modules/payment/includes/commerce_payment.forms.inc +++ b/modules/payment/includes/commerce_payment.forms.inc @@ -29,6 +29,7 @@ function commerce_payment_order_transaction_add_form($form, &$form_state, $order '#type' => 'fieldset', '#title' => t('Payment terminal: @title', array('@title' => $payment_method['title'])), '#attributes' => array('class' => array('payment-terminal')), + '#element_validate' => array('commerce_payment_order_transaction_add_form_payment_terminal_validate'), ); // Establish defaults for the amount if possible. @@ -44,7 +45,7 @@ function commerce_payment_order_transaction_add_form($form, &$form_state, $order $form['payment_terminal']['amount'] = array( '#type' => 'textfield', '#title' => t('Amount'), - '#default_value' => $default_amount, + '#default_value' => commerce_currency_amount_to_decimal($default_amount, $default_currency_code), '#size' => 10, '#prefix' => '