Comment intégrer le framework PAD dans un environnement Salesforce.com

Le framework PAD est un processus de développement favorisant la normalisation d'un projet avec quelques bonnes pratiques à respecter. Il facilite ainsi l'arrivée d'un nouveau développeur au sein de l'équipe d'un projet. Cet article explique comment l'intégrer dans un environnement Salesforce.com.

Commentez Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Lorsque l'on travaille sur un projet Salesforce.com, il peut arriver de reprendre le code d'une autre équipe ou de travailler avec plusieurs personnes sur un même projet et peut-être même des personnes parlant une autre langue. De ce fait, ça peut rapidement devenir le désordre s'il n'y a pas un minimum de rigueur et de règles mises en place.

Le framework PAD est là pour corriger tout ça, il s'agit d'un processus de développement favorisant la normalisation d'un projet. Ainsi, lorsqu'un développeur rejoint une équipe, il n'a pas besoin de s'adapter à son équipe et il s'y retrouve ainsi beaucoup plus facilement.

Cela permet à un projet de gagner :

  • en vitesse de développement ;
  • en coût ;
  • en délivrabilité.

Que demande le peuple (enfin, surtout les managers) ?

Je préfère préciser tout de suite pour qu'il n'y ait pas d'ambiguïté, que je ne suis pas le créateur de ce framework, mais simplement un utilisateur. Les créateurs sont Jean-Luc Antoine et Sovan Bin.

Avant d'aller plus loin dans cet article, je vous propose quelques rappels sur les termes techniques qui vont être utilisés :

  • Salesforce.com est un CRM (Customer Relationship Manager) orienté SaaS (Software as a Service) et PaaS (Platform as a Service) permettant la gestion des relations clients d'une entreprise ;
  • Apex, est un langage de programmation côté serveur permettant de modifier la logique métier et le traitement des données ;
  • SOQL (Salesforce Object Query Language) est le langage permettant d'effectuer des requêtes dans la base de données de Salesforce.com.

Le but de cet article n'est pas que vous maîtrisiez complètement cette méthode de développement (je ne la connais pas entièrement non plus) mais plutôt que vous en compreniez le principe pour que vous sachiez l'intégrer vous-même dans une instance Salesforce.com.

Afin de faciliter la compréhension du concept, je vais l'intégrer dans un petit projet comprenant une page Visualforce avec un contrôleur Apex ainsi qu'un trigger Apex.

Le scénario du projet est le suivant, un manager souhaite suivre les opportunités de son équipe commerciale et ainsi consulter l'avancement des objectifs de chacun par rapport à celui attendu.

Bien entendu, certains de mes arguments peuvent être sujets à discussion, je donne simplement mon point de vue par rapport à mon expérience, à ce que l'on m'a conseillé ou à ce que j'ai pu entendre et constater.

II. Prérequis

Avant toute chose, vous devez disposer d'une instance afin de pouvoir suivre ce tutoriel. Si ce n'est pas le cas, je vous invite à vous rendre à cette adresse. Salesforce met à disposition plusieurs types d'instances qui proposent différentes fonctionnalités et donc à différents prix (prix par licence utilisateur par mois). La seule instance que vous pourrez généreusement obtenir de la part de Salesforce est celle de développement (celle que je vous propose via le lien ci-dessus) qui, comme son nom l'indique, sert à tester et développer des services. Je vous laisse juger par vous-même la grille tarifaire sur les diverses instances que propose le CRM.

Vous aurez également besoin d'un environnement de développement pour écrire votre code, vous pouvez utiliser celui fourni par Salesforce ou alors vous procurer celui compatible avec Eclipse et qui s'appelle Force.com IDE (je l'utiliserai tout au long de ce tutoriel). Pour ce dernier, rendez-vous sur l'Eclipse MarketPlace et installez-le.

Pour récupérer le framework PAD, c'est assez simple, il vous suffit d'envoyer un mail à l'adresse en y saisissant le mot-clé PAD (dans l'objet du mail par exemple).

III. Récupérer le framework

Derrière cette adresse mail, il y a un batch qui tourne toutes les 10 minutes environ et qui vous enverra la dernière version du framework accompagné d'un document PowerPoint vous expliquant le fonctionnement en détail de tout ceci. À l'heure où j'écris cet article, la dernière version est la 2.14.

Le framework se compose de deux composants :

  • une classe Apex ;
  • un composant Visualforce.

IV. Configurer une instance Salesforce.com avec le framework PAD

Comme précisé dans le chapitre précédent, le framework n'est composé que de seulement deux composants, mais il faut également créer un champ personnalisé sur l'objet User (j'utilise Salesforce.com en anglais) de type multipicklist : PAD_BypassTrigger__c, peu importe le label, ce qui est important ici est le nom API puisqu'il est utilisé dans la classe Apex du framework, vous comprendrez tout à l'heure le pourquoi du comment.

Voilà à quoi doit ressembler le champ :

Image non disponible

Maintenant que le champ personnalisé est créé, vous pouvez insérer dans Salesforce.com, la classe Apex PAD ainsi que le composant Visualforce du même nom.

Si vous le faites dans l'autre sens, vous ne pourrez pas sauvegarder puisque Salesforce vous dira que vous mentionnez une référence qui n'existe pas.

Je pense que la classe Apex date d'un certain moment puisque lorsque j'ai tenté de l'intégrer dans un projet, Salesforce.com me l'a refusé et pour plusieurs raisons :

  • les méthodes de test ne peuvent pas cohabiter dans le même fichier qu'une classe. Chaque classe de test doit maintenant obligatoirement avoir son propre « fichier » ;
  • certaines méthodes depuis la version 31 de l'API ont été supprimées et ne peuvent plus être utilisées (Limits.getFieldsDescribes(), Limits.getPicklistDescribes() et Limits.getScriptStatements() dans notre cas).

Pour corriger cela, j'ai dû créer une nouvelle classe (PAD_TEST) pour y couper et coller les méthodes de test de la classe Apex et commenter les lignes 80, 85 et 89.

En attendant que cela soit corrigé par les créateurs du framework PAD, je vous fournis tout cela dans une archive compressée disponible dans la partie VIII. Ressources utiles.

V. Quelques bonnes pratiques

Pour des raisons d'optimisation du temps de développement (compréhension du projet, des fonctionnalités et des développements déjà effectués), la méthode de conception PAD dispose de best practices qu'il est recommandé de respecter afin d'avoir un projet bien ordonné et dont je vais vous en présenter quelques-unes (puisque je ne les connais pas toutes).

Ce chapitre n'étant pas une étape obligatoire à l'intégration du framework PAD dans un environnement Salesforce, vous pouvez passer au chapitre suivant VI. Utilisation du framework PAD.

V-A. Les triggers Apex

Comme il n'est pas possible dans Salesforce.com de choisir l'ordre d'exécution de deux triggers portant sur un même objet et un même événement (l'ordre peut différer d'un environnement à un autre), il est nécessaire de créer un seul trigger Apex par objet et par événement pour ensuite placer dans l'ordre que vous le souhaitez, vos traitements. Il est même recommandé de ne pas mettre vos traitements dans vos triggers Apex, mais de créer des classes pour ça et de les appeler depuis vos triggers, cela permet de factoriser vos traitements et d'y voir plus clair, car cela minimise les lignes dans le trigger Apex et si plusieurs ont besoin de cette même méthode, elle est stockée à un seul endroit et il n'y a pas de redondance du code.

V-B. Les étiquettes personnalisées

Ne jamais utiliser de valeur en dur dans votre code, toujours passer par une étiquette personnalisée pour la simple et bonne raison que c'est bien plus maintenable et que si, cette valeur vient à changer, il n'y aura simplement qu'à modifier la valeur du custom label alors que si la valeur est directement ancrée dans votre développement, chaque modification souhaitée en production entraînera obligatoirement un nouveau déploiement et fera donc appel à un développeur et cela lui prendra du temps pour presque rien.

V-C. Les règles de nommage

À quoi servent les règles de nommage ? Elles servent tout simplement à ce que le nom soit très explicite sur le comportement ou le but (un contrôleur, une simple classe Apex, un batch Apex), sur quels objet et événement porte un trigger, etc.

Les règles de nommage portent sur tout ce qui se crée dans Salesforce :

  • les classes Apex ;
  • les triggers Apex ;
  • les pages Visualforce ;
  • les étiquettes personnalisées ;
  • les règles de validation ;
  • les workflows ;
  • etc.

V-C-1. Les classes Apex

Elles doivent commencer par « AP » suivi de leur index et enfin d'un nom très court décrivant son utilité. Par exemple, si vous avez déjà trois classes Apex présentes dans votre instance Salesforce.com et que la prochaine que souhaitez créer porte sur les comptes et doit compter le nombre de contacts qui lui sont rattachés ayant le type d'enregistrement « Blue » (champ Record Type, vous pouvez attribuer à cette classe ce nom (à titre d'exemple) AP04_Count_Blue_Contacts.

V-C-2. Les pages Visualforce

Les pages Visualforce commencent toujours par « VF » suivi de leur index (le nombre total de pages déjà créées + 1) et d'un nom décrivant leur fonction. Par exemple VF02_CreateNewUser.

V-C-3. Les contrôleurs Visualforce

Les contrôleurs Visualforce sont des classes Apex qui servent à exécuter des traitements lors d'interactions avec des pages Visualforce. De ce fait, ils portent le même nom que les pages Visualforce à l'exception qu'ils possèdent en plus un « C » après le « VF » soit en reprenant l'exemple ci-dessus de la page Visualforce, cela donnerait VFC02_CreateNewUser.

V-C-4. Les triggers Apex

Comme vu précédemment, un trigger ne doit porter que sur un objet et que sur un événement pour s'assurer de l'ordre d'exécution du code que l'on souhaite. Son nom dépend donc des conditions citées précédemment, c'est-à-dire qu'il comporte une abréviation du nom de l'objet sur lequel il porte et du nom de l'événement. Par exemple, pour un trigger sur les comptes se déclenchant avant la mise à jour d'un enregistrement, il se nommera AccountBeforeUpdate ou OpportunityAfterInsert pour une opportunité après sa création.

V-C-5. Les étiquettes personnalisées

Les étiquettes personnalisées (custom labels en anglais) servent à stocker toutes sortes de valeurs (une phrase traduite dans plusieurs langues, l'identifiant d'un enregistrement, le nombre d'enregistrements retournés pour une pagination, etc.) et peuvent être utilisées dans les pages Visualforces, des contrôleurs Apex, des triggers Apex ou encore des formules personnalisées (custom formulas).

Cela vous permet d'éviter de stocker des valeurs en dur dans votre code et vous permet ainsi une plus grande maintenabilité si vous devez modifier des valeurs puisque vous n'avez pas besoin d'effectuer un nouveau déploiement en production, juste la modification de l'étiquette personnalisée suffit et le tour est joué.

Pour leur nommage, je préconise de préfixer avec l'endroit où il est utilisé comme le nom d'une page Visualforce, d'une règle de validation ou le nom d'un profile, etc.

V-C-6. Les workflows

Pour correctement nommer ses workflows, il faut les préfixer avec le nom de l'objet sur lequel il porte, puis de à quel(s) moment(s) il se déclenche.

Par exemple, s'il s'agit d'un workflow qui se déclenche à la création de comptes localisée dans la région de l'Île-de-France, il pourrait s'appeler Account Creation Ile de France.

V-C-7. Les actions de mise à jour de champ

Le nommage des field updates est assez similaire à celui des workflows, à la différence qu'il suffit de spécifier sur quel objet il agit ainsi qu'une brève description de son action.

S'il met à jour le département d'un compte, son nom pourrait être Account update name.

V-C-8. Les règles de validation

Les règles de validations ont un nom assez simple. C'est-à-dire qu'il faut qu'elles commencent par « VR » (pour validation rule) suivi de leur index (nombre de validation rules + 1) ainsi que de l'objet sur lequel elle porte et enfin d'une brève description, exemple VR03_Account_Mandatory_PostalCode.

V-C-9. Les classes de test

Elles se nomment de la même façon que la classe Apex ou le contrôleur Visualforce qu'elles testent sauf qu'elles finissent par « _TEST ». Grâce à cela, vous pouvez facilement savoir qu'elle est la classe de test d'une classe ou d‘un contrôleur et elle est également placée juste en dessous dans la liste.

V-D. Penser Bulk

Certes, il est plus facile lorsque que l'on développe un trigger de se dire que lorsque l'on crée ou met à jour un enregistrement, il n'y en aura forcément qu'un seul dans la liste des enregistrements du trigger (Trigger.new). Mais dans ce cas, que se passe-t-il si vous insérez des enregistrements en masse via le dataloader de Salesforce.com ?

Votre trigger ne fonctionnera pas correctement et vous pouvez vous retrouvez avec des données erronées, ce qui est embêtant pour une production.

C'est pourquoi il est indispensable que lorsque l'on effectue un développement, que ce soit un trigger Apex, une classe Apex, une page Visualforce ou autres, de toujours penser Bulk (grande quantité de données).

V-E. Utiliser la méthode System.debug()

Cette méthode peut devenir votre meilleur ami lorsque vous effectuez du débogage puisqu'elle sert à afficher dans les logs, le texte que vous lui donnez en paramètre.

Je vous recommande de l'utiliser au début et à la fin de vos méthodes et de vos triggers Apex afin de suivre en détail le cheminement de l'exécution de vos méthodes. Vous pouvez également afficher le résultat de vos requêtes et insérer du débogage dans vos conditions (if) pour voir où votre code passe.

V-F. Commenter son code

Cela peut vous paraître inutile, mais il s'agit là simplement de clarté et de gagner du temps lorsque vous revenez sur votre code plusieurs mois après l'avoir développé ou que vous intervenez sur le travail d'une autre personne. Cela afin de plus facilement rentrer dans sa logique et de vous rappeler plus facilement son utilité. Vous pouvez aussi ajouter des commentaires au début de vos classes pour rapidement expliquer dans quelles circonstances elles sont utilisées et de même qu'à vos fonctions (paramètres d'entrées, de sorties, ce qu'elles font, expliquer une condition if).

V-G. Les classes de test

Même s'il s'agit de classes de test et qu'elles ne seront pas exécutées en production en utilisation normale (mis à part lors d'un déploiement), cela n'empêche pas qu'elles doivent être correctement développées pour être performantes et le code doit être factorisé au maximum pour éviter la redondance.

Tout ce qui est plusieurs fois doit être factorisé dans une fonction et vous devez ensuite l'appeler.

Cela facilite la maintenance lorsqu'il y a une modification à effectuer, il ne vous faudra le faire qu'à un seul endroit et non à plusieurs.

Par exemple, dans une classe de test, on met généralement en œuvre plusieurs méthodes de test et il faut donc un jeu de données pour chacune d'entre elles. Donc au lieu d'écrire plusieurs fois le même jeu de données, que doit-on faire ? Écrire une méthode et y insérer un jeu de données et appeler cette méthode au début de chaque méthode de test.

Tout comme lorsqu'il s'agit d'initialiser une page Visualforce, cela peut être fait à l'intérieur d'une méthode.

Quel est le bon taux de couverture à obtenir pour vos classes ?

Salesforce exige au minimum 75 % de couverture générale sur tout l'environnement, mais plus cette couverture sera élevée, plus vous vous assurerez du bon fonctionnement de votre code.

Mais comment s'assurer que lorsque vous exécutez votre classe de test, même si vous obtenez un taux de couverture de 90 % ou plus, que tout s'est correctement déroulé et que le résultat obtenu est bien celui attendu ?

Pour cela, vous avez une tripotée de méthodes pour vérifier vos valeurs obtenues en renseignant également celles que vous auriez dû obtenir ou même ne pas obtenir, ce sont les méthodes System.assert(condition, msg), System.assertEquals(expected, actual, msg) et System.assertNotEquals(expected, actual, msg). Vous pouvez retrouver la documentation à ce sujet pour en apprendre davantage.

J'en profite à ce sujet pour proposer un article que j'ai rédigé sur les tests dans Salesforce : Tester son code Apex et How To Test Your Apex Triggers (rédigé en anglais et intégré dans la bibliothèque technique de Salesforce)

V-H. Nom des méthodes et des variables explicites

Toujours pour une facilité de compréhension de votre code, n'hésitez pas à nommer vos méthodes ainsi que vos variables de façon explicite, c'est-à-dire que vous compreniez tout de suite pourquoi elles existent grâce à une simple lecture, cela vous évite de relire tout votre code pour comprendre et vous évite donc une perte de temps inutile.

VI. Utilisation du framework PAD

Le framework PAD a quelques fonctionnalités plutôt intéressantes.

Si vous respectez la règle d'un trigger Apex par objet et par événement et de la règle de nommage des classes Apex (cf. chapitre V.C.1. Les classes Apex), en plus de choisir l'ordre d'exécution de vos traitements, vous pouvez également décider si un utilisateur a le droit ou non d'exécuter un trigger Apex grâce au fameux champ PAD_BypassTrigger__c sur l'objet User» dont je vous avais parlé dans le chapitre IV. Configurer une instance Salesforce.com avec le framework PAD et à la méthode PAD.canTrigger() en renseignant en paramètre le nom de la classe Apex (mais uniquement le AP[index] (« AP03 » par exemple)) pour que celle-ci ne soit exécutée qu'une seule fois puisque PAD garde en mémoire, le nom des classes exécutées. Cela évite ainsi les appels en cascade.

VII. Le projet

Revenons au projet, le but sera qu'un manager puisse visualiser les opportunités de son équipe commerciale groupées par mois et années avec leur objectif en cours par rapport à celui qu'elle doit atteindre où il pourra le modifier en temps réel.

Pour ce faire, nous aurons besoin d'une page Visualforce accompagnée de son contrôleur Apex et d'un trigger Apex qui mettra à jour les objectifs des commerciaux.

VII-A. Le modèle de données

Le modèle de données pour ce projet est assez simple.

Nous avons simplement besoin d'un objet personnalisé que l'on appellera Commercial objective où l'on y stockera pour un commercial :

  • son objectif ;
  • le mois de l'objectif ;
  • l'année de l'objectif ;
  • l'objectif en cours (à combien il en est actuellement) ;
  • le pourcentage de réussite de son objectif ;
  • une représentation graphique si le commercial a réussi ou non à atteindre son objectif (croix verte = Oui, croix rouge = Non) ;
  • une date représentant le mois et l'année de l'objectif (utilisé pour filtrer dans les rapports et les tableaux de bord) ;
  • le lien vers le commercial.

Ci-dessous, une image de à quoi doit ressembler l'objet :

Image non disponible

Le champ Objective in progress sera calculé par rapport à la somme des champs Amount sur les opportunités qui lui sont reliées.

Pour ce faire, nous avons besoin de créer une relation entre les opportunités et les objectifs :

Image non disponible

Pour finir sur cette partie, je vous montre un aperçu du Visualisateur de schéma (Schema Builder) dans Salesforce :

Image non disponible

Il permet de visualiser le modèle de données, mais également de créer des objets personnalisés, des champs personnalisés, etc.

VII-B. Le contrôleur Apex

Ce que nous avons besoin de faire dans le contrôleur Apex est de récupérer les opportunités des commerciaux du mois et de l'année sélectionnés (via la page Visualforce).

Nous partons du principe que le manager voit toutes les opportunités de ses commerciaux.

Nous avons simplement besoin de récupérer en paramètres, le mois et l'année sélectionnés ensuite de récupérer les objectifs des commerciaux ainsi que les opportunités qui leur sont associées.

Afin de faciliter l'affichage des données dans la page Visualforce, j'ai créé une classe interne dans laquelle je stocke le commercial, l'enregistrement Objective et la liste des opportunités de l'objectif.

Voici le code du contrôleur :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
public with sharing class VFC01_OpportunitiesByCommercial {

    public Commercial_objectif__c theFakeCommercialObjectif{get; set;}
   public List<WrapperClass> wrapperList{get; set;}
   private List<User> commercialList;
   private List<Commercial_objectif__c> theObjectifList;
   private Date dateToday;
   public static map<Integer, String> monthMap = new map<Integer, String>{
        1 => 'January',
        2 => 'February',
        3 => 'March',
        4 => 'April',
        5 => 'May',
        6 => 'June',
        7 => 'July',
        8 => 'August',
        9 => 'September',
        10 => 'October',
        11 => 'November',
        12 => 'December'
    };
    
    /** Constructeur du contrôleur Apex **/
   public VFC01_OpportunitiesByCommercial(){
       this.theFakeCommercialObjectif = new Commercial_objectif__c();
       
       this.dateToday = Date.today();
       
       this.setMonthAndYear();
               
       this.commercialList = this.getCommercialList();
               
       this.wrapperList = this.getOpportunitiesByCommercial(theFakeCommercialObjectif.Month__c, theFakeCommercialObjectif.Year__c, this.commercialList);
   }

   /** Récupère la liste des objectifs et des opportunités par rapport au mois et l'année sélectionnés et de la liste des commerciaux **/
   public List<WrapperClass> getOpportunitiesByCommercial(String theSelectedMonth, String theSelectedYear, List<User> theCommercialList){
       List<WrapperClass> result = new List<WrapperClass>();
           
       try{   
           theObjectifList = [
               SELECT Id, User__c, Month__c, Year__c, Objectif__c, Percent_in_progress__c, Objectif_in_progress__c, (SELECT Id, Name, CloseDate, Amount, Account.Id, Account.Name FROM Opportunities__r)
               FROM Commercial_objectif__c
               WHERE User__c IN :theCommercialList
               AND Year__c = :theSelectedYear
               AND Month__c = :theSelectedMonth
           ];
           
           map<Id, Commercial_objectif__c> objectifMap = this.setObjectifMap(theObjectifList);
           
           for(User anUser : theCommercialList){
               
               if(objectifMap.containsKey(anUser.Id)){
                   
                   Commercial_objectif__c anObjectif = objectifMap.get(anUser.Id);
                   
                   result.add(new WrapperClass(
                       anUser,
                       anObjectif.Opportunities__r,
                       anObjectif
                   ));
               }
           }
       }catch(Exception e){
           this.displayErrorMessage(e.getMessage());
       }
       
       return result;
   }
   
   /** Récupère la liste des commerciaux **/
   public List<User> getCommercialList(){
       return [
           SELECT Id, Name
           FROM User
       ];
   }
   
   /** Configure le mois et l'année **/
   public void setMonthAndYear(){
       if(theFakeCommercialObjectif.Year__c == NULL || theFakeCommercialObjectif.Year__c.equals('')){
           theFakeCommercialObjectif.Year__c = String.valueOf(this.dateToday.year());
       }
       
       if(theFakeCommercialObjectif.Month__c == NULL || theFakeCommercialObjectif.Month__c.equals('')){
           theFakeCommercialObjectif.Month__c = VFC01_OpportunitiesByCommercial.monthMap.get(this.dateToday.month());
       }
   }
   
   /** Rafraîchit les opportunités en fonction du mois et de l'année **/
   public PageReference refreshOpportunities(){
       try{
           // Récupére le mois et l'année des opportunités
           this.setMonthAndYear();
           
           // Récupère les objectifs des commerciaux ainsi que leurs opportunités
           this.wrapperList = this.getOpportunitiesByCommercial(theFakeCommercialObjectif.Month__c, theFakeCommercialObjectif.Year__c, this.commercialList);
       }catch(Exception e){
           this.displayErrorMessage(e.getMessage());
       }
       
       return NULL;
   }
   
   /** Met à jour les objectifs **/
   public PageReference updateData(){
       try{
           update this.theObjectifList;
           
           this.refreshOpportunities();
       }catch(Exception e){
           this.displayErrorMessage(e.getMessage());
       }
       
       return NULL;
   }
   
   /** Crée la map des objectifs triés par commercial **/
   public map<Id, Commercial_objectif__c> setObjectifMap(List<Commercial_objectif__c> theObjectifList){
       map<Id, Commercial_objectif__c> result = new map<Id, Commercial_objectif__c>();
       
       for(Commercial_objectif__c anObjectif : theObjectifList){
           
           result.put(anObjectif.User__c, anObjectif);
       }
       
       return result;
   }
   
   /** Classe interne pour faciliter l'affichage dans la page Visualforce **/
   public class WrapperClass{
   
       public User theUser{get; set;}
       public List<Opportunity> theOpportunityList{get; set;}
       public Commercial_objectif__c theObjectif{get; set;}
       
       public WrapperClass(User anUser, List<Opportunity> anOpportunityList, Commercial_objectif__c anObjectif){
           this.theUser = anUser;
           this.theOpportunityList = anOpportunityList;
           this.theObjectif = anObjectif;
       }
   }
   
   /** Affiche un message d'erreur sur la page Visualforce **/
   public void displayErrorMessage(String theErrorMessage){
       ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.Error,theErrorMessage));
   }
}

Comme énoncé dans le chapitre des best practices:

  • le nom du contrôleur Apex est le nom de la page Visualforce avec un « C » après le « VF » ;
  • le code est commenté, mais surtout au niveau de chaque méthode pour me rappeler quel est son rôle dans le cas où je reviens dessus quelque temps après et que je ne m'en souviens plus.

VII-C. La page Visualforce

Au niveau de la page Visualforce, nous affichons un menu déroulant permettant au manager de sélectionner un mois et une année (par défaut lors du premier chargement de la page, il s'agit du mois et de l'année actuels).

La modification du mois ou de l'année (après validation du formulaire) aura pour conséquence, le rechargement de la page et de son contenu en fonction du mois et de l'année :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
<apex:page controller="VFC01_OpportunitiesByCommercial">

   <apex:form >
   
       <apex:pageBlock >
       
           <apex:pageBlockButtons location="both">
               <apex:commandButton value="{!$Label.Labs_Sf_Valider}" action="{!updateData}" />
           </apex:pageBlockButtons>
           
           <div>
               <apex:outputText value="{!$Label.Labs_Sf_Select_Month_Year} : " />
               <apex:inputField value="{!theFakeCommercialObjectif.Month__c}" />
               <apex:inputField value="{!theFakeCommercialObjectif.Year__c}" />
               <apex:commandButton value="{!$Label.Labs_Sf_Select_Opportunities}" action="{!refreshOpportunities}" />
           </div>
           
           <apex:repeat value="{!wrapperList}" var="aWrapperClass">
       
               <apex:pageBlockSection title="{!aWrapperClass.theUser.Name}">
               
                   <table>
                       <tr>
                           <td><apex:inputField value="{!aWrapperClass.theObjectif.Objectif__c}" style="width:100px;" /></td>
                       </tr>
                       <tr>
                           <td><apex:outputField value="{!aWrapperClass.theObjectif.Percent_in_progress__c}" /></td>
                       </tr>
                   </table>
                   
                   <apex:pageBlockTable value="{!aWrapperClass.theOpportunityList}" var="anOpportunity">
                   
                       <apex:column value="{!anOpportunity.Account.Name}" >
                           <apex:facet name="header">{!$ObjectType.Account.Fields.Name.Label}</apex:facet>
                       </apex:column>
                       <apex:column value="{!anOpportunity.Name}" >
                           <apex:facet name="header">{!$ObjectType.Opportunity.Fields.Name.Label}</apex:facet>
                       </apex:column>
                       <apex:column value="{!anOpportunity.Amount}" >
                           <apex:facet name="header">{!$ObjectType.Opportunity.Fields.Amount.Label}</apex:facet>
                       </apex:column>
                       <apex:column value="{!anOpportunity.CloseDate}" >
                           <apex:facet name="header">{!$ObjectType.Opportunity.Fields.CloseDate.Label}</apex:facet>
                       </apex:column>
                   
                   </apex:pageBlockTable>
               
               </apex:pageBlockSection>
           
           </apex:repeat>
       
       </apex:pageBlock>
   
   </apex:form>

</apex:page>

Voilà visuellement à quoi elle ressemble :

Image non disponible

VII-D. Le trigger Apex

Le trigger Apex est utilisé lorsqu'une opportunité est créée ou modifiée.

Nous avons donc deux triggers Apex qui appellent tous deux la même méthode (comme vu dans les best practices pour externaliser le comportement) et qui se charge de mettre à jour le champ Objective in progress de l'objet Commercial objective en faisant la somme du champ Amount des opportunités qui lui sont attachées.

Voici les deux triggers Apex que je propose :

OpportunityAfterInsert

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
trigger OpportunityAfterInsert on Opportunity (after insert) {

    List<Opportunity> opportunitiesToExecute = new List<Opportunity>();
    
    for(Opportunity anOpportunity : Trigger.new){
        
        if(anOpportunity.Amount != NULL){
            opportunitiesToExecute.add(anOpportunity);
        }
    }
    
    if(opportunitiesToExecute.size() > 0){
        
        // Mise à jour des objectifs des commerciaux
        AP01_UpdateObjectifInProgress.updateObjectifInProgress(opportunitiesToExecute);
    }
}

OpportunityAfterUpdate

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
trigger OpportunityAfterUpdate on Opportunity (after update) {

    List<Opportunity> opportunitiesToExecute = new List<Opportunity>();
    
    for(Opportunity anOpportunity : Trigger.new){
        
        if(anOpportunity.Amount != NULL){
            opportunitiesToExecute.add(anOpportunity);
        }
    }
    
    if(opportunitiesToExecute.size() > 0){
        
        // Mise à jour des objectifs des commerciaux
        AP01_UpdateObjectifInProgress.updateObjectifInProgress(opportunitiesToExecute);
    }
}

Les deux triggers Apex sont déclenchés après la création et la mise à jour parce que nous avons obligatoirement besoin de leur identifiant pour récupérer toutes les opportunités d'un objectif et de faire leur somme.

Voici la méthode qui effectue le traitement :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
public with sharing class AP01_UpdateObjectifInProgress{

    /** Met à jour les objectifs des commerciaux **/
    public static void updateObjectifInProgress(List<Opportunity> theOpportunityList){
       Set<String> commercialObjectifIdSet = new Set<String>();
       
       for(Opportunity anOpportunity : theOpportunityList){
       
           // Récupération de l'id de l'objectif attaché à l'opportunité
           if(!commercialObjectifIdSet.contains(anOpportunity.Commercial_objectif__c)){
               commercialObjectifIdSet.add(anOpportunity.Commercial_objectif__c);
           }
       }
       
       // Récupération des objectifs ainsi que des opportunités attachées
       List<Commercial_objectif__c> commercialsObjectifsList = [
           SELECT Id, Year__c, Month__c, Objectif__c, Objectif_in_progress__c, User__c, (SELECT Id, Amount FROM Opportunities__r WHERE Amount != NULL)
           FROM Commercial_objectif__c
           WHERE Id IN :commercialObjectifIdSet
       ];
       
       if(commercialsObjectifsList.size() > 0){
           
           // Pour chaque objectif
           for(Commercial_objectif__c aCommercialObjectif : commercialsObjectifsList){
               
               aCommercialObjectif.Objectif_in_progress__c = 0;
               
               // Pour chaque opportunité attachée à l'objectif
               for(Opportunity anOpportunity : aCommercialObjectif.Opportunities__r){
       
                   // Calcul du montant de l'objectif en cours
                   aCommercialObjectif.Objectif_in_progress__c += anOpportunity.Amount;
               }
           }
           
           update commercialsObjectifsList;
       }    
   }
}

En clair, je récupère dans un premier temps, les identifiants des objectifs des opportunités de la liste pour ensuite les récupérer via une requête SOQL ainsi que toutes leurs opportunités qui leur sont attachées (d'où l'intérêt que les triggers Apex (surtout celui de l'insertion) se déclenchent en after.

J'itère ensuite sur chaque objectif puis sur chaque opportunité pour calculer la somme de l'objectif en cours (via le champ Amount sur les opportunités).

La fin de la méthode est assez évidente, je mets à jour les objectifs avec leur nouvelle valeur.

VII-E. Test du contrôleur Apex

Pour correctement tester le contrôleur Apex, il nous faut reconstruire un jeu de données identique à ce qu'il y aurait dans notre instance.

Nous avons donc besoin de :

  • une liste de commerciaux ;
  • une liste d'objectifs pour les commerciaux ;
  • une liste d'opportunités attachées aux objectifs des commerciaux.

Voilà ce que ça peut donner :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
@isTest
private class VFC01_OpportunitiesByCommercial_TEST {

    static Profile aProfile;
    static User anUser;
    static List<Commercial_objectif__c> commercialsObjectifsList;
    static List<Opportunity> opportunityList;
    static String stageName = 'Prospecting';
    static String yearLabel = 'selectedYear';
    static String monthLabel = 'selectedMonth';
    static Decimal amount1 = 100;
    static Decimal amount2 = 400;
    static Decimal amount3 = 200;
    static Decimal objectif1 = 1000;
    static Decimal objectif2 = 1025;
    static Decimal objectif3 = 7500;
    static Date todayDate = Date.today();
    static map<Integer, String> monthMap = VFC01_OpportunitiesByCommercial.monthMap;

    static void init(){
        
        /** Profile **/
            aProfile = [
                SELECT Id
                FROM Profile
                WHERE Name='Standard User'
            ]; 
     
        /** Utilisateur **/
            anUser = new User(
                Alias = 'standt',
                Email='standarduser@testorg.com', 
                 EmailEncodingKey='UTF-8',
                 LastName='Testing',
                 LanguageLocaleKey='en_US', 
               LocaleSidKey='en_US',
               ProfileId = aProfile.Id, 
               TimeZoneSidKey='America/Los_Angeles',
               UserName='testUser@testTrailhead.com'
             );
             
             insert anUser;
        
        /** Objectifs des commerciaux **/
            commercialsObjectifsList = new List<Commercial_objectif__c>();
            commercialsObjectifsList.add(new Commercial_objectif__c(
                Year__c = String.valueOf(todayDate.year()),
                Month__c = monthMap.get(todayDate.month()),
                Objectif__c = objectif1,
                User__c = anUser.Id
            ));
            commercialsObjectifsList.add(new Commercial_objectif__c(
                Year__c = String.valueOf(todayDate.year()),
                Month__c = monthMap.get(todayDate.month() + 2),
                Objectif__c = objectif2,
                User__c = anUser.Id
            ));
            commercialsObjectifsList.add(new Commercial_objectif__c(
                Year__c = String.valueOf(todayDate.year()),
                Month__c = monthMap.get(todayDate.month() + 3),
                Objectif__c = objectif3,
                User__c = anUser.Id
            ));
            
            insert commercialsObjectifsList;
            
        /** Opportunités **/
            opportunityList = new List<Opportunity>();
            opportunityList.add(new Opportunity(
                Name = 'Test opportunity 1',
                CloseDate = Date.newInstance(todayDate.year(), todayDate.month(), 1),
                Amount = amount1,
                Commercial_objectif__c = commercialsObjectifsList[0].Id,
                StageName = stageName
            ));
            opportunityList.add(new Opportunity(
                Name = 'Test opportunity 2',
                CloseDate = Date.newInstance(todayDate.year(), todayDate.month(), 1),
                Amount = amount1,
                Commercial_objectif__c = commercialsObjectifsList[1].Id,
                StageName = stageName
            ));
            opportunityList.add(new Opportunity(
                Name = 'Test opportunity 3',
                CloseDate = Date.newInstance(todayDate.year(), todayDate.month(), 1),
                Amount = amount1,
                Commercial_objectif__c = commercialsObjectifsList[2].Id,
                StageName = stageName
            ));
            
            insert opportunityList;
    }

    /** Test de la page Visualforce **/
   static testMethod void testVFPage() {
       init();
       
       Test.startTest();
       
       // Utilisation de la page Visualforce pour être dans le bon contexte
       PageReference pageRef = Page.VF01_OpportunitiesByCommercial;
       
       Test.setCurrentPage(pageRef);
       
       VFC01_OpportunitiesByCommercial theController = new VFC01_OpportunitiesByCommercial();
       
       ApexPages.currentPage().getParameters().put(yearLabel, String.valueOf(todayDate.addYears(1).year()));
       ApexPages.currentPage().getParameters().put(monthLabel, monthMap.get(todayDate.month()));
       
       // Rafraîchit les opportunités en fonction du mois et de l'année sélectionnés
       theController.refreshOpportunities();
       
       // Met à jour les objectifs
       theController.updateData();    
       
       // Pour la couverture de code
       theController.displayErrorMessage('Error !');
       
       Test.stopTest();
   }
}

VII-F. Test des triggers Apex

Pareil que pour la classe de test du contrôleur Apex, nous avons besoin de reconstituer un environnement de données à tester.

Comme nous testons des opportunités, nous avons donc besoin d'opportunités, mais comme elles sont attachées à des objectifs, nous avons également besoin d'objectifs et de commerciaux.

Voici la liste de ce dont nous avons besoin :

  • une liste de commerciaux ;
  • une liste d'objectifs des commerciaux ;
  • une liste d'opportunités attachées aux objectifs des commerciaux.
 
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
@isTest
private class AP01_UpdateObjectifInProgress_TEST {

    static List<Commercial_objectif__c> commercialsObjectifsList;
    static List<Opportunity> opportunityList;
    static String stageName = 'Prospecting';
    static Decimal amount1 = 100;
    static Decimal amount2 = 400;
    static Decimal amount3 = 200;
    static Decimal objectif1 = 1000;
    static Decimal objectif2 = 1025;
    static Decimal objectif3 = 7500;
    static Date todayDate = Date.today();
    static map<Integer, String> monthMap = new map<Integer, String>{
        1 => 'January',
        2 => 'February',
        3 => 'March',
        4 => 'April',
        5 => 'May',
        6 => 'June',
        7 => 'July',
        8 => 'August',
        9 => 'September',
        10 => 'October',
        11 => 'November',
        12 => 'December'
    };

    static void init(){
        /** Objectifs des commerciaux **/
            commercialsObjectifsList = new List<Commercial_objectif__c>();
            commercialsObjectifsList.add(new Commercial_objectif__c(
                Year__c = String.valueOf(todayDate.year()),
                Month__c = monthMap.get(todayDate.month()),
                Objectif__c = objectif1
            ));
            commercialsObjectifsList.add(new Commercial_objectif__c(
                Year__c = String.valueOf(todayDate.year()),
                Month__c = monthMap.get(todayDate.month() + 2),
                Objectif__c = objectif2
            ));
            commercialsObjectifsList.add(new Commercial_objectif__c(
                Year__c = String.valueOf(todayDate.year()),
                Month__c = monthMap.get(todayDate.month() + 3),
                Objectif__c = objectif3
            ));
            
            insert commercialsObjectifsList;
            
        /** Opportunités **/
            opportunityList = new List<Opportunity>();
            opportunityList.add(new Opportunity(
                Name = 'Test opportunity 1',
                CloseDate = Date.newInstance(todayDate.year(), todayDate.month(), 1),
                Amount = amount1,
                Commercial_objectif__c = commercialsObjectifsList[0].Id,
                StageName = stageName
            ));
            opportunityList.add(new Opportunity(
                Name = 'Test opportunity 2',
                CloseDate = Date.newInstance(todayDate.year(), todayDate.month(), 1),
                Amount = amount1,
                Commercial_objectif__c = commercialsObjectifsList[1].Id,
                StageName = stageName
            ));
            opportunityList.add(new Opportunity(
                Name = 'Test opportunity 3',
                CloseDate = Date.newInstance(todayDate.year(), todayDate.month(), 1),
                Amount = amount1,
                Commercial_objectif__c = commercialsObjectifsList[2].Id,
                StageName = stageName
            ));
    }

    /** Test à la création **/
   static testMethod void testCreation() {
       init();
       
       Test.startTest();
       
       insert opportunityList;
       
       Test.stopTest();
       
       Commercial_objectif__c theCommercialObjectif = [
           SELECT Id, Objectif_in_progress__c
           FROM Commercial_objectif__c
           WHERE Id = :commercialsObjectifsList[0].Id
       ];
       
       System.assertEquals(amount1, theCommercialObjectif.Objectif_in_progress__c);
   }
   
   /** Test à la mise à jour **/
   static testMethod void testUpdate() {
       init();
       
       insert opportunityList;
       
       Test.startTest();
       
       opportunityList[0].Amount = amount3;
       
       opportunityList.add(new Opportunity(
           Name = 'Test opportunity 2',
           CloseDate = Date.newInstance(todayDate.year(), todayDate.month(), 15),
            Amount = amount2,
            Commercial_objectif__c = commercialsObjectifsList[0].Id,
            StageName = stageName
       ));
       
       upsert opportunityList;
       
       Test.stopTest();
       
       Commercial_objectif__c theCommercialObjectif = [
           SELECT Id, Objectif_in_progress__c
           FROM Commercial_objectif__c
           WHERE Id = :commercialsObjectifsList[0].Id
       ];
       
       System.assertEquals((amount2 + amount3), theCommercialObjectif.Objectif_in_progress__c);
   }
}

Pour vérifier le bon comportement des triggers Apex, je vérifie à la fin les valeurs des objectifs des commerciaux par rapport à la valeur qu'elles devraient avoir grâce à la méthode System.assertEquals().

VIII. Plus si affinités

Comme je l'ai mentionné au début de cet article, je ne suis qu'un simple utilisateur du framework. Il y a plein de fonctionnalités que je n'utilise pas parce que je n'ai pas encore eu l'occasion ou que je n'ai pas connaissance de ce qu'il est possible de faire.

Donc si vous êtes tombés amoureux du framework PAD, je vous invite à lire la documentation si vous désirez aller plus loin dans son utilisation.

IX. Conclusion

Le framework PAD est une bonne solution pour mener correctement un projet, il impose des normes qu'il est conseillé de respecter lorsque l'on travaille à plusieurs ou seul sur un projet.

Il vous permettra de gagner du temps lors de vos développements.

X. Ressources utiles

Pour obtenir le framework PAD, vous avez deux possibilités :

  • envoyer un mail à en y saisissant le mot-clé « PAD » (dans l'objet du mail par exemple) pour recevoir la dernière version ;
  • télécharger cette archive pour recevoir la version 2.14 corrigée pour la version 31 de l'API de Salesforce.com.

Le code de cet article est disponible dans un dépôt sur Github accessible à tous : Manage-commercials-opportunities.

XI. Remerciements

Je tiens à remercier tous ceux qui m'ont aidé à la réalisation de cet article :

  • vermine ;
  • Claude LELOUP.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2015 Aurélien Laval. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.