? modules/ecommerce/product/apparel.module
Index: modules/ecommerce/cart/cart.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ecommerce/cart/cart.module,v
retrieving revision 1.44.2.11
diff -u -r1.44.2.11 cart.module
--- modules/ecommerce/cart/cart.module	30 Oct 2005 19:02:46 -0000	1.44.2.11
+++ modules/ecommerce/cart/cart.module	22 Nov 2005 00:24:49 -0000
@@ -582,7 +582,7 @@
 
   /* Make sure we can add a product */
   $node = node_load(array('nid' => $nid));
-  $bool_cart_add = module_invoke($node->ptype, 'productapi', $node, 'cart add item');
+  $bool_cart_add = product_invoke_productapi($node, 'cart add item');
   if (is_null($bool_cart_add)) {
     $bool_cart_add = true;
   }
Index: modules/ecommerce/parcel/parcel.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ecommerce/parcel/parcel.module,v
retrieving revision 1.18.2.3
diff -u -r1.18.2.3 parcel.module
--- modules/ecommerce/parcel/parcel.module	30 Oct 2005 19:06:23 -0000	1.18.2.3
+++ modules/ecommerce/parcel/parcel.module	22 Nov 2005 00:26:04 -0000
@@ -51,7 +51,7 @@
 
         /* With this approach, we can't have parcels within parcels! */
         if ($product->ptype != 'parcel') {
-          if (module_invoke($product->ptype, 'productapi', $product, 'in_stock', $error)) {
+          if (product_invoke_productapi($product, 'in_stock', $error)) {
             return TRUE;
           }
         }
@@ -67,7 +67,7 @@
 
         /* With this approach, we can't have parcels within parcels! */
         if ($product->ptype != 'parcel') {
-          $bool = module_invoke($product->ptype, 'productapi', $product, 'is_shippable', $error);
+          $bool = product_invoke_productapi($product, 'is_shippable', $error);
           if ($bool[0]) {
             return array(true);
           }
@@ -84,7 +84,7 @@
           $foo->nid = $nid;
           $product = product_load($foo);
           $product->stock = $product->stock - 1;
-          module_invoke($product->ptype, 'productapi', $product, 'on payment completion', $error);
+          product_invoke_productapi($product, 'on payment completion', $error);
         }
       }
       break;
Index: modules/ecommerce/product/product.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ecommerce/product/product.module,v
retrieving revision 1.58.2.7
diff -u -r1.58.2.7 product.module
--- modules/ecommerce/product/product.module	10 Nov 2005 14:43:23 -0000	1.58.2.7
+++ modules/ecommerce/product/product.module	21 Nov 2005 23:42:54 -0000
@@ -7,6 +7,12 @@
 
 //TODO: Make sure only authenticated users can purchase recurring products.
 
+define('PRODUCT_API_OP_BOOL', 1);
+define('PRODUCT_API_OP_STRING', 2);
+define('PRODUCT_API_OP_ARRAY', 3);
+define('PRODUCT_API_OP_OBJECT', 4);
+define('PRODUCT_API_OP_QUERY', 5);
+
 /**
  * Theme function to render product node.
  */
@@ -116,12 +122,12 @@
   $output .= filter_form('format', $node->format);
   $output .= product_get_base_form_elements($node);
   $output .= form_hidden('ptype', $node->ptype);
-  $bool = module_invoke($node->ptype, 'productapi', $node, 'is_shippable');
+  $bool = product_invoke_productapi($node, 'is_shippable');
   if (module_invoke(variable_get('shipping_method', 'none'), 'shippingapi', $node, 'per_product') && $bool[0]) {
     $output .= form_hidden('per_product_shipping', 1);
     $output .= module_invoke(variable_get('shipping_method', 'none'), 'shippingapi', $node, 'per_product_form');
   }
-  $output .= module_invoke($node->ptype, 'productapi', $node, 'form');
+  $output .= product_invoke_productapi($node, 'form');
 
   return $output;
 }
@@ -158,7 +164,7 @@
     if ($node->hide_cart_link == 0) {
 
       /* Right here we need to check if a given product type is in stock */
-      if (module_invoke($node->ptype, 'productapi', $node, 'in_stock')) {
+      if (product_invoke_productapi($node, 'in_stock')) {
         // Is it already in our cart?
         if ($item[$node->nid]->qty) {
           $links[] = t('This item is in <a href="%cart_view">your shopping cart</a>.', array('%cart_view' => url('cart/view')));
@@ -185,7 +191,7 @@
   if ($products[$node->nid] === NULL) {
     $product = db_fetch_object(db_query('SELECT * FROM {ec_product} WHERE nid = %d', $node->nid));
     /* Merge the product info for the specific type. */
-    if ($product_type = module_invoke($product->ptype, 'productapi', $product, 'load')) {
+    if ($product_type = product_invoke_productapi($product, 'load')) {
       foreach ($product_type as $key => $value) {
         $product->$key = $value;
       }
@@ -198,7 +204,7 @@
       $f = variable_get('shipping_method', 'none').'_shippingapi';
       $per_product = $f($edit, 'per_product');
     }
-    if ($per_product && module_invoke($product->ptype, 'productapi', $product, 'is_shippable')) {
+    if ($per_product && product_invoke_productapi($product, 'is_shippable')) {
       if ($shipping = module_invoke(variable_get('shipping_method', 'none'), 'shippingapi', $product, 'per_product_load')) {
         foreach ($shipping as $key => $value) {
           $product->$key = $value;
@@ -286,7 +292,7 @@
         else {
           $output = product_get_base_form_elements($edit);
           $output .= form_hidden('ptype', $edit->ptype);
-          $output .= module_invoke($edit->ptype, 'productapi', $edit, 'form');
+          $output .= product_invoke_productapi($edit, 'form');
 
           if ($edit->ptype && !form_get_errors()) {
             $output .= form_submit(t('Update product'));
@@ -303,12 +309,12 @@
       default:
         $output = product_get_base_form_elements($edit);
         $output .= form_hidden('ptype', $edit->ptype);
-        $bool = module_invoke($edit->ptype, 'productapi', $edit, 'is_shippable');
+        $bool = product_invoke_productapi($edit, 'is_shippable');
         if (module_invoke(variable_get('shipping_method', 'none'), 'shippingapi', $edit, 'per_product') && $bool[0]) {
           $output .= form_hidden('per_product_shipping', 1);
           $output .= module_invoke(variable_get('shipping_method', 'none'), 'shippingapi', $edit, 'per_product_form');
         }
-        $output .= module_invoke($edit->ptype, 'productapi', $edit, 'form');
+        $output .= product_invoke_productapi($edit, 'form');
 
         if ($node->ptype) {
           $output .= form_submit(t('Update product'));
@@ -412,19 +418,130 @@
  */
 function product_invoke_productapi(&$node, $op, $a3 = null, $a4 = null) {
 
+  // If a product type is indicated, invoke its api call.
+  if (is_string($node) || $node->ptype) {
+    $results = array();
+    $ptype = $node->ptype ? $node->ptype : $node;
+    $op_data_types = array(
+      'fields' => PRODUCT_API_OP_ARRAY,
+      'validate' => PRODUCT_API_OP_ARRAY,
+      'wizard_select' => PRODUCT_API_OP_ARRAY,
+      'in_stock' => PRODUCT_API_OP_BOOL,
+      'is_shippable' => PRODUCT_API_OP_BOOL,
+      'on payment completion' => PRODUCT_API_OP_QUERY,
+      'form' => PRODUCT_API_OP_STRING,
+      'load' => PRODUCT_API_OP_OBJECT,
+      'insert' => PRODUCT_API_OP_QUERY,
+      'update' => PRODUCT_API_OP_QUERY,
+      'delete' => PRODUCT_API_OP_QUERY
+    );
+    switch ($op_data_types[$op]) {
+      case PRODUCT_API_OP_BOOL:
+        $return = FALSE;
+        break;
+      case PRODUCT_API_OP_STRING:
+        $return = '';
+        break;
+      case PRODUCT_API_OP_ARRAY:
+        $return = array();
+        break;
+      case PRODUCT_API_OP_OBJECT:
+        $return = new StdClass();
+        break;
+      case PRODUCT_API_OP_QUERY:
+        $return = NULL;
+        break;
+    }
+    $string_types = array('form');
+    $parent_ptypes = product_invoke_productapi($name, 'parent_ptypes');
+    $results[] = module_invoke($ptype, 'productapi', $node, $op);
+    // If there are parent product types, invoke their calls.
+    while (array_key_exists($ptype, $parent_ptypes)) {
+      $ptype = $parent_ptypes[$ptype];
+      $results[] = module_invoke($ptype, 'productapi', $node, $op);
+    }
+    foreach ($results as $result) {
+      switch ($op_data_types[$op]) {
+        case PRODUCT_API_OP_BOOL:
+          // Accept any TRUE in chain.
+          if ($return == FALSE) {
+            $return = result;
+          }
+          break;
+        case PRODUCT_API_OP_STRING:
+          $return .= $result;
+          break;
+        case PRODUCT_API_OP_ARRAY:
+          $return = array_merge($return, $result);
+          break;
+        case PRODUCT_API_OP_OBJECT:
+          if (is_object($result)) {
+            foreach ($result as $key => $value) {
+              $return->$key = $value;
+            }
+          }
+          break;
+        case PRODUCT_API_OP_QUERY:
+          $return = NULL;
+        break;
+      }
+    }
+  }
+  // Otherwise, invoke all productapi calls.
+  else {
+    $return = array();
+    foreach (module_list() as $name) {
+      $function = $name . '_productapi';
+      if (function_exists($function)) {
+        $result = $function($node, $op, $a3, $a4);
+        if (isset($result)) {
+          $return = array_merge($return, $result);
+        }
+      }
+    }
+  }
+  return $return;
+}
+
+function product_wizard_select_options() {
   $return = array();
-  foreach (module_list() as $name) {
-    $function = $name ."_productapi";
-    if (function_exists($function)) {
-      $result = $function($node, $op, $a3, $a4);
-      if (isset($result)) {
-        $return = array_merge($return, $result);
+  $name = NULL;
+  $ptypes = product_invoke_productapi($name, 'wizard_select');
+  asort($ptypes);
+  $parent_ptypes = product_invoke_productapi($name, 'parent_ptypes');
+  ksort($parent_ptypes);
+  if (is_array($parent_ptypes)) {
+    foreach ($ptypes as $ptype => $label) {
+      // If this type has no parent, add it to the return array.
+      if (!array_key_exists($ptype, $parent_ptypes)) {
+        $return[$ptype] = $label;
+      }
+      $depth = 0;
+      while (in_array($ptype, $parent_ptypes)) {
+        foreach ($parent_ptypes as $child => $parent) {
+          if ($ptype == $parent) {
+            $ptype = $child;
+            $depth++;
+            break;
+          }
+        }
+        $return[$ptype] = _product_ptype_depth($depth) . $ptypes[$ptype];
       }
     }
   }
+  else {
+    $return = $ptypes;
+  }
   return $return;
 }
 
+function _product_ptype_depth($depth, $graphic = '--') {
+  for ($n = 0; $n < $depth; $n++) {
+    $result .= $graphic;
+  }
+  return $result;
+}
+
 /**
  * The controller for managing products.  Callback happens via node/add/product.
  */
@@ -482,7 +599,7 @@
 function product_get_base_form_elements($edit) {
   $output = '';
   if (!empty($edit->ptype)) { // List the product type for reference.
-    $ptype = module_invoke($edit->ptype, 'productapi', $edit, 'wizard_select');
+    $ptype = product_invoke_productapi($edit, 'wizard_select');
     $output .= t('<p><b>Product type:</b> %ptype</p>', array('%ptype' => $ptype[$edit->ptype]));
   }
   $output .= form_textfield(t('Price'), 'price', $edit->price, 25, 50, t('How much does this product retail for? Note: This price may be different from the selling price due to price adjustments elsewhere.'). $help);
@@ -526,8 +643,7 @@
   $ptypes_display = array(-1 => 'please choose');
 
   /* Grab a user-friendly name for this product type. If one isn't defined, use the plugin name. */
-  $ptypes = product_invoke_productapi($name, 'wizard_select');
-  asort($ptypes);
+  $ptypes = product_wizard_select_options();
   if (empty($ptypes)) {
     form_set_error('ptype', t('No product types modules are installed!  Please install a product type module such as tangible.module or file.module.'));
   }
@@ -572,13 +688,13 @@
 
     // Display shipping options if we're building a shippable product and we
     // have the per product shipping option enabled.
-    $bool = module_invoke($edit->ptype, 'productapi', $edit, 'is_shippable');
+    $bool = product_invoke_productapi($edit, 'is_shippable');
     if (module_invoke(variable_get('shipping_method', 'none'), 'shippingapi', $edit, 'per_product') && $bool[0]) {
       $output .= form_hidden('per_product_shipping', 1);
       $output .= module_invoke(variable_get('shipping_method', 'none'), 'shippingapi', $edit, 'per_product_form');
     }
 
-    $output .= module_invoke($edit->ptype, 'productapi', $edit, 'form', $error);
+    $output .= product_invoke_productapi($edit, 'form', $error);
 
     if (db_result(db_query('SELECT COUNT(nid) FROM {ec_product} WHERE nid = %d', $edit->nid))) {
       $output .= form_submit(t('Update this product'));
@@ -618,10 +734,7 @@
   }
 
   if (!$errors['ptype']) {
-    $f = $edit->ptype. '_productapi';
-    if (function_exists($f)) {
-      $f($edit, 'validate');
-    }
+    product_invoke_productapi($edit, 'validate');
   }
 
   foreach ($errors as $name => $message) {
@@ -654,7 +767,7 @@
         }
       }
       db_query('UPDATE {ec_product} SET '. implode(', ', $q) ." WHERE nid = $node->nid", $v);
-      module_invoke($node->ptype, 'productapi', $node, 'update');
+      product_invoke_productapi($node, 'update');
       if ($node->per_product_shipping) {
         module_invoke(variable_get('shipping_method', 'none'), 'shippingapi', $node, 'per_product_update');
       }
@@ -669,7 +782,7 @@
       }
 
       db_query('INSERT INTO {ec_product} ('. implode(', ', $k) .') VALUES('. implode(', ', $s) .')', $v);
-      module_invoke($node->ptype, 'productapi', $node, 'insert');
+      product_invoke_productapi($node, 'insert');
       if ($node->per_product_shipping) {
         module_invoke(variable_get('shipping_method', 'none'), 'shippingapi', $node, 'per_product_insert');
       }
@@ -725,7 +838,7 @@
   /* Gather the rest of the product data from the other plugins and pass to
     node_save as a node object, which will in turn pass that object onto our
     nodeapi insert hook */
-  $fields = module_invoke($edit->ptype, 'productapi', $edit, 'fields');
+  $fields = product_invoke_productapi($edit, 'fields');
   foreach ((array) $fields as $key => $value) {
     $node->$key = $value;
   }
@@ -761,7 +874,7 @@
 function product_delete($node) {
   db_query('DELETE FROM {ec_product} WHERE nid = %d', $node->nid);
   module_invoke('cart', 'productapi', $node, 'delete');
-  module_invoke($node->ptype, 'productapi', $node, 'delete');
+  product_invoke_productapi($node, 'delete');
 
   drupal_set_message(t('product deleted'));
 }
@@ -839,7 +952,7 @@
 
   $product = db_fetch_object(db_query('SELECT * FROM {ec_product} WHERE nid = %d', $nid));
 
-  $bool = (array) module_invoke($product->ptype, 'productapi', $product, 'is_shippable');
+  $bool = (array) product_invoke_productapi($product, 'is_shippable');
   if (!is_array($bool)) {
     return (boolean) $bool;
   }
@@ -858,7 +971,7 @@
   $on_payment_roles = array_flip((array) variable_get('on_payment_roles', ''));
 
   $new_price  = $node->price;
-  $prod_price = module_invoke($node->ptype, 'productapi', $node, 'adjust_price', $new_price);
+  $prod_price = product_invoke_productapi($node, 'adjust_price', $new_price);
   $new_price = $prod_price > 0 ? $prod_price: $new_price;
 
   return (($new_price > 0) ? $new_price : $node->price);
Index: modules/ecommerce/store/store.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ecommerce/store/store.module,v
retrieving revision 1.44.2.11
diff -u -r1.44.2.11 store.module
--- modules/ecommerce/store/store.module	5 Nov 2005 07:32:05 -0000	1.44.2.11
+++ modules/ecommerce/store/store.module	22 Nov 2005 00:26:45 -0000
@@ -655,7 +655,7 @@
           }
         }
         db_query('INSERT INTO {ec_transaction_product} ('. implode(', ', $k) .') VALUES('. implode(', ', $s) .')', $v);
-        module_invoke($item->ptype, 'productapi', $item, 'transaction', 'insert');
+        product_invoke_productapi($item, 'transaction', 'insert');
       }
     }
     elseif ($edit['nids']) {
Index: modules/ecommerce/tax/tax.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ecommerce/tax/tax.module,v
retrieving revision 1.6.2.1
diff -u -r1.6.2.1 tax.module
--- modules/ecommerce/tax/tax.module	30 Oct 2005 19:11:22 -0000	1.6.2.1
+++ modules/ecommerce/tax/tax.module	22 Nov 2005 00:29:00 -0000
@@ -277,7 +277,7 @@
 
   $output .= form_hidden('realm', $edit['realm']);
   $output .= form_textfield(t('Adjustment'), 'rate', $edit['rate'], 10, 13, t('This rate can be a simple price addition or a percentage multiplier. For example, to add a 5.00 tax, enter +5.00. To multiply the gross price times 75%, enter 75%. If no operand is given, addition is assumed.'));
-  $ptypes = product_invoke_productapi($name, 'wizard_select');
+  $ptypes = product_wizard_select_options();
   asort($ptypes);
   $output .= form_checkboxes(t('Product type'), 'ptype', $edit['ptype'], $ptypes, t('Check the product types this tax rule applies to.'), NULL, TRUE);
 
