Prendre le contôle sur les prix affichés avec Drupal commerce

Téléviseur dans un champ

Drupal commerce dispose d'un nombre important de modules contribués permettant de modifier l'affichage du prix d'un produit sur une boutique en ligne. Le module Commerce price by components permet d'afficher ou non les différents composants d'un prix (prix, taxe, promotion, etc.), le module Commerce Price Savings Formatter permet d'afficher le prix original et le prix en promotion, indiquant le montant de la réduction, le module Commerce Price FlexyFormatter vous permet d'afficher le prix en sélectionnant les composants disponibles (taxe, promotion, prix de base, etc.), et d'afficher plusieurs prix différents selon les composants qui le constitueront. Au vu de ces quelques exemples (le tour d'horizon n'est pas exhaustif, loin s'en faut), nous disposons déjà d'une belle panoplie d'outils prêt à l'emploi pour modifier l'apparence des prix affichés sur un site e-commerce.

Mais si nous avons besoin par exemple d'afficher pour le prix d'un produit

  • Le prix de vente TTC (avec la TVA, et les éventuelles promotions)
  • Le prix de vente original TTC (si une promotion est appliquée sur le prix)
  • Le prix TTC avec une TVA alternative (ceci est particulièrement valable pour les produits éligibles à la TVA réduite si leur achat est réalisé en même temps qu'une prestation de pose)
  • Le prix HT du produit
  • Tout cela avec (ou non selon les cas) l'unité de vente du produit (m2, ml, unité, etc.)
  • Et un préfixe ou un suffixe selon les différents modes de vue du produit (typiquement Prix TTC : 10 € / m2 ou encore 10 € TTC / m2)
  • Et un commentaire pour l'application de la TVA réduite.

Avouez que nous risquons d'être un peu court avec les modules existants tant les besoins métier ici sont complexes et spécifiques. Découvrons comment prendre le contrôle total de l'affichage du prix de votre produit en créant notre propre formateur de champ qui sera en charge du rendu de celui-ci.

Le formateur de champ

Un formateur de champ nous permet d'indiquer à Drupal comment formater le contenu d'un champ afin de l'afficher sur la page. Le formateur de champ nous permet également de régler les différents paramètres d'affichages, que nous aurons nous même définis dans les options du formateur, depuis le backoffice.

Les options permettant d'afficher le prix d'un produit sont relativement limités de base avec drupal commerce.

Pour un rendu tout aussi simple et efficace, pour qui cela suffit.

 

Développement d'un formateur de champ sur mesure

Nous allons donc créer notre module qui implémentera notre formateur de champ. Nommons ce module Commerce price tax alternative et créons le fichier commerce_price_tax_alternative.info de déclaration du module qui contiendra les éléments ci-dessous, dans le répertoire sites/all/modules/commerce_price_tax_alternative (à créer) de votre site Drupal.

name = Commerce price tax alternative
description = Provides formater for commerce price
core = 7.x
dependencies[] = commerce
dependencies[] = commerce_price
dependencies[] = commerce_tax

core = "7.x"

Notons les dépendances que nous déclarons dans ce module qui aura besoin de Commerce, Commerce Price et Tax.

Nous allons créer le fichier commerce_price_tax_alternative.module qui contiendra le code du formateur de champ.

Pour créer un formateur de champs nous aurons besoin de cinq hook

  • hook_field_formatter_info(), qui va nous permettre de déclarer notre formateur à Drupal, ainsi que de définir toutes les options de paramétrages qui seront disponibles
  • hook_field_formatter_settings_form(), qui nous permettra de construire le formulaire de saisie des options de paramétrages
  • hook_field_formatter_settings_summary(), chargé d'afficher le récapitulatif des options de paramétrages sélectionnés dans le backoffice
  • hook_field_formatter_prepare_view(), pour préparer le rendu du champ, et permettre à d'autres modules de l'altérer si nécessaire
  • hook_field_formatter_view(), qui sera responsable de générer le rendu html du champ

Pour simplifier la lecture du billet, nous allégerons le code donné en exemple. Vous pourrez trouver l'intégralité du code source, permettant d'implémenter les différents cas de figure cités en introduction, sur la sandbox Commerce price tax alternative.

Déclaration du formateur de champ

Nous déclarons ici notre formateur de champ à Drupal dont le nom système est commerce_price_all_tax.

 /**
 * Implements hook_field_formatter_info().
 */
function commerce_price_tax_alternative_field_formatter_info() {
  return array(
    'commerce_price_all_tax' => array(
      'label' => t('Commerce price with all tax'),
      'field types' => array('commerce_price'),
      'settings' => array(
        'calculation' => TRUE,
        'show_price_calculated' => TRUE,
        'prefix_price_calculated' => '',
        'suffix_price_calculated' => '',
        'show_price_alternative' => FALSE,
        'prefix_price_alternative' => '',
        'suffix_price_alternative' => '',
        'tax_alternative' => NULL,
      ),
    ),
  );
} 

Que faisons-nous dans cette déclaration ? Nous affectons le formateur aux champs de type commerce_price, i.e. ce formateur ne sera disponible dans les options d'affichage que pour les champs de type Price, indiquons à Drupal Commerce que le champ doit être calculé ('calculation' => TRUE), avec l'application des différentes règles éventuelles (tax, discount, etc.), et enfin définissons nos propres options de configuration, ici en l'occurence le choix d'afficher le prix calculé et/ou le prix avec une taxe alternative, et la possibilité de paramétrer des préfixes et suffixes au prix.

Construction du formulaire

Nous implémentons désormais le formulaire pour nous permettre de saisir les options de configuration depuis le backffice.

 /**
 * Implements hook_field_formatter_settings_form().
 */
function commerce_price_tax_alternative_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $element = array();

  switch ($display['type']) {
    case 'commerce_price_all_tax':

      $element['calculation'] = array(
        '#type' => 'value',
        '#value' => TRUE,
      );

      $element['show_price_calculated'] = array(
        '#type' => 'checkbox',
        '#title' => t('Show calculated price'),
        '#default_value' => empty($settings['show_price_calculated']) ? 'TRUE' : $settings['show_price_calculated'],
      );

      $element['prefix_price_calculated'] = array(
        '#type' => 'textfield',
        '#title' => t('Prefix for price calculated'),
        '#default_value' => empty($settings['prefix_price_calculated']) ? '' : $settings['prefix_price_calculated'],
        '#states' => array(
          'invisible' => array(
            ':input[name="fields[commerce_price][settings_edit_form][settings][show_price_calculated]"]' => array('checked' => FALSE),
          ),
        ),
      );

      $element['suffix_price_calculated'] = array(
        '#type' => 'textfield',
        '#title' => t('Suffix for price calculated'),
        '#default_value' => empty($settings['suffix_price_calculated']) ? '' : $settings['suffix_price_calculated'],
        '#states' => array(
          'invisible' => array(
            ':input[name="fields[commerce_price][settings_edit_form][settings][show_price_calculated]"]' => array('checked' => FALSE),
          ),
        ),
      );

      $element['show_price_alternative'] = array(
        '#type' => 'checkbox',
        '#title' => t('Show the alternative price based on tax selected'),
        '#default_value' => empty($settings['show_price_alternative']) ? 'FALSE' : $settings['show_price_alternative'],
      );

      $element['prefix_price_alternative'] = array(
        '#type' => 'textfield',
        '#title' => t('Prefix for alternative price'),
        '#default_value' => empty($settings['prefix_price_alternative']) ? '' : $settings['prefix_price_alternative'],
        '#states' => array(
          'invisible' => array(
            ':input[name="fields[commerce_price][settings_edit_form][settings][show_price_alternative]"]' => array('checked' => FALSE),
          ),
        ),
      );

      $element['suffix_price_alternative'] = array(
        '#type' => 'textfield',
        '#title' => t('Suffix for alternative price'),
        '#default_value' => empty($settings['suffix_price_alternative']) ? '' : $settings['suffix_price_alternative'],
        '#states' => array(
          'invisible' => array(
            ':input[name="fields[commerce_price][settings_edit_form][settings][show_price_alternative]"]' => array('checked' => FALSE),
          ),
        ),
      );

      $tax_rates = commerce_tax_rates();
      $filter_tax = array();
      foreach ($tax_rates as $key => $value) {
        $tax_component  = $value['name'];
        $filter_tax[$tax_component] = $value['display_title'];
      }

      $element['tax_alternative'] = array(
        '#type' => 'select',
        '#title' => t('Alternative tax'),
        '#options' => $filter_tax,
        '#description' => t('Choose the alternative Tax to applied on price.'),
        '#default_value' => empty($settings['tax_alternative']) ? NULL : $settings['tax_alternative'],
        '#states' => array(
          'invisible' => array(
            ':input[name="fields[commerce_price][settings_edit_form][settings][show_price_alternative]"]' => array('checked' => FALSE),
          ),
        ),
      );

      break;

  }

  return $element;
} 

Pour chacune des options de configuration déclarées pour le formateur, nous construisons le formulaire correspondant, en indiquant le type du formulaire (case à cocher, champ texte, liste déroulante, etc.), sa valeur par défaut, l'affichage ou non des sous-options selon l'état de l'option principale. Toutes les options de la Form API de Drupal peuvent être utilisées.

Construction du sommaire du formateur de champ

Ce hook va nous permettre d'afficher un récapitulatif des options de configuration sélectionnées, sans devoir retrouner dans la configuration elle-même du formateur de champ.

 /**
 * Implements hook_field_formatter_settings_summary().
 */
function commerce_price_tax_alternative_field_formatter_settings_summary($field, $instance, $view_mode) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $summary = array();

  switch ($display['type']) {
    case 'commerce_price_all_tax':

      if ($settings['show_price_calculated'] == TRUE) {
        $summary[] = t('Show the calculated price. The prefix is : @prefix. The suffix is : @suffix.',
          array('@prefix' => $settings['prefix_price_calculated'], '@suffix' => $settings['suffix_price_calculated']));
      }

      if ($settings['show_price_alternative'] == TRUE) {
        $tax_rates = commerce_tax_rates();
        $index = $settings['tax_alternative'];
        $tax_name = $tax_rates[$index]['display_title'];
        $summary[] = t('Show the alternative price based on tax selected : @tax. The prefix is : @prefix. The suffix is : @suffix',
          array('@tax' => $tax_name, '@prefix' => $settings['prefix_price_alternative'], '@suffix' => $settings['suffix_price_alternative']));
      }

      break;

  }
  return implode('<br />', $summary);
} 

Nous récupérons ici tout simplement les options saisies par l'administrateur, pour les retourner sous la forme d'un markup HTML afin de disposer d'un récapitulatif des options de configuration accessible au niveau du backoffice, comme indiqué dans la capture ci-dessous.

Préparation et rendu du champ

Et enfin nous allons préparer et construire l'affichage de prix en fonction des options de configuration.

 /**
 * Implements hook_field_formatter_prepare_view().
 */
function commerce_price_tax_alternative_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
  // Allow other modules to prepare the item values prior to formatting.
  foreach (module_implements('commerce_price_field_formatter_prepare_view') as $module) {
    $function = $module . '_commerce_price_field_formatter_prepare_view';
    $function($entity_type, $entities, $field, $instances, $langcode, $items, $displays);
  }

} 

Puis notre formateur de champ proprement dit.

 /**
 * Implements hook_field_formatter_view().
 */
function commerce_price_tax_alternative_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $element = array();

  // Loop through each price value in this field.
  foreach ($items as $delta => $item) {
    // Theme the display of the price based on the display type.
    switch ($display['type']) {

      case 'commerce_price_all_tax':
        $tax_rates = commerce_tax_rates();
        $text_format = empty($display['settings']['text_format']) ? NULL : $display['settings']['text_format'];

        // Initialise variables
        $prices = array();
        $quantity_unit = '';
        $price_comment_tax = '';

        // Prepare the quantity unit
        if (!empty($display['settings']['show_quantity_unit']) && $display['settings']['show_quantity_unit'] != 'no_quantity_unit') {
          $field_name = $display['settings']['show_quantity_unit'];
          $wrapper = entity_metadata_wrapper('commerce_product', $entity);
          $quantity_unit = $wrapper->{$field_name}->name->value();
        }

        // The comment on tax alternative
        if (!empty($display['settings']['price_comment_tax_alternative']) && $display['settings']['show_price_alternative'] == TRUE) {
          $price_comment_tax = check_markup($display['settings']['price_comment_tax_alternative'], $text_format);
        }

        if ($display['settings']['show_price_calculated'] == TRUE) {
          $discounted = commerce_price_tax_alternative_check_if_price_is_discounted($item);
          $prefix_replace = !empty($display['settings']['prefix_price_calculated_with_discount']) ?
            check_plain($display['settings']['prefix_price_calculated_with_discount']) : check_plain($display['settings']['prefix_price_calculated']);
          $prices['calculated']['price'] = commerce_currency_format($item['amount'], $item['currency_code'], $entity);
          $prices['calculated']['prefix'] = ($discounted & $display['settings']['show_price_without_discount']) ? $prefix_replace : check_plain($display['settings']['prefix_price_calculated']);
          $prices['calculated']['suffix'] = check_plain($display['settings']['suffix_price_calculated']);
          $prices['calculated']['classes_array'][] = ($discounted) ? 'discounted' : '';
        }

        // Display the alternative price
        if (!empty($display['settings']['show_price_alternative'])) {
          $tax_alternative = $display['settings']['tax_alternative'];
          $tax_rate = $tax_rates[$tax_alternative];

          // Copy the item in new variable
          $price = $item;

          // Apply the alternative tax
          $price = commerce_price_tax_alternative_apply_alternative_tax($price, $tax_rate);

          $prices['alternative']['price'] = commerce_currency_format($price['amount'], $price['currency_code'], $entity);
          $prices['alternative']['prefix'] = check_plain($display['settings']['prefix_price_alternative']);
          $prices['alternative']['suffix'] = check_plain($display['settings']['suffix_price_alternative']);
          $prices['alternative']['classes_array'] = array();
        }
       
        $element[$delta] = array(
          '#markup' => theme('commerce_price_all_tax', array(
            'prices' => $prices,
            'quantity_unit' => $quantity_unit,
            'price_comment_tax' => $price_comment_tax,
            'price_original' => $item
            )
          ),
        );

        break;

    }
  }

  return $element;
} 

A noter que nous utilisons une fonction de thème pour construire le résultat, fonction à laquelle nous passons les différentes valeurs construites depuis les options de configurations. Nous créons cette fonction de thème en implémentant hook_theme() et pour laquelle nous lui déclarons les variables attendues et qui pourront par la suite être exploitées (à noter que les variables disponibles et passées à la fonction de thème sont plus nombreuses que dans les exemples ci-dessus qui ont été simplifiés pour l'occasion).

 /**
 * Implements hook_theme().
 */
function commerce_price_tax_alternative_theme() {
  return array(
    'commerce_price_all_tax' => array(
      'variables' => array('prices' => array(), 'quantity_unit' => '', 'price_comment_tax' => '', 'price_original' => array()),
    ),
  );
} 

Et la fonction de thème chargée du rendu final

/**
 * Themes a price with multiple tax.
 *
 * @param $variables
 *   Includes the 'components' array and original 'price' array as well as comment and quantity unit.
 */
function theme_commerce_price_all_tax($variables) {
  drupal_add_css(drupal_get_path('module', 'commerce_price_tax_alternative') . '/commerce_price_tax_alternative.css', array('group' => CSS_DEFAULT));
  $markup = '';

  foreach ($variables['prices'] as $key => $value) {
    $key = drupal_html_class($key);
    $classes = implode(' ', $value['classes_array']);

    $markup .= '<div class="wrapper-price-' . $key . ' ' . $classes . '">';
    $markup .= '<span class="prefix-' . $key . '">' . $value['prefix'] . '</span>';
    $markup .= '<span class="price-' . $key . '">' . $value['price'] . '</span>';
    $markup .= '<span class="suffix-' . $key . '">' . $value['suffix'] . '</span>';
    $markup .= '</div>';
  }


  return $markup;
}

L'intérêt d'utiliser une fonction de thème est qu'elle peut être surchargée au niveau du thème, et qu'on peut donc altérer son rendu sans modifier le module source.

Conclusion

En déclarant quelques HOOK, et avec quelques lignes de codes, nous sommes maintenant en mesure de contôler de manière très fine le rendu des prix de nos produits, avec une configuration depuis le backoffice qui permet de jouer simplement avec les paramètres pour en modifier le rendu selon les modes d'affichages des produits.

Et le résultat final

Vous pouvez retrouver l'intégralité du code source de ce tutoriel sur la sandbox Commerce price tax alternative. Vous souhaitez des précisions ? N'hésitez pas à les demander dans les commentaires.

 

Ajouter un commentaire