You could use the grouped format without having to make a new product type with a few changes:
(default theme)
app/design/frontend/base/default/template/catalog/product/view/type/grouped.phtml
<?php $this->setPreconfiguredValue(); ?>
<?php $_product = $this->getProduct(); ?>
<?php $_associatedProducts = $this->getAssociatedProducts(); ?>
<?php $_hasAssociatedProducts = count($_associatedProducts) > 0; ?>
<?php if ($_product->isAvailable() && $_hasAssociatedProducts): ?>
<p class="availability in-stock"><?php echo $this->__('Availability:') ?> <span><?php echo $this->__('In stock') ?></span></p>
<?php else: ?>
<p class="availability out-of-stock"><?php echo $this->__('Availability:') ?> <span><?php echo $this->__('Out of stock') ?></span></p>
<?php endif; ?>
<?php echo $this->getChildHtml('product_type_data_extra') ?>
<table class="data-table grouped-items-table" id="super-product-table">
<col />
<col />
<col width="1" />
<thead>
<tr>
<th><?php echo $this->__('Product Name') ?></th>
<?php if ($this->getCanShowProductPrice($_product)): ?>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php if ($_hasAssociatedProducts): ?>
<?php foreach ($_associatedProducts as $_item): ?>
<?php $_finalPriceInclTax = $this->helper('tax')->getPrice($_item, $_item->getFinalPrice(), true) ?>
<tr>
<td><?php echo $this->htmlEscape($_item->getName()) ?></td>
<?php if ($_product->isSaleable()): ?>
<td class="a-center">
<?php if ($_item->isSaleable()) : ?>
<input type="hidden" name="super_group[<?php echo $_item->getId() ?>]" maxlength="12" value="<?php echo $_item->getQty()*1 ?>" title="<?php echo $this->__('Qty') ?>" class="input-text qty" />
<?php else: ?>
<p class="availability out-of-stock"><span><?php echo $this->__('Out of stock') ?></span></p>
<?php endif; ?>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td colspan="<?php if ($_product->isSaleable()): ?>4<?php else : ?>3<?php endif; ?>"><?php echo $this->__('No options of this product are available.') ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
<script type="text/javascript">decorateTable('super-product-table')</script>
replace the hidden input qty value with 1 if you always want 1 item of each sub product.
Next you need to modify the product view
(default theme)
app/design/frontend/base/default/template/catalog/product/view.phtml
<?php $_helper = $this->helper('catalog/output'); ?>
<?php $_product = $this->getProduct(); ?>
<script type="text/javascript">
var optionsPrice = new Product.OptionsPrice(<?php echo $this->getJsonConfig() ?>);
</script>
<div id="messages_product_view"><?php echo $this->getMessagesBlock()->getGroupedHtml() ?></div>
<div class="product-view">
<div class="product-essential">
<form action="<?php echo $this->getSubmitUrl($_product) ?>" method="post"
id="product_addtocart_form"<?php if ($_product->getOptions()): ?> enctype="multipart/form-data"<?php endif; ?>>
<div class="no-display">
<input type="hidden" name="product" value="<?php echo $_product->getId() ?>"/>
<input type="hidden" name="related_product" id="related-products-field" value=""/>
</div>
<div class="product-shop">
<div class="product-name">
<h1><?php echo $_helper->productAttribute($_product, $_product->getName(), 'name') ?></h1>
<?php
// if it's grouped
if ($_product->getTypeId() == 'grouped') {
// get the associated products
$associatedProducts = $_product->getTypeInstance(true)->getAssociatedProducts($_product);
foreach ($associatedProducts as $item) {
// get the final price of each item
$finalPrice[] = Mage::helper('tax')->getPrice($item, $item->getFinalPrice(), true);
}
// show the sum as a price
echo "Price:" . Mage::helper('core')->currency(array_sum($finalPrice), true, false);
}?>
</div>
<?php if ($this->canEmailToFriend()): ?>
<p class="email-friend"><a
href="<?php echo $this->helper('catalog/product')->getEmailToFriendUrl($_product) ?>"><?php echo $this->__('Email to a Friend') ?></a>
</p>
<?php endif; ?>
<?php echo $this->getReviewsSummaryHtml($_product, false, true) ?>
<?php echo $this->getChildHtml('alert_urls') ?>
<?php echo $this->getChildHtml('product_type_data') ?>
<?php echo $this->getTierPriceHtml() ?>
<?php echo $this->getChildHtml('extrahint') ?>
<?php if (!$this->hasOptions()): ?>
<div class="add-to-box">
<?php if ($_product->isSaleable()): ?>
<?php echo $this->getChildHtml('addtocart') ?>
<?php if ($this->helper('wishlist')->isAllow() || $_compareUrl = $this->helper('catalog/product_compare')->getAddUrl($_product)): ?>
<span class="or"><?php echo $this->__('OR') ?></span>
<?php endif; ?>
<?php endif; ?>
<?php echo $this->getChildHtml('addto') ?>
</div>
<?php echo $this->getChildHtml('extra_buttons') ?>
<?php elseif (!$_product->isSaleable()): ?>
<div class="add-to-box">
<?php echo $this->getChildHtml('addto') ?>
</div>
<?php endif; ?>
<?php if ($_product->getShortDescription()): ?>
<div class="short-description">
<h2><?php echo $this->__('Quick Overview') ?></h2>
<div
class="std"><?php echo $_helper->productAttribute($_product, nl2br($_product->getShortDescription()), 'short_description') ?></div>
</div>
<?php endif; ?>
<?php echo $this->getChildHtml('other'); ?>
<?php if ($_product->isSaleable() && $this->hasOptions()): ?>
<?php echo $this->getChildChildHtml('container1', '', true, true) ?>
<?php endif; ?>
</div>
<div class="product-img-box">
<?php echo $this->getChildHtml('media') ?>
</div>
<div class="clearer"></div>
<?php if ($_product->isSaleable() && $this->hasOptions()): ?>
<?php echo $this->getChildChildHtml('container2', '', true, true) ?>
<?php endif; ?>
</form>
<script type="text/javascript">
//<![CDATA[
var productAddToCartForm = new VarienForm('product_addtocart_form');
productAddToCartForm.submit = function (button, url) {
if (this.validator.validate()) {
var form = this.form;
var oldUrl = form.action;
if (url) {
form.action = url;
}
var e = null;
try {
this.form.submit();
} catch (e) {
}
this.form.action = oldUrl;
if (e) {
throw e;
}
if (button && button != 'undefined') {
button.disabled = true;
}
}
}.bind(productAddToCartForm);
productAddToCartForm.submitLight = function (button, url) {
if (this.validator) {
var nv = Validation.methods;
delete Validation.methods['required-entry'];
delete Validation.methods['validate-one-required'];
delete Validation.methods['validate-one-required-by-name'];
// Remove custom datetime validators
for (var methodName in Validation.methods) {
if (methodName.match(/^validate-datetime-.*/i)) {
delete Validation.methods[methodName];
}
}
if (this.validator.validate()) {
if (url) {
this.form.action = url;
}
this.form.submit();
}
Object.extend(Validation.methods, nv);
}
}.bind(productAddToCartForm);
//]]>
</script>
</div>
<div class="product-collateral">
<?php foreach ($this->getChildGroup('detailed_info', 'getChildHtml') as $alias => $html): ?>
<div class="box-collateral <?php echo "box-{$alias}" ?>">
<?php if ($title = $this->getChildData($alias, 'title')): ?>
<h2><?php echo $this->escapeHtml($title); ?></h2>
<?php endif; ?>
<?php echo $html; ?>
</div>
<?php endforeach; ?>
<?php echo $this->getChildHtml('upsell_products') ?>
<?php echo $this->getChildHtml('product_additional_data') ?>
</div>
</div>
You would need to add in some additional logic:
- Handle things like out of stock on the child products
- Checkout/cart display templates use getParentIdsByChild() to test if it's a child
- Redirecting simple product pages to their parents (modifying the product controller)
That would cover most of it.