diff --git a/spp_mis_demo_v2/__manifest__.py b/spp_mis_demo_v2/__manifest__.py index b166e37e..8f57e70c 100644 --- a/spp_mis_demo_v2/__manifest__.py +++ b/spp_mis_demo_v2/__manifest__.py @@ -38,6 +38,7 @@ "post_init_hook": "post_init_hook", "data": [ "security/ir.model.access.csv", + "data/vocabulary_group_membership_type.xml", "data/demo_currencies.xml", "data/demo_constants.xml", "data/demo_personas.xml", diff --git a/spp_mis_demo_v2/data/vocabulary_group_membership_type.xml b/spp_mis_demo_v2/data/vocabulary_group_membership_type.xml new file mode 100644 index 00000000..dc76df25 --- /dev/null +++ b/spp_mis_demo_v2/data/vocabulary_group_membership_type.xml @@ -0,0 +1,28 @@ + + + + + child + Child + Child member of the group/household. + 2 + + + + + spouse + Spouse + Spouse or partner of the head of household. + 3 + + + + + other + Other + Other member of the group/household (e.g., relative, dependent). + 10 + + diff --git a/spp_mis_demo_v2/models/mis_demo_generator.py b/spp_mis_demo_v2/models/mis_demo_generator.py index 5ae10807..77d75afb 100644 --- a/spp_mis_demo_v2/models/mis_demo_generator.py +++ b/spp_mis_demo_v2/models/mis_demo_generator.py @@ -901,21 +901,27 @@ def _create_household_members(self, group, profile, days_back): registration_date = fields.Date.today() - datetime.timedelta(days=days_back) members_created = [] + VocabCode = self.env["spp.vocabulary.code"] + type_ids_by_code = {} + for code in ("head", "spouse", "child", "other"): + rec = VocabCode.get_code("urn:openspp:vocab:group-membership-type", code) + type_ids_by_code[code] = rec.id if rec else False + + def _membership_type_commands(code): + tid = type_ids_by_code.get(code) + return [Command.link(tid)] if tid else [] + # Create head of household head_data = profile.get("head", {}) if head_data: head = self._create_individual_member(head_data, registration_date) if head: members_created.append(head) - # Add as head member - head_membership_type = self.env["spp.vocabulary.code"].get_code( - "urn:openspp:vocab:group-membership-type", "head" - ) self.env["spp.group.membership"].create( { "group": group.id, "individual": head.id, - "membership_type_ids": [Command.link(head_membership_type.id)] if head_membership_type else [], + "membership_type_ids": _membership_type_commands("head"), } ) @@ -929,6 +935,7 @@ def _create_household_members(self, group, profile, days_back): { "group": group.id, "individual": spouse.id, + "membership_type_ids": _membership_type_commands("spouse"), } ) @@ -941,6 +948,7 @@ def _create_household_members(self, group, profile, days_back): { "group": group.id, "individual": adult.id, + "membership_type_ids": _membership_type_commands("other"), } ) @@ -953,6 +961,7 @@ def _create_household_members(self, group, profile, days_back): { "group": group.id, "individual": child.id, + "membership_type_ids": _membership_type_commands("child"), } ) @@ -2681,7 +2690,7 @@ def _get_demo_user(self, role): "given_name": "Baby Morales", "family_name": "Morales", "birthdate": fields.Date.today(), - "relationship_xmlid": "spp_registry.group_membership_kind_child", + "relationship_xmlid": "spp_mis_demo_v2.code_membership_type_child", }, }, # Phase 5.1: Add remove_member CR diff --git a/spp_mis_demo_v2/models/seeded_volume_generator.py b/spp_mis_demo_v2/models/seeded_volume_generator.py index 5c0c8946..8bc71f2d 100644 --- a/spp_mis_demo_v2/models/seeded_volume_generator.py +++ b/spp_mis_demo_v2/models/seeded_volume_generator.py @@ -72,7 +72,7 @@ def __init__(self, env, locale, seed=42): # Caches self._gender_cache = {} - self._head_type_id = None + self._membership_type_cache = {} self._group_type_id = None # ========================================================================= @@ -177,7 +177,13 @@ def generate_all_households(self, blueprints): # Phase 4: Create memberships and link to groups _logger.info("Phase 4/%d: Creating %d memberships...", 4, len(individuals)) membership_vals_list = [] - head_type_id = self._get_head_type_id() + role_to_type_code = { + "head": "head", + "spouse": "spouse", + "child": "child", + "adult": "other", + "elderly": "other", + } current_group = None has_head_for_current_group = False @@ -196,8 +202,17 @@ def generate_all_households(self, blueprints): "start_date": group_record.registration_date, } - if member_spec["role"] == "head" and not has_head_for_current_group and head_type_id: - mval["membership_type_ids"] = [(4, head_type_id)] + role = member_spec["role"] + if role == "head" and has_head_for_current_group: + type_code = "other" + else: + type_code = role_to_type_code.get(role, "other") + + type_id = self._get_membership_type_id(type_code) + if type_id: + mval["membership_type_ids"] = [(4, type_id)] + + if role == "head" and not has_head_for_current_group: has_head_for_current_group = True # Update group name to head's family name group_record.name = individual.family_name or individual.name @@ -483,12 +498,12 @@ def _get_gender_id(self, gender): self._gender_cache[gender] = code.id if code else False return self._gender_cache[gender] - def _get_head_type_id(self): - """Get the 'head' membership type ID, with caching.""" - if self._head_type_id is None: - head_type = self.env["spp.vocabulary.code"].get_code("urn:openspp:vocab:group-membership-type", "head") - self._head_type_id = head_type.id if head_type else False - return self._head_type_id + def _get_membership_type_id(self, code): + """Get a group-membership-type vocabulary code ID, with caching.""" + if code not in self._membership_type_cache: + rec = self.env["spp.vocabulary.code"].get_code("urn:openspp:vocab:group-membership-type", code) + self._membership_type_cache[code] = rec.id if rec else False + return self._membership_type_cache[code] def _get_group_type_id(self): """Get a default group type ID, with caching.""" diff --git a/spp_mis_demo_v2/tests/test_mis_demo_generator.py b/spp_mis_demo_v2/tests/test_mis_demo_generator.py index 38d7dfe2..6dc5d09c 100644 --- a/spp_mis_demo_v2/tests/test_mis_demo_generator.py +++ b/spp_mis_demo_v2/tests/test_mis_demo_generator.py @@ -645,6 +645,52 @@ def test_head_of_household_has_correct_membership_type(self): len(head_membership), 1, f"Household '{story_name}' should have exactly one head of household" ) + def test_non_head_members_have_membership_types(self): + """Spouse, child, and other adult members each get the matching membership type.""" + self._run_generator_for_stories() + + VocabCode = self.env["spp.vocabulary.code"] + ns = "urn:openspp:vocab:group-membership-type" + types = {code: VocabCode.get_code(ns, code) for code in ("head", "spouse", "child", "other")} + + if not all(types.values()): + self.skipTest("Group-membership-type vocabulary codes not configured") + + for story_name in ["Bautista", "Navarro", "Morales"]: + group = self.env["res.partner"].search([("name", "=", story_name), ("is_group", "=", True)], limit=1) + if not group: + continue + + memberships = self.env["spp.group.membership"].search([("group", "=", group.id)]) + self.assertTrue(memberships, f"{story_name} should have memberships") + + # Every membership should carry exactly one type from our set + for m in memberships: + assigned = m.membership_type_ids & (types["head"] | types["spouse"] | types["child"] | types["other"]) + self.assertEqual( + len(assigned), + 1, + f"Membership for {m.individual.name} in '{story_name}' should have exactly one " + f"group-membership-type code, got {m.membership_type_ids.mapped('code')}", + ) + + # At most one spouse per household + spouse_memberships = memberships.filtered(lambda x: types["spouse"] in x.membership_type_ids) + self.assertLessEqual(len(spouse_memberships), 1, f"{story_name} should have at most one spouse") + + # 'child' members are younger than the household head + head_membership = memberships.filtered(lambda x: types["head"] in x.membership_type_ids) + if head_membership and head_membership.individual.birthdate: + head_birthdate = head_membership.individual.birthdate + for m in memberships.filtered(lambda x: types["child"] in x.membership_type_ids): + if m.individual.birthdate: + self.assertGreater( + m.individual.birthdate, + head_birthdate, + f"Member {m.individual.name} tagged as 'child' in '{story_name}' " + f"should be younger than the head", + ) + def test_idempotent_member_creation(self): """Test that running generator twice doesn't duplicate members.""" # Run generator first time