Country Overlays — Uganda, Kenya, Tanzania, Zanzibar
Each EAC country that has adopted HCPI ships a small overlay of modules that layers its own administrative geography, outlet linkage, and (sometimes) workflow tweaks on top of the generic hcpi_* core. The generic modules don't know what a "district" is; the overlays fill those slots in.
This page covers all four current overlays — Uganda, Kenya, Tanzania, Zanzibar — and ends with the recipe for adding a fifth.
See Country Variants for the broader story of branches and how country code is kept separate from the EAC base.
At a glance
| Country | Branch | Modules | Location levels | Notes |
|---|---|---|---|---|
| Uganda | ug_18 |
ug_location, ug_outlet, ug_base_import |
5 (region → district → county → sub-county → parish) | No own data_collection overlay — uses the base directly. |
| Kenya | ke_18_mtnce |
ke_location, ke_outlet, ke_data_collection, ke_base_import |
3 (region → county → zone) | Zone is the assignment unit; supervisor/collector live on it. Adds a review state. |
| Tanzania | tz_18_mtnce |
tz_location, tz_outlet, tz_data_collection, tz_base_import |
2 (region → district) | District is the assignment unit. Adds bulk + assign questionnaire wizards. |
| Zanzibar | zar_18 |
zar_location, zar_outlet, zar_data_collection |
3 (region → district → collection center) | Uses "collection center" as the leaf — closer to operational reality. No own base_import. |
The level count and the model that sits at the bottom — parish / zone / district / collection center — is the single biggest structural difference between countries. Everything else hangs off that choice.
Uganda (ug_18)
The original adopter and the model the others follow most closely.
ug_location — five-level hierarchy
Each level:
name(Char, required, tracked).code— auto-padded with a leading zero under 10 (so1displays as01).- FK to its parent.
- Inherits
mail.activity.mixin.
The parish is the leaf — what an outlet attaches to.
ug_outlet — extends hcpi.outlet
class HcpiOutletInherit(models.Model):
_inherit = 'hcpi.outlet'
parish_id = fields.Many2one('ug.parish', ondelete='restrict')
sub_county_id = fields.Many2one('ug.sub.county',
related='parish_id.sub_county_id', store=True)
county_id = fields.Many2one('ug.county',
related='sub_county_id.county_id', store=True)
district_id = fields.Many2one('ug.district',
related='county_id.district_id', store=True, readonly=False)
region_id = fields.Many2one('ug.region',
related='county_id.region_id', store=True)
The stored-related chain is the pattern every country uses for fast filtering — "all outlets in Kampala district" becomes a single equality, no joins required.
ug_base_import
Tiny module. Plugs Uganda-specific validation into base_import_inherit (mostly Uganda location-code parsing).
What's missing from the Uganda overlay
No ug_data_collection. Uganda uses the base hcpi_data_collection state machine and workflow unchanged. Supervisor/collector assignment happens by other means (manual on the questionnaire, or via the base groups).
Kenya (ke_18_mtnce)
Three location levels and the assignment unit moved into the geography.
ke_location — three-level hierarchy
ke.regionandke.countyare standard — name, code, paddedregion_code/county_codecomputes,mail.thread+mail.activity.mixin.ke.zoneis where Kenya gets interesting. It carries the supervisor and collector assignment directly:
class KeZone(models.Model):
_name = 'ke.zone'
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char(required=True, tracking=True)
county_id = fields.Many2one('ke.county', required=True, tracking=True)
region_id = fields.Many2one('ke.region',
related='county_id.region_id', store=True)
code = fields.Integer(required=True, tracking=True)
# Assignment moved into the location hierarchy
data_supervisor_id = fields.Many2one('res.users',
domain=lambda self: [
('groups_id', 'in', self.env.ref('hcpi_data_collection.group_data_collection_supervisor').id),
('groups_id', 'not in', self.env.ref('hcpi_data_collection.group_data_collection_statician').id),
])
data_collector_id = fields.Many2one('res.users', domain=lambda self: [...])
data_supervisor_email = fields.Char()
data_collector_email = fields.Char()
zone_code = fields.Char(compute='_compute_zone_code') # "01.02.03"
That domain=lambda self: [...] on the M2O fields is filtered by group membership — only users in the group_data_collection_supervisor group (and not also in _statician) can be picked as a zone's supervisor. Same idea for the collector.
ke_outlet — extends hcpi.outlet
zone_id = fields.Many2one('ke.zone', ondelete='restrict')
zone_code = fields.Integer(related='zone_id.code', store=True, readonly=False)
county_id = fields.Many2one('ke.county', related='zone_id.county_id', store=True)
region_id = fields.Many2one('ke.region', related='zone_id.region_id', store=True)
Shorter chain than Uganda's because Kenya only has three levels.
ke_data_collection — adds a review state
This is where Kenya diverges from the base workflow. Two things happen:
1. The state machine gains a review step:
state = fields.Selection(selection=[
('draft', 'Draft'),
('survey', 'Price Survey'),
('review', 'Review'), # ← Kenya-specific
('standardization', 'Standardization'),
('validation', 'Validation'),
('done', 'Done'),
('cancel', 'Cancelled'),
], default='draft', tracking=True)
2. Supervisor and collector flow through from the zone, not set manually:
zone_id = fields.Many2one('ke.zone', related='outlet_id.zone_id')
data_supervisor_id = fields.Many2one('res.users', related='outlet_id.zone_id.data_supervisor_id')
data_collector_id = fields.Many2one('res.users', related='outlet_id.zone_id.data_collector_id')
When you create a questionnaire for an outlet, the people responsible are already known — the zone has them.
3. A submit_for_review and a customised validate method that auto-creates an hcpi.data.validation record per (zone, month) so reviews are batched by area and time period.
ke_base_import
Same shape as ug_base_import — Kenya-specific row validation on top of base_import_inherit.
Tanzania (tz_18_mtnce)
Two location levels — the simplest hierarchy of the four.
tz_location — two-level hierarchy
tz.region— standard region model withcodevalidation (@api.constrains('code')rejects zero).tz.district— like Kenya's zone, carries supervisor and collector assignment with the samegroups_id-filtered domain.
tz_outlet — extends hcpi.outlet
district_id = fields.Many2one('tz.district', ondelete='restrict')
district_code = fields.Integer(related='district_id.code', store=True, readonly=False)
region_id = fields.Many2one('tz.region', related='district_id.region_id', store=True)
tz_data_collection — adds bulk + assign wizards
Tanzania's workflow extension is wizard-driven rather than state-driven (no extra review state like Kenya).
| Wizard | What it does |
|---|---|
bulk_questionnaire_wizard |
Create many questionnaires at once across selected outlets/items. |
assign_questionnaire_wizard |
Reassign existing questionnaires to a different supervisor or collector. |
If your country needs mass questionnaire creation at the start of each pricing cycle, this is the pattern to copy.
tz_base_import
Same shape as the others.
Zanzibar (zar_18)
Three levels, with the leaf named for what it actually is.
zar_location — three-level hierarchy
The leaf is named zar.collection.center rather than a generic administrative term — closer to the operational reality (a collection centre is a specific physical place price data is gathered).
zar.region exposes smart-button counts and a supervisor_id at the regional level:
class ZarRegion(models.Model):
_name = 'zar.region'
code = fields.Char(required=True, tracking=True) # Note: Char, not Integer
name = fields.Char(required=True, tracking=True)
supervisor_id = fields.Many2one('res.users', string='Regional Supervisor')
district_ids = fields.One2many('zar.district', 'region_id')
district_count = fields.Integer(compute='_compute_district_count', store=True)
collection_center_count = fields.Integer(compute='_compute_collection_center_count', store=True)
zar.collection.center carries the operational assignment and computes a hierarchical code:
class ZarCollectionCenter(models.Model):
_name = 'zar.collection.center'
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char(required=True, tracking=True)
code = fields.Char(required=True, tracking=True)
district_id = fields.Many2one('zar.district', required=True, tracking=True)
region_id = fields.Many2one('zar.region',
related='district_id.region_id', store=True, readonly=True)
# "01.02" — region.code + center.code, joined
collection_center_code = fields.Char(compute='_compute_collection_center_code', store=True)
supervisor_id = fields.Many2one('res.users',
related='region_id.supervisor_id', readonly=False)
data_collector_id = fields.Many2one('res.users')
Notice supervisor_id is related from the region — Zanzibar manages supervision at the region level and inherits it down. The collector is set per centre.
zar_outlet — extends hcpi.outlet
collection_center_id = fields.Many2one('zar.collection.center', ondelete='restrict', tracking=True)
district_id = fields.Many2one('zar.district',
related='collection_center_id.district_id', store=True, readonly=True)
region_id = fields.Many2one('zar.region',
related='collection_center_id.region_id', store=True, readonly=True)
zar_data_collection — same wizards as Tanzania
Bulk + assign questionnaire wizards, no state-machine modification.
What's missing from the Zanzibar overlay
No zar_base_import. Zanzibar uses base_import_inherit directly. If you find yourself adding country-specific import validation for Zanzibar, this is the gap to fill.
Common patterns across all four
Despite the differences in level count and assignment unit, every country overlay does the same five things:
| Pattern | Where you see it |
|---|---|
Inherit mail.thread/mail.activity.mixin on location models |
All location levels in all four countries. |
code zero-padding via compute or formatting |
UG (auto), KE (_compute_*_code), TZ (_compute_*_code), ZAR (string concat). |
Stored related FKs on hcpi.outlet up the location chain |
All four — the fast-filter pattern. |
ondelete='restrict' on outlet → leaf-location FK |
All four — protects outlets from accidental geography deletion. |
group_data_collection_*-filtered domains on supervisor/collector pickers |
KE (on zone), TZ (on district), ZAR (per region). UG does this on the base questionnaire instead. |
And two patterns specific to some countries:
| Pattern | Countries |
|---|---|
Assignment moved into the geography (data_supervisor_id + data_collector_id on the leaf location, not the questionnaire) |
Kenya (ke.zone), Tanzania (tz.district), Zanzibar (zar.collection.center). Uganda does not — it stays on the questionnaire. |
| Bulk / assign wizards | Tanzania, Zanzibar. Kenya and Uganda don't ship them. |
Extra workflow state (review) |
Kenya only. |
The recipe for a new country
Standing up HCPI for a fifth country (say, Rwanda = rw_*):
Step 1: branch
Cut a new branch off 18.0:
Country code lives on this branch only. The generic hcpi_* modules on 18.0 don't change.
Step 2: rw_location/
Mirror the country's actual administrative hierarchy. Don't copy Uganda's five levels just because — match what the national statistics office uses. Rwanda might be: rw.province → rw.district → rw.sector → rw.cell (4 levels).
Files:
rw_location/
├── __init__.py
├── __manifest__.py
├── models/
│ ├── __init__.py
│ ├── rw_province.py
│ ├── rw_district.py
│ ├── rw_sector.py
│ └── rw_cell.py
├── data/
│ └── rw_geography.xml ← seed the real hierarchy from NSO data
├── security/
│ ├── security.xml
│ └── ir.model.access.csv
└── views/
├── rw_province_views.xml
├── ...
└── rw_location_menus.xml
Decide on each level whether to:
- Track changes (
mail.thread). - Carry supervisor/collector assignment (the KE/TZ/ZAR pattern) or leave that on the questionnaire (the UG pattern).
Step 3: rw_outlet/
Mirror one of the existing outlet overlays. Pick the leaf location as the M2O target, then stored-related FK up the chain.
Step 4 (optional): rw_data_collection/
Only if you need to:
- Add a workflow state (like KE's
review). - Add bulk/assign wizards (like TZ/ZAR).
- Override supervisor/collector flow-through from the geography.
If the base workflow does what you want, skip this module — Uganda does. Less code, fewer surfaces to maintain.
Step 5 (optional): rw_base_import/
Only if you need country-specific import validation (parsing Rwanda location codes from CSV/Excel headers, etc.). Zanzibar skipped this — country with simple imports can use base_import_inherit directly.
Step 6: update addons_path
hcpi.conf addons_path is what tells Odoo where to find the new modules. After dropping rw_location/, rw_outlet/, etc. into custom/, restart Odoo with -u rw_location -u rw_outlet -u rw_data_collection -u rw_base_import (or update all from the Apps screen with developer mode on).
Where to look to change something
| You want to… | Open |
|---|---|
| Add a geographic level to Uganda | ug_location/models/ — new model + FK on the level above + view file |
| Reassign a Kenyan zone's supervisor | ke.zone form (UI) — and the data_supervisor_id filter domain if you need to broaden who's pickable |
| Add a workflow state for a country | The country's *_data_collection (Kenya is the worked example); if there isn't one, create it |
| Add a bulk questionnaire wizard | Copy Tanzania's bulk_questionnaire_wizard model + view, adjust for the country's location leaf |
| Change Zanzibar's supervisor delegation | zar.region.supervisor_id cascades down to zar.collection.center.supervisor_id via related= — change the cascade or the source |
| Show the full location breadcrumb on the outlet form | The country's *_outlet/views/hcpi_outlet_views.xml — add the stored-related fields |
| Reject outlets with invalid location codes on import | The country's *_base_import/models/base_import.py (create the module if missing) |
| Change which users are pickable as data supervisors | The domain=lambda self: [...] on the leaf location's data_supervisor_id field |
Common gotchas
store=Truerelated fields can drift. If you bulk-update via SQL or restore an old backup, the stored-relatedregion_id/district_id/etc. on outlets may not match the current geography. Re-trigger the compute by writing the source field back to itself in the shell:- Don't put country logic in the generic modules. "Just one tiny
if country == 'KE'" inhcpi_outletis exactly how core gets polluted. Use_inheritfrom the overlay. - Match the country's real geography. Don't copy Uganda's five-level model out of habit. Tanzania has two because that's what TZ uses.
- The
domain=lambda self: [...]group filter on supervisor/collector depends on the user actually being in the righthcpi_data_collectiongroup. If supervisors disappear from the dropdown, check Settings → Users — they probably lost their group assignment. - The
mtncebranches are the active ones. Kenya and Tanzania each ship two_18branches — the plain one is the initial port;_mtnceis what's actively maintained. Always check both before assuming a feature isn't there.
Next
- Country Variants — the broader branch / overlay / base-module picture across the EAC.
- Outlets — the generic model every country's
*_outletextends. - Data Collection — the base workflow the country overlays modify.
- Infrastructure —
base_import_inherit, the generic counterpart to the*_base_importoverlays.