Developer Guide: The Complete Guide to QWeb & PDF Report Generation in Odoo

About This Guide

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.

What You Will Learn

  • Report Setup: How to organize your report files and automatically add a “Print” option to your app’s menu.
  • Designing the Layout: How to use basic structure and styling to create clean documents complete with your company logo, headers, footers, and page numbers.
  • Smart Content & Flexibility: How to display information conditionally (such as showing a default message if notes are missing) and reuse common design blocks across different reports.
  • Special Features: How to add scannable barcodes or QR codes, preview reports in your web browser, and prepare documents for multi-language translation.

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.

Understanding File Structure

The architecture of a PDF report consists of two main components:

  1. QWeb XML Template (report/*.xml): Defines the HTML/QWeb visual layout of the report.
  2. Report Action (report/*_reports.xml): Defines the ir.actions.report record that registers the report in Odoo and adds it to the Print menu of the model view.

Both files are stored inside a dedicated report/ folder at the top level of your module’s directory.

Complete Directory Layout

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,

}

Setting Up Report Test Data

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:

  • 3 Patients: 1 in Draft state, 1 in Confirmed state, and 1 in Done state.
  • Ensure at least one patient has filled-in Medical History Notes and another has empty notes so you can test display conditions.

Creating a Minimal QWeb Template

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>

Directives Explained Simply

  • web.html_container: Prepares the standard web HTML canvas and injects default stylesheets/fonts.
  • web.external_layout: Automatically adds your company logo, header, footer, page numbers, and address to the PDF.
  • t-foreach=”docs” t-as=”patient”: Loops through all selected patient records (equivalent to for patient in docs: in Python).
  • class=”table”: Utilizes built-in Twitter Bootstrap CSS formatting to keep design clean without manual CSS styling.

Registering the Report Action (ir.actions.report)

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>

Action Configuration Breakdown

  • model: Specifies the technical model name (hospital.patient).
  • report_type: Set to qweb-pdf to generate a downloadable PDF (or qweb-html for web previews).
  • report_name: Points directly to <your_module_name>.<template_id>.
  • binding_model_id: Automatically inserts this report into the Print dropdown menu on the patient form and list views (ref=”model_hospital_patient”).
  • print_report_name: Sets the default dynamic file name when saving the PDF locally.

Guided Exercises & Solutions

Exercise 1: Conditional Empty Notes Logic (t-if & t-else)

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 &amp; 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>

 

Modular Sub-Templates

Sub-templates help break long reports into smaller, readable pieces and allow you to reuse common layout chunks across multiple reports.

Step 1: Extract a Common Table into a Sub-Template

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>

 

Exercise 2: Create a Doctor Report for res.users

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>

QWeb Report Inheritance (xpath)

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>

Additional QWeb Report Features

  • your QWeb HTML tags. Clean text structures allow Odoo’s translation tools to extract and translate report text automatically.
  • Web Browser Previews: You can test your layouts directly inside your web browser without downloading PDF files by switching report_type from qweb-pdf to qweb-html.
  • Generating Barcodes & QR Codes: Standard barcode image generators are built directly into Odoo reports:
  • XML

<!– 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;”/>