Skip to content

Other View Types

Display date-based records on a calendar.

{
"data_type": "UiView",
"identifier": "meeting_calendar_view",
"type": "Calendar",
"model": "Meeting",
"arch": [
{
"content": [
{
"type": "field",
"name": "name",
"display_mode": "title"
},
{
"type": "field",
"name": "start_time",
"display_mode": "start"
},
{
"type": "field",
"name": "end_time",
"display_mode": "end"
}
]
}
]
}
ModeDescription
titleEvent title
startStart datetime
endEnd datetime
{
"content": [
{"type": "field", "name": "name", "display_mode": "title"},
{"type": "field", "name": "start_time", "display_mode": "start"},
{"type": "field", "name": "end_time", "display_mode": "end"},
{
"type": "QuickCreate",
"context": {"form_identifier": "meeting_quick_create_form"}
}
]
}

Define filters and groupings for list/kanban views.

{
"data_type": "UiView",
"identifier": "contact_search_view",
"type": "Search",
"model": "Contact",
"arch": [
{
"type": "filter",
"identifier": "customers",
"description": "Customers",
"filter": "(Q(is_customer=True))"
},
{
"type": "filter",
"identifier": "suppliers",
"description": "Suppliers",
"filter": "(Q(is_supplier=True))"
},
{
"type": "group",
"identifier": "group_country",
"name": "country",
"description": "Country"
}
]
}

Predefined filter conditions:

{
"type": "filter",
"identifier": "active_only",
"description": "Active",
"filter": "(Q(active=True))"
}
"(Q(field='value'))" # String equality (shorthand)
"(Q(field__eq='value'))" # String equality (explicit)
"(Q(field=True))" # Boolean check
"(Q(field__isnull=True))" # ManyToOne null check (use field name, not field_id)
"(Q(stage__is_won=True))" # Related field lookup (traverse relation)
"(Q(active=True) & Q(status='open'))" # AND condition
"(Q(user__id__eq=uid))" # Current user - ManyToOne requires field__id__eq=uid
"(Q(company__id__eq=cid))" # Current company - ManyToOne requires field__id__eq=cid

Important for ManyToOne fields with uid/cid:

  • Use field__id__eq=uid syntax (NOT field_id=uid or field=uid)
  • This traverses the relationship to compare the related record’s ID
VariableDescription
uidCurrent logged-in user’s ID
cidCurrent active company ID (user’s selected company for the session)
D()Dynamic date helper (see below)

The D() helper generates dynamic dates for use in filters. It returns a date or datetime object that is evaluated at query time.

Basic usage:

D('today') # today's date
D('now') # current datetime

Relative days/weeks/months/years:

D('today', days=-30) # 30 days ago
D('today', days=7) # 7 days from now
D('today', weeks=-2) # 2 weeks ago
D('today', months=-3) # 3 months ago
D('today', years=-1) # 1 year ago

Period boundaries (offset: 0=current, -1=previous, 1=next):

ExpressionDescription
D('start_of_week', 0)Monday of this week
D('end_of_week', 0)Sunday of this week
D('start_of_month', 0)1st of this month
D('end_of_month', 0)Last day of this month
D('start_of_month', -1)Start of last month
D('end_of_month', -1)End of last month
D('start_of_quarter', 0)Start of this quarter
D('end_of_quarter', 1)End of next quarter
D('start_of_year', 0)Jan 1st this year
D('end_of_year', -1)Dec 31st last year

Example filters using D():

{
"type": "filter",
"identifier": "overdue",
"description": "Overdue",
"filter": "(Q(state='Posted') & Q(due_date__lt=D('today')))"
}
{
"type": "filter",
"identifier": "due_this_week",
"description": "Due This Week",
"filter": "(Q(due_date__gte=D('start_of_week', 0)) & Q(due_date__lte=D('end_of_week', 0)))"
}
{
"type": "filter",
"identifier": "last_30_days",
"description": "Last 30 Days",
"filter": "(Q(create_date__gte=D('today', days=-30)))"
}
{
"type": "filter",
"identifier": "this_quarter",
"description": "This Quarter",
"filter": "(Q(date__gte=D('start_of_quarter', 0)) & Q(date__lte=D('end_of_quarter', 0)))"
}
OperatorExampleDescription
= or __eqQ(state='Pending')Exact match
__neqQ(state__neq='Draft')Not equal
__isnullQ(user__isnull=True)Null check (use field name, not field_id)
__gtQ(amount__gt=100)Greater than
__gteQ(amount__gte=100)Greater than or equal
__ltQ(amount__lt=100)Less than
__lteQ(amount__lte=100)Less than or equal
__inQ(state__in=['Draft','Pending'])Value in list
__ninQ(state__nin=['Done'])Value not in list
__containsQ(name__contains='test')Contains (case-sensitive)
__icontainsQ(name__icontains='test')Contains (case-insensitive)
__iexactQ(name__iexact='test')Case-insensitive exact

For ManyToOne fields, you can filter by related record’s fields using __fieldname:

{
"type": "filter",
"identifier": "won_filter",
"description": "Won",
"filter": "(Q(stage__is_won=True))"
}

Note: When filtering by user-definable data (like stages), use semantic boolean flags (e.g., is_won, is_lost) rather than names or identifiers which users can change.

Grouping options for the view:

{
"type": "group",
"identifier": "group_stage",
"name": "stage",
"description": "Stage"
}

The browse panel provides a sidebar for faceted filtering. It displays field values with record counts, allowing users to quickly filter by clicking on values.

Add browsepanel elements to your search view:

{
"data_type": "UiView",
"identifier": "contact_search_view",
"type": "Search",
"model": "Contact",
"arch": [
{
"type": "filter",
"identifier": "customers",
"description": "Customers",
"filter": "(Q(is_customer=True))"
},
{
"type": "browsepanel",
"name": "company_type",
"description": "Type",
"expanded": true
},
{
"type": "browsepanel",
"name": "categories",
"description": "Categories"
},
{
"type": "browsepanel",
"name": "country",
"description": "Country"
}
]
}
PropertyTypeDefaultDescription
namestringrequiredField name to browse by
descriptionstringfield labelDisplay label in the panel
expandedbooleanfalseWhether section is expanded by default
limitnumber8Maximum values to show before “Show more”

The browse panel supports the following field types:

Field TypeBehavior
ManyToOneShows related record names with counts
ManyToManyShows related record names with counts
OneToOneShows related record names with counts
SelectionShows selection choices with counts
  1. Multi-select: Users can select multiple values within a field (OR logic)
  2. Cross-field AND: Selections across different fields use AND logic
  3. Dynamic counts: Record counts update based on other active filters
  4. Self-exclusion: A facet is never narrowed by its own selection — selecting a value keeps that facet fully browsable while still cross-narrowing the others, so the counts stay meaningful
  5. Toggle visibility: Users can show/hide the panel via the toolbar button

When a browsepanel field is a ManyToOne/ManyToMany whose target model is a tree (it opts into the hierarchy primitive via _parent_field), the panel automatically renders that facet as an expandable, multi-level tree instead of a flat list. No extra view configuration is needed.

  • Nodes nest under their parents to any depth, with per-node expand/collapse.
  • Counts are rolled up the tree, so a parent shows the total for itself plus all descendants.
  • Selecting a node filters the record list to that node and its entire subtree (matched via the materialised parent_path), so picking a top-level category captures everything beneath it.
  • Only branches that lead to matching records appear; an ancestor with no direct records is still shown so users can drill through it.

For example, a category browse field on Product becomes a drill-down tree as soon as ProductCategory declares _parent_field = "parent" — the search view entry stays exactly the same {"type": "browsepanel", "name": "category"}.

{
"data_type": "UiView",
"identifier": "product_search_view",
"type": "Search",
"model": "Product",
"arch": [
{
"type": "browsepanel",
"name": "category",
"description": "Category",
"expanded": true
},
{
"type": "browsepanel",
"name": "brand",
"description": "Brand"
},
{
"type": "browsepanel",
"name": "product_type",
"description": "Type",
"limit": 5
},
{
"type": "filter",
"identifier": "in_stock",
"description": "In Stock",
"filter": "(Q(quantity__gt=0))"
}
]
}
{
"data_type": "UiView",
"identifier": "lead_search_view",
"type": "Search",
"model": "CrmLead",
"arch": [
{
"type": "filter",
"identifier": "my_opportunities",
"description": "My Opportunities",
"filter": "(Q(user__id__eq=uid))"
},
{
"type": "filter",
"identifier": "unassigned",
"description": "Unassigned",
"filter": "(Q(user_id__isnull=True))"
},
{
"type": "filter",
"identifier": "won_filter",
"description": "Won",
"filter": "(Q(stage__is_won=True))"
},
{
"type": "filter",
"identifier": "lost_filter",
"description": "Lost",
"filter": "(Q(stage__is_lost=True))"
},
{
"type": "group",
"identifier": "group_stage",
"name": "stage",
"description": "Stage"
},
{
"type": "group",
"identifier": "group_user",
"name": "user",
"description": "Salesperson"
},
{
"type": "group",
"identifier": "group_source",
"name": "source",
"description": "Source"
}
]
}

Wizard views define the UI for transient models (wizards). Wizards are temporary forms used for user input before executing an action.

{
"data_type": "UiView",
"identifier": "record_payment_wizard_view",
"type": "Wizard",
"model": "RecordPaymentWizard",
"arch": [
{
"type": "row",
"content": [
{
"type": "column",
"span": 6,
"content": [
{"type": "field", "name": "amount", "properties": {"widget": "NumberInput"}}
]
},
{
"type": "column",
"span": 6,
"content": [
{"type": "field", "name": "date", "properties": {"widget": "DatePickerInput"}}
]
}
]
},
{
"type": "footer",
"content": [
{
"type": "actionButton",
"properties": {
"label": "Confirm",
"method": "action_confirm",
"variant": "primary"
}
}
]
}
]
}

Control how the wizard opens with view-level properties:

PropertyValuesDefaultDescription
sizexs, sm, md, lg, xl, fullscreenAutoModal/drawer size
displayModedrawer, modalAutoHow the wizard opens

Default behavior (when not specified):

  • Multi-step wizards (has stepper): Fullscreen modal (xl)
  • Single-step wizards: Drawer (md)
{
"data_type": "UiView",
"identifier": "large_wizard_view",
"type": "Wizard",
"model": "MyWizard",
"size": "lg",
"displayMode": "drawer",
"arch": [...]
}

Use a stepper element for multi-step wizards:

{
"data_type": "UiView",
"identifier": "onboarding_wizard_view",
"type": "Wizard",
"model": "OnboardingWizard",
"arch": [
{
"type": "stepper",
"steps": [
{"label": "Company Info", "key": "company"},
{"label": "Users", "key": "users"},
{"label": "Settings", "key": "settings"}
]
},
{
"type": "step",
"key": "company",
"content": [
{"type": "field", "name": "company_name"}
]
},
{
"type": "step",
"key": "users",
"content": [
{"type": "field", "name": "admin_email"}
]
},
{
"type": "step",
"key": "settings",
"content": [
{"type": "field", "name": "timezone"}
]
},
{
"type": "footer",
"content": [
{"type": "actionButton", "properties": {"label": "Complete", "method": "action_confirm", "variant": "primary"}}
]
}
]
}

The footer element defines action buttons:

{
"type": "footer",
"content": [
{
"type": "actionButton",
"properties": {
"label": "Confirm",
"method": "action_confirm",
"variant": "primary"
}
},
{
"type": "actionButton",
"properties": {
"label": "Save Draft",
"method": "action_save_draft",
"variant": "outline"
}
}
]
}

A Cancel button is automatically added to all wizards.

Connect views to the application:

{
"data_type": "WindowAction",
"identifier": "leads_action",
"name": "Leads",
"model": "CrmLead",
"modes": "Kanban,List,Form",
"default_view": "lead_kanban_view",
"views": [
"lead_kanban_view",
"lead_list_view",
"lead_form_view"
],
"search_view": "lead_search_view",
"context": {
"view": { "filters": ["my_leads"] }
}
}
PropertyDescription
nameAction display name
modelModel to display
modesAvailable view modes
default_viewDefault view identifier
viewsList of view identifiers
search_viewSearch view identifier
contextDefault context
limitRecords per page
global_filterAlways-applied filter

Context: field prefills vs. view directives

Section titled “Context: field prefills vs. view directives”

The action context carries two distinct kinds of entry, kept in separate namespaces so they can never be confused:

  • default_<field> — pre-fills <field> on a new record created from the action (e.g. default_company: 3, default_type: "Opportunity", or a relational command-form default like default_lines: [["C", {...}]]). The create path maps every default_<field> straight onto the model field of that name.
  • view: { filters: [...], groups: [...] }list/search view directives: which search-view filter / group identifiers start active. These describe the view, not a record, and are read only by the list/search layer.

Keeping view directives under view (rather than default_filters / default_groups) is deliberate: their names reference data dimensions and would otherwise collide with a same-named model field. For example User owns a filters relation, so a default_filters key would be mis-read as a field prefill and inject bogus relational command data on user creation. The view namespace makes that collision structurally impossible — no reserved-key list to maintain.