From 6f07239a07b3d4fd5f8afa358725900b4ccc9ca9 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 13 Mar 2026 16:28:04 +0800 Subject: [PATCH 01/15] chore: add maintainers to spp_cel_vocabulary manifest --- spp_cel_vocabulary/__manifest__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/spp_cel_vocabulary/__manifest__.py b/spp_cel_vocabulary/__manifest__.py index 3c8e39e2..386b50e4 100644 --- a/spp_cel_vocabulary/__manifest__.py +++ b/spp_cel_vocabulary/__manifest__.py @@ -18,5 +18,6 @@ ], "installable": True, "auto_install": True, + "maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"], "post_init_hook": "post_init_hook", } From f6d825426dc0f59fc542d3a1cbf489addbf80785 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 13 Mar 2026 16:28:32 +0800 Subject: [PATCH 02/15] fix: use label instead of display_name in test concept group helper display_name is a computed field in Odoo and gets silently ignored during create(). The correct field on spp.vocabulary.concept.group is label. Also migrate old-style tuple to Command API. --- spp_cel_domain/tests/common.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/spp_cel_domain/tests/common.py b/spp_cel_domain/tests/common.py index d3b0d818..2cef25aa 100644 --- a/spp_cel_domain/tests/common.py +++ b/spp_cel_domain/tests/common.py @@ -7,6 +7,8 @@ import time +from odoo.fields import Command + class CELTestDataMixin: """Mixin for creating CEL test data. @@ -179,12 +181,12 @@ def _create_test_vocabulary_code(cls, vocabulary=None, code=None, display=None, return VocabularyCode.create(values) @classmethod - def _create_test_concept_group(cls, name=None, display_name=None, cel_function=None, codes=None, **kwargs): + def _create_test_concept_group(cls, name=None, label=None, cel_function=None, codes=None, **kwargs): """Create a test concept group. Args: name: Group name. If not provided, generates unique name - display_name: Display name. If not provided, uses name + label: Human-readable label. If not provided, generates from name cel_function: CEL function name codes: List of vocabulary code records to add to group **kwargs: Additional fields to pass to create() @@ -194,12 +196,12 @@ def _create_test_concept_group(cls, name=None, display_name=None, cel_function=N """ test_id = getattr(cls, "_test_id", int(time.time() * 1000)) name = name or f"test_group_{test_id}" - display_name = display_name or name.replace("_", " ").title() + label = label or name.replace("_", " ").title() ConceptGroup = cls.env["spp.vocabulary.concept.group"] values = { "name": name, - "display_name": display_name, + "label": label, } if cel_function: values["cel_function"] = cel_function @@ -208,7 +210,7 @@ def _create_test_concept_group(cls, name=None, display_name=None, cel_function=N group = ConceptGroup.create(values) if codes: - group.write({"code_ids": [(6, 0, [c.id for c in codes])]}) + group.write({"code_ids": [Command.set([c.id for c in codes])]}) return group From 8eadd2850c57667c8c2e5c0de4842f20f24ecfa4 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 13 Mar 2026 16:31:41 +0800 Subject: [PATCH 03/15] docs: fix field names in service docstrings and add sudo() justification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix r.gender → r.gender_id and m._link.kind → m.relationship_type in all function docstrings. Add comment explaining why sudo() is needed in head() function. --- .../services/cel_vocabulary_functions.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/spp_cel_vocabulary/services/cel_vocabulary_functions.py b/spp_cel_vocabulary/services/cel_vocabulary_functions.py index d824aced..9d8b1083 100644 --- a/spp_cel_vocabulary/services/cel_vocabulary_functions.py +++ b/spp_cel_vocabulary/services/cel_vocabulary_functions.py @@ -16,9 +16,9 @@ def code(env, identifier): """Resolve a vocabulary code by URI or alias. Usage in CEL: - r.gender == code("urn:iso:std:iso:5218#2") # By URI - r.gender == code("female") # By alias - r.gender == code("babae") # Local alias (PH) + 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 (PH) Args: env: Odoo environment (injected by CEL evaluator) @@ -51,8 +51,8 @@ def in_group(env, code_value, group_name): session-level caching for even better performance. Usage in CEL: - in_group(r.gender, "feminine_gender") - members.exists(m, in_group(m.gender, "feminine_gender")) + in_group(r.gender_id, "feminine_gender") + members.exists(m, in_group(m.gender_id, "feminine_gender")) Args: env: Odoo environment (injected by CEL evaluator) @@ -99,11 +99,11 @@ def code_eq(env, code_field, identifier): """Safe code comparison handling local code mappings. Usage in CEL: - code_eq(r.gender, "female") + code_eq(r.gender_id, "female") This is equivalent to: - r.gender.uri == "urn:iso:std:iso:5218#2" - OR r.gender.reference_uri == "urn:iso:std:iso:5218#2" + r.gender_id.uri == "urn:iso:std:iso:5218#2" + OR r.gender_id.reference_uri == "urn:iso:std:iso:5218#2" Args: env: Odoo environment (injected by CEL evaluator) @@ -167,7 +167,7 @@ def is_male(env, code_value): """Check if code is in masculine_gender group. Usage in CEL: - is_male(r.gender) + is_male(r.gender_id) Args: env: Odoo environment (injected by CEL evaluator) @@ -184,7 +184,7 @@ def is_head(env, code_value): Usage in CEL: is_head(r.relationship_type) - members.exists(m, is_head(m._link.kind)) + members.exists(m, is_head(m.relationship_type)) Args: env: Odoo environment (injected by CEL evaluator) @@ -200,7 +200,7 @@ def is_pregnant(env, code_value): """Check if code is in pregnant_eligible group. Usage in CEL: - is_pregnant(r.pregnancy_status) + is_pregnant(r.pregnancy_status_id) Args: env: Odoo environment (injected by CEL evaluator) @@ -232,7 +232,10 @@ def head(env, member, _membership=None, _group=None): if not member: return False - # Get the 'head' vocabulary code + # Get the 'head' vocabulary code. + # sudo() is needed because this function may be called in restricted user + # contexts during CEL evaluation, and the head membership type code is + # reference data that should always be accessible regardless of user ACLs. # nosemgrep: odoo-sudo-without-context head_code = env["spp.vocabulary.code"].sudo().get_code("urn:openspp:vocab:group-membership-type", "head") if not head_code: From 22c692b70e583f49b4215729ed40df1170bff3e4 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 13 Mar 2026 16:31:46 +0800 Subject: [PATCH 04/15] fix: improve translator error messages and fix docstring field names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add actionable guidance to warning messages (menu paths, field name hints). Fix r.gender → r.gender_id in docstring examples. --- .../models/cel_vocabulary_translator.py | 62 +++++++++++++++---- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/spp_cel_vocabulary/models/cel_vocabulary_translator.py b/spp_cel_vocabulary/models/cel_vocabulary_translator.py index bd639221..78ebe72b 100644 --- a/spp_cel_vocabulary/models/cel_vocabulary_translator.py +++ b/spp_cel_vocabulary/models/cel_vocabulary_translator.py @@ -90,7 +90,7 @@ def _handle_in_group(self, model: str, node: P.Call, cfg: dict[str, Any], ctx: d tuple: (plan, explain) for the in_group check Example: - in_group(r.gender, "feminine_gender") + in_group(r.gender_id, "feminine_gender") → ["|", ("gender_id.uri", "in", ["urn:iso:std:iso:5218#2", ...]), ("gender_id.reference_uri", "in", ["urn:iso:std:iso:5218#2", ...]) @@ -102,7 +102,11 @@ def _handle_in_group(self, model: str, node: P.Call, cfg: dict[str, Any], ctx: d field_name, field_model = self._resolve_field(model, field_node, cfg, ctx) field_name = self._normalize_field_name(field_model or model, field_name) except Exception as e: - _logger.warning("[CEL Vocabulary] Failed to resolve field in in_group(): %s", e) + _logger.warning( + "[CEL Vocabulary] Failed to resolve field in in_group(): %s. " + "Check that the field name matches the Odoo model field (e.g., gender_id, not gender).", + e, + ) return ( LeafDomain(model, [("id", "=", 0)]), "in_group() [FIELD RESOLUTION ERROR]", @@ -129,7 +133,11 @@ def _handle_in_group(self, model: str, node: P.Call, cfg: dict[str, Any], ctx: d group = self.env["spp.vocabulary.concept.group"].search([("name", "=", group_name)], limit=1) if not group: - _logger.warning("[CEL Vocabulary] Concept group '%s' not found, returning empty domain", group_name) + _logger.warning( + "[CEL Vocabulary] Concept group '%s' not found. " + "Check Settings > Vocabularies > Concept Groups.", + group_name, + ) # Return domain that matches nothing return ( LeafDomain(field_model or model, [("id", "=", 0)]), @@ -140,7 +148,11 @@ def _handle_in_group(self, model: str, node: P.Call, cfg: dict[str, Any], ctx: d uri_list = group.get_code_uris() if not uri_list: - _logger.warning("[CEL Vocabulary] Concept group '%s' has no codes", group_name) + _logger.warning( + "[CEL Vocabulary] Concept group '%s' has no codes. " + "Add codes via Settings > Vocabularies > Concept Groups.", + group_name, + ) return ( LeafDomain(field_model or model, [("id", "=", 0)]), f"in_group({field_name}, '{group_name}') [EMPTY GROUP]", @@ -197,7 +209,12 @@ def _handle_semantic_helper( field_name, field_model = self._resolve_field(model, field_node, cfg, ctx) field_name = self._normalize_field_name(field_model or model, field_name) except Exception as e: - _logger.warning("[CEL Vocabulary] Failed to resolve field in %s(): %s", func_name, e) + _logger.warning( + "[CEL Vocabulary] Failed to resolve field in %s(): %s. " + "Check that the field name matches the Odoo model field (e.g., gender_id, not gender).", + func_name, + e, + ) return ( LeafDomain(model, [("id", "=", 0)]), f"{func_name}() [FIELD RESOLUTION ERROR]", @@ -208,7 +225,8 @@ def _handle_semantic_helper( if not group: _logger.warning( - "[CEL Vocabulary] Concept group '%s' not found for %s(), returning empty domain", + "[CEL Vocabulary] Concept group '%s' not found for %s(). " + "Check Settings > Vocabularies > Concept Groups.", group_name, func_name, ) @@ -221,7 +239,12 @@ def _handle_semantic_helper( uri_list = group.get_code_uris() if not uri_list: - _logger.warning("[CEL Vocabulary] Concept group '%s' has no codes for %s()", group_name, func_name) + _logger.warning( + "[CEL Vocabulary] Concept group '%s' has no codes for %s(). " + "Add codes via Settings > Vocabularies > Concept Groups.", + group_name, + func_name, + ) return ( LeafDomain(field_model or model, [("id", "=", 0)]), f"{func_name}({field_name}) [GROUP EMPTY]", @@ -251,7 +274,7 @@ def _handle_code_eq(self, model: str, node: P.Call, cfg: dict[str, Any], ctx: di tuple: (plan, explain) for the code_eq check Example: - code_eq(r.gender, "female") + code_eq(r.gender_id, "female") → ["|", ("gender_id.uri", "=", "urn:iso:std:iso:5218#2"), ("gender_id.reference_uri", "=", "urn:iso:std:iso:5218#2") @@ -263,7 +286,11 @@ def _handle_code_eq(self, model: str, node: P.Call, cfg: dict[str, Any], ctx: di field_name, field_model = self._resolve_field(model, field_node, cfg, ctx) field_name = self._normalize_field_name(field_model or model, field_name) except Exception as e: - _logger.warning("[CEL Vocabulary] Failed to resolve field in code_eq(): %s", e) + _logger.warning( + "[CEL Vocabulary] Failed to resolve field in code_eq(): %s. " + "Check that the field name matches the Odoo model field (e.g., gender_id, not gender).", + e, + ) return ( LeafDomain(model, [("id", "=", 0)]), "code_eq() [FIELD RESOLUTION ERROR]", @@ -289,7 +316,8 @@ def _handle_code_eq(self, model: str, node: P.Call, cfg: dict[str, Any], ctx: di if not target_code: _logger.warning( - "[CEL Vocabulary] Could not resolve code identifier '%s', returning empty domain", + "[CEL Vocabulary] Could not resolve code identifier '%s'. " + "Verify the vocabulary code exists (check by URI, code value, or display name).", identifier, ) return ( @@ -329,7 +357,7 @@ def _handle_code_comparison( tuple: (plan, explain) for the comparison Example: - r.gender == code("female") + r.gender_id == code("female") → ["|", ("gender_id.uri", "=", "urn:iso:std:iso:5218#2"), ("gender_id.reference_uri", "=", "urn:iso:std:iso:5218#2") @@ -359,7 +387,11 @@ def _handle_code_comparison( field_name, field_model = self._resolve_field(model, field_node, cfg, ctx) field_name = self._normalize_field_name(field_model or model, field_name) except Exception as e: - _logger.warning("[CEL Vocabulary] Failed to resolve field in code comparison: %s", e) + _logger.warning( + "[CEL Vocabulary] Failed to resolve field in code comparison: %s. " + "Check that the field name matches the Odoo model field (e.g., gender_id, not gender).", + e, + ) return ( LeafDomain(model, [("id", "=", 0)]), "code() comparison [FIELD RESOLUTION ERROR]", @@ -386,7 +418,11 @@ def _handle_code_comparison( target_code = self.env["spp.vocabulary.code"].resolve_alias(identifier) if not target_code: - _logger.warning("[CEL Vocabulary] Could not resolve code '%s'", identifier) + _logger.warning( + "[CEL Vocabulary] Could not resolve code '%s'. " + "Verify the vocabulary code exists (check by URI, code value, or display name).", + identifier, + ) return ( LeafDomain(field_model or model, [("id", "=", 0)]), f"{field_name} {op} code('{identifier}') [NOT FOUND]", From 8170630566a0f39635ed7c914214aadd62881aef Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 13 Mar 2026 16:32:47 +0800 Subject: [PATCH 05/15] docs: fix README.md prefix, field names, structure diagram, and examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standardize on r prefix, fix gender → gender_id, add vocabulary_cache.py and all test files to structure diagram, update test command, fix XML example to use label and search-by-name pattern, add head() to helpers. --- spp_cel_vocabulary/README.md | 105 +++++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/spp_cel_vocabulary/README.md b/spp_cel_vocabulary/README.md index 82b27915..2f787945 100644 --- a/spp_cel_vocabulary/README.md +++ b/spp_cel_vocabulary/README.md @@ -5,6 +5,9 @@ This module extends the CEL (Common Expression Language) system with vocabulary-aware functions that enable robust eligibility rules across different deployment vocabularies. +> **Note:** Both `r` and `me` are valid prefixes for the registrant symbol. This guide +> uses `r` (matching ADR-008). The `me` alias is available via YAML profile configuration. + ## Features ### Core Functions @@ -14,9 +17,9 @@ functions that enable robust eligibility rules across different deployment vocab Resolve a vocabulary code by URI or alias. ```cel -me.gender == code("urn:iso:std:iso:5218#2") # By URI -me.gender == code("female") # By alias -me.gender == code("babae") # Local alias (Philippines) +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) ``` #### `in_group(code_field, group_name)` @@ -24,8 +27,8 @@ me.gender == code("babae") # Local alias (Philippines) Check if a vocabulary code belongs to a concept group. ```cel -in_group(me.gender, "feminine_gender") -members.exists(m, in_group(m.gender, "feminine_gender")) +in_group(r.gender_id, "feminine_gender") +members.exists(m, in_group(m.gender_id, "feminine_gender")) ``` #### `code_eq(code_field, identifier)` @@ -33,7 +36,7 @@ members.exists(m, in_group(m.gender, "feminine_gender")) Safe code comparison handling local code mappings. ```cel -code_eq(me.gender, "female") +code_eq(r.gender_id, "female") ``` ### Semantic Helpers @@ -44,20 +47,22 @@ User-friendly functions for common checks: - `is_male(code_field)` - Check if code is in masculine_gender group - `is_head(code_field)` - Check if code is in head_of_household group - `is_pregnant(code_field)` - Check if code is in pregnant_eligible group +- `head(member)` - Check if a member is the head of household (takes a member record, not a code field) ### Example Usage #### Simple Gender Check ```cel -is_female(me.gender) +is_female(r.gender_id) ``` #### Complex Eligibility Rule ```cel # Pregnant women or mothers with children under 5 -is_pregnant(me.pregnancy_status) or +# 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) ``` @@ -65,7 +70,8 @@ is_pregnant(me.pregnancy_status) or ```cel # Works in any deployment, even with local terminology -in_group(me.hazard_type, "climate_hazards") +# Note: hazard_type_id is provided by country-specific modules +in_group(r.hazard_type_id, "climate_hazards") ``` ## How It Works @@ -100,7 +106,7 @@ Example: CEL expressions are translated to Odoo domains: ```cel -in_group(me.gender, "feminine_gender") +in_group(r.gender_id, "feminine_gender") ``` Translates to: @@ -120,24 +126,28 @@ Translates to: ## Configuration -### Defining Concept Groups +### Concept Groups + +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. -Create concept groups via UI or data files: +To add codes to a group via data files: ```xml - - feminine_gender - Feminine Gender - is_female - Codes representing feminine gender identity - - + + + + +``` + +Or via Python: + +```python +group = env['spp.vocabulary.concept.group'].search( + [('name', '=', 'feminine_gender')], limit=1 +) +group.write({'code_ids': [Command.link(female_code.id)]}) ``` ### Local Code Mapping @@ -161,36 +171,43 @@ Map local codes to standard codes: ``` spp_cel_vocabulary/ -├── __init__.py +├── __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 +│ ├── cel_vocabulary_functions.py # Function registration +│ └── cel_vocabulary_translator.py # Domain translation ├── services/ │ ├── __init__.py -│ └── cel_vocabulary_functions.py # Pure Python functions +│ ├── cel_vocabulary_functions.py # Pure Python functions +│ └── vocabulary_cache.py # Session-scoped cache ├── tests/ │ ├── __init__.py -│ └── test_cel_vocabulary.py # Comprehensive tests -└── security/ - └── ir.model.access.csv +│ ├── 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 ``` ### Design Patterns 1. **Pure Functions** - Services contain stateless Python functions -2. **Environment Injection** - Models wrap functions with Odoo env +2. **Environment Injection** - Functions marked with `_cel_needs_env=True`; CEL service injects fresh env at evaluation time 3. **Function Registry** - Dynamic registration with CEL system 4. **Domain Translation** - AST transformation to Odoo domains +5. **Two-Layer Caching** - `@ormcache` (registry-scoped) + `VocabularyCache` (session-scoped) ## Testing Run tests: ```bash -# From openspp-odoo-19-migration/ directory -invoke test-spp-deps --modules=spp_cel_vocabulary --skip=queue_job --mode=update --db-filter='^devel$' +./scripts/test_single_module.sh spp_cel_vocabulary ``` ## Related Documentation @@ -207,20 +224,20 @@ invoke test-spp-deps --modules=spp_cel_vocabulary --skip=queue_job --mode=update **Before (fragile):** ```cel -me.gender == "female" +r.gender_id == "female" ``` **After (robust):** ```cel # Option 1: Semantic helper -is_female(me.gender) +is_female(r.gender_id) # Option 2: Concept group -in_group(me.gender, "feminine_gender") +in_group(r.gender_id, "feminine_gender") # Option 3: Safe comparison -code_eq(me.gender, "female") +code_eq(r.gender_id, "female") ``` ### From Hardcoded Values @@ -235,15 +252,18 @@ if member.pregnancy_status_id.code == "pregnant": **After:** ```python -pregnant_group = env.ref('spp_vocabulary.group_pregnant_eligible') -if pregnant_group.contains(member.pregnancy_status_id): +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: ```cel -in_group(me.pregnancy_status, "pregnant_eligible") +# Note: pregnancy_status_id is provided by country-specific modules +in_group(r.pregnancy_status_id, "pregnant_eligible") ``` ## Benefits @@ -258,6 +278,7 @@ in_group(me.pregnancy_status, "pregnant_eligible") - Code resolution uses `@ormcache` for fast lookups - Concept group URIs are pre-computed and stored as JSON +- Session-scoped `VocabularyCache` eliminates N+1 queries during evaluation - Domain translation happens once at compile time - No per-record overhead in query execution From 68703ce013e2970d51642992ccef69a60b0b6b3d Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 13 Mar 2026 16:34:55 +0800 Subject: [PATCH 06/15] fix: replace display_name with label and migrate to Command API in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace display_name → label in concept group create() calls (17 occurrences). display_name is a computed field that gets silently ignored; label is the correct field. Also migrate old-style tuples (4, id) and (6, 0, [ids]) to Command.link() and Command.set(). --- .../tests/test_cel_vocabulary.py | 17 +++++++------- .../tests/test_init_and_coverage.py | 13 ++++++----- .../tests/test_vocabulary_cache.py | 19 +++++++-------- .../tests/test_vocabulary_in_exists.py | 23 ++++++++++--------- 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/spp_cel_vocabulary/tests/test_cel_vocabulary.py b/spp_cel_vocabulary/tests/test_cel_vocabulary.py index 32169bf5..d55237f2 100644 --- a/spp_cel_vocabulary/tests/test_cel_vocabulary.py +++ b/spp_cel_vocabulary/tests/test_cel_vocabulary.py @@ -13,6 +13,7 @@ ADR-016 Phase 3: CEL Integration for vocabulary-aware expressions. """ +from odoo.fields import Command from odoo.tests import TransactionCase, tagged from odoo.addons.spp_cel_domain.tests.common import CELTestDataMixin @@ -64,12 +65,12 @@ def setUpClass(cls): existing_feminine = ConceptGroup.search([("name", "=", "feminine_gender")], limit=1) if existing_feminine: # Add our test codes to the existing group - existing_feminine.write({"code_ids": [(4, cls.code_female.id), (4, cls.code_babae.id)]}) + existing_feminine.write({"code_ids": [Command.link(cls.code_female.id), Command.link(cls.code_babae.id)]}) cls.group_feminine = existing_feminine else: cls.group_feminine = cls._create_test_concept_group( name="feminine_gender", - display_name="Feminine Gender", + label="Feminine Gender", cel_function="is_female", codes=[cls.code_female, cls.code_babae], description="Codes representing feminine gender identity", @@ -78,12 +79,12 @@ def setUpClass(cls): existing_masculine = ConceptGroup.search([("name", "=", "masculine_gender")], limit=1) if existing_masculine: # Add our test code to the existing group - existing_masculine.write({"code_ids": [(4, cls.code_male.id)]}) + existing_masculine.write({"code_ids": [Command.link(cls.code_male.id)]}) cls.group_masculine = existing_masculine else: cls.group_masculine = cls._create_test_concept_group( name="masculine_gender", - display_name="Masculine Gender", + label="Masculine Gender", cel_function="is_male", codes=[cls.code_male], description="Codes representing masculine gender identity", @@ -663,24 +664,24 @@ def setUpClass(cls): existing_feminine = ConceptGroup.search([("name", "=", "feminine_gender")], limit=1) if existing_feminine: - existing_feminine.write({"code_ids": [(4, cls.code_female.id)]}) + existing_feminine.write({"code_ids": [Command.link(cls.code_female.id)]}) cls.group_feminine = existing_feminine else: cls.group_feminine = cls._create_test_concept_group( name="feminine_gender", - display_name="Feminine Gender", + label="Feminine Gender", cel_function="is_female", codes=[cls.code_female], ) existing_masculine = ConceptGroup.search([("name", "=", "masculine_gender")], limit=1) if existing_masculine: - existing_masculine.write({"code_ids": [(4, cls.code_male.id)]}) + existing_masculine.write({"code_ids": [Command.link(cls.code_male.id)]}) cls.group_masculine = existing_masculine else: cls.group_masculine = cls._create_test_concept_group( name="masculine_gender", - display_name="Masculine Gender", + label="Masculine Gender", cel_function="is_male", codes=[cls.code_male], ) diff --git a/spp_cel_vocabulary/tests/test_init_and_coverage.py b/spp_cel_vocabulary/tests/test_init_and_coverage.py index eed5a2c7..e1103b99 100644 --- a/spp_cel_vocabulary/tests/test_init_and_coverage.py +++ b/spp_cel_vocabulary/tests/test_init_and_coverage.py @@ -13,6 +13,7 @@ from unittest.mock import patch +from odoo.fields import Command from odoo.tests import TransactionCase, tagged from odoo.addons.spp_cel_domain.tests.common import CELTestDataMixin @@ -251,7 +252,7 @@ def setUpClass(cls): { "group": cls.group_partner.id, "individual": cls.individual_partner.id, - "membership_type_ids": [(6, 0, [cls.head_code.id])], + "membership_type_ids": [Command.set([cls.head_code.id])], } ) @@ -368,11 +369,11 @@ def setUpClass(cls): ConceptGroup = cls.env["spp.vocabulary.concept.group"] existing_feminine = ConceptGroup.search([("name", "=", "feminine_gender")], limit=1) if existing_feminine: - existing_feminine.write({"code_ids": [(4, cls.code_female.id)]}) + existing_feminine.write({"code_ids": [Command.link(cls.code_female.id)]}) else: cls._create_test_concept_group( name="feminine_gender", - display_name="Feminine Gender", + label="Feminine Gender", codes=[cls.code_female], ) @@ -455,7 +456,7 @@ def setUpClass(cls): # Create an empty concept group (no codes) cls.empty_group = cls._create_test_concept_group( name=f"empty_group_{cls._test_id}", - display_name="Empty Group", + label="Empty Group", ) cls.translator = cls.env["spp.cel.translator"] @@ -689,11 +690,11 @@ def setUpClass(cls): ConceptGroup = cls.env["spp.vocabulary.concept.group"] cls.test_group = ConceptGroup.search([("name", "=", "feminine_gender")], limit=1) if cls.test_group: - cls.test_group.write({"code_ids": [(4, cls.code_female.id)]}) + cls.test_group.write({"code_ids": [Command.link(cls.code_female.id)]}) else: cls.test_group = cls._create_test_concept_group( name="feminine_gender", - display_name="Feminine Gender", + label="Feminine Gender", codes=[cls.code_female], ) diff --git a/spp_cel_vocabulary/tests/test_vocabulary_cache.py b/spp_cel_vocabulary/tests/test_vocabulary_cache.py index 70ea9612..f62b9152 100644 --- a/spp_cel_vocabulary/tests/test_vocabulary_cache.py +++ b/spp_cel_vocabulary/tests/test_vocabulary_cache.py @@ -10,6 +10,7 @@ - Cache invalidation on concept group changes """ +from odoo.fields import Command from odoo.tests import TransactionCase, tagged from odoo.addons.spp_cel_domain.tests.common import CELTestDataMixin @@ -45,7 +46,7 @@ def setUpClass(cls): # Create concept group cls.group_feminine = cls._create_test_concept_group( name=f"test_feminine_{cls._test_id}", - display_name="Test Feminine Gender", + label="Test Feminine Gender", codes=[cls.code_female], ) @@ -94,7 +95,7 @@ def test_cache_invalidation_on_code_add(self): self.assertNotIn(self.code_male.uri, uri_set_before) # Add male code to feminine group - self.group_feminine.write({"code_ids": [(4, self.code_male.id)]}) + self.group_feminine.write({"code_ids": [Command.link(self.code_male.id)]}) # Cache should be invalidated - new lookup should include male uri_set_after = ConceptGroup._get_group_uris_cached(self.group_feminine.name) @@ -107,7 +108,7 @@ def test_cache_invalidation_on_group_delete(self): # Create a temporary group temp_group = self._create_test_concept_group( name=f"temp_group_{self._test_id}", - display_name="Temporary Group", + label="Temporary Group", codes=[self.code_female], ) @@ -155,23 +156,23 @@ def setUpClass(cls): existing_feminine = ConceptGroup.search([("name", "=", "feminine_gender")], limit=1) if existing_feminine: - existing_feminine.write({"code_ids": [(4, cls.code_female.id)]}) + existing_feminine.write({"code_ids": [Command.link(cls.code_female.id)]}) cls.group_feminine = existing_feminine else: cls.group_feminine = cls._create_test_concept_group( name="feminine_gender", - display_name="Feminine Gender", + label="Feminine Gender", codes=[cls.code_female], ) existing_masculine = ConceptGroup.search([("name", "=", "masculine_gender")], limit=1) if existing_masculine: - existing_masculine.write({"code_ids": [(4, cls.code_male.id)]}) + existing_masculine.write({"code_ids": [Command.link(cls.code_male.id)]}) cls.group_masculine = existing_masculine else: cls.group_masculine = cls._create_test_concept_group( name="masculine_gender", - display_name="Masculine Gender", + label="Masculine Gender", codes=[cls.code_male], ) @@ -281,12 +282,12 @@ def setUpClass(cls): existing_feminine = ConceptGroup.search([("name", "=", "feminine_gender")], limit=1) if existing_feminine: - existing_feminine.write({"code_ids": [(4, cls.code_female.id)]}) + existing_feminine.write({"code_ids": [Command.link(cls.code_female.id)]}) cls.group_feminine = existing_feminine else: cls.group_feminine = cls._create_test_concept_group( name="feminine_gender", - display_name="Feminine Gender", + label="Feminine Gender", codes=[cls.code_female], ) diff --git a/spp_cel_vocabulary/tests/test_vocabulary_in_exists.py b/spp_cel_vocabulary/tests/test_vocabulary_in_exists.py index 6619eb73..0ebc531b 100644 --- a/spp_cel_vocabulary/tests/test_vocabulary_in_exists.py +++ b/spp_cel_vocabulary/tests/test_vocabulary_in_exists.py @@ -14,6 +14,7 @@ - The full is_female_headed pattern """ +from odoo.fields import Command from odoo.tests import TransactionCase, tagged from odoo.addons.spp_cel_domain.tests.common import CELTestDataMixin @@ -61,14 +62,14 @@ def setUpClass(cls): cls.group_feminine = cls.env["spp.vocabulary.concept.group"].create( { "name": "feminine_gender", - "display_name": "Feminine Gender", + "label": "Feminine Gender", "cel_function": "is_female", } ) # Add our test code to the group cls.group_feminine.write( { - "code_ids": [(4, cls.code_female.id)], + "code_ids": [Command.link(cls.code_female.id)], } ) @@ -82,14 +83,14 @@ def setUpClass(cls): cls.group_masculine = cls.env["spp.vocabulary.concept.group"].create( { "name": "masculine_gender", - "display_name": "Masculine Gender", + "label": "Masculine Gender", "cel_function": "is_male", } ) # Add our test code to the group cls.group_masculine.write( { - "code_ids": [(4, cls.code_male.id)], + "code_ids": [Command.link(cls.code_male.id)], } ) @@ -368,14 +369,14 @@ def setUpClass(cls): cls.group_feminine = cls.env["spp.vocabulary.concept.group"].create( { "name": "feminine_gender", - "display_name": "Feminine Gender", + "label": "Feminine Gender", "cel_function": "is_female", } ) # Add our test code to the group cls.group_feminine.write( { - "code_ids": [(4, cls.code_female.id)], + "code_ids": [Command.link(cls.code_female.id)], } ) @@ -389,14 +390,14 @@ def setUpClass(cls): cls.group_masculine = cls.env["spp.vocabulary.concept.group"].create( { "name": "masculine_gender", - "display_name": "Masculine Gender", + "label": "Masculine Gender", "cel_function": "is_male", } ) # Add our test code to the group cls.group_masculine.write( { - "code_ids": [(4, cls.code_male.id)], + "code_ids": [Command.link(cls.code_male.id)], } ) @@ -435,7 +436,7 @@ def setUpClass(cls): { "group": cls.hh_female_head.id, "individual": cls.female_head.id, - "membership_type_ids": [(6, 0, [cls.head_type.id])], + "membership_type_ids": [Command.set([cls.head_type.id])], } ) @@ -461,7 +462,7 @@ def setUpClass(cls): { "group": cls.hh_male_head.id, "individual": cls.male_head.id, - "membership_type_ids": [(6, 0, [cls.head_type.id])], + "membership_type_ids": [Command.set([cls.head_type.id])], } ) @@ -560,7 +561,7 @@ def setUpClass(cls): ) or cls.env["spp.vocabulary.concept.group"].create( { "name": "feminine_gender", - "display_name": "Feminine Gender", + "label": "Feminine Gender", } ) From 04a1f1936ea8cc0d5a5eda80eab8c355c41a7997 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 13 Mar 2026 16:35:03 +0800 Subject: [PATCH 07/15] docs: fix USAGE.md prefix, field names, patterns, and add decision guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standardize on r prefix (49 me. → r.), fix field names to use _id suffix (gender_id, not gender), replace m._link.kind with head(m), add head() to Quick Reference, add "When to Use Which" guide, add disclaimers for country-module fields and external functions. --- spp_cel_vocabulary/USAGE.md | 145 +++++++++++++++++++++--------------- 1 file changed, 85 insertions(+), 60 deletions(-) diff --git a/spp_cel_vocabulary/USAGE.md b/spp_cel_vocabulary/USAGE.md index dbb143e1..024dc6b4 100644 --- a/spp_cel_vocabulary/USAGE.md +++ b/spp_cel_vocabulary/USAGE.md @@ -3,17 +3,36 @@ This guide provides practical examples of using vocabulary-aware CEL functions in OpenSPP eligibility rules and filters. +> **Note:** Both `r` and `me` are valid prefixes for the registrant symbol. This guide uses `r` (matching ADR-008). The `me` alias is available via YAML profile configuration. + ## Quick Reference -| Function | Purpose | Example | -| ------------------------ | ------------------------------ | ---------------------------------------- | -| `code(id)` | Resolve code by URI/alias | `me.gender == code("female")` | -| `in_group(field, group)` | Check concept group membership | `in_group(me.gender, "feminine_gender")` | -| `code_eq(field, id)` | Safe code comparison | `code_eq(me.gender, "female")` | -| `is_female(field)` | Check feminine gender | `is_female(me.gender)` | -| `is_male(field)` | Check masculine gender | `is_male(me.gender)` | -| `is_head(field)` | Check head of household | `is_head(me.relationship)` | -| `is_pregnant(field)` | Check pregnancy status | `is_pregnant(me.pregnancy_status)` | +| Function | Purpose | Example | +| ------------------------ | ------------------------------ | ------------------------------------------ | +| `code(id)` | Resolve code by URI/alias | `r.gender_id == code("female")` | +| `in_group(field, group)` | Check concept group membership | `in_group(r.gender_id, "feminine_gender")` | +| `code_eq(field, id)` | Safe code comparison | `code_eq(r.gender_id, "female")` | +| `is_female(field)` | Check feminine gender | `is_female(r.gender_id)` | +| `is_male(field)` | Check masculine gender | `is_male(r.gender_id)` | +| `is_head(field)` | Check head of household code | `is_head(r.relationship_type)` | +| `is_pregnant(field)` | Check pregnancy status | `is_pregnant(r.pregnancy_status_id)` | +| `head(member)` | Check if member is household head | `head(m)` (takes member record, not a code field) | + +### When to Use Which Function + +| Need | Use | Example | +|------|-----|---------| +| Check if a code belongs to a semantic group | `in_group()` | `in_group(r.gender_id, "feminine_gender")` | +| Use a predefined semantic check | `is_female()`, `is_male()`, etc. | `is_female(r.gender_id)` | +| Compare a field to a specific code | `code_eq()` | `code_eq(r.gender_id, "female")` | +| Use a code value in a comparison | `code()` | `r.gender_id == code("female")` | +| Check if member is head of household | `head()` | `head(m)` | + +**`is_female(r.gender_id)`** vs **`in_group(r.gender_id, "feminine_gender")`**: Identical behavior. Semantic helpers are shortcuts for `in_group()` with standard concept groups. + +**`code_eq(r.gender_id, "female")`** vs **`r.gender_id == code("female")`**: Identical behavior. `code_eq()` is more concise; the comparison form is more readable for complex expressions. + +**`is_head(code_field)`** vs **`head(member)`**: Different! `is_head()` checks if a vocabulary code is in the head_of_household group. `head()` checks if a member record is the head of their household (looks up membership type). ## Basic Examples @@ -22,19 +41,19 @@ OpenSPP eligibility rules and filters. **Use Case:** Filter for female registrants ```cel -is_female(me.gender) +is_female(r.gender_id) ``` **What it does:** -- Checks if `me.gender` is in the `feminine_gender` concept group +- Checks if `r.gender_id` is in the `feminine_gender` concept group - Works with any code in the group (including local codes) - Returns boolean (true/false) **Equivalent to:** ```cel -in_group(me.gender, "feminine_gender") +in_group(r.gender_id, "feminine_gender") ``` ### Example 2: Code Comparison @@ -42,20 +61,20 @@ in_group(me.gender, "feminine_gender") **Use Case:** Check if gender is explicitly "Female" ```cel -code_eq(me.gender, "Female") +code_eq(r.gender_id, "Female") ``` **What it does:** - Resolves "Female" to a vocabulary code -- Compares `me.gender` to that code +- Compares `r.gender_id` to that code - Handles local codes via `reference_uri` **Also works with:** ```cel -me.gender == code("Female") -me.gender == code("urn:iso:std:iso:5218#2") # By URI +r.gender_id == code("Female") +r.gender_id == code("urn:iso:std:iso:5218#2") # By URI ``` ### Example 3: Head of Household @@ -63,23 +82,27 @@ me.gender == code("urn:iso:std:iso:5218#2") # By URI **Use Case:** Find heads of households ```cel -is_head(me.relationship_type) +is_head(r.relationship_type) ``` **In group context:** ```cel -members.exists(m, is_head(m._link.kind)) +members.exists(m, head(m)) ``` +> **Note:** Functions like `age_years()`, `members.exists()`, and `members.count()` are provided by `spp_cel_domain`, not this module. + ## Complex Eligibility Rules ### Example 4: 4Ps Pregnant Women Set **Use Case:** Female registrants who are pregnant +> **Note:** Fields like `pregnancy_status_id` and `hazard_type_id` are provided by country-specific modules, not the base registry. + ```cel -is_female(me.gender) and is_pregnant(me.pregnancy_status) +is_female(r.gender_id) and is_pregnant(r.pregnancy_status_id) ``` **What it checks:** @@ -93,7 +116,7 @@ is_female(me.gender) and is_pregnant(me.pregnancy_status) ```cel members.exists(m, - is_head(m._link.kind) and is_female(m.gender) + head(m) and is_female(m.gender_id) ) ``` @@ -108,7 +131,7 @@ members.exists(m, **Use Case:** Pregnant women or mothers with infants ```cel -is_pregnant(me.pregnancy_status) or +is_pregnant(r.pregnancy_status_id) or members.exists(child, age_years(child.birthdate) < 1 ) @@ -124,8 +147,8 @@ is_pregnant(me.pregnancy_status) or **Use Case:** Households affected by climate hazards ```cel -in_group(me.hazard_type, "climate_hazards") and - me.affected_date >= days_ago(90) +in_group(r.hazard_type_id, "climate_hazards") and + r.affected_date >= days_ago(90) ``` **What it checks:** @@ -139,7 +162,7 @@ in_group(me.hazard_type, "climate_hazards") and ```cel members.exists(head, - is_head(head._link.kind) and is_female(head.gender) + head(head) and is_female(head.gender_id) ) and members.exists(child, age_years(child.birthdate) < 18 @@ -168,7 +191,7 @@ members.exists(child, **CEL Expression:** ```cel -is_female(me.gender) +is_female(r.gender_id) ``` **Works for all of:** @@ -189,7 +212,7 @@ is_female(me.gender) **CEL Expression:** ```cel -in_group(me.hazard_type, "climate_hazards") +in_group(r.hazard_type_id, "climate_hazards") ``` **Matches:** @@ -207,7 +230,7 @@ in_group(me.hazard_type, "climate_hazards") ```cel # Female head of household members.exists(m, - is_head(m._link.kind) and is_female(m.gender) + head(m) and is_female(m.gender_id) ) and # With children under 14 @@ -217,12 +240,12 @@ members.exists(child, # Either pregnant or recently gave birth ( - is_pregnant(me.pregnancy_status) or - (me.last_birth_date >= days_ago(180)) + is_pregnant(r.pregnancy_status_id) or + (r.last_birth_date >= days_ago(180)) ) and # Low income -me.monthly_income < 5000 +r.monthly_income < 5000 ``` ### Example 12: Priority Scoring @@ -234,13 +257,13 @@ me.monthly_income < 5000 50 + # +20 points for female head -(members.exists(m, is_head(m._link.kind) and is_female(m.gender)) ? 20 : 0) + +(members.exists(m, head(m) and is_female(m.gender_id)) ? 20 : 0) + # +15 points per child under 5 (members.count(m, age_years(m.birthdate) < 5) * 15) + # +25 points if pregnant -(is_pregnant(me.pregnancy_status) ? 25 : 0) + +(is_pregnant(r.pregnancy_status_id) ? 25 : 0) + # +10 points for elderly member (members.exists(m, age_years(m.birthdate) >= 60) ? 10 : 0) @@ -252,8 +275,8 @@ me.monthly_income < 5000 ```cel # In flood-prone area -in_group(me.location_hazard_risk, "climate_hazards") and -in_group(me.location_hazard_type, "water_related") and +in_group(r.location_hazard_risk, "climate_hazards") and +in_group(r.location_hazard_type, "water_related") and # With vulnerable members ( @@ -262,9 +285,9 @@ in_group(me.location_hazard_type, "water_related") and # Elderly members.exists(m, age_years(m.birthdate) >= 60) or # Pregnant women - members.exists(m, is_pregnant(m.pregnancy_status)) or + members.exists(m, is_pregnant(m.pregnancy_status_id)) or # Persons with disability - members.exists(m, in_group(m.disability_type, "persons_with_disability")) + members.exists(m, in_group(m.disability_type_id, "persons_with_disability")) ) ``` @@ -275,9 +298,9 @@ in_group(me.location_hazard_type, "water_related") and **Use Case:** Women of reproductive age ```cel -is_female(me.gender) and -age_years(me.birthdate) >= 15 and -age_years(me.birthdate) <= 49 +is_female(r.gender_id) and +age_years(r.birthdate) >= 15 and +age_years(r.birthdate) <= 49 ``` ### Example 15: Time-based Eligibility @@ -285,8 +308,8 @@ age_years(me.birthdate) <= 49 **Use Case:** Recently affected households ```cel -in_group(me.hazard_type, "climate_hazards") and -days_since(me.affected_date) <= 90 +in_group(r.hazard_type_id, "climate_hazards") and +days_since(r.affected_date) <= 90 ``` ### Example 16: Enrollment Status @@ -294,8 +317,8 @@ days_since(me.affected_date) <= 90 **Use Case:** Unenrolled eligible individuals ```cel -is_female(me.gender) and -age_years(me.birthdate) >= 18 and +is_female(r.gender_id) and +age_years(r.birthdate) >= 18 and enrollments.count(e, e.state == "enrolled") == 0 ``` @@ -326,10 +349,12 @@ print(code.display) # Female ### Validate Expression +> **Note:** `validate_expression()` checks whether an expression is syntactically and semantically valid and returns `{valid: True/False, error: ...}`. Use `compile_expression()` to compile an expression to a domain/plan and return the full translation result. + ```python service = env['spp.cel.service'] result = service.validate_expression( - 'is_female(me.gender)', + 'is_female(r.gender_id)', 'registry_individuals' ) @@ -345,20 +370,20 @@ print(result['error']) # Error message if invalid ✅ **Use concept groups for semantic checks** ```cel -in_group(me.hazard_type, "climate_hazards") +in_group(r.hazard_type_id, "climate_hazards") ``` ✅ **Cache code resolution** (automatic via `@ormcache`) ```cel -is_female(me.gender) # First call resolves, subsequent calls cached +is_female(r.gender_id) # First call resolves, subsequent calls cached ``` ✅ **Combine conditions efficiently** ```cel # Good: Short-circuit evaluation -is_female(me.gender) and is_pregnant(me.pregnancy_status) +is_female(r.gender_id) and is_pregnant(r.pregnancy_status_id) ``` ### Don'ts @@ -367,23 +392,23 @@ is_female(me.gender) and is_pregnant(me.pregnancy_status) ```cel # Bad: Multiple lookups -me.gender == code("female") and me.gender == code("F") +r.gender_id == code("female") and r.gender_id == code("F") # Good: Use concept group -in_group(me.gender, "feminine_gender") +in_group(r.gender_id, "feminine_gender") ``` ❌ **Don't nest complex expressions** ```cel # Bad: Hard to read and maintain -members.exists(m, in_group(m.gender, "feminine_gender") and +members.exists(m, in_group(m.gender_id, "feminine_gender") and age_years(m.birthdate) < 5 and members.exists(n, n.id != m.id and age_years(n.birthdate) < 10)) # Good: Break into separate conditions members.exists(m, - in_group(m.gender, "feminine_gender") and + in_group(m.gender_id, "feminine_gender") and age_years(m.birthdate) < 5 ) and members.count(m, age_years(m.birthdate) < 10) >= 2 @@ -396,7 +421,7 @@ members.count(m, age_years(m.birthdate) < 10) >= 2 **Problem:** ```cel -in_group(me.status, "nonexistent_group") +in_group(r.status, "nonexistent_group") ``` **Result:** Always returns false, domain matches nothing @@ -408,7 +433,7 @@ in_group(me.status, "nonexistent_group") **Problem:** ```cel -is_female(me.name) # name is Char, not Many2one to vocabulary.code +is_female(r.name) # name is Char, not Many2one to vocabulary.code ``` **Result:** Type error or unexpected behavior @@ -420,7 +445,7 @@ is_female(me.name) # name is Char, not Many2one to vocabulary.code **Problem:** ```cel -in_group(me.gender, "feminine_gender") # Group exists but has no codes +in_group(r.gender_id, "feminine_gender") # Group exists but has no codes ``` **Result:** Always returns false @@ -432,7 +457,7 @@ in_group(me.gender, "feminine_gender") # Group exists but has no codes **Problem:** ```cel -code_eq(me.gender, "FEMALE") # Wrong case +code_eq(r.gender_id, "FEMALE") # Wrong case ``` **Result:** May not match if vocabulary uses "Female" @@ -445,12 +470,12 @@ code_eq(me.gender, "FEMALE") # Wrong case ```cel # Old (fragile) -me.gender.code == "F" or me.gender.code == "female" +r.gender_id.code == "F" or r.gender_id.code == "female" ``` ```cel # New (robust) -is_female(me.gender) +is_female(r.gender_id) ``` ### Before: Hardcoded Values @@ -463,19 +488,19 @@ if member.gender_id.code == "F": ```cel # New (robust) -is_female(me.gender) +is_female(r.gender_id) ``` ### Before: No Local Code Support ```cel # Old (doesn't work with local codes) -me.gender.code == "F" +r.gender_id.code == "F" ``` ```cel # New (works with "F", "Female", "babae", etc.) -is_female(me.gender) +is_female(r.gender_id) ``` ## References From 15295ff1bcd794d2471924105a1f7ea74d59487a Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 13 Mar 2026 16:35:09 +0800 Subject: [PATCH 08/15] docs: fix data/README.md menu path, XML IDs, label, and Command API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix menu path to Settings > Vocabularies > Concept Groups, note that groups are created by hook (no XML IDs), fix display_name → label in XML examples, fix Python example to use Command API, add note about cel_function being metadata only. --- spp_cel_vocabulary/data/README.md | 75 ++++++++++++++++++------------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/spp_cel_vocabulary/data/README.md b/spp_cel_vocabulary/data/README.md index a6430bd7..d494df52 100644 --- a/spp_cel_vocabulary/data/README.md +++ b/spp_cel_vocabulary/data/README.md @@ -26,7 +26,7 @@ them. ### Option 1: Through UI -1. Go to **Social Protection → Configuration → Vocabulary → Concept Groups** +1. Go to **Settings → Vocabularies → Concept Groups** 2. Open a concept group 3. Click **Edit** 4. In the **Codes** tab, add your vocabulary codes @@ -34,6 +34,10 @@ them. ### Option 2: Through Data Files +Concept groups are created via `post_init_hook` in Python and have no XML IDs. The XML +IDs shown below (e.g. `spp_cel_vocabulary.group_feminine_gender`) are **for illustration +only** and will not resolve in practice. Use a Python-based lookup instead (see Option 3). + Create a data file in your deployment module: ```xml @@ -48,9 +52,9 @@ Create a data file in your deployment module: @@ -63,8 +67,8 @@ Create a data file in your deployment module: @@ -76,10 +80,15 @@ Create a data file in your deployment module: ```python # In a post_init_hook or migration script +# Note: from odoo.fields import Command def add_codes_to_groups(env): - # Get concept groups - feminine_group = env.ref('spp_cel_vocabulary.group_feminine_gender') - masculine_group = env.ref('spp_cel_vocabulary.group_masculine_gender') + # Get concept groups by name (no XML IDs — groups are created via post_init_hook) + feminine_group = env['spp.vocabulary.concept.group'].search( + [('name', '=', 'feminine_gender')], limit=1 + ) + masculine_group = env['spp.vocabulary.concept.group'].search( + [('name', '=', 'masculine_gender')], limit=1 + ) # Get vocabulary codes female_code = env['spp.vocabulary.code'].search([ @@ -93,8 +102,8 @@ def add_codes_to_groups(env): ], limit=1) # Add codes to groups - feminine_group.write({'code_ids': [(4, female_code.id)]}) - masculine_group.write({'code_ids': [(4, male_code.id)]}) + feminine_group.write({'code_ids': [Command.link(female_code.id)]}) + masculine_group.write({'code_ids': [Command.link(male_code.id)]}) ``` ## Example: Philippines 4Ps Deployment @@ -104,6 +113,7 @@ def add_codes_to_groups(env): + @@ -124,8 +134,8 @@ def add_codes_to_groups(env): @@ -138,8 +148,8 @@ def add_codes_to_groups(env): @@ -152,8 +162,8 @@ def add_codes_to_groups(env): @@ -166,11 +176,11 @@ def add_codes_to_groups(env): @@ -188,13 +198,13 @@ service = env['spp.cel.service'] # Test feminine_gender group result = service.compile_expression( - 'is_female(me.gender_id)', + 'is_female(r.gender_id)', 'registry_individuals' ) # Test climate_hazards group result = service.compile_expression( - 'in_group(me.hazard_type_id, "climate_hazards")', + 'in_group(r.hazard_type_id, "climate_hazards")', 'registry_individuals' ) ``` @@ -206,23 +216,28 @@ If you need additional concept groups: ```xml my_custom_group - My Custom Group + My Custom Group is_my_custom Description of what this group represents ``` +> **Note on `cel_function`:** This field is metadata only. It records which CEL helper +> function maps to this group, but setting it does **not** automatically create or +> register a new CEL function. New functions must be implemented in Python and registered +> separately in your module. + Then use in CEL: ```cel -in_group(me.some_field_id, "my_custom_group") +in_group(r.some_field_id, "my_custom_group") # Or create a helper function in your module ``` From 82c965b207b5e00564d74ab1fea92e50fa1b04a7 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 13 Mar 2026 16:35:38 +0800 Subject: [PATCH 09/15] docs: add readme/USAGE.md for OCA readme generator Concise quick reference with function table, basic examples, and when-to-use-which guide for inclusion in README.rst. --- spp_cel_vocabulary/readme/USAGE.md | 50 ++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 spp_cel_vocabulary/readme/USAGE.md diff --git a/spp_cel_vocabulary/readme/USAGE.md b/spp_cel_vocabulary/readme/USAGE.md new file mode 100644 index 00000000..02840863 --- /dev/null +++ b/spp_cel_vocabulary/readme/USAGE.md @@ -0,0 +1,50 @@ +### Quick Reference + +| Function | Purpose | Example | +| ---------------------------------- | ------------------------------------------ | ------------------------------------------------ | +| `code(identifier)` | Resolve code by URI or alias | `r.gender_id == code("female")` | +| `in_group(field, group)` | Check if code is in a concept group | `in_group(r.gender_id, "feminine_gender")` | +| `code_eq(field, identifier)` | Safe code comparison with local support | `code_eq(r.gender_id, "female")` | +| `is_female(field)` | Check feminine_gender group | `is_female(r.gender_id)` | +| `is_male(field)` | Check masculine_gender group | `is_male(r.gender_id)` | +| `is_head(field)` | Check head_of_household group | `is_head(r.relationship_type)` | +| `is_pregnant(field)` | Check pregnant_eligible group | `is_pregnant(r.pregnancy_status_id)` | +| `head(member)` | Check if member is head of household | `members.exists(m, head(m))` | + +> **Note:** Both `r` and `me` are valid prefixes for the registrant symbol. This guide +> uses `r` (matching ADR-008). + +### Basic Examples + +```javascript +// Simple gender check +is_female(r.gender_id) + +// Code comparison +code_eq(r.gender_id, "female") + +// Group membership check +in_group(r.gender_id, "feminine_gender") + +// Head of household who is female +members.exists(m, head(m) && is_female(m.gender_id)) +``` + +### When to Use Which Function + +| Need | Use | +| --------------------------------------------- | ----------------------------- | +| Check if code belongs to a semantic group | `in_group()` | +| Use a predefined semantic check | `is_female()`, `is_male()` | +| Compare a field to a specific code | `code_eq()` | +| Use a code value in a comparison | `code()` | +| Check if member is head of household | `head()` | + +**`is_female(r.gender_id)`** vs **`in_group(r.gender_id, "feminine_gender")`**: Identical +behavior. Semantic helpers are shortcuts for `in_group()` with standard concept groups. + +**`is_head(code_field)`** vs **`head(member)`**: Different! `is_head()` checks if a vocabulary +code is in the head_of_household group. `head()` checks if a member record is the head of +their household by looking up membership type. + +For the complete usage guide with advanced examples, see the root `USAGE.md` file. From 87c54c366f33688575b9e12aa4c824726ba89db1 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 13 Mar 2026 16:37:51 +0800 Subject: [PATCH 10/15] style: apply ruff-format to cel_vocabulary_translator.py --- spp_cel_vocabulary/models/cel_vocabulary_translator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spp_cel_vocabulary/models/cel_vocabulary_translator.py b/spp_cel_vocabulary/models/cel_vocabulary_translator.py index 78ebe72b..4f4afa52 100644 --- a/spp_cel_vocabulary/models/cel_vocabulary_translator.py +++ b/spp_cel_vocabulary/models/cel_vocabulary_translator.py @@ -134,8 +134,7 @@ def _handle_in_group(self, model: str, node: P.Call, cfg: dict[str, Any], ctx: d if not group: _logger.warning( - "[CEL Vocabulary] Concept group '%s' not found. " - "Check Settings > Vocabularies > Concept Groups.", + "[CEL Vocabulary] Concept group '%s' not found. Check Settings > Vocabularies > Concept Groups.", group_name, ) # Return domain that matches nothing From f3c4727b35d2bec01ff1a76819379b1bcdba1556 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 13 Mar 2026 16:38:00 +0800 Subject: [PATCH 11/15] style: apply prettier formatting to markdown files --- spp_cel_vocabulary/README.md | 16 +++++---- spp_cel_vocabulary/USAGE.md | 59 +++++++++++++++++++------------ spp_cel_vocabulary/data/README.md | 7 ++-- 3 files changed, 50 insertions(+), 32 deletions(-) diff --git a/spp_cel_vocabulary/README.md b/spp_cel_vocabulary/README.md index 2f787945..9de7e2a8 100644 --- a/spp_cel_vocabulary/README.md +++ b/spp_cel_vocabulary/README.md @@ -6,7 +6,8 @@ This module extends the CEL (Common Expression Language) system with vocabulary- functions that enable robust eligibility rules across different deployment vocabularies. > **Note:** Both `r` and `me` are valid prefixes for the registrant symbol. This guide -> uses `r` (matching ADR-008). The `me` alias is available via YAML profile configuration. +> uses `r` (matching ADR-008). The `me` alias is available via YAML profile +> configuration. ## Features @@ -47,7 +48,8 @@ User-friendly functions for common checks: - `is_male(code_field)` - Check if code is in masculine_gender group - `is_head(code_field)` - Check if code is in head_of_household group - `is_pregnant(code_field)` - Check if code is in pregnant_eligible group -- `head(member)` - Check if a member is the head of household (takes a member record, not a code field) +- `head(member)` - Check if a member is the head of household (takes a member record, + not a code field) ### Example Usage @@ -129,8 +131,8 @@ Translates to: ### Concept Groups 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. +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: @@ -197,10 +199,12 @@ spp_cel_vocabulary/ ### Design Patterns 1. **Pure Functions** - Services contain stateless Python functions -2. **Environment Injection** - Functions marked with `_cel_needs_env=True`; CEL service injects fresh env at evaluation time +2. **Environment Injection** - Functions marked with `_cel_needs_env=True`; CEL service + injects fresh env at evaluation time 3. **Function Registry** - Dynamic registration with CEL system 4. **Domain Translation** - AST transformation to Odoo domains -5. **Two-Layer Caching** - `@ormcache` (registry-scoped) + `VocabularyCache` (session-scoped) +5. **Two-Layer Caching** - `@ormcache` (registry-scoped) + `VocabularyCache` + (session-scoped) ## Testing diff --git a/spp_cel_vocabulary/USAGE.md b/spp_cel_vocabulary/USAGE.md index 024dc6b4..37eb274b 100644 --- a/spp_cel_vocabulary/USAGE.md +++ b/spp_cel_vocabulary/USAGE.md @@ -3,36 +3,44 @@ This guide provides practical examples of using vocabulary-aware CEL functions in OpenSPP eligibility rules and filters. -> **Note:** Both `r` and `me` are valid prefixes for the registrant symbol. This guide uses `r` (matching ADR-008). The `me` alias is available via YAML profile configuration. +> **Note:** Both `r` and `me` are valid prefixes for the registrant symbol. This guide +> uses `r` (matching ADR-008). The `me` alias is available via YAML profile +> configuration. ## Quick Reference -| Function | Purpose | Example | -| ------------------------ | ------------------------------ | ------------------------------------------ | -| `code(id)` | Resolve code by URI/alias | `r.gender_id == code("female")` | -| `in_group(field, group)` | Check concept group membership | `in_group(r.gender_id, "feminine_gender")` | -| `code_eq(field, id)` | Safe code comparison | `code_eq(r.gender_id, "female")` | -| `is_female(field)` | Check feminine gender | `is_female(r.gender_id)` | -| `is_male(field)` | Check masculine gender | `is_male(r.gender_id)` | -| `is_head(field)` | Check head of household code | `is_head(r.relationship_type)` | -| `is_pregnant(field)` | Check pregnancy status | `is_pregnant(r.pregnancy_status_id)` | +| Function | Purpose | Example | +| ------------------------ | --------------------------------- | ------------------------------------------------- | +| `code(id)` | Resolve code by URI/alias | `r.gender_id == code("female")` | +| `in_group(field, group)` | Check concept group membership | `in_group(r.gender_id, "feminine_gender")` | +| `code_eq(field, id)` | Safe code comparison | `code_eq(r.gender_id, "female")` | +| `is_female(field)` | Check feminine gender | `is_female(r.gender_id)` | +| `is_male(field)` | Check masculine gender | `is_male(r.gender_id)` | +| `is_head(field)` | Check head of household code | `is_head(r.relationship_type)` | +| `is_pregnant(field)` | Check pregnancy status | `is_pregnant(r.pregnancy_status_id)` | | `head(member)` | Check if member is household head | `head(m)` (takes member record, not a code field) | ### When to Use Which Function -| Need | Use | Example | -|------|-----|---------| -| Check if a code belongs to a semantic group | `in_group()` | `in_group(r.gender_id, "feminine_gender")` | -| Use a predefined semantic check | `is_female()`, `is_male()`, etc. | `is_female(r.gender_id)` | -| Compare a field to a specific code | `code_eq()` | `code_eq(r.gender_id, "female")` | -| Use a code value in a comparison | `code()` | `r.gender_id == code("female")` | -| Check if member is head of household | `head()` | `head(m)` | +| Need | Use | Example | +| ------------------------------------------- | -------------------------------- | ------------------------------------------ | +| Check if a code belongs to a semantic group | `in_group()` | `in_group(r.gender_id, "feminine_gender")` | +| Use a predefined semantic check | `is_female()`, `is_male()`, etc. | `is_female(r.gender_id)` | +| Compare a field to a specific code | `code_eq()` | `code_eq(r.gender_id, "female")` | +| Use a code value in a comparison | `code()` | `r.gender_id == code("female")` | +| Check if member is head of household | `head()` | `head(m)` | -**`is_female(r.gender_id)`** vs **`in_group(r.gender_id, "feminine_gender")`**: Identical behavior. Semantic helpers are shortcuts for `in_group()` with standard concept groups. +**`is_female(r.gender_id)`** vs **`in_group(r.gender_id, "feminine_gender")`**: +Identical behavior. Semantic helpers are shortcuts for `in_group()` with standard +concept groups. -**`code_eq(r.gender_id, "female")`** vs **`r.gender_id == code("female")`**: Identical behavior. `code_eq()` is more concise; the comparison form is more readable for complex expressions. +**`code_eq(r.gender_id, "female")`** vs **`r.gender_id == code("female")`**: Identical +behavior. `code_eq()` is more concise; the comparison form is more readable for complex +expressions. -**`is_head(code_field)`** vs **`head(member)`**: Different! `is_head()` checks if a vocabulary code is in the head_of_household group. `head()` checks if a member record is the head of their household (looks up membership type). +**`is_head(code_field)`** vs **`head(member)`**: Different! `is_head()` checks if a +vocabulary code is in the head_of_household group. `head()` checks if a member record is +the head of their household (looks up membership type). ## Basic Examples @@ -91,7 +99,8 @@ is_head(r.relationship_type) members.exists(m, head(m)) ``` -> **Note:** Functions like `age_years()`, `members.exists()`, and `members.count()` are provided by `spp_cel_domain`, not this module. +> **Note:** Functions like `age_years()`, `members.exists()`, and `members.count()` are +> provided by `spp_cel_domain`, not this module. ## Complex Eligibility Rules @@ -99,7 +108,8 @@ members.exists(m, head(m)) **Use Case:** Female registrants who are pregnant -> **Note:** Fields like `pregnancy_status_id` and `hazard_type_id` are provided by country-specific modules, not the base registry. +> **Note:** Fields like `pregnancy_status_id` and `hazard_type_id` are provided by +> country-specific modules, not the base registry. ```cel is_female(r.gender_id) and is_pregnant(r.pregnancy_status_id) @@ -349,7 +359,10 @@ print(code.display) # Female ### Validate Expression -> **Note:** `validate_expression()` checks whether an expression is syntactically and semantically valid and returns `{valid: True/False, error: ...}`. Use `compile_expression()` to compile an expression to a domain/plan and return the full translation result. +> **Note:** `validate_expression()` checks whether an expression is syntactically and +> semantically valid and returns `{valid: True/False, error: ...}`. Use +> `compile_expression()` to compile an expression to a domain/plan and return the full +> translation result. ```python service = env['spp.cel.service'] diff --git a/spp_cel_vocabulary/data/README.md b/spp_cel_vocabulary/data/README.md index d494df52..f62d2091 100644 --- a/spp_cel_vocabulary/data/README.md +++ b/spp_cel_vocabulary/data/README.md @@ -36,7 +36,8 @@ them. Concept groups are created via `post_init_hook` in Python and have no XML IDs. The XML IDs shown below (e.g. `spp_cel_vocabulary.group_feminine_gender`) are **for illustration -only** and will not resolve in practice. Use a Python-based lookup instead (see Option 3). +only** and will not resolve in practice. Use a Python-based lookup instead (see Option +3). Create a data file in your deployment module: @@ -231,8 +232,8 @@ If you need additional concept groups: > **Note on `cel_function`:** This field is metadata only. It records which CEL helper > function maps to this group, but setting it does **not** automatically create or -> register a new CEL function. New functions must be implemented in Python and registered -> separately in your module. +> register a new CEL function. New functions must be implemented in Python and +> registered separately in your module. Then use in CEL: From f352eabd20b23045b1c1f7733afd52af5f9937a1 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 13 Mar 2026 16:44:30 +0800 Subject: [PATCH 12/15] chore: remove readme/USAGE.md placeholder pending QA testing guide The readme/USAGE.md will contain a UI testing guide for QA. Current developer quick reference content is already in root USAGE.md. --- spp_cel_vocabulary/readme/USAGE.md | 50 ------------------------------ 1 file changed, 50 deletions(-) delete mode 100644 spp_cel_vocabulary/readme/USAGE.md diff --git a/spp_cel_vocabulary/readme/USAGE.md b/spp_cel_vocabulary/readme/USAGE.md deleted file mode 100644 index 02840863..00000000 --- a/spp_cel_vocabulary/readme/USAGE.md +++ /dev/null @@ -1,50 +0,0 @@ -### Quick Reference - -| Function | Purpose | Example | -| ---------------------------------- | ------------------------------------------ | ------------------------------------------------ | -| `code(identifier)` | Resolve code by URI or alias | `r.gender_id == code("female")` | -| `in_group(field, group)` | Check if code is in a concept group | `in_group(r.gender_id, "feminine_gender")` | -| `code_eq(field, identifier)` | Safe code comparison with local support | `code_eq(r.gender_id, "female")` | -| `is_female(field)` | Check feminine_gender group | `is_female(r.gender_id)` | -| `is_male(field)` | Check masculine_gender group | `is_male(r.gender_id)` | -| `is_head(field)` | Check head_of_household group | `is_head(r.relationship_type)` | -| `is_pregnant(field)` | Check pregnant_eligible group | `is_pregnant(r.pregnancy_status_id)` | -| `head(member)` | Check if member is head of household | `members.exists(m, head(m))` | - -> **Note:** Both `r` and `me` are valid prefixes for the registrant symbol. This guide -> uses `r` (matching ADR-008). - -### Basic Examples - -```javascript -// Simple gender check -is_female(r.gender_id) - -// Code comparison -code_eq(r.gender_id, "female") - -// Group membership check -in_group(r.gender_id, "feminine_gender") - -// Head of household who is female -members.exists(m, head(m) && is_female(m.gender_id)) -``` - -### When to Use Which Function - -| Need | Use | -| --------------------------------------------- | ----------------------------- | -| Check if code belongs to a semantic group | `in_group()` | -| Use a predefined semantic check | `is_female()`, `is_male()` | -| Compare a field to a specific code | `code_eq()` | -| Use a code value in a comparison | `code()` | -| Check if member is head of household | `head()` | - -**`is_female(r.gender_id)`** vs **`in_group(r.gender_id, "feminine_gender")`**: Identical -behavior. Semantic helpers are shortcuts for `in_group()` with standard concept groups. - -**`is_head(code_field)`** vs **`head(member)`**: Different! `is_head()` checks if a vocabulary -code is in the head_of_household group. `head()` checks if a member record is the head of -their household by looking up membership type. - -For the complete usage guide with advanced examples, see the root `USAGE.md` file. From 21e5f7190fa520f550a79d0915e5ab97dbdef0f7 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 13 Mar 2026 17:41:51 +0800 Subject: [PATCH 13/15] fix: address expert review findings for stable release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - H-1: replace phantom relationship_type with relationship_type_id, add notes that is_head() requires deployment-defined code field - H-2: replace old-style tuple in concept_groups.xml comment with Python search-and-write pattern - M-2: clarify "Display name" → "Label (display value)" in README.md - L-1: standardize && to and in DESCRIPTION.md and docstrings - L-2: standardize menu path separator → to > in data/README.md --- spp_cel_vocabulary/README.md | 2 +- spp_cel_vocabulary/USAGE.md | 10 +++++++--- spp_cel_vocabulary/data/README.md | 2 +- spp_cel_vocabulary/data/concept_groups.xml | 10 +++++----- spp_cel_vocabulary/readme/DESCRIPTION.md | 8 ++++---- .../services/cel_vocabulary_functions.py | 12 +++++++++--- 6 files changed, 27 insertions(+), 17 deletions(-) diff --git a/spp_cel_vocabulary/README.md b/spp_cel_vocabulary/README.md index 9de7e2a8..b68350c1 100644 --- a/spp_cel_vocabulary/README.md +++ b/spp_cel_vocabulary/README.md @@ -84,7 +84,7 @@ The `code()` function resolves identifiers in this order: 1. Full URI (e.g., `"urn:iso:std:iso:5218#2"`) 2. Code value in active vocabulary -3. Display name +3. Label (display value) 4. Reference URI mapping (for local codes) ### Concept Groups diff --git a/spp_cel_vocabulary/USAGE.md b/spp_cel_vocabulary/USAGE.md index 37eb274b..52455dc6 100644 --- a/spp_cel_vocabulary/USAGE.md +++ b/spp_cel_vocabulary/USAGE.md @@ -16,7 +16,7 @@ OpenSPP eligibility rules and filters. | `code_eq(field, id)` | Safe code comparison | `code_eq(r.gender_id, "female")` | | `is_female(field)` | Check feminine gender | `is_female(r.gender_id)` | | `is_male(field)` | Check masculine gender | `is_male(r.gender_id)` | -| `is_head(field)` | Check head of household code | `is_head(r.relationship_type)` | +| `is_head(field)` | Check head of household code | `is_head(r.relationship_type_id)` | | `is_pregnant(field)` | Check pregnancy status | `is_pregnant(r.pregnancy_status_id)` | | `head(member)` | Check if member is household head | `head(m)` (takes member record, not a code field) | @@ -89,11 +89,15 @@ r.gender_id == code("urn:iso:std:iso:5218#2") # By URI **Use Case:** Find heads of households +> **Note:** `is_head()` requires a Many2one vocabulary code field (e.g., +> `relationship_type_id`) defined by your deployment module. For checking if a member is +> head of household without a code field, use `head(m)` instead. + ```cel -is_head(r.relationship_type) +is_head(r.relationship_type_id) ``` -**In group context:** +**In group context (recommended — no code field needed):** ```cel members.exists(m, head(m)) diff --git a/spp_cel_vocabulary/data/README.md b/spp_cel_vocabulary/data/README.md index f62d2091..4fdecc95 100644 --- a/spp_cel_vocabulary/data/README.md +++ b/spp_cel_vocabulary/data/README.md @@ -26,7 +26,7 @@ them. ### Option 1: Through UI -1. Go to **Settings → Vocabularies → Concept Groups** +1. Go to **Settings > Vocabularies > Concept Groups** 2. Open a concept group 3. Click **Edit** 4. In the **Codes** tab, add your vocabulary codes diff --git a/spp_cel_vocabulary/data/concept_groups.xml b/spp_cel_vocabulary/data/concept_groups.xml index 7f9e47f8..0a834a83 100644 --- a/spp_cel_vocabulary/data/concept_groups.xml +++ b/spp_cel_vocabulary/data/concept_groups.xml @@ -18,12 +18,12 @@ - elderly - persons_with_disability - To add codes to a concept group, use the UI or data files like: + To add codes to a concept group, use the UI or Python code: - - - - + group = env['spp.vocabulary.concept.group'].search( + [('name', '=', 'feminine_gender')], limit=1 + ) + group.write({'code_ids': [Command.link(your_female_code.id)]}) ADR-016 Phase 3: CEL Integration --> diff --git a/spp_cel_vocabulary/readme/DESCRIPTION.md b/spp_cel_vocabulary/readme/DESCRIPTION.md index 2914db45..6c268f8a 100644 --- a/spp_cel_vocabulary/readme/DESCRIPTION.md +++ b/spp_cel_vocabulary/readme/DESCRIPTION.md @@ -34,14 +34,14 @@ r.gender_id == code("urn:iso:std:iso:5218#2") // Group membership in_group(r.gender_id, "feminine_gender") -members.exists(m, in_group(m.relationship_type, "head_of_household")) +members.exists(m, in_group(m.relationship_type_id, "head_of_household")) // Semantic helpers -is_female(r.gender_id) && age_years(r.birthdate) >= 18 -members.exists(m, is_male(m.gender_id) && is_head(m.relationship_type)) +is_female(r.gender_id) and age_years(r.birthdate) >= 18 +members.exists(m, is_male(m.gender_id) and is_head(m.relationship_type_id)) // Head of household check -members.exists(m, head(m) && is_female(m.gender_id)) +members.exists(m, head(m) and is_female(m.gender_id)) ``` ### Security diff --git a/spp_cel_vocabulary/services/cel_vocabulary_functions.py b/spp_cel_vocabulary/services/cel_vocabulary_functions.py index 9d8b1083..fa9ff61b 100644 --- a/spp_cel_vocabulary/services/cel_vocabulary_functions.py +++ b/spp_cel_vocabulary/services/cel_vocabulary_functions.py @@ -183,8 +183,14 @@ def is_head(env, code_value): """Check if code is in head_of_household group. Usage in CEL: - is_head(r.relationship_type) - members.exists(m, is_head(m.relationship_type)) + is_head(r.relationship_type_id) # Requires deployment-defined code field + members.exists(m, is_head(m.relationship_type_id)) + + Note: + This function checks if a vocabulary code is in the head_of_household + group. It requires a Many2one field pointing to spp.vocabulary.code. + For checking if a member record is head of household (without a code + field), use head(member) instead. Args: env: Odoo environment (injected by CEL evaluator) @@ -216,7 +222,7 @@ def head(env, member, _membership=None, _group=None): """Check if a member is the head of household. This function is designed for use in member aggregate expressions like: - members.exists(head(m) && is_female(m.gender_id)) + members.exists(m, head(m) and is_female(m.gender_id)) It checks if the member's membership in the group has the 'head' type. From b9a28158007686270b831214e096db2fd3a7959b Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 13 Mar 2026 17:46:57 +0800 Subject: [PATCH 14/15] docs: regenerate README.rst via oca-gen-addon-readme --- spp_cel_vocabulary/README.rst | 22 +++++++++++++++---- .../static/description/index.html | 10 +++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/spp_cel_vocabulary/README.rst b/spp_cel_vocabulary/README.rst index 7f30a0d3..20fab41d 100644 --- a/spp_cel_vocabulary/README.rst +++ b/spp_cel_vocabulary/README.rst @@ -87,14 +87,14 @@ Example eligibility rules using vocabulary functions: // Group membership in_group(r.gender_id, "feminine_gender") - members.exists(m, in_group(m.relationship_type, "head_of_household")) + members.exists(m, in_group(m.relationship_type_id, "head_of_household")) // Semantic helpers - is_female(r.gender_id) && age_years(r.birthdate) >= 18 - members.exists(m, is_male(m.gender_id) && is_head(m.relationship_type)) + is_female(r.gender_id) and age_years(r.birthdate) >= 18 + members.exists(m, is_male(m.gender_id) and is_head(m.relationship_type_id)) // Head of household check - members.exists(m, head(m) && is_female(m.gender_id)) + members.exists(m, head(m) and is_female(m.gender_id)) Security ~~~~~~~~ @@ -150,6 +150,20 @@ Authors Maintainers ----------- +.. |maintainer-jeremi| image:: https://github.com/jeremi.png?size=40px + :target: https://github.com/jeremi + :alt: jeremi +.. |maintainer-gonzalesedwin1123| image:: https://github.com/gonzalesedwin1123.png?size=40px + :target: https://github.com/gonzalesedwin1123 + :alt: gonzalesedwin1123 +.. |maintainer-emjay0921| image:: https://github.com/emjay0921.png?size=40px + :target: https://github.com/emjay0921 + :alt: emjay0921 + +Current maintainers: + +|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-emjay0921| + This module is part of the `OpenSPP/OpenSPP2 `_ project on GitHub. You are welcome to contribute. \ No newline at end of file diff --git a/spp_cel_vocabulary/static/description/index.html b/spp_cel_vocabulary/static/description/index.html index 6fd1f7f0..8f49d9a3 100644 --- a/spp_cel_vocabulary/static/description/index.html +++ b/spp_cel_vocabulary/static/description/index.html @@ -444,14 +444,14 @@

Usage in CEL Expressions

// Group membership in_group(r.gender_id, "feminine_gender") -members.exists(m, in_group(m.relationship_type, "head_of_household")) +members.exists(m, in_group(m.relationship_type_id, "head_of_household")) // Semantic helpers -is_female(r.gender_id) && age_years(r.birthdate) >= 18 -members.exists(m, is_male(m.gender_id) && is_head(m.relationship_type)) +is_female(r.gender_id) and age_years(r.birthdate) >= 18 +members.exists(m, is_male(m.gender_id) and is_head(m.relationship_type_id)) // Head of household check -members.exists(m, head(m) && is_female(m.gender_id)) +members.exists(m, head(m) and is_female(m.gender_id))
@@ -519,6 +519,8 @@

Authors

Maintainers

+

Current maintainers:

+

jeremi gonzalesedwin1123 emjay0921

This module is part of the OpenSPP/OpenSPP2 project on GitHub.

You are welcome to contribute.

From 00919a98cd6f7cf6a8263e884d63e87725513300 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Fri, 13 Mar 2026 17:51:19 +0800 Subject: [PATCH 15/15] docs: add UI testing guide for QA in readme/USAGE.md 10 test scenarios covering: concept group verification, adding codes, idempotent installation, CEL expression validation, domain translation, empty/nonexistent group behavior, local codes, security, and search. --- spp_cel_vocabulary/README.rst | 295 +++++++++++++++ spp_cel_vocabulary/readme/USAGE.md | 242 ++++++++++++ .../static/description/index.html | 344 +++++++++++++++++- 3 files changed, 873 insertions(+), 8 deletions(-) create mode 100644 spp_cel_vocabulary/readme/USAGE.md diff --git a/spp_cel_vocabulary/README.rst b/spp_cel_vocabulary/README.rst index 20fab41d..abaa5d33 100644 --- a/spp_cel_vocabulary/README.rst +++ b/spp_cel_vocabulary/README.rst @@ -129,6 +129,301 @@ Dependencies .. contents:: :local: +Usage +===== + +UI Testing Guide +---------------- + +This module has no views of its own. All UI interaction is through views +provided by ``spp_vocabulary`` and ``spp_cel_domain``. This guide covers +what to verify after installing ``spp_cel_vocabulary``. + +Prerequisites +~~~~~~~~~~~~~ + +- Install ``spp_cel_vocabulary`` (auto-installs when both + ``spp_cel_domain`` and ``spp_vocabulary`` are present) +- Log in as an admin user +- Have at least one vocabulary with codes (e.g., a gender vocabulary) + +-------------- + +Test 1: Verify Concept Groups Are Created +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path:** Settings > Vocabularies > Concept Groups + +**Steps:** + +1. Navigate to **Settings > Vocabularies > Concept Groups** +2. Verify the following 10 groups exist: + +=========================== ======================= =============== +Name Label CEL Function +=========================== ======================= =============== +``feminine_gender`` Feminine Gender ``is_female`` +``masculine_gender`` Masculine Gender ``is_male`` +``head_of_household`` Head of Household ``is_head`` +``pregnant_eligible`` Pregnant/Eligible ``is_pregnant`` +``climate_hazards`` Climate-related Hazards *(empty)* +``geophysical_hazards`` Geophysical Hazards *(empty)* +``children`` Children *(empty)* +``adults`` Adults *(empty)* +``elderly`` Elderly/Senior Citizens *(empty)* +``persons_with_disability`` Persons with Disability *(empty)* +=========================== ======================= =============== + +3. Open ``feminine_gender`` and verify: + + - Label: "Feminine Gender" + - CEL Function: ``is_female`` + - Description is populated + - **Codes** tab is empty (groups are created empty by default) + +**Expected:** All 10 groups are present with correct labels, CEL +functions, and descriptions. + +-------------- + +Test 2: Add Codes to a Concept Group +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path:** Settings > Vocabularies > Concept Groups > (select group) > +Codes tab + +**Precondition:** A gender vocabulary with at least a "Female" code must +exist. + +**Steps:** + +1. Open ``feminine_gender`` concept group +2. Click **Edit** +3. Go to the **Codes** tab +4. Click **Add a line** +5. Search for and select your "Female" vocabulary code +6. Click **Save** + +**Verify:** + +- The code appears in the Codes tab with its vocabulary, code value, and + display name +- If the code has a URI, it should be visible (may need Technical tab + for ``code_uris`` field) + +**Repeat** for ``masculine_gender`` with a "Male" code if available. + +-------------- + +Test 3: Idempotent Installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Steps:** + +1. Note the current concept groups and their IDs +2. Upgrade ``spp_cel_vocabulary`` module (Settings > Apps > + spp_cel_vocabulary > Upgrade) +3. Navigate back to **Settings > Vocabularies > Concept Groups** + +**Expected:** + +- No duplicate groups were created +- Existing groups retain their codes (codes added in Test 2 are still + there) +- All 10 groups still present + +-------------- + +Test 4: CEL Expression Validation with Vocabulary Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path:** Custom > CEL Domain > Tools > Rule Preview + +**Precondition:** Codes have been added to ``feminine_gender`` group +(Test 2). + +**Steps:** + +1. Navigate to **Custom > CEL Domain > Tools > Rule Preview** +2. Select a model (e.g., the individual registrant model) +3. Enter the expression: ``is_female(r.gender_id)`` +4. Click **Validate & Preview** + +**Expected:** + +- Validation succeeds (no error) +- The **Summary** tab shows: + + - ``preview_count``: number of matching records (may be 0 if no + registrants have gender set) + - ``explain_text``: a human-readable explanation mentioning + ``feminine_gender`` and URIs + +**Repeat with these expressions:** + ++-----------------------------------------------------------+----------------------------------+ +| Expression | Should Validate | ++===========================================================+==================================+ +| ``is_female(r.gender_id)`` | Yes | ++-----------------------------------------------------------+----------------------------------+ +| ``is_male(r.gender_id)`` | Yes | ++-----------------------------------------------------------+----------------------------------+ +| ``in_group(r.gender_id, "feminine_gender")`` | Yes | ++-----------------------------------------------------------+----------------------------------+ +| ``code_eq(r.gender_id, "female")`` | Yes (if code exists) | ++-----------------------------------------------------------+----------------------------------+ +| ``r.gender_id == code("female")`` | Yes (if code exists) | ++-----------------------------------------------------------+----------------------------------+ +| ``members.exists(m, head(m) and is_female(m.gender_id))`` | Yes | ++-----------------------------------------------------------+----------------------------------+ +| ``in_group(r.gender_id, "nonexistent_group")`` | Yes (validates but matches | +| | nothing) | ++-----------------------------------------------------------+----------------------------------+ +| ``is_female(r.name)`` | Depends on translator behavior | ++-----------------------------------------------------------+----------------------------------+ + +-------------- + +Test 5: Domain Translation Verification +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path:** Custom > CEL Domain > Tools > Rule Preview + +**Precondition:** ``feminine_gender`` group has at least one code with a +URI. + +**Steps:** + +1. Open Rule Preview +2. Enter: ``is_female(r.gender_id)`` +3. Click **Validate & Preview** +4. Check the **Summary** tab explanation text + +**Expected:** + +- The explanation should reference ``feminine_gender`` and list the code + URIs +- The generated domain should check both ``gender_id.uri`` and + ``gender_id.reference_uri`` + +-------------- + +Test 6: Empty Group Behavior +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path:** Custom > CEL Domain > Tools > Rule Preview + +**Precondition:** ``climate_hazards`` group exists but has no codes +assigned. + +**Steps:** + +1. Open Rule Preview +2. Enter: ``in_group(r.gender_id, "climate_hazards")`` +3. Click **Validate & Preview** + +**Expected:** + +- Validation succeeds but matches 0 records +- The explanation should indicate the group is empty (e.g., + ``[EMPTY GROUP]``) +- Check Odoo logs for a ``[CEL Vocabulary]`` warning about the empty + group + +-------------- + +Test 7: Nonexistent Group Behavior +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Steps:** + +1. Open Rule Preview +2. Enter: ``in_group(r.gender_id, "does_not_exist")`` +3. Click **Validate & Preview** + +**Expected:** + +- Validation succeeds but matches 0 records +- The explanation should indicate the group was not found (e.g., + ``[GROUP NOT FOUND]``) +- Check Odoo logs for a warning with guidance: "Check Settings > + Vocabularies > Concept Groups" + +-------------- + +Test 8: Local Code Support (if applicable) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Precondition:** A local vocabulary code exists with +``is_local = True`` and a ``reference_uri`` pointing to a standard +code's URI. Both codes are in the same concept group. + +**Steps:** + +1. Create a local code (e.g., "Babae" with ``reference_uri`` pointing to + the Female standard code's URI) +2. Add both the standard and local codes to ``feminine_gender`` group +3. Open Rule Preview +4. Enter: ``is_female(r.gender_id)`` +5. Click **Validate & Preview** + +**Expected:** + +- The domain checks both ``uri`` and ``reference_uri`` fields +- Records with either the standard code or the local code should be + matched + +-------------- + +Test 9: Security - Read-Only Access +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Steps:** + +1. Log in as a non-admin user (base user group) +2. Navigate to **Settings > Vocabularies > Concept Groups** + +**Expected:** + +- User can view concept groups (read access) +- User cannot create, edit, or delete concept groups + +-------------- + +Test 10: Concept Group Search and Filters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Path:** Settings > Vocabularies > Concept Groups + +**Steps:** + +1. Use the search bar to search for "gender" +2. Apply the "Has CEL Function" filter + +**Expected:** + +- Search by "gender" finds ``feminine_gender`` and ``masculine_gender`` +- "Has CEL Function" filter shows only groups with a CEL function set + (``feminine_gender``, ``masculine_gender``, ``head_of_household``, + ``pregnant_eligible``) + +-------------- + +Troubleshooting +~~~~~~~~~~~~~~~ + +If expressions return unexpected results: + +1. **Check logs** for ``[CEL Vocabulary]`` entries — error messages + include guidance on how to fix the issue +2. **Verify codes are in groups** — open the concept group and check the + Codes tab +3. **Verify code URIs** — codes need a ``uri`` field populated for + domain matching +4. **Check field names** — vocabulary functions require Many2one fields + pointing to ``spp.vocabulary.code`` (e.g., ``gender_id``, not + ``gender``) + Bug Tracker =========== diff --git a/spp_cel_vocabulary/readme/USAGE.md b/spp_cel_vocabulary/readme/USAGE.md new file mode 100644 index 00000000..dc7d929e --- /dev/null +++ b/spp_cel_vocabulary/readme/USAGE.md @@ -0,0 +1,242 @@ +## UI Testing Guide + +This module has no views of its own. All UI interaction is through views provided +by `spp_vocabulary` and `spp_cel_domain`. This guide covers what to verify after +installing `spp_cel_vocabulary`. + +### Prerequisites + +- Install `spp_cel_vocabulary` (auto-installs when both `spp_cel_domain` and + `spp_vocabulary` are present) +- Log in as an admin user +- Have at least one vocabulary with codes (e.g., a gender vocabulary) + +--- + +### Test 1: Verify Concept Groups Are Created + +**Path:** Settings > Vocabularies > Concept Groups + +**Steps:** + +1. Navigate to **Settings > Vocabularies > Concept Groups** +2. Verify the following 10 groups exist: + +| Name | Label | CEL Function | +| ------------------------ | ------------------------ | -------------- | +| `feminine_gender` | Feminine Gender | `is_female` | +| `masculine_gender` | Masculine Gender | `is_male` | +| `head_of_household` | Head of Household | `is_head` | +| `pregnant_eligible` | Pregnant/Eligible | `is_pregnant` | +| `climate_hazards` | Climate-related Hazards | _(empty)_ | +| `geophysical_hazards` | Geophysical Hazards | _(empty)_ | +| `children` | Children | _(empty)_ | +| `adults` | Adults | _(empty)_ | +| `elderly` | Elderly/Senior Citizens | _(empty)_ | +| `persons_with_disability`| Persons with Disability | _(empty)_ | + +3. Open `feminine_gender` and verify: + - Label: "Feminine Gender" + - CEL Function: `is_female` + - Description is populated + - **Codes** tab is empty (groups are created empty by default) + +**Expected:** All 10 groups are present with correct labels, CEL functions, and descriptions. + +--- + +### Test 2: Add Codes to a Concept Group + +**Path:** Settings > Vocabularies > Concept Groups > (select group) > Codes tab + +**Precondition:** A gender vocabulary with at least a "Female" code must exist. + +**Steps:** + +1. Open `feminine_gender` concept group +2. Click **Edit** +3. Go to the **Codes** tab +4. Click **Add a line** +5. Search for and select your "Female" vocabulary code +6. Click **Save** + +**Verify:** + +- The code appears in the Codes tab with its vocabulary, code value, and display name +- If the code has a URI, it should be visible (may need Technical tab for `code_uris` field) + +**Repeat** for `masculine_gender` with a "Male" code if available. + +--- + +### Test 3: Idempotent Installation + +**Steps:** + +1. Note the current concept groups and their IDs +2. Upgrade `spp_cel_vocabulary` module (Settings > Apps > spp_cel_vocabulary > Upgrade) +3. Navigate back to **Settings > Vocabularies > Concept Groups** + +**Expected:** + +- No duplicate groups were created +- Existing groups retain their codes (codes added in Test 2 are still there) +- All 10 groups still present + +--- + +### Test 4: CEL Expression Validation with Vocabulary Functions + +**Path:** Custom > CEL Domain > Tools > Rule Preview + +**Precondition:** Codes have been added to `feminine_gender` group (Test 2). + +**Steps:** + +1. Navigate to **Custom > CEL Domain > Tools > Rule Preview** +2. Select a model (e.g., the individual registrant model) +3. Enter the expression: `is_female(r.gender_id)` +4. Click **Validate & Preview** + +**Expected:** + +- Validation succeeds (no error) +- The **Summary** tab shows: + - `preview_count`: number of matching records (may be 0 if no registrants have gender set) + - `explain_text`: a human-readable explanation mentioning `feminine_gender` and URIs + +**Repeat with these expressions:** + +| Expression | Should Validate | +| ------------------------------------------------- | --------------- | +| `is_female(r.gender_id)` | Yes | +| `is_male(r.gender_id)` | Yes | +| `in_group(r.gender_id, "feminine_gender")` | Yes | +| `code_eq(r.gender_id, "female")` | Yes (if code exists) | +| `r.gender_id == code("female")` | Yes (if code exists) | +| `members.exists(m, head(m) and is_female(m.gender_id))` | Yes | +| `in_group(r.gender_id, "nonexistent_group")` | Yes (validates but matches nothing) | +| `is_female(r.name)` | Depends on translator behavior | + +--- + +### Test 5: Domain Translation Verification + +**Path:** Custom > CEL Domain > Tools > Rule Preview + +**Precondition:** `feminine_gender` group has at least one code with a URI. + +**Steps:** + +1. Open Rule Preview +2. Enter: `is_female(r.gender_id)` +3. Click **Validate & Preview** +4. Check the **Summary** tab explanation text + +**Expected:** + +- The explanation should reference `feminine_gender` and list the code URIs +- The generated domain should check both `gender_id.uri` and `gender_id.reference_uri` + +--- + +### Test 6: Empty Group Behavior + +**Path:** Custom > CEL Domain > Tools > Rule Preview + +**Precondition:** `climate_hazards` group exists but has no codes assigned. + +**Steps:** + +1. Open Rule Preview +2. Enter: `in_group(r.gender_id, "climate_hazards")` +3. Click **Validate & Preview** + +**Expected:** + +- Validation succeeds but matches 0 records +- The explanation should indicate the group is empty (e.g., `[EMPTY GROUP]`) +- Check Odoo logs for a `[CEL Vocabulary]` warning about the empty group + +--- + +### Test 7: Nonexistent Group Behavior + +**Steps:** + +1. Open Rule Preview +2. Enter: `in_group(r.gender_id, "does_not_exist")` +3. Click **Validate & Preview** + +**Expected:** + +- Validation succeeds but matches 0 records +- The explanation should indicate the group was not found (e.g., `[GROUP NOT FOUND]`) +- Check Odoo logs for a warning with guidance: + "Check Settings > Vocabularies > Concept Groups" + +--- + +### Test 8: Local Code Support (if applicable) + +**Precondition:** A local vocabulary code exists with `is_local = True` and a +`reference_uri` pointing to a standard code's URI. Both codes are in the same +concept group. + +**Steps:** + +1. Create a local code (e.g., "Babae" with `reference_uri` pointing to the Female + standard code's URI) +2. Add both the standard and local codes to `feminine_gender` group +3. Open Rule Preview +4. Enter: `is_female(r.gender_id)` +5. Click **Validate & Preview** + +**Expected:** + +- The domain checks both `uri` and `reference_uri` fields +- Records with either the standard code or the local code should be matched + +--- + +### Test 9: Security - Read-Only Access + +**Steps:** + +1. Log in as a non-admin user (base user group) +2. Navigate to **Settings > Vocabularies > Concept Groups** + +**Expected:** + +- User can view concept groups (read access) +- User cannot create, edit, or delete concept groups + +--- + +### Test 10: Concept Group Search and Filters + +**Path:** Settings > Vocabularies > Concept Groups + +**Steps:** + +1. Use the search bar to search for "gender" +2. Apply the "Has CEL Function" filter + +**Expected:** + +- Search by "gender" finds `feminine_gender` and `masculine_gender` +- "Has CEL Function" filter shows only groups with a CEL function set + (`feminine_gender`, `masculine_gender`, `head_of_household`, `pregnant_eligible`) + +--- + +### Troubleshooting + +If expressions return unexpected results: + +1. **Check logs** for `[CEL Vocabulary]` entries — error messages include + guidance on how to fix the issue +2. **Verify codes are in groups** — open the concept group and check the Codes tab +3. **Verify code URIs** — codes need a `uri` field populated for domain matching +4. **Check field names** — vocabulary functions require Many2one fields pointing to + `spp.vocabulary.code` (e.g., `gender_id`, not `gender`) diff --git a/spp_cel_vocabulary/static/description/index.html b/spp_cel_vocabulary/static/description/index.html index 8f49d9a3..1f2d1dc6 100644 --- a/spp_cel_vocabulary/static/description/index.html +++ b/spp_cel_vocabulary/static/description/index.html @@ -493,16 +493,344 @@

Dependencies

Table of contents

+
+

Usage

+
+

UI Testing Guide

+

This module has no views of its own. All UI interaction is through views +provided by spp_vocabulary and spp_cel_domain. This guide covers +what to verify after installing spp_cel_vocabulary.

+
+
+ +
+

Prerequisites

+
    +
  • Install spp_cel_vocabulary (auto-installs when both +spp_cel_domain and spp_vocabulary are present)
  • +
  • Log in as an admin user
  • +
  • Have at least one vocabulary with codes (e.g., a gender vocabulary)
  • +
+
+
+
+

Test 1: Verify Concept Groups Are Created

+

Path: Settings > Vocabularies > Concept Groups

+

Steps:

+
    +
  1. Navigate to Settings > Vocabularies > Concept Groups
  2. +
  3. Verify the following 10 groups exist:
  4. +
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameLabelCEL Function
feminine_genderFeminine Genderis_female
masculine_genderMasculine Genderis_male
head_of_householdHead of Householdis_head
pregnant_eligiblePregnant/Eligibleis_pregnant
climate_hazardsClimate-related Hazards(empty)
geophysical_hazardsGeophysical Hazards(empty)
childrenChildren(empty)
adultsAdults(empty)
elderlyElderly/Senior Citizens(empty)
persons_with_disabilityPersons with Disability(empty)
+
    +
  1. Open feminine_gender and verify:
      +
    • Label: “Feminine Gender”
    • +
    • CEL Function: is_female
    • +
    • Description is populated
    • +
    • Codes tab is empty (groups are created empty by default)
    • +
    +
  2. +
+

Expected: All 10 groups are present with correct labels, CEL +functions, and descriptions.

+
+
+
+

Test 2: Add Codes to a Concept Group

+

Path: Settings > Vocabularies > Concept Groups > (select group) > +Codes tab

+

Precondition: A gender vocabulary with at least a “Female” code must +exist.

+

Steps:

+
    +
  1. Open feminine_gender concept group
  2. +
  3. Click Edit
  4. +
  5. Go to the Codes tab
  6. +
  7. Click Add a line
  8. +
  9. Search for and select your “Female” vocabulary code
  10. +
  11. Click Save
  12. +
+

Verify:

+
    +
  • The code appears in the Codes tab with its vocabulary, code value, and +display name
  • +
  • If the code has a URI, it should be visible (may need Technical tab +for code_uris field)
  • +
+

Repeat for masculine_gender with a “Male” code if available.

+
+
+
+

Test 3: Idempotent Installation

+

Steps:

+
    +
  1. Note the current concept groups and their IDs
  2. +
  3. Upgrade spp_cel_vocabulary module (Settings > Apps > +spp_cel_vocabulary > Upgrade)
  4. +
  5. Navigate back to Settings > Vocabularies > Concept Groups
  6. +
+

Expected:

+
    +
  • No duplicate groups were created
  • +
  • Existing groups retain their codes (codes added in Test 2 are still +there)
  • +
  • All 10 groups still present
  • +
+
+
+
+

Test 4: CEL Expression Validation with Vocabulary Functions

+

Path: Custom > CEL Domain > Tools > Rule Preview

+

Precondition: Codes have been added to feminine_gender group +(Test 2).

+

Steps:

+
    +
  1. Navigate to Custom > CEL Domain > Tools > Rule Preview
  2. +
  3. Select a model (e.g., the individual registrant model)
  4. +
  5. Enter the expression: is_female(r.gender_id)
  6. +
  7. Click Validate & Preview
  8. +
+

Expected:

+
    +
  • Validation succeeds (no error)
  • +
  • The Summary tab shows:
      +
    • preview_count: number of matching records (may be 0 if no +registrants have gender set)
    • +
    • explain_text: a human-readable explanation mentioning +feminine_gender and URIs
    • +
    +
  • +
+

Repeat with these expressions:

+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ExpressionShould Validate
is_female(r.gender_id)Yes
is_male(r.gender_id)Yes
in_group(r.gender_id, "feminine_gender")Yes
code_eq(r.gender_id, "female")Yes (if code exists)
r.gender_id == code("female")Yes (if code exists)
members.exists(m, head(m) and is_female(m.gender_id))Yes
in_group(r.gender_id, "nonexistent_group")Yes (validates but matches +nothing)
is_female(r.name)Depends on translator behavior
+
+
+
+

Test 5: Domain Translation Verification

+

Path: Custom > CEL Domain > Tools > Rule Preview

+

Precondition: feminine_gender group has at least one code with a +URI.

+

Steps:

+
    +
  1. Open Rule Preview
  2. +
  3. Enter: is_female(r.gender_id)
  4. +
  5. Click Validate & Preview
  6. +
  7. Check the Summary tab explanation text
  8. +
+

Expected:

+
    +
  • The explanation should reference feminine_gender and list the code +URIs
  • +
  • The generated domain should check both gender_id.uri and +gender_id.reference_uri
  • +
+
+
+
+

Test 6: Empty Group Behavior

+

Path: Custom > CEL Domain > Tools > Rule Preview

+

Precondition: climate_hazards group exists but has no codes +assigned.

+

Steps:

+
    +
  1. Open Rule Preview
  2. +
  3. Enter: in_group(r.gender_id, "climate_hazards")
  4. +
  5. Click Validate & Preview
  6. +
+

Expected:

+
    +
  • Validation succeeds but matches 0 records
  • +
  • The explanation should indicate the group is empty (e.g., +[EMPTY GROUP])
  • +
  • Check Odoo logs for a [CEL Vocabulary] warning about the empty +group
  • +
+
+
+
+

Test 7: Nonexistent Group Behavior

+

Steps:

+
    +
  1. Open Rule Preview
  2. +
  3. Enter: in_group(r.gender_id, "does_not_exist")
  4. +
  5. Click Validate & Preview
  6. +
+

Expected:

+
    +
  • Validation succeeds but matches 0 records
  • +
  • The explanation should indicate the group was not found (e.g., +[GROUP NOT FOUND])
  • +
  • Check Odoo logs for a warning with guidance: “Check Settings > +Vocabularies > Concept Groups”
  • +
+
+
+
+

Test 8: Local Code Support (if applicable)

+

Precondition: A local vocabulary code exists with +is_local = True and a reference_uri pointing to a standard +code’s URI. Both codes are in the same concept group.

+

Steps:

+
    +
  1. Create a local code (e.g., “Babae” with reference_uri pointing to +the Female standard code’s URI)
  2. +
  3. Add both the standard and local codes to feminine_gender group
  4. +
  5. Open Rule Preview
  6. +
  7. Enter: is_female(r.gender_id)
  8. +
  9. Click Validate & Preview
  10. +
+

Expected:

+
    +
  • The domain checks both uri and reference_uri fields
  • +
  • Records with either the standard code or the local code should be +matched
  • +
+
+
+
+

Test 9: Security - Read-Only Access

+

Steps:

+
    +
  1. Log in as a non-admin user (base user group)
  2. +
  3. Navigate to Settings > Vocabularies > Concept Groups
  4. +
+

Expected:

+
    +
  • User can view concept groups (read access)
  • +
  • User cannot create, edit, or delete concept groups
  • +
+
+
+
+

Test 10: Concept Group Search and Filters

+

Path: Settings > Vocabularies > Concept Groups

+

Steps:

+
    +
  1. Use the search bar to search for “gender”
  2. +
  3. Apply the “Has CEL Function” filter
  4. +
+

Expected:

+
    +
  • Search by “gender” finds feminine_gender and masculine_gender
  • +
  • “Has CEL Function” filter shows only groups with a CEL function set +(feminine_gender, masculine_gender, head_of_household, +pregnant_eligible)
  • +
+
+
+
+

Troubleshooting

+

If expressions return unexpected results:

+
    +
  1. Check logs for [CEL Vocabulary] entries — error messages +include guidance on how to fix the issue
  2. +
  3. Verify codes are in groups — open the concept group and check the +Codes tab
  4. +
  5. Verify code URIs — codes need a uri field populated for +domain matching
  6. +
  7. Check field names — vocabulary functions require Many2one fields +pointing to spp.vocabulary.code (e.g., gender_id, not +gender)
  8. +
-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -510,15 +838,15 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • OpenSPP.org
-

Maintainers

+

Maintainers

Current maintainers:

jeremi gonzalesedwin1123 emjay0921

This module is part of the OpenSPP/OpenSPP2 project on GitHub.