Joule pour ABAP : L IA generative de SAP pour le developpement ABAP Cloud

Catégorie
Trends
Publié
Auteur
Johannes

Joule est l’IA generative de SAP, integree directement dans ADT (ABAP Development Tools) depuis 2024. En tant qu’assistant SAP natif, Joule comprend la syntaxe ABAP, les patterns RAP et les concepts specifiques SAP et peut aider les developpeurs ABAP dans leur travail quotidien.

Qu’est-ce que Joule ?

Joule = L’assistant IA generatif de SAP

  • Base sur des Large Language Models, entraine sur la documentation SAP et le code ABAP
  • Integre nativement dans les outils SAP (ADT, BAS, SAP Build)
  • Comprend les concepts specifiques SAP : RAP, CDS, Fiori, OData
  • Peut generer, expliquer et documenter du code
  • Genere des tests unitaires et des donnees de test
  • Respecte les regles ABAP Cloud et les guidelines Clean ABAP

Joule vs. GitHub Copilot

AspectJouleGitHub Copilot
EditeurSAPGitHub/Microsoft
IntegrationADT, BAS, SAP BuildVS Code, Eclipse
Connaissance SAPNative (documentation SAP)Generique (Open Source)
Support RAPTres bon (Behavior, CDS, Service)Bon (a partir d’exemples de code)
ABAP CloudOptimise pour Released APIsMixte (aussi Classic ABAP)
Clean ABAPIntegreDoit etre entraine
LicenceLicence SAP requise10-19$/mois
Hors ligneNon (base sur le cloud)Non (base sur le cloud)
Protection des donneesCloud SAP (serveurs UE possibles)Cloud Microsoft

Recommandation :

  • Joule pour le developpement centre sur SAP et les projets RAP
  • Copilot pour les environnements mixtes (ABAP + autres langages)
  • Les deux ensemble pour une productivite maximale

Activer Joule dans ADT

Prerequis

  • SAP BTP ou S/4HANA Cloud avec autorisation Joule
  • ADT (Eclipse) version 2024 ou plus recente
  • Licence Joule incluse dans le contrat SAP
  • Connexion Internet (base sur le cloud)

Activation dans ADT

1. Ouvrir ADT → Window → Preferences
2. ABAP Development → Joule
3. Activer "Enable Joule AI Assistant"
4. Selectionner la connexion systeme (BTP ou S/4HANA Cloud)
5. Authentification avec utilisateur SAP
6. OK → Redemarrer ADT

Ouvrir le panneau Joule :

View → Other → ABAP → Joule Assistant
Ou : Ctrl+Shift+J (Raccourci)

Generation de code avec Joule

Exemple 1 : Creer un RAP Business Object

Prompt a Joule :

Cree un RAP Business Object pour les commandes (Orders) avec :
- Champs : OrderId, CustomerId, OrderDate, TotalAmount, Status
- Scenario Managed avec Draft
- Validation : OrderDate ne doit pas etre dans le passe
- Determination : Calculer TotalAmount a partir des OrderItems
- Action : approve pour approuver la commande

Joule genere :

  1. Table de base de donnees :
@EndUserText.label : 'Order Header"
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
define table zorder_hdr {
key client : abap.clnt not null;
key order_id : abap.numc(10) not null;
customer_id : abap.numc(10);
order_date : abap.dats;
total_amount : abap.dec(15,2);
currency : abap.cuky(5);
status : abap.char(1);
created_by : abp_creation_user;
created_at : abp_creation_tstmpl;
last_changed_by : abp_lastchange_user;
last_changed_at : abp_lastchange_tstmpl;
local_changed_at: abp_locinst_lastchange_tstmpl;
}
  1. CDS Interface View :
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Order Interface View"
define root view entity ZI_Order
as select from zorder_hdr
composition [0..*] of ZI_OrderItem as _Items
association [0..1] to I_Customer as _Customer
on $projection.CustomerId = _Customer.Customer
{
key order_id as OrderId,
customer_id as CustomerId,
order_date as OrderDate,
@Semantics.amount.currencyCode: 'Currency"
total_amount as TotalAmount,
@Semantics.currencyCode: true
currency as Currency,
status as Status,
@Semantics.user.createdBy: true
created_by as CreatedBy,
@Semantics.systemDateTime.createdAt: true
created_at as CreatedAt,
@Semantics.user.lastChangedBy: true
last_changed_by as LastChangedBy,
@Semantics.systemDateTime.lastChangedAt: true
last_changed_at as LastChangedAt,
@Semantics.systemDateTime.localInstanceLastChangedAt: true
local_changed_at as LocalChangedAt,
_Items,
_Customer
}
  1. Behavior Definition :
managed implementation in class zbp_i_order unique;
strict ( 2 );
with draft;
define behavior for ZI_Order alias Order
persistent table zorder_hdr
draft table zorder_hdr_d
lock master total etag LastChangedAt
authorization master ( global )
etag master LocalChangedAt
{
field ( readonly ) OrderId, CreatedBy, CreatedAt, LastChangedBy, LastChangedAt;
field ( readonly : update ) CustomerId;
create;
update;
delete;
// Draft Actions
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft action Resume;
draft determine action Prepare;
// Custom Action
action approve result [1] $self;
// Validation
validation validateOrderDate on save { create; update; field OrderDate; }
// Determination
determination calculateTotalAmount on modify { field TotalAmount; }
association _Items { create; with draft; }
mapping for zorder_hdr
{
OrderId = order_id;
CustomerId = customer_id;
OrderDate = order_date;
TotalAmount = total_amount;
Currency = currency;
Status = status;
CreatedBy = created_by;
CreatedAt = created_at;
LastChangedBy = last_changed_by;
LastChangedAt = last_changed_at;
LocalChangedAt = local_changed_at;
}
}
  1. Behavior Implementation :
CLASS zbp_i_order DEFINITION PUBLIC ABSTRACT FINAL
FOR BEHAVIOR OF zi_order.
ENDCLASS.
CLASS zbp_i_order IMPLEMENTATION.
ENDCLASS.
CLASS lhc_order DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS:
get_global_authorizations FOR GLOBAL AUTHORIZATION
IMPORTING REQUEST requested_authorizations
FOR Order RESULT result,
validateOrderDate FOR VALIDATE ON SAVE
IMPORTING keys FOR Order~validateOrderDate,
calculateTotalAmount FOR DETERMINE ON MODIFY
IMPORTING keys FOR Order~calculateTotalAmount,
approve FOR MODIFY
IMPORTING keys FOR ACTION Order~approve RESULT result.
ENDCLASS.
CLASS lhc_order IMPLEMENTATION.
METHOD get_global_authorizations.
" Simplifie : Autoriser toutes les operations
result = VALUE #( ( %action-approve = if_abap_behv=>auth-allowed ) ).
ENDMETHOD.
METHOD validateOrderDate.
" Lire les donnees de commande
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
FIELDS ( OrderDate )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
" Obtenir la date actuelle
DATA(lv_today) = cl_abap_context_info=>get_system_date( ).
" Verifier chaque commande
LOOP AT lt_orders INTO DATA(ls_order).
IF ls_order-OrderDate < lv_today.
" Date de commande dans le passe → Erreur
APPEND VALUE #(
%tky = ls_order-%tky
%element-OrderDate = if_abap_behv=>mk-on
) TO failed-order.
APPEND VALUE #(
%tky = ls_order-%tky
%element-OrderDate = if_abap_behv=>mk-on
%msg = new_message_with_text(
severity = if_abap_behv_message=>severity-error
text = |La date de commande { ls_order-OrderDate DATE = USER } est dans le passe|
)
) TO reported-order.
ENDIF.
ENDLOOP.
ENDMETHOD.
METHOD calculateTotalAmount.
" Lire les commandes avec les articles
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order BY \_Items
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_items).
" Calculer le total par commande
LOOP AT keys INTO DATA(ls_key).
DATA(lv_total) = REDUCE decfloat34(
INIT sum = CONV decfloat34( 0 )
FOR item IN lt_items WHERE ( OrderId = ls_key-OrderId )
NEXT sum = sum + ( item-Quantity * item-UnitPrice )
).
" Mettre a jour le total de la commande
MODIFY ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
UPDATE FIELDS ( TotalAmount )
WITH VALUE #( (
%tky = ls_key-%tky
TotalAmount = lv_total
) )
FAILED DATA(lt_failed)
REPORTED DATA(lt_reported).
ENDLOOP.
ENDMETHOD.
METHOD approve.
" Lire le statut actuel
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
FIELDS ( Status )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_orders).
" Mettre a jour le statut a 'A' (Approuve)
MODIFY ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
UPDATE FIELDS ( Status )
WITH VALUE #(
FOR order IN lt_orders (
%tky = order-%tky
Status = 'A"
)
)
FAILED failed
REPORTED reported.
" Retourner les commandes mises a jour
READ ENTITIES OF zi_order IN LOCAL MODE
ENTITY Order
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_result).
result = VALUE #(
FOR order IN lt_result (
%tky = order-%tky
%param = order
)
).
ENDMETHOD.
ENDCLASS.

Exemple 2 : CDS View avec aggregations

Prompt a Joule :

Cree un CDS View pour les statistiques de ventes :
- Groupe par mois et vendeur
- Affiche le nombre de commandes, le chiffre d'affaires total, la valeur moyenne
- Filtre sur Status = 'Completed"
- Trie par chiffre d'affaires decroissant

Joule genere :

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Sales Statistics by Month and Salesperson"
@Analytics.dataCategory: #CUBE
@ObjectModel.usageType: {
serviceQuality: #C,
sizeCategory: #L,
dataClass: #MIXED
}
define view entity ZI_SalesStatistics
as select from ZI_Order as Order
association [0..1] to I_CalendarMonth as _CalendarMonth
on $projection.OrderMonth = _CalendarMonth.CalendarMonth
{
// Dimensions
@EndUserText.label: 'Mois de commande"
concat(
cast( substring( cast( Order.OrderDate as abap.char(8) ), 1, 6 ) as abap.char(6) ),
'"
) as OrderMonth,
@EndUserText.label: 'Vendeur"
Order.CreatedBy as Salesperson,
@EndUserText.label: 'Devise"
@Semantics.currencyCode: true
Order.Currency,
// Measures
@EndUserText.label: 'Nombre de commandes"
@DefaultAggregation: #SUM
@Aggregation.default: #SUM
cast( 1 as abap.int4 ) as OrderCount,
@EndUserText.label: 'Chiffre d affaires total"
@Semantics.amount.currencyCode: 'Currency"
@DefaultAggregation: #SUM
@Aggregation.default: #SUM
Order.TotalAmount as TotalRevenue,
// Association
_CalendarMonth
}
where
Order.Status = 'C' // Completed

Explication et documentation du code

Faire expliquer du code

Selectionnez du code et demandez a Joule :

Explique ce code ABAP :
DATA(lt_filtered) = FILTER #(
lt_orders USING KEY status_key
WHERE status = 'A' AND customer_id IS NOT INITIAL
).

Joule repond :

Cette ligne utilise l'expression FILTER ABAP avec une recherche
optimisee via une cle secondaire :
1. **FILTER #( lt_orders ... )** :
Filtre la table interne lt_orders
2. **USING KEY status_key** :
Utilise la cle secondaire nommee 'status_key' pour la recherche.
C'est nettement plus performant qu'un scan complet de table,
car la cle est utilisee comme index trie ou hash.
3. **WHERE status = 'A' AND customer_id IS NOT INITIAL** :
Conditions de filtre :
- status doit etre 'A' (Approved)
- customer_id ne doit pas etre initial (vide)
4. **DATA(lt_filtered) = ...** :
Declaration inline - le type est automatiquement deduit du resultat.
**Note de performance :**
La cle secondaire doit correspondre a la definition de la table :
- Pour WHERE avec = → Utiliser HASHED KEY
- Pour WHERE avec plage → Utiliser SORTED KEY
**Equivalent (plus lent) :**
LOOP AT lt_orders INTO DATA(ls_order)
WHERE status = 'A' AND customer_id IS NOT INITIAL.
APPEND ls_order TO lt_filtered.
ENDLOOP.

Generer de la documentation

Prompt :

Genere ABAP Doc pour cette methode :
METHOD calculate_discount.
DATA(lv_base) = iv_amount.
IF iv_customer_type = 'VIP'.
rv_discount = lv_base * '0.15'.
ELSEIF iv_amount > 1000.
rv_discount = lv_base * '0.10'.
ELSEIF iv_amount > 500.
rv_discount = lv_base * '0.05'.
ELSE.
rv_discount = 0.
ENDIF.
ENDMETHOD.

Joule genere :

"! <p class="shorttext synchronized">Calcule le montant de la remise</p>
"! Determine la remise en fonction du type de client et de la valeur de commande.
"! Les clients VIP recoivent toujours 15% de remise. Pour les autres clients :
"! <ul>
"! <li>Commande > 1000 : 10% de remise</li>
"! <li>Commande > 500 : 5% de remise</li>
"! <li>Sinon : pas de remise</li>
"! </ul>
"!
"! @parameter iv_amount | <p class="shorttext synchronized">Montant de commande</p>
"! La valeur brute de commande comme base de calcul
"! @parameter iv_customer_type | <p class="shorttext synchronized">Type de client</p>
"! 'VIP' pour clients VIP, sinon Standard
"! @parameter rv_discount | <p class="shorttext synchronized">Montant de remise</p>
"! La remise calculee dans la meme devise
"! @raising cx_parameter_invalid | Pour un montant de commande negatif
METHOD calculate_discount.
IF iv_amount < 0.
RAISE EXCEPTION TYPE cx_parameter_invalid
EXPORTING parameter_name = 'IV_AMOUNT'.
ENDIF.
IF iv_customer_type = 'VIP'.
rv_discount = iv_amount * '0.15'.
ELSEIF iv_amount > 1000.
rv_discount = iv_amount * '0.10'.
ELSEIF iv_amount > 500.
rv_discount = iv_amount * '0.05'.
ELSE.
rv_discount = 0.
ENDIF.
ENDMETHOD.

Generation de tests unitaires

Prompt pour les tests unitaires

Genere des tests unitaires pour la methode calculate_discount :
- Test VIP avec 100 EUR → 15 EUR de remise
- Test Normal avec 1500 EUR → 150 EUR de remise (10%)
- Test Normal avec 750 EUR → 37.50 EUR de remise (5%)
- Test Normal avec 100 EUR → 0 EUR de remise
- Test montant negatif → Exception

Joule genere :

"! @testing ZCL_DISCOUNT_CALCULATOR
CLASS ltc_discount_calculator DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO zcl_discount_calculator.
METHODS:
setup,
"! Les clients VIP recoivent toujours 15% de remise
test_vip_customer_15_percent FOR TESTING,
"! Commande > 1000 recoit 10% de remise
test_large_order_10_percent FOR TESTING,
"! Commande > 500 recoit 5% de remise
test_medium_order_5_percent FOR TESTING,
"! Commande <= 500 sans VIP ne recoit pas de remise
test_small_order_no_discount FOR TESTING,
"! Un montant negatif leve une exception
test_negative_amount_exception FOR TESTING.
ENDCLASS.
CLASS ltc_discount_calculator IMPLEMENTATION.
METHOD setup.
mo_cut = NEW zcl_discount_calculator( ).
ENDMETHOD.
METHOD test_vip_customer_15_percent.
" Arrange
DATA(lv_amount) = CONV decfloat34( 100 ).
DATA(lv_customer_type) = 'VIP'.
" Act
DATA(lv_discount) = mo_cut->calculate_discount(
iv_amount = lv_amount
iv_customer_type = lv_customer_type
).
" Assert
cl_abap_unit_assert=>assert_equals(
act = lv_discount
exp = CONV decfloat34( 15 )
msg = 'VIP avec 100 EUR devrait recevoir 15 EUR de remise (15%)"
).
ENDMETHOD.
METHOD test_large_order_10_percent.
" Arrange
DATA(lv_amount) = CONV decfloat34( 1500 ).
DATA(lv_customer_type) = 'STD'.
" Act
DATA(lv_discount) = mo_cut->calculate_discount(
iv_amount = lv_amount
iv_customer_type = lv_customer_type
).
" Assert
cl_abap_unit_assert=>assert_equals(
act = lv_discount
exp = CONV decfloat34( 150 )
msg = 'Standard avec 1500 EUR devrait recevoir 150 EUR de remise (10%)"
).
ENDMETHOD.
METHOD test_medium_order_5_percent.
" Arrange
DATA(lv_amount) = CONV decfloat34( 750 ).
DATA(lv_customer_type) = 'STD'.
" Act
DATA(lv_discount) = mo_cut->calculate_discount(
iv_amount = lv_amount
iv_customer_type = lv_customer_type
).
" Assert
cl_abap_unit_assert=>assert_equals(
act = lv_discount
exp = CONV decfloat34( '37.5' )
msg = 'Standard avec 750 EUR devrait recevoir 37.50 EUR de remise (5%)"
).
ENDMETHOD.
METHOD test_small_order_no_discount.
" Arrange
DATA(lv_amount) = CONV decfloat34( 100 ).
DATA(lv_customer_type) = 'STD'.
" Act
DATA(lv_discount) = mo_cut->calculate_discount(
iv_amount = lv_amount
iv_customer_type = lv_customer_type
).
" Assert
cl_abap_unit_assert=>assert_equals(
act = lv_discount
exp = CONV decfloat34( 0 )
msg = 'Standard avec 100 EUR ne devrait pas recevoir de remise"
).
ENDMETHOD.
METHOD test_negative_amount_exception.
" Arrange
DATA(lv_amount) = CONV decfloat34( -100 ).
" Act & Assert
TRY.
mo_cut->calculate_discount(
iv_amount = lv_amount
iv_customer_type = 'STD"
).
cl_abap_unit_assert=>fail(
msg = 'Exception attendue pour montant negatif"
).
CATCH cx_parameter_invalid INTO DATA(lx_error).
" Exception attendue
cl_abap_unit_assert=>assert_bound(
act = lx_error
msg = 'Une exception devrait etre levee"
).
ENDTRY.
ENDMETHOD.
ENDCLASS.

Limitations et bonnes pratiques

Limitations de Joule

LimitationDescriptionSolution de contournement
Logique metierNe connait pas les regles metier internesPrompts specifiques avec contexte
Tables ZNe connait pas les structures personnaliseesFournir la structure dans le prompt
Architecture complexeNe peut pas planifier une architecture completeDiviser les taches en petites parties
100% exactitudeLe code genere peut contenir des erreursToujours tester et reviser
PerformanceN’optimise pas pour des volumes de donnees specifiquesEffectuer des tests de performance
SecuritePeut introduire des failles de securiteEffectuer une revue de securite
Hors ligneNecessite une connexion InternetAlternative : snippets locaux

Bonnes pratiques pour Joule

" ✓ FAIRE : Prompts specifiques avec contexte
" Prompt : "Cree une Validation pour Travel.BeginDate :
" - Doit etre >= aujourd'hui
" - Doit etre <= EndDate
" - Message Class : ZTRAVEL, Numero 001 et 002"
" ✓ FAIRE : Travailler de maniere iterative
" 1. Faire generer le code brut
" 2. Reviser et adapter
" 3. Faire affiner des parties specifiques
" ✓ FAIRE : Toujours tester le code
" - Faire generer des tests unitaires
" - Executer manuellement
" - Verifier les cas limites
" ✓ FAIRE : Suivre Clean ABAP
" Joule connait les guidelines Clean ABAP
" Prompt : "Refactore pour suivre les conventions de nommage Clean ABAP"
" ✗ NE PAS FAIRE : Adopter aveuglement
" - Reviser chaque code genere
" - Comprendre la logique
" - Adapter aux directives de l'entreprise
" ✗ NE PAS FAIRE : Trop a la fois
" Mauvais : "Cree une application ERP complete"
" Bon : "Cree un CDS View pour l'apercu des clients"
" ✗ NE PAS FAIRE : Ignorer la securite
" - Ajouter des verifications d'autorisation
" - Verifier l'injection SQL
" - Pas de secrets dans le code

Conclusion

Joule est un assistant IA puissant pour le developpement ABAP Cloud avec une integration SAP native. Compare a GitHub Copilot, Joule offre une meilleure comprehension des concepts specifiques SAP comme RAP, CDS Views et Fiori. La combinaison de generation de code, d’explication et de generation de tests peut augmenter significativement la productivite.

Important : Joule est un assistant, pas un remplacement du savoir-faire des developpeurs. Le code genere doit toujours etre verifie, teste et adapte aux standards de l’entreprise.

Ressources supplementaires