ptype || !isset($node->nid)) { return; } // Provide variations functionality. if ($variations = subproducts_get_variations($node->ptype)) { switch ($op) { case 'load': if ($node->pparent) { return array('variations' => subproducts_get_node_variations($node->nid)); } else { return array('children' => subproducts_get_children($node)); } case 'view': if (count($node->children)) { $items = cart_get_items(); $child_variations = subproducts_get_child_attributes($node); $form['#method'] = 'post'; $form['#action'] = url("cart/add/$node->nid", "destination=node/$node->nid"); $form['#tree'] = TRUE; foreach ($variations as $variation) { $options = array(); foreach($variation->attributes as $attribute) { if (in_array($attribute->aid, $child_variations)) { $options[$attribute->aid] = subproducts_surcharge_extra($attribute); } } $form['variations'][$variation->vid] = array( '#type' => 'select', '#title' => $variation->name, '#default_value' => $node->variations[$variation->vid], '#options' => $options, '#description' => t('The %name of this product', array('%name' => theme('placeholder', $variation->name))) ); } $form['add_to_cart'] = array( '#type' => 'submit', '#value' => t('add to cart') ); $output .= drupal_get_form('subproducts_add_to_cart', $form); /* search for subproducts in the cart and display table */ $subproducts = $node->children; foreach ($items as $snid => $item) { if (in_array($snid, $subproducts)) { $product = node_load($snid); $rows[] = array( array('data' => $product->title), array('data' => $item->qty, 'align' => 'right'), array('data' => l(t('add'), "cart/add/$product->nid", array(), "destination=node/$product->pparent")) ); } } if ($rows) { $header = array( array('data' => t('product')), array('data' => t('quantity')), array('data' => ''), ); $table = theme('table', $header, $rows); $output .= theme('subproducts_in_cart', $table); } $node->subproduct_options = $output; $node->body = theme('subproducts_product_options', $node); } else if ($node->pparent) { if ($node->variations) { foreach ($node->variations as $aid) { $wheres[] = 'a.aid = ' . $aid; } $attributes = array(); $result = db_query('SELECT a.*, v.name AS variation FROM {ec_attribute} a INNER JOIN {ec_variation} v ON a.vid = v.vid WHERE ' . implode(' OR ', $wheres) . ' ORDER BY a.weight, a.name'); while ($attribute = db_fetch_object($result)) { $attributes[] = $attribute; // This will automate the price setting, so that prices don't need to be updated every time a surcharge is changed. //$node->price += $attribute->surcharge; } $node->body .= theme('subproducts_attribute_items', $attributes); } } return; case 'form post': if ($node->pparent || ((arg(3) == 'parent') && ($node->pparent = arg(4)))) { $form['#tree'] = TRUE; $form['variations'] = array( '#type' => 'fieldset', '#title' => t('Variations') ); foreach ($variations as $variation) { $options = array(); foreach($variation->attributes as $attribute) { $options[$attribute->aid] = subproducts_surcharge_extra($attribute); } $form['variations'][$variation->vid] = array( '#type' => 'select', '#title' => $variation->name, '#default_value' => $node->variations[$variation->vid], '#options' => $options, '#description' => t('The %name of this product', array('%name' => theme('placeholder', $variation->name))), ); } $output = drupal_get_form('subproducts_variations_form_post', $form); } return $output; case 'validate': if ($node->pparent && $node->variations) { // Check if a subproduct with these variations, and, if we're editing, that it's not the one being edited. if ($nid = subproducts_get_variations_subproduct($node->pparent, $node->variations)) { if ($nid != $node->nid) { form_set_error('variations', t('A subproduct with these variations already exists.')); } } } return; case 'submit': if ($node->nid && !$node->pparent) { $existing_node = node_load($node->nid); if ($node->price != $existing_node->price) { $node->reprice_subproducts = TRUE; $node->old_price = $existing_node->price; } if ($node->title != $existing_node->title) { $node->retitle_subproducts = TRUE; } if ($node->status != $existing_node->status) { $node->update_subproducts_status = TRUE; } } return; case 'update': if (!$node->variations) { // If price has changed for a parent product, update price of subproducts. if ($node->reprice_subproducts) { $count = 0; foreach (subproducts_get_children($node) as $nid) { $subproduct = node_load($nid); subproducts_reset_price_variation($subproduct, $node); unset($subproduct[0]); node_save($subproduct); $count++; } drupal_set_message(format_plural($count, '@count subproduct price updated.', '%count subproduct prices updated.')); } // If title has changed for a parent product, update title of subproducts. if ($node->retitle_subproducts) { $count = 0; foreach (subproducts_get_children($node) as $nid) { $subproduct = node_load($nid); $attributes = subproducts_get_node_variations_string($nid); $subproduct->title = $node->title . ' ' . implode(' ', $attributes); node_save($subproduct); $count++; } drupal_set_message(format_plural($count, '@count subproduct title updated.', '%count subproduct titles updated.')); } // If the product has been unpublished, unpublish all subproducts. if ($node->update_subproducts_status && count($node->children)) { db_query('UPDATE {node} SET status = %d WHERE nid IN('. implode(',', $node->children) .')', $node->status); } return; } else { db_query("DELETE FROM {ec_product_attribute} WHERE nid = %d", $node->nid); } // Having removed obsolete values, we continue on to insert. case 'insert': if ($node->variations) { foreach ($node->variations as $variation) { db_query("INSERT INTO {ec_product_attribute} (nid, aid) VALUES (%d, %d)", $node->nid, $variation); } } elseif (subproducts_access('administer', $node)) { $_REQUEST['edit']['destination'] = "node/$node->nid/subproducts/generate"; } return; case 'delete': // Delete any subproducts. if (variable_get('subproducts_delete_children', 1) && count($node->children)) { foreach ($node->children as $nid) { node_delete($nid); } } // Delete any products that have this as a base product. $result = db_query("SELECT product FROM {ec_product_base} WHERE base = %d", $node->nid); if (db_num_rows($result)) { while($product = db_fetch_object($result)) { node_delete($product->product); } } db_query("DELETE FROM {ec_product_attribute} WHERE nid = %d", $node->nid); return; } } // Provide base product functionality. if ($bases = subproducts_base_product_types($node->ptype)) { switch ($op) { case 'load': if ($node->pparent) { return db_fetch_array(db_query('SELECT b.base, p.price AS base_price FROM {ec_product_base} b INNER JOIN {ec_product} p ON b.base = p.nid WHERE b.product = %d', $node->nid)); } else { $price_type = db_query('SELECT type AS price_type FROM {ec_subproduct_pricing} WHERE nid = %d', $node->nid); $children = subproducts_get_children($node); if (db_num_rows($price_type)) { return array_merge(db_fetch_array($price_type), array('children' => $children)); } else { return array('children' => $children); } } case 'view': if (count($node->children) && (arg(0) == 'node' || arg(0) == 'send')) { $items = cart_get_items(); $subproducts = $node->children; if ($base_attributes = subproducts_get_base_attributes($node)) { $ptypes = module_invoke('product', 'get_ptypes'); // This variable is used to generate data to be read in through javascript on the client side. $products = array(); $form['#method'] = 'post'; $form['#action'] = url("cart/add/$node->nid", "destination=node/$node->nid"); $form['#tree'] = TRUE; foreach($bases as $ptype) { $wheres = array(); $options = array(); // Find all published parents of published base products of subproducts of the current node. $base_parents = subproducts_get_base_data($node); $products[$ptype] = array(); $products[$ptype]['products'] = array(); $group = '
' . t('Based on %title.', array('%url' => url('node/' . $node->base), '%title' => $base->title)) . '
'; } return; case 'form post': if ($node->pparent || ((arg(3) == 'parent') && ($node->pparent = arg(4)))) { $form['group'] = array( '#type' => 'fieldset', '#title' => t('Base') ); if ($node->base) { $base = node_load($node->base); $based_on = t('Based on %title.', array('%url' => url('node/' . $node->base), '%title' => $base->title)); $form['group']['based_on'] = array('#type' => 'markup', '#value' => $based_on); $form['group']['base'] = array( '#type' => 'hidden', '#value' => $node->base, ); } else { $form['group']['no_interface'] = array('#type' => 'markup', '#value' => t('No interface yet for manually selecting base products.')); } $output = drupal_get_form('subproducts_base_form', $form); } return $output; case 'validate': // This validation is not yet used, as we don't manually generate new subproducts. // Before being used, it would need to be adapted to work for node updates as well as inserts. // See the attribute version of validation, above. if (!$node->nid && $node->base) { if (db_num_rows(db_query("SELECT * FROM {ec_product_base} WHERE product IN (%s) AND base = %d", implode(',', $node->children), $node->base))) { form_set_error('variations', t('A subproduct with this base already exists.')); } } return; case 'submit': if($node->nid && !$node->base) { // Determine whether a new price or a new price type has been assigned to a parent product. // If so, set a property that will be referenced when the node is updated. $existing_node = node_load($node->nid); if (($node->price != $existing_node->price) || (isset($node->price_type) && ($node->price_type != $existing_node->price_type))) { $node->reprice_subproducts = TRUE; } if ($node->title != $existing_node->title) { $node->retitle_subproducts = TRUE; } if ($node->status != $existing_node->status) { $node->update_subproducts_status = TRUE; } } return; case 'update': if (!$node->base) { // If price has changed for a parent product, update price of subproducts. if ($node->reprice_subproducts) { $count = 0; foreach (subproducts_get_children($node) as $nid) { $subproduct = node_load($nid); subproducts_reset_price_base($subproduct, $node); node_save($subproduct); $count++; } drupal_set_message(format_plural($count, '@count subproduct price updated.', '%count subproduct prices updated.')); // We can put this update operation here because it will be called if there has been a data change. if (isset($node->price_type)) { db_query("UPDATE {ec_subproduct_pricing} SET type = '%d' WHERE nid = '%d'", $node->price_type, $node->nid); } } // If title has changed for a parent product, update title of subproducts. if ($node->retitle_subproducts) { $count = 0; foreach (subproducts_get_children($node) as $nid) { $subproduct = node_load($nid); $base = db_fetch_object(db_query('SELECT title FROM {node} WHERE nid = %d', $subproduct->base)); $subproduct->title = $node->title . ' ' . $base->title; node_save($subproduct); $count++; } drupal_set_message(format_plural($count, '@count subproduct title updated.', '%count subproduct titles updated.')); } // If the product has been unpublished, unpublish all subproducts. if ($node->update_subproducts_status && count($node->children)) { db_query('UPDATE {node} SET status = %d WHERE nid IN('. implode(',', $node->children) .')', $node->status); } return; } else { db_query("DELETE FROM {ec_product_base} WHERE product = %d", $node->nid); } // Having removed obsolete values, we continue on to insert. case 'insert': if ($node->base) { db_query("INSERT INTO {ec_product_base} (product, base) VALUES (%d, %d)", $node->nid, $node->base); } elseif (subproducts_access('administer', $node)) { $_REQUEST['edit']['destination'] = "node/$node->nid/subproducts/generate"; } if (isset($node->price_type)) { db_query("INSERT INTO {ec_subproduct_pricing} (nid, type) VALUES ('%d', '%d')", $node->nid, $node->price_type); } return; case 'delete': // Delete any subproducts. if (variable_get('subproducts_delete_children', 1) && count($node->children)) { foreach ($node->children as $nid) { node_delete($nid); } } db_query("DELETE FROM {ec_product_base} WHERE product = %d", $node->nid); db_query('DELETE FROM {ec_subproduct_pricing} WHERE nid = %d', $node->nid); return; } } } /** * Implementation of hook_perm(). */ function subproducts_perm() { return array('administer variations', 'administer subproducts', 'administer own subproducts'); } /** * Implementation of hook_access(). */ function subproducts_access($op, $node) { global $user; if (user_access('administer products')) { return TRUE; } if ($op == 'administer') { // Must have create access to product type. $access = module_invoke('product', 'access', 'create', $node); if ($access == TRUE) { // In addition, must have permission to administer subproducts. if (user_access('administer subproducts') || (user_access('administer own subproducts') && ($user->uid == $node->uid))) { return TRUE; } } } return FALSE; } /** * Implementation of hook_menu(). * * Users with the 'administer variations' permission have access to * a UI for creating and editing variations and attributes associated * with given product types. This UI is accessed through a navigation * menu item administer > product variations. * * Users with permissions to administer subproducts have access to * a 'subproducts' tab when viewing (potential) parent nodes. Sub-tabs * are 'add' (for variation-type subproducts) or 'select bases' for * base-type ones and 'list' if subproducts are already registered. */ function subproducts_menu($may_cache) { $items = array(); if ($may_cache) { $items[] = array( 'path' => 'admin/ecsettings/subproducts', 'title' => 'Sub Products', 'callback' => 'drupal_get_form', 'callback arguments' => array('subproducts_ec_settings'), 'access' => user_access('administer store'), 'type' => MENU_NORMAL_ITEM, ); } else { if ($ptypes = subproducts_variations_product_types()) { $access = user_access('administer variations'); $items[] = array( 'path' => 'admin/ecsettings/variation', 'title' => t('Product Variations'), 'description' => 'Create and update product variations.', 'callback' => 'subproducts_admin', 'access' => $access, 'type' => MENU_NORMAL_ITEM); $default = current($ptypes); $types = module_invoke('product', 'get_ptypes'); foreach ($ptypes as $ptype) { $items[] = array( 'path' => 'admin/ecsettings/variation/' . $ptype, 'title' => $types[$ptype], 'access' => $access, 'type' => ($ptype == $default) ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK, 'weight' => ($ptype == $default) ? -10 : 0); $items[] = array( 'path' => 'admin/ecsettings/variation/' . $ptype . '/list', 'title' => t('list'), 'access' => $access, 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10); $items[] = array( 'path' => 'admin/ecsettings/variation/' . $ptype . '/add/variation', 'title' => t('add variation'), 'access' => $access, 'type' => MENU_LOCAL_TASK, 'weight' => -5); // Only display add attribute option if there is already at least one variation. if (db_num_rows(db_query("SELECT vid FROM {ec_variation} WHERE ptype = '%s'", $ptype))) { $items[] = array( 'path' => 'admin/ecsettings/variation/' . $ptype . '/add/attribute', 'title' => t('add attribute'), 'access' => $access, 'type' => MENU_LOCAL_TASK); } $items[] = array( 'path' => 'admin/ecsettings/variation/' . $ptype . '/edit/variation', 'title' => t('edit variation'), 'access' => $access, 'type' => MENU_CALLBACK); $items[] = array( 'path' => 'admin/ecsettings/variation/' . $ptype . '/edit/attribute', 'title' => t('edit attribute'), 'access' => $access, 'type' => MENU_CALLBACK); } } if ((arg(0) == 'node' || arg(0) == 'send') && is_numeric(arg(1))) { $node = node_load(arg(1)); if ($access = subproducts_access('administer', $node) && is_array($node->children)) { $items[] = array( 'path' => 'node/' . arg(1) . '/subproducts', 'title' => t('subproducts'), 'callback' => 'subproducts_list', 'access' => $access, 'type' => MENU_LOCAL_TASK, 'weight' => 3 ); $items[] = array( 'path' => 'node/' . arg(1) . '/subproducts/generate', 'title' => count($node->children) ? t('add') : t('generate'), 'callback' => 'subproducts_generate', 'access' => $access, 'type' => MENU_LOCAL_TASK, 'weight' => 1 ); $items[] = array('path' => 'node/' . arg(1) . '/subproducts/list', 'title' => t('list'), 'callback' => 'subproducts_list', 'access' => $access, 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => 0 ); } } } return $items; } /** * Implementation of hook_help() */ function subproducts_help($section) { switch ($section) { case 'admin/help#subproducts': return t('Definitions
After enabling at least one product module that supports variations, you can define variations and their attributes. You do this in administer >> ecsettings >> product variations. Here, for each product type supporting variations, you can define a list of variations and then give each of them attributes. The interface is based on the forum adminstration interface, so should be familiar to anyone who has created forum containers and forums.
This assumes you have enabled the apparel product. You will then need to browse to administer >> ecsettings >> product variations and select the apparel tab. It may be the only one available if apparel is the only product installed that supports variations.
When you have variations and their attributes defined for a particular product type, you can automatically generate subproducts. To do so, create a new product, view it, and click the "subproducts" tab.
From here, generating the subproducts is a two-step process.
The first screen lists all available variation parameters. Here, you select which apply to this product, and set values inventory. So, for example, if a particular product only comes in black, you would uncheck all other colours.
The stock inventory values here will be summed up to create default values for particular subproducts.
Before submitting this page, you can choose whether to keep these settings as defaults for next time.
The next page presents all combinations of the selected variations. On this page you select which variations to generate, and you can tweak the stock amounts. When you submit, you\'re redirected to the subproducts tab listing the newly generated subproducts.
To have variations, a product type needs to register itself as supporting subproducts of its own product type. This is done through a _productapi() hook with $op = \'subproduct_types\'.
For example, to enable variations for a product type "sandwich", you would include the following lines in function sandwich_productapi() within the switch($op) block:
case \'subproduct_types\':
return array(\'sandwich\');
', array('%variation_url' => url('admin/ecsettings/variation')));
}
}
/**
* Return an array of children for a given product.
*
* This method is called by the 'load' option of nodeapi to add
* subproduct information to parent products, and can also be
* called in other situations where a node has not undergone a
* full node_load and so lacks this property or when subproduct
* data may have changed since a node was last loaded.
*
* @param $node
* A node object representing a parent product.
* @return
* Array of nids of child products.
*/
function subproducts_get_children($node) {
$result = db_query(db_rewrite_sql("SELECT n.nid FROM {node} n INNER JOIN {ec_product} p ON n.vid = p.vid WHERE p.pparent = %d"), $node->nid);
$children = array();
while ($subproduct = db_fetch_object($result)) {
$children[] = $subproduct->nid;
}
return $children;
}
/**
* Menu callback; administration page for maintaining product variations.
*
* Similar to (and based on) Forum module's UI for administering forum
* containers and forums.
*/
function subproducts_admin() {
$op = $_POST['op'];
$edit = $_POST['edit'];
if (empty($op)) {
$op = arg(5);
}
switch ($op) {
case 'add':
if (arg(6) == 'attribute') {
$output = subproducts_form_attribute(array('ptype' => arg(4)));
}
else if (arg(6) == 'variation') {
$output = subproducts_form_variation(array('ptype' => arg(4)));
}
break;
case 'edit':
if (arg(6) == 'attribute') {
$edit = (array) subproducts_get_attribute(arg(7));
$edit = $edit + array('ptype' => arg(4));
$output = subproducts_form_attribute($edit);
}
else if (arg(6) == 'variation') {
$output = subproducts_form_variation((array) subproducts_get_variation(arg(7)));
}
break;
case t('Delete'):
if (!$edit['confirm']) {
if (arg(6) == 'attribute') {
$output = _subproducts_confirm_attribute_delete($edit['aid']);
break;
}
else if (arg(6) == 'variation') {
$output = _subproducts_confirm_variation_delete($edit['vid']);
break;
}
}
else {
$edit['name'] = 0;
}
case t('Submit'):
switch (arg(6)) {
case 'attribute':
if (!$edit['confirm'] && !subproducts_attribute_form_validate($edit)) {
drupal_goto($edit['aid'] ? 'admin/ecsettings/variation/' . $edit['ptype'] . '/edit/attribute/' . $edit['aid'] : 'admin/ecsettings/variation/' . $edit['ptype'] . '/add/attribute/');
}
else {
subproducts_save_attribute($edit);
}
break;
case 'variation':
subproducts_save_variation($edit);
break;
}
drupal_goto('admin/ecsettings/variation/' . $edit['ptype']);
break;
default:
$output = subproducts_variations_overview();
}
return $output;
}
/**
* Recalculate the price of a variation-type subproduct after a change in price of its parent
* or its parent's pricing type.
*
* @param $subproduct
* A node object representing a subproduct, passed by reference.
* @param $parent
* A node object representing the subproduct's parent product.
*/
function subproducts_reset_price_variation(&$subproduct, $parent) {
$subproduct->price = $parent->price;
$variations = subproducts_get_node_variations($subproduct->nid);
foreach ($variations as $aid) {
$attribute = subproducts_get_attribute($aid);
$subproduct->price = floatval($subproduct->price) + floatval($attribute->surcharge);
}
}
/**
* Recalculate the price of a base-type subproduct after a change in price of its parent
* or its parent's pricing type.
*
* The price of a base-type subproduct reflects
*
* @param $subproduct
* A node object representing a subproduct, passed by reference.
* @param $parent
* A node object representing the subproduct's parent product.
*/
function subproducts_reset_price_base(&$subproduct, $parent) {
// Enable other modules to adjust the price.
$surcharges = module_invoke_all('subproducts_alter_price', $subproduct);
foreach ($surcharges as $surcharge) {
$subproduct->base_price += $surcharge;
}
switch($parent->price_type) {
case 0:
// Set markup
$subproduct->price = floatval($subproduct->base_price) + floatval($parent->price);
break;
case 1:
// Percentage markup
$subproduct->price = floatval($subproduct->base_price) + (floatval($subproduct->base_price) * (floatval($parent->price) * 0.01));
break;
case 2:
// Set price
if ($parent->price > $subproduct->base_price) {
$subproduct->price = $parent->price;
}
else {
$subproduct->price = $subproduct->base_price;
drupal_set_message(t('The price of %title has been set at the cost of the base product.', array('%title' => $parent->title)));
}
break;
}
module_invoke_all('productapi', 'adjust_subproduct_price', $subproduct, NULL);
}
/**
* Fetch product types supporting variations.
*
* @return
* Array of product types.
*/
function subproducts_variations_product_types() {
$node = NULL;
$ptypes = module_invoke('product', 'get_ptypes');
if (empty($ptypes)) {
return FALSE;
}
asort($ptypes);
foreach (array_keys($ptypes) as $ptype) {
$subtypes = module_invoke($ptype, 'productapi', $node, 'subproduct_types');
if (!is_array($subtypes) || !in_array($ptype, $subtypes)) {
unset($ptypes[$ptype]);
}
}
return !empty($ptypes) ? array_keys($ptypes) : FALSE;
}
/**
* Set the matching subproduct for a set of product variation attributes.
*
* This function should be called in _productapi hooks with op = 'cart add item'
* for products supporting subproducts with variations. When called, the
* function will search for a matching subproduct and, if found, set the
* $node object to that subproduct (rather than the parent product). Since
* the node is passed by reference, the cart module will receive a valid
* subproduct reference.
*
* @param $node
* A node object representing a parent product.
* @param $data
* An object with properties representing, in this case, subproduct attributes.
* @return
* Boolean indicating success or failure in setting a subproduct.
*/
function subproducts_cart_set_subproduct_variation(&$node, $data) {
if ((arg(0) == 'cart') && (arg(1) == 'add') && !$data->variations) {
// We already know the product.
return TRUE;
}
if ($nid = subproducts_get_variations_subproduct($node->nid, $data->variations)) {
$node = node_load($nid);
return TRUE;
}
else {
drupal_set_message(t('Unable to find the correct product based upon the variation combination.'));
return FALSE;
}
}
/**
* Set the matching subproduct for a set of base product variation attributes.
*
* This function should be called in _productapi hooks with op = 'cart add item'
* for products supporting subproducts with base products. When called, the
* function will search for a matching subproduct and, if found, set the
* $node object to that subproduct (rather than the parent product). Since
* the node is passed by reference, the cart module will receive a valid
* subproduct reference.
*
* @param $node
* A node object representing a parent product.
* @param $data
* An object with properties representing, in this case, subproduct attributes
* and a parent base product.
* @return
* Boolean indicating success or failure in setting a subproduct.
*/
function subproducts_cart_set_subproduct_base(&$node, $data) {
$ptype = $data->ptype;
if ($ptype && is_array($data->$ptype)) {
foreach ($data->$ptype as $key => $value) {
switch ($key) {
case 'variations':
$variations = $value;
break;
case 'base_parent':
$base_parent = $value;
break;
}
}
}
if ((arg(0) == 'cart') && (arg(1) == 'add') && !$variations) {
// We already know the product.
return TRUE;
}
// First we search for a base product matching the requested attributes.
if ($base = subproducts_get_variations_subproduct($base_parent, $variations)) {
$match = db_fetch_object(db_query("SELECT pb.product FROM {ec_product_base} pb INNER JOIN {ec_product} p ON pb.product = p.nid WHERE pb.base = %d AND p.pparent = %d", $base, $data->pparent));
$node = node_load($match->product);
return TRUE;
}
else {
drupal_set_message(t('Unable to find the correct product based upon the combination of attributes.'));
return FALSE;
}
}
/**
* Fetch the list of foreign subproduct types for a given product type.
*
* This method allows us to determine, for a given product type, whether it
* supports base-type subproducts, and, if so, which product types its
* subproducts are based on.
*
* @param $ptype
* A product type.
* @return
* Array of product types, or boolean FALSE if none is found.
*/
function subproducts_base_product_types($ptype) {
$node = NULL;
$subtypes = module_invoke($ptype, 'productapi', $node, 'subproduct_types');
// We're looking for types not the same as the parent type, so if it is the same unset it.
if (is_array($subtypes) && in_array($ptype, $subtypes)) {
foreach ($subtypes as $key => $value) {
if ($value == $ptype) {
unset($subtypes[$key]);
break;
}
}
}
return !empty($subtypes) ? $subtypes : FALSE;
}
/**
* Menu callback; generate subproducts of a given product based on variations.
*
* We use a multi-stage process for generating variation-type subproducts.
*/
function subproducts_generate() {
$op = $_POST['op'];
if (empty($op)) {
$op = 'wizard1';
}
include_once(drupal_get_path('module', 'subproducts'). '/subproducts.inc');
$node = node_load(arg(1));
$variation_type = ($node->ptype && !$node->pparent && (in_array($node->ptype, subproducts_variations_product_types())));
$base_type = ($node->ptype && !$node->pparent && ($ptypes = subproducts_base_product_types($node->ptype)));
if($variation_type || $base_type) {
switch($op) {
case 'wizard1':
return $variation_type ? subproducts_generate_wizard1($node) : subproducts_select_bases_wizard1($node);
case t('Review options'):
return $variation_type ? subproducts_generate_wizard2($node) : subproducts_select_bases_wizard2($node);
case t('Generate'):
return $variation_type ? subproducts_generate_wizard3($node) : subproducts_select_bases_wizard3($node);
}
}
}
/**
* Return appropriate sql clause giving admins access to unpublished nodes.
*/
function subproducts_admin_sql() {
return user_access('administer nodes') ? '' : 'n.status = 1 AND ';
}
/**
* Find all the permutations of a given set of arrays.
*
* This helper function is used to generate all the possible combinations
* (permutations) of a set of attributes. In this way, we generate
* lists of potential subproducts, from which users can select which
* ones to generate.
*
* @param $array
* Structured array of attributes.
* @param $start
* Used internally, should not be passed in.
* @param $value
* Used internally, should not be passed in.
* @param $results
* Used internally, should not be passed in.
*/
function subproducts_permute($array, $start = 0, $value = array(), $results = array()) {
$keys = array_keys($array);
$number = count($keys) - 1;
foreach ($array[$keys[$start]] as $value[$start] ) {
if ($start < $number) {
$results = subproducts_permute($array, $start + 1, $value, $results);
}
else {
$values = array();
for ($i = 0; $i <= $number; $i++){
$values[$keys[$i]] = $value[$i] ;
}
$results[] = $values;
}
}
return $results;
}
/**
* Menu callback; list subproducts for a specified product.
*
* The page includes admin functions for mass handling of a
* product's subproducts (publish, unpublish, delete).
*/
function subproducts_list() {
$node = node_load(arg(1));
drupal_set_title(t('%title: subproducts', array('%title' => theme('placeholder', $node->title))));
/*
** Operations
*/
$operations = array(
'publish' => array(t('Publish the selected subproducts'), 'UPDATE {node} SET status = 1 WHERE nid = %d'),
'unpublish' => array(t('Unpublish the selected subproducts'), 'UPDATE {node} SET status = 0 WHERE nid = %d'),
'delete' => array(t('Delete the selected subproducts'), '')
);
// Handle operations
$op = $_POST['op'];
$edit = $_POST['edit'];
if (($op == t('Update') || $op == t('Delete all')) && isset($edit['operation']) && isset($edit['nodes'])) {
$edit['nodes'] = array_diff($edit['nodes'], array(0));
if (count($edit['nodes']) == 0) {
form_set_error('', t('Please select some items to perform the update on.'));
}
else {
if ($operations[$edit['operation']][1]) {
// Flag changes
$operation = $operations[$edit['operation']][1];
foreach ($edit['nodes'] as $nid => $value) {
if ($value) {
db_query($operation, $nid);
}
}
drupal_set_message(t('The update has been performed.'));
}
else if ($edit['operation'] == 'delete') {
// Mass delete
if ($edit['confirm']) {
foreach ($edit['nodes'] as $nid => $value) {
node_delete($nid);
}
// Refresh $node->children to reflect what's been deleted.
$node->children = subproducts_get_children($node);
drupal_set_message(t('The items have been deleted.'));
}
else {
$form['#tree'] = TRUE;
$message = '' . t('You may optionally choose to aggregate product variations by selecting an aggregation field here. If you had apparel products that varied by manufacture, color, and size, choosing to aggregate by size would mean that users would select from available manufacture and color options, and then be offered available sizes on a subsequent screen.') . '
'; $message .= '' . t('Note: aggregation not yet implemented!'); $form['subproducts_aggregation']['message'] = array( '#type' => 'markup', '#value' => $message ); foreach ($ptypes as $ptype) { $options = array('0' => t('none')); foreach ($variations[$ptype] as $variation) { $options[$variation->vid] = $variation->name; } $form['subproducts_aggregation'][$ptype] = array( '#type' => 'select', '#title' => $types[$ptype], '#default_value' => array_key_exists($ptype, $aggregation) ? $aggregation[$ptype] : 0, '#options' => $options ); } } return system_settings_form($form); } /** * Find all the attributes of a product's subproducts. * * @param $node * Node object representing a parent product. * @return * Array of attribute ids. */ function subproducts_get_child_attributes($node) { $attributes = array(); $result = db_query("SELECT a.aid FROM {ec_product_attribute} a INNER JOIN {ec_product} p ON a.nid = p.nid INNER JOIN {node} n ON p.nid = n.nid WHERE n.status = 1 AND p.pparent = %d", $node->nid); while ($attribute = db_fetch_object($result)) { $attributes[] = $attribute->aid; } return $attributes; } /** * Find all the attributes of a product's subproducts' base products. * * For a product with base-type subproducts, this method finds all the * attributes of the products that its subproducts are based on. For example, * if a logo product was available on a large white and a small white shirt, * an array of the attribute ids for small, large, and white would be returned. * * @param $node * Parent product node object. * @return * Array of attributes. */ function subproducts_get_base_attributes($node) { $attributes = array(); // Find all base products. $bases = array(); $result = db_query("SELECT b.base FROM {ec_product_base} b INNER JOIN {ec_product} p ON b.product = p.nid INNER JOIN {node} n ON p.nid = n.nid WHERE n.status = 1 AND p.pparent = %d", $node->nid); if (!db_num_rows($result)) { return FALSE; } while ($product = db_fetch_object($result)) { $wheres[] = '(nid = ' . $product->base . ')'; } $wheres = implode(' OR ', $wheres); $result = db_query('SELECT DISTINCT(aid) FROM {ec_product_attribute} WHERE ' . $wheres); if (!db_num_rows($result)) { return FALSE; } while ($attribute = db_fetch_object($result)) { $attributes[] = $attribute->aid; } return $attributes; } /** * If there is a surcharge, add information about it to an attribute's display text. * * As the attribute UI allows the setting of surcharges for attributes (e.g. size 'extra * large' might cost $2 extra), this function allows the display of such surcharge * information. * * @param $attribute * Attribute object. * @param $override * Boolean allowing the overriding of this behavior. * @return * String representation of attribute name and any surcharge. */ function subproducts_surcharge_extra($attribute, $override = FALSE) { return ((variable_get('subproducts_dynamic_pricing', 0) || $override) && ($attribute->surcharge != 0)) ? t('%name (+ %surcharge)', array('%name' => $attribute->name, '%surcharge' => module_invoke('payment', 'format', $attribute->surcharge))) : $attribute->name; } /** * @defgroup subproducts_themes Themable functions controlling the display of subproducts */ /** * Theme a list of product attributes * * @ingroup subproducts_themes * @return * Formatted HTML */ function theme_subproducts_attribute_items($attributes) { $title = t('Attributes'); $items = array(); foreach ($attributes as $attribute) { $items[] = '' . $attribute->variation . ': ' . $attribute->name; } return theme('item_list', $items, $title); } /** * Adds subproduct options to node_body. * * @ingroup subproducts_themes * @return * Formatted HTML */ function theme_subproducts_product_options (&$node){ $content = $node->body . $node->subproduct_options; return $content; } /** * Theme to allow the removal of the add to cart form, useful when displaying * subproduct options within other forms * @ingroup subproducts_themes * @return * Formatted HTML */ function theme_subproducts_add_to_cart($form) { return theme('fieldset', array('#title' => t('Variations'), '#children' => drupal_render($form))); } /** * Provides a display of the shopping cart, themed to allow removal of the cart display * @ingroup subproducts_themes * @return * Formatted HTML */ function theme_subproducts_in_cart($table) { return theme('fieldset', array('#title' => t('Products in cart'), '#children' => $table)); }