fix(portal): /my/orders 500 — QWeb t-value is Python not Jinja, |length is bitwise OR

orders|length in t-value parses as orders | length, not as a Jinja
length filter. orders is a sale.order recordset; the `length`
identifier resolves to None; Python evaluates
recordset | None and raises TypeError. Use len(orders) instead.

Also documents the gotcha in CLAUDE.md (rule 19) so future templates
don't reach for Jinja-style filters in t-value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-18 00:13:33 -04:00
parent f8fc6be370
commit ab7ff3eea5
2 changed files with 2 additions and 1 deletions

View File

@@ -179,6 +179,7 @@ These modules have **source code in this repo** but are **intentionally NOT inst
- `fusion_plating_invoicing` blocks `action_post()` when `invoice_payment_term_id` is unset. Bypass: pass `'invoice_payment_term_id': self.env.ref('account.account_payment_term_immediate').id` in the create vals. - `fusion_plating_invoicing` blocks `action_post()` when `invoice_payment_term_id` is unset. Bypass: pass `'invoice_payment_term_id': self.env.ref('account.account_payment_term_immediate').id` in the create vals.
Both are test-data scaffolding; neither weakens assertions and neither must appear in production code paths. Both are test-data scaffolding; neither weakens assertions and neither must appear in production code paths.
18. **Portal list pages — no pagination, 500-record cap**: All FP portal list routes (quote requests, jobs, certifications, deliveries) load up to 500 records and rely on client-side JS filtering. Do NOT re-add `portal_pager` to these routes. The `fp_portal_list_controls` macro + `fp_portal_list_search.js` handle filtering, counting, and the sort dropdown. Hidden `<td class="d-none">` cells inside each row carry extra searchable text (part number, customer PO, contact) that isn't displayed but is matched by the JS. 18. **Portal list pages — no pagination, 500-record cap**: All FP portal list routes (quote requests, jobs, certifications, deliveries) load up to 500 records and rely on client-side JS filtering. Do NOT re-add `portal_pager` to these routes. The `fp_portal_list_controls` macro + `fp_portal_list_search.js` handle filtering, counting, and the sort dropdown. Hidden `<td class="d-none">` cells inside each row carry extra searchable text (part number, customer PO, contact) that isn't displayed but is matched by the JS.
19. **QWeb `t-value` is Python, not Jinja**: `t-value="orders|length"` does NOT call a filter — Python parses `|` as bitwise/recordset OR, so on a non-empty recordset it tries `recordset | length_var` and raises `TypeError: unsupported operand types in: sale.order(…) | None` (when `length` is undefined) or returns a merged recordset (when `length` happens to be another recordset). Use `len(orders)` or `bool(orders)` or `(orders and orders[0]) or False` — explicit Python. Same trap applies to `|default`, `|first`, `|join`, etc. — none of these Jinja filters exist in QWeb. Bit us 2026-05-18 on `fp_sale_order_portal.xml` injecting `result_total` into the list-controls macro.
## Naming ## Naming
- **New custom models** (post-2026-04): `fp.*` prefix (e.g. `fp.part.catalog`, `fp.certificate`) - **New custom models** (post-2026-04): `fp.*` prefix (e.g. `fp.part.catalog`, `fp.certificate`)

View File

@@ -39,7 +39,7 @@
<!-- Odoo's portal.portal_table emits a <table class="o_portal_my_doc_table"> <!-- Odoo's portal.portal_table emits a <table class="o_portal_my_doc_table">
so we don't need to add our own id; the JS just needs a stable selector. --> so we don't need to add our own id; the JS just needs a stable selector. -->
<t t-set="target" t-value="'.o_portal_my_doc_table tbody'"/> <t t-set="target" t-value="'.o_portal_my_doc_table tbody'"/>
<t t-set="result_total" t-value="orders|length if orders else 0"/> <t t-set="result_total" t-value="len(orders) if orders else 0"/>
<t t-set="clipped" t-value="False"/> <t t-set="clipped" t-value="False"/>
</t> </t>
</xpath> </xpath>