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 :
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 :
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 :
Pour finir sur cette partie, je vous montre un aperçu du Visualisateur de schéma (Schema Builder) dans Salesforce :
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 :
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 :
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 :
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
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
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 :
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 :
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.
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.