By Noé Berdoz
This file stands for my notes while learning Odoo with the Odoo 14 Development cookbook
My main work on a custom module is present here: local/my_library
The functions of the subdirectories are as follows:
• src/: This contains the clone of Odoo itself, as well as the various third-party add-on projects (we have added Odoo source code to the next step in this recipe).
• local/: This is used to save your instance-specific add-ons.
• bin/: This includes various helper executable shell scripts.
• filestore/: This is used as a file store.
• logs/ (optional): This is used to store the server log files.
From your odoo instance:
- Authenticate as admin
- Go to Settings menu from up-left menu
- Activate Debug mode on Developer Tools
From URL:
-> http://localhost:8069/web#menu_id=102&action=94
--> http://localhost:8069/web?debug=1#menu_id=102&action=94.
Debug with assets:
http://localhost:8069/web?debug=assets#menu_id=102&action=94.
The with assets mode won't minify the CSS and JS code, it's useful when debugging web
- From the menu where you activated it
- Edit URL and add ?debug=0
- Click on the bug in the menu bar and choose Leave developer tools
They are found with the manifest.py file inside their directory.
From the web interface with an administrator account Or from command line wiht -i parameter env/bin/python src/odoo/odoo-bin -d odoo14-dev -i stock,website -c .odoorc
Do it with the web interface or with a shell command
Shut down your odoo server and do this command where you want the new module to be stored
$ ../env/bin/python ../src/odoo/odoo-bin scaffold my_module
If you want to manually set the module path: Consider the following example:
$ ~/odoo-dev/odoo/odoo-bin scaffold my_module ~/odoo-dev/local-addons
A default template is used, but you can also set a theme template fr website with option -t:
$ ~/odoo-dev/odoo/odoo-bin scaffold -t path/to/template my_module
You need to add an init.py file at the root of the module telling odoo what to init The init files are letting our module aware of the file it has to handle The manifest.py file is used to create the module base information
The base tree of the module should be so:
my_module/
├── controllers
├── controllers.py
│ └── init.py
├── demo
│ └── demo.xml
├── init.py
├── manifest.py
├── models
│ ├── init.py
│ └── models.py
├── security
│ └── ir.model.access.csv
└── views
├── templates.xml
└── views.xml
From command line:
$ env/bin/python src/odoo/odoo-bin -c .odoorc -u my_library -d odoo14-dev
Or you can upgrade it from the web interface
The model folder is used to generate the database table for the module You have to implement it with a class
from odoo import models, fields
class LibraryBook(models.Model):
_name = 'library.book' # This will be library_book in the database
name = fields.Char('Title', required=True)
date_release = fields.Date('Release Date')
author_ids = fields.Many2many(
'res.partner',
string='Authors'
)
To see a model structure from the GUI, activate de developer mode and go to Settings -> Technical | Database Structure | Models
When a model is modified you need to upgrade the module
When you implement a new module with a view, be aware that you need to configure permissions on the view, to see the view without configurations, go to the superuser mode -> debug icon -> Become superuser
Models have structural attributes, they start with an underscore '_'
_name defines the internal global identifier (_name="library.book" -> id will be library_book)
_rac_name is used to set the field that's used as a representation or title for the records, Odoo GUI will show this one for records instead of name
_order is used to set the order in which the records are presented
If your model doesn't have a name field and no specification for the records name, the name in GUI will be model name and record ID like (library.book, 1)
If you want to display a special name for the records, like a combination of the name and release date you have to override the name_get() function:
def name_get(self):
result = []
for record in self:
rec_name = "%s (%s)" % (record.name, record.date_release)
result.append((record.id, rec_name))
return result
- Char -> string
- Text -> multiline string
- Selection -> enum (Be aware that '0' can't be set as integer key)
- Html -> similar to Text but with rich text storage in Html format
- Binary -> binary files (images or documents)
- Boolean -> Bool
- Date -> Date handled as Python date object (fields.Date.today() to set current date as default)
- Datetime -> datetime handled as Python datetime object (fields.Date.now to set current datetime as default)
- Integer -> int
- Float -> float, their precision can be defined
- Monetary -> amount with currency
Default fields that shouldn't be set manually:
- id -> id
- create_date -> record creation timestamp
- create_uid -> user who created record
- write_date -> last edit timestamp
- write_uid -> user from last edit
Special fields:
- _log_access=False -> To disable the creation of the 4 log fields:
- active -> only records with active set to True are visible
- string -> field's title used in GUI, if not set it will show an adapted field name (first case Upper, '_' as ' ')
- translate -> make the field translatable depending on user language
- default -> default field value
- help -> explanation text on field hover
- groups -> make field restricted to user group
- states -> dynamically set a state value from a selection value (states are attributes: readonly, required, invisible)
- copy -> set field to copy or not when record duplicated (True for m2m, False for o2m)
- index -> set True to create a database index (for faster searches)
- readonly -> readonly security permission
- required -> required field
- company_dependant -> field store different value for each company
- group_operator -> SQL group operator (count, count_distinct, array_agg, bool_and, bool_or, max, min, avg, sum)
- sanitize -> secure content from injections, used by Html field
- sanitize_tags=True -> remove tags that are not part of the whitelist (default)
- sanitize_attributes=True -> remove attributes of tags that are not part of whitelist
- sanitize_style=True -> remove style properties that are not part of whitelist
- strip_style=True -> remove all style elements
- strip_class=True -> remove the class attributes
Computed fields are fields that are calculated from other fields. Ex. total amount calculated from multiple prices and quantity.
@api.depends('date_release')
def _compute_age(self):
today = fields.Date.today()
for book in self:
if book.date_release:
delta = today - book.date_release
book.age_days = delta.days
else:
book.age_days = 0
Computed fields are dynamically calculated at runtime, they are not stored in the database. The ORM uses caching to avoid useless recalculating.
To use the ORM caching you have to use the @depends decorator to let it detect when the value changes
Be aware that the compute function need a value for the @depends decorator to work, if there are conditions setting the value to NULL, il will raise an error tricky to debug.
There are many computed flags:
- store -> set to True to store field in the database, no needs to implement a search method.
- compute_sudo -> set to True to do the computation with elevated privileges
Odoo has his own ORM API
Depending on the relations between the model, the database structure will be adapted.
Many-to-many relations don't add columns to te tables for the models, they create an intermediate table with two columns to store the related ID's.
Odoo handles that, the intermediate table name will be by default the two related models alphabetically sorted with a '_rel' suffix
- many-to-one -> commonly abbreviated m2o
- one-to-many -> commonly abbreviated 02m
- many-to-one -> commonly abbreviated m2m
- ondelete -> determines what happens when record is deleted (set null, restrict, cascade)
- context -> adds variable to client context
- domain -> search filter to limit the list of available records related
- relation -> only m2m, overrides the default name of intermediate table
- comodel_name -> target model identifier
- column1 -> name for the m2o field
- column2 -> name for the m2o field linking to comodel
- inverse_name -> only for o2m, field name in the target model for inverse many-to-one relation
- limit -> for o2m and m2m, limit number of records to read in GUI
More than once Many2many relations between the same two models:
Provide the relation table name for the second relation, otherwise the default name will conflict
Intermediate table with a default name > 63 characters:
use the relation attribute to set a shorter name
PostgreSQL identifier limit is 63 characters, however odoo puts the identifier name liek so
_relid_id_key
Odoo provides 3 types of inheritance:
- Class inheritance (extension)
- Prototype inheritance
- Delegation inheritance
When a model class is defined with the _inherit attribute it adds modification instead of replacing.
If you add a new method by inheriting existing methods, you should include a super statement to call its version in the parent class
authored_book_ids = fields.Many2many( # many authors to many books, m2m
'library.book',
string='Authored Books'
)
count_books = fields.Integer(
'Number of Authored Books',
compute='_compute_count_books'
)
@api.depends('authored_book_ids')
def _compute_count_books(self):
for r in self:
r.count_books = len(r.authored_book_ids)
Prototype inheritance copy the entire definition of the existing model.
It won't work if you use the same model name in the _inherit and _name attributes, it will behave like a normal extension inheritance.
Prototype inheritance is rarely used in practice, we usually use delegation inheritance as it doesn't need to duplicate data structures.
When you don't want to modify an existing model, but also don't want to create duplicate data structures, you can use delegation inheritance. Instead of _inherit it uses the _inherits class attributes,
Delegation inheritance only works for fields and not for methods.
_inherits = {'res.partner': 'partner_id'} # Delegation inheritance, sets the parent models to inherit from
partner_id = fields.Many2one(
'res.partner',
ondelete='cascade' # Deleting a partner will delete the corresponding member
)
# or like this with delegation attribute shortcut
partner_id = fields.Many2one(
'res.partner',
ondelete='cascade', # Deleting a partner will delete the corresponding member
delegate=True
)
When you want to add a feature to multiple model, use an abstract model and inherit it to a regular model.
class BaseArchive(models.AbstractModel):
_name = 'base.archive'
active = fields.Boolean(default=True)
def do_archive(self):
for record in self:
record.active = not record.active # set it to opposite
And then in the regular model:
class LibraryBook(models.Model):
_name = 'library.book'
_inherit = ['base.archive'] # Inherit abstract model base.archive
- res.partner -> represent people, organizations, addresses
Views are built with XML within an tag
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- Example custom tree (list) view -->
<record id="library_book_view_tree" model="ir.ui.view">
<field name="name">Library Book List</field>
<field name="model">library.book</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="date_release"/>
<field name="short_name"/>
</tree>
</field>
</record>
</odoo>
There are inheritance in views that are really useful to manipulate a view based on a parent view,
You can for example replace a field or add something before/after it, Here we are inheriting a base res.partner view and we are replacing the place of the "ref" field before the "type" field.
<!-- Replace parent position of reference and global numbers -->
<field name="ref" position="replace"/>
<field name="type" position="before">
<field name="ref" string="Internal Reference"/>
<field name="global_id"/>
<field name="global_id"/>
</field>
You can define users/groups permissions on records/views
Add base rules in a xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!-- Security rules -->
<odoo>
<record id="group_ibrarian" model="res.groups">
<field name="name">Librarians</field>
<field name="users" eval="[(4, ref('base.user_admin'))]"/>
</record>
</odoo>
and set permissions in a csv like so:
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
acl_book,library.book default,model_library_book,,1,0,0,0
acl_book_librarian,library.book_librarian,model_library_book,group_librarian,1,1,1,1
Models can have validations preventing them undesired data Odoo supports two different type of constraints:
- Checked at the database level
- Checked at the server level
Database level constraints are limited to the one supported by PostgreSQL. ex: UNIQUE, CHECK, EXCLUDE.
If they are not matching our needs, we can use Odoo server level constraints that are made with Python.
Database level constraints
_sql_constraints = [Book Category model
(
'name_uniq',
'UNIQUE (name)',
'Book title must be unique.'
),
(
'positive_page',
'CHECK(pages>0)',
'No of pages must be positive'
)
]
Server level constraints
@api.constrains('date_release')
def check_release_date(self):
for record in self:
if record.date_release and record.date_release > fields.Date.today():
raise models.ValidationError(
'Release date must be in the past'
)
Odoo manage the Python raised exception in different ways depending on the exception class. The exceptions are caught by the RPC layer (Remote Procedure Call).
Any exception that is not defined in odoo.exceptions will be handled as an internal server error (HTTP status 500)
To show a pop-up to the user:
msg = _('Moving from %s to %s is not allowed') % (book.state, new_state)
raise UserError(msg)
Error classes examples:
- ValidationError -> Python constraint is not respected
- Access Error -> User action not permitted
- RedirectWaning -> For this error, you can show a redirection button with the error message
The user with a user_id = 1 represent the administrator user
The _() function is defined in odoo.tools.translate It is used to mark a string as translatable
The env attribute of any recordset is available as
self.env[model_name].
It's an instance of the Environment class that is
defined in the odoo.api module.
This class provides a shortcut access to the registry by giving a Python dictionary. It gives an empty recordset of the [model_name]
It also provides a cr attribute which is an SQL cursor
It has a user attribute which is a reference to the current user performing the call
It has a context attribute which is a dictionary that contains the context of the call, it contains the language of the user, time zone, current selection of records ...
To add multiple records in one in the database For example in a model:
def create_categories(self):
categ1 = {
'name': 'Child category 1',
'description': 'Description for child 1'
}
categ2 = {
'name': 'Child category 2',
'description': 'Description for child 2'
}
parent_category_val = {
'name': 'Parent category',
'description': 'Description for parent category',
'child_ids': [
(0, 0, categ1),
(0, 0, categ2),
]
}
record = self.env['library.book.category'].create(parent_category_val)
# it's also possible like so:
# mutliple_records = self.env['library.book.category'].create([categ1, categ2])
To create a record with a Many2one relation, set the relation value with an integer that represent the related ID
To create a record with a One2many or a Many2many relation, set a tuple with three elements (0, 0, dict_val) -> Creates a relation to the main record (6, 0, id_list) -> Creates a relation to many records (Caution: when used on a One2many field, will remove the records from previous relations)
To update records in database, there is 3 possibilities
By attributing value
self.ensure_one() # update only one record
self.date_release = fields.Date.today()
By using the update() method
self.update({
'date_release': fields.Datetime.now(),
'another_field': 'value'
...
})
Or by using the write() method