diff --git a/spp_change_request_v2/README.rst b/spp_change_request_v2/README.rst
index 754b51a3..59e61398 100644
--- a/spp_change_request_v2/README.rst
+++ b/spp_change_request_v2/README.rst
@@ -853,6 +853,22 @@ Before declaring a new CR type complete:
Changelog
=========
+19.0.3.1.14
+~~~~~~~~~~~
+
+- fix(change_request): group-scope conflict rules work again.
+ ``_get_group_member_ids`` traversed ``spp.group.membership`` records
+ through ``individual_id`` and ``group_id``, but that model names its
+ many2ones ``individual`` and ``group`` — so resolving a household's
+ members raised ``KeyError``/``AttributeError`` instead of returning
+ them. A change request whose type carried an active group-scope
+ conflict rule crashed on creation for any group registrant, and for
+ any individual registrant with a live membership — exactly the
+ registrants the rule exists to check. The one existing test called the
+ method with a member-less individual, the single shape that happened
+ to work; group-scope detection is now tested with real memberships in
+ both directions, including that ended memberships are excluded.
+
19.0.3.1.13
~~~~~~~~~~~
diff --git a/spp_change_request_v2/__manifest__.py b/spp_change_request_v2/__manifest__.py
index 50a68562..a6484a53 100644
--- a/spp_change_request_v2/__manifest__.py
+++ b/spp_change_request_v2/__manifest__.py
@@ -1,6 +1,6 @@
{
"name": "OpenSPP Change Request V2",
- "version": "19.0.3.1.13",
+ "version": "19.0.3.1.14",
"sequence": 50,
"category": "OpenSPP",
"summary": "Configuration-driven change request system with UX improvements, conflict detection and duplicate prevention",
diff --git a/spp_change_request_v2/models/conflict_mixin.py b/spp_change_request_v2/models/conflict_mixin.py
index 3bc622e7..96b368f4 100644
--- a/spp_change_request_v2/models/conflict_mixin.py
+++ b/spp_change_request_v2/models/conflict_mixin.py
@@ -279,17 +279,17 @@ def _get_group_member_ids(self):
if registrant.is_group:
if hasattr(registrant, "group_membership_ids"):
member_ids.extend(
- registrant.group_membership_ids.filtered(lambda m: not m.ended_date).mapped("individual_id.id")
+ registrant.group_membership_ids.filtered(lambda m: not m.ended_date).mapped("individual.id")
)
# If registrant is an individual, find their groups and group members
else:
if hasattr(registrant, "individual_membership_ids"):
for membership in registrant.individual_membership_ids.filtered(lambda m: not m.ended_date):
- group = membership.group_id
+ group = membership.group
member_ids.append(group.id)
member_ids.extend(
- group.group_membership_ids.filtered(lambda m: not m.ended_date).mapped("individual_id.id")
+ group.group_membership_ids.filtered(lambda m: not m.ended_date).mapped("individual.id")
)
return list(set(member_ids))
diff --git a/spp_change_request_v2/readme/HISTORY.md b/spp_change_request_v2/readme/HISTORY.md
index f90bf854..5cf9dd00 100644
--- a/spp_change_request_v2/readme/HISTORY.md
+++ b/spp_change_request_v2/readme/HISTORY.md
@@ -1,3 +1,7 @@
+### 19.0.3.1.14
+
+- fix(change_request): group-scope conflict rules work again. `_get_group_member_ids` traversed `spp.group.membership` records through `individual_id` and `group_id`, but that model names its many2ones `individual` and `group` — so resolving a household's members raised `KeyError`/`AttributeError` instead of returning them. A change request whose type carried an active group-scope conflict rule crashed on creation for any group registrant, and for any individual registrant with a live membership — exactly the registrants the rule exists to check. The one existing test called the method with a member-less individual, the single shape that happened to work; group-scope detection is now tested with real memberships in both directions, including that ended memberships are excluded.
+
### 19.0.3.1.13
- fix(change_request): a selectable field on a dynamic-approval type may now be applied through more than one mapping. Apply is narrowed to the field a request was routed and approved on, matched against the mapping's `source_field` — which assumed every selectable value is a physical source field. They need not be: a name may be offered as one choice but stored as separate components, so one selectable value legitimately drives several mappings, and matching on `source_field` alone matched none of them, applying nothing at all. A mapping can now declare the selectable value it serves via `routing_field`, defaulting to `source_field`, so existing configurations are unchanged. Narrowing still holds — a mapping belonging to another routing key is still not applied.
diff --git a/spp_change_request_v2/static/description/index.html b/spp_change_request_v2/static/description/index.html
index 281deb2d..cd7c7753 100644
--- a/spp_change_request_v2/static/description/index.html
+++ b/spp_change_request_v2/static/description/index.html
@@ -1339,6 +1339,23 @@
Changelog
+
19.0.3.1.14
+
+- fix(change_request): group-scope conflict rules work again.
+_get_group_member_ids traversed spp.group.membership records
+through individual_id and group_id, but that model names its
+many2ones individual and group — so resolving a household’s
+members raised KeyError/AttributeError instead of returning
+them. A change request whose type carried an active group-scope
+conflict rule crashed on creation for any group registrant, and for
+any individual registrant with a live membership — exactly the
+registrants the rule exists to check. The one existing test called the
+method with a member-less individual, the single shape that happened
+to work; group-scope detection is now tested with real memberships in
+both directions, including that ended memberships are excluded.
+
+
+
19.0.3.1.13
- fix(change_request): a selectable field on a dynamic-approval type may
@@ -1355,7 +1372,7 @@
19.0.3.1.13
routing key is still not applied.
-
+
19.0.3.1.12
- fix(change_request): auto-apply-on-approve runs through the public
@@ -1370,7 +1387,7 @@
19.0.3.1.12
the applying user is still recorded as the approver.
-
+
19.0.3.1.11
- fix(change_request): field-mapping transform expressions are evaluated
@@ -1409,7 +1426,7 @@
19.0.3.1.11
the full traceback is logged only at DEBUG.
-
+
19.0.3.1.10
- fix(security): conflict and duplicate detection now decide whether a
@@ -1449,7 +1466,7 @@
19.0.3.1.10
configured mapping.
-
+
19.0.3.1.9
- fix(security): duplicate detection now scores the fields both change
@@ -1466,7 +1483,7 @@
19.0.3.1.9
requester-writable selected_field_name / field_to_modify.
-
+
19.0.3.1.8
- fix(security): scope the Create-Group member wizards to the parent
@@ -1484,7 +1501,7 @@
19.0.3.1.8
access-control entry grants.
-
+
19.0.3.1.7
- fix(security): require change-request manager rights to apply a change
@@ -1499,7 +1516,7 @@
19.0.3.1.7
endpoint.
-
+
19.0.3.1.6
- fix(security): derive conflict and duplicate detection from the change
@@ -1513,7 +1530,7 @@
19.0.3.1.6
an empty one, so detection cannot silently disable itself.
-
+
19.0.3.1.5
- fix(security): scope the CR Requestor, Local Validator and HQ
@@ -1525,7 +1542,7 @@
19.0.3.1.5
are noupdate.
-
+
19.0.3.1.4
- fix(security): add ownership and area record rules to every concrete
@@ -1542,7 +1559,7 @@
19.0.3.1.4
unrestricted delete their access-control entries grant.
-
+
19.0.3.1.3
- fix(security): route and apply the same single field for
@@ -1555,7 +1572,7 @@
19.0.3.1.3
the routing selector.
-
+
19.0.3.1.2
- fix(change_request_v2): adding an ID now looks for a live one of that
@@ -1564,7 +1581,7 @@
19.0.3.1.2
(#1136)
-
+
19.0.3.1.1
- fix(change_request): enforce the (cr_type_id, reason) uniqueness
@@ -1578,7 +1595,7 @@
19.0.3.1.1
applied) so the constraint applies cleanly on upgrade.
-
+
19.0.3.1.0
- revert(change_request): restore the create-a-new-individual Add
@@ -1596,7 +1613,7 @@
19.0.3.1.0
not restored here; reinstate separately if needed.
-
+
19.0.3.0.0
- feat(change_request): redesign the group/membership CR flows (#242) —
@@ -1618,7 +1635,7 @@
19.0.3.0.0
must adapt (see #1133).
-
+
19.0.2.0.8
- fix(views): disable inline creation of CR document types on the Change
@@ -1629,7 +1646,7 @@
19.0.2.0.8
Documents” modal (missing Name field) that blocked saving (#1125)
-
+
19.0.2.0.7
- fix(security): align CR Requestor / CR Local Validator / CR HQ
@@ -1641,7 +1658,7 @@
19.0.2.0.7
dependencies.
-
+
19.0.2.0.6
- fix(views): route post-submit CRs (pending / approved / applied /
@@ -1656,7 +1673,7 @@
19.0.2.0.6
list so row-click goes through the stage router.
-
+
19.0.2.0.5
- fix(security): add a global ir.rule on spp.change.request that
@@ -1669,27 +1686,27 @@
19.0.2.0.5
roles).
-
+
19.0.2.0.3
- fix: add HTML escaping to all computed Html fields with
sanitize=False to prevent stored XSS (#50)
-
+
19.0.2.0.2
- fix: fix batch approval wizard line deletion (#130)
-
+
19.0.2.0.1
- fix: skip field types before getattr and isolate detail prefetch
(#129)
-
+
19.0.2.0.0
- Initial migration to OpenSPP2
diff --git a/spp_change_request_v2/tests/test_conflict_detection_extended.py b/spp_change_request_v2/tests/test_conflict_detection_extended.py
index 0cd42572..a1e54918 100644
--- a/spp_change_request_v2/tests/test_conflict_detection_extended.py
+++ b/spp_change_request_v2/tests/test_conflict_detection_extended.py
@@ -10,7 +10,7 @@
- Spec scenarios
"""
-from odoo import Command
+from odoo import Command, fields
from odoo.exceptions import UserError, ValidationError
from odoo.tests import TransactionCase
@@ -493,17 +493,15 @@ def setUpClass(cls):
}
)
- def test_group_scope_same_household_members(self):
- """Test group scope detects conflicts for household members."""
- # Create household with members
- self.env["res.partner"].create(
+ def _create_household(self):
+ """Create a household with two member individuals and return all three."""
+ group = self.env["res.partner"].create(
{
"name": "Test Household",
"is_registrant": True,
"is_group": True,
}
)
-
individual1 = self.env["res.partner"].create(
{
"name": "Member 1",
@@ -511,30 +509,81 @@ def test_group_scope_same_household_members(self):
"is_group": False,
}
)
-
- self.env["res.partner"].create(
+ individual2 = self.env["res.partner"].create(
{
"name": "Member 2",
"is_registrant": True,
"is_group": False,
}
)
+ self.env["spp.group.membership"].create(
+ [
+ {"group": group.id, "individual": individual1.id},
+ {"group": group.id, "individual": individual2.id},
+ ]
+ )
+ return group, individual1, individual2
- # Add members to household (if membership model exists)
- # This is implementation-dependent
- # For now, test the _get_group_member_ids method directly
+ def test_group_scope_same_household_members(self):
+ """Test group scope detects conflicts for household members."""
+ group, individual1, individual2 = self._create_household()
- # Create CR for individual1
- cr1 = self.env["spp.change.request"].create(
+ # Create CR for individual2 first, then for individual1: the group-scope
+ # rule must flag the second CR because both registrants share a household.
+ self.env["spp.change.request"].create(
+ {
+ "request_type_id": self.cr_type.id,
+ "registrant_id": individual2.id,
+ }
+ )
+ cr = self.env["spp.change.request"].create(
+ {
+ "request_type_id": self.cr_type.id,
+ "registrant_id": individual1.id,
+ }
+ )
+
+ # _get_group_member_ids resolves the household and every co-member
+ member_ids = cr._get_group_member_ids()
+ self.assertIn(individual1.id, member_ids)
+ self.assertIn(group.id, member_ids)
+ self.assertIn(individual2.id, member_ids)
+
+ # The rule's action is "warn", so the second CR is flagged, not blocked
+ self.assertEqual(cr.conflict_status, "warning")
+
+ def test_group_scope_group_registrant(self):
+ """A CR whose registrant is the group itself resolves its members."""
+ group, individual1, individual2 = self._create_household()
+
+ cr = self.env["spp.change.request"].create(
+ {
+ "request_type_id": self.cr_type.id,
+ "registrant_id": group.id,
+ }
+ )
+
+ member_ids = cr._get_group_member_ids()
+ self.assertIn(group.id, member_ids)
+ self.assertIn(individual1.id, member_ids)
+ self.assertIn(individual2.id, member_ids)
+
+ def test_group_scope_ended_membership_excluded(self):
+ """Members whose membership has ended are not conflict candidates."""
+ group, individual1, individual2 = self._create_household()
+ individual2.individual_membership_ids.ended_date = fields.Datetime.now()
+
+ cr = self.env["spp.change.request"].create(
{
"request_type_id": self.cr_type.id,
"registrant_id": individual1.id,
}
)
- # Test _get_group_member_ids returns expected members
- member_ids = cr1._get_group_member_ids()
+ member_ids = cr._get_group_member_ids()
self.assertIn(individual1.id, member_ids)
+ self.assertIn(group.id, member_ids)
+ self.assertNotIn(individual2.id, member_ids)
class TestDuplicateDetectionAdvanced(TransactionCase):