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
Où 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. |
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#...
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.
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
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 __manifest__.py
est le manifeste Odoo du module.
Il contient les informations permettant à Odoo de charger ce module:
{
'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
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:
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.
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é.
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'
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()
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.
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.
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.
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
.
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:
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:
from . import models
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 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
:
<?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:
'demo': [
'demo/demo.xml'
]
Créez également un fichier de sécurité 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:
L’option |
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 colonnesname
etdescription
-
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
|
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 |
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:
<?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:
N’hésitez pas à vous faire réexpliquer si besoin. |
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. |
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>
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>
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 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
:
<?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:
<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.
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 permettant de rechercher un cours sur son nom ou sur sa description. Mettez-là à la suite de la vue formulaire:
</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.
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.
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:
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.
<!-- 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
:
access_openacademy_session,access_openacademy_session,model_openacademy_session,base.group_user,1,1,1,1
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.
À 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
:
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
:
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:
<sheet>
<group>
<field name="name"/>
<field name="responsible_id"/>
</group>
<notebook>
<page string="Description">
-
Créez une vue liste pour Course:
<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:
<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.
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:
session_ids = fields.One2many(
'openacademy.session', 'course_id', string="Sessions")
-
Ajoutez le champ dans la vue du formulaire de cours:
<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.
À 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
:
attendee_ids = fields.Many2many('res.partner', string="Attendees")
-
Adaptez la vue formulaire de la session en conséquence:
<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. |
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.
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é:
|
Ajoute le contenu de l’élément |
|
Remplace l’élément sélectionné par le contenu de l’élément |
|
Insère le contenu de l’élément |
|
Insère le contenu de l’élément |
|
Modifie les attributs de l’élément sélectionné en suivant les directives des balises |
Note
|
Lorsque l’on cherche un seul élément, l’attribut <xpath expr="//field[@name='description']" position="after">
<field name="idea_ids" />
</xpath>
<field name="description" position="after">
<field name="idea_ids" />
</field> |
-
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. |
-
Créez un fichier
openacademy/models/partner.py
et importez-le dans__init__.py
demodels
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)
-
Créez un fichier openacademy/views/partner.xml et ajoutez-le à
__manifest__.py
dans lesdata
:
<?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.
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.
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
:
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.
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:
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 :
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".
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
|
L’objet Itérer sur |
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))
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
-
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:
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:
<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:
<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.
Tout champ peut recevoir une valeur par défaut.
Dans la définition de champ, ajoutez l’option default=X
où X
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
|
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
:
(...)
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.
|
Odoo propose deux façons de configurer des invariants vérifiés automatiquement: - Les contraintes Python - Les contraintes SQL
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
Ajoutez une contrainte qui vérifie que le formateur n’est pas présent dans les participants de sa propre session:
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.
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.
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:
_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.
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.
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.
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")
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"/>
-
Définissez une vue de formulaire pour l’assistant.
-
Ajoutez l’action pour la lancer dans le contexte du modèle de session.
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.
_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.
<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 Cela fonctionne sur n’importe quelle vue, pas seulement les assistants. |
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 {}
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é"
|