Skip to content

Commit 9cef192

Browse files
fix: harden spp_alerts module for public release
Address 78 findings from staff engineer and UX expert reviews: Security: - Remove user browse record from safe_eval domain context (SE-1) - Add _check_company_auto and check_company on rule_id (SE-20) - Add domain filter validation constraint on save (SE-21) Bug fixes: - Fix priority ordering: add priority_sequence stored computed field so alerts sort critical > high > medium > low (SE-22) - Add state guard on action_acknowledge (only active alerts) (SE-5) - Add state guard on action_resolve (already-resolved raises) (SE-5) - Remove resolution_notes required attribute that blocked form saves Models: - Add mail.thread on alert rules with tracking on key fields (SE-24) - Add res_name computed field for source record display name (UX-5) - Add action_view_source, action_view_related_alerts methods - Add model_name related field for domain widget (SE-R2-8) - Add alert_count computed field with action_view_alerts (UX-14) - Extract _domain_eval_context helper to remove duplication - Fix _prepare_alert_vals to conditionally include threshold_value - Use named placeholders in translation strings Views: - Add kanban view with state grouping, progressbar, dropdown actions - Add stat buttons: View Source, Related Alerts on alert form - Add stat button: alert count on rule form - Add resolution guidance banner on Resolution tab - Add domain widget for rule filter configuration - Add chatter on rule form - Add named groups throughout for downstream extensibility - Fix group-by to use <group name="groupby"> (Odoo 19) - Fix Resolve button visibility for resolve-from-active flow - Add hotkeys, sample data, decorations, optional columns - Use fully qualified group references in all views Tests: - Replace skipTest guards with env.ref for vocab codes - Add 29 new tests (104 total, ~99% model coverage) - Fix 2 tests broken by new domain validation constraint Description: - Rewrite DESCRIPTION.md to match actual implementation - Rename ACL entry IDs to follow access_{model}_{group} convention - Remove empty assets from manifest
1 parent 9c2376e commit 9cef192

14 files changed

Lines changed: 886 additions & 520 deletions

File tree

spp_alerts/README.rst

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,26 @@ OpenSPP Alerts
2323
|badge1| |badge2| |badge3|
2424

2525
Generic alert engine for threshold monitoring, expiry tracking, and
26-
deadline management. Provides base models and state machine for alert
27-
lifecycle tracking. Consumer modules (like ``spp_drims``) extend these
28-
models and implement evaluation logic to generate alerts based on
29-
domain-specific conditions.
26+
deadline management. Evaluates configurable rules on a daily schedule or
27+
on-demand and generates alerts when conditions are met. Consumer modules
28+
(like ``spp_drims``) extend these models to add domain-specific fields.
3029

3130
Key Capabilities
3231
~~~~~~~~~~~~~~~~
3332

34-
- Track alert lifecycle through state machine: active → acknowledged →
35-
resolved
36-
- Record resolution details including user, timestamp, and notes
37-
- Classify alerts by type using ``spp.vocabulary`` codes (threshold,
38-
expiry, deadline, manual, system)
39-
- Prioritize alerts as low, medium, high, or critical
40-
- Send mail notifications via ``mail.thread`` integration
41-
- Auto-generate alert references in ALR-YYYY-NNNNN format
33+
- Define alert rules with threshold or date conditions against any model
34+
- Evaluate rules via daily cron or "Run Now" button
35+
- Compare numeric fields using 5 operators: <, <=, >, >=, =
36+
- Check date/datetime fields against a days-before window
37+
- Prevent duplicates: skip records with existing active/acknowledged
38+
alerts
39+
- Filter monitored records using a visual domain builder
40+
- Track alert lifecycle: active → acknowledged → resolved
41+
- Record resolution details: user, timestamp, and notes
42+
- Navigate from alert to source record via stat button
43+
- Classify alerts by type using ``spp.vocabulary`` codes
44+
- Prioritize as low, medium, high, or critical
45+
- Auto-generate references in ``ALR-YYYY-NNNNN`` format
4246

4347
Key Models
4448
~~~~~~~~~~
@@ -47,10 +51,10 @@ Key Models
4751
| Model | Description |
4852
+====================+=================================================+
4953
| ``spp.alert`` | Alert instance with state tracking and |
50-
| | resolution workflow |
54+
| | resolution audit |
5155
+--------------------+-------------------------------------------------+
52-
| ``spp.alert.rule`` | Rule configuration for monitoring criteria and |
53-
| | thresholds |
56+
| ``spp.alert.rule`` | Rule configuration with evaluation engine and |
57+
| | scheduling |
5458
+--------------------+-------------------------------------------------+
5559

5660
Configuration
@@ -59,40 +63,44 @@ Configuration
5963
After installing:
6064

6165
1. Navigate to **Settings > Technical > Alerts > Alert Rules**
62-
2. Create rules specifying alert type, priority, threshold values, and
63-
days before deadline
64-
3. Consumer modules implement checking logic (e.g., cron jobs or event
65-
handlers) to evaluate rules and create alerts
66+
2. Create rules: select model, rule type (threshold/date), and
67+
conditions
68+
3. The daily cron "Alerts: Evaluate Alert Rules" is active by default
6669

6770
UI Location
6871
~~~~~~~~~~~
6972

7073
- **Menu**: Settings > Technical > Alerts > Alerts
7174
- **Configuration**: Settings > Technical > Alerts > Alert Rules
72-
- **Form Tabs**: Details, Resolution (alerts); Thresholds (rules)
75+
- **Views**: List, kanban (grouped by state), and form
76+
- **Alert form tabs**: Details, Resolution
77+
- **Rule form**: Description above tabs; Evaluation tab with settings +
78+
domain builder
7379

7480
Security
7581
~~~~~~~~
7682

77-
=================================== ====================================
78-
Group Access
79-
=================================== ====================================
80-
``spp_alerts.group_alerts_viewer`` Read alerts
81-
``spp_alerts.group_alerts_officer`` Read/Write/Create (no delete) alerts
82-
``spp_alerts.group_alerts_manager`` Full CRUD on alerts and rules
83-
=================================== ====================================
83+
+-------------------------------------+----------------------------+-----------+
84+
| Group | Alerts | Rules |
85+
+=====================================+============================+===========+
86+
| ``spp_alerts.group_alerts_viewer`` | Read | Read |
87+
+-------------------------------------+----------------------------+-----------+
88+
| ``spp_alerts.group_alerts_officer`` | Read/Write/Create (no | Read |
89+
| | delete) | |
90+
+-------------------------------------+----------------------------+-----------+
91+
| ``spp_alerts.group_alerts_manager`` | Full CRUD | Full CRUD |
92+
+-------------------------------------+----------------------------+-----------+
8493

8594
Extension Points
8695
~~~~~~~~~~~~~~~~
8796

88-
- Inherit ``spp.alert`` to add domain-specific fields (e.g., stock
89-
levels, document references)
90-
- Inherit ``spp.alert.rule`` to add custom threshold or evaluation
91-
criteria
92-
- Override ``action_acknowledge()`` or ``action_resolve()`` to add
93-
custom workflow steps
94-
- Consumer modules implement alert checking via cron jobs or event
95-
handlers that evaluate rules and call ``create()`` on ``spp.alert``
97+
- Inherit ``spp.alert`` to add domain-specific fields
98+
- Inherit ``spp.alert.rule`` to add custom evaluation criteria
99+
- Override ``_evaluate_threshold()`` or ``_evaluate_date()`` for custom
100+
logic
101+
- Override ``action_acknowledge()`` or ``action_resolve()`` for custom
102+
workflows
103+
- Rules can be configured via UI without code
96104

97105
Dependencies
98106
~~~~~~~~~~~~

spp_alerts/__manifest__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
"views/alert_rule_views.xml",
3333
"views/menus.xml",
3434
],
35-
"assets": {},
3635
"application": False,
3736
"installable": True,
3837
"auto_install": False,

spp_alerts/models/alert.py

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
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

1017
class 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

Comments
 (0)