fix(change_request): make group-scope conflict rules resolve household members - #477
Conversation
…d members _get_group_member_ids traversed spp.group.membership through individual_id and group_id, but the model names its many2ones individual and group. Resolving a household's members therefore 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 resolution is now tested with real memberships in both directions, including that ended memberships are excluded. Fixes #476
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 19.0 #477 +/- ##
=======================================
Coverage 76.26% 76.27%
=======================================
Files 662 662
Lines 44225 44218 -7
=======================================
- Hits 33729 33727 -2
+ Misses 10496 10491 -5
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
_create_household already creates both membership rows, so the ended membership test can set ended_date through the individual's own membership instead of querying the pair back by (group, individual). That drops a query and, more usefully, a second place spelling the membership field names — the very drift this branch repairs. Also name the change request under test `cr`, matching the sibling tests; the `cr1` name dated from a `cr2` that no longer exists. Restore the README table padding and column widths emitted by CI's generator: regenerating locally on Python 3.14 re-renders an unrelated docutils table one column wider than CI's Python 3.11, which failed the pre-commit check. Only the changelog entry this branch owns remains.
gonzalesedwin1123
left a comment
There was a problem hiding this comment.
Expert review
Verdict: approve with follow-ups. The fix is correct, minimal, and exactly what #476 describes: spp.group.membership really does name its many2ones individual and group (spp_registry/models/group_membership.py:16,22 — whitelisted o2m inverses in scripts/lint/check_naming.py), and this mixin was the only place in the repo traversing membership records with the _id-suffixed names (repo-wide grep; every other individual_id hit is a detail-line model's own field). Version chain is right: 19.0 is at 3.1.13, so .14 is the correct next.
Independently verified
- Green run:
./spp t spp_change_request_v2→ 0 failed, 0 errors of 415 tests (verified in the unittest log, not the exit code). - Red check (TDD claim): with only
conflict_mixin.pyreverted toorigin/19.0, exactly the 3 group-scope tests error with exactly the claimed exceptions —KeyError: 'individual_id'(group branch) andAttributeError: 'spp.group.membership' object has no attribute 'group_id'(individual branch) — the other 412 tests unaffected. - Test integrity: the pre-existing
test_group_scope_same_household_memberswas strengthened, not weakened — its old assertion is implied by the new ones, plus the new end-to-endconflict_status == "warning"check. Nothing removed. - Lint: clean on the changed files; only the known oca-gen README width quirk (unrelated modules) and two pre-existing pylint W8113s outside the diff.
- Generated files:
README.rst/index.htmldiff is the new changelog section plus puresection-Nrenumbering; no unrelated modules dragged in.
Follow-ups worth filing (neither blocks this PR)
not m.ended_datedisagrees with the model's own "ended" semantics.is_ended/statustreat a membership as ended only whenended_date <= now, but the mixin's filter drops any membership withended_dateset — including a future-scheduled exit, which this very module produces (strategies/exit_registrant.pywrites user-supplieddetail.exit_date). A member with a scheduled future exit is still a live member today but silently vanishes from conflict candidates — a false negative in a control that can be configuredaction="block". Note that simply switching tois_endedisn't right either: it's a stored compute depending only onended_datebut compared againstnow()at write time, so a future-dated exit staysis_ended=Falseforever after the date passes (nothing recomputes it — that staleness affects every repo consumer ofis_ended). The correct predicate isnot (m.ended_date and m.ended_date <= fields.Datetime.now()); the stale stored compute in spp_registry deserves its own issue.- Group-scope detection fails open for the requester role.
_detect_conflictssearches as the current user, andrule_cr_user(security/rules.xml) restricts agroup_cr_userto their own CRs — so a co-member's CR created by someone else is invisible and a group-scope rule (evenblock) never fires for requesters; it only works for validators/managers. Pre-existing, but this PR makes the path executable for the first time. Not a simplesudo()fix:conflicting_cr_idsis displayed on the CR form, so populating it with unreadable records trades fail-open for anAccessError/display_name leak.
Suggestions (take or leave)
tests/test_conflict_detection_extended.py: the ended-membership test exercises only the individual branch (nested filter, L292); the group-registrant branch's filter (L282) never runs in an excluding state. One line intest_group_scope_group_registrant— end a membership, assert absence — closes it.test_group_scope_group_registrantpairs the sharedtarget_type="individual"CR type with a group registrant — a combination the UI blocks (onchange-only guard, socreate()accepts it). Atarget_type="group"/"both"type would make the test a reachable configuration.- The
hasattr()guards (L280/287) are dead code —spp_registryis a hard dep, so the fields always exist — and they read as defensive while the actual failure mode was a hard crash one line inside them. Same pattern inmodels/change_request.py,models/res_partner.py,wizards/create_wizard.py; worth a cleanup sweep. list(set(member_ids))→sorted(set(...))for reproducible domains (cosmetic, pre-existing).- The docstring is an explicit override point but doesn't document the branch asymmetry: an individual expands to group + co-members, a group only to itself + its members (not to members' other groups). One sentence would help overriders.
|
Thanks for the review @gonzalesedwin1123 — the follow-ups and suggestions are now filed against
The stale stored-compute half of follow-up 1 was already filed as #417 (with #421 / #420 for the |
Fixes #476.
Problem
ConflictDetectionMixin._get_group_member_idstraversedspp.group.membershiprecords throughindividual_idandgroup_id, but the model names its many2onesindividualandgroup(spp_registry/models/group_membership.py). Resolving a household's members therefore crashed instead of returning them:KeyError: 'individual_id'unconditionally — even for an empty group, sincemapped()resolves the field name before iterating.AttributeErroron the first non-ended membership — exactly when a group-scope rule has work to do.Any change request type with an active
scope = "group"conflict rule crashed on CR creation for these registrants, making group-scope rules unusable.Why tests didn't catch it
The one existing test called the method directly on an individual with zero memberships — the single shape that happens to work, since the loop body and
mapped()calls never execute.Fix
individual,group) — no behavior change beyond un-breaking the traversal.test_group_scope_same_household_membersnow creates real memberships and asserts both the resolved member set and the end-to-endwarningstatus on the second CR in the household.test_group_scope_group_registrantcovers the group-registrant branch.test_group_scope_ended_membership_excludedpins that ended memberships are excluded from conflict candidates.TDD: the three tests were written first and reproduced both exceptions (
KeyError: 'individual_id',AttributeError: ... no attribute 'group_id') before the fix.Verification
./spp t spp_change_request_v2: 0 failed, 0 error(s) of 415 tests (red run before the fix: the 3 new tests errored, 412 passed).pre-commit clean && pre-commit run --all-files: clean for this module (semgrep crashes locally on Python 3.14 — CI is the authoritative run; README regeneration of unrelated drifted modules was reverted).Found while reviewing #418 (pre-existing on 19.0, out of scope there).