diff --git a/modules/cart/commerce_cart.info b/modules/cart/commerce_cart.info index 24f3a91..9a3f60a 100644 --- a/modules/cart/commerce_cart.info +++ b/modules/cart/commerce_cart.info @@ -15,6 +15,7 @@ core = 7.x ; Views handlers files[] = includes/views/handlers/commerce_cart_handler_field_add_to_cart_form.inc +files[] = includes/views/handlers/commerce_cart_handler_field_cart_line_item_link_edit.inc files[] = includes/views/handlers/commerce_cart_plugin_argument_default_current_cart_order_id.inc ; Simple tests diff --git a/modules/cart/commerce_cart.module b/modules/cart/commerce_cart.module index 25f5cb9..0f53fe2 100644 --- a/modules/cart/commerce_cart.module +++ b/modules/cart/commerce_cart.module @@ -39,6 +39,20 @@ function commerce_cart_menu() { 'file' => 'includes/commerce_cart.pages.inc', ); + $items['cart/line-items/%commerce_line_item/edit'] = array( + 'title' => 'Cart Edit', + 'title callback' => 'commerce_cart_line_item_form_menu_item_title', + 'title arguments' => array(2), + 'page callback' => 'commerce_cart_line_item_form_wrapper', + 'page arguments' => array(2), + 'access callback' => 'commerce_cart_line_item_form_menu_item_access', + 'access arguments' => array(2, 'access checkout'), + 'type' => MENU_NORMAL_ITEM, + 'weight' => -5, + 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, + 'file' => 'includes/commerce_cart.pages.inc', + ); + return $items; } @@ -68,6 +82,59 @@ function commerce_cart_menu_item_title() { } /** + * Returns the title of the cart line item form + */ +function commerce_cart_line_item_form_menu_item_title($line_item) { + $tokens = array( + '@title' => 'item', + '@label' => '', + ); + + if ($item_title = commerce_line_item_title($line_item)) { + $tokens['@title'] = $item_title; + } + + if (!empty($line_item->line_item_label)) { + if (!$item_title || strpos($item_title, $line_item->line_item_label) === FALSE) { + $tokens['@label'] = ' (' . $line_item->line_item_label . ')'; + } + } + + return t('Edit @title@label', $tokens); +} + +/** + * Returns TRUE if access is allowed to the cart line item form + */ +function commerce_cart_line_item_form_menu_item_access($line_item, $permission = 'access checkout') { + // DENY if the user does not have permission + if (!empty($permission) && !user_access($permission)) { + return FALSE; + } + + // DENY if no order associated with this line item + if (empty($line_item->order_id)) { + return FALSE; + } + + // Load the associated order + $order = commerce_order_load($line_item->order_id); + + // DENY if the order does not exist or cannot load + if (empty($order)) { + return FALSE; + } + + // DENY if the order is not a cart + if (!commerce_cart_order_is_cart($order)) { + return FALSE; + } + + // ALLOW if all checks pass + return TRUE; +} + +/** * Redirects a valid page request to cart/my to the cart page. */ function commerce_cart_menu_item_redirect() { @@ -1049,38 +1116,7 @@ function commerce_cart_product_add($uid, $line_item, $combine = TRUE) { // If we are supposed to look for a line item to combine into... if ($combine) { - // Generate an array of properties and fields to compare. - $comparison_properties = array('type', 'commerce_product'); - - // Add any field that was exposed on the Add to Cart form to the array. - // TODO: Bypass combination when an exposed field is no longer available but - // the same base product is added to the cart. - foreach (field_info_instances('commerce_line_item', $line_item->type) as $info) { - if (!empty($info['commerce_cart_settings']['field_access'])) { - $comparison_properties[] = $info['field_name']; - } - } - - // Allow other modules to specify what properties should be compared when - // determining whether or not to combine line items. - drupal_alter('commerce_cart_product_comparison_properties', $comparison_properties); - - // Loop over each line item on the order. - foreach ($order_wrapper->commerce_line_items as $delta => $matching_line_item_wrapper) { - // Examine each of the comparison properties on the line item. - foreach ($comparison_properties as $property) { - // If any property does not match the same property on the incoming line - // item... - if ($matching_line_item_wrapper->{$property}->raw() != $line_item_wrapper->{$property}->raw()) { - // Continue the loop with the next line item. - continue 2; - } - } - - // If every comparison line item matched, combine into this line item. - $matching_line_item = $matching_line_item_wrapper->value(); - break; - } + $matching_line_item = commerce_cart_matching_product_line_item_in_order($line_item, $order); } // If no matching line item was found... @@ -1164,6 +1200,232 @@ function commerce_cart_product_add_by_id($product_id, $quantity = 1, $combine = } /** + * Update an existing line item in the order + * + * @param $uid + * The uid of the user whose cart you are adding the product to. + * @param $line_item + * An existing product line item to be added to the cart with the following data + * on the line item being used to determine how to add the product to the cart: + * @param $combine + * Boolean indicating whether or not to combine like products on the same line + * item, incrementing an existing line item's quantity instead of adding a + * new line item to the cart order. When the incoming line item is combined + * into an existing line item, field data on the existing line item will be + * left unchanged. Only the quantity will be incremented and the data array + * will be updated by merging the data from the existing line item onto the + * data from the incoming line item, giving precedence to the most recent data. + * + * @return + * The new or updated line item object or FALSE on failure. + */ +function commerce_cart_line_item_cart_update($uid, $line_item, $combine = TRUE) { + // Do not add the line item if it doesn't have a unit price. + $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item); + + if (is_null($line_item_wrapper->commerce_unit_price->value())) { + return FALSE; + } + + // Use line item order if available + if (!empty($line_item->order_id)) { + $order = commerce_order_load($line_item->order_id); + } + + // Attempt to load the customer's shopping cart order. + if (empty($order)) { + $order = commerce_cart_order_load($uid); + } + + // If no order, exit + if (empty($order)) { + return FALSE; + } + + // Wrap the order for easy access to field data. + $order_wrapper = entity_metadata_wrapper('commerce_order', $order); + + // Extract the product and quantity from the incoming line item. + $product = $line_item_wrapper->commerce_product->value(); + $quantity = $line_item->quantity; + + // Determine if the product already exists on the order and increment its + // quantity instead of adding a new line if it does. + $matching_line_item = NULL; + + // If we are supposed to look for a line item to combine into... + if ($combine) { + $matching_line_item = commerce_cart_matching_product_line_item_in_order($line_item, $order); + } + + // If no matching line item was found... + if (empty($matching_line_item)) { + // Save the incoming line item + commerce_line_item_save($line_item); + } + else { + // Increment the quantity of the matching line item, update the data array, + // and save it. + $matching_line_item->quantity += $quantity; + $matching_line_item->data = array_merge($line_item->data, $matching_line_item->data); + + commerce_line_item_save($matching_line_item); + + // Clear the line item cache so the updated quantity will be available to + // the ensuing load instead of the original quantity as loaded above. + entity_get_controller('commerce_line_item')->resetCache(array($matching_line_item->line_item_id)); + + // Remove the duplicate incoming line item + commerce_line_item_delete($line_item->line_item_id); + + // Update the line item variable for use in the invocation and return value. + $line_item = $matching_line_item; + } + + // Save the order to update totals, etc. + commerce_order_save($order); + + // Invoke the product add event with the newly saved or updated line item. + rules_invoke_all('commerce_cart_product_add', $order, $product, $quantity, $line_item); + + // Return the line item. + return $line_item; +} + +/** + * Returns first matching product line item in the given order that can + * be combined with the given line item + * + * @param $line_item + * Line item object to compare + * @param $order + * Order object. If not provided, uses the line item order (if available). + * + * @return + * Matching line item, else NULL + */ +function commerce_cart_matching_product_line_item_in_order($line_item, $order = NULL) { + // Resolve order + if (empty($order)) { + // Use line item order if available + if (!empty($line_item->order_id)) { + $order = commerce_order_load($line_item->order_id); + } + + // Exit if no order found + if (empty($order)) { + return FALSE; + } + } + + // Wrap the order for easy access to field data. + $order_wrapper = entity_metadata_wrapper('commerce_order', $order); + + // Loop over each line item on the order. + foreach ($order_wrapper->commerce_line_items as $delta => $matching_line_item_wrapper) { + $matching_line_item = $matching_line_item_wrapper->value(); + + // If every comparison line item matched, combine into this line item ... + if (commerce_cart_product_line_item_can_combine($line_item, $matching_line_item)) { + return $matching_line_item; + } + } +} + +/** + * Returns TRUE if the product line items can be combined + * + * @param $a + * Line item object + * @param $b + * Line item object + * + * @return + * TRUE if the line items can be combined + */ +function commerce_cart_product_line_item_can_combine($a, $b) { + // Exclude self + if (!empty($a->line_item_id) && !empty($b->line_item_id) && $a->line_item_id == $b->line_item_id) { + return FALSE; + } + + return commerce_cart_product_line_item_match($a, $b); +} + +/** + * Returns TRUE if the product line items have equivalent comparison properties + * + * @param $a + * Line item object + * @param $b + * Line item object + * + * @return + * TRUE if the line items are similar + */ +function commerce_cart_product_line_item_match($a, $b) { + // Use the first line item's type to resolve the properties + $comparison_properties = commerce_cart_product_comparison_properties($a->type); + if (empty($comparison_properties)) { + return FALSE; + } + + // Wrap the lines to compare + $a_wrapper = entity_metadata_wrapper('commerce_line_item', $a); + $b_wrapper = entity_metadata_wrapper('commerce_line_item', $b); + + // Examine each of the comparison properties on the line item. + foreach ($comparison_properties as $property) { + // If any property does not match the same property on the other line item ... + if ($a_wrapper->{$property}->raw() != $b_wrapper->{$property}->raw()) { + return FALSE; + } + } + + // If every comparison property matched ... + return TRUE; +} + +/** + * Returns an array of comparison properties used to determine whether + * or not a product line item can be combined into an existing line + * item when added to the cart. + * + * @param $comparison_properties + * The array of property names (including field names) that map to properties + * on the line item wrappers being compared to check for combination. + */ +function commerce_cart_product_comparison_properties($line_item_type) { + $props = &drupal_static(__FUNCTION__, array()); + + if (!isset($props[$line_item_type])) { + $props[$line_item_type] = array(); + + // Generate an array of properties and fields to compare. + $comparison_properties = array('type', 'commerce_product'); + + // Add any field that was exposed on the Add to Cart form to the array. + // TODO: Bypass combination when an exposed field is no longer available but + // the same base product is added to the cart. + foreach (field_info_instances('commerce_line_item', $line_item_type) as $info) { + if (!empty($info['commerce_cart_settings']['field_access'])) { + $comparison_properties[] = $info['field_name']; + } + } + + // Allow other modules to specify what properties should be compared when + // determining whether or not to combine line items. + drupal_alter('commerce_cart_product_comparison_properties', $comparison_properties); + + // Update cache + $props[$line_item_type] = $comparison_properties; + } + + return $props[$line_item_type]; +} + + +/** * Deletes a product line item from a shopping cart order. * * @param $order @@ -1366,6 +1628,11 @@ function commerce_cart_forms($form_id, $args) { ); } + $forms['commerce_cart_line_item_edit_form'] = array( + 'callback' => 'commerce_cart_add_to_cart_form', + ); + + return $forms; } @@ -1404,7 +1671,7 @@ function commerce_cart_forms($form_id, $args) { * @return * The form array. */ -function commerce_cart_add_to_cart_form($form, &$form_state, $line_item, $show_quantity = FALSE, $context = array()) { +function commerce_cart_add_to_cart_form($form, &$form_state, $line_item, $show_quantity = FALSE, $context = array(), $show_price = FALSE) { global $user; // Store the context in the form state for use during AJAX refreshes. @@ -1837,6 +2104,14 @@ function commerce_cart_add_to_cart_form($form, &$form_state, $line_item, $show_q '#weight' => 45, ); } + + // Render price field display if necessary + if ($show_price) { + // Add price display + $form['price'] = field_view_field('commerce_product', $default_product, 'commerce_price', $context['view_mode']); + $form['price']['#prefix'] = '
array('cart-line-item-edit-form-' . $line_item->line_item_id . '-product-commerce-price'))) . '>'; + $form['price']['#suffix'] = '
'; + } // Add the line item's fields to a container on the form. $form['line_item_fields'] = array( @@ -1871,6 +2146,13 @@ function commerce_cart_add_to_cart_form($form, &$form_state, $line_item, $show_q '#validate' => array('commerce_cart_add_to_cart_form_disabled_validate'), ); } + elseif (!empty($line_item->line_item_id)) { + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Update'), + '#weight' => 50, + ); + } else { $form['submit'] = array( '#type' => 'submit', @@ -2009,44 +2291,54 @@ function commerce_cart_add_to_cart_form_attributes_refresh($form, $form_state) { function commerce_cart_add_to_cart_form_submit($form, &$form_state) { $product_id = $form_state['values']['product_id']; $product = commerce_product_load($product_id); + $line_item_is_new = empty($form_state['line_item']->line_item_id); // If the line item passed to the function is new... - if (empty($form_state['line_item']->line_item_id)) { + if ($line_item_is_new) { // Create the new product line item of the same type. $line_item = commerce_product_line_item_new($product, $form_state['values']['quantity'], 0, $form_state['line_item']->data, $form_state['line_item']->type); + } + else { + // Update the existing line item + $line_item = $form_state['line_item']; + $line_item->quantity = $form_state['values']['quantity']; + commerce_product_line_item_populate($line_item, $product); + } - // Allow modules to prepare this as necessary. This hook is defined by the - // Product Pricing module. - drupal_alter('commerce_product_calculate_sell_price_line_item', $line_item); + // Allow modules to prepare this as necessary. This hook is defined by the + // Product Pricing module. + drupal_alter('commerce_product_calculate_sell_price_line_item', $line_item); - // Remove line item field values the user didn't have access to modify. - foreach ($form_state['values']['line_item_fields'] as $field_name => $value) { - // Note that we're checking the Commerce Cart settings that we inserted - // into this form element array back when we built the form. This means a - // module wanting to alter a line item field widget to be available must - // update both its form element's #access value and the field_access value - // of the #commerce_cart_settings array. - if (empty($form['line_item_fields'][$field_name]['#commerce_cart_settings']['field_access'])) { - unset($form_state['values']['line_item_fields'][$field_name]); - } + // Remove line item field values the user didn't have access to modify. + foreach ($form_state['values']['line_item_fields'] as $field_name => $value) { + // Note that we're checking the Commerce Cart settings that we inserted + // into this form element array back when we built the form. This means a + // module wanting to alter a line item field widget to be available must + // update both its form element's #access value and the field_access value + // of the #commerce_cart_settings array. + if (empty($form['line_item_fields'][$field_name]['#commerce_cart_settings']['field_access'])) { + unset($form_state['values']['line_item_fields'][$field_name]); } + } - // Unset the line item field values array if it is now empty. - if (empty($form_state['values']['line_item_fields'])) { - unset($form_state['values']['line_item_fields']); - } + // Unset the line item field values array if it is now empty. + if (empty($form_state['values']['line_item_fields'])) { + unset($form_state['values']['line_item_fields']); + } - // Add field data to the line item. - field_attach_submit('commerce_line_item', $line_item, $form['line_item_fields'], $form_state); + // Add field data to the line item. + field_attach_submit('commerce_line_item', $line_item, $form['line_item_fields'], $form_state); - // Process the unit price through Rules so it reflects the user's actual - // purchase price. - rules_invoke_event('commerce_product_calculate_sell_price', $line_item); + // Process the unit price through Rules so it reflects the user's actual + // purchase price. + rules_invoke_event('commerce_product_calculate_sell_price', $line_item); - // Only attempt an Add to Cart if the line item has a valid unit price. - $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item); + // Wrap the line item + $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item); - if (!is_null($line_item_wrapper->commerce_unit_price->value())) { + // Only attempt an Add to Cart if the line item has a valid unit price. + if (!is_null($line_item_wrapper->commerce_unit_price->value())) { + if ($line_item_is_new) { // Add the product to the specified shopping cart. $form_state['line_item'] = commerce_cart_product_add( $form_state['values']['uid'], @@ -2055,9 +2347,17 @@ function commerce_cart_add_to_cart_form_submit($form, &$form_state) { ); } else { - drupal_set_message(t('%title could not be added to your cart.', array('%title' => $product->title)), 'error'); + // Update existing line item + $form_state['line_item'] = commerce_cart_line_item_cart_update( + $form_state['values']['uid'], + $line_item, + isset($line_item->data['context']['add_to_cart_combine']) ? $line_item->data['context']['add_to_cart_combine'] : TRUE + ); } } + else { + drupal_set_message(t('%title could not be added to your cart.', array('%title' => $product->title)), 'error'); + } } /** diff --git a/modules/cart/includes/commerce_cart.pages.inc b/modules/cart/includes/commerce_cart.pages.inc index 23409b9..2f69d73 100644 --- a/modules/cart/includes/commerce_cart.pages.inc +++ b/modules/cart/includes/commerce_cart.pages.inc @@ -52,3 +52,52 @@ function commerce_cart_view() { return $content; } + +/** + * Form callback wrapper: edit a cart line item. + * + * @param $line_item + * The line item object to edit through the form. + * @param $show_quantity + * Boolean indicating whether or not to show the quantity widget; defaults to + * FALSE resulting in a hidden field holding the quantity. + * @param $context + * Information on the context of the form's placement, allowing it to update + * product fields on the page based on the currently selected default product. + * + * @see commerce_cart_add_to_cart_form() + */ +function commerce_cart_line_item_form_wrapper($line_item, $show_quantity = NULL, $context = array()) { + // resolve $show_quantity + $show_quantity_default = FALSE; + if (isset($show_quantity)) { + // Set to function argument + $show_quantity = (bool) $show_quantity; + } + elseif (isset($_GET['show_quantity'])) { + // Safely resolve from query parameter + $show_quantity = !empty($_GET['show_quantity']); + } + else { + // Set to default + $show_quantity = $show_quantity_default; + } + + if (empty($context)) { + $context = array( + 'view_mode' => 'display', + 'class_prefix' => 'cart-line-item-edit-form-' . $line_item->line_item_id + ); + } + +/** @todo: check that line item order is 'in cart' status? ****/ + // render add to cart form for new line items + if (empty($line_item->line_item_id)) { + $form = drupal_get_form('commerce_cart_add_to_cart_form', $line_item, $show_quantity, $context, TRUE); + } + + // render edit form + $form = drupal_get_form('commerce_cart_line_item_edit_form', $line_item, $show_quantity, $context, TRUE); + + return $form; +} diff --git a/modules/cart/includes/views/commerce_cart.views.inc b/modules/cart/includes/views/commerce_cart.views.inc index 5eca188..8bbac3e 100644 --- a/modules/cart/includes/views/commerce_cart.views.inc +++ b/modules/cart/includes/views/commerce_cart.views.inc @@ -15,6 +15,14 @@ function commerce_cart_views_data_alter(&$data) { 'handler' => 'commerce_cart_handler_field_add_to_cart_form', ), ); + + $data['commerce_line_item']['cart_line_item_link_edit'] = array( + 'field' => array( + 'title' => t('Cart Line Item Edit link'), + 'help' => t('Link to the edit form for a cart line item.'), + 'handler' => 'commerce_cart_handler_field_cart_line_item_link_edit', + ), + ); } /** diff --git a/modules/cart/includes/views/handlers/commerce_cart_handler_field_cart_line_item_link_edit.inc b/modules/cart/includes/views/handlers/commerce_cart_handler_field_cart_line_item_link_edit.inc new file mode 100644 index 0000000..0ac7690 --- /dev/null +++ b/modules/cart/includes/views/handlers/commerce_cart_handler_field_cart_line_item_link_edit.inc @@ -0,0 +1,62 @@ +additional_fields['line_item_id'] = 'line_item_id'; + } + + function option_definition() { + $options = parent::option_definition(); + + $options['text'] = array('default' => '', 'translatable' => TRUE); + $options['show_quantity'] = array('default' => FALSE); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['text'] = array( + '#type' => 'textfield', + '#title' => t('Text to display'), + '#default_value' => $this->options['text'], + ); + + $form['show_quantity'] = array( + '#type' => 'checkbox', + '#title' => t('Display a textfield quantity widget on the edit form.'), + '#default_value' => $this->options['show_quantity'], + ); + } + + function query() { + $this->ensure_my_table(); + $this->add_additional_fields(); + } + + function render($values) { + $text = !empty($this->options['text']) ? $this->options['text'] : t('edit'); + $line_item_id = $this->get_value($values, 'line_item_id'); + + $path = 'cart/line-items/' . $line_item_id . '/edit'; + $menu_item = menu_get_item($path); + + // Exit if no access to the menu path + if (empty($menu_item['access'])) { + return; + } + + // Build query parameters + $query_params = drupal_get_destination(); + $query_params['show_quantity'] = $this->options['show_quantity']; + + // render link + return l($text, $path, array('query' => $query_params)); + } +}