Upgrading to Odoo 12

Odoo 12.0 is out since october. I am currently investigating the differences with previous versions to update the instance of the association Les Compagnons du CEP. A lot of changes have been made in a few years but the workflow stays about the same. For example, merging purchase order is impossible in this version. This article describes my workflow, the backup policy, how a module was used and fixed to restore this feature. Finally, the changes in my custom product import function are presented.

Production and debug setup

Agayon.be instance of Odoo runs inside a Docker container on a small VPS. Postgresql is installed as a core package. This setup is great in production but it is difficult to debug some python code with this configuration. The first step is to run Odoo with an IDE (I personally use the excellent Pycharm).

Running and debugging Odoo

The first step is the creation of the virtualenv. Once the Postgresql instance is ready, you can build the environment.

git clone https://www.github.com/odoo/odoo --depth 1 --branch 12.0
$ virtualenv myenv
$ source myenv/bin/activate
(myenv)$ pip install -r odoo/requirements.txt
(myenv)$ mkdir custom-addons
(myenv)$ chown odoo: odoo11-custom-addons

Edit odoo.conf and then run odoo:

odoo/odoo-bin -c odoo.conf

Merge purchase order

Unfortunately, a major feature has disappeared in version 12.0: automatic merge of purchase order. I don't know why since it seems unrealistic to send each quotation separately to your vendor. Fortunately a free module can be used to perform the merge but it has a critical bug. Some quotation lines are merged even if they concerns different products.


After forking it, I decided to start by refactoring it. The module is quite small but a lot of code is duplicated. As I try to avoid spaghetti code, it needed to be refactored.I think the new code may be improved but no line is duplicated. Finally, the bug has been fixed.

Backup management

Since a few version, the filestore is mandatory in Odoo. If it is incoherent with the database, some really annoying errors are raised and the solution is quite tedious. My backup procedure has been updated to avoid losing any data. It is based on the article from zeroheure. The backups are saved with the auto_backup module. Restoring the data is not possible with the web interface because the process reaches the memory limit but it can be performed with the following shell script.



if [ -z "$FILESTORE_DIR" ] || [ -z "$DBNAME" ] || [ -z "$BACKUPLOCATION" ]
    echo "verify your variables"
    rm filestore 
    unzip -q $DBNAME.zip
    cp -r filestore $DBNAME
    sudo rm -rf $FILESTORE_DIR/$DBNAME
    sudo chown -R odoo:odoo $FILESTORE_DIR/$DBNAME
    dropdb -U odoo $DBNAME
    createdb -U odoo $DBNAME
    psql $DBNAME --quiet < dump.sql


Wine import with Django website

Version 12.0 needs some minor changes in the code displayed in the previous article. These modifications includes:

  • removing state property in the product template.
  • Adding the invoice_policy and purchase_method in the product template.1
  • Add a reference to the standard price in the product_supplierinfo dictionary. This value is used in the orders when purchasing wines to suppliers.
def research(default_code, supplier_code, wine_name):

    # 1) search if default code is used?)
    # 2) search if suppliers is in the database
    # 3) search if the name is already used

    # Retrieve the dataframes. This example comes from a jupyter nootebook.
    # df_product and df_sellers are already defined.
    # In a real case, we should use class variables.

    n_code, df_code = search_df(df=df_product, col_name='default_code', search_item=default_code,
    n_supplier, df_suppliers = search_df(df=df_suppliers, col_name='function',
                                              search_item=supplier_code, search_int=False)
    n_name, df_name = search_df(df=df_product, col_name='name', search_item=wine_name, search_int=False)

        # ids_product = list(df_code['id'])
        ids_product = df_code['id'].tolist()
    except AttributeError:
        ids_product = []

    if n_code == 0 and n_name == 0 and n_supplier != 0:
        return 'success', ids_product, n_supplier, n_name
    if n_code != 0 and n_name != 0:
        # Another product uses the same name with another code
        return 'e_code_used_same_name', ids_product, n_supplier, n_name
    if n_code != 0:
        # the code is already used
        return 'e_code_used', ids_product, n_supplier, n_name
    if n_supplier == 0:
        # Cannot find the supplier
        return 'e_missing_seller', ids_product, n_supplier, n_name
    if n_name != 0:
        # A product with the same name and another code exists.
        return 'e_code_used_different_name', ids_product, n_supplier, n_name

def import2odoo():
    route_warehouse0_mto = 1
    route_warehouse0_manufacture = 5
    # iterate over all rows, read the cells and assign the wine parameters to variables
    # each row correspond to one wine
    for row in rows:
        seller_name = row[0]
        default_code = row[1]
        name = row[2]
        do_import = row[2]
        comment = row[3]
        name = row[4]
        default_code = row[5]
        standard_price = row[6]
        list_price = row[7]
        seller_code = row[8]
        res_search, ids_product, n_supplier, n_name = research(default_code, seller_code, name)
        if res_search == 'success' and do_import == "1":

            product_template = {
                'name': name,
                'active': True,
                'standard_price': standard_price,
                'list_price': list_price,
                'description': comment ,
                'default_code': default_code,
                'purchase_ok': 1,
                'sale_ok': 1,
                'uom_id': 1,
                'uom_po_id': 1,
                'type': 'product',
                'cost_method': 'standard',
                'invoice_policy' : 'order' , # ordered quantities
                'purchase_method' : 'receive', # control received quantities (or ordered ones, test to delivery)
                'route_ids': [(6, 0, [route_warehouse0_mto, route_warehouse0_manufacture])]
            # For each wine, a template must be created
            template_id = sock.execute(dbname, uid, pwd, 'product.template',

            # Create the supplier information for the wine
            product_supplierinfo = {
                'name': name,
                'product_code': row[1],  # code for supplier
                'product_name': row[15],  # name for supplier
                'min_qty': 1,
                'delay': 300,
                'product_tmpl_id': template_id,
                'price' : standard_price,
            # Create the supplier information for the wine
            product_supplierinfo_id = sock.execute(dbname, uid, pwd,
                                                        'create', product_supplierinfo)
            logging.info("Wine {} : {} has been added".format(default_code, name))

        # Here we take into account the exceptions and several cases: the wine is already present, the seller is missing etc.


After using Odoo in production for 4 years, I still love it. My users are happy to use it daily and the improvements over the year are impressive. Like any other alive project, some changes may break the workflow but Odoo and it's community make it relatively simple to adapt with plugins. Odoo 12 is strong and I look forward to use it for the next 3 years :-).


  1. See this post and this issue