Index: updates.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ecommerce/updates.inc,v
retrieving revision 1.6.2.4
diff -u -F^f -r1.6.2.4 updates.inc
--- updates.inc	1 Jul 2005 03:56:25 -0000	1.6.2.4
+++ updates.inc	28 Oct 2005 02:27:53 -0000
@@ -15,7 +15,8 @@
   '2005-04-11' => 'update_5',
   '2005-04-23: first update since Drupal 4.6 release' => 'update_6',
   '2005-06-09' => 'update_7',
-  '2005-06-30' => 'update_8'
+  '2005-06-30' => 'update_8',
+  '2005-10-22' => 'update_11'
 );
 
 function update_1() {
@@ -120,6 +121,16 @@ function update_8() {
   return $ret;
 }
 
+function update_11() {
+  if ($GLOBALS['db_type'] == 'mysql') {
+    $ret[] = update_sql('ALTER TABLE {ec_product} ADD parent INT(10) NOT NULL, ADD children VARCHAR(255) NOT NULL, ADD variation LONGTEXT NOT NULL');
+  }
+  else {
+    // pgsql goes here.
+  }
+  return $ret;
+}
+
 function update_sql($sql) {
   $edit = $_POST["edit"];
   $result = db_query($sql);
Index: cart/cart.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ecommerce/cart/cart.module,v
retrieving revision 1.44.2.10
diff -u -F^f -r1.44.2.10 cart.module
--- cart/cart.module	12 Oct 2005 14:24:15 -0000	1.44.2.10
+++ cart/cart.module	28 Oct 2005 02:27:55 -0000
@@ -132,7 +132,7 @@ function theme_cart_display_block() {
       foreach ($item as $i) {
         $node = node_load(array("nid" => $i->nid));
         $total += product_adjust_price($node) * $i->qty;
-        $output .= l("$i->qty x $node->title", "node/$node->nid"). "<br />";
+        $output .= l("$i->qty x $node->title", "node/" .($node->parent ? $node->parent : $node->nid)). "<br />";
       }
       $output .= "</div><div class=\"total\">". payment_format($total) . "</div>";
       $output .= '<div class="checkout">'. t('Ready to <a href="%checkout-url">checkout</a>?', array('%checkout-url' => url('cart/checkout'))) .'</div>';
@@ -197,7 +197,7 @@ function cart_page() {
 
   switch (arg(1)) {
     case 'add':
-      cart_add_item(arg(2), arg(3));
+      cart_add_item(arg(2), arg(3), 'redirect', $edit);
       $output = theme("cart_view");
       break;
 
@@ -517,8 +517,27 @@ function cart_get_id() {
 function cart_add_item($nid, $qty = 1, $action = "redirect", $data = array()) {
 
   /* Make sure we can add a product */
+  $bool_cart_add = true; /* we assume we can until we can't */
   $node = node_load(array('nid' => $nid));
-  $bool_cart_add = module_invoke($node->ptype, 'productapi', $node, 'cart add item');
+
+  /* if this is a parent product, we do not add this directly, but the child
+   * instead. The parent is not really a product, just a place holder for
+   * all the related/sub products. */
+  if ($node->children) {
+    if ($subproduct = module_invoke($node->ptype, 'productapi', $node, 'cart get subproduct', $data)) {
+      /* we have a sub product. We need to change the the parent to the
+       * child so the child is added to the cart, and not the parent */
+      $node = $subproduct;
+      $nid = $node->nid;
+    }
+    else {
+      $bool_cart_add = false;
+    }
+  }
+
+  /* if the parent has a subproduct then the $bool_cart_add may already be
+   * no, so why bother calling the hook */
+  $bool_cart_add = $bool_cart_add ? module_invoke($node->ptype, 'productapi', $node, 'cart add item') : false;
   if (is_null($bool_cart_add)) {
     $bool_cart_add = true;
   }
Index: product/product.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ecommerce/product/product.module,v
retrieving revision 1.58.2.4
diff -u -F^f -r1.58.2.4 product.module
--- product/product.module	26 Sep 2005 20:48:33 -0000	1.58.2.4
+++ product/product.module	28 Oct 2005 02:27:58 -0000
@@ -41,7 +41,7 @@ function theme_product_view_collection()
   $columns  = 3;
   $rows     = 5;
 
-  $result = pager_query(db_rewrite_sql('SELECT n.nid FROM {node} n INNER JOIN {ec_product} p ON n.nid = p.nid WHERE n.status = 1 ORDER BY n.sticky DESC, n.created DESC'), $rows * $columns, 0);
+  $result = pager_query(db_rewrite_sql('SELECT n.nid FROM {node} n INNER JOIN {ec_product} p ON n.nid = p.nid WHERE n.status = 1 AND p.parent = 0 ORDER BY n.sticky DESC, n.created DESC'), $rows * $columns, 0);
 
   $output = '<table width="100%" cellpadding="10">';
   for ($i = 0; $node = db_fetch_object($result); $i++) {
@@ -170,6 +170,12 @@ function product_link($type, $node = nul
         $links[] = t('sold out');
       }
     }
+    if (user_access('create products') && !$node->parent && module_invoke($node->ptype, 'productapi', $node, 'subproduct_types')) {
+      $links[] = l(t('add sub product'), "node/add/product/parent/$node->nid", array(), drupal_get_destination());
+    }
+    if ($node->parent) {
+      $links[] = l(t('view parent product'), "node/$node->parent");
+    }
   }
 
   return $links;
@@ -183,6 +189,10 @@ function product_load($node) {
 
   if ($products[$node->nid] === NULL) {
     $product = db_fetch_object(db_query('SELECT * FROM {ec_product} WHERE nid = %d', $node->nid));
+
+    /* unpack the variation fields stored in the ec_product table */
+    $product->variation = unserialize($product->variation);
+
     /* Merge the product info for the specific type. */
     if ($product_type = module_invoke($product->ptype, 'productapi', $product, 'load')) {
       foreach ($product_type as $key => $value) {
@@ -233,6 +243,12 @@ function product_menu($may_cache) {
           'callback' => 'product_to_product', 'access' => user_access('administer products'),
           'type' => MENU_LOCAL_TASK, 'weight' => 2);
       }
+
+      if (db_result(db_query(db_rewrite_sql("SELECT COUNT(n.nid) FROM {ec_product} p INNER JOIN {node} n ON p.nid = n.nid WHERE p.nid = %d AND p.children <> ''"), arg(1))) > 0) {
+        $items[] = array('path' => 'node/' .arg(1) .'/subproducts', 'title' => t('sub products'),
+          'callback' => 'product_sub_products', 'access' => user_access('administer products'),
+          'type' => MENU_LOCAL_TASK, 'weight' => 3);
+      }
     }
   }
 
@@ -318,6 +334,28 @@ function product_to_product() {
   }
 }
 
+function product_sub_products() {
+  drupal_set_title(t('view sub products'));
+  $node = node_load(array('nid' => arg(1)));
+  $products = product_get_variations($node);
+
+  $headers = array(
+    array('data' => t('title')),
+    array('data' => t('operations'), 'colspan' => 2)
+    );
+
+  foreach ($products as $nid => $product) {
+    $rows[] = array(
+      array('data' => $product->title),
+      array('data' => l(t('view'), "node/$nid")),
+      array('data' => l(t('edit'), "node/$nid/edit", array(), drupal_get_destination())),
+      );
+  }
+  $output.= theme('table', $headers, $rows);
+  $output.= form_item('', t('%add addition sub products', array('%add' => l(t('add'), "node/add/product/parent/$node->nid", array(), drupal_get_destination()))));
+  print theme('page', $output);
+}
+
 /**
  * Implementation of hook_node_name().
  */
@@ -481,7 +519,9 @@ function product_get_base_form_elements(
   }
   $output .= form_textfield(t('SKU'), 'sku', $edit->sku, 25, 50, t('If you have an unique identifier for this product from another system or database, enter that here. This is optional, as system IDs are automatically created for each product.'));
   $output .= form_radios(t("'Add to cart' link"), 'hide_cart_link', $edit->hide_cart_link, array(t("Visible"), t("Hidden")), t("Choose whether or not you want the 'Add to cart' link visible for this product."));
-
+  if ($edit->parent) {
+    $output .= form_hidden('parent', $edit->parent);
+  }
   return $output;
 }
 
@@ -492,12 +532,38 @@ function product_get_base_form_elements(
  */
 function product_wizard_form($edit) {
 
+  /* check to see if we have a parent product. and load it as the template */
+
+  if (!$edit && arg(3) == 'parent') {
+    if ($parent = node_load(array('nid' => arg(4)))) {
+      if ($ptypes = module_invoke($parent->ptype, 'productapi', $edit, 'subproduct_types')) {
+        foreach ($parent as $key => $value) {
+          if (!in_array($key, array('nid', 'status', 'children', 'parent', 'ptype', 'path'))) {
+            $edit->$key = $parent->$key;
+          }
+          if ($edit->type != 'product') {
+            $edit->type = 'product';
+          }
+        }
+        if (count($ptypes) == 1) {
+          $edit->ptype = $ptypes[0];
+        }
+      }
+      $edit->parent = $parent->nid;
+    }
+  }
+  
   /* Ask node.module to build the node form and we'll strip off the form tags. */
 
   $node_form = ($edit->uid && $edit->name && $edit->type) ? node_form($edit) : node_add('product');
   $node_form = preg_replace("'</?(form).*?>'", '', $node_form);
   $node_form = preg_replace("'</?(input type=\"submit\").*?>'", '', $node_form);
-  $node_form .= product_form_product_types();
+  if ($edit->ptype) {
+    $node_form .= form_submit(t('Create product'));
+  }
+  else {
+    $node_form .= product_form_product_types();
+  }
 
   return form($node_form, 'post', null, array('id' => 'node-form'));
 }
@@ -513,6 +579,16 @@ function product_form_product_types() {
     form_set_error('ptype', t('No product types modules are installed!  Please install a product type module such as tangible.module or file.module.'));
   }
   else {
+    /* purge types that are not compatible with the parent product */
+    if ($edit->parent) {
+      $parent = node_load(array('nid' => $edit->parent));
+      $subproduct_types = module_invoke($parent->ptype, 'productapi', $edit, 'subproduct_types');
+      foreach ($ptypes as $key => $value) {
+        if (!array_key_exists($key, $subproduct_types)) {
+          unset($ptypes[$key]);
+        }
+      }
+    }
     $ptypes_display = $ptypes_display + $ptypes;
   }
   $output = form_select(t('Type of product to create'), 'ptype', $edit->ptype, $ptypes_display, t('You cannot change the product type once it\'s created.'));
@@ -620,6 +696,12 @@ function product_form_validate(&$edit) {
 function product_save($node) {
   if ($node->ptype) {
     $node->is_recurring = ($node->price_interval) ? 1 : 0;
+
+    /* simple product variations will be stored in the ec_product table.
+     * More complex ones may need to be stored in another table. */
+    if ($node->variation) {
+      $node->variation = serialize($node->variation);
+    }
     $fields = product_fields();
 
     /*
@@ -655,6 +737,23 @@ function product_save($node) {
         module_invoke(variable_get('shipping_method', 'none'), 'shippingapi', $node, 'per_product_insert');
       }
     }
+
+    /* update the parent product's list of children. */
+
+    if ($node->parent) {
+      if ($parent = node_load(array('nid' => $node->parent))) {
+        if ($parent->children) {
+          $children = explode(',', $parent->children);
+        }
+        else {
+          $children = array();
+        }
+        if (!in_array($node->nid, $children)) {
+          $children[] = $node->nid;
+          db_query('UPDATE {ec_product} SET children = \'%s\' WHERE nid = %d', implode(',', $children), $parent->nid);
+        }
+      }
+    }
   }
 }
 
@@ -1026,9 +1125,24 @@ function product_send_recurring_payment_
 }
 
 /**
+ * retrieve all the subproducts for a parent product
+ */
+function product_get_variations($node) {
+  $products = array();
+
+  foreach (explode(',', $node->children) as $nid) {
+    if ($child = node_load(array('nid' => $nid))) {
+      $products[$child->nid] = $child;
+    }
+  }
+  
+  return $products;
+}
+
+/**
  * The names of the database columns in the table.
  */
 function product_fields($table = 'ec_product') {
-  return array('nid', 'sku', 'price', 'is_recurring', 'price_interval', 'price_unit', 'price_cycle', 'auto_charge', 'ptype', 'hide_cart_link');
+  return array('nid', 'sku', 'price', 'is_recurring', 'price_interval', 'price_unit', 'price_cycle', 'auto_charge', 'ptype', 'hide_cart_link', 'parent', 'children', 'children', 'variation');
 }
 ?>
Index: product/product.mysql
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ecommerce/product/product.mysql,v
retrieving revision 1.6
diff -u -F^f -r1.6 product.mysql
--- product/product.mysql	5 Mar 2005 19:47:54 -0000	1.6
+++ product/product.mysql	28 Oct 2005 02:27:58 -0000
@@ -9,6 +9,9 @@
   auto_charge tinyint(3) unsigned NOT NULL default '0',
   ptype varchar(75) NOT NULL default '',
   hide_cart_link int(2) unsigned NOT NULL default '0',
+  parent int(10) NOT NULL default '0',
+  children varchar(255) NOT NULL default '',
+  variation longtext NOT NULL,
   UNIQUE KEY nid (nid),
   KEY ptype (ptype)
 ) TYPE=MyISAM;
Index: store/store.mysql
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ecommerce/store/Attic/store.mysql,v
retrieving revision 1.16.2.4
diff -u -F^f -r1.16.2.4 store.mysql
--- store/store.mysql	2 Oct 2005 18:15:26 -0000	1.16.2.4
+++ store/store.mysql	28 Oct 2005 02:27:59 -0000
@@ -108,6 +108,9 @@
   auto_charge tinyint(3) unsigned NOT NULL default '0',
   ptype varchar(75) NOT NULL default '',
   hide_cart_link int(2) unsigned NOT NULL default '0',
+  parent int(10) NOT NULL default '0',
+  children varchar(255) NOT NULL default '',
+  variation longtext NOT NULL,
   UNIQUE KEY nid (nid),
   KEY ptype (ptype)
 ) TYPE=MyISAM;
