custom module productcoption
config.xml
<config>
<modules>
<Somemod_ProductCoption>
<version>0.0.1</version>
</Somemod_ProductCoption>
</modules>
<global>
<resources>
<productcoption_setup>
<setup>
<module>Somemod_ProductCoption</module>
</setup>
</productcoption_setup>
</resources>
<blocks>
<productcoption>
<class>Somemod_ProductCoption_Block</class>
</productcoption>
<adminhtml>
<rewrite>
<catalog_product_edit_tab_options_option>Somemod_ProductCoption_Block_Adminhtml_ProductEditTabOptions_Option</catalog_product_edit_tab_options_option>
<sales_items_abstract>Somemod_ProductCoption_Block_Adminhtml_SalesItems_Abstract</sales_items_abstract>
</rewrite>
</adminhtml>
</blocks>
<helpers>
<productcoption>
<class>Somemod_ProductCoption_Helper</class>
</productcoption>
</helpers>
<models>
<productcoption>
<class>Somemod_ProductCoption_Model</class>
<resourceModel>productcoption_resource</resourceModel>
</productcoption>
<productcoption_resource>
<class>Somemod_ProductCoption_Model_Resource</class>
</productcoption_resource>
<catalog>
<rewrite>
<product_option>Somemod_ProductCoption_Model_Option</product_option>
<product_option_type_file>Somemod_ProductCoption_Model_Option_Type_File</product_option_type_file>
<product_option_type_text>Somemod_ProductCoption_Model_Option_Type_Text</product_option_type_text>
<product_type_simple>Somemod_ProductCoption_Model_ProductType_Simple</product_type_simple>
<product_type_virtual>Somemod_ProductCoption_Model_ProductType_Virtual</product_type_virtual>
</rewrite>
</catalog>
</models>
<catalog>
<product>
<options>
<custom>
<groups>
<productcoption translate="label" module="productcoption">
<label>ProductCoption Type</label>
<render>productcoption/adminhtml_productEditTabOptions_type</render>
<types>
<source translate="label" module="productcoption">
<label>Custom Source</label>
</source>
<country translate="label" module="productcoption">
<label>Country</label>
</country>
<region translate="label" module="productcoption">
<label>Region</label>
</region>
</types>
</productcoption>
</groups>
</custom>
</options>
</product>
</catalog>
</global>
<frontend>
<routers>
<productcoption>
<use>standard</use>
<args>
<module>Somemod_ProductCoption</module>
<frontName>productcoption</frontName>
</args>
</productcoption>
<sales>
<args>
<modules>
<Somemod_productcoption before="Mage_Sales">Somemod_ProductCoption_Sales</Somemod_productcoption>
</modules>
</args>
</sales>
</routers>
<layout>
<updates>
<emgs>
<file>productcoption.xml</file>
</emgs>
</updates>
</layout>
</frontend>
<adminhtml>
<translate>
<modules>
<Somemod_ProductCoption>
<files>
<default>Somemod_ProductCoption.csv</default>
</files>
</Somemod_ProductCoption>
</modules>
</translate>
</adminhtml>
<admin>
<routers>
<adminhtml>
<args>
<modules>
<Somemod_ProductCoption before="Mage_Adminhtml">Somemod_ProductCoption_Adminhtml</Somemod_ProductCoption>
</modules>
</args>
</adminhtml>
</routers>
</admin>
</config>
class Scicom_ProductCoption_Block_Adminhtml_ProductEditTabOptions_Type extends Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Options_Type_Abstract
{
/**
* Render in admin > product's Custom Options tab
*/
public function __construct()
{
parent::__construct();
$this->setTemplate('productcoption/productEditOptions/type.phtml');
}
}
The phtml is the same as app\design\adminhtml\default\default\template\catalog\product\edit\options\type\date.phtml
<script type="text/javascript">
OptionTemplateProductcoption = '<table class="border" cellpadding="0" cellspacing="0">'+
'<tr class="headings">'+
<?php if ($this->getCanReadPrice() !== false) : ?>
'<th class="type-price"><?php echo Mage::helper('core')->jsQuoteEscape(Mage::helper('catalog')->__('Price')) ?></th>' +
'<th class="type-type"><?php echo Mage::helper('core')->jsQuoteEscape(Mage::helper('catalog')->__('Price Type')) ?></th>' +
<?php endif; ?>
'<th class="last"><?php echo Mage::helper('core')->jsQuoteEscape(Mage::helper('catalog')->__('SKU')) ?></th>'+
'</tr>'+
'<tr>'+
<?php if ($this->getCanReadPrice() !== false) : ?>
'<td><input type="text" class="input-text validate-number product-option-price" id="product_option_{{option_id}}_price" name="product[options][{{option_id}}][price]" value="{{price}}"<?php if ($this->getCanEditPrice() === false) : ?> disabled="disabled"<?php endif; ?>></td>' +
'<td><?php echo $this->getPriceTypeSelectHtml() ?>{{checkboxScopePrice}}</td>' +
<?php else : ?>
'<input type="hidden" id="product_option_{{option_id}}_price" name="product[options][{{option_id}}][price]">' +
'<input type="hidden" name="product[options][{{option_id}}][price_type]" id="product_option_{{option_id}}_price_type">' +
<?php endif; ?>
'<td class="last"><input type="text" class="input-text type-sku" name="product[options][{{option_id}}][sku]" value="{{sku}}"></td>'+
'</tr>'+
'</table>';
if ($('option_panel_type_productcoption')) {
$('option_panel_type_productcoption').remove();
}
</script>
productcoption.xml insert html in frontend product view:
<layout version="0.1.0">
<catalog_product_view>
<reference name="product.info.options">
<action method="addOptionRenderer"><type>source</type><block>productcoption/productViewOptions_type_source</block><template>productcoption/productViewOptions/type/source.phtml</template></action>
</reference>
</catalog_product_view>
</layout>
app\design\adminhtml\default\default\productoption\productEditOptions\option.phtml
Search for case 'field':
, for the 2 locations found, add like the following:
switch(element.getValue()){
case 'field':
case 'area':
template = OptionTemplateText;
group = 'text';
break;
// add to the end of switch but before default
case 'source':
case 'country':
case 'region':
template = OptionTemplateProductcoption;
group = 'productcoption';
break;
Block Override:
/**
* Overwrite
* - reset template for the added custom source
* - add renderer to the Custom Options tab in admin product page
*/
class Scicom_ProductCoption_Block_Adminhtml_ProductEditTabOptions_Option extends Mage_Adminhtml_Block_Catalog_Product_Edit_Tab_Options_Option
{
public function __construct()
{
parent::__construct();
//$this->setTemplate('catalog/product/edit/options/option.phtml'); //original
$this->setTemplate('productcoption/productEditOptions/option.phtml');
$this->setCanReadPrice(true);
$this->setCanEditPrice(true);
}
public function getTemplatesHtml()
{
$this->getChild('productcoption_option_type')
->setCanReadPrice($this->getCanReadPrice())
->setCanEditPrice($this->getCanEditPrice());
$templates = parent::getTemplatesHtml(). "\n" . $this->getChildHtml('productcoption_option_type');
return $templates;
}
}
Model Override:
class Some_Module_Model_Catalog_Product_Option extends Mage_Catalog_Model_Product_Option
{
/**
* Get group name of option by given option type
*
* @param string $type
* @return array
*/
public function getGroupByType($type = null)
{
if (is_null($type)) {
$type = $this->getType();
}
if ($group = parent::getGroupByType($type)) {
return $group;
}
$optionGroupsToTypes = array(
'source' => 'source',
'country'=> 'source',
);
return isset($optionGroupsToTypes[$type])?$optionGroupsToTypes[$type]:'';
}
/**
* Group model factory
*
* @param string $type Option type
* @return Mage_Catalog_Model_Product_Option_Group_Abstract
*/
public function groupFactory($type)
{
$group = $this->getGroupByType($type);
if ($group == 'source') {
return Mage::getModel('somemodule/catalog_product_option_type_' . $group);
}
if (!empty($group)) {
return Mage::getModel('catalog/product_option_type_' . $group);
}
Mage::throwException(Mage::helper('catalog')->__('Wrong option type to get group instance.'));
}
/**
* Get the source model
*
* @return class
*/
public function getSource()
{
$title = $this->getType() == 'country' ? 'country' : lcfirst(str_replace(' ', '', $this->getTitle()));
return Mage::getSingleton('somemodule/source_productOption_'.$title);
}
}
The above model is mainly for saving the custom options to the product in admin.
The group factory model:
class Some_Module_Model_Catalog_Product_Option_Type_Source extends Mage_Catalog_Model_Product_Option_Type_Default
{
/**
* Validate user input for option
*
* @throws Mage_Core_Exception
* @param array $values All product option values, i.e. array (option_id => mixed, option_id => mixed...)
* @return Mage_Catalog_Model_Product_Option_Type_Default
*/
public function validateUserValue($values)
{
parent::validateUserValue($values);
$option = $this->getOption();
$value = $this->getUserValue();
if (empty($value) && $option->getIsRequire() && !$this->getSkipCheckRequiredOption()) {
$this->setIsValid(false);
Mage::throwException(Mage::helper('catalog')->__('Please specify the product required option(s).'));
}
return $this;
}
/**
* Prepare option value for cart
*
* @throws Mage_Core_Exception
* @return mixed Prepared option value
*/
public function prepareForCart()
{
if ($this->getIsValid() && $this->getUserValue()) {
return $this->getUserValue();
} else {
return null;
}
}
/**
* Return formatted option value for quote option
*
* @param string $optionValue Prepared for cart option value
* @return string
*/
public function getFormattedOptionValue($optionValue)
{
if ($this->_formattedOptionValue === null) {
$this->_formattedOptionValue = Mage::helper('core')->escapeHtml(
$this->getEditableOptionValue($optionValue)
);
}
return $this->_formattedOptionValue;
}
/**
* Return printable option value
*
* @param string $optionValue Prepared for cart option value
* @return string
*/
public function getPrintableOptionValue($optionValue)
{
return $this->getFormattedOptionValue($optionValue);
}
/**
* Return currently unavailable product configuration message
*
* @return string
*/
protected function _getWrongConfigurationMessage()
{
return Mage::helper('catalog')->__('Some of the selected item options are not currently available.');
}
/**
* Return formatted option value ready to edit, ready to parse
*
* @param string $optionValue Prepared for cart option value
* @return string
*/
public function getEditableOptionValue($optionValue)
{
$option = $this->getOption();
$result = '';
if ($source = $this->getOption()->getSource()) {
$result = $source->getOptionText($optionValue);
} else {
if ($this->getListener()) {
$this->getListener()
->setHasError(true)
->setMessage(
$this->_getWrongConfigurationMessage()
);
}
$result = $optionValue;
}
return $result;
}
/**
* Parse user input value and return cart prepared value, i.e. "one, two" => "1,2"
*
* @param string $optionValue
* @param array $productOptionValues Values for product option
* @return string|null
*/
public function parseOptionValue($optionValue, $productOptionValues)
{
$_values = array();
if (array_key_exists($optionValue, $productOptionValues)) {
return $productOptionValues[$optionValue];
}
return null;
}
}
Block:
class Some_Module_Block_Catalog_Product_View_Options_Type_Source extends Mage_Catalog_Block_Product_View_Options_Abstract
{
public function getSource()
{
if (!$this->getData('source')) {
$this->setData('source', $this->getOption()->getSource());
}
return $this->getData('source');
}
public function getOptions()
{
if (!$this->getData('options')) {
$this->setData('options', $this->getSource()->toOptionArray($this->__('-- Please Select --')));
}
return $this->getData('options');
}
public function toOptionAttributeHtml($option)
{
$html = '';
unset($option['label']);
foreach ($option as $k=>$v) {
$html .= $k.'="'.$v.'" ';
}
return $html;
}
public function getNoteHtml()
{
$html = $this->getSource()->getNoteHtml();
if (!$this->getOptions() && $this->getOption()->getMaxCharacters()) {
$html .= '<br />'.Mage::helper('catalog')->__('Maximum number of characters:').'<strong>'.$this->getOption()->getMaxCharacters().'</strong>';
}
return $html;
}
public function getDefaultValue()
{
return $this->getProduct()->getPreconfiguredValues()->getData('options/' . $this->getOption()->getId());
}
}
somemodule/catalog/product/view/options/type/source.phtml:
<?php $_option = $this->getOption(); ?>
<dt><label<?php if ($_option->getIsRequire()) echo ' class="required"' ?>><?php if ($_option->getIsRequire()) echo '<em>*</em>' ?><?php echo $this->htmlEscape($_option->getTitle(), array('br')) ?></label>
<?php echo $this->getFormatedPrice() ?></dt>
<dd<?php if ($_option->decoratedIsLast){?> class="last"<?php }?>>
<div class="input-box">
<?php if($options = $this->getOptions()): ?>
<?php $default = $this->escapeHtml($this->getDefaultValue()) ?>
<select title="" class="<?php echo $_option->getIsRequire() ? ' required-entry' : '' ?> product-custom-option" id="select_<?php echo $_option->getId() ?>" name="options[<?php echo $_option->getId() ?>]">
<?php foreach($options as $option): ?>
<option <?php echo $this->toOptionAttributeHtml($option) ?> <?php if($option['value']==$default): ?>selected="selected"<?php endif; ?>><?php echo $option['label'] ?></option>
<?php endforeach; ?>
</select>
<?php else: ?>
<input type="text" onchange="opConfig.reloadPrice()" id="options_<?php echo $_option->getId() ?>_text" class="input-text<?php echo $_option->getIsRequire() ? ' required-entry' : '' ?> <?php echo $_option->getMaxCharacters() ? ' validate-length maximum-length-'.$_option->getMaxCharacters() : '' ?> product-custom-option" name="options[<?php echo $_option->getId() ?>]" value="<?php echo $this->escapeHtml($this->getDefaultValue()) ?>" />
<?php endif; ?>
<?php if($note = $this->getNoteHtml()): ?>
<p class="note"><?php echo $note ?></p>
<?php endif; ?>
</div>
</dd>
Finally, add some source models, eg:
class Some_Module_Model_Source_ProductOption_Country extends Some_Module_Model_Source_Country
{
public function toOptionArray($iso3=true, $emptyLabel='')
{
if (!$emptyLabel) {
$emptyLabel = Mage::helper('somemodule')->__('-- Please Select --');
}
return parent::toOptionArray($iso3, $emptyLabel);
}
public function getOptionText($value)
{
$options = $this->toOptionArray();
foreach ($options as $item) {
if ($item['value'] == $value) {
return $item['label'];
}
}
return $value;
}
public function getNoteHtml()
{
return '';
}
}
In adminhtml layout template for backend reordering:
<!-- see Mage_Adminhtml_Block_Catalog_Product_Composite_Fieldset_Options -->
<ADMINHTML_CATALOG_PRODUCT_COMPOSITE_CONFIGURE>
<reference name="product.composite.fieldset.options">
<action method="addOptionRenderer"><type>source</type><block>emgs/catalog_product_view_options_type_source</block><template>emgs/catalog/product/composite/fieldset/options/type/source.phtml</template></action>
</reference>
</ADMINHTML_CATALOG_PRODUCT_COMPOSITE_CONFIGURE>
Copy the .phtml template from the frontend to adminhtml folder.
To Do
Add pricing for the option, overwrite Mage_Catalog_Block_Product_View_Options::getJsonConfig()