Skip to content

Commit ae10935

Browse files
dnplkndllclaude
andcommitted
[IMP] spreadsheet_oca: add KPI alerts with cron-based cell monitoring
Add spreadsheet.alert model for threshold-based KPI monitoring. Users configure cell reference, operator, and threshold; a scheduled cron evaluates cells and posts chatter notifications on trigger. Supports edge (fire-once) and level (fire-every-time) trigger modes. Includes QWeb email template, security rules, demo data, and tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e63e44d commit ae10935

13 files changed

Lines changed: 1050 additions & 0 deletions

spreadsheet_oca/__manifest__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
"security/security.xml",
1515
"security/ir.model.access.csv",
1616
"views/spreadsheet_spreadsheet.xml",
17+
"views/spreadsheet_alert_views.xml",
18+
"data/mail_templates.xml",
1719
"data/spreadsheet_spreadsheet_import_mode.xml",
20+
"data/spreadsheet_alert_cron.xml",
1821
"wizards/spreadsheet_select_row_number.xml",
1922
"wizards/spreadsheet_spreadsheet_import.xml",
2023
],
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<!-- Copyright 2025 Ledo Enterprises LLC
3+
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
4+
<odoo>
5+
<!-- ══ Alert notification (posted to Chatter) ═══════════════════════
6+
Rendered by spreadsheet.alert._fire_notification().
7+
Variables: alert, value_str, op_label, threshold_str, base_url
8+
9+
Customise via Settings > Technical > Views, search for
10+
"spreadsheet.alert.notification". -->
11+
<record model="ir.ui.view" id="spreadsheet_alert_notification_template">
12+
<field name="name">spreadsheet.alert.notification</field>
13+
<field name="type">qweb</field>
14+
<field name="arch" type="xml">
15+
<t t-name="spreadsheet_oca.spreadsheet_alert_notification_template">
16+
<div
17+
style="font-family:Arial,Helvetica,sans-serif;font-size:14px;color:#333;max-width:600px"
18+
>
19+
<!-- Header bar -->
20+
<div
21+
style="background:#c0392b;color:#fff;padding:10px 16px;border-radius:4px 4px 0 0;font-size:13px"
22+
>
23+
&#9888; KPI Alert Triggered
24+
</div>
25+
<!-- Body -->
26+
<div
27+
style="border:1px solid #e0e0e0;border-top:none;padding:16px;border-radius:0 0 4px 4px"
28+
>
29+
<p
30+
style="margin:0 0 10px 0;font-size:15px;font-weight:bold;color:#222"
31+
>
32+
<t t-out="alert.name" />
33+
</p>
34+
<table
35+
style="font-size:13px;color:#555;line-height:1.6;border-collapse:collapse"
36+
cellpadding="0"
37+
cellspacing="0"
38+
>
39+
<tr>
40+
<td
41+
style="padding:2px 12px 2px 0;color:#888;white-space:nowrap"
42+
>Cell</td>
43+
<td style="padding:2px 0">
44+
<b>
45+
<t t-out="alert.cell_ref" />
46+
</b>
47+
<t t-if="alert.sheet_name">
48+
on sheet <i>
49+
<t t-out="alert.sheet_name" />
50+
</i>
51+
</t>
52+
</td>
53+
</tr>
54+
<tr>
55+
<td
56+
style="padding:2px 12px 2px 0;color:#888;white-space:nowrap"
57+
>Value</td>
58+
<td style="padding:2px 0">
59+
<b>
60+
<t t-out="value_str" />
61+
</b>
62+
</td>
63+
</tr>
64+
<tr>
65+
<td
66+
style="padding:2px 12px 2px 0;color:#888;white-space:nowrap"
67+
>Condition</td>
68+
<td style="padding:2px 0">
69+
<t t-out="op_label" />
70+
&#160;
71+
<t t-out="threshold_str" />
72+
</td>
73+
</tr>
74+
</table>
75+
<!-- Action links -->
76+
<div
77+
style="margin-top:14px;padding-top:12px;border-top:1px solid #eee;font-size:12px"
78+
>
79+
<t
80+
t-set="spreadsheet_url"
81+
t-value="'%s/web#id=%d&amp;model=spreadsheet.spreadsheet&amp;view_type=form' % (base_url, alert.spreadsheet_id.id)"
82+
/>
83+
<t
84+
t-set="alert_url"
85+
t-value="'%s/web#id=%d&amp;model=spreadsheet.alert&amp;view_type=form' % (base_url, alert.id)"
86+
/>
87+
<a
88+
t-att-href="spreadsheet_url"
89+
style="color:#2980b9;text-decoration:none;margin-right:16px"
90+
>
91+
&#128196; Open Spreadsheet
92+
</a>
93+
<a
94+
t-att-href="alert_url"
95+
style="color:#2980b9;text-decoration:none"
96+
>
97+
&#9881; Alert Settings
98+
</a>
99+
</div>
100+
</div>
101+
</div>
102+
</t>
103+
</field>
104+
</record>
105+
</odoo>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<!-- Copyright 2025 Ledo Enterprises LLC
3+
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
4+
<odoo noupdate="1">
5+
<record model="ir.cron" id="spreadsheet_alert_cron">
6+
<field name="name">Spreadsheet KPI Alert Evaluation</field>
7+
<field name="model_id" ref="model_spreadsheet_alert" />
8+
<field name="state">code</field>
9+
<field name="code">model._cron_evaluate_all()</field>
10+
<field name="interval_number">1</field>
11+
<field name="interval_type">hours</field>
12+
<field name="active">True</field>
13+
</record>
14+
</odoo>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{
2+
"version": 21,
3+
"sheets": [
4+
{
5+
"id": "dashboard",
6+
"name": "Dashboard",
7+
"colNumber": 26,
8+
"rowNumber": 100,
9+
"rows": {},
10+
"cols": {},
11+
"merges": [],
12+
"cells": {
13+
"A1": {"content": "KPI", "style": 1, "border": 1},
14+
"B1": {"content": "Target", "style": 1, "border": 1},
15+
"C1": {"content": "Actual", "style": 1, "border": 1},
16+
"D1": {"content": "Variance", "style": 1, "border": 1},
17+
"E1": {"content": "Status", "style": 1, "border": 1},
18+
"A2": {"content": "Cost Per Lead"},
19+
"B2": {"content": "50", "format": 1},
20+
"C2": {"content": "42", "format": 1},
21+
"D2": {"content": "=C2-B2", "format": 1},
22+
"E2": {"content": "=IF(C2<=B2,\"On Track\",\"Over\")"},
23+
"A3": {"content": "Revenue Growth"},
24+
"B3": {"content": "0.15", "format": 2},
25+
"C3": {"content": "0.12", "format": 2},
26+
"D3": {"content": "=C3-B3", "format": 2},
27+
"E3": {"content": "=IF(C3>=B3,\"On Track\",\"Below\")"},
28+
"A4": {"content": "Customer Churn"},
29+
"B4": {"content": "0.05", "format": 2},
30+
"C4": {"content": "0.032", "format": 2},
31+
"D4": {"content": "=C4-B4", "format": 2},
32+
"E4": {"content": "=IF(C4<=B4,\"On Track\",\"High\")"},
33+
"A5": {"content": "Avg Deal Size"},
34+
"B5": {"content": "25000", "format": 1},
35+
"C5": {"content": "28500", "format": 1},
36+
"D5": {"content": "=C5-B5", "format": 1},
37+
"E5": {"content": "=IF(C5>=B5,\"On Track\",\"Low\")"},
38+
"A6": {"content": "NPS Score"},
39+
"B6": {"content": "70"},
40+
"C6": {"content": "78"},
41+
"D6": {"content": "=C6-B6"},
42+
"E6": {"content": "=IF(C6>=B6,\"On Track\",\"Low\")"},
43+
"A8": {"content": "Last Updated"},
44+
"B8": {"content": "2026-03-01"},
45+
"A9": {"content": "Updated By"},
46+
"B9": {"content": "Admin"}
47+
},
48+
"conditionalFormats": [],
49+
"figures": [],
50+
"filterTables": [],
51+
"tables": [],
52+
"dataValidationRules": [],
53+
"comments": {},
54+
"headerGroups": {"ROW": [], "COL": []},
55+
"areGridLinesVisible": true,
56+
"isVisible": true
57+
}
58+
],
59+
"settings": {},
60+
"customTableStyles": {},
61+
"styles": {
62+
"1": {"bold": true, "align": "center"}
63+
},
64+
"formats": {
65+
"1": "$#,##0",
66+
"2": "0.00%"
67+
},
68+
"borders": {
69+
"1": {
70+
"top": ["thin", "#000"],
71+
"bottom": ["thin", "#000"],
72+
"left": ["thin", "#000"],
73+
"right": ["thin", "#000"]
74+
}
75+
},
76+
"revisionId": "START_REVISION",
77+
"uniqueFigureIds": true,
78+
"odooVersion": 12,
79+
"globalFilters": [],
80+
"pivots": {},
81+
"pivotNextId": 1,
82+
"lists": {},
83+
"listNextId": 1,
84+
"chartOdooMenusReferences": {}
85+
}

spreadsheet_oca/demo/spreadsheet_spreadsheet.xml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@
7878
/>
7979
</record>
8080

81+
<record id="demo_kpi_dashboard" model="spreadsheet.spreadsheet">
82+
<field name="name">KPI Dashboard</field>
83+
<field name="owner_id" ref="base.user_admin" />
84+
<field
85+
name="spreadsheet_binary_data"
86+
type="base64"
87+
file="spreadsheet_oca/demo/demo_kpi_dashboard.json"
88+
/>
89+
</record>
90+
8191
<record id="demo_pivot_dashboard" model="spreadsheet.spreadsheet">
8292
<field name="name">Partner Pivot Dashboard</field>
8393
<field name="owner_id" ref="base.user_admin" />
@@ -87,4 +97,27 @@
8797
file="spreadsheet_oca/demo/demo_pivot_dashboard.json"
8898
/>
8999
</record>
100+
101+
<!-- ════════════════════════════════════════════════════════════════════
102+
KPI Alerts
103+
════════════════════════════════════════════════════════════════════ -->
104+
105+
<record id="demo_alert_revenue" model="spreadsheet.alert">
106+
<field name="name">Revenue Below Target</field>
107+
<field name="spreadsheet_id" ref="demo_spreadsheet" />
108+
<field name="cell_ref">E8</field>
109+
<field name="sheet_name">Sales Pipeline</field>
110+
<field name="operator" eval="'&lt;'" />
111+
<field name="threshold">2000000</field>
112+
<field name="trigger_mode">edge</field>
113+
</record>
114+
115+
<record id="demo_alert_cost_lead" model="spreadsheet.alert">
116+
<field name="name">Cost Per Lead Warning</field>
117+
<field name="spreadsheet_id" ref="demo_kpi_dashboard" />
118+
<field name="cell_ref">C2</field>
119+
<field name="operator" eval="'&gt;'" />
120+
<field name="threshold">45</field>
121+
<field name="trigger_mode">level</field>
122+
</record>
90123
</odoo>

spreadsheet_oca/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
from . import spreadsheet_oca_revision
66
from . import ir_websocket
77
from . import spreadsheet_spreadsheet_import_mode
8+
from . import spreadsheet_alert

0 commit comments

Comments
 (0)