Skip to content

Views

A view describes how one model is rendered on screen — as a table, a record editor, a board, a card grid, and so on. You never build these screens as bespoke UI: you declare them in view XML, and Victor's generic framework renders them for you. Point a menu or action at a view, and the framework does the rest.

This page covers the view XML DSL: the file structure, the view types, the <field> element and its widgets, form layout tags, search filters, record-action buttons, and view inheritance. It assumes you already have a model to render — see Models & fields for defining one.

The view file

Every view file has a single root element, <victor>, which wraps all of the view, action, menu, and seed declarations for a module:

<victor>
  <!-- <view>, <action>, <menu> and seed records go here -->
</victor>

View files are activated by listing them under the data: key of your module's module.yaml:

name: contact
label: Contacts
version: 0.1.0
requires: [base]
models: [models]
data: [views/contact.xml, data/contact.xml]

Note

Files under data: load in order. If a view references another view (for example, an action that opens a form), make sure the referenced view is declared first.

Declaring a view

Each <view> element declares one view for one model:

<view id="view_list" model="contact" type="list">
  <field name="display_name"/>
  <field name="email"/>
</view>
Attribute Purpose
id Unique reference within the module. Actions and menus point at a view by this id (e.g. contact.view_form).
model The target model name the view renders.
type One of list, form, kanban, search, card, graph, dashboard.

A model may have one view per type — a list, a form, a kanban, a search, and so on. The framework picks the right one for the context (a collection view uses list/kanban/card; opening a record uses form; the search bar uses search).

View types at a glance

Type What it renders
list A sortable, paginated table. <field> children are columns.
form A single-record editor with layout tags and editable field rows.
kanban A column board grouped by a field; records are draggable cards.
search The filter chips and Group By options for a model's collection views.
card A responsive card grid (as used by the Module Manager).
graph A specialized node-graph canvas for record graphs such as a workflow's steps.
dashboard A screen of blocks (counters, mini lists) that can query any model. Your app can have its own, and every module can add blocks to the shared home one.

Fields in views

<field> is the core element. It places a model field into a view — a column in a list, kanban, or card, and an editable row in a form:

<field name="email" widget="email"/>
Attribute Applies to Meaning
name all Required. The model field name to render.
widget all Optional widget-name override (see Widgets).
readonly="1" form Render the field read-only.
required="0" form Suppress the required * marker.
invisible form "1" to always hide, or an expression evaluated against the current form values.

Widget resolution

When you don't specify a widget, the framework resolves one in this order:

  1. The widget name you gave, if it is a registered widget.
  2. Otherwise, the default widget for the field's data type.
  3. Otherwise, a plain string.

Tip

Prefer letting the field's data type pick the widget. Only add an explicit widget when you want a different presentation than the type's default (for example, a String field rendered as an email link, or a Boolean rendered as a toggle).

Conditional visibility

In a form, invisible can be a simple flag or an expression compared against the current record's values:

<view id="view_form" model="contact" type="form">
  <group string="Details">
    <field name="is_company"/>
    <field name="company_name" invisible="is_company != 'true'"/>
    <field name="birthday" invisible="is_company == 'true'"/>
  </group>
</view>

Supported expression forms are field == 'value' and field != 'value'.

Widgets

Data-type defaults

If a <field> has no widget, its data type selects the widget:

Field type Default rendering
String Single-line text
Text Multiline text
Integer Number input
Float Number input
Boolean Checkbox (check cell in collections)
Date Localized date / date picker
Datetime Date and time
Reference Many-to-one autocomplete with quick-create
Selection Dropdown, shown as a labelled badge
CheckboxSet Stacked multi-checkbox group
Many2many Tag chips with autocomplete
One2many Inline editable child-row grid (full form width)

Name overrides

Set widget="…" to override the default. The registered widget names are:

Widget Renders
email mailto: link (read) / email input (edit)
url Clickable link (read) / URL input (edit)
phone tel: link (read) / tel input (edit)
tags Many-to-many as removable tag chips with type-ahead and create-on-the-spot
radio A Selection as a vertical radio group instead of a dropdown
checkbox-inline A CheckboxSet as a wrapping inline row of checkboxes
textarea Forces a multiline text input
toggle A boolean as an on/off switch
badge The value as a pill/badge
monetary A number formatted with grouping and 2 decimals
date Localized date (read) / date picker (edit)
markdown A Text field as Markdown: source editor beside a live preview, full form width. Read mode and list cells show the rendered text.
html A Text field as HTML: source editor beside a live preview (sandboxed), full form width. Read mode and list cells show the source, never live markup.

Rich text is a widget, not a field type

There is no Markdown or Html field type — both are stored in a plain Text field and only the rendering differs, so you pick it per view: <field name="body" widget="markdown"/>.

Warning

Only the widget names above are registered. If you set widget to an unregistered name, the field silently falls back to its data-type default. Stick to this list.

Form layout

Forms arrange field rows in a two-column label/value grid. Use these layout tags to structure them:

Tag Purpose
<group string="…"> Groups fields into the two-column grid; the optional string is a section heading. A group whose children are all currently invisible disappears entirely, heading included.
<notebook> A tabbed container that holds <page> children.
<page string="…"> One tab inside a <notebook>; string is the tab label.
<setting string="…" help="…"> One settings row: the name and a muted help description on the left, the row's controls (buttons, nolabel fields) on the right. Use it for preference/settings pages instead of bare field grids.
<title> The record heading (Odoo's oe_title): fields inside render in large heading typography, editable in place — put nolabel="1" fields in it, e.g. <title><field name="display_name" nolabel="1"/></title> at the top of a form.
<sheet> An optional container that lays its <field> children out in the standard grid.

A <field nolabel="1"> renders just its value cell with no label column — for fields inside a <setting>, whose left column already names the row.

Plain field content without a layout tag lays out the same way as inside a <group>.

<view id="view_form" model="contact" type="form">
  <group string="Details">
    <field name="display_name"/>
    <field name="email" widget="email"/>
    <field name="is_company"/>
    <field name="stage"/>
    <field name="parent_id"/>
    <field name="tag_ids" widget="tags"/>
  </group>
  <notebook>
    <page string="Notes">
      <field name="email"/>
    </page>
  </notebook>
</view>

<button> children in a form render in the form toolbar as record actions — see View actions.

List views

A list is a sortable, paginated table. Its <field> children are the columns, rendered left-to-right. Clicking a column header toggles the sort between ascending, descending, and none. Add <button> children for per-row record actions.

<view id="view_list" model="contact" type="list">
  <field name="display_name"/>
  <field name="email"/>
  <field name="is_company"/>
  <field name="stage"/>
  <field name="tag_ids" widget="tags"/>
</view>

Kanban views

A kanban renders records as draggable cards, grouped into columns. Put the card's fields inside the <kanban> body — the first <field> becomes the bold card title, the rest render as card lines.

<view id="view_kanban" model="contact" type="kanban">
  <kanban default_group_by="is_company" records_draggable="1">
    <field name="display_name"/>
    <field name="email"/>
  </kanban>
</view>
<kanban> attribute Meaning
default_group_by The field the board columns are grouped by.
records_draggable "0" disables dragging cards between columns; drag is enabled by default.

Card views

A card view is a responsive card grid. It takes the same <field> and <button> children as a list: the first <field> is the card title, and the rest render as label/value rows.

<view id="view_module_card" model="module" type="card">
  <field name="shortdesc"/>   <!-- first field = card title -->
  <field name="latest_version"/>
  <field name="author"/>
  <field name="state"/>
  <button action="install" string="Install" invisible="state == 'installed'"/>
  <button action="uninstall" string="Uninstall" variant="destructive" invisible="can_uninstall != 'true'"/>
</view>

Search views and filters

A search view defines the filter chips and Group By options for a model's collection views. Its <search> body holds <filter> children.

<view id="view_search" model="contact" type="search">
  <search>
    <filter name="companies" string="Companies" domain='[["is_company", "=", true]]' group="kind"/>
    <filter name="people" string="People" domain='[["is_company", "=", false]]' group="kind"/>
    <filter name="by_company" string="Company" context="{'group_by': 'is_company'}"/>
  </search>
</view>

Domain filters

A <filter> with a domain is a toggleable chip. When active, its domain is ANDed into the record query.

<filter> attribute Meaning
name Unique key for the filter.
string Chip label; defaults to name.
domain A JSON list of [field, op, value] triples.
group Optional. Combines related filters.

A domain is a list of triples, each [field, operator, value]. In the example above, [["is_company", "=", true]] keeps only records where is_company is true.

Filters that share the same group are combined together. Several same-field equality filters in one group become an OR (an in match) rather than a contradictory AND — so companies and people above behave as a single toggle set.

Group By options

A <filter> that carries a group_by context becomes a Group By menu option instead of a domain chip:

<filter name="by_company" string="Company" context="{'group_by': 'is_company'}"/>

Buttons

<button> surfaces a registered server action — as a column in list/card views, or as a toolbar button in a form. Its attributes include action, string, variant, confirm, preview, invisible, show_on_new, and run_on_save. See View actions for the full semantics.

<button action="uninstall" string="Uninstall" variant="destructive" invisible="can_uninstall != 'true'"/>

Graph views

A graph view is a specialized node-graph canvas for record graphs — for example, the steps of a workflow. You declare it with type="graph"; the graph is derived from the record's own data rather than from <field> columns, so it has no column children:

<view id="view_graph" model="workflow" type="graph"/>

Graph views are used by workflow-style features; their authoring surface is documented alongside those features.

Dashboard views

A dashboard view is a screen of blocks. Each block names the model it queries — one dashboard can mix blocks from any number of models. You declare it like any other view and mount it through your action's views=; putting dashboard first makes it the app's landing screen, with the list one tab away:

<victor>
  <view id="view_dashboard" model="ticket" type="dashboard">
    <block id="open" type="count" model="ticket" string="Open" tone="warning"
           domain='[["state", "=", "open"]]'/>
    <block id="overdue" type="count" model="ticket" string="Overdue" tone="error"
           domain='[["sla_state", "=", "breached"]]'/>
    <block id="newest" type="list" model="ticket" string="Newest"
           order="created_at:desc" limit="5" empty="No tickets yet.">
      <field name="display_name"/>
      <field name="state"/>
    </block>
    <block id="customers" type="list" model="contact" string="New customers">
      <field name="display_name"/>
    </block>
  </view>
  <action id="action_tickets" name="Tickets" model="ticket" views="dashboard,list,form" path="tickets"/>
</victor>

The view's model= is only the action's anchor — blocks are free to query other models (the contact block above). Blocks sit straight under <view>; consecutive counters render as a row of tiles, consecutive lists as a row of cards, in the order you wrote them. <group id="…" string="…"> is optional — use it when you want a heading over a set of blocks.

<block> attribute Meaning
type The view kind this block embeds: count — a single number; list — the newest few records; chart — an aggregation of group_by (see below). (Other kinds may support embedding in the future.)
model The model this block queries.
string The block's label.
domain Which records count (same domain syntax as a filter).
order Sort for a list block, e.g. created_at:desc.
limit How many rows a list block shows (default 5).
icon Icon name, same set as menu icons.
tone error, warning or success — colours a count block.
hide_if_empty "1" hides the block when the count is zero.
group_by chart only. A Boolean/String/Selection field → bars of counts per value. A Date/Datetime field with a granularity suffix (created_at:day) → a line of counts over time.
days chart lines only: the trailing window in days (default 30). Quiet days show as zero.
empty Line shown when a list block has no rows.

In a list block the first <field> is the row's title; the rest become its subtitle. Every row links to the record, and the block header links to the model's full list. Blocks respect access rights: a user who cannot read a block's model simply does not see that block. Install-time validation checks each block's fields against that block's model, so a typo fails the install instead of silently showing nothing.

The home dashboard

The home screen is also a dashboard view — the one view that belongs to no model. Victor ships a single one — base.view_home_dashboard — with two groups: attention (things that need a human) and activity (what happened recently). Your module puts its own blocks in them with <extend>:

<victor>
  <extend id="dashboard_helpdesk" view="base.view_home_dashboard">
    <inside id="attention">
      <block id="tickets_overdue" type="count" model="ticket" string="Tickets overdue"
             icon="Warning" tone="error" hide_if_empty="1"
             domain='[["state", "=", "overdue"]]'/>
    </inside>
    <inside id="activity">
      <block id="tickets_new" type="list" model="ticket" string="New tickets"
             icon="Ticket" order="created_at:desc" limit="5" empty="No tickets yet.">
        <field name="display_name"/>
        <field name="state"/>
      </block>
    </inside>
  </extend>
</victor>

Blocks here use exactly the attributes from the table above. One extra rule of the house: use hide_if_empty="1" on anything you put in attention. The home screen is meant to be quiet when nothing is wrong — a counter that permanently reads zero teaches people to stop looking at the row. (When every block in a group hides, the group shows its empty line instead.)

Extending views

To change a view defined by another module (or to keep your customizations separate from a base view), use <extend> instead of editing the original file. Target a base view by its ref, then apply ops that locate a node and modify it.

<victor>
  <extend id="contact_form_lead" view="contact.view_form">
    <after field="email"><field name="lead_count"/></after>
  </extend>
</victor>
<extend> attribute Meaning
id Unique reference for this extension.
view The base view ref to modify (e.g. contact.view_form).

Inheritance ops

Inside <extend>, each op locates a target node and changes the arch:

Op Effect
<after> Insert its children as siblings after the target.
<before> Insert its children as siblings before the target.
<inside> Append its children into the target.
<replace> Swap the target for the op's children.
<remove> Delete the target node.
<set> Set attributes on the target node.

Each op picks its target with one of these selectors:

Selector Locates by
field="…" A <field> with that name.
id="…" A node with that id.
select="…" A path of tag[@attr='value'] steps.
<victor>
  <extend id="contact_form_tweaks" view="contact.view_form">
    <!-- add a field after email -->
    <after field="email"><field name="lead_count"/></after>
    <!-- make the stage field read-only -->
    <set field="stage" readonly="1"/>
    <!-- drop a field entirely -->
    <remove field="parent_id"/>
  </extend>
</victor>

Tip

Extending is the clean way to adapt built-in apps. To add a lead count to the built-in contact form, ship an <extend> from your own module rather than forking the contact app's view file.

See also