quote → cash
FR/EN Contact

Pricing·Avancé·11 min·Mis à jour le 05/10/2026

Guide · Pricing

Avenant négatif à 0 € : la ligne qui ne sait pas qu’elle a un prix

Retirer des licences par avenant produit une ligne à zéro, donc pas d’avoir. Trois semaines de fausses pistes, une branche morte dans la procédure standard, et l’étape Map Line Item qui manquait.

D’où vient chaque affirmation
Salesforce docsdocumenté par SalesforceTerrainconstaté sur une vraie orgRecommandéce que nous recommandonsÀ vérifiernon vérifié
Architecture du pricing contexte → procédure → prix
ENTRÉE DÉCIDER EXÉCUTER SORTIE ContextDefinition ce que le pricing voit DecisionTable si conditionnel Barème d’ajustement paliers et franchises PricebookEntry si sans condition Pricing procedure ExpressionSet Prix net sur la ligne

Scroll the diagram sideways

La règle qui garde un catalogue maintenable : une decision table seulement s’il y a une condition ou un palier. Un prix fixe vit sur le PricebookEntry. Les decision tables ajoutées par réflexe sont ce qui remplit une procédure jusqu’à son plafond.

Et la procédure qui s’exécute est la version compilée _Auto, pas la source que vous éditez.

Un client retire une licence en cours d’année. On crée l’avenant, la ligne sort avec une quantité de −1 et un montant de 0 €. Pas de montant, pas d’échéancier négatif, pas d’avoir. Le client continue de payer une licence qu’il a rendue.

L’avenant positif, lui, marche parfaitement : +2 licences, montant au prorata, facture juste. C’est cette asymétrie qui met trois semaines à s’expliquer.

Le moteur connaissait le prix

Premier réflexe : le prix est perdu quelque part. Faux.

Le détail de ligne — l’enregistrement de détail rattaché à la ligne de devis, et son équivalent sur la commande — porte le bon prix négocié, la bonne quantité négative, et la référence à la vente d’origine. Seul son montant total vaut 0. Terrain

Le moteur n’a jamais manqué d’information. Il manquait une multiplication.

La branche morte

Dans la procédure standard, le groupe qui traite les lignes « prix de la dernière transaction » est filtré sur un attribut ItemPricingSource égal à LastTransaction. Cet attribut est alimenté par un champ de source de prix sur la ligne de devis et de commande. Sur notre org en Summer ’26, ce champ n’existait que sur l’actif. L’attribut était donc vide partout, et le groupe ne s’exécutait jamais. Terrain

Les lignes normales passent par la branche opposée, « différent de LastTransaction », que la valeur vide satisfait. C’est exactement pour ça que l’avenant positif marchait et pas le négatif.

On a réanimé le filtre pour tester. La ligne est restée à 0 : la formule du groupe multipliait un prix net vide au niveau de la ligne. Le prix était sur le détail, pas sur la ligne. Ce n’était pas un filtre mal réglé, c’était une étape absente.

Les fausses pistes, et pourquoi elles échouent

  • Un hook Apex dans le procedure plan. Pour poser le prix avant la procédure. Le hook écrit bien dans le contexte, mais la section pricing suivante relit les enregistrements et écrase tout. Terrain
  • Poser le prix à la main sur la commande d’avenant. Le « Reprice All » l’efface, puis le verrouille : « You can’t edit the Unit Price because the Order Product has related Order Product Detail records ». Terrain
  • Accuser le devis. La commande fait exactement pareil. Le problème est dans la procédure, pas dans l’objet d’entrée. Terrain

La réponse : Map Line Item

C’est le support Salesforce qui a donné la clé. Avec Advanced Transaction Detail Line Pricing activé, la procédure doit contenir l’élément Map Line Item, en deuxième position, juste après les réglages de pricing. Il fait le lien entre la ligne et ses détails. Sans lui, les valeurs ne circulent pas entre les deux niveaux. Doc

Une sandbox de démonstration, sans ce réglage avancé, n’avait pas le problème. C’est ce qui rendait le bug si difficile à reproduire — et ce qui explique qu’il ne soit documenté nulle part comme un symptôme.

Côté métadonnée, l’élément s’appelle BreakdownLineMapping : c’est une valeur du type d’action d’une étape d’expression set, en API 64 et au-delà. Ses paramètres ne sont documentés nulle part. On les a repris d’un dépôt public de référence : une section par attribut, entrée vers sortie, de l’élément de transaction vers son détail, dix-huit sections au total. Terrain

Résultat : l’avenant −1 sort au prorata, au bon prix, et l’échéancier négatif suit. Terrain

Le piège Winter ’27 qui a tout bloqué

En production, en Winter ’27, impossible de sauvegarder la moindre procédure contenant une étape de prix catalogue : « Ensure that the lookup table in the ListPrice step is valid ». Même une procédure neuve, générée par Salesforce. Terrain

La cause : la pricing recipe, créée avant Winter ’27, avait un sous-type d’usage vide. Winter ’27 valide les tables de lookup par rapport à une recette de sous-type Revenue Cloud. Le champ n’est pas modifiable sur une recette existante. Il faut créer une deuxième recette, avec les mêmes mappings et le bon sous-type. Terrain

Et deux réflexes qui évitent de perdre une journée :

  • Une version active ne se modifie pas. On ajoute une version de rang supérieur ; la précédente reste en place pour revenir en arrière.
  • Ne désactivez jamais la version en production avant que la nouvelle soit active. Une procédure sans version active, c’est tout le pricing à l’arrêt — et le symptôme, toutes les lignes au prix catalogue, ressemble au bug qu’on vient de corriger.

Ce qui reste ouvert

  • Le détail reprend le prix catalogue, pas le prix négocié. Quand rien ne lui fournit la dernière transaction. Selon la politique d’avoir, ça se décide avec la finance, pas en configuration.
  • La facturation d’un swap natif. Refuse les lignes de type SwapIn ou SwapOut dès qu’il y en a une dans l’appel. Filtrer ces lignes permet de facturer le nouveau bundle, pas de créditer l’ancien. Terrain

Le symptôme voisin — un renouvellement qui sort au prix catalogue — a une autre cause, et sa propre démonstration : voir Renouvellement au prix catalogue.

Composants Salesforce

Sélectionnez-en un pour ouvrir son détail.

Sur quoi ça repose

  • Le problème n’apparaît qu’avec Advanced Transaction Detail Line Pricing activé. Vérifiez ce réglage avant toute chose : sans lui, ce n’est pas votre bug.
  • Le champ de source de prix est apparu depuis sur notre org. Le comportement de la branche « dernière transaction » peut donc changer avec une release : à vérifier sur la vôtre.
  • Les paramètres de BreakdownLineMapping viennent d’un dépôt public, pas de la documentation officielle. Ils fonctionnent, ils ne sont pas garantis.
  • Constaté sur une org en Summer ’26 / Winter ’27.

Ressources Salesforce