I. Introduction

English version of the tutorial here :
OpenERP Tutorial: Module creation and modification of the Point Of SaleOpenERP Tutorial: Module creation and modification of the Point Of Sale

La programmation d'OpenERP est une tâche assez compliquée. Il faut, tout d'abord, connaître plusieurs langages de programmation et il faut aussi avoir une bonne idée du fonctionnement d'OpenERP.

La documentation est assez limitée et très succincte. Il y a très peu de forums ou de blogs spécialisés sur ce progiciel de gestion, et la quasi-totalité sont en anglais.

Les quelques rares tutoriels qu'on trouve sur le Web sont plutôt limités à des problèmes ponctuels.
Ils ne permettent pas de réaliser entièrement un module complexe, ils vous permettront tout juste de rajouter un bouton ou une fonction par-ci, par-là. Ce n'est pas suffisant.

Le tutoriel que je vous propose est le fruit de plusieurs centaines d'heures de bourrage de crâne de tout ce que j'ai pu trouver dans la (maigre) documentation officielle et sur le Web, ainsi que quantité de lignes de code, d'innombrables tests, bogues et autres joyeusetés. Vous savez de quoi je parle, vous êtes également passés par là.

C'est suite à une demande d'un client que j'ai eu pour mission de réaliser un module permettant la création de caissiers pour le Point De Vente.

Le contexte
OpenERP est installé sur un serveur Debian.
Le client commercialise des produits par le biais de sociétés franchisées. Chaque société a un accès à OpenERP, qui est configuré en Multi-Sociétés.
Chaque société dispose d'un PC dans son local, connecté au Point De vente d'OpenERP. Et chaque société emploie plusieurs caissiers.

La demande du client
Comment avoir la possibilité d'effectuer des achats par le Point De Vente sur le même poste (PC) sans avoir à créer une session lorsqu'un caissier veut réaliser une vente ?

Le Point De Vente permet effectivement de réaliser des ventes avec plusieurs vendeurs. Pour cela, rien de plus facile, il suffit de créer des utilisateurs dans la société, de les mettre dans le groupe Point De Vente/Utilisateur et de leur attribuer un Point De Vente et le tour est joué.
Seulement, chaque vendeur devra alors ouvrir une session pour réaliser la vente.

Ces fonctions, pourtant natives, ne convenaient pas au client.

J'ai donc réalisé un module qui permet de créer un autre genre d'utilisateurs, les caissiers. Vous verrez plus loin que cela n'a rien de très compliqué. Là où ça se complique, c'est la partie du module, le Module Web, qui agit dans le Point De Vente et qui permet d'ouvrir une seule session le matin et d'effectuer les ventes par plusieurs caissiers sans avoir à quitter le Point De Vente, et donc de fermer la session précédente pour en ouvrir une nouvelle.

Enfin, la dernière instruction du client : le caissier est obligatoire. La vente ne doit donc pas être possible si les Points De Vente n'ont pas de caissiers.

Voici donc de quoi il s'agit.
Je vous montrerai les différentes étapes qui vous permettront par la suite de créer vous-même vos propres modules.

Le code méritera certainement quelques améliorations ou optimisations, n'hésitez pas à me faire part de vos observations qui me permettront également d'avancer encore plus dans les méandres de la programmation d'OpenERP.

II. Prérequis

Pour se plonger dans ce tutoriel, il faut une bonne connaissance des langages de programmation utilisés dans OpenERP :

  • Python ;
  • XML ;
  • JavaScript ;
  • Qweb/JQuery ;
  • commandes Linux.

III. Principes de base

Pour schématiser, on peut dire que la réalisation d'un module classique se fait en trois étapes.

  • La création des fichiers d'initialisation du module.
  • La création de l'objet en Python.
  • La création des vues en XML.

En ce qui concerne le Point De Vente, il s'agit d'un module « Web ». Lorsque vous ouvrez une session sur le Point De Vente, OpenERP disparaît pour faire apparaître l'interface du Point De Vente. Il faut ensuite fermer le Point De Vente pour retourner sous OpenERP.

C'est un cas particulier qui nécessitera d'autres étapes de programmation. Le Point de Vente n'étant pas intégré à OpenERP, c'est toute son interface Web qui nous occupera.

IV. Les outils nécessaires

Je vous livre ici les outils logiciels que j'utilise, libre à vous de les utiliser ou de trouver des logiciels équivalents.

  • WinSCPWinSCP : un client SFTP/FTP très agréable à utiliser et qui permet de naviguer facilement dans l'arborescence des fichiers.
  • PuTTYPuTTY : un client Telnet/SSH qui permettra d'exécuter des commandes Linux sur le serveur (WinSCP le permet aussi, mais avec certaines limitations)
  • SublimeSublime : un éditeur de texte très bien fait. Il porte bien son nom.
  • FirefoxFirefox avec l'extension FirebugFirebug. Indispensable pour le débogage JavaScript.

OpenERP est installé sur un serveur de développement Debian.
Je travaille sur un PC sous Windows 7 pro.

Configurer WinSCP
Nous allons configurer WinSCP pour que lorsque nous double-cliquerons sur un fichier, il s'ouvre directement dans votre éditeur de code préféré.

  • Ouvrez WinSCP.
  • Allez dans le menu Voir, puis Préférences.
  • Dans la fenêtre qui s'affiche, cliquez sur Éditeur dans la colonne de gauche.
  • Dans le cadre Préférences éditeurs, cliquez sur Ajouter.
  • Dans la fenêtre qui apparaît, cochez Éditeur externe puis sélectionnez le fichier exécutable de votre éditeur de texte.
  • Dans Utiliser cet éditeur pour les fichiers suivants sélectionnez le filtre *.* (tous les fichiers).
  • Validez.

Dans ce tutoriel, nous partons du principe qu'OpenERP est installé sur votre serveur de développement Linux/Debian et que nous travaillons sur un PC sous Windows.
Ici j'ai installé OpenERP Version 7.0-20130703-231023 (la version date donc du 03 juillet 2013).
Si vous travaillez directement sur le serveur, vous adapterez les consignes qui concernent l'administration du serveur (fichiers, droits, etc.). La construction du module reste la même.

V. Les étapes de la réalisation

Création du module interne à OpenERP

  • Création des fichiers d'initialisation du module.
  • Création du fichier Python qui contiendra l'Objet
  • Création des vues tableau/formulaire.
  • Création d'un menu pour le groupe POS/Manager
  • Création des droits pour le module
  • Création des règles d'enregistrement pour le module
  • Création de l'icône pour le module

Création du module du Point De Vente

  • Création du fichier JavaScript qui contiendra les actions du module
  • Création des éléments dans l'interface (liste déroulante, labels, etc.) dans un fichier XML
  • Création du fichier de style *.css pour le design des éléments

Internationalisation du module

  • Création du modèle pour la traduction (*.pot)
  • Création du fichier en français (*.po)

Avertissement :
Dans ce tutoriel, le Point De Vente sera appelé POS (Point Of Sale en anglais)
Le code sera écrit en anglais, mais tous les labels seront ensuite traduits.
Le module sera appelé pos_cashier.

Une fois terminé, le module POS Cashiers apparaîtra dans la liste des modules installés, comme dans l'image ci-dessous.

Le module pos_cashier installé
Le module pos_cashier installé

Attention
Le tutoriel pourra vous sembler très long, mais j'ai essayé de vous décortiquer toutes les étapes de la réalisation d'un module et je me suis efforcé également d'expliquer du mieux que j'ai pu tout le code source et les différents fichiers qui le composent.

VI. Structure du module

Voici l'arborescence des fichiers qui composent le module pos_cashier.

Arborescence du module pos_cashier
Arborescence du module pos_cashier

Le répertoire i18n :
Il contient les fichiers de traduction du module.

Le répertoire security :
Il contient les fichiers de contrôle d'accès et les règles pour les enregistrements.

Le répertoire static :
Il contient la partie « Web » du module.

Il contient le répertoire css qui accueillera la feuille de style, le dossier img qui accueillera l'icône du module ainsi que les images nécessaires, le répertoire js qui accueillera le script JavaScript et le répertoire xml qui accueillera la vue du module.

On trouve également, à la racine du module les fichiers Python du module ainsi que les vues XML.

VII. Réalisation du module de base pour OpenERP

En tout premier lieu, nous allons réaliser le module qui permettra de créer des caissiers à l'intérieur d'OpenERP. La partie Web du module (celle qui va dans le POS), sera étudiée plus loin.

VII-A. Création des répertoires

Les modules OpenERP sont généralement placés dans le répertoire « addons » d'OpenERP, mais je vous recommande de créer un répertoire spécial (hors d'OpenERP) où vous placerez vos modules personnels.
Pour le tutoriel, nous allons créer un répertoire modules-openerp dans le répertoire /opt de votre serveur.

Ouvrez une session avec WinSCP (en root) et connectez-vous à votre serveur de développement.
Placez-vous dans le répertoire /opt et créez un nouveau dossier. Nommez-le modules-openerp.
Ouvrez une session avec PuTTY (depuis WinSCP) et entrez les commandes suivantes :

Modifier le propriétaire et les droits
Sélectionnez
cd /opt [+Entrée]
chown openerp:openerp ./modules-openerp [+Entrée]
chmod 0755 ./modules-openerp [+Entrée]

Nous avons attribué le répertoire /opt/modules-openerp à l'utilisateur openerp et au groupe openerp, puis nous avons modifié les droits.

Pour que nos futurs modules soient pris en compte par OpenERP, nous devons modifier le fichier de configuration du serveur et ajouter le chemin vers notre répertoire.

Modifier le fichier openerp-server.conf

  • Dans WinSCP, naviguez jusqu'au répertoire /etc.
  • Suivant les versions, le fichier de configuration openerp-server.conf peut se trouver dans le dossier /etc ou dans /etc/openerp.
  • Double-cliquez sur le fichier pour l'éditer.
  • Modifiez la ligne ci-dessous en ajoutant le chemin complet vers le répertoire de modules que l'on vient de créer.
openerp-server.conf
Sélectionnez
addons_path = /opt/openerp/addons,/opt/openerp/server/openerp/addons,/opt/openerp/web/addons,/opt/modules-openerp

Si cette ligne n'existe pas, rajoutez-la en ne mettant que le chemin vers votre répertoire.
Il peut y avoir de nombreux chemins vers des répertoires. Ils doivent être séparés par une virgule.

Sauvegardez puis fermez le fichier.
Redémarrez le serveur avec la commande ci-dessous

Redémarrer OpenERP
Sélectionnez
/etc/init.d/openerp-server restart [+Entrée]

Assurez-vous que le serveur est bien démarré en ouvrant une page Web avec l'URL correspondante

http://IP_DE_VOTRE_SERVEUR:8069

Créez ensuite les différents répertoires à l'intérieur du répertoire modules-openerp

  • pos_cashier
    • i18n
    • security
    • static
      • src
        • css
        • img
        • js
        • xml

Modifiez ensuite l'utilisateur, le groupe et les droits avec la commande ci-dessous

Modifier le propriétaire et les droits
Sélectionnez
cd /opt [+Entrée]
chown openerp:openerp ./modules-openerp -R [+Entrée]
chmod 0755 ./modules-openerp -R [+Entrée]

VII-B. Les fichiers Python obligatoires

Il y a trois fichiers obligatoires lorsque vous créez un module.

  • __init__.py
  • __openerp__.py
  • le_fameux_module.py

VII-B-1. Le fichier __init__.py

C'est le fichier qui va inviter OpenERP à charger notre module.
Le contenu de ce fichier est très simple :

__init__.py
Sélectionnez
import pos_cashier

Mettez le nom du module. C'est aussi le nom du répertoire.

VII-B-2. Le fichier __openerp__.py

 

C'est le fichier qui contient toutes les informations sur votre module : le nom, la version, la catégorie, la description, les fichiers à charger, etc.

__openerp__.py
Sélectionnez
# -*- coding: utf-8 -*-
{
    'name': 'POS Cashiers',
    'version': '1.0.0',
    'category': 'Point Of Sale',
    'sequence': 3,
    'author': 'Thierry Godin',
    'summary': 'Manage cashiers for Point Of Sale',
    'description': """
Manage several cashiers for each Point Of Sale
======================================

This could be handy in case of using the same POS at the same cash register while it is used by several cashiers.
Cashier's name is displayed on the payement receipt and on the order.

Cashiers are allowed to change the current cashier (by choosing their name in the drop-down list) and can make a sell without creating a new session.

Cashier's name is mandatory. You cannot perform a sell if no cashier has been created or is active in your POS.

The shop manager will know who made the sell.
    """,
    'depends': ["point_of_sale"],
    'data': [
        'security/pos_cashier_security.xml',
        'security/ir.model.access.csv',
        'cashier_view.xml',
        'order_cashier_view.xml',
    ],
    'js': [
        'static/src/js/pos_cashier.js',
    ],
    'css': [
        'static/src/css/pos_cashier.css',
    ],
    'qweb': [
        'static/src/xml/pos_cashier.xml',
    ],
    'installable': True,
    'application': False,
    'auto_install': False,
}

Si vous copiez le code depuis cette page
Prenez garde à l'indentation du code Python. Celle-ci peut être approximative dans cet article. Ceci est dû à l'éditeur utilisé pour rédiger cet article.
Les sources du module sont téléchargeables en bas de la page.

Le fichier est à remplir comme ceci

 
Sélectionnez
'parametre': 'valeur',
'parametre': ['valeur1','valeur2','valeur3'],

Les données sont notées sous la forme clé:valeur séparés par une virgule (en fin de ligne).
La valeur peut également contenir un tableau comme dans le cas des paramètres depends, data, js, etc.

Les différents paramètres :

  • name : le nom de votre module ;
  • version : la version du module ;
  • category : la catégorie dans laquelle vous classez votre module ;
  • sequence : c'est un nombre qui fera apparaître votre module dans la liste des modules. 1, il sera en haut, 100 il sera en bas ;
  • author : l'auteur du module ;
  • summary : un résumé qui explique ce que fait votre module. Un texte très court, il apparaît sous le nom du module dans la liste des modules ;
  • description : la description complète du module ;
  • depends : les modules dont votre module dépend ;
  • data : les fichiers à charger ;
  • js : dans le cas d'un module Web comme celui-ci, le(s) script(s) JavaScript ;
  • css : le fichier de style pour la partie Web ;
  • qweb : la vue de la partie Web ;
  • installable : si votre module est installable ou non ;
  • application : laissez à False. Votre module ne sera pas reconnu comme une application. C'est OpenERP qui délivre les certificats qui qualifient votre module d'application ;
  • auto_install : laissez à False, nous l'installerons à la main. (Avec un bouton, quand même…)

Il existe d'autres paramètres, mais pour ce module nous en aurons assez comme ça.

Le paramètre « description »
Pour insérer un texte sur plusieurs lignes, vous devez l'entourer avec trois guillemets doubles
(""" le texte """)

Mettez une description la plus complète possible.
Pour revenir à la ligne, vous devez en fait, en sauter une, sinon le retour à la ligne ne sera pas visible dans OpenERP.

Souligner un texte avec le signe = le fera apparaître comme dans la balise Web <h1></h1>.

Nous avons créé les deux fichiers (qui porteront toujours les mêmes noms) d'initialisation du module. Mais ce n'est pas suffisant !

Comme nous l'avons déclaré dans le fichier __init__.py, OpenERP tentera de charger le module pos_cashier. Nous devons donc maintenant créer le fichier pos_cashier.py (le module en lui-même).

VII-B-3. Le fichier pos_cashier.py

Je vous mets le contenu du fichier complet.
Ne vous inquiétez pas, on va décortiquer tout ça tranquillement.

pos_cashier.py
Sélectionnez
# -*- coding: utf-8 -*-
##############################################################################
#    
# Module : pos_cashier
# Créé le : 2013-06-06 par Thierry Godin
#
# Module permettant la création de vendeurs pour les points de vente
#
##############################################################################
import openerp
from openerp import netsvc, tools, pooler
from openerp.osv import fields, osv
from openerp.tools.translate import _
import time

class pos_cashier(osv.osv):
    _name = 'pos.cashier'
    _order = 'cashier_name asc'

    _columns = {
        'pos_config_id' : fields.many2one('pos.config', 'Point Of Sale', required=True),
        'cashier_name': fields.char('Cashier', size=128, required=True),
        'active': fields.boolean('Active', help="If a cashier is not active, it will not be displayed in POS"),
    }

    _defaults = {
        'cashier_name' : '',
        'active' : True,
        'pos_config_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).pos_config.id,
    }

    _sql_constraints = [
        ('uniq_name', 'unique(cashier_name, pos_config_id)', "A cashier already exists with this name in this Point Of sale. Cashier's name must be unique!"),
    ]


class inherit_pos_order_for_cashiers(osv.osv):
    _name='pos.order'
    _inherit='pos.order'
                   
    def create_from_ui(self, cr, uid, orders, context=None):
        #_logger.info("orders: %r", orders)
        order_ids = []
        for tmp_order in orders:
            order = tmp_order['data']
            order_id = self.create(cr, uid, {
                'name': order['name'],
                'user_id': order['user_id'] or False,
                'session_id': order['pos_session_id'],
                'lines': order['lines'],
                'pos_reference':order['name'],
                'cashier_name': order['cashier_name']
            }, context)

            for payments in order['statement_ids']:
                payment = payments[2]
                self.add_payment(cr, uid, order_id, {
                    'amount': payment['amount'] or 0.0,
                    'payment_date': payment['name'],
                    'statement_id': payment['statement_id'],
                    'payment_name': payment.get('note', False),
                    'journal': payment['journal_id']
                }, context=context)

            if order['amount_return']:
                session = self.pool.get('pos.session').browse(cr, uid, order['pos_session_id'], context=context)
                cash_journal = session.cash_journal_id
                cash_statement = False
                if not cash_journal:
                    cash_journal_ids = filter(lambda st: st.journal_id.type=='cash', session.statement_ids)
                    if not len(cash_journal_ids):
                        raise osv.except_osv( _('error!'),
                            _("No cash statement found for this session. Unable to record returned cash."))
                    cash_journal = cash_journal_ids[0].journal_id
                self.add_payment(cr, uid, order_id, {
                    'amount': -order['amount_return'],
                    'payment_date': time.strftime('%Y-%m-%d %H:%M:%S'),
                    'payment_name': _('return'),
                    'journal': cash_journal.id,
                }, context=context)
            order_ids.append(order_id)
            wf_service = netsvc.LocalService("workflow")
            wf_service.trg_validate(uid, 'pos.order', order_id, 'paid', cr)
        return order_ids

    _columns = {
        'cashier_name': fields.char('Cashier', size=128),
    }


inherit_pos_order_for_cashiers()

Au tout début du fichier
Nous allons importer les bibliothèques dont nous aurons besoin pour le module.

 
Sélectionnez
import openerp
from openerp import netsvc, tools, pooler
from openerp.osv import fields, osv
from openerp.tools.translate import _
import time

Ceci est indispensable, car nous allons utiliser des fonctions natives d'OpenERP et de Python.

Ensuite nous allons créer l'objet

Un objet se déclare comme ceci
Sélectionnez
class pos_cashier(osv.osv):

À partir de maintenant, il faut faire très attention à l'indentation du code. Vous remarquerez qu'il n'y a pas de signal de fin de l'objet.
C'est pourquoi il faut faire attention à l'éditeur de code que vous utilisez, il faut qu'il soit capable de gérer Python pour réaliser l'indentation qui convient.
En cas de doute, n'hésitez pas à lire cette page : FAQ PythonFAQ Python ainsi que celle-ci : Cours PythonCours Python.

VII-B-3-a. Les déclarations

_name : c'est le nom de la table dans OpenERP. En fait, la table se nommera réellement « pos_cashier » dans la base de données.

_name
Sélectionnez
_name = 'pos.cashier'

_order : vous l'aurez compris, c'est ce qui correspond en SQL à « ORDER BY ».
Ici nous afficherons les caissiers par ordre alphabétique par rapport à leur nom.

_order
Sélectionnez
_order = 'cashier_name asc'

_columns : ce sont les champs que l'on va créer dans la table pos_cashier.

_columns
Sélectionnez
_columns = {
'pos_config_id' : fields.many2one('pos.config', 'Point Of Sale', required=True),
'cashier_name': fields.char('Cashier', size=128, required=True),
'active': fields.boolean('Active', help="If a cashier is not active, it will not be displayed in POS"),
}

Le champ pos_config_id
On enregistrera ici l'ID du Point De Vente de l'utilisateur. Ce champ fait la relation avec la table pos_config qui contient les paramètres de chaque Point De Vente.

Le champ cashier_name
C'est dans ce champ qu'on enregistrera le nom du caissier.

Le champ active
Ce champ nous permettra d'activer ou de désactiver un caissier (lorsqu'il sera en congé, en maladie ou en déplacement, par exemple).

Dans OpenERP, le champ active est un champ spécial. Lorsque le champ active = False, l'enregistrement est automatiquement invisible.

_defaults : ce sont les valeurs par défaut pour les enregistrements.

_columns
Sélectionnez
_defaults = {
'cashier_name' : '',
'active' : True,
'pos_config_id': lambda self,cr,uid,c: self.pool.get('res.users').browse(cr, uid, uid, c).pos_config.id,
}

Par défaut, la case active du formulaire sera cochée et on récupérera automatiquement l'ID du Point De Vente de l'utilisateur. C'est-à-dire que lorsqu'on créera un nouveau caissier, le Point De Vente de l'utilisateur sera sélectionné dans la liste déroulante (pos_config_id) du formulaire. Le champ nom (cashier_name) quant à lui sera vide.

_sql_constraints : ce sont les règles d'enregistrement, ce qui correspond en SQL à CONSTRAINT.

_sql_constraints
Sélectionnez
_sql_constraints = [
('uniq_name', 'unique(cashier_name, pos_config_id)', "A cashier already exists with this name in this Point Of sale. Cashier's name must be unique!"),
]

Les règles sont à enregistrer comme ceci :

 
Sélectionnez
('NOM DE LA RÈGLE', 'RÈGLE', "MESSAGE EN CAS DE VIOLATION DE LA RÈGLE")

La règle unique(cashier_name, pos_config_id) signifie qu'il ne peut y avoir qu'un seul caissier avec le même nom dans le même Point De Vente.

_sql_constraints est un tableau. Vous pouvez entrer plusieurs règles séparées par une virgule.

Le message d'erreur s'affiche dans une fenêtre
Le message d'erreur s'affiche dans une fenêtre

VII-B-3-b. Surcharge de l'objet pos.order du Point De Vente

En plus d'avoir créé l'objet pos_cashier qui nous permet de gérer les caissiers, nous avons besoin de surcharger l'objet original pos_order du Point De Vente.

Ce module se trouve dans le répertoire original du Point De Vente. Vous le trouverez dans le fichier point_of_sale.py vers la ligne 479.

chemin_openerp/addons/point_of_sale/point_of_sale.py

Nous aurons besoin de modifier la fonction create_from_ui() et d'ajouter un champ cashier_name dans la table des commandes pos_order.
Pour cela, on va créer un nouvel objet qui héritera de la classe parente originale.

C'est la raison pour laquelle nous avons déclaré que notre module dépendait du module point_of_sale dans le fichier __openerp__.py.

inherit_pos_order_for_cashiers
Sélectionnez
class inherit_pos_order_for_cashiers(osv.osv):
    _name='pos.order'
    _inherit='pos.order'

Pour que notre module hérite du module pos_order, nous allons lui attribuer le même nom

_name
Sélectionnez
_name='pos.order'

Et on va rajouter la déclaration _inherit en précisant le nom du module parent

_inherit
Sélectionnez
_inherit='pos.order'

Pour l'héritage des objets OpenERP, je vous renvoie à cette page sur le site de l'éditeur :
OpenERP Object InheritanceOpenERP Object Inheritance.
Attention, c'est la documentation pour la version 6.x, mais les instructions sont toujours valables pour la version 7.x d'OpenERP.

On va simplement se contenter ensuite de copier toute la fonction originale create_from_ui() dans notre fichier.
Une fois fait, on va rajouter un champ pour les commandes.

Une petite explication
Le Point De Vente fonctionne avec des scripts JavaScript.
Les commandes sont enregistrées dans le navigateur (LocalStorage).
Voir : Principe de fonctionnement du Point De VentePrincipe de fonctionnement du Point De Vente.
Tant que vous effectuez une commande, celle-ci est stockée dans le navigateur. Lorsque vous validez la commande, la fonction create_from_ui() est appelée. Elle enverra la commande dans la base de données d'OpenERP.
En fait, elle enverra toutes les commandes valides qui sont stockées dans le navigateur.
Ceci permet au Point De Vente de fonctionner en mode Hors-Connexion (sic.)

Dans la boucle for tmp_order in orders:, nous allons rajouter le champ cashier_name dans la fonction self.create() comme ci-dessous

Boucle for tmp_order in orders:
Sélectionnez
for tmp_order in orders:
    order = tmp_order['data']
    order_id = self.create(cr, uid, {
        'name': order['name'],
        'user_id': order['user_id'] or False,
        'session_id': order['pos_session_id'],
        'lines': order['lines'],
        'pos_reference':order['name'],          # <---------- VIRGULE     
        'cashier_name': order['cashier_name']   # <---------- ICI LE CHAMP A RAJOUTER
    }, context)

Vous n'oublierez pas d'ajouter une virgule à la fin de la ligne précédente.

Le nom du caissier sera enregistré dans le tableau order[].
Nous pourrons alors envoyer le nom du caissier dans le champ cashier_name de la table pos_order

C'est tout ce que nous ajoutons à cette fonction.

La dernière chose à faire, c'est d'ajouter le champ cashier_name à la table pos_order.
Comme dans le module précédent, il suffira de rajouter le champ dans la déclaration _columns

_columns
Sélectionnez
_columns = {
        'cashier_name': fields.char('Cashier', size=128),
    }

On y est presque. Il ne reste plus que l'appel à l'objet pour qu'OpenERP le prenne en compte.

 
Sélectionnez
inherit_pos_order_for_cashiers()

Nous avons terminé le module Python pos_cashier.
Sauvegardez le fichier à la racine du module.

VII-B-4. Le fichier cashier_view.xml

C'est le fichier des vues du module pos_cashier. Plus exactement les vues pour les caissiers. On mettra les vues pour les commandes dans un autre fichier.

Dans ce fichier on va créer la vue tableau (tree_view), la vue formulaire (form_view), les menus, le filtre de recherche (search_view) et l'action du menu « Caissiers ».

Comme précédemment, je vous mets le code complet qu'on va décortiquer au fur et à mesure.

cashier_view.xml
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<openerp>
  <data>

    <record id="pos_cashier_form" model="ir.ui.view">
        <field name="name">pos.cashier.form</field>
        <field name="model">pos.cashier</field>
        <field name="arch" type="xml">
          <form string="Cashiers" version="7.0">  
          <group col="4">     
            <field name="cashier_name" />
            <field name="pos_config_id" widget="selection" eval="ref('pos.config.name')" />  
            <field name="active"/>
            </group>
          </form>
        </field>
    </record>
  
    <record id="pos_cashier_tree" model="ir.ui.view">
      <field name="name">pos.cashier.tree</field>
      <field name="model">pos.cashier</field>
      <field name="arch" type="xml">
        <tree string="Cashiers">
          <field name="cashier_name"/> 
          <field name="pos_config_id" ref="pos.config.name"/>       
          <field name="active"/> 
        </tree>
      </field>
    </record>

    <record model="ir.ui.view" id="pos_cashier_search">
      <field name="name">pos.cashier.search</field>
      <field name="model">pos.cashier</field>
      <field name="arch" type="xml">
        <search string="Point of Sale Cashier">
          <field name="cashier_name" />
          <filter name="filter_see_all" string="All" domain="['|', ('active', '=',True), ('active', '=',False)]" />
          <filter name="filter_see_active" string="Active" domain="[('active', '=',True)]" />
          <filter name="filter_see_inactive" string="Inactive" domain="[('active', '=',False)]" />
        </search>
      </field>
    </record>

    <!-- L'action du menu -->
    <record model="ir.actions.act_window" id="action_pos_cashier">
      <field name="name">Cashiers</field>
      <field name="type">ir.actions.act_window</field>
      <field name="res_model">pos.cashier</field>
      <field name="view_type">form</field>
      <field name="view_mode">tree,form</field>
      <field name="view_id" ref="pos_cashier_tree"/>
      <field name="context">{"search_default_filter_see_all":1}</field>
      <field name="help" type="html">
        <p class="oe_view_nocontent_create">
          Click here to create a cashier for the Point Of Sale.
        </p>
      </field>
    </record>

    <!-- Menu gauche  Vendeurs -->   
    <menuitem 
              name="Cashiers" 
              id="menu_point_of_sale_cashiers" 
              parent="point_of_sale.menu_point_root" 
              sequence="16" 
              groups="point_of_sale.group_pos_manager"
              />
    <menuitem
            id="menu_action_pos_cashier"
            parent="menu_point_of_sale_cashiers"
            action="action_pos_cashier"    
            />
    <!-- # -->
    
  </data>
</openerp>

Un fichier de vues OpenERP est toujours construit de cette façon

Structure d'une vue OpenERP
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<openerp>
  <data>
  
      <record>
          <!-- Ici, les divers champs de la vue -->
      </record>
      
      <menuitem/>
      
      <!-- Etc. -->

  </data>
</openerp>

VII-B-4-a. La vue formulaire

Vue pos_cashier_form
Sélectionnez
    <record id="pos_cashier_form" model="ir.ui.view">
        <field name="name">pos.cashier.form</field>
        <field name="model">pos.cashier</field>
        <field name="arch" type="xml">
          <form string="Cashiers" version="7.0">  
          <group col="4">     
            <field name="cashier_name" />
            <field name="pos_config_id" widget="selection" eval="ref('pos.config.name')" />  
            <field name="active"/>
            </group>
          </form>
        </field>
    </record>

La première ligne comporte l'identifiant de la vue et le modèle utilisé.

id="pos_cashier_form"
Pour que ce soit facile à déboguer par la suite, je vous recommande de mettre le nom du module, suivi du type de vue.

model="ir.ui.view"
Comme c'est une « Vue », le modèle utilisé sera toujours ir.ui.view (la vue sera enregistrée dans la table ir_ui_view d'OpenERP).

Les trois champs suivants (obligatoires)

 
Sélectionnez
<field name="name">pos.cashier.form</field>
<field name="model">pos.cashier</field>
<field name="arch" type="xml">
    <!-- les autres objets à l'intérieur du champ "arch" -->
</field>

Le champ name="name"
C'est le nom de la vue. Remettez l'identifiant de la vue, mais remplacez les tirets bas par un point pour qu'il n'y ait pas de confusion.

Le champ name="model"
C'est le nom de la table utilisée. Ici, nous utilisons la table pos.cashier.

Le champ name="arch"
C'est à l'intérieur de cette balise qu'on va mettre la vue proprement dite.

On va donc y insérer un formulaire

 
Sélectionnez
<form string="Cashiers" version="7.0">
    <group col="4">
        <field name="cashier_name" />
        <field name="pos_config_id" widget="selection" eval="ref('pos.config.name')" />  
        <field name="active"/>
    </group>
</form>

Lorsque vous ajoutez l'attribut string dans un champ, c'est ce texte qui sera affiché (à la place de celui du nom du champ de la base de données, si c'est le cas).

On rajoute donc les champs du formulaire dont on a besoin.

name="cashier_name"
C'est le champ texte qui permet de saisir le nom du caissier.

name="pos_config_id"
Ce champ affichera la liste déroulante des Points De Vente disponibles grâce à l'attribut widget="selection".

Avec l'attribut « eval », nous demandons à OpenERP d'afficher le nom du Point De Vente.

name="active"
C'est la case à cocher qui permet d'activer/désactiver un caissier.

Vous aurez remarqué que les champs du formulaire sont à l'intérieur d'une balise <group>.
Ceci indique à OpenERP comment afficher les champs dans la page du formulaire.

Ici nous spécifions, avec l'attribut col, le nombre de colonnes à utiliser.

Les champs du formulaire apparaîtront comme ceci

Distributions des champs dans un formulaire avec la balise <group> et l'attribut col=4
Distributions des champs dans un formulaire avec la balise <group> et l'attribut col=4

Voici comment apparaîtra le formulaire de création des caissiers

Formulaire de création des caissiers
Formulaire de création des caissiers

VII-B-4-b. La vue tableau

Vue pos_cashier_tree
Sélectionnez
<record id="pos_cashier_tree" model="ir.ui.view">
    <field name="name">pos.cashier.tree</field>
    <field name="model">pos.cashier</field>
    <field name="arch" type="xml">
        <tree string="Cashiers">
            <field name="cashier_name"/> 
            <field name="pos_config_id" ref="pos.config.name"/>       
            <field name="active"/> 
        </tree>
    </field>
</record>

À la place de la balise <form>, nous allons insérer une balise <tree>.
Ces colonnes apparaîtront dans le tableau des caissiers.

VII-B-4-c. La vue de recherche

C'est une vue spéciale. Elle va permettre de créer des filtres de recherche qui apparaissent en cliquant sur la flèche du formulaire de recherche, en haut à droite de la page.

Vue pos_cashier_search
Sélectionnez
<record model="ir.ui.view" id="pos_cashier_search">
    <field name="name">pos.cashier.search</field>
    <field name="model">pos.cashier</field>
    <field name="arch" type="xml">
        <search string="Point of Sale Cashier">
            <field name="cashier_name" />
            <filter name="filter_see_all" string="All" domain="['|', ('active', '=',True), ('active', '=',False)]" />
            <filter name="filter_see_active" string="Active" domain="[('active', '=',True)]" />
            <filter name="filter_see_inactive" string="Inactive" domain="[('active', '=',False)]" />
        </search>
    </field>
</record>

Cette fois-ci, à la place de la balise <tree>, nous allons insérer une balise <search> dans laquelle nous allons spécifier un champ de recherche ainsi que plusieurs filtres.

Nous ajoutons donc le champ suivant

 
Sélectionnez
<field name="cashier_name" />

Nous allons maintenant ajouter un filtre de recherche grâce à la balise <filter>

 
Sélectionnez
<filter name="filter_see_all" string="All" domain="['|', ('active', '=',True), ('active', '=',False)]" />

name="filter_see_all"
Ici nous appelons notre filtre filter_see_all.

string="All"
C'est le mot qui apparaîtra dans le formulaire de recherche.

domain="['|', ('active', '=',True), ('active', '=',False)]"
C'est le domaine de la recherche.
Ici on recherche des caissiers actifs ou non actifs. On veut voir tous les caissiers.

L'attribut domain est un tableau dans lequel vous mettez les paramètres de recherche.

Ici, l'opérateur « | » (ou) indique qu'une des deux conditions au moins doit être remplie.

Par défaut les objets inactifs ne sont pas visibles dans les tableaux ou les formulaires. On est donc obligés de créer un filtre pour afficher les caissiers inactifs afin de pouvoir les activer en cas de besoin.

Les deux autres filtres afficheront les caissiers actifs ou inactifs.

Les filtres de recherche apparaissent lorsqu'on clique sur la flèche du champ de recherche.
Les filtres de recherche apparaissent lorsqu'on clique sur la flèche du champ de recherche.

VII-B-4-d. Le menu Caissiers

Nous allons créer maintenant un menu Caissiers qui apparaîtra dans la rubrique Caissiers du menu de gauche du Point De Vente.

Nous aurions pu nous contenter de rajouter seulement le menu Caissiers sans rajouter de rubrique éponyme, mais cela vous montre comment créer une rubrique dans un menu.
De plus, vous verrez que cette rubrique ne sera visible que par un groupe d'utilisateurs. Si dans le futur on décidait de rajouter un menu dans cette rubrique, seuls les utilisateurs appartenant au groupe autorisé pourraient le voir.

Je vous présente la création du menu avant la création de l'action pour une meilleure compréhension, mais en fait, dans le code, il faudra que le menu soit écrit après l'action, car faisant référence à l'action, si celui-ci est écrit avant, Python vous renverra l'erreur action_pos_cashier n'existe pas.

La rubrique « Caissiers »

La rubrique Caissiers
Sélectionnez
<menuitem 
        name="Cashiers" 
        id="menu_point_of_sale_cashiers" 
        parent="point_of_sale.menu_point_root" 
        sequence="16" 
        groups="point_of_sale.group_pos_manager"
        />

Un menu s'écrit dans une balise <menuitem/>.

name="Cashiers"
C'est le nom de la rubrique.

id
Comme d'habitude, on spécifie un identifiant pour la rubrique.

parent
C'est ce qui nous permet d'insérer la rubrique dans un menu déjà existant.
Ici, nous insérons notre rubrique dans le menu du Point De Vente, il faut donc récupérer l'identifiant du menu dans le fichier original du Point De Vente. Comme ce menu n'appartient pas à notre module, nous y faisons référence en utilisant la syntaxe à point traditionnelle.

sequence
C'est le nombre qui permet de classer la rubrique. Plus le nombre sera petit, plus la rubrique sera en haut du menu existant.

groups
Nous souhaitons restreindre l'accès à ce menu au groupe POS/Manager (les dirigeants des Points De Vente).
Nous utilisons ici également la syntaxe à point pour faire référence au groupe en question. Nous pourrions autoriser plusieurs groupes. Il suffit de les ajouter en les séparant par une virgule.

Un menu qui n'a pas d'attribut action devient alors une rubrique.

Le menu « Caissiers »

Le menu Caissiers
Sélectionnez
<menuitem
        id="menu_action_pos_cashier"
        parent="menu_point_of_sale_cashiers"
        action="action_pos_cashier"    
        />

Cette fois-ci, nous ajoutons l'attribut action qui fait référence à l'action que nous allons définir plus loin.
Vous noterez également que le parent du menu est la rubrique que nous avons créée plus tôt. En clair, le menu sera à l'intérieur de cette rubrique.

Le menu apparaîtra dans l'onglet Menus du groupe POS/Manager (depuis le menu de Configuration/Groupes d'OpenERP) comme le montre l'image ci-dessous.

Le menu apparaît dans le groupe POS/Manager
Le menu apparaît dans le groupe POS/Manager

On voit bien ici le classement des menus selon la séquence.

VII-B-4-e. L'action du menu

Lorsqu'on cliquera sur le menu Caissiers, l'action ci-dessous sera exécutée.

action_pos_cashier
Sélectionnez
<record model="ir.actions.act_window" id="action_pos_cashier">
    <field name="name">Cashiers</field>
    <field name="type">ir.actions.act_window</field>
    <field name="res_model">pos.cashier</field>
    <field name="view_type">form</field>
    <field name="view_mode">tree,form</field>
    <field name="view_id" ref="pos_cashier_tree"/>
    <field name="context">{"search_default_filter_see_all":1}</field>
    <field name="help" type="html">
        <p class="oe_view_nocontent_create">
            Click here to create a cashier for the Point Of Sale.
        </p>
    </field>
</record>

Lorsqu'il s'agit d'une action, nous employons alors le modèle ir.actions.act_window (l'action sera enregistrée dans la table ir_act_window d'OpenERP).

name="type"
C'est le type de l'action.

name="res_model"
C'est le nom de la table utilisée.

name="view_type"
C'est le type de vue

name="view_mode"
C'est le type de vues disponibles. Ici on permettra la vue formulaire et la vue tableau. Il existe une autre forme de vue, la vue Kaban.

name="view_id"
C'est l'ID de la vue à laquelle s'appliquera cette action. Ici, la vue tableau.

name="context"
Le contexte de la vue qui s'appliquera à la vue tableau. Ici, on applique un filtre par défaut, le filtre filter_see_all qu'on a créé auparavant. On verra donc tous les caissiers.

name="help" type="html"
Ce champ va permettre d'afficher du contenu au format HTML si le tableau est vide.
En utilisant la classe spéciale oe_view_nocontent_create, un texte sera affiché avec une flèche vers le bouton Créer.

Si aucun caissier n'a été créé dans le Point De Vente, le texte ci-dessus sera affiché.
Si aucun caissier n'a été créé dans le Point De Vente, le texte ci-dessus sera affiché.

Dans le Point De Vente, lorsque nous cliquerons sur le menu « Caissiers », la vue tableau s'affichera avec le filtre « All » qui permettra de voir tous les caissiers. Un bouton « Créer» sera affiché au-dessus du tableau.

Le menu Caissiers dans la rubrique Point De Vente
Le menu Caissiers dans la rubrique Point De Vente

VII-B-5. Le fichier order_cashier_view.xml

Nous allons maintenant créer la vue pour les commandes passées, afin que le nom du caissier apparaisse.

order_cashier_view.xml
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<openerp>
    <data>   
        <!-- Vue formulaire -->
        <record model="ir.ui.view" id="view_pos_cashier_form">
            <field name="model">pos.order</field>
            <field name="name">view.inherit.pos.order.form</field>
            <field name="view_type">form</field>
            <field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
            <field name="arch" type="xml">
                <field name="partner_id" position="after">
                    <field name="cashier_name"/>
                </field>
            </field>
        </record>

        <!-- Vue Tree -->
        <record model="ir.ui.view" id="view_pos_cashier_tree">
            <field name="model">pos.order</field>
            <field name="name">view.inherit.pos.order.tree</field>
            <field name="view_type">tree</field>
            <field name="inherit_id" ref="point_of_sale.view_pos_order_tree"/>
            <field name="arch" type="xml">
                <field name="user_id" position="replace">
                    <field name="cashier_name"/>
                </field>
            </field>
        </record>
    </data>
</openerp>

Comme dans le fichier précédent, nous allons créer une vue « formulaire » et une vue « tableau ».

Attention !
Si vous vous rappelez bien, l'objet inherit_pos_order_for_cashiers hérite de l'objet pos_order, l'objet d'origine du Point De Vente.

Il faut donc reprendre l'en-tête de la vue d'origine, modifier le champ name et rajouter le champ inherit_id.

inherit_id
Sélectionnez
<field name="inherit_id" ref="point_of_sale.view_pos_order_tree"/>

Vous noterez au passage qu'on a mis l'identifiant de la vue d'origine, en n'omettant pas de le faire précéder du nom du module d'origine (syntaxe à point), puisque cette vue n'appartient pas à notre module, mais à celle de son parent.

VII-B-5-a. La vue formulaire

Vue formulaire
Sélectionnez
<record model="ir.ui.view" id="view_pos_cashier_form">
    <field name="model">pos.order</field>
    <field name="name">view.inherit.pos.order.form</field>
    <field name="view_type">form</field>
    <field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
    <field name="arch" type="xml">
        <field name="partner_id" position="after">
            <field name="cashier_name"/>
        </field>
    </field>
</record>

Nous allons insérer le champ cashier_name dans le formulaire.
Pour cela, nous allons utiliser un champ qui existe déjà et rajouter l'attribut position.

Les différentes positions

  • after : le champ sera inséré après celui qui contient l'attribut position ;
  • before : le champ sera inséré avant celui qui contient l'attribut position ;
  • replace : le champ remplacera celui qui contient l'attribut position ;

Dans le cas présent, le champ cashier_name sera placé après le champ partner_id (le client) dans le formulaire.

Le champ cashier_name dans le formulaire de la commande
Le champ cashier_name dans le formulaire de la commande

VII-B-5-b. La vue tableau

La vue tableau
Sélectionnez
<record model="ir.ui.view" id="view_pos_cashier_tree">
    <field name="model">pos.order</field>
    <field name="name">view.inherit.pos.order.tree</field>
    <field name="view_type">tree</field>
    <field name="inherit_id" ref="point_of_sale.view_pos_order_tree"/>
    <field name="arch" type="xml">
        <field name="user_id" position="replace">
            <field name="cashier_name"/>
        </field>
    </field>
</record>

Dans la vue tableau, on va remplacer le champ user_id (l'utilisateur du Point De Vente) par le champ cashier_name.

Rappelez-vous que nous créons ce module pour que plusieurs caissiers puissent passer des commandes sans être obligés d'ouvrir une session à chaque fois qu'on change de caissier. C'est la raison pour laquelle nous ne désirons pas que le nom de l'utilisateur du Point De Vente, qui sera alors le même pour tous les caissiers, apparaisse dans le tableau des commandes. En revanche, faire apparaître le nom du caissier permettra au gérant du Magasin de visualiser tout de suite quel caissier aura fait telle vente.

Le champ cashier_name dans le tableau des commandes
Le champ cashier_name dans le tableau des commandes

VII-C. Paramètres de sécurité du module

Comme nous avons défini l'accès au menu à un groupe particulier, nous allons maintenant appliquer quelques règles de sécurité sur le module afin de restreindre l'accès et de fixer une règle sur les enregistrements.

VII-C-1. Les droits d'accès

Nous allons créer un fichier spécial dans le répertoire security du module.

/opt/modules-openerp/pos_cashier/security

Le fichier qui définit les droits d'accès aux enregistrements dans la base de données est un fichier CSV.
Il porte toujours le même nom : ir.model.access.csv.

La première ligne contient le nom des champs séparés par une virgule.

Les champs CSV
Sélectionnez
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink

id
Un identifiant unique pour la règle d'accès.

name
Le nom de la règle. Il apparaîtra dans les pages de configuration d'OpenERP.

model_id:id
La table à laquelle s'applique cette règle. Le nom de la table doit toujours être précédé du préfixe model_.

group_id:id
Le groupe d'utilisateurs auquel s'applique cette règle.

perm_read
La permission de lire les données (1 ou 0).

perm_write
La permission de modifier les données (1 ou 0).

perm_create
La permission de créer des données (1 ou 0).

perm_unlink
La permission de supprimer les données (1 ou 0).

Nous allons donc maintenant ajouter deux lignes supplémentaires.
Une ligne pour les droits des utilisateurs du POS (le groupe POS/User) avec les droits en lecture seulement.
Et une ligne pour les droits des managers du POS (le groupe POS/Manager) avec tous les droits.

Les champs CSV
Sélectionnez
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_pos_cashier_u,pos.cashier user,model_pos_cashier,point_of_sale.group_pos_user,1,0,0,0
access_pos_cashier_m,pos.cashier manager,model_pos_cashier,point_of_sale.group_pos_manager,1,1,1,1

Pour ce qui concerne le groupe POS/User

  • id = access_pos_cashier_u
  • name = pos.cashier user
  • model_id:id = model_pos_cashier
  • group_id:id = point_of_sale.group_pos_user
  • perm_read = 1
  • perm_write = 0
  • perm_create = 0
  • perm_unlink = 0

Pour ce qui concerne le groupe POS/Manager

  • id = access_pos_cashier_m
  • name = pos.cashier manager
  • model_id:id = model_pos_cashier
  • group_id:id = point_of_sale.group_pos_manager
  • perm_read = 1
  • perm_write = 1
  • perm_create = 1
  • perm_unlink = 1

Lorsque vous irez dans le menu Configuration/Sécurité/Liste des contrôles d'accès d'OpenERP vous verrez ces deux lignes dans le tableau.

Les droits d'accès pour le module pos_cashier
Les droits d'accès pour le module pos_cashier

VII-C-2. Les règles sur les enregistrements

Maintenant, nous allons définir une règle sur les enregistrements afin de s'assurer qu'un manager du Point De Vente puisse créer un caissier seulement pour son Point De Vente.

Lors de la création d'un caissier, si le manager sélectionne un Point De Vente qui ne lui appartient pas dans la liste déroulante, un message d'erreur sera affiché.

L'administrateur d'OpenERP (utilisateur Admin) peut créer des caissiers dans n'importe quel Point De Vente.
L'administrateur a tous les droits sur la base de données. Il peut donc configurer toute l'application. Les règles de sécurité ne s'appliquent pas pour lui.

Pour cela, toujours dans le répertoire security du module, nous allons créer un fichier XML que nous allons appeler pos_cashier_security.xml.

pos_cashier_security.xml
Sélectionnez
<?xml version="1.0" encoding="utf-8"?>
<openerp>
    <data noupdate="0">
        <record id="rule_pos_cashier" model="ir.rule">
            <field name="name">Point Of Sale Cashiers</field>
            <field name="model_id" ref="model_pos_cashier" />
            <field name="global" eval="True" />
            <field name="domain_force">[('pos_config_id', '=', user.pos_config.id)]</field>
        </record>
    </data>
</openerp>

Ici, comme il s'agit de règles de sécurité, le modèle utilisé sera toujours ir.rule (la règle sera enregistrée dans la table ir_rule d'OpenERP).

Comme pour les fichiers XML précédents, nous allons lui mettre un identifiant, puis nous ajouterons quelques champs.

name="model_id"
C'est le nom de la table concernée (précédé du préfixe model_).

name="global" eval="True"
Si cette règle est globale, elle s'applique à tout le monde. Si celle-ci n'est pas globale, on devra alors spécifier les groupes d'utilisateurs pour lesquels elle s'applique.

name="domain_force"
C'est la règle en elle-même.

domain_force
Sélectionnez
[('pos_config_id', '=', user.pos_config.id)]

Ici, un enregistrement ne pourra se faire que si le Point De Vente sélectionné appartient à l'utilisateur.

Voici comment apparaîtra cette règle dans le menu Configuration/Sécurité/Règles sur les enregistrements d'OpenERP

Règle d'enregistrement pour le module pos_cashier (vue tableau)
Règle d'enregistrement pour le module pos_cashier (vue tableau)

Si vous cliquez sur cette règle, elle apparaît dans le formulaire ci-dessous.

Règle d'enregistrement du module pos_cashier (vue formulaire)
Règle d'enregistrement du module pos_cashier (vue formulaire)

VII-D. Rajouter une icône au module

Pour que votre module affiche une icône dans le menu Configuration/Modules d'OpenERP, nous allons simplement créer une image PNG de 64 pixels par 64 pixels que nous appellerons icon.png.

Cette icône est à placer dans le sous-répertoire img du répertoire static du module.

/opt/modules-openerp/pos_cashier/static/src/img

Au chargement OpenERP, et lors du chargement des modules, l'application recherche un fichier icon.png dans ce répertoire pour l'afficher à côté du nom du module.

L'icône est affiché à côté du nom du module
L'icône est affichée à côté du nom du module

VII-E. Fin du module de base OpenERP

Nous avons terminé le module « de base » pour OpenERP.

Tous les fichiers que nous avons créés sont à ajouter dans le tableau data[] du fichier __openerp__.py
Voir ici.

Nous ne pouvons pas encore installer notre module, car comme nous avons déclaré d'autres fichiers dans le fichier __openerp__.py, si ceux-ci ne sont pas créés, OpenERP nous retournera une erreur.


Il va falloir patienter encore un peu… Image non disponible

VIII. Réalisation du module Web pour le Point De Vente

Maintenant que nous avons créé le module qui permet de créer des caissiers dans le Point De vente, nous allons créer les fichiers nécessaires qui permettront d'utiliser les caissiers dans le Point De Vente.

Je vous rappelle que le Point De Vente n'est pas, à proprement parler, intégré dans OpenERP. C'est une interface Web complètement différente qui s'affiche à la place d'OpenERP.

L'interface d'origine du Point De Vente
L'interface d'origine du Point De Vente

Lorsque nous aurons terminé, vous verrez apparaître la liste de sélection des caissiers en bas à gauche du Point De Vente, sous le pavé numérique, comme nous le montre l'image ci-dessous.

L'interface du Point De Vente avec la liste des caissiers
L'interface du Point De Vente avec la liste des caissiers

VIII-A. Le fichier pos_cashier.js

Bon, là, c'est un peu chaud. Je vous mets quand même tout le script puis je vais vous expliquer pas à pas les différentes fonctions.

Ce fichier est à créer dans le répertoire js du module.

/opt/modules-openerp/pos_cashier/static/src/js
pos_cashier.js
Sélectionnez

function openerp_pos_cashier(instance, module){ //module is instance.point_of_sale
    var module = instance.point_of_sale;
    var QWeb = instance.web.qweb;
    _t = instance.web._t;

    globalCashier = null;

    module.CashierWidget = module.PosWidget.include({
        template: 'PosWidget',  

        init: function(parent, options) {
            this._super(parent);
            var  self = this;    
        },

        // recuperation de l'ID du POS
        get_cur_pos_config_id: function(){
            var self = this;
            var config = self.pos.get('pos_config');
            var config_id = null;
                     
            if(config){
                config_id = config.id;
                
                return config_id;
            }        
            return '';    
        },

        fetch: function(model, fields, domain, ctx){
            return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all()
        },

        cashier_change: function(name){
            globalCashier = name;

            $('#pay-screen-cashier-name').html(name);
            console.log('cashier_change : ' + name);
            
            if(name != ''){
                $('.gotopay-button').removeAttr('disabled');                 
            } else{
                $('.gotopay-button').attr('disabled', 'disabled');
            }
        },

        get_cashiers: function(config_id){
            var self = this;
            var cashier_list = [];

            var loaded = self.fetch('pos.cashier',['cashier_name'],[['pos_config_id','=', config_id], ['active', '=','true']])
                .then(function(cashiers){
                     for(var i = 0, len = cashiers.length; i < len; i++){
                        cashier_list.push(cashiers[i].cashier_name);
                     }

                    if(cashier_list.length > 0){
                        
                        for(var i = 0, len = cashier_list.length; i < len; i++){
                            var content = self.$('#cashier-select').html();
                            var new_option = '<option value="' + cashier_list[i] + '">' + cashier_list[i] + '</option>\n';
                            self.$('#cashier-select').html(content + new_option);
                            }

                        self.$('#AlertNoCashier').css('display', 'none');
                        self.$('#cashier-select').selectedIndex = 0;
                        globalCashier = cashier_list[0];
                        self.cashier_change(globalCashier);

                    } else{

                        // if there are no cashier
                        self.$('#AlertNoCashier').css('display', 'block');
                        self.$('.gotopay-button').attr('disabled', 'disabled');
                    }
                });
        }, 

        renderElement: function() {
            var self = this;
            this._super();

            self.$('#cashier-select').change(function(){
                var name = this.value;
                self.cashier_change(name);
            });
        },

        
        build_widgets: function() {
            var self = this;

            // --------  Screens ---------

            this.product_screen = new module.ProductScreenWidget(this,{});
            this.product_screen.appendTo($('#rightpane'));

            this.receipt_screen = new module.ReceiptScreenWidget(this, {});
            this.receipt_screen.appendTo($('#rightpane'));

            this.payment_screen = new module.PaymentScreenWidget(this, {});
            this.payment_screen.appendTo($('#rightpane'));

            this.welcome_screen = new module.WelcomeScreenWidget(this,{});
            this.welcome_screen.appendTo($('#rightpane'));

            this.client_payment_screen = new module.ClientPaymentScreenWidget(this, {});
            this.client_payment_screen.appendTo($('#rightpane'));

            this.scale_invite_screen = new module.ScaleInviteScreenWidget(this, {});
            this.scale_invite_screen.appendTo($('#rightpane'));

            this.scale_screen = new module.ScaleScreenWidget(this,{});
            this.scale_screen.appendTo($('#rightpane'));

            // --------  Popups ---------

            this.help_popup = new module.HelpPopupWidget(this, {});
            this.help_popup.appendTo($('.point-of-sale'));

            this.error_popup = new module.ErrorPopupWidget(this, {});
            this.error_popup.appendTo($('.point-of-sale'));

            this.error_product_popup = new module.ProductErrorPopupWidget(this, {});
            this.error_product_popup.appendTo($('.point-of-sale'));

            this.error_session_popup = new module.ErrorSessionPopupWidget(this, {});
            this.error_session_popup.appendTo($('.point-of-sale'));

            this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {});
            this.choose_receipt_popup.appendTo($('.point-of-sale'));

            this.error_negative_price_popup = new module.ErrorNegativePricePopupWidget(this, {});
            this.error_negative_price_popup.appendTo($('.point-of-sale'));

            // --------  Misc ---------

            this.notification = new module.SynchNotificationWidget(this,{});
            this.notification.appendTo(this.$('#rightheader'));

            this.username   = new module.UsernameWidget(this,{});
            this.username.replace(this.$('.placeholder-UsernameWidget'));

            this.action_bar = new module.ActionBarWidget(this);
            this.action_bar.appendTo($(".point-of-sale #rightpane"));

            this.left_action_bar = new module.ActionBarWidget(this);
            this.left_action_bar.appendTo($(".point-of-sale #leftpane"));

            this.gotopay = new module.GoToPayWidget(this, {});
            this.gotopay.replace($('#placeholder-GoToPayWidget'));

            this.paypad = new module.PaypadWidget(this, {});
            this.paypad.replace($('#placeholder-PaypadWidget'));

            this.numpad = new module.NumpadWidget(this);
            this.numpad.replace($('#placeholder-NumpadWidget'));

            this.order_widget = new module.OrderWidget(this, {});
            this.order_widget.replace($('#placeholder-OrderWidget'));

            this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, {
                'keyboard_model': 'simple'
            });
            this.onscreen_keyboard.appendTo($(".point-of-sale #content")); 

            this.close_button = new module.HeaderButtonWidget(this,{
                label: _t('Close'),
                action: function(){ self.try_close(); },
            });
            this.close_button.appendTo(this.$('#rightheader'));

            this.client_button = new module.HeaderButtonWidget(this,{
                label: _t('Self-Checkout'),
                action: function(){ self.screen_selector.set_user_mode('client'); },
            });
            this.client_button.appendTo(this.$('#rightheader'));

            
            // --------  Screen Selector ---------

            this.screen_selector = new module.ScreenSelector({
                pos: this.pos,
                screen_set:{
                    'products': this.product_screen,
                    'payment' : this.payment_screen,
                    'client_payment' : this.client_payment_screen,
                    'scale_invite' : this.scale_invite_screen,
                    'scale':    this.scale_screen,
                    'receipt' : this.receipt_screen,
                    'welcome' : this.welcome_screen,
                },
                popup_set:{
                    'help': this.help_popup,
                    'error': this.error_popup,
                    'error-product': this.error_product_popup,
                    'error-session': this.error_session_popup,
                    'error-negative-price': this.error_negative_price_popup,
                    'choose-receipt': this.choose_receipt_popup,
                },
                default_client_screen: 'welcome',
                default_cashier_screen: 'products',
                default_mode: this.pos.iface_self_checkout ?  'client' : 'cashier',
            });

            if(this.pos.debug){
                this.debug_widget = new module.DebugWidget(this);
                this.debug_widget.appendTo(this.$('#content'));
            }
        },

    });  

    module.CashierPayScreenWidget = module.PaymentScreenWidget.include({
        template: 'PaymentScreenWidget', 

        show: function(){
            this._super();
            var self = this;
            this.$('#pay-screen-cashier-name').html(globalCashier);
            this.$('#ticket-screen-cashier-name').html(globalCashier);
            this.pos.get('selectedOrder').set_cashier_name(globalCashier);

            this.paypad = new module.PaypadWidget(this, {});
            this.paypad.replace($('#placeholder-PaypadWidget'));
        },

    }); 

    module.CashierReceiptScreenWidget = module.ReceiptScreenWidget.include({

        refresh: function() {
            this._super();
            $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{widget:this}));

            if(globalCashier != ''){
                this.$('#ticket-screen-cashier-name').html(globalCashier);           
            }         
        },
        
    });

    module.GoToPayWidget = module.PosBaseWidget.extend({
        template: 'GoToPayWidget',
        init: function(parent, options) {
            this._super(parent);
        },

        renderElement: function() {
            var self = this;
            this._super();

            var button = new module.GoToPayButtonWidget(self);
            button.appendTo(self.$el);
        },
    });

    module.GoToPayButtonWidget = module.PosBaseWidget.extend({
        template: 'GoToPayButtonWidget',
        init: function(parent, options) {
            this._super(parent);
        },

        renderElement: function() {
            var self = this;
            this._super();

            this.$el.click(function(){
                self.pos_widget.screen_selector.set_current_screen('payment');
            });
        },
    });

    
    module.Order = Backbone.Model.extend({
        initialize: function(attributes){
            Backbone.Model.prototype.initialize.apply(this, arguments);
            this.set({
                creationDate:   new Date(),
                orderLines:     new module.OrderlineCollection(),
                paymentLines:   new module.PaymentlineCollection(),
                name:           "Order " + this.generateUniqueId(),
                client:         null,
                cashier_name:   null,
            });
            this.pos =     attributes.pos; 
            this.selected_orderline = undefined;
            this.screen_data = {};  // see ScreenSelector
            this.receipt_type = 'receipt';  // 'receipt' || 'invoice'
            return this;
        },
        generateUniqueId: function() {
            return new Date().getTime();
        },
        addProduct: function(product, options){
            options = options || {};
            var attr = product.toJSON();
            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());
        },
        removeOrderline: function( line ){
            this.get('orderLines').remove(line);
            this.selectLine(this.getLastOrderline());
        },
        getLastOrderline: function(){
            return this.get('orderLines').at(this.get('orderLines').length -1);
        },
        addPaymentLine: function(cashRegister) {
            var paymentLines = this.get('paymentLines');
            var newPaymentline = new module.Paymentline({},{cashRegister:cashRegister});
            if(cashRegister.get('journal').type !== 'cash'){
                newPaymentline.set_amount( this.getDueLeft() );
            }
            paymentLines.add(newPaymentline);
        },
        getName: function() {
            return this.get('name');
        },
        getSubtotal : function(){
            return (this.get('orderLines')).reduce((function(sum, orderLine){
                return sum + orderLine.get_display_price();
            }), 0);
        },
        getTotalTaxIncluded: function() {
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
                return sum + orderLine.get_price_with_tax();
            }), 0);
        },
        getDiscountTotal: function() {
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
                return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100) * orderLine.get_quantity());
            }), 0);
        },
        getTotalTaxExcluded: function() {
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
                return sum + orderLine.get_price_without_tax();
            }), 0);
        },
        getTax: function() {
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
                return sum + orderLine.get_tax();
            }), 0);
        },
        getPaidTotal: function() {
            return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
                return sum + paymentLine.get_amount();
            }), 0);
        },
        getChange: function() {
            return this.getPaidTotal() - this.getTotalTaxIncluded();
        },
        getDueLeft: function() {
            return this.getTotalTaxIncluded() - this.getPaidTotal();
        },
        set_cashier_name: function(name){
            this.set('cashier_name', name);
        },
        // sets the type of receipt 'receipt'(default) or 'invoice'
        set_receipt_type: function(type){
            this.receipt_type = type;
        },
        get_receipt_type: function(){
            return this.receipt_type;
        },
        // the client related to the current order.
        set_client: function(client){
            this.set('client',client);
        },
        get_client: function(){
            return this.get('client');
        },
        get_client_name: function(){
            var client = this.get('client');
            return client ? client.name : "";
        },
        // the order also stores the screen status, as the PoS supports
        // different active screens per order. This method is used to
        // store the screen status.
        set_screen_data: function(key,value){
            if(arguments.length === 2){
                this.screen_data[key] = value;
            }else if(arguments.length === 1){
                for(key in arguments[0]){
                    this.screen_data[key] = arguments[0][key];
                }
            }
        },
        //see set_screen_data
        get_screen_data: function(key){
            return this.screen_data[key];
        },
        // exports a JSON for receipt printing
        export_for_printing: function(){
            var orderlines = [];
            this.get('orderLines').each(function(orderline){
                orderlines.push(orderline.export_for_printing());
            });

            var paymentlines = [];
            this.get('paymentLines').each(function(paymentline){
                paymentlines.push(paymentline.export_for_printing());
            });
            var client  = this.get('client');
            var cashier = this.pos.get('cashier') || this.pos.get('user');
            var company = this.pos.get('company');
            var shop    = this.pos.get('shop');
            var date = new Date();

            return {
                orderlines: orderlines,
                paymentlines: paymentlines,
                subtotal: this.getSubtotal(),
                total_with_tax: this.getTotalTaxIncluded(),
                total_without_tax: this.getTotalTaxExcluded(),
                total_tax: this.getTax(),
                total_paid: this.getPaidTotal(),
                total_discount: this.getDiscountTotal(),
                change: this.getChange(),
                name : this.getName(),
                client: client ? client.name : null ,
                invoice_id: null,   //TODO
                cashier: cashier ? cashier.name : null,
                date: { 
                    year: date.getFullYear(), 
                    month: date.getMonth(), 
                    date: date.getDate(),       // day of the month 
                    day: date.getDay(),         // day of the week 
                    hour: date.getHours(), 
                    minute: date.getMinutes() 
                }, 
                company:{
                    email: company.email,
                    website: company.website,
                    company_registry: company.company_registry,
                    contact_address: company.contact_address, 
                    vat: company.vat,
                    name: company.name,
                    phone: company.phone,
                },
                shop:{
                    name: shop.name,
                },
                currency: this.pos.get('currency'),
            };
        },
        exportAsJSON: function() {
            var orderLines, paymentLines;
            orderLines = [];
            (this.get('orderLines')).each(_.bind( function(item) {
                return orderLines.push([0, 0, item.export_as_JSON()]);
            }, this));
            paymentLines = [];
            (this.get('paymentLines')).each(_.bind( function(item) {
                return paymentLines.push([0, 0, item.export_as_JSON()]);
            }, this));
            return {
                name: this.getName(),
                amount_paid: this.getPaidTotal(),
                amount_total: this.getTotalTaxIncluded(),
                amount_tax: this.getTax(),
                amount_return: this.getChange(),
                lines: orderLines,
                statement_ids: paymentLines,
                pos_session_id: this.pos.get('pos_session').id,
                partner_id: this.pos.get('client') ? this.pos.get('client').id : undefined,
                user_id: this.pos.get('cashier') ? this.pos.get('cashier').id : this.pos.get('user').id,
                cashier_name: this.pos.get('selectedOrder').get('cashier_name'),
            };
        },
        getSelectedLine: function(){
            return this.selected_orderline;
        },
        selectLine: function(line){
            if(line){
                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);
                }
            }else{
                this.selected_orderline = undefined;
            }
        },
    });

    

};

openerp.point_of_sale = function(instance) {
    instance.point_of_sale = {};

    var module = instance.point_of_sale;

    openerp_pos_db(instance,module);            // import db.js
    openerp_pos_models(instance,module);        // import pos_models.js
    openerp_pos_basewidget(instance,module);    // import pos_basewidget.js
    openerp_pos_keyboard(instance,module);      // import  pos_keyboard_widget.js
    openerp_pos_scrollbar(instance,module);     // import pos_scrollbar_widget.js
    openerp_pos_screens(instance,module);       // import pos_screens.js
    openerp_pos_widgets(instance,module);       // import pos_widgets.js
    openerp_pos_devices(instance,module);       // import pos_devices.js

    // cashiers
    openerp_pos_cashier(instance,module);       // import openerp_pos_cashier

    instance.web.client_actions.add('pos.ui', 'instance.point_of_sale.PosWidget');
};

Il y a deux fonctions principales dans ce fichier.

La fonction openerp_pos_cashier()
Elle va nous permettre d'ajouter les fonctions nécessaires au module et elle va nous permettre également de modifier certaines fonctions d'origine.

La fonction openerp.point_of_sale()
Nous allons la modifier pour que notre module soit pris en compte dans le Point De Vente.

VIII-A-1. La fonction openerp_pos_cashier()

Cette fonction se déclare comme ceci :

Déclaration de la fonction openerp_pos_cashier()
Sélectionnez
function openerp_pos_cashier(instance, module){ 
}

Cette fonction sera appelée plus loin dans la fonction qui crée le Point De Vente.

Je ne vais pas tout pouvoir vous expliquer. J'ai procédé en étudiant les scripts des modules d'OpenERP à la loupe pour essayer d'en déduire comment m'y prendre.
De fait, si à un moment vous relevez une erreur ou une approximation dans mes explications, ou même dans le code, n'hésitez pas à me contacter pour m'en faire part.

Ensuite, on va instancier quelques objets :

 
Sélectionnez
var module = instance.point_of_sale;
var QWeb = instance.web.qweb;
_t = instance.web._t;

Le module sera une instance du Point De Vente.
QWeb, c'est le moteur de rendu des modèles. Je vous invite à lire la documentation sur le site d'OpenERP : Documentation QWebDocummentation QWeb
_t est une instance de _t (?!??). Pour tout vous dire, je ne sais pas ce que c'est. Lorsque j'ai créé le module pour la première fois, cette instance n'existait pas dans la version d'OpenERP que j'avais. Elle a été rajoutée ensuite. Elle est utilisée pour l'action du bouton Fermer qui est en haut du Point De Vente. Comme vous le verrez plus loin, on importera le bouton de fermeture du Point De Vente, cette instance est donc nécessaire.

Nous allons ensuite déclarer une variable globale. Elle nous servira plus loin pour stocker le nom du caissier sélectionné.

Variable globale
Sélectionnez
globalCashier = null;

Pour qu'une variable soit globale, on la déclare sans le mot-clé var.
Une variable locale se déclare comme ceci : var Mavariable = 'quelque chose';

VIII-A-2. Le module CashierWidget

Maintenant nous allons surcharger le module d'origine PosWidget.
C'est en fait, le module qui contient tout le Point De Vente.

Nous n'allons pas réellement « surcharger » le module, nous allons y inclure des fonctions supplémentaires quand ce sera possible, avec la fonction include().

module.CashierWidget
Sélectionnez

    module.CashierWidget = module.PosWidget.include({
        template: 'PosWidget',  

        // recuperation de l'ID du POS
        get_cur_pos_config_id: function(){
            var self = this;
            var config = self.pos.get('pos_config');
            var config_id = null;
                     
            if(config){
                config_id = config.id;
                
                return config_id;
            }        
            return '';    
        },

        fetch: function(model, fields, domain, ctx){
            return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all()
        },

        cashier_change: function(name){
            globalCashier = name;

            $('#pay-screen-cashier-name').html(name);
            //console.log('cashier_change : ' + name);
            
            if(name != ''){
                $('.gotopay-button').removeAttr('disabled');                 
            } else{
                $('.gotopay-button').attr('disabled', 'disabled');
            }
        },

        get_cashiers: function(config_id){
            var self = this;
            var cashier_list = [];

            var loaded = self.fetch('pos.cashier',['cashier_name'],[['pos_config_id','=', config_id], ['active', '=','true']])
                .then(function(cashiers){
                     for(var i = 0, len = cashiers.length; i < len; i++){
                        cashier_list.push(cashiers[i].cashier_name);
                     }

                    if(cashier_list.length > 0){
                        
                        for(var i = 0, len = cashier_list.length; i < len; i++){
                            var content = self.$('#cashier-select').html();
                            var new_option = '<option value="' + cashier_list[i] + '">' + cashier_list[i] + '</option>\n';
                            self.$('#cashier-select').html(content + new_option);
                            }

                        self.$('#AlertNoCashier').css('display', 'none');
                        self.$('#cashier-select').selectedIndex = 0;
                        globalCashier = cashier_list[0];
                        self.cashier_change(globalCashier);

                    } else{

                        // if there are no cashier
                        self.$('#AlertNoCashier').css('display', 'block');
                        self.$('.gotopay-button').attr('disabled', 'disabled');
                    }
                });
        }, 

        renderElement: function() {
            var self = this;
            this._super();

            self.$('#cashier-select').change(function(){
                var name = this.value;
                self.cashier_change(name);
            });
        },

        
        build_widgets: function() {
            var self = this;

            // --------  Screens ---------

            this.product_screen = new module.ProductScreenWidget(this,{});
            this.product_screen.appendTo($('#rightpane'));

            this.receipt_screen = new module.ReceiptScreenWidget(this, {});
            this.receipt_screen.appendTo($('#rightpane'));

            this.payment_screen = new module.PaymentScreenWidget(this, {});
            this.payment_screen.appendTo($('#rightpane'));

            this.welcome_screen = new module.WelcomeScreenWidget(this,{});
            this.welcome_screen.appendTo($('#rightpane'));

            this.client_payment_screen = new module.ClientPaymentScreenWidget(this, {});
            this.client_payment_screen.appendTo($('#rightpane'));

            this.scale_invite_screen = new module.ScaleInviteScreenWidget(this, {});
            this.scale_invite_screen.appendTo($('#rightpane'));

            this.scale_screen = new module.ScaleScreenWidget(this,{});
            this.scale_screen.appendTo($('#rightpane'));

            // --------  Popups ---------

            this.help_popup = new module.HelpPopupWidget(this, {});
            this.help_popup.appendTo($('.point-of-sale'));

            this.error_popup = new module.ErrorPopupWidget(this, {});
            this.error_popup.appendTo($('.point-of-sale'));

            this.error_product_popup = new module.ProductErrorPopupWidget(this, {});
            this.error_product_popup.appendTo($('.point-of-sale'));

            this.error_session_popup = new module.ErrorSessionPopupWidget(this, {});
            this.error_session_popup.appendTo($('.point-of-sale'));

            this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {});
            this.choose_receipt_popup.appendTo($('.point-of-sale'));

            this.error_negative_price_popup = new module.ErrorNegativePricePopupWidget(this, {});
            this.error_negative_price_popup.appendTo($('.point-of-sale'));

            // --------  Misc ---------

            this.notification = new module.SynchNotificationWidget(this,{});
            this.notification.appendTo(this.$('#rightheader'));

            this.username   = new module.UsernameWidget(this,{});
            this.username.replace(this.$('.placeholder-UsernameWidget'));

            this.action_bar = new module.ActionBarWidget(this);
            this.action_bar.appendTo($(".point-of-sale #rightpane"));

            this.left_action_bar = new module.ActionBarWidget(this);
            this.left_action_bar.appendTo($(".point-of-sale #leftpane"));

            this.gotopay = new module.GoToPayWidget(this, {});
            this.gotopay.replace($('#placeholder-GoToPayWidget'));

            this.paypad = new module.PaypadWidget(this, {});
            this.paypad.replace($('#placeholder-PaypadWidget'));

            this.numpad = new module.NumpadWidget(this);
            this.numpad.replace($('#placeholder-NumpadWidget'));

            this.order_widget = new module.OrderWidget(this, {});
            this.order_widget.replace($('#placeholder-OrderWidget'));

            this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, {
                'keyboard_model': 'simple'
            });
            this.onscreen_keyboard.appendTo($(".point-of-sale #content")); 

            this.close_button = new module.HeaderButtonWidget(this,{
                label: _t('Close'),
                action: function(){ self.try_close(); },
            });
            this.close_button.appendTo(this.$('#rightheader'));

            this.client_button = new module.HeaderButtonWidget(this,{
                label: _t('Self-Checkout'),
                action: function(){ self.screen_selector.set_user_mode('client'); },
            });
            this.client_button.appendTo(this.$('#rightheader'));

            
            // --------  Screen Selector ---------

            this.screen_selector = new module.ScreenSelector({
                pos: this.pos,
                screen_set:{
                    'products': this.product_screen,
                    'payment' : this.payment_screen,
                    'client_payment' : this.client_payment_screen,
                    'scale_invite' : this.scale_invite_screen,
                    'scale':    this.scale_screen,
                    'receipt' : this.receipt_screen,
                    'welcome' : this.welcome_screen,
                },
                popup_set:{
                    'help': this.help_popup,
                    'error': this.error_popup,
                    'error-product': this.error_product_popup,
                    'error-session': this.error_session_popup,
                    'error-negative-price': this.error_negative_price_popup,
                    'choose-receipt': this.choose_receipt_popup,
                },
                default_client_screen: 'welcome',
                default_cashier_screen: 'products',
                default_mode: this.pos.iface_self_checkout ?  'client' : 'cashier',
            });

            if(this.pos.debug){
                this.debug_widget = new module.DebugWidget(this);
                this.debug_widget.appendTo(this.$('#content'));
            }
        },

    });

Vous noterez que les déclarations ou les fonctions à l'intérieur du module sont séparées par une virgule.

En premier lieu, nous déclarons le modèle de vue qui sera utilisé pour le module.

template
Sélectionnez
template: 'PosWidget', 

Cela veut dire que lorsque le module sera chargé, il se reportera également à la vue PosWidget, que nous étudierons plus loin.

Nous allons ensuite créer une fonction qui nous permettra de récupérer l'ID du Point De Vente pour ensuite récupérer les caissiers qui appartiennent à ce Point De Vente.

 
get_cur_pos_config_id()
Sélectionnez

        // recuperation de l'ID du POS
        get_cur_pos_config_id: function(){
            var self = this;
            var config = self.pos.get('pos_config');
            var config_id = null;
                     
            if(config){
                config_id = config.id;
                
                return config_id;
            }        
            return '';    
        },

Cette fonction utilise la fonction pos.get() qui a été définie dans le module d'origine.
Lorsqu'une fonction appartient au module d'origine du Point De Vente, elle s'écrira pos.la_fonction().
Si un enregistrement a été trouvé, elle retournera l'ID du POS actuellement utilisé.

Nous allons ensuite ajouter une fonction que j'ai piquée dans un module. Cette fonction permet d'effectuer une requête sur une table de la base de données.

fetch()
Sélectionnez
fetch: function(model, fields, domain, ctx){
    return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all()
},

Avec les paramètres adéquats, on pourra récupérer les données de la table. Nous verrons ceci plus loin.

Nous allons ensuite créer une fonction qui sera appelée depuis l'interface du POS (lors du onchange() de la liste des caissiers, par exemple).

cashier_change()
Sélectionnez

cashier_change: function(name){
    globalCashier = name;

    $('#pay-screen-cashier-name').html(name);
    //console.log('cashier_change : ' + name);
            
    if(name != ''){
        $('.gotopay-button').removeAttr('disabled');                 
    } else{
        $('.gotopay-button').attr('disabled', 'disabled');
    }
},

J'ai laissé pour vous un commentaire particulier :
//console.log('cashier_change : ' + name);
Si vous utilisez l'extension Firebug, qui est très efficace pour le débogage JavaScript, et que vous décommentez cette ligne, vous verrez le message dans la console de Firebug à chaque fois que la fonction cashier_change() sera appelée.
Lorsque vous construirez votre module perso, n'hésitez pas à user de cette astuce dans vos scripts JavaScript, notamment pour vérifier que les fonctions sont bien exécutées et que vos variables contiennent bien les valeurs que vous attendiez.
Évidemment, vous n'oublierez pas de supprimer vos commandes console.log() ou de les commenter avant de passer en production.
Enfin, dernière petite astuce, si vous utilisez console.log() en y passant un objet, vous ne manquerez pas de l'utiliser ainsi : console.log(JSON.stringify(monObjet));

Dès que la fonction sera appelée, le nom du caissier sera stocké dans la variable globale globalCashier.
Puis le nom du caissier sera également envoyé à la balise <div id="pay-screen-cashier-name"></div> qui apparaît sur la page de paiement.

Également, si le nom du caissier est vide (il n'y a donc pas de caissier pour ce Point De Vente), on désactivera le bouton Payer du Point De Vente. Aucune vente ne pourra se faire.

Une petite explication !
La partie gauche du Point De Vente affiche normalement un pavé numérique ainsi que les boutons de paiement.
Il y aura autant de boutons que vous aurez configuré de modes de paiement pour le Point De Vente.
Afin d'interdire la vente si aucun caissier n'a été créé dans le Point De Vente, j'ai déplacé les boutons de paiement sur la page de paiement, et à la place j'ai mis un bouton Payer.
Il est plus facile de désactiver un seul bouton qui est écrit « en dur » dans la vue, plutôt que de concocter une fonction qui désactiverait les boutons de paiement qui sont générés dynamiquement.

L'interface de paiement standard avec les divers boutons de paiement
L'interface de paiement standard avec
les divers boutons de paiement
L'interface de paiement avec seulement le bouton Payer et la liste déroulante
L'interface de paiement avec seulement
le bouton Payer et la liste déroulante

Nous allons maintenant ajouter la fonction get_cashiers() qui va récupérer les caissiers dans la base de données et construire les options de la liste déroulante des caissiers.

get_cashiers()
Sélectionnez

get_cashiers: function(config_id){
    var self = this;
    var cashier_list = [];

    var loaded = self.fetch('pos.cashier',['cashier_name'],[['pos_config_id','=', config_id], ['active', '=','true']])
               .then(function(cashiers){
                   for(var i = 0, len = cashiers.length; i < len; i++){
                       cashier_list.push(cashiers[i].cashier_name);
                   }

                   if(cashier_list.length > 0){
                        
                       for(var i = 0, len = cashier_list.length; i < len; i++){
                           var content = self.$('#cashier-select').html();
                           var new_option = '<option value="' + cashier_list[i] + '">' + cashier_list[i] + '</option>\n';
                           self.$('#cashier-select').html(content + new_option);
                       }

                       self.$('#AlertNoCashier').css('display', 'none');
                       self.$('#cashier-select').selectedIndex = 0;
                       globalCashier = cashier_list[0];
                       self.cashier_change(globalCashier);

                  } else{

                      // if there are no cashier
                      self.$('#AlertNoCashier').css('display', 'block');
                      self.$('.gotopay-button').attr('disabled', 'disabled');
                  }
              });
},

Nous créons d'abord un tableau vide

 
Sélectionnez
var cashier_list = [];

Puis nous effectuons notre requête avec la fonction fetch() que nous avions vue auparavant

 
Sélectionnez
var loaded = self.fetch('pos.cashier',['cashier_name'],[['pos_config_id','=', config_id], ['active', '=','true']])

Ici, on va effectuer une requête (SELECT) sur la table pos_cashier; on va récupérer le champ cashier_name des caissiers qui appartiennent au Point De Vente dont pos_config_id sera égal à config_id qu'on aura passé en paramètre ET qui seront actifs !

Puis on va créer les options de la liste déroulante avec la fonction qui sera exécutée à la suite de la requête.

 
Sélectionnez

.then(function(cashiers){
     for(var i = 0, len = cashiers.length; i < len; i++){
        cashier_list.push(cashiers[i].cashier_name);
    }

    if(cashier_list.length > 0){
                        
        for(var i = 0, len = cashier_list.length; i < len; i++){
            var content = self.$('#cashier-select').html();
            var new_option = '<option value="' + cashier_list[i] + '">' + cashier_list[i] + '</option>\n';
            self.$('#cashier-select').html(content + new_option);
        }

        self.$('#AlertNoCashier').css('display', 'none');
        self.$('#cashier-select').selectedIndex = 0;
        globalCashier = cashier_list[0];
        self.cashier_change(globalCashier);

    } else{

        // if there are no cashier
        self.$('#AlertNoCashier').css('display', 'block');
        self.$('.gotopay-button').attr('disabled', 'disabled');
    }
});

Je vous laisse décortiquer tout seul la fonction ci-dessus.
À noter que s'il n'y a aucun caissier, on affichera un message d'erreur dans la <div id="AlertNoCashier"></div> et on désactivera le bouton Payer.

Un message d'erreur est affiché et le bouton Payer est désactivé
Un message d'erreur est affiché et le bouton Payer est désactivé

Vous voyez également que dès que la liste des caissiers est construite, on stocke le nom du premier caissier dans la variable globale globalCashier, puis on appelle la fonction cashier_change() en lui passant le nom du premier caissier de la liste.

Puis nous allons ajouter une fonction qui sera appelée au chargement du module et qui appellera la fonction cashier_change() pour initialiser le Point De Vente.

renderElement()
Sélectionnez

renderElement: function() {
    var self = this;
    this._super();

    self.$('#cashier-select').change(function(){
        var name = this.value;
        self.cashier_change(name);
    });
},

Et pour finir, on va copier/coller la fonction build_widgets() d'origine, qui se trouve dans le fichier widgets.js du module point_of_sale, et la modifier.

build_widgets()
Sélectionnez
build_widgets: function() {
            var self = this;

            // --------  Screens ---------

            this.product_screen = new module.ProductScreenWidget(this,{});
            this.product_screen.appendTo($('#rightpane'));

            this.receipt_screen = new module.ReceiptScreenWidget(this, {});
            this.receipt_screen.appendTo($('#rightpane'));

            this.payment_screen = new module.PaymentScreenWidget(this, {});
            this.payment_screen.appendTo($('#rightpane'));

            this.welcome_screen = new module.WelcomeScreenWidget(this,{});
            this.welcome_screen.appendTo($('#rightpane'));

            this.client_payment_screen = new module.ClientPaymentScreenWidget(this, {});
            this.client_payment_screen.appendTo($('#rightpane'));

            this.scale_invite_screen = new module.ScaleInviteScreenWidget(this, {});
            this.scale_invite_screen.appendTo($('#rightpane'));

            this.scale_screen = new module.ScaleScreenWidget(this,{});
            this.scale_screen.appendTo($('#rightpane'));

            // --------  Popups ---------

            this.help_popup = new module.HelpPopupWidget(this, {});
            this.help_popup.appendTo($('.point-of-sale'));

            this.error_popup = new module.ErrorPopupWidget(this, {});
            this.error_popup.appendTo($('.point-of-sale'));

            this.error_product_popup = new module.ProductErrorPopupWidget(this, {});
            this.error_product_popup.appendTo($('.point-of-sale'));

            this.error_session_popup = new module.ErrorSessionPopupWidget(this, {});
            this.error_session_popup.appendTo($('.point-of-sale'));

            this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {});
            this.choose_receipt_popup.appendTo($('.point-of-sale'));

            this.error_negative_price_popup = new module.ErrorNegativePricePopupWidget(this, {});
            this.error_negative_price_popup.appendTo($('.point-of-sale'));

            // --------  Misc ---------

            this.notification = new module.SynchNotificationWidget(this,{});
            this.notification.appendTo(this.$('#rightheader'));

            this.username   = new module.UsernameWidget(this,{});
            this.username.replace(this.$('.placeholder-UsernameWidget'));

            this.action_bar = new module.ActionBarWidget(this);
            this.action_bar.appendTo($(".point-of-sale #rightpane"));

            this.left_action_bar = new module.ActionBarWidget(this);
            this.left_action_bar.appendTo($(".point-of-sale #leftpane"));

            this.gotopay = new module.GoToPayWidget(this, {});         // On ajoute ici la création
            this.gotopay.replace($('#placeholder-GoToPayWidget'));     // du widget qui affiche le bouton Payer

            this.paypad = new module.PaypadWidget(this, {});
            this.paypad.replace($('#placeholder-PaypadWidget'));

            this.numpad = new module.NumpadWidget(this);
            this.numpad.replace($('#placeholder-NumpadWidget'));

            this.order_widget = new module.OrderWidget(this, {});
            this.order_widget.replace($('#placeholder-OrderWidget'));

            this.onscreen_keyboard = new module.OnscreenKeyboardWidget(this, {
                'keyboard_model': 'simple'
            });
            this.onscreen_keyboard.appendTo($(".point-of-sale #content")); 

            this.close_button = new module.HeaderButtonWidget(this,{
                label: _t('Close'),
                action: function(){ self.try_close(); },
            });
            this.close_button.appendTo(this.$('#rightheader'));

            this.client_button = new module.HeaderButtonWidget(this,{
                label: _t('Self-Checkout'),
                action: function(){ self.screen_selector.set_user_mode('client'); },
            });
            this.client_button.appendTo(this.$('#rightheader'));

            
            // --------  Screen Selector ---------

            this.screen_selector = new module.ScreenSelector({
                pos: this.pos,
                screen_set:{
                    'products': this.product_screen,
                    'payment' : this.payment_screen,
                    'client_payment' : this.client_payment_screen,
                    'scale_invite' : this.scale_invite_screen,
                    'scale':    this.scale_screen,
                    'receipt' : this.receipt_screen,
                    'welcome' : this.welcome_screen,
                },
                popup_set:{
                    'help': this.help_popup,
                    'error': this.error_popup,
                    'error-product': this.error_product_popup,
                    'error-session': this.error_session_popup,
                    'error-negative-price': this.error_negative_price_popup,
                    'choose-receipt': this.choose_receipt_popup,
                },
                default_client_screen: 'welcome',
                default_cashier_screen: 'products',
                default_mode: this.pos.iface_self_checkout ?  'client' : 'cashier',
            });

            if(this.pos.debug){
                this.debug_widget = new module.DebugWidget(this);
                this.debug_widget.appendTo(this.$('#content'));
            }
        },

On rajoutera dans la liste des widgets à construire, le widget qui affichera le bouton Payer qu'on verra plus loin.

 
Sélectionnez

this.gotopay = new module.GoToPayWidget(this, {});                  
this.gotopay.replace($('#placeholder-GoToPayWidget')); 

Ici on précise que le widget sera placé dans la <div id="placeholder-GoToPayWidget"></div> qu'on mettra dans la vue plus tard.

VIII-A-3. Le module CashierPayScreenWidget

Ce module va nous permettre d'ajouter une fonction sur le module d'origine module.PaymentScreenWidget (qui se trouve dans le fichier screen.js du module point_of_sale), toujours grâce à la fonction include()

PaymentScreenWidget
Sélectionnez
module.CashierPayScreenWidget = module.PaymentScreenWidget.include({
    template: 'PaymentScreenWidget', 

    show: function(){
        this._super();
        var self = this;
        this.$('#pay-screen-cashier-name').html(globalCashier);
        this.$('#ticket-screen-cashier-name').html(globalCashier);
        this.pos.get('selectedOrder').set_cashier_name(globalCashier);

        this.paypad = new module.PaypadWidget(this, {});
        this.paypad.replace($('#placeholder-PaypadWidget'));
    },

}); 

Le module sera rattaché au modèle de vue PaymentScreenWidget.

On va rajouter quelques instructions dans la fonction show() du module.

On va tout d'abord récupérer le nom du caissier qui se trouve dans la variable globale globalCashier qu'on va afficher sur la page de paiement et sur le ticket de caisse.

On va ensuite enregistrer le nom du caissier dans la commande en cours avec la fonction ci-dessous

set_cashier_name
Sélectionnez
this.pos.get('selectedOrder').set_cashier_name(globalCashier);

Cette fonction n'existe pas encore, nous allons la rajouter plus tard.

Finalement, nous allons recréer les boutons de paiement que j'avais enlevés, de plus, ils seront détruits à la fin de chaque commande.

PaypadWidget
Sélectionnez
this.paypad = new module.PaypadWidget(this, {});
this.paypad.replace($('#placeholder-PaypadWidget'));

Le module PaypadWidget est celui d'origine créé pour le Point De Vente. C'est celui qu'on avait retiré auparavant, il était à côté du pavé numérique, et qu'on a remplacé par le bouton Payer.

VIII-A-4. Le module CashierReceiptScreenWidget

Ici, nous allons rajouter des instructions dans la fonction d'origine refresh() du module ReceiptScreenWidget qui est dans le fichier screen.js du module point_of_sale. C'est le module qui permettra d'afficher le nom du caissier sur le ticket de caisse.

En fait, on va recopier la fonction d'origine et rajouter les trois dernières lignes (la condition if()).

CashierReceiptScreenWidget
Sélectionnez
module.CashierReceiptScreenWidget = module.ReceiptScreenWidget.include({

    refresh: function() {
        this._super();
        $('.pos-receipt-container', this.$el).html(QWeb.render('PosTicket',{widget:this}));

        if(globalCashier != ''){
            this.$('#ticket-screen-cashier-name').html(globalCashier);           
        }         
    }, 
 });

Vous voyez donc qu'on récupère le nom du caissier qui est dans la variable globalCashier pour l'envoyer dans la <div id="ticket-screen-cashier-name"></div> qui apparaîtra sur le ticket. On le retrouvera plus tard lorsqu'on s'occupera du fichier des vues (XML).

VIII-A-5. Le module GoToPayWidget

Maintenant, nous allons créer le module qui va accueillir le bouton Payer.
Attention, ce n'est pas le bouton en lui-même, c'est simplement le « container » qui va accueillir le bouton.

 
Sélectionnez
module.GoToPayWidget = module.PosBaseWidget.extend({
    template: 'GoToPayWidget',
    init: function(parent, options) {
        this._super(parent);
    },

    renderElement: function() {
        var self = this;
        this._super();

        var button = new module.GoToPayButtonWidget(self);
        button.appendTo(self.$el);
    },
});

Comme pour les autres modules d'origine du Point De Vente, ce sont des extensions du module PosBaseWidget.
On lui attribue le modèle de vue avec la déclaration ci-dessous :

 
Sélectionnez
template: 'GoToPayWidget',

Puis on ajoute la fonction init() comme pour les modules d'origine.

Nous allons ensuite ajouter une fonction qui ordonnera à QWeb (le moteur de rendu de templates) de rajouter le bouton Payer, qu'on verra juste après, dans ce module (dans son container, en quelque sorte).

renderElement
Sélectionnez
renderElement: function() {
    var self = this;
    this._super();

    var button = new module.GoToPayButtonWidget(self);
    button.appendTo(self.$el);
},

C'est tout pour ce module.

VIII-A-6. Le module GoToPayButtonWidget (Le bouton Payer)

Cette fois-ci, nous allons créer le module du bouton Payer.
Comme pour le module précédent, ce sera une extension du module de base PosBaseWidget, nous lui attribuons le modèle de vue GoToPayButtonWidget, nous lui ajoutons la fonction init() qui va bien, puis nous ajoutons également la fonction renderElement().

GoToPayButtonWidget
Sélectionnez
module.GoToPayButtonWidget = module.PosBaseWidget.extend({
    template: 'GoToPayButtonWidget',
    init: function(parent, options) {
        this._super(parent);
    },

    renderElement: function() {
        var self = this;
        this._super();

        this.$el.click(function(){
            self.pos_widget.screen_selector.set_current_screen('payment');
         });
    },
 });

Nous ajoutons la fonction click() dans renderElement().
Cette fonction sera exécutée, vous l'avez compris, lors du clic sur le bouton (événement onclick()).
Cette fonction elle-même appellera la fonction qui permet d'afficher les différents écrans du Point De Vente.
En l'occurrence, on affichera la page de paiement.

set_current_screen()
Sélectionnez
this.$el.click(function(){
    self.pos_widget.screen_selector.set_current_screen('payment');
});

VIII-A-7. Le module Order

C'est le module qui crée les commandes, qui les stocke dans le navigateur (LocalStorage), puis qui envoie la commande dans la base de données.

Il n'est pas possible de surcharger le module d'origine, car dès son initialisation le module renvoie un objet (la commande) dans le Point De Vente, et nous devons le modifier pour qu'il prenne en compte le nom du caissier.

Nous allons donc copier/coller tout le module que vous trouverez dans le fichier model.js du module point_of_sale, puis nous allons rajouter deux ou trois choses.

Je vous mets le code du module et je vous expliquerai seulement les fonctions ou objets que j'ai ajoutés.

module.Order
Sélectionnez

module.Order = Backbone.Model.extend({
        initialize: function(attributes){
            Backbone.Model.prototype.initialize.apply(this, arguments);
            this.set({
                creationDate:   new Date(),
                orderLines:     new module.OrderlineCollection(),
                paymentLines:   new module.PaymentlineCollection(),
                name:           "Order " + this.generateUniqueId(),
                client:         null,
                cashier_name:   null,
            });
            this.pos =     attributes.pos; 
            this.selected_orderline = undefined;
            this.screen_data = {};  // see ScreenSelector
            this.receipt_type = 'receipt';  // 'receipt' || 'invoice'
            return this;
        },
        generateUniqueId: function() {
            return new Date().getTime();
        },
        addProduct: function(product, options){
            options = options || {};
            var attr = product.toJSON();
            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());
        },
        removeOrderline: function( line ){
            this.get('orderLines').remove(line);
            this.selectLine(this.getLastOrderline());
        },
        getLastOrderline: function(){
            return this.get('orderLines').at(this.get('orderLines').length -1);
        },
        addPaymentLine: function(cashRegister) {
            var paymentLines = this.get('paymentLines');
            var newPaymentline = new module.Paymentline({},{cashRegister:cashRegister});
            if(cashRegister.get('journal').type !== 'cash'){
                newPaymentline.set_amount( this.getDueLeft() );
            }
            paymentLines.add(newPaymentline);
        },
        getName: function() {
            return this.get('name');
        },
        getSubtotal : function(){
            return (this.get('orderLines')).reduce((function(sum, orderLine){
                return sum + orderLine.get_display_price();
            }), 0);
        },
        getTotalTaxIncluded: function() {
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
                return sum + orderLine.get_price_with_tax();
            }), 0);
        },
        getDiscountTotal: function() {
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
                return sum + (orderLine.get_unit_price() * (orderLine.get_discount()/100) * orderLine.get_quantity());
            }), 0);
        },
        getTotalTaxExcluded: function() {
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
                return sum + orderLine.get_price_without_tax();
            }), 0);
        },
        getTax: function() {
            return (this.get('orderLines')).reduce((function(sum, orderLine) {
                return sum + orderLine.get_tax();
            }), 0);
        },
        getPaidTotal: function() {
            return (this.get('paymentLines')).reduce((function(sum, paymentLine) {
                return sum + paymentLine.get_amount();
            }), 0);
        },
        getChange: function() {
            return this.getPaidTotal() - this.getTotalTaxIncluded();
        },
        getDueLeft: function() {
            return this.getTotalTaxIncluded() - this.getPaidTotal();
        },
        set_cashier_name: function(name){
            this.set('cashier_name', name);
        },
        // sets the type of receipt 'receipt'(default) or 'invoice'
        set_receipt_type: function(type){
            this.receipt_type = type;
        },
        get_receipt_type: function(){
            return this.receipt_type;
        },
        // the client related to the current order.
        set_client: function(client){
            this.set('client',client);
        },
        get_client: function(){
            return this.get('client');
        },
        get_client_name: function(){
            var client = this.get('client');
            return client ? client.name : "";
        },
        // the order also stores the screen status, as the PoS supports
        // different active screens per order. This method is used to
        // store the screen status.
        set_screen_data: function(key,value){
            if(arguments.length === 2){
                this.screen_data[key] = value;
            }else if(arguments.length === 1){
                for(key in arguments[0]){
                    this.screen_data[key] = arguments[0][key];
                }
            }
        },
        //see set_screen_data
        get_screen_data: function(key){
            return this.screen_data[key];
        },
        // exports a JSON for receipt printing
        export_for_printing: function(){
            var orderlines = [];
            this.get('orderLines').each(function(orderline){
                orderlines.push(orderline.export_for_printing());
            });

            var paymentlines = [];
            this.get('paymentLines').each(function(paymentline){
                paymentlines.push(paymentline.export_for_printing());
            });
            var client  = this.get('client');
            var cashier = this.pos.get('cashier') || this.pos.get('user');
            var company = this.pos.get('company');
            var shop    = this.pos.get('shop');
            var date = new Date();

            return {
                orderlines: orderlines,
                paymentlines: paymentlines,
                subtotal: this.getSubtotal(),
                total_with_tax: this.getTotalTaxIncluded(),
                total_without_tax: this.getTotalTaxExcluded(),
                total_tax: this.getTax(),
                total_paid: this.getPaidTotal(),
                total_discount: this.getDiscountTotal(),
                change: this.getChange(),
                name : this.getName(),
                client: client ? client.name : null ,
                invoice_id: null,   //TODO
                cashier: cashier ? cashier.name : null,
                date: { 
                    year: date.getFullYear(), 
                    month: date.getMonth(), 
                    date: date.getDate(),       // day of the month 
                    day: date.getDay(),         // day of the week 
                    hour: date.getHours(), 
                    minute: date.getMinutes() 
                }, 
                company:{
                    email: company.email,
                    website: company.website,
                    company_registry: company.company_registry,
                    contact_address: company.contact_address, 
                    vat: company.vat,
                    name: company.name,
                    phone: company.phone,
                },
                shop:{
                    name: shop.name,
                },
                currency: this.pos.get('currency'),
            };
        },
        exportAsJSON: function() {
            var orderLines, paymentLines;
            orderLines = [];
            (this.get('orderLines')).each(_.bind( function(item) {
                return orderLines.push([0, 0, item.export_as_JSON()]);
            }, this));
            paymentLines = [];
            (this.get('paymentLines')).each(_.bind( function(item) {
                return paymentLines.push([0, 0, item.export_as_JSON()]);
            }, this));
            return {
                name: this.getName(),
                amount_paid: this.getPaidTotal(),
                amount_total: this.getTotalTaxIncluded(),
                amount_tax: this.getTax(),
                amount_return: this.getChange(),
                lines: orderLines,
                statement_ids: paymentLines,
                pos_session_id: this.pos.get('pos_session').id,
                partner_id: this.pos.get('client') ? this.pos.get('client').id : undefined,
                user_id: this.pos.get('cashier') ? this.pos.get('cashier').id : this.pos.get('user').id,
                cashier_name: this.pos.get('selectedOrder').get('cashier_name'),
            };
        },
        getSelectedLine: function(){
            return this.selected_orderline;
        },
        selectLine: function(line){
            if(line){
                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);
                }
            }else{
                this.selected_orderline = undefined;
            }
        },
    });  

Au tout début du module, nous allons rajouter un champ cashier_name dans la commande.

initialize()
Sélectionnez
initialize: function(attributes){
    Backbone.Model.prototype.initialize.apply(this, arguments);
    this.set({
        creationDate:   new Date(),
        orderLines:     new module.OrderlineCollection(),
        paymentLines:   new module.PaymentlineCollection(),
        name:           "Order " + this.generateUniqueId(),
        client:         null,           <!-- Ne pas oublier la virgule -->
        cashier_name:   null,           <!--  Ici on rajoute le champ cashier_name -->
    });
    this.pos =     attributes.pos; 
    this.selected_orderline = undefined;
    this.screen_data = {};  // see ScreenSelector
    this.receipt_type = 'receipt';  // 'receipt' || 'invoice'
    return this;
},

Comme vous le voyez, la commande contient plusieurs champs. On rajoute simplement le champ cashier_name.
Maintenant que le champ est créé, on pourra envoyer le nom du caissier dans la commande au moment opportun.

Ensuite, on va ajouter une fonction dans la liste de fonctions qui existent déjà.

set_cashier_name()
Sélectionnez
set_cashier_name: function(name){
    this.set('cashier_name', name);
},

Quand la fonction set_cashier_name() sera appelée, elle enverra le nom du caissier dans le champ qu'on a ajouté précédemment.

Si vous vous rappelez bien, cette fonction est appelée dans la fonction show() du module CashierPayScreenWidget.
Donc lorsqu'on affichera la page de paiement, le nom du caissier sera envoyé dans la commande.

Maintenant, on va modifier la fonction qui envoie la commande à la base de données.

exportAsJSON()
Sélectionnez
exportAsJSON: function() {
    var orderLines, paymentLines;
    orderLines = [];
    (this.get('orderLines')).each(_.bind( function(item) {
        return orderLines.push([0, 0, item.export_as_JSON()]);
    }, this));
    paymentLines = [];
    (this.get('paymentLines')).each(_.bind( function(item) {
        return paymentLines.push([0, 0, item.export_as_JSON()]);
    }, this));
    return {
        name: this.getName(),
        amount_paid: this.getPaidTotal(),
        amount_total: this.getTotalTaxIncluded(),
        amount_tax: this.getTax(),
        amount_return: this.getChange(),
        lines: orderLines,
        statement_ids: paymentLines,
        pos_session_id: this.pos.get('pos_session').id,
        partner_id: this.pos.get('client') ? this.pos.get('client').id : undefined,
        user_id: this.pos.get('cashier') ? this.pos.get('cashier').id : this.pos.get('user').id,
        cashier_name: this.pos.get('selectedOrder').get('cashier_name'),
    };
},

Nous avons rajouté le champ cashier_name dans le retour de la fonction.

cashier_name
Sélectionnez
cashier_name: this.pos.get('selectedOrder').get('cashier_name'),

Cette fois-ci, nous récupérons le nom du caissier qui a été envoyé à la commande auparavant.

Le module openerp_pos_cashier() est terminé !

VIII-A-8. Le meilleur pour la fin

Le fichier JavaScript est presque terminé.
Il nous faut maintenant inclure notre module à l'intérieur du Point De vente.

Pour cela, il n'y a pas d'autre moyen que de reprendre la fonction qui crée le Point De Vente et d'y ajouter notre module.
Nous rajoutons donc, à la suite du module, la fonction openerp.point_of_sale() qui se trouve dans le fichier main.js du module point_of_sale.

openerp.point_of_sale()
Sélectionnez
openerp.point_of_sale = function(instance) {
    instance.point_of_sale = {};

    var module = instance.point_of_sale;

    openerp_pos_db(instance,module);            // import db.js
    openerp_pos_models(instance,module);        // import pos_models.js
    openerp_pos_basewidget(instance,module);    // import pos_basewidget.js
    openerp_pos_keyboard(instance,module);      // import  pos_keyboard_widget.js
    openerp_pos_scrollbar(instance,module);     // import pos_scrollbar_widget.js
    openerp_pos_screens(instance,module);       // import pos_screens.js
    openerp_pos_widgets(instance,module);       // import pos_widgets.js
    openerp_pos_devices(instance,module);       // import pos_devices.js

    // cashiers
    openerp_pos_cashier(instance,module);       // import openerp_pos_cashier

    instance.web.client_actions.add('pos.ui', 'instance.point_of_sale.PosWidget');
};

On rajoute la ligne openerp_pos_cashier.

Et c'est fini pour le fichier pos_cashier.js !!!

VIII-B. Le fichier pos_cashier.xml

C'est le fichier des vues dont on a besoin pour afficher les données dans le Point De Vente.

Ce fichier est à placer dans le répertoire xml du module.

/opt/modules-openerp/pos_cashiers/static/src/xml
pos_cashier.xml
Sélectionnez
<?xml version="1.0" encoding="UTF-8"?>
<!-- vim:fdl=1:
-->
<templates id="template" xml:space="preserve">

    <!-- Cashiers drop-down list under NumPad -->
    <t t-extend="PosWidget" >
        <t t-jquery="footer" t-operation="append">
            <div id="AlertNoCashier">You must create at least one cashier!</div>
            <div id="cashier-footer">
                <div id="cashier-title">
                    Select a cashier :            
                </div>
                <div id="cashier-frame">
                    <t t-esc="widget.get_cashiers(widget.get_cur_pos_config_id())" />
                    <select id="cashier-select"></select>   
                </div>           
            </div>
        </t>
    </t>

    <!-- Name of the cashier on Payement Page -->
    <t t-extend="PaymentScreenWidget" >
        <t t-jquery=".pos-step-container" t-operation="prepend">
            <div id="pay-screen-cashier">Cashier : 
                <span id="pay-screen-cashier-name">
                </span>
            </div>
        </t>
    </t>

    <!-- Name of the cashier on Ticket -->
    <t t-extend="PosTicket" >
        <t t-jquery="#header-ticket" t-operation="append">
            Cashier : <span id="ticket-screen-cashier-name"></span>
        </t>
    </t>

</templates>

Ici on va utiliser des balises spéciales qui vont permettre au moteur de rendu Qweb d'insérer les objets dans la page.

VIII-B-1. La liste déroulante des caissiers

On va placer la liste déroulante des caissiers sous le pavé numérique.

PosWidget
Sélectionnez

    <!-- Cashiers drop-down list under NumPad -->
    <t t-extend="PosWidget" >
        <t t-jquery="footer" t-operation="append">
            <div id="AlertNoCashier">You must create at least one cashier!</div>
            <div id="cashier-footer">
                <div id="cashier-title">
                    Select a cashier :            
                </div>
                <div id="cashier-frame">
                    <t t-esc="widget.get_cashiers(widget.get_cur_pos_config_id())" />
                    <select id="cashier-select"></select>   
                </div>           
            </div>
        </t>
    </t>

Pour modifier le template d'origine, on utilisera l'attribut t-extend.

t-extend
Sélectionnez

    <!-- Cashiers drop-down list under NumPad -->
    <t t-extend="PosWidget" >
        -
        -
        -
    </t>

Comme vous voyez ci-dessus, un template s'écrit dans les balises <t></t>.

Pour voir les différents attributs et leurs fonctions, je vous invite à lire cette page sur le site de l'éditeur :
Documentation QWebDocumentation QWeb

Ensuite, un peu comme dans le fichier XML des vues du module Python où nous avions utilisé l'attribut position pour placer des objets avant, après ou à la place des objets du template d'origine, nous allons utiliser ici l'attribut t-operation précédé de l'attribut t-jquery pour spécifier l'objet du template d'origine concerné.

t-jquery
Sélectionnez

        <t t-jquery="footer" t-operation="append">
            <div id="AlertNoCashier">You must create at least one cashier!</div>
            <div id="cashier-footer">
                <div id="cashier-title">
                    Select a cashier :            
                </div>
                <div id="cashier-frame">
                    <t t-esc="widget.get_cashiers(widget.get_cur_pos_config_id())" />
                    <select id="cashier-select"></select>   
                </div>           
            </div>
        </t>

Ici, nous souhaitons ajouter des objets dans la balise <footer> </footer>, à la suite de ceux du template PosWidget d'origine.

Nous retrouvons la balise qui contient le message d'erreur en cas de défaut de caissier, suivi de la liste déroulante des caissiers.

La liste déroulante
La liste déroulante
Le message d'erreur pour défaut de caissier
Le message d'erreur pour défaut de caissier
Liste des caissiers
Sélectionnez

<t t-esc="widget.get_cashiers(widget.get_cur_pos_config_id())" />
<select id="cashier-select"></select>  

La première balise utilise l'attribut t-esc qui permet d'insérer des commandes JavaScript standard.
Lors du chargement de la page du Point De Vente, nous récupérerons l'ID du Point De Vente avec la fonction get_cur_pos_config_id().
Puis, juste en dessous, nous insérons la liste déroulante des caissiers qui appartiennent à ce Point De Vente.

Pour afficher le nom du caissier sur la page de paiement, on va étendre le module PaymentScreenWidget d'origine.

PaymentScreenWidget
Sélectionnez

    <!-- Name of the cashier on Payement Page -->
    <t t-extend="PaymentScreenWidget" >
        <t t-jquery=".pos-step-container" t-operation="prepend">
            <div id="pay-screen-cashier">Cashier : 
                <span id="pay-screen-cashier-name">
                </span>
            </div>
        </t>
    </t>
Le caissier sur la page de paiement
Le caissier sur la page de paiement

Finalement, le nom du caissier doit apparaître aussi sur le ticket de caisse, on va donc également étendre le module PosTicket d'origine.

PosTicket
Sélectionnez

    <!-- Name of the cashier on Ticket -->
    <t t-extend="PosTicket" >
        <t t-jquery="#header-ticket" t-operation="append">
            Cashier : <span id="ticket-screen-cashier-name"></span>
        </t>
    </t>
Le caissier sur le ticket de caisse
Le caissier sur le ticket de caisse

C'est terminé pour le fichier XML.

VIII-C. Le fichier pos_cashier.css

Un simple fichier *.CSS à placer dans le répertoire css du module :

/opt/modules-openerp/pos_cashiers/static/src/css
pos_cashier.css
Sélectionnez

#cashier-title{
    vertical-align: middle;
    display:inline-block;
    text-align: left;
    font-size: 16px;
    font-weight: normal;
    font-style: italic;
    width: 45%;
}

#cashier-frame {
    text-align: center;
    vertical-align: middle;
    display:inline-block;
    border: 1px solid #000000;
    width: 55%;
    padding: 5px 0px 5px 0px;
}

#cashier-select{
    width:95%;
}

#cashier-footer{
    background: linear-gradient(#7B7979, #393939) repeat scroll 0 0 transparent;
    display:block;  
    color: #ffcc00;
    padding: 10px 5px 10px 5px;
}

#pay-screen-cashier{
    color: black;
    border-bottom: 1px dashed #666666;
    padding: 2px 2px 2px 2px;
    text-align: left;
    font-size: 14px;
    font-weight: normal;
    font-style: italic;
}

#ticket-screen-cashier{
    font-style: italic;
    border-bottom : 1px solid gray;
    padding-bottom: 2px;
}

#AlertNoCashier{
    background: red url("../img/error.png") no-repeat 4px;
    color: white;
    font-size: 14px;
    font-weight: bold;
    padding: 12px 4px 4px 30px;
    height: 24px;
    text-transform: uppercase;
}

Ici vous pouvez mettre vos styles additionnels pour votre module, et vous pouvez également modifier ceux d'origine, si nécessaire.

IX. Installation du module

Cette fois-ci, nous pouvons installer notre module.
Connectez-vous à OpenERP en tant qu'administrateur, puis cliquez sur le menu Configuration.
Cliquez sur le lien « Mettre à jour la liste des modules »
Puis cliquez sur « Modules installés » et supprimez le filtre « Installed » dans la barre de recherche.
Notre module va apparaître.

Cliquez sur le bouton Installer et patientez jusqu'à la fin de l'installation.

Le module POS Cashiers dans les modules installés
Le module POS Cashiers dans les modules installés

X. Internationalisation

Vous avez remarqué que les textes, labels et titres étaient tous en anglais ?
Il y a deux raisons à cela.

  • La première est que ce module est totalement fonctionnel, il pourra donc servir à d'autres personnes. Il ne leur restera plus qu'à effectuer la traduction dans leur langage. J'aurai pu le faire, mais je parle très mal le hongrois ou le coréen…
  • La deuxième raison est que je voulais vous expliquer comment faire la traduction d'un module. C'est la meilleure raison, finalement.

Le système d'internationalisation est un peu complexe.
Sachez qu'il nous faut créer un fichier pos_cashier.pot, qu'on va mettre dans le répertoire i18n.

/opt/modules-openerp/pos_cashier/i18n

À partir ce fichier pos_cashier.pot, on créera des fichiers pour les différentes langues.
Un fichier *.pot est un modèle de fichier de traduction. Il ne contient que les termes d'origine, il ne contient pas les termes traduits.

Mais créer un fichier *.pot de A à Z est un peu compliqué.
Heureusement, les développeurs d'OpenERP ont pensé à tout.

Pour créer un fichier *.pot

  • Connectez-vous à OpenERP en tant qu'administrateur.
  • Cliquez sur Configuration dans le menu du haut.
  • Dans la rubrique Traduction, cliquez sur Export de la traduction.
  • La fenêtre ci-dessous apparaît.
Exporter le fichier de traduction
Exporter le fichier de traduction
  • Dans le champ Langue, sélectionnez New Language.
  • Dans Format de fichier, sélectionnez Fichier PO.
  • Dans Modules à exporter, sélectionnez POS Cashiers.
  • Cliquez sur Exporter.
  • Une deuxième fenêtre apparaît.
Le fichier de traduction à télécharger
Le fichier de traduction à télécharger
  • Téléchargez le fichier en cliquant sur le lien de téléchargement.
  • Renommez le fichier sous le nom pos_cashier.pot.
  • Et ouvrez-le dans votre éditeur de code.
pos_cashier.pot
Sélectionnez

# Translation of OpenERP Server.
# This file contains the translation of the following modules:
#    * pos_cashier
#
msgid ""
msgstr ""
"Project-Id-Version: OpenERP Server 7.0-20130703-231023\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-07-13 00:04+0000\n"
"PO-Revision-Date: 2013-07-13 00:04+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"

#. module: pos_cashier
#: view:pos.cashier:0
msgid "All"
msgstr ""

#. module: pos_cashier
#: model:ir.model,name:pos_cashier.model_pos_order
msgid "Point of Sale"
msgstr ""

#. module: pos_cashier
#: model:ir.actions.act_window,help:pos_cashier.action_pos_cashier
msgid "<p class=\"oe_view_nocontent_create\">\n"
"          Click here to create a cashier for the Point Of Sale.\n"
"        </p>\n"
"      "
msgstr ""

#. module: pos_cashier
#: view:pos.cashier:0
msgid "Point of Sale Cashier"
msgstr ""

#. module: pos_cashier
#: field:pos.cashier,cashier_name:0
#: field:pos.order,cashier_name:0
msgid "Cashier"
msgstr ""

#. module: pos_cashier
#: view:pos.cashier:0
msgid "Inactive"
msgstr ""

#. module: pos_cashier
#: help:pos.cashier,active:0
msgid "If a cashier is not active, it will not be displayed in POS"
msgstr ""

#. module: pos_cashier
#: sql_constraint:pos.cashier:0
msgid "A cashier already exists with this name in this Point Of sale. Cashier's name must be unique!"
msgstr ""

#. module: pos_cashier
#: view:pos.cashier:0
#: field:pos.cashier,active:0
msgid "Active"
msgstr ""

#. module: pos_cashier
#: model:ir.model,name:pos_cashier.model_pos_cashier
msgid "pos.cashier"
msgstr ""

#. module: pos_cashier
#: model:ir.actions.act_window,name:pos_cashier.action_pos_cashier
#: model:ir.ui.menu,name:pos_cashier.menu_action_pos_cashier
#: model:ir.ui.menu,name:pos_cashier.menu_point_of_sale_cashiers
#: view:pos.cashier:0
msgid "Cashiers"
msgstr ""

#. module: pos_cashier
#: field:pos.cashier,pos_config_id:0
msgid "Point Of Sale"
msgstr ""

Voici à quoi ressemble un fichier *.pot.

Avant de le recopier, nous allons rajouter quelques instructions.
Vous avez peut-être remarqué que les termes qui sont dans ce fichier sont les noms des champs, les contraintes ou les commentaires que nous avons créés dans les tables.

Nous voulons aussi traduire des mots que l'on a mis « en dur » dans les fichiers XML de certaines vues.
Notamment, nous voulons traduire le message d'erreur qui apparaît dans le Point De Vente lorsqu'il n'y a pas de caissier, nous voulons également traduire le mot « caissier », etc.

Nous allons rajouter des portions de code comme ci-dessous.

pos_cashier.pot
Sélectionnez

#. module: pos_cashier
#. openerp-web
#: code:static/src/xml/pos_cashier.xml:9
#, python-format
msgid "You must create at least one cashier!"
msgstr ""

Encore une fois, étant donné qu'il n'y a pas de documentation là-dessus, j'ai fouiné dans les fichiers de traduction des autres modules.
On peut traduire un mot ou une phrase dans un fichier XML en précisant la source du fichier (depuis la racine du module) suivi du numéro de la ligne.
Dans l'exemple ci-dessus, la phrase à traduire se trouve à la ligne 9 du fichier static/src/xml/pos_cashier.xml.

Puis nous rajoutons les deux traductions suivantes.

pos_cashier.pot
Sélectionnez

#. module: pos_cashier
#. openerp-web
#: code:static/src/xml/pos_cashier.xml:12
#, python-format
msgid "Select a cashier :"
msgstr ""

#. module: pos_cashier
#. openerp-web
#: code:static/src/xml/pos_cashier.xml:26
#: code:static/src/xml/pos_cashier.xml:36
#, python-format
msgid "Cashier :"
msgstr ""

Veuillez noter que la chaîne de caractères à traduire se trouve en face du mot-clé msgid . C'est l'identifiant de la chaîne.
À la ligne suivante, nous avons le mot-clé msgstr suivi d'une chaîne de caractères vide.
Sauvegardez le fichier, puis recopiez le fichier en le renommant, cette fois-ci fr.po.
Ce sera notre fichier de traduction pour la langue française.
Évidemment, vous l'aurez compris, il suffira alors d'ajouter les traductions des chaînes de caractères dans les msgstr correspondants.

Voici un extrait du fichier fr.po :

fr.po
Sélectionnez

#. module: pos_cashier
#: help:pos.cashier,active:0
msgid "If a cashier is not active, it will not be displayed in POS"
msgstr "Un caissier désactivé ne sera pas visible dans le Point De Vente"

#. module: pos_cashier
#: sql_constraint:pos.cashier:0
msgid ""
"A cashier already exists with this name in this Point Of sale. Cashier's "
"name must be unique!"
msgstr ""
"Un caissier existe déjà avec le même nom dans ce Point De Vente. Le nom du "
"caissier doit être unique!"

Si vous souhaitez traduire dans plusieurs langues, il vous suffit de recopier et renommer le fichier pos_cashier.pot en un fichier xx.po.
Pour connaître les différentes langues prises en compte, regardez simplement dans le répertoire i18n des autres modules.

Tant que vous y êtes, dupliquez donc le fichier fr.po pour la Belgique et la Suisse.

Lorsque vous aurez terminé les fichiers de traduction, il vous faudra redémarrer OpenERP pour être certain que tout se charge correctement.
Puis vous allez pouvoir aller dans le POS titiller la liste déroulante après avoir créé deux ou trois caissiers !

XI. Conclusion

Dans ce tutoriel nous aurons vu pas mal de choses, finalement :

  • la création d'un module Python ;
  • la création des vues XML (formulaire et tableau) ;
  • la création de filtres de recherche ;
  • la création d'un menu ;
  • les droits d'accès au module ;
  • les règles sur les enregistrements ;
  • rajouter une icône à un module ;
  • modifier le Point De Vente (rajouter une liste déroulante et un bouton) ;
  • les Templates et QWeb ;
  • l'installation du module ;
  • la traduction du module.

N'hésitez pas à consulter les quelques pages de documentation sur le site de l'éditeur, notamment :

Si vous avez des améliorations à apporter ou même des corrections, n'hésitez pas à me faire part de vos observations.
Merci .

XII. Téléchargement du module

XIII. Remerciements

Un grand Merci aux membres de l'équipe de la rédaction de Developpez.com pour leurs conseils et corrections.