diff --git a/spp_alerts/README.rst b/spp_alerts/README.rst index 54f87c0d..3c726bc6 100644 --- a/spp_alerts/README.rst +++ b/spp_alerts/README.rst @@ -23,22 +23,26 @@ OpenSPP Alerts |badge1| |badge2| |badge3| Generic alert engine for threshold monitoring, expiry tracking, and -deadline management. Provides base models and state machine for alert -lifecycle tracking. Consumer modules (like ``spp_drims``) extend these -models and implement evaluation logic to generate alerts based on -domain-specific conditions. +deadline management. Evaluates configurable rules on a daily schedule or +on-demand and generates alerts when conditions are met. Consumer modules +(like ``spp_drims``) extend these models to add domain-specific fields. Key Capabilities ~~~~~~~~~~~~~~~~ -- Track alert lifecycle through state machine: active → acknowledged → - resolved -- Record resolution details including user, timestamp, and notes -- Classify alerts by type using ``spp.vocabulary`` codes (threshold, - expiry, deadline, manual, system) -- Prioritize alerts as low, medium, high, or critical -- Send mail notifications via ``mail.thread`` integration -- Auto-generate alert references in ALR-YYYY-NNNNN format +- Define alert rules with threshold or date conditions against any model +- Evaluate rules via daily cron or "Run Now" button +- Compare numeric fields using 5 operators: <, <=, >, >=, = +- Check date/datetime fields against a days-before window +- Prevent duplicates: skip records with existing active/acknowledged + alerts +- Filter monitored records using a visual domain builder +- Track alert lifecycle: active → acknowledged → resolved +- Record resolution details: user, timestamp, and notes +- Navigate from alert to source record via stat button +- Classify alerts by type using ``spp.vocabulary`` codes +- Prioritize as low, medium, high, or critical +- Auto-generate references in ``ALR-YYYY-NNNNN`` format Key Models ~~~~~~~~~~ @@ -47,10 +51,10 @@ Key Models | Model | Description | +====================+=================================================+ | ``spp.alert`` | Alert instance with state tracking and | -| | resolution workflow | +| | resolution audit | +--------------------+-------------------------------------------------+ -| ``spp.alert.rule`` | Rule configuration for monitoring criteria and | -| | thresholds | +| ``spp.alert.rule`` | Rule configuration with evaluation engine and | +| | scheduling | +--------------------+-------------------------------------------------+ Configuration @@ -59,40 +63,44 @@ Configuration After installing: 1. Navigate to **Settings > Technical > Alerts > Alert Rules** -2. Create rules specifying alert type, priority, threshold values, and - days before deadline -3. Consumer modules implement checking logic (e.g., cron jobs or event - handlers) to evaluate rules and create alerts +2. Create rules: select model, rule type (threshold/date), and + conditions +3. The daily cron "Alerts: Evaluate Alert Rules" is active by default UI Location ~~~~~~~~~~~ - **Menu**: Settings > Technical > Alerts > Alerts - **Configuration**: Settings > Technical > Alerts > Alert Rules -- **Form Tabs**: Details, Resolution (alerts); Thresholds (rules) +- **Views**: List, kanban (grouped by state), and form +- **Alert form tabs**: Details, Resolution +- **Rule form**: Description above tabs; Evaluation tab with settings + + domain builder Security ~~~~~~~~ -=================================== ==================================== -Group Access -=================================== ==================================== -``spp_alerts.group_alerts_viewer`` Read alerts -``spp_alerts.group_alerts_officer`` Read/Write/Create (no delete) alerts -``spp_alerts.group_alerts_manager`` Full CRUD on alerts and rules -=================================== ==================================== ++-------------------------------------+----------------------------+-----------+ +| Group | Alerts | Rules | ++=====================================+============================+===========+ +| ``spp_alerts.group_alerts_viewer`` | Read | Read | ++-------------------------------------+----------------------------+-----------+ +| ``spp_alerts.group_alerts_officer`` | Read/Write/Create (no | Read | +| | delete) | | ++-------------------------------------+----------------------------+-----------+ +| ``spp_alerts.group_alerts_manager`` | Full CRUD | Full CRUD | ++-------------------------------------+----------------------------+-----------+ Extension Points ~~~~~~~~~~~~~~~~ -- Inherit ``spp.alert`` to add domain-specific fields (e.g., stock - levels, document references) -- Inherit ``spp.alert.rule`` to add custom threshold or evaluation - criteria -- Override ``action_acknowledge()`` or ``action_resolve()`` to add - custom workflow steps -- Consumer modules implement alert checking via cron jobs or event - handlers that evaluate rules and call ``create()`` on ``spp.alert`` +- Inherit ``spp.alert`` to add domain-specific fields +- Inherit ``spp.alert.rule`` to add custom evaluation criteria +- Override ``_evaluate_threshold()`` or ``_evaluate_date()`` for custom + logic +- Override ``action_acknowledge()`` or ``action_resolve()`` for custom + workflows +- Rules can be configured via UI without code Dependencies ~~~~~~~~~~~~ @@ -104,6 +112,662 @@ Dependencies .. contents:: :local: +Usage +===== + +UI Testing Guide +---------------- + +This guide covers all user-facing functionality in the ``spp_alerts`` +module. Follow each section in order. Each test case includes the steps, +what to verify, and the expected result. + +**Prerequisites:** + +- Install the ``spp_alerts`` module +- Log in as **admin** (has all permissions by default) +- Ensure at least one model is available (e.g., ``res.partner`` — always + present) + +-------------- + +1. Navigation and Menu Access +----------------------------- + +**TC-1.1: Menu visibility** + +1. Navigate to **Settings > Technical** +2. Scroll down to find the **Alerts** section + +**Verify:** + +- ☐ An **Alerts** menu group is visible under Settings > Technical +- ☐ It contains two submenus: **Alerts** and **Alert Rules** + +**TC-1.2: URL paths** + +1. Navigate to **Settings > Technical > Alerts > Alerts** +2. Check the browser URL + +**Verify:** + +- ☐ URL ends with ``/odoo/alerts`` + +3. Navigate to **Settings > Technical > Alerts > Alert Rules** + +**Verify:** + +- ☐ URL ends with ``/odoo/alert-rules`` + +-------------- + +2. Alert Rules — List View +-------------------------- + +**TC-2.1: Empty state** + +1. Navigate to **Settings > Technical > Alerts > Alert Rules** +2. Remove any active search filters + +**Verify (if no rules exist):** + +- ☐ Empty state shows smiley face icon +- ☐ Message reads: "No alert rules configured" +- ☐ Description mentions configuring rules for thresholds, expiry dates, + and deadlines + +**TC-2.2: List view columns** + +1. Create at least one alert rule (see TC-3.1) +2. Return to the list view + +**Verify:** + +- ☐ Columns visible: drag handle (sequence), Name, Alert Type, Model to + Monitor, Rule Type, Priority, Threshold Value, Days Before, Active +- ☐ Rule Type, Threshold Value, and Days Before have "optional column" + toggles +- ☐ Rows can be reordered by dragging the handle icon +- ☐ Sample data appears in the background when the list is empty + +**TC-2.3: Search and filters** + +1. Click the search bar +2. Try searching by Name, Alert Type, and Model + +**Verify:** + +- ☐ Filters available: Active, Inactive, Critical Priority, High + Priority +- ☐ Group By options: Alert Type, Model, Priority, Rule Type + +-------------- + +3. Alert Rules — Form View +-------------------------- + +**TC-3.1: Create a threshold rule** + +1. Click **New** on the Alert Rules list +2. Fill in: + + - **Rule Name**: "Test Low Color Warning" + - **Alert Type**: select "Threshold Alert" from the dropdown (cannot + type to create new) + - **Default Priority**: "High" + - **Model to Monitor**: select "Contact" (res.partner) + - **Rule Type**: select "Threshold" + +3. Optionally add a **Description** in the text area below the main + fields +4. Observe the **Evaluation** tab appears after selecting Rule Type + +**Verify:** + +- ☐ Alert Type dropdown does NOT show a "Create" option +- ☐ After selecting Model and Rule Type, the Evaluation tab appears +- ☐ Evaluation tab shows **Threshold Settings** section with: Monitored + Field, Comparison, Threshold Value + +5. In the Evaluation tab: + + - **Monitored Field**: select "Color Index" (or any numeric field) + - **Comparison**: "Less Than (<)" + - **Threshold Value**: 5 + +6. Under **Record Filter**, observe the visual domain builder + +**Verify:** + +- ☐ Domain builder shows fields from the selected model + (Contact/res.partner) +- ☐ You can add filter conditions visually (e.g., "Active is set") + +7. Save the rule + +**Verify:** + +- ☐ Rule saves without errors +- ☐ Chatter (message log) appears at the bottom of the form +- ☐ An **Alerts** stat button appears in the top-right showing "0 + Alerts" + +**TC-3.2: Create a date rule** + +1. Create a new rule: + + - **Rule Name**: "Test Deadline Warning" + - **Alert Type**: "Deadline Alert" + - **Model to Monitor**: "Contact" + - **Rule Type**: "Date / Deadline" + +**Verify:** + +- ☐ Evaluation tab shows **Date Settings** section (not Threshold + Settings) +- ☐ Date Settings has: Date Field and Days Before + +2. Fill in: + + - **Date Field**: select "Last Updated on" (write_date) or any + date/datetime field + - **Days Before**: 30 + +3. Save + +**Verify:** + +- ☐ Rule saves without errors + +**TC-3.3: Run Now button** + +1. Open the threshold rule created in TC-3.1 +2. Click the **Run Now** button in the header + +**Verify:** + +- ☐ A notification appears: "X alert(s) created by rule 'Test Low Color + Warning'" +- ☐ The Alerts stat button count updates to reflect created alerts +- ☐ Click the stat button — it opens a filtered list of alerts from this + rule + +**TC-3.4: Archive and unarchive** + +1. Open any rule +2. Click **Action > Archive** + +**Verify:** + +- ☐ A red **Archived** ribbon appears on the form +- ☐ The rule disappears from the default list view (which filters active + rules) + +3. In the list view, add the "Inactive" filter + +**Verify:** + +- ☐ The archived rule appears +- ☐ Open it and click **Action > Unarchive** — ribbon disappears + +**TC-3.5: Validation errors** + +1. Create a new rule with Rule Type = "Threshold" but leave Monitored + Field empty +2. Try to save + +**Verify:** + +- ☐ Error: "A monitored field is required for threshold rules." + +3. Create a new rule with Rule Type = "Date / Deadline" but leave Date + Field empty +4. Try to save + +**Verify:** + +- ☐ Error: "A date field is required for date rules." + +5. Create a rule and set Domain Filter to an invalid expression (e.g., + type ``INVALID`` in the domain builder's code editor if available) +6. Try to save + +**Verify:** + +- ☐ Error: "Invalid domain filter: ..." + +**TC-3.6: Run Now without configuration** + +1. Create a rule with no Rule Type and no Model +2. Observe the header + +**Verify:** + +- ☐ The **Run Now** button is NOT visible (it requires both Rule Type + and Model) + +-------------- + +4. Alerts — Creating Manually +----------------------------- + +**TC-4.1: Create a manual alert** + +1. Navigate to **Settings > Technical > Alerts > Alerts** +2. Click **New** +3. Fill in: + + - **Alert Type**: "Manual Alert" + - **Priority**: "Critical" + - **Title**: "Test Manual Alert" + +4. Go to the **Details** tab and add a description +5. Save + +**Verify:** + +- ☐ Reference auto-generated in format ``ALR-YYYY-NNNNN`` (e.g., + ALR-2026-00001) +- ☐ State shows "Active" in the statusbar +- ☐ **Acknowledge** button (blue) is visible in the header +- ☐ **Resolve** button (green) is visible in the header +- ☐ Priority and Alert Type fields are editable +- ☐ Chatter (message log) appears at the bottom +- ☐ No "View Source" or "Related Alerts" stat buttons (manual alert has + no source) + +**TC-4.2: Unique references** + +1. Create three alerts quickly + +**Verify:** + +- ☐ Each alert gets a unique, sequential reference number + +-------------- + +5. Alerts — List View +--------------------- + +**TC-5.1: List view appearance** + +1. Navigate to **Settings > Technical > Alerts > Alerts** +2. Ensure some alerts exist (use Run Now on a rule, or create manually) + +**Verify:** + +- ☐ Columns visible: Reference, Title, Alert Type, Priority (badge), + State (badge), Created On +- ☐ Critical-priority rows have a red tint +- ☐ High-priority rows have an orange/warning tint +- ☐ Resolved rows are muted/grayed out +- ☐ Priority badges: Critical = red, High = orange, Medium = blue +- ☐ State badges: Active = red, Acknowledged = orange, Resolved = green +- ☐ Source Rule and Company columns are hidden by default (use optional + column toggle) +- ☐ Default filter shows only Active alerts (check search bar for + "Active" filter chip) + +**TC-5.2: Search panel** + +1. Look at the left side of the list view + +**Verify:** + +- ☐ Search panel shows three filter groups: State, Priority, Alert Type +- ☐ Each option shows a count of matching alerts +- ☐ Clicking a filter value narrows the list immediately + +**TC-5.3: Search filters and Group By** + +1. Click the search bar + +**Verify:** + +- ☐ Can search by Reference, Title, Alert Type +- ☐ Filters: Active, Acknowledged, Resolved, Critical, High Priority +- ☐ Group By: State, Priority, Type + +-------------- + +6. Alerts — Kanban View +----------------------- + +**TC-6.1: Switch to kanban** + +1. On the Alerts list, click the kanban view icon (grid icon in the view + switcher) + +**Verify:** + +- ☐ Alerts are displayed as cards grouped into columns by State: Active, + Acknowledged, Resolved +- ☐ Each column header shows the state name and alert count +- ☐ A colored progress bar appears at the top of each column showing + priority distribution (gray = low, blue = medium, orange = high, red = + critical) +- ☐ Quick create is disabled (no "+" button at top of columns) + +**TC-6.2: Kanban card content** + +1. Examine an alert card + +**Verify:** + +- ☐ Card shows: priority stars, reference (bold), title, alert type, + creation date +- ☐ A three-dot dropdown menu appears on hover (top-right of card) + +**TC-6.3: Kanban dropdown actions** + +1. Hover over an **Active** alert card and click the three-dot menu + +**Verify:** + +- ☐ Dropdown shows: **Acknowledge** and **Resolve** + +2. Click **Acknowledge** + +**Verify:** + +- ☐ Card moves to the Acknowledged column + +3. Hover over the now-acknowledged card, open dropdown + +**Verify:** + +- ☐ Dropdown shows only **Resolve** (Acknowledge is gone) + +4. Click **Resolve** + +**Verify:** + +- ☐ A dialog or form opens requesting resolution notes (since the action + requires notes) + +-------------- + +7. Alert State Transitions +-------------------------- + +**TC-7.1: Active to Acknowledged** + +1. Open an Active alert +2. Click the **Acknowledge** button + +**Verify:** + +- ☐ State changes to "Acknowledged" +- ☐ Statusbar updates to highlight "Acknowledged" +- ☐ Acknowledge button disappears +- ☐ Resolve button remains visible +- ☐ Fields (Alert Type, Priority, Title) are still editable +- ☐ Chatter logs a state change message + +**TC-7.2: Acknowledged to Resolved** + +1. On the acknowledged alert, go to the **Resolution** tab + +**Verify:** + +- ☐ An info banner reads: "Please add resolution notes describing how + this alert was addressed, then click Resolve." +- ☐ Resolved By and Resolved At fields are empty + +2. Enter resolution notes in the text area +3. Click the **Resolve** button + +**Verify:** + +- ☐ State changes to "Resolved" +- ☐ Resolved By shows your user name +- ☐ Resolved At shows the current timestamp +- ☐ Resolution Notes are preserved and now read-only +- ☐ Info banner disappears +- ☐ Both Acknowledge and Resolve buttons disappear +- ☐ All main fields (Alert Type, Priority, Title, Description) become + read-only + +**TC-7.3: Active to Resolved directly (skip Acknowledge)** + +1. Create a new alert and leave it in Active state +2. Go to the **Resolution** tab +3. Enter resolution notes +4. Click **Resolve** + +**Verify:** + +- ☐ Alert goes directly from Active to Resolved (skipping Acknowledged) +- ☐ All resolution fields are populated correctly + +**TC-7.4: Resolve without notes** + +1. Create a new Active alert +2. Click **Resolve** without entering resolution notes + +**Verify:** + +- ☐ Error message: "Please provide resolution notes before resolving the + alert." +- ☐ Alert remains in its current state + +**TC-7.5: Double-acknowledge prevention** + +1. Acknowledge an alert +2. Try to acknowledge it again (via API or another browser tab) + +**Verify:** + +- ☐ Error: "Only active alerts can be acknowledged." +- ☐ The Acknowledge button is not visible on the form (it only shows for + Active alerts) + +**TC-7.6: Double-resolve prevention** + +1. Resolve an alert +2. Try to resolve it again + +**Verify:** + +- ☐ Error: "Alert 'ALR-YYYY-NNNNN' is already resolved." +- ☐ The Resolve button is not visible on the form + +-------------- + +8. Alert Form — Rule-Generated Alerts +------------------------------------- + +**TC-8.1: Source tracking fields** + +1. Run a rule (via **Run Now** on an alert rule) +2. Open one of the generated alerts + +**Verify:** + +- ☐ Right side of the form shows a **Source** section with: Source Rule, + Source Model, Source Record, Source Record Name +- ☐ If the rule is a threshold rule, a **Metrics** section shows: + Current Value, Threshold, Days Until +- ☐ A **View Source** stat button appears in the top-right +- ☐ A **Related Alerts** stat button appears (if rule created multiple + alerts) + +**TC-8.2: View Source button** + +1. Click the **View Source** stat button + +**Verify:** + +- ☐ Opens the source record's form view (e.g., a Contact form) +- ☐ The correct record is displayed + +**TC-8.3: Related Alerts button** + +1. Go back to the alert and click the **Related Alerts** stat button + +**Verify:** + +- ☐ Opens a list of other alerts from the same rule (excluding the + current alert) +- ☐ Default filter shows Active alerts + +-------------- + +9. Keyboard Shortcuts +--------------------- + +**TC-9.1: Hotkeys** + +1. Open an Active alert form +2. Press **Alt+A** (or the platform equivalent for ``data-hotkey="a"``) + +**Verify:** + +- ☐ Alert is acknowledged + +3. Press **Alt+R** + +**Verify:** + +- ☐ Resolve action is triggered (will ask for notes if none provided) + +-------------- + +10. Cron Job +------------ + +**TC-10.1: Scheduled action exists** + +1. Navigate to **Settings > Technical > Scheduled Actions** +2. Search for "Alerts" + +**Verify:** + +- ☐ A scheduled action named **"Alerts: Evaluate Alert Rules"** exists +- ☐ It is active +- ☐ Interval is set to 1 day + +**TC-10.2: Cron execution** + +1. Ensure at least one active alert rule with matching records exists +2. Click **Run Manually** on the scheduled action + +**Verify:** + +- ☐ New alerts are created for matching records +- ☐ No duplicate alerts for records that already have + active/acknowledged alerts + +-------------- + +11. Security and Access Control +------------------------------- + +Test with three different users. Create them via **Settings > Users & +Companies > Users** and assign the appropriate group under the SPP Admin +section. + +**TC-11.1: Viewer role** + +1. Log in as a user with **Alerts Viewer** group only +2. Navigate to **Settings > Technical > Alerts > Alerts** + +**Verify:** + +- ☐ Can see the Alerts menu and list +- ☐ Can open and read alert details +- ☐ Cannot create new alerts (New button absent or errors on save) +- ☐ Cannot edit existing alerts +- ☐ Cannot acknowledge or resolve alerts (buttons error on click) +- ☐ **Alert Rules** submenu is NOT visible + +**TC-11.2: Officer role** + +1. Log in as a user with **Alerts Officer** group +2. Navigate to **Settings > Technical > Alerts > Alerts** + +**Verify:** + +- ☐ Can create new alerts +- ☐ Can edit alerts (change priority, title, etc.) +- ☐ Can acknowledge and resolve alerts +- ☐ Cannot delete alerts +- ☐ Can see **Alert Rules** submenu but rules are read-only +- ☐ Cannot create or edit alert rules + +**TC-11.3: Manager role** + +1. Log in as a user with **Alerts Manager** group + +**Verify:** + +- ☐ Full access to alerts: create, read, update, delete +- ☐ Full access to alert rules: create, read, update, delete +- ☐ Can click **Run Now** on alert rules +- ☐ Can archive/unarchive rules + +-------------- + +12. Multi-Company (if applicable) +--------------------------------- + +Only test this section if multi-company is enabled. + +**TC-12.1: Company isolation** + +1. Create an alert in Company A +2. Switch to Company B + +**Verify:** + +- ☐ The alert from Company A is not visible in Company B +- ☐ New alerts default to the current company + +-------------- + +13. Alert Types (Vocabulary) +---------------------------- + +**TC-13.1: Pre-installed types** + +1. Open any alert or rule form +2. Click the **Alert Type** dropdown + +**Verify the following types are available:** + +- ☐ Threshold Alert +- ☐ Expiry Alert +- ☐ Deadline Alert +- ☐ Manual Alert +- ☐ System Alert +- ☐ Cannot create new types from the dropdown (no "Create" option) + +-------------- + +14. Edge Cases +-------------- + +**TC-14.1: Empty state** + +1. Delete or resolve all alerts +2. Remove the "Active" default filter + +**Verify:** + +- ☐ Empty state shows: "No active alerts" with smiley face + +**TC-14.2: Sorting** + +1. Create alerts with different priorities (low, medium, high, critical) +2. View the list (default sort) + +**Verify:** + +- ☐ Alerts are sorted by priority (critical first) then by creation date + (newest first) +- ☐ This is semantic ordering: critical > high > medium > low (not + alphabetical) + Bug Tracker =========== diff --git a/spp_alerts/__manifest__.py b/spp_alerts/__manifest__.py index 53128cd7..65b48aa4 100644 --- a/spp_alerts/__manifest__.py +++ b/spp_alerts/__manifest__.py @@ -32,7 +32,6 @@ "views/alert_rule_views.xml", "views/menus.xml", ], - "assets": {}, "application": False, "installable": True, "auto_install": False, diff --git a/spp_alerts/models/alert.py b/spp_alerts/models/alert.py index 8976c420..b4738661 100644 --- a/spp_alerts/models/alert.py +++ b/spp_alerts/models/alert.py @@ -6,6 +6,13 @@ _logger = logging.getLogger(__name__) +PRIORITY_SEQUENCE = { + "low": 1, + "medium": 2, + "high": 3, + "critical": 4, +} + class Alert(models.Model): """Generic alert model for monitoring thresholds, expiries, and deadlines. @@ -25,8 +32,9 @@ class Alert(models.Model): _name = "spp.alert" _description = "Alert" _inherit = ["mail.thread"] - _order = "priority desc, create_date desc" + _order = "priority_sequence desc, create_date desc" _rec_name = "reference" + _check_company_auto = True reference = fields.Char( string="Reference", @@ -47,6 +55,8 @@ class Alert(models.Model): help="Type of alert from alert types vocabulary", ) + # Named `alert_type` (not `alert_type_code`) for concise domain filtering. + # Use `alert_type_id` for the vocabulary record, `alert_type` for the code string. alert_type = fields.Char( related="alert_type_id.code", store=True, @@ -69,6 +79,16 @@ class Alert(models.Model): help="Priority level of the alert", ) + priority_sequence = fields.Integer( + string="Priority Sequence", + compute="_compute_priority_sequence", + store=True, + help="Numeric ordering of priority for consistent sorting (low=1, medium=2, high=3, critical=4)", + ) + + # Alert states differ from the standard approval workflow states (draft/pending/approved) + # because the alert lifecycle is fundamentally different: alerts are raised, acknowledged, + # and resolved — there is no approval decision involved. state = fields.Selection( [ ("active", "Active"), @@ -102,6 +122,7 @@ class Alert(models.Model): index=True, readonly=True, ondelete="set null", + check_company=True, help="Alert rule that generated this alert (empty for manually created alerts)", ) @@ -120,6 +141,12 @@ class Alert(models.Model): help="Record that triggered this alert", ) + res_name = fields.Char( + string="Source Record Name", + compute="_compute_res_name", + help="Display name of the record that triggered this alert", + ) + # Metrics for threshold and deadline tracking current_value = fields.Float( string="Current Value", @@ -164,6 +191,28 @@ class Alert(models.Model): help="Company this alert belongs to", ) + @api.depends("priority") + def _compute_priority_sequence(self): + """Compute numeric priority sequence for consistent ordering.""" + for record in self: + record.priority_sequence = PRIORITY_SEQUENCE.get(record.priority, 0) + + def _compute_res_name(self): + """Compute the display name of the source record. + + Handles missing models or deleted records gracefully by returning an + empty string rather than raising an error. + """ + for record in self: + if not record.res_model or not record.res_id: + record.res_name = "" + continue + try: + source = self.env[record.res_model].browse(record.res_id) + record.res_name = source.display_name if source.exists() else "" + except (KeyError, AttributeError): + record.res_name = "" + @api.model_create_multi def create(self, vals_list): """Override create to auto-generate reference from sequence.""" @@ -180,7 +229,13 @@ def action_acknowledge(self): This is typically the first step in addressing an alert - acknowledging that it has been seen and is being investigated. + + Raises: + UserError: If the alert is not in the active state. """ + for record in self: + if record.state != "active": + raise UserError(_("Only active alerts can be acknowledged.")) self.write({"state": "acknowledged"}) return True @@ -194,9 +249,13 @@ def action_resolve(self, notes=None): bool: True on success Raises: - UserError: If resolution notes are not provided. + UserError: If the alert is already resolved or resolution notes are missing. """ + # Fail-fast: if any record in the batch lacks resolution notes, none are resolved. + # This prevents partial resolution of related alert batches. for record in self: + if record.state == "resolved": + raise UserError(_("Alert '%s' is already resolved.", record.reference)) resolution = notes or record.resolution_notes if not resolution: raise UserError(_("Please provide resolution notes before resolving the alert.")) @@ -210,3 +269,38 @@ def action_resolve(self, notes=None): vals["resolution_notes"] = notes self.write(vals) return True + + def action_view_related_alerts(self): + """Return an action to view other alerts from the same rule. + + Returns: + dict: An ir.actions.act_window action dict, or False if no rule is set. + """ + self.ensure_one() + if not self.rule_id: + return False + return { + "type": "ir.actions.act_window", + "name": _("Related Alerts"), + "res_model": "spp.alert", + "view_mode": "list,form", + "domain": [("rule_id", "=", self.rule_id.id), ("id", "!=", self.id)], + "context": {"search_default_filter_active": 1}, + } + + def action_view_source(self): + """Return an action to open the source record in a form view. + + Returns: + dict: An ir.actions.act_window action dict, or False if no source is set. + """ + self.ensure_one() + if not self.res_model or not self.res_id: + return False + return { + "type": "ir.actions.act_window", + "res_model": self.res_model, + "res_id": self.res_id, + "view_mode": "form", + "target": "current", + } diff --git a/spp_alerts/models/alert_rule.py b/spp_alerts/models/alert_rule.py index a31722f9..158a4408 100644 --- a/spp_alerts/models/alert_rule.py +++ b/spp_alerts/models/alert_rule.py @@ -35,6 +35,7 @@ class AlertRule(models.Model): _name = "spp.alert.rule" _description = "Alert Rule" + _inherit = ["mail.thread"] _order = "sequence, name" name = fields.Char( @@ -55,6 +56,14 @@ class AlertRule(models.Model): "ir.model", string="Model to Monitor", help="Odoo model this rule monitors", + tracking=True, + ) + + model_name = fields.Char( + related="model_id.model", + string="Model Name", + readonly=True, + help="Technical model name, used by the domain filter widget", ) priority = fields.Selection( @@ -68,12 +77,14 @@ class AlertRule(models.Model): default="medium", required=True, help="Default priority for alerts created by this rule", + tracking=True, ) active = fields.Boolean( string="Active", default=True, help="Inactive rules will not create alerts", + tracking=True, ) sequence = fields.Integer( @@ -92,6 +103,7 @@ class AlertRule(models.Model): help="Determines evaluation logic:\n" "- Threshold: Compare a numeric field against threshold_value\n" "- Date: Check if a date field is within days_before of today", + tracking=True, ) domain_filter = fields.Text( @@ -132,12 +144,14 @@ class AlertRule(models.Model): threshold_value = fields.Float( string="Threshold Value", help="Threshold value for comparison (e.g., minimum stock level, maximum days)", + tracking=True, ) days_before = fields.Integer( string="Days Before", default=0, help="Days before expiry/deadline to trigger alert (0 = at deadline)", + tracking=True, ) description = fields.Text( @@ -153,6 +167,35 @@ class AlertRule(models.Model): help="Company this rule applies to (empty = all companies)", ) + alert_count = fields.Integer( + string="Alert Count", + compute="_compute_alert_count", + help="Number of alerts created by this rule", + ) + + def _compute_alert_count(self): + """Compute the number of alerts associated with each rule.""" + alert_data = self.env["spp.alert"].read_group( + [("rule_id", "in", self.ids)], + ["rule_id"], + ["rule_id"], + ) + count_map = {d["rule_id"][0]: d["rule_id_count"] for d in alert_data} + for rule in self: + rule.alert_count = count_map.get(rule.id, 0) + + def action_view_alerts(self): + """Open list view of alerts created by this rule.""" + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": _("Alerts"), + "res_model": "spp.alert", + "view_mode": "list,form", + "domain": [("rule_id", "=", self.id)], + "context": {"default_rule_id": self.id}, + } + # ------------------------------------------------------------------------- # Constraints # ------------------------------------------------------------------------- @@ -170,6 +213,32 @@ def _check_rule_configuration(self): if rule.rule_type == "date" and not rule.date_field_id: raise ValidationError(_("A date field is required for date rules.")) + def _domain_eval_context(self): + """Return the safe_eval context used when parsing domain filter expressions.""" + return { + "datetime": safe_eval.datetime, + "dateutil": safe_eval.dateutil, + "time": safe_eval.time, + "uid": self.env.uid, + } + + @api.constrains("domain_filter") + def _check_domain_filter(self): + """Validate that domain_filter is a parseable Odoo domain expression.""" + for rule in self: + if not rule.domain_filter or rule.domain_filter.strip() == "[]": + continue + try: + result = safe_eval.safe_eval( # nosemgrep: odoo-unsafe-safe-eval + rule.domain_filter, self._domain_eval_context() + ) + if not isinstance(result, list): + raise ValidationError(_("Domain filter must be a list, got %s.", type(result).__name__)) + except ValidationError: + raise + except Exception as e: + raise ValidationError(_("Invalid domain filter: %s", e)) from e + # ------------------------------------------------------------------------- # Actions # ------------------------------------------------------------------------- @@ -182,14 +251,21 @@ def action_evaluate(self): if not self.model_id: raise UserError(_("Cannot evaluate rule '%s': no model to monitor is configured.", self.name)) - count = self._evaluate_rule() + try: + count = self._evaluate_rule() + except Exception as e: + raise UserError(_("Error evaluating rule '%(rule)s': %(error)s", rule=self.name, error=e)) from e return { "type": "ir.actions.client", "tag": "display_notification", "params": { "title": _("Rule Evaluated"), - "message": _("%d alert(s) created by rule '%s'.", count, self.name), + "message": _( + "%(count)d alert(s) created by rule '%(rule)s'.", + count=count, + rule=self.name, + ), "type": "success" if count > 0 else "info", "sticky": False, }, @@ -215,21 +291,16 @@ def _evaluate_rule(self): try: Model = self.env[model_name] except KeyError: - _logger.warning("Alert rule '%s': model '%s' not found, skipping.", self.id, model_name) + _logger.warning("Alert rule '%s' (ID: %d): model '%s' not found, skipping.", self.name, self.id, model_name) return 0 # Parse domain filter try: - eval_context = { - "datetime": safe_eval.datetime, - "dateutil": safe_eval.dateutil, - "time": safe_eval.time, - "uid": self.env.uid, - "user": self.env.user, - } - domain = safe_eval.safe_eval(self.domain_filter or "[]", eval_context) # nosemgrep: odoo-unsafe-safe-eval + domain = safe_eval.safe_eval( # nosemgrep: odoo-unsafe-safe-eval + self.domain_filter or "[]", self._domain_eval_context() + ) except Exception as e: - _logger.error("Alert rule '%s': invalid domain filter: %s", self.id, e) + _logger.error("Alert rule '%s' (ID: %d): invalid domain filter: %s", self.name, self.id, e) return 0 records = Model.search(domain) @@ -238,7 +309,7 @@ def _evaluate_rule(self): if self.rule_type == "threshold": return self._evaluate_threshold(records, model_name) - elif self.rule_type == "date": + if self.rule_type == "date": return self._evaluate_date(records, model_name) return 0 @@ -260,6 +331,8 @@ def _evaluate_threshold(self, records, model_name): existing = self._get_existing_alert_keys(model_name, records.ids) alerts_to_create = [] + # Performance note: iterates records in Python. For very large recordsets (10k+), + # consider batch-reading field values via mapped() or read(). for record in records: if record.id in existing: continue @@ -291,6 +364,8 @@ def _evaluate_date(self, records, model_name): existing = self._get_existing_alert_keys(model_name, records.ids) alerts_to_create = [] + # Performance note: iterates records in Python. For very large recordsets (10k+), + # consider batch-reading field values via mapped() or read(). for record in records: if record.id in existing: continue @@ -360,7 +435,6 @@ def _prepare_alert_vals(self, record, model_name, current_value=None, days_until "description": self.description or "", "res_model": model_name, "res_id": record.id, - "threshold_value": self.threshold_value, } if self.company_id: @@ -368,6 +442,7 @@ def _prepare_alert_vals(self, record, model_name, current_value=None, days_until if current_value is not None: vals["current_value"] = current_value + vals["threshold_value"] = self.threshold_value if days_until is not None: vals["days_until"] = days_until @@ -378,6 +453,9 @@ def _prepare_alert_vals(self, record, model_name, current_value=None, days_until # Cron # ------------------------------------------------------------------------- + # Cron runs as superuser (OdooBot). Rule evaluation searches monitored models with + # full access, bypassing record rules. This is intentional — only managers can create + # rules, so the monitored scope is admin-controlled. @api.model def _cron_evaluate_rules(self): """Scheduled action to evaluate all active, configured rules.""" diff --git a/spp_alerts/readme/DESCRIPTION.md b/spp_alerts/readme/DESCRIPTION.md index 66f7d653..69fee072 100644 --- a/spp_alerts/readme/DESCRIPTION.md +++ b/spp_alerts/readme/DESCRIPTION.md @@ -1,49 +1,61 @@ -Generic alert engine for threshold monitoring, expiry tracking, and deadline management. Provides base models and state machine for alert lifecycle tracking. Consumer modules (like `spp_drims`) extend these models and implement evaluation logic to generate alerts based on domain-specific conditions. +Generic alert engine for threshold monitoring, expiry tracking, and deadline +management. Evaluates configurable rules on a daily schedule or on-demand and +generates alerts when conditions are met. Consumer modules (like `spp_drims`) +extend these models to add domain-specific fields. ### Key Capabilities -- Track alert lifecycle through state machine: active → acknowledged → resolved -- Record resolution details including user, timestamp, and notes -- Classify alerts by type using `spp.vocabulary` codes (threshold, expiry, deadline, manual, system) -- Prioritize alerts as low, medium, high, or critical -- Send mail notifications via `mail.thread` integration -- Auto-generate alert references in ALR-YYYY-NNNNN format +- Define alert rules with threshold or date conditions against any model +- Evaluate rules via daily cron or "Run Now" button +- Compare numeric fields using 5 operators: <, <=, >, >=, = +- Check date/datetime fields against a days-before window +- Prevent duplicates: skip records with existing active/acknowledged alerts +- Filter monitored records using a visual domain builder +- Track alert lifecycle: active → acknowledged → resolved +- Record resolution details: user, timestamp, and notes +- Navigate from alert to source record via stat button +- Classify alerts by type using `spp.vocabulary` codes +- Prioritize as low, medium, high, or critical +- Auto-generate references in `ALR-YYYY-NNNNN` format ### Key Models -| Model | Description | -| ------------------ | ------------------------------------------------------- | -| `spp.alert` | Alert instance with state tracking and resolution workflow | -| `spp.alert.rule` | Rule configuration for monitoring criteria and thresholds | +| Model | Description | +| ---------------- | -------------------------------------------------------- | +| `spp.alert` | Alert instance with state tracking and resolution audit | +| `spp.alert.rule` | Rule configuration with evaluation engine and scheduling | ### Configuration After installing: 1. Navigate to **Settings > Technical > Alerts > Alert Rules** -2. Create rules specifying alert type, priority, threshold values, and days before deadline -3. Consumer modules implement checking logic (e.g., cron jobs or event handlers) to evaluate rules and create alerts +2. Create rules: select model, rule type (threshold/date), and conditions +3. The daily cron "Alerts: Evaluate Alert Rules" is active by default ### UI Location - **Menu**: Settings > Technical > Alerts > Alerts - **Configuration**: Settings > Technical > Alerts > Alert Rules -- **Form Tabs**: Details, Resolution (alerts); Thresholds (rules) +- **Views**: List, kanban (grouped by state), and form +- **Alert form tabs**: Details, Resolution +- **Rule form**: Description above tabs; Evaluation tab with settings + domain builder ### Security -| Group | Access | -| -------------------------------- | ---------------------------------- | -| `spp_alerts.group_alerts_viewer` | Read alerts | -| `spp_alerts.group_alerts_officer` | Read/Write/Create (no delete) alerts | -| `spp_alerts.group_alerts_manager` | Full CRUD on alerts and rules | +| Group | Alerts | Rules | +| --------------------------------- | ----------------------------- | --------- | +| `spp_alerts.group_alerts_viewer` | Read | Read | +| `spp_alerts.group_alerts_officer` | Read/Write/Create (no delete) | Read | +| `spp_alerts.group_alerts_manager` | Full CRUD | Full CRUD | ### Extension Points -- Inherit `spp.alert` to add domain-specific fields (e.g., stock levels, document references) -- Inherit `spp.alert.rule` to add custom threshold or evaluation criteria -- Override `action_acknowledge()` or `action_resolve()` to add custom workflow steps -- Consumer modules implement alert checking via cron jobs or event handlers that evaluate rules and call `create()` on `spp.alert` +- Inherit `spp.alert` to add domain-specific fields +- Inherit `spp.alert.rule` to add custom evaluation criteria +- Override `_evaluate_threshold()` or `_evaluate_date()` for custom logic +- Override `action_acknowledge()` or `action_resolve()` for custom workflows +- Rules can be configured via UI without code ### Dependencies diff --git a/spp_alerts/readme/USAGE.md b/spp_alerts/readme/USAGE.md new file mode 100644 index 00000000..3622dde5 --- /dev/null +++ b/spp_alerts/readme/USAGE.md @@ -0,0 +1,588 @@ +## UI Testing Guide + +This guide covers all user-facing functionality in the `spp_alerts` module. +Follow each section in order. Each test case includes the steps, what to verify, +and the expected result. + +**Prerequisites:** + +- Install the `spp_alerts` module +- Log in as **admin** (has all permissions by default) +- Ensure at least one model is available (e.g., `res.partner` — always present) + +--- + +## 1. Navigation and Menu Access + +**TC-1.1: Menu visibility** + +1. Navigate to **Settings > Technical** +2. Scroll down to find the **Alerts** section + +**Verify:** + +- [ ] An **Alerts** menu group is visible under Settings > Technical +- [ ] It contains two submenus: **Alerts** and **Alert Rules** + +**TC-1.2: URL paths** + +1. Navigate to **Settings > Technical > Alerts > Alerts** +2. Check the browser URL + +**Verify:** + +- [ ] URL ends with `/odoo/alerts` + +3. Navigate to **Settings > Technical > Alerts > Alert Rules** + +**Verify:** + +- [ ] URL ends with `/odoo/alert-rules` + +--- + +## 2. Alert Rules — List View + +**TC-2.1: Empty state** + +1. Navigate to **Settings > Technical > Alerts > Alert Rules** +2. Remove any active search filters + +**Verify (if no rules exist):** + +- [ ] Empty state shows smiley face icon +- [ ] Message reads: "No alert rules configured" +- [ ] Description mentions configuring rules for thresholds, expiry dates, and deadlines + +**TC-2.2: List view columns** + +1. Create at least one alert rule (see TC-3.1) +2. Return to the list view + +**Verify:** + +- [ ] Columns visible: drag handle (sequence), Name, Alert Type, Model to Monitor, Rule Type, Priority, Threshold Value, Days Before, Active +- [ ] Rule Type, Threshold Value, and Days Before have "optional column" toggles +- [ ] Rows can be reordered by dragging the handle icon +- [ ] Sample data appears in the background when the list is empty + +**TC-2.3: Search and filters** + +1. Click the search bar +2. Try searching by Name, Alert Type, and Model + +**Verify:** + +- [ ] Filters available: Active, Inactive, Critical Priority, High Priority +- [ ] Group By options: Alert Type, Model, Priority, Rule Type + +--- + +## 3. Alert Rules — Form View + +**TC-3.1: Create a threshold rule** + +1. Click **New** on the Alert Rules list +2. Fill in: + - **Rule Name**: "Test Low Color Warning" + - **Alert Type**: select "Threshold Alert" from the dropdown (cannot type to create new) + - **Default Priority**: "High" + - **Model to Monitor**: select "Contact" (res.partner) + - **Rule Type**: select "Threshold" +3. Optionally add a **Description** in the text area below the main fields +4. Observe the **Evaluation** tab appears after selecting Rule Type + +**Verify:** + +- [ ] Alert Type dropdown does NOT show a "Create" option +- [ ] After selecting Model and Rule Type, the Evaluation tab appears +- [ ] Evaluation tab shows **Threshold Settings** section with: Monitored Field, Comparison, Threshold Value + +5. In the Evaluation tab: + - **Monitored Field**: select "Color Index" (or any numeric field) + - **Comparison**: "Less Than (<)" + - **Threshold Value**: 5 +6. Under **Record Filter**, observe the visual domain builder + +**Verify:** + +- [ ] Domain builder shows fields from the selected model (Contact/res.partner) +- [ ] You can add filter conditions visually (e.g., "Active is set") + +7. Save the rule + +**Verify:** + +- [ ] Rule saves without errors +- [ ] Chatter (message log) appears at the bottom of the form +- [ ] An **Alerts** stat button appears in the top-right showing "0 Alerts" + +**TC-3.2: Create a date rule** + +1. Create a new rule: + - **Rule Name**: "Test Deadline Warning" + - **Alert Type**: "Deadline Alert" + - **Model to Monitor**: "Contact" + - **Rule Type**: "Date / Deadline" + +**Verify:** + +- [ ] Evaluation tab shows **Date Settings** section (not Threshold Settings) +- [ ] Date Settings has: Date Field and Days Before + +2. Fill in: + - **Date Field**: select "Last Updated on" (write_date) or any date/datetime field + - **Days Before**: 30 +3. Save + +**Verify:** + +- [ ] Rule saves without errors + +**TC-3.3: Run Now button** + +1. Open the threshold rule created in TC-3.1 +2. Click the **Run Now** button in the header + +**Verify:** + +- [ ] A notification appears: "X alert(s) created by rule 'Test Low Color Warning'" +- [ ] The Alerts stat button count updates to reflect created alerts +- [ ] Click the stat button — it opens a filtered list of alerts from this rule + +**TC-3.4: Archive and unarchive** + +1. Open any rule +2. Click **Action > Archive** + +**Verify:** + +- [ ] A red **Archived** ribbon appears on the form +- [ ] The rule disappears from the default list view (which filters active rules) + +3. In the list view, add the "Inactive" filter + +**Verify:** + +- [ ] The archived rule appears +- [ ] Open it and click **Action > Unarchive** — ribbon disappears + +**TC-3.5: Validation errors** + +1. Create a new rule with Rule Type = "Threshold" but leave Monitored Field empty +2. Try to save + +**Verify:** + +- [ ] Error: "A monitored field is required for threshold rules." + +3. Create a new rule with Rule Type = "Date / Deadline" but leave Date Field empty +4. Try to save + +**Verify:** + +- [ ] Error: "A date field is required for date rules." + +5. Create a rule and set Domain Filter to an invalid expression (e.g., type `INVALID` in + the domain builder's code editor if available) +6. Try to save + +**Verify:** + +- [ ] Error: "Invalid domain filter: ..." + +**TC-3.6: Run Now without configuration** + +1. Create a rule with no Rule Type and no Model +2. Observe the header + +**Verify:** + +- [ ] The **Run Now** button is NOT visible (it requires both Rule Type and Model) + +--- + +## 4. Alerts — Creating Manually + +**TC-4.1: Create a manual alert** + +1. Navigate to **Settings > Technical > Alerts > Alerts** +2. Click **New** +3. Fill in: + - **Alert Type**: "Manual Alert" + - **Priority**: "Critical" + - **Title**: "Test Manual Alert" +4. Go to the **Details** tab and add a description +5. Save + +**Verify:** + +- [ ] Reference auto-generated in format `ALR-YYYY-NNNNN` (e.g., ALR-2026-00001) +- [ ] State shows "Active" in the statusbar +- [ ] **Acknowledge** button (blue) is visible in the header +- [ ] **Resolve** button (green) is visible in the header +- [ ] Priority and Alert Type fields are editable +- [ ] Chatter (message log) appears at the bottom +- [ ] No "View Source" or "Related Alerts" stat buttons (manual alert has no source) + +**TC-4.2: Unique references** + +1. Create three alerts quickly + +**Verify:** + +- [ ] Each alert gets a unique, sequential reference number + +--- + +## 5. Alerts — List View + +**TC-5.1: List view appearance** + +1. Navigate to **Settings > Technical > Alerts > Alerts** +2. Ensure some alerts exist (use Run Now on a rule, or create manually) + +**Verify:** + +- [ ] Columns visible: Reference, Title, Alert Type, Priority (badge), State (badge), Created On +- [ ] Critical-priority rows have a red tint +- [ ] High-priority rows have an orange/warning tint +- [ ] Resolved rows are muted/grayed out +- [ ] Priority badges: Critical = red, High = orange, Medium = blue +- [ ] State badges: Active = red, Acknowledged = orange, Resolved = green +- [ ] Source Rule and Company columns are hidden by default (use optional column toggle) +- [ ] Default filter shows only Active alerts (check search bar for "Active" filter chip) + +**TC-5.2: Search panel** + +1. Look at the left side of the list view + +**Verify:** + +- [ ] Search panel shows three filter groups: State, Priority, Alert Type +- [ ] Each option shows a count of matching alerts +- [ ] Clicking a filter value narrows the list immediately + +**TC-5.3: Search filters and Group By** + +1. Click the search bar + +**Verify:** + +- [ ] Can search by Reference, Title, Alert Type +- [ ] Filters: Active, Acknowledged, Resolved, Critical, High Priority +- [ ] Group By: State, Priority, Type + +--- + +## 6. Alerts — Kanban View + +**TC-6.1: Switch to kanban** + +1. On the Alerts list, click the kanban view icon (grid icon in the view switcher) + +**Verify:** + +- [ ] Alerts are displayed as cards grouped into columns by State: Active, Acknowledged, Resolved +- [ ] Each column header shows the state name and alert count +- [ ] A colored progress bar appears at the top of each column showing priority distribution + (gray = low, blue = medium, orange = high, red = critical) +- [ ] Quick create is disabled (no "+" button at top of columns) + +**TC-6.2: Kanban card content** + +1. Examine an alert card + +**Verify:** + +- [ ] Card shows: priority stars, reference (bold), title, alert type, creation date +- [ ] A three-dot dropdown menu appears on hover (top-right of card) + +**TC-6.3: Kanban dropdown actions** + +1. Hover over an **Active** alert card and click the three-dot menu + +**Verify:** + +- [ ] Dropdown shows: **Acknowledge** and **Resolve** + +2. Click **Acknowledge** + +**Verify:** + +- [ ] Card moves to the Acknowledged column + +3. Hover over the now-acknowledged card, open dropdown + +**Verify:** + +- [ ] Dropdown shows only **Resolve** (Acknowledge is gone) + +4. Click **Resolve** + +**Verify:** + +- [ ] A dialog or form opens requesting resolution notes (since the action requires notes) + +--- + +## 7. Alert State Transitions + +**TC-7.1: Active to Acknowledged** + +1. Open an Active alert +2. Click the **Acknowledge** button + +**Verify:** + +- [ ] State changes to "Acknowledged" +- [ ] Statusbar updates to highlight "Acknowledged" +- [ ] Acknowledge button disappears +- [ ] Resolve button remains visible +- [ ] Fields (Alert Type, Priority, Title) are still editable +- [ ] Chatter logs a state change message + +**TC-7.2: Acknowledged to Resolved** + +1. On the acknowledged alert, go to the **Resolution** tab + +**Verify:** + +- [ ] An info banner reads: "Please add resolution notes describing how this alert was addressed, then click Resolve." +- [ ] Resolved By and Resolved At fields are empty + +2. Enter resolution notes in the text area +3. Click the **Resolve** button + +**Verify:** + +- [ ] State changes to "Resolved" +- [ ] Resolved By shows your user name +- [ ] Resolved At shows the current timestamp +- [ ] Resolution Notes are preserved and now read-only +- [ ] Info banner disappears +- [ ] Both Acknowledge and Resolve buttons disappear +- [ ] All main fields (Alert Type, Priority, Title, Description) become read-only + +**TC-7.3: Active to Resolved directly (skip Acknowledge)** + +1. Create a new alert and leave it in Active state +2. Go to the **Resolution** tab +3. Enter resolution notes +4. Click **Resolve** + +**Verify:** + +- [ ] Alert goes directly from Active to Resolved (skipping Acknowledged) +- [ ] All resolution fields are populated correctly + +**TC-7.4: Resolve without notes** + +1. Create a new Active alert +2. Click **Resolve** without entering resolution notes + +**Verify:** + +- [ ] Error message: "Please provide resolution notes before resolving the alert." +- [ ] Alert remains in its current state + +**TC-7.5: Double-acknowledge prevention** + +1. Acknowledge an alert +2. Try to acknowledge it again (via API or another browser tab) + +**Verify:** + +- [ ] Error: "Only active alerts can be acknowledged." +- [ ] The Acknowledge button is not visible on the form (it only shows for Active alerts) + +**TC-7.6: Double-resolve prevention** + +1. Resolve an alert +2. Try to resolve it again + +**Verify:** + +- [ ] Error: "Alert 'ALR-YYYY-NNNNN' is already resolved." +- [ ] The Resolve button is not visible on the form + +--- + +## 8. Alert Form — Rule-Generated Alerts + +**TC-8.1: Source tracking fields** + +1. Run a rule (via **Run Now** on an alert rule) +2. Open one of the generated alerts + +**Verify:** + +- [ ] Right side of the form shows a **Source** section with: Source Rule, Source Model, Source Record, Source Record Name +- [ ] If the rule is a threshold rule, a **Metrics** section shows: Current Value, Threshold, Days Until +- [ ] A **View Source** stat button appears in the top-right +- [ ] A **Related Alerts** stat button appears (if rule created multiple alerts) + +**TC-8.2: View Source button** + +1. Click the **View Source** stat button + +**Verify:** + +- [ ] Opens the source record's form view (e.g., a Contact form) +- [ ] The correct record is displayed + +**TC-8.3: Related Alerts button** + +1. Go back to the alert and click the **Related Alerts** stat button + +**Verify:** + +- [ ] Opens a list of other alerts from the same rule (excluding the current alert) +- [ ] Default filter shows Active alerts + +--- + +## 9. Keyboard Shortcuts + +**TC-9.1: Hotkeys** + +1. Open an Active alert form +2. Press **Alt+A** (or the platform equivalent for `data-hotkey="a"`) + +**Verify:** + +- [ ] Alert is acknowledged + +3. Press **Alt+R** + +**Verify:** + +- [ ] Resolve action is triggered (will ask for notes if none provided) + +--- + +## 10. Cron Job + +**TC-10.1: Scheduled action exists** + +1. Navigate to **Settings > Technical > Scheduled Actions** +2. Search for "Alerts" + +**Verify:** + +- [ ] A scheduled action named **"Alerts: Evaluate Alert Rules"** exists +- [ ] It is active +- [ ] Interval is set to 1 day + +**TC-10.2: Cron execution** + +1. Ensure at least one active alert rule with matching records exists +2. Click **Run Manually** on the scheduled action + +**Verify:** + +- [ ] New alerts are created for matching records +- [ ] No duplicate alerts for records that already have active/acknowledged alerts + +--- + +## 11. Security and Access Control + +Test with three different users. Create them via **Settings > Users & Companies > Users** +and assign the appropriate group under the SPP Admin section. + +**TC-11.1: Viewer role** + +1. Log in as a user with **Alerts Viewer** group only +2. Navigate to **Settings > Technical > Alerts > Alerts** + +**Verify:** + +- [ ] Can see the Alerts menu and list +- [ ] Can open and read alert details +- [ ] Cannot create new alerts (New button absent or errors on save) +- [ ] Cannot edit existing alerts +- [ ] Cannot acknowledge or resolve alerts (buttons error on click) +- [ ] **Alert Rules** submenu is NOT visible + +**TC-11.2: Officer role** + +1. Log in as a user with **Alerts Officer** group +2. Navigate to **Settings > Technical > Alerts > Alerts** + +**Verify:** + +- [ ] Can create new alerts +- [ ] Can edit alerts (change priority, title, etc.) +- [ ] Can acknowledge and resolve alerts +- [ ] Cannot delete alerts +- [ ] Can see **Alert Rules** submenu but rules are read-only +- [ ] Cannot create or edit alert rules + +**TC-11.3: Manager role** + +1. Log in as a user with **Alerts Manager** group + +**Verify:** + +- [ ] Full access to alerts: create, read, update, delete +- [ ] Full access to alert rules: create, read, update, delete +- [ ] Can click **Run Now** on alert rules +- [ ] Can archive/unarchive rules + +--- + +## 12. Multi-Company (if applicable) + +Only test this section if multi-company is enabled. + +**TC-12.1: Company isolation** + +1. Create an alert in Company A +2. Switch to Company B + +**Verify:** + +- [ ] The alert from Company A is not visible in Company B +- [ ] New alerts default to the current company + +--- + +## 13. Alert Types (Vocabulary) + +**TC-13.1: Pre-installed types** + +1. Open any alert or rule form +2. Click the **Alert Type** dropdown + +**Verify the following types are available:** + +- [ ] Threshold Alert +- [ ] Expiry Alert +- [ ] Deadline Alert +- [ ] Manual Alert +- [ ] System Alert +- [ ] Cannot create new types from the dropdown (no "Create" option) + +--- + +## 14. Edge Cases + +**TC-14.1: Empty state** + +1. Delete or resolve all alerts +2. Remove the "Active" default filter + +**Verify:** + +- [ ] Empty state shows: "No active alerts" with smiley face + +**TC-14.2: Sorting** + +1. Create alerts with different priorities (low, medium, high, critical) +2. View the list (default sort) + +**Verify:** + +- [ ] Alerts are sorted by priority (critical first) then by creation date (newest first) +- [ ] This is semantic ordering: critical > high > medium > low (not alphabetical) diff --git a/spp_alerts/security/ir.model.access.csv b/spp_alerts/security/ir.model.access.csv index 8674d63a..f17a88ee 100644 --- a/spp_alerts/security/ir.model.access.csv +++ b/spp_alerts/security/ir.model.access.csv @@ -1,13 +1,13 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_spp_alert_sysadmin,Alert System Admin,model_spp_alert,base.group_system,1,1,1,1 -access_spp_alert_rule_sysadmin,Alert Rule System Admin,model_spp_alert_rule,base.group_system,1,1,1,1 -access_spp_alert_admin,Alert Admin,model_spp_alert,spp_security.group_spp_admin,1,1,1,1 -access_spp_alert_rule_admin,Alert Rule Admin,model_spp_alert_rule,spp_security.group_spp_admin,1,1,1,1 -access_spp_alert_read,Alert Read,model_spp_alert,group_alerts_read,1,0,0,0 -access_spp_alert_rule_read,Alert Rule Read,model_spp_alert_rule,group_alerts_read,1,0,0,0 -access_spp_alert_write,Alert Write,model_spp_alert,group_alerts_write,1,1,0,0 -access_spp_alert_create,Alert Create,model_spp_alert,group_alerts_create,1,1,1,0 -access_spp_alert_manager,Alert Manager,model_spp_alert,group_alerts_manager,1,1,1,1 -access_spp_alert_rule_manager,Alert Rule Manager,model_spp_alert_rule,group_alerts_manager,1,1,1,1 -access_spp_alert_officer,Alert Officer,model_spp_alert,group_alerts_officer,1,1,1,0 -access_spp_alert_viewer,Alert Viewer,model_spp_alert,group_alerts_viewer,1,0,0,0 +access_spp_alert_system,Alert System Admin,model_spp_alert,base.group_system,1,1,1,1 +access_spp_alert_rule_system,Alert Rule System Admin,model_spp_alert_rule,base.group_system,1,1,1,1 +access_spp_alert_spp_admin,Alert SPP Admin,model_spp_alert,spp_security.group_spp_admin,1,1,1,1 +access_spp_alert_rule_spp_admin,Alert Rule SPP Admin,model_spp_alert_rule,spp_security.group_spp_admin,1,1,1,1 +access_spp_alert_alerts_read,Alert Read,model_spp_alert,group_alerts_read,1,0,0,0 +access_spp_alert_rule_alerts_read,Alert Rule Read,model_spp_alert_rule,group_alerts_read,1,0,0,0 +access_spp_alert_alerts_write,Alert Write,model_spp_alert,group_alerts_write,1,1,0,0 +access_spp_alert_alerts_create,Alert Create,model_spp_alert,group_alerts_create,1,1,1,0 +access_spp_alert_alerts_manager,Alert Manager,model_spp_alert,group_alerts_manager,1,1,1,1 +access_spp_alert_rule_alerts_manager,Alert Rule Manager,model_spp_alert_rule,group_alerts_manager,1,1,1,1 +access_spp_alert_alerts_officer,Alert Officer,model_spp_alert,group_alerts_officer,1,1,1,0 +access_spp_alert_alerts_viewer,Alert Viewer,model_spp_alert,group_alerts_viewer,1,0,0,0 diff --git a/spp_alerts/static/description/index.html b/spp_alerts/static/description/index.html index 78cf219d..9fe102a8 100644 --- a/spp_alerts/static/description/index.html +++ b/spp_alerts/static/description/index.html @@ -371,21 +371,25 @@

OpenSPP Alerts

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: LGPL-3 OpenSPP/OpenSPP2

Generic alert engine for threshold monitoring, expiry tracking, and -deadline management. Provides base models and state machine for alert -lifecycle tracking. Consumer modules (like spp_drims) extend these -models and implement evaluation logic to generate alerts based on -domain-specific conditions.

+deadline management. Evaluates configurable rules on a daily schedule or +on-demand and generates alerts when conditions are met. Consumer modules +(like spp_drims) extend these models to add domain-specific fields.

Key Capabilities

@@ -403,11 +407,11 @@

Key Models

spp.alert Alert instance with state tracking and -resolution workflow +resolution audit spp.alert.rule -Rule configuration for monitoring criteria and -thresholds +Rule configuration with evaluation engine and +scheduling @@ -417,10 +421,9 @@

Configuration

After installing:

  1. Navigate to Settings > Technical > Alerts > Alert Rules
  2. -
  3. Create rules specifying alert type, priority, threshold values, and -days before deadline
  4. -
  5. Consumer modules implement checking logic (e.g., cron jobs or event -handlers) to evaluate rules and create alerts
  6. +
  7. Create rules: select model, rule type (threshold/date), and +conditions
  8. +
  9. The daily cron “Alerts: Evaluate Alert Rules” is active by default
@@ -428,7 +431,10 @@

UI Location

@@ -436,22 +442,28 @@

Security

-++ - + + - + + - + + - + +
GroupAccessAlertsRules
spp_alerts.group_alerts_viewerRead alertsReadRead
spp_alerts.group_alerts_officerRead/Write/Create (no delete) alertsRead/Write/Create (no +delete)Read
spp_alerts.group_alerts_managerFull CRUD on alerts and rulesFull CRUDFull CRUD
@@ -459,14 +471,13 @@

Security

Extension Points

@@ -475,16 +486,686 @@

Dependencies

Table of contents

+
+

Usage

+
+

UI Testing Guide

+

This guide covers all user-facing functionality in the spp_alerts +module. Follow each section in order. Each test case includes the steps, +what to verify, and the expected result.

+

Prerequisites:

+
    +
  • Install the spp_alerts module
  • +
  • Log in as admin (has all permissions by default)
  • +
  • Ensure at least one model is available (e.g., res.partner — always +present)
  • +
+
+
+ +
+
+

2. Alert Rules — List View

+

TC-2.1: Empty state

+
    +
  1. Navigate to Settings > Technical > Alerts > Alert Rules
  2. +
  3. Remove any active search filters
  4. +
+

Verify (if no rules exist):

+
    +
  • ☐ Empty state shows smiley face icon
  • +
  • ☐ Message reads: “No alert rules configured”
  • +
  • ☐ Description mentions configuring rules for thresholds, expiry dates, +and deadlines
  • +
+

TC-2.2: List view columns

+
    +
  1. Create at least one alert rule (see TC-3.1)
  2. +
  3. Return to the list view
  4. +
+

Verify:

+
    +
  • ☐ Columns visible: drag handle (sequence), Name, Alert Type, Model to +Monitor, Rule Type, Priority, Threshold Value, Days Before, Active
  • +
  • ☐ Rule Type, Threshold Value, and Days Before have “optional column” +toggles
  • +
  • ☐ Rows can be reordered by dragging the handle icon
  • +
  • ☐ Sample data appears in the background when the list is empty
  • +
+

TC-2.3: Search and filters

+
    +
  1. Click the search bar
  2. +
  3. Try searching by Name, Alert Type, and Model
  4. +
+

Verify:

+
    +
  • ☐ Filters available: Active, Inactive, Critical Priority, High +Priority
  • +
  • ☐ Group By options: Alert Type, Model, Priority, Rule Type
  • +
+
+
+
+

3. Alert Rules — Form View

+

TC-3.1: Create a threshold rule

+
    +
  1. Click New on the Alert Rules list
  2. +
  3. Fill in:
      +
    • Rule Name: “Test Low Color Warning”
    • +
    • Alert Type: select “Threshold Alert” from the dropdown (cannot +type to create new)
    • +
    • Default Priority: “High”
    • +
    • Model to Monitor: select “Contact” (res.partner)
    • +
    • Rule Type: select “Threshold”
    • +
    +
  4. +
  5. Optionally add a Description in the text area below the main +fields
  6. +
  7. Observe the Evaluation tab appears after selecting Rule Type
  8. +
+

Verify:

+
    +
  • ☐ Alert Type dropdown does NOT show a “Create” option
  • +
  • ☐ After selecting Model and Rule Type, the Evaluation tab appears
  • +
  • ☐ Evaluation tab shows Threshold Settings section with: Monitored +Field, Comparison, Threshold Value
  • +
+
    +
  1. In the Evaluation tab:
      +
    • Monitored Field: select “Color Index” (or any numeric field)
    • +
    • Comparison: “Less Than (<)”
    • +
    • Threshold Value: 5
    • +
    +
  2. +
  3. Under Record Filter, observe the visual domain builder
  4. +
+

Verify:

+
    +
  • ☐ Domain builder shows fields from the selected model +(Contact/res.partner)
  • +
  • ☐ You can add filter conditions visually (e.g., “Active is set”)
  • +
+
    +
  1. Save the rule
  2. +
+

Verify:

+
    +
  • ☐ Rule saves without errors
  • +
  • ☐ Chatter (message log) appears at the bottom of the form
  • +
  • ☐ An Alerts stat button appears in the top-right showing “0 +Alerts”
  • +
+

TC-3.2: Create a date rule

+
    +
  1. Create a new rule:
      +
    • Rule Name: “Test Deadline Warning”
    • +
    • Alert Type: “Deadline Alert”
    • +
    • Model to Monitor: “Contact”
    • +
    • Rule Type: “Date / Deadline”
    • +
    +
  2. +
+

Verify:

+
    +
  • ☐ Evaluation tab shows Date Settings section (not Threshold +Settings)
  • +
  • ☐ Date Settings has: Date Field and Days Before
  • +
+
    +
  1. Fill in:
      +
    • Date Field: select “Last Updated on” (write_date) or any +date/datetime field
    • +
    • Days Before: 30
    • +
    +
  2. +
  3. Save
  4. +
+

Verify:

+
    +
  • ☐ Rule saves without errors
  • +
+

TC-3.3: Run Now button

+
    +
  1. Open the threshold rule created in TC-3.1
  2. +
  3. Click the Run Now button in the header
  4. +
+

Verify:

+
    +
  • ☐ A notification appears: “X alert(s) created by rule ‘Test Low Color +Warning’”
  • +
  • ☐ The Alerts stat button count updates to reflect created alerts
  • +
  • ☐ Click the stat button — it opens a filtered list of alerts from this +rule
  • +
+

TC-3.4: Archive and unarchive

+
    +
  1. Open any rule
  2. +
  3. Click Action > Archive
  4. +
+

Verify:

+
    +
  • ☐ A red Archived ribbon appears on the form
  • +
  • ☐ The rule disappears from the default list view (which filters active +rules)
  • +
+
    +
  1. In the list view, add the “Inactive” filter
  2. +
+

Verify:

+
    +
  • ☐ The archived rule appears
  • +
  • ☐ Open it and click Action > Unarchive — ribbon disappears
  • +
+

TC-3.5: Validation errors

+
    +
  1. Create a new rule with Rule Type = “Threshold” but leave Monitored +Field empty
  2. +
  3. Try to save
  4. +
+

Verify:

+
    +
  • ☐ Error: “A monitored field is required for threshold rules.”
  • +
+
    +
  1. Create a new rule with Rule Type = “Date / Deadline” but leave Date +Field empty
  2. +
  3. Try to save
  4. +
+

Verify:

+
    +
  • ☐ Error: “A date field is required for date rules.”
  • +
+
    +
  1. Create a rule and set Domain Filter to an invalid expression (e.g., +type INVALID in the domain builder’s code editor if available)
  2. +
  3. Try to save
  4. +
+

Verify:

+
    +
  • ☐ Error: “Invalid domain filter: …”
  • +
+

TC-3.6: Run Now without configuration

+
    +
  1. Create a rule with no Rule Type and no Model
  2. +
  3. Observe the header
  4. +
+

Verify:

+
    +
  • ☐ The Run Now button is NOT visible (it requires both Rule Type +and Model)
  • +
+
+
+
+

4. Alerts — Creating Manually

+

TC-4.1: Create a manual alert

+
    +
  1. Navigate to Settings > Technical > Alerts > Alerts
  2. +
  3. Click New
  4. +
  5. Fill in:
      +
    • Alert Type: “Manual Alert”
    • +
    • Priority: “Critical”
    • +
    • Title: “Test Manual Alert”
    • +
    +
  6. +
  7. Go to the Details tab and add a description
  8. +
  9. Save
  10. +
+

Verify:

+
    +
  • ☐ Reference auto-generated in format ALR-YYYY-NNNNN (e.g., +ALR-2026-00001)
  • +
  • ☐ State shows “Active” in the statusbar
  • +
  • Acknowledge button (blue) is visible in the header
  • +
  • Resolve button (green) is visible in the header
  • +
  • ☐ Priority and Alert Type fields are editable
  • +
  • ☐ Chatter (message log) appears at the bottom
  • +
  • ☐ No “View Source” or “Related Alerts” stat buttons (manual alert has +no source)
  • +
+

TC-4.2: Unique references

+
    +
  1. Create three alerts quickly
  2. +
+

Verify:

+
    +
  • ☐ Each alert gets a unique, sequential reference number
  • +
+
+
+
+

5. Alerts — List View

+

TC-5.1: List view appearance

+
    +
  1. Navigate to Settings > Technical > Alerts > Alerts
  2. +
  3. Ensure some alerts exist (use Run Now on a rule, or create manually)
  4. +
+

Verify:

+
    +
  • ☐ Columns visible: Reference, Title, Alert Type, Priority (badge), +State (badge), Created On
  • +
  • ☐ Critical-priority rows have a red tint
  • +
  • ☐ High-priority rows have an orange/warning tint
  • +
  • ☐ Resolved rows are muted/grayed out
  • +
  • ☐ Priority badges: Critical = red, High = orange, Medium = blue
  • +
  • ☐ State badges: Active = red, Acknowledged = orange, Resolved = green
  • +
  • ☐ Source Rule and Company columns are hidden by default (use optional +column toggle)
  • +
  • ☐ Default filter shows only Active alerts (check search bar for +“Active” filter chip)
  • +
+

TC-5.2: Search panel

+
    +
  1. Look at the left side of the list view
  2. +
+

Verify:

+
    +
  • ☐ Search panel shows three filter groups: State, Priority, Alert Type
  • +
  • ☐ Each option shows a count of matching alerts
  • +
  • ☐ Clicking a filter value narrows the list immediately
  • +
+

TC-5.3: Search filters and Group By

+
    +
  1. Click the search bar
  2. +
+

Verify:

+
    +
  • ☐ Can search by Reference, Title, Alert Type
  • +
  • ☐ Filters: Active, Acknowledged, Resolved, Critical, High Priority
  • +
  • ☐ Group By: State, Priority, Type
  • +
+
+
+
+

6. Alerts — Kanban View

+

TC-6.1: Switch to kanban

+
    +
  1. On the Alerts list, click the kanban view icon (grid icon in the view +switcher)
  2. +
+

Verify:

+
    +
  • ☐ Alerts are displayed as cards grouped into columns by State: Active, +Acknowledged, Resolved
  • +
  • ☐ Each column header shows the state name and alert count
  • +
  • ☐ A colored progress bar appears at the top of each column showing +priority distribution (gray = low, blue = medium, orange = high, red = +critical)
  • +
  • ☐ Quick create is disabled (no “+” button at top of columns)
  • +
+

TC-6.2: Kanban card content

+
    +
  1. Examine an alert card
  2. +
+

Verify:

+
    +
  • ☐ Card shows: priority stars, reference (bold), title, alert type, +creation date
  • +
  • ☐ A three-dot dropdown menu appears on hover (top-right of card)
  • +
+

TC-6.3: Kanban dropdown actions

+
    +
  1. Hover over an Active alert card and click the three-dot menu
  2. +
+

Verify:

+
    +
  • ☐ Dropdown shows: Acknowledge and Resolve
  • +
+
    +
  1. Click Acknowledge
  2. +
+

Verify:

+
    +
  • ☐ Card moves to the Acknowledged column
  • +
+
    +
  1. Hover over the now-acknowledged card, open dropdown
  2. +
+

Verify:

+
    +
  • ☐ Dropdown shows only Resolve (Acknowledge is gone)
  • +
+
    +
  1. Click Resolve
  2. +
+

Verify:

+
    +
  • ☐ A dialog or form opens requesting resolution notes (since the action +requires notes)
  • +
+
+
+
+

7. Alert State Transitions

+

TC-7.1: Active to Acknowledged

+
    +
  1. Open an Active alert
  2. +
  3. Click the Acknowledge button
  4. +
+

Verify:

+
    +
  • ☐ State changes to “Acknowledged”
  • +
  • ☐ Statusbar updates to highlight “Acknowledged”
  • +
  • ☐ Acknowledge button disappears
  • +
  • ☐ Resolve button remains visible
  • +
  • ☐ Fields (Alert Type, Priority, Title) are still editable
  • +
  • ☐ Chatter logs a state change message
  • +
+

TC-7.2: Acknowledged to Resolved

+
    +
  1. On the acknowledged alert, go to the Resolution tab
  2. +
+

Verify:

+
    +
  • ☐ An info banner reads: “Please add resolution notes describing how +this alert was addressed, then click Resolve.”
  • +
  • ☐ Resolved By and Resolved At fields are empty
  • +
+
    +
  1. Enter resolution notes in the text area
  2. +
  3. Click the Resolve button
  4. +
+

Verify:

+
    +
  • ☐ State changes to “Resolved”
  • +
  • ☐ Resolved By shows your user name
  • +
  • ☐ Resolved At shows the current timestamp
  • +
  • ☐ Resolution Notes are preserved and now read-only
  • +
  • ☐ Info banner disappears
  • +
  • ☐ Both Acknowledge and Resolve buttons disappear
  • +
  • ☐ All main fields (Alert Type, Priority, Title, Description) become +read-only
  • +
+

TC-7.3: Active to Resolved directly (skip Acknowledge)

+
    +
  1. Create a new alert and leave it in Active state
  2. +
  3. Go to the Resolution tab
  4. +
  5. Enter resolution notes
  6. +
  7. Click Resolve
  8. +
+

Verify:

+
    +
  • ☐ Alert goes directly from Active to Resolved (skipping Acknowledged)
  • +
  • ☐ All resolution fields are populated correctly
  • +
+

TC-7.4: Resolve without notes

+
    +
  1. Create a new Active alert
  2. +
  3. Click Resolve without entering resolution notes
  4. +
+

Verify:

+
    +
  • ☐ Error message: “Please provide resolution notes before resolving the +alert.”
  • +
  • ☐ Alert remains in its current state
  • +
+

TC-7.5: Double-acknowledge prevention

+
    +
  1. Acknowledge an alert
  2. +
  3. Try to acknowledge it again (via API or another browser tab)
  4. +
+

Verify:

+
    +
  • ☐ Error: “Only active alerts can be acknowledged.”
  • +
  • ☐ The Acknowledge button is not visible on the form (it only shows for +Active alerts)
  • +
+

TC-7.6: Double-resolve prevention

+
    +
  1. Resolve an alert
  2. +
  3. Try to resolve it again
  4. +
+

Verify:

+
    +
  • ☐ Error: “Alert ‘ALR-YYYY-NNNNN’ is already resolved.”
  • +
  • ☐ The Resolve button is not visible on the form
  • +
+
+
+
+

8. Alert Form — Rule-Generated Alerts

+

TC-8.1: Source tracking fields

+
    +
  1. Run a rule (via Run Now on an alert rule)
  2. +
  3. Open one of the generated alerts
  4. +
+

Verify:

+
    +
  • ☐ Right side of the form shows a Source section with: Source Rule, +Source Model, Source Record, Source Record Name
  • +
  • ☐ If the rule is a threshold rule, a Metrics section shows: +Current Value, Threshold, Days Until
  • +
  • ☐ A View Source stat button appears in the top-right
  • +
  • ☐ A Related Alerts stat button appears (if rule created multiple +alerts)
  • +
+

TC-8.2: View Source button

+
    +
  1. Click the View Source stat button
  2. +
+

Verify:

+
    +
  • ☐ Opens the source record’s form view (e.g., a Contact form)
  • +
  • ☐ The correct record is displayed
  • +
+

TC-8.3: Related Alerts button

+
    +
  1. Go back to the alert and click the Related Alerts stat button
  2. +
+

Verify:

+
    +
  • ☐ Opens a list of other alerts from the same rule (excluding the +current alert)
  • +
  • ☐ Default filter shows Active alerts
  • +
+
+
+
+

9. Keyboard Shortcuts

+

TC-9.1: Hotkeys

+
    +
  1. Open an Active alert form
  2. +
  3. Press Alt+A (or the platform equivalent for data-hotkey="a")
  4. +
+

Verify:

+
    +
  • ☐ Alert is acknowledged
  • +
+
    +
  1. Press Alt+R
  2. +
+

Verify:

+
    +
  • ☐ Resolve action is triggered (will ask for notes if none provided)
  • +
+
+
+
+

10. Cron Job

+

TC-10.1: Scheduled action exists

+
    +
  1. Navigate to Settings > Technical > Scheduled Actions
  2. +
  3. Search for “Alerts”
  4. +
+

Verify:

+
    +
  • ☐ A scheduled action named “Alerts: Evaluate Alert Rules” exists
  • +
  • ☐ It is active
  • +
  • ☐ Interval is set to 1 day
  • +
+

TC-10.2: Cron execution

+
    +
  1. Ensure at least one active alert rule with matching records exists
  2. +
  3. Click Run Manually on the scheduled action
  4. +
+

Verify:

+
    +
  • ☐ New alerts are created for matching records
  • +
  • ☐ No duplicate alerts for records that already have +active/acknowledged alerts
  • +
+
+
+
+

11. Security and Access Control

+

Test with three different users. Create them via Settings > Users & +Companies > Users and assign the appropriate group under the SPP Admin +section.

+

TC-11.1: Viewer role

+
    +
  1. Log in as a user with Alerts Viewer group only
  2. +
  3. Navigate to Settings > Technical > Alerts > Alerts
  4. +
+

Verify:

+
    +
  • ☐ Can see the Alerts menu and list
  • +
  • ☐ Can open and read alert details
  • +
  • ☐ Cannot create new alerts (New button absent or errors on save)
  • +
  • ☐ Cannot edit existing alerts
  • +
  • ☐ Cannot acknowledge or resolve alerts (buttons error on click)
  • +
  • Alert Rules submenu is NOT visible
  • +
+

TC-11.2: Officer role

+
    +
  1. Log in as a user with Alerts Officer group
  2. +
  3. Navigate to Settings > Technical > Alerts > Alerts
  4. +
+

Verify:

+
    +
  • ☐ Can create new alerts
  • +
  • ☐ Can edit alerts (change priority, title, etc.)
  • +
  • ☐ Can acknowledge and resolve alerts
  • +
  • ☐ Cannot delete alerts
  • +
  • ☐ Can see Alert Rules submenu but rules are read-only
  • +
  • ☐ Cannot create or edit alert rules
  • +
+

TC-11.3: Manager role

+
    +
  1. Log in as a user with Alerts Manager group
  2. +
+

Verify:

+
    +
  • ☐ Full access to alerts: create, read, update, delete
  • +
  • ☐ Full access to alert rules: create, read, update, delete
  • +
  • ☐ Can click Run Now on alert rules
  • +
  • ☐ Can archive/unarchive rules
  • +
+
+
+
+

12. Multi-Company (if applicable)

+

Only test this section if multi-company is enabled.

+

TC-12.1: Company isolation

+
    +
  1. Create an alert in Company A
  2. +
  3. Switch to Company B
  4. +
+

Verify:

+
    +
  • ☐ The alert from Company A is not visible in Company B
  • +
  • ☐ New alerts default to the current company
  • +
+
+
+
+

13. Alert Types (Vocabulary)

+

TC-13.1: Pre-installed types

+
    +
  1. Open any alert or rule form
  2. +
  3. Click the Alert Type dropdown
  4. +
+

Verify the following types are available:

+
    +
  • ☐ Threshold Alert
  • +
  • ☐ Expiry Alert
  • +
  • ☐ Deadline Alert
  • +
  • ☐ Manual Alert
  • +
  • ☐ System Alert
  • +
  • ☐ Cannot create new types from the dropdown (no “Create” option)
  • +
+
+
+
+

14. Edge Cases

+

TC-14.1: Empty state

+
    +
  1. Delete or resolve all alerts
  2. +
  3. Remove the “Active” default filter
  4. +
+

Verify:

+
    +
  • ☐ Empty state shows: “No active alerts” with smiley face
  • +
+

TC-14.2: Sorting

+
    +
  1. Create alerts with different priorities (low, medium, high, critical)
  2. +
  3. View the list (default sort)
  4. +
+

Verify:

+
    +
  • ☐ Alerts are sorted by priority (critical first) then by creation date +(newest first)
  • +
  • ☐ This is semantic ordering: critical > high > medium > low (not +alphabetical)
  • +
+
-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -492,15 +1173,15 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • OpenSPP.org
-

Maintainers

+

Maintainers

Current maintainers:

jeremi gonzalesedwin1123 emjay0921

This module is part of the OpenSPP/OpenSPP2 project on GitHub.

diff --git a/spp_alerts/tests/common.py b/spp_alerts/tests/common.py index cdc69cf6..b830616d 100644 --- a/spp_alerts/tests/common.py +++ b/spp_alerts/tests/common.py @@ -15,47 +15,11 @@ def setUpClass(cls): super().setUpClass() # Get vocabulary codes for alert types - cls.vocab_code = cls.env["spp.vocabulary.code"] - - cls.alert_type_threshold = cls.vocab_code.search( - [ - ("vocabulary_id.namespace_uri", "=", "urn:openspp:vocab:alerts"), - ("code", "=", "threshold"), - ], - limit=1, - ) - - cls.alert_type_expiry = cls.vocab_code.search( - [ - ("vocabulary_id.namespace_uri", "=", "urn:openspp:vocab:alerts"), - ("code", "=", "expiry"), - ], - limit=1, - ) - - cls.alert_type_deadline = cls.vocab_code.search( - [ - ("vocabulary_id.namespace_uri", "=", "urn:openspp:vocab:alerts"), - ("code", "=", "deadline"), - ], - limit=1, - ) - - cls.alert_type_manual = cls.vocab_code.search( - [ - ("vocabulary_id.namespace_uri", "=", "urn:openspp:vocab:alerts"), - ("code", "=", "manual"), - ], - limit=1, - ) - - cls.alert_type_system = cls.vocab_code.search( - [ - ("vocabulary_id.namespace_uri", "=", "urn:openspp:vocab:alerts"), - ("code", "=", "system"), - ], - limit=1, - ) + cls.alert_type_threshold = cls.env.ref("spp_alerts.code_alert_threshold") + cls.alert_type_expiry = cls.env.ref("spp_alerts.code_alert_expiry") + cls.alert_type_deadline = cls.env.ref("spp_alerts.code_alert_deadline") + cls.alert_type_manual = cls.env.ref("spp_alerts.code_alert_manual") + cls.alert_type_system = cls.env.ref("spp_alerts.code_alert_system") # Create test companies for multi-company testing cls.company_main = cls.env.company diff --git a/spp_alerts/tests/test_alert.py b/spp_alerts/tests/test_alert.py index 24b4dcba..0dcb8091 100644 --- a/spp_alerts/tests/test_alert.py +++ b/spp_alerts/tests/test_alert.py @@ -1,6 +1,6 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. from odoo import fields -from odoo.exceptions import AccessError +from odoo.exceptions import AccessError, UserError from odoo.tests import tagged from .common import AlertsTestCommon @@ -16,9 +16,6 @@ class TestAlert(AlertsTestCommon): def test_create_alert_with_auto_sequence(self): """Test that alert creation auto-generates a unique reference from sequence.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert( title="Test Auto-Sequence Alert", ) @@ -31,9 +28,6 @@ def test_create_alert_with_auto_sequence(self): def test_create_alert_all_required_fields(self): """Test alert creation with all required fields.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert( alert_type_id=self.alert_type_threshold.id, title="Complete Test Alert", @@ -52,9 +46,6 @@ def test_create_alert_all_required_fields(self): def test_unique_alert_references(self): """Test that multiple alerts get unique references.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert1 = self._create_test_alert(title="Alert 1") alert2 = self._create_test_alert(title="Alert 2") alert3 = self._create_test_alert(title="Alert 3") @@ -66,9 +57,6 @@ def test_unique_alert_references(self): def test_state_transition_active_to_acknowledged(self): """Test state transition from active to acknowledged.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert() self.assertEqual(alert.state, "active") @@ -79,9 +67,6 @@ def test_state_transition_active_to_acknowledged(self): def test_state_transition_acknowledged_to_resolved(self): """Test state transition from acknowledged to resolved.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert() alert.action_acknowledge() self.assertEqual(alert.state, "acknowledged") @@ -93,9 +78,6 @@ def test_state_transition_acknowledged_to_resolved(self): def test_state_transition_active_to_resolved_directly(self): """Test that an alert can be resolved directly from active state.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert() self.assertEqual(alert.state, "active") @@ -106,9 +88,6 @@ def test_state_transition_active_to_resolved_directly(self): def test_action_resolve_with_notes(self): """Test that action_resolve correctly records resolution details.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert() resolution_notes = "Stock was replenished to safe levels" @@ -125,21 +104,13 @@ def test_action_resolve_with_notes(self): def test_action_resolve_without_notes_raises(self): """Test that action_resolve raises UserError when no notes provided.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert() - from odoo.exceptions import UserError - with self.assertRaises(UserError): alert.action_resolve() def test_priority_levels_all_valid(self): """Test that all priority levels can be set correctly.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - priorities = ["low", "medium", "high", "critical"] for priority in priorities: @@ -150,40 +121,25 @@ def test_priority_levels_all_valid(self): self.assertEqual(alert.priority, priority) def test_alert_ordering_by_priority_and_date(self): - """Test that alerts are ordered by priority (desc) then create_date (desc).""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - - # Create alerts with different priorities + """Test that alerts are ordered by semantic priority (critical first), then date.""" alert_low = self._create_test_alert(title="Low", priority="low") alert_critical = self._create_test_alert(title="Critical", priority="critical") alert_medium = self._create_test_alert(title="Medium", priority="medium") alert_high = self._create_test_alert(title="High", priority="high") - # Search all alerts by explicit ordering (same as model's _order) alerts = self.env["spp.alert"].search( - [ - ( - "id", - "in", - [alert_low.id, alert_critical.id, alert_medium.id, alert_high.id], - ) - ], - order="priority desc, create_date desc", + [("id", "in", [alert_low.id, alert_critical.id, alert_medium.id, alert_high.id])], ) - # Verify all priorities are present + # Verify semantic ordering: critical(4) > high(3) > medium(2) > low(1) priorities = [a.priority for a in alerts] - self.assertIn("critical", priorities) - self.assertIn("high", priorities) - self.assertIn("medium", priorities) - self.assertIn("low", priorities) + self.assertEqual(priorities[0], "critical") + self.assertEqual(priorities[1], "high") + self.assertEqual(priorities[2], "medium") + self.assertEqual(priorities[3], "low") def test_multi_company_isolation(self): """Test that alerts are properly isolated by company.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - # Create alert in main company alert_main = self._create_test_alert( title="Main Company Alert", @@ -210,9 +166,6 @@ def test_multi_company_isolation(self): def test_alert_type_vocabulary_link(self): """Test that alert_type_id correctly links to vocabulary and alert_type is computed.""" - if not self.alert_type_deadline: - self.skipTest("Alert type deadline not found") - alert = self._create_test_alert( alert_type_id=self.alert_type_deadline.id, title="Deadline Alert", @@ -226,9 +179,6 @@ def test_alert_type_vocabulary_link(self): def test_alert_with_threshold_metrics(self): """Test alert with current_value and threshold_value for threshold monitoring.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert( title="Low Stock Alert", current_value=15.0, @@ -241,9 +191,6 @@ def test_alert_with_threshold_metrics(self): def test_alert_with_days_until_positive(self): """Test alert with positive days_until (future deadline).""" - if not self.alert_type_deadline: - self.skipTest("Alert type deadline not found") - alert = self._create_test_alert( alert_type_id=self.alert_type_deadline.id, title="Upcoming Deadline", @@ -255,9 +202,6 @@ def test_alert_with_days_until_positive(self): def test_alert_with_days_until_negative(self): """Test alert with negative days_until (overdue).""" - if not self.alert_type_deadline: - self.skipTest("Alert type deadline not found") - alert = self._create_test_alert( alert_type_id=self.alert_type_deadline.id, title="Overdue Task", @@ -269,9 +213,6 @@ def test_alert_with_days_until_negative(self): def test_alert_with_days_until_zero(self): """Test alert with zero days_until (due today).""" - if not self.alert_type_deadline: - self.skipTest("Alert type deadline not found") - alert = self._create_test_alert( alert_type_id=self.alert_type_deadline.id, title="Due Today", @@ -283,9 +224,6 @@ def test_alert_with_days_until_zero(self): def test_alert_default_company(self): """Test that alert gets default company from environment.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert(title="Default Company Test") # Should have the current company @@ -293,9 +231,6 @@ def test_alert_default_company(self): def test_alert_rec_name_is_reference(self): """Test that alert record name (_rec_name) is the reference field.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert(title="Test Rec Name") # display_name should equal the reference (Odoo 19+ uses display_name) @@ -303,9 +238,6 @@ def test_alert_rec_name_is_reference(self): def test_security_viewer_can_read(self): """Test that viewer can read alerts.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert(title="Security Test Alert") # Viewer should be able to read @@ -315,9 +247,6 @@ def test_security_viewer_can_read(self): def test_security_viewer_cannot_write(self): """Test that viewer cannot modify alerts.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert(title="Security Test Alert") # Viewer should not be able to write @@ -327,9 +256,6 @@ def test_security_viewer_cannot_write(self): def test_security_viewer_cannot_acknowledge(self): """Test that viewer cannot acknowledge alerts.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert(title="Security Test Alert") # Viewer should not be able to acknowledge @@ -339,9 +265,6 @@ def test_security_viewer_cannot_acknowledge(self): def test_security_officer_can_create(self): """Test that officer can create alerts.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - # Officer should be able to create alert = ( self.env["spp.alert"] @@ -360,9 +283,6 @@ def test_security_officer_can_create(self): def test_security_officer_can_acknowledge_and_resolve(self): """Test that officer can acknowledge and resolve alerts.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert(title="Officer Test Alert") # Officer should be able to acknowledge and resolve @@ -384,9 +304,6 @@ def test_alert_with_all_type_codes(self): ] for alert_type_rec, expected_code in alert_types: - if not alert_type_rec: - continue - alert = self._create_test_alert( alert_type_id=alert_type_rec.id, title=f"Test {expected_code} alert", @@ -397,9 +314,6 @@ def test_alert_with_all_type_codes(self): def test_alert_mail_tracking(self): """Test that alert has mail tracking capabilities.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert( title="Tracking Test", priority="low", @@ -415,9 +329,6 @@ def test_alert_mail_tracking(self): def test_multiple_state_transitions(self): """Test multiple state transitions on the same alert.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert() # Start in active state @@ -438,9 +349,6 @@ def test_multiple_state_transitions(self): def test_alert_empty_description(self): """Test that alert can be created without description.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert( title="Alert without description", description=False, @@ -451,9 +359,6 @@ def test_alert_empty_description(self): def test_alert_zero_threshold_values(self): """Test alert with zero values for thresholds.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - alert = self._create_test_alert( title="Zero Threshold Test", current_value=0.0, @@ -462,3 +367,127 @@ def test_alert_zero_threshold_values(self): self.assertEqual(alert.current_value, 0.0) self.assertEqual(alert.threshold_value, 0.0) + + def test_action_acknowledge_non_active_raises(self): + """Test that acknowledging a non-active alert raises UserError.""" + alert = self._create_test_alert() + alert.action_acknowledge() + self.assertEqual(alert.state, "acknowledged") + + # Trying to acknowledge again should fail + with self.assertRaises(UserError): + alert.action_acknowledge() + + def test_action_resolve_already_resolved_raises(self): + """Test that resolving an already-resolved alert raises UserError.""" + alert = self._create_test_alert() + alert.action_resolve(notes="Resolved") + self.assertEqual(alert.state, "resolved") + + with self.assertRaises(UserError): + alert.action_resolve(notes="Resolving again") + + def test_resolve_directly_from_active(self): + """Test resolving an alert directly from active state (skipping acknowledge).""" + alert = self._create_test_alert() + self.assertEqual(alert.state, "active") + + alert.action_resolve(notes="Direct resolution") + self.assertEqual(alert.state, "resolved") + self.assertEqual(alert.resolution_notes, "Direct resolution") + self.assertTrue(alert.resolved_by_id) + self.assertTrue(alert.resolved_at) + + def test_priority_sequence_values(self): + """Test that priority_sequence is correctly computed for all priority levels.""" + for priority, expected_seq in [("low", 1), ("medium", 2), ("high", 3), ("critical", 4)]: + alert = self._create_test_alert(title=f"Test {priority}", priority=priority) + self.assertEqual(alert.priority_sequence, expected_seq) + + def test_res_name_computed(self): + """Test that res_name is computed from source record.""" + partner = self.env["res.partner"].create({"name": "Test Partner"}) + alert = self._create_test_alert( + res_model="res.partner", + res_id=partner.id, + ) + self.assertEqual(alert.res_name, "Test Partner") + + def test_res_name_empty_when_no_source(self): + """Test that res_name is empty when no source record is set.""" + alert = self._create_test_alert() + self.assertEqual(alert.res_name, "") + + def test_action_view_source(self): + """Test that action_view_source returns correct action dict.""" + partner = self.env["res.partner"].create({"name": "Source Partner"}) + alert = self._create_test_alert( + res_model="res.partner", + res_id=partner.id, + ) + action = alert.action_view_source() + self.assertEqual(action["type"], "ir.actions.act_window") + self.assertEqual(action["res_model"], "res.partner") + self.assertEqual(action["res_id"], partner.id) + + def test_action_view_source_no_source(self): + """Test that action_view_source returns False when no source record.""" + alert = self._create_test_alert() + result = alert.action_view_source() + self.assertFalse(result) + + def test_action_view_related_alerts(self): + """Test that action_view_related_alerts returns correct action dict.""" + rule = self.env["spp.alert.rule"].create( + { + "name": "Test Rule", + "alert_type_id": self.alert_type_threshold.id, + } + ) + alert1 = self._create_test_alert(rule_id=rule.id, title="Alert 1") + self._create_test_alert(rule_id=rule.id, title="Alert 2") + + action = alert1.action_view_related_alerts() + self.assertEqual(action["type"], "ir.actions.act_window") + self.assertEqual(action["res_model"], "spp.alert") + self.assertIn(("rule_id", "=", rule.id), action["domain"]) + self.assertIn(("id", "!=", alert1.id), action["domain"]) + + def test_action_view_related_alerts_no_rule(self): + """Test that action_view_related_alerts returns False when no rule.""" + alert = self._create_test_alert() + result = alert.action_view_related_alerts() + self.assertFalse(result) + + def test_res_name_deleted_record(self): + """Test that res_name is empty when source record has been deleted.""" + partner = self.env["res.partner"].create({"name": "To Delete"}) + alert = self._create_test_alert( + res_model="res.partner", + res_id=partner.id, + ) + self.assertEqual(alert.res_name, "To Delete") + + # Delete the source record + partner.unlink() + alert.invalidate_recordset(["res_name"]) + self.assertEqual(alert.res_name, "") + + def test_res_name_invalid_model(self): + """Test that res_name handles nonexistent model gracefully.""" + alert = self._create_test_alert( + res_model="nonexistent.model", + res_id=1, + ) + self.assertEqual(alert.res_name, "") + + def test_resolve_with_preset_resolution_notes(self): + """Test resolving when resolution_notes are already set on the record.""" + alert = self._create_test_alert() + # Pre-set resolution_notes on the record + alert.write({"resolution_notes": "Pre-filled notes"}) + + # Resolve without passing notes argument — should use the record's notes + alert.action_resolve() + self.assertEqual(alert.state, "resolved") + self.assertEqual(alert.resolution_notes, "Pre-filled notes") diff --git a/spp_alerts/tests/test_alert_rule.py b/spp_alerts/tests/test_alert_rule.py index 7f2d017e..2414b9f9 100644 --- a/spp_alerts/tests/test_alert_rule.py +++ b/spp_alerts/tests/test_alert_rule.py @@ -15,9 +15,6 @@ class TestAlertRule(AlertsTestCommon): def test_create_alert_rule_basic(self): """Test basic alert rule creation with required fields.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_test_alert_rule( name="Test Low Stock Rule", alert_type_id=self.alert_type_threshold.id, @@ -31,9 +28,6 @@ def test_create_alert_rule_basic(self): def test_create_alert_rule_with_all_fields(self): """Test alert rule creation with all optional fields.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_test_alert_rule( name="Complete Alert Rule", alert_type_id=self.alert_type_threshold.id, @@ -59,9 +53,6 @@ def test_create_alert_rule_with_all_fields(self): def test_rule_default_values(self): """Test that alert rule gets correct default values.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self.env["spp.alert.rule"].create( { "name": "Minimal Rule", @@ -82,9 +73,6 @@ def test_rule_default_values(self): def test_rule_activation_deactivation(self): """Test activating and deactivating alert rules.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_test_alert_rule( name="Test Activation Rule", active=True, @@ -102,9 +90,6 @@ def test_rule_activation_deactivation(self): def test_rule_model_linking(self): """Test linking alert rule to specific Odoo model.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - # Use res.partner model which is always available partner_model = self.env["ir.model"].search([("model", "=", "res.partner")], limit=1) @@ -118,9 +103,6 @@ def test_rule_model_linking(self): def test_rule_without_model(self): """Test that alert rule can be created without model_id.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_test_alert_rule( name="Generic Rule", model_id=False, @@ -130,9 +112,6 @@ def test_rule_without_model(self): def test_rule_priority_levels(self): """Test creating rules with different priority levels.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - priorities = ["low", "medium", "high", "critical"] for priority in priorities: @@ -144,9 +123,6 @@ def test_rule_priority_levels(self): def test_rule_sequence_ordering(self): """Test that rules are ordered by sequence and name.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule_high_seq = self._create_test_alert_rule( name="High Sequence", sequence=100, @@ -169,9 +145,6 @@ def test_rule_sequence_ordering(self): def test_rule_threshold_value_configuration(self): """Test configuring threshold_value for different alert types.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - # Threshold rule for low stock (e.g., minimum stock level) rule = self._create_test_alert_rule( name="Low Stock Threshold", @@ -183,9 +156,6 @@ def test_rule_threshold_value_configuration(self): def test_rule_threshold_value_zero(self): """Test that threshold_value can be set to zero.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_test_alert_rule( name="Zero Threshold", threshold_value=0.0, @@ -195,9 +165,6 @@ def test_rule_threshold_value_zero(self): def test_rule_threshold_value_negative(self): """Test that threshold_value can be negative if needed.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_test_alert_rule( name="Negative Threshold", threshold_value=-10.0, @@ -207,9 +174,6 @@ def test_rule_threshold_value_negative(self): def test_rule_days_before_positive(self): """Test days_before for advance warning (positive values).""" - if not self.alert_type_expiry: - self.skipTest("Alert type expiry not found") - rule = self._create_test_alert_rule( name="Expiry Warning 30 Days", alert_type_id=self.alert_type_expiry.id, @@ -221,9 +185,6 @@ def test_rule_days_before_positive(self): def test_rule_days_before_zero(self): """Test days_before set to zero (alert at exact deadline).""" - if not self.alert_type_deadline: - self.skipTest("Alert type deadline not found") - rule = self._create_test_alert_rule( name="Deadline Today", alert_type_id=self.alert_type_deadline.id, @@ -235,9 +196,6 @@ def test_rule_days_before_zero(self): def test_rule_multi_company_main(self): """Test rule specific to main company.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_test_alert_rule( name="Main Company Rule", company_id=self.company_main.id, @@ -247,9 +205,6 @@ def test_rule_multi_company_main(self): def test_rule_multi_company_secondary(self): """Test rule specific to secondary company.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_test_alert_rule( name="Secondary Company Rule", company_id=self.company_secondary.id, @@ -280,9 +235,6 @@ def test_rule_all_alert_types(self): def test_rule_search_active_only(self): """Test searching for only active rules.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - active_rule = self._create_test_alert_rule( name="Active Rule", active=True, @@ -305,9 +257,6 @@ def test_rule_search_active_only(self): def test_rule_search_inactive(self): """Test searching for inactive rules (including in search).""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - active_rule = self._create_test_alert_rule( name="Active Rule 2", active=True, @@ -333,9 +282,6 @@ def test_rule_search_inactive(self): def test_security_viewer_can_read_rules(self): """Test that viewer can read alert rules.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_test_alert_rule(name="Security Test Rule") # Viewer should be able to read rules @@ -345,9 +291,6 @@ def test_security_viewer_can_read_rules(self): def test_security_officer_cannot_modify_rules(self): """Test that officer cannot modify alert rules.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_test_alert_rule(name="Officer Test Rule") # Officer should not be able to modify rules @@ -357,9 +300,6 @@ def test_security_officer_cannot_modify_rules(self): def test_security_officer_cannot_create_rules(self): """Test that officer cannot create alert rules.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - # Officer should not be able to create rules with self.assertRaises(AccessError): self.env["spp.alert.rule"].with_user(self.user_officer).create( @@ -371,9 +311,6 @@ def test_security_officer_cannot_create_rules(self): def test_security_manager_can_create_rules(self): """Test that manager can create alert rules.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - # Manager should be able to create rules rule = ( self.env["spp.alert.rule"] @@ -392,9 +329,6 @@ def test_security_manager_can_create_rules(self): def test_security_manager_can_modify_rules(self): """Test that manager can modify alert rules.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_test_alert_rule(name="Manager Test Rule") # Manager should be able to modify rules @@ -411,9 +345,6 @@ def test_security_manager_can_modify_rules(self): def test_security_manager_can_deactivate_rules(self): """Test that manager can deactivate alert rules.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_test_alert_rule( name="Deactivation Test", active=True, @@ -427,9 +358,6 @@ def test_security_manager_can_deactivate_rules(self): def test_rule_description_optional(self): """Test that description field is optional.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_test_alert_rule( name="No Description Rule", description=False, @@ -440,9 +368,6 @@ def test_rule_description_optional(self): def test_rule_with_long_description(self): """Test rule with detailed description.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - long_description = """This is a detailed alert rule that monitors stock levels. When stock falls below the configured threshold, an alert will be created. The alert will include information about: @@ -461,9 +386,6 @@ def test_rule_with_long_description(self): def test_multiple_rules_same_type(self): """Test creating multiple rules for the same alert type.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule1 = self._create_test_alert_rule( name="Low Stock Rule 50", alert_type_id=self.alert_type_threshold.id, @@ -484,3 +406,95 @@ def test_multiple_rules_same_type(self): self.assertEqual(rule1.threshold_value, 50.0) self.assertEqual(rule2.threshold_value, 100.0) self.assertEqual(rule3.threshold_value, 25.0) + + def test_domain_filter_validation_valid(self): + """Test that a valid domain filter passes validation.""" + rule = self._create_test_alert_rule( + name="Valid Domain Rule", + model_id=self.test_model.id, + domain_filter='[("active", "=", True)]', + ) + # Should not raise + self.assertEqual(rule.domain_filter, '[("active", "=", True)]') + + def test_domain_filter_validation_invalid(self): + """Test that an invalid domain filter raises ValidationError on save.""" + from odoo.exceptions import ValidationError + + with self.assertRaises(ValidationError): + self._create_test_alert_rule( + name="Invalid Domain Rule", + model_id=self.test_model.id, + domain_filter="NOT A VALID DOMAIN", + ) + + def test_domain_filter_validation_non_list(self): + """Test that a domain filter that evaluates to non-list raises ValidationError.""" + from odoo.exceptions import ValidationError + + with self.assertRaises(ValidationError): + self._create_test_alert_rule( + name="Non-List Domain Rule", + model_id=self.test_model.id, + domain_filter='"not a list"', + ) + + def test_alert_count_computed(self): + """Test that alert_count is correctly computed.""" + rule = self._create_test_alert_rule(name="Count Test Rule") + self.assertEqual(rule.alert_count, 0) + + # Create alerts linked to this rule + self.env["spp.alert"].create( + { + "alert_type_id": self.alert_type_threshold.id, + "title": "Alert 1", + "rule_id": rule.id, + } + ) + self.env["spp.alert"].create( + { + "alert_type_id": self.alert_type_threshold.id, + "title": "Alert 2", + "rule_id": rule.id, + } + ) + + rule.invalidate_recordset(["alert_count"]) + self.assertEqual(rule.alert_count, 2) + + def test_action_view_alerts(self): + """Test that action_view_alerts returns correct action dict.""" + rule = self._create_test_alert_rule(name="View Alerts Test") + action = rule.action_view_alerts() + + self.assertEqual(action["type"], "ir.actions.act_window") + self.assertEqual(action["res_model"], "spp.alert") + self.assertIn(("rule_id", "=", rule.id), action["domain"]) + + def test_model_name_related_field(self): + """Test that model_name is computed from model_id.""" + rule = self._create_test_alert_rule( + name="Model Name Test", + model_id=self.test_model.id, + ) + self.assertEqual(rule.model_name, "res.partner") + + def test_action_evaluate_surfaces_errors(self): + """Test that action_evaluate returns a notification on success.""" + partner_model = self.env["ir.model"].search([("model", "=", "res.partner")], limit=1) + field_color = self.env["ir.model.fields"].search( + [("model_id", "=", partner_model.id), ("name", "=", "color")], limit=1 + ) + rule = self.env["spp.alert.rule"].create( + { + "name": "Error Surface Test", + "alert_type_id": self.alert_type_threshold.id, + "model_id": partner_model.id, + "rule_type": "threshold", + "monitored_field_id": field_color.id, + "domain_filter": "[]", + } + ) + result = rule.action_evaluate() + self.assertEqual(result["type"], "ir.actions.client") diff --git a/spp_alerts/tests/test_rule_evaluation.py b/spp_alerts/tests/test_rule_evaluation.py index 3d3e3734..3245bdb3 100644 --- a/spp_alerts/tests/test_rule_evaluation.py +++ b/spp_alerts/tests/test_rule_evaluation.py @@ -97,9 +97,6 @@ def _create_date_rule(self, **kwargs): def test_threshold_rule_creates_alerts(self): """Test that threshold rule creates alerts for matching records.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_threshold_rule(threshold_value=8.0, comparison="lt") count = rule._evaluate_rule() @@ -111,9 +108,6 @@ def test_threshold_rule_creates_alerts(self): def test_threshold_rule_gt_comparison(self): """Test greater-than comparison.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_threshold_rule(threshold_value=8.0, comparison="gt") count = rule._evaluate_rule() @@ -122,9 +116,6 @@ def test_threshold_rule_gt_comparison(self): def test_threshold_rule_eq_comparison(self): """Test equal comparison.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_threshold_rule(threshold_value=5.0, comparison="eq") count = rule._evaluate_rule() @@ -133,9 +124,6 @@ def test_threshold_rule_eq_comparison(self): def test_threshold_rule_lte_comparison(self): """Test less-than-or-equal comparison.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_threshold_rule(threshold_value=5.0, comparison="lte") count = rule._evaluate_rule() @@ -144,9 +132,6 @@ def test_threshold_rule_lte_comparison(self): def test_threshold_rule_gte_comparison(self): """Test greater-than-or-equal comparison.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_threshold_rule(threshold_value=5.0, comparison="gte") count = rule._evaluate_rule() @@ -155,9 +140,6 @@ def test_threshold_rule_gte_comparison(self): def test_threshold_alert_values(self): """Test that created alerts have correct field values.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_threshold_rule(threshold_value=2.0, comparison="lt") rule._evaluate_rule() @@ -180,9 +162,6 @@ def test_date_rule_creates_alerts(self): write_date is today (just created), so days_until=0 which is <= any positive days_before. """ - if not self.alert_type_deadline or not self.field_write_date: - self.skipTest("Required alert type or field not found") - # All 3 partners were just created, so write_date is today. # days_until = 0, which is <= 14 rule = self._create_date_rule(days_before=14) @@ -192,9 +171,6 @@ def test_date_rule_creates_alerts(self): def test_date_rule_negative_window(self): """Test date rule with negative days_before only catches past dates.""" - if not self.alert_type_deadline or not self.field_write_date: - self.skipTest("Required alert type or field not found") - # write_date is today (days_until=0), -1 means only overdue (negative days_until) rule = self._create_date_rule(days_before=-1) count = rule._evaluate_rule() @@ -208,9 +184,6 @@ def test_date_rule_negative_window(self): def test_duplicate_prevention(self): """Test that running evaluation twice does not create duplicates.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_threshold_rule(threshold_value=8.0, comparison="lt") count1 = rule._evaluate_rule() @@ -224,9 +197,6 @@ def test_duplicate_prevention(self): def test_duplicate_allows_after_resolve(self): """Test that resolved alerts allow new alerts for same record.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_threshold_rule(threshold_value=2.0, comparison="lt") # First run creates 1 alert (partner_low, color=1) @@ -251,9 +221,6 @@ def test_duplicate_allows_after_resolve(self): def test_cron_evaluate_rules(self): """Test that cron evaluates only active, configured rules.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - # Active rule with type — should be evaluated active_rule = self._create_threshold_rule(name="Active Rule", threshold_value=2.0) @@ -278,14 +245,15 @@ def test_cron_evaluate_rules(self): def test_cron_continues_on_error(self): """Test that cron continues if one rule fails.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - - # Rule with invalid domain — should fail gracefully - self._create_threshold_rule( + # Create rule with valid domain, then corrupt it via SQL to bypass constraint + bad_rule = self._create_threshold_rule( name="Bad Domain Rule", - domain_filter="INVALID DOMAIN", ) + self.env.cr.execute( + "UPDATE spp_alert_rule SET domain_filter = %s WHERE id = %s", + ("INVALID DOMAIN", bad_rule.id), + ) + bad_rule.invalidate_recordset(["domain_filter"]) # Valid rule — should still succeed good_rule = self._create_threshold_rule( @@ -304,9 +272,6 @@ def test_cron_continues_on_error(self): def test_validation_threshold_without_field(self): """Test that threshold rule without monitored field raises error.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - with self.assertRaises(ValidationError): self.env["spp.alert.rule"].create( { @@ -320,9 +285,6 @@ def test_validation_threshold_without_field(self): def test_validation_date_without_field(self): """Test that date rule without date field raises error.""" - if not self.alert_type_deadline: - self.skipTest("Alert type deadline not found") - with self.assertRaises(ValidationError): self.env["spp.alert.rule"].create( { @@ -336,9 +298,6 @@ def test_validation_date_without_field(self): def test_validation_rule_type_without_model(self): """Test that rule type without model raises error.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - with self.assertRaises(ValidationError): self.env["spp.alert.rule"].create( { @@ -356,9 +315,6 @@ def test_validation_rule_type_without_model(self): def test_action_evaluate_returns_notification(self): """Test that action_evaluate returns a notification action.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_threshold_rule(threshold_value=2.0) result = rule.action_evaluate() @@ -368,9 +324,6 @@ def test_action_evaluate_returns_notification(self): def test_action_evaluate_no_type_raises(self): """Test that action_evaluate raises UserError when no rule_type.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self.env["spp.alert.rule"].create( { "name": "No Type Rule", @@ -382,11 +335,28 @@ def test_action_evaluate_no_type_raises(self): with self.assertRaises(UserError): rule.action_evaluate() + def test_action_evaluate_no_model_raises(self): + """Test that action_evaluate raises UserError when model_id is not set.""" + # Create rule without rule_type (bypasses constraint), then set rule_type via SQL + rule = self.env["spp.alert.rule"].create( + { + "name": "No Model Rule", + "alert_type_id": self.alert_type_threshold.id, + "priority": "medium", + } + ) + # Set rule_type without model_id via SQL to bypass constraint + self.env.cr.execute( + "UPDATE spp_alert_rule SET rule_type = %s WHERE id = %s", + ("threshold", rule.id), + ) + rule.invalidate_recordset(["rule_type"]) + + with self.assertRaises(UserError): + rule.action_evaluate() + def test_rule_without_type_returns_zero(self): """Test that _evaluate_rule returns 0 when no rule_type.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self.env["spp.alert.rule"].create( { "name": "No Type Rule", @@ -400,19 +370,19 @@ def test_rule_without_type_returns_zero(self): def test_invalid_domain_returns_zero(self): """Test that invalid domain logs error and returns 0.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") + # Create rule with valid domain, then corrupt it via SQL to bypass constraint + rule = self._create_threshold_rule() + self.env.cr.execute( + "UPDATE spp_alert_rule SET domain_filter = %s WHERE id = %s", + ("NOT A VALID DOMAIN", rule.id), + ) + rule.invalidate_recordset(["domain_filter"]) - rule = self._create_threshold_rule(domain_filter="NOT A VALID DOMAIN") count = rule._evaluate_rule() - self.assertEqual(count, 0) def test_no_matching_records_returns_zero(self): """Test that rule returns 0 when no records match domain.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_threshold_rule( domain_filter='[("id", "=", -1)]', threshold_value=100.0, @@ -423,9 +393,6 @@ def test_no_matching_records_returns_zero(self): def test_rule_propagates_company(self): """Test that company_id from rule is propagated to alerts.""" - if not self.alert_type_threshold: - self.skipTest("Alert type threshold not found") - rule = self._create_threshold_rule( threshold_value=2.0, company_id=self.company_main.id, @@ -434,3 +401,92 @@ def test_rule_propagates_company(self): alert = self.env["spp.alert"].search([("rule_id", "=", rule.id)]) self.assertEqual(alert.company_id, self.company_main) + + def test_company_propagation_no_company_on_rule(self): + """Test that when rule has no company_id, alert gets default company from environment.""" + rule = self._create_threshold_rule( + threshold_value=2.0, + company_id=False, + ) + rule._evaluate_rule() + + alert = self.env["spp.alert"].search([("rule_id", "=", rule.id)]) + self.assertEqual(len(alert), 1) + # When rule has no company, the alert should get default company from environment + self.assertEqual(alert.company_id, self.env.company) + + def test_get_existing_alert_keys_empty_ids(self): + """Test _get_existing_alert_keys with empty record_ids returns empty set.""" + rule = self._create_threshold_rule() + result = rule._get_existing_alert_keys("res.partner", []) + self.assertEqual(result, set()) + + def test_prepare_alert_vals_threshold_includes_threshold_value(self): + """Test that threshold alerts include threshold_value in vals.""" + rule = self._create_threshold_rule(threshold_value=5.0) + vals = rule._prepare_alert_vals(self.partner_low, "res.partner", current_value=1.0) + + self.assertEqual(vals["current_value"], 1.0) + self.assertEqual(vals["threshold_value"], 5.0) + + def test_prepare_alert_vals_date_excludes_threshold_value(self): + """Test that date alerts do NOT include threshold_value in vals.""" + rule = self._create_date_rule() + vals = rule._prepare_alert_vals(self.partner_low, "res.partner", days_until=5) + + self.assertNotIn("threshold_value", vals) + self.assertEqual(vals["days_until"], 5) + + def test_res_name_on_rule_generated_alert(self): + """Test that alerts created by rule evaluation have correct res_name.""" + rule = self._create_threshold_rule(threshold_value=2.0, comparison="lt") + rule._evaluate_rule() + + alert = self.env["spp.alert"].search([("rule_id", "=", rule.id)]) + self.assertEqual(len(alert), 1) + # res_name should be the display_name of the partner + self.assertEqual(alert.res_name, self.partner_low.display_name) + + def test_evaluate_rule_model_not_found(self): + """Test that _evaluate_rule returns 0 when model no longer exists in registry.""" + rule = self._create_threshold_rule() + # Corrupt model_name via SQL to simulate a model that was uninstalled + self.env.cr.execute( + "UPDATE ir_model SET model = %s WHERE id = %s", + ("nonexistent.model", self.partner_model.id), + ) + rule.invalidate_recordset() + self.partner_model.invalidate_recordset() + + count = rule._evaluate_rule() + self.assertEqual(count, 0) + + # Restore the model name for other tests + self.env.cr.execute( + "UPDATE ir_model SET model = %s WHERE id = %s", + ("res.partner", self.partner_model.id), + ) + self.partner_model.invalidate_recordset() + + def test_date_rule_skips_empty_date_field(self): + """Test that date rule skips records where the date field is empty/False.""" + # Get the date field on res.partner — use 'date' field + field_date = self.env["ir.model.fields"].search( + [("model_id", "=", self.partner_model.id), ("name", "=", "date")], + limit=1, + ) + if not field_date: + # 'date' field may not exist on res.partner in all configurations + return + + # Create a partner with no date value + partner_no_date = self.env["res.partner"].create({"name": "No Date Partner", "date": False}) + + rule = self._create_date_rule( + date_field_id=field_date.id, + days_before=30, + domain_filter=f'[("id", "=", {partner_no_date.id})]', + ) + count = rule._evaluate_rule() + # Should skip the record because date is False + self.assertEqual(count, 0) diff --git a/spp_alerts/views/alert_rule_views.xml b/spp_alerts/views/alert_rule_views.xml index e88719c5..90e9f614 100644 --- a/spp_alerts/views/alert_rule_views.xml +++ b/spp_alerts/views/alert_rule_views.xml @@ -5,7 +5,7 @@ spp.alert.rule.list spp.alert.rule - + @@ -37,28 +37,39 @@ string="Run Now" class="btn-primary" invisible="not rule_type or not model_id" - groups="group_alerts_manager,base.group_system" + groups="spp_alerts.group_alerts_manager,base.group_system" />
- +
- - + + + - + + + + - - - - + + + - - + + - + + + + + - - - -
+
@@ -145,27 +162,28 @@ string="High Priority" domain="[('priority', '=', 'high')]" /> - - - - - + + + + + +
diff --git a/spp_alerts/views/alert_views.xml b/spp_alerts/views/alert_views.xml index 75e53cbc..5f646213 100644 --- a/spp_alerts/views/alert_views.xml +++ b/spp_alerts/views/alert_views.xml @@ -7,8 +7,10 @@ @@ -28,7 +30,7 @@ decoration-success="state == 'resolved'" /> - + + +

-

- -

- - + + + - - - - - - - + + + + + + + + + + + + - + - + + @@ -136,7 +166,6 @@ @@ -206,35 +235,89 @@ domain="[('priority', '=', 'high')]" /> - - - + + + + + + + + spp.alert.kanban + spp.alert + + + + + + + + + + + + + + + Acknowledge + + + Resolve + + + +
+ +
+ +
+ +
+
+ +
+
+
+
+
+
+ Alerts spp.alert - list,form + list,kanban,form alerts {'search_default_filter_active': 1}

- No alerts found + No active alerts

Alerts are automatically created when monitoring rules detect diff --git a/spp_alerts/views/menus.xml b/spp_alerts/views/menus.xml index 2ddafddb..3043d8af 100644 --- a/spp_alerts/views/menus.xml +++ b/spp_alerts/views/menus.xml @@ -8,7 +8,7 @@ name="Alerts" parent="base.menu_custom" sequence="100" - groups="group_alerts_viewer" + groups="spp_alerts.group_alerts_viewer" /> @@ -18,7 +18,7 @@ parent="menu_spp_alerts_root" action="action_spp_alert" sequence="10" - groups="group_alerts_viewer" + groups="spp_alerts.group_alerts_viewer" /> @@ -28,6 +28,6 @@ parent="menu_spp_alerts_root" action="action_spp_alert_rule" sequence="20" - groups="group_alerts_manager" + groups="spp_alerts.group_alerts_manager" />