image gauche logo Clairinfo image droite

Version du : 02/02/2007

TUTORIAL PARADOX POUR WINDOWS

Leçon 9 - On attaque la facture !

Introduction :

Bien vous avez maintenant acquis les bases du développement d'une application en Paradox il faut maintenant relever le défi de construire un formulaire de facturation robuste et fonctionnel...Nous allons donc construire une fiche maître-détail qui nous permettra de créer et de visualiser une facture.

Créer une nouvelle fiche Fact.fsl :

- Créez une nouvelle fiche vierge dans le répertoire de travail et appelez là Facture.fsl
- Donnez-lui la même taille que vos autres fiches (Menu fichier / Mise en page)
- Copiez/Collez les éléments principaux (la page ou la boite principale) de la fiche client par exemple pour démarrer sur un écran semblable à :

- En copiant ainsi des objets d'une autre fiche vous copiez non seulement l'objet graphique mais également le code. Paradox, en outre vous insère automatiquement les tables nécessaires dans votre modèle relationnel. Comme cet écran client nécessitait la table client, celle se trouve maintenant dans le modèle relationnel de la fiche.

Définir le modèle relationnel :

Dans le modèle relationnel (Menu Format) vous pouvez supprimer la table Client dont nous n'avons pas besoin puis vous allez ajouter les tables Fact.db et FactD.db en les appelant bien par l'alias :APP: comme expliqué précédemment. Vous allez ensuite définir à la souris la jointure souhaitée entre ces deux tables en faisant un cliqué glissé sur le champ NoFact depuis la table Fact.db jusqu'à la table FactD.db pour arriver à :

Paradox a déterminé pour nous le type de jointure à effectuer (ici une relation de 1 à plusieurs) en analysant la clé des 2 tables. Effectivement une facture (l'entête si vous voulez) est bien composée de plusieurs lignes détail. La flèche double (à son extrémité) vous indique visuellement qu'il s'agit d'un lien de 1 à plusieurs. Pour un lien de 1 à 1 la flèche dessinée par Paradox est simple.

Puisqu'on y est, nous allons prendre 2 précautions importantes à ce niveau en décochant pour les 2 tables l'option "Ajout automatique" dans le menu contextuel de chacune des tables (bouton droit sur la table). Comme vu auparavant nous préférons contrôler par code l'ajout d'une facture ou d'une nouvelle ligne de facture afin de pouvoir gérer la numérotation notamment.

En validant ce modèle relationnel Paradox va vous avertir qu'un objet de l'interface va perdre sa source de donnée :

Effectivement nous avions laissé un champ CodeClient sur la fiche et nous avons supprimé la table client. Ce n'est pas grave nous validons tout de même, en répondant Non, ce qui va rendre le champ CodeClient non défini. Attention le code OPAL situé sur le champ est toujours là lui ! Nous réglerons ce point plus tard.

Placer les champs de l'entête de facture

Nous commençons par ré affecter le premier champ non défini sur le N° de facture (Selection à la souris, bouton droit, Définir le champ). Le modèle relationnel nous permet d'atteindre tous les champs de toutes les tables présentes. Nous choisissons la table Fact.db et le champ NoFact. Nous modifions dans la foulée le code OPAL sur l'événement ChangeValue pour obtenir

method changeValue(var eventInfo ValueEvent)
if Self<>"" then
 disabledefault
 msgStop("Attention !","Vous ne devez pas modifier ce champ...")
endif
endMethod

Nous plaçons ensuite les autres champs pour arriver à quelque chose proche de :

Pas de grande difficulté, Le champ TypeFact est défini comme une liste déroulante et affichera les choix FACTURE ou AVOIR. Le champ Taux TVA pointe sur une table de référence Tva.db. Concernant les listes déroulantes, les données pourraient bien sûr provenir de tables (il y a pour cela 2 grandes techniques, l'utilisation d'un TCursor ou l'assignation directe d'un champ d'une table comme datasource de la liste) mais pour simplifier nous l' avons ici simplement définie dans la conception de la liste. Point important, une liste déroulante n'empêche pas la saisie d'une autre valeur donc pour verrouiller les entrées possibles il faudra installer un modèle sur le champ.

Pour information un champ de type liste déroulante ne permet pas d'afficher un libellé et de récupérer un code associé pour le stockage. C'est bien dommage et cela oblige à quelques pirouettes en OPAL si l'on souhaite obtenir ce résultat pourtant classique.

Utiliser des modèles de saisie

Paradox vous permet de définir des modèles de saisie soit au niveau de la table (voir l'écran de restructuration dans les options sur un champ), soit sur une fiche. Notre préférence va à la fiche afin de faciliter la maintenance. Il est plus facile de faire évoluer une fiche et de la transmettre au client que de faire évoluer les tables qui contiennent les données du client. Pour placer un modèle sur un champ il suffit de le sélectionner et d'accéder à ses propriétés. Vous passez sur l'onglet Modèle où vous cliquez sur <Ajouter un modèle personnalisé>. Cette boite de dialogue a beaucoup bougé entre les versions de Paradox mais les principales options utiles sont les suivantes :

Dans notre exemple nous définissons pour le champ TypeFact le modèle {AVOIR,FACTURE}. Autre avantage de ce type de modèle, il suffit de taper une lettre pour voir sa saisie complétée.

Utiliser les formats monétaires ou numériques

Pour nos champs Prix, Montant (ligne de facture) ainsi que Total HT, Total TVA et Total TTC il faudra demander un format Win monnaie afin de faire apparaître l'euro. Pour le taux de TVA on préféra un format numérique afin de faire apparaître les 2 décimales.

Utiliser un objet cadre de table

Pour afficher nos lignes de facture nous allons utiliser un cadre de table, un des plus beaux objets fournis par Paradox tant sa souplesse est inégalée. Il suffit de sélectionner l'objet dans la barre d'outils puis de dessiner un rectangle à la souris à l'emplacement souhaité. Par le menu contextuel (bouton droit) vous pouvez définir la table associée. Ici nous allons récupérer les champs de la table FactD.db afin d'obtenir :

Nous avons ici simplement changé les propriétés de couleur de fond ou de fonte, adapté les libellés d'entête de colonnes et ajouté un ascenseur vertical tout cela étant accessible via les propriétés des différents objets. Nous avons également pris soin de demander un affichage centré pour les champs concernés qu'ils soient autonomes ou dans le cadre de table (propriété onglet Texte / centré). Pour la grille du cadre de table nous avons également demandé le "Diviseur d'enregistrement" (case à cocher dans les propriétés du cadre de table onglet Grille) afin d'obtenir un trait horizontal entre chaque enregistrement à l'exécution.

Cet objet cadre de table apparaît vraiment fantastique lorsque l'on voit avec quelle facilité on peut :

Il suffit à nouveau d'avoir testé d'autres outils de développement pour comprendre l'extraordinaire qualité des composants natifs de Paradox !

Vous pouvez à ce stade tester l'exécution de votre fiche (par <F8>) mais comme nous n'avons pas adapté le code peu de chose vont fonctionner correctement.

Gérer le comportement de la fiche en objectpal

Nous allons maintenant régler les différent points pour rendre notre fiche pleinement fonctionnelle

La création d'une facture ou d'une ligne de facture

Le bouton + comme la touche <Inser> doivent permettre d'ajouter une facture ou une ligne de facture en fonction de la position du curseur. Si celui-ci est dans le cadre de table nous lancerons la création d'une ligne de facture sinon nous créerons une facture. Nous disposons maintenant dans notre bibliothèque d'une routine centralisée de création de clé autoincrémentée, GetNextID, que nous allons bien sûr réutiliser mais nous souhaiterions également disposer d'une routine capable d'incrémenter un N° de ligne pour nos lignes de facture ou plus généralement pour tout cadre de table.

Ajout d'une méthode IncrementeLigne dans notre bibliothèque

Insérez la méthode suivante dans la bibliothèque Lib.lsl :

method IncrementeLigne(vUio UiObject) LongInt
Var
  vNoL LongInt
  P TCursor
EndVar

;Récup N° ligne

vNoL=0
P.attach(vUio)
P.Home()
while not P.Eot()
 vNoL=vNoL+1
 P.NextRecord()
endwhile
P.close()

return vNoL+1

endmethod 

Cette méthode prend en paramètre un Uiobject, ici un cadre de table, attache un TCursor dessus et balaye les lignes en incrémentant une variable compteur. Ici nous transmettons une variable par valeur donc notre Uiobject ne serait pas affecté par les éventuelles modifications que nous ferions sur cette variable, contrairement à un passage de paramètre par référence que nous verrons plus loin.

Remarque :

Il nous faut ici envisager le cas où le gentil utilisateur n'en fait qu'à sa tête et change de son côté les N° de ligne, par exemple pour intervertir 2 lignes. Ce qui peut très bien arriver c'est qu'il ne renumérote pas correctement ses lignes et que notre routine nous renvoie un N° déjà utilisé. Ceci va provoquer une "violation de clé" au moment de la validation dans la base car Paradox va refuser fort logiquement cette insertion. Au regard de la définition de la table FactD, notamment de sa clé primaire, nous ne pouvons pas avoir 2 lignes de facture avec les champs NoFact et NoLigne identiques.

Concrètement notre gentil utilisateur n'aura que 2 possiblités pour continuer sa saisie : corriger manuellement son N° de ligne ou supprimer la ligne ce qui va provoquer un recalcul général des N° de ligne.

Si l'intégrité de nos données n'est pas ici remise en cause il faudra réfléchir à une solution de contournement pour éviter d'en arriver à cette violation de clé.

Mise à disposition des routines de la bibliothèque

Comme nous pensons utiliser plusieurs routines de cette bibliothèque fréquemment nous décidons de l'ouvrir à l'ouverture de la fiche. Nous déclarons donc une variable Lib globale à la fiche (donc dans la section var de la fiche). Pour faire propre nous choisissons de fermer notre librairie dans le close de la fiche :

Var
 Lib Library
endVar
Uses Objectpal
 "Lib.lsl"
endUses

Ici nous utilisons une autre forme possible pour appeler des méthodes de notre bibliothèque. Plutôt que de déclarer chaque routine, comme nous l'avions fait jusqu'ici, nous référençons notre librairie par son nom.

method open(var eventInfo Event)
if eventInfo.isPreFilter() then
 ;// Ce code s'exécute pour chaque objet de la fiche
else
 ;// Ce code s'exécute seulement pour la fiche
 if not Lib.open(":App:Lib") then
  errorshow("Impossible d'ouvrir la librairie Lib")
  return
 endif
 Lib.OpenForm()
endIf
endMethod
method close(var eventInfo Event)
if eventInfo.isPreFilter() then
 ;// Ce code s'exécute pour chaque objet de la fiche
else
 ;// Ce code s'exécute seulement pour la fiche
 Lib.close()
endIf
endMethod

Nous pouvons donc maintenant utiliser comme bon nous semble les routines de notre bibliothèque sans avoir besoin de l'ouvrir à chaque fois.

Le "bouton" + de la barre d'outils (l'image en fait)
method mouseClick(var eventInfo MouseEvent)
Var
 P,Plig TCursor
EndVar

if not isEdit() then
 action(DataBeginEdit)
endif

if FactD.arrived then

;//----------------------------------------
;//  Ajout d'une nouvelle ligne de facture
;//----------------------------------------

;// Supprimer les filtres éventuels
FactD.DropGenFilter()

;// Ajout d'une ligne
PLig.attach(FactD)
PLig.edit()
PLig.end()
PLig.InsertAfterRecord()
PLig.NoFact=NoFact
PLig.NoLigne=Lib.IncrementeLigne(FactD)
PLig.endedit()

FactD.resync(PLig)
FactD.ForceRefresh()

CodeProduit.Moveto()

else

;//----------------------------------------
;//    Ajout d'une nouvelle facture
;//----------------------------------------

;// Supprimer les filtres éventuels
NoFact.DropGenFilter()

;// Ajout d'une nouvelle facture

action(DataInsertRecord)
NoFact=Lib.GetNextID(1)

;// Valeurs par défaut

DateFact=today()
TypeF="FACTURE"
TauxTVA=19.6

CodeClient.Moveto()

endif

endmethod

Nous testons si nous sommes positionnés sur le cadre de table (propriété arrived) auquel cas nous partons sur la création d'une ligne de facture, sinon nous créons une nouvelle facture. Notez la précaution concernant la présence éventuelle de filtres posés par l'utilisateur et qui pourraient perturber le bon déroulement du processus. Par précaution nous supprimons ces filtres.

Nous enchaînons sur la suppression d'une ligne de facture (nous allons interdire la suppression d'une facture)

Le "bouton" - de la barre d'outils (l'image en fait)
method mouseClick(var eventInfo MouseEvent)
Var
 PLig TCursor
 vNoL LongInt
EndVar

;// On ne supprime pas de facture

if not FactD.arrived then
 MsgStop("Attention !","On ne supprime pas de facture, on peut par contre faire un avoir...")
 return
endif

;// Confirmation

if MsgQuestion("Confirmation :","Supprimer cette ligne de facture")<>"Yes" then
 return
endif

;// Traitement

if not isEdit() then
 action(DataBeginEdit)
endif

FactD.action(DataDeleteRecord)

;// Renuméroter les lignes

vNoL=0
PLig.attach(FactD)
Plig.Home()
PLig.edit()
while not PLig.Eot()
 vNoL=vNoL+1
 PLig.NoLigne=vNoL
 PLig.NextRecord()
endwhile
PLig.endedit()

FactD.resync(Plig)
FacTD.ForceRefresh()

CodeProduit.MoveTo()

MAJFact()

endmethod

Ici nous rencontrons plusieurs difficultés. En premier lieu nous souhaitons interdire la suppression d'une facture. Ensuite Il faut s'assurer de la bonne re numérotation des lignes après cette suppression et enfin il faut s'occuper du re calcul de la facture.

Le premier point se règle rapidement comme vu plus haut.

Le seond point recèle quelques pièges car si l'on cherchait à atteindre le résultat par une boucle scan par exemple on risquerait d'être surpris du résultat. En effet comme nous changeons la clé des enregistrements, ceci provoque potentiellement le déplacement physique des enregistrements dans la table et le scan pointe alors sur un autre enregistrement qu'initialement. Pour ce type de traitement il faut privilégier la boucle while. Nous avons en outre adopté une technique classique en effectuant le travail directement sur la table par un TCursor en arrière plan en nous déconnectant donc de l'interface utilisateur. Notez l'usage de la méthode resync qui permet de synchroniser un uiobject avec un TCursor. Ceci nous permet, une fois le travail effectué de mettre à jour l'affichage.

Le calcul de la facture

Il nous faut maintenant écrire le code qui va calculer notre facture, d'abord en ligne avec notamment l'opération Qté*Prix=Montant HT puis en cumul avec le calcul du TotalHT, de la TVA et du TotalTTC. Nous séparons donc notre code en 2 routines MAJLigne() et MAJFact() que nous plaçons au niveau fiche afin de les rendre accessibles à nos différents champs :

method MAJFact()
Var
 P TCursor
 vHT,vTVA,vTTC Number
EndVar

SetMouseShape(MouseWait)
action(DataPostRecord)

;// Initialiser les variables

vHT=0
vTVA=0
vTTC=0

;// Supprimer les filtres

FactD.DropGenFilter()

;// Balayer les lignes pour calculer le Total HT

P.Attach(FactD)

Scan P for P.Montant<>"" and P.Montant<>0 :
 vHT=vHT+P.Montant
EndScan

P.close()

vHT=vHT.round(2)

;// TVA et TTC

vTVA=vHT*TauxTVA/100
vTVA=vTVA.round(2)

vTTC=vHT+vTVA
vTTC=vTTC.round(2)

;// Mettre à jour l'entête

DmPut("Fact","TotalHT",vHT)
DmPut("Fact","TotalTVA",vTVA)
DmPut("Fact","TotalTTC",vTTC)

action(DataPostRecord)
action(DataUnlockRecord)

SetMouseShape(MouseArrow)

endMethod
method MAJLigne()
Var
 vHT,vTHT Number
EndVar

active.action(DataPostRecord)

;// Le CodeProduit est obligatoire

if CodeProduit="" then
 MsgInfo("Message :","Impossible de calculer la ligne sans un CodeProduit !")
 DmPut("FactD","Montant","")
 return
endif

if Qte<>"" then
 vHT=Qte*Prix
 vHT=vHT.round(2)
 if vHT<>0 then
  DmPut("FactD","Montant",vHT)
 endif
else
 vHT=HT
 vHT=vHT.round(2)
 DmPut("FactD","Montant",vHT)
endif

active.action(DataPostRecord)

MAJFact()

endMethod

La fonction DmPut est très utile pour mettre à jour des champ d'une table sans déclencher le code des événements des uiobjets associés sur la fiche. En effet, prenons le cas du calcul en ligne : nous voulons déclencher ce calcul aussi bien sur un changement de Qté que de Prix ou de Montant. Le risque est alors d'avoir des événements ChangeValue qui s'appellent en boucle. Le changement de Qté provoque un changevalue qui recalcule le Montant de la ligne qui provoque un change value qui appelle... En procédant comme nous l'avons fait nous pouvons maintenant très tranquillement appeler notre routine MAJLigne() sur tous les ChangeValue concerné (Qté , Prix, Montant) :

method changeValue(var eventInfo ValueEvent)
DoDefault
MAJLigne()
endMethod

Nous avons bien pensé à intégrer un recalcul de la facture (MAJFact) lors de la suppression d'une ligne de facture mais il faut également penser au cas d'un changement de taux de TVA

method changeValue(var eventInfo ValueEvent)
DoDefault
MAJFact()
endMethod
Attention ici à un bug pénible de Paradox (toutes les versions sont concernées) !

Lorsqu'un champ a une table de référence et que l'utilisateur choisit une valeur par CTRL-<Espace> l'événement ChangeValue n'est pas déclenché !! Pour contourner ce problème nous pouvons construire notre propre dialogue de lookup (nous le verrons dans la leçon suivante) ou placer le code suivant sur la méthode action du champ concerné :

method action(var eventInfo ActionEvent)
if eventinfo.id()=DataLookUp then
  doDefault
  Self=self
endif
endMethod

Ce code provoque en cas d'appel à la fonctionnalité lookup (table de référence) une nouvelle affectation de valeur au champ pour déclencher cette fois le ChangeValue...

Avant de commencer vos premiers tests je vous suggère d'ouvrir votre table NoPiece.Db afin de vérifier qu'elle contient bien l'enregistrement suivant :

ID NomPiece NoPiece TableControle
1 N°Facture 0 Fact.db

Insérez en outre les taux de TVA usuels dans la table Tva.db :

TauxTVA LibelleTVA
2.10 TVA à 2,1 %
5.50 TVA à 5.5%
19.60 TVA à 19.60

Dernières petites précautions avant les premiers tests

Les champs de cumuls

Il vaut mieux empêcher les utilisateurs d'atteindre nos champs cumuls Total HT,Total TVA,Total TTC. Un moyen rapide consiste simplement à désactiver l'arrêt tabulation pour ces 3 champs (Accessible dans les propriétés d'exécution). Il n'est alors plus possible de modifier manuellement ces champs.

Contrôler les raccourcis claviers

Comme vu précédemment il faut absolument empêcher les utilisateurs d'utiliser directement des raccourcis claviers qui passeraient outre notre gestion. Nous ajoutons donc une méthode keyphysical au niveau fiche. nous pouvons recopier celle de la fiche Client.fsl elle marchera ici à l'identique !

method keyPhysical(var eventInfo KeyEvent)
Var
 sChar String
EndVar

if eventInfo.isPreFilter() then

;// Ce code s'exécute pour chaque objet de la fiche :

sChar=eventInfo.vChar()

switch
case sChar="VK_INSERT" :
  disableDefault
  imInser.MouseClick()
Case sChar="VK_DELETE" and eventInfo.isControlKeyDown() :
  disableDefault
  imSupp.MouseClick()
case sChar="VK_ESCAPE" :
  disableDefault
  imExit.MouseClick()
case sChar="VK_F1" or sChar="VK_F2" or sChar="VK_F3" or sChar="VK_F4" :
  disabledefault
case sChar="VK_F5" :
  disableDefault
  imRech.MouseClick()
case sChar="VK_F6" :
  disableDefault
  imRechS.MouseClick()
case sChar="VK_F7" or sChar="VK_F9" :
  disabledefault
case sChar="VK_F10" :
  disableDefault
  if isEdit() then
    active.action(DataCancelRecord)
  endif
case sChar="VK_F11" or sChar="VK_F12" :
  disabledefault
endSwitch

else
;// Ce code s'exécute seulement pour la fiche :

endif
endMethod

Tester les boutons de la barre d'outils

Et oui , tout fonctionne sans intervention de notre part (hormis l'ajout et la suppression), magique Paradox !

Conclusion

Voici à quoi pourrait ressembler votre écran de facturation à ce stade. Tout devrait fonctionner correctement mais certaines fonctionnalités importantes sont encore manquantes comme une recherche aisée de produits ou de clients ou comme la création rapide d'un avoir, l'impression de la facture etc...

 

Haut de page    Précédent    A Suivre...

© Copyright 2000-2007 , Clairinfo ® , http://www.clairinfo.fr