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
| Aspect | Joule | GitHub Copilot |
|---|---|---|
| Editeur | SAP | GitHub/Microsoft |
| Integration | ADT, BAS, SAP Build | VS Code, Eclipse |
| Connaissance SAP | Native (documentation SAP) | Generique (Open Source) |
| Support RAP | Tres bon (Behavior, CDS, Service) | Bon (a partir d’exemples de code) |
| ABAP Cloud | Optimise pour Released APIs | Mixte (aussi Classic ABAP) |
| Clean ABAP | Integre | Doit etre entraine |
| Licence | Licence SAP requise | 10-19$/mois |
| Hors ligne | Non (base sur le cloud) | Non (base sur le cloud) |
| Protection des donnees | Cloud 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 → Preferences2. ABAP Development → Joule3. Activer "Enable Joule AI Assistant"4. Selectionner la connexion systeme (BTP ou S/4HANA Cloud)5. Authentification avec utilisateur SAP6. OK → Redemarrer ADTOuvrir le panneau Joule :
View → Other → ABAP → Joule AssistantOu : 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 commandeJoule genere :
- Table de base de donnees :
@EndUserText.label : 'Order Header"@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE@AbapCatalog.tableCategory : #TRANSPARENT@AbapCatalog.deliveryClass : #Adefine 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;}- 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}- Behavior Definition :
managed implementation in class zbp_i_order unique;strict ( 2 );with draft;
define behavior for ZI_Order alias Orderpersistent table zorder_hdrdraft table zorder_hdr_dlock master total etag LastChangedAtauthorization 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; }}- 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 decroissantJoule 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' // CompletedExplication 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 rechercheoptimisee 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 negatifMETHOD 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 → ExceptionJoule genere :
"! @testing ZCL_DISCOUNT_CALCULATORCLASS 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
| Limitation | Description | Solution de contournement |
|---|---|---|
| Logique metier | Ne connait pas les regles metier internes | Prompts specifiques avec contexte |
| Tables Z | Ne connait pas les structures personnalisees | Fournir la structure dans le prompt |
| Architecture complexe | Ne peut pas planifier une architecture complete | Diviser les taches en petites parties |
| 100% exactitude | Le code genere peut contenir des erreurs | Toujours tester et reviser |
| Performance | N’optimise pas pour des volumes de donnees specifiques | Effectuer des tests de performance |
| Securite | Peut introduire des failles de securite | Effectuer une revue de securite |
| Hors ligne | Necessite une connexion Internet | Alternative : 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 codeConclusion
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
- GitHub Copilot pour ABAP - IA alternative pour le developpement ABAP
- RAP Basics - Bases du ABAP RESTful Application Programming Model
- Clean ABAP - Guidelines pour un code ABAP maintenable
- Definition ABAP Cloud - Qu’est-ce que ABAP Cloud ?