Newer
Older
Jérome Maes
committed
.. highlight:: python
Jérome Maes
committed
Odoo Guidelines
Jérome Maes
committed
This page introduces the Odoo Coding Guidelines. Those aim to improve the
quality of Odoo Apps code. Indeed proper code improves readability, eases
maintenance, helps debugging, lowers complexity and promotes reliability.
These guidelines should be applied to every new module and to all new development.
.. warning::
When modifying existing files in **stable version** the original file style
strictly supersedes any other style guidelines. In other words please never
modify existing files in order to apply these guidelines. It avoids disrupting
the revision history of code lines. Diff should be kept minimal. For more
details, see our `pull request guide <https://odoo.com/submit-pr>`_.
Jérome Maes
committed
When modifying existing files in **master (development) version** apply those
guidelines to existing code only for modified code or if most of the file is
under revision. In other words modify existing files structure only if it is
going under major changes. In that case first do a **move** commit then apply
the changes related to the feature.
Jérome Maes
committed
Module structure
================
Directories
A module is organised in important directories. Those contain the business logic; having a look at them should make you understand the purpose of the module.
Jérome Maes
committed
- *data/* : demo and data xml
- *models/* : models definition
- *controllers/* : contains controllers (HTTP routes)
Jérome Maes
committed
- *views/* : contains the views and templates
- *static/* : contains the web assets, separated into *css/, js/, img/, lib/, ...*
Other optional directories compose the module.
- *wizard/* : regroups the transient models (``models.TransientModel``) and their views
- *report/* : contains the printable reports and models based on SQL views. Python objects and XML views are included in this directory
- *tests/* : contains the Python tests
Jérome Maes
committed
File naming
For *views* declarations, distinguish the backend views definition (list, form, kanban, ...), the templates (QWeb pages) and bundle (JS and CSS assets). Bundles are theorically QWeb templates, but should be in their own file :file:`assets.xml`.
Jérome Maes
committed
For *models*, split the business logic by sets of models, in each set
Jérome Maes
committed
select a main model, this model gives its name to the set. If there is
only one model, its name is the same as the module name. For
Jérome Maes
committed
each set named <main_model> the following files may be created:
- :file:`models/{<main_model>}.py`
- :file:`models/{<inherited_main_model>}.py`
- :file:`views/{<main_model>}_templates.xml`
- :file:`views/{<main_model>}_views.xml`
Jérome Maes
committed
For instance, *sale* module introduces ``sale_order`` and
Jérome Maes
committed
``sale_order_line`` where ``sale_order`` is dominant. So the
``<main_model>`` files will be named :file:`models/sale_order.py` and
:file:`views/sale_order_views.py`.
Jérome Maes
committed
For *data*, split them by purpose : demo or data. The filename will be
the main_model name, suffixed by *_demo.xml* or *_data.xml*.
For *controllers*, the only file should be named *<my_module_name>.py*. If you need to inherit an existing controller from another module, its name will be *<module_name>.py*. Unlike *models*, each controller class should be contained in a separated file. For instance, *purchase* module implements its part of the customer portal in :file:`controllers/portal.py` and its own typical routes on :file:`controllers/purchase.py`.
For *static files*, since the resources can be used in different contexts (frontend, backend, both), they will be included in only one bundle. So, CSS/Less, JavaScript and XML files should be suffixed with the name of the bundle type. i.e.: *im_chat_common.css*, *im_chat_common.js* for 'assets_common' bundle, and *im_chat_backend.css*, *im_chat_backend.js* for 'assets_backend' bundle.
If the module owns only one file, the convention will be *<module_name>.ext* (i.e.: *project.js*).
Don't link data (image, libraries) outside Odoo: do not use an
URL to an image but copy it in our codebase instead.
Regarding *data*, split them by purpose: data or demo. The filename will be
the *main_model* name, suffixed by *_data.xml* or *_demo.xml*.
Regarding *wizards*, naming convention is :
- :file:`{<main_transient>}.py`
- :file:`{<main_transient>}_views.xml`
Where *<main_transient>* is the name of the dominant transient model, just like for *models*. <main_transient>.py can contains the models 'transient_model.action' and 'transient_model.action.line'.
For *statistics reports*, their names should look like :
- :file:`{<report_name_A>}_report.py`
- :file:`{<report_name_A>}_report_views.py` (often pivot and graph views)
For *printable reports*, you should have :
- :file:`{<print_report_name>}_reports.py` (report actions, paperformat definition, ...)
- :file:`{<print_report_name>}_templates.xml` (xml report templates)
Jérome Maes
committed
Jérome Maes
committed
.. code-block:: text
addons/<my_module_name>/
|-- __init__.py
Jérome Maes
committed
|-- controllers/
| |-- __init__.py
Jérome Maes
committed
|-- data/
| |-- <main_model>_data.xml
| `-- <inherited_main_model>_demo.xml
|-- models/
| |-- __init__.py
| |-- <main_model>.py
| `-- <inherited_main_model>.py
|-- report/
| |-- __init__.py
| |-- <main_stat_report_model>.py
| |-- <main_stat_report_model>_views.xml
| |-- <main_print_report>_reports.xml
| `-- <main_print_report>_templates.xml
Jérome Maes
committed
|-- security/
| |-- ir.model.access.csv
| `-- <main_model>_security.xml
|-- static/
| |-- img/
| | |-- my_little_kitten.png
| | `-- troll.jpg
| |-- lib/
| | `-- external_lib/
| `-- src/
| |-- js/
| | |-- <my_module_name>.js
`-- <my_widget_A>.js
Jérome Maes
committed
| |-- css/
| | `-- <my_module_name>.css
| |-- scss/
| | `-- <my_module_name>.scss
Jérome Maes
committed
| `-- xml/
| |-- <my_module_name>.xml
`-- <my_widget_A>.xml
|-- views/
| |-- <main_model>_templates.xml
| |-- <main_model>_views.xml
| |-- <inherited_main_model>_templates.xml
| `-- <inherited_main_model>_views.xml
`-- wizard/
|-- <main_transient_A>.py
|-- <main_transient_A>_views.xml
|-- <main_transient_B>.py
`-- <main_transient_B>_views.xml
Jérome Maes
committed
.. note:: File names should only contain ``[a-z0-9_]`` (lowercase
alphanumerics and ``_``)
Jérome Maes
committed
.. warning:: Use correct file permissions : folder 755 and file 644.
XML files
=========
Format
------
To declare a record in XML, the **record** notation (using *<record>*) is recommended:
Jérome Maes
committed
- Place ``id`` attribute before ``model``
- For field declaration, ``name`` attribute is first. Then place the
*value* either in the ``field`` tag, either in the ``eval``
attribute, and finally other attributes (widget, options, ...)
ordered by importance.
- Try to group the record by model. In case of dependencies between
action/menu/views, this convention may not be applicable.
Jérome Maes
committed
- Use naming convention defined at the next point
- The tag *<data>* is only used to set not-updatable data with ``noupdate=1``.
If there is only not-updatable data in the file, the ``noupdate=1`` can be
set on the ``<odoo>`` tag and do not set a ``<data>`` tag.
Jérome Maes
committed
.. code-block:: xml
<record id="view_id" model="ir.ui.view">
<field name="name">view.name</field>
<field name="model">object_name</field>
<field name="priority" eval="16"/>
<field name="arch" type="xml">
<tree>
<field name="my_field_1"/>
<field name="my_field_2" string="My Label" widget="statusbar" statusbar_visible="draft,sent,progress,done" />
Jérome Maes
committed
</tree>
</field>
</record>
Odoo supports custom tags acting as syntactic sugar:
- menuitem: use it as a shortcut to declare a ``ir.ui.menu``
- template: use it to declare a QWeb View requiring only the ``arch`` section of the view.
- report: use to declare a :ref:`report action <reference/actions/report>`
- act_window: use it if the record notation can't do what you want
The 4 first tags are prefered over the *record* notation.
Jérome Maes
committed
Naming xml_id
-------------
Security, View and Action
~~~~~~~~~~~~~~~~~~~~~~~~~
Use the following pattern :
* For a menu: :samp:`{<model_name>}_menu`, or :samp:`{<model_name>}_menu_{do_stuff}` for submenus.
* For a view: :samp:`{<model_name>}_view_{<view_type>}`, where *view_type* is
``kanban``, ``form``, ``tree``, ``search``, ...
* For an action: the main action respects :samp:`{<model_name>}_action`.
Others are suffixed with :samp:`_{<detail>}`, where *detail* is a
lowercase string briefly explaining the action.
This is used only if multiple actions are declared for the
Jérome Maes
committed
model.
* For a group: :samp:`{<model_name>}_group_{<group_name>}` where *group_name*
is the name of the group, generally 'user', 'manager', ...
* For a rule: :samp:`{<model_name>}_rule_{<concerned_group>}` where
Jérome Maes
committed
*concerned_group* is the short name of the concerned group ('user'
for the 'model_name_group_user', 'public' for public user, 'company'
for multi-company rules, ...).
.. code-block:: xml
<!-- views and menus -->
<record id="model_name_view_form" model="ir.ui.view">
...
</record>
<record id="model_name_view_kanban" model="ir.ui.view">
...
</record>
<menuitem
id="model_name_menu_root"
name="Main Menu"
sequence="5"
/>
<menuitem
id="model_name_menu_action"
name="Sub Menu 1"
parent="module_name.module_name_menu_root"
action="model_name_action"
sequence="10"
/>
Jérome Maes
committed
<!-- actions -->
<record id="model_name_action" model="ir.actions.act_window">
...
</record>
<record id="model_name_action_child_list" model="ir.actions.act_window">
...
</record>
<!-- security -->
<record id="module_name_group_user" model="res.groups">
Jérome Maes
committed
...
</record>
<record id="model_name_rule_public" model="ir.rule">
...
</record>
<record id="model_name_rule_company" model="ir.rule">
...
</record>
.. note:: View names use dot notation ``my.model.view_type`` or
``my.model.view_type.inherit`` instead of *"This is the form view of
My Model"*.
Jérome Maes
committed
Inherited XML
~~~~~~~~~~~~~
The naming pattern of inherited view is
#. Extension mode: Use the same xml id than the original view you are extending, and suffix it by :samp:`{_inherit}` . For instance, the view :samp:`project.project_view_form` can be extended by :samp:`project_forecast.project_view_form_inherit`.
#. Primary mode: Keep the original xml id.
Jérome Maes
committed
.. code-block:: xml
<record id="module2.model_view_form_inherit" model="ir.ui.view">
<field name="inherit_id" ref="module1.model_view_form"/>
Jérome Maes
committed
...
</record>
<record id="module2.model_view_form" model="ir.ui.view">
<field name="inherit_id" ref="module1.model_view_form"/>
<field name="mode">primary</field>
...
</record>
Jérome Maes
committed
Python
======
PEP8 options
------------
Using a linter can help show syntax and semantic warnings or errors. Odoo
source code tries to respect Python standard, but some of them can be ignored.
Jérome Maes
committed
- E501: line too long
- E301: expected 1 blank line, found 0
- E302: expected 2 blank lines, found 1
- E126: continuation line over-indented for hanging indent
- E123: closing bracket does not match indentation of opening bracket's line
- E127: continuation line over-indented for visual indent
- E128: continuation line under-indented for visual indent
- E265: block comment should start with '# '
Imports
-------
The imports are ordered as
#. External libraries (one per line sorted and split in python stdlib)
#. Imports of ``odoo``
Jérome Maes
committed
#. Imports from Odoo modules (rarely, and only if necessary)
Inside these 3 groups, the imported lines are alphabetically sorted.
.. code-block:: python
# 1 : imports of python lib
import base64
import re
import time
# 2 : imports of odoo
import odoo
from odoo import api, fields, models # alphabetically ordered
from odoo.tools.safe_eval import safe_eval as eval
from odoo.tools.translate import _
from odoo.addons.website.models.website import slug
from odoo.addons.web.controllers.main import login_redirect
Jérome Maes
committed
Idiomatics Python Programming
-----------------------------
Jérome Maes
committed
- Each python file should have ``# -*- coding: utf-8 -*-`` as first line.
- Always favor *readability* over *conciseness* or using the language features or idioms.
- Don't use ``.clone()``
Jérome Maes
committed
.. code-block:: python
Jérome Maes
committed
# bad
new_dict = my_dict.clone()
new_list = old_list.clone()
# good
new_dict = dict(my_dict)
new_list = list(old_list)
- Python dictionary : creation and update
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
.. code-block:: python
# -- creation empty dict
my_dict = {}
my_dict2 = dict()
# -- creation with values
# bad
my_dict = {}
my_dict['foo'] = 3
my_dict['bar'] = 4
# good
my_dict = {'foo': 3, 'bar': 4}
# -- update dict
# bad
my_dict['foo'] = 3
my_dict['bar'] = 4
my_dict['baz'] = 5
# good
my_dict.update(foo=3, bar=4, baz=5)
my_dict = dict(my_dict, **my_dict2)
- Use meaningful variable/class/method names
- Useless variable : Temporary variables can make the code clearer by giving
names to objects, but that doesn't mean you should create temporary variables
all the time:
.. code-block:: python
# pointless
schema = kw['schema']
params = {'schema': schema}
# simpler
params = {'schema': kw['schema']}
- Multiple return points are OK, when they're simpler
.. code-block:: python
# a bit complex and with a redundant temp variable
def axes(self, axis):
axes = []
if type(axis) == type([]):
axes.extend(axis)
else:
axes.append(axis)
return axes
# clearer
def axes(self, axis):
if type(axis) == type([]):
return list(axis) # clone the axis
else:
return [axis] # single-element list
- Know your builtins : You should at least have a basic understanding of all
the Python builtins (http://docs.python.org/library/functions.html)
.. code-block:: python
value = my_dict.get('key', None) # very very redundant
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
Also, ``if 'key' in my_dict`` and ``if my_dict.get('key')`` have very different
meaning, be sure that you're using the right one.
- Learn list comprehensions : Use list comprehension, dict comprehension, and
basic manipulation using ``map``, ``filter``, ``sum``, ... They make the code
easier to read.
.. code-block:: python
# not very good
cube = []
for i in res:
cube.append((i['id'],i['name']))
# better
cube = [(i['id'], i['name']) for i in res]
- Collections are booleans too : In python, many objects have "boolean-ish" value
when evaluated in a boolean context (such as an if). Among these are collections
(lists, dicts, sets, ...) which are "falsy" when empty and "truthy" when containing
items:
.. code-block:: python
bool([]) is False
bool([1]) is True
bool([False]) is True
So, you can write ``if some_collection:`` instead of ``if len(some_collection):``.
- Iterate on iterables
.. code-block:: python
# creates a temporary list and looks bar
for key in my_dict.keys():
"do something..."
# better
for key in my_dict:
"do something..."
# accessing the key,value pair
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
for key, value in my_dict.items():
"do something..."
- Use dict.setdefault
.. code-block:: python
# longer.. harder to read
values = {}
for element in iterable:
if element not in values:
values[element] = []
values[element].append(other_value)
# better.. use dict.setdefault method
values = {}
for element in iterable:
values.setdefault(element, []).append(other_value)
- As a good developper, document your code (docstring on methods, simple
comments for tricky part of code)
- In additions to these guidelines, you may also find the following link
interesting: http://python.net/~goodger/projects/pycon/2007/idiomatic/handout.html
(a little bit outdated, but quite relevant)
Programming in Odoo
-------------------
- Avoid to create generators and decorators: only use the ones provided by
the Odoo API.
- As in python, use ``filtered``, ``mapped``, ``sorted``, ... methods to
ease code reading and performance.
Make your method work in batch
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When adding a function, make sure it can process multiple records. Typically,
such methods are decorated with the ``api.multi`` decorator. Then you will have
to iterate on ``self`` to treat each record.
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
.. code-block:: python
@api.multi
def my_method(self)
for record in self:
record.do_cool_stuff()
Avoid to use ``api.one`` decorator : this will probably not do what you expected,
and extending a such method is not as easy than a *api.multi* method, since it
returns a list of result (ordered by recordset ids).
For performance issue, when developping a 'stat button' (for instance), do not
perform a ``search`` or a ``search_count`` in a loop in a ``api.multi`` method. It
is recommended to use ``read_group`` method, to compute all value in only one request.
.. code-block:: python
@api.multi
def _compute_equipment_count(self):
""" Count the number of equipement per category """
equipment_data = self.env['hr.equipment'].read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id'])
mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in equipment_data])
for category in self:
category.equipment_count = mapped_data.get(category.id, 0)
Propagate the context
~~~~~~~~~~~~~~~~~~~~~
The context is a ``frozendict`` that cannot be modified. To call a method with
a different context, the ``with_context`` method should be used :
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
.. code-block:: python
records.with_context(new_context).do_stuff() # all the context is replaced
records.with_context(**additionnal_context).do_other_stuff() # additionnal_context values override native context ones
Passing parameter in context can have dangerous side-effects. Since the values
are propagated automatically, some behavior can appears. Calling ``create()``
method of a model with *default_my_field* key in context will set the default
value of *my_field* for the concerned model. But if curing this creation, other
object (such as sale.order.line, on sale.order creation) having a field
name *my_field*, their default value will be set too.
If you need to create a key context influencing the behavior of some object,
choice a good name, and eventually prefix it by the name of the module to
isolate its impact. A good example are the keys of ``mail`` module :
*mail_create_nosubscribe*, *mail_notrack*, *mail_notify_user_signature*, ...
Do not bypass the ORM
~~~~~~~~~~~~~~~~~~~~~
You should never use the database cursor directly when the ORM can do the same
thing! By doing so you are bypassing all the ORM features, possibly the
transactions, access rights and so on.
And chances are that you are also making the code harder to read and probably
less secure.
.. code-block:: python
# very very wrong
self.env.cr.execute('SELECT id FROM auction_lots WHERE auction_id in (' + ','.join(map(str, ids))+') AND state=%s AND obj_price > 0', ('draft',))
auction_lots_ids = [x[0] for x in self.env.cr.fetchall()]
# no injection, but still wrong
self.env.cr.execute('SELECT id FROM auction_lots WHERE auction_id in %s '\
'AND state=%s AND obj_price > 0', (tuple(ids), 'draft',))
auction_lots_ids = [x[0] for x in self.env.cr.fetchall()]
# better
auction_lots_ids = self.search([('auction_id','in',ids), ('state','=','draft'), ('obj_price','>',0)])
No SQL injections, please !
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Care must be taken not to introduce SQL injections vulnerabilities when using
manual SQL queries. The vulnerability is present when user input is either
incorrectly filtered or badly quoted, allowing an attacker to introduce
undesirable clauses to a SQL query (such as circumventing filters or
executing UPDATE or DELETE commands).
The best way to be safe is to never, NEVER use Python string concatenation (+)
or string parameters interpolation (%) to pass variables to a SQL query string.
The second reason, which is almost as important, is that it is the job of the
database abstraction layer (psycopg2) to decide how to format query parameters,
not your job! For example psycopg2 knows that when you pass a list of values
it needs to format them as a comma-separated list, enclosed in parentheses !
.. code-block:: python
# the following is very bad:
# - it's a SQL injection vulnerability
# - it's unreadable
# - it's not your job to format the list of ids
self.env.cr.execute('SELECT distinct child_id FROM account_account_consol_rel ' +
'WHERE parent_id IN ('+','.join(map(str, ids))+')')
# better
self.env.cr.execute('SELECT DISTINCT child_id '\
'FROM account_account_consol_rel '\
'WHERE parent_id IN %s',
(tuple(ids),))
This is very important, so please be careful also when refactoring, and most
importantly do not copy these patterns!
Here is a memorable example to help you remember what the issue is about (but
do not copy the code there). Before continuing, please be sure to read the
online documentation of pyscopg2 to learn of to use it properly:
- The problem with query parameters (http://initd.org/psycopg/docs/usage.html#the-problem-with-the-query-parameters)
- How to pass parameters with psycopg2 (http://initd.org/psycopg/docs/usage.html#passing-parameters-to-sql-queries)
- Advanced parameter types (http://initd.org/psycopg/docs/usage.html#adaptation-of-python-values-to-sql-types)
Keep your methods short/simple when possible
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Functions and methods should not contain too much logic: having a lot of small
and simple methods is more advisable than having few large and complex methods.
A good rule of thumb is to split a method as soon as:
- it has more than one responsibility (see http://en.wikipedia.org/wiki/Single_responsibility_principle)
- it is too big to fit on one screen.
Also, name your functions accordingly: small and properly named functions are the starting point of readable/maintainable code and tighter documentation.
This recommendation is also relevant for classes, files, modules and packages. (See also http://en.wikipedia.org/wiki/Cyclomatic_complexity)
Never commit the transaction
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The Odoo framework is in charge of providing the transactional context for
all RPC calls. The principle is that a new database cursor is opened at the
beginning of each RPC call, and committed when the call has returned, just
before transmitting the answer to the RPC client, approximately like this:
.. code-block:: python
def execute(self, db_name, uid, obj, method, *args, **kw):
db, pool = pooler.get_db_and_pool(db_name)
# create transaction cursor
cr = db.cursor()
try:
res = pool.execute_cr(cr, uid, obj, method, *args, **kw)
cr.commit() # all good, we commit
except Exception:
cr.rollback() # error, rollback everything atomically
raise
finally:
cr.close() # always close cursor opened manually
return res
If any error occurs during the execution of the RPC call, the transaction is
rolled back atomically, preserving the state of the system.
Similarly, the system also provides a dedicated transaction during the execution
of tests suites, so it can be rolled back or not depending on the server
startup options.
The consequence is that if you manually call ``cr.commit()`` anywhere there is
a very high chance that you will break the system in various ways, because you
will cause partial commits, and thus partial and unclean rollbacks, causing
among others:
#. inconsistent business data, usually data loss
#. workflow desynchronization, documents stuck permanently
#. tests that can't be rolled back cleanly, and will start polluting the
database, and triggering error (this is true even if no error occurs
during the transaction)
Here is the very simple rule:
You should **NEVER** call ``cr.commit()`` yourself, **UNLESS** you have
created your own database cursor explicitly! And the situations where you
need to do that are exceptional!
And by the way if you did create your own cursor, then you need to handle
error cases and proper rollback, as well as properly close the cursor when
you're done with it.
And contrary to popular belief, you do not even need to call ``cr.commit()``
in the following situations:
- in the ``_auto_init()`` method of an *models.Model* object: this is taken
care of by the addons initialization method, or by the ORM transaction when
creating custom models
- in reports: the ``commit()`` is handled by the framework too, so you can
update the database even from within a report
- within *models.Transient* methods: these methods are called exactly like
regular *models.Model* ones, within a transaction and with the corresponding
``cr.commit()/rollback()`` at the end
- etc. (see general rule above if you have in doubt!)
All ``cr.commit()`` calls outside of the server framework from now on must
have an **explicit comment** explaining why they are absolutely necessary, why
they are indeed correct, and why they do not break the transactions. Otherwise
they can and will be removed !
Use translation method correctly
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Odoo uses a GetText-like method named "underscore" ``_( )`` to indicate that
a static string used in the code needs to be translated at runtime using the
language of the context. This pseudo-method is accessed within your code by
importing as follows:
.. code-block:: python
from odoo.tools.translate import _
A few very important rules must be followed when using it, in order for it to
work and to avoid filling the translations with useless junk.
Basically, this method should only be used for static strings written manually
in the code, it will not work to translate field values, such as Product names,
etc. This must be done instead using the translate flag on the corresponding
field.
The rule is very simple: calls to the underscore method should always be in
the form ``_('literal string')`` and nothing else:
.. code-block:: python
# good: plain strings
error = _('This record is locked!')
# good: strings with formatting patterns included
error = _('Record %s cannot be modified!') % record
# ok too: multi-line literal strings
error = _("""This is a bad multiline example
about record %s!""") % record
error = _('Record %s cannot be modified' \
'after being validated!') % record
# bad: tries to translate after string formatting
# (pay attention to brackets!)
# This does NOT work and messes up the translations!
error = _('Record %s cannot be modified!' % record)
# bad: dynamic string, string concatenation, etc are forbidden!
# This does NOT work and messes up the translations!
error = _("'" + que_rec['question'] + "' \n")
# bad: field values are automatically translated by the framework
# This is useless and will not work the way you think:
error = _("Product %s is out of stock!") % _(product.name)
# and the following will of course not work as already explained:
error = _("Product %s is out of stock!" % product.name)
# bad: field values are automatically translated by the framework
# This is useless and will not work the way you think:
error = _("Product %s is not available!") % _(product.name)
# and the following will of course not work as already explained:
error = _("Product %s is not available!" % product.name)
# Instead you can do the following and everything will be translated,
# including the product name if its field definition has the
# translate flag properly set:
error = _("Product %s is not available!") % product.name
Also, keep in mind that translators will have to work with the literal values
that are passed to the underscore function, so please try to make them easy to
understand and keep spurious characters and formatting to a minimum. Translators
must be aware that formatting patterns such as %s or %d, newlines, etc. need
to be preserved, but it's important to use these in a sensible and obvious manner:
.. code-block:: python
# Bad: makes the translations hard to work with
error = "'" + question + _("' \nPlease enter an integer value ")
# Better (pay attention to position of the brackets too!)
error = _("Answer to question %s is not valid.\n" \
"Please enter an integer value.") % question
In general in Odoo, when manipulating strings, prefer ``%`` over ``.format()``
(when only one variable to replace in a string), and prefer ``%(varname)`` instead
of position (when multiple variables have to be replaced). This makes the
translation easier for the community translators.
Symbols and Conventions
-----------------------
Jérome Maes
committed
- Model name (using the dot notation, prefix by the module name) :
- When defining an Odoo Model : use singular form of the name (*res.partner*
and *sale.order* instead of *res.partnerS* and *saleS.orderS*)
- When defining an Odoo Transient (wizard) : use ``<related_base_model>.<action>``
where *related_base_model* is the base model (defined in *models/*) related
to the transient, and *action* is the short name of what the transient do. Avoid the *wizard* word.
For instance : ``account.invoice.make``, ``project.task.delegate.batch``, ...
- When defining *report* model (SQL views e.i.) : use
``<related_base_model>.report.<action>``, based on the Transient convention.
- Odoo Python Class : use camelcase (Object-oriented style).
Jérome Maes
committed
.. code-block:: python
class AccountInvoice(models.Model):
...
- Variable name :
- use camelcase for model variable
- use underscore lowercase notation for common variable.
- prefix your variable name with *_id* or *_ids* if it contains a record id or list of id. Don't use ``partner_id`` to contain a record of res.partner
Jérome Maes
committed
.. code-block:: python
Partner = self.env['res.partner']
partners = Partner.browse(ids)
Jérome Maes
committed
partner_id = partners[0].id
- ``One2Many`` and ``Many2Many`` fields should always have *_ids* as suffix (example: sale_order_line_ids)
- ``Many2One`` fields should have *_id* as suffix (example : partner_id, user_id, ...)
- Method conventions
- Compute Field : the compute method pattern is *_compute_<field_name>*
- Search method : the search method pattern is *_search_<field_name>*
- Default method : the default method pattern is *_default_<field_name>*
- Selection method: the selection method pattern is *_selection_<field_name>*
Jérome Maes
committed
- Onchange method : the onchange method pattern is *_onchange_<field_name>*
- Constraint method : the constraint method pattern is *_check_<constraint_name>*
- Action method : an object action method is prefix with *action_*. Its decorator is
``@api.multi``, but since it use only one record, add ``self.ensure_one()``
at the beginning of the method.
Jérome Maes
committed
- In a Model attribute order should be
#. Private attributes (``_name``, ``_description``, ``_inherit``, ...)
#. Default method and ``_default_get``
#. Compute, inverse and search methods in the same order as field declaration
#. Selection method (methods used to return computed values for selection fields)
Jérome Maes
committed
#. Constrains methods (``@api.constrains``) and onchange methods (``@api.onchange``)
#. CRUD methods (ORM overrides)
#. Action methods
#. And finally, other business methods.
.. code-block:: python
class Event(models.Model):
# Private attributes
_name = 'event.event'
_description = 'Event'
# Default methods
def _default_name(self):
...
# Fields declaration
name = fields.Char(string='Name', default=_default_name)
seats_reserved = fields.Integer(oldname='register_current', string='Reserved Seats',
store=True, readonly=True, compute='_compute_seats')
seats_available = fields.Integer(oldname='register_avail', string='Available Seats',
store=True, readonly=True, compute='_compute_seats')
price = fields.Integer(string='Price')
type = fields.Selection(string="Type", selection='_selection_type')
Jérome Maes
committed
# compute and search fields, in the same order of fields declaration
Jérome Maes
committed
@api.multi
@api.depends('seats_max', 'registration_ids.state', 'registration_ids.nb_register')
def _compute_seats(self):
...
@api.model
def _selection_type(self):
return []
Jérome Maes
committed
# Constraints and onchanges
@api.constrains('seats_max', 'seats_available')
def _check_seats_limit(self):
...
@api.onchange('date_begin')
def _onchange_date_begin(self):
...
# CRUD methods (and name_get, name_search, ...) overrides
def create(self, values):
Jérome Maes
committed
...
# Action methods
@api.multi
def action_validate(self):
self.ensure_one()
...
# Business methods
def mail_user_confirm(self):
...
Javascript and CSS
==================
Static files organization
--------------------------
Odoo addons have some conventions on how to structure various files. We explain
here in more details how web assets are supposed to be organized.
The first thing to know is that the Odoo server will serve (statically) all files
located in a *static/* folder, but prefixed with the addon name. So, for example,
if a file is located in *addons/web/static/src/js/some_file.js*, then it will be
statically available at the url *your-odoo-server.com/web/static/src/js/some_file.js*
The convention is to organize the code according to the following structure:
- *static*: all static files in general
- *static/lib*: this is the place where js libs should be located, in a sub folder.
So, for example, all files from the *jquery* library are in *addons/web/static/lib/jquery*
- *static/src*: the generic static source code folder
- *static/src/css*: all css files
- *static/src/fonts*
- *static/src/img*
- *static/src/js*
- *static/src/scss*: scss files
- *static/src/xml*: all qweb templates that will be rendered in JS
- *static/tests*: this is where we put all test related files.
Javascript coding guidelines
----------------------------
Jérome Maes
committed
- ``use strict;`` is recommended for all javascript files
- Use a linter (jshint, ...)
- Never add minified Javascript Libraries
- Use camelcase for class declaration
- Unless your code is supposed to run on every page, target specific pages
using the ``if_dom_contains`` function of website module. Target an
element which is specific to the pages your code needs to run on
using JQuery.
.. code-block:: javascript
odoo.website.if_dom_contains('.jquery_class_selector', function () {
Jérome Maes
committed
CSS coding guidelines
---------------------
Jérome Maes
committed
- Prefix all your classes with *o_<module_name>* where *module_name* is the
technical name of the module ('sale', 'im_chat', ...) or the main route
reserved by the module (for website module mainly, i.e. : 'o_forum' for
*website_forum* module). The only exception for this rule is the
webclient: it simply uses *o_* prefix.
Jérome Maes
committed
- Use underscore lowercase notation to name class
Git
===
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
Configure your git
------------------
Based on ancestral experience and oral tradition, the following things go a long
way towards making your commits more helpful:
- Be sure to define both the user.email and user.name in your local git config
.. code-block:: text
git config --global <var> <value>
- Be sure to add your full name to your Github profile here. Please feel fancy
and add your team, avatar, your favorite quote, and whatnot ;-)
Commit message structure
------------------------
Commit message has four parts: tag, module, short description and full
description. Try to follow the preferred structure for your commit messages
.. code-block:: text
[TAG] module: describe your change in a short sentence (ideally < 50 chars)
Long version of the change description, including the rationale for the change,
or a summary of the feature being introduced.
Please spend a lot more time describing WHY the change is being done rather
than WHAT is being changed. This is usually easy to grasp by actually reading
the diff. WHAT should be explained only if there are technical choices
or decision involved. In that case explain WHY this decision was taken.
End the message with references, such as task or bug numbers, PR numbers, and
OPW tickets, following the suggested format:
Related to task #taskId
Fixes #12345 (link and close issue on Github)
Closes #7865 (link and close PR on Github)