Bạn cần thêm một dòng ghi chú vào mẫu in hoá đơn của Odoo, hoặc chèn logo, hoặc đổi một cái class Bootstrap. Bản năng đầu tiên là chép nguyên template gốc về rồi sửa. Đừng. Mẫu in trong Odoo cũng là một QWeb template, mà template thì kế thừa được — hệt như bạn kế thừa một <form> view. Chép nguyên bản là tự nhận nợ: bản gốc của Odoo cập nhật theo phiên bản, còn bản chép của bạn thì đứng yên và dần lệch.

Bài này làm đúng một việc: lấy mẫu in report_the_document đã có trong module quan_ca_phe và chèn thêm ba thứ vào đó mà không sửa một ký tự nào của template gốc — bằng inherit_id và xpath.

Nguyên tắc: report là template, template thì kế thừa được

Một ir.actions.report trỏ tới một report_name, và report_name là external id của một <template>. Template đó không có gì đặc biệt so với template của view — nó vẫn nằm trong bảng ir.ui.view, vẫn nhận cơ chế kế thừa. Nghĩa là để sửa, bạn khai một template mới với inherit_id trỏ về template gốc, rồi mô tả các thay đổi bằng xpath:

Ảnh chụp đoạn mã XML nền tối của file report_the_inherit.xml, khai template report_the_inherit với inherit_id trỏ tới quan_ca_phe report_the_document, gồm ba khối xpath: khối một position after thẻ h2 chèn một thẻ p phụ đề, khối hai position attributes trên bảng đầu tiên đổi thuộc tính class thêm table-striped, khối ba position before đoạn p có class text-muted và style margin-top 24px chèn một div chính sách

Hình 1: File report_the_inherit.xml. Không có nội dung nào của mẫu gốc được chép lại — chỉ có inherit_id và ba lệnh xpath mô tả thay đổi ở đâu, làm gì. Odoo áp các lệnh này lên cây XML của template gốc lúc dựng view, trước khi render.

Ba lệnh trong hình tương ứng ba kiểu thao tác hay dùng nhất:

  • position="after" — chèn nội dung ngay sau node khớp expr.
  • position="attributes" — đổi thuộc tính của node (ở đây thêm class table-striped vào bảng), không đụng nội dung bên trong.
  • position="before" — chèn nội dung ngay trước node khớp. Còn hai kiểu nữa: inside (chèn vào cuối, làm con của node) và replace (thay cả node — mạnh nhưng dễ vỡ nhất khi bản gốc đổi).

Thêm file mới vào module thì khai nó trong data của manifest rồi nâng cấp module. Không cần khai lại action, không cần sửa report_the.xml gốc — action vẫn trỏ tới report_the_document như cũ, chỉ là template đó giờ đã được bồi thêm.

# __manifest__.py
'data': [
    ...
    'views/report_the.xml',          # template gốc + action
    'views/report_the_inherit.xml',  # phần kế thừa, thêm vào
],
docker exec odoo19 odoo -d blog19 -u quan_ca_phe --stop-after-init
docker restart odoo19

Trước và sau: cùng một action, cùng một PDF, khác nội dung

Đây là điểm hay nhất: ir.actions.report không đổi gì cả. Vẫn report_name = quan_ca_phe.report_the_document. Người dùng vẫn bấm cùng một nút In. Nhưng nội dung PDF ra khác, vì template phía sau đã được kế thừa.

Bản gốc, trước khi thêm file inherit:

Ảnh chụp trang PDF khổ A5 ngang của thẻ thành viên VIP-0001 khi chưa kế thừa, gồm tiêu đề Thẻ thành viên VIP-0001, bảng thông tin khách hàng hạng thẻ điểm trạng thái ngày cấp, bảng lịch sử tích điểm ba dòng và tổng cộng, dòng cảm ơn quý khách ở cuối

Hình 2: PDF trước khi kế thừa — chính là mẫu report_the_document nguyên bản. Chưa có phụ đề, bảng thông tin chưa kẻ sọc, chưa có khối chính sách.

Sau khi thêm report_the_inherit.xml và nâng cấp module, cùng thẻ VIP-0001 in ra:

Ảnh chụp trang PDF khổ A5 ngang của cùng thẻ VIP-0001 sau khi kế thừa, ngay dưới tiêu đề có thêm dòng phụ đề in nghiêng Thẻ ưu đãi thành viên Hệ thống Cà Phê Việt, bảng thông tin giữ nguyên nội dung, và ngay trước dòng cảm ơn xuất hiện một khối nền kem viền trái nâu ghi Chính sách thẻ có giá trị khi còn hạn điểm tích luỹ không quy đổi thành tiền mặt

Hình 3: PDF sau khi kế thừa. Dưới tiêu đề có phụ đề in nghiêng (lệnh after //h2); khối "Chính sách" nền kem viền nâu nằm ngay trước dòng cảm ơn (lệnh before đoạn cảm ơn). Ba dòng lịch sử và tổng cộng giữ nguyên. Toàn bộ được thêm vào mà template gốc không mất một dòng nào — dung lượng PDF tăng từ 56.002 lên 61.105 byte, đo bằng chính _render_qweb_pdf.

Cả ba thay đổi có mặt đủ — kiểm bằng cách render HTML của report rồi tìm chuỗi:

report = env.ref('quan_ca_phe.action_report_the')
html = report._render_qweb_html('quan_ca_phe.report_the_document', [card_id])[0].decode()
'Thẻ ưu đãi thành viên' in html   # True  (phụ đề, từ position=after)
'table-striped'          in html   # True  (class mới, từ position=attributes)
'Chính sách'             in html   # True  (khối chính sách, từ position=before)

Một cái bẫy có thật: xpath chạy tuần tự trên cây đã bị sửa

Ở lần đầu tôi viết lệnh thứ ba là //div[hasclass('page')]/p với ý "chèn trước đoạn văn trong trang". Kết quả: khối "Chính sách" nhảy lên đầu trang, không phải trước dòng cảm ơn. Lý do rất Odoo:

Các lệnh xpath trong một khối kế thừa chạy lần lượt, trên cây XML đã bị các lệnh trước mutate. Lệnh 1 đã chèn thêm một thẻ <p> (phụ đề) làm con trực tiếp đầu tiên của div.page. Nên tới lệnh 3, //div[hasclass('page')]/p khớp đúng cái <p> phụ đề vừa chèn (node <p> đầu tiên), chứ không phải đoạn cảm ơn — và chèn khối chính sách trước nó, tức là lên đầu.

Cách sửa là cho expr bám vào một đặc điểm chỉ đoạn cảm ơn mới có, không dính node mới:

<!-- mong manh: khớp cả <p> vừa được chèn ở lệnh trước -->
<xpath expr="//div[hasclass('page')]/p" position="before"> ...

<!-- chắc chắn: đoạn cảm ơn có class text-muted VÀ style margin-top:24px -->
<xpath expr="//p[hasclass('text-muted') and contains(@style,'margin-top:24px')]"
       position="before"> ...

Bài học rút ra và nên nhớ khi viết kế thừa report: expr càng cụ thể càng bền. Đừng chọn node theo vị trí ("cái <p> đầu tiên") vì thứ tự thay đổi sau mỗi lệnh chèn và sau mỗi lần Odoo cập nhật bản gốc; chọn theo dấu hiệu nội dung riêng của đúng node bạn nhắm tới.

Áp lên mẫu in có sẵn của Odoo

Ở đây tôi kế thừa template của chính module mình cho dễ đo, nhưng cơ chế y hệt khi bạn nhắm vào mẫu in lõi của Odoo. Muốn thêm dòng "Cảm ơn đã mua hàng" vào cuối hoá đơn, bạn khai:

<template id="them_loi_cam_on" inherit_id="account.report_invoice_document">
    <xpath expr="//div[@id='total']" position="after">
        <p class="text-center mt8">Cảm ơn quý khách đã mua hàng ☕</p>
    </xpath>
</template>

Chỉ cần inherit_id đúng external id của template gốc (mở mẫu in trong Cài đặt → Kỹ thuật → Chế độ xem để tra), và module của bạn khai depends module chứa nó (account, sale...). Từ đó mọi hoá đơn in ra đều có thêm dòng của bạn, còn bản gốc của Odoo vẫn tự do cập nhật qua các phiên bản mà không xoá mất tuỳ chỉnh của bạn — vì tuỳ chỉnh nằm ở file riêng, không phải bản chép đè.

Ba ý mang về

  1. Mẫu in là QWeb template, nên kế thừa được như view: khai một <template> mới với inherit_id trỏ tới mẫu gốc, mô tả thay đổi bằng xpath — không chép, không sửa bản gốc. ir.actions.report giữ nguyên report_name.
  2. position quyết định kiểu thao tác: after/before chèn cạnh node, inside chèn vào trong, attributes đổi thuộc tính, replace thay cả node. Ưu tiên after/before/attributes vì bền hơn replace khi bản gốc đổi.
  3. xpath chạy tuần tự trên cây đã bị sửa — chọn node bằng dấu hiệu nội dung riêng (class + style), đừng chọn theo vị trí, kẻo lệnh sau khớp nhầm node mà lệnh trước vừa chèn.

Có mẫu in rồi và biết cách bồi thêm, phần sau ta chuẩn hoá phần khung: Phần sau nói về header/footer công ty trong report — logo, địa chỉ, chân trang lấy từ đâu ra và tuỳ biến web.external_layout thế nào.