DOCX Template Variables
This document describes the variables available in Word templates rendered with docxtpl, and the first shared variables available to HTML/QWeb PDF templates.
The DOCX context intentionally uses simple dictionaries instead of direct Odoo records. Templates should use the variables below rather than internal field names such as partner_id.case_birthdate.
Text variables are escaped automatically when DOCX files are generated. Template authors should not add the Jinja |e filter to ordinary text variables. This keeps characters such as &, <, and > visible as text in generated documents.
Unknown Variables
During the POC phase, DOCX generation is tolerant of unknown variables. If a template uses a variable that is not available in the DOCX context, generation does not fail. The generated document displays a visible marker instead.
Example template code:
{{ hearing.date }}
Generated result:
[VARIABLE INCONNUE: hearing.date]
Odoo also displays a warning notification listing the unavailable variables. This behavior is intentional for model testing: it lets template authors find missing variables directly in the generated document.
Production note: this tolerant behavior should be reviewed before go-live. For final documents, a stricter mode may be preferable, for example blocking generation or requiring explicit confirmation when unknown variables are found.
Case, Capitals, and Word Fields
Variable names are case-sensitive. {{ document.generated_on_long }} works; {{ DOCUMENT.GENERATED_ON_LONG }} is an unknown variable. Type variables in lowercase, even inside a title written in capitals.
To display a value in capitals, keep the variable in lowercase and add the upper filter:
JUGEMENT DU {{ document.generated_on_long|upper }}
Generated result: JUGEMENT DU 5 OCTOBRE 2026. The lower filter does the opposite. Alternatively, apply the Word/OnlyOffice "All caps" formatting to the variable: it changes the display only, the variable name stays in lowercase.
Variables inside a field (content control). Odoo reads the field's content, not its placeholder text. A field still in its placeholder state can display one text and contain another — measured on 2026-10-05: the placeholder showed {{ document.generated_on_long }}, the content was {{ DOCUMENT.GENERATED_ON_LONG }}, and the generated document reported an unknown variable. When a variable in a field is reported unknown although it looks right, click into the field, delete its content, and retype the variable.
HTML/QWeb PDF Variables
HTML templates use the same marker shape as building blocks:
{{tag}}
{{group.tag}}
The HTML structure is authored in Odoo. Values inserted into markers are escaped before rendering, so a debtor name containing < or & remains text.
During the POC phase, unknown PDF variables are rendered as visible markers:
[UNKNOWN VARIABLE: tag]
Invoice PDF
The first PDF context is the court invoice prose body configured on an invoice type.
Simple aliases:
amount
amount_words
montant
montant_en_lettres
date
due_date
echeance
debtor_name
debiteur
case_reference
reference_dossier
authority
autorite
iban
Structured variables:
invoice.name
invoice.date
invoice.due_date
invoice.payment_reference
invoice.amount_total
invoice.amount_total_words
invoice.amount_residual
invoice.piece_code
invoice.type
invoice.type_name
case.name
case.description
case.sequence_number
case.year
case.language
case.instance_name
case.instance_code
debtor.name
debtor.address
debtor.street
debtor.street2
debtor.zip
debtor.city
debtor.country
company.name
company.address
company.street
company.street2
company.zip
company.city
company.country
company.phone
company.email
company.website
company.vat
company.iban
Top-Level Variables
case
document
participants
recipients
copies
parties
company
judges
clerks
secretaries
signers
signer
Case
case.name
case.description
case.sequence_number
case.year
case.language
case.state
case.state_label
case.instance_name
case.instance_code
case.judge_names
case.clerk_names
case.secretary_names
Example:
{{ case.name }}
{{ case.description }}
{{ case.sequence_number }}
{{ case.year }}
{{ case.instance_name }}
{{ case.instance_code }}
{{ case.judge_names }}
Document
document.name
document.generated_on
document.generated_on_long
document.state
document.origin_type
document.template_name
document.template_code
document.template_kind
document.template_kind_label
Example:
{{ document.name }}
{{ document.generated_on }}
Company
company.name
company.logo
company.address
company.street
company.street2
company.zip
company.city
company.country
company.phone
company.email
company.website
company.vat
company.logo inserts the case company's logo as an image in the DOCX.
company.address provides a simple multiline address composed from:
street
street2
zip city
country
Example:
{{ company.name }}
{{ company.address }}
{{ company.logo }}
This can be placed in the document body or in a Word/OnlyOffice header. For the current test implementation, the logo is rendered with a fixed width of 200 mm.
The company comes from the case (case.folder.company_id), not from the active company selected by the current user.
Raw address fields remain available when a template needs a custom layout:
{{ company.street }}
{{ company.street2 }}
{{ company.zip }} {{ company.city }}
{{ company.country }}
{{ company.phone }}
{{ company.email }}
{{ company.website }}
{{ company.vat }}
Participant Collections
participants contains every selected document participant.
recipients contains only participants with line_kind == "recipient".
copies contains only participants with line_kind == "cc".
parties contains only participants with line_kind == "party".
judges, clerks, and secretaries contain the case team users selected on the case.
Example:
{% for party in parties %}
{{ party.role }} : {{ party.name }}
{{ party.address }}
{% endfor %}
Case Team
Simple text variables are available when a template only needs the names:
case.judge_names
case.clerk_names
case.secretary_names
These values contain comma-separated names.
Example:
{{ case.judge_names }}
Structured lists are available when the template needs to choose one person or control the layout:
judges
clerks
secretaries
Each item contains:
name
email
phone
mobile
function
vat
lang
title
capacity_name
display_name_with_title
salutation_title
signature_title
inline_title
Example for a handwritten signature block using the first judge:
{% if judges %}{{ judges[0].name }}{% endif %}
Example listing all judges:
{% for judge in judges %}
{{ judge.name }}{% if not loop.last %}, {% endif %}
{% endfor %}
The case team comes from the case fields judge_user_ids, clerk_user_ids, and secretary_user_ids. The current implementation sorts these users by name for stable rendering. If a true principal signing magistrate is needed, it should be represented by a dedicated case field rather than relying on list order.
Titles, Functions, and Signer
Contacts can have a default title and a default function in Odoo. The title is a nominal prefix such as Dr, Prof., or Me. The function contains contextual wording such as salutation, signature wording, and inline wording.
Case related persons can override the contact defaults for a specific case.
The document stores a final ordered list of signers and signing functions. These values are selected in the document creation wizard and are kept on the generated document.
Document templates define the expected default signature lines. A template can define one signer, two signers, or more, for example judge then clerk. Each line can be prefilled from the case judges, clerks, secretaries, or left manual for the user to complete in the wizard.
User-facing configuration:
Contacts > Configuration > Titles
Contacts > Configuration > Functions
Typical setup:
Contact:
- Gender
- Title, for example Dr, Prof., Me
- Function, for example Judge, President, Lawyer, Doctor
Case related person:
- optional Title override for this case
- optional Function override for this case
Document template:
- Default Signers, for example Judge then Clerk
Document creation wizard:
- final ordered signer lines
- final signing function for each signer
The source UI labels are in English and translated through the module translation files. In French, Title is displayed as Titre, Function as Fonction, Signer as Signataire, and Signing Function as Fonction de signature.
Open Question: Contact Tags and Functions
The project currently uses standard Odoo contact tags to classify some contacts, for example lawyers. These tags should not be confused with document functions:
Contact tags:
- classification
- search
- filtering
- cross-module usage
Functions:
- document wording
- salutations
- signature labels
- DOCX variables
Current recommendation: keep contact tags for classification and keep functions for document wording. Do not abandon tags for contacts such as lawyers.
Possible future improvement:
Function -> optional related contact tag
Example:
Function: Lawyer
Related contact tag: Lawyer
This could later support a soft synchronization or an admin action, but automatic synchronization should be introduced carefully. A contact can have many tags but only one default function, and a case related person can override the function for a specific case without changing the contact's global tags.
Each participant-style item provides:
title
capacity_name
display_name_with_title
salutation_title
signature_title
inline_title
The signers list contains the final ordered signer lines selected on the generated document. Each item provides:
name
title
capacity_name
display_name_with_title
salutation_title
signature_title
inline_title
email
phone
mobile
function
vat
lang
address
street
street2
zip
city
country
profession
gender
note
sequence
signature_marker
signature_marker places the signer's electronic signature in the generated PDF: see "Electronic Signature Placement" below.
The signer object is a convenience alias for the first item in signers. It provides:
signer.name
signer.title
signer.capacity_name
signer.display_name_with_title
signer.salutation_title
signer.signature_title
signer.inline_title
signer.email
signer.phone
signer.mobile
signer.function
signer.vat
signer.lang
signer.address
signer.street
signer.street2
signer.zip
signer.city
signer.country
signer.profession
signer.gender
signer.note
signer.sequence
signer.signature_marker
Signature example:
{{ signer.display_name_with_title }}
{{ signer.signature_title }}
{{ signer.signature_marker }}
Recipient salutation example:
{% if recipients %}{{ recipients[0].salutation_title }}{% endif %}
Party mention example:
{{ party.inline_title }} {{ party.display_name_with_title }}
Priority for participant-style items:
1. title/capacity override on the case related person
2. default title/capacity on the contact
3. salutation fallback from the contact gender
Priority for signers:
1. signing capacity selected on the generated document signer line
2. default capacity on the signing contact
3. salutation fallback from the contact gender
Copyable Signature Examples
Standard signature block:
{{ signer.display_name_with_title }}
{{ signer.signature_title }}
{{ signer.signature_marker }}
Ordered signature block:
{% for signer in signers %}
{{ signer.signature_title }} :
{{ signer.name }}
{{ signer.signature_marker }}
{% endfor %}
Double signature block (side by side: put each signer in its own table cell, marker centred in the cell):
{% if signers|length > 0 %}
{{ signers[0].signature_title }} :
{{ signers[0].name }}
{{ signers[0].signature_marker }}
{% endif %}
{% if signers|length > 1 %}
{{ signers[1].signature_title }} :
{{ signers[1].name }}
{{ signers[1].signature_marker }}
{% endif %}
Signature block with fallback when no signer was selected:
{% if signer.name %}
{{ signer.display_name_with_title }}
{{ signer.signature_title }}
{% endif %}
Recipient salutation for the first recipient:
{% if recipients %}{{ recipients[0].salutation_title }}{% endif %}
Party with title and function in a phrase:
{% for party in parties %}
{{ party.inline_title }} {{ party.display_name_with_title }}
{% endfor %}
Electronic Signature Placement (signature_marker)
Where each signer's electronic signature appears in the generated PDF is set by a placement marker, one per signer.
- Place one marker per signer, where that signer's signature belongs (usually under their function and name):
{{ signers[0].signature_marker }}for the first,{{ signers[1].signature_marker }}for the second, and so on, or once inside a loop oversigners.{{ signer.signature_marker }}without an index is the first signer only. - What it renders. A code such as
[[SIG:JUDGE]]or[[SIG:CLERK]], derived from the code of the signer's signing function (Contacts > Configuration > Functions; the function form shows its marker). Two signers with the same function get[[SIG:JUDGE]]and[[SIG:JUDGE#2]]. - Never type the code by hand. A static
[[SIG:...]]is copied as is by a loop, and a duplicated or mistyped code stays readable in the PDF: only the first matching marker is covered. Use the variable. - At signature time, a stamp of 5 cm × 1.6 cm ("Demonstration signature", name, date and time) is centred on the signer's marker and covers it — never narrower than the marker, so long codes are covered too. The stamp is the visible appearance of the PAdES signature, added incrementally: the signatures already in the PDF stay valid.
- Layout. Leave about 1.6 cm free below each marker, or the stamp covers the text that follows. Side by side: one table cell per signer, marker centred in its cell — two stamps then fit on an A4 page without touching.
- Before signing, the marker is visible text in the finalized PDF. It can be set in a small or white font in the template to stay discreet: it is still found, by its text, at signature time. It also remains in the PDF text layer under the stamp (a copy-paste of the page reveals it).
signersis the list,signerone signer.{{ signers.signature_marker }}is an unknown variable (seen in a letter template on 2026-10-05): write{{ signer.signature_marker }}or{{ signers[0].signature_marker }}.- No marker for a signer: the stamp goes to the bottom right of the last page. That spot is the same for every signer without a marker, so their stamps would overlap — hence one marker per signer.
Background: SEALING_WORKFLOW.md §7b (its "overlay painted on the page" no longer holds since 2026-10-05) and CURRENT_STATE96.md §H.
Participant Fields
Each item in participants, recipients, copies, and parties provides:
name
role
role_code
role_category
line_kind
address
street
street2
zip
city
country
email
phone
vat
lang
profession
gender
birthdate
birthplace
origin_place
origin_country
nationalities
civil_status
filiation
father_name
mother_name
spouse_name
note
sequence
representative_names
representatives
title
capacity_name
display_name_with_title
salutation_title
signature_title
inline_title
Example:
{% for party in parties %}
{{ party.name }}, {{ party.profession }}
{{ party.address }}
{% if party.birthdate or party.birthplace %}
Born: {{ party.birthdate }} {{ party.birthplace }}
{% endif %}
{% if party.representative_names %}
Represented by: {{ party.representative_names }}
{% endif %}
{% endfor %}
Gender
gender can be:
male
female
neutral
Example:
{% if party.gender == "female" %}nee{% elif party.gender == "male" %}ne{% else %}ne(e){% endif %}
Representatives
representative_names is a simple comma-separated text value.
representatives is a list of representative contacts. Each representative contains the same contact-style fields as a participant, without nested representatives.
In Odoo, representatives are selected on a party line through the Represented By field. They should not be created as standalone lines in the case related persons list. By default, the representative selector prioritizes contacts identified as lawyers. The user can enable the case option Show All Contacts as Representatives when the representative is not a lawyer.
Current lawyer detection:
1. contact default Function with code lawyer
2. or contact tag Avocat
The function code is preferred for document semantics. The contact tag remains useful for classification and broader Odoo filtering.
Example:
{% for party in parties %}
{{ party.name }}
{% for representative in party.representatives %}
Representative: {{ representative.name }}
{{ representative.address }}
{% endfor %}
{% endfor %}
Address Formatting
address currently provides a simple multiline address composed from:
street
street2
zip city
country
Example:
{{ party.address }}
Raw address fields remain available when a template needs a custom layout:
{{ party.street }}
{{ party.street2 }}
{{ party.zip }} {{ party.city }}
{{ party.country }}
Future templates will likely need several address renderings depending on the document area:
- postal block for the envelope window
- compact inline address for copies or footer mentions
- address with or without country
- address combined with role, profession, or representation details
Possible future composed variables:
party.address_block
party.address_inline
party.recipient_block
party.copy_line
party.representation_line
For now, use address for the standard multiline block and the raw fields for specific layouts.
Recommended Pattern
Prefer parties for document parties and filter with role_code when the template needs a specific business role.
role_code comes from the configurable role code, for example D10. role_category comes from the configurable role category code, for example claimant, respondent, or representative. It is a broader technical category and should usually be treated as secondary in DOCX templates.
Example:
{% for party in parties %}
{% if party.role_code == "D10" %}
Claimant: {{ party.name }}
{% endif %}
{% endfor %}
The older flat variables such as case_name, document_name, claimants, and respondents are not part of the new DOCX context.
Copyable Examples
The examples below are meant to be copied directly into Word DOCX templates. Adapt the role codes such as D10 and D20 to the role configuration used in Odoo.
Case Header
{{ case.name }}
Instance : {{ case.instance_code }}
Annee : {{ case.year }}
Document Date
{{ document.generated_on }}
Use this when the template should display the date on which the document is generated. If a true "today" variable is later needed independently from the document generation date, it should be added explicitly to the DOCX context.
document.generated_on is the short date in the case language (05.10.2026). document.generated_on_long is the same date in words, in the case language: 5 octobre 2026, 5. Oktober 2026. French writes the first of the month 1er (1er novembre 2026).
{{ company.city }}, le {{ document.generated_on_long }}
In a title written in capitals: {{ document.generated_on_long|upper }} gives 5 OCTOBRE 2026 (see "Case, Capitals, and Word Fields").
All Parties
{% for party in parties %}
{{ party.role }} : {{ party.name }}
{{ party.address }}
{% endfor %}
Parties With Contact Details
{% for party in parties %}
{{ party.role }} : {{ party.name }}
{% if party.profession %}Profession : {{ party.profession }}{% endif %}
{% if party.birthdate %}Date de naissance : {{ party.birthdate }}{% endif %}
{% if party.civil_status %}Etat civil : {{ party.civil_status }}{% endif %}
{% if party.nationalities %}Nationalite : {{ party.nationalities }}{% endif %}
{{ party.address }}
{% endfor %}
Claimants By Role Code
Example with role code D10:
{% for party in parties %}
{% if party.role_code == "D10" %}
{{ party.name }}
{{ party.address }}
{% endif %}
{% endfor %}
Respondents By Role Code
Example with role code D20:
{% for party in parties %}
{% if party.role_code == "D20" %}
{{ party.name }}
{{ party.address }}
{% endif %}
{% endfor %}
Recipient Block
{% for recipient in recipients %}
{{ recipient.name }}
{{ recipient.address }}
{% endfor %}
Salutation for One, Two, or More Recipients
French templates. Paste the block in one piece, as plain text, on one line:
{% set filled = recipients | selectattr("salutation_title") | list %}{% set s = filled | map(attribute="salutation_title") | unique | list %}{% set plural = {"Madame": "Mesdames", "Monsieur": "Messieurs", "la": "les", "le": "les"} %}{% if filled|length > 2 or not s %}Madame, Monsieur{% elif s|length == 1 and filled|length == 2 and s[0] != "Madame, Monsieur" %}{% for w in s[0].split() %}{{ plural[w] if w in plural else (w if w[-1] in "sxz" else w ~ "s") }}{% if not loop.last %} {% endif %}{% endfor %}{% elif s|length == 1 %}{{ s[0] }}{% else %}{{ s[0] }}, {{ s[1] }}{% endif %},
Generated results (tested on 2026-10-05, in a letter):
one recipient (Madame la Juge) -> Madame la Juge,
two recipients (Maître, Madame) -> Maître, Madame,
two identical (Maître, Maître) -> Maîtres,
two identical (Madame la Juge) -> Mesdames les Juges,
two identical (Monsieur le Président) -> Messieurs les Présidents,
two without gender (Madame, Monsieur) -> Madame, Monsieur,
three recipients or more, or none -> Madame, Monsieur,
- Only recipients whose salutation is filled count; a recipient without one is ignored.
- Plural of two identical salutations:
MadamebecomesMesdames,MonsieurbecomesMessieurs,laandlebecomeles; any other word takes ansunless it already ends in s, x, or z. This covers every salutation of the current functions (Juge, Président·e, Greffier·ère, Secrétaire, Curateur·trice, Maître, Docteur). An irregular plural, or a salutation withl', must be added to theplurallist. - The block ends with a comma. Where the template already puts one, for example in
Veuillez agréer, … l'expression…, remove the final,of the block (just after{% endif %}), or the document showsMonsieur,,. - Paste, do not retype: if Word or OnlyOffice turns the straight quotes
"into typographic quotes, the template can no longer be read.
Copy Recipients
Copies :
{% for copy in copies %}
- {{ copy.name }}{% if copy.role %}, {{ copy.role }}{% endif %}
{% endfor %}
Representatives
Simple version:
{% for party in parties %}
{{ party.name }}
{% if party.representative_names %}
Represente par : {{ party.representative_names }}
{% endif %}
{% endfor %}
Detailed version:
{% for party in parties %}
{{ party.name }}
{% for representative in party.representatives %}
Represente par : {{ representative.name }}
{{ representative.address }}
{% endfor %}
{% endfor %}
Gender-Based Wording
{% if party.gender == "female" %}nee{% elif party.gender == "male" %}ne{% else %}ne(e){% endif %}
Longer example:
{% for party in parties %}
{{ party.name }},
{% if party.gender == "female" %}demanderesse{% elif party.gender == "male" %}demandeur{% else %}partie demanderesse{% endif %}
{% endfor %}
Address With Raw Fields
Use this when the template layout needs manual control, for example in a narrow address block:
{{ party.name }}
{{ party.street }}
{% if party.street2 %}{{ party.street2 }}{% endif %}
{{ party.zip }} {{ party.city }}
{{ party.country }}
Bulleted Lists Without Empty Final Bullet
For Word/OnlyOffice bullet lists, create one bullet line in the DOCX template, then paste the whole Jinja block on that bullet line.
When the list is filtered, first create a filtered list with set, then loop on that filtered list. This makes loop.last reliable and avoids an empty bullet at the end.
Claimants, role code D10:
{% set claimants = parties | selectattr("role_code", "equalto", "D10") | list %}{% for party in claimants %}{{ party.name }}{% if party.street %}, {{ party.street }}{% endif %}{% if party.street2 %}, {{ party.street2 }}{% endif %}{% if party.zip or party.city %}, {{ party.zip }} {{ party.city }}{% endif %}{% if not loop.last %}
{% endif %}{% endfor %}
Respondents, role code D11:
{% set respondents = parties | selectattr("role_code", "equalto", "D11") | list %}{% for party in respondents %}{{ party.name }}{% if party.street %}, {{ party.street }}{% endif %}{% if party.street2 %}, {{ party.street2 }}{% endif %}{% if party.zip or party.city %}, {{ party.zip }} {{ party.city }}{% endif %}{% if not loop.last %}
{% endif %}{% endfor %}
Recipients:
{% set recipient_list = recipients | list %}{% for recipient in recipient_list %}{{ recipient.name }}{% if recipient.street %}, {{ recipient.street }}{% endif %}{% if recipient.street2 %}, {{ recipient.street2 }}{% endif %}{% if recipient.zip or recipient.city %}, {{ recipient.zip }} {{ recipient.city }}{% endif %}{% if not loop.last %}
{% endif %}{% endfor %}
Copy recipients:
{% set copy_list = copies | list %}{% for copy in copy_list %}{{ copy.name }}{% if copy.street %}, {{ copy.street }}{% endif %}{% if copy.street2 %}, {{ copy.street2 }}{% endif %}{% if copy.zip or copy.city %}, {{ copy.zip }} {{ copy.city }}{% endif %}{% if not loop.last %}
{% endif %}{% endfor %}
Recipients with salutation titles on one line:
{% for recipient in recipients %}{{ recipient.salutation_title }}{% if not loop.last %}, {% endif %}{% endfor %}
Recipients with full address blocks:
{% for recipient in recipients %}
{{ recipient.display_name_with_title }}
{{ recipient.street }}
{% if recipient.street2 %}{{ recipient.street2 }}{% endif %}
{{ recipient.zip }} {{ recipient.city }}
{{ recipient.country }}
{% endfor %}
Copy recipients on one compact line:
{% for copy in copies %}{{ copy.display_name_with_title }}{% if copy.role %}, {{ copy.role }}{% endif %}{% if not loop.last %} ; {% endif %}{% endfor %}
Parties with detailed representatives:
{% for party in parties %}
{{ party.display_name_with_title }}
{% if party.role %}{{ party.role }}{% endif %}
{% for representative in party.representatives %}
Represented by:
{{ representative.display_name_with_title }}
{{ representative.address }}
{% endfor %}
{% endfor %}
Claimants followed by respondents in the same bullet list:
{% set claimants = parties | selectattr("role_code", "equalto", "D10") | list %}{% set respondents = parties | selectattr("role_code", "equalto", "D11") | list %}{% set party_list = claimants + respondents %}{% for party in party_list %}{{ party.name }}{% if party.street %}, {{ party.street }}{% endif %}{% if party.street2 %}, {{ party.street2 }}{% endif %}{% if party.zip or party.city %}, {{ party.zip }} {{ party.city }}{% endif %}{% if not loop.last %}
{% endif %}{% endfor %}
Avoid Undefined Loop Variables
A variable such as party.name only works inside a loop that defines party.
Correct:
{% for party in parties %}
{{ party.name }}
{% endfor %}
Incorrect:
{{ party.name }}
Manual Fields After Merge
Some DOCX templates need empty places that the user must complete manually after the document has been generated.
Preferred approach:
- use a Word plain text content control
- do not use simple bracket placeholders such as
[to complete]as the final solution, because they remain ordinary text and are slower to replace
Recommended setup in Microsoft Word:
- Enable the Developer tab if it is not visible.
- Place the cursor where the manual field should be completed.
- In the Developer tab, insert a Plain Text Content Control.
- Open the content control properties.
- Set a clear placeholder, for example
A completer. - Optionally set a title and tag, for example
motivation. - Choose a visible color for the content control.
- Enable the option to remove the content control when the contents are edited.
Notes:
- plain text content controls are best for short fields
- rich text content controls can be used for longer manual sections, but they may affect layout more visibly
- in OnlyOffice, content controls may only become clearly visible when the mouse hovers over them or when they are selected
- this is still preferable to plain text placeholders because the document keeps a real editable field after the Odoo merge
Future Feature: Template Custom Fields
Some document templates need values that are specific to one generated document and are not permanent fields on the case or contact, for example a hearing date, a reply deadline, a meeting room, or a free text instruction.
Recommended future design:
- configure custom fields on the document template
- show these fields in the document creation wizard
- validate required custom fields before generation
- store the final values on the generated document
- expose the values to DOCX templates through a
customdictionary
The implementation should not create dynamic Odoo model fields. Instead, it should use configuration and value models so the feature stays reversible and does not create migration-heavy schema changes.
Proposed template configuration fields:
name
code
field_type
required
default_value
help
sequence
Initial field types should stay conservative:
text
textarea
date
boolean
Possible DOCX usage:
{{ custom.hearing_date }}
{{ custom.hearing_location }}
{{ custom.reply_deadline }}
{% if custom.is_urgent %}
URGENT
{% endif %}
The custom field code should be restricted to lowercase letters, numbers, and underscores, for example:
hearing_date
reply_deadline
is_urgent
Avoid spaces, hyphens, dots, and labels intended only for display.
Migration From Classic Word Merge Fields
Classic Word merge fields such as MERGEFIELD case_name are not used by the current DOCX generation engine. Templates must use Jinja/docxtpl syntax.
Migration rules:
MERGEFIELD case_name -> {{ case.name }}
MERGEFIELD document_name -> {{ document.name }}
MERGEFIELD company_city -> {{ company.city }}
MERGEFIELD signer_name -> {{ signer.name }}
MERGEFIELD signer_title -> {{ signer.signature_title }}
MERGEFIELD recipient_name -> use a loop on recipients
MERGEFIELD copy_name -> use a loop on copies
MERGEFIELD party_name -> use a loop on parties
Single value example:
{{ case.name }}
Repeated value example:
{% for recipient in recipients %}
{{ recipient.display_name_with_title }}
{{ recipient.address }}
{% endfor %}
When converting templates, do not invent variable names. If a required business value does not exist in this document, use the closest available variable for testing and document the missing need as a future variable or custom field.
Test Protocol for DOCX Templates
Use this protocol after changing the DOCX context or after converting a Word model.
- Create or duplicate a document template in Odoo.
- Upload a DOCX file containing a small diagnostic block.
- Generate the document from a case with at least one recipient, one copy, one party, one representative, and one signer when possible.
- Open the generated DOCX.
- Confirm that normal variables are filled.
- Confirm that special characters such as
&,<, and>remain visible as text. - Add an intentionally unknown variable such as
{{ hearing.date }}. - Generate again and confirm that the document contains
[VARIABLE INCONNUE: hearing.date]. - Confirm that Odoo shows a warning notification listing the unknown variable.
- Regenerate the generated document and confirm the same behavior from the
Regeneratebutton.
Minimal diagnostic block:
CASE
case.name:
{{ case.name }}
case.description:
{{ case.description }}
COMPANY
company.name:
{{ company.name }}
company.city:
{{ company.city }}
SIGNERS
{% for signer in signers %}
Signer {{ loop.index }}:
{{ signer.display_name_with_title }}
{{ signer.signature_title }}
{% endfor %}
RECIPIENTS
{% for recipient in recipients %}
Recipient {{ loop.index }}:
{{ recipient.display_name_with_title }}
{{ recipient.salutation_title }}
{{ recipient.address }}
{% endfor %}
COPIES
{% for copy in copies %}
Copy {{ loop.index }}:
{{ copy.display_name_with_title }}
{{ copy.address }}
{% endfor %}
PARTIES
{% for party in parties %}
Party {{ loop.index }}:
{{ party.display_name_with_title }}
{{ party.role }}
{{ party.address }}
Representatives: {{ party.representative_names }}
{% endfor %}
UNKNOWN VARIABLE TEST
{{ hearing.date }}
Expected result:
UNKNOWN VARIABLE TEST
[VARIABLE INCONNUE: hearing.date]
DOCX Template Variables
This document describes the variables available in Word templates rendered with docxtpl, and the first shared variables available to HTML/QWeb PDF templates.
The DOCX context intentionally uses simple dictionaries instead of direct Odoo records. Templates should use the variables below rather than internal field names such as partner_id.case_birthdate.
Text variables are escaped automatically when DOCX files are generated. Template authors should not add the Jinja |e filter to ordinary text variables. This keeps characters such as &, <, and > visible as text in generated documents.
Unknown Variables
During the POC phase, DOCX generation is tolerant of unknown variables. If a template uses a variable that is not available in the DOCX context, generation does not fail. The generated document displays a visible marker instead.
Example template code:
{{ hearing.date }}Generated result:
[VARIABLE INCONNUE: hearing.date]Odoo also displays a warning notification listing the unavailable variables. This behavior is intentional for model testing: it lets template authors find missing variables directly in the generated document.
Production note: this tolerant behavior should be reviewed before go-live. For final documents, a stricter mode may be preferable, for example blocking generation or requiring explicit confirmation when unknown variables are found.
Case, Capitals, and Word Fields
Variable names are case-sensitive. {{ document.generated_on_long }} works; {{ DOCUMENT.GENERATED_ON_LONG }} is an unknown variable. Type variables in lowercase, even inside a title written in capitals.
To display a value in capitals, keep the variable in lowercase and add the upper filter:
JUGEMENT DU {{ document.generated_on_long|upper }}Generated result: JUGEMENT DU 5 OCTOBRE 2026. The lower filter does the opposite. Alternatively, apply the Word/OnlyOffice "All caps" formatting to the variable: it changes the display only, the variable name stays in lowercase.
Variables inside a field (content control). Odoo reads the field's content, not its placeholder text. A field still in its placeholder state can display one text and contain another — measured on 2026-10-05: the placeholder showed {{ document.generated_on_long }}, the content was {{ DOCUMENT.GENERATED_ON_LONG }}, and the generated document reported an unknown variable. When a variable in a field is reported unknown although it looks right, click into the field, delete its content, and retype the variable.
HTML/QWeb PDF Variables
HTML templates use the same marker shape as building blocks:
{{tag}}
{{group.tag}}The HTML structure is authored in Odoo. Values inserted into markers are escaped before rendering, so a debtor name containing < or & remains text.
During the POC phase, unknown PDF variables are rendered as visible markers:
[UNKNOWN VARIABLE: tag]Invoice PDF
The first PDF context is the court invoice prose body configured on an invoice type.
Simple aliases:
amount
amount_words
montant
montant_en_lettres
date
due_date
echeance
debtor_name
debiteur
case_reference
reference_dossier
authority
autorite
ibanStructured variables:
invoice.name
invoice.date
invoice.due_date
invoice.payment_reference
invoice.amount_total
invoice.amount_total_words
invoice.amount_residual
invoice.piece_code
invoice.type
invoice.type_name
case.name
case.description
case.sequence_number
case.year
case.language
case.instance_name
case.instance_code
debtor.name
debtor.address
debtor.street
debtor.street2
debtor.zip
debtor.city
debtor.country
company.name
company.address
company.street
company.street2
company.zip
company.city
company.country
company.phone
company.email
company.website
company.vat
company.ibanTop-Level Variables
case
document
participants
recipients
copies
parties
company
judges
clerks
secretaries
signers
signerCase
case.name
case.description
case.sequence_number
case.year
case.language
case.state
case.state_label
case.instance_name
case.instance_code
case.judge_names
case.clerk_names
case.secretary_namesExample:
{{ case.name }}
{{ case.description }}
{{ case.sequence_number }}
{{ case.year }}
{{ case.instance_name }}
{{ case.instance_code }}
{{ case.judge_names }}Document
document.name
document.generated_on
document.generated_on_long
document.state
document.origin_type
document.template_name
document.template_code
document.template_kind
document.template_kind_labelExample:
{{ document.name }}
{{ document.generated_on }}Company
company.name
company.logo
company.address
company.street
company.street2
company.zip
company.city
company.country
company.phone
company.email
company.website
company.vatcompany.logo inserts the case company's logo as an image in the DOCX.
company.address provides a simple multiline address composed from:
street
street2
zip city
countryExample:
{{ company.name }}
{{ company.address }}
{{ company.logo }}This can be placed in the document body or in a Word/OnlyOffice header. For the current test implementation, the logo is rendered with a fixed width of 200 mm.
The company comes from the case (case.folder.company_id), not from the active company selected by the current user.
Raw address fields remain available when a template needs a custom layout:
{{ company.street }}
{{ company.street2 }}
{{ company.zip }} {{ company.city }}
{{ company.country }}
{{ company.phone }}
{{ company.email }}
{{ company.website }}
{{ company.vat }}Participant Collections
participants contains every selected document participant.
recipients contains only participants with line_kind == "recipient".
copies contains only participants with line_kind == "cc".
parties contains only participants with line_kind == "party".
judges, clerks, and secretaries contain the case team users selected on the case.
Example:
{% for party in parties %}
{{ party.role }} : {{ party.name }}
{{ party.address }}
{% endfor %}Case Team
Simple text variables are available when a template only needs the names:
case.judge_names
case.clerk_names
case.secretary_namesThese values contain comma-separated names.
Example:
{{ case.judge_names }}Structured lists are available when the template needs to choose one person or control the layout:
judges
clerks
secretariesEach item contains:
name
email
phone
mobile
function
vat
lang
title
capacity_name
display_name_with_title
salutation_title
signature_title
inline_titleExample for a handwritten signature block using the first judge:
{% if judges %}{{ judges[0].name }}{% endif %}Example listing all judges:
{% for judge in judges %}
{{ judge.name }}{% if not loop.last %}, {% endif %}
{% endfor %}The case team comes from the case fields judge_user_ids, clerk_user_ids, and secretary_user_ids. The current implementation sorts these users by name for stable rendering. If a true principal signing magistrate is needed, it should be represented by a dedicated case field rather than relying on list order.
Titles, Functions, and Signer
Contacts can have a default title and a default function in Odoo. The title is a nominal prefix such as Dr, Prof., or Me. The function contains contextual wording such as salutation, signature wording, and inline wording.
Case related persons can override the contact defaults for a specific case.
The document stores a final ordered list of signers and signing functions. These values are selected in the document creation wizard and are kept on the generated document.
Document templates define the expected default signature lines. A template can define one signer, two signers, or more, for example judge then clerk. Each line can be prefilled from the case judges, clerks, secretaries, or left manual for the user to complete in the wizard.
User-facing configuration:
Contacts > Configuration > Titles
Contacts > Configuration > FunctionsTypical setup:
Contact:
- Gender
- Title, for example Dr, Prof., Me
- Function, for example Judge, President, Lawyer, Doctor
Case related person:
- optional Title override for this case
- optional Function override for this case
Document template:
- Default Signers, for example Judge then Clerk
Document creation wizard:
- final ordered signer lines
- final signing function for each signerThe source UI labels are in English and translated through the module translation files. In French, Title is displayed as Titre, Function as Fonction, Signer as Signataire, and Signing Function as Fonction de signature.
Open Question: Contact Tags and Functions
The project currently uses standard Odoo contact tags to classify some contacts, for example lawyers. These tags should not be confused with document functions:
Contact tags:
- classification
- search
- filtering
- cross-module usage
Functions:
- document wording
- salutations
- signature labels
- DOCX variablesCurrent recommendation: keep contact tags for classification and keep functions for document wording. Do not abandon tags for contacts such as lawyers.
Possible future improvement:
Function -> optional related contact tagExample:
Function: Lawyer
Related contact tag: LawyerThis could later support a soft synchronization or an admin action, but automatic synchronization should be introduced carefully. A contact can have many tags but only one default function, and a case related person can override the function for a specific case without changing the contact's global tags.
Each participant-style item provides:
title
capacity_name
display_name_with_title
salutation_title
signature_title
inline_titleThe signers list contains the final ordered signer lines selected on the generated document. Each item provides:
name
title
capacity_name
display_name_with_title
salutation_title
signature_title
inline_title
email
phone
mobile
function
vat
lang
address
street
street2
zip
city
country
profession
gender
note
sequence
signature_markersignature_marker places the signer's electronic signature in the generated PDF: see "Electronic Signature Placement" below.
The signer object is a convenience alias for the first item in signers. It provides:
signer.name
signer.title
signer.capacity_name
signer.display_name_with_title
signer.salutation_title
signer.signature_title
signer.inline_title
signer.email
signer.phone
signer.mobile
signer.function
signer.vat
signer.lang
signer.address
signer.street
signer.street2
signer.zip
signer.city
signer.country
signer.profession
signer.gender
signer.note
signer.sequence
signer.signature_markerSignature example:
{{ signer.display_name_with_title }}
{{ signer.signature_title }}
{{ signer.signature_marker }}Recipient salutation example:
{% if recipients %}{{ recipients[0].salutation_title }}{% endif %}Party mention example:
{{ party.inline_title }} {{ party.display_name_with_title }}Priority for participant-style items:
1. title/capacity override on the case related person
2. default title/capacity on the contact
3. salutation fallback from the contact genderPriority for signers:
1. signing capacity selected on the generated document signer line
2. default capacity on the signing contact
3. salutation fallback from the contact genderCopyable Signature Examples
Standard signature block:
{{ signer.display_name_with_title }}
{{ signer.signature_title }}
{{ signer.signature_marker }}Ordered signature block:
{% for signer in signers %}
{{ signer.signature_title }} :
{{ signer.name }}
{{ signer.signature_marker }}
{% endfor %}Double signature block (side by side: put each signer in its own table cell, marker centred in the cell):
{% if signers|length > 0 %}
{{ signers[0].signature_title }} :
{{ signers[0].name }}
{{ signers[0].signature_marker }}
{% endif %}
{% if signers|length > 1 %}
{{ signers[1].signature_title }} :
{{ signers[1].name }}
{{ signers[1].signature_marker }}
{% endif %}Signature block with fallback when no signer was selected:
{% if signer.name %}
{{ signer.display_name_with_title }}
{{ signer.signature_title }}
{% endif %}Recipient salutation for the first recipient:
{% if recipients %}{{ recipients[0].salutation_title }}{% endif %}Party with title and function in a phrase:
{% for party in parties %}
{{ party.inline_title }} {{ party.display_name_with_title }}
{% endfor %}Electronic Signature Placement (signature_marker)
Where each signer's electronic signature appears in the generated PDF is set by a placement marker, one per signer.
- Place one marker per signer, where that signer's signature belongs (usually under their function and name):
{{ signers[0].signature_marker }}for the first,{{ signers[1].signature_marker }}for the second, and so on, or once inside a loop oversigners.{{ signer.signature_marker }}without an index is the first signer only. - What it renders. A code such as
[[SIG:JUDGE]]or[[SIG:CLERK]], derived from the code of the signer's signing function (Contacts > Configuration > Functions; the function form shows its marker). Two signers with the same function get[[SIG:JUDGE]]and[[SIG:JUDGE#2]]. - Never type the code by hand. A static
[[SIG:...]]is copied as is by a loop, and a duplicated or mistyped code stays readable in the PDF: only the first matching marker is covered. Use the variable. - At signature time, a stamp of 5 cm × 1.6 cm ("Demonstration signature", name, date and time) is centred on the signer's marker and covers it — never narrower than the marker, so long codes are covered too. The stamp is the visible appearance of the PAdES signature, added incrementally: the signatures already in the PDF stay valid.
- Layout. Leave about 1.6 cm free below each marker, or the stamp covers the text that follows. Side by side: one table cell per signer, marker centred in its cell — two stamps then fit on an A4 page without touching.
- Before signing, the marker is visible text in the finalized PDF. It can be set in a small or white font in the template to stay discreet: it is still found, by its text, at signature time. It also remains in the PDF text layer under the stamp (a copy-paste of the page reveals it).
signersis the list,signerone signer.{{ signers.signature_marker }}is an unknown variable (seen in a letter template on 2026-10-05): write{{ signer.signature_marker }}or{{ signers[0].signature_marker }}.- No marker for a signer: the stamp goes to the bottom right of the last page. That spot is the same for every signer without a marker, so their stamps would overlap — hence one marker per signer.
Background: SEALING_WORKFLOW.md §7b (its "overlay painted on the page" no longer holds since 2026-10-05) and CURRENT_STATE96.md §H.
Participant Fields
Each item in participants, recipients, copies, and parties provides:
name
role
role_code
role_category
line_kind
address
street
street2
zip
city
country
email
phone
vat
lang
profession
gender
birthdate
birthplace
origin_place
origin_country
nationalities
civil_status
filiation
father_name
mother_name
spouse_name
note
sequence
representative_names
representatives
title
capacity_name
display_name_with_title
salutation_title
signature_title
inline_titleExample:
{% for party in parties %}
{{ party.name }}, {{ party.profession }}
{{ party.address }}
{% if party.birthdate or party.birthplace %}
Born: {{ party.birthdate }} {{ party.birthplace }}
{% endif %}
{% if party.representative_names %}
Represented by: {{ party.representative_names }}
{% endif %}
{% endfor %}Gender
gender can be:
male
female
neutralExample:
{% if party.gender == "female" %}nee{% elif party.gender == "male" %}ne{% else %}ne(e){% endif %}Representatives
representative_names is a simple comma-separated text value.
representatives is a list of representative contacts. Each representative contains the same contact-style fields as a participant, without nested representatives.
In Odoo, representatives are selected on a party line through the Represented By field. They should not be created as standalone lines in the case related persons list. By default, the representative selector prioritizes contacts identified as lawyers. The user can enable the case option Show All Contacts as Representatives when the representative is not a lawyer.
Current lawyer detection:
1. contact default Function with code lawyer
2. or contact tag AvocatThe function code is preferred for document semantics. The contact tag remains useful for classification and broader Odoo filtering.
Example:
{% for party in parties %}
{{ party.name }}
{% for representative in party.representatives %}
Representative: {{ representative.name }}
{{ representative.address }}
{% endfor %}
{% endfor %}Address Formatting
address currently provides a simple multiline address composed from:
street
street2
zip city
countryExample:
{{ party.address }}Raw address fields remain available when a template needs a custom layout:
{{ party.street }}
{{ party.street2 }}
{{ party.zip }} {{ party.city }}
{{ party.country }}Future templates will likely need several address renderings depending on the document area:
- postal block for the envelope window
- compact inline address for copies or footer mentions
- address with or without country
- address combined with role, profession, or representation details
Possible future composed variables:
party.address_block
party.address_inline
party.recipient_block
party.copy_line
party.representation_lineFor now, use address for the standard multiline block and the raw fields for specific layouts.
Recommended Pattern
Prefer parties for document parties and filter with role_code when the template needs a specific business role.
role_code comes from the configurable role code, for example D10. role_category comes from the configurable role category code, for example claimant, respondent, or representative. It is a broader technical category and should usually be treated as secondary in DOCX templates.
Example:
{% for party in parties %}
{% if party.role_code == "D10" %}
Claimant: {{ party.name }}
{% endif %}
{% endfor %}The older flat variables such as case_name, document_name, claimants, and respondents are not part of the new DOCX context.
Copyable Examples
The examples below are meant to be copied directly into Word DOCX templates. Adapt the role codes such as D10 and D20 to the role configuration used in Odoo.
Case Header
{{ case.name }}
Instance : {{ case.instance_code }}
Annee : {{ case.year }}Document Date
{{ document.generated_on }}Use this when the template should display the date on which the document is generated. If a true "today" variable is later needed independently from the document generation date, it should be added explicitly to the DOCX context.
document.generated_on is the short date in the case language (05.10.2026). document.generated_on_long is the same date in words, in the case language: 5 octobre 2026, 5. Oktober 2026. French writes the first of the month 1er (1er novembre 2026).
{{ company.city }}, le {{ document.generated_on_long }}In a title written in capitals: {{ document.generated_on_long|upper }} gives 5 OCTOBRE 2026 (see "Case, Capitals, and Word Fields").
All Parties
{% for party in parties %}
{{ party.role }} : {{ party.name }}
{{ party.address }}
{% endfor %}Parties With Contact Details
{% for party in parties %}
{{ party.role }} : {{ party.name }}
{% if party.profession %}Profession : {{ party.profession }}{% endif %}
{% if party.birthdate %}Date de naissance : {{ party.birthdate }}{% endif %}
{% if party.civil_status %}Etat civil : {{ party.civil_status }}{% endif %}
{% if party.nationalities %}Nationalite : {{ party.nationalities }}{% endif %}
{{ party.address }}
{% endfor %}Claimants By Role Code
Example with role code D10:
{% for party in parties %}
{% if party.role_code == "D10" %}
{{ party.name }}
{{ party.address }}
{% endif %}
{% endfor %}Respondents By Role Code
Example with role code D20:
{% for party in parties %}
{% if party.role_code == "D20" %}
{{ party.name }}
{{ party.address }}
{% endif %}
{% endfor %}Recipient Block
{% for recipient in recipients %}
{{ recipient.name }}
{{ recipient.address }}
{% endfor %}Salutation for One, Two, or More Recipients
French templates. Paste the block in one piece, as plain text, on one line:
{% set filled = recipients | selectattr("salutation_title") | list %}{% set s = filled | map(attribute="salutation_title") | unique | list %}{% set plural = {"Madame": "Mesdames", "Monsieur": "Messieurs", "la": "les", "le": "les"} %}{% if filled|length > 2 or not s %}Madame, Monsieur{% elif s|length == 1 and filled|length == 2 and s[0] != "Madame, Monsieur" %}{% for w in s[0].split() %}{{ plural[w] if w in plural else (w if w[-1] in "sxz" else w ~ "s") }}{% if not loop.last %} {% endif %}{% endfor %}{% elif s|length == 1 %}{{ s[0] }}{% else %}{{ s[0] }}, {{ s[1] }}{% endif %},Generated results (tested on 2026-10-05, in a letter):
one recipient (Madame la Juge) -> Madame la Juge,
two recipients (Maître, Madame) -> Maître, Madame,
two identical (Maître, Maître) -> Maîtres,
two identical (Madame la Juge) -> Mesdames les Juges,
two identical (Monsieur le Président) -> Messieurs les Présidents,
two without gender (Madame, Monsieur) -> Madame, Monsieur,
three recipients or more, or none -> Madame, Monsieur,- Only recipients whose salutation is filled count; a recipient without one is ignored.
- Plural of two identical salutations:
MadamebecomesMesdames,MonsieurbecomesMessieurs,laandlebecomeles; any other word takes ansunless it already ends in s, x, or z. This covers every salutation of the current functions (Juge, Président·e, Greffier·ère, Secrétaire, Curateur·trice, Maître, Docteur). An irregular plural, or a salutation withl', must be added to theplurallist. - The block ends with a comma. Where the template already puts one, for example in
Veuillez agréer, … l'expression…, remove the final,of the block (just after{% endif %}), or the document showsMonsieur,,. - Paste, do not retype: if Word or OnlyOffice turns the straight quotes
"into typographic quotes, the template can no longer be read.
Copy Recipients
Copies :
{% for copy in copies %}
- {{ copy.name }}{% if copy.role %}, {{ copy.role }}{% endif %}
{% endfor %}Representatives
Simple version:
{% for party in parties %}
{{ party.name }}
{% if party.representative_names %}
Represente par : {{ party.representative_names }}
{% endif %}
{% endfor %}Detailed version:
{% for party in parties %}
{{ party.name }}
{% for representative in party.representatives %}
Represente par : {{ representative.name }}
{{ representative.address }}
{% endfor %}
{% endfor %}Gender-Based Wording
{% if party.gender == "female" %}nee{% elif party.gender == "male" %}ne{% else %}ne(e){% endif %}Longer example:
{% for party in parties %}
{{ party.name }},
{% if party.gender == "female" %}demanderesse{% elif party.gender == "male" %}demandeur{% else %}partie demanderesse{% endif %}
{% endfor %}Address With Raw Fields
Use this when the template layout needs manual control, for example in a narrow address block:
{{ party.name }}
{{ party.street }}
{% if party.street2 %}{{ party.street2 }}{% endif %}
{{ party.zip }} {{ party.city }}
{{ party.country }}Bulleted Lists Without Empty Final Bullet
For Word/OnlyOffice bullet lists, create one bullet line in the DOCX template, then paste the whole Jinja block on that bullet line.
When the list is filtered, first create a filtered list with set, then loop on that filtered list. This makes loop.last reliable and avoids an empty bullet at the end.
Claimants, role code D10:
{% set claimants = parties | selectattr("role_code", "equalto", "D10") | list %}{% for party in claimants %}{{ party.name }}{% if party.street %}, {{ party.street }}{% endif %}{% if party.street2 %}, {{ party.street2 }}{% endif %}{% if party.zip or party.city %}, {{ party.zip }} {{ party.city }}{% endif %}{% if not loop.last %}
{% endif %}{% endfor %}Respondents, role code D11:
{% set respondents = parties | selectattr("role_code", "equalto", "D11") | list %}{% for party in respondents %}{{ party.name }}{% if party.street %}, {{ party.street }}{% endif %}{% if party.street2 %}, {{ party.street2 }}{% endif %}{% if party.zip or party.city %}, {{ party.zip }} {{ party.city }}{% endif %}{% if not loop.last %}
{% endif %}{% endfor %}Recipients:
{% set recipient_list = recipients | list %}{% for recipient in recipient_list %}{{ recipient.name }}{% if recipient.street %}, {{ recipient.street }}{% endif %}{% if recipient.street2 %}, {{ recipient.street2 }}{% endif %}{% if recipient.zip or recipient.city %}, {{ recipient.zip }} {{ recipient.city }}{% endif %}{% if not loop.last %}
{% endif %}{% endfor %}Copy recipients:
{% set copy_list = copies | list %}{% for copy in copy_list %}{{ copy.name }}{% if copy.street %}, {{ copy.street }}{% endif %}{% if copy.street2 %}, {{ copy.street2 }}{% endif %}{% if copy.zip or copy.city %}, {{ copy.zip }} {{ copy.city }}{% endif %}{% if not loop.last %}
{% endif %}{% endfor %}Recipients with salutation titles on one line:
{% for recipient in recipients %}{{ recipient.salutation_title }}{% if not loop.last %}, {% endif %}{% endfor %}Recipients with full address blocks:
{% for recipient in recipients %}
{{ recipient.display_name_with_title }}
{{ recipient.street }}
{% if recipient.street2 %}{{ recipient.street2 }}{% endif %}
{{ recipient.zip }} {{ recipient.city }}
{{ recipient.country }}
{% endfor %}Copy recipients on one compact line:
{% for copy in copies %}{{ copy.display_name_with_title }}{% if copy.role %}, {{ copy.role }}{% endif %}{% if not loop.last %} ; {% endif %}{% endfor %}Parties with detailed representatives:
{% for party in parties %}
{{ party.display_name_with_title }}
{% if party.role %}{{ party.role }}{% endif %}
{% for representative in party.representatives %}
Represented by:
{{ representative.display_name_with_title }}
{{ representative.address }}
{% endfor %}
{% endfor %}Claimants followed by respondents in the same bullet list:
{% set claimants = parties | selectattr("role_code", "equalto", "D10") | list %}{% set respondents = parties | selectattr("role_code", "equalto", "D11") | list %}{% set party_list = claimants + respondents %}{% for party in party_list %}{{ party.name }}{% if party.street %}, {{ party.street }}{% endif %}{% if party.street2 %}, {{ party.street2 }}{% endif %}{% if party.zip or party.city %}, {{ party.zip }} {{ party.city }}{% endif %}{% if not loop.last %}
{% endif %}{% endfor %}Avoid Undefined Loop Variables
A variable such as party.name only works inside a loop that defines party.
Correct:
{% for party in parties %}
{{ party.name }}
{% endfor %}Incorrect:
{{ party.name }}Manual Fields After Merge
Some DOCX templates need empty places that the user must complete manually after the document has been generated.
Preferred approach:
- use a Word plain text content control
- do not use simple bracket placeholders such as
[to complete]as the final solution, because they remain ordinary text and are slower to replace
Recommended setup in Microsoft Word:
- Enable the Developer tab if it is not visible.
- Place the cursor where the manual field should be completed.
- In the Developer tab, insert a Plain Text Content Control.
- Open the content control properties.
- Set a clear placeholder, for example
A completer. - Optionally set a title and tag, for example
motivation. - Choose a visible color for the content control.
- Enable the option to remove the content control when the contents are edited.
Notes:
- plain text content controls are best for short fields
- rich text content controls can be used for longer manual sections, but they may affect layout more visibly
- in OnlyOffice, content controls may only become clearly visible when the mouse hovers over them or when they are selected
- this is still preferable to plain text placeholders because the document keeps a real editable field after the Odoo merge
Future Feature: Template Custom Fields
Some document templates need values that are specific to one generated document and are not permanent fields on the case or contact, for example a hearing date, a reply deadline, a meeting room, or a free text instruction.
Recommended future design:
- configure custom fields on the document template
- show these fields in the document creation wizard
- validate required custom fields before generation
- store the final values on the generated document
- expose the values to DOCX templates through a
customdictionary
The implementation should not create dynamic Odoo model fields. Instead, it should use configuration and value models so the feature stays reversible and does not create migration-heavy schema changes.
Proposed template configuration fields:
name
code
field_type
required
default_value
help
sequenceInitial field types should stay conservative:
text
textarea
date
booleanPossible DOCX usage:
{{ custom.hearing_date }}
{{ custom.hearing_location }}
{{ custom.reply_deadline }}
{% if custom.is_urgent %}
URGENT
{% endif %}The custom field code should be restricted to lowercase letters, numbers, and underscores, for example:
hearing_date
reply_deadline
is_urgentAvoid spaces, hyphens, dots, and labels intended only for display.
Migration From Classic Word Merge Fields
Classic Word merge fields such as MERGEFIELD case_name are not used by the current DOCX generation engine. Templates must use Jinja/docxtpl syntax.
Migration rules:
MERGEFIELD case_name -> {{ case.name }}
MERGEFIELD document_name -> {{ document.name }}
MERGEFIELD company_city -> {{ company.city }}
MERGEFIELD signer_name -> {{ signer.name }}
MERGEFIELD signer_title -> {{ signer.signature_title }}
MERGEFIELD recipient_name -> use a loop on recipients
MERGEFIELD copy_name -> use a loop on copies
MERGEFIELD party_name -> use a loop on partiesSingle value example:
{{ case.name }}Repeated value example:
{% for recipient in recipients %}
{{ recipient.display_name_with_title }}
{{ recipient.address }}
{% endfor %}When converting templates, do not invent variable names. If a required business value does not exist in this document, use the closest available variable for testing and document the missing need as a future variable or custom field.
Test Protocol for DOCX Templates
Use this protocol after changing the DOCX context or after converting a Word model.
- Create or duplicate a document template in Odoo.
- Upload a DOCX file containing a small diagnostic block.
- Generate the document from a case with at least one recipient, one copy, one party, one representative, and one signer when possible.
- Open the generated DOCX.
- Confirm that normal variables are filled.
- Confirm that special characters such as
&,<, and>remain visible as text. - Add an intentionally unknown variable such as
{{ hearing.date }}. - Generate again and confirm that the document contains
[VARIABLE INCONNUE: hearing.date]. - Confirm that Odoo shows a warning notification listing the unknown variable.
- Regenerate the generated document and confirm the same behavior from the
Regeneratebutton.
Minimal diagnostic block:
CASE
case.name:
{{ case.name }}
case.description:
{{ case.description }}
COMPANY
company.name:
{{ company.name }}
company.city:
{{ company.city }}
SIGNERS
{% for signer in signers %}
Signer {{ loop.index }}:
{{ signer.display_name_with_title }}
{{ signer.signature_title }}
{% endfor %}
RECIPIENTS
{% for recipient in recipients %}
Recipient {{ loop.index }}:
{{ recipient.display_name_with_title }}
{{ recipient.salutation_title }}
{{ recipient.address }}
{% endfor %}
COPIES
{% for copy in copies %}
Copy {{ loop.index }}:
{{ copy.display_name_with_title }}
{{ copy.address }}
{% endfor %}
PARTIES
{% for party in parties %}
Party {{ loop.index }}:
{{ party.display_name_with_title }}
{{ party.role }}
{{ party.address }}
Representatives: {{ party.representative_names }}
{% endfor %}
UNKNOWN VARIABLE TEST
{{ hearing.date }}Expected result:
UNKNOWN VARIABLE TEST
[VARIABLE INCONNUE: hearing.date]