I have created a new module that is similar to subproducts, but a lot better.

You can choose up to three product options easily from a list of existing products and it shows in the cart and invoice as a description (list) under the main product.

Features:
- list up to three options for a product or package
- choose the name for each option (ie. size, color, whatever)
- choose whether the list displays as radio buttons or a select drop list

This also works very well for a "two for one" or package discount type situation where you want the customer to be able to choose from a list of items for the package.

Wish list:
- tangible options: in stock, etc.

View a sample here: http://nwdfilms.com/shop/special (yes, I'm still working on the site theme)

download / try it out here: http://drupal.org/node/76815, I could use some feedback!

alynner

Comments

StevenSokulski’s picture

That looks like a good eCommerce add-on. Seems a lot smoother than subproducts, which I find clumsy most of the time.

Thanks!

alynner’s picture

That's basically why I created it, I didn't like the way subproducts work and I needed a way to package existing products together.

Let me know if you have any suggestions! I can create more than three options, but thats all I needed. I'm not sure how to make it so that you can dynamically add infinite options, that would be nice.

I also don't need the ability to track stock, but it probably would be a good idea for most people, its on my low priority list :-)

StevenSokulski’s picture

Tracking stock would be a very appealing option to most, including me. What I'm looking to eventually work on is a stock track system similar to the one seen here (http://www.bustedtees.com/shirt/aweso/male) I especially like the use of tabs to display different stock data for different options. Yet this seems very complex and is not something I have the time to try and emulate at the present moment.

alynner’s picture

I found that I needed it for shipping purposes anyway. It only applies to the package item, it would be nice for it to look for out of stock on the options and either not display them or attach a note. I'll add that to the list.

Find the most recent update at the original posting: http://drupal.org/node/76815

EDIT: I have added the ability for it to show (SOLD OUT) next to the sub-item if it has inventory options and it is out of stock, see the above link for updated file

valentin@irata.ch’s picture

I'm currently seeking for something little different (a product option with a text field - to add a name or whatever) but what i saw is really efficient..

@the module author:
Do you think it would be possible to add what i'm seeking for?
I mean a text field the client could fill himself...
If so, I could try to do it and add it to your module...

alynner’s picture

I didn't think it would be that hard, so I tried to do it up. I haven't thoroughly tested it, but give it a go...

<?php
// $Id: dynamic_parcel.module 2006/11/28 alynner 
// added function of being able to put text in as an option

/********************************************************************
 * You need to make a couple changes to store and cart in order to see the options that the customer has chosen:
 
 store.module - in the function theme_store_invoice(), add this code at line 308: 
 		if (module_exist('dynamic_parcel')){
			$details .= dynamic_parcel_list($p->data,'serialized','theme');
		}

 cart.module - in the function theme_cart_view(), add this code at line 183:
	if (module_exist('dynamic_parcel')){
		$desc .= dynamic_parcel_list($nid,'nid','string');
	}
	
 these two changes just make the options display in the cart and the store.

 ********************************************************************/

/********************************************************************
 * Drupal Hooks
 ********************************************************************/

/**
 * Implementation  of hook_help().
 */
function dynamic_parcel_help($section = 'admin/help#dynamic_parcel') {
  switch ($section) {
    case 'admin/modules#description':
      return t('Create packages of ecommerce items. Dependency: product.module');
    case 'admin/help#dynamic_parcel':
      return t("<p>This module provides a way for you to create a package of products. First, individually create the products and then create a product package and add those items. These actions can be done using any of the options under <a href=\"%node_add\">create content</a>. The child items may be unpublished if you wish to only have them included in the package</p>", array('%node_add' => 'node/add'));
    case 'node/add/product#dynamic_parcel':
      return t('A dynamic collection of products is a package or group of items sold as a whole.');
  }
}

/**
 * Implementation of hook_perm().
 */
function dynamic_parcel_perm() {
  return array('create dynamic collections of products', 'edit own dynamic collections of products');
}

/**
 * Implementation of hook_access().
 */
function dynamic_parcel_access($op, $node) {
  global $user;

  if ($op == 'create') {
    return user_access('create dynamic collections of products');
  }

  if ($op == 'update' || $op == 'delete') {
    if (user_access('edit own dynamic collections of products') && ($user->uid == $node->uid)) {
      return TRUE;
    }
  }
}




function dynamic_parcel_productapi(&$node, $op, $a3 = null, $a4 = null) {

  switch ($op) {
	
	case 'fields':
	  return array_merge(array('dp_option1' => $node->dp_option1), module_invoke('tangible', 'productapi', $node, $op, $a3, $a4));
      break;

    case 'validate':
      // is_null provides a mechanism for us to determine if this is the first viewing of the form.
      if (!is_null($node->dp_option1)) {
        if ($node->dp_option1 == '') {
          form_set_error('dp_option1', t('You must add at least one existing nid to option one.'));
        }
      }
	  return module_invoke('tangible', 'productapi', $node, $op, $a3, $a4);
      break;

      case 'wizard_select':
        return array('dynamic_parcel' => t('dynamic collection of products'));


    case 'attributes':
      $attributes[] = 'is_shippable';
	  if (($node->manage_stock && $node->stock > 0) || !$node->manage_stock) {
        $attributes[] = 'in_stock';
      }
	  return $attributes;

      break;


    case 'form':
      $form['dp_options'] = array(
        '#type' => 'fieldset',
        '#title' => t('Product Options'),
		'#collapsible' => TRUE,
		'#collapsed' => FALSE,
		'#tree' => TRUE
      );
	  $result = db_query(db_rewrite_sql("SELECT n.nid, n.title FROM {node} n, {ec_product} p WHERE n.nid = p.nid AND n.nid != %d AND p.ptype != 'dynamic_parcel'", $node->nid));
	  while ($dat = db_fetch_object($result)) {
	  	$pids[$dat->nid] = $dat->title;
	  }
      $form['dp_options'][0] = array(
        '#type' => 'fieldset',
        '#title' => t('Option 1'),
      );
      $form['dp_options'][0]['label'] = array(
        '#type' => 'textfield',
        '#title' => t('label'),
		'#default_value' => $node->dp_options[0]['label'],
		'#required' => TRUE,
      );
      $form['dp_options'][0]['enabled'] = array(
        '#type' => 'hidden',
        '#value' => 1,
      );
      $form['dp_options'][0]['listtype'] = array(
        '#type' => 'select',
        '#title' => t('list type'),
		'#default_value' => $node->dp_options[0]['listtype'],
		'#options' => array('select'=>'select','radios'=>'radios','textfield'=>'textfield'),
		'#multiple' => FALSE,
        '#description' => t('Choose how you want this list to show: select (drop list) or radios (buttons).'),
		'#required' => TRUE,
      );
	  if (is_array($node->dp_options[0]['choices'])){
	  	$defaultchoices = array_keys($node->dp_options[0]['choices']);
	  }
      $form['dp_options'][0]['choices'] = array(
        '#type' => 'select',
        '#title' => t('choices'),
		'#default_value' => $defaultchoices,
		'#options' => $pids,
		'#multiple' => TRUE,
        '#description' => t('Choose multiple items by using the CTRL or SHIFT keys.'),
		'#required' => TRUE,
      );
	  
      $form['dp_options'][1] = array(
        '#type' => 'fieldset',
        '#title' => t('Option 2'),
      );
      $form['dp_options'][1]['enabled'] = array(
        '#type' => 'checkbox',
        '#title' => t('Enabled'),
		'#default_value' => $node->dp_options[1]['enabled'],
      );
      $form['dp_options'][1]['label'] = array(
        '#type' => 'textfield',
        '#title' => t('label'),
		'#default_value' => $node->dp_options[1]['label'],
      );
      $form['dp_options'][1]['listtype'] = array(
        '#type' => 'select',
        '#title' => t('list type'),
		'#default_value' => $node->dp_options[1]['listtype'],
		'#options' => array('select'=>'select','radios'=>'radios','textfield'=>'textfield'),
		'#multiple' => FALSE,
        '#description' => t('Choose how you want this list to show: select (drop list) or radios (buttons).'),
      );
	  if (is_array($node->dp_options[1]['choices'])){
	  	$defaultchoices1 = array_keys($node->dp_options[1]['choices']);
	  }
      $form['dp_options'][1]['choices'] = array(
        '#type' => 'select',
        '#title' => t('Choices'),
		'#default_value' => $defaultchoices1,
		'#options' => $pids,
		'#multiple' => TRUE,
        '#description' => t('Choose multiple items by using the CTRL or SHIFT keys.'),
      );
	  
      $form['dp_options'][2] = array(
        '#type' => 'fieldset',
        '#title' => t('Option 3'),
      );
      $form['dp_options'][2]['label'] = array(
        '#type' => 'textfield',
        '#title' => t('label'),
		'#default_value' => $node->dp_options[2]['label'],
      );
      $form['dp_options'][2]['enabled'] = array(
        '#type' => 'checkbox',
        '#title' => t('Enabled'),
		'#default_value' => $node->dp_options[2]['enabled'],
      );
      $form['dp_options'][2]['listtype'] = array(
        '#type' => 'select',
        '#title' => t('list type'),
		'#default_value' => $node->dp_options[2]['listtype'],
		'#options' => array('select'=>'select','radios'=>'radios','textfield'=>'textfield'),
		'#multiple' => FALSE,
        '#description' => t('Choose how you want this list to show: select (drop list) or radios (buttons).'),
      );
	  if (is_array($node->dp_options[2]['choices'])){
	  	$defaultchoices2 = array_keys($node->dp_options[2]['choices']);
	  }
      $form['dp_options'][2]['choices'] = array(
        '#type' => 'select',
        '#title' => t('Choices'),
		'#default_value' => $defaultchoices2,
		'#options' => $pids,
		'#multiple' => TRUE,
        '#description' => t('Choose multiple items by using the CTRL or SHIFT keys.'),
      );
      return array_merge($form, module_invoke('tangible', 'productapi', $node, $op, $a3, $a4));

      /* Similar to node_load */
    case 'load':
        $result = db_query('SELECT * FROM {ec_product_dynamic} WHERE nid = %d', $node->nid);
        while ($data = db_fetch_object($result)) {
			$result2 = db_query('SELECT p.*, n.title FROM {ec_product_dynamic_parcel} p, {node} n WHERE n.nid = p.child_nid AND p.nid = %d AND p.optionnum = %d', $node->nid, $data->optionid);
			while ($data2 = db_fetch_object($result2)) {
				$p->dp_options[$data->optionid]['choices'][$data2->child_nid] = $data2->title;
			}
			$p->dp_options[$data->optionid]['label'] = $data->label;
			$p->dp_options[$data->optionid]['enabled'] = $data->enabled;
			$p->dp_options[$data->optionid]['listtype'] = $data->listtype;
		
        }

        return array_merge($p, module_invoke('tangible', 'productapi', $node, $op, $a3, $a4));
      break;

    case 'insert':
      return array_merge(dynamic_parcel_save($node, 'insert'), module_invoke('tangible', 'productapi', $node, $op, $a3, $a4));

    case 'update':
      return array_merge(dynamic_parcel_save($node, 'update'), module_invoke('tangible', 'productapi', $node, $op, $a3, $a4));

    case 'delete':
      db_query('DELETE FROM {ec_product_dynamic} WHERE nid = %d', $node->nid);
	  db_query('DELETE FROM {ec_product_dynamic_parcel} WHERE nid = %d', $node->nid);
	  return module_invoke('tangible', 'productapi', $node, $op, $a3, $a4);
	  
    case 'in_stock':
    case 'is_shippable':
    case 'on payment completion':
    default:
      return module_invoke('tangible', 'productapi', $node, $op, $a3, $a4);
	  
  }
}

function dynamic_parcel_save($node, $mode) {

  if ($mode == 'update') {
    db_query('DELETE FROM {ec_product_dynamic_parcel} WHERE nid = %d', $node->nid);
    db_query('DELETE FROM {ec_product_dynamic} WHERE nid = %d', $node->nid);
  }

  for($x=0;$x<count($node->dp_options);$x++) {
	  
	  db_query("INSERT INTO {ec_product_dynamic} (nid, optionid, enabled, label, listtype) VALUES ( %d, %d, %d, '%s','%s')", $node->nid, $x, $node->dp_options[$x]['enabled'], $node->dp_options[$x]['label'], $node->dp_options[$x]['listtype']);
	  
	  
	  
	  foreach ($node->dp_options[$x]['choices'] as $child) {
		  db_query('INSERT INTO {ec_product_dynamic_parcel} (nid, child_nid, optionnum) VALUES (%d, %d, %d)', $node->nid, $child, $x);
	  }
  
  } 
}

function theme_product_dynamic_parcel_view($node, $teaser = 0, $page = 0) {
    $price = product_adjust_price($node)+product_get_specials($node, true);
	$price_string = '<div class="price"><strong>'. t('Price') .'</strong>: ' . module_invoke('payment', 'format', $price) ;
	$price_string .= '</div>';
  if ($node->is_recurring) {
    $price_string .= '<div class="recurring-details">'. product_recurring_nice_string($node) . '<div>';
  }
  $node->teaser .= $price_string;
  $node->body .= $price_string;

  if (!$teaser) {
	
	$form['#method'] = 'post';
	$form['#action'] = "../cart/add/".$node->nid;
	$form['#tree'] = TRUE;
	
	for($i=0;$i<count($node->dp_options);$i++){
		if ($node->dp_options[$i]['enabled']== 1){
			foreach($node->dp_options[$i]['choices'] as $choice=>$value){
				$choicenode = node_load($choice);
				if (($choicenode->manage_stock && $choicenode->stock > 0) || !$choicenode->manage_stock) {
					$choices[$i][$choice] = $value;
				}
				else{
					$choices[$i][$choice] = $value.' (CURRENTLY SOLD OUT)';
				}
			}
			if ($node->dp_options[$i]['listtype'] != 'textfield'){
				$form['options'][$i] = array(
				  '#type' => $node->dp_options[$i]['listtype'],
				  '#title' => $node->dp_options[$i]['label'],
				  '#options' => $choices[$i],
				  '#required' => TRUE,
				);
			}
			else {
				$form['options'][$i] = array(
				  '#type' => $node->dp_options[$i]['listtype'],
				  '#title' => $node->dp_options[$i]['label'],
				  '#required' => TRUE,
				);
			}
		}
		$j = $i;
	}
	//check to see if there is already an instance of this node in cart
	$data = cart_get_items();
	if($data[$node->nid]){
		$cartoptions = $data[$node->nid]->options;
	}
	for($x=0;$x<count($cartoptions);$x++){
		$form['options'][$j+$x] = array(
		  '#type' => 'hidden',
		  '#value' => $cartoptions[$x],
		);
	}
	
	$form['add_to_cart'] = array(
		'#type' => 'submit',
		'#value' => t("add to cart")
	);
	
	$node->body .= drupal_get_form('dynamic_parcel_add_to_cart', $form);

  }

  return $node;
}


function dynamic_parcel_list($data, $input = 'serialized', $output = 'array'){
	switch ($input) {

		case 'serialized':
			$optionarr = unserialize($data); 
			if (is_array($optionarr[options])) {
				
				for ($j=0;$j<count($optionarr[options]);$j++) {
					$pop = node_load($optionarr[options][$j]);
					$items[] = $pop->title;
				}
			}
			continue;
		
		case 'nid':
			$p = cart_get_items();
			if($p[$data]){
				$choices = $p[$data]->options;
			}
			if ($choices) {
				foreach($choices as $key=>$value){
					//$check = array_keys($choice);
					if (is_numeric($value)) {
						$n = node_load($value);
						$items[] = $n->title;
					}
					else {
						$items[] = $value;
					}
				}
			}
			continue;
	}
	
	switch ($output) {
		
		case 'array':
			return $items;
			break;
		
		case 'string':
			for ($i=0;$i<=count($items);$i++){
				if (!(is_numeric($i) & ($i&1)) & ($i>0)) {
					$stringitems .= '<br />'; //add more<br />
				}
				$stringitems .= '<br /><font size=1>'.$items[$i].'</font>';
			}
			return $stringitems;
			break;
			
		case 'theme':
			$themeitems .= theme('item_list', $items);
			return $themeitems;
			break;
			
	
	}
}

valentin@irata.ch’s picture

Amazing! thanks for the so quick answer!

I'm currently testing it, but as far as i see, there's no problems... perhaps just a question...
In your module, it's wrote that i should add three lines of code at line 308 of the store.module file of E-Commerce, that's fine, i did it, but the second line is for me a little bit strange.

$details .= dynamic_parcel_list($p->data,'serialized','theme');
Using the .= would means that somewhere before, the $details should be at least define...
but here's what i got if i follow your instructions:

function theme_store_invoice($txn, $print_mode = TRUE, $trial = FALSE) {
  global $base_url;

  $header = array();
  $row    = array();
	
	if (module_exist('dynamic_parcel')){
		$details .= dynamic_parcel_list($p->data,'serialized','theme');
	}

  if (empty($txn->mail) && $txn->uid > 0) {
.......

So did i put the code at the wrong place?

By the way, some other things (i'm trying to fix)
In the products options, if you choose a textfield, the choices should not be required i guess

They are also some errors displaying (mostly, the following ones):
# warning: array_merge() [function.array-merge]: Argument #2 is not an array in /var/www/test/public/daruma2/modules/ecommerce/dynamic_parcel/dynamic_parcel.module on line 230.
# warning: array_merge() [function.array-merge]: Argument #1 is not an array in /var/www/test/public/daruma2/modules/ecommerce/dynamic_parcel/dynamic_parcel.module on line 237.

And a major bug: textfield didn't appear on the product page (but they are appearing when you make a preview).
The "add to cart" button doesn't works for me, it sends me somewhere... (but the "add to cart" adds the product, can't see at the moment if the options are saved with but i guess it will break)

I'll try to fix the few things i've mentioned above and tell you if i succeed.
Once again, thanks

alynner’s picture

sorry, I did mess around with the add to cart link, change line 290 to:

$form['#action'] = "/cart/add/".$node->nid;

The store code: yes, there must be a new version of the store module. Here is what that function should look like up to the stuff I added to display dynamic_parcel:

function theme_store_invoice($txn, $print_mode = TRUE, $trial = FALSE) {
  global $base_url;

  $header = array();
  $row    = array();

  if (empty($txn->mail) && $txn->uid > 0) {
    $txn->mail = db_result(db_query('SELECT mail FROM {users} WHERE uid = %d', $txn->uid));
  }

  if ($txn->items) {
    $header = array(t('Quantity'), t('Item'), t('Price'));

    $shippable = FALSE;
    foreach ($txn->items as $p) {
      $prod = product_load($p);
      if (product_is_shippable($p->vid)) $shippable = TRUE;

      $price = store_adjust_misc($txn, $p);

      $subtotal += (product_has_quantity($p) ? $p->qty * $price : $price);
      $details = '';
		if (module_exist('dynamic_parcel')){
			$details .= dynamic_parcel_list($p->data,'serialized','theme');
		}
...

I'm not sure about the array_merge errors, when exactly do they show up?
I'm also not sure why the textfield isn't actually showing up, it did when I tested it...

glad I could help!
alynner

Zane Dog’s picture

This looks very useful. I am confused by your instruction "add this code at line 308" does it mean before or after line 308?
What should the lines around it look like?

Thanks

alynner’s picture

I haven't made that patch yet, so I thought I would reply...

store:

307 $details = '';
308 if (module_exist('dynamic_parcel')){
309 $details .= dynamic_parcel_list($p->data,'serialized','theme');
310 }
311 if (is_array($p->data)) {

cart:

180 if ($form['items'][$nid]['recurring']) {
181 $desc.= '

'. form_render($form['items'][$nid]['recurring']) .'

';
182 }
183 if (module_exist('dynamic_parcel')){
184 $desc .= dynamic_parcel_list($nid,'nid','string');
185 }
186 if ($form['items'][$nid]['availability']) {
187 $desc.= form_render($form['items'][$nid]['availability']);
188 }

:-)

Zane Dog’s picture

I tried update #3 from August 4. When creating a dynamic parcel product, I hit the "Preview" button rather than "Submit".
This resulted in the immediate creation of an empty node, and I got a message saying "Your was created."

I had administratively required preview for other reasons; when I turn off that requirement and submit from the parcel creation page, it appears to work.

Another problem: If I first add a dynamic parcel to my cart, I cannot then add another instance of that parcel, with different options. The "add to cart" link on the product page changes to "This item is in your cart".

Also, it would be nice to be able to adjust the price based on options.

Sorry to be Mister Complainer, I know it's a very young module.

alynner’s picture

Thanks for all your input and trying it out. I knew I couldn't find all the problems myself. I'll look into your issues and let you know when I have a new version.

Adding multiple items is a bit of a challenge but it can be done. Right now I have the ability to add more instances but it would list 4 items as a description of the one product with a quantity of two. Which is okay but not ideal. I scrapped it because I didn't need it for my site, but I will take another stab at it - see if I can get the layout better.

Someone else mentioned that you can add to cart even if you don't choose any options, so I'll fix that in the next version too.

I'll also add better instructions and maybe a patch for the store and cart modules.

alynner

alynner’s picture

this is turning into a duplicate posting, so please post any further comments here: http://drupal.org/node/76815

cheers
alynner