This guide provides a straightforward overview of how to create printable and downloadable PDF documents (like patient discharge summaries, medical cards, and prescription slips) in Odoo. It breaks down the process of setting up report layouts, adding custom business details, and placing a “Print” button directly inside your app.
This Guide expands on QWeb’s primary use cases: creating downloadable and printable PDF reports. PDF reports allow Odoo modules to summarize and present business data such as patient discharge summaries, prescription cards, or doctor appointment lists in clean, formatted layouts complete with company headers and footers.
The architecture of a PDF report consists of two main components:
Both files are stored inside a dedicated report/ folder at the top level of your module’s directory.
Plaintext
custom_hospital_management/
│
├── __init__.py
├── __manifest__.py
│
├── models/
│ ├── __init__.py
│ └── patient.py
│
├── report/
│ ├── patient_templates.xml <– QWeb HTML report layouts & sub-templates
│ └── patient_reports.xml <– ir.actions.report definitions
│
├── security/
│ └── ir.model.access.csv
│
└── views/
├── patient_views.xml
└── menu_views.xml
Manifest Sequence Rule: Always add your report files to the data list in your __manifest__.py file. Remember that files listed in a manifest are loaded sequentially by Odoo!
Python
# File location: custom_hospital_management/__manifest__.py
{
‘name’: ‘Hospital Management’,
‘version’: ‘1.0.0’,
‘summary’: ‘Manage patient records, appointments, and medical histories’,
‘category’: ‘Healthcare’,
‘author’: ‘Your Company Name’,
‘license’: ‘LGPL-3’,
‘depends’: [‘base’],
‘data’: [
‘security/ir.model.access.csv’, # 1. Security loads first
‘views/patient_views.xml’, # 2. Views load second
‘views/menu_views.xml’, # 3. Menus load third
# 4. Report Actions & Templates
‘report/patient_reports.xml’, # Action definition
‘report/patient_templates.xml’, # QWeb layout templates
],
‘installable’: True,
‘application’: True,
}
Before writing report code, you need sample data in your database to test your visual layout incrementally. For our hospital module, make sure you have created at least:
A minimal viable report layout uses basic HTML, Bootstrap styling classes, and QWeb control statements (t- directives).
XML
<!– File location: custom_hospital_management/report/patient_templates.xml –>
<?xml version=”1.0″ encoding=”utf-8″?>
<odoo>
<template id=”report_patient_card_template”>
<t t-call=”web.html_container”>
<t t-foreach=”docs” t-as=”patient”>
<t t-call=”web.external_layout”>
<div class=”page”>
<h2>
<span t-field=”patient.name”/>
</h2>
<div class=”mb-3″>
<strong>Age: </strong>
<span t-field=”patient.age”/>
</div>
<table class=”table”>
<thead>
<tr>
<th>Gender</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td><span t-field=”patient.gender”/></td>
<td><span t-field=”patient.state”/></td>
</tr>
</tbody>
</table>
</div>
</t>
</t>
</t>
</template>
</odoo>
Now that the QWeb template exists, tell Odoo to register it as an official printable report and link it to the patient model’s Print menu.
XML
<!– File location: custom_hospital_management/report/patient_reports.xml –>
<?xml version=”1.0″ encoding=”utf-8″?>
<odoo>
<record id=”action_report_patient_card” model=”ir.actions.report”>
<field name=”name”>Patient Medical Card</field>
<field name=”model”>hospital.patient</field>
<field name=”report_type”>qweb-pdf</field>
<field name=”report_name”>custom_hospital_management.report_patient_card_template</field>
<field name=”report_file”>custom_hospital_management.report_patient_card_template</field>
<field name=”print_report_name”>’Patient Card – %s’ % (object.name)</field>
<field name=”binding_model_id” ref=”model_hospital_patient”/>
<field name=”binding_type”>report</field>
</record>
</odoo>
Add logic so that if a patient has no medical notes recorded, the report prints a helpful notice instead of leaving a blank space.
XML
<!– Solution: Replace the contents inside <div class=”page”> with this –>
<div class=”page”>
<h2>
<span t-field=”patient.name”/>
</h2>
<div>
<strong>Age: </strong><span t-field=”patient.age”/> |
<strong>Gender: </strong><span t-field=”patient.gender”/>
</div>
<!– Check if note field contains text –>
<t t-if=”patient.note”>
<div class=”mt-3″>
<strong>Medical History & Notes:</strong>
<p t-field=”patient.note”/>
</div>
</t>
<t t-else=””>
<div class=”mt-3 text-muted”>
<em>No medical history or notes recorded for this patient yet.</em>
</div>
</t>
</div>
Sub-templates help break long reports into smaller, readable pieces and allow you to reuse common layout chunks across multiple reports.
Create a standalone sub-template containing the patient metrics table:
XML
<!– File location: custom_hospital_management/report/patient_templates.xml –>
<odoo>
<!– Sub-template Definition –>
<template id=”patient_details_table”>
<table class=”table table-bordered text-center”>
<thead class=”table-light”>
<tr>
<th>Patient Name</th>
<th>Age</th>
<th>Gender</th>
<th>Status</th>
<th>Is Minor</th>
</tr>
</thead>
<tbody>
<tr>
<td><span t-field=”patient.name”/></td>
<td><span t-field=”patient.age”/></td>
<td><span t-field=”patient.gender”/></td>
<td><span t-field=”patient.state”/></td>
<td><span t-field=”patient.is_minor”/></td>
</tr>
</tbody>
</table>
</template>
<!– Main Template calling the sub-template –>
<template id=”report_patient_card_template”>
<t t-call=”web.html_container”>
<t t-foreach=”docs” t-as=”patient”>
<t t-call=”web.external_layout”>
<div class=”page”>
<h3>Patient Information Summary</h3>
<!– Call the external sub-template –>
<t t-call=”custom_hospital_management.patient_details_table”/>
</div>
</t>
</t>
</t>
</template>
</odoo>
Create a report linked to the User settings form (res.users) that prints out all patients assigned to a system user.
Because res.users is a core Odoo model, set binding_model_id to ref=”base.model_res_users”.
XML
<!– File location: custom_hospital_management/report/patient_reports.xml –>
<record id=”action_report_doctor_patients” model=”ir.actions.report”>
<field name=”name”>Assigned Patient Overview</field>
<field name=”model”>res.users</field>
<field name=”report_type”>qweb-pdf</field>
<field name=”report_name”>custom_hospital_management.report_doctor_patients_template</field>
<field name=”report_file”>custom_hospital_management.report_doctor_patients_template</field>
<field name=”binding_model_id” ref=”base.model_res_users”/>
<field name=”binding_type”>report</field>
</record>
Report templates inherit and modify parent templates using <xpath> expressions identical to Odoo view inheritance.
To extend a template from another module (or modify an existing report), set inherit_id to <module>.<parent_template_id>:
XML
<!– Extend the Patient Card Report –>
<odoo>
<template id=”report_patient_card_inherit” inherit_id=”custom_hospital_management.report_patient_card_template”>
<!– Target the heading tag and insert a verified notice below it –>
<xpath expr=”//h3″ position=”after”>
<div class=”alert alert-info” role=”alert”>
<span>Official Hospital Record – Confirmed by Medical Staff</span>
</div>
</xpath>
</template>
</odoo>
<!– Render a Code128 Barcode based on patient name –>
<img t-att-src=”‘/report/barcode/?barcode_type=%s&value=%s&width=%s&height=%s’ % (‘Code128’, patient.name, 600, 100)” style=”width:300px;height:50px;”/>