Registry
Contents
Registry#
This article explains how to customize OpenSPP's registry system by introducing a new top-level group type. As a practical example, we'll add a new top-level group type (such as "Village") that can contain regular groups (households), along with custom UI, data, and actions.
Core Models
res.partner: Main registry model for individuals and groups.g2p.group.kind: Defines types of groups (e.g., Household, Village).g2p.group.membership: Manages group membership relationships.
Key Features
Hierarchical group structure (e.g., Villages > Households > Individuals)
Custom group kinds and indicators
Computed statistics for top-level groups
Custom actions for navigation and reporting
Prerequisites#
Solid understanding of Odoo 17 module development, including Python, XML, and XPath.
Required modules:
g2p_registry_base,g2p_registry_group,g2p_registry_individual,g2p_registry_membership,spp_registry_group_hierarchy.To set up OpenSPP for development, please refer to the Development Setup Guide.
Module Structure#
A typical registry customization module follows the standard Odoo structure.
Here's the structure for our example module, spp_top_level_groups:
spp_top_level_groups/
├── __init__.py
├── __manifest__.py
├── models/
│ ├── __init__.py
│ ├── res_partner.py
│ └── group_kind.py
├── views/
│ ├── top_level_group_views.xml
│ ├── group_views.xml
│ └── group_kind_views.xml
├── data/
│ └── group_kind_data.xml
└── security/
└── ir.model.access.csv
Step-by-Step Guide#
Create the Module Scaffold#
Create a new directory for your module (e.g., spp_top_level_groups) and populate it with the files and folders shown above.
Define Module Manifest#
Create a manifest file with the necessary dependencies and data files:
{
"name": "OpenSPP Top Level Groups",
"summary": "Adds top-level group types like villages and communities to the registry system.",
"category": "OpenSPP",
"version": "17.0.1.0.0",
"author": "Your Organization",
"website": "https://your-website.com",
"license": "LGPL-3",
"depends": [
"base",
"g2p_registry_base",
"g2p_registry_individual",
"g2p_registry_group",
"g2p_registry_membership",
"spp_registry_group_hierarchy",
],
"data": [
"data/group_kind_data.xml",
"views/group_kind_views.xml",
"views/top_level_group_views.xml",
"views/group_views.xml",
"security/ir.model.access.csv",
],
"application": True,
"installable": True,
"auto_install": False,
}
Add Custom Group Kind Data#
Create data/group_kind_data.xml to define the new group kind:
<odoo>
<data noupdate="1">
<record id="group_kind_village" model="g2p.group.kind">
<field name="name">Village</field>
<field name="description">A village that contains multiple households</field>
<field name="allow_all_member_type">True</field>
</record>
</data>
</odoo>
Extend the Registry Model#
Create models/res_partner.py to add custom fields, indicators, and actions:
from odoo import fields, models, api
class G2PTopLevelGroup(models.Model):
_inherit = "res.partner"
village_code = fields.Char("Village Code", help="Official village code or identifier")
population = fields.Integer("Population", help="Total population of the village/community")
total_households = fields.Integer(
"Total Households", compute="_compute_total_households", store=True,
help="Total number of households in this village"
)
total_individuals = fields.Integer(
"Total Individuals", compute="_compute_total_individuals", store=True,
help="Total number of individuals in this village"
)
avg_household_size = fields.Float(
"Average Household Size", compute="_compute_avg_household_size", store=True,
help="Average number of individuals per household"
)
def _compute_total_households(self):
for rec in self:
if not rec.is_group or not rec.kind or rec.kind.name != 'Village':
rec.total_households = 0
continue
household_groups = rec.group_membership_ids.mapped('individual').filtered(
lambda x: x.is_group and x.kind and x.kind.name == 'Household'
)
rec.total_households = len(household_groups)
def _compute_total_individuals(self):
for rec in self:
if not rec.is_group or not rec.kind or rec.kind.name != 'Village':
rec.total_individuals = 0
continue
total = 0
household_groups = rec.group_membership_ids.mapped('individual').filtered(
lambda x: x.is_group and x.kind and x.kind.name == 'Household'
)
for household in household_groups:
total += len(household.group_membership_ids.mapped('individual').filtered(lambda x: not x.is_group))
rec.total_individuals = total
def _compute_avg_household_size(self):
for rec in self:
if rec.total_households > 0:
rec.avg_household_size = rec.total_individuals / rec.total_households
else:
rec.avg_household_size = 0.0
def action_view_households(self):
self.ensure_one()
household_groups = self.group_membership_ids.mapped('individual').filtered(
lambda x: x.is_group and x.kind and x.kind.name == 'Household'
)
return {
'name': f'Households in {self.name}',
'type': 'ir.actions.act_window',
'res_model': 'res.partner',
'view_mode': 'tree,form',
'domain': [('id', 'in', household_groups.ids)],
'context': {'default_is_group': True, 'default_kind': self.env.ref('g2p_registry_group.group_kind_household').id},
}
def action_view_individuals(self):
self.ensure_one()
all_individuals = self.env['res.partner']
household_groups = self.group_membership_ids.mapped('individual').filtered(
lambda x: x.is_group and x.kind and x.kind.name == 'Household'
)
for household in household_groups:
individuals = household.group_membership_ids.mapped('individual').filtered(lambda x: not x.is_group)
all_individuals |= individuals
return {
'name': f'Individuals in {self.name}',
'type': 'ir.actions.act_window',
'res_model': 'res.partner',
'view_mode': 'tree,form',
'domain': [('id', 'in', all_individuals.ids)],
'context': {'default_is_group': False},
}
Extend Group Kind Model#
Create models/group_kind.py to add custom flags:
from odoo import fields, models
class SPPGroupKind(models.Model):
_inherit = "g2p.group.kind"
is_top_level_group = fields.Boolean("Is Top Level Group", default=False, help="Indicates if this group kind represents a top-level group")
can_contain_households = fields.Boolean("Can Contain Households", default=False, help="Indicates if this group kind can contain household groups")
Create View Extensions#
Top Level Group Views (views/top_level_group_views.xml)#
Update all field names to remove the prefix:
<odoo>
<!-- Village Form View -->
<record id="view_villages_form" model="ir.ui.view">
<field name="name">view_villages_form</field>
<field name="model">res.partner</field>
<field name="arch" type="xml">
<form string="Village">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_view_households" type="object" class="oe_stat_button" icon="fa-home">
<field name="total_households" widget="statinfo" string="Households"/>
</button>
<button name="action_view_individuals" type="object" class="oe_stat_button" icon="fa-users">
<field name="total_individuals" widget="statinfo" string="Individuals"/>
</button>
</div>
<div class="oe_title mb24">
<h1>
<field name="name" placeholder="Village Name"/>
</h1>
</div>
<group>
<group>
<field name="kind" domain="[('name', '=', 'Village')]"/>
<field name="village_code"/>
<field name="population"/>
</group>
<group>
<field name="avg_household_size"/>
<field name="is_group" invisible="1"/>
</group>
</group>
<notebook>
<page string="Members" name="members">
<field name="group_membership_ids" context="{'default_group': active_id}">
<tree editable="bottom">
<field name="individual" domain="[('is_group', '=', True), ('kind.name', '=', 'Household')]"/>
<field name="kind"/>
<field name="start_date"/>
<field name="ended_date"/>
<field name="status"/>
</tree>
</field>
</page>
<page string="Statistics" name="statistics">
<group>
<group>
<field name="total_households"/>
<field name="total_individuals"/>
</group>
<group>
<field name="avg_household_size"/>
</group>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Village Tree View -->
<record id="view_villages_tree" model="ir.ui.view">
<field name="name">view_villages_tree</field>
<field name="model">res.partner</field>
<field name="arch" type="xml">
<tree string="Villages">
<field name="name"/>
<field name="kind"/>
<field name="village_code"/>
<field name="population"/>
<field name="total_households"/>
<field name="total_individuals"/>
<field name="avg_household_size"/>
</tree>
</field>
</record>
<!-- Village Search View -->
<record id="view_villages_search" model="ir.ui.view">
<field name="name">view_villages_search</field>
<field name="model">res.partner</field>
<field name="arch" type="xml">
<search string="Villages">
<field name="name"/>
<field name="village_code"/>
<field name="kind"/>
<filter string="Villages" name="villages" domain="[('kind.name', '=', 'Village')]"/>
<group expand="0" string="Group By">
<filter string="Group Kind" name="group_kind" context="{'group_by': 'kind'}"/>
</group>
</search>
</field>
</record>
<!-- Villages Action -->
<record id="action_villages" model="ir.actions.act_window">
<field name="name">Villages</field>
<field name="res_model">res.partner</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('is_group', '=', True), ('kind.name', '=', 'Village')]</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('view_villages_tree')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('view_villages_form')})]"/>
<field name="search_view_id" ref="view_villages_search"/>
<field name="context">{'default_is_group': True, 'default_kind': ref('group_kind_village')}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first village!
</p>
<p>
Villages can contain multiple households as members.
</p>
</field>
</record>
<!-- Menu Item -->
<menuitem id="menu_villages"
name="Villages"
parent="g2p_registry_group.menu_groups"
action="action_villages"
sequence="20"/>
</odoo>
Group Kind Views (views/group_kind_views.xml)#
<odoo>
<record id="view_group_kind_tree_top_level" model="ir.ui.view">
<field name="name">view_group_kind_tree_top_level</field>
<field name="model">g2p.group.kind</field>
<field name="inherit_id" ref="g2p_registry_group.view_group_kind_tree" />
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="is_top_level_group"/>
<field name="can_contain_households"/>
</xpath>
</field>
</record>
</odoo>
Group Views (views/group_views.xml)#
<odoo>
<record id="view_groups_form_village_info" model="ir.ui.view">
<field name="name">view_groups_form_village_info</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="g2p_registry_group.view_groups_form" />
<field name="arch" type="xml">
<xpath expr="//page[@name='basic_info']/group/group[1]" position="after">
<group colspan="2">
<field name="group_membership_ids" invisible="1"/>
<field name="belongs_to_village"
compute="_compute_belongs_to_village"
readonly="1"
string="Belongs to Village"/>
</group>
</xpath>
</field>
</record>
</odoo>
Add Security Access#
Create security/ir.model.access.csv:
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_top_level_groups_admin,top.level.groups.admin,base.model_res_partner,g2p_registry_base.group_g2p_admin,1,1,1,1
Install and Test#
Install the module via the Apps menu.
Configure group kinds: Registry > Configuration > Group Kinds. Ensure "Village" is present and allows group/individual members.
Create villages: Registry > Groups > Villages. Add households as members.
Test: Use action buttons to view households and individuals. Check computed indicators.
Example Use Case#
Village A (Top Level Group)
├── Household 1 (Regular Group)
│ ├── John Doe (Individual)
│ └── Jane Doe (Individual)
└── Household 2 (Regular Group)
├── Bob Smith (Individual)
└── Alice Smith (Individual)
Best Practices#
Use descriptive names for group kinds.
Enable hierarchy only for appropriate group kinds.
Use domains and validation to enforce group relationships.
Provide clear navigation and action buttons for users.
References#
For more information on extending OpenSPP modules, refer to:
Related guides: Customizing Fields, Customizing Indicators
openspp.org