From cf17b9ec50f1a0fdc664f6171c62036170394de2 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Wed, 22 Jul 2026 10:30:01 +0800 Subject: [PATCH 1/2] fix(farmer_demo): assign FARM group type to farm demo data (#1120) --- .../models/farmer_demo_generator.py | 26 +++++++++++++++++++ .../models/seeded_farm_generator.py | 25 ++++++++++++++++++ .../tests/test_demo_generator.py | 20 ++++++++++++++ .../tests/test_seeded_farm_generator.py | 3 +++ 4 files changed, 74 insertions(+) diff --git a/spp_farmer_registry_demo/models/farmer_demo_generator.py b/spp_farmer_registry_demo/models/farmer_demo_generator.py index e5b963ec1..b4f8d74a9 100644 --- a/spp_farmer_registry_demo/models/farmer_demo_generator.py +++ b/spp_farmer_registry_demo/models/farmer_demo_generator.py @@ -901,6 +901,12 @@ def _create_farm( "farm_size_idle": farm_size_idle, "experience_years": experience_years, } + # OP#1120: farm groups default to the FARM group type (a farm stays a + # farm even when it becomes a member of a cooperative — only the + # cooperative container is typed COOPERATIVE, in _create_cooperatives). + farm_group_type_id = self._ensure_farm_group_type() + if farm_group_type_id: + farm_vals["group_type_id"] = farm_group_type_id if phone: farm_vals["phone"] = phone farm = Partner.create(farm_vals) @@ -1395,6 +1401,26 @@ def _ensure_cooperative_group_type(self): _logger.warning("Could not create cooperative group type vocabulary code") return False + def _ensure_farm_group_type(self): + """Ensure the 'farm' group type vocabulary code exists (OP#1120). + + Farm groups default to this type. Mirrors _ensure_cooperative_group_type + (ADR-016: local codes with is_local=True bypass system protection). + + Returns: + int: vocabulary code ID for the farm group type, or False. + """ + try: + code = self.env["spp.vocabulary.code"].get_or_create_local( + "urn:openspp:vocab:group-type", + "farm", + display="Farm", + ) + return code.id + except Exception: + _logger.warning("Could not create farm group type vocabulary code") + return False + # ────────────────────────────────────────────────────────────────────── # GIS Data (Coordinates + Land Records) # ────────────────────────────────────────────────────────────────────── diff --git a/spp_farmer_registry_demo/models/seeded_farm_generator.py b/spp_farmer_registry_demo/models/seeded_farm_generator.py index ffafea0b1..c5c5e4a07 100644 --- a/spp_farmer_registry_demo/models/seeded_farm_generator.py +++ b/spp_farmer_registry_demo/models/seeded_farm_generator.py @@ -369,6 +369,7 @@ def __init__(self, env, locale="fil_PH", seed=42): self._vocab_cache = {} self._species_cache = {} self._head_type_id = None + self._farm_group_type_id = None # OP#1120 # OP#1114: names already handed out this run, so the ~730 farms drawn # from an 86-surname pool don't end up with duplicate farm names (and, @@ -500,6 +501,10 @@ def generate_all_farms(self, blueprints): "farm_size_idle": idle, "experience_years": experience, } + # OP#1120: seeded farm groups default to the FARM group type. + farm_group_type_id = self._ensure_farm_group_type() + if farm_group_type_id: + gvals["group_type_id"] = farm_group_type_id if gps: gvals["coordinates"] = json.dumps({"type": "Point", "coordinates": [gps[0], gps[1]]}) if area_id: @@ -1151,6 +1156,26 @@ def _get_vocab_code(self, namespace_uri, code): self._vocab_cache[cache_key] = vocab.id if vocab else False return self._vocab_cache[cache_key] + def _ensure_farm_group_type(self): + """Ensure and return the 'farm' group type vocabulary code ID (OP#1120). + + Seeded farm groups default to this type. Created via get_or_create_local + so it exists even if the wizard hasn't already made it, and cached for + the run. + """ + if self._farm_group_type_id is None: + try: + code = self.env["spp.vocabulary.code"].get_or_create_local( + "urn:openspp:vocab:group-type", + "farm", + display="Farm", + ) + self._farm_group_type_id = code.id + except Exception: + _logger.warning("Could not create farm group type vocabulary code") + self._farm_group_type_id = False + return self._farm_group_type_id + def _resolve_species(self, species_code): """Map a species code string to a vocabulary code ID.""" if species_code in self._species_cache: diff --git a/spp_farmer_registry_demo/tests/test_demo_generator.py b/spp_farmer_registry_demo/tests/test_demo_generator.py index 7ad5929fb..37044030b 100644 --- a/spp_farmer_registry_demo/tests/test_demo_generator.py +++ b/spp_farmer_registry_demo/tests/test_demo_generator.py @@ -219,6 +219,8 @@ def test_create_story_farms_are_groups(self): for farm in story_farms.values(): self.assertTrue(farm.is_group) self.assertTrue(farm.is_registrant) + # OP#1120: story farm groups are typed FARM. + self.assertEqual(farm.group_type_id.code, "farm") def test_create_story_farms_have_members(self): """Test story farms have farmer members.""" @@ -262,6 +264,24 @@ def test_create_farm_helper(self): self.assertEqual(farm.name, "Test Farm") self.assertTrue(farm.is_group) self.assertEqual(farm.farm_total_size, 3.0) + # OP#1120: farm groups default to the FARM group type. + self.assertEqual(farm.group_type_id.code, "farm") + + def test_1120_cooperative_container_typed_cooperative(self): + """OP#1120: the cooperative container is typed COOPERATIVE, while its + member farms stay FARM (a farm is still a farm inside a cooperative).""" + wizard = self.Generator.create({"name": _unique("Coop Type Test")}) + story_farms = wizard._create_story_farms() + + cooperatives = wizard._create_cooperatives(story_farms) + self.assertTrue(cooperatives, "expected at least one cooperative") + + for coop in cooperatives.values(): + self.assertEqual(coop.group_type_id.code, "cooperative") + members = self.env["spp.group.membership"].search([("group", "=", coop.id)]) + self.assertTrue(members, "cooperative should have member farms") + for membership in members: + self.assertEqual(membership.individual.group_type_id.code, "farm") def test_maria_santos_profile(self): """Test Maria Santos persona - smallholder rice farmer.""" diff --git a/spp_farmer_registry_demo/tests/test_seeded_farm_generator.py b/spp_farmer_registry_demo/tests/test_seeded_farm_generator.py index a965a2b1b..bbdd7dbce 100644 --- a/spp_farmer_registry_demo/tests/test_seeded_farm_generator.py +++ b/spp_farmer_registry_demo/tests/test_seeded_farm_generator.py @@ -964,6 +964,9 @@ def test_generate_all_farms_mixed_farm_type(self): self.assertTrue(farm.is_group) self.assertTrue(farm.is_registrant) self.assertEqual(len(result["members"]), 2) + # OP#1120: seeded farm groups are typed FARM. + self.assertTrue(farm.group_type_id, "farm group must have a group type") + self.assertEqual(farm.group_type_id.code, "farm") # Verify farm details details = farm.farm_details_id From 0e85af81ad7d1a9b65953b6a66ddb8bb25034589 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Wed, 19 Aug 2026 12:10:44 +0800 Subject: [PATCH 2/2] fix(farmer_demo): bump version and resolve the farm group type once per run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version bump with its changelog entry, per the convention that bumps land in the PR. Nothing malfunctions without an upgrade — this is Python in a demo generator — but a same-version change gives upgraders no signal. The group-type lookup ran once per story farm. It is now resolved once before the loop and passed down, with the per-farm lookup kept as the fallback for direct calls such as the tests. Caching it on the recordset the way the seeded generator does is not available here: that one is a plain utility class, while this is a TransientModel, and recordsets use __slots__ — the first attempt at this raised AttributeError across nine tests. --- spp_farmer_registry_demo/README.rst | 8 ++++++++ spp_farmer_registry_demo/__manifest__.py | 2 +- .../models/farmer_demo_generator.py | 19 +++++++++++++++++-- spp_farmer_registry_demo/readme/HISTORY.md | 4 ++++ .../static/description/index.html | 13 +++++++++++-- 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/spp_farmer_registry_demo/README.rst b/spp_farmer_registry_demo/README.rst index 281dd5018..4283a2405 100644 --- a/spp_farmer_registry_demo/README.rst +++ b/spp_farmer_registry_demo/README.rst @@ -120,6 +120,14 @@ Dependencies Changelog ========= +19.0.2.1.3 +~~~~~~~~~~ + +- fix(farmer_demo): type demo farm groups as FARM. Farm groups were + created with no group type, so they did not read as farms in the + registry; a farm stays typed FARM even when it joins a cooperative, + and only the cooperative container is typed COOPERATIVE (#1120) + 19.0.2.1.1 ~~~~~~~~~~ diff --git a/spp_farmer_registry_demo/__manifest__.py b/spp_farmer_registry_demo/__manifest__.py index 3a0fbab2b..49baee952 100644 --- a/spp_farmer_registry_demo/__manifest__.py +++ b/spp_farmer_registry_demo/__manifest__.py @@ -3,7 +3,7 @@ "name": "OpenSPP Farmer Registry Demo", "summary": "Demo generator for Farmer Registry with fixed stories and volume generation", "category": "OpenSPP", - "version": "19.0.2.1.1", + "version": "19.0.2.1.3", "sequence": 1, "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", diff --git a/spp_farmer_registry_demo/models/farmer_demo_generator.py b/spp_farmer_registry_demo/models/farmer_demo_generator.py index b4f8d74a9..bb3d326a8 100644 --- a/spp_farmer_registry_demo/models/farmer_demo_generator.py +++ b/spp_farmer_registry_demo/models/farmer_demo_generator.py @@ -808,12 +808,16 @@ def _create_story_farms(self): story_farms = {} + # OP#1120 review: resolved once for the run rather than per farm. + farm_group_type_id = self._ensure_farm_group_type() + for story_id, story_data in STORY_FARMS.items(): farm_type_id = self._get_vocab_code("urn:openspp:vocab:farm-type", story_data["farm_type"]) tenure_id = self._get_vocab_code("urn:openspp:vocab:land-tenure", story_data["tenure"]) holder_id = self._get_vocab_code("urn:openspp:vocab:holder-type", "individual") farm = self._create_farm( + farm_group_type_id=farm_group_type_id, name=story_data["farm_name"], farmer_name=story_data["farmer_name"], farm_type_id=farm_type_id, @@ -882,8 +886,13 @@ def _create_farm( phone=None, bank_name=None, age=None, + farm_group_type_id=None, ): - """Create a farm with the given attributes.""" + """Create a farm with the given attributes. + + ``farm_group_type_id`` is resolved by the caller once per run; left None + (a direct call, as in the tests) it is looked up here. + """ Partner = self.env["res.partner"].sudo() # nosemgrep farm_vals = { @@ -904,7 +913,8 @@ def _create_farm( # OP#1120: farm groups default to the FARM group type (a farm stays a # farm even when it becomes a member of a cooperative — only the # cooperative container is typed COOPERATIVE, in _create_cooperatives). - farm_group_type_id = self._ensure_farm_group_type() + if farm_group_type_id is None: + farm_group_type_id = self._ensure_farm_group_type() if farm_group_type_id: farm_vals["group_type_id"] = farm_group_type_id if phone: @@ -1407,6 +1417,11 @@ def _ensure_farm_group_type(self): Farm groups default to this type. Mirrors _ensure_cooperative_group_type (ADR-016: local codes with is_local=True bypass system protection). + Resolved once per run by the caller and passed down, rather than looked + up per farm: recordsets use __slots__, so the run-scoped attribute the + seeded generator caches on (it is a plain utility class, not a model) + is not available here (OP#1120 review). + Returns: int: vocabulary code ID for the farm group type, or False. """ diff --git a/spp_farmer_registry_demo/readme/HISTORY.md b/spp_farmer_registry_demo/readme/HISTORY.md index f397e16d8..cfd8acbc1 100644 --- a/spp_farmer_registry_demo/readme/HISTORY.md +++ b/spp_farmer_registry_demo/readme/HISTORY.md @@ -1,3 +1,7 @@ +### 19.0.2.1.3 + +- fix(farmer_demo): type demo farm groups as FARM. Farm groups were created with no group type, so they did not read as farms in the registry; a farm stays typed FARM even when it joins a cooperative, and only the cooperative container is typed COOPERATIVE (#1120) + ### 19.0.2.1.1 - fix(demo): name each farm after its head member and give every member the head's family name so a household reads as one family; farm names and registry IDs stay unique and generation remains seed-deterministic, resolving duplicate farm names and duplicate Tax/National IDs (#1114) diff --git a/spp_farmer_registry_demo/static/description/index.html b/spp_farmer_registry_demo/static/description/index.html index 4e9aa8e7b..ee53001b5 100644 --- a/spp_farmer_registry_demo/static/description/index.html +++ b/spp_farmer_registry_demo/static/description/index.html @@ -488,6 +488,15 @@

Changelog

+

19.0.2.1.3

+
    +
  • fix(farmer_demo): type demo farm groups as FARM. Farm groups were +created with no group type, so they did not read as farms in the +registry; a farm stays typed FARM even when it joins a cooperative, +and only the cooperative container is typed COOPERATIVE (#1120)
  • +
+
+

19.0.2.1.1

  • fix(demo): name each farm after its head member and give every member @@ -502,7 +511,7 @@

    19.0.2.1.1

    (#1114)
-
+

19.0.2.1.0

  • feat(demo): add GIS + irrigation scenario (FM4) with reservoir + canal @@ -521,7 +530,7 @@

    19.0.2.1.0

    tables and the CR overview
-
+

19.0.2.0.0

  • Initial migration to OpenSPP2