Skip to content

Commit fbb2ca9

Browse files
committed
Merge branch 'feat/876-create-group-cr-redesign' into feat/871-add-member-cr-redesign
2 parents bd2c5b1 + 73d5cf2 commit fbb2ca9

3 files changed

Lines changed: 278 additions & 9 deletions

File tree

spp_change_request_v2/models/change_request.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,13 +1154,74 @@ def _generate_review_comparison_html(self):
11541154

11551155
action = changes.pop("_action", None)
11561156
header = changes.pop("_header", None)
1157+
tables = changes.pop("_tables", None)
1158+
sections = changes.pop("_sections", None)
11571159

11581160
# Determine if this is a field-mapping type (has old/new dicts)
11591161
has_comparison = any(isinstance(v, dict) and "old" in v and "new" in v for v in changes.values())
11601162

11611163
if has_comparison:
1162-
return self._render_comparison_table(changes, header=header)
1163-
return self._render_action_summary(action, changes, header=header)
1164+
html = self._render_comparison_table(changes, header=header)
1165+
else:
1166+
html = self._render_action_summary(action, changes, header=header)
1167+
if tables:
1168+
html += self._render_data_tables(tables)
1169+
if sections:
1170+
html += self._render_data_sections(sections)
1171+
return html
1172+
1173+
def _render_data_tables(self, tables):
1174+
"""Render preview() ``_tables`` entries as separate HTML tables.
1175+
1176+
Each entry is ``{"title", "columns", "rows"}`` where ``rows`` is a list
1177+
of cell-string lists. Used to show one2many data (phones, bank accounts,
1178+
ID documents, ...) on the review page instead of a bare count (OP#876).
1179+
"""
1180+
out = []
1181+
for table in tables:
1182+
columns = table.get("columns") or []
1183+
rows = table.get("rows") or []
1184+
out.append(f'<h6 class="mt-3 mb-1">{html_escape(table.get("title") or "")}</h6>')
1185+
if not rows:
1186+
out.append('<div class="text-muted small mb-0"><i class="fa fa-info-circle me-2"></i>None.</div>')
1187+
continue
1188+
out.append('<table class="table table-sm table-bordered mb-0" style="width:100%">')
1189+
out.append(
1190+
"<thead><tr>"
1191+
+ "".join(f'<th class="bg-light">{html_escape(c)}</th>' for c in columns)
1192+
+ "</tr></thead>"
1193+
)
1194+
out.append("<tbody>")
1195+
for row in rows:
1196+
out.append(
1197+
"<tr>" + "".join(f"<td>{html_escape('' if c is None else str(c))}</td>" for c in row) + "</tr>"
1198+
)
1199+
out.append("</tbody></table>")
1200+
return "".join(out)
1201+
1202+
def _render_data_sections(self, sections):
1203+
"""Render preview() ``_sections`` entries — one labelled detail block per
1204+
entity (e.g. each new group member): its fields as a key/value table plus
1205+
any nested ``tables`` (e.g. that member's phone numbers) (OP#876).
1206+
"""
1207+
out = []
1208+
for section in sections:
1209+
out.append(f'<h6 class="mt-3 mb-1">{html_escape(section.get("title") or "")}</h6>')
1210+
field_rows = section.get("fields") or []
1211+
if field_rows:
1212+
out.append('<table class="table table-sm table-bordered mb-0" style="width:100%">')
1213+
out.append("<tbody>")
1214+
for label, value in field_rows:
1215+
display = html_escape(value) if value else '<span class="text-muted">—</span>'
1216+
out.append(
1217+
f'<tr><td class="bg-light" style="width:30%"><strong>{html_escape(label)}</strong></td>'
1218+
f"<td>{display}</td></tr>"
1219+
)
1220+
out.append("</tbody></table>")
1221+
nested = section.get("tables")
1222+
if nested:
1223+
out.append(self._render_data_tables(nested))
1224+
return "".join(out)
11641225

11651226
def _render_comparison_table(self, changes, header=None):
11661227
"""Render a three-column comparison table for field-mapping CR types."""

spp_change_request_v2/strategies/create_group.py

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,104 @@ def preview(self, change_request):
6666
elif new_heads:
6767
head_label = new_heads[0].full_name
6868

69+
location = None
70+
if detail.latitude or detail.longitude:
71+
location = f"{detail.latitude}, {detail.longitude}"
72+
73+
# One2many lines are shown as separate tables on the review page (OP#876).
74+
# preview() supplies them via the generic "_tables" contract:
75+
# each entry is {title, columns, rows} with rows as lists of cell strings.
76+
tables = []
77+
phone_rows = [
78+
[p.phone_no or "", p.country_id.display_name or "", _("Yes") if p.is_primary else ""]
79+
for p in detail.phone_line_ids
80+
]
81+
if phone_rows:
82+
tables.append(
83+
{"title": _("Phone Numbers"), "columns": [_("Number"), _("Country"), _("Primary")], "rows": phone_rows}
84+
)
85+
bank_rows = [
86+
[b.acc_number or "", b.acc_holder_name or "", b.bank_id.display_name or ""] for b in detail.bank_line_ids
87+
]
88+
if bank_rows:
89+
tables.append(
90+
{
91+
"title": _("Bank Accounts"),
92+
"columns": [_("Account Number"), _("Account Holder"), _("Bank")],
93+
"rows": bank_rows,
94+
}
95+
)
96+
id_doc_rows = [
97+
[d.id_type_id.display_name or "", d.value or "", str(d.expiry_date) if d.expiry_date else ""]
98+
for d in detail.id_doc_line_ids
99+
]
100+
if id_doc_rows:
101+
tables.append(
102+
{
103+
"title": _("ID Documents"),
104+
"columns": [_("Type"), _("Number"), _("Expiry Date")],
105+
"rows": id_doc_rows,
106+
}
107+
)
108+
109+
# Existing members: a simple Name + Role table.
110+
existing_rows = [
111+
[m.individual_id.name or "", m.membership_type_id.display or ""] for m in detail.member_existing_ids
112+
]
113+
if existing_rows:
114+
tables.append({"title": _("Existing Members"), "columns": [_("Name"), _("Role")], "rows": existing_rows})
115+
116+
# New members: one labelled detail block each (full individual record plus
117+
# that member's own phone numbers), via the generic "_sections" contract.
118+
sections = []
119+
for m in detail.member_new_ids:
120+
title = _("New member: %s") % (m.full_name or "")
121+
if m.membership_type_id:
122+
title = f"{title} ({m.membership_type_id.display})"
123+
member_phone_rows = [
124+
[p.phone_no or "", p.country_id.display_name or "", _("Yes") if p.is_primary else ""]
125+
for p in m.phone_line_ids
126+
]
127+
member_tables = []
128+
if member_phone_rows:
129+
member_tables.append(
130+
{
131+
"title": _("Phone Numbers"),
132+
"columns": [_("Number"), _("Country"), _("Primary")],
133+
"rows": member_phone_rows,
134+
}
135+
)
136+
sections.append(
137+
{
138+
"title": title,
139+
"fields": [
140+
[_("Role"), m.membership_type_id.display or ""],
141+
[_("Date of Birth"), str(m.birthdate) if m.birthdate else ""],
142+
[_("Gender"), m.gender_id.display_name or ""],
143+
[_("Civil Status"), m.civil_status_id.display_name or ""],
144+
[_("Occupation"), m.occupation_id.display_name or ""],
145+
[_("Birth Place"), m.birth_place or ""],
146+
[_("Income"), str(m.income) if m.income else ""],
147+
[_("Area"), m.area_id.display_name or ""],
148+
[_("Address"), m.address or ""],
149+
[_("Email"), m.email or ""],
150+
],
151+
"tables": member_tables,
152+
}
153+
)
154+
69155
return {
70156
"_action": "create_group",
71157
"_header": _("The following group is to be added:"),
72158
"group_name": detail.group_name,
73159
"group_type": detail.group_type_id.display if detail.group_type_id else None,
160+
"area": detail.area_id.display_name if detail.area_id else None,
74161
"address": detail.address,
75-
"phone_count": len(detail.phone_line_ids),
76-
"bank_count": len(detail.bank_line_ids),
77-
"id_doc_count": len(detail.id_doc_line_ids),
78-
"existing_member_count": len(detail.member_existing_ids),
79-
"new_member_count": len(detail.member_new_ids),
162+
"email": detail.email,
163+
"location": location,
80164
"head_of_household": head_label,
165+
"_tables": tables,
166+
"_sections": sections,
81167
}
82168

83169
# ──────────────────────────────────────────────────────────────────────

spp_change_request_v2/tests/test_create_group_strategy.py

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,11 +327,133 @@ def test_preview(self):
327327
preview = cr.action_preview_changes()
328328
self.assertEqual(preview["_action"], "create_group")
329329
self.assertEqual(preview["group_name"], "Preview Group")
330-
self.assertEqual(preview["new_member_count"], 1)
331-
self.assertEqual(preview["bank_count"], 1)
330+
# The new member is now a "_sections" detail block, not a count (OP#876).
331+
self.assertNotIn("new_member_count", preview)
332+
self.assertEqual(len(preview["_sections"]), 1)
333+
# The bank line is now surfaced as a "_tables" entry, not a count (OP#876).
334+
self.assertNotIn("bank_count", preview)
335+
bank_tables = [t for t in preview["_tables"] if t["title"] == "Bank Accounts"]
336+
self.assertEqual(len(bank_tables), 1)
337+
self.assertEqual(len(bank_tables[0]["rows"]), 1)
338+
self.assertIn("12-34-56", bank_tables[0]["rows"][0])
332339
if self.head_kind:
333340
self.assertEqual(preview["head_of_household"], "PERSON, Head")
334341

342+
def test_preview_one2many_as_tables_and_scalars(self):
343+
"""OP#876: phones / banks / ID docs are surfaced as `_tables` (actual
344+
data, not counts), and the previously-missing scalar fields are added."""
345+
id_type = self.env["spp.vocabulary.code"].search(
346+
[("vocabulary_id.namespace_uri", "=", "urn:openspp:vocab:id-type")], limit=1
347+
)
348+
cr = self._make_cr(
349+
group_name="Tables Group",
350+
address="12 Rizal St",
351+
email="group@example.com",
352+
phone_line_ids=[
353+
(0, 0, {"phone_no": "+63911111111", "is_primary": True}),
354+
(0, 0, {"phone_no": "+63922222222"}),
355+
],
356+
bank_line_ids=[(0, 0, {"acc_number": "ACC-1", "acc_holder_name": "Jane"})],
357+
id_doc_line_ids=([(0, 0, {"id_type_id": id_type.id, "value": "ID-9"})] if id_type else []),
358+
)
359+
preview = cr.action_preview_changes()
360+
361+
# Previously-missing scalar fields are now surfaced.
362+
self.assertEqual(preview["email"], "group@example.com")
363+
self.assertEqual(preview["address"], "12 Rizal St")
364+
self.assertIn("area", preview)
365+
366+
# Counts are replaced by the generic `_tables` contract.
367+
for removed in ("phone_count", "bank_count", "id_doc_count"):
368+
self.assertNotIn(removed, preview)
369+
370+
titles = [t["title"] for t in preview["_tables"]]
371+
self.assertIn("Phone Numbers", titles)
372+
self.assertIn("Bank Accounts", titles)
373+
phones = next(t for t in preview["_tables"] if t["title"] == "Phone Numbers")
374+
self.assertEqual(phones["columns"], ["Number", "Country", "Primary"])
375+
self.assertEqual(len(phones["rows"]), 2)
376+
self.assertIn("+63911111111", phones["rows"][0])
377+
if id_type:
378+
self.assertIn("ID Documents", titles)
379+
380+
def test_preview_members_table_and_sections(self):
381+
"""OP#876: existing members render as a Name/Role table; new members
382+
render as per-member detail sections (with their own phones)."""
383+
existing = self.partner_model.create({"name": "Existing Member A", "is_registrant": True, "is_group": False})
384+
cr = self._make_cr(
385+
group_name="Members Group",
386+
member_existing_ids=[
387+
(
388+
0,
389+
0,
390+
{
391+
"individual_id": existing.id,
392+
"membership_type_id": self.member_kind.id if self.member_kind else False,
393+
},
394+
)
395+
],
396+
member_new_ids=[
397+
(
398+
0,
399+
0,
400+
{
401+
"given_name": "Nina",
402+
"family_name": "Cruz",
403+
"birthdate": "2000-05-05",
404+
"membership_type_id": self.head_kind.id if self.head_kind else False,
405+
"phone_line_ids": [(0, 0, {"phone_no": "+63999999999", "is_primary": True})],
406+
},
407+
)
408+
],
409+
)
410+
preview = cr.action_preview_changes()
411+
412+
# Counts are gone.
413+
self.assertNotIn("existing_member_count", preview)
414+
self.assertNotIn("new_member_count", preview)
415+
416+
# Existing members -> a Name/Role table.
417+
existing_tbl = [t for t in preview["_tables"] if t["title"] == "Existing Members"]
418+
self.assertEqual(len(existing_tbl), 1)
419+
self.assertIn("Existing Member A", existing_tbl[0]["rows"][0])
420+
421+
# New members -> a per-member detail section with fields + own phones.
422+
self.assertEqual(len(preview["_sections"]), 1)
423+
sec = preview["_sections"][0]
424+
self.assertIn("Nina", sec["title"])
425+
labels = [f[0] for f in sec["fields"]]
426+
self.assertIn("Date of Birth", labels)
427+
self.assertIn("Gender", labels)
428+
self.assertTrue(any(t["title"] == "Phone Numbers" for t in sec["tables"]))
429+
self.assertIn("+63999999999", sec["tables"][0]["rows"][0])
430+
431+
# The rendered review HTML surfaces the member data.
432+
html = cr._generate_review_comparison_html()
433+
self.assertIn("Existing Members", html)
434+
self.assertIn("Existing Member A", html)
435+
self.assertIn("Nina", html)
436+
self.assertIn("+63999999999", html)
437+
438+
def test_review_comparison_html_renders_tables(self):
439+
"""The review page HTML shows the actual phone / bank rows as tables,
440+
not a bare count (OP#876)."""
441+
cr = self._make_cr(
442+
group_name="HTML Group",
443+
email="g@example.com",
444+
phone_line_ids=[(0, 0, {"phone_no": "+63900000000", "is_primary": True})],
445+
bank_line_ids=[(0, 0, {"acc_number": "BANK-777"})],
446+
)
447+
html = cr._generate_review_comparison_html()
448+
self.assertIn("Phone Numbers", html)
449+
self.assertIn("+63900000000", html)
450+
self.assertIn("Bank Accounts", html)
451+
self.assertIn("BANK-777", html)
452+
self.assertIn("g@example.com", html)
453+
# Raw count keys must not leak into the review output.
454+
self.assertNotIn("phone_count", html)
455+
self.assertNotIn("bank_count", html)
456+
335457
# ──────────────────────────────────────────────────────────────────
336458
# Wizard flow (OP#876 round 2): Add Member wizard, both modes
337459
# ──────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)