products with multiple features



  • There is still uncertain, if this function will eventually be installed in the core, here’s the guide from the PS forum, for those who can not wait or do not know yet.

    Step 1
    go to your database in prefix_feature_product
    all 3 entrys with primary key

    from this
    0_1514327207678_586559fc-475a-4dd3-be40-02e80355a783-grafik.png


    to this
    0_1514327227032_19e20564-00cb-48ea-a683-2231d53e5712-grafik.png

    step 2

    create “Product.php” at folder /override/classes/

    <?php
    /**
     * Modification Name: Multiple features for Prestashop
     * Description: Allows the user to select multiple features for a product
     * Version: 1.6
     * Author: Mellow <http://www.prestashop.com/forums/user/344943-mellow>
     * Adaptation to Prestashop 1.5.6: David Bucur <http://www.tricksfordevelopers.com>
     * Prestashop 1.6 version: Josef Gullstr�m <http://www.prestashop.com/forums/user/597992-jgullstr>
     * License: GPL2
     */
    class Product extends ProductCore
    {
      public static function getFrontFeaturesStatic($id_lang, $id_product)
    	{
    		if (!Feature::isFeatureActive())
    			return array();
    		if (!array_key_exists($id_product.'-'.$id_lang, self::$_frontFeaturesCache))
    		{
          // Display multi-valued features as comma-separated values in product
          // data sheet.
    			self::$_frontFeaturesCache[$id_product.'-'.$id_lang] = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
    				SELECT name, GROUP_CONCAT(value SEPARATOR \', \') AS value, pf.id_feature
    				FROM '._DB_PREFIX_.'feature_product pf
    				LEFT JOIN '._DB_PREFIX_.'feature_lang fl ON (fl.id_feature = pf.id_feature AND fl.id_lang = '.(int)$id_lang.')
    				LEFT JOIN '._DB_PREFIX_.'feature_value_lang fvl ON (fvl.id_feature_value = pf.id_feature_value AND fvl.id_lang = '.(int)$id_lang.')
    				LEFT JOIN '._DB_PREFIX_.'feature f ON (f.id_feature = pf.id_feature AND fl.id_lang = '.(int)$id_lang.')
    				'.Shop::addSqlAssociation('feature', 'f').'
    				WHERE pf.id_product = '.(int)$id_product.'
            GROUP BY pf.id_feature
    				ORDER BY f.position ASC'
    			);
    		}
    		return self::$_frontFeaturesCache[$id_product.'-'.$id_lang];
    	}
    
      public function addFeaturesToDB($id_feature, $id_value, $cust = 0)
    	{
        // Default behavior.
        if ($cust || !is_array($id_value)) {
    			return parent::addFeaturesToDB($id_feature, $id_value, $cust);
    		}
    
        // For multi-value features, build array of rows and insert into db.
        $base =  array(
          'id_feature' => (int)$id_feature,
          'id_product' => (int)$this->id,
        );
        $rows = array();
        foreach ($id_value as $value) {
          if(!empty($value)) {
            $rows[] = $base + array('id_feature_value' => $value);
          }
        }
        if(!empty($rows)) {
          Db::getInstance()->insert('feature_product', $rows);
        }
    
        // From parent.
        SpecificPriceRule::applyAllRules(array((int)$this->id));
    		if ($id_value) {
    			return ($id_value);
        }
    	}
    }
    

    Step 3
    create “AdminProductsController.php” at folder /override/controllers/admin/

    <?php
    /**
     * Modification Name: Multiple features for Prestashop
     * Description: Allows the user to select multiple features for a product
     * Version: 1.6
     * Author: Mellow <http://www.prestashop.com/forums/user/344943-mellow>
     * Adaptation to Prestashop 1.5.6: David Bucur <http://www.tricksfordevelopers.com>
     * Prestashop 1.6 version: Josef Gullstr�m <http://www.prestashop.com/forums/user/597992-jgullstr>
     * License: GPL2
     */
    class AdminProductsController extends AdminProductsControllerCore
    {
      public function initFormFeatures($obj)
      {
    
        if (!$this->default_form_language)
          $this->getLanguages();
    
        $data = $this->createTemplate($this->tpl_form);
    		$data->assign('default_form_language', $this->default_form_language);
    
        if (!Feature::isFeatureActive())
          $this->displayWarning($this->l('This feature has been disabled. ').' <a href="index.php?tab=AdminPerformance&token='.Tools::getAdminTokenLite('AdminPerformance').'#featuresDetachables">'.$this->l('Performances').'</a>');
        else
        {
          if ($obj->id)
          {
            if ($this->product_exists_in_shop)
            {
              $features = Feature::getFeatures($this->context->language->id, (Shop::isFeatureActive() && Shop::getContext() == Shop::CONTEXT_SHOP));
    
              // Mellow modification.
              foreach ($features as $k => $tab_features)
              {
                $features[$k]['current_item'] = false;
                $features[$k]['val'] = array();
    
                $features[$k]['custom'] = true;
                foreach ($obj->getFeatures() as $tab_products) {
                  if ($tab_products['id_feature'] == $tab_features['id_feature'])
                    $features[$k]['current_item'][] = $tab_products['id_feature_value'];
                }
    
                if (!$features[$k]['current_item']) {
                  $features[$k]['current_item'][0] = null;
                }
    
                $features[$k]['featureValues'] = FeatureValue::getFeatureValuesWithLang($this->context->language->id, (int)$tab_features['id_feature']);
                if (count($features[$k]['featureValues'])) {
                  foreach ($features[$k]['featureValues'] as $value) {
                    if (in_array($value['id_feature_value'], $features[$k]['current_item'])) {
                      $features[$k]['custom'] = false;
                    }
                  }
                }
                if ($features[$k]['custom']) {
                  $features[$k]['val'] = FeatureValue::getFeatureValueLang($features[$k]['current_item'][0]);
                }
              }
              // EOF Mellow modification.
              $data->assign('available_features', $features);
    
              $data->assign('product', $obj);
              $data->assign('link', $this->context->link);
              $data->assign('languages', $this->_languages);
              $data->assign('default_form_language', $this->default_form_language);
            }
            else
              $this->displayWarning($this->l('You must save the product in this shop before adding features.'));
          }
          else
            $this->displayWarning($this->l('You must save this product before adding features.'));
        }
        $this->tpl_form_vars['custom_form'] = $data->fetch();
      }
    
      public function processFeatures()
      {
        if (!Feature::isFeatureActive())
          return;
    
        if (Validate::isLoadedObject($product = new Product((int)Tools::getValue('id_product'))))
        {
          // delete all objects
          $product->deleteFeatures();
    
          // add new objects
          $languages = Language::getLanguages(false);
          foreach ($_POST as $key => $val)
          {
            if (preg_match('/^feature_([0-9]+)_value/i', $key, $match))
            {
              //  "&& $val[0] != 0" added by override.
              if ($val && $val[0] != 0) {
                $product->addFeaturesToDB($match[1], $val);
              }
              else {
                if ($default_value = $this->checkFeatures($languages, $match[1]))
                {
                  $id_value = $product->addFeaturesToDB($match[1], 0, 1);
                  foreach ($languages as $language)
                  {
                    if ($cust = Tools::getValue('custom_'.$match[1].'_'.(int)$language['id_lang']))
                      $product->addFeaturesCustomToDB($id_value, (int)$language['id_lang'], $cust);
                    else
                      $product->addFeaturesCustomToDB($id_value, (int)$language['id_lang'], $default_value);
                  }
                }
              }
            }
          }
        }
        else
          $this->errors[] = Tools::displayError('A product must be created before adding features.');
      }
    }
    
    

    Step 4
    create “features.tpl” at folder /override/controllers/admin/templates/products/

    {*
    * 2007-2014 PrestaShop
    *
    * NOTICE OF LICENSE
    *
    * This source file is subject to the Academic Free License (AFL 3.0)
    * that is bundled with this package in the file LICENSE.txt.
    * It is also available through the world-wide-web at this URL:
    * http://opensource.org/licenses/afl-3.0.php
    * If you did not receive a copy of the license and are unable to
    * obtain it through the world-wide-web, please send an email
    * to license@prestashop.com so we can send you a copy immediately.
    *
    * DISCLAIMER
    *
    * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
    * versions in the future. If you wish to customize PrestaShop for your
    * needs please refer to http://www.prestashop.com for more information.
    *
    *  @author PrestaShop SA <contact@prestashop.com>
    *  @author Josef Gullstr�m <http://www.prestashop.com/forums/user/597992-jgullstr>
    *  @copyright  2007-2014 PrestaShop SA
    *  @license    http://opensource.org/licenses/afl-3.0.php  Academic Free License (AFL 3.0)
    *  International Registered Trademark & Property of PrestaShop SA
    *}
    
    {if isset($product->id)}
    <div id="product-features" class="panel product-tab">
    	<input type="hidden" name="submitted_tabs[]" value="Features" />
    	<h3>{l s='Assign features to this product'}</h3>
    
    	<div class="alert alert-info">
    		{l s='You can specify a value for each relevant feature regarding this product. Empty fields will not be displayed.'}<br/>
    		{l s='You can either create a specific value, or select among the existing pre-defined values you\'ve previously added.'}
    	</div>
    
    	<table class="table">
    		<thead>
    			<tr>
    				<th><span class="title_box">{l s='Feature'}</span></th>
    				<th><span class="title_box">{l s='Pre-defined value'}</span></th>
    				<th><span class="title_box"><u>{l s='or'}</u> {l s='Customized value'}</span></th>
    			</tr>
    		</thead>
    
    		<tbody>
    		{foreach from=$available_features item=available_feature}
    		
    			<tr>
    				<td>{$available_feature.name}</td>
    				<td>
            {* Changed for multiple-value support *}
    				{if sizeof($available_feature.featureValues)}
    					<input type="checkbox" style="display:none;" name="feature_{$available_feature.id_feature}_value[]" id="feature_{$available_feature.id_feature}_is_custom" value="0" {if $available_feature.custom}checked="checked"{/if}/>
              <select class="feature-select" multiple="multiple" data-placeholder="{l s='Select feature values...'}" id="feature_{$available_feature.id_feature}_value" name="feature_{$available_feature.id_feature}_value[]"
    						onchange="$('.custom_{$available_feature.id_feature}_').val('');$('#feature_{$available_feature.id_feature}_is_custom').removeAttr('checked')">
                {foreach from=$available_feature.featureValues item=value}
    						<option value="{$value.id_feature_value}"{if in_array($value.id_feature_value, $available_feature.current_item)}selected="selected"{/if} >
    							{$value.value|truncate:40}
    						</option>
    						{/foreach}
    					</select>
    				{else}
            {* /Changed for multiple-value support *}
    					<input type="hidden" name="feature_{$available_feature.id_feature}_value" value="0" />
    					<span>{l s='N/A'} - 
    						<a href="{$link->getAdminLink('AdminFeatures')|escape:'html':'UTF-8'}&amp;addfeature_value&amp;id_feature={$available_feature.id_feature}"
    					 	class="confirm_leave btn btn-link"><i class="icon-plus-sign"></i> {l s='Add pre-defined values first'} <i class="icon-external-link-sign"></i></a>
    					</span>
    				{/if}
    				</td>
    				<td>
    				
    				<div class="row lang-0" style='display: none;'>
    					<div class="col-lg-9">
    						<textarea class="custom_{$available_feature.id_feature}_ALL textarea-autosize"	name="custom_{$available_feature.id_feature}_ALL"
    								cols="40" style='background-color:#CCF'	rows="1" onkeyup="{foreach from=$languages key=k item=language}$('.custom_{$available_feature.id_feature}_{$language.id_lang}').val($(this).val());{/foreach}" >{$available_feature.val[1].value|escape:'html':'UTF-8'|default:""}</textarea>
    
    					</div>
    					{if $languages|count > 1}
    						<div class="col-lg-3">
    							<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
    										{l s='ALL'}
    								<span class="caret"></span>
    							</button>
    							<ul class="dropdown-menu">
    								{foreach from=$languages item=language}
    									<li>
    										<a href="javascript:void(0);" onclick="restore_lng($(this),{$language.id_lang});">{$language.iso_code}</a>
    									</li>
    								{/foreach}
    							</ul>
    						</div>
    					{/if}
    				</div>
    			
    				{foreach from=$languages key=k item=language}
    					{if $languages|count > 1}
    					<div class="row translatable-field lang-{$language.id_lang}">
    						<div class="col-lg-9">
    						{/if}
    						<textarea
    								class="custom_{$available_feature.id_feature}_ textarea-autosize"
    								name="custom_{$available_feature.id_feature}_{$language.id_lang}"
    								cols="40"
    								rows="1"
                    {* Changed for multiple-value support *}onkeyup="if (isArrowKey(event)) return ;$('#feature_{$available_feature.id_feature}_value option:selected').removeAttr('selected').trigger('chosen:updated');;$('#feature_{$available_feature.id_feature}_is_custom').prop('checked', true);"{* /Changed for multiple-value support *}>{$available_feature.val[$k].value|escape:'html':'UTF-8'|default:""}</textarea>
    								
    					{if $languages|count > 1}
    						</div>
    						<div class="col-lg-3">
    							<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
    								{$language.iso_code}
    								<span class="caret"></span>
    							</button>
    							<ul class="dropdown-menu">
    								<li><a href="javascript:void(0);" onclick="all_languages($(this));">{l s='ALL'}</a></li>								
    								{foreach from=$languages item=language}
    								<li>
    									<a href="javascript:hideOtherLanguage({$language.id_lang});">{$language.iso_code}</a>
    								</li>
    								{/foreach}
    							</ul>
    						</div>
    					</div>
    					{/if}
    					{/foreach}
    				</td>
    
    			</tr>
    			{foreachelse}
    			<tr>
    				<td colspan="3" style="text-align:center;"><i class="icon-warning-sign"></i> {l s='No features have been defined'}</td>
    			</tr>
    			{/foreach}
    		</tbody>
    	</table>
    
    	<a href="{$link->getAdminLink('AdminFeatures')|escape:'html':'UTF-8'}&amp;addfeature" class="btn btn-link confirm_leave button">
    		<i class="icon-plus-sign"></i> {l s='Add a new feature'} <i class="icon-external-link-sign"></i>
    	</a>
    	<div class="panel-footer">
    		<a href="{$link->getAdminLink('AdminProducts')|escape:'html':'UTF-8'}" class="btn btn-default"><i class="process-icon-cancel"></i> {l s='Cancel'}</a>
    		<button type="submit" name="submitAddproduct" class="btn btn-default pull-right"><i class="process-icon-save"></i> {l s='Save'}</button>
    		<button type="submit" name="submitAddproductAndStay" class="btn btn-default pull-right"><i class="process-icon-save"></i> {l s='Save and stay'}</button>
    	</div>
    </div>
    {/if}
    
    <script type="text/javascript">
    	hideOtherLanguage({$default_form_language});
    {literal}
      $('select.feature-select').chosen({width: '250px'}); // Chosen for multi-selects
    	$(".textarea-autosize").autosize();
    	function all_languages(pos)
    	{
    {/literal}
    {foreach from=$languages key=k item=language}
    		pos.parents('td').find('.lang-{$language.id_lang}').addClass('nolang-{$language.id_lang}').removeClass('lang-{$language.id_lang}');
    {/foreach}
    		pos.parents('td').find('.translatable-field').hide();
    		pos.parents('td').find('.lang-0').show();
    {literal}
    	}
    
    	function restore_lng(pos,i)
    	{
    {/literal}
    {foreach from=$languages key=k item=language}
    		pos.parents('td').find('.nolang-{$language.id_lang}').addClass('lang-{$language.id_lang}').removeClass('nolang-{$language.id_lang}');
    {/foreach}
    {literal}
    		pos.parents('td').find('.lang-0').hide();
    		hideOtherLanguage(i);
    	}
    </script>
    {/literal}
    

    product backoffice
    0_1514328929917_09338a37-a5a6-4716-8f6b-074851d6a4a9-grafik.png

    product frontoffice
    0_1514328976394_0f4deb5f-db50-4f95-8bc4-cfa8454784d3-grafik.png

    if you use blocklayered - your customer have more option when you change the features option in the modul
    0_1514329074331_8d813be5-90e3-4d05-b5be-d66a8e983a70-grafik.png



  • Step 1 wont let me make a primary outta id_feature_value, it gives me this error: #1062 - Duplicate entry ‘53’ for key ‘PRIMARY’.
    (edit) Had to do an export, modify the primary, then an import, but i got it…



  • I always mark with me all 3 options and then set the primary key for all completely new.

    0_1514369559402_tb_feature_product phpMyAdmin.png



  • Prestashop has announced multiple features for 1.7.3. I haven’t checked out how they will do it but I would prefer to keep compatibility with them.

    http://build.prestashop.com/news/prestashop-1-7-3-0-beta-1/



  • @musicmaster thx for the information. I missed that… This is a must have for tb as well…



  • Hi, guide works great in Prestashop 1.6.1.20. How could feature-values be ordered alphabetically both at front (product datasheet) and backoffice ? thanks!



  • Hi, guide works well in Prestashop 1.6.20.1. How could features-values be ordered alphabeticall both at front (product datasheet) and backoffice (product entry) ? thanks!



  • @Galloper What are you talking about? This forum has nothing to do with PS 1.6.20.1



  • Hello.

    I’m new in Prestashop, got my page done few days ago.
    I’m looking for the option mentioned in this topic. I have electronic equipment and my goal is to be able to assign multiple values of the same feature to one device (e.g. a device can have 24Vdc or 230Vac power supply version) so the customers can filter it correctly.
    I do not care about printing this features on product front page. All I want to do is

    I went through the whole Internet and the only useful information I can see here and here: https://www.prestashop.com/forums/topic/176242-modification-select-multiple-values-for-one-feature/ .

    I’m working on Prestashop 1.7.2.1 and I’m too scared to update to newest 1.7.4.2 🙂 Yes, I’ve heard that multi features option is available since 1.7.3 but for sure it can be also done by modifying core files in 1.7.2

    What I’ve done so far:

    1. Step 1 of this instruction. SQL database accepts multiple id_feature_value for the same id_feature. I can manually add suitable records to SQL ps_feature_products and it is working properly on the website, filtering etc. . Even when I open product editing page, Prestashop loads all the features from the DB. The problem is that when I want to change something in the product, only the last feature is saved (Prestashop overrides SQL database) - I want to change this.

    2. From what I understand the whole thing is about modifying processFeatures() funtion in AdminProductsController.php and maybe addFeaturesToDB() function in Product.php.

    The approach is to gather feature values in processFeatures() function and pass it to addFeaturesToDB() function as an array.
    Then in addFeaturesToDB() function add to the array id_feature, id_product and write it to DB.

    AdminProductsController.php:

       public function processFeatures($id_product = null)
        {
            if (!Feature::isFeatureActive()) {
                return;
            }
    
            $id_product = (int) $id_product ? $id_product : (int)Tools::getValue('id_product');
    
            if (Validate::isLoadedObject($product = new Product($id_product))) {
                // delete all objects
                $product->deleteFeatures();
    
                // add new objects
                $languages = Language::getLanguages(false);
                foreach ($_POST as $key => $val) {
                    if (preg_match('/^feature_([0-9]+)_value/i', $key, $match)) {
                        if ($val && $val[0] != 0){
    						foreach ($val as $feature_val) $product->addFeaturesToDB($match[1], $feature_val);
                        } else {
                            if ($default_value = $this->checkFeatures($languages, $match[1])) {
                                $id_value = $product->addFeaturesToDB($match[1], 0, 1);
                                foreach ($languages as $language) {
                                    if ($cust = Tools::getValue('custom_'.$match[1].'_'.(int)$language['id_lang'])) {
                                        $product->addFeaturesCustomToDB($id_value, (int)$language['id_lang'], $cust);
                                    } else {
                                        $product->addFeaturesCustomToDB($id_value, (int)$language['id_lang'], $default_value);
                                    }
                                }
                            }
                        }
                    }
                }
            } else {
                $this->errors[] = $this->trans('A product must be created before adding features.', array(), 'Admin.Catalog.Notification');
            }
        }
    

    This suppose to get all the features values into array val and pass it to addFeaturesToDB().
    I have also tried the way it is shown in this post so without “foreach ($val as $feature_val) $product->addFeaturesToDB($match[1], $feature_val);” but only “$product->addFeaturesToDB($match[1], $val);” in this place.
    Then we have:
    addFeaturesToDB():

    public function addFeaturesToDB($id_feature, $id_value, $cust = 0)
    	{
        // Default behavior.
        if ($cust)
    		{
    			$row = array('id_feature' => (int)$id_feature, 'custom' => 1);
    			Db::getInstance()->insert('feature_value', $row);
    			$id_value = Db::getInstance()->Insert_ID();
    		}
    
        // For multi-value features, build array of rows and insert into db.
        $base =  array(
          'id_feature' => (int)$id_feature,
          'id_product' => (int)$this->id,
        );
        $rows = array();
        foreach ($id_value as $value) {
          if(!empty($value)) {
            $rows[] = $base + array('id_feature_value' => $value);
          }
        }
        $row = array('id_feature' => (int)$id_feature, 'id_product' => (int)$this->id, 'id_feature_value' => (int)$id_value);
    		Db::getInstance()->insert('feature_product', $row);
    		SpecificPriceRule::applyAllRules(array((int)$this->id));
    		if ($id_value)
    			return ($id_value);
    	}
    

    I build an array with id_feature and id_product and add id_value array which is val from processFeatures() function.

    But it does not work.

    Can you help me, please? Maybe since Prestashop 1.7.4 is released someone can look up how they are solving this issue?

    Best regards,
    Jacek



  • I’m working on Prestashop 1.7.2.1

    Say hello to thirty bees! Here we actually solve misalignments and bugs.



  • @traumflug said in products with multiple features:

    Say hello to thirty bees! Here we actually solve misalignments and bugs.

    Great answer 🙂 It seems ps users are looking for answers on the TB forum


 

Looks like your connection to thirty bees forum was lost, please wait while we try to reconnect.