Skip to content

Commit d850155

Browse files
authored
Merge pull request #242 from OpenSPP/feat/876-create-group-cr-redesign
feat(change_request): redesign CR flows — Create Group, Add/Remove Member, Change HoH, Split Household (#876 #871 #872 #873 #877)
2 parents 84fe095 + 67e72f7 commit d850155

57 files changed

Lines changed: 4075 additions & 3479 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ jobs:
123123
needs: [detect-changes, build]
124124
if: needs.detect-changes.outputs.has_modules == 'true'
125125
runs-on: ubuntu-latest
126-
timeout-minutes: 30
126+
timeout-minutes: 45
127127
strategy:
128128
fail-fast: false
129129
matrix:

spp_change_request_v2/README.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -853,6 +853,27 @@ Before declaring a new CR type complete:
853853
Changelog
854854
=========
855855

856+
19.0.3.0.0
857+
~~~~~~~~~~
858+
859+
- feat(change_request): redesign the group/membership CR flows (#242) —
860+
Create Group (#876), Add Member now searches an existing member
861+
(#871), Remove Member first-page/review cleanup (#872), Change Head of
862+
Household via a per-member role table (#873), and Split Household as a
863+
relational member move with single-head validation (#877). Review
864+
pages render the real data as tables / detail sections.
865+
- fix(change_request): Split Household "New Household Information" shows
866+
a fillable Address field, and the new-household **Latitude/Longitude**
867+
are surfaced with an interactive **map** (``spp_gis``, OSM basemap) to
868+
pick/preview the location — the two stay in sync and out-of-range
869+
coordinates are rejected. The review page surfaces the coordinates
870+
plus the new household's Phone / Bank / ID lines as tables (#877)
871+
- **Breaking:** the Add Member detail no longer exposes the
872+
create-a-new-individual fields (``created_individual_id``,
873+
``given_name``, ``family_name``, ``birthdate``, ``gender_id``,
874+
``relationship_id``); downstream modules that extended the old flow
875+
must adapt (see #1133).
876+
856877
19.0.2.0.8
857878
~~~~~~~~~~
858879

spp_change_request_v2/__manifest__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "OpenSPP Change Request V2",
3-
"version": "19.0.2.0.8",
3+
"version": "19.0.3.0.0",
44
"sequence": 50,
55
"category": "OpenSPP",
66
"summary": "Configuration-driven change request system with UX improvements, conflict detection and duplicate prevention",
@@ -14,6 +14,7 @@
1414
"spp_base_common",
1515
"spp_registry",
1616
"spp_area",
17+
"spp_gis",
1718
"spp_security",
1819
"spp_approval",
1920
"spp_event_data",
@@ -43,7 +44,9 @@
4344
"views/detail_transfer_member_views.xml",
4445
"views/detail_update_id_views.xml",
4546
"views/detail_create_group_views.xml",
47+
"views/create_group_member_wizard_views.xml",
4648
"views/detail_merge_registrants_views.xml",
49+
"views/detail_split_household_gis_views.xml",
4750
"views/detail_split_household_views.xml",
4851
"views/preview_changes_wizard_views.xml",
4952
"views/document_upload_wizard_views.xml",
Lines changed: 148 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
from odoo import api, fields, models
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
"""Detail model for the Add Member CR (OP#871).
3+
4+
The first page searches for an existing individual registrant and adds them to
5+
the group with a role. (The earlier create-a-new-individual flow was replaced
6+
per the updated #871 spec — Add Member now selects an existing member.)
7+
"""
8+
9+
from odoo import _, api, fields, models
10+
from odoo.exceptions import ValidationError
211

312

413
class SPPCRDetailAddMember(models.Model):
@@ -8,57 +17,151 @@ class SPPCRDetailAddMember(models.Model):
817
_description = "CR Detail: Add Group Member"
918
_inherit = ["spp.cr.detail.base", "mail.thread"]
1019

11-
# ══════════════════════════════════════════════════════════════════════════
12-
# MEMBER INFORMATION - Real Odoo fields with full features
13-
# ══════════════════════════════════════════════════════════════════════════
20+
# ──────────────────────────────────────────────────────────────────────
21+
# Target group identity (helps distinguish same-named groups — OP#871)
22+
# ──────────────────────────────────────────────────────────────────────
23+
registrant_registration_date = fields.Date(
24+
related="change_request_id.registrant_id.registration_date",
25+
string="Group Registration Date",
26+
readonly=True,
27+
)
28+
registrant_id_display = fields.Char(
29+
string="Group ID(s)",
30+
compute="_compute_registrant_id_display",
31+
help="The target group's registry ID type(s) and value(s).",
32+
)
1433

15-
member_name = fields.Char(
16-
string="Full Name",
34+
# ──────────────────────────────────────────────────────────────────────
35+
# Member to add (existing individual)
36+
# ──────────────────────────────────────────────────────────────────────
37+
individual_id = fields.Many2one(
38+
"res.partner",
39+
string="Member",
1740
tracking=True,
18-
help="Required before apply. Auto-computed from given/family names.",
41+
help="Search for an existing individual registrant to add to the group.",
1942
)
20-
given_name = fields.Char(string="Given Name", tracking=True)
21-
family_name = fields.Char(string="Family Name", tracking=True)
22-
birthdate = fields.Date(string="Date of Birth", tracking=True)
23-
gender_id = fields.Many2one(
43+
# Domain string (computed) restricting the picker to existing individual
44+
# registrants who are not already active members of the group. A computed
45+
# domain is used instead of a materialised Many2many so the picker scales
46+
# with a large registry (mirrors spp.change.request.registrant_domain).
47+
individual_domain = fields.Char(compute="_compute_individual_domain")
48+
49+
# ──────────────────────────────────────────────────────────────────────
50+
# Role (per-member, single row)
51+
# ──────────────────────────────────────────────────────────────────────
52+
membership_type_id = fields.Many2one(
2453
"spp.vocabulary.code",
25-
string="Gender",
26-
domain="[('namespace_uri', '=', 'urn:iso:std:iso:5218')]",
54+
string="Role",
55+
domain="[('vocabulary_id.namespace_uri', '=', 'urn:openspp:vocab:group-membership-type')]",
2756
tracking=True,
57+
help="Role of the new member in the group. Optionality controlled by the CR type.",
2858
)
29-
relationship_id = fields.Many2one(
30-
"spp.vocabulary.code",
31-
string="Relationship to Head",
32-
domain="[('vocabulary_id.namespace_uri', '=', 'urn:openspp:vocab:group-membership-type'),"
33-
" ('code', '!=', 'head')]",
34-
tracking=True,
59+
60+
# ──────────────────────────────────────────────────────────────────────
61+
# Type-config mirrors (for view conditionals)
62+
# ──────────────────────────────────────────────────────────────────────
63+
type_requires_head = fields.Boolean(
64+
related="change_request_id.request_type_id.requires_head",
65+
string="Requires Head",
66+
)
67+
roles_available = fields.Boolean(
68+
compute="_compute_roles_available",
69+
string="Has Membership Roles",
70+
help="True when the urn:openspp:vocab:group-membership-type vocabulary has any active code.",
3571
)
36-
id_number = fields.Char(string="ID Number", tracking=True)
37-
phone = fields.Char(string="Phone Number", tracking=True)
3872

39-
# Reference to created individual (set after apply)
40-
created_individual_id = fields.Many2one(
41-
"res.partner",
42-
string="Created Individual",
43-
readonly=True,
73+
# ──────────────────────────────────────────────────────────────────────
74+
# Read-only context: existing members of the group
75+
# ──────────────────────────────────────────────────────────────────────
76+
existing_membership_ids = fields.Many2many(
77+
"spp.group.membership",
78+
string="Existing Members",
79+
compute="_compute_existing_memberships",
4480
)
4581

46-
# ══════════════════════════════════════════════════════════════════════════
47-
# ONCHANGE - Full Odoo functionality
48-
# ══════════════════════════════════════════════════════════════════════════
49-
50-
@api.onchange("given_name", "family_name")
51-
def _onchange_names(self):
52-
"""Auto-compute full name from given + family."""
53-
if self.given_name or self.family_name:
54-
name_vals = [
55-
f"{self.family_name},"
56-
if self.family_name and self.given_name
57-
else f"{self.family_name}"
58-
if self.family_name
59-
else "",
60-
self.given_name,
61-
]
62-
63-
name = " ".join(filter(None, name_vals))
64-
self.member_name = name.upper()
82+
# ──────────────────────────────────────────────────────────────────────
83+
# Computes
84+
# ──────────────────────────────────────────────────────────────────────
85+
def _active_member_individual_ids(self, group):
86+
"""ids of individuals who are active members of the group."""
87+
if not group or not group.is_group:
88+
return []
89+
memberships = self.env["spp.group.membership"].search([("group", "=", group.id), ("status", "=", "active")])
90+
return memberships.mapped("individual").ids
91+
92+
@api.depends("change_request_id", "change_request_id.registrant_id")
93+
def _compute_individual_domain(self):
94+
for rec in self:
95+
member_ids = rec._active_member_individual_ids(rec.change_request_id.registrant_id)
96+
rec.individual_domain = str(
97+
[
98+
("is_registrant", "=", True),
99+
("is_group", "=", False),
100+
("id", "not in", member_ids),
101+
]
102+
)
103+
104+
@api.depends_context("uid")
105+
def _compute_roles_available(self):
106+
has_any = bool(
107+
self.env["spp.vocabulary.code"].search_count(
108+
[("vocabulary_id.namespace_uri", "=", "urn:openspp:vocab:group-membership-type")]
109+
)
110+
)
111+
for rec in self:
112+
rec.roles_available = has_any
113+
114+
@api.depends("change_request_id", "change_request_id.registrant_id")
115+
def _compute_existing_memberships(self):
116+
Membership = self.env["spp.group.membership"]
117+
for rec in self:
118+
group = rec.change_request_id.registrant_id
119+
if group and group.is_group:
120+
rec.existing_membership_ids = Membership.search([("group", "=", group.id), ("status", "=", "active")])
121+
else:
122+
rec.existing_membership_ids = Membership.browse([])
123+
124+
@api.depends("change_request_id.registrant_id.reg_ids")
125+
def _compute_registrant_id_display(self):
126+
"""Render the target group's registry IDs as 'Type: value; ...' so a
127+
group can be told apart from others with the same name (OP#871)."""
128+
for rec in self:
129+
group = rec.change_request_id.registrant_id
130+
ids = group.reg_ids if group and "reg_ids" in group._fields else False
131+
rec.registrant_id_display = (
132+
"; ".join(f"{i.id_type_as_str or ''}: {i.value or ''}".strip(": ") for i in ids) if ids else False
133+
)
134+
135+
@api.constrains("membership_type_id")
136+
def _check_single_head(self):
137+
"""A group can have at most one Head of Household. Reject choosing the
138+
Head role when the target group already has an active head — at save /
139+
submit time, not just on apply (OP#871, uniform with the other CRs)."""
140+
for rec in self:
141+
role = rec.membership_type_id
142+
group = rec.change_request_id.registrant_id
143+
if not role or role.code != "head" or not group or not group.is_group:
144+
continue
145+
group_has_head = self.env["spp.group.membership"].search_count(
146+
[
147+
("group", "=", group.id),
148+
("status", "=", "active"),
149+
("membership_type_ids.code", "=", "head"),
150+
]
151+
)
152+
if group_has_head:
153+
raise ValidationError(_("This group already has a Head of Household. Only one member can be Head."))
154+
155+
156+
class SPPGroupMembership(models.Model):
157+
"""Expose the member's registration date for the Add Member CR's
158+
Current Members table (OP#871) — membership start_date is the join date,
159+
not the individual's registration date, so they are shown separately."""
160+
161+
_inherit = "spp.group.membership"
162+
163+
individual_registration_date = fields.Date(
164+
related="individual.registration_date",
165+
string="Registration Date",
166+
readonly=True,
167+
)

0 commit comments

Comments
 (0)