ADR-016 Phase 3: CEL Integration for vocabulary-aware expressions
This module extends the CEL (Common Expression Language) system with vocabulary-aware functions that enable robust eligibility rules across different deployment vocabularies.
Note: Both
randmeare valid prefixes for the registrant symbol. This guide usesr(matching ADR-008). Themealias is available via YAML profile configuration.
Resolve a vocabulary code by URI or alias.
r.gender_id == code("urn:iso:std:iso:5218#2") # By URI
r.gender_id == code("female") # By alias
r.gender_id == code("babae") # Local alias (Philippines)
Check if a vocabulary code belongs to a concept group.
in_group(r.gender_id, "feminine_gender")
members.exists(m, in_group(m.gender_id, "feminine_gender"))
Safe code comparison handling local code mappings.
code_eq(r.gender_id, "female")
User-friendly functions for common checks:
is_female(code_field)- Check if code is in feminine_gender groupis_male(code_field)- Check if code is in masculine_gender groupis_head(code_field)- Check if code is in head_of_household groupis_pregnant(code_field)- Check if code is in pregnant_eligible grouphead(member)- Check if a member is the head of household (takes a member record, not a code field)
is_female(r.gender_id)
# Pregnant women or mothers with children under 5
# Note: pregnancy_status_id is provided by country-specific modules
is_pregnant(r.pregnancy_status_id) or
members.exists(m, age_years(m.birthdate) < 5)
# Works in any deployment, even with local terminology
# Note: hazard_type_id is provided by country-specific modules
in_group(r.hazard_type_id, "climate_hazards")
The code() function resolves identifiers in this order:
- Full URI (e.g.,
"urn:iso:std:iso:5218#2") - Code value in active vocabulary
- Label (display value)
- Reference URI mapping (for local codes)
Concept groups provide semantic abstraction:
- Business logic checks concepts, not specific code values
- Works across deployments with different vocabularies
- Supports local codes via
reference_urimapping
Example:
# Concept Group: feminine_gender
- urn:iso:std:iso:5218#2 (Female, ISO standard)
- urn:openspp:ph:vocab:gender#babae (Babae, PH local → maps to Female)CEL expressions are translated to Odoo domains:
in_group(r.gender_id, "feminine_gender")
Translates to:
["|",
("gender_id.uri", "in", ["urn:iso:std:iso:5218#2", "urn:openspp:ph:vocab:gender#babae"]),
("gender_id.reference_uri", "in", ["urn:iso:std:iso:5218#2", ...])
]- Install dependencies:
spp_cel_domain,spp_vocabulary - Install this module
- Functions are automatically registered on installation
Standard concept groups are created automatically via post_init_hook on module
installation (search-or-create pattern, safe for upgrades). They have no XML IDs — look
them up by name.
To add codes to a group via data files:
<!-- Look up by name since groups are created by hook, not XML -->
<function model="spp.vocabulary.concept.group" name="search">
<!-- Use write() to add codes after finding the group -->
</function>Or via Python:
group = env['spp.vocabulary.concept.group'].search(
[('name', '=', 'feminine_gender')], limit=1
)
group.write({'code_ids': [Command.link(female_code.id)]})Map local codes to standard codes:
<record id="code_babae" model="spp.vocabulary.code">
<field name="vocabulary_id" ref="vocab_gender_ph" />
<field name="code">babae</field>
<field name="display">Babae (Female)</field>
<field name="is_local">True</field>
<field name="reference_uri">urn:iso:std:iso:5218#2</field>
<field name="equivalence">equivalent</field>
</record>spp_cel_vocabulary/
├── __init__.py # post_init_hook, concept group creation
├── __manifest__.py
├── models/
│ ├── __init__.py
│ ├── cel_vocabulary_functions.py # Function registration
│ └── cel_vocabulary_translator.py # Domain translation
├── services/
│ ├── __init__.py
│ ├── cel_vocabulary_functions.py # Pure Python functions
│ └── vocabulary_cache.py # Session-scoped cache
├── tests/
│ ├── __init__.py
│ ├── test_cel_vocabulary.py # Core function and translation tests
│ ├── test_vocabulary_cache.py # Cache behavior tests
│ ├── test_vocabulary_in_exists.py # Vocabulary in exists() predicates
│ └── test_init_and_coverage.py # Init, edge cases, coverage
├── security/
│ └── ir.model.access.csv
└── data/
├── concept_groups.xml # Documentation (groups created by hook)
└── README.md # Data configuration guide
- Pure Functions - Services contain stateless Python functions
- Environment Injection - Functions marked with
_cel_needs_env=True; CEL service injects fresh env at evaluation time - Function Registry - Dynamic registration with CEL system
- Domain Translation - AST transformation to Odoo domains
- Two-Layer Caching -
@ormcache(registry-scoped) +VocabularyCache(session-scoped)
Run tests:
./scripts/test_single_module.sh spp_cel_vocabulary- ADR-016: Vocabulary Profiles and Code URIs
- ADR-009: Vocabulary System
- CEL Domain module:
spp_cel_domain - Vocabulary module:
spp_vocabulary
Before (fragile):
r.gender_id == "female"
After (robust):
# Option 1: Semantic helper
is_female(r.gender_id)
# Option 2: Concept group
in_group(r.gender_id, "feminine_gender")
# Option 3: Safe comparison
code_eq(r.gender_id, "female")
Before:
if member.pregnancy_status_id.code == "pregnant":
grant_maternal_benefit()After:
group = env['spp.vocabulary.concept.group'].search(
[('name', '=', 'pregnant_eligible')], limit=1
)
if group.contains(member.pregnancy_status_id):
grant_maternal_benefit()Or use CEL:
# Note: pregnancy_status_id is provided by country-specific modules
in_group(r.pregnancy_status_id, "pregnant_eligible")
- Deployment Flexibility - Works with any vocabulary configuration
- Local Terminology - Supports country-specific codes seamlessly
- Semantic Clarity - Business logic expresses intent, not implementation
- Interoperability - URI-based identification enables data exchange
- Maintainability - Changes to vocabularies don't break logic
- Code resolution uses
@ormcachefor fast lookups - Concept group URIs are pre-computed and stored as JSON
- Session-scoped
VocabularyCacheeliminates N+1 queries during evaluation - Domain translation happens once at compile time
- No per-record overhead in query execution
- All functions validated against CEL security model
- No direct database access from expressions
- Environment injection controlled by module
- Attribute access blocked via CEL's safe evaluation
- Requires
spp_vocabularymodule with URI support - Concept groups must be defined before use in expressions
- Local codes require explicit reference_uri mapping
- Functions only work with vocabulary code fields (Many2one to spp.vocabulary.code)
- Auto-discovery of concept groups from vocabulary metadata
- Expression linting/validation warnings for non-profile codes
- UI for testing vocabulary functions with sample data
- Performance monitoring and caching statistics
- Additional semantic helpers based on deployment needs
Follow OpenSPP development guidelines:
- Read
CLAUDE.mdin project root - Follow naming conventions (
spp_*prefix) - Write tests (85%+ coverage target)
- Update this README for new functions
LGPL-3
OpenSPP.org