Odooを読む

業務システムのバックエンドの作り方を知りたいので、Odooから勉強する。

  1. 基本的な構成を理解
  2. 個々の機能を理解
  3. 機能追加時のDBの変更を確認する

基本的な構成

OwlによるSPAだが、JSONRPCを使っていて一般的なJSON色付けではなさそう。

docker composeで立ち上げる

  • 5433でローカルから接続する
  • クエリのログを確認できるようにする
services:
  web:
    image: odoo:16.0
    container_name: odoo_server
    depends_on:
      - db
    ports:
      - "8069:8069"
  db:
    image: postgres:15
    container_name: odoo_postgres
    command: postgres -c log_destination=stderr -c log_statement=all -c log_connections=on -c log_disconnections=on
    env_file:
      - .env
    ports:
      - "5433:5432"
    volumes:
      - odoo_data:/var/lib/postgresql/data
volumes:
  odoo_data:

デフォルトのDBのテーブル定義書をA5:SQL Mk-2で出力

アドオンを追加して、テーブル定義書の差分をとることで、追加されたテーブルを確認する
在庫アドオンを追加すると、以下のテーブルが追加される

 diff default_entities/index.html stock_entities/index.html | grep -oE "(\">).*?(</a>)" | sed -e 's/\">//g' | sed -e 's/<\/a>//g'

追加されたテーブル

account_analytic_account
account_analytic_applicability
account_analytic_distribution_model
account_analytic_line
account_analytic_plan
barcode_nomenclature
barcode_rule
base_module_install_request
base_module_install_review
confirm_stock_sms
crm_tag
crm_team
crm_team_member
digest_digest
digest_digest_res_users_rel
digest_tip
digest_tip_res_users_rel
email_template_attachment_rel
fetchmail_server
iap_account
iap_account_res_company_rel
ir_act_server_res_partner_rel
lot_label_layout
lot_label_layout_stock_picking_rel
mail_activity
mail_activity_rel
mail_activity_type
mail_activity_type_mail_template_rel
mail_alias
mail_blacklist
mail_blacklist_remove
mail_channel
mail_channel_member
mail_channel_res_groups_rel
mail_channel_rtc_session
mail_compose_message
mail_compose_message_ir_attachments_rel
mail_compose_message_res_partner_rel
mail_followers
mail_followers_mail_message_subtype_rel
mail_gateway_allowed
mail_guest
mail_ice_server
mail_link_preview
mail_mail
mail_mail_res_partner_rel
mail_message
mail_message_reaction
mail_message_res_partner_rel
mail_message_res_partner_starred_rel
mail_message_schedule
mail_message_subtype
mail_notification
mail_notification_mail_resend_message_rel
mail_resend_message
mail_resend_partner
mail_shortcode
mail_template
mail_template_mail_template_reset_rel
mail_template_preview
mail_template_reset
mail_tracking_value
mail_wizard_invite
mail_wizard_invite_res_partner_rel
message_attachment_rel
payment_country_rel
payment_icon
payment_icon_payment_provider_rel
payment_link_wizard
payment_provider
payment_provider_onboarding_wizard
payment_token
payment_transaction
phone_blacklist
phone_blacklist_remove
picking_label_type
picking_label_type_stock_picking_rel
portal_share
portal_share_res_partner_rel
portal_wizard
portal_wizard_res_partner_rel
portal_wizard_user
privacy_log
privacy_lookup_wizard
privacy_lookup_wizard_line
procurement_group
product_attribute
product_attribute_custom_value
product_attribute_product_template_rel
product_attribute_value
product_attribute_value_product_template_attribute_line_rel
product_attr_exclusion_value_ids_rel
product_category
product_label_layout
product_label_layout_product_product_rel
product_label_layout_product_template_rel
product_label_layout_stock_move_line_rel
product_packaging
product_pricelist
product_pricelist_item
product_product
product_product_stock_track_confirmation_rel
product_removal
product_replenish
product_replenish_stock_route_rel
product_supplierinfo
product_tag
product_tag_product_product_rel
product_tag_product_template_rel
product_template
product_template_attribute_exclusion
product_template_attribute_line
product_template_attribute_value
product_variant_combination
report_stock_quantity
resource_calendar
resource_calendar_attendance
resource_calendar_leaves
resource_resource
res_country_group_pricelist_rel
res_groups_spreadsheet_dashboard_rel
res_partner_autocomplete_sync
res_users_settings
res_users_settings_volumes
sms_composer
sms_resend
sms_resend_recipient
sms_sms
sms_template
sms_template_preview
sms_template_reset
sms_template_sms_template_reset_rel
snailmail_letter
snailmail_letter_format_error
snailmail_letter_missing_required_fields
spreadsheet_dashboard
spreadsheet_dashboard_group
stock_assign_serial
stock_backorder_confirmation
stock_backorder_confirmation_line
stock_change_product_qty
stock_conflict_quant_rel
stock_immediate_transfer
stock_immediate_transfer_line
stock_inventory_adjustment_name
stock_inventory_adjustment_name_stock_quant_rel
stock_inventory_conflict
stock_inventory_conflict_stock_quant_rel
stock_inventory_warning
stock_inventory_warning_stock_quant_rel
stock_location
stock_lot
stock_move
stock_move_line
stock_move_line_consume_rel
stock_move_move_rel
stock_orderpoint_snooze
stock_orderpoint_snooze_stock_warehouse_orderpoint_rel
stock_package_destination
stock_package_level
stock_package_type
stock_package_type_stock_putaway_rule_rel
stock_picking
stock_picking_backorder_rel
stock_picking_sms_rel
stock_picking_transfer_rel
stock_picking_type
stock_putaway_rule
stock_quant
stock_quantity_history
stock_quant_package
stock_quant_stock_request_count_rel
stock_quant_stock_track_confirmation_rel
stock_replenishment_info
stock_replenishment_option
stock_request_count
stock_return_picking
stock_return_picking_line
stock_route
stock_route_categ
stock_route_move
stock_route_packaging
stock_route_product
stock_route_warehouse
stock_rule
stock_rules_report
stock_rules_report_stock_warehouse_rel
stock_scheduler_compute
stock_scrap
stock_storage_category
stock_storage_category_capacity
stock_traceability_report
stock_track_confirmation
stock_track_line
stock_warehouse
stock_warehouse_orderpoint
stock_warn_insufficient_qty_scrap
stock_wh_resupply_table
team_favorite_user_rel
uom_category
uom_uom
utm_campaign
utm_medium
utm_source
utm_stage
utm_tag
utm_tag_rel

DBのログからstock_pickingをgrepしたもの

2023-07-02 06:36:28.977 UTC [36] LOG:  statement: SELECT "stock_picking_type".id FROM "stock_picking_type" WHERE ("stock_picking_type"."active" = true) AND ("stock_picking_type"."company_id" in (1)) ORDER BY  "stock_picking_type"."sequence" ,"stock_picking_type"."id"   LIMIT 80
2023-07-02 06:36:28.978 UTC [36] LOG:  statement: SELECT "stock_picking_type"."id" AS "id", "stock_picking_type"."color" AS "color", "stock_picking_type"."code" AS "code", COALESCE("stock_picking_type"."name"->>'ja_JP', "stock_picking_type"."name"->>'en_US') AS "name" FROM "stock_picking_type" WHERE ("stock_picking_type"."company_id" in (1)) AND "stock_picking_type".id IN (1, 2, 6)
                    SELECT min("stock_picking".id) AS id, count("stock_picking".id) AS "picking_type_id_count" , "stock_picking"."picking_type_id" as "picking_type_id"
                    FROM "stock_picking"
                    WHERE ((("stock_picking"."state" = 'draft') AND (("stock_picking"."state" not in ('done', 'cancel')) OR "stock_picking"."state" IS NULL)) AND ("stock_picking"."picking_type_id" in (1, 2, 6))) AND ("stock_picking"."company_id" in (1))
                    GROUP BY "stock_picking"."picking_type_id"
                    SELECT min("stock_picking".id) AS id, count("stock_picking".id) AS "picking_type_id_count" , "stock_picking"."picking_type_id" as "picking_type_id"
                    FROM "stock_picking"
                    WHERE ((("stock_picking"."state" in ('confirmed', 'waiting')) AND (("stock_picking"."state" not in ('done', 'cancel')) OR "stock_picking"."state" IS NULL)) AND ("stock_picking"."picking_type_id" in (1, 2, 6))) AND ("stock_picking"."company_id" in (1))
                    GROUP BY "stock_picking"."picking_type_id"
                    SELECT min("stock_picking".id) AS id, count("stock_picking".id) AS "picking_type_id_count" , "stock_picking"."picking_type_id" as "picking_type_id"
                    FROM "stock_picking"
                    WHERE ((("stock_picking"."state" = 'assigned') AND (("stock_picking"."state" not in ('done', 'cancel')) OR "stock_picking"."state" IS NULL)) AND ("stock_picking"."picking_type_id" in (1, 2, 6))) AND ("stock_picking"."company_id" in (1))
                    GROUP BY "stock_picking"."picking_type_id"
                    SELECT min("stock_picking".id) AS id, count("stock_picking".id) AS "picking_type_id_count" , "stock_picking"."picking_type_id" as "picking_type_id"
                    FROM "stock_picking"
                    WHERE ((("stock_picking"."state" in ('assigned', 'waiting', 'confirmed')) AND (("stock_picking"."state" not in ('done', 'cancel')) OR "stock_picking"."state" IS NULL)) AND ("stock_picking"."picking_type_id" in (1, 2, 6))) AND ("stock_picking"."company_id" in (1))
                    GROUP BY "stock_picking"."picking_type_id"
                    SELECT min("stock_picking".id) AS id, count("stock_picking".id) AS "picking_type_id_count" , "stock_picking"."picking_type_id" as "picking_type_id"
                    FROM "stock_picking"
                    WHERE (((("stock_picking"."scheduled_date" < '2023-07-02 06:36:28') AND ("stock_picking"."state" in ('assigned', 'waiting', 'confirmed'))) AND (("stock_picking"."state" not in ('done', 'cancel')) OR "stock_picking"."state" IS NULL)) AND ("stock_picking"."picking_type_id" in (1, 2, 6))) AND ("stock_picking"."company_id" in (1))
                    GROUP BY "stock_picking"."picking_type_id"
                    SELECT min("stock_picking".id) AS id, count("stock_picking".id) AS "picking_type_id_count" , "stock_picking"."picking_type_id" as "picking_type_id"
                    FROM "stock_picking"
                    WHERE ((("stock_picking"."backorder_id" IS NOT NULL AND ("stock_picking"."state" in ('confirmed', 'assigned', 'waiting'))) AND (("stock_picking"."state" not in ('done', 'cancel')) OR "stock_picking"."state" IS NULL)) AND ("stock_picking"."picking_type_id" in (1, 2, 6))) AND ("stock_picking"."company_id" in (1))
                    GROUP BY "stock_picking"."picking_type_id"

クエリした結果をみると、数字が多く何を意味しているかよくわからない。

 id | picking_type_id_count | picking_type_id 
----+-----------------------+-----------------
  9 |                     3 |               1
  1 |                     3 |               2

サーバーへのリクエストを見てみる

/web/dataset/call_kwへのPOSTがデータを取得しているように見える。

2023-07-02 07:00:25,722 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:25] "GET /web HTTP/1.1" 200 - 10 0.008 0.014
2023-07-02 07:00:26,349 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:26] "POST /web/action/load HTTP/1.1" 200 - 9 0.007 0.011
2023-07-02 07:00:26,425 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:26] "POST /mail/init_messaging HTTP/1.1" 200 - 32 0.019 0.023
2023-07-02 07:00:26,708 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:26] "POST /web/dataset/call_kw/res.users/systray_get_activities HTTP/1.1" 200 - 2 0.001 0.003
2023-07-02 07:00:26,800 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:26] "POST /web/dataset/call_kw/stock.picking.type/get_views HTTP/1.1" 200 - 2 0.002 0.011
2023-07-02 07:00:26,823 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:26] "GET /web/image/res.company/1/favicon HTTP/1.1" 304 - 5 0.003 0.008
2023-07-02 07:00:26,846 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:26] "POST /mail/load_message_failures HTTP/1.1" 200 - 9 0.022 0.012
2023-07-02 07:00:26,849 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:26] "POST /web/dataset/call_kw/stock.picking.type/web_search_read HTTP/1.1" 200 - 9 0.012 0.012
2023-07-02 07:00:26,852 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:26] "GET /web/image/res.company/1/favicon HTTP/1.1" 304 - 5 0.007 0.012
2023-07-02 07:00:27,140 1 INFO ? werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:27] "GET /mail/static/src/audio/ting.ogg HTTP/1.1" 206 - 0 0.000 0.001
2023-07-02 07:00:27,146 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:27] "GET /web/image/res.company/1/favicon HTTP/1.1" 304 - 5 0.002 0.006
2023-07-02 07:00:27,185 1 INFO test werkzeug: 172.27.0.1 - - [02/Jul/2023 07:00:27] "GET /web/image?model=res.users&field=avatar_128&id=2 HTTP/1.1" 304 - 9 0.007 0.010

コードを読む

odoo/addons/web/controllers/dataset.py

from odoo.api import call_kw

    @http.route(['/web/dataset/call_kw', '/web/dataset/call_kw/<path:path>'], type='json', auth="user")
    def call_kw(self, model, method, args, kwargs, path=None):
        return self._call_kw(model, method, args, kwargs)

    def _call_kw(self, model, method, args, kwargs):
        check_method_name(method)
        return call_kw(request.env[model], method, args, kwargs)

odoo/odoo/api.py

def call_kw(model, name, args, kwargs):
    """ Invoke the given method ``name`` on the recordset ``model``. """
    method = getattr(type(model), name)
    api = getattr(method, '_api', None)
    if api == 'model':
        result = _call_kw_model(method, model, args, kwargs)
    elif api == 'model_create':
        result = _call_kw_model_create(method, model, args, kwargs)
    else:
        result = _call_kw_multi(method, model, args, kwargs)
    model.env.flush_all()
    return result

リクエストのペイロードはdeveloper toolで確認できる。

{
  "id": 3,
  "jsonrpc": "2.0",
  "method": "call",
  "params": {
    "model": "stock.picking.type",
    "method": "get_views",
    "args": [],
    "kwargs": {
      "context": {
        "lang": "ja_JP",
        "tz": "Asia/Tokyo",
        "uid": 2,
        "allowed_company_ids": [
          1
        ],
        "params": {
          "action": 371,
          "model": "stock.picking.type",
          "view_type": "kanban",
          "cids": 1,
          "menu_id": 204
        }
      },
      "views": [
        [
          false,
          "kanban"
        ],
        [
          false,
          "form"
        ],
        [
          false,
          "search"
        ]
      ],
      "options": {
        "action_id": 371,
        "load_filters": true,
        "toolbar": true,
        "mobile": true
      }
    }
  }
}

rpcでほぼ同じパラメータでリクエストしてるのを発見

odoo/addons/web/static/src/legacy/js/services/data_manager.js

            this._cache.views[viewsKey] = rpc.query({
                args: [],
                kwargs: { context, options, views: views_descr },
                model,
                method: 'get_views',
            })

これは、web.rpcで定義されている

var rpc = require('web.rpc');

この実際の定義が見つからない。addons/web/static/src/legacy/js/core/rpc.jsにはあるが、legacyとあるのでこれを使っているのかがわからない。

次回、公式ドキュメントから学んでいく https://www.odoo.com/documentation/16.0/developer/tutorials/getting_started/01_architecture.html