Les conventions internes Ezway, la checklist code review, les pièges classiques et les patterns avancés.
thanatokit_odoo, thanatokit_reporting, gestautosnake_case, jamais d'accent ni de tiret__manifest__.py : version 19.0.X.Y.Z où X.Y.Z suit semverauthor = 'Ezway Technology', license = 'LGPL-3' (ou propriétaire si client)_name en module.entity : thanatokit.dossier_description (visible dans Settings → Technical)mail.thread + mail.activity.mixin sur tout modèle "métier" (activité, chatter)_order explicite (sinon tri par id aléatoire)string= en français (UX cohérente)_id ; collections : _idstracking=True sur les champs sensibles (état, prix, dates)@api.dependscurrency_field défini_ : _compute_xxx, _check_xxx, _get_xxxaction_ : action_confirm, action_cancelcron_ : cron_send_remindersself.ensure_one() au début d'une méthode qui ne supporte qu'un seul recordid du record : view_<model>_<type> → view_library_book_formview_<model>_<type>_inherit_<module>action_<model>menu_<module>_<entity>help avec o_view_nocontent_smiling_face sur les actions principalesAvant d'ouvrir une PR Ezway, vérifier point par point :
base)license, version, category, summarydata : security → data → views → menus → reportsstatic/description/icon.png (140×140 px).sudo() sans justification commentéeauth='public' sont audités%s paramétré (injection SQL)groups= sur la vue.browse() dans une bouclesearch() à l'intérieur d'un compute non stored (recalcul N fois)index=True) sur les FK très requêtéesread sur 10 000+ records sans limit ni paginationprint(), utiliser _loggertry/except: pass silencieux (au minimum logger)_("...") partout dans le code businessenv.ref('module.xml_id')fields.Date.today() / fields.Datetime.now()action_constraints (cas valide + cas invalide)# PAS : self.env['res.partner'].browse(42) ← fragile
admin_partner = self.env.ref('base.partner_admin')
book_id = fields.Many2one(
'library.book',
domain="[('state', '=', 'available'), ('id', 'in', allowed_book_ids)]",
)
allowed_book_ids = fields.Many2many('library.book', compute='_compute_allowed')
def action_view_reservations(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Réservations'),
'res_model': 'library.reservation',
'view_mode': 'list,form',
'domain': [('book_id', '=', self.id)],
'context': {'default_book_id': self.id},
}
<div class="oe_button_box" name="button_box">
<button name="action_view_reservations"
type="object" class="oe_stat_button" icon="fa-book">
<field name="reservation_count" widget="statinfo" string="Prêts"/>
</button>
</div>
price_with_tax = fields.Float(compute='_compute_price', inverse='_inverse_price', store=True)
@api.depends('price', 'tax_id.amount')
def _compute_price(self):
for rec in self:
rec.price_with_tax = rec.price * (1 + (rec.tax_id.amount or 0) / 100)
def _inverse_price(self):
# Permet d'écrire directement dans le champ computed → recalcule price
for rec in self:
if rec.tax_id:
rec.price = rec.price_with_tax / (1 + rec.tax_id.amount / 100)
<record id="cron_overdue_reminders" model="ir.cron">
<field name="name">Library: Rappels retards</field>
<field name="model_id" ref="model_library_reservation"/>
<field name="state">code</field>
<field name="code">model.cron_send_overdue_reminders()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="active" eval="True"/>
</record>
@api.model
def cron_send_overdue_reminders(self):
overdue = self.search([
('state', '=', 'overdue'),
('reminder_sent', '=', False),
])
for reservation in overdue:
reservation._send_reminder_email()
reservation.reminder_sent = True
printimport logging
_logger = logging.getLogger(__name__)
class MyModel(models.Model):
def do_stuff(self):
_logger.info("Démarrage du process pour %s records", len(self))
try:
...
except Exception as e:
_logger.error("Erreur lors du process : %s", e, exc_info=True)
raise
| Piège | Conséquence | Solution |
|---|---|---|
Utiliser attrs="..." en XML | ❌ Crash en v19 (supprimé) | Utiliser invisible="..." directement |
Décorateur @api.multi | Pas d'erreur mais inutile | Le supprimer (par défaut depuis v13) |
self.create({...}) (single dict) | Warning + deprecated | self.create([{...}]) + @api.model_create_multi |
OWL 1 syntax (useState sans destructuring) | Composant non rendu | Lire la doc OWL 2 attentivement |
<tree> au lieu de <list> | Fonctionne mais warning | Utiliser <list> (v19 standard) |
Tracking sans mail.thread | Aucun effet | Hériter ['mail.thread', 'mail.activity.mixin'] |
git checkout -b feat/library-isbn-validation--dev=reload,qweb,xml pour itérer vitegit rebase sur main pour avoir un historique propredocker compose logs -f odoo | grep -i error
docker compose exec odoo odoo -d test19 -u library --stop-after-init -i base
import pdb; pdb.set_trace() # pour débug ligne par ligne
# ou en mieux :
import ipdb; ipdb.set_trace() # avec autocomplete
Puis lancer Odoo en foreground (PAS -d en daemon) :
docker compose run --rm --service-ports odoo odoo
Prochaine étape : faire les 3 exercices pratiques en autonomie, puis lire le code d'un module Ezway réel.