I. Introduction▲
English version of the article here :
OpenERP : Module TG_POS_ENHANCED - Or how to override the module Point Of SaleOpenERP : Module TG_POS_ENHANCED - Or how to override the module Point Of Sale
Le module Point Of Sale, le point de vente d'OpenERP, est à l'origine plutôt basique. C'est-à-dire qu'il permet de vendre des produits et c'est tout.
Pour autant, lorsqu'on l'utilise au sein d'une société comme logiciel principal, il s'avère être trop succinct en l'état, et demande certains aménagements. C'est d'ailleurs là qu'on peut se rendre compte réellement de la souplesse d'OpenERP et de la qualité de l'ORM. En fait, je trouve pour ma part que c'est un logiciel génial, car il permet précisément de faire tout ce que vous voulez sur une base très bien organisée et très bien pensée. Ce n'est que mon avis, mais il est partagé par beaucoup de monde au sein de la communauté OpenERP.
Comme tous les logiciels/progiciels, il n'est pas exempt de défauts, mais ils peuvent être corrigés assez rapidement, et on peut étendre ses capacités ou ses fonctions à l'infini.
En l'occurrence, dans le cas qui m'a occupé pendant pas mal de temps, nous souhaitions que les vendeurs puissent se connecter avec un seul compte, qu'ils puissent accéder aux fichiers clients pour les modifier ou en créer de nouveaux et qu'ils puissent avoir également une vue sur l'historique de leurs achats. Nous souhaitions également pouvoir vendre des packs de produits « à la demande » et appliquer des remises diverses. Également nous avons imaginé pouvoir communiquer avec tous les magasins en leur envoyant un simple message qui s'afficherait directement dans le point de vente.
Voici donc, après de nombreuses heures de recherches et développements, un module qui ajoute ces fonctions au point de vente, le module tg_pos_enhanced
Mes connaissances en OpenERP étant tout de même limitées, il est probable que je n'aie pas utilisé l'ORM ou JQuery au maximum. Cependant, cet article vous permettra peut-être de comprendre certains fonctionnements ou concepts du système, et devrait vous aiguiller dans le développement de vos modules.
Si toutefois vous décelez une anomalie ou une erreur, ou si vous souhaitez éclaircir un point, n'hésitez pas à en discuter sur le forum.
Pour rappel, le point de vente étant la principale application utilisée par les vendeurs, nous souhaitions que toutes ces fonctions soient accessibles directement depuis le point de vente lui-même, c'est-à-dire sans être obligé de se déconnecter pour ouvrir une autre application.
II. Avertissement▲
Le module tg_pos_enhanced correspond à certains besoins et il est possible que votre société n'ait pas les mêmes, c'est pourquoi vous pourriez trouver telle ou telle fonction inutile dans votre cas, mais libre à vous ensuite de modifier le module pour l'adapter.
Vous aurez en tous cas, une base supplémentaire sur laquelle vous appuyer, d'autant plus que je vais vous expliquer au fur et à mesure le fonctionnement du module ainsi que certaines fonctions importantes.
Du fait de la conception du module tg_pos_enhanced, le point de vente ne fonctionnera plus en mode « Off-line », c'est-à-dire que comme le module fait des appels à la base de données d'OpenERP, il nécessite bien sûr une connexion quasi-permanente avec celle-ci.
C'est le choix que j'ai fait car dans le fonctionnement actuel du point de vente, toutes les données sont chargées à l'ouverture de la session. Cela peut être jouable si vous avez peu de produits, mais le module tg_pos_enhanced permettant , entre autres, de sélectionner, modifier ou créer des clients, il est préférable de ne pas les charger tous à l'ouverture du point de vente, surtout si vous en avez plusieurs dizaines de milliers. D'autre part, le module permet également d'afficher l'historique des ventes, il n'était donc pas pensable de charger toutes ces données dans le navigateur (LocalStorage).
III. Présentation du module▲
Pour des raisons pratiques, le module a été décomposé en quatre modules :
- le module tg_partner_firstname : un module très simple, il ajoute un champ « prénom » aux clients ;
- le module tg_pos_packs : c'est le module des packs customisables ;
- le module tg_pos_message : c'est le module de messagerie interne au point de vente ;
- le module tg_pos_enhanced : c'est le module principal.
III-A. Le module tg_partner_firstname▲
Quelques lignes de Python et quelques lignes de XML suffiront pour ce module qui rajoute simplement un champ prénom dans la table res_partner.
Les noms sont formatés en majuscules et les prénoms sont formatés en minuscules avec la première lettre en majuscule.
III-B. Le module tg_pos_packs▲
Ce module permet de créer des packs à partir des produits déjà existants.
Le pack créé devient un « service », il ne générera pas de mouvement de stock, il peut contenir plusieurs items (autant que vous le voulez), chaque item peut contenir plusieurs produits (autant que vous le voulez).
Lors de la vente, on choisira un produit par item, et on pourra choisir également la variante du produit (couleur, taille, etc.)
C'est le prix du pack qui sera affiché dans la commande, les produits quant à eux auront leur prix ramené à 0.00 euro et ils généreront des mouvements de stock.
III-C. Le module tg_pos_message▲
Il permet de créer et planifier des messages au format HTML qui seront affichés dans les points de vente sélectionnés pendant une période déterminée.
Ceci est utile dans le cas d'une société avec plusieurs magasins afin de pouvoir communiquer rapidement avec ceux-ci.
III-D. Le module tg_pos_enhanced▲
Il reprend et utilise toutes les fonctions des modules précédents.
Le module tg_pos_enhanced apporte les fonctions et modifications suivantes :
- la fonction « caissiers »: elle permet de faire des ventes en utilisant le compte « manager » du point de vente sans avoir à se déconnecter/reconnecter à chaque fois qu'un vendeur fait une vente ;
- les fonctions de gestion des clients : elles permettent de sélectionner, modifier ou ajouter des clients sans sortir du point de vente ;
- l'historique des achats : une fois le client sélectionné, on peut visualiser les achats qu'il a faits. Cela permettra, par exemple, de vérifier que le client a bel et bien acheté le produit qu'il ramène en cas de dysfonctionnement (SAV) ;
- le ticket de caisse : il présente le détail des produits, les taxes et les remises ainsi que le montant cumulé des achats du client ;
- les « Custom Packs » : ce sont des packs de produits. Vous pouvez choisir les produits qui composent le pack ainsi que les variantes de produits s'il y en a ;
- les remises spéciales : comme le compte « manager » est utilisé par tout le monde, nous avions besoin que les remises globales appliquées soient contrôlées par le gérant du magasin. Cette fonction lui permet donc d'appliquer une remise spéciale à un client. Pour cela, il devra saisir un mot de passe prévu à cet effet, et il devra préciser le motif de la remise ;
- La messagerie interne : les messages sont affichés directement dans le point de vente. Ils peuvent être programmés pour apparaître une seule fois ou toutes les X heures ;
- le retour de produits : cette fonction permet de retourner un produit en générant un mouvement de stock. Dans le cas présent, une commande négative ne peut pas aboutir, car nous souhaitions qu'un produit retourné fasse l'objet au minimum d'un échange ;
IV. Détail du module tg_partner_firstname▲
Ce module permet tout simplement de rajouter le champ « prénom » (firstname) aux clients (res.partner)
En effet, OpenERP n'a pas prévu ce champ et le problème, c'est que lorsqu'on utilise le champ de recherche pour trouver un client en entrant trois ou quatre lettres d'un nom, les résultats retournés ne correspondront pas toujours à vos attentes.
Par exemple, si vous cherchez le client « Richet » en entrant « ric » dans le champ pour filtrer la recherche, OpenERP peut vous renvoyer ceci :
- richard alain ;
- mathurot eric ;
- stephan brice ;
- richet alain.
Bien que cette fonction puisse être parfois utile, elle ne l'est pas lorsqu'il s'agit de trouver des gens par leur nom. C'est là que le champ « prénom » intervient. Il faudra bien sûr renseigner les champs « nom » et « prénom » correctement, mais une fois dans le point de vente, lorsque vous utiliserez le champ de recherche, la recherche portera alors seulement sur le champ « nom » des clients.
Pour voir les effets de ce module, allez dans le module « Sale » (vente) et cliquez sur le menu « Partners » (clients) puis cliquez sur le bouton « Éditer ». Le formulaire ci-dessous apparaît :
Comme précisé auparavant, les noms et prénoms sont formatés. Nous avions détecté que les vendeurs entraient les noms/prénoms n'importe comment (majuscules, minuscules), c'est pourquoi nous avons appliqué la règle suivante : tous les noms en majuscules et les prénoms en minuscules avec la première lettre en majuscule.
Au niveau développement, cela ne nécessite pas beaucoup de temps, comme le montre la classe ci-dessous :
On rajoute simplement un champ puis on surcharge les fonctions write() et create() pour prendre en compte le formatage des noms/prénoms.
class
inherit_res_partner
(osv.osv):
_name=
'
res
.
partner
'
_inherit=
'
res
.
partner
'
def
write
(self, cr, uid, ids, vals, context=
None
):
v_name =
None
v_firstname =
None
if
vals.get
('
name
'
):
#
name
to
Uppercase
v_name =
vals['
name
'
].strip
()
vals['
name
'
] =
v_name.upper
()
if
vals.get
('
firstname
'
):
#
firstname
capitalized
v_firstname =
vals['
firstname
'
].strip
()
vals['
firstname
'
] =
v_firstname.title
()
result =
super
(inherit_res_partner,self).write
(cr, uid, ids, vals, context=
context)
return
result
def
create
(self, cr, uid, vals, context=
None
):
v_name =
None
v_firstname =
None
if
vals.get
('
name
'
):
#
name
to
Uppercase
v_name =
vals['
name
'
].strip
()
vals['
name
'
] =
v_name.upper
()
if
vals.get
('
firstname
'
):
#
firstname
capitalized
v_firstname =
vals['
firstname
'
].strip
()
vals['
firstname
'
] =
v_firstname.title
()
result =
super
(inherit_res_partner,self).create
(cr, uid, vals, context=
context)
return
result
_columns =
{
'
firstname
'
: fields.char
('
Firstname
'
, size=
128
),
}
Vous avez ci-dessus l'illustration de la surcharge des fonctions natives de l'ORM. Ici, nos fonction write() et create() seront exécutées avant d'appeler les fonctions de l'ORM grâce à la fonction super()
Voir les fonctions de l'ORM dans le Memento Technique
Pour ce qui est de la vue, une simple surcharge de la vue d'origine pour ajouter le champ :
<!--
Vue
formulaire
-->
<
record
model
=
"
ir.ui.view
"
id
=
"
view_tg_partner_form
"
>
<
field
name
=
"
model
"
>
res.partner<
/
field
>
<
field
name
=
"
name
"
>
view.tg.partner.form<
/
field
>
<
field
name
=
"
view_type
"
>
form<
/
field
>
<
field
name
=
"
inherit_id
"
ref
=
"
base.view_partner_form
"
/
>
<
field
name
=
"
arch
"
type
=
"
xml
"
>
<
field
name
=
"
name
"
position
=
"
after
"
>
<
h5
class
=
"
oe_edit_only
"
>
<
label
for
=
"
firstname
"
/
>
<
/
h5
>
<
h2
>
<
field
name
=
"
firstname
"
/
>
<
/
h2
>
<
/
field
>
<
/
field
>
<
/
record
>
Le champ est ici placé juste après le champ « nom » d'origine.
Comme vous pourrez le voir dans le fichier tg_partner_view.xml, j'ai également surchargé les vues « tree » et « kanban ».
V. Détail du module tg_pos_packs▲
Le module tg_pos_packs dépend du module product_variant_multi développé par Akretion. Vous le trouverez également dans le fichier ZIP dans le paragraphe « Téléchargement »
Il existe dans OpenERP un module qui permet de créer des nomenclatures. Mais ces nomenclatures sont figées, elles ne permettent pas de choisir « à la demande » les produits qui les composent.
C'est pourquoi j'ai imaginé une sorte de Pack Customisable.
Le Pack Customisable a la particularité de pouvoir embarquer autant de produits que l'on souhaite. Lors de la vente, il suffira de sélectionner parmi les produits.
Évidemment, il faudra composer le pack avec des produits ayant sensiblement le même prix.
V-A. Créer un Custom Pack▲
En tout premier lieu, il faut que vous ayez inséré des produits dans le module « Sale » (vente).
Pour créer un pack, il faut procéder comme pour créer un produit courant. Attention, n'utilisez pas le menu « Product template » du module product_variant_multi, qui lui, permet de créer des modèles de produits avec des variantes, pour ensuite générer des produits.
Comme vous le voyez dans l'image ci-dessus, lorsque vous cochez la case « Custom Pack », le type de produit devient un « service », il n'est pas modifiable, et l'onglet « Pack » apparaît.
Cliquez sur l'onglet « Pack ».
Cliquez sur le lien « Ajouter un élément » pour ajouter les produits. Ici, en fait vous ajouterez des modèles de produits (Product template). Ne vous inquiétez pas, les templates sont créés automatiquement.
Les produits sont regroupés par groupes numérotés.
Pour que vous compreniez bien le principe de fonctionnement, voici comment va apparaître le pack dans le point de vente :
Vous avez remarqué que dans le pack, deux éléments étaient dans le groupe « 2 » (en jaune).
Chaque groupe d'articles sera affiché dans une liste déroulante « Item ». Si plusieurs produits sont dans le même groupe, ils apparaîtront alors dans la même liste (item). Vous pourrez donc choisir parmi les produits de la liste et proposer plusieurs combinaisons à vos clients.
Ici, le client aura le choix entre la « Cymbale Meinl Byzance Ride 20 » et la « Zildjian K Custom Ride 20 » du pack.
Pour grouper les produits entre eux, il suffit simplement de modifier le chiffre dans le champ « Group number » du formulaire.
Afin que ce soit plus lisible, j'ai fait en sorte que les groupes apparaissent de différentes couleurs.
De la même façon, un produit peut avoir plusieurs variantes comme dans l'image ci-dessous :
Les variantes de produits apparaissent dans les listes de droite. Si un produit n'a pas de variante, il n'y aura donc pas de choix possible, la liste n'affichera que le nom du produit.
Si un produit a plusieurs variantes, il suffira alors de sélectionner la variante souhaitée par le client. Ici la variante est une couleur.
Le pack contiendra un « Sonor Essential Floor Tom 16x16 » et le client pourra choisir la couleur :
- Piano Black ;
- Dark Forest ;
- Maple 46 ;
- Blue fade ;
- Brown fade ;
- Green fade.
Évidemment, ces produits existent déjà dans le point de vente, ils peuvent aussi être vendus à l'unité.
Toutes les combinaisons s'offrent à vous.
Si vous vendez des pièces de voiture et que vous souhaitez que dans votre pack le client puisse acheter deux batteries + un train de roues, vous aurez besoin alors de 3 groupes.
Vous pouvez tout a fait proposer deux fois le même groupe.
Par exemple, vous proposez des batteries de plusieurs marques différentes et plusieurs couleurs. Vous pouvez faire un premier groupe avec :
- Batterie Fulmen 12V (rouge, vert, bleu) ;
- Batterie Varta 12V (grise, rouge) ;
- Batterie Feu Vert 12V (noire) : pas de variante.
Créez ensuite le deuxième groupe à l'identique et votre client pourra repartir avec une Fulmen rouge et une Feu Vert noire s'il le désire.
Rajoutez dans votre pack, le train de roues dans le groupe « 3 » est l'affaire est dans le sac.
Vous pouvez mettre autant d'articles que vous le souhaitez dans un Custom Pack.
A l'installation, le module crée une catégorie « Custom Packs » dans le point de vente. Les packs sont automatiquement rangés dans cette catégorie lorsque vous les créez.
VI. Détail du module tg_pos_message▲
Ce module vous permet donc de créer des messages qui apparaîtront dans le point de vente.
Voici un cas concret d'utilisation :
Un client utilise un produit que vous lui avez vendu, il se blesse, s'empoisonne, ou que sais-je encore … bref, le produit doit être retiré de la vente de toute urgence.
Il vous suffira alors de créer un nouveau message et de le programmer pour qu'il s'affiche toutes les heures pendant les 3 prochains jours.
Autre cas concret : les ventes du produit X sont en baisse, incitez les vendeurs de vos magasins à les mettre en avant en leur affichant un message tous les matins pendant une semaine.
Ou tout simplement pour envoyer des messages d'encouragement à vos équipes de vendeurs ou leur souhaiter la nouvelle année !
Le menu du module apparaît seulement pour l'administrateur (base.group_no_one) dans le menu Point de vente/Outils
Vous pouvez définir une date de début et une date de fin, décider d'afficher le message seulement une fois ou bien de l'afficher toutes les X heures.
Vous pouvez sélectionner les points de vente destinataires du message.
Une icône sera affichée sur le message selon le type de message, un peu comme les boîtes de dialogue dans les logiciels.
Le message peut être au format HTML. Vous pouvez y mettre des images (hébergées) en tenant compte de la taille moyenne des écrans de vos boutiques. N'affichez pas une image de dimension supérieure aux plus petites résolutions d'écran.
Les messages sont récupérés à l'ouverture de session du point de vente, et à chaque fois que vous créez une nouvelle commande.
Il faut que l'utilisateur clique sur le bouton avec la croix en haut à droite pour fermer le message et continuer les ventes.
A ce moment-là, la date de lecture est enregistrée dans la base de données (datetime), elle servira ensuite lors de la récupération des nouveaux messages selon l'intervalle de temps que vous aurez défini si c'est le cas.
VII. Détail du module tg_pos_enhanced▲
C'est le plus gros morceau.
Ici il a été question de surcharger le module original (point_of_sale) afin qu'en cas de mise à jour d'OpenERP les modifications apportées ne soient pas écrasées.
Voici donc le point de vente revisité. Tous les modules présentés auparavant trouveront leur utilisation ici.
VII-A. Le panneau latéral gauche (#leftpane)▲
Pour commencer, voici le panneau latéral gauche modifié (#leftpane)
On retrouve (1) le module de caissiers, qui est cette fois-ci intégré au module tg_pos_enhanced lui-même.
Au dessus du panier (2), le panneau du client sélectionné avec les boutons de recherche et de création des clients.
VII-A-1. A propos du module des caissiers▲
Une petite modification a été apportée.
Dans le cas où vous auriez créé plus d'un caissier, à chaque nouvelle commande la liste des caissiers se réinitialise et vous devez sélectionner à nouveau votre nom de caissier dans la liste .
Ceci, afin d'éviter que tous les vendeurs n'utilisent le même caissier.
La partie de code qui permet ceci :
if
(nbCashiers >
1
){
var
new_option =
'
<option
value="nobody"></option>\n
'
;
self.
$('
#cashier-select
'
).
html
(content +
new_option);
}
Si on détecte plus d'un caissier, on rajoute un élément vide à la liste des caissiers. C'est lui qui sera automatiquement sélectionné à chaque nouvelle commande comme le montre le code ci-dessous.
if
(nbCashiers >
1
){
$('
#cashier-select
'
).
val
('
nobody
'
);
globalCashier =
'
nobody
'
;
cashier_change
(globalCashier);
}
VII-B. Trouver un client▲
Pour trouver un client, il suffit de cliquer sur l'icône dans le panneau de gauche.
Vous pouvez rechercher un client par la première lettre de son nom ou à l'aide du formulaire de recherche.
VII-B-1. Recherche par lettre▲
Cliquez sur une lettre pour afficher les clients dont le nom commence par cette lettre.
Le panneau de gauche (1) est grisé. Cliquez sur une lettre (2), les clients s'afficheront dans le tableau (3). Lorsque vous sélectionnez un client, le panneau se ferme automatiquement le panneau de gauche n'est plus grisé et les produits réapparaissent.
Vous pouvez rafraîchir le tableau à tout moment (4), fermer le panneau ou voir le nombre de clients dans le tableau.
VII-B-2. Recherche à l'aide du formulaire▲
Entrez les premières lettres du nom recherché puis appuyez sur la touche « Entrée » de votre clavier ou cliquez sur la loupe.
La fonction de recherche a été modifiée.
Dans OpenERP, le champ de recherche utilise le mot clé SQL « ilike » mais il est traduit en fait comme ceci « ilike '%mot%' ».
Le fait de mettre le signe % avant et après le mot recherché, permet de trouver les noms d'objet où le mot recherché apparaît à n'importe quel endroit. Cela pourrait être pratique par moment, mais en l'occurrence, cela ne nous aidait pas trop.
En rajoutant le signe = devant ilike, on obtient l'expression « ilike 'mot%' ». C'est l'effet que nous recherchions.
Pour effectuer une recherche dans la base de données, j'utilise la fonction ci-dessous :
//
get
data
from
DB
var
fetch =
function
(model,
fields,
domain,
ctx){
return
new
instance.
web.
Model
(model).
query
(fields).
filter
(domain).
context
(ctx).
all
();
}
;
On voit ici l'attribut « filter(domain) ».
Il suffira d'écrire un filtre adéquat pour récupérer les clients selon la première lettre de leur nom ou selon le mot entré dans le formulaire de recherche.
var
l_filter =
[
[
'
customer
'
,
'
=
'
,
true
]
,
[
'
name
'
,
'
=ilike
'
,
letter +
'
%
'
]
]
;
if
(letter =
=
'
0-9
'
){
l_filter =
[
[
'
customer
'
,
'
=
'
,
true
]
,
'
|
'
,
'
|
'
,
'
|
'
,
'
|
'
,
'
|
'
,
'
|
'
,
'
|
'
,
'
|
'
,
'
|
'
,
[
'
name
'
,
'
=ilike
'
,
'
0%
'
]
,
[
'
name
'
,
'
=ilike
'
,
'
1%
'
]
,
[
'
name
'
,
'
=ilike
'
,
'
2%
'
]
,
[
'
name
'
,
'
=ilike
'
,
'
3%
'
]
,
[
'
name
'
,
'
=ilike
'
,
'
4%
'
]
,
[
'
name
'
,
'
=ilike
'
,
'
5%
'
]
,
[
'
name
'
,
'
=ilike
'
,
'
6%
'
]
,
[
'
name
'
,
'
=ilike
'
,
'
7%
'
]
,
[
'
name
'
,
'
=ilike
'
,
'
8%
'
]
,
[
'
name
'
,
'
=ilike
'
,
'
9%
'
]
]
;
}
Au cas où on cliquerait sur le bouton « 0-9 » pour rechercher un client dont le nom commence par un chiffre, le filtre l_filter est alors redéfini pour récupérer tous les clients dont le nom commence par n'importe quel chiffre.
La variable « letter » correspond ici soit à la lettre qui a été cliquée, soit au mot entré dans le champ de recherche grâce à cette fonction :
this
.
$('
#input_search
'
).
keypress
(function
(e) {
if
(e.
which =
=
13
) {
var
l =
$(this
).
val
().
trim
();
if
(l !
=
'
'
){
$('
#input_search
'
).
val
(l);
self.
get_clients
(l);
}
}
}
);
VII-C. Un client sélectionné▲
Lorsqu'un client est sélectionné, il apparaît au dessus du panier comme le montre l'image ci-dessous.
Ici, le client a accumulé 1040,00 euros d'achat dans nos boutiques. C'est un « VIP »
VII-C-1. Éditer ou créer un client▲
Vous pouvez modifier les informations du client en cliquant sur l'icône avec un crayon ou créer un nouveau client en cliquant sur l'icône avec le signe +.
Le formulaire ci-dessous apparaît :
Lorsque vous validez le formulaire, les informations sont envoyées directement dans la base de données et le client est automatiquement sélectionné pour la commande en cours.
Pour écrire dans la base de données depuis JavaScript, on peut utiliser la fonction ci-dessous :
var
Partners =
new
instance.
web.
Model
('
res.partner
'
);
Partners.
call
('
write_partner_from_pos
'
,
[
cid,
cname,
cfirstname,
czip,
cphone,
cmobile,
cemail,
ccomment]
,
undefined,
{
shadow:
true
}
)
.
fail
(function
(clientId){
alert
('
Error
:
customer
has
not
been
created
nor
updated
'
);
}
)
.
done
(function
(clientId){
var
sel_client =
cname.
toUpperCase
() +
'
'
+
cfirstname;
$('
#selected-customer-name
'
).
html
(ellipseName
(sel_client));
$('
#input_partner_id
'
).
val
(clientId);
global_letter =
sel_client[
0
]
;
self.
get_clients
(global_letter);
self.
pos.
get
('
selectedOrder
'
).
set_partner_id
(clientId);
self.
select_client
(clientId,
cname.
toUpperCase
(),
cfirstname,
c_montant_cumule);
self.
hide_form_client
();
}
);
Je ne vais pas trop entrer dans les détails, mais la fonction call() ci-dessus est appliquée à la fonction write_partner_from_pos() qui se trouve dans le fichier Python :
#
créer
/
modifier
un
client
depuis
le
POS
def
write_partner_from_pos
(self, cr, uid, cid, cname, cfirstname, czip, cphone, cmobile, cemail, ccomment, context=
None
):
if
context is
None
:
context =
{}
name =
cname.upper
()
firstname =
cfirstname.capitalize
()
#
mise
a
jour
client
client_id =
int
(cid)
if
client_id !
=
0
:
self.write
(cr, uid, client_id, {
'
name
'
: name,
'
firstname
'
: firstname,
'
zip
'
: czip,
'
phone
'
: cphone,
'
mobile
'
: cmobile,
'
email
'
: cemail,
'
pos_comment
'
: ccomment,
}, context=
context)
idClient =
client_id
#
creation
client
else
:
idClient =
self.create
(cr, uid, {
'
name
'
:name,
'
firstname
'
:firstname,
'
zip
'
:czip,
'
phone
'
:cphone,
'
mobile
'
:cmobile,
'
email
'
:cemail,
'
pos_comment
'
: ccomment,
'
company_id
'
:False
,
}, context=
context)
return
idClient
Cette fonction est utilisée pour créer ou modifier un client. En gros, quand on envoie un ID de client, c'est une mise à jour, si il n'y a pas d'ID ou que l'ID est égal à 0, c'est une création.
VII-C-2. A propos du champ « Comment »▲
Le champ « comment » (commentaire) est un champ spécial. Le texte que vous entrez ici sera visible sur le ticket de caisse.
Voici une des applications possibles :
Par exemple, au Portugal, un ticket de caisse doit présenter un numéro fiscal du client. C'est donc ce champ qui vous servira à renseigner ce numéro.
VII-D. L'historique des achats du client sélectionné▲
Lorsqu'un client est sélectionné et qu'il a déjà fait des achats dans vos magasins, vous pouvez voir l'historique de ses achats en cliquant sur l'icône ci-dessus.
Le panneau de gauche est à nouveau grisé et les ventes du client apparaissent dans le panneau principal.
Cliquez sur une commande pour voir le détail.
VII-D-1. Pour récupérer les commandes d'un client▲
Ici on appelle une fonction Python depuis le fichier JavaScript du Point De Vente avec la fonction call():
show_histo
: function
(){
var
self =
this
;
//
remove
sales
lines
$('
#sales-list
tr
'
).
remove
();
var
cid =
$('
#input_partner_id
'
).
val
();
if
(cid &
&
cid !
=
0
){
var
Mread =
new
instance.
web.
Model
('
pos.order
'
);
Mread.
call
('
get_partner_orders
'
,
[
cid]
,
undefined,
{
shadow:
true
}
)
.
fail
(function
(orders){
alert
('
Partner
orders
:
get_partner_orders
=
failed
'
);
}
)
.
done
(function
(orders){
if
(orders.
length >
0
){
//
la
suite
du
code
}
}
) ;
}
}
,
Dans le code ci-dessus, nous appelons la fonction get_partner_orders() qui se trouve dans le fichier tg_pos_enhanced.py en lui envoyant l'ID du client.
def
get_partner_orders
(self, cr, uid, partner_id, context=
None
):
if
context is
None
:
context =
{}
result =
[]
o_ids =
self.search
(cr, SUPERUSER_ID, [('
partner_id
'
, '
=
'
, int
(partner_id))])
if
o_ids:
for
o_id in
o_ids:
order =
self.browse
(cr, SUPERUSER_ID, o_id)
res_o =
{
'
id
'
: o_id,
'
pos_reference
'
: order.pos_reference,
'
date_order
'
: order.date_order,
'
name
'
: order.name,
'
state
'
: order.state,
'
cashier_name
'
: order.cashier_name,
'
discount
'
: order.discount,
'
amount_total
'
: order.amount_total
}
result.append
(res_o)
return
result
La fonction parcourt la table pos.order et renvoie les commandes du client s'l y en a.
Attention, dans le cas d'une utilisation d'OpenERP en multi-sociétés, un utilisateur ne peut pas récupérer les commandes opérées par un autre utilisateur, c'est pourquoi nous utilisons ici la constante SUPERUSER_ID qui permet d'exécuter des actions sur l'ORM en tant qu'administrateur (à utiliser avec prudence).
Pour cela, il faut rajouter au début du fichier Python :
from
openerp import
SUPERUSER_ID
VII-E. Vendre des Custom Packs▲
Comme nous l'avons vu auparavant, il est possible à présent de vendre des packs de produits personnalisables.
Lorsqu'on sélectionne un Custom Pack dans les produits disponibles, le formulaire ci-dessous apparaît :
C'est ici que vous pouvez sélectionner les produits et variantes en fonction des désirs du client.
Lorsqu'on clique sur le bouton « Ajouter le Pack à la commande », le pack apparaît dans le panier comme ci-dessous :
Comme vous le voyez dans l'image ci-dessus, le nom du pack et des éléments qui le composent ont été modifiés. Ils présentent le caractère ■ pour le pack et ├ pour les éléments (1).
De plus, comme vous le voyez, seul le pack affiche son prix, les éléments du pack sont eux à 0,00 euros (2). Ceci permettra de faire afficher tous ces produits sur le ticket de caisse et permettra également de sortir les produits du stock, sauf le pack lui-même qui je vous le rappelle est un service et qui ne génère pas de mouvement de stock.
Sur la page de paiement, rien de changé :
Par contre, sur le ticket de caisse, le Custom Pack apparaît comme dans le panier :
VII-E-1. Comment ça marche ?▲
Eh oui, c'est probablement la question que vous vous posez.
Ce n'est pas très simple mais je vais essayer de vous expliquer comment ça fonctionne.
Tout d'abord, le module tg_pos_packs ajoute un champ à la table product.product. Le champ 'is_pack' :
_columns =
{
'
is_pack
'
: fields.boolean
('
Custom
Pack
'
),
'
pack_ids
'
: fields.one2many
('
product
.
pack
'
, '
product_id
'
, '
Items
in
the
pack
'
),
}
Lorsqu'un produit est un Custom Pack, le champ 'is_pack' est alors mis à 'True'.
Ensuite, dans le fichier tg_pos.js, on détectera si un produit est un Custom Pack ou non. Pour cela, j'ai dû modifier la fonction originale du point de vente addProduct() :
addProduct
: function
(product,
options){
options =
options |
|
{
}
;
var
attr =
product.
toJSON
();
//
if
is_pack
=
false,
this
is
a
real
product
//
we
send
it
to
the
order
if
(attr.
is_pack =
=
false
){
attr.
pos =
this
.
pos;
attr.
order =
this
;
var
line =
new
module.
Orderline
({
}
,
{
pos:
this
.
pos,
order:
this
,
product:
product}
);
if
(options.
quantity !
=
=
undefined){
line.
set_quantity
(options.
quantity);
}
if
(options.
price !
=
=
undefined){
line.
set_unit_price
(options.
price);
}
var
last_orderline =
this
.
getLastOrderline
();
if
( last_orderline &
&
last_orderline.
can_be_merged_with
(line) &
&
options.
merge !
=
=
false
){
last_orderline.
merge
(line);
}
else
{
this
.
get
('
orderLines
'
).
add
(line);
}
this
.
selectLine
(this
.
getLastOrderline
());
}
else
{
//
this
is
a
Pack
!!
this
.
show_screen_pack
(attr.
name);
//
get
templates
this
.
get_templates
(attr.
id);
}
}
,
On récupère l'attribut 'attr.is_pack' du produit. Si c'est à 'false', c'est un produit courant et il est ajouté à la commande normalement.
Si c'est un Custom Pack, on affiche alors le formulaire des Custom Packs et on récupère les éléments/variantes des produits.
Lorsque ensuite on valide le formulaire du Custom Pack pour l'envoyer à la commande, on va renommer les produits pour qu'ils apparaissent dans le panier comme un Custom Pack et on va mettre le prix des produits à 0,00 euros.
add_products_from_pack
: function
(){
var
self =
this
;
var
nb_items =
$('
#input_nb_items
'
).
val
();
var
selectedOrder =
this
.
pos.
get
('
selectedOrder
'
);
var
pack_id =
$('
#pack_product_id
'
).
val
()
var
pack_product =
null
;
pack_product =
self.
pos.
db.
get_product_by_id
(parseInt
(pack_id));
//
add
pack
product
to
the
order
if
(pack_product){
var
is_pack_previous =
pack_product.
is_pack;
var
pack_name =
pack_product.
name;
pack_product.
is_pack =
false
;
pack_product.
name =
'
■
'
+
pack_product.
name;
var
m_pack_product =
new
module.
Product
(pack_product);
selectedOrder.
addProduct
(m_pack_product);
var
cur_oline =
selectedOrder.
getSelectedLine
();
cur_oline.
product.
set
('
is_pack
'
,
is_pack_previous);
pack_product.
is_pack =
is_pack_previous;
pack_product.
name =
pack_name;
}
for
(var
i =
1
;
i <=
nb_items;
i+
+
){
var
field =
$('
#v_
'
+
i);
var
product_id =
parseInt
(field.
val
());
var
product =
self.
pos.
db.
get_product_by_id
(product_id);
//
add
products
to
the
order
if
(product){
//
change
name
(suffix)
+
price
=
0.00
var
previous_name =
product.
name;
var
previous_price =
product.
price;
product.
name =
'
├
'
+
product.
name;
product.
price =
'
0.00
'
;
var
m_product =
new
module.
Product
(product);
selectedOrder.
addProduct
(m_product);
//
change
name
+
price
back
product.
name =
previous_name;
product.
price =
previous_price;
}
}
;
self.
hide_screen_pack
();
}
,
Pour le pack lui-même et les éléments qui le composent, on récupère les produits dans la base de données temporaire chargée au démarrage du point de vente (LocalStorage), on modifie le nom et le prix pour les éléments, puis on les envoie à la commande comme des produits courants en rappelant la fonction d'origine :
var
m_pack_product =
new
module.
Product
(pack_product);
selectedOrder.
addProduct
(m_pack_product);
Ensuite on renomme à nouveau les produits tels qu'ils étaient à l'origine.
C'est un peu tordu, mais c'est pour l'instant la meilleure façon que j'aie trouvée.
VII-E-2. Caractéristiques spéciales d'un Custom Pack▲
Vous ne pouvez plus sélectionner les éléments du Custom pack dans le panier. Et vous ne pouvez pas changer la quantité d'un Custom Pack. La raison est simple : il s'agit d'afficher les Custom Packs distinctement dans le panier et sur le ticket de caisse.
Vous ne pouvez donc pas supprimer un élément du pack, en revanche vous pouvez supprimer un pack complet. Dans ce cas, les éléments du pack sont également supprimés.
C'est pourquoi j'ai également modifié la fonction de sélection des éléments du panier.
selectLine
: function
(line){
if
(line){
var
product_name =
line.
product.
attributes.
name;
var
is_pack =
line.
product.
attributes.
is_pack;
if
(product_name[
1
]
!
=
'
├
'
){
//
this.selected_orderline.set_selected(undefined);
if
(line !
=
=
this
.
selected_orderline){
if
(this
.
selected_orderline){
this
.
selected_orderline.
set_selected
(false
);
}
this
.
selected_orderline =
line;
this
.
selected_orderline.
set_selected
(true
);
}
if
(is_pack =
=
false
){
$('
#numpad-return
'
).
removeAttr
('
disabled
'
);
$('
#return_img
'
).
attr
('
src
'
,
'
tg_pos_enhanced/static/src/img/return_product.png
'
);
}
else
{
$('
#numpad-return
'
).
attr
('
disabled
'
,
'
disabled
'
);
$('
#return_img
'
).
attr
('
src
'
,
'
tg_pos_enhanced/static/src/img/disabled_return_product.png
'
);
}
}
else
{
$('
#numpad-return
'
).
attr
('
disabled
'
,
'
disabled
'
);
$('
#return_img
'
).
attr
('
src
'
,
'
tg_pos_enhanced/static/src/img/disabled_return_product.png
'
);
}
}
else
{
this
.
selected_orderline =
undefined;
$('
#numpad-return
'
).
attr
('
disabled
'
,
'
disabled
'
);
$('
#return_img
'
).
attr
('
src
'
,
'
tg_pos_enhanced/static/src/img/disabled_return_product.png
'
);
}
}
,
Dans la fonction ci-dessus, tous les produits dont le nom commence par '├' (les éléments du pack) ne sont plus sélectionnables.
Également, et on verra ça plus loin, si un produit est un Custom Pack, il ne peut pas être retourné. On ne peut retourner que des produits à l'unité.
VII-E-3. Supprimer un Custom Pack▲
Lorsqu'on supprime un produit, on modifie en fait la quantité pour la ramener à zéro.
En réalité, quand un produit doit être retiré du panier, la quantité devient 'remove'.
Lorsqu'on veut supprimer un Custom Pack, il faut également supprimer les éléments qu'il contient.
Pour cela, j'ai également dû modifier la fonction d'origine du point de vente set_quantity() :
if
(quantity =
=
=
'
remove
'
){
//
when
we
remove
a
pack
//
we
have
too
remove
items
too
!
if
(product_name[
0
]
=
=
'
■
'
){
var
o_lines =
[
]
;
var
cur_order =
this
.
pos.
get
('
selectedOrder
'
);
var
sel_line =
cur_order.
selected_orderline;
(cur_order.
get
('
orderLines
'
)).
each
(_.
bind
( function
(item) {
return
o_lines.
push
(item);
}
,
this
));
var
flag_cur =
false
;
for
(var
i =
0
,
len =
o_lines.
length;
i <
len;
i+
+
){
//
when
we
found
current
line
if
(o_lines[
i]
=
=
sel_line){
flag_cur =
true
;
}
//
we
delete
items
of
the
pack
if
(flag_cur =
=
true
){
var
cur_product_name =
o_lines[
i]
.
product.
attributes.
name;
if
(cur_product_name[
1
]
=
=
'
├
'
){
this
.
order.
removeOrderline
(o_lines[
i]
);
}
else
{
//
until
we
found
that
this
is
not
an
item
of
the
selected
pack
if
(cur_product_name[
0
]
!
=
'
■
'
){
flag_cur =
false
;
}
}
}
}
//
then
we
delete
the
pack
this
.
order.
removeOrderline
(this
);
}
Donc ici c'est le même principe, on va détecter si un produit est un Custom Pack en récupérant le premier caractère de son nom.
Si le premier caractère est ■ , alors on va en premier lieu supprimer toutes les lignes des produits en dessous qui commencent par le caractère ├ . Ce sont les éléments du pack.
Puis enfin, on supprimera le pack lui-même.
VII-F. Retourner un produit▲
Le retour d'un produit est aussi un point intéressant du module.
Imaginez, vous vendez un Custom Pack à un client qui contient 2 paires de baguettes Vic Firth 5A (5A c'est la taille).
Le client revient une semaine plus tard et vous explique que les 5A sont vraiment trop lourdes pour lui et il souhaite que vous lui échangiez une paire de 5A contre une paire de 7A plus fines et plus légères.
Pas de problème, le client est roi.
Vous allez donc retourner la paire de baguettes 5A et lui échanger contre une paire de 7A.
J'ai un peu modifié le pavé numérique. J'ai supprimé des boutons qui ne nous étaient pas utiles et j'ai rajouté un bouton « Retour de produit ».
Vous pouvez tout à fait remettre les boutons d'origine et placer le bouton « Retour de produit » où vous le souhaitez. Il vous suffira d'éditer le template NumpadWidget dans le fichier tg_pos.xml
Pour retourner un produit, il suffit d'ajouter le produit au panier, puis de cliquer sur le bouton « Retour de produit »
Ci-dessous j'ai retourné une paire de baguettes 5A et j'ai ajouté une paire de baguettes 7A au panier. Cela correspond donc à un échange de produit.
Comme on le voit dans l'image ci-dessus, la paire de baguette retournée affiche un prix négatif.
Sur la page de paiement, un produit retourné + un produit vendu = 0.
Sur le ticket de caisse, mes deux produits apparaissent comme dans le panier.
Rappel :
On ne peut pas retourner un Custom Pack.
VII-F-1. A propos du stock▲
Nous avons retourné un produit puis vendu un autre produit.
Si on va dans l'application « Warehouse » (Entrepôt), et qu'on affiche les mouvements de stock, nous pouvons voir les deux lignes qui concernent nos produits échangés.
Dans l'image ci-dessus, on voit que la paire de baguettes 5A est venue de l'extérieur vers le stock et que la 7A est partie du stock vers le client.
Pour réussir ce tour de passe-passe, il aura suffi de modifier la fonction du point de vente create_picking() et faire en sorte que lorsqu'un prix est négatif d'inverser les emplacements de stock.
#
line
price
<
0
=
c'est
un
retour
produit
#
on
retourne
le
produit
de
sortie
->
Stock
if
line.price_subtotal <
0
:
location_id, output_id =
output_id, location_id
Ici, on inverse l'entrée et la sortie du stock si le prix est négatif.
VII-G. La remise spéciale▲
Dans nos magasins, nous avons restreint les fonctions de remises afin que les vendeurs n'appliquent pas de remises, ou plusieurs remises à un client.
Pour cela, j'ai rajouté une fonction qui permettra au responsable du magasin d'appliquer une remise spéciale. Certains des magasins appartenant à la société, les autres étant des franchises, nous souhaitions que le responsable spécifie la raison pour laquelle il a appliqué une remise à tel client.
La remise spéciale est appliquée après avoir saisi le mot de passe prévu à cet effet.
Pour cela, un champ supplémentaire a été ajouté à la table res.users
Le mot de passe n'est pas chiffré, mais il est seulement visible par l'administrateur (Technical settings)
VII-G-1. Appliquer une remise spéciale▲
Lorsque vous souhaitez appliquer une remise spéciale à un client, vous devez appeler le responsable du magasin.
Le responsable doit ensuite cliquer sur l'icône prévue à cet effet. L'écran devient noir et un formulaire apparaît pour permettre au responsable de saisir une remise.
Entrez le montant de la remise (valeur, pas un pourcentage), puis le mot de passe.
Cliquez ensuite sur « Valider »
Comme on le voit dans l'image ci-dessus, une remise de 30,00 euros a été appliquée à la commande qui a été recalculée.
Sur le ticket de caisse la remise apparaît très clairement.
VIII. Installation des modules▲
Tout d'abord, effectuez une sauvegarde de votre base de données.
Décompressez le fichier ZIP dans un répertoire et copiez les dossiers de chaque module dans votre répertoire de modules.
Créez un répertoire de modules hors de l'arborescence d'OpenERP, ceci vous permettra de vous y retrouver plus facilement.
Voir : Comment ajouter un chemin vers les modulesComment ajouter un chemin vers les modules
Veillez à installer les modules suivants dans l'ordre ci-dessous. Vous pouvez redémarrer le serveur après l'installation d'un module pour vérifier le fonctionnement.
- Installez bien sûr le module « Point Of Sale » (Point de Vente) original ;
J'ai moi-même effectué une installation d'OpenERP datant du 06/12/2013
(version 7.0-20131206-002433) et je n'ai pas utilisé les données de démonstration. Je n'ai rencontré aucun problème de fonctionnement.
- Installez le module product_vatiant_multi ;
- Installez le module tg_partner_firstname ;
- Installez le module tg_pos_packs ;
- Installez le module tg_pos_message ;
- Installez enfin le module tg_pos_enhanced ;
- Créez ensuite quelques produits, quelques clients ;
- Créez un Custom Pack à l'aide des produits créés précédemment ;
- Créez au moins un caissier pour le point de vente (Menu Point de Vente/Caissiers) ;
- Créez un mot de passe pour les remises spéciales (Menu Configuration/Utilisateurs → Onglet Point de vente) ;
- Créez un message de bienvenue pour le point de vente (Menu Point De Vente/Outils/Messages) ;
N'oubliez pas de redémarrer le serveur, puis ouvrez une nouvelle session dans le Point De Vente et utilisez les nouvelles fonctionnalités.
Une fois que vous aurez fait le tour des nouveautés, vous pourrez alors installer ces modules sur votre environnement de production.
IX. Téléchargement▲
Téléchargement : openerp_tg_pos_enhanced_package_1.0.1.zipTélécharger (197 Ko)
X. Remerciements▲
Remerciements à OpenERP SA pour leur logiciel OpenERPOpenERP.
Remerciements à Philippe Duval pour les conseils et corrections