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 diff --git a/spp_cel_vocabulary/README.md b/spp_cel_vocabulary/README.md index 82b27915..b68350c1 100644 --- a/spp_cel_vocabulary/README.md +++ b/spp_cel_vocabulary/README.md @@ -5,6 +5,10 @@ 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 +18,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 +28,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 +37,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 +48,23 @@ 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 +72,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 @@ -76,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 @@ -100,7 +108,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 +128,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 +173,45 @@ 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 +228,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 +256,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 +282,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 diff --git a/spp_cel_vocabulary/README.rst b/spp_cel_vocabulary/README.rst index 7f30a0d3..abaa5d33 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 ~~~~~~~~ @@ -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 =========== @@ -150,6 +445,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/USAGE.md b/spp_cel_vocabulary/USAGE.md index dbb143e1..52455dc6 100644 --- a/spp_cel_vocabulary/USAGE.md +++ b/spp_cel_vocabulary/USAGE.md @@ -3,17 +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. + ## 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_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) | + +### 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 +49,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,44 +69,54 @@ 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 **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(me.relationship_type) +is_head(r.relationship_type_id) ``` -**In group context:** +**In group context (recommended — no code field needed):** ```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 +130,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 +145,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 +161,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 +176,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 +205,7 @@ members.exists(child, **CEL Expression:** ```cel -is_female(me.gender) +is_female(r.gender_id) ``` **Works for all of:** @@ -189,7 +226,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 +244,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 +254,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 +271,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 +289,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 +299,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 +312,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 +322,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 +331,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 +363,15 @@ 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 +387,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 +409,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 +438,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 +450,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 +462,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 +474,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 +487,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 +505,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 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", } diff --git a/spp_cel_vocabulary/data/README.md b/spp_cel_vocabulary/data/README.md index a6430bd7..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 **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,11 @@ 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 +53,9 @@ Create a data file in your deployment module: @@ -63,8 +68,8 @@ Create a data file in your deployment module: @@ -76,10 +81,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 +103,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 +114,7 @@ def add_codes_to_groups(env): + @@ -124,8 +135,8 @@ def add_codes_to_groups(env): @@ -138,8 +149,8 @@ def add_codes_to_groups(env): @@ -152,8 +163,8 @@ def add_codes_to_groups(env): @@ -166,11 +177,11 @@ def add_codes_to_groups(env): @@ -188,13 +199,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 +217,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 ``` 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/models/cel_vocabulary_translator.py b/spp_cel_vocabulary/models/cel_vocabulary_translator.py index bd639221..4f4afa52 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,10 @@ 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 +147,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 +208,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 +224,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 +238,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 +273,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 +285,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 +315,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 +356,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 +386,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 +417,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]", 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/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/services/cel_vocabulary_functions.py b/spp_cel_vocabulary/services/cel_vocabulary_functions.py index d824aced..fa9ff61b 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) @@ -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._link.kind)) + 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) @@ -200,7 +206,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) @@ -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. @@ -232,7 +238,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: diff --git a/spp_cel_vocabulary/static/description/index.html b/spp_cel_vocabulary/static/description/index.html index 6fd1f7f0..1f2d1dc6 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))
@@ -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,17 @@

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.

You are welcome to contribute.

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", } )