Configure user roles
Role configuration is where compliance lives or dies. The shipped seven-group hierarchy gives you a defensible SOC 2 evidence trail out of the box — provided every staff member is assigned the right group and nothing else.

The seven shipped groups
Each group is implicit-rebased (an Officer is a portal user with extra rights; a Branch Manager is a Supervisor with extra rights; and so on). Never tick more than one of these groups for the same user.
| Group | XML id | Can… | Cannot… |
|---|---|---|---|
| Loan Officer | mfi_base.group_officer | Capture members, take applications, draft savings transactions | Approve loans, override fees, close session |
| Senior Officer | mfi_base.group_senior_officer | All Officer + approve loans ≤ KES 50K, override late fees | Write off, change product config |
| Branch Supervisor | mfi_base.group_supervisor | All Senior + approve ≤ KES 250K, close session, second leg of two-person | Write off > KES 50K, change product config |
| Branch Manager | mfi_base.group_branch_manager | All Supervisor + approve ≤ KES 1M, vault open/close, hire | System config, GL posting |
| Credit Committee | mfi_base.group_credit_committee | Approve ≤ KES 5M, sit on write-off committee | Originate, disburse |
| Internal Audit | mfi_base.group_audit | Read all, run audit reports, sign-off evidence | Write anything |
| System Admin | mfi_base.group_system_admin | Config, install, upgrade | Originate, approve, disburse — segregation enforced |
Create staff users

For each staff member open Settings → Users & Companies → Users → New and capture:
- Login: the staff member's corporate email; never use 'admin' or shared logins.
- Authentication: enable TOTP via auth_totp — required by SOC 2 control AC-7.
- Allowed companies: the parent company plus any subsidiary they cover.
- Default branch: the branch they sit in (Mumias for a Mumias loan officer).
- Allowed branches: usually the same as default branch; a roving officer may have multiple.
- Group: exactly one of the seven shipped groups.
- HR linked employee: the matching hr.employee for officer-performance tracking.
Segregation-of-duties matrix
The shipped two-person rules (see next page) enforce these blocks at the action level. The matrix here documents the policy.
| Action | Required role | Cannot also be |
|---|---|---|
| Originate loan | Officer / Senior Officer | The approver of the same loan |
| Approve loan band 1 | Senior Officer | The originator |
| Approve loan band 2 | Supervisor | The originator |
| Approve loan band 3 | Branch Manager | The originator or supervisor |
| Approve loan band 4 | Credit Committee (3 of 5) | The originator |
| Disburse via M-Pesa | Teller | The approver |
| Cash out > KES 100K | Teller + Supervisor | Same person |
| Open vault | Branch Manager + Supervisor | Same person |
| Write off loan | Credit Committee + Audit sign-off | The originator or any prior approver |
| Change product config | System Admin | Anyone in the originate/approve chain (rotate) |
Officer performance KPIs
The mfi_officer_perf module ships with seven KPIs that score every officer monthly. Link each user to an hr.employee so the scorecard fills.
- New members onboarded — count of members with state=active in the month.
- Loans originated — count of loans applied for.
- Loans approved on first appraisal — quality proxy.
- Portfolio at risk > 30 days — must be ≤ 5%.
- Average days to first call — for dunning officers, must be ≤ 3.
- Cash variance count — must be 0.
- Member NPS — pulled from the iBANK post-transaction survey.
What gets audited
Every login, every group change, every record rule override is logged by mfi_audit. The Internal Audit user can pull the trail from MFI → Audit → Access log.
-- Sample query: who approved every loan in Q1
SELECT loan.loan_number, loan.approved_amount,
u_approver.login AS approver,
loan.approval_date
FROM mfi_loan loan
JOIN res_users u_approver ON u_approver.id = loan.approved_by_id
WHERE loan.approval_date BETWEEN '2026-01-01' AND '2026-03-31'
ORDER BY loan.approval_date;
Worked scenarios
Scenario — Onboarding Peter as a new Mumias loan officer
| Character | Role |
|---|---|
| Peter Otieno | New loan officer, Mumias |
| Florence Achieng | System admin |
| Kimani Mwangi | Finance manager — approves the new user request |
Timeline
- Day 1, 11:00: Florence opens Settings → Users → New, sets login peter.otieno@bridgemfi.co.ke. (res.users draft)
- Day 1, 11:02: She picks group Loan Officer (single), default branch Mumias, allowed branches Mumias. (Groups + branches set)
- Day 1, 11:05: She links to hr.employee Peter Otieno already created by HR. (Officer KPI scorecard initialised at 0)
- Day 1, 11:08: She enables auth_totp and shows Peter the QR code. (TOTP secret stored)
- Day 1, 11:10: Peter installs Microsoft Authenticator and confirms the 6-digit token works. (TOTP confirmed)
- Day 1, 11:15: Kimani receives the auto-email request and approves it in the Sign tool. (Approval log written)
- Day 1, 11:20: Florence runs a smoke test: logs in as Peter, tries to approve a loan — blocked as expected. (Record rule denial logged)
- Day 1, 11:25: Florence tries to originate a loan as Peter — allowed. (mfi.loan draft created)
- Day 3, 08:00: Peter starts work; first member onboarded by 09:15. (Live)
Outcome — Peter is operational on day 1 with a clean audit trail, MFA enforced, and the right blocks in place.
Reference
res.users key fields
| Field | Type | Notes |
|---|---|---|
| login | Char | Corporate email, unique |
| totp_secret | Char (encrypted) | Required for SOC 2 |
| company_ids | Many2many | Allowed companies |
| company_id | Many2one | Default company |
| x_default_branch_id | Many2one | res.branch — default |
| x_allowed_branch_ids | Many2many | res.branch — allowed set |
| groups_id | Many2many | Pick ONE of seven shipped |
| employee_id | Many2one | hr.employee for KPI |
| session_lifetime_override | Integer | Hours; 0 = use system default |
XML ids of shipped groups
| Group | XML id | Implied by |
|---|---|---|
| Loan Officer | mfi_base.group_officer | base.group_user |
| Senior Officer | mfi_base.group_senior_officer | group_officer |
| Supervisor | mfi_base.group_supervisor | group_senior_officer |
| Branch Manager | mfi_base.group_branch_manager | group_supervisor |
| Credit Committee | mfi_base.group_credit_committee | group_supervisor (parallel branch) |
| Internal Audit | mfi_base.group_audit | base.group_user (read-only override) |
| System Admin | mfi_base.group_system_admin | base.group_system |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| User can see all branches' members despite default branch set | Allowed branches list empty (None = all) | Set allowed_branch_ids explicitly to the same branch as default_branch_id. |
| Officer can approve a loan band 1 | Group set to Senior Officer not Officer | Demote to Officer; remove Senior Officer; save. |
| Approval log shows 'system' instead of the user | Action executed via server action without sudo_uid | Patch server action to use self.with_user(env.user).action_approve() not sudo(). |
| Cannot delete a former employee's user | Audit log foreign keys block delete | Archive (active=False) instead — never delete; the audit trail must outlive the employment. |

