Site icon Freelance Expert PrestaShop – WordPress – WooCommerce : Arnaud Merigeau

[PrestaShop 1.7] Ajouter des champs texte aux produits

Aujourd’hui, nouveau tuto PrestaShop pour ajouter un champ aux produits pour ensuite l’utiliser dans vos intégrations front office ou bien dans des développements admin.

Constat : besoins spécifiques pour certains e-commerçants

Nombreux sont les projets où mes clients me demandent d’ajouter des données pour leurs produits sans pouvoir utiliser les champs disponibles par défaut. Voici le type de cas où je préfère utiliser des champs personnalisés pour les produits :

Dans ces cas précis, ce tutoriel PrestaShop risque combler vos attentes.
Ah mais attends, ce tuto PrestaShop va aussi vous faire gagner de l’argent.
Mais ça on va le voir plus loin.

Tuto PrestaShop : ajouter un champ pour les produits

C’est parti pour résoudre le cas numéro 1. Le tuto PrestaShop se déroule en 5 étapes assez simples vous allez voir. Penchons-nous d’abord sur les 4 premières étapes :

1. Installer le module AmProductFields

[Edit du 06/02/2020] J’ai modifié le module pour que les 2 champs texte et zone de texte soient compatibles avec le multilingue. Pour info, il manquait la class translation-field sur les champs. C’est à présent corrigé 🙂 La version 1.1.2 du module est donc disponible au téléchargement pour ajouter un champ texte au produit sous PrestaShop 1.7 !
Téléchargez le module PrestaShop AmProductFields v1.1 ci-dessous (module modifié issu du bon travail de Hervé Hennes dans cet article) puis installez-le simplement via le back office de PrestaShop. Aucune configuration n’est nécessaire, facile donc !
[email-download-link namefield= »YES » id= »4″]

2. Installer l’override Product.php

Pour vous simplifiez la tâche, j’ai compris dans le dossier du module le fichier override. Déplacez le fichier www/modules/amproductfields/doc_and_override/Product.php vers www/override/classes/ mais attention si vous avez déjà un fichier Product.php : dans ce cas, copiez le contenu du fichier que je fournis et adaptez-le avec le fichier Product.php déjà existant dans le dossier override/classes.
Supprimez le fichier de cache suivant : www/app/cache/prod/class_index.php

3. Rédigez vos contenus

Pour la base de départ, j’ai prévu 3 champs dont :

Rédigez vos contenus comme bon vous semble afin d’enrichir vos fiches produits.

4. Affichez vos contenus

Pour ma part, j’ai voulu afficher les informations saisies sur la fiche produit, sous les images, car par défaut le thème Classic offre de la place à cet endroit. On peut donc facilement rendre visible de l’information importante !
Ouvrez le fichier www/themes/classic/templates/catalog/product.tpl et ajoutez les lignes 74 à 77 :

{**
* 2007-2017 PrestaShop
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (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:
* https://opensource.org/licenses/AFL-3.0
* 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>
* @copyright 2007-2017 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*}
{extends file=$layout}

{block name='head_seo' prepend}
<link rel="canonical" href="{$product.canonical_url}">
{/block}

{block name='head' append}
<meta property="og:type" content="product">
<meta property="og:url" content="{$urls.current_url}">
<meta property="og:title" content="{$page.meta.title}">
<meta property="og:site_name" content="{$shop.name}">
<meta property="og:description" content="{$page.meta.description}">
<meta property="og:image" content="{$product.cover.large.url}">
<meta property="product:pretax_price:amount" content="{$product.price_tax_exc}">
<meta property="product:pretax_price:currency" content="{$currency.iso_code}">
<meta property="product:price:amount" content="{$product.price_amount}">
<meta property="product:price:currency" content="{$currency.iso_code}">
{if isset($product.weight) && ($product.weight != 0)}
<meta property="product:weight:value" content="{$product.weight}">
<meta property="product:weight:units" content="{$product.weight_unit}">
{/if}
{/block}

{block name='content'}

<section id="main" itemscope itemtype="https://schema.org/Product">
<meta itemprop="url" content="{$product.url}">

<div class="row">
<div class="col-md-6">
{block name='page_content_container'}
<section class="page-content" id="content">
{block name='page_content'}
{block name='product_flags'}
<ul class="product-flags">
{foreach from=$product.flags item=flag}
<li class="product-flag {$flag.type}">{$flag.label}</li>
{/foreach}
</ul>
{/block}

{block name='product_cover_thumbnails'}
{include file='catalog/_partials/product-cover-thumbnails.tpl'}
{/block}
<div class="scroll-box-arrows">
<i class="material-icons left"></i>
<i class="material-icons right"></i>
</div>

<section id="amproductfields">
<h2>{$product.custom_field_lang}</h2>
<div>{$product.custom_field_lang_wysiwyg nofilter}</div>
</section>

{/block}
</section>
{/block}
</div>
<div class="col-md-6">
{block name='page_header_container'}
{block name='page_header'}
<h1 class="h1" itemprop="name">{block name='page_title'}{$product.name}{/block}</h1>
{/block}
{/block}
{block name='product_prices'}
{include file='catalog/_partials/product-prices.tpl'}
{/block}

<div class="product-information">
{block name='product_description_short'}
<div id="product-description-short-{$product.id}" itemprop="description">{$product.description_short nofilter}</div>
{/block}

{if $product.is_customizable && count($product.customizations.fields)}
{block name='product_customization'}
{include file="catalog/_partials/product-customization.tpl" customizations=$product.customizations}
{/block}
{/if}

<div class="product-actions">
{block name='product_buy'}
<form action="{$urls.pages.cart}" method="post" id="add-to-cart-or-refresh">
<input type="hidden" name="token" value="{$static_token}">
<input type="hidden" name="id_product" value="{$product.id}" id="product_page_product_id">
<input type="hidden" name="id_customization" value="{$product.id_customization}" id="product_customization_id">

{block name='product_variants'}
{include file='catalog/_partials/product-variants.tpl'}
%2<br />
0 {/block}

{block name='product_pack'}
{if $packItems}
<section class="product-pack">
<h3 class="h4">{l s='This pack contains' d='Shop.Theme.Catalog'}</h3>
{foreach from=$packItems item="product_pack"}
{block name='product_miniature'}
{include file='catalog/_partials/miniatures/pack-product.tpl' product=$product_pack}
{/block}
{/foreach}
</section>
{/if}
{/block}

{block name='product_discounts'}
{include file='catalog/_partials/product-discounts.tpl'}
{/block}

{block name='product_add_to_cart'}
{include file='catalog/_partials/product-add-to-cart.tpl'}
{/block}

{block name='product_additional_info'}
{include file='catalog/_partials/product-additional-info.tpl'}
{/block}

{block name='product_refresh'}
<input class="product-refresh ps-hidden-by-js" name="refresh" type="submit" value="{l s='Refresh' d='Shop.Theme.Actions'}">
{/block}
</form>
{/block}

</div>

{block name='hook_display_reassurance'}
{hook h='displayReassurance'}
{/block}
</div>
</div>
</div>

<div id="product_tabs" class="row">
<div class="col-xs-12">
{block name='product_tabs'}
<div class="tabs">
<ul class="nav nav-tabs" role="tablist">
{if $product.description}
<li class="nav-item">
<a
class="nav-link{if $product.description} active{/if}"
data-toggle="tab"
href="#description"
role="tab"
aria-controls="description"
{if $product.description} aria-selected="true"{/if}>{l s='Description' d='Shop.Theme.Catalog'}</a>
</li>
{/if}
<li class="nav-item">
<a
class="nav-link{if !$product.description} active{/if}"
data-toggle="tab"
href="#product-details"
role="tab"
aria-controls="product-details"
{if !$product.description} aria-selected="true"{/if}>{l s='Product Details' d='Shop.Theme.Catalog'}</a>
</li>
{if $product.attachments}
<li class="nav-item">
<a
class="nav-link"
data-toggle="tab"
href="#attachments"
role="tab"
aria-controls="attachments">{l s='Attachments' d='Shop.Theme.Catalog'}</a>
</li>
{/if}
{foreach from=$product.extraContent item=extra key=extraKey}
<li class="nav-item">
<a
class="nav-link"
data-toggle="tab"
href="#extra-{$extraKey}"
role="tab"
aria-controls="extra-{$extraKey}">{$extra.title}</a>
</li>
{/foreach}
</ul>

<div class="tab-content" id="tab-content">
<div class="tab-pane fade in{if $product.description} active{/if}" id="description" role="tabpanel">
{block name='product_description<br />
'}
<div class="product-description">{$product.description nofilter}</div>
{/block}
</div>

{block name='product_details'}
{include file='catalog/_partials/product-details.tpl'}
{/block}

{block name='product_attachments'}
{if $product.attachments}
<div class="tab-pane fade in" id="attachments" role="tabpanel">
<section class="product-attachments">
<h3 class="h5 text-uppercase">{l s='Download' d='Shop.Theme.Actions'}</h3>
{foreach from=$product.attachments item=attachment}
<div class="attachment">
<h4><a href="{url entity='attachment' params=['id_attachment' => $attachment.id_attachment]}">{$attachment.name}</a></h4>
<p>{$attachment.description}</p
<a href="{url entity='attachment' params=['id_attachment' => $attachment.id_attachment]}">
{l s='Download' d='Shop.Theme.Actions'} ({$attachment.file_size_formatted})
</a>
</div>
{/foreach}
</section>
</div>
{/if}
{/block}

{foreach from=$product.extraContent item=extra key=extraKey}
<div class="tab-pane fade in {$extra.attr.class}" id="extra-{$extraKey}" role="tabpanel" {foreach $extra.attr as $key => $val} {$key}="{$val}"{/foreach}>
{$extra.content nofilter}
</div>
{/foreach}
</div>
</div>
{/block}
</div>
</div>

{block name='product_accessories'}
{if $accessories}
<section class="product-accessories clearfix">
<h3 class="h5 text-uppercase">{l s='You might also like' d='Shop.Theme.Catalog'}</h3>
<div class="products">
{foreach from=$accessories item="product_accessory"}
{block name='product_miniature'}
{include file='catalog/_partials/miniatures/product.tpl' product=$product_accessory}
{/block}
{/foreach}
</div>
</section>
{/if}
{/block}

{block name='product_footer'}
{hook h='displayFooterProduct' product=$product category=$category}
{/block}

{block name='product_images_modal'}
{include file='catalog/_partials/product-images-modal.tpl'}
{/block}

{block name='page_footer_container'}
<footer class="page-footer">
{block name='page_footer'}
<!-- Footer content -->
{/block}
</footer>
{/block}
</section>

{/block}

On affiche alors les 2 champs traduisibles dans une <section> avec un id dans le cas où vous auriez besoin de donner un style particulier à l’élément. J’ai aussi encadré d’un <h2> le premier champ pour avoir un code optimisé pour le référencement.

Comment afficher les champs sur la page panier ?

[Edit du 21/07/2018] J’ai reçu plusieurs commentaires à ce sujet c’est pourquoi j’ai décidé de mettre à jour cet article. Je vais vous expliquer comment afficher les nouveaux champs de la fiche produit sur la page panier. Vous allez voir que c’est assez simple finalement !
J’ouvre d’abord le fichier www/classes/Cart.php et je copie la fonction getProducts dans un override que je place à cet endroit : www/override/classes/Cart.php.
Il faut ajouter les deux nouveaux champs dans la requête SQL SELECT ce qui donne ceci si on affiche la fonction simplement :

/**
* Return cart products
*
* @param bool $refresh
* @param bool $id_product
* @param int $id_country
* @param bool $fullInfos
*
* @return array Products
*/
public function getProducts($refresh = false, $id_product = false, $id_country = null, $fullInfos = true)
{
if (!$this->id) {
return array();
}
// Product cache must be strictly compared to NULL, or else an empty cart will add dozens of queries
if ($this->_products !== null && !$refresh) {
// Return product row with specified ID if it exists
if (is_int($id_product)) {
foreach ($this->_products as $product) {
if ($product['id_product'] == $id_product) {
return array($product);
}
}
return array();
}
return $this->_products;
}

// Build query
$sql = new DbQuery();

// Build SELECT
$sql->select('cp.`id_product_attribute`, cp.`id_product`, cp.`quantity` AS cart_quantity, cp.id_shop, cp.`id_customization`, pl.`name`, p.`is_virtual`,
pl.`description_short`, pl.`available_now`, pl.`available_later`, product_shop.`id_category_default`, p.`id_supplier`,
p.`id_manufacturer`, m.`name` AS manufacturer_name, product_shop.`on_sale`, product_shop.`ecotax`, product_shop.`additional_shipping_cost`,
product_shop.`available_for_order`, product_shop.`show_price`, product_shop.`price`, product_shop.`active`, product_shop.`unity`, product_shop.`unit_price_ratio`,
stock.`quantity` AS quantity_available, p.`width`, p.`height`, p.`depth`, stock.`out_of_stock`, p.`weight`,
p.`available_date`, p.`date_add`, p.`date_upd`, IFNULL(stock.quantity, 0) as quantity, pl.`link_rewrite`, cl.`link_rewrite` AS category, pl.`custom_field_lang_wysiwyg`, pl.`custom_field_lang`,
CONCAT(LPAD(cp.`id_product`, 10, 0), LPAD(IFNULL(cp.`id_product_attribute`, 0), 10, 0), IFNULL(cp.`id_address_delivery`, 0), IFNULL(cp.`id_customization`, 0)) AS unique_id, cp.id_address_delivery,
product_shop.advanced_stock_management, ps.product_supplier_reference supplier_reference');

// Build FROM
$sql->from('cart_product', 'cp');

// Build JOIN
$sql->leftJoin('product', 'p', 'p.`id_product` = cp.`id_product`');
$sql->innerJoin('product_shop', 'product_shop', '(product_shop.`id_shop` = cp.`id_shop` AND product_shop.`id_product` = p.`id_product`)');
$sql->leftJoin(
'product_lang',
'pl',
'p.`id_product` = pl.`id_product`
AND pl.`id_lang` = '.(int)$this->id_lang.Shop::addSqlRestrictionOnLang('pl', 'cp.id_shop')
);

$sql->leftJoin(
'category_lang',
'cl',
'product_shop.`id_category_default` = cl.`id_category`
AND cl.`id_lang` = '.(int)$this->id_lang.Shop::addSqlRestrictionOnLang('cl', 'cp.id_shop')
);

$sql->leftJoin('product_supplier', 'ps', 'ps.`id_product` = cp.`id_product` AND ps.`id_product_attribute` = cp.`id_product_attribute` AND ps.`id_supplier` = p.`id_supplier`');
$sql->leftJoin('manufacturer', 'm', 'm.`id_manufacturer` = p.`id_manufacturer`');

// @todo test if everything is ok, then refactorise call of this method
$sql->join(Product::sqlStock('cp', 'cp'));

// Build WHERE clauses
$sql->where('cp.`id_cart` = '.(int)$this->id);
if ($id_product) {
$sql->where('cp.`id_product` = '.(int)$id_product);
}
$sql->where('p.`id_product` IS NOT NULL');

// Build ORDER BY
$sql->orderBy('cp.`date_add`, cp.`id_product`, cp.`id_product_attribute` ASC');

if (Customization::isFeatureActive()) {
$sql->select('cu.`id_customization`, cu.`quantity` AS customization_quantity');
$sql->leftJoin(
'customization',
'cu',
'p.`id_product` = cu.`id_product` AND cp.`id_product_attribute` = cu.`id_product_attribute` AND cp.`id_customization` = cu.`id_customization` AND cu.`id_cart` = '.(int)$this->id
);
$sql->groupBy('cp.`id_product_attribute`, cp.`id_product`, cp.`id_shop`, cp.`id_customization`');
} else {
$sql->select('NULL AS customization_quantity, NULL AS id_customization');
}

if (Combination::isFeatureActive()) {
$sql->select('
product_attribute_shop.`price` AS price_attribute, product_attribute_shop.`ecotax` AS ecotax_attr,
IF (IFNULL(pa.`reference`, \'\') = \'\', p.`reference`, pa.`reference`) AS reference,
(p.`weight`+ pa.`weight`) weight_attribute,
IF (IFNULL(pa.`ean13`, \'\') = \'\', p.`ean13`, pa.`ean13`) AS ean13,
IF (IFNULL(pa.`isbn`, \'\') = \'\', p.`isbn`, pa.`isbn`) AS isbn,
IF (IFNULL(pa.`upc`, \'\') = \'\', p.`upc`, pa.`upc`) AS upc,
IFNULL(product_attribute_shop.`minimal_quantity`, product_shop.`minimal_quantity`) as minimal_quantity,
IF(product_attribute_shop.wholesale_price > 0, product_attribute_shop.wholesale_price, prod<br />
uct_shop.`wholesale_price`) wholesale_price
');

$sql->leftJoin('product_attribute', 'pa', 'pa.`id_product_attribute` = cp.`id_product_attribute`');
$sql->leftJoin('product_attribute_shop', 'product_attribute_shop', '(product_attribute_shop.`id_shop` = cp.`id_shop` AND product_attribute_shop.`id_product_attribute` = pa.`id_product_attribute`)');
} else {
$sql->select(
'p.`reference` AS reference, p.`ean13`, p.`isbn`,
p.`upc` AS upc, product_shop.`minimal_quantity` AS minimal_quantity, product_shop.`wholesale_price` wholesale_price'
);
}

$sql->select('image_shop.`id_image` id_image, il.`legend`');
$sql->leftJoin('image_shop', 'image_shop', 'image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop='.(int)$this->id_shop);
$sql->leftJoin('image_lang', 'il', 'il.`id_image` = image_shop.`id_image` AND il.`id_lang` = '.(int)$this->id_lang);

$result = Db::getInstance()->executeS($sql);

// Reset the cache before the following return, or else an empty cart will add dozens of queries
$products_ids = array();
$pa_ids = array();
if ($result) {
foreach ($result as $key => $row) {
$products_ids[] = $row['id_product'];
$pa_ids[] = $row['id_product_attribute'];
$specific_price = SpecificPrice::getSpecificPrice($row['id_product'], $this->id_shop, $this->id_currency, $id_country, $this->id_shop_group, $row['cart_quantity'], $row['id_product_attribute'], $this->id_customer, $this->id);
if ($specific_price) {
$reduction_type_row = array('reduction_type' => $specific_price['reduction_type']);
} else {
$reduction_type_row = array('reduction_type' => 0);
}

$result[$key] = array_merge($row, $reduction_type_row);
}
}
// Thus you can avoid one query per product, because there will be only one query for all the products of the cart
Product::cacheProductsFeatures($products_ids);
Cart::cacheSomeAttributesLists($pa_ids, $this->id_lang);

$this->_products = array();
if (empty($result)) {
return array();
}

if ($fullInfos) {
$ecotax_rate = (float)Tax::getProductEcotaxRate($this->{Configuration::get('PS_TAX_ADDRESS_TYPE')});
$apply_eco_tax = Product::$_taxCalculationMethod == PS_TAX_INC && (int)Configuration::get('PS_TAX');
$cart_shop_context = Context::getContext()->cloneContext();

$gifts = $this->getCartRules(CartRule::FILTER_ACTION_GIFT);
$givenAwayProductsIds = array();

if ($this->shouldSplitGiftProductsQuantity && count($gifts) > 0) {
foreach ($gifts as $gift) {
foreach ($result as $rowIndex => $row) {
if (!array_key_exists('is_gift', $result[$rowIndex])) {
$result[$rowIndex]['is_gift'] = false;
}

if (
$row['id_product'] == $gift['gift_product'] &&
$row['id_product_attribute'] == $gift['gift_product_attribute']
) {
$row['is_gift'] = true;
$result[$rowIndex] = $row;
}
}

$index = $gift['gift_product'] . '-' . $gift['gift_product_attribute'];
if (!array_key_exists($index, $givenAwayProductsIds)) {
$givenAwayProductsIds[$index] = 1;
} else {
$givenAwayProductsIds[$index]++;
}
}
}

foreach ($result as &$row) {
if (!array_key_exists('is_gift', $row)) {
$row['is_gift'] = false;
}

$additionalRow = Product::getProductProperties((int)$this->id_lang, $row);
$row['reduction'] = $additionalRow['reduction'];
$row['price_without_reduction'] = $additionalRow['price_without_reduction'];
$row['specific_prices'] = $additionalRow['specific_prices'];
unset($additionalRow);

$givenAwayQuantity = 0;
$giftIndex = $row['id_product'] . '-' . $row['id_product_attribute'];
if ($row['is_gift'] && array_key_exists($giftIndex, $givenAwayProductsIds)) {
$givenAwayQuantity = $givenAwayProductsIds[$giftIndex];
}

if (!$row['is_gift'] || (int)$row['cart_quantity'] === $givenAwayQuantity) {
$row = $this->applyProductCalculations($row, $cart_shop_context);
} else {
// Separate products given away from those manually added to cart
%2<br />
0 $this->_products[] = $this->applyProductCalculations($row, $cart_shop_context, $givenAwayQuantity);
unset($row['is_gift']);
$row = $this->applyProductCalculations(
$row,
$cart_shop_context,
$row['cart_quantity'] - $givenAwayQuantity
);
}

$this->_products[] = $row;
}
} else {
$this->_products = $result;
}

return $this->_products;
}

Notez l’ajout de pl.custom_field_lang_wysiwyg, pl.custom_field_lang, dans la requête SQL.
Une fois que c’est modifié, supprimez le fichier de cache suivant : www/app/cache/prod/class_index.php
Enfin, vous pouvez afficher les nouveaux champs dans le fichier www/prestashop/themes/mon_theme/templates/checkout/_partials/cart-detailed-product-line.tpl.tpl en utilisant :

<section id="amproductfields">
<h2>{$product.custom_field_lang}</h2>
<div>{$product.custom_field_lang_wysiwyg nofilter}</div>
</section>

 

Comment gagner de l’argent avec ce tuto PrestaShop ?

La réponse est assez simple vous allez voir.
Aujourd’hui, vous savez à présent comment afficher de nouveaux champs sur vos fiches produits. Très bien, et alors ??
Et bien, vous allez donc pouvoir proposer une fiche produit avec les infos clés en haut de page et un bouton d’ajout panier visible même sur les écrans de taille moyenne.

Enfin, vous allez pouvoir travailler votre référencement en ajoutant du contenu à vos fiches produits.
Je peux conseiller le format suivant :

Le résultat :

Vous avez un bel outil aujourd’hui pour développer votre business alors faites en bon usage 😉

Module PrestaShop : ACF – Advanced Custom Fields for Prestashop

Mise à jour de l’article [PrestaShop 1.7] Ajouter des champs texte aux produits du 14 septembre 2021

J’ai reçu un bon nombre de demandes depuis la création de cet article qui présent une solution pour ajouter un champ à la fiche produit.

Cependant certains d’entre vous me disent : « Je voudrais ajouter du texte et des options avec des cases à cocher » !

Ce à quoi je répond : « Hum le module que je propose gratuitement ne fait malheureusement pas ça mais … »

« Mais quoi ?! »

« Mais il existe une solution… »

Cette solution c’est un petit module qui va vous permettre d’avoir un module comme ACF pour WordPress pour ceux qui connaissent : un vrai bijou donc !

Vous pouvez avec ce module ajouter 9 types de champs :

  1. text
  2. textarea
  3. WYSIWYG editor (text – html – images – videos)
  4. switch (les fameux oui / non)
  5. checkbox
  6. checklist
  7. select
  8. radio
  9. date

Parmi les autres caractéristiques, voici celles qui vont vous faire craquer :

Mais plutôt qu’un long discours, voici des captures écrans pour illustrer mes propos :

Lien du module que j’utilise pour de nombreux projets (30$) : Advanced Custom Fields for Prestashop

A bientôt pour un nouvel article !


Quitter la version mobile