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.Modeland 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.yamlmanifest 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— commonlybase(the framework's foundation, including the root menu) orcontact.
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:
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:
Many2many — a link to many rows of another model, e.g. tags on a contact:
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>— hereemailandtags. 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. Adomainis a list-of-lists predicate ([["is_company", "=", true]]); acontextwithgroup_byturns a filter into a group-by shortcut. Filters that share agroupare combined with OR — selecting several at once broadens the results (a union), while separate groups (and filters with nogroup) 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:
-
Create the folder and manifest. In your project repo, add a
helpdesk/folder with amodule.yaml: -
Add the package markers. Create an empty
helpdesk/__init__.py, and ahelpdesk/models/__init__.pythat imports your model: -
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) -
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> -
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
ticketmodel — 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.