Appliquer une promotion sur base du total de la vente ou selon une autre condition

0000003171     -      05/07/2023

Le paramétrage expliqué ici montre comment appliquer une promotion sur base du total de la vente. Si le total de la vente atteint un certain montant, alors la promotion sera appliquée. Cette promotion s'entend au sens des possibilités offertes par "Outils / Remises". Dès lors, toutes les fonctionnalités de cet écran sont disponibles et deviennent conditionnelles au montant total de la vente :

  • remise en fonction de l'article, du rayon, de la famille, ...
  • remise en % ou remise en EUR
  • remise limitée à certains clients
  • ...

Pour disposer de cette fonctionnalité, il faut d'abord ajouter cette colonne dans la table BAREMESV_PIEDS :

alter table BAREMESV_PIEDS add FILTER_KEY char(50) not null default ''
create index FILTER_KEY on BAREMESV_PIEDS(FILTER_KEY)

La colonne BAREMESV_PIEDS.FILTER_KEY est une colonne optionnelle reconnue par Mercator. Elle fonctionne conjointement avec la propriété BaremesVFilterKey du BillingEngine.

  • si BaremesVFilterKey est vide, ce qui correspond à la situation "normale", alors seules les remises correspondant à BAREMESV_PIEDS.FILTER_KEY vide sont prises en considération ;
  • si BaremesVFilterKey est non vide, ce qui correspond à la situation "application de la promotion conditionnelle", alors seules les remises où BAREMESV_PIEDS.FILTER_KEY = BaremesVFilterKey  sont prises en considération.

Le mécanisme de base va donc consister, si le total de la vente est atteint, à fixer temporairement une valeur de BillingEngine.BaremesVFilterKey dans l'événement BeforeBeforePaymentOrSave et de recalculer les prix à ce moment. Ce mécanisme peut s'appliquer à toute autre condition qu'un total minimum de la vente :

  • présence de certains articles,
  • restitution par le client d'un voucher-promo,
  • nombre d'articles achetés
  • ...

Dans cette première version du customizer, les prix sont simplement recalculés en tentant compte de la promotion dont le filtre a été fixé (dans notre exemple, ce filtre placé en pied de remise vaut MIN100).

Zoom
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Windows.Forms;
using MercatorApi;
using MercatorUi;
using MercatorExtensions;

namespace Billing
{
    public class Customizer : MercatorUi.ICustomizers.IBillingEngineCreated, MercatorUi.ICustomizers.IBillingEngineClosed
    {
        public void BillingEngineCreated(MercatorUi.Engine.Gescom.BillingEngine BillingEngine)
        {
            BillingEngine.BeforeBeforePaymentOrSave += BillingEngine_BeforeBeforePaymentOrSave;
        }

        public void BillingEngineClosed(MercatorUi.Engine.Gescom.BillingEngine BillingEngine)
        {
            BillingEngine.BeforeBeforePaymentOrSave -= BillingEngine_BeforeBeforePaymentOrSave;
        }

        private const double minTot = 100;

        private void BillingEngine_BeforeBeforePaymentOrSave(object sender, MercatorUi.Engine.Gescom.BillingEngine.BeforeBeforePaymentOrSaveEventArgs e)
        {
            MercatorUi.Engine.Gescom.BillingEngine billingEngine = (MercatorUi.Engine.Gescom.BillingEngine)sender;
            billingEngine.ChangeAllPrices();
            if (billingEngine.PiedsVRecord.TOT_TTC_DV.CompareTo(minTot, billingEngine.NDec) > 0)
            {
                bool PRIO_PV_changed = false;
                string PRIO_PV = MercatorUi.Globals.Params["PRIO_PV"];

                try
                {
                    if (PRIO_PV != "PROMOSOLDE")
                    {
                        MercatorUi.Globals.Params["PRIO_PV"] = "PROMOSOLDE";
                        PRIO_PV_changed = true;
                    }
                    billingEngine.BaremesVFilterKey = "MIN100";
                    billingEngine.ChangeAllPrices();
                }
                finally
                {
                    billingEngine.BaremesVFilterKey = null;
                    if (PRIO_PV_changed)
                        MercatorUi.Globals.Params["PRIO_PV"] = PRIO_PV;
                }
            }
        }
    }
}

Notez que via ce code, tous les prix sont recalculés avant la sauvegarde via la méthode ChangeAllPrices. Le premier appel de cette méthode permet de remettre les prix sans cette promotion spéciale, pour le cas où ce document a été modifié et que le seuil n'est plus atteint.

La méthode ChangeAllPrices peut s'appliquer en tenant compte d'une exclusion d'articles. Par exemple, pour exclure un article "frais de port" dont le S_ID serait ID_PORT :

Zoom
billingEngine.ChangeAllPrices("id_article<>'ID_PORT'");

Il est aussi possible d'exclure certains articles de ChangeAllPrices en mettant à true LIGNES_V.NON_MOD_PRIX (bit) sur les lignes correspondantes.

Si on souhaite une application plus rapide du mécanisme, par exemple après l'insertion de chaque article, il suffit d'utiliser l'événement AfterInsertItem à la place de BeforeBeforePaymentOrSave.


Dans cette seconde version du customizer, les prix des articles ne sont pas recalculés mais une simulation de promotion est effectuée. Dans le cas où cette simulation donne un total de vente inférieur au total de la vente effectuée, une ligne avec un article "promotion" est ajoutée. Le montant de cette ligne est cette différence en négatif.

Zoom
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Windows.Forms;
using MercatorApi;
using MercatorUi;
using MercatorExtensions;
using System.Linq;

namespace Billing
{
    public class Customizer : MercatorUi.ICustomizers.IBillingEngineCreated, MercatorUi.ICustomizers.IBillingEngineClosed
    {
        public void BillingEngineCreated(MercatorUi.Engine.Gescom.BillingEngine BillingEngine)
        {
            BillingEngine.BeforeBeforePaymentOrSave += BillingEngine_BeforeBeforePaymentOrSave;
        }

        public void BillingEngineClosed(MercatorUi.Engine.Gescom.BillingEngine BillingEngine)
        {
            BillingEngine.BeforeBeforePaymentOrSave -= BillingEngine_BeforeBeforePaymentOrSave;
        }

        private const double minTot = 100;
        private const string id_art_promo = "PROM51JNJT";

        private void BillingEngine_BeforeBeforePaymentOrSave(object sender, MercatorUi.Engine.Gescom.BillingEngine.BeforeBeforePaymentOrSaveEventArgs e)
        {
            MercatorUi.Engine.Gescom.BillingEngine billingEngine = (MercatorUi.Engine.Gescom.BillingEngine)sender;
            foreach (DataRow drLigne in billingEngine.LIGNES.RowsEnumerable(dr => dr["id_article"].Equals(id_art_promo)).ToArray())
            {
                billingEngine.LIGNES.Rows.Remove(drLigne);
            }
            billingEngine.UpdateAmounts();
            double totInitial = billingEngine.PiedsVRecord.TOT_TTC_DV;
            
            if (totInitial.CompareTo(minTot, billingEngine.NDec) > 0)
            {
                DataTable dtLignesCopy = billingEngine.LIGNES.Copy();
                bool PRIO_PV_changed = false;
                string PRIO_PV = MercatorUi.Globals.Params["PRIO_PV"];
                try
                {
                    if (PRIO_PV != "PROMOSOLDE")
                    {
                        MercatorUi.Globals.Params["PRIO_PV"] = "PROMOSOLDE";
                        PRIO_PV_changed = true;
                    }
                    billingEngine.BaremesVFilterKey = "MIN100";
                    billingEngine.ChangeAllPrices();
                }
                finally
                {
                    billingEngine.BaremesVFilterKey = null;
                    if (PRIO_PV_changed)
                        MercatorUi.Globals.Params["PRIO_PV"] = PRIO_PV;
                }

                double totPromo = Math.Round(totInitial - billingEngine.PiedsVRecord.TOT_TTC_DV, billingEngine.NDec);

                // remettre les valeurs initiales dans billingEngine.LIGNES
                foreach (DataRow dr in dtLignesCopy.Rows)
                {
                    DataRow drLigne = billingEngine.LIGNES.RowsEnumerable(p => p["dl_id"].Equals(dr["dl_id"])).First();
                    Api.DataRowMerge(drLigne, dr, false, new string[4] {"ID", "JOURNAL", "PIECE", "DL_ID" });
                }
                // ajouter une ligne promo le cas échéant
                if (totPromo.CompareTo(0d, billingEngine.NDec) > 0)
                {
                    int n = billingEngine.AppendLine();
                    if (!billingEngine.InsertItem(id_art_promo, billingEngine.LIGNES.Rows[n]))
                    {
                        MercatorUi.Dialogs.Stop(string.Format("Impossible d'ajouter l'article \"{0}\" !", id_art_promo));
                        e.Cancel = true;
                    }
                    billingEngine.LignesVRecords[n].DESIGNATIO = "Promo panier min. € " + minTot;
                    billingEngine.LignesVRecords[n].REMISE = 0;
                    billingEngine.LignesVRecords[n].REMISE2 = 0;
                    billingEngine.LignesVRecords[n].REMISE3 = 0;
                    billingEngine.LignesVRecords[n].REMISE4 = 0;
                    if (billingEngine.PiedsVRecord.REGIME == MercatorDatabase.RegimesEnum.Normal)
                        billingEngine.LignesVRecords[n].PU = -totPromo / (1 + (billingEngine.LignesVRecords[n].TAUX_TVA / 100));
                    else
                        billingEngine.LignesVRecords[n].PU = -totPromo;
                }
                billingEngine.UpdateAmounts();
            }
        }
    }
}

La ligne id_art_promo = "PROM51JNJT" doit être adaptée avec le S_ID d'un article "promotion".

Les lignes avec REMISE2, REMISE3 et REMISE4 doivent être retirées si ces colonnes n'existent pas dans la table LIGNES_V.

La promotion est calculée sur base du montant TTC. Si le régime de la vente est Normal, alors le montant HTVA de la promotion est calculé en utilisant le taux de TVA de l'article "promotion" utilisé. Le cas échéant, il peut être nécessaire d'ajouter plusieurs ligne "promotion" avec des taux de TVA différents au prorata des bases TVA du document.


Notes concernant de possibles exclusions ou cumuls de remises

Le paramétrage décrit ici doit être vu comme un outil mis à disposition. Le point de départ de cette mécanique se trouve dans un customizer, ce qui permet de personnaliser le calcul de la promotion conditionnelle. 

Il est par exemple possible d'exclure de cette mécanique les articles déjà en promotion par ailleurs. Dans ce cas, ce n'est pas le total du document qui devra être pris en compte, mais le total des articles hors promotion. Pour cela il faudra remplacer

billingEngine.PiedsVRecord.TOT_TTC_DV
par
Math.Round(billingEngine.LignesVRecords.Where(l => !l.PROMOSOLDE).Sum(l => l.TOTAL), billingEngine.NDec) 

Et ces lignes

billingEngine.ChangeAllPrices();
seront remplacées par 
billingEngine.ChangeAllPrices("promosolde=0");

Si on souhaite cumuler les remises "normales" et les remises conditionnelles sur base du total, il peut s'avérer nécessaire de dupliquer les promotions "normales" dans "Outils / Remises" en leur ajoutant la remise supplémentaire pour minimum de panier.

Par exemple :

  • sur le rayon A : remise de 10%
  • si le montant de la vente atteint 100 EUR, remise supplémentaire de 5%

Dans ce cas, deux pieds de barème sont créés :

  • le premier, sans filtre, avec la remise de 10% sur le rayon A
  • le second, avec filtre,et
    • en remise 1 : 10% (ici on répète donc le premier barème)
    • en remise 2 : 5%