Les modules transporteurs - fonctionnement, création, configuration

Cet article a été écrit par Fabien Serny, et publié sur le blog de PrestaShop le 26 septembre 2011.

Préambule

Attention cet article est à l'attention de développeurs avancés (connaissance des hooks, de l'objet Carrier, ...) et est valable à partir de la version 1.4 de PrestaShop.
Pour savoir ce qu'est un hook, je vous conseille de vous reporter à l'article de Julien Breux ici : Mieux comprendre et utiliser les hooks

De base PrestaShop permet de créer des transporteurs et de configurer le calcul des frais de transport à partir de tranches de poids et de tranches de prix que vous pouvez spécifier dans le back office.

Ce système atteint sa limite lorsque vous voulez récupérer les prix via des webservices (UPS, USPS, Fedex) ou encore que vous voulez faire votre propre système de calcul de frais de transports (selon nombre d'articles dans le panier, limitation d'un transporteur à un ou plusieurs pays, etc...).

C'est à ce moment-là qu'interviennent les modules transporteurs.

Par où commencer ?

Il existe un module de base que vous pouvez télécharger ici. L'article partira de ce module de base mais il vous est bien entendu possible de le modifier et de l'adapter à vos besoins.

Notre exemple permet de gérer deux transporteurs auxquels il est possible de fixer un coût additionnel en plus du tarif par tranches configuré dans le back office.

Certaines parties du code sont volontairement dupliquées pour mieux comprendre le mécanisme. Il vous appartient de gérer la liste des transporteurs proprement (à l'aide d'une table de données par exemple).

Explication de l'exemple

Tout d'abord, notez le fait que le module est extends de la classe CarrierModule et non Module. A présent, faisons une petite liste des méthodes contenues dans ce module et son fonctionnement général.

Les méthodes constructeur, installation, désinstallation

Le constructeur :

// L'identifiant carrier se remplit automatiquement avec le bon id selon le transporteur.
// Nous verrons son utilité un peu plus loin.
public  $id_carrier;

private $_html = '';
private $_postErrors = array();
private $_moduleName = 'mycarrier';
/*
** Construct Method
**
*/
public function __construct()
{
       // Variables communes à tous les modules (plus besoin de les présenter)
    $this->name = 'mycarrier';
    $this->tab = 'shipping_logistics';
    $this->version = '1.0';
    $this->author = 'YourName';
    $this->limited_countries = array('fr', 'us');
    parent::__construct ();
    $this->displayName = $this->l('My Carrier');
    $this->description = $this->l('Delivery methods that you want');
       // Si le module est installé, nous faisons quelques vérifications
    if (self::isInstalled($this->name))
    {
        // Nous récupérons la liste des ids des transporteurs
        global $cookie;
        $carriers = Carrier::getCarriers($cookie->id_lang, true, false, false,
        NULL, PS_CARRIERS_AND_CARRIER_MODULES_NEED_RANGE);
        $id_carrier_list = array();
        foreach($carriers as $carrier)
            $id_carrier_list[] .= $carrier['id_carrier'];

        // Nous regardons si les transporteurs du module ont bien été créés 
        // Et si les frais additionnels ont bien été configurés
        // Ces warnings s'afficheront sur la page qui liste les modules
        $warning = array();
        if (!in_array((int)(Configuration::get('MYCARRIER1_CARRIER_ID')),
                   $id_carrier_list)) $warning[] .= $this->l('"Carrier 1"').' ';
        if (!in_array((int)(Configuration::get('MYCARRIER2_CARRIER_ID')),
               $id_carrier_list)) $warning[] .= $this->l('"Carrier 2 Overcost"').' ';
        if (!Configuration::get('MYCARRIER1_OVERCOST'))
            $warning[] .= $this->l('"Carrier 1 Overcost"').' ';
        if (!Configuration::get('MYCARRIER2_OVERCOST'))
            $warning[] .= $this->l('"Carrier 2 Overcost"').' ';
        if (count($warning))
    $this->warning .= implode(' , ',$warning).$this->l('must be <br />configured');
    }
}

L'installation :

/*
** Install / Uninstall Methods
**
*/
public function install()
{
    // On crée un tableau contenant les informations des deux transporteurs
    // que l'on veut créer
    $carrierConfig = array(
    0 => array('name' => 'Carrier1',
      'id_tax_rules_group' => 0, // On n'applique pas de taxe 
        au transporteur
      'active' => true, 'deleted' => 0, 
      'shipping_handling' => false,
      'range_behavior' => 0,
      'delay' => array(
        'fr' => 'Description 1',
        'en' => 'Description 1',
        Language::getIsoById(Configuration::get
                ('PS_LANG_DEFAULT')) => 'Description 1'),
      'id_zone' => 1, // Zone pour laquelle le transporteur fonctionne
      'is_module' => true, // On précise que c'est un module
      'shipping_external' => true,
      'external_module_name' => 'mycarrier', // On précise le nom 
        du module
      'need_range' => true // On précise que l'on veut le calcul
        des tranches
     // qui sont configurés dans le back office
      ),
     1 => array('name' => 'Carrier2',
      'id_tax_rules_group' => 0,
      'active' => true,
      'deleted' => 0,
      'shipping_handling' => false,
      'range_behavior' => 0,
      'delay' => array(
        'fr' => 'Description 1',
        'en' => 'Description 1',
        Language::getIsoById(Configuration::get
               ('PS_LANG_DEFAULT')) => 'Description 1'),
      'id_zone' => 1,
      'is_module' => true,
      'shipping_external' => true,
      'external_module_name' => 'mycarrier',
      'need_range' => true
    ),
);
    // On crée les deux transporteurs et on récupère les id_carrier
    // Et nous enregistrons les ids en base de données
    //Je vous invite à regarder dans le code le fonctionnement 
    //de <br />installExternalCarrier
    // Cependant vous n'aurez normalement pas besoin de modifier cette fonction
    $id_carrier1 = $this->installExternalCarrier($carrierConfig[0]);
    $id_carrier2 = $this->installExternalCarrier($carrierConfig[1]);
    Configuration::updateValue('MYCARRIER1_CARRIER_ID', (int)$id_carrier1);
    Configuration::updateValue('MYCARRIER2_CARRIER_ID', (int)$id_carrier2);
    // Puis on procède à une installation de modules standard
    // Nous verrons un peu plus loin à quoi sert le hook updatecarrier
    if (!parent::install() ||
        !Configuration::updateValue('MYCARRIER1_OVERCOST', '') ||
        !Configuration::updateValue('MYCARRIER2_OVERCOST', '') ||
        !$this->registerHook('updateCarrier'))
        return false;

    return true;
}

La désinstallation :

public function uninstall()
{
    // On procède tout d'abord à la désinstallation classique d'un module
    if (!parent::uninstall() ||
        !Configuration::deleteByName('MYCARRIER1_OVERCOST') ||
        !Configuration::deleteByName('MYCARRIER2_OVERCOST') ||
        !$this->unregisterHook('updateCarrier'))
        return false;        
    // On efface les transporteurs que l'on avait créé
    $Carrier1 = new Carrier((int)(Configuration::get('MYCARRIER1_CARRIER_ID')));
    $Carrier2 = new Carrier((int)(Configuration::get('MYCARRIER2_CARRIER_ID')));
// Si l'un des deux modules était le transporteur par défaut, 
//on en choisit un <br />autre
    if (Configuration::get('PS_CARRIER_DEFAULT') == (int)($Carrier1->id) ||
        Configuration::get('PS_CARRIER_DEFAULT') == (int)($Carrier2->id))
    {
        global $cookie;
        $carriersD = Carrier::getCarriers($cookie->id_lang, true, false, false,
                NULL, PS_CARRIERS_AND_CARRIER_MODULES_NEED_RANGE);
        foreach($carriersD as $carrierD)
            if ($carrierD['active'] AND !$carrierD['deleted']
                AND ($carrierD['name'] != $this->_config['name']))
                Configuration::updateValue('PS_CARRIER_DEFAULT',
                                $carrierD['id_carrier']);
    }
    // Puis on efface les transporteurs via la variable delete
    // afin de garder l'historique des transporteurs pour les commandes qui ont 
    //été passés avec
    $Carrier1->deleted = 1;
    $Carrier2->deleted = 1;
    if (!$Carrier1->update() || !$Carrier2->update())
        return false;

    return true;
}

Les méthodes back office

Je ne m'attarderai pas sur la plupart des méthodes du back office (getContent, _displayForm, _postValidation et _postProcess) qui sont relativement simples et qui sont là pour permettre d'administrer les coûts des transporteurs. Je vous laisse les découvrir dans le module d'exemple.

Cependant, j'attire votre attention sur la méthode hookupdateCarrier ci-dessous.
Dans PrestaShop, à chaque fois que l'on modifie un transporteur, celui-ci est automatiquement archivé et un nouveau transporteur est créé.

Techniquement, on passe le flag deleted du transporteur à 1 et on en créé un nouveau.
C'est pour cela que lorsque vous modifiez un transporteur via l'onglet « Transporteurs » de votre back office, son id change.

Il faut donc hooké le module afin d'actualiser l'id du transporteur lorsque celui-ci change.

/*
** Hook update carrier
**
*/
public function hookupdateCarrier($params)
{
    // On actualise l'id du carrier 1
    if ((int)($params['id_carrier']) == (int)(Configuration::get<br />('MYCARRIER1_CARRIER_ID')))
        Configuration::updateValue('MYCARRIER1_CARRIER_ID', (int)
                ($params['carrier']->id));
    // On actualise l'id du carrier 2
    if ((int)($params['id_carrier']) == (int)(Configuration::get<br />('MYCARRIER2_CARRIER_ID')))
        Configuration::updateValue('MYCARRIER2_CARRIER_ID', (int)
               ($params['carrier']->id));
}

Les méthodes front office

/*
** Front Methods
**
** Si vous avez configuré la variable need_range à true lorsque vous avez créé votre 
** transporteur dans la méthode install(), la méthode appelée par la classe Cart sera 
** getOrderShippingCost()
** Sinon, la méthode appelée sera getOrderShippingCostExternal
**
** La variable $params contient le panier, le client et ses addresses
** La variable $shipping_cost contient les frais calculé par les tranches de prix  
** configurés pour le transporteur dans le back-office
**
*/
public function getOrderShippingCost($params, $shipping_cost)
{
    // Cet exemple retourne les frais de port avec le coût supplémentaire
    // mais vous pouvez appeler un webservice ou faire le calcul que vous voulez
    // avant de retourner le frais de port final

    if ($this->id_carrier == (int)(Configuration::get('MYCARRIER1_CARRIER_ID')) &&
        Configuration::get('MYCARRIER1_OVERCOST') > 1)
        return (float)(Configuration::get('MYCARRIER1_OVERCOST'));

    if ($this->id_carrier == (int)(Configuration::get('MYCARRIER2_CARRIER_ID')) &&
        Configuration::get('MYCARRIER2_OVERCOST') > 1)
        return (float)(Configuration::get('MYCARRIER2_OVERCOST'));
    // Si le transporteur n'est pas renconnu, il suffit de retourner false
    // le transporteur n'apparaitra alors pas dans la liste des transporteurs
    return false;
}
public function getOrderShippingCostExternal($params)
{
    // Cet exemple retourne le coût supplémentaire
    // mais vous pouvez appeler un webservice ou faire le calcul que vous voulez
    // avant de retourner le frais de port final
    if ($this->id_carrier == (int)(Configuration::get('MYCARRIER1_CARRIER_ID')) &&
        Configuration::get('MYCARRIER1_OVERCOST') > 1)
                return (float)(Configuration::get('MYCARRIER1_OVERCOST'));
    if ($this->id_carrier == (int)(Configuration::get('MYCARRIER2_CARRIER_ID')) &&
            Configuration::get('MYCARRIER2_OVERCOST') > 1)
                return (float)(Configuration::get('MYCARRIER2_OVERCOST'));
    // Si le transporteur n'est pas renconnu, il suffit de retourner false
    // le transporteur n'apparaitra alors pas dans la liste des transporteurs
    return false;
}

Pour aller plus loin

Ici l'exemple est très simple, mais vous pouvez créer des modules plus complexes en y associant, par exemple, des webservices (comme c'est le cas dans les modules UPS par exemple) ou encore faire un calcul basé sur le nombre de produits dans votre panier.

Bref, vous avez à présent totalement la main sur le calcul des frais de transport (smile)