@@ -55,7 +55,55 @@ def create_field_element(self, div_element, model_field_id, is_ind=False):
5555 modifiers = {"readonly" : True }
5656 new_field .set ("modifiers" , json .dumps (modifiers ))
5757
58- def _get_view (self , view_id = None , view_type = "form" , ** options ):
58+ def _group_fields_by_group (self , fields_list ):
59+ """
60+ Group fields by their field_group_id and sort by sequence.
61+ Returns a list of tuples (group_record, fields_in_group).
62+ Fields without a group are returned with group_record=None.
63+ """
64+ from collections import defaultdict
65+
66+ grouped = defaultdict (list )
67+
68+ # Check if field_group_id exists on the model
69+ has_group_field = hasattr (fields_list [0 ], "field_group_id" ) if fields_list else False
70+ has_sequence_field = hasattr (fields_list [0 ], "sequence" ) if fields_list else False
71+
72+ for field in fields_list :
73+ group_id = None
74+ if has_group_field and field .field_group_id :
75+ group_id = field .field_group_id .id
76+ grouped [group_id ].append (field )
77+
78+ # Sort fields within each group by sequence
79+ for group_id in grouped :
80+ if has_sequence_field :
81+ grouped [group_id ] = sorted (grouped [group_id ], key = lambda f : (f .sequence , f .field_description ))
82+ else :
83+ grouped [group_id ] = sorted (grouped [group_id ], key = lambda f : f .field_description )
84+
85+ # Get group records and sort groups by sequence
86+ result = []
87+ group_ids = [gid for gid in grouped .keys () if gid is not None ]
88+ if group_ids and "spp.custom.field.group" in self .env :
89+ try :
90+ group_records = self .env ["spp.custom.field.group" ].browse (group_ids )
91+ group_records = group_records .sorted (key = lambda g : g .sequence )
92+ for group in group_records :
93+ result .append ((group , grouped [group .id ]))
94+ except KeyError :
95+ # Model doesn't exist, treat all fields as ungrouped
96+ _logger .warning ("spp.custom.field.group model not found, ignoring field groups" )
97+ if None in grouped :
98+ result .append ((None , grouped [None ]))
99+
100+ # Add fields without a group at the end
101+ if None in grouped :
102+ result .append ((None , grouped [None ]))
103+
104+ return result
105+
106+ def _get_view (self , view_id = None , view_type = "form" , ** options ): # noqa: C901
59107 arch , view = super ()._get_view (view_id , view_type , ** options )
60108
61109 if view_type == "form" :
@@ -69,7 +117,7 @@ def _get_view(self, view_id=None, view_type="form", **options):
69117
70118 model_fields_id = self .env ["ir.model.fields" ].search (
71119 [("model_id" , "=" , "res.partner" )],
72- order = "ttype, field_description" ,
120+ order = "sequence, ttype, field_description" ,
73121 )
74122 if basic_info_page :
75123 if action_id .context :
@@ -79,22 +127,109 @@ def _get_view(self, view_id=None, view_type="form", **options):
79127 custom_page = etree .Element ("page" , {"string" : "Additional Details" , "name" : "additional_details" })
80128 indicators_page = etree .Element ("page" , {"string" : "Indicators" , "name" : "indicators" })
81129
82- custom_div = etree .SubElement (custom_page , "div" , {"class" : "row mt16 o_settings_container" })
83- indicators_div = etree .SubElement (indicators_page , "div" , {"class" : "row mt16 o_settings_container" })
130+ # Separate custom and indicator fields
131+ custom_fields = []
132+ indicator_fields = []
133+
84134 for rec in model_fields_id :
85135 els = rec .name .split ("_" )
86136 if len (els ) >= 3 and (els [2 ] == "grp" and not is_group or els [2 ] == "indv" and is_group ):
87137 continue
88138
89139 if len (els ) >= 2 and els [1 ] == "cst" :
90- self .create_field_element (custom_div , rec )
91-
140+ custom_fields .append (rec )
92141 elif len (els ) >= 2 and els [1 ] == "ind" :
93- self .create_field_element (indicators_div , rec , is_ind = True )
94-
95- if custom_div .getchildren ():
142+ indicator_fields .append (rec )
143+
144+ # Process custom fields with grouping
145+ if custom_fields :
146+ grouped_custom_fields = self ._group_fields_by_group (custom_fields )
147+
148+ # Create main container row for side-by-side layout
149+ main_row = etree .SubElement (custom_page , "div" , {"class" : "row" })
150+
151+ for group_record , fields_in_group in grouped_custom_fields :
152+ if group_record :
153+ # Create a half-width column for each group
154+ group_col = etree .SubElement (
155+ main_row ,
156+ "div" ,
157+ {"class" : "col-12 col-lg-6" },
158+ )
159+ # Add group label/title
160+ group_label_div = etree .SubElement (
161+ group_col ,
162+ "div" ,
163+ {"class" : "o_horizontal_separator mt-2 mb-3" },
164+ )
165+ group_label = etree .SubElement (
166+ group_label_div ,
167+ "strong" ,
168+ )
169+ group_label .text = group_record .name
170+ # Create fields container
171+ group_div = etree .SubElement (group_col , "div" , {"class" : "row mt16 o_settings_container" })
172+ for field in fields_in_group :
173+ self .create_field_element (group_div , field )
174+ else :
175+ # Fields without a group go in full width at the bottom
176+ if not custom_page .xpath (".//div[@class='row mt16 o_settings_container o_no_group']" ):
177+ custom_div = etree .SubElement (
178+ custom_page , "div" , {"class" : "row mt16 o_settings_container o_no_group" }
179+ )
180+ else :
181+ custom_div = custom_page .xpath (
182+ ".//div[@class='row mt16 o_settings_container o_no_group']"
183+ )[0 ]
184+ for field in fields_in_group :
185+ self .create_field_element (custom_div , field )
186+
187+ # Process indicator fields with grouping
188+ if indicator_fields :
189+ grouped_indicator_fields = self ._group_fields_by_group (indicator_fields )
190+
191+ # Create main container row for side-by-side layout
192+ main_row = etree .SubElement (indicators_page , "div" , {"class" : "row" })
193+
194+ for group_record , fields_in_group in grouped_indicator_fields :
195+ if group_record :
196+ # Create a half-width column for each group
197+ group_col = etree .SubElement (
198+ main_row ,
199+ "div" ,
200+ {"class" : "col-12 col-lg-6" },
201+ )
202+ # Add group label/title
203+ group_label_div = etree .SubElement (
204+ group_col ,
205+ "div" ,
206+ {"class" : "o_horizontal_separator mt-2 mb-3" },
207+ )
208+ group_label = etree .SubElement (
209+ group_label_div ,
210+ "strong" ,
211+ )
212+ group_label .text = group_record .name
213+ # Create fields container
214+ group_div = etree .SubElement (group_col , "div" , {"class" : "row mt16 o_settings_container" })
215+ for field in fields_in_group :
216+ self .create_field_element (group_div , field , is_ind = True )
217+ else :
218+ # Fields without a group go in full width at the bottom
219+ if not indicators_page .xpath (".//div[@class='row mt16 o_settings_container o_no_group']" ):
220+ indicators_div = etree .SubElement (
221+ indicators_page , "div" , {"class" : "row mt16 o_settings_container o_no_group" }
222+ )
223+ else :
224+ indicators_div = indicators_page .xpath (
225+ ".//div[@class='row mt16 o_settings_container o_no_group']"
226+ )[0 ]
227+ for field in fields_in_group :
228+ self .create_field_element (indicators_div , field , is_ind = True )
229+
230+ if custom_page .getchildren ():
96231 basic_info_page [0 ].addnext (custom_page )
97- if indicators_div .getchildren ():
232+ if indicators_page .getchildren ():
98233 basic_info_page [0 ].addnext (indicators_page )
99234
100235 arch = doc
0 commit comments