Khách đưa thẻ thành viên, nhân viên quét một cái là ra hồ sơ — cái mã vuông đó (QR) và cái mã sọc dưới nó (Code128) đều do Odoo tự sinh, không cần thư viện ngoài. Nhưng nếu bạn làm y hệt tài liệu hướng dẫn, rất có thể tờ PDF in ra chỉ có... hai ô trống. Bài này chỉ cách sinh mã cho report, và quan trọng hơn: vì sao cách phổ biến hay hỏng và cách nào chắc ăn.

Odoo sinh mã bằng gì

Không cần cài qrcode hay python-barcode. Model ir.actions.report có sẵn method barcode(barcode_type, value, **kwargs) trả về bytes ảnh PNG. Gõ thử trong odoo shell:

Ảnh chụp phiên odoo shell nền tối, gán R bằng env ir.actions.report, gọi R.barcode QR VIP-0001 width 120 height 120 lấy 8 byte đầu trả về chữ ký PNG b x89PNG, len của QR trả về 2977 khoảng 3 KB, len của Code128 VIP-0001 trả về 572 khoảng nửa KB, kèm chú thích các kiểu hỗ trợ QR Code128 EAN13 EAN8 Code39 UPCA I2of5 Codabar POSTNET và các tham số width height humanreadable quiet barLevel

Hình 1: ir.actions.report.barcode() trả về ảnh PNG thật — 8 byte đầu đúng chữ ký \x89PNG. QR ~3 KB, Code128 ~0.5 KB. Method này bọc thư viện reportlab nên hỗ trợ nhiều kiểu: QR, Code128, EAN13, EAN8, Code39, UPCA, I2of5, Codabar, POSTNET... humanreadable=1 in luôn chuỗi gốc dưới mã vạch.

Tài liệu Odoo (ngay trong docstring của controller /report/barcode) gợi ý dùng trong QWeb thế này:

<img t-att-src="'/report/barcode/QR/%s' % o.name"/>

Trông gọn. Và nó hỏng trong nhiều môi trường.

Cái bẫy: ô trống thay vì mã QR

Tôi làm đúng theo mẫu đó, thêm vào report, in ra — và nhận về hai khung viền rỗng. Không lỗi, không cảnh báo, chỉ là ảnh không tải được.

Lý do nằm ở cách Odoo dựng PDF. Bộ render (wkhtmltopdf) nhận HTML rồi tự gọi ngược HTTP về chính máy chủ Odoo để lấy từng ảnh <img src="/report/barcode/...">. Nếu Odoo đang chạy workers = 0 (một tiến trình — mặc định khi phát triển), thì tiến trình duy nhất ấy đang bận dựng PDF, không còn ai phục vụ cái request ảnh kia. Ảnh timeout → khung rỗng. Trên server nhiều worker thì thường ổn, nhưng bạn không muốn report của mình phụ thuộc vào việc đúng lúc đó có worker rảnh hay không.

Cách chắc ăn: sinh ảnh sẵn rồi nhúng thẳng

Bỏ vòng HTTP đi. Sinh mã trong Python thành một trường Binary của model, rồi để QWeb nhúng bằng image_data_uri — ảnh đã nằm sẵn trong HTML dưới dạng data-URI, không cần gọi ngược gì cả:

Ảnh chụp đoạn mã nền tối hai phần, phần trên là models the_thanh_vien.py khai import base64, hai trường qr_ma và barcode_ma kiểu fields Binary compute _compute_ma_vach, method _compute_ma_vach với api depends name lặp qua self tạo url caphe-viet.vn the tên thẻ rồi gọi Report barcode QR cho url và Report barcode Code128 cho tên thẻ có humanreadable 1, cuối cùng gán qr_ma và barcode_ma bằng base64 b64encode; phần dưới là report nhúng bằng img t-att-src image_data_uri o qr_ma và image_data_uri o barcode_ma kèm chú thích cách này render offline được, và một dòng bẫy dùng URL report barcode QR sẽ ra ô rỗng khi workers 0

Hình 2: Sinh mã một lần trong _compute_ma_vach — Report.barcode(...) cho ra bytes PNG, base64.b64encode để nhét vào trường Binary. Trong report, image_data_uri(o.qr_ma) biến trường Binary đó thành data:image/png;base64,... ngay tại chỗ. Không có request HTTP nào phát sinh khi dựng PDF, nên không có gì để nghẽn.

image_data_uri là hàm dựng sẵn của QWeb, chuyên nhận một chuỗi base64 (giá trị của field Binary) và trả về data-URI hoàn chỉnh cho thuộc tính src. Đây cũng chính là cách Odoo nhúng logo công ty vào header — logo cũng là field Binary.

Nâng cấp module, in lại cùng thẻ VIP-0001:

Ảnh chụp trang PDF khổ A5 ngang thẻ thành viên VIP-0001, dưới bảng thông tin khách hàng có một mã QR vuông đặc nét bên trái và một mã vạch Code128 bên phải với chuỗi VIP-0001 in rõ bên dưới các vạch, tiếp đến là bảng lịch sử tích điểm ba dòng và chân trang thông tin công ty

Hình 3: PDF in ra với mã QR (mã hoá https://caphe-viet.vn/the/VIP-0001 — quét bằng điện thoại ra đúng đường dẫn) và mã vạch Code128 kèm chuỗi đọc được "VIP-0001". Cả hai đặc nét, không còn ô rỗng. Dung lượng PDF tăng từ 62.881 lên 72.029 byte vì ảnh giờ nằm hẳn trong tài liệu — đo bằng _render_qweb_pdf.

Vài lưu ý khi dùng thật

  • QR nên mã hoá URL, không mã hoá mỗi cái mã thẻ. Quét "VIP-0001" thì điện thoại chẳng biết làm gì; quét https://caphe-viet.vn/the/VIP-0001 thì mở thẳng trang tra cứu. Giá trị QR là dữ liệu bạn muốn máy quét đọc được.
  • Code128 nhận gần như mọi ký tự ASCII; còn EAN13 đòi đúng 13 chữ số (12 số + 1 số kiểm) — truyền sai định dạng, barcode() ném ValueError, nên value phải sạch trước khi đưa vào.
  • Field Binary compute mặc định không lưu (store=False) — mã sinh lại mỗi lần đọc. Với thẻ in thỉnh thoảng thì ổn; nếu in hàng loạt và cần nhanh, thêm store=True để cache lại, đổi name thì @api.depends('name') tự tính lại.
  • humanreadable=1 rất đáng bật cho mã vạch tuyến tính: khi máy quét chập chờn, nhân viên còn đọc tay được con số.

Ba ý mang về

  1. Odoo sinh QR/Code128/EAN13... sẵn qua ir.actions.report.barcode() (bọc reportlab), trả về bytes PNG — không cần thư viện ngoài.
  2. Nhúng bằng URL /report/barcode/ có thể ra ô rỗng khi workers=0, vì wkhtmltopdf gọi ngược HTTP mà không ai phục vụ. Cách chắc ăn: sinh ảnh thành field Binary rồi image_data_uri(o.field) — không vòng HTTP, render offline được.
  3. QR mã hoá URL để quét ra hành động thật; Code128 bật humanreadable=1; giá trị phải đúng định dạng của từng loại mã kẻo barcode() ném lỗi.

Report đã có đủ khung, mã vạch, giờ tới lúc cho người dùng chọn trước khi in: Phần sau dựng report in ra từ một wizard — mở hộp thoại hỏi tham số rồi mới xuất PDF theo lựa chọn đó.