Odoo od środka

Tryb deweloperski

Uruchamiamy moduł "Ustawienia" i klikamy w link (po prawej u dołu): "Uruchom tryb deweloperski" (Activate the developer mode). Ewentualnie uruchamiamy go otwierając link: /event?debug=1

Dostajemy szereg dodatkowych funkcji - między innymi przeglądanie struktury bazy danych (Ustawienia -> Techniczne -> Struktura bazy danych -> Modele). Przyjrzyjmy się przykładowo modelowi res.users - czyli użytkowników systemu. Odpowiada mu tabela res_users w bazie danych (zwróć uwagę na konwencję: kropka w nazwie moduły staje się podkreśleniem w nazwie tabeli).

Uruchamianie Odoo interaktywnie

Skrypt odoo-bin można uruchomić z parametrem shell. Środowisko Odoo jest wówczas udostępniane poprzez konsolę IPython.

Poniżej przykładowy skrypt wykorzystujący tą możliwość do uruchomienia Odoo interaktywnie, ale z parametrami konkretnej aplikacji:

#!/bin/bash

ODOOHOME=<katalog instalacji>
export PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
SCRIPT=$ODOOHOME/odoo-bin
CONFIG=$ODOOHOME/<plik konfiguracyjny>.conf
LOGFILE=$ODOOHOME/odoo-server.log
PIDFILE=/tmp/odoo.pid
USER=jurek
export LOGNAME=$USER

test -x $SCRIPT || exit 0
set -e

$SCRIPT shell  --config $CONFIG -d <nawa bazy>

exit 0

Na początek warto zauważyć, że obiekty Odoo nieco się różnią od typowych obiektów Pythona. Środowisko w którym są uruchamiane nie jest "czystą tablicą". Uruchom ipython i wykonaj dir(). Następnie uruchom konsolę Odoo (powyższym skryptem) i ponownie wykonaj dir(). Widzisz różnicę?

Dodatkowo otrzymaliśmy:

 'env',
 'odoo',
 'openerp',
 'self'

Atrybuty tych obiektów można wyświetlić pisząc na przykład dir(env).

Możemy pominąć openerp - dodany ze względów historycznych alias do odoo.

In [1]: print odoo.__name__
odoo

In [2]: print openerp.__name__
odoo

Obiekt env zawiera spis wszystkich modeli (tabel bazy danych).

Przykład

users = env['res.users'].search([])
for u in users:
  print u.name

Możemy też sięgać wprost do bazy, korzystając z kursora bazy danych env.cr:

cr = env.cr             
cr.execute('select login from res_users')
for (login,) in cr.fetchall():
  print login

Oczywiście na starcie mamy domyślą bazę. Możemy to zmienić (lub musimy - jeśli wcześniej nie wybraliśmy bazy):

cnx = odoo.sql_db.db_connect('<nazwa bazy>').
cr = cnx.cursor()
cr.execute('select login from res_users')
for user in cr.fetchall():
  print user

A co z wyjątkami i zamknięciem transakcji? Aby się tym zbytnio nie kłopotać warto używać instrukcji with Pythona:

from contextlib import closing

connection=odoo.sql_db.db_connect('e14p')
with closing(connection.cursor()) as cr:
  cr.execute('select login from res_users')
  for (login,) in cr.fetchall():
    print login

Szczegóły implementacji:

Zob. też: https://webkul.com/blog/beginner-guide-odoo-clicommand-line-interface/

Obiekty Odoo a obiekty Pythona

Typowe wykorzystanie obiektów Pythona wygląda następująco:

class klasa():
  def witaj(self):
    print('Witaj świecie')

obiekt=klasa()
obiekt.witaj()

Na wyjściu pojawi się napis "Witaj świecie"

Spróbujmy z podobnym obiektem reprezentującym model Odoo:

from odoo import models, api

class klasa(models.Model):
  _name = 'test'  
  def witaj(self):
    print('Witaj świecie')

Trzeba zwrócić uwagę na to, że obiekt (jak wszystkie modele) jest dziedziczony z models.Model. Jednak po podstawieniu:

obiekt = klasa()

uzyskamy w zmiennej obiekt wartość wartość pustą.

Używanie obiektów Odoo nie jest więc identyczne jak w "czystym" Pythonie. Twórcy Odoo wykorzystali mechanizm metaklas (https://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python), aby zmodyfikować działanie obiektów. Modele z zainstalowanych modułów są dostępne poprzez zmienną env. Aby sprawdzić działanie bez rejestracji nowego modułu możemy z konsoli wpisać:

# TO TYLKO DO TESTÓW - NIE RÓB TEGO W PROGRAMACH
env.registry.models['test'] = klasa
env['test'].witaj()

Zmiana typowego zachowania obiektów obejmuje także modyfikację zmiennej self (w przeciwieństwie do innych języków self w Pythonie to nie tylko zapis odwołania się do atrybutów obiektu, ale zmienna którą można modyfikować). Dla modeli zmienna self przyjmuje wartość recordset (zbiór rekordów). Możemy więc na przykład sprawdzać wielkość tego zbioru funkcją len():

class klasam(models.Model):
   _name = 'testm'

   def drukuj(self):
     print 'len(self)=%s' % len(self)


env.registry.models['testm'] = klasam
env['testm'].drukuj()

Oczywiście wynikiem dla tego fikcyjnego modelu będzie 0.

Więcej na ten temat: http://www.odoo.com/documentation/10.0/reference/orm.html

Tematowi temu będzie też poświęcony rozdział "Modele".

Skrypty wywoływane z linii komend

Odoo umożliwia tworzenie skryptów w Pythonie, które mogą korzystać z obiektowego środowiska (w tym api). Poniżej szkielet takiego skryptu:

#!/usr/bin/env python


import odoo
from odoo import models, fields, api, SUPERUSER_ID
from odoo.tools import config
from odoo.modules.registry import RegistryManager
from odoo.api import Environment
from odoo.modules import get_modules, get_module_path

import sys
import os
from os.path import join as joinpath, isdir
import logging

PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, PROJECT_ROOT)

__import__('os').environ['TZ'] = 'UTC'
__import__('pkg_resources').declare_namespace('odoo.addons')

CONFIG = <tu wstaw ścieżkę do swoejgo pliku konfiguracyjnego >


def wykonaj(dbname='demo', uid=1, context=None):
    args = ['--config', CONFIG, ] # możesz dodać inne elementy konf.
    odoo.tools.config._parse_config(args)
    # otwieram baz
    r = RegistryManager.get(dbname)
    cr = r.cursor()
    Environment.reset()
    env = Environment(cr, uid, context or {})
    for module in get_modules(): # wczytanie modułów
        if isdir(joinpath(get_module_path(module), 'cli')):
            __import__('odoo.addons.' + module)

    model = env['nazwa.modelu'] # przyklad....

    ....... przetwarzanie ......

    cr.commit()

Debugging

Najprostsza jest analiza (debuggowanie) z konsoli. Możliwe do wykonania nawet na serwerze bez środowiska graficznego. Mamy tu dwa użyteczne programy:

  • pudb
  • ipdb

Jeśli wolimy środowisko zintegrowane IDE - możemy wybrać PyDev (na bazie Eclipse - zob. http://www.odooclass.com/shop/product/36-accelerate-your-odoo-development-with-eclipse-36), lub lub PyCharm (http://www.mindissoftware.com/Run-Odoo-in-PyCharm-Ubuntu/). Trzeba zwrócić uwagę na to, aby wersja Pythona odpowiadała wymogom Odoo (dla Odoo 10 musi to być 2.7). PyCharm standardowo korzysta ze środowiska wirtualnego w podkatalogu venv obszaru roboczego. Można go ustawić/zmienić poleceniem z(z konsoli): virtualenv venv. Dodatkowo można zmienić opis w pliku ~/.PyCharmCE2017.3/config/options/py_sdk_settings.xml. Niestety trzeba w tym środowisku wykonać także instalację Odoo:

source venv/bin/activate
pip install -r requirements.txt

W analizie wykonanie tak złożonej aplikacji istotne jest znalezienie punktu startowego - w którym można założyć pułapkę debuggera. Można na wstępie wykonać graf przepływów programem pycallgraph.

Przygotujmy w tym celu program startowy kopiując odoo-bin na odoo-bin.py (pycallgraph dodaje ".py"). Przykładowy skrypt uruchamiający pycallgraph:

SCRIPT="odoo-bin.py"
DB="edu"
FILE="odoo10.png"
CONFIG="edu.conf"


pycallgraph -v -d -s --max-depth=8 \
 -e *pkgutil* \
 -e *pkg_resources*  \
 -e *email*  \
 -e *plistlib*  \
 -e *posixpath*  \
 -e *zipfile*  \
 graphviz --output-file=$FILE -- $SCRIPT --config=$CONFIG -d $DB
  • max-deph - maksymalna wysokość drzewa powiązań między modułami / funkcjami
  • -e - wykluczenie z grafu modułów (standardowe moduły zaciemniają wykres)

Wykonanie programu przerywamy klawiszami ^C.

Widzimy, że po kolei sterowanie przechodzi poprzez:

  • odoo.cli.server.Server.run
  • odoo.cli.server.main
  • odoo.cli.server.start
  • odoo.cli.server.ThreadedServer.run

Cały proces startu aplikacji opisuje Ying Liu: http://www.mindissoftware.com/Odoo-Start-Process/

Z punktu widzenia analizy (debugging) wykonania ciekawsze są przepływy zaczynające się od modułów odoo.service, odoo.addons, odoo.http, i odoo.report.

Pewną trudność w debugowaniu aplikacji sprawia fakt, że działa ona zarówno po stronie klienta (frontend - Javascript) jak i serwera (backend - Python). Gdy mamy do czynienia z modułami związanymi z portalem (website) – sprawa jest w miarę prosta, gdyż łatwo można odszukać kontroler, a w nim funkcję odpowiadającą na konkretne zapytania. W przypadku modułów zaplecza interakcja jest dużo bardziej złożona. Dla przykładu kliknięcie w przycisk otwierający stronę wcale nie powoduje wywołania funkcji renderującej, ale wypracowanie odpowiedzi JSON o następnym oknie. Dopiero następne pytanie spowoduje renderowanie wyświetlanej strony. Aby debugować ten proces, trzeba ustawić pułapkę na funkcję load_views w module odoo/models.