The data model you choose when writing an addon might turn out to have some weaknesses, so you may need to adjust it during the life cycle of your addon. In order to allow that without a lot of hacks, Odoo supports versioning in addons and running migrations if necessary.
We assume that in an earlier version of our module, the date_release field was a character field, where people wrote whatever they saw fit as the date. Now we realize we need this field for comparisons and aggregations, which is why we want to change its type to Date
. Odoo does a great job at type conversions, but, in this case, we're on our own, which is why we need to provide instructions on how to transform a database with the previous version of our module installed to a database where the current version can run:
__openerp__.py
manifest:'version': '9.0.1.0.1',
migrations/9.0.1.0.1/pre-migrate.py
:def migrate(cr, version): cr.execute('ALTER TABLE library_book RENAME COLUMN date_release TO date_release_char')
migrations/9.0.1.0.1/post-migrate.py
:from openerp import fields from datetime import date def migrate(cr, version): cr.execute('SELECT id, date_release_char FROM library_book') for record_id, old_date in cr.fetchall(): # check if the field happens to be set in Odoo's internal # format new_date = None try: new_date = fields.Date.from_string(old_date) except: if len(old_date) == 4 and old_date.isdigit(): # probably a year new_date = date(int(old_date), 1, 1) else: # try some separators, play with day/month/year # order # ... if new_date: cr.execute('UPDATE library_book SET date_release=%s', (new_date,))
Without this code, Odoo would have renamed the old date_release column to date_release_moved and created a new one, because there's no automatic conversion from character fields to date fields. From the point of view of the user, the data in date_release would simply be gone.
The first crucial point is that you increase the version number of your addon, as migrations run only between different versions. During every update, Odoo writes the version number from the manifest at the time of the update into the table ir_module_module
. The version number is prefixed with Odoo's major and minor version, if the version number has three or fewer components. In the example above, we explicitly also named Odoo's major and minor version, which is good practice, but a value of 1.0.1
would have had the same effect, because, internally, Odoo prefixes short version numbers for addons with its own major and minor version number. Generally, using the long notation is a good thing because you can see with just a glance at the manifest file for which version of Odoo an addon is meant.
The two migration files are just code files that don't need to be registered anywhere. When updating an addon, Odoo compares the addon's version as noted in ir_module_module
with the version in the addon's manifest. If the manifest's version is higher (after possibly adding Odoo's major and minor version), this addon's migrations
folder will be searched if it contains folders with the version(s) in between, up to, and including the version that is currently updated.
Then, within the folders found, it searches for Python files whose names start with pre-
, loads them, and expects them to define a function called migrate,
which has two parameters. This function is called with a database cursor as the first argument and the currently installed version as the second argument. This happens before Odoo even looks at the rest of the code the addon defines, so you can assume nothing changed in your database layout compared to the previous version.
After all pre-migrate
functions run successfully, Odoo loads the models and data declared in the addon, which can cause changes in the database layout. Given we renamed date_release in pre-migrate.py
, Odoo will just create a new column with that name, but with the correct data type.
After that, with the same search algorithm, post-migrate
files will be searched and executed if found. In our case, we need to look at every value and see if we can make something usable out of it, otherwise, we keep NULL
as data. Don't write scripts iterating over a whole table yourself if not absolutely necessary; in this case, we should have written a very big unreadable SQL switch that does what we want.
In both the pre- and post-migration steps, you only have access to a cursor, which is not very convenient if you're used to Odoo environments. It can lead to unexpected results to use models at this stage, because in the pre-
step, the addon's models are not yet loaded and also, in the post- step, models defined by addons depending on the current addon are not yet loaded too. But if this is not a problem for you, either because you want to use a model your addon doesn't touch or a model for which you know the aforementioned is not a problem, you can create the environment you're used to by writing the following:
from openerp import SUPERUSER_ID from openerp.api import Environment def migrate(cr, version): env = Environment(cr, SUPERUSER_ID, {}) # env holds all currently loaded models
When writing migration scripts, you'll often be confronted with repetitive tasks like checking if a column or table exists, renaming things, or mapping some old values to new values. It's frustrating and error-prone to reinvent the wheel here; consider using https://github.com/OCA/openupgradelib if you can afford the extra dependency.