66
77_logger = logging .getLogger (__name__ )
88
9+ PRIORITY_SEQUENCE = {
10+ "low" : 1 ,
11+ "medium" : 2 ,
12+ "high" : 3 ,
13+ "critical" : 4 ,
14+ }
15+
916
1017class Alert (models .Model ):
1118 """Generic alert model for monitoring thresholds, expiries, and deadlines.
@@ -25,8 +32,9 @@ class Alert(models.Model):
2532 _name = "spp.alert"
2633 _description = "Alert"
2734 _inherit = ["mail.thread" ]
28- _order = "priority desc, create_date desc"
35+ _order = "priority_sequence desc, create_date desc"
2936 _rec_name = "reference"
37+ _check_company_auto = True
3038
3139 reference = fields .Char (
3240 string = "Reference" ,
@@ -47,6 +55,8 @@ class Alert(models.Model):
4755 help = "Type of alert from alert types vocabulary" ,
4856 )
4957
58+ # Named `alert_type` (not `alert_type_code`) for concise domain filtering.
59+ # Use `alert_type_id` for the vocabulary record, `alert_type` for the code string.
5060 alert_type = fields .Char (
5161 related = "alert_type_id.code" ,
5262 store = True ,
@@ -69,6 +79,16 @@ class Alert(models.Model):
6979 help = "Priority level of the alert" ,
7080 )
7181
82+ priority_sequence = fields .Integer (
83+ string = "Priority Sequence" ,
84+ compute = "_compute_priority_sequence" ,
85+ store = True ,
86+ help = "Numeric ordering of priority for consistent sorting (low=1, medium=2, high=3, critical=4)" ,
87+ )
88+
89+ # Alert states differ from the standard approval workflow states (draft/pending/approved)
90+ # because the alert lifecycle is fundamentally different: alerts are raised, acknowledged,
91+ # and resolved — there is no approval decision involved.
7292 state = fields .Selection (
7393 [
7494 ("active" , "Active" ),
@@ -102,6 +122,7 @@ class Alert(models.Model):
102122 index = True ,
103123 readonly = True ,
104124 ondelete = "set null" ,
125+ check_company = True ,
105126 help = "Alert rule that generated this alert (empty for manually created alerts)" ,
106127 )
107128
@@ -120,6 +141,12 @@ class Alert(models.Model):
120141 help = "Record that triggered this alert" ,
121142 )
122143
144+ res_name = fields .Char (
145+ string = "Source Record Name" ,
146+ compute = "_compute_res_name" ,
147+ help = "Display name of the record that triggered this alert" ,
148+ )
149+
123150 # Metrics for threshold and deadline tracking
124151 current_value = fields .Float (
125152 string = "Current Value" ,
@@ -164,6 +191,28 @@ class Alert(models.Model):
164191 help = "Company this alert belongs to" ,
165192 )
166193
194+ @api .depends ("priority" )
195+ def _compute_priority_sequence (self ):
196+ """Compute numeric priority sequence for consistent ordering."""
197+ for record in self :
198+ record .priority_sequence = PRIORITY_SEQUENCE .get (record .priority , 0 )
199+
200+ def _compute_res_name (self ):
201+ """Compute the display name of the source record.
202+
203+ Handles missing models or deleted records gracefully by returning an
204+ empty string rather than raising an error.
205+ """
206+ for record in self :
207+ if not record .res_model or not record .res_id :
208+ record .res_name = ""
209+ continue
210+ try :
211+ source = self .env [record .res_model ].browse (record .res_id )
212+ record .res_name = source .display_name if source .exists () else ""
213+ except (KeyError , AttributeError ):
214+ record .res_name = ""
215+
167216 @api .model_create_multi
168217 def create (self , vals_list ):
169218 """Override create to auto-generate reference from sequence."""
@@ -180,7 +229,13 @@ def action_acknowledge(self):
180229
181230 This is typically the first step in addressing an alert - acknowledging
182231 that it has been seen and is being investigated.
232+
233+ Raises:
234+ UserError: If the alert is not in the active state.
183235 """
236+ for record in self :
237+ if record .state != "active" :
238+ raise UserError (_ ("Only active alerts can be acknowledged." ))
184239 self .write ({"state" : "acknowledged" })
185240 return True
186241
@@ -194,9 +249,13 @@ def action_resolve(self, notes=None):
194249 bool: True on success
195250
196251 Raises:
197- UserError: If resolution notes are not provided .
252+ UserError: If the alert is already resolved or resolution notes are missing .
198253 """
254+ # Fail-fast: if any record in the batch lacks resolution notes, none are resolved.
255+ # This prevents partial resolution of related alert batches.
199256 for record in self :
257+ if record .state == "resolved" :
258+ raise UserError (_ ("Alert '%s' is already resolved." , record .reference ))
200259 resolution = notes or record .resolution_notes
201260 if not resolution :
202261 raise UserError (_ ("Please provide resolution notes before resolving the alert." ))
@@ -210,3 +269,38 @@ def action_resolve(self, notes=None):
210269 vals ["resolution_notes" ] = notes
211270 self .write (vals )
212271 return True
272+
273+ def action_view_related_alerts (self ):
274+ """Return an action to view other alerts from the same rule.
275+
276+ Returns:
277+ dict: An ir.actions.act_window action dict, or False if no rule is set.
278+ """
279+ self .ensure_one ()
280+ if not self .rule_id :
281+ return False
282+ return {
283+ "type" : "ir.actions.act_window" ,
284+ "name" : _ ("Related Alerts" ),
285+ "res_model" : "spp.alert" ,
286+ "view_mode" : "list,form" ,
287+ "domain" : [("rule_id" , "=" , self .rule_id .id ), ("id" , "!=" , self .id )],
288+ "context" : {"search_default_filter_active" : 1 },
289+ }
290+
291+ def action_view_source (self ):
292+ """Return an action to open the source record in a form view.
293+
294+ Returns:
295+ dict: An ir.actions.act_window action dict, or False if no source is set.
296+ """
297+ self .ensure_one ()
298+ if not self .res_model or not self .res_id :
299+ return False
300+ return {
301+ "type" : "ir.actions.act_window" ,
302+ "res_model" : self .res_model ,
303+ "res_id" : self .res_id ,
304+ "view_mode" : "form" ,
305+ "target" : "current" ,
306+ }
0 commit comments