在報銷、費用單等業務裏,明細行常常會掛一個「發票附件」字段:
- 有時候是 圖片(JPG/PNG 等);
- 有時候是 PDF,而且可能還是 多頁 PDF。
在 QWeb 報表裏,我們通常希望:
- HTML 預覽裏能直接看到發票;
- 導出 PDF 報表時,每一頁 PDF 的每一頁發票能 清晰、單獨地 展示;
- 最好一套代碼同時支持「單張圖片」和「多頁 PDF」。
通過一個模型方法 _get_invoice_image_pages,把一份附件統一抽象成「若干張 PNG 頁面」,在 QWeb 裏循環渲染即可。
一、業務場景與目標
模型(以海外報銷明細行為例)大致如下:
# 發票附件:可能是圖片,也可能是 PDF(二進制 + 文件名)
invoice_image = fields.Binary("Invoice Image", readonly=True)
invoice_image_filename = fields.Char("Invoice Filename")
目標:
- 如果附件是 圖片:直接按圖片展示即可;
- 如果附件是 PDF(多頁):
- 拆成多張 PNG,每頁一張;
- 在 QWeb 報表中,每張 PNG 單獨一頁展示(page-break-after: always)。
二、核心思路:統一抽象為「圖片列表」
關鍵是寫一個模型方法:
> def _get_invoice_image_pages(self, dpi=150) -> list[bytes]
返回值是一個 list,每個元素都是 一張 PNG 的 base64(二進制):
- 如果是圖片:返回 [原圖片];
- 如果是 PDF:用 pdf2image 按頁轉換成多張 PNG,依次放進列表。
這樣,QWeb 模板完全不需要關心「圖片還是 PDF」——
只管循環 pages,一張圖片一頁地渲染就行了。
三、模型代碼:_get_invoice_image_pages
- 依賴導入
# -*- coding: utf-8 -*-
import base64
import io
import logging
from odoo import models, fields
_logger = logging.getLogger(__name__)
try:
from pdf2image import convert_from_bytes
PDF2IMAGE_AVAILABLE = True
except Exception as e:
PDF2IMAGE_AVAILABLE = False
_logger.warning("pdf2image 未安裝,PDF 轉圖片功能不可用: %s", e)
依賴説明:
- pdf2image:把 PDF 轉成 PIL Image 對象
- 安裝:pip install pdf2image
- 系統還需安裝 poppler-utils(Linux 下:yum install poppler-utils / apt-get install poppler-utils)
- 方法實現
class OverseasExpenseLine(models.Model):
_name = "overseas.expense.line"
_description = "Overseas Expense Reimbursement Line"
_order = "sequence, id desc"
# ... 上面已有的字段略 ...
def _get_invoice_image_pages(self, dpi=150):
"""
返回一個 list,每個元素是 base64 的 PNG 圖片(一頁一個元素)
- 如果是圖片:只返回 [原圖片]
- 如果是 PDF:按頁拆成多張 PNG
"""
self.ensure_one()
# 沒附件,直接空列表
if not self.invoice_image:
return []
# 1)判斷是否為 PDF(通過文件名後綴)
is_pdf = bool(self.invoice_image_filename and
self.invoice_image_filename.lower().endswith('.pdf'))
# 2)不是 PDF,當圖片用:直接返回一張
if not is_pdf:
return [self.invoice_image]
# 3)是 PDF,但沒裝 pdf2image
if not PDF2IMAGE_AVAILABLE:
_logger.warning(
"記錄 %s 為 PDF,但未安裝 pdf2image,無法拆分頁面。",
self.sequence or self.id
)
return []
# 4)是 PDF,嘗試拆頁
try:
pdf_bytes = base64.b64decode(self.invoice_image)
pages = convert_from_bytes(pdf_bytes, dpi=dpi)
if not pages:
return []
images_b64 = []
for page in pages:
buf = io.BytesIO()
page.save(buf, format='PNG')
buf.seek(0)
images_b64.append(base64.b64encode(buf.getvalue()))
return images_b64
except Exception as e:
_logger.error(
"記錄 %s PDF 拆頁失敗: %s",
self.sequence or self.id, str(e), exc_info=True
)
return []
四、QWeb 模板:明細行圖片 + PDF 的統一展示
假設你的報銷單報表模板裏有 o.expense_line(One2many 明細行)。
我們希望 每條明細的每一頁發票 佔用 一整頁 報表。
<t t-foreach="o.expense_line" t-as="line">
<!-- 取出當前明細行的所有「頁面圖片」 -->
<t t-set="pages" t-value="line._get_invoice_image_pages()"/>
<t t-if="pages">
<t t-foreach="pages" t-as="img">
<!-- 每張圖片單獨一頁 -->
<p style="page-break-after:always;"/>
<!-- 可選:顯示明細序號等信息 -->
<h4 t-field="line.sequence" class="pt-4"/>
<!-- 渲染圖片(img 是 base64) -->
<img t-attf-src="data:image/png;base64,{{ img.decode('utf-8') if isinstance(img, bytes) else img }}"
class="pt-3"
style="max-width: 100%; height: auto;"/>
</t>
</t>
</t>