Skip to content

Instantly share code, notes, and snippets.

@msupko
Last active August 29, 2015 14:00
Show Gist options
  • Save msupko/6fb60b3107e5d44413cb to your computer and use it in GitHub Desktop.
Save msupko/6fb60b3107e5d44413cb to your computer and use it in GitHub Desktop.
Fixing price calculation for commerce_bundle_add_to_cart_form()
<?php
/**
* @file
* Provides product bundles for Drupal Commerce
*/
module_load_include('inc', 'commerce_bundle', 'includes/commerce_bundle.field');
/**
* Implements hook_views_api().
*/
function commerce_bundle_views_api() {
return array(
'api' => 3,
'path' => drupal_get_path('module', 'commerce_bundle') . '/includes/views',
);
}
/**
* Implements of hook_entity_info_alter().
*/
function commerce_bundle_entity_info_alter(&$info) {
// Add custom view mode for add to cart.
if (isset($info['commerce_product'])) {
$info['commerce_product']['view modes']['commerce_bundle_add_to_cart_form'] = array(
'label' => t('Commerce Bundle: Add to cart form'),
'custom settings' => FALSE,
);
}
return $info;
}
/**
* Retuns an array of bundle line item types
*/
function commerce_bundle_line_item_types() {
$types = array();
foreach (commerce_line_item_types() as $type => $line_item_type) {
if (!empty($line_item_type['product']) && !empty($line_item_type['bundle'])) {
$types[] = $type;
}
}
return $types;
}
/**
* Implements hook_commerce_bundle_item_can_delete().
*/
function commerce_bundle_commerce_bundle_item_can_delete($entity) {
$line_item_types = commerce_bundle_line_item_types();
if (empty($line_item_types)) {
return TRUE;
}
// Use EntityFieldQuery to look for line items referencing this bundle item
// and do not allow the delete to occur if one exists.
$query = new EntityFieldQuery();
$query
->addTag('commerce_bundle_commerce_bundle_item_can_delete')
->entityCondition('entity_type', 'commerce_line_item', '=')
->entityCondition('bundle', $line_item_types, 'IN')
->fieldCondition('commerce_bundle_item_id', 'target_id', $entity->item_id, '=')
->addTag('DANGEROUS_ACCESS_CHECK_OPT_OUT')
->count();
return $query->execute() == 0;
}
/**
* Implements hook_commerce_line_item_type_info().
*/
function commerce_bundle_commerce_line_item_type_info() {
$line_item_types = array();
$line_item_types['commerce_bundle_line_item'] = array(
'type' => 'commerce_bundle_line_item',
'name' => t('Bundle Line Item'),
'description' => t('References Bundle Groups, Bundle Items, and Bundle Products.'),
'product' => TRUE,
'bundle' => TRUE,
'add_form_submit_value' => t('Add bundle'),
'base' => 'commerce_product_line_item',
'callbacks' => array(
'title' => 'commerce_bundle_line_item_title',
),
);
return $line_item_types;
}
/**
* Returns a title of the bundle line item for use in Views.
*
* @param $line_item
* The bundle line item object whose title should be returned.
*
* @return
* The appropriate title depending on if the line item is a control row or not.
*/
function commerce_bundle_line_item_title($line_item) {
$line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
// Currently, just return the product's title. However, in the future replace
// this with extra bundle info.
if ($product = $line_item_wrapper->commerce_product->value()) {
return check_plain($product->title);
}
}
/**
* Generates unique config ids used to group similar line items.
*
* @param $line_items
* An array of line item objects.
*
* @return
* An array keyed by bundle configuration IDs with values of keys from the
* given line items array.
*/
function commerce_bundle_line_item_get_bundle_configs($line_items) {
$config_field_names = array(
'commerce_bundle_id',
'commerce_bundle_group_id',
'commerce_bundle_item_id',
'commerce_product',
);
$add_to_cart_combine = FALSE;
$bundles = array();
foreach ($line_items as $delta => $line_item) {
$line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
foreach ($config_field_names as $config_field_name) {
if (!isset($line_item_wrapper->{$config_field_name})) {
continue 2;
}
}
$bundle_id = NULL;
$config_ids = array();
foreach ($config_field_names as $config_field_name) {
$entity_id = $line_item_wrapper->{$config_field_name}->raw();
if (empty($entity_id)) {
continue 2;
}
if ($config_field_name == 'commerce_bundle_id') {
$bundle_id = $entity_id;
}
$config_ids[$config_field_name] = $entity_id;
}
if ($bundle_id) {
$bundles[$bundle_id][$delta] = implode('_', $config_ids);
// Set flag if we want each bundle as its own line item instead of
// combining bundles. We only care if we find one TRUE.
$add_to_cart_combine = $line_item->data['context']['add_to_cart_combine'] || $add_to_cart_combine;
}
}
$return = array();
if (!empty($bundles)) {
foreach ($bundles as $bundle_id => $configs) {
// Hash entity ids representing the config.
$hash_data = implode('_', array_values($configs));
// If we do not want to combine line items then add a unique value to the
// hash data.
$hash_data = !$add_to_cart_combine ? $hash_data . '_' . microtime(true) : $hash_data;
$hash = drupal_hash_base64($hash_data);
// Store line item ids;
$return[$hash] = array();
foreach ($configs as $delta => $combined_config_ids) {
$return[$hash][$delta] = $line_items[$delta];
}
}
}
return $return;
}
/**
* Creates a new bundle product line item populated with the proper product values.
*
* @param $product
* The fully loaded product entity.
* @param $bundle_item
* The fully loaded bundle item entity.
* @param $group
* The fully loaded product group.
* @param $bundler
* The fully loaded entity bundling all the product groups.
* @param $quantity
* The quantity to set for the product.
* @param $order_id
* The ID of the order the line item belongs to (if available).
* @param $data
* A data array to set on the new line item. The following information in the
* data array may be used on line item creation:
* - $data['context']['display_path']: if present will be used to set the line
* item's display_path field value.
* @param $type
* The type of product line item to create. Must be a product line item as
* defined in the line item type info array, and the line item type must
* include the expected product related fields. Defaults to the base product
* line item type defined by the Product Reference module.
*
* @return
* The fully loaded line item populated with the product data as specified.
*/
function commerce_bundle_product_line_item_new($product, $bundle_item, $group, $bundler, $quantity = 1, $order_id = 0, $data = array(), $type = 'commerce_bundle_line_item') {
// Ensure a default product line item type.
if (empty($type)) {
$type = 'commerce_bundle_line_item';
}
// Create the new line item.
$line_item = entity_create('commerce_line_item', array(
'type' => $type,
'order_id' => $order_id,
'quantity' => $quantity,
'data' => $data,
));
// Populate it with the entity information.
commerce_bundle_product_line_item_populate($line_item, $bundler, $group, $bundle_item, $product);
// Return the line item.
return $line_item;
}
/**
* Populates an existing bundle product line item with the product and quantity data.
*
* @param $line_item
* The fully loaded line item object, populated by reference.
* @param $bundler
* The fully loaded entity bundling all the product groups.
* @param $group
* The fully loaded product group.
* @param $bundle_item
* The fully loaded bundle item entity.
* @param $product
* The fully loaded product entity.
*/
function commerce_bundle_product_line_item_populate($line_item, $bundler, $group, $bundle_item, $product) {
// Wrap the entities.
$line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
$group_wrapper = entity_metadata_wrapper('commerce_product', $group);
$bundle_item_wrapper = entity_metadata_wrapper('commerce_bundle_item', $bundle_item);
// Set the bundler reference - lazy set to allow any entity type.
if (isset($line_item_wrapper->commerce_bundle_id)) {
$line_item_wrapper->commerce_bundle_id = $bundler;
}
// Set the product group reference.
if (isset($line_item_wrapper->commerce_bundle_group_id)) {
$line_item_wrapper->commerce_bundle_group_id = $group;
}
// Set the bundle item reference.
if (isset($line_item_wrapper->commerce_bundle_item_id)) {
$line_item_wrapper->commerce_bundle_item_id = $bundle_item->item_id;
}
// Set the product reference.
$line_item_wrapper->commerce_product = $product->product_id;
// Set the label to be the product SKU.
/** @todo: should label be set to something indicating bundle? **/
$line_item->line_item_label = $product->sku;
// Set unit quantity and update line item quantity.
$unit_quantity = NULL;
if (isset($group_wrapper->commerce_bundle_unit_quantity) &&
isset($line_item_wrapper->commerce_bundle_unit_quantity)) {
// Set the unit quanity.
$unit_quantity = $group_wrapper->commerce_bundle_unit_quantity->value();
$line_item_wrapper->commerce_bundle_unit_quantity = $unit_quantity;
// Update line item quantity.
$line_item->quantity *= (int) $unit_quantity;
}
$bundle_unit_price = commerce_bundle_get_bundle_product_price($group->product_id, $bundle_item->item_id);
// Set the unit price as the resolved bundle price.
$line_item_wrapper->commerce_unit_price = $bundle_unit_price;
// TODO: add an alter hook for bundle price ?
if (!is_null($bundle_unit_price)) {
$line_item_wrapper->commerce_unit_price->data = commerce_price_component_add(
$bundle_unit_price,
'base_price',
$bundle_unit_price,
TRUE
);
}
}
/**
* Builds an appropriate cart form ID based on the product groups on the form.
*
* @see commerce_bundle_forms().
*/
function commerce_bundle_add_to_cart_form_id($group_ids) {
// Make sure the length of the form id is limited.
$data = implode('_', $group_ids);
if (strlen($data) > 50) {
$data = drupal_hash_base64($data);
}
return 'commerce_bundle_add_to_cart_form_' . $data;
}
/**
* Implements hook_forms().
*
* To provide distinct form IDs for add to cart forms, the product IDs
* referenced by the form are appended to the base ID,
* commerce_cart_add_to_cart_form. When such a form is built or submitted, this
* function will return the proper callback function to use for the given form.
*/
function commerce_bundle_forms($form_id, $args) {
$forms = array();
// Construct a valid cart form ID from the arguments.
if (strpos($form_id, 'commerce_bundle_add_to_cart_form_') === 0) {
$forms[$form_id] = array(
'callback' => 'commerce_bundle_add_to_cart_form',
);
}
return $forms;
}
/**
* Builds an Add to Cart form for a set of products.
*
* @param $line_items
* An array of fully formed bundle product line itema whose data will be
* used in the following ways by the form:
* - $line_item->data['context']['add_to_cart_combine']: a boolean indicating
* whether or not to attempt to combine the product added to the cart with
* existing line items of matching fields.
* - $line_item->data['context']['show_single_product_attributes']: a boolean
* indicating whether or not product attribute fields with single options
* should be shown on the Add to Cart form.
* - $line_item->quantity: the default value for the quantity widget if
* included (determined by the $show_quantity parameter).
* - $line_item->commerce_product: the value of this field will be used as the
* default product ID when the form is built for multiple products.
* The line item's data array will be used on submit to set the data array of
* the product line item created by 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.
* Should be an associative array containing the following keys:
* - class_prefix: a prefix used to target HTML containers for replacement
* with rendered fields as the default product is updated. For example,
* nodes display product fields in their context wrapped in spans with the
* class node-#-product-field_name. The class_prefix for the add to cart
* form displayed on a node would be node-# with this form's AJAX refresh
* adding the suffix -product-field_name.
* - view_mode: a product view mode that tells the AJAX refresh how to render
* the replacement fields.
* If no context is specified, AJAX replacement of rendered fields will not
* happen. This parameter only affects forms containing multiple products.
*
* @return
* The form array.
*/
function commerce_bundle_add_to_cart_form($form, &$form_state, $line_items, $show_quantity = FALSE, $default_quantity = 1, $context = array()) {
global $user;
// Store the context in the form state for use during AJAX refreshes.
$form_state['context'] = $context;
// Store the line item passed to the form builder for reference on submit.
$form_state['line_items'] = &$line_items;
// Initial product group storage.
if (!isset($form_state['product_groups'])) {
$form_state['product_groups'] = array();
}
// Add a generic class ID.
$form['#attributes']['class'][] = drupal_html_class('commerce-bundle-add-to-cart');
$form['#attributes']['class'][] = drupal_html_class('commerce-add-to-cart');
// Store the customer uid in the form so other modules can override with a
// selection widget if necessary.
$form['uid'] = array(
'#type' => 'value',
'#value' => $user->uid,
);
// Add container for all product groups.
$form['product_groups'] = array(
'#type' => 'container',
'#attributes' => array('class' => array(
'commerce-bundle-product-groups',
)),
);
$groups_container = &$form['product_groups'];
// Build attribute forms per product group.
$group_ids = array();
$form_state['bundle_total_price'] = 0;
$currency_code = NULL;
foreach ($line_items as &$line_item) {
$line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
if (!isset($line_item_wrapper->commerce_bundle_group_id)) {
continue;
}
$group_unit_qty = 1;
if (isset($line_item_wrapper->commerce_bundle_unit_quantity)) {
$group_unit_qty = $line_item_wrapper->commerce_bundle_unit_quantity->value();
// Skip if unit qty is set to 0.
if (!$group_unit_qty) {
continue;
}
}
$group_id = $line_item_wrapper->commerce_bundle_group_id->raw();
$group_ids[] = $line_item_wrapper->commerce_bundle_group_id->raw();
$group_element_key = 'product_group_' . $group_id;
// Build group form.
$groups_container[$group_element_key] = array(
'#type' => 'container',
'#attributes' => array('class' => array(
'commerce-bundle-product-group',
'commerce-bundle-product-group-' . $group_id,
)),
'#tree' => TRUE,
);
$group_form = &$groups_container[$group_element_key];
// Initialize group form state.
if (!isset($form_state['product_groups'][$group_element_key])) {
$form_state['product_groups'][$group_element_key] = array();
}
// Add group fields.
// TODO: add setting for group product view mode?
$group_form['header'] = array(
'#prefix' => '<div class="commerce-bundle-group-header">',
'#suffix' => '</div>',
'unit_quantity' => array(
'#prefix' => '<span class="commerce-bundle-group-quantity">',
'#markup' => t('@unit_quantity x', array('@unit_quantity' => $group_unit_qty)),
'#suffix' => '</span> ',
),
'title' => array(
'#prefix' => '<span class="commerce-bundle-group-title">',
'#suffix' => '</span>',
),
);
if ($group_title = $line_item_wrapper->commerce_bundle_group_id->title->value()) {
$group_form['header']['title']['#markup'] = check_plain($group_title);
}
else {
$group_form['header']['title']['#markup'] = t('Product');
}
// Build attribute form.
$group_form = commerce_bundle_line_item_attribute_form($line_item, $group_form, $form_state);
// Update product to attributes default product.
if (!empty($form_state['product_groups'][$group_element_key]['default_product'])) {
$group_default_product = $form_state['product_groups'][$group_element_key]['default_product'];
$line_item_wrapper->commerce_product = $group_default_product;
// Find the bundle_item for the default product.
$default_bundle_item = commerce_bundle_item_get_product_item(
$line_item_wrapper->commerce_bundle_group_id->commerce_bundle_items->value(),
$group_default_product->product_id
);
// Set line item bundle item even if return is NULL.
$line_item_wrapper->commerce_bundle_item_id = $default_bundle_item;
}
// Update the bundle total price. Default currency code is the first line
// item in $form_state.
if (empty($currency_code)) {
$currency_code = $line_item_wrapper->commerce_unit_price->currency_code->value();
}
$bundle_product_price = commerce_bundle_get_bundle_product_price($group_id, $line_item_wrapper->commerce_bundle_item_id->raw(), TRUE);
$form_state['bundle_total_price'] += $group_unit_qty * $bundle_product_price['amount'];
// Loop through the fields on the referenced product's type.
foreach (field_info_instances('commerce_product', $line_item_wrapper->commerce_product->type->value()) as $product_field_name => $product_field) {
$reference_view_mode = $context['view_mode'];
if (!isset($product_field['display'][$reference_view_mode])) {
$reference_view_mode = 'default';
}
// Only prepare visible fields.
if (!isset($product_field['display'][$reference_view_mode]['type']) || $product_field['display'][$reference_view_mode]['type'] != 'hidden') {
// Add the product field to the entity's content array.
$content_key = 'product:' . $product_field_name;
$group_form[$content_key] = field_view_field('commerce_product', $line_item_wrapper->commerce_product->value(), $product_field_name, $reference_view_mode);
// Construct an array of classes that will be used to theme and
// target the rendered field for AJAX replacement.
$classes = array(
'commerce-product-field',
drupal_html_class('commerce-product-field-' . $product_field_name),
drupal_html_class('field-' . $product_field_name),
drupal_html_class(implode('-', array($context['entity_type'], $context['entity_id'], 'product', $product_field_name))),
);
// Add an extra class to distinguish empty product fields.
if (empty($group_form[$content_key])) {
$classes[] = 'commerce-product-field-empty';
}
// Ensure the field's content array has a prefix and suffix key.
$group_form[$content_key] += array(
'#prefix' => '',
'#suffix' => '',
);
// Add the custom div before and after the prefix and suffix.
$group_form[$content_key]['#prefix'] = '<div class="' . implode(' ', $classes) . '">' . $group_form[$content_key]['#prefix'];
$group_form[$content_key]['#suffix'] .= '</div>';
}
}
// Attach "extra fields" to the bundle representing all the extra fields
// currently attached to products.
foreach (field_info_extra_fields('commerce_product', $line_item_wrapper->commerce_product->type->value(), 'display') as $product_extra_field_name => $product_extra_field) {
$display = field_extra_fields_get_display('commerce_product', $line_item_wrapper->commerce_product->type->value(), $reference_view_mode);
// Only include extra fields that specify a theme function and that
// are visible on the current view mode.
if (!empty($product_extra_field['theme']) &&
!empty($display[$product_extra_field_name]['visible'])) {
// Add the product extra field to the entity's content array.
$content_key = 'product:' . $product_extra_field_name;
$variables = array(
$product_extra_field_name => $line_item_wrapper->commerce_product->{$product_extra_field_name}->value(),
'label' => $product_extra_field['label'] . ':',
'product' => $line_item_wrapper->commerce_product->value(),
);
$group_form[$content_key] = array(
'#markup' => theme($product_extra_field['theme'], $variables),
'#attached' => array(
'css' => array(drupal_get_path('module', 'commerce_product') . '/theme/commerce_product.theme.css'),
),
);
// Construct an array of classes that will be used to theme and
// target the rendered field for AJAX replacement.
$classes = array(
'commerce-product-extra-field',
drupal_html_class('commerce-product-extra-field-' . $product_extra_field_name),
drupal_html_class(implode('-', array($context['entity_type'], $context['entity_id'], 'product', $product_extra_field_name))),
);
// Add an extra class to distinguish empty fields.
if (empty($group_form[$content_key])) {
$classes[] = 'commerce-product-extra-field-empty';
}
// Ensure the extra field's content array has a prefix and suffix key.
$group_form[$content_key] += array(
'#prefix' => '',
'#suffix' => '',
);
// Add the custom div before and after the prefix and suffix.
$group_form[$content_key]['#prefix'] = '<div class="' . implode(' ', $classes) . '">' . $group_form[$content_key]['#prefix'];
$group_form[$content_key]['#suffix'] .= '</div>';
}
}
}
unset($line_item, $group_form, $group_state);
// Process bundle configurations after default product is set.
$bundle_configs = commerce_bundle_line_item_get_bundle_configs($form_state['line_items']);
foreach ($bundle_configs as $bundle_config_id => $bundle_config_line_items) {
foreach ($bundle_config_line_items as $line_item) {
$line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
if (isset($line_item_wrapper->commerce_bundle_config_id)) {
$line_item_wrapper->commerce_bundle_config_id = $bundle_config_id;
}
}
}
// Store the form ID as a class of the form to avoid the incrementing form ID
// from causing the AJAX refresh not to work.
$form['#attributes']['class'][] = drupal_html_class(commerce_bundle_add_to_cart_form_id($group_ids));
// Render the quantity field as either a textfield if shown or a hidden
// field if not.
if ($show_quantity) {
$form['quantity'] = array(
'#type' => 'textfield',
'#title' => t('Quantity'),
'#default_value' => $default_quantity,
'#datatype' => 'integer',
'#size' => 5,
'#weight' => 45,
);
}
else {
$form['quantity'] = array(
'#type' => 'hidden',
'#value' => $default_quantity,
'#datatype' => 'integer',
'#weight' => 45,
);
}
// Add the total bundle price to the form.
$form['bundle_total_price'] = array(
'#prefix' => '<div class="total-bundle-price">',
'#markup' => t('@label', array('@label' => $context['bundle_total_price_label'])) . ' ' . commerce_currency_format($form_state['bundle_total_price'], $currency_code),
'#suffix' => '</div>',
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Add to cart'),
'#weight' => 50,
);
// Add the handlers manually since we're using hook_forms() to associate this
// form with form IDs based on the $product_ids.
$form['#validate'][] = 'commerce_bundle_add_to_cart_form_validate';
$form['#submit'][] = 'commerce_bundle_add_to_cart_form_submit';
return $form;
}
/**
* Form validate handler: validate the bundle and quantity to add to the cart.
*/
function commerce_bundle_add_to_cart_form_validate($form, &$form_state) {
// Check to ensure the quantity is valid.
if (!is_numeric($form_state['values']['quantity']) || $form_state['values']['quantity'] <= 0) {
form_set_error('quantity', t('You must specify a valid quantity to add to the cart.'));
}
// If the custom data type attribute of the quantity element is integer,
// ensure we only accept whole number values.
if ($form['quantity']['#datatype'] == 'integer' &&
(int) $form_state['values']['quantity'] != $form_state['values']['quantity']) {
form_set_error('quantity', t('You must specify a whole number for the quantity.'));
}
// Validate any line item fields that may have been included on the form.
if (!empty($form_state['line_items'])) {
foreach ($form_state['line_items'] as $line_item) {
$line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
if (!isset($line_item_wrapper->commerce_bundle_group_id)) {
continue;
}
/** @todo: throw an error if we dont have a group? **/
$group_id = $line_item_wrapper->commerce_bundle_group_id->raw();
if (empty($form['product_groups']['product_group_' . $group_id]) ||
empty($form_state['values']['product_group_' . $group_id])) {
continue;
}
$group_form = $form['product_groups']['product_group_' . $group_id];
$group_form_state_values = &$form_state['values']['product_group_' . $group_id];
// If the attributes matching product selector was used, set the value of the
// product_id field to match; this will be fixed on rebuild when the actual
// default product will be selected based on the product selector value.
if (!empty($group_form_state_values['attributes']['product_select'])) {
form_set_value($group_form['product_id'], $group_form_state_values['attributes']['product_select'], $form_state);
}
/** @todo: pass in nest form state? or does this figure it out ***/
if (!empty($group_form['line_item_fields'])) {
field_attach_form_validate('commerce_line_item', $line_item, $group_form['line_item_fields'], $form_state);
}
}
}
}
/**
* Form submit handler: add the selected bundle to the cart.
*/
function commerce_bundle_add_to_cart_form_submit($form, &$form_state) {
if (empty($form_state['line_items'])) {
return;
}
$add_to_cart_deltas = array();
$bundle_quantity = isset($form_state['values']['quantity']) ? $form_state['values']['quantity'] : 1;
foreach ($form_state['line_items'] as $state_line_item_delta => &$state_line_item) {
// If the line item passed to the function is new...
if (empty($state_line_item->line_item_id)) {
$state_line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $state_line_item);
if (!isset($state_line_item_wrapper->commerce_bundle_id) ||
!isset($state_line_item_wrapper->commerce_bundle_group_id) ||
!isset($state_line_item_wrapper->commerce_bundle_item_id)) {
continue;
}
$group_id = $state_line_item_wrapper->commerce_bundle_group_id->raw();
if (empty($form_state['values']['product_group_' . $group_id])) {
continue;
}
$group_form_state_values = &$form_state['values']['product_group_' . $group_id];
$group_form = $form['product_groups']['product_group_' . $group_id];
$product_id = $group_form_state_values['product_id'];
$product = commerce_product_load($product_id);
$bundler_wrapper = $state_line_item_wrapper->commerce_bundle_id;
$group_wrapper = $state_line_item_wrapper->commerce_bundle_group_id;
$bundle_item_wrapper = $state_line_item_wrapper->commerce_bundle_item_id;
// Create the new bundle product line item of the same type.
$line_item = commerce_bundle_product_line_item_new($product,
$bundle_item_wrapper->value(),
$group_wrapper->value(),
$bundler_wrapper->value(),
$bundle_quantity,
0,
$state_line_item->data,
$state_line_item->type
);
// 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.
if (!empty($group_form_state_values['line_item_fields'])) {
foreach ($group_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($group_form_state_values['line_item_fields'][$field_name]);
}
}
}
// Unset the line item field values array if it is now empty.
if (empty($group_form_state_values['line_item_fields'])) {
unset($group_form_state_values['line_item_fields']);
}
// Add field data to the line item.
if (!empty($group_form['line_item_fields'])) {
field_attach_submit('commerce_line_item', $line_item, $group_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);
// 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);
if (!is_null($line_item_wrapper->commerce_unit_price->value())) {
// Add the product to the specified shopping cart.
$state_line_item = $line_item;
$add_to_cart_deltas[] = $state_line_item_delta;
}
else {
drupal_set_message(t('%title could not be added to your cart.', array('%title' => entity_label($bundler_wrapper->type(), $bundler_wrapper->value()))), 'error');
return;
}
}
}
unset($state_line_item);
// Set bundle configuration.
$line_item_configs_updated = array();
$bundle_configs = commerce_bundle_line_item_get_bundle_configs($form_state['line_items']);
foreach ($bundle_configs as $bundle_config_id => $bundle_config_line_items) {
foreach ($bundle_config_line_items as $line_item) {
$line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
if (isset($line_item_wrapper->commerce_bundle_config_id)) {
$line_item_wrapper->commerce_bundle_config_id = $bundle_config_id;
if (isset($line_item->line_item_id)) {
$line_item_configs_updated[] = $line_item->line_item_id;
}
}
}
}
// Save line items.
foreach ($form_state['line_items'] as $delta => $line_item) {
if (in_array($delta, $add_to_cart_deltas)) {
// Add new line items to the cart.
$form_state['line_items'][$delta] = commerce_cart_product_add(
$form_state['values']['uid'],
$line_item,
isset($line_item->data['context']['add_to_cart_combine']) ? $line_item->data['context']['add_to_cart_combine'] : TRUE
);
}
elseif (isset($line_item->line_item_id) &&
in_array($line_item->line_item_id, $line_item_configs_updated)) {
// Update existing line items.
commerce_line_item_save($line_item);
// Clear the line item cache so that updates show on next load..
entity_get_controller('commerce_line_item')->resetCache(array($line_item->line_item_id));
}
}
}
/**
* Returns the attribute form for a given bundle line item.
*
* @param $line_item
* The bundle line item.
* @param $form
* The form array.
* @param $form_state
* The form state.
*
* @return
* An attribute form array
*/
function commerce_bundle_line_item_attribute_form($line_item, $form, &$form_state) {
$line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
if (!isset($line_item_wrapper->commerce_bundle_group_id)) {
return $form;
}
$group_wrapper = $line_item_wrapper->commerce_bundle_group_id;
if (!isset($group_wrapper->commerce_bundle_items)) {
return $form;
}
$group_id = $line_item_wrapper->commerce_bundle_group_id->raw();
$group_element_key = 'product_group_' . $group_id;
$group_state_storage = &$form_state['product_groups'][$group_element_key];
$group_state_values = &$form_state['values'][$group_element_key];
$group_product_ids = array();
foreach ($group_wrapper->commerce_bundle_items as $bundle_item_wrapper) {
// Skip disabled bundle items.
if (!$bundle_item_wrapper->status->value()) {
continue;
}
if (isset($bundle_item_wrapper->commerce_bundle_product) &&
($bundle_item_product_id = $bundle_item_wrapper->commerce_bundle_product->raw())) {
$group_product_ids[$bundle_item_product_id] = $bundle_item_product_id;
}
}
if (empty($group_product_ids)) {
return $form;
}
// We've gathered all the product id's for the current group of interest.
// Load all the active products intended for sale on this form.
$products = commerce_product_load_multiple($group_product_ids, array('status' => 1));
// If no products were returned...
if (count($products) == 0) {
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Product not available'),
'#weight' => 15,
// Do not set #disabled in order not to prevent submission.
'#attributes' => array('disabled' => 'disabled'),
'#validate' => array('commerce_cart_add_to_cart_form_disabled_validate'),
);
}
else {
// If the form is for a single product and displaying attributes on a single
// product Add to Cart form is disabled in the form context, store the
// product_id in a hidden form field for use by the submit handler.
if (count($products) == 1 && empty($line_item->data['context']['show_single_product_attributes'])) {
$group_state_storage['default_product'] = reset($products);
$form['product_id'] = array(
'#type' => 'hidden',
'#value' => key($products),
);
}
else {
// However, if more than one products are represented on it, attempt to
// use smart select boxes for the product selection. If the products are
// all of the same type and there are qualifying fields on that product
// type, display their options for customer selection.
$qualifying_fields = array();
$same_type = TRUE;
$type = '';
// Find the default product so we know how to set default options on the
// various Add to Cart form widgets and an array of any matching product
// based on attribute selections so we can add a selection widget.
$matching_products = array();
$default_product = NULL;
$attribute_names = array();
$unchanged_attributes = array();
foreach ($products as $product_id => $product) {
$product_wrapper = entity_metadata_wrapper('commerce_product', $product);
// Store the first product type.
if (empty($type)) {
$type = $product->type;
}
// If the current product type is different from the first, we are not
// dealing with a set of same typed products.
if ($product->type != $type) {
$same_type = FALSE;
}
// If the form state contains a set of attribute data, use it to try
// and determine the default product.
$changed_attribute = NULL;
if (!empty($group_state_values['attributes'])) {
$match = TRUE;
// Set an array of checked attributes for later comparison against the
// default matching product.
if (empty($attribute_names)) {
$attribute_names = (array) array_diff_key($group_state_values['attributes'], array('product_select' => ''));
$unchanged_attributes = $group_state_values['unchanged_attributes'];
}
foreach ($attribute_names as $key => $value) {
// If this is the attribute widget that was changed...
if ($value != $unchanged_attributes[$key]) {
// Store the field name.
$changed_attribute = $key;
// Clear the input for the "Select a product" widget now if it
// exists on the form since we know an attribute was changed.
unset($form_state['input'][$group_element_key]['attributes']['product_select']);
}
// If a field name has been stored and we've moved past it to
// compare the next attribute field...
if (!empty($changed_attribute) && $changed_attribute != $key) {
// Wipe subsequent values from the form state so the attribute
// widgets can use the default values from the new default product.
unset($form_state['input'][$group_element_key]['attributes'][$key]);
// Don't accept this as a matching product.
continue;
}
if ($product_wrapper->{$key}->raw() != $value) {
$match = FALSE;
}
}
// If the changed field name has already been stored, only accept the
// first matching product by ignoring the rest that would match. An
// exception is granted for additional matching products that share
// the exact same attribute values as the first.
if ($match && !empty($changed_attribute) && !empty($matching_products)) {
reset($matching_products);
$matching_product = $matching_products[key($matching_products)];
$matching_product_wrapper = entity_metadata_wrapper('commerce_product', $matching_product);
foreach ($attribute_names as $key => $value) {
if ($product_wrapper->{$key}->raw() != $matching_product_wrapper->{$key}->raw()) {
$match = FALSE;
}
}
}
if ($match) {
$matching_products[$product_id] = $product;
}
}
}
// Set the default product now if it isn't already set.
if (empty($matching_products)) {
// If a product ID value was passed in, use that product if it exists.
if (!empty($group_state_values['product_id']) &&
!empty($products[$group_state_values['product_id']])) {
$default_product = $products[$group_state_values['product_id']];
}
elseif (empty($form_state['values']) &&
!empty($line_item_wrapper->commerce_product) &&
!empty($products[$line_item_wrapper->commerce_product->raw()])) {
// If this is the first time the form is built, attempt to use the
// product specified by the line item.
$default_product = $products[$line_item_wrapper->commerce_product->raw()];
}
else {
reset($products);
$default_product = $products[key($products)];
}
}
else {
// If the product selector has a value, use that.
if (!empty($group_state_values['attributes']['product_select']) &&
!empty($products[$group_state_values['attributes']['product_select']]) &&
in_array($products[$group_state_values['attributes']['product_select']], $matching_products)) {
$default_product = $products[$group_state_values['attributes']['product_select']];
}
else {
reset($matching_products);
$default_product = $matching_products[key($matching_products)];
}
}
// Wrap the default product for later use.
$default_product_wrapper = entity_metadata_wrapper('commerce_product', $default_product);
$group_state_storage['default_product'] = $default_product;
// If all the products are of the same type...
if ($same_type) {
// Loop through all the field instances on that product type.
foreach (field_info_instances('commerce_product', $type) as $field_name => $instance) {
// A field qualifies if it is single value, required and uses a widget
// with a definite set of options. For the sake of simplicity, this is
// currently restricted to fields defined by the options module.
$field = field_info_field($field_name);
// If the instance is of a field type that is eligible to function as
// a product attribute field and if its attribute field settings
// specify that this functionality is enabled...
if (commerce_cart_field_attribute_eligible($field) && commerce_cart_field_instance_is_attribute($instance)) {
// Get the options properties from the options module for the
// attribute widget type selected for the field, defaulting to the
// select list options properties.
$commerce_cart_settings = commerce_cart_field_instance_attribute_settings($instance);
switch ($commerce_cart_settings['attribute_widget']) {
case 'checkbox':
$widget_type = 'onoff';
break;
case 'radios':
$widget_type = 'buttons';
break;
default:
$widget_type = 'select';
}
$properties = _options_properties($widget_type, FALSE, TRUE, TRUE);
// Try to fetch localized names.
$allowed_values = NULL;
// Prepare translated options if using the i18n_field module.
if (module_exists('i18n_field')) {
if (($translate = i18n_field_type_info($field['type'], 'translate_options'))) {
$allowed_values = $translate($field);
_options_prepare_options($allowed_values, $properties);
}
// Translate the field title if set.
if (!empty($instance['label'])) {
$instance['label'] = i18n_field_translate_property($instance, 'label');
}
}
// Otherwise just use the base language values.
if (empty($allowed_values)) {
$allowed_values = _options_get_options($field, $instance, $properties, 'commerce_product', $default_product);
}
// Only consider this field a qualifying attribute field if we could
// derive a set of options for it.
if (!empty($allowed_values)) {
$qualifying_fields[$field_name] = array(
'field' => $field,
'instance' => $instance,
'commerce_cart_settings' => $commerce_cart_settings,
'options' => $allowed_values,
'weight' => $instance['widget']['weight'],
'required' => $instance['required'],
);
}
}
}
}
// Otherwise for products of varying types, display a simple select list
// by product title.
if (!empty($qualifying_fields)) {
$used_options = array();
$field_has_options = array();
// Sort the fields by weight.
uasort($qualifying_fields, 'drupal_sort_weight');
foreach ($qualifying_fields as $field_name => $data) {
// Build an options array of widget options used by referenced products.
foreach ($products as $product_id => $product) {
$product_wrapper = entity_metadata_wrapper('commerce_product', $product);
// Only add options to the present array that appear on products that
// match the default value of the previously added attribute widgets.
foreach ($used_options as $used_field_name => $unused) {
// Don't apply this check for the current field being evaluated.
if ($used_field_name == $field_name) {
continue;
}
if (isset($form['attributes'][$used_field_name]['#default_value'])) {
if ($product_wrapper->{$used_field_name}->raw() != $form['attributes'][$used_field_name]['#default_value']) {
continue 2;
}
}
}
// With our hard dependency on widgets provided by the Options
// module, we can make assumptions about where the data is stored.
if ($product_wrapper->{$field_name}->raw() != NULL) {
$field_has_options[$field_name] = TRUE;
}
$used_options[$field_name][] = $product_wrapper->{$field_name}->raw();
}
// If for some reason no options for this field are used, remove it
// from the qualifying fields array.
if (empty($field_has_options[$field_name]) || empty($used_options[$field_name])) {
unset($qualifying_fields[$field_name]);
}
else {
$form['attributes'][$field_name] = array(
'#type' => $data['commerce_cart_settings']['attribute_widget'],
'#title' => commerce_cart_attribute_widget_title($data['instance']),
'#options' => array_intersect_key($data['options'], drupal_map_assoc($used_options[$field_name])),
'#default_value' => $default_product_wrapper->{$field_name}->raw(),
'#weight' => $data['instance']['widget']['weight'],
'#ajax' => array(
'callback' => 'commerce_bundle_add_to_cart_form_attributes_refresh',
),
);
// Add the empty value if the field is not required and products on
// the form include the empty value.
if (!$data['required'] && in_array('', $used_options[$field_name])) {
$form['attributes'][$field_name]['#empty_value'] = '';
}
$form['unchanged_attributes'][$field_name] = array(
'#type' => 'value',
'#value' => $default_product_wrapper->{$field_name}->raw(),
);
}
}
if (!empty($form['attributes'])) {
$form['attributes'] += array(
'#tree' => 'TRUE',
'#prefix' => '<div class="attribute-widgets">',
'#suffix' => '</div>',
'#weight' => 0,
);
$form['unchanged_attributes'] += array(
'#tree' => 'TRUE',
);
// If the matching products array is empty, it means this is the first
// time the form is being built. We should populate it now with
// products that match the default attribute options.
if (empty($matching_products)) {
foreach ($products as $product_id => $product) {
$product_wrapper = entity_metadata_wrapper('commerce_product', $product);
$match = TRUE;
foreach (element_children($form['attributes']) as $field_name) {
if ($product_wrapper->{$field_name}->raw() != $form['attributes'][$field_name]['#default_value']) {
$match = FALSE;
}
}
if ($match) {
$matching_products[$product_id] = $product;
}
}
}
// If there were more than one matching products for the current
// attribute selection, add a product selection widget.
if (count($matching_products) > 1) {
$options = array();
foreach ($matching_products as $product_id => $product) {
$options[$product_id] = $product->title;
}
// Note that this element by default is a select list, so its
// #options are not sanitized here. Sanitization will occur in a
// check_plain() in the function form_select_options(). If you alter
// this element to another #type, such as 'radios', you are also
// responsible for looping over its #options array and sanitizing
// the values.
$form['attributes']['product_select'] = array(
'#type' => 'select',
'#title' => t('Select a product'),
'#options' => $options,
'#default_value' => $default_product->product_id,
'#weight' => 40,
'#ajax' => array(
'callback' => 'commerce_bundle_add_to_cart_form_attributes_refresh',
),
);
}
$form['product_id'] = array(
'#type' => 'hidden',
'#value' => $default_product->product_id,
);
}
}
// If the products referenced were of different types or did not posess
// any qualifying attribute fields...
if (!$same_type || empty($qualifying_fields)) {
// For a single product form, just add the hidden product_id field.
if (count($products) == 1) {
$form['product_id'] = array(
'#type' => 'hidden',
'#value' => $default_product->product_id,
);
}
else {
// Otherwise add a product selection widget.
$options = array();
foreach ($products as $product_id => $product) {
$options[$product_id] = $product->title;
}
// Note that this element by default is a select list, so its #options
// are not sanitized here. Sanitization will occur in a check_plain() in
// the function form_select_options(). If you alter this element to
// another #type, such as 'radios', you are also responsible for looping
// over its #options array and sanitizing the values.
$form['product_id'] = array(
'#type' => 'select',
'#options' => $options,
'#default_value' => $default_product->product_id,
'#weight' => 0,
'#ajax' => array(
'callback' => 'commerce_bundle_add_to_cart_form_attributes_refresh',
),
);
}
}
}
}
return $form;
}
/**
* Ajax callback: returns AJAX commands when an attribute widget is changed.
*/
function commerce_bundle_add_to_cart_form_attributes_refresh($form, $form_state) {
$commands = array();
// Render the form afresh to capture any changes to the available widgets
// based on the latest selection.
$commands[] = ajax_command_replace('.' . drupal_html_class($form['#form_id']), drupal_render($form));
// Allow other modules to add arbitrary AJAX commands on the refresh.
drupal_alter('commerce_bundle_add_to_cart_form_attributes_refresh', $commands, $form, $form_state);
return array('#type' => 'ajax', '#commands' => $commands);
}
/**
* Implements hook_commerce_cart_line_item_refresh().
*
* Ensures the proper price for a bundle line item is set according to bundles
* pricing precedence (group, bundle item, product).
*/
function commerce_bundle_commerce_cart_line_item_refresh($line_item, $order_wrapper) {
if ($line_item->type == 'commerce_bundle_line_item') {
$line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
commerce_bundle_product_line_item_populate($line_item_wrapper->value(),
$line_item_wrapper->commerce_bundle_id->value(),
$line_item_wrapper->commerce_bundle_group_id->value(),
$line_item_wrapper->commerce_bundle_item_id->value(),
$line_item_wrapper->commerce_product->value());
// Process the unit price through Rules so it reflects the user's actual
// current purchase price.
rules_invoke_event('commerce_product_calculate_sell_price', $line_item);
}
}
/**
* Implements hook_commerce_cart_product_comparison_properties_alter().
*/
function commerce_bundle_commerce_cart_product_comparison_properties_alter(&$comparison_properties, $line_item_clone) {
// Force separate line items based on generated bundle config id.
$comparison_properties[] = 'commerce_bundle_config_id';
}
/**
* Returns the correct price for a bundle product based on pricing precedence.
*
* @param $group_id
* The grouping products product id.
* @param $bundle_item_id
* The bundle item's id.
* @return array $bundle_unit_price
* A price field data array.
*/
function commerce_bundle_get_bundle_product_price($group_id, $bundle_item_id, $apply_discounts = FALSE) {
$group_product = commerce_product_load($group_id);
$group_wrapper = entity_metadata_wrapper('commerce_product', $group_product);
$bundle_item = entity_load('commerce_bundle_item', array($bundle_item_id));
$bundle_item_wrapper = entity_metadata_wrapper('commerce_bundle_item', current($bundle_item));
// Calculate the bundle unit price on the line item.
if ($group_wrapper->commerce_bundle_group_price->value()) {
$bundle_unit_price = commerce_price_wrapper_value($group_wrapper, 'commerce_bundle_group_price', TRUE);
}
elseif ($bundle_item_wrapper->commerce_bundle_price->value()) {
$bundle_unit_price = commerce_price_wrapper_value($bundle_item_wulesapper, 'commerce_bundle_price', TRUE);
}
elseif ($apply_discounts) {
$p = commerce_product_load($bundle_item_wrapper->commerce_bundle_product->product_id->raw());
$bundle_unit_price = commerce_product_calculate_sell_price($p);
}
else {
$bundle_unit_price = commerce_price_wrapper_value($bundle_item_wrapper->commerce_bundle_product, 'commerce_price', TRUE);
}
return $bundle_unit_price;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment