Comment fonctionne Odoo - Les méthodes et API de l'ORM


Dans cet article destiné aux développeurs, nous vous expliquons comment est organisé Odoo et comment utiliser son ORM

 1. Architecture d'Odoo

Odoo est un ERP composé de différents modules pour gérer tous les domaines d'une entreprise. Il permet d'entrer en contact avec ses fournisseurs, de recevoir des commandes, de lancer des campagnes marketing, etc. Les entreprises l'utilisent pour optimiser leurs flux de travail, centraliser leurs données et automatiser leurs processus.

Chaque module est destiné à un domaine précis et s'intègre aux autres pour faciliter les échanges de données, comme l'achat qui fait une entrée en stock. Ceci est possible grâce à la force de son ORM. 

En plus d'être un ERP, il est tout aussi un framework architecturé sous un modèle 3-tiers client-serveurs :

  1. le serveur de base de données PostgreSQL
  2. le serveur d’application Odoo basé sur Werkzeug
  3. le client web basé sur JavaScript, utilisant le framework OWL au détriment de JQuery et BackboneJS des versions inférieures à Odoo 14.

2. Structure des modules

Chaque module Odoo dépends d’un ou plusieurs autres modules et présente une structure qui contient tous les éléments nécessaires à son fonctionnement comme les modèles de base de données, les vues, les droits d’accès, etc. Par exemple, si vous allez dans le module de gestion des achats, vous allez constater que ce dernier contient plus ou moins une structure avec les dossiers suivants :

  • data: toutes les données statiques sont conservées à ce niveau (menus, crons jobs, données à exporter, etc.)
  • models: pour définir les tables de la base de donnée
  • reports: tous les rapports, PDF et autres documents à imprimer y sont
  • security: pour la conservation de toutes les règles de sécurité et autres droits d’accès
  • views: sont définies ici de toutes les vues : formulaires, listes, calendrier, graphes, etc.
  • wizard, dossier dans lequel toutes les fenêtres modales sont répertoriées
  • static: pour conserver les images et autres codes JavaScript et CSS
  • i18n pour les traductions
  • tests pour les tests unitaires
  • controllers pour les méthodes HTTP spécifiques
  • migrations pour les scripts de migration de la base de données

Il y a aussi les fichiers README pour la description du projet et __manifest__, pour la définition globale du module et indispensable à l'installation.

3. Les types de données

L’ORM d’Odoo est très riche et contient de nombreux champs dont on peut modifier l’aspect en utilisant des widgets précis. Lorsque vous importez l’objet odoo.fields, vous avez accès aux champs suivant :

  • odoo.fields.Char() pour les textes courts, équivalent du <input type=”text”/> en HTML
  • odoo.fields.Text() pour les textes longs, équivalent du <textarea/> en HTML
  • odoo.fields.Boolean() pour les booléens, équivalent du input type=”checkbox”/>
  • odoo.fields.Selection() pour afficher une liste d’éléments à sélectionner, équivalent du <option/> en HTML
  • odoo.fields.Float() pour les nombres décimaux
  • odoo.fields. Int() pour les nombres entiers
  • odoo.fields.Monetary() pour les nombres, notamment les prix
  • odoo.fields.Binary() pour le stockage des fichiers comme les PDF. À noter qu’il y a aussi Image() réservée uniquement aux images
  • odoo.fields.Html() pour afficher un éditeur WYSIWYG
  • odoo.fields. Date() et Datetime() respectivement pour les dates simples et les dates accompagnées de temps. Ces champs affichent un calendrier dans l’interface via leurs widgets respectifs.
  • odoo.fields.Json() quant à lui est un champ qui permet d'enregistrer les données de type JSON
  • odoo.fields.Many2one(), odoo.fields.One2Many() et odoo.fields.Many2many() sont des champs relationnels (n,1), (1,n) et (n,n)

Il existe aussi 2 types de champs particuliers (ou semi-relationnel) qui permettent d’interagir directement avec les données. Il s'agit de méthodes qu’on peut ajouter aux champs plus haut via les attributs compute et related.

  • L’attribut "compute" permet d’affecter le résultat d’un calcul à un champ
  • L’attribut "related" permet d’affecter la valeur d’un champ d’un autre modèle à un autre champ précis

Il existe aussi des méthodes précises qu’on utilise pour les champs Many2one(), One2Many() et Many2many(). Ces méthodes permettent de faire des actions bien précises lors des calculs, notamment mettre à jour ou modifier des données. Ce sont :

  • odoo.fields.Command.create(values) ou (0, 0, { values }) : faire un lien vers un nouvel enregistrement qui doit être créé avec le dictionnaire de valeurs donné
  • odoo.fields.Command.update(id, values) ou (1, id, { values }) : mettre à jour l'enregistrement ayant l'identifiant id
  • odoo.fields.Command.delete(id) ou (2, id) : supprimer l'enregistrement ayant l'identifiant id (appels unlink sur id, cela va supprimer complètement l'objet, ainsi que le lien vers celui-ci)
  • odoo.fields.Command.unlink(id) ou (3, id) coupe le lien vers l'enregistrement lié à l'identifiant id (supprime la relation entre les deux objets, mais ne supprime pas l'objet cible lui-même)
  • odoo.fields.Command. link(id) ou (4, ID) ajoute un lien vers un enregistrement existant ayant l'identifiant id
  • odoo.fields.Command.clear() ou (5) dissocier tout (comme utiliser (3, id) pour tous les enregistrements liés)
  • odoo.fields.Command.set(ids) ou (6, 0, [ids]) remplace la liste des id liées (utilise (5) puis (4,id) pour chaque id dans la liste des id)

4. Les méthodes de l'ORM

L'ORM d'Odoo présente une cartographie relationnelle des objets. Elle possède :

  • une structure hiérarchique
  • une certaine cohérence dans la validation des contraintes
  • un traitement optimisé de chaque requête : permet par exemple en un seul appel de faire plusieurs actions à la foi
  • la capacité d'assigner des valeurs de champ par défaut
  • des types de champs variés comme vue plus haut (Char(), Boolean(), Selection(), etc.)

Créer de nouveaux enregistrements

odoo.models.Model.create(), accepte un dictionnaire ou une liste de dictionnaire

self.env['model.name'].create({'field_1': value_1,	'field_2': value_2,})

self.env['model.name'].create([{'field_1': value_1,	'field_2': value_2,}, {'field_1': value_1,	'field_2': value_2,}])

Modifier un enregistrement

odoo.models.Model.write() , accepte aussi un dictionnaire ou une liste de dictionnaire

self.env['model.name'].write({'field_1': value_1,	'field_2': value_2,})

self.env['model.name'].write([{'field_1': value_1,	'field_2': value_2,}, {'field_1': value_1,	'field_2': value_2,}])

Dupliquer un enregistrement tout en permettant de modifier certaines valeurs lors de la copie

odoo.models.Model.copy()

new_record = original_record.copy(default=None)

Rechercher des enregistrements à partir de leur id

odoo.models.Model.browse([ids]), prend en compte un id ou une liste d'id

partner = self.env['res.partner'].browse(partner_id)

Rechercher des enregistrements précis

odoo.models.Model. search(domain[, offset=0][, limit=None][, order=None])

partners = self.env['res.partner'].search([('name', '=', 'Peef')])

Compter des enregistrements précis

odoo.models.Model. search_count(domain[, limit=None]) : exemple, tous les produits ayant pour référence 123

count = self.env['product.template'].search_count([('default_code', '=', 123)])

Trouver des enregistrements et retourner le résultat dans une liste contenant les champs précis de cet enregistrement

odoo.models.Model. read([fields])

partner_data = partner.read(['name', 'email'])

Supprimer un enregistrement

odoo.models.Model.unlink()

partner.unlink()

Forcer l'enregistrement d'une donnée

odoo.models.Model.flush()

self.env.cr.flush()

Retourner les valeurs d'un champ précis dans une liste

Dans cet exemple, on retourne les noms de toutes les factures d'une vente précise

invoices = self.env['sale.order'].mapped('invoice_ids.name)

Appliquer un filtre et retourner des enregistrements précis

Dans cet exemple, on retourne tous les produits dont la référence n'est pas définie

self.line_ids = self.line_ids.filtered(key=lambda x: x.product_id.default_code is False)

Trier des enregistrements sur la base de champs spécifiques

Dans cet exemple, on va trier les lignes sur la base des noms de chaque produit

self.line_ids = self.line_ids.sorted(key=lambda x: x.product_id.name)

5. L'héritage ou extension de modèles

Odoo propose trois mécanismes pour étendre les modèles de manière modulaire :

Créer un nouveau modèle à partir d'un modèle existant

class PartnerPremium(models.Model):
    _name = 'res.partner.premium'
    _inherit = 'res.partner'

Ajouter de nouvelles informations à la copie tout en laissant le module d'origine tel quel

class ResPartner(models.Model):
    _inherit = 'res.partner'
    
    birth_date = fields.Date()

Appliquer les modèles définis dans d'autres modules

class Website(models.Model):
    _name = 'website'
    _inherit = ['website', 'mail.thread', 'mail.activity.mixin']

Il est également important de noter ceci :

  1. odoo.models.Model() permet d'instancier la classe sur la base de données lorsque le module de la classe est installé
  2. odoo.models.TransientModel() permet de définir les fenêtres modales ou wizard
  3. odoo.models.AbstractModel() permet la création d'une classe abstraite destinée à être héritée par les modèles classiques (Model et TransientModel). Il est très utilisé dans l'édition des rapports (report)

À travers ces modèles, vous pouvez utiliser les décorateurs suivant pour implémenter vos solutions :

odoo.api.onchange() permet de changer la valeur d’un champ de manière dynamique lorsque la valeur d’un autre champ sur la vue est modifiée. Ce décorateur prend en paramètre le ou les champs qui vont déclencher le changement de la valeur de l’autre champ.

Dans cet exemple, le contenu du message change en fonction du partenaire choisit

@api.onchange('partner_id')
def _onchange_partner(self):
    self.message = "Dear %s" % (self.partner_id.name or "")

odoo.api.depends() permet de faire un calcul au niveau des champs calculé par exemple. Il prend en paramètre les champs qui seront utilisés pour le calcul.

Dans cet exemple, si le partenaire est une entreprise, son prix est ajouté de 0.5 au prix du produit, sinon on conserve le prix du produit.

price = fields.Float(compute='_compute_price')

@api.depends('partner_id.is_company', 'product_id')
def _compute_price(self):
    for record in self:
        if record.partner_id.is_company:
            record.price = product_id.lst_price + 0.5
        else:
            record.price = product_id.lst_price

odoo.api.constrains() pour ajouter une contrainte de validation d’un ou plusieurs champs à partir des champs qui seront en paramètre.

Dans cet exemple, on retourne une erreur de validation si la limite de crédit du client est supérieurs à un million.

@api.constrains('credit_limit')
def _check_credit_limit(self):
    for record in self:
        if record.credit_limit > 1000000:
            raise ValidationError("Customer out of credit")

Il existe plusieurs autres décorateurs et informations détaillées que vous pouvez obtenir dans la documentation officielle d’Odoo. Nous vous recommandons fortement de le consulter lorsque vous travailler.