OpenERP : à la découverte des addons web : field et widget

par Georges Racinet, mis à jour le 29/10/2012

Un compte-rendu de découverte des éléments de formulaires HTML d'OpenERP web en vue d'un cas concret.

Cela faisait quelque temps que je comptais apprendre un minimum sur les addons web d'OpenERP, j'ai franchi le pas avec un cas concret : remplacer le sélecteur simple sur les catégories dans la page de recherche des produits par un menu dépliable dynamiquement en javascript. On est en v6.1, les vues de recherches seront fonctionnellement assez différentes en v7, mais peu importe ici, on est là pour apprendre.

Le résultat s'appelle web_dynatree, en référence à la librairie jquery.dynatree sur lequel il est basé, et qui est faite précisément pour ce genre de besoins.

Cet article est écrit comme un rapport d'investigation, la documentation sur les addons web étant presque inexistante, mais vous devriez pouvoir aussi le prendre j'espère comme un tutoriel. Ce sera alors plus un tutoriel pour apprendre que pour faire. Il suit complètement la démarche que j'ai suivie pour me débrouiller.

Avec un peu de chance, si vous avez un profil proche du mien, à savoir beaucoup de python, et très peu de pratique de Javascript, cela vous sera utile pour apprendre par vous-même aussi.

Dernière précaution avant de plonger, tout ceci est pour OpenERP 6.1, car c'est le contexte de mon besoin. On peut espérer que ce soit assez proche dans le futur OpenERP 7.

Premiers pas

D'abord la documentation générale des addons web explique la structure générale d'un addon web, et comment on enregistre sources Javascript, CSS auprès d'OpenERP. C'est un prérequis pour la suite de cet article.

Ensuite, il y a un addon web d'exemple : addons/web_hello, mais il n'y est pas du tout question d'éléments de formulaires.

Pour finir, comme toujours dans ce genre de cas (et en particulier avec le logiciel libre), les addons web standards sont une mine d'exemples, et en particulier addons/web lui-même.

Widget ou Field ?

Voici une déclaration typique de vue de recherche:

<field name="categ_id" widget="selection" operator="child_of"
       groups="base.group_extended"/>

(extrait de product/product_view.xml).

Dans une démarche classique de séparation contenu / présentation, on comprend volontiers qu'il faut présenter le champ "categ_id", et que l'élément d'interface correspondant s'appelle sans doute un widget.

En fait pas du tout, l'élément d'interface côté front est un Field. Voici quelques extraits de search.js dans addons/web. D'abord, il y a un registre qui fait la correspondance entre l'attribut widget plus haut et la classe de Field:

/** @namespace */
openerp.web.search = {};
/**
 * Registry of search fields, called by :js:class:`openerp.web.SearchView` to
 * find and instantiate its field widgets.
 */
openerp.web.search.fields = new openerp.web.Registry({
  'char': 'openerp.web.search.CharField',
  'text': 'openerp.web.search.CharField',
  'boolean': 'openerp.web.search.BooleanField',
  'integer': 'openerp.web.search.IntegerField',
  'id': 'openerp.web.search.IntegerField',
  'float': 'openerp.web.search.FloatField',
  'selection': 'openerp.web.search.SelectionField',
  'datetime': 'openerp.web.search.DateTimeField',
  'date': 'openerp.web.search.DateField',
  'many2one': 'openerp.web.search.ManyToOneField',
  'many2many': 'openerp.web.search.CharField',
  'one2many': 'openerp.web.search.CharField'
});

et ici le début de la définition de la classe Field:

openerp.web.search.SelectionField = openerp.web.search.Field.extend(/** @lends openerp.web.search.SelectionField# */{
// This implementation is a basic <select> field, but it may have to be..

On voit au passage que les champs de sélection sont des classes à part des champs de vues formulaire. D'ailleurs cela se reflète dans ce qu'on voit de l'API.

Extrait de l'API openerp.search.Field

Voici une liste des méthodes qu'il pourra nous être utile de surcharger.

  • init: sert manifestement surtout à enregistrer des paramètres depuis la définition XML.
  • render: affichage initial
  • start: appelée une fois l'affichage initial effectué. C'est donc l'endroit idéal pour activer des bibliothèques javascript comme dynatree, qui viennent s'accrocher sur un élément supposé bien présent dans la page. Sert aussi à retourner une tâche asynchrone (Deferred) que le framework doit attendre avant d'utiliser le Field pour de bon.
  • get_value: l'entrée utilisateur, ou plutôt ce qu'on en sait à l'instant t
  • get_domain: retourne le domaine correspondant à l'entrée utilisateur. L'implémentation générique appelle get_value et utilise l'attribut operator de la déclaration XML

Mon premier Field

Impatients de mettre en oeuvre ce que nous venons d'apprendre, essayons de faire un Field plus simple que notre but final : le LowerCharField, qui passe tout en bas de casse, ce qui est assez inutile en pratique, mais facile à tester et à brancher.

Pour cela nous allons étendre le CharField (une simple entrée de chaîne de caractères dans le formulaire de recherche). Faissons donc un addon web appelé web_lowerchar en nous basant sur la structure de web_hello. Créons un fichier static/src/js/lowerchar.js.

Tout d'abord, nous devons enregistrer notre classe, comme le fait search.js lui-même, voici le minimum:

openerp.web_lowerchar = function(openerp) {
   openerp.web.search.LowerCharField = openerp.web.search.CharField.extend({
     render : function (default) {
        console.info("LowerCharField render", default);
        this._super(default);
        }
   });
   openerp.web.search.fields.add('lowerchar',
                                 'openerp.web.search.LowerCharField');

}

À ce stade, notre Field ne fait rien d'autre qu'une entrée en console pour prouver qu'il est bien branché.

Plusieurs remarques s'imposent:

  • Dans la première ligne, les deux occurrences de l'identifiant openerp n'ont absolument rien à voir. La première est un registre qui permet aux addons web d'enregistrer leur code d'initialisation, pour appel ultérieur dans le bon ordre. La seconde est l'espace de nommage dans lequel on devra mettre nos objets. Il contient déjà le registre des noms de champs qui nous intéresse. Cette ligne est comme dans les modules web d'OpenERP 6.1. Dans la future v7, la convention a été améliorée, on utilise instance pour le second.
  • Règle de nommage à respecter à la première ligne : pour chaque addon web, le framework ira chercher l'attribut du même nom dans le registre d'initialisation, pour l'exécuter en lui passant le namespace général. Pas question de mettre autre chose ici que le nom de l'addon lui-même, sous peine de se faire ignorer.
  • Notez l'usage de super. Cela rappelle quelque chose au pythoneur que je suis, mais ça ne fait pas partie du langage lui-même.

Finalement, il nous reste à remplacer un champ de la vue de recherche pour tester. Puisque c'est juste pour apprendre à se brancher, autant ne pas se gêner et directement modifier dans addons/product/product_view.xml:

<field name="name" widget="lowerchar" operator="=" />

On relance tout, on recharge tout dans le navigateur, et on voit bien le message dans la console.

Ne reste plus qu'à s'amuser avec get_value:

openerp.web.search.LowerCharField = openerp.web.search.CharField.extend({
  render : function (default) {
  console.info("LowerCharField render", default);
  this._super(default);
  },
  get_value : function () {
     return this._super().toLowerChar();
  }
})

On pourrait aussi changer get_domain mais ce n'est pas l'esprit : get_domain appelle make_domain avec le résultat de get_value, et make_domain s'occupe d'appliquer l'opérateur indiqué dans la configuration XMl. Autant les laisser faire leur travail.

Côté serveur: les contrôleurs

On retourne dans le code python. Voici un exemple simplissime de requête HTTP:

try:
    import openerp.addons.web.common.http as openerpweb
except ImportError:
    import web.common.http as openerpweb

class PocController(openerpweb.Controller):

    _cp_path = '/poc'

    @openerpweb.httprequest
    def hello(self, request):
        return 'Hello, client\n'

Ne nous étendons pas trop sur le bloc try/except, je l'ai simplement recopié depuis un autre addon web, c'est pour la compatibilité entre les deux modes du serveur web (embarqué ou independant).

On peut tester ce code directement avec un outil comme wget, pas de piège, le _cp_path est bien le préfixe à appeler depuis la racine du site, et ensuite on ajoute le nom de la méthode décorée. En local sur mon instance de développement, cela nous donne donc:

~ $  wget -q -O- http://localhost:8069/web/dynatree/hello
Hello, client

Contrôleurs, toujours: appels authentifiés en JSON

Jusqu'ici tout va bien, maintenant, en lisant d'autres contrôleurs, notamment openerp/addons/web/controllers/main.py, on voit comment attaquer l'API des modèles depuis le controleur:

@openerpweb.jsonrequest
def whoami(self, request):
    """Return the name of the current user."""
    session = request.session
    return session.model('res.users').read([session._uid])[0]['name']

Le décorateur jsonrequest permet essentiellement de convertir automatiquement en JSON en entrée et en sortie, pas de piège ici.

Par contre, même dûment connecté à une base OpenERP dans votre navigateur, vous pouvez tenter de pointer sur /poc/whoami, vous aurez une erreur d'authentification, pourtant vous aurez forcément les bons cookies.

Manifestement, en temps normal, le framework web commence par rafraîchir la session avant de faire son appel, ou quelque chose de ce même genre (je n'ai pas creusé plus).

C'est fâcheux, mais de toute façon en moyenne, ce que nous voudrons, c'est faire des appels depuis le code Javascript. Le framework web fournit la méthode à tout faire, présente sur la plupart des objets. Voici un exemple, cette fois extrait de web_dynatree, depuis la fonction appelée quand on déplie un noeud:

onLazyRead: function(node) {
    field.rpc('/web/dynatree/expand', {
        'node_id': node.data.oerp_id,
        'model': field.attrs.relation
    }).then(function(result) {
        node.setLazyNodeStatus(DTNodeStatus_Ok);
        node.addChild(result);
    });
},

Ici, field est l'instance active de notre DynatreeField, /web/dynatree/expand est une méthode décorée par @jsonrpc.

On voit ici aussi comment récupérer les attributs de la déclaration XML (en l'occurrence, relation est automatique et contient le nom de modèle du champ many2one qui nous intéresse).

Dernière remarque sur cet extrait : nous sommes en Javascript, tout est affaire d'appels reportés (deferred), de rappels (callback). La fonction rpc() ne renvoie pas directement la réponse serveur : elle renvoie un objet jQuery.Deferred, dont la méthode then() sera appelée une fois le résultat reçu.

Ceux d'entre vous qui ont déjà fait du Twisted ne seront pas trop dépaysés.

Relais direct sur les méthodes ORM

Au moment de finalier cet article, je viens d'apprendre l'existence d'une API Javascript pour relayer directement sur les méthodes ORM : openerp.web.Model, avec sa méthode call. Allez voir la définition dans data.js et greppez pour des exemples d'utilisation.