Appliquer un taux de TVA à un produit avec Drupal commerce 2

Une calculatrice

Drupal commerce 2 permet désormais nativement de gérer les différentes taxes et TVA à appliquer sur une boutique en ligne, quelque soit son pays et leurs règles respectives en cette matière. La plupart des modules contribués permettant de gérer ces éléments sur Commerce 1.x ne sont donc plus nécessaires. Découvrons comment utiliser le concept des Resolver de Drupal commerce 2.x pour définir le taux de TVA à appliquer sur différents produits.

Création d'un type de Taxe

Avant de déterminer quel taux de TVA nous souhaitons appliquer sur tel ou tel type de produits, nous devons dans un premier temps configurer le type de taxe qui sera disponible pour notre boutique en ligne.

Drupal commerce intègre désormais dans son coeur toute la gestion et détection des taxes. Et il intègre également des types de taxes déjà paramétrées, telle que la TVA concernant l'Union Européenne, et notamment les différents taux applicables par pays.

Drupal commerce TVA en France

Ainsi l'ajout d'une taxe se fait en quelques clics. Il suffit de créer un type de taxe, et de sélectionner le Plugin correspondant à la TVA européenne (dans notre cas de figure), et le tour est joué.

Création d'un type de taxe

Et pour déterminer quels seront les taux de TVA applicables à la boutique en ligne, il suffit de paramétrer votre boutique en ligne en lui spécifiant pour quel pays il faudra appliquer les règles de taxes.

Drupal commerce store tax settings

Ici en spécifiant la France, les taux de TVA qui seront automatiquement associés à la boutique en ligne seront ceux de ce même pays. Après cette très brève introduction sur le paramétrage initial d'une boutique en ligne Drupal commerce 2.x, venons en au coeur du sujet à savoir la détermination du taux de TVA à appliquer en fonction du type de produit.

Le concept des Resolver de Drupal commerce 2.x

Drupal commerce 2.x utilise de façon massive le concept de Resolver, qui ne sont ni plus ni moins que des collecteurs de services. Pour déterminer de façon dynamique (et aussi très facilement altérable) la taxe applicable à un produit, le type de commande correspondant à un produit, le type de tunnel d'achat correspondant à une commande, le prix correspondant à un produit, etc.

Bref pour chacune de ces actions / réactions nous disposons d'un collecteur de service qui va collecter tous les services d'un certain tag, ordonnés par priorité puis les tester un par un, jusqu'à ce qu'un service retourne une valeur. Et si tout simplement aucun service n'est présent (ou ne retourne pas de valeur), alors le Resolver implémenté par défaut par le coeur de Drupal commerce jouera son rôle.

Ainsi chaque service Resolver déclaré doit implémenter une méthode obligatoire resolve(), que le collecteur de service (ou le ChainTaxResolver) va évaluer.

/**
 * {@inheritdoc}
 */
public function resolve(TaxZone $zone, OrderItemInterface $order_item, ProfileInterface $customer_profile) {
  $result = NULL;
  foreach ($this->resolvers as $resolver) {
    $result = $resolver->resolve($zone, $order_item, $customer_profile);
    if ($result) {
      break;
    }
  }

  return $result;
}

Et pour ce qui concerne le taux de TVA, Drupal commerce implémente donc un Resolver de taxe par défaut au moyen de ce service dont la priorité est réglée à -100.

services:
  commerce_tax.default_tax_rate_resolver:
    class: Drupal\commerce_tax\Resolver\DefaultTaxRateResolver
    tags:
      - { name: commerce_tax.tax_rate_resolver, priority: -100 }

Ce service détermine le taux de taxe de façon assez simple.

/**
 * Returns the tax zone's default tax rate.
 */
class DefaultTaxRateResolver implements TaxRateResolverInterface {

  /**
   * {@inheritdoc}
   */
  public function resolve(TaxZone $zone, OrderItemInterface $order_item, ProfileInterface $customer_profile) {
    $rates = $zone->getRates();
    // Take the default rate, or fallback to the first rate.
    $resolved_rate = reset($rates);
    foreach ($rates as $rate) {
      if ($rate->isDefault()) {
        $resolved_rate = $rate;
        break;
      }
    }
    return $resolved_rate;
  }

}

Il retourne tout simplement un taux de taxe qui est défini par défaut par le Plugin de taxe correspondant, ou si aucun taux n'est défini par défaut, tout simplement le premier taux de ceux correspondant à la zone géographique.

C'est une règle de base, qui peut très facilement suffire à une solution e-commerce  qui ne vend que des produits dont la TVA applicable est celle par défaut du Pays concerné.

Mais si plusieurs taux de TVA sont éligibles en fonction du produit (Taux de TVA réduit pour des prestations de services, Taux de TVA correspondant aux produits culturels, etc.), alors nous pouvons très facilement déterminer quel taux de TVA appliquer selon des règles qui peuvent s'appuyer sur n'importe quel attribut du produit concerné, ou du profil du client. 

Déterminer un taux de TVA avec Drupal Commerce

Pour faire varier un taux de TVA applicable en fonction d'un produit, ou d'un type de produit, il nous suffit donc d'implémenter un service qui déclarera le tag commerce_tax.tax_rate_resolver avec une priorité au moins supérieur au Resolver par défaut fourni par Drupal Commerce.

Déclarons ce service.

services:
  my_module.product_tax_resolver:
    class: Drupal\my_module\Resolver\ProductTaxResolver
    tags:
      - { name: commerce_tax.tax_rate_resolver, priority: 100 }

Nous lui donnons une priorité de 100 afin qu'il soit évalué avant le Resolver par défaut.

Puis nous pouvons alors évaluer le taux de taxe à appliquer au moyen de la méthode resolve() de notre service.

<?php

namespace Drupal\my_module\Resolver;

use Drupal\commerce_product\Entity\ProductInterface;
use Drupal\commerce_product\Entity\ProductVariationInterface;
use Drupal\commerce_order\Entity\OrderItemInterface;
use Drupal\commerce_tax\Resolver\TaxRateResolverInterface;
use Drupal\commerce_tax\TaxZone;
use Drupal\profile\Entity\ProfileInterface;

/**
 * Returns the tax zone's default tax rate.
 */
class ProductTaxResolver implements TaxRateResolverInterface {

  /**
   * {@inheritdoc}
   */
  public function resolve(TaxZone $zone, OrderItemInterface $order_item, ProfileInterface $customer_profile) {
    $rates = $zone->getRates();

    // Get the purchased entity.
    $item = $order_item->getPurchasedEntity();
    
    // Get the corresponding product.
    $product = $item->getProduct();

    $product_type = $product->bundle();

    // Take a rate depending on the product type.
    switch ($product_type) {
      case 'book':
        $rate_id = 'reduced';
        break;
      default:
        // The rate for other product type can be resolved using the default tax
        // rate resolver.
        return NULL;
    }

    foreach ($rates as $rate) {
      if ($rate->getId() == $rate_id) {
        return $rate;
      }
    }

    // If no rate has been found, let's others resolvers try to get it.
    return NULL;
  }

}

Dans cet exemple, nous vérifions simplement le type de produit associé, et s'il s'agit d'un produit de type book (un livre donc éligible au taux de TVA à 5.5 %), alors nous retournons simplement le taux de TVA correspondant. Et pour tous les autres produits, éligibles au taux de TVA par défaut, nous laissons le Resolver par défaut faire son travail.

Nous pouvons voir ici que nous aurions pu tout aussi bien évaluer un taux de TVA en fonction par exemple d'un attribut du produit, et non pas de façon globale pour un type de produit. Cette approche basée sur un collecteur de service nous permet d'implémenter des règles métier qui peuvent être très complexes en quelques lignes de façon extrêmement simple et modulaire.

A vos Resolvers

Comme nous l'avons évoqué plus haut, les Resolvers sont utilisés de façon massive sur Drupal commerce 2.x. Ils en sont, avec les Processor, une composante ominiprésente. Vous voulez faire varier le tunnel d'achat en fonction des produits d'une commande. Dégainez un Resolver. Vous voulez calculer le prix d'un produit de façon dynamique, en fonction de règles métiers spécifiques, dégainez un autre Resolver. Vous souhaitez faire varier dynamiquement le type de commande par produit, encore un autre Resolver.

Les Resolver vont ouvrir des perspectives pour voir fleurir des modules contribués dont la vocation première sera d'offrir une interface de configuration selon des règles métier prédéfinies pour un certain type de Resolver. Mais on peut dès maintenant implémenter un Resolver très simplement pour n'importe quel type de règle métier, de la plus simple à la plus singulière, et ce sans devoir sortir l'artillerie lourde.

Feu !  

 

Ajouter un commentaire