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.

Module tg_pos_enhanced : le point de vente revisité
Module tg_pos_enhanced : le point de vente revisité

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 :

tg_partner_firstname : le champ prénom dans le formulaire client
tg_partner_firstname : le champ prénom dans le formulaire client

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.

tg_partner_firstname.py
Sélectionnez
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 :

view_tg_partner_form
Sélectionnez
<!-- 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.

Custom Pack : onglet Informations
Custom Pack : onglet Informations

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 ».

Custom Pack : onglet Pack
Custom Pack : 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 :

Custom Pack : Point de vente - sélection de produits
Custom Pack : Point de vente - sélection de produits

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 :

Custom Pack : Point de vente - choix de la variante
Custom Pack : Point de vente - choix de la variante

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

Le module de messagerie du point de vente
Le module de messagerie du point de vente

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.

Le module de messagerie du point de vente : un message
Le module de messagerie du point de vente : un message

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.

Le point de vente
Le point de vente

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) 

#leftpane
#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 .

La liste des caissiers se réinitialise à chaque nouvelle commande
La liste des caissiers se réinitialise à chaque nouvelle commande

Ceci, afin d'éviter que tous les vendeurs n'utilisent le même caissier.

Sélection d'un caissier
Sélection d'un caissier

La partie de code qui permet ceci :

tg_pos.js (ligne 2837)
Sélectionnez
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.

tg_pos.js (ligne 905)
Sélectionnez
if(nbCashiers > 1){
    $('#cashier-select').val('nobody');
    globalCashier = 'nobody';
    cashier_change(globalCashier);
}

VII-B. Trouver un client

Image non disponible

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.

Chercher un client par lettre
Chercher un client par 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

Recherche à l'aide du formulaire
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 :

tg_pos.js (ligne 430)
Sélectionnez
// 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.

tg_pos.js (ligne 1966)
Sélectionnez
 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 :

tg_pos.js (ligne 2421)
Sélectionnez
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.

Client sélectionné
Client sélectionné

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 :

Créer ou modifier un client
Créer ou modifier un client

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 :

tg_pos.js (ligne 2066)
Sélectionnez
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 :

tg_pos_enhanced.py (ligne 68)
Sélectionnez
# 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é

Image non disponible

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.

Historique des ventes
Historique des ventes

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() - tg_pos.js ligne 2469
Sélectionnez
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.

tg_pos_enhanced (tg_pos_enhanced.py ligne 295)
Sélectionnez
    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 :

SUPERUSER_ID (tg_pos_enhanced.py ligne 29)
Sélectionnez
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 :

Formulaire du Custom Pack
Formulaire du Custom Pack

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 :

Le Custom Pack dans le panier
Le Custom Pack dans le panier

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é :

Page de paiement
Page de paiement

Par contre, sur le ticket de caisse, le Custom Pack apparaît comme dans le panier :

Le ticket de caisse avec un Custom Pack
Le ticket de caisse avec un Custom Pack

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' :

tg_pos_product_pack.py (ligne 52)
Sélectionnez
_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() :

add_product() (tg_pos.js ligne 1110)
Sélectionnez
        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() (tg_pos.js ligne 1221)
Sélectionnez
        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 :

tg_pos_js ligne 1239
Sélectionnez
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.

select_line() (tg_pos.js ligne 1511)
Sélectionnez
        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() :

set_quantity() (tg_pos.js ligne 1588)
Sélectionnez
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.

Le bouton de retour de produit
Le bouton de retour de produit

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.

Echange de produit
Échange de produit

Comme on le voit dans l'image ci-dessus, la paire de baguette retournée affiche un prix négatif.

Page de paiement
Page de paiement

Sur la page de paiement, un produit retourné + un produit vendu = 0.

Ticket de caisse
Ticket de caisse

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.

Mouvements de stock
Mouvements de stock

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.

tg_pos_enhanced.py ligne 268
Sélectionnez
# 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

Image non disponible
Le champ mot de passe pour la remise spéciale

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.

Appliquer une remise spéciale
Appliquer une remise spéciale

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.

Image non disponible
Écran de remise spéciale

Entrez le montant de la remise (valeur, pas un pourcentage), puis le mot de passe.

Cliquez ensuite sur « Valider »

Remise spéciale
Remise spéciale

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.

Ticket de caisse avec remise spéciale
Ticket de caisse avec remise spéciale

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. 

  1. 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.

  1. Installez le module product_vatiant_multi ;
  2. Installez le module tg_partner_firstname ;
  3. Installez le module tg_pos_packs ;
  4. Installez le module tg_pos_message ;
  5. 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

X. Remerciements

Remerciements à OpenERP SA pour leur logiciel OpenERPOpenERP.

Remerciements à Philippe Duval pour les conseils et corrections