What You Will Build
A complete "Equipment Tracking" module with: a model for equipment items, form and list views, search filters, security rules, a menu entry, and basic tests. This covers every fundamental concept you need for Odoo module development.
Step 1: Module Structure
equipment_tracking/
├── __manifest__.py
├── __init__.py
├── models/
│ ├── __init__.py
│ └── equipment.py
├── views/
│ └── equipment_views.xml
├── security/
│ └── ir.model.access.csv
├── data/
│ └── equipment_data.xml
└── tests/
├── __init__.py
└── test_equipment.pyStep 2: __manifest__.py
{
'name': 'Equipment Tracking',
'version': '19.0.1.0.0',
'category': 'Operations',
'summary': 'Track company equipment and assignments',
'description': 'Manage equipment inventory, assignments, and maintenance schedules.',
'author': 'Your Company',
'depends': ['base', 'hr', 'mail'],
'data': [
'security/ir.model.access.csv',
'views/equipment_views.xml',
'data/equipment_data.xml',
],
'installable': True,
'application': True,
'license': 'LGPL-3',
}Step 3: Model
# models/__init__.py
from . import equipment
# models/equipment.py
from odoo import api, fields, models
from odoo.exceptions import ValidationError
class Equipment(models.Model):
_name = 'equipment.item'
_description = 'Equipment Item'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'name'
name = fields.Char(string='Name', required=True, tracking=True)
serial_number = fields.Char(string='Serial Number', required=True)
department_id = fields.Many2one('hr.department', string='Department')
assigned_to = fields.Many2one('hr.employee', string='Assigned To', tracking=True)
status = fields.Selection([
('available', 'Available'),
('in_use', 'In Use'),
('maintenance', 'In Maintenance'),
('retired', 'Retired'),
], string='Status', default='available', tracking=True)
purchase_date = fields.Date(string='Purchase Date')
warranty_expiry = fields.Date(string='Warranty Expiry')
purchase_cost = fields.Float(string='Purchase Cost')
notes = fields.Text(string='Notes')
# Computed field
days_until_warranty = fields.Integer(
string='Days Until Warranty Expiry',
compute='_compute_days_until_warranty',
)
# Constraint (Odoo 19 style)
_serial_unique = models.Constraint(
"UNIQUE(serial_number)",
"Serial number must be unique."
)
@api.depends('warranty_expiry')
def _compute_days_until_warranty(self):
today = fields.Date.today()
for record in self:
if record.warranty_expiry:
delta = record.warranty_expiry - today
record.days_until_warranty = delta.days
else:
record.days_until_warranty = 0
@api.constrains('purchase_cost')
def _check_purchase_cost(self):
for record in self:
if record.purchase_cost and record.purchase_cost < 0:
raise ValidationError("Purchase cost cannot be negative.")Step 4: Views
<!-- views/equipment_views.xml -->
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- List View -->
<record id="equipment_item_view_list" model="ir.ui.view">
<field name="name">equipment.item.list</field>
<field name="model">equipment.item</field>
<field name="arch" type="xml">
<list decoration-danger="days_until_warranty < 30 and days_until_warranty > 0">
<field name="name"/>
<field name="serial_number"/>
<field name="department_id"/>
<field name="assigned_to"/>
<field name="status" widget="badge"
decoration-success="status == 'available'"
decoration-info="status == 'in_use'"
decoration-warning="status == 'maintenance'"/>
<field name="warranty_expiry"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="equipment_item_view_form" model="ir.ui.view">
<field name="name">equipment.item.form</field>
<field name="model">equipment.item</field>
<field name="arch" type="xml">
<form>
<header>
<field name="status" widget="statusbar"
statusbar_visible="available,in_use,maintenance,retired"/>
</header>
<sheet>
<div class="oe_title">
<h1><field name="name" placeholder="Equipment Name"/></h1>
</div>
<group>
<group string="Details">
<field name="serial_number"/>
<field name="department_id"/>
<field name="assigned_to"/>
</group>
<group string="Purchase Info">
<field name="purchase_date"/>
<field name="purchase_cost"/>
<field name="warranty_expiry"/>
<field name="days_until_warranty"
invisible="not warranty_expiry"/>
</group>
</group>
<notebook>
<page string="Notes" name="notes">
<field name="notes"/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<!-- Search View -->
<record id="equipment_item_view_search" model="ir.ui.view">
<field name="name">equipment.item.search</field>
<field name="model">equipment.item</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="serial_number"/>
<filter name="available" string="Available" domain="[('status','=','available')]"/>
<filter name="in_use" string="In Use" domain="[('status','=','in_use')]"/>
<group string="Group By">
<filter name="group_dept" string="Department" context="{'group_by':'department_id'}"/>
<filter name="group_status" string="Status" context="{'group_by':'status'}"/>
</group>
</search>
</field>
</record>
<!-- Action -->
<record id="equipment_item_action" model="ir.actions.act_window">
<field name="name">Equipment</field>
<field name="res_model">equipment.item</field>
<field name="view_mode">list,form</field>
</record>
<!-- Menu -->
<menuitem id="equipment_menu_root" name="Equipment" sequence="100"/>
<menuitem id="equipment_menu_items" name="Equipment Items"
parent="equipment_menu_root" action="equipment_item_action"/>
</odoo>Step 5: Security
# security/ir.model.access.csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_equipment_user,equipment.item.user,model_equipment_item,base.group_user,1,1,1,0
access_equipment_admin,equipment.item.admin,model_equipment_item,base.group_system,1,1,1,1Step 6: Install and Test
# Install the module
python odoo-bin -d testdb -i equipment_tracking --stop-after-init
# Run tests
python odoo-bin -d testdb --test-tags /equipment_tracking --stop-after-initAI-Powered Module Creation
Everything above can be generated by Claude Code in 10 minutes. Describe the module in natural language, and Claude creates all files with correct Odoo 19 syntax. Deploy the result to DeployMonkey with Git integration.