If you’ve changed what a quotation PDF prints, you’ve touched QWeb. The same syntax also drives website pages and the whole web client, but two different engines render it, and mixing them up is where most of the confusion starts.
Two engines, one syntax
On the server, ir.qweb renders templates stored as views. Reports go through it, get wrapped in a layout and are turned into PDF by wkhtmltopdf. Website and portal pages use the same Python engine. In the browser, OWL compiles QWeb templates from your addon’s static XML files into components.
Most directives work on both sides, but not all. t-field, which formats a record field according to its type and can take t-options such as a widget, is Python only. t-name, t-inherit and t-inherit-mode are how static JavaScript templates are named and extended.
The directives that do the work
t-if, t-elif and t-else handle conditions. t-foreach with t-as loops, and gives you extra variables like line_index, line_first and line_last when you write t-as="line". t-set with t-value stores a value. t-call renders another template, and whatever you put inside the call is available there as 0. t-att-href computes an attribute value, and t-attf-class builds one from a format string.
For output, use t-out. It escapes HTML unless the value is already marked safe, such as an HTML field or a markupsafe.Markup. t-esc still works as an alias and the docs haven’t formally deprecated it, but they list it under deprecated output directives, so new code should say t-out. t-raw was deprecated in Odoo 15.
Adding a line to the quotation PDF
The sale order document, sale.report_saleorder_document, has a div with id="informations" holding the reference and date blocks in 17, 18 and 19. That id makes a steady anchor:
<template id="report_saleorder_document_delivery_window"
inherit_id="sale.report_saleorder_document">
<xpath expr="//div[@id='informations']" position="inside">
<div t-if="doc.delivery_window" class="col" name="informations_delivery_window">
<strong>Delivery window</strong>
<div t-field="doc.delivery_window"/>
</div>
</xpath>
</template>
doc is the variable the sale report sets inside its loop over docs. The positions and locators are the ones described under XPath view inheritance. The inner classes around that div changed between 17 and 18, so check the rendered PDF on each version you support.
Where QWeb work goes wrong
Copying the whole template. Duplicating report_saleorder_document to change one line means the next Odoo release’s fixes never reach your copy. Inherit and xpath instead. That’s how erpfly delivers report changes in the addons it generates, and our Odoo customization page walks through a fuller quotation example.
Custom report models that lose docs. If you define report.<module>.<report_name> with _get_report_values, Odoo stops providing docs, doc_ids and doc_model for you. Return them yourself, or any template that loops over docs breaks.
Expecting t-lang everywhere. Translating a report into the partner’s language works by putting t-lang on a t-call. On any other element it won’t translate anything.
Wrapping values in Markup to silence escaping. It tells QWeb the content is safe without checking. For anything a user typed, that’s an XSS hole.
If you’re coming from ERPNext, the closest counterpart is the Print Format, which uses Jinja rather than XML directives.