Skip to content

How Victor modules work

Everything you build on Victor is a module — a self-contained app you drop into your instance, much like an addon in Odoo. A module adds screens, data, and behavior. And you build it the same way whether you are extending a built-in feature or shipping something brand new: there is no privileged "core" path. The Contacts app you already have is just a module named contact, assembled from exactly the same pieces you are about to use.

The key idea: you declare, and Victor renders. You describe your data models and your screens; the generic framework turns those declarations into a live, navigable UI. You never write React, HTML, or CSS. There is no frontend to build and no API endpoints to wire up — a working list view, form, search bar, and menu entry all fall out of your declarations.

Coming from Odoo?

The mental model maps almost one-to-one. A module is an addon. A model is a model. Views are XML. Menus and actions are XML. requires is your depends. If you have written an Odoo addon, you already know the shape of this.

What a module is made of

A module is a folder with three kinds of ingredients:

Ingredient Language What it declares
Manifest module.yaml The module's identity, its dependencies, and which files to load
Models Python Your data — typed fields, and the behavior attached to them
Views & data XML Structure — list/form/search/kanban screens, menus, actions, and seed records

The split is deliberate and worth internalizing:

  • Python declares data and behavior. You subclass models.Model and assign field descriptors. This is the what — the shape of a record and the logic it carries.
  • XML declares structure. Views, menus, actions, and seed records are all declarative. This is the how it appears and how you reach it.

Both are tagged to the module that owns them, so your additions never quietly collide with someone else's.

What you can build

  • Database-backed models with typed fields (text, numbers, dates, booleans, selections, links to other records).
  • Screens: list, form, search, and kanban views over any model.
  • Navigation: menus and actions that put your screens in the app.
  • Seed records that ship with the module and are installed alongside it.
  • Extensions that reach into another module — add a field to its model, or inject a field into its form — without editing that module's files.

Before you start

You will need:

  • A module folder on a module path. In practice that means your Victor Cloud project's git repo — modules you add there are the overlay that your instance loads.
  • A module.yaml manifest at the folder root.
  • Python 3 for models and XML for views, menus, and data.
  • Any module you build on top of named in requires — commonly base (the framework's foundation, including the root menu) or contact.

Anatomy of a module

A minimal module for a small helpdesk app looks like this:

helpdesk/
  module.yaml          # name, label, version, requires, models, data
  __init__.py          # package marker
  models/
    __init__.py        # imports the model modules so they register
    ticket.py          # a models.Model subclass
  views/
    ticket.xml         # views + menus + actions
  data/
    ticket.xml         # seed records (optional)

The manifest: module.yaml

The manifest sits at the module root and declares who the module is, what it depends on, and what to load.

name: helpdesk
label: Helpdesk
author: Acme
version: 0.1.0
requires: [base, contact]
models: [models]
data: [views/ticket.xml, data/ticket.xml]
Key Meaning
name Technical id — snake_case, singular. Used to qualify everything the module owns.
label Human-readable name shown in the Module Manager.
author Free-text author string.
version Semver string, e.g. 0.1.0.
requires Modules this one depends on. They install first, and you may build on their models and views.
models Python packages to import so your model declarations register — typically [models].
data XML files to load, in order. Views usually come before seed data that references them.

Here is the real thing, from the built-in contact module:

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

Declaring a model

A model is a Python class that subclasses models.Model. You set a technical name, a human label, a required _description (a one-line summary of what the model holds), and then one attribute per field. Every model your module owns must set _description — a module whose model omits it is refused at install.

from victor import fields, models


class Ticket(models.Model):
    name = "ticket"                    # technical name: snake_case, singular
    label = "Ticket"                   # shown in menus and views
    _description = "A customer support request."
    subject = fields.String(label="Subject", required=True)
    description = fields.Text(label="Description")
    priority = fields.Selection(
        options=[("low", "Low"), ("high", "High")],
        label="Priority", default="low",
    )
    customer_id = fields.Reference(target="contact", label="Customer")
    done = fields.Boolean(label="Done", default=False)

You never declare id, created_at, or updated_at

Every table-backed model automatically gets an integer primary key id, plus created_at and updated_at timestamps. Don't declare them — they are always there for you.

For a model to load, it has to be imported. That is what models/__init__.py is for — importing a submodule registers the class:

from . import ticket  # noqa: F401  (import registers the model)

Field types

Import field descriptors from victor.fields. The common ones:

Field Stores Typical kwargs
String Short text label, required, default
Text Long / multi-line text label, required, default
Integer Whole number label, required, default
Float Decimal number label, required, default
Boolean True / false label, default
Date Calendar date label, required, default
Datetime Date + time label, required, default
Selection One value from a fixed set of choices options, default, label
CheckboxSet Several values from a fixed set options, label
Reference A link to one row of another model (stores its id) target, label
Many2many A link to many rows of another model target, label
One2many The reverse side of a link — the rows on target that point back at this record target, inverse, label
Vector A vector value (used by search / knowledge features) dim, label

Three field types do most of the interesting work:

Selection — a fixed set of string choices. Renders as a dropdown in a form and a labelled badge in a list.

priority = fields.Selection(
    options=[("low", "Low"), ("high", "High")],
    label="Priority", default="low",
)

Reference — a link to a single row of another model (it stores that row's id). This is the "many-to-one" link, e.g. a ticket's customer:

customer_id = fields.Reference(target="contact", label="Customer")

Many2many — a link to many rows of another model, e.g. tags on a contact:

tag_ids = fields.Many2many(target="tag", label="Tags")

See Models & fields for the full reference on each type.

Declaring the UI

Screens live in XML. Every data file has a single <victor> root, and its children are processed in document order. Inside you place <view>, <action>, <menu>, and <seed> elements.

Here is a complete UI for the helpdesk ticket — a list, a form, an action, and a menu:

<victor>
  <view id="view_list" model="ticket" type="list">
    <field name="subject"/>
    <field name="priority"/>
    <field name="customer_id"/>
    <field name="done"/>
  </view>
  <view id="view_form" model="ticket" type="form">
    <group string="Details">
      <field name="subject"/>
      <field name="description"/>
      <field name="priority"/>
      <field name="customer_id"/>
      <field name="done"/>
    </group>
  </view>
  <action id="action_tickets" name="Tickets" model="ticket" views="list,form" path="tickets"/>
  <menu id="menu" parent="base.menu_root" label="Helpdesk" action="helpdesk.action_tickets" icon="Users" sequence="20"/>
</victor>

That is the whole screen. No component code, no styling — the framework renders the list, the form, the field editors, and the navigation entry for you.

The XML elements

Element What it does Key attributes
<view id model type> Declares a view. Its children are the layout. id (local to the module), model, type (list | form | search | kanban)
<field name widget> Places a model field in a view. In a form the widget controls how it's edited; in a list, how it's rendered. name, widget (optional hint)
<group string> Groups form fields under an optional heading. string
<notebook> / <page string> Tabbed sections in a form. page: string (tab label)
<action id name model views path> Opens a model in one or more view types; a menu targets it. id, name, model, views (comma-separated), path (URL slug)
<menu id parent label action icon sequence> A navigation entry. Points at an action, or is action-less to act as a grouping parent. id, parent (qualified ref), label, action (qualified, optional), icon, sequence

Local ids are auto-qualified

A view or action id is local to your module and gets qualified to module.id. In the helpdesk example the action action_tickets is referenced from the menu as helpdesk.action_tickets, and it hangs off the framework's root menu, base.menu_root.

A richer view

Views scale up to kanban boards, search filters, tabbed forms, and field widgets. This is the real contact module — a good tour of the dialect:

<victor>
  <view id="view_list" model="contact" type="list">
    <field name="display_name"/>
    <field name="email"/>
    <field name="tag_ids" widget="tags"/>
  </view>
  <view id="view_form" model="contact" type="form">
    <group string="Details">
      <field name="display_name"/>
      <field name="email" widget="email"/>
      <field name="parent_id"/>
    </group>
    <notebook>
      <page string="Notes"><field name="email"/></page>
    </notebook>
  </view>
  <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>
  <view id="view_search" model="contact" type="search">
    <search>
      <filter name="companies" string="Companies" domain='[["is_company", "=", true]]' group="kind"/>
      <filter name="by_company" string="Company" context="{'group_by': 'is_company'}"/>
    </search>
  </view>
</victor>

A few things to notice:

  • Widgets are hints on a <field> — here email and tags. They tune how the value is edited and displayed.
  • <kanban default_group_by records_draggable> builds a board: cards grouped into columns by a field, optionally draggable.
  • <search> <filter> defines predefined filters. A domain is a list-of-lists predicate ([["is_company", "=", true]]); a context with group_by turns a filter into a group-by shortcut. Filters that share a group are combined with OR — selecting several at once broadens the results (a union), while separate groups (and filters with no group) are AND-ed together.

The full catalog of view types, widgets, and search options lives in Views.

Seed records

Seed records are rows that install with the module. Inside a <seed>, each child element is a field name and its text is coerced to the field's type:

<victor>
  <seed model="ticket" id="rec_example">
    <subject>Welcome to Helpdesk</subject>
    <priority>low</priority>
    <done>false</done>
  </seed>
</victor>

Building on another module

You don't have to fork a module to change it. Two mechanisms let you extend one cleanly, with your additions owned by your module.

Add a field to another model. Declare a model with the same name and list only your new fields; they merge into the existing model. Here the acme_crm sample adds a field to the built-in contact:

from victor import fields, models


class ContactExt(models.Model):
    name = "contact"                       # extend the core contact model
    lead_count = fields.Integer(label="Leads")

Inject that field into the other module's view. An <extend> block points at a qualified base view and carries anchor ops that mutate its layout in place:

<victor>
  <extend id="contact_form_lead" view="contact.view_form">
    <after field="email"><field name="lead_count" widget="integer"/></after>
  </extend>
</victor>

The anchor ops available inside <extend>:

Op Effect
<after> Insert content after the target
<before> Insert content before the target
<inside> Append content inside the target
<replace> Replace the target
<remove> Delete the target
<set> Change attributes on the target

Each op carries exactly one targeter: field= (a field by name), id=, or select=.

Your model can also reference another module's model. The acme_crm sample's lead links to a contact:

from victor import fields, models


class Lead(models.Model):
    name = "lead"
    label = "Lead"
    _description = "A sales lead - a potential deal linked to a contact."
    title = fields.String(label="Title", required=True)
    contact_id = fields.Reference(target="contact", label="Contact")

Extensions are covered in depth in Extending other modules.

5-minute quickstart

Build a working helpdesk module from nothing:

  1. Create the folder and manifest. In your project repo, add a helpdesk/ folder with a module.yaml:

    name: helpdesk
    label: Helpdesk
    author: Acme
    version: 0.1.0
    requires: [base, contact]
    models: [models]
    data: [views/ticket.xml]
    
  2. Add the package markers. Create an empty helpdesk/__init__.py, and a helpdesk/models/__init__.py that imports your model:

    from . import ticket  # noqa: F401
    
  3. Declare the model in helpdesk/models/ticket.py:

    from victor import fields, models
    
    
    class Ticket(models.Model):
        name = "ticket"
        label = "Ticket"
        _description = "A customer support request."
        subject = fields.String(label="Subject", required=True)
        description = fields.Text(label="Description")
        priority = fields.Selection(
            options=[("low", "Low"), ("high", "High")],
            label="Priority", default="low",
        )
        customer_id = fields.Reference(target="contact", label="Customer")
        done = fields.Boolean(label="Done", default=False)
    
  4. Declare the UI in helpdesk/views/ticket.xml — a list, a form, an action, and a menu:

    <victor>
      <view id="view_list" model="ticket" type="list">
        <field name="subject"/>
        <field name="priority"/>
        <field name="customer_id"/>
        <field name="done"/>
      </view>
      <view id="view_form" model="ticket" type="form">
        <group string="Details">
          <field name="subject"/>
          <field name="description"/>
          <field name="priority"/>
          <field name="customer_id"/>
          <field name="done"/>
        </group>
      </view>
      <action id="action_tickets" name="Tickets" model="ticket" views="list,form" path="tickets"/>
      <menu id="menu" parent="base.menu_root" label="Helpdesk" action="helpdesk.action_tickets" icon="Users" sequence="20"/>
    </victor>
    
  5. Install it. Open the Module Manager in your instance, find Helpdesk, and install it. Your new Helpdesk menu appears with a working list and form over the ticket model — no React, no HTML, no CSS.

Where to go next

  • Models & fields — every field type and its options, in depth.
  • Views — list, form, search, and kanban layouts, widgets, and search filters.
  • Menus & actions — placing your screens in the navigation tree.
  • Extending other modules — model and view extensions, and the full set of anchor ops.