[App] JSON REST API trong Odoo

  Sep 9, 2025      2m      0   
 

Tut 19: JSON REST API trong Odoo

[App] JSON REST API trong Odoo

Odoo Tut 19: JSON REST API trong Odoo

Trước khi xem bài viết này, bạn vui lòng hoàn thành hướng dẫn ở Tut 5: Controller trong OdooTut 9: API trong Odoo - XML-RPC để hiểu cách mở kết nối API từ Odoo, mà cho phép ứng dụng bên ngoài có thể gọi vào. Odoo hỗ trợ sẵn giao thức kết nối XML-RPC, nhưng giao thức XML-RPC hiện không còn phổ biến nữa.

Giao thức kết nối API giữa các hệ thống phổ biến hiện nay là JSON (JavaScript Object Notation). Để có thể mở API JSON từ Odoo, ta phải tự viết controller như Tut 5 để trả về nội dung json hoặc mua một addon/module trên Odoo App Store để thực hiện chức năng này. Hiện Minh đang đăng bán một addon để expose API JSON trong Odoo một cách dễ dàng bằng giao diện, hỗ trợ tất cả model trong Odoo và cả custom model do bạn tự hiện thực trong customized addon (tức bạn tự định nghĩa model mới thì vẫn có thể mở API Json cho nó mà không phải tự lập trình gì thêm).

Bài viết này minhng.info sẽ hướng dẫn bạn:

  • Động lực để Minh phát triển addon API
  • Mua addon Secure REST API Management for Odoo (secure_api) trên cửa hàng chính thức Odoo Apps.
  • Cài đặt addon secure_api
  • Giới thiệu tính năng chính của addon secure_api:
    • Mở JSON API dễ dàng bằng giao diện
    • Mở JSON API có xác thực OAuth2.0
    • Các tiện ích quản lý API
  • Mua addon secure_api trực tiếp từ minhng.info

Môi trường lập trình:

  • Ubuntu
  • Python 3
  • Odoo 14 -> 18

Tham khảo bài viết sau để dựng môi trường lập trình Odoo: Tut 2: Hướng dẫn cài đặt Odoo

odoo secure_api json rest api gif

Động lực để Minh phát triển addon API

  • Tăng tính kết nối cho Odoo, có thể biến Odoo thành backend-as-service (BaaS) khi mà công nghệ frontend đang phát triển mạnh mẽ. Như vậy, ta có thể dùng Odoo làm backend cung cấp data thông qua API để giao diện hiển thị.
  • Mở API cho Odoo một cách dễ dàng (easy), nhanh chóng (fast) và thống nhất (consistency) mà không phải lập trình controller.
  • Zero dependency: addon do Minh phát triển không phụ thuộc vào các addon liên quan đến nghiệp vụ (functional) cho doanh nghiệp như sale, inventory, website, … Tức chỉ việc cài đặt thì sẽ tương thích với built-in addons và custom addons đang triển khai.
  • Quản lý tập trung các API tại một nơi duy nhất.
  • Bảo mật API hơn so với tính năng mặc định có sẵn trong Odoo như:
    • Cho phép tắt/ vô hiệu hóa XML-RPC
    • Hỗ trợ OAuth2.0 cho client application
    • Làm mờ khóa "id" của dữ liệu => do Odoo mặc định khóa primary key của dữ liệu (id) sẽ là số nguyên tăng dần, do đó người khác có thể truy vấn đọc dữ liệu khác mà bạn không mong muốn bằng cách tăng id lên để scan dữ liệu. Addon secure_api là một trong những giải pháp API mà Minh đã xử lý vấn đề này bằng cách làm mờ khóa "id" bằng cách encode (hashing) thành dạng chuỗi (ví dụ: 4QB2a, Z8Kw8, 8kWg8, …).
  • Cung cấp thêm các tiện ích khác cho quản lý API để dễ dàng tích hợp giao diện:
    • Thống kế số lượt truy cập (request)
    • In log của mỗi request gần đây

Mua addon "Secure REST API Management for Odoo" (secure_api) trên cửa hàng chính thức Odoo Apps

odoo secure_api shopping cart

Cài đặt addon secure_api

  • Di chuyển module đã mua "secure_api" vào thư mục "addon" của Odoo. Nếu bạn triển khai Odoo dùng docker-compose từ minhng.info, bạn cần di chuyển vào "addons/secure_api"
  • Để tiến hành cài đặt một module mới, ta phải chuyển sang developer mode.
  • Trong Odoo mình thêm ?debug=True vào URL để kích hoạt chế độ debug. Sau đó, mình nhấn vào menu "Update Apps List" (chỉ xuất hiện khi ta ở chế độ debug / developer) và chọn "Update" để module secure_api mới của chúng ta xuất hiện trong danh sách. Thao tác này chính là "refresh" lại danh sách app.
  • Ta tiến hành cài đặt addon bằng cách nhấn nút "Install" tại addon secure_api

Giới thiệu tính năng chính của addon secure_api

1. Mở JSON API dễ dàng bằng giao diện

Ta có thể dễ dàng mở REST API hỗ trợ đầy đủ CRUD (create, read, update, delete) chỉ cần đặc tả các thông số như hình bên dưới:

odoo secure_api create new api

Sau khi mở API thành công, ta có thể request để truy xuất dữ liệu và nhận về dữ liệu theo format JSON đảm bảo security như:

  • Che giấu id của dữ liệu
  • Lọc lại dữ liệu trả về
  • Giới hạn các trường dữ liệu trả về cụ thể cho CREATE, READ, UPDATE và riêng cho SEARCH (ít trường dữ liệu hơn)

Ngoài REST API phổ biến, ta có thể đặc tả kiểu RPC cho API để gọi một phương thức trong model (binding route -> method).

Format dữ liệu trả về bởi addon secure_api thống nhất dễ xử lý:

// SEARCH Response:
{
    "jsonrpc": "2.0",
    "id": null,
    "result": {
        "length": 36,
        "records": [
            {
                "id": "NnRZn",
                "email": "brandon.freeman55@example.com",
                "image_128": "/api/image/2/res.partner/NnRZn/image_128",
                "name": "Brandon Freeman"
            },
            ...
        ]
    }
}

// CREATE Response:
{
    "jsonrpc": "2.0",
    "id": null,
    "result": {
        "id": "P2y3b",
        "company_id": [],
        "email": "",
        "image_1024": "",
        "image_128": "",
        "name": "Test Partner (CRUD Demo Bash)",
        "phone": ""
    }
}

// READ Response:
{
    "jsonrpc": "2.0",
    "id": null,
    "result": {
        "id": "8blEP",
        "company_id": [],
        "email": "azure.Interior24@example.com",
        "image_1024": "/api/image/2/res.partner/8blEP/image_1024",
        "image_128": "/api/image/2/res.partner/8blEP/image_128",
        "name": "Azure Interior",
        "phone": "(870)-931-0505"
    }
}

// UPDATE Response:
{
    "jsonrpc": "2.0",
    "id": null,
    "result": {
        "id": "8blEP",
        "company_id": [],
        "email": "azure.Interior24@example.com",
        "image_1024": "/api/image/2/res.partner/8blEP/image_1024",
        "image_128": "/api/image/2/res.partner/8blEP/image_128",
        "name": "Test Partner (CRUD Demo Bash) (Updated)",
        "phone": "(870)-931-0505"
    }
}

// DELETE Response:
{
    "jsonrpc": "2.0",
    "id": null,
    "result": [
        "NnRZn"
    ]
}

Bảng mã các loại phản hồi (response) mô tả như sau:

Kiểu phản hồiMã HTTPDữ liệu trả về
Thành công200 OKCó key "result"
Lỗi ứng dụng200 OKCó key "error"
Lỗi HTTPMã 4xx hoặc 5xx 

2. Mở JSON API có xác thực OAuth2.0

Cách mở API có xác thực OAuth2.0 phù hợp cho một backend khác thực hiện truy vấn API vào Odoo. Client application phải gọi vào một endpoint để lấy access token trước khi gọi API truy vấn dữ liệu.

Việc cấp phát client app cho một bên thứ ba dễ dàng bằng cách tạo form trên giao diện trong module secure_api. Qua đó, ta có thể chia sẻ dữ liệu của backend Odoo một cách an toàn và bảo mật.

odoo secure_api client application oauth2.0

3. Các tiện ích quản lý API

  • Ta có thể quản lý các API bằng cách gán danh mục (category) hoặc thẻ (tag).
  • Có giao diện thống kê (đếm) số lượt gọi API thành công hoặc thất bại (có tính tỉ lệ).
  • Có giao diện xem các request gần đây để tiện debug các failed / error response trong quá trình tích hợp với hệ thống khác hoặc giao diện.

odoo secure_api statistics, monitor requests

Mua addon secure_api trực tiếp từ minhng.info

Giá bán: 1,700,000 vnđ

Vì sao Minh có thiết lập giá này?

  • Chính sách giá của Odoo Apps bắt buộc module bán trực tiếp hoặc trên các nền tảng khác phải có giá ít nhất tương đương với module đang bán trên Odoo Apps.
  • Số lượng dòng code để hoàn thiện module này: >4000 LOC (lines of code)

Hướng dẫn mua hàng trực tiếp từ minhng.info (chỉ hỗ trợ thanh toán qua chuyển khoản):

  • Bước 1: Điền đầy đủ thông tin mua hàng vào biểu mẫu bên dưới, nhấn nút "Gửi Yêu Cầu Mua Hàng" để lấy mã QR Code.
    • Chọn 01 phiên bản Odoo.
    • Địa chỉ email phải chính xác để nhận addon secure_api.
    • Nếu không có mã QR Code, có thể do hệ thống đang gián đoạn => bạn có thể liên hệ mua hàng qua email sales@minhng.info.
  • Bước 2: Thanh toán bằng cách quét mã QR Code để thực hiện chuyển khoản ngân hàng.
    • Giữ nguyên nội dung chuyển khoản đã được điền sẵn sau khi quét mã QR. Ví dụ: "RZD7QVM MINH NGUYEN".
    • Nếu nội dung chuyển khoản trống không được điền sẵn sau khi quét mã QR, có thể do hệ thống đang gián đoạn => bạn có thể liên hệ mua hàng qua email sales@minhng.info.
  • Bước 3 (TÙY CHỌN): Gửi hình giao dịch thành công.
    • Đính kèm ảnh thanh toán thành công và nhấn nút "Gửi hình ảnh".
    • Quá trình xác thực sẽ được diễn ra nhanh hơn khi bạn gửi hình giao dịch thành công.
  • Bước 4: Trong vòng 24h kể từ khi Minh nhận được chuyển khoản, bạn sẽ nhận được một email đính kèm addon secure_api theo phiên bản Odoo đã chọn.

Lưu ý:

  • minhng.info không hỗ trợ xuất hóa đơn.
  • Các thanh toán đã chuyển khoản sẽ không được hoàn lại.
  • Email hỗ trợ nếu bạn gặp sự cố hoặc thắc mắc khi mua hàng trên minhng.info: sales@minhng.info

<meta name=referrer content=no-referrer>

<div role=region aria-label="Notifications (F8)" tabindex=-1 style=pointer-events:none> <ol tabindex=-1 class="fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]"> </ol>
<section aria-label="Notifications alt+T" tabindex=-1 aria-live=polite aria-relevant="additions text"
    aria-atomic=false></section>
<div class="min-h-screen bg-gradient-background flex items-center justify-center p-6">
    <div class="rounded-lg border-form text-card-foreground shadow-sm w-full shadow-form">
        <div class="flex flex-col p-6 text-center space-y-2">
            <h2>Giỏ Hàng</h2>
            <p class="text-muted-foreground">Điền thông tin dưới đây để mua hàng trực tiếp trên minhng.info</p>
        </div>
        <div class="p-6 pt-0 space-y-6">
            <form class=space-y-6>
                <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
                    <div class=space-y-2>
                        <label for=name>Họ và Tên *</label>
                        <input
                            class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
                            id="name" placeholder="Nhập họ và tên của bạn" value>
                    </div>
                    <div class=space-y-2>
                        <label for=phone>Số Điện Thoại *</label>
                        <input
                            class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
                            id="phone" placeholder="Nhập số điện thoại của bạn" value>
                    </div>
                </div>

                <div class=space-y-2>
                    <label for=email>Email *</label>
                    <input type=email
                        class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
                        id="email" placeholder="Nhập địa chỉ email của bạn" value>
                    <p class="text-xs text-muted-foreground">Sản phẩm sẽ được gửi tới email này. Vui lòng đảm bảo
                        chính xác email.</p>
                </div>
                <div class=space-y-2><label for=street>Địa Chỉ *</label>
                    <textarea
                        class="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 min-h-[100px]"
                        id="street" placeholder="Nhập địa chỉ của bạn"></textarea>
                    <p class="text-xs text-muted-foreground">Địa chỉ của bạn được lưu trữ an toàn chỉ để xử lý giao
                        dịch mua hàng và không bao giờ chia sẻ với bên thứ ba.</p>
                </div>
                <div class="space-y-4 p-4 bg-muted/50 rounded-lg border">
                    <!-- <h3 class="text-lg font-semibold text-foreground">Giỏ Hàng</h3> -->
                    <div class="flex items-center space-x-4"><img
                            src="https://firebasestorage.googleapis.com/v0/b/minh-nguyen-blog.appspot.com/o/apps_odoo%2Fsecure_api%2Ficon%20(1).png?alt=media&token=48329741-a149-4d53-9671-af5ac45fc8ea"
                            data-lov-id=src/components/OdooAddonForm.tsx:208:16 data-lov-name=div
                            data-component-path=src/components/OdooAddonForm.tsx data-component-line=208
                            data-component-file=OdooAddonForm.tsx data-component-name=div
                            data-component-content=%7B%22className%22%3A%22w-12%20h-12%20bg-primary%2F10%20rounded-lg%20flex%20items-center%20justify-center%22%7D
                            class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center" />
                        <div class=flex-1>
                            <h4 class="font-medium text-foreground"><a id="addon_link_on_store"
                                    href="https://apps.odoo.com/apps/modules/18.0/secure_api" target="_blank"
                                    rel="noopener noreferrer">Secure REST API Management for
                                    Odoo</a></h4>
                            <p class="text-sm text-muted-foreground">by minhng.info</p>
                            <div class="flex-wrap gap-2">
                                <span>Phiên Bản Odoo *</span>
                                <button
                                    class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 border border-input hover:text-accent-foreground h-10 px-4 py-2 bg-background hover:bg-muted"
                                    type=button>14.0</button>
                                <button
                                    class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 border border-input hover:text-accent-foreground h-10 px-4 py-2 bg-background hover:bg-muted"
                                    type=button>15.0</button>
                                <button
                                    class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 border border-input hover:text-accent-foreground h-10 px-4 py-2 bg-background hover:bg-muted"
                                    type=button>16.0</button>
                                <button
                                    class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 border border-input hover:text-accent-foreground h-10 px-4 py-2 bg-background hover:bg-muted"
                                    type=button>17.0</button>
                                <button
                                    class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 border border-input hover:text-accent-foreground h-10 px-4 py-2 bg-background hover:bg-muted"
                                    type=button>18.0</button>
                            </div>
                            <p class="text-xs text-muted-foreground" style="font-style: italic;">Vui lòng kiểm tra
                                thông tin và chọn đúng phiên bản Odoo.</p>
                        </div>
                        <div class=text-right>
                            <p id="addon_price" class="text-2xl font-bold text-primary"></p>
                        </div>
                    </div>
                </div>
                <div class=space-y-2>
                    <!-- <p class="text-xs text-muted-foreground">Vui lòng kiểm tra thông tin và chọn đúng phiên bản Odoo.</p> -->
                    <label for="is_mail_subscription" style="font-weight: bold;">
                        <input type="checkbox" id="is_mail_subscription" name="is_mail_subscription" checked>
                        Tôi đồng ý nhận email về các chương trình khuyến mãi, voucher, thông tin sản phẩm / dịch vụ
                        từ minhng.info
                    </label>
                    <br />
                    <div id="purchase_section">
                        <button id="purchase_now"
                            class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 text-primary-foreground hover:bg-primary/90 h-10 px-4 w-full bg-gradient-primary hover:opacity-90 transition-all duration-300 py-6 text-lg font-semibold"
                            type=submit>Mua Hàng</button>
                    </div>
                </div>
            </form>

            <div id="payment-section">
            </div>

            <div id="payment-image" class="hidden">
                <h3 style="color: #2c5aa0; margin-bottom: 15px;">Tải lên ảnh đã thanh toán</h3>

                <div style="text-align: center;">
                    <div id="dropzone" class="dropzone" tabindex="0" role="button"
                        aria-label="Choose or drop an image">
                        <div class="dz-inner">
                            <div class="dz-title">Nhấn để chọn ảnh hoặc kéo thả vào đây</div>
                            <div class="dz-sub">Định dạng: PNG / JPG / JPEG • Tối đa 01 ảnh không quá 20MB</div>
                        </div>
                        <input id="fileInput" type="file" accept="image/*" class="hidden" />
                    </div>

                    <div id="preview" class="preview hidden">
                        <img id="previewImg" alt="preview" />
                        <div>
                            <div class="meta" id="fileMeta"></div>
                            <div class="actions">
                                <button id="send_image_now"
                                    class="inline-flex items-center justify-center gap- whitespace-nowrap rounded-md ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 text-primary-foreground hover:bg-primary/90 h-10 px-4 w-full bg-gradient-primary hover:opacity-90 transition-all duration-300 py-6 text-lg font-semibold"
                                    style="display:none;">Gửi hình ảnh</button>
                                <button id="reset_image"
                                    class="inline-flex items-center justify-center gap- whitespace-nowrap rounded-md ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&amp;_svg]:pointer-events-none [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 text-primary h-10 px-4 w-full bg-muted/50 hover:opacity-90 transition-all duration-300 py-6 text-lg font-semibold"
                                    type="button">Chọn ảnh khác</button>
                            </div>
                        </div>
                    </div>

                    <div id="message" class="msg" aria-live="polite"></div>
                </div>
            </div>

            <div id="payment-thankyou" class="hidden">
                <div
                    style="margin-top: 20px; padding: 15px; background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 5px; text-align: center;">
                    <strong>Cảm ơn bạn đã thanh toán</strong><br />
                    Đơn hàng sẽ được kiểm tra và xử lý trong vòng 24 giờ.<br />
                    Bạn hãy chú ý email đến từ <strong>sales@minhng.info</strong> nhé!<br />
                </div>
            </div>

        </div>
    </div>
</div>

</div>

Hãy tham gia group Facebook để trao đổi và học hỏi thêm về Odoo nhé các bạn => Khám phá Odoo: https://facebook.com/groups/odoo-dev


Cài đặt Odoo:

Danh sách bài viết series Odoo:

Tham gia ngay group trên Facebook để cùng thảo luận với đồng bọn nhé:

Khám phá Odoo


Khám phá xử lý ảnh - GVGroup




-->