Every t-foreach loop in QWeb hands back more than just the current item — a whole set of automatically-named variables derived from whatever name you gave t-as, covering position, size, and whether this is the first or last row. Most tutorials show two or three of them and stop there; two others that get copy-pasted just as often are quietly deprecated.
The short version: t-foreach="doc.order_line" t-as="line" gives you line itself plus line_index, line_size, line_first, line_last, and line_value automatically — no extra directive needed. line_odd, line_even, and line_parity exist too and still work, but are marked deprecated in Odoo's own documentation; prefer computing parity from line_index directly in new templates.
The full set
Per Odoo’s own QWeb reference, a loop written as t-foreach="collection" t-as="item" automatically defines these, substituting your own t-as name in place of item:
| Variable | What it holds |
|---|---|
item | The current value itself — identical to what you’d expect from the loop variable in any language. |
item_index | The current position, zero-based — the first row is 0, not 1. |
item_size | The total size of the collection being iterated, when it’s available. |
item_first | True on the first iteration only. |
item_last | True on the last iteration only — requires the collection’s size to be knowable, same as item_size. |
item_value | The current iteration value — identical to item for a list, but for a mapping/dict it’s the value where item itself would be the key. |
<tr t-foreach="doc.order_line" t-as="line">
<td>
<span t-if="line_first">First line: </span>
<span t-field="line.name"/>
<span t-if="line_last"> (last line)</span>
</td>
</tr>
The three that are deprecated: _odd, _even, _parity
item_odd and item_even (booleans) and item_parity (the string 'even' or 'odd') all exist and still function — but Odoo’s own reference documentation marks all three as deprecated. They’re common in older templates and community tutorials precisely because they were the original way to alternate row styling in a table, before it became clear that the same result is trivial to compute directly from item_index, which isn’t going anywhere:
<!-- Deprecated, still works: -->
<tr t-foreach="doc.order_line" t-as="line" t-attf-class="{{ 'bg-light' if line_even else '' }}">
<!-- Preferred: compute parity from item_index directly -->
<tr t-foreach="doc.order_line" t-as="line" t-attf-class="{{ 'bg-light' if line_index % 2 == 0 else '' }}">
Neither form is wrong today, and existing templates using _odd/_even/_parity don’t need urgent migration — but a new template has no reason to reach for the deprecated form when item_index % 2 does the identical thing with no dependency on a variable Odoo’s own docs flag as on its way out.
Watch out: item_last (and item_size) depend on the collection's size actually being knowable up front — true for the lists/recordsets a report normally iterates, but worth keeping in mind if a t-foreach is ever pointed at something more exotic (a generator-like expression) where the total size genuinely isn't available until iteration finishes.
One JavaScript-only variable that doesn’t exist in reports
A further variable, item_all (the entire collection being iterated, not just the current item), exists only in JavaScript-side QWeb — used in the web client’s own dynamic templates — and has no equivalent in the Python-side QWeb rendering that PDF/HTML reports actually use. Copy-pasting a snippet that relies on _all from a web-client template into a report template silently does nothing useful, since server-side rendering never defines it.
Nested loops: each level scopes its own variables
A t-foreach inside another t-foreach’s body — a sales order’s lines, each with its own sub-lines, say — doesn’t require anything special: each loop’s t-as name is independent, so an outer line and an inner sub_line each get their own _index/_first/_last without colliding:
<tr t-foreach="doc.order_line" t-as="line">
<td>
<span t-esc="line_index + 1"/>. <span t-field="line.name"/>
<ul>
<li t-foreach="line.sub_lines" t-as="sub_line">
<span t-esc="sub_line_index + 1"/>) <span t-field="sub_line.name"/>
<!-- line_index here still refers to the OUTER loop, untouched by the inner one -->
</li>
</ul>
</td>
</tr>
The outer loop’s variables (line_index, line_first, etc.) stay accessible and unchanged from inside the inner loop’s body — nothing about entering a nested t-foreach shadows or resets them, since each is scoped to its own distinct t-as name rather than to “the current loop” generically.
Frequently asked questions
Do these variables work the same way in a nested t-foreach?
Yes, each loop's own t-as name scopes its own set of variables independently — a nested loop with a different t-as value gets its own _index/_first/_last without colliding with the outer loop's.
Is item_index 0-based or 1-based?
0-based — the first iteration's item_index is 0, not 1. Displaying a human-facing row number needs item_index + 1 (or an expression like t-esc="line_index + 1").
What happens if I reference item_size on a collection whose size isn't available?
It's simply unavailable in that case, per the documentation's own wording ("if it is available") — an expression relying on it should be expected to degrade rather than assumed to always resolve, the same honest-placeholder philosophy this site's own preview engine follows for any unresolvable expression.
Can I still use _odd/_even in a brand new template, or should I avoid them entirely?
They still function correctly today — "deprecated" here means "don't rely on it going forward, and don't introduce new dependencies on it," not "broken." For a new template, computing parity from _index is simply the more future-proof habit to build.
Does t-as need to be a single word, or can it be an expression?
t-as takes the plain variable name the loop body will refer to (e.g. "line") — it's the name being bound, not an expression to evaluate, which is why every automatic variable above is built by suffixing that exact name.
Further reading
- QWeb Templates — official Odoo 18.0 developer documentation, the complete reference for
t-foreachand every other QWeb directive.
And on this site: the Report Builder’s repeater blocks compile to exactly this t-foreach/t-as shape — insert one from the Add panel to see the generated XML directly, or the QWeb Report Snippet Generator for the wider set of report-building snippets beyond loops.