1- from odoo import api , fields , models
1+ from odoo import _ , api , fields , models
2+ from odoo .exceptions import ValidationError
3+
4+ ROLE_NAMESPACE = "urn:openspp:vocab:group-membership-type"
5+ HEAD_ROLE_CODE = "head"
26
37
48class SPPCRDetailChangeHOH (models .Model ):
5- """Detail model for Change Head of Household CR type."""
9+ """Detail model for Change Head of Household CR type (OP#873) ."""
610
711 _name = "spp.cr.detail.change_hoh"
812 _description = "CR Detail: Change Head of Household"
@@ -19,51 +23,26 @@ class SPPCRDetailChangeHOH(models.Model):
1923 store = True ,
2024 readonly = True ,
2125 )
22- available_individual_ids = fields .Many2many (
23- "res.partner" ,
24- string = "Available Individuals" ,
25- compute = "_compute_available_individuals" ,
26- help = "Active members excluding current head of household" ,
27- )
28- new_head_id = fields .Many2one (
29- "res.partner" ,
30- string = "New Head of Household" ,
31- tracking = True ,
32- domain = "[('is_group', '=', False), ('is_registrant', '=', True)]" ,
33- help = "Select the individual who will become the new head of household" ,
34- )
35- new_head_membership_id = fields .Many2one (
36- "spp.group.membership" ,
37- string = "New Head Membership" ,
38- readonly = True ,
39- help = "Membership record for the new head (automatically set)" ,
40- )
41- previous_head_new_role_id = fields .Many2one (
42- "spp.vocabulary.code" ,
43- string = "Previous Head's New Role" ,
44- domain = "[('vocabulary_id.namespace_uri', '=', 'urn:openspp:vocab:group-membership-type'),"
45- " ('code', '!=', 'head')]" ,
46- tracking = True ,
47- help = "The new role for the previous head (e.g., Spouse, Other Adult)" ,
26+ # One editable role line per active group member. The new head is whichever
27+ # member is assigned the Head role (OP#873 — replaces the single new-head
28+ # dropdown with a members-with-roles table).
29+ member_line_ids = fields .One2many (
30+ "spp.cr.detail.change_hoh.member" ,
31+ "detail_id" ,
32+ string = "Members" ,
4833 )
4934 reason = fields .Selection (
5035 [
5136 ("deceased" , "Head Deceased" ),
5237 ("incapacitated" , "Head Incapacitated" ),
5338 ("left_household" , "Head Left Household" ),
5439 ("age_change" , "Age-based Change" ),
55- ("voluntary" , "Voluntary Transfer" ),
5640 ("correction" , "Data Correction" ),
5741 ("other" , "Other" ),
5842 ],
5943 string = "Reason for Change" ,
6044 tracking = True ,
6145 )
62- effective_date = fields .Date (
63- string = "Effective Date" ,
64- default = fields .Date .today ,
65- tracking = True ,
66- )
6746 remarks = fields .Text (string = "Remarks" , tracking = True )
6847
6948 # ══════════════════════════════════════════════════════════════════════════
@@ -73,58 +52,80 @@ class SPPCRDetailChangeHOH(models.Model):
7352 @api .depends ("change_request_id.registrant_id" )
7453 def _compute_current_head (self ):
7554 """Find the current head of household."""
76- head_kind = self .env ["spp.vocabulary.code" ].get_code ("urn:openspp:vocab:group-membership-type" , "head" )
55+ head_kind = self .env ["spp.vocabulary.code" ].get_code (ROLE_NAMESPACE , HEAD_ROLE_CODE )
7756 for rec in self :
7857 current_head = False
7958 if rec .change_request_id .registrant_id and head_kind :
80- memberships = self .env ["spp.group.membership" ].search (
59+ membership = self .env ["spp.group.membership" ].search (
8160 [
8261 ("group" , "=" , rec .change_request_id .registrant_id .id ),
8362 ("membership_type_ids" , "in" , [head_kind .id ]),
8463 ("status" , "=" , "active" ),
8564 ],
8665 limit = 1 ,
8766 )
88- if memberships :
89- current_head = memberships .individual
67+ if membership :
68+ current_head = membership .individual
9069 rec .current_head_id = current_head
9170
92- @api .depends ("change_request_id.registrant_id" , "current_head_id" )
93- def _compute_available_individuals (self ):
94- """Compute available individuals excluding current head of household."""
95- head_kind = self .env ["spp.vocabulary.code" ].get_code ("urn:openspp:vocab:group-membership-type" , "head" )
96- for rec in self :
97- available_individuals = self .env ["res.partner" ]
98- if rec .change_request_id .registrant_id :
99- # Get all active memberships for this group
100- all_memberships = self .env ["spp.group.membership" ].search (
101- [
102- ("group" , "=" , rec .change_request_id .registrant_id .id ),
103- ("status" , "=" , "active" ),
104- ]
105- )
106- # Exclude current head
107- if head_kind :
108- available_memberships = all_memberships .filtered (lambda m : head_kind not in m .membership_type_ids )
109- else :
110- available_memberships = all_memberships
111- # Get the individuals from these memberships
112- available_individuals = available_memberships .mapped ("individual" )
113- rec .available_individual_ids = available_individuals
71+ @api .model_create_multi
72+ def create (self , vals_list ):
73+ details = super ().create (vals_list )
74+ for detail in details :
75+ if not detail .member_line_ids :
76+ detail ._seed_member_lines ()
77+ return details
11478
115- @api .onchange ("new_head_id" )
116- def _onchange_new_head_id (self ):
117- """Set the membership_id when individual is selected."""
118- self .new_head_membership_id = False
119- if self .new_head_id and self .change_request_id .registrant_id :
120- # Find the membership for this individual in this group
121- membership = self .env ["spp.group.membership" ].search (
122- [
123- ("group" , "=" , self .change_request_id .registrant_id .id ),
124- ("individual" , "=" , self .new_head_id .id ),
125- ("status" , "=" , "active" ),
126- ],
127- limit = 1 ,
79+ def _seed_member_lines (self ):
80+ """Populate one editable role line per active group member, defaulting
81+ each member's new role to their current role."""
82+ self .ensure_one ()
83+ group = self .change_request_id .registrant_id
84+ if not group or not group .is_group :
85+ return
86+ memberships = self .env ["spp.group.membership" ].search ([("group" , "=" , group .id ), ("status" , "=" , "active" )])
87+ lines = []
88+ for membership in memberships :
89+ current_roles = membership .membership_type_ids
90+ lines .append (
91+ (
92+ 0 ,
93+ 0 ,
94+ {
95+ "individual_id" : membership .individual .id ,
96+ "membership_id" : membership .id ,
97+ "old_role_display" : ", " .join (current_roles .mapped ("display" )),
98+ "new_role_id" : current_roles [:1 ].id if current_roles else False ,
99+ },
100+ )
128101 )
129- if membership :
130- self .new_head_membership_id = membership
102+ self .member_line_ids = lines
103+
104+
105+ class SPPCRDetailChangeHOHMember (models .Model ):
106+ """One editable role line per current group member (Change HoH, OP#873)."""
107+
108+ _name = "spp.cr.detail.change_hoh.member"
109+ _description = "CR Detail: Change HoH - Member Role"
110+
111+ detail_id = fields .Many2one (
112+ "spp.cr.detail.change_hoh" ,
113+ required = True ,
114+ ondelete = "cascade" ,
115+ )
116+ individual_id = fields .Many2one ("res.partner" , string = "Member" , readonly = True )
117+ membership_id = fields .Many2one ("spp.group.membership" , readonly = True )
118+ old_role_display = fields .Char (string = "Current Role" , readonly = True )
119+ new_role_id = fields .Many2one (
120+ "spp.vocabulary.code" ,
121+ string = "New Role" ,
122+ domain = "[('vocabulary_id.namespace_uri', '=', 'urn:openspp:vocab:group-membership-type')]" ,
123+ )
124+
125+ @api .constrains ("new_role_id" )
126+ def _check_single_head (self ):
127+ """A group can have at most one Head of Household."""
128+ head = self .env ["spp.vocabulary.code" ].get_code (ROLE_NAMESPACE , HEAD_ROLE_CODE )
129+ for detail in self .mapped ("detail_id" ):
130+ if head and len (detail .member_line_ids .filtered (lambda r : r .new_role_id == head )) > 1 :
131+ raise ValidationError (_ ("A group can have at most one Head of Household." ))
0 commit comments