Se rendre au contenu

Variables



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 over signers. {{ 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).
  • signers is the list, signer one 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: Madame becomes Mesdames, Monsieur becomes Messieurs, la and le become les; any other word takes an s unless 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 with l', must be added to the plural list.
  • 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 shows Monsieur,,.
  • 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:

  1. Enable the Developer tab if it is not visible.
  2. Place the cursor where the manual field should be completed.
  3. In the Developer tab, insert a Plain Text Content Control.
  4. Open the content control properties.
  5. Set a clear placeholder, for example A completer.
  6. Optionally set a title and tag, for example motivation.
  7. Choose a visible color for the content control.
  8. 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 custom dictionary

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.

  1. Create or duplicate a document template in Odoo.
  2. Upload a DOCX file containing a small diagnostic block.
  3. Generate the document from a case with at least one recipient, one copy, one party, one representative, and one signer when possible.
  4. Open the generated DOCX.
  5. Confirm that normal variables are filled.
  6. Confirm that special characters such as &, <, and > remain visible as text.
  7. Add an intentionally unknown variable such as {{ hearing.date }}.
  8. Generate again and confirm that the document contains [VARIABLE INCONNUE: hearing.date].
  9. Confirm that Odoo shows a warning notification listing the unknown variable.
  10. Regenerate the generated document and confirm the same behavior from the Regenerate button.

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]