Enable Combinations for Virtual Products in Prestashop 1.5

Prestashop 1.4 used to let you create combinations for downloadable items. It was the same file for every combinations, but we could use them, at least. This is no longer possible with Prestashop 1.5’s virtual products, so let’s see how to turn this feature back!

The problem: You cannot use combinations with a virtual product

If you ever tried to add attribute combinations to a virtual product, or turn into virtual a normal one that already had them, you might have run into this message. In the worst case, you just upgraded from 1.4 and discovered your products didn’t have the previous files attached anymore, if they had combinations. Despite having to re-bind all the downloadable files in the latter case, we can re-enable combinations for virtual products in Prestashop 1.5 using some overrides:

  • AdminProductsController
  • combinations.tpl (back office template file)
  • virtualproduct.tpl

We will simply disable a couple of checks Prestashop does, which turn off combinations if the product is virtual, and vice-versa.

The AdminProductsController Override

First of all, let’s disable the virtual product check in the product’s back office. Create a new file in override/controllers/admin and call it AdminProductsController.php. Add the following to begin with, inside php tags of course:

class AdminProductsController extends AdminProductsControllerCore
{
}

At this point, we need to extend and modify the method named initFormAttributes. Therefore, open up the original AdminProductsController.php (controllers/admin), copy initFormAttributes from there (about line 3628 in Prestashop 1.5.6), and paste the whole function inside the new override.

Then, locate the following:

if ($this->product_exists_in_shop)
			{
				if ($product->is_virtual)
				{
					$data->assign('product', $product);
					$this->displayWarning($this->l('A virtual product cannot have combinations.'));
				}
				else
				{
					$attribute_js = array();
					$attributes = Attribute::getAttributes($this->context->language->id, true);
					foreach ($attributes as $k => $attribute)
						$attribute_js[$attribute['id_attribute_group']][$attribute['id_attribute']] = $attribute['name'];
					$currency = $this->context->currency;
					$data->assign('attributeJs', $attribute_js);
					$data->assign('attributes_groups', AttributeGroup::getAttributesGroups($this->context->language->id));

					$data->assign('currency', $currency);

					$images = Image::getImages($this->context->language->id, $product->id);

					$data->assign('tax_exclude_option', Tax::excludeTaxeOption());
					$data->assign('ps_weight_unit', Configuration::get('PS_WEIGHT_UNIT'));

					$data->assign('ps_use_ecotax', Configuration::get('PS_USE_ECOTAX'));
					$data->assign('field_value_unity', $this->getFieldValue($product, 'unity'));

					$data->assign('reasons', $reasons = StockMvtReason::getStockMvtReasons($this->context->language->id));
					$data->assign('ps_stock_mvt_reason_default', $ps_stock_mvt_reason_default = Configuration::get('PS_STOCK_MVT_REASON_DEFAULT'));
					$data->assign('minimal_quantity', $this->getFieldValue($product, 'minimal_quantity') ? $this->getFieldValue($product, 'minimal_quantity') : 1);
					$data->assign('available_date', ($this->getFieldValue($product, 'available_date') != 0) ? stripslashes(htmlentities($this->getFieldValue($product, 'available_date'), $this->context->language->id)) : '0000-00-00');

					$i = 0;
					$data->assign('imageType', ImageType::getByNameNType('small_default', 'products'));
					$data->assign('imageWidth', (isset($image_type['width']) ? (int)($image_type['width']) : 64) + 25);
					foreach ($images as $k => $image)
					{
						$images[$k]['obj'] = new Image($image['id_image']);
						++$i;
					}
					$data->assign('images', $images);

					$data->assign($this->tpl_form_vars);
					$data->assign(array(
						'list' => $this->renderListAttributes($product, $currency),
						'product' => $product,
						'id_category' => $product->getDefaultCategory(),
						'token_generator' => Tools::getAdminTokenLite('AdminAttributeGenerator'),
						'combination_exists' => (Shop::isFeatureActive() && (Shop::getContextShopGroup()->share_stock) && count(AttributeGroup::getAttributesGroups($this->context->language->id)) > 0 && $product->hasAttributes())
					));
				}
			}
			else
				$this->displayWarning($this->l('You must save the product in this shop before adding combinations.'));

Can you see where it reads if ($product->is_virtual)? Get rid of this if/else, so it looks like this:

			if ($this->product_exists_in_shop)
			{
				// removed virtual product restriction
				$attribute_js = array();
				$attributes = Attribute::getAttributes($this->context->language->id, true);
				foreach ($attributes as $k => $attribute)
					$attribute_js[$attribute['id_attribute_group']][$attribute['id_attribute']] = $attribute['name'];
				$currency = $this->context->currency;
				$data->assign('attributeJs', $attribute_js);
				$data->assign('attributes_groups', AttributeGroup::getAttributesGroups($this->context->language->id));

				$data->assign('currency', $currency);

				$images = Image::getImages($this->context->language->id, $product->id);

				$data->assign('tax_exclude_option', Tax::excludeTaxeOption());
				$data->assign('ps_weight_unit', Configuration::get('PS_WEIGHT_UNIT'));

				$data->assign('ps_use_ecotax', Configuration::get('PS_USE_ECOTAX'));
				$data->assign('field_value_unity', $this->getFieldValue($product, 'unity'));

				$data->assign('reasons', $reasons = StockMvtReason::getStockMvtReasons($this->context->language->id));
				$data->assign('ps_stock_mvt_reason_default', $ps_stock_mvt_reason_default = Configuration::get('PS_STOCK_MVT_REASON_DEFAULT'));
				$data->assign('minimal_quantity', $this->getFieldValue($product, 'minimal_quantity') ? $this->getFieldValue($product, 'minimal_quantity') : 1);
				$data->assign('available_date', ($this->getFieldValue($product, 'available_date') != 0) ? stripslashes(htmlentities($this->getFieldValue($product, 'available_date'), $this->context->language->id)) : '0000-00-00');

				$i = 0;
				$type = ImageType::getByNameNType('%', 'products', 'height');
				if (isset($type['name']))
					$data->assign('imageType', $type['name']);
				else
					$data->assign('imageType', 'small_default');
				$data->assign('imageWidth', (isset($image_type['width']) ? (int)($image_type['width']) : 64) + 25);
				foreach ($images as $k => $image)
				{
					$images[$k]['obj'] = new Image($image['id_image']);
					++$i;
				}
				$data->assign('images', $images);

				$data->assign($this->tpl_form_vars);
				$data->assign(array(
					'list' => $this->renderListAttributes($product, $currency),
					'product' => $product,
					'id_category' => $product->getDefaultCategory(),
					'token_generator' => Tools::getAdminTokenLite('AdminAttributeGenerator'),
					'combination_exists' => (Shop::isFeatureActive() && (Shop::getContextShopGroup()->share_stock) && count(AttributeGroup::getAttributesGroups($this->context->language->id)) > 0 && $product->hasAttributes())
				));

			}
			else
				$this->displayWarning($this->l('You must save the product in this shop before adding combinations.'));

Save the file, go to cache/ and delete class_index.php. Then access the product’s back office again, load a virtual product and click on the combinations tab. Nothing! At least, it’s better than the error message. We need to tackle another step.

Editing the attribute combinations template

Inside the same folder where we placed our override, create the following structure: templates/products/. Then, reach the admin folder themes/default/template/controllers/products. Copy combinations.tpl from here into the other folder we just created.

At this point, open the newly cloned file, and right at the beginning we can read:

{if isset($product->id) && !$product->is_virtual}

Just get rid of && !$product->is_virtual. Save and access the combinations tab again!

Finishing touches

You can create combinations for a virtual product right away. However, if you try to convert an existing one into virtual, you will get the very same annoying message you got before. We need to hardcode a small fix in another couple of files, first: admin-products.js, which can be found inside the js folder. Open it up and locate the following:

				if (has_combinations)
				{
					$('#simple_product').attr('checked', true);
					$('#warn_virtual_combinations').show();
				}
				else
				{
					$('li.tab-row a[id*="VirtualProduct"]').show().click();
					$('#is_virtual').val(1);

					tabs_manager.onLoad('VirtualProduct', function(){
						$('#is_virtual_good').attr('checked', true);
						$('#virtual_good').show();
					});

					tabs_manager.onLoad('Quantities', function(){
						$('.stockForVirtualProduct').hide();
					});

					$('li.tab-row a[id*="Shipping"]').hide();

					tabs_manager.onLoad('Informations', function(){
						$('#condition').attr('disabled', true);
						$('#condition option[value=refurbished]').removeAttr('selected');
						$('#condition option[value=used]').removeAttr('selected');
					});
				}

Change it to


					$('li.tab-row a[id*="VirtualProduct"]').show().click();
					$('#is_virtual').val(1);

					tabs_manager.onLoad('VirtualProduct', function(){
						$('#is_virtual_good').attr('checked', true);
						$('#virtual_good').show();
					});

					tabs_manager.onLoad('Quantities', function(){
						$('.stockForVirtualProduct').hide();
					});

					$('li.tab-row a[id*="Shipping"]').hide();

					tabs_manager.onLoad('Informations', function(){
						$('#condition').attr('disabled', true);
						$('#condition option[value=refurbished]').removeAttr('selected');
						$('#condition option[value=used]').removeAttr('selected');
					});

Then, once more, reach the themes/default/template/controllers/products folder. This time, copy virtualproduct.tpl to the same folder where we added combinations.tpl. Open it up and inspect the following snippet:

			{* Don't display file form if the product has combinations *}
			{if empty($product->cache_default_attribute)}
				{if $product->productDownload->id}
					<input type="hidden" id="virtual_product_id" name="virtual_product_id" value="{$product->productDownload->id}" />
				{/if}
				<table cellpadding="5" style="float: left; margin-left: 10px;">
					<tr id="upload_input" {if $is_file}style="display:none"{/if}>
						<td class="col-left">
							<label id="virtual_product_file_label" for="virtual_product_file" class="t">{l s='Upload a file'}</label>
						</td>
						<td class="col-right">
							<input type="file" id="virtual_product_file" name="virtual_product_file" onchange="uploadFile();" maxlength="{$upload_max_filesize}" />
							<p class="preference_description">{l s='Your server\'s maximum file-upload size is'}:&nbsp;{$upload_max_filesize} {l s='MB'}</p>
						</td>
					</tr>
					<tr id="upload-error" style="display:none">
						<td colspan=2></td>
					</tr>
					<tr id="upload-confirmation" style="display:none">
						<td colspan=2>
							{if $up_filename}
								<input type="hidden" id="virtual_product_filename" name="virtual_product_filename" value="{$up_filename}" />
							{/if}
							<div class="conf">
							<script>
								delete_this_file = '{l s='Delete this file'}';
							</script>
								<a class="delete_virtual_product" id="delete_downloadable_product" href="{$currentIndex}&deleteVirtualProduct=true&token={$token}&id_product={$product->id}" class="red">
									<img src="../img/admin/delete.gif" alt="{l s='Delete this file'}"/>
								</a>
							</div>
						</td>
					</tr>
					{if $is_file}
						<tr>
							<td class="col-left">
								<input type="hidden" id="virtual_product_filename" name="virtual_product_filename" value="{$product->productDownload->filename}" />
								<label class="t">{l s='Link to the file:'}</label>
							</td>
							 <td class="col-right">
								{$product->productDownload->getHtmlLink(false, true)}
								<a href="{$currentIndex}&deleteVirtualProduct=true&token={$token}&id_product={$product->id}" class="red delete_virtual_product">
									<img src="../img/admin/delete.gif" alt="{l s='Delete this file'}"/>
								</a>
							</td>
						</tr>
					{/if}
					<tr>
						<td class="col-left">
							<label for="virtual_product_name" class="t">{l s='Filename'}</label>
						</td>
						<td class="col-right">
							<input type="text" id="virtual_product_name" name="virtual_product_name" style="width:200px" value="{$product->productDownload->display_filename|escape:'htmlall':'UTF-8'}" />
							<p class="preference_description" name="help_box">{l s='The full filename with its extension (e.g. Book.pdf)'}</p>
						</td>
					</tr>
					<tr>
						<td class="col-left">
							<label for="virtual_product_nb_downloable" class="t">{l s='Number of allowed downloads'}</label>
						</td>
						<td class="col-right">
							<input type="text" id="virtual_product_nb_downloable" name="virtual_product_nb_downloable" value="{$product->productDownload->nb_downloadable|htmlentities}" class="" size="6" />
							<p class="preference_description">{l s='Number of downloads allowed per customer. (Set to 0 for unlimited downloads)'}</p>
						</td>
					</tr>
					<tr>
						<td class="col-left">
							<label for="virtual_product_expiration_date" class="t">{l s='Expiration date'}</label>
						</td>
						<td class="col-right">
							<input class="datepicker" type="text" id="virtual_product_expiration_date" name="virtual_product_expiration_date" value="{$product->productDownload->date_expiration}" size="11" maxlength="10" autocomplete="off" /> {l s='Format: YYYY-MM-DD'}
							<p class="preference_description">{l s='If set, the file will not be downloadable after this date. Leave blank if you do not wish to attach an expiration date.'}</p>
						</td>
					</tr>
						<td class="col-left">
							<label for="virtual_product_nb_days" class="t">{l s='Number of days'}</label>
						</td>
						<td class="col-right">
							<input type="text" id="virtual_product_nb_days" name="virtual_product_nb_days" value="{$product->productDownload->nb_days_accessible|htmlentities}" class="" size="4" /><sup> *</sup>
							<p class="preference_description">{l s='Number of days this file can be accessed by customers'} - <em>({l s='Set to zero for unlimited access.'})</em></p>
						</td>
					</tr>
					{* Feature not implemented *}
					{*<tr>*}
						{*<td class="col-left">*}
							{*<label for="virtual_product_is_shareable" class="t">{l s='is shareable'}</label>*}
						{*</td>*}
						{*<td class="col-right">*}
							{*<input type="checkbox" id="virtual_product_is_shareable" name="virtual_product_is_shareable" value="1" {if $product->productDownload->is_shareable}checked="checked"{/if} />*}
							{*<span class="hint" name="help_box" style="display:none">{l s='Please specify if the file can be shared.'}</span>*}
						{*</td>*}
					{*</tr>*}
				{else}
					<div class="hint clear" style="display: block;width: 70%;">{l s='You cannot edit your file here because you used combinations. Please edit this file in the Combinations tab.'}</div>
					<br />
					{if isset($error_product_download)}{$error_product_download}{/if}
				{/if}

Change it to:


			{if $product->productDownload->id}
				<input type="hidden" id="virtual_product_id" name="virtual_product_id" value="{$product->productDownload->id}" />
			{/if}
			<table cellpadding="5" style="float: left; margin-left: 10px;">
				<tr id="upload_input" {if $is_file}style="display:none"{/if}>
					<td class="col-left">
						<label id="virtual_product_file_label" for="virtual_product_file" class="t">{l s='Upload a file'}</label>
					</td>
					<td class="col-right">
						<input type="file" id="virtual_product_file" name="virtual_product_file" onchange="uploadFile();" maxlength="{$upload_max_filesize}" />
						<p class="preference_description">{l s='Your server\'s maximum file-upload size is'}:&nbsp;{$upload_max_filesize} {l s='MB'}</p>
					</td>
				</tr>
				<tr id="upload-error" style="display:none">
					<td colspan=2></td>
				</tr>
				<tr id="upload-confirmation" style="display:none">
					<td colspan=2>
						{if $up_filename}
							<input type="hidden" id="virtual_product_filename" name="virtual_product_filename" value="{$up_filename}" />
						{/if}
						<div class="conf">
						<script>
							delete_this_file = '{l s='Delete this file'}';
						</script>
							<a class="delete_virtual_product" id="delete_downloadable_product" href="{$currentIndex}&deleteVirtualProduct=true&token={$token}&id_product={$product->id}" class="red">
								<img src="../img/admin/delete.gif" alt="{l s='Delete this file'}"/>
							</a>
						</div>
					</td>
				</tr>
				{if $is_file}
					<tr>
						<td class="col-left">
							<input type="hidden" id="virtual_product_filename" name="virtual_product_filename" value="{$product->productDownload->filename}" />
							<label class="t">{l s='Link to the file:'}</label>
						</td>
						 <td class="col-right">
							{$product->productDownload->getHtmlLink(false, true)}
							<a href="{$currentIndex}&deleteVirtualProduct=true&token={$token}&id_product={$product->id}" class="red delete_virtual_product">
								<img src="../img/admin/delete.gif" alt="{l s='Delete this file'}"/>
							</a>
						</td>
					</tr>
				{/if}
				<tr>
					<td class="col-left">
						<label for="virtual_product_name" class="t">{l s='Filename'}</label>
					</td>
					<td class="col-right">
						<input type="text" id="virtual_product_name" name="virtual_product_name" style="width:200px" value="{$product->productDownload->display_filename|escape:'htmlall':'UTF-8'}" />
						<p class="preference_description" name="help_box">{l s='The full filename with its extension (e.g. Book.pdf)'}</p>
					</td>
				</tr>
				<tr>
					<td class="col-left">
						<label for="virtual_product_nb_downloable" class="t">{l s='Number of allowed downloads'}</label>
					</td>
					<td class="col-right">
						<input type="text" id="virtual_product_nb_downloable" name="virtual_product_nb_downloable" value="{$product->productDownload->nb_downloadable|htmlentities}" class="" size="6" />
						<p class="preference_description">{l s='Number of downloads allowed per customer. (Set to 0 for unlimited downloads)'}</p>
					</td>
				</tr>
				<tr>
					<td class="col-left">
						<label for="virtual_product_expiration_date" class="t">{l s='Expiration date'}</label>
					</td>
					<td class="col-right">
						<input class="datepicker" type="text" id="virtual_product_expiration_date" name="virtual_product_expiration_date" value="{$product->productDownload->date_expiration}" size="11" maxlength="10" autocomplete="off" /> {l s='Format: YYYY-MM-DD'}
						<p class="preference_description">{l s='If set, the file will not be downloadable after this date. Leave blank if you do not wish to attach an expiration date.'}</p>
					</td>
				</tr>
					<td class="col-left">
						<label for="virtual_product_nb_days" class="t">{l s='Number of days'}</label>
					</td>
					<td class="col-right">
						<input type="text" id="virtual_product_nb_days" name="virtual_product_nb_days" value="{$product->productDownload->nb_days_accessible|htmlentities}" class="" size="4" /><sup> *</sup>
						<p class="preference_description">{l s='Number of days this file can be accessed by customers'} - <em>({l s='Set to zero for unlimited access.'})</em></p>
					</td>
				</tr>
				{* Feature not implemented *}
				{*<tr>*}
					{*<td class="col-left">*}
						{*<label for="virtual_product_is_shareable" class="t">{l s='is shareable'}</label>*}
					{*</td>*}
					{*<td class="col-right">*}
						{*<input type="checkbox" id="virtual_product_is_shareable" name="virtual_product_is_shareable" value="1" {if $product->productDownload->is_shareable}checked="checked"{/if} />*}
						{*<span class="hint" name="help_box" style="display:none">{l s='Please specify if the file can be shared.'}</span>*}
					{*</td>*}
				{*</tr>*}

Explanation: we basically got rid of the if/else statement which was triggering the message, instead of displaying the virtual product form.

Save, and we are done!

Final note

Although we used overrides to ensure changes are preserved after an upgrade, remember to re-apply the admin-products.js fix, as we had to hardcode that one!.

Need Prestashop Modules? Have a look at my Prestashop Addons Store!

Looking for quality PrestaShop Web Hosting? Look no further than Arvixe Web Hosting!

Tags: , , , , , , , , , , , , | Posted under PrestaShop | RSS 2.0

Author Spotlight

Fabio Porta

Fabio Porta

Fabio has been involved in web development and design since 2005, when launched his first website at the age of 16. He’s now highly skilled in both client and server side development, along with design, and since August 2012 runs a successful website about PrestaShop tutorials and Prestashop Modules called Nemo’s Post Scriptum, at http://nemops.com

Leave a Reply

Your email address will not be published. Required fields are marked *


1 × 7 =

You may use these HTML tags and attributes: <a href="" title="" rel=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>