Ce que tu vas apprendre
Texte
Char, Text, Html — différences, paramètres clés, stockage PG.
Nombres
Integer, Float, Monetary et le piège currency_field.
Temps
Date vs Datetime : UTC en base, fuseau utilisateur à l'affichage.
Fichiers
Binary, Image — attachment=True, max_width, stockage ir.attachment.
- Avoir lu T09 — Attributs de modèles
- Le module
odooskills_helpdeskinstallé (version 19.0.1.1.0 minimum) - Un environnement Odoo 19 fonctionnel
odooskills_helpdesk de la version 19.0.1.1.0 à
19.0.1.2.0. On ajoute 12 champs scalaires au modèle
helpdesk.ticket.
Les types de champs scalaires, en un coup d'œil
Chaque type Odoo correspond à un type PostgreSQL précis. La colonne Odoo Field est ce que tu écris en Python ; la colonne PG Type est ce qui est stocké en base.
Paramètres communs à tous les champs
Avant de détailler chaque type, voici les paramètres que tu rencontreras sur n'importe quel champ Odoo :
name = fields.Char(
string='Libellé affiché', # Label dans les vues (défaut : nom de l'attribut)
required=True, # Champ obligatoire (NOT NULL côté ORM)
readonly=True, # Non modifiable par l'utilisateur
copy=False, # Exclu lors du duplicate d'un record
default='valeur', # Valeur par défaut (scalaire ou callable)
index=True, # Crée un index PG (btree) sur la colonne
tracking=True, # Trace les changements dans le chatter mail
help='Texte d\'aide tooltip', # Tooltip dans les formulaires
)
default — Pour un default dynamique (date du jour, utilisateur courant…),
passe un callable :
default=fields.Date.today (sans parenthèses) ou
default=lambda self: self.env.user.id
1. Char — texte court
fields.Char stocke une chaîne courte en VARCHAR PostgreSQL.
Sans paramètre size, la colonne est illimitée.
name = fields.Char(string='Sujet', required=True, tracking=True)
reference = fields.Char(string='Référence', copy=False, index=True)
Le paramètre size est facultatif :
# Limité à 50 caractères en BDD
code = fields.Char(string='Code interne', size=50)
size n'est pas validé côté Python avant l'INSERT ;
la troncature se fait au niveau PostgreSQL et lève une exception
value too long for type character varying(N).
Préfère une contrainte @api.constrains si tu veux un message d'erreur propre.
2. Text et Html — texte long
Les deux sont stockés en TEXT PostgreSQL, mais l'interface les traite différemment :
Text→ textarea simple, pas de formatageHtml→ éditeur WYSIWYG Odoo (Quill/OWL) avec gras, listes, liens…
# Texte brut — note de résolution sans mise en forme
resolution_note = fields.Text(string='Note de résolution')
# HTML riche — corps du ticket avec formatage
body_html = fields.Html(string='Corps HTML', sanitize=True)
sanitize=True — Ce paramètre (activé par défaut sur Html)
filtre les balises et attributs HTML dangereux (<script>, gestionnaires d'événements…).
Ne le désactive que si tu maîtrises entièrement la source du contenu.
3. Integer et Float — nombres
# Integer → INTEGER PG, jamais NULL si default=0
incident_count = fields.Integer(string='Nb incidents liés', default=0)
# Float → NUMERIC PG avec précision contrôlée
# digits=(total_chiffres, decimales) → (6, 2) = max 9999.99
hours_spent = fields.Float(string='Heures passées', digits=(6, 2))
Différence Float vs Integer en PG
En Python, un champ Integer retourne toujours 0 quand il est vide
(jamais False ou None). Un champ Float retourne 0.0.
group_operator — Les champs numériques ont un paramètre
group_operator qui définit l'agrégation dans les vues groupées :
'sum' (défaut), 'avg', 'min', 'max', 'count'.
hours_spent = fields.Float(..., group_operator='sum')
4. Monetary — montant en devise
Monetary est un Float enrichi : il connaît sa devise et formate
l'affichage (1 500,00 €). En base, c'est un NUMERIC, mais Odoo
utilise la précision définie par la devise (ex. 2 décimales pour EUR).
Monetary doit toujours être
accompagné d'un champ currency_id de type Many2one('res.currency', ...)
et pointer dessus via currency_field='currency_id'.
Sans ça, l'interface affiche une erreur et le widget ne formate rien.
# Le montant — lié à la devise via currency_field
estimated_cost = fields.Monetary(
string='Coût estimé',
currency_field='currency_id', # NOM de l'attribut Python, pas le champ PG
)
# Le Many2one devise — obligatoire pour Monetary
currency_id = fields.Many2one(
comodel_name='res.currency',
string='Devise',
default=lambda self: self.env.company.currency_id,
)
Le default lambda récupère la devise de la société courante
au moment de la création du record. Si tu travailles en multi-sociétés,
l'utilisateur peut changer la devise manuellement dans le formulaire.
5. Boolean et Selection
Boolean
Un booléen en Odoo n'est jamais NULL en base.
Si tu oublies default, Odoo insère False automatiquement.
Il s'affiche sous forme de case à cocher dans les vues.
is_urgent = fields.Boolean(string='Urgent', default=False, tracking=True)
Selection
Une liste fermée de couples (valeur_stockée, libellé).
La valeur stockée (clé technique) est ce qui va en base — garde-la courte et stable.
Le libellé est traduit par Odoo.
channel = fields.Selection(
selection=[
('email', 'Email'),
('phone', 'Téléphone'),
('portal', 'Portail'),
('chat', 'Chat'),
],
string='Canal',
tracking=True, # chaque changement de canal apparaît dans le chatter
)
selection='_selection_channel' pour construire la liste à la volée.
Utile si les valeurs dépendent du contexte ou de la configuration.
6. Date et Datetime
La distinction principale : Date stocke juste le jour (YYYY-MM-DD),
Datetime stocke date + heure.
# Date : uniquement YYYY-MM-DD, pas de fuseau horaire
deadline = fields.Date(string='Échéance', tracking=True)
# Datetime : date + heure UTC en base
# Odoo convertit automatiquement vers le fuseau de l'utilisateur à l'affichage
resolved_at = fields.Datetime(string='Résolu le', readonly=True)
Datetime en UTC dans PostgreSQL (TIMESTAMP WITHOUT TIME ZONE).
La conversion vers le fuseau de l'utilisateur se fait côté client.
En Python (méthodes server-side), les datetime sont donc en UTC :
from odoo import fields
# Bonne pratique : utiliser fields.Datetime.now() côté serveur
self.resolved_at = fields.Datetime.now() # retourne datetime UTC
# Comparer avec une date du jour
today = fields.Date.today() # retourne date YYYY-MM-DD
Exemple pratique : marquer un ticket comme résolu
def action_close(self):
"""Passe le ticket en 'done' et enregistre la date/heure de résolution."""
self.write({
'state': 'done',
'resolved_at': fields.Datetime.now(),
})
7. Binary et Image — fichiers
Binary
Stocke n'importe quel fichier binaire. Deux modes :
- Sans
attachment=True→ données encodées en base64 directement dans la colonne PG (BYTEA). Déconseillé pour les gros fichiers. - Avec
attachment=True→ données déportées dansir.attachment, la colonne PG ne contient qu'une référence. C'est le mode recommandé.
# Fichier binaire — stocké dans ir.attachment (hors colonne PG)
screenshot = fields.Binary(
string='Capture d\'écran',
attachment=True, # recommandé pour tout fichier > quelques Ko
)
Image
Image est une spécialisation de Binary qui ajoute le
redimensionnement automatique : à chaque sauvegarde, Odoo redimensionne
l'image si elle dépasse max_width × max_height.
avatar = fields.Image(
string='Avatar',
max_width=256, # largeur max en pixels
max_height=256, # hauteur max en pixels
# attachment=True est implicite pour Image
)
Image — Le redimensionnement se déclenche
au save du record, pas à l'affichage. Si tu oublies
max_width/max_height, des photos haute résolution (5 Mo+)
seront stockées telles quelles et ralentiront le chargement des listes.
Pour les avatars et thumbnails, 128×128 ou 256×256 est suffisant.
Image avec variantes tailles
Le pattern natif Odoo pour les images avec plusieurs tailles (utilisé dans res.partner) :
image_1920 = fields.Image(max_width=1920, max_height=1920)
image_128 = fields.Image(related='image_1920', max_width=128, max_height=128, store=True)
image_256 = fields.Image(related='image_1920', max_width=256, max_height=256, store=True)
Mise en pratique — helpdesk_ticket.py complet
Voici le fichier complet après T10, version 19.0.1.2.0 :
from odoo import models, fields
class HelpdeskTicket(models.Model):
_name = 'helpdesk.ticket'
_description = 'Ticket Helpdesk'
_inherit = ['mail.thread', 'mail.activity.mixin', 'odooskills.helpdesk.mixin']
_order = 'priority desc, create_date desc'
_rec_name = 'name'
_unique_reference = models.Constraint(
'UNIQUE (reference)',
"La référence du ticket doit être unique.",
)
# ── Champs de base (T08/T09) ──────────────────────────────────────────────
name = fields.Char(string='Sujet', required=True, tracking=True)
reference = fields.Char(string='Référence', copy=False, index=True)
description = fields.Text(string='Description')
category_id = fields.Many2one(
comodel_name='helpdesk.ticket.category',
string='Catégorie',
ondelete='restrict',
)
partner_id = fields.Many2one('res.partner', string='Client')
state = fields.Selection(
selection=[
('new', 'Nouveau'),
('in_progress', 'En cours'),
('done', 'Résolu'),
],
default='new',
required=True,
tracking=True,
)
# ── Champs non-relationnels (T10) ─────────────────────────────────────────
deadline = fields.Date(string='Échéance', tracking=True)
resolved_at = fields.Datetime(string='Résolu le', readonly=True)
hours_spent = fields.Float(string='Heures passées', digits=(6, 2))
incident_count = fields.Integer(string='Nb incidents liés', default=0)
is_urgent = fields.Boolean(string='Urgent', default=False, tracking=True)
channel = fields.Selection(
selection=[
('email', 'Email'),
('phone', 'Téléphone'),
('portal', 'Portail'),
('chat', 'Chat'),
],
string='Canal',
tracking=True,
)
resolution_note = fields.Text(string='Note de résolution')
body_html = fields.Html(string='Corps HTML', sanitize=True)
screenshot = fields.Binary(string="Capture d'écran", attachment=True)
avatar = fields.Image(string='Avatar', max_width=256, max_height=256)
estimated_cost = fields.Monetary(
string='Coût estimé',
currency_field='currency_id',
)
currency_id = fields.Many2one(
comodel_name='res.currency',
string='Devise',
default=lambda self: self.env.company.currency_id,
)
Upgrade du module
On passe de 19.0.1.1.0 à 19.0.1.2.0 dans le manifest :
'version': '19.0.1.2.0',
Puis on met à jour :
./odoo-bin -c config/odoo.conf -u odooskills_helpdesk -d ta_base --stop-after-init
Odoo détecte automatiquement les nouveaux champs et crée les colonnes PG
(ALTER TABLE helpdesk_ticket ADD COLUMN ...).
Aucune migration manuelle n'est nécessaire pour des ajouts de champs.
-u, tu peux vérifier
la structure de la table en psql :
\d helpdesk_ticket
Récapitulatif — choisir le bon type
| Besoin | Champ Odoo | Piège principal |
|---|---|---|
| Texte court (nom, code…) | Char | size tronque côté PG sans message propre |
| Texte long sans formatage | Text | — |
| Texte riche (gras, listes…) | Html | Désactiver sanitize = risque XSS |
| Entier | Integer | Retourne 0, jamais None |
| Décimal | Float | Sans digits, arrondi imprécis possible |
| Montant en devise | Monetary | currency_field obligatoire |
| Vrai/Faux | Boolean | Jamais NULL, toujours False si omis |
| Liste fermée | Selection | Changer une clé = migration des données existantes |
| Date seule | Date | Ne pas y mettre une heure |
| Date + heure | Datetime | UTC en base, convertir avec fields.Datetime.now() |
| Fichier quelconque | Binary + attachment=True | Sans attachment=True → stocké en colonne BYTEA |
| Image avec resize auto | Image | Oublier max_width/height = images pleine taille stockées |
Prochain article — T11
On aborde les champs relationnels :
Many2one, One2many, Many2many.
Comment relier les modèles entre eux et quand utiliser chaque type de relation.