Skip to content

Latest commit

 

History

History
1649 lines (1247 loc) · 55.1 KB

tutoriel.adoc

File metadata and controls

1649 lines (1247 loc) · 55.1 KB

Tutoriel de développement Odoo

Démarrage et arrêt du serveur Odoo

Vous devez avoir installé Odoo depuis les sources comme décrit ici.

Pour rappel, le serveur Odoo se démarre avec la commande suivante dans le dossier d’installation d’Odoo :

workspace/odoo$ python3 odoo-bin -d tutoriel -c odoo.conf

tutoriel est le nom de la base de données que vous allez utiliser pour ce tutoriel.

Le serveur s’arrête avec Ctrl-C.

Note
A chaque modification du code source, vous devez redémarrer Odoo pour qu’il prenne effet.

Mode debug

Le mode debug permet au développeur d’avoir accès à des informations et menus supplémentaires dans l’interface par rapport à l’utilisateur standard.

Pour passer en mode debug, ajouter ?debug=1 à l’URL odoo, juste avant le #:

http://localhost:8069/web?debug=1#...

Création d’un module Odoo

Aussi bien les extensions du serveur Odoo que celle du client sont packagées dans des modules qui pourront être chargés dans la base de données.

Les modules Odoo peuvent ajouter des fonctionnalités business complètement nouvelles ou modifier/étendre des logiques déjà implémentées par d’autres modules. Par exemple, un module pourrait être créé pour ajouter les règles de comptabilité propres à un pays, alors qu’un autre pourrait créer une visualisation en temps réel d’une flotte d’autobus.

Ainsi, dans Odoo, tout les développements sont des modules.

Dans ce tutoriel, nous allons créer un module, appelé openacademy permettant de gérer des cours.

Composition d’un module

Un module Odoo peut contenir plusieurs éléments:

Objets métier

Déclarées comme classes Python, ces ressources sont automatiquement conservées par Odoo en fonction de leur configuration

Vues d’objets

Définition de l’interface utilisateur des objets métier

Fichiers de données

Fichiers XML ou CSV déclarant les métadonnées du modèle:

  • Vues ou rapports ,

  • Données de configuration (paramétrage des modules, règles de sécurité ),

  • Données de démonstration

  • …​

Contrôleurs Web

Gére les demandes des navigateurs Web

Données Web statiques

Images, fichiers CSS ou javascript utilisés par l’interface Web ou le site Web

Structure d’un module

Chaque module est un dossier dans un dossier de modules. Les dossiers de modules sont spécifiés à l’aide de l’option addons_path dans le fichier de configuration.

La structure typique d’un module est la suivante

openacademy/
    __manifest__.py
    __init__.py
    models/
        __init__.py
    views/
    data/
    demo/
    security/
    i18n/
    controllers/
    static/
    test/
    report/
    wizard/

Le fichier de manifeste

Le fichier __manifest__.py est le manifeste Odoo du module. Il contient les informations permettant à Odoo de charger ce module:

__manifest__.py
{
    'name': "Open Academy",

    'summary': """Manage trainings""",

    'description': """
        Open Academy module for managing trainings:
            - training courses
            - training sessions
            - attendees registration
    """,

    'author': "My Company",
    'website': "http://www.yourcompany.com",
    'category': 'Test',
    'version': '0.1',

    # any module necessary for this one to work correctly
    'depends': ['base'],

    # always loaded
    'data': [],
    # only loaded in demonstration mode
    'demo': [],
}

La plupart des clés du fichier décrivent ce que fait le module.

3 clés méritent notre attention:

depends

La liste des modules Odoo dont ce module dépend. Ici notre module openacademy ne dépend que du module base.

data

Tous les fichiers qui ne sont pas des fichiers Python doivent être déclarés ici pour qu’il soient pris en compte.

demo

Les fichiers de données de démonstration qui ne seront chargés que lorsqu’Odoo est en mode démonstration doivent être déclarés ici.

Les fichiers __init__.py

Les fichiers __init__.py sont des fichiers natifs python qui permettent de déclarer les packages python.

Dans le cadre d’Odoo, ces fichiers doivent déclarer tous les fichiers python du dossier où ils se trouvent (à l’exception notable du manifeste), ainsi que tous les sous-dossiers où il y a d’autres fichiers python.

Dans le fichier __init__.py à la racine du module, nous n’avons pas de fichier python, en revanche, nous avons un sous-dossier models avec lui-même un __init__.py. Nous déclarons donc ce sous-dossier:

__init__.py
from . import models

Dans le dossier models, il n’y a pas de fichier python pour l’instant. Notre __init__.py est pour l’instant vide.

models/__init__.py

Créez votre premier module

Dans workspace/odoo_modules, créez un dossier openacademy. Dans ce dossier:

  • Recopiez les fichiers __manifest__.py, __init__.py ci-dessus

  • Créez un dossier models et mettez-y un fichier __init__.py vide.

Votre premier module ne fait rien, mais il peut déjà être installé.

  • Redémarrez votre serveur Odoo

  • Passez en mode debug.

  • Allez dans le menu "Applications"

  • Cliquez sur "Mettre à jour la liste des applications" et validez la popup

  • Une fois la mise à jour effectuée, supprimez le filtre "Applications" dans la barre de recherche et tapez "openacademy" pour chercher votre module.

  • Votre module doit apparaitre dans la liste, vous pouvez alors l’installer en cliquant sur "Installer"

Note
Une fois que votre module est reconnu, vous n’aurez plus à cliquer sur "Mettre à jour la liste des applications", il sera toujours disponible.

Vérifiez dans la liste que votre module est bien marqué comme étant installé.

Couche ORM

Un composant clé d’Odoo est la couche ORM. Cette couche évite d’avoir à écrire la plupart du SQL à la main et fournit des services d’extensibilité et de sécurité.

Les objets métier sont déclarés en tant que classes Python étendant la classe Model qui les intègre dans le système de persistance automatisé.

Les modèles peuvent être configurés en définissant un certain nombre d’attributs lors de leur définition. L’attribut le plus important est _name qui est requis et définit le nom du modèle dans le système Odoo. Voici une définition minimale complète d’un modèle:

from odoo import models

class MinimalModel(models.Model):
    _name = 'test.model'

Champs de modèle

Les champs sont utilisés pour définir ce que le modèle peut stocker et où. Les champs sont définis comme des attributs sur la classe de modèle:

from odoo import models, fields

class LessMinimalModel(models.Model):
    _name = 'test.model2'

    name = fields.Char()

Attributs communs

Tout comme le modèle lui-même, ses champs peuvent être configurés, en passant des attributs de configuration comme paramètres:

name = field.Char(required=True)

Certains attributs sont disponibles sur tous les champs, voici les plus courants:

string

(unicode, par défaut: nom du champ)

Le libellé du champ dans l’interface utilisateur (visible par les utilisateurs).

required

(bool, Par défaut: False)

Si True le champ ne peut pas être vide, il doit soit avoir une valeur par défaut, soit toujours recevoir une valeur lors de la création d’un enregistrement.

help

(unicode, Par défaut: "")

Fournit une info-bulle d’aide aux utilisateurs de l’interface utilisateur.

index

(bool, Par défaut: False)

Demande à Odoo de créer un index de base de données sur la colonne.

Champs simples

Il existe deux grandes catégories de champs:

  • les champs «simples» qui sont des valeurs atomiques stockées directement dans la table du modèle

  • les champs «relationnels» reliant les enregistrements (du même modèle ou de modèles différents).

Par exemple, Boolean, Date, Char sont des types de champs simples.

Champs réservés

Odoo crée quelques champs dans tous les modèles. Ces champs sont gérés par le système et ne doivent pas être modifiés manuellement. En revanche, ils peuvent être lus si nécessaires:

id

(Integer) Identificateur unique d’un enregistrement dans son modèle.

create_date

(Datetime) Date de création de l’enregistrement.

create_uid

(Many2one) Utilisateur qui a créé l’enregistrement.

write_date

(Datetime) Dernière date de modification de l’enregistrement.

write_uid

(Many2one) Dernier utilisateur ayant modifié l’enregistrement.

Champs spéciaux

Par défaut, Odoo requiert également un champ name sur tous les modèles pour différents comportements d’affichage et de recherche. Le champ utilisé à ces fins peut être remplacé par la définition _rec_name.

Créez votre premier modèle dans votre module

Définissez un nouvel objet "cours" sur le modèle de données dans le module openacademy. Un cours a un titre et une description. Les cours doivent obligatoirement avoir un titre.

Pour cela, créez un fichier models/models.py pour y mettre votre modèle:

models/models.py
from odoo import models, fields, api

class Course(models.Model):
    _name = 'openacademy.course'
    _description = "OpenAcademy Courses"

    name = fields.Char(string="Title", required=True)
    description = fields.Text()
Important
Prenez le temps de bien comprendre le sens du code ci-dessus. N’hésitez pas à vous le faire réexpliquer.

Modifiez ensuite le fichier models/__init__.py pour charger votre nouveau fichier:

models/__init__.py
from . import models

Fichiers de données

Odoo est un système hautement piloté par les données. Bien que le comportement soit personnalisé à l’aide du code Python, une partie de la valeur d’un module se trouve dans les données qu’il configure lors du chargement.

Note
Certains modules existent uniquement pour ajouter des données dans Odoo

Les données du module sont déclarées via des fichiers de données XML avec des balises <record>. Chaque balise <record> crée ou met à jour un enregistrement de base de données.

<odoo>

    <record model="{model name}" id="{record identifier}">
        <field name="{a field name}">{a value}</field>
    </record>

</odoo>
model

le nom du modèle Odoo pour l’enregistrement.

id

un identifiant externe, il permet de se référer à l’enregistrement (sans avoir à connaître son identifiant en base de données).

<field>

Ces balises ont un name qui est le nom du champ dans le modèle (par exemple description). Leur corps est la valeur du champ.

Les fichiers de données doivent être déclarés dans le fichier manifeste à charger, ils peuvent être déclarés :

  • Soit dans le liste 'data' (toujours chargée)

  • Soit dans la liste 'demo' (uniquement chargée en mode démonstration).

Créez votre premier fichier de données

Créez des données de démonstration en remplissant le modèle de cours avec quelques cours de démonstration.

Pour ce faire, créez un fichier demo/demo.xml:

demo/demo.xml
<?xml version="1.0" encoding="UTF-8"?>
<odoo>

    <record model="openacademy.course" id="course0">
        <field name="name">Course 0</field>
        <field name="description">Course 0's description

Can have multiple lines
        </field>
    </record>
    <record model="openacademy.course" id="course1">
        <field name="name">Course 1</field>
    </record>
    <record model="openacademy.course" id="course2">
        <field name="name">Course 2</field>
        <field name="description">Course 2's description</field>
    </record>

</odoo>

Rappelez-vous: il faut maintenant déclarer notre nouveau fichier dans le manifeste. Modifiez la ligne avec la clé demo de la façon suivante:

__manifest__.py
'demo': [
    'demo/demo.xml'
]

Créez également un fichier de sécurité security/ir.model.access.csv:

security/ir.model.access.csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_openacademy_course,access_openacademy_course,model_openacademy_course,base.group_user,1,1,1,1

Et ajouter le dans le fichier manifeste dans les data.

Redémarrez maintenant votre serveur Odoo, puis retournez dans le menu des applications pour mettre à jour votre module.

Note

Pour éviter d’avoir à remettre à jour manuellement votre module, redémarrez dorénavant votre serveur avec la commande suivante:

workspace/odoo$ python3 odoo-bin -d tutoriel -u openacademy -c odoo.conf

L’option -u permet de faire la mise à jour du module donné au démarrage du serveur.

Vérifiez maintenant que votre base de données a été modifiée :

  • Une table openacademy_course a été créée qui contient notamment deux colonnes name et description

  • 3 enregistrements ont été créés ("Course 0", "Course 1" et "Course 2") suite au chargement du fichier demo/demo.xml

Vous pouvez le faire avec l’outil SQL de votre choix. Par exemple avec psql:

$ psql tutoriel
tutoriel=# SELECT * FROM openacademy_course;
Important
Le contenu des fichiers de données n’est chargé que lorsqu’un module est installé ou mis à jour.
Note

Vous pouvez aussi installer le client GUI de base de données pour PostgreSQL pgadmin3 avec la commande

$ sudo apt-get install pgadmin3

Actions et menus

Les actions et les menus sont des enregistrements comme les autres dans la base de données, généralement déclarés via des fichiers de données. Les actions peuvent être déclenchées de trois manières:

  • en cliquant sur les éléments de menu (liés à des actions spécifiques)

  • en cliquant sur les boutons dans les vues (s’ils sont liés à des actions)

  • comme actions contextuelles sur l’objet

Parce que les menus sont quelque peu complexes à déclarer, il existe un raccourci <menuitem> pour déclarer un enregistrement sur le modèle ir.ui.menu et le connecter plus facilement à l’action correspondante.

Par exemple:

<record model="ir.actions.act_window" id="action_list_ideas">
    <field name="name">Ideas</field>
    <field name="res_model">idea.idea</field>
    <field name="view_mode">tree,form</field>
</record>
<menuitem id="menu_ideas" parent="menu_root" name="Ideas" sequence="10"
          action="action_list_ideas"/>
Important

L’action doit être déclarée avant son menu correspondant dans le fichier XML.

Les fichiers de données sont exécutés séquentiellement, les id d’actions doivent être présentes dans la base de données avant que le menu puisse être créé.

Crééz maintenant une action et un menu

Définissez de nouvelles entrées de menu pour accéder aux cours sous l’entrée de menu OpenAcademy. Un utilisateur doit pouvoir:

  • Afficher une liste de tous les cours

  • Créer / modifier des cours

Pour ce faire, créez un fichier views/openacademy.xml avec le contenu suivant:

views/openacademy.xml
<?xml version="1.0" encoding="UTF-8"?>
<odoo>

    <!-- action -->
    <record model="ir.actions.act_window" id="course_list_action">
        <field name="name">Courses</field>
        <field name="res_model">openacademy.course</field>
        <field name="view_mode">tree,form</field>
        <field name="help" type="html">
            <p class="o_view_nocontent_smiling_face">Create the first course
            </p>
        </field>
    </record>

    <!-- top level menu: no parent -->
    <menuitem id="main_openacademy_menu" name="Open Academy"/>
    <!-- A first level in the left side menu is needed
         before using action= attribute -->
    <menuitem id="openacademy_menu" name="Open Academy"
              parent="main_openacademy_menu"/>
    <!-- the following menuitem should appear *after*
         its parent openacademy_menu and *after* its
         action course_list_action -->
    <menuitem id="courses_menu" name="Courses" parent="openacademy_menu"
              action="course_list_action"/>

</odoo>
Important
N’oubliez pas de déclarer ce nouveau fichier dans la liste data du manifeste.

Redémarrez votre serveur.

Vous devez voir apparaitre un menu "Open Academy" vous permettant d’accéder aux cours. Ajoutez, supprimez, modifiez des cours et vérifiez dans la base de données que les modifications ont bien été prises en compte.

Note

Avant d’aller plus loin, assurez-vous d’avoir bien compris:

  • Ce qu’est un modèle, comment sa déclaration impacte à la fois la base de données et l’interface utilisateur

  • Le fait que la base de données contient à la fois des données utilisateur (celles que vous avez créé dans l’interface) et des données de définition, comme les actions et les menus, qui relèvent du développement de l’application.

N’hésitez pas à vous faire réexpliquer si besoin.

Vues de base

Les vues définissent la façon dont les enregistrements d’un modèle sont affichés. Chaque type de vue représente un mode de visualisation (liste des enregistrements, formulaire, graphique,…). Les vues peuvent être demandées de manière générique via leur type (par exemple une liste de partenaires) ou spécifiquement via leur identifiant. Pour les demandes génériques, la vue avec le type correct et la priorité la plus basse sera utilisée (donc la vue de priorité la plus basse de chaque type est la vue par défaut pour ce type).

L’héritage des vues permet de modifier les vues déclarées ailleurs (ajout ou suppression de contenu).

Note
Jusque là, vous n’avez pas spécifié de vue, mais vous avez quand même pu accéder aux cours. C’est parce qu’Odoo vous a généré automatiquement des vues standards.

Déclaration générique d’une vue

Une vue est déclarée comme un enregistrement du modèle ir.ui.view. Le type de vue est déduit de l’élément racine du champ arch:

<record model="ir.ui.view" id="view_id">
    <field name="name">view.name</field>
    <field name="model">object_name</field>
    <field name="priority" eval="16"/>
    <field name="arch" type="xml">
        <!-- view content: <form>, <tree>, <graph>, ... -->
    </field>
</record>

Vues listes

Les vues listes affichent les enregistrements sous forme de tableau.

Leur élément racine est <tree>. La forme la plus simple de liste répertorie simplement tous les champs à afficher dans le tableau (chaque champ sous forme de colonne):

<tree string="Idea list">
    <field name="name"/>
    <field name="inventor_id"/>
</tree>

Vues formulaires

Les formulaires sont utilisés pour créer et modifier des enregistrements.

Leur élément racine est <form>. Ils sont composés d’éléments de structure de haut niveau (groupes, onglets) et d’éléments interactifs (boutons et champs):

<form string="Idea form">
    <group colspan="4">
        <group colspan="2" col="2">
            <separator string="General stuff" colspan="2"/>
            <field name="name"/>
            <field name="inventor_id"/>
        </group>

        <group colspan="2" col="2">
            <separator string="Dates" colspan="2"/>
            <field name="active"/>
            <field name="invent_date" readonly="1"/>
        </group>

        <notebook colspan="4">
            <page string="Description">
                <field name="description" nolabel="1"/>
            </page>
        </notebook>

        <field name="state"/>
    </group>
</form>

Créez une vue formulaire

Créez votre propre vue de formulaire pour l’objet Course. Les données affichées doivent être: le nom et la description du cours.

Insérez un nouveau <record> dans le fichier views/openacademy.xml:

views/openacademy.xml
<?xml version="1.0" encoding="UTF-8"?>
<odoo>

    <record model="ir.ui.view" id="course_form_view">
        <field name="name">course.form</field>
        <field name="model">openacademy.course</field>
        <field name="arch" type="xml">
            <form string="Course Form">
                <sheet>
                    <group>
                        <field name="name"/>
                        <field name="description"/>
                    </group>
                </sheet>
            </form>
        </field>
    </record>

    <!-- action -->
    <!-- ... -->

Redémarrez le serveur et allez sur la vue formulaire dans le menu "Course" pour voir le nouveau formulaire.

Nous allons maintenant placer le champ de description sous un onglet, de sorte qu’il sera plus facile d’ajouter d’autres onglets plus tard, contenant des informations supplémentaires.

Modifiez votre vue formulaire de la façon suivante:

views/openacademy.xml
            <form>
                <sheet>
                    <group>
                        <field name="name"/>
                    </group>
                    <notebook>
                        <page string="Description">
                            <field name="description"/>
                        </page>
                        <page string="About">
                            This is an example of notebooks
                        </page>
                    </notebook>
                </sheet>
            </form>

Redémarrez le serveur pour observer les modifications.

Vues de recherche

Les vues de recherche personnalisent le champ de recherche associé à la vue de liste (et aux autres vues agrégées). Leur élément racine est <search> et ils sont composés de champs définissant quels champs peuvent être recherchés:

<search>
    <field name="name"/>
    <field name="inventor_id"/>
</search>

Si aucune vue de recherche n’existe pour le modèle, Odoo en génère une qui ne permet que la recherche sur le champ name.

Créez une vue de recherche

Créez une vue de recherche permettant de rechercher un cours sur son nom ou sur sa description. Mettez-là à la suite de la vue formulaire:

views/openacademy.xml
        </field>
    </record>

    <record model="ir.ui.view" id="course_search_view">
        <field name="name">course.search</field>
        <field name="model">openacademy.course</field>
        <field name="arch" type="xml">
            <search>
                <field name="name"/>
                <field name="description"/>
            </search>
        </field>
    </record>

    <!-- action -->

Redémarrez le serveur et tapez quelques lettres dans la barre de recherche d’Odoo pour voir la possibilité de chercher par nom ou par description.

Relations entre modèles

Un enregistrement d’un modèle peut être lié à un enregistrement d’un autre modèle. Par exemple, un enregistrement de commande client est lié à un enregistrement client qui contient les données client; il est également lié à ses enregistrements de ligne de commande.

Créez un modèle de session

Pour le module Open Academy, nous considérons un modèle de sessions : une session est une occurrence d’un cours enseigné à un moment donné pour un public donné.

Créez un modèle pour les sessions. Une session a un nom, une date de début, une durée et un nombre de sièges. Ajoutez une action et un élément de menu pour les afficher. Rendez le nouveau modèle visible via un élément de menu.

Créez la classe pour la session dans models/models.py à la fin du fichier:

models/models.py
class Session(models.Model):
    _name = 'openacademy.session'
    _description = "OpenAcademy Sessions"

    name = fields.Char(required=True)
    start_date = fields.Date()
    duration = fields.Float(digits=(6, 2), help="Duration in days")
    seats = fields.Integer(string="Number of seats")
Note
digits=(6, 2) spécifie la précision d’un nombre flottant: 6 est le nombre total de chiffres, tandis que 2 est le nombre de chiffres après la virgule.

Ajoutez l’accès à l’objet session dans views/openacademy.xml, à la fin du fichier.

views/openacademy.xml
    <!-- session form view -->
    <record model="ir.ui.view" id="session_form_view">
        <field name="name">session.form</field>
        <field name="model">openacademy.session</field>
        <field name="arch" type="xml">
            <form string="Session Form">
                <sheet>
                    <group>
                        <field name="name"/>
                        <field name="start_date"/>
                        <field name="duration"/>
                        <field name="seats"/>
                    </group>
                </sheet>
            </form>
        </field>
    </record>

    <record model="ir.actions.act_window" id="session_list_action">
        <field name="name">Sessions</field>
        <field name="res_model">openacademy.session</field>
        <field name="view_mode">tree,form</field>
    </record>

    <menuitem id="session_menu" name="Sessions"
              parent="openacademy_menu"
              action="session_list_action"/>

</odoo>

Enfin, ajouter les droits d’accès en ajoutant la ligne suivante à la fin du fichier security/ir.model.access.csv:

security/ir.model.access.csv
access_openacademy_session,access_openacademy_session,model_openacademy_session,base.group_user,1,1,1,1

Champs relationnels

Les champs relationnels relient les enregistrements, du même modèle (hiérarchies) ou entre différents modèles.

Les types de champs relationnels sont:

Many2one(other_model, ondelete='set null')

Un simple lien vers un autre objet.

One2many(other_model, related_field)

Une relation virtuelle, inverse de a Many2one. Un One2many se comporte comme un conteneur d’enregistrements, y accéder entraîne un ensemble (éventuellement vide) d’enregistrements.

Many2many(other_model)

Relation multiple bidirectionnelle, tout enregistrement d’un côté peut être lié à n’importe quel nombre d’enregistrements de l’autre côté. Se comporte comme un conteneur d’enregistrements, y accéder entraîne également un ensemble d’enregistrements éventuellement vide.

Créez des relations Many2One

À l’aide de many2one, modifiez les modèles de cours et de session pour refléter leur relation avec d’autres modèles:

  • Un cours a un utilisateur responsable ; la valeur de ce champ est un enregistrement du modèle intégré res.users.

  • Une session a un instructeur ; la valeur de ce champ est un enregistrement du modèle intégré res.partner.

  • Une session est liée à un cours ; la valeur de ce champ est un enregistrement du modèle openacademy.course et est obligatoire.

Dans la classe Course, ajouter le champ responsible_id:

models/models.py
    responsible_id = fields.Many2one('res.users',
        ondelete='set null', string="Responsible", index=True)

Dans la classe Session, ajouter les champs instructor_id et course_id:

models/models.py
    instructor_id = fields.Many2one('res.partner', string="Instructor")
    course_id = fields.Many2one('openacademy.course',
        ondelete='cascade', string="Course", required=True)

Adaptez les vues avec les nouveaux champs:

  • Modifiez la vue formulaire de Course:

views/openacademy.xml
        <sheet>
            <group>
                <field name="name"/>
                <field name="responsible_id"/>
            </group>
            <notebook>
                <page string="Description">
  • Créez une vue liste pour Course:

views/openacademy.xml
    <record model="ir.ui.view" id="course_tree_view">
        <field name="name">course.tree</field>
        <field name="model">openacademy.course</field>
        <field name="arch" type="xml">
            <tree string="Course Tree">
                <field name="name"/>
                <field name="responsible_id"/>
            </tree>
        </field>
    </record>

    <!-- action -->
  • Enfin modifiez la vue formulaire de Session, et créez une vue liste:

views/openacademy.xml
           <form string="Session Form">
                <sheet>
                    <group>
                        <group string="General">
                            <field name="course_id"/>
                            <field name="name"/>
                            <field name="instructor_id"/>
                        </group>
                        <group string="Schedule">
                            <field name="start_date"/>
                            <field name="duration"/>
                            <field name="seats"/>
                        </group>
                    </group>
                </sheet>
            </form>
        </field>
    </record>

    <!-- session tree/list view -->
    <record model="ir.ui.view" id="session_tree_view">
        <field name="name">session.tree</field>
        <field name="model">openacademy.session</field>
        <field name="arch" type="xml">
            <tree string="Session Tree">
                <field name="name"/>
                <field name="course_id"/>
            </tree>
        </field>
    </record>

Relancez le serveur. Créez des Sessions, rattachez-les aux Cours existants. Ajouter des responsables et des instructeurs.

Créez une relation One2Many

En utilisant le champ relationnel inverse one2many, modifiez les modèles pour refléter la relation entre les cours et les sessions.

  • Modifiez la classe Course pour y intégrer le champ session_ids:

models/models.py
    session_ids = fields.One2many(
        'openacademy.session', 'course_id', string="Sessions")
  • Ajoutez le champ dans la vue du formulaire de cours:

views/openacademy.xml
                <page string="Description">
                    <field name="description"/>
                </page>
                <page string="Sessions">
                    <field name="session_ids">
                        <tree string="Registered sessions">
                            <field name="name"/>
                            <field name="instructor_id"/>
                        </tree>
                    </field>
                </page>
            </notebook>
        </sheet>

Redémarrez le serveur. Observez la liste des sessions depuis un cours. Créez une nouvelle session et définissez son cours: retournez sur le cours et constatez qu’il a une nouvelle session.

Créez une relation Many2Many

À l’aide du champ relationnel many2many, modifiez le modèle de session pour relier chaque session à un ensemble de participants. Les participants seront représentés par les enregistrements des partenaires, nous allons donc nous rapporter au modèle intégré res.partner.

  • Modifiez la classe Session pour y ajouter le champ attendee_ids:

models/models.py
    attendee_ids = fields.Many2many('res.partner', string="Attendees")
  • Adaptez la vue formulaire de la session en conséquence:

views/openacademy.xml
                            <field name="seats"/>
                        </group>
                    </group>
                    <label for="attendee_ids"/>
                    <field name="attendee_ids"/>
                </sheet>
            </form>
        </field>

Redémarrez le serveur. Ajoutez des participants aux sessions.

Note
Prenez le temps de bien comprendre ces trois types de relations entre modèles. Inspectez la base de données pour voir comment chacune de ces relations est implémentée.

Héritage

Héritage des modèles

Odoo fournit deux mécanismes d' héritage pour étendre un modèle existant de manière modulaire.

Le premier mécanisme d’héritage permet à un module de modifier le comportement d’un modèle défini dans un autre module:

  • ajouter des champs à un modèle,

  • remplacer la définition des champs sur un modèle,

  • ajouter des contraintes à un modèle,

  • ajouter des méthodes à un modèle,

  • remplacer les méthodes existantes sur un modèle.

Le deuxième mécanisme d’héritage (délégation) permet de lier chaque enregistrement d’un modèle à un enregistrement dans un modèle parent et fournit un accès transparent aux champs de l’enregistrement parent.

inheritance methods

Héritage des vues

Au lieu de modifier les vues existantes en place (en les écrasant), Odoo fournit l’héritage des vues où les vues "d’extension" sont appliquées au-dessus des vues racine, et peuvent ajouter ou supprimer du contenu.

Une vue d’extension fait référence à son parent à l’aide du champ inherit_id, et au lieu d’une seule vue, son champ arch est composé d’un certain nombre d’éléments xpath sélectionnant et modifiant le contenu de leur vue parent:

<!-- improved idea categories list -->
<record id="idea_category_list2" model="ir.ui.view">
    <field name="name">id.category.list2</field>
    <field name="model">idea.category</field>
    <field name="inherit_id" ref="id_category_list"/>
    <field name="arch" type="xml">
        <!-- find field description and add the field
             idea_ids after it -->
        <xpath expr="//field[@name='description']" position="after">
          <field name="idea_ids" string="Number of ideas"/>
        </xpath>
    </field>
</record>

Les éléments xpath possèdent les attributs suivants:

expr

Une expression XPath qui permet la sélection d’un seul élément dans la vue parent. Génère une erreur si elle ne correspond à aucun élément ou à plusieurs éléments.

position

Opération à appliquer à l’élément sélectionné:

inside

Ajoute le contenu de l’élément xpath à la fin de l’élément sélectionné

replace

Remplace l’élément sélectionné par le contenu de l’élément xpath

before

Insère le contenu de l’élément xpath avant l’élément sélectionné

after

Insère le contenu de l’élément xpath après l’élément sélectionné

attributes

Modifie les attributs de l’élément sélectionné en suivant les directives des balises attribute

Note

Lorsque l’on cherche un seul élément, l’attribut position peut être défini directement sur l’élément à trouver. Les deux héritages ci-dessous donneront le même résultat:

<xpath expr="//field[@name='description']" position="after">
    <field name="idea_ids" />
</xpath>

<field name="description" position="after">
    <field name="idea_ids" />
</field>

Modifiez des modèles et vues existantes

  • En utilisant l’héritage du modèle, modifiez le modèle Partner existant pour ajouter un champ instructor booléen et un champ many2many qui correspond à la relation session-partenaire

  • En utilisant l’héritage des vues, affichez ces champs dans la vue du formulaire partenaire

Note
Avec le mode debug, vous pouvez inspecter la vue pour trouver son ID externe et l’endroit où mettre le nouveau champ.
  1. Créez un fichier openacademy/models/partner.py et importez-le dans __init__.py de models

models/partner.py
from odoo import fields, models

class Partner(models.Model):
    _inherit = 'res.partner'

    # Add a new column to the res.partner model, by default partners are not
    # instructors
    instructor = fields.Boolean("Instructor", default=False)

    session_ids = fields.Many2many('openacademy.session',
        string="Attended Sessions", readonly=True)
  1. Créez un fichier openacademy/views/partner.xml et ajoutez-le à __manifest__.py dans les data:

views/partner.xml
<?xml version="1.0" encoding="UTF-8"?>
 <odoo>

        <!-- Add instructor field to existing view -->
        <record model="ir.ui.view" id="partner_instructor_form_view">
            <field name="name">partner.instructor</field>
            <field name="model">res.partner</field>
            <field name="inherit_id" ref="base.view_partner_form"/>
            <field name="arch" type="xml">
                <notebook position="inside">
                    <page string="Sessions">
                        <group>
                            <field name="instructor"/>
                            <field name="session_ids"/>
                        </group>
                    </page>
                </notebook>
            </field>
        </record>

        <record model="ir.actions.act_window" id="contact_list_action">
            <field name="name">Contacts</field>
            <field name="res_model">res.partner</field>
            <field name="view_mode">tree,form</field>
        </record>
        <menuitem id="configuration_menu" name="Configuration"
                  parent="main_openacademy_menu"/>
        <menuitem id="contact_menu" name="Contacts"
                  parent="configuration_menu"
                  action="contact_list_action"/>

</odoo>

Redémarrez votre serveur. Vous devez maintenant avoir un menu avec les contacts.

Lorsque vous ouvrez le formulaire d’un contact, vous devez avoir un onglet "Session" correspondant au code que vous avez écrit ci-dessus.

Domaines

Dans Odoo, les domaines de recherche sont des valeurs qui codent des conditions sur des enregistrements. Un domaine est une liste de critères utilisés pour sélectionner un sous-ensemble des enregistrements d’un modèle. Chaque critère est un triple avec un nom de champ, un opérateur et une valeur.

Par exemple, lorsqu’il est utilisé sur le modèle des articles, le domaine suivant sélectionne tous les services avec un prix unitaire supérieur à 1000 :

[('product_type', '=', 'service'), ('unit_price', '>', 1000)]

Par défaut, les critères sont combinés avec un ET implicite. Les opérateurs logiques & (AND), | (OR) et ! (NOT) peuvent être utilisés pour combiner explicitement des critères. Ils sont utilisés en position de préfixe (l’opérateur est inséré avant ses arguments plutôt qu’entre). Par exemple, pour sélectionner des produits "qui sont des services OU ont un prix unitaire qui n’est PAS compris entre 1000 et 2000":

[ '|' ,
    ( 'product_type' ,  '=' ,  'service' ),
    '!' ,  '&' ,
        ( 'prix_unitaire' ,  '>=' ,  1000 ),
        ( 'prix_unitaire' ,  '<' ,  2000 )]

Un paramètre domain peut être ajouté aux champs relationnels pour limiter les enregistrements valides pour la relation lorsque vous essayez de sélectionner des enregistrements dans l’interface client.

Ajouter des domaines aux champs relationnels

Domaine simple

Lors de la sélection de l’instructeur pour une session , seuls les instructeurs (partenaires avec le champ instructor à vrai) doivent être visibles. Modifiez en conséquence le champ instructor_id dans la session pour y ajouter le domain:

models/models.py
    instructor_id = fields.Many2one('res.partner', string="Instructor",
        domain=[('instructor', '=', True)])
Note
Un domaine déclaré en tant que liste littérale est évalué côté serveur et ne peut pas faire référence à des valeurs dynamiques sur le côté droit. A l’inverse, un domaine déclaré en tant que chaîne de caractères est évalué côté client et autorise les noms de champ sur le côté droit.

Redémarrez le serveur et constatez que vous ne pouvez sélectionner que des partenaires instructeurs.

Domaine complexe

Créez de nouvelles catégories de partenaires Enseignant / Niveau 1 et Enseignant / Niveau 2 . L’instructeur d’une session peut être un instructeur ou un enseignant (de n’importe quel niveau).

  • Modifier le domaine du modèle de session:

models/models.py
    instructor_id = fields.Many2one('res.partner', string="Instructor",
        domain=['|', ('instructor', '=', True),
                     ('category_id.name', 'ilike', "Teacher")])

Modifiez openacademy/views/partner.xml pour accéder aux catégories de partenaires :

views/partner.xml
                  parent="configuration_menu"
                  action="contact_list_action"/>

        <record model="ir.actions.act_window" id="contact_cat_list_action">
            <field name="name">Contact Tags</field>
            <field name="res_model">res.partner.category</field>
            <field name="view_mode">tree,form</field>
        </record>
        <menuitem id="contact_cat_menu" name="Contact Tags"
                  parent="configuration_menu"
                  action="contact_cat_list_action"/>

        <record model="res.partner.category" id="teacher1">
            <field name="name">Teacher / Level 1</field>
        </record>
        <record model="res.partner.category" id="teacher2">
            <field name="name">Teacher / Level 2</field>
        </record>

</odoo>

Redémarrez votre serveur. Vous devez maintenant pouvoir sélectionner comme instructeur des partenaires qui ne sont pas instructeurs, mais qui ont au moins une étiquette "Teacher".

Champs calculés et valeurs par défaut

Jusqu’à présent, les champs ont été stockés directement et récupérés directement dans la base de données. Les champs peuvent également être calculés. Dans ce cas, la valeur du champ n’est pas récupérée de la base de données mais calculée à la volée en appelant une méthode du modèle.

Pour créer un champ calculé, créez un champ et définissez son attribut compute sur le nom d’une méthode. La méthode de calcul doit simplement définir la valeur du champ à calculer sur chaque enregistrement dans self.

Important

self est une collection

L’objet self est un jeu d' enregistrements, c’est-à-dire une collection ordonnée d’enregistrements. Il prend en charge les opérations Python standard sur les collections, comme len(self)et iter(self), ainsi que les opérations de set supplémentaires comme recs1 + recs2.

Itérer sur self donne les enregistrements un par un, où chaque enregistrement est lui-même une collection de taille 1. Vous pouvez accéder / affecter des champs sur des enregistrements uniques en utilisant la notation par points, comme record.name.

import random
from odoo import models, fields, api

class ComputedModel(models.Model):
    _name = 'test.computed'

    name = fields.Char(compute='_compute_name')

    def _compute_name(self):
        for record in self:
            record.name = str(random.randint(1, 1e6))

Dépendances

La valeur d’un champ calculé dépend généralement des valeurs des autres champs de l’enregistrement calculé. L’ORM attend du développeur qu’il spécifie ces dépendances sur la méthode de calcul avec le décorateur api.depends(). Les dépendances données sont utilisées par l’ORM pour déclencher le recalcul du champ chaque fois que certaines de ses dépendances ont été modifiées:

from odoo import models, fields, api

class ComputedModel(models.Model):
    _name = 'test.computed'

    name = fields.Char(compute='_compute_name')
    value = fields.Integer()

    @api.depends('value')
    def _compute_name(self):
        for record in self:
            record.name = "Record with value %s" % record.value

Créez un champ calculé

  • Ajouter le pourcentage de sièges occupés au modèle de session

  • Afficher ce champ dans l’arborescence et les vues de formulaire

  • Afficher le champ sous forme de barre de progression

Modifiez votre modèle de session pour y ajouter le champ calculé et sa fonction de calcul:

models/models.py
    taken_seats = fields.Float(string="Taken seats", compute='_taken_seats')

    @api.depends('seats', 'attendee_ids')
    def _taken_seats(self):
        for r in self:
            if not r.seats:
                r.taken_seats = 0.0
            else:
                r.taken_seats = 100.0 * len(r.attendee_ids) / r.seats

Affichez le champs dans la vue formulaire de la session:

views/openacademy.xml
                                <field name="start_date"/>
                                <field name="duration"/>
                                <field name="seats"/>
                                <field name="taken_seats" widget="progressbar"/>
                            </group>
                        </group>
                        <label for="attendee_ids"/>

Et dans sa vue liste:

views/openacademy.xml
               <tree string="Session Tree">
                    <field name="name"/>
                    <field name="course_id"/>
                    <field name="taken_seats" widget="progressbar"/>
                </tree>
            </field>
        </record>

Redémarrez votre serveur pour voir votre champ calculé. Modifiez la liste des participants et/ou le nombre de places disponibles pour voir le champ calculé se mettre à jour automatiquement.

Valeurs par défaut

Tout champ peut recevoir une valeur par défaut. Dans la définition de champ, ajoutez l’option default=XX est: - soit une valeur littérale Python (booléen, entier, flottant, chaîne) - soit une fonction prenant un jeu d’enregistrements et renvoyant une valeur:

name = fields.Char(default="Unknown")
user_id = fields.Many2one('res.users', default=lambda self: self.env.user)
Note

L’objet self.env donne accès aux paramètres de requête et à d’autres choses utiles:

  • self.env.cr est l' objet curseur de la base de données ; il est utilisé pour interroger la base de données en direct.

  • self.env.uid est l’ID de l’utilisateur actuel dans la base de données.

  • self.env.user est l’enregistrement de l’utilisateur actuel

  • self.env.context est le dictionnaire de contexte

  • self.env.ref(xml_id) renvoie l’enregistrement correspondant à un identifiant XML

  • self.env[model_name] renvoie une instance du modèle donné

Créez des champs avec valeur par défaut

Sur l’objet session:

  • Définissez la valeur par défaut du champ start_date à aujourd’hui.

  • Ajoutez un champ activedans la classe Session et définissez les sessions comme actives par défaut.

Modifiez la session dans le fichier models/models.py:

models/models.py
(...)
    start_date = fields.Date(default=fields.Date.today)
(...)
    active = fields.Boolean(default=True)
Note
Le champ active est un champ "magique": tous les enregistrements pour lesquels active == False sont rendus invisibles dans l’interface d’Odoo.

Contraintes sur les modèles

Odoo propose deux façons de configurer des invariants vérifiés automatiquement: - Les contraintes Python - Les contraintes SQL

Contraintes Python

Une contrainte Python est définie comme une méthode décorée api.constrains() et invoquée sur un jeu d’enregistrements. Le décorateur spécifie les champs impliqués dans la contrainte, de sorte que la contrainte est automatiquement évaluée lorsque l’un d’eux est modifié. La méthode doit déclencher une exception si sa contrainte n’est pas satisfaite:

from odoo.exceptions import ValidationError

@api.constrains('age')
def _check_something(self):
    for record in self:
        if record.age > 20:
            raise ValidationError("Your record is too old: %s" % record.age)
    # all records passed the test, don't return anything

Créez une contrainte Python

Ajoutez une contrainte qui vérifie que le formateur n’est pas présent dans les participants de sa propre session:

models/models.py
from odoo.exceptions import ValidationError

(...)

    @api.constrains('instructor_id', 'attendee_ids')
    def _check_instructor_not_in_attendees(self):
        for r in self:
            if r.instructor_id and r.instructor_id in r.attendee_ids:
                raise ValidationError("A session's instructor can't be an attendee")
Note
La ligne d’import des dépendances doit être placée en début de fichier

Redémarrez le serveur et vérifiez la contrainte.

Contraintes SQL

Les contraintes SQL sont définies via l’attribut de modèle _sql_constraints. Ce dernier est affecté à une liste de triplets de chaînes (name, sql_definition, message), où name est un nom de contrainte SQL valide, sql_definition une expression SQL de type table_constraint et message le message d’erreur si la condition n’est pas remplie.

Créez une contrainte SQL

Ajoutez les contraintes suivantes:

  • VÉRIFIEZ que la description et le titre du cours sont différents

  • Rendre le nom du cours UNIQUE

Modifiez le modèle du cours pour y intégrer les contraintes:

models/models.py
    _sql_constraints = [
        ('name_description_check',
         'CHECK(name != description)',
         "The title of the course should not be the description"),

        ('name_unique',
         'UNIQUE(name)',
         "The course title must be unique"),
    ]

Redémarrez le serveur et vérifiez les deux contraintes.

Assistants

Les assistants décrivent des sessions interactives avec l’utilisateur (ou des boîtes de dialogue) via des formulaires dynamiques. Un assistant est simplement un modèle qui étend la classe TransientModel au lieu de Model. La classe TransientModel étend Model et réutilise tous ses mécanismes existants, avec les particularités suivantes:

  • Les enregistrements de l’assistant ne sont pas censés être persistants; ils sont automatiquement supprimés de la base de données après un certain temps. C’est pourquoi ils sont appelés transitoires.

  • Les modèles d’assistant ne nécessitent pas de droits d’accès explicites: les utilisateurs ont toutes les autorisations sur les enregistrements de l’assistant.

  • Les enregistrements de l’assistant peuvent faire référence à des enregistrements réguliers ou des enregistrements de l’assistant via les champs many2one, mais les enregistrements réguliers ne peuvent pas faire référence aux enregistrements de l’assistant via un champ many2one.

Créez un assistant

Nous voulons créer un assistant qui permet aux utilisateurs de créer des participants pour une session particulière ou pour une liste de sessions à la fois.

Créez un modèle d’assistant avec une relation many2one avec le modèle Session et une relation many2many avec le modèle Partner.

Créez un nouveau fichier pour cela (openacademy/models/wizard.py) et n’oubliez pas de l’importer.

models/wizard.py
from odoo import models, fields, api

class Wizard(models.TransientModel):
    _name = 'openacademy.wizard'
    _description = "Wizard: Quick Registration of Attendees to Sessions"

    session_id = fields.Many2one('openacademy.session',
        string="Session", required=True)
    attendee_ids = fields.Many2many('res.partner', string="Attendees")

Lancement des assistants

Les assistants sont lancés par des actions, avec le champ target défini sur la valeur new. Ce dernier ouvre la vue de l’assistant dans une fenêtre contextuelle. L’action peut être déclenchée par un élément de menu.

Il existe une autre façon de lancer l’assistant: en utilisant un ir.actions.act_window enregistrement comme ci-dessus, mais avec un champ supplémentaire binding_model_id qui spécifie dans le contexte du modèle l’action disponible. L’assistant apparaîtra dans les actions contextuelles du modèle, au-dessus de la vue principale. En raison de certains hooks internes dans l’ORM, une telle action est déclarée en XML avec la balise act_window.

<act_window id="launch_the_wizard"
            name="Launch the Wizard"
            binding_model="context.model.name"
            res_model="wizard.model.name"
            view_mode="form"
            target="new"/>

Lancez votre assistant

  • Définissez une vue de formulaire pour l’assistant.

  • Ajoutez l’action pour la lancer dans le contexte du modèle de session.

views/openacademy.xml
                  parent="openacademy_menu"
                  action="session_list_action"/>

        <record model="ir.ui.view" id="wizard_form_view">
            <field name="name">wizard.form</field>
            <field name="model">openacademy.wizard</field>
            <field name="arch" type="xml">
                <form string="Add Attendees">
                    <group>
                        <field name="session_id"/>
                        <field name="attendee_ids"/>
                    </group>
                </form>
            </field>
        </record>

        <act_window id="launch_session_wizard"
                    name="Add Attendees"
                    binding_model="openacademy.session"
                    res_model="openacademy.wizard"
                    view_mode="form"
                    target="new"/>

</odoo>
  • Définissez une valeur par défaut pour le champ de session dans l’assistant; utilisez la clé du contexte (self._context) active_id pour récupérer la session en cours.

models/wizard.py
    _name = 'openacademy.wizard'
    _description = "Wizard: Quick Registration of Attendees to Sessions"

    def _default_session(self):
        return self.env['openacademy.session'].browse(self._context.get('active_id'))

    session_id = fields.Many2one('openacademy.session',
        string="Session", required=True, default=_default_session)
    attendee_ids = fields.Many2many('res.partner', string="Attendees")
  • Ajoutez des boutons à l’assistant et implémentez la méthode correspondante pour ajouter les participants à la session donnée.

views/openacademy.xml
                        <field name="session_id"/>
                        <field name="attendee_ids"/>
                    </group>
                    <footer>
                        <button name="subscribe" type="object"
                                string="Subscribe" class="oe_highlight"/>
                        or
                        <button special="cancel" string="Cancel"/>
                    </footer>
                </form>
            </field>
        </record>
Note

La balise <button> avec type="object" permet d’appeler une méthode depuis l’interface.

Cela fonctionne sur n’importe quelle vue, pas seulement les assistants.

models/wizard.py
    session_id = fields.Many2one('openacademy.session',
        string="Session", required=True, default=_default_session)
    attendee_ids = fields.Many2many('res.partner', string="Attendees")

    def subscribe(self):
        self.session_id.attendee_ids |= self.attendee_ids
        return {}

Méthodes

Vous trouverez dans la documentation Odoo la liste des méthodes les plus courantes qui sont disponibles. Rapidement:

create

Permet de créer un nouvel enregistrement. Si vous voulez créer un enregistrement sur un autre modèle, appelez la méthode sur l’objet self.env['nom.du.modele']:

rec = self.env['openacademy.course'].create({
    'name': "Cours 2",
    'description': "Description du cours 2"
})
search

Permet de chercher un enregistrement dans la base de données sur la base d’un domaine:

records = self.env['openacademy.course'].search([('name', '=', "Cours 2")])
write

Permet de mettre à jour un ou plusieurs enregistrements.

records = self.env['openacademy.course'].search([('name', '=', "Cours 2")])
records.write({
    'name': "Cours 2 modifié",
    'description': "Description du cours 2"
})
Note
Lorsque vous voulez modifier un seul champ sur un seul enregistrement, vous pouvez affecter le champ directement: record.name = "Cours 2 modifié"