Skip to content

Commit 6082310

Browse files
dnplkndllclaude
andcommitted
[IMP] spreadsheet_oca: add server-side pivot refresh, alerts, XLSX export, scenarios, writeback, subscriptions, and input parameters
New features for the OCA spreadsheet module: - Scheduled data refresh with cron-based pivot re-computation and Chatter notifications (with input parameter substitution) - KPI alerts with configurable cell watches, threshold conditions, edge/level trigger modes, and partner notifications - What-if scenarios with cell overrides, base-case comparison, and export to new spreadsheet copies - Cell writeback allowing direct Odoo record edits from list-view cells with full audit logging and rollback capability - Email subscriptions for periodic digest emails (daily/weekly/monthly) with optional pivot data summaries - Input parameters for named cell references with automatic domain substitution during refresh cycles - XLSX export with dedicated pivot sheets and styled headers - QWeb email templates for alerts, digests, and refresh notifications (customisable via Settings > Technical > Views) - Demo data with pivot dashboard, KPI alerts, and guided feature tour - Record rules tying child model visibility to parent spreadsheet access Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6b75258 commit 6082310

49 files changed

Lines changed: 7557 additions & 109 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

spreadsheet_oca/README.rst

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
.. image:: https://odoo-community.org/readme-banner-image
2-
:target: https://odoo-community.org/get-involved?utm_source=readme
3-
:alt: Odoo Community Association
4-
51
===============
62
Spreadsheet Oca
73
===============
@@ -17,7 +13,7 @@ Spreadsheet Oca
1713
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
1814
:target: https://odoo-community.org/page/development-status
1915
:alt: Beta
20-
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
16+
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
2117
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
2218
:alt: License: AGPL-3
2319
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fspreadsheet-lightgray.png?logo=github
@@ -32,11 +28,29 @@ Spreadsheet Oca
3228

3329
|badge1| |badge2| |badge3| |badge4| |badge5|
3430

35-
This module adds a functionality for adding and editing Spreadsheets
36-
using Odoo CE.
37-
38-
It is an alternative to the proprietary module ``spreadsheet_edition``
39-
of Odoo Enterprise Edition.
31+
This module provides a full-featured spreadsheet editor for Odoo CE using
32+
the ``o-spreadsheet`` engine. It serves as a community alternative that
33+
requires only Odoo CE and OCA dependencies.
34+
35+
Beyond basic spreadsheet editing, the module includes server-side features
36+
for operational use:
37+
38+
- **Scheduled Refresh** — cron-based pivot data refresh with email digest
39+
notifications and input parameter substitution in domains
40+
- **KPI Alerts** — cell-value threshold monitors with edge or level trigger
41+
modes, sending notifications when conditions are met
42+
- **What-If Scenarios** — named cell-override sets for scenario planning,
43+
with comparison export and apply-to-copy workflow
44+
- **Email Subscriptions** — partner-level daily/weekly/monthly digest emails
45+
with optional pivot data summaries
46+
- **Input Parameters** — named cell registry for domain token substitution
47+
(e.g. ``%(start_date)s``) used by scheduled refresh and alerts
48+
- **Cell Writeback** — edit Odoo record fields directly from list-view cells
49+
in the spreadsheet, with full audit trail and rollback
50+
- **XLSX Export** — server-rendered ``.xlsx`` download with fresh pivot data
51+
on dedicated sheets, styled headers, and static cell content
52+
- **Collaborative Editing** — revision-based multi-user editing with conflict
53+
resolution via the OWL-based spreadsheet component
4054

4155
**Table of contents**
4256

spreadsheet_oca/__manifest__.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,27 @@
55
"name": "Spreadsheet Oca",
66
"summary": """
77
Allow to edit spreadsheets""",
8-
"version": "18.0.1.2.3",
8+
"version": "18.0.2.0.0",
99
"license": "AGPL-3",
1010
"author": "CreuBlanca,Odoo Community Association (OCA)",
1111
"website": "https://github.com/OCA/spreadsheet",
12-
"depends": ["spreadsheet", "base_sparse_field", "bus"],
12+
"depends": ["spreadsheet", "base_sparse_field", "bus", "web_tour"],
1313
"data": [
1414
"security/security.xml",
1515
"security/ir.model.access.csv",
1616
"views/spreadsheet_spreadsheet.xml",
17+
"views/spreadsheet_refresh_schedule_views.xml",
18+
"views/spreadsheet_alert_views.xml",
19+
"views/spreadsheet_subscription_views.xml",
20+
"views/spreadsheet_scenario_views.xml",
21+
"views/spreadsheet_xlsx_export_views.xml",
22+
"views/spreadsheet_writeback_views.xml",
23+
"views/spreadsheet_input_param_views.xml",
24+
"data/mail_templates.xml",
1725
"data/spreadsheet_spreadsheet_import_mode.xml",
26+
"data/spreadsheet_alert_cron.xml",
27+
"data/spreadsheet_subscription_cron.xml",
28+
"data/web_tour_tour.xml",
1829
"wizards/spreadsheet_select_row_number.xml",
1930
"wizards/spreadsheet_spreadsheet_import.xml",
2031
],
@@ -28,6 +39,7 @@
2839
"spreadsheet_oca/static/src/spreadsheet/list_controller.esm.js",
2940
"spreadsheet_oca/static/src/spreadsheet/list_renderer.esm.js",
3041
"spreadsheet_oca/static/src/spreadsheet/list_controller.xml",
42+
"spreadsheet_oca/static/src/tours/spreadsheet_feature_tour.esm.js",
3143
],
3244
"web.assets_backend_lazy": [
3345
"spreadsheet_oca/static/src/spreadsheet/pivot_controller.esm.js",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
from . import main
2+
from . import spreadsheet_writeback
3+
from . import spreadsheet_input_params
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright 2025 Ledo Enterprises LLC
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
"""
4+
JSON endpoint for spreadsheet input parameters.
5+
6+
Provides a lightweight API so that a future JS plugin can read the current
7+
named-parameter values without having to parse spreadsheet_raw itself.
8+
"""
9+
10+
from odoo.http import Controller, request, route
11+
12+
13+
class SpreadsheetInputParamsController(Controller):
14+
@route(
15+
"/spreadsheet/input_params/<int:spreadsheet_id>",
16+
type="json",
17+
auth="user",
18+
)
19+
def get_input_params(self, spreadsheet_id):
20+
"""Return {name: current_value} for all active parameters."""
21+
spreadsheet = request.env["spreadsheet.spreadsheet"].browse(spreadsheet_id)
22+
if not spreadsheet.exists():
23+
return {"error": "Spreadsheet not found."}
24+
spreadsheet.check_access("read")
25+
params = request.env["spreadsheet.input_param"].search(
26+
[
27+
("spreadsheet_id", "=", spreadsheet_id),
28+
("active", "=", True),
29+
]
30+
)
31+
return {p.name: p.current_value for p in params}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Copyright 2025 Ledo Enterprises LLC
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
"""
4+
Controller for cell writeback: edit list cells to update Odoo records.
5+
6+
The JS list cell-edit handler POSTs here with:
7+
spreadsheet_id — int
8+
model — str (e.g. "sale.order")
9+
record_id — int
10+
field_name — str (e.g. "name")
11+
new_value — any (JSON-decoded by Odoo's JSON-RPC dispatcher)
12+
13+
Returns a JSON-serialisable dict:
14+
{'success': True, 'old_value': str, 'new_value': str, 'log_id': int}
15+
or
16+
{'error': '<message>'}
17+
18+
All exceptions are caught so a writeback failure never results in a
19+
500 error reaching the browser.
20+
"""
21+
22+
import logging
23+
24+
from odoo import _
25+
from odoo.exceptions import AccessError
26+
from odoo.http import Controller, request, route
27+
28+
_logger = logging.getLogger(__name__)
29+
30+
31+
class SpreadsheetWriteback(Controller):
32+
@route(
33+
"/spreadsheet/writeback",
34+
type="json",
35+
auth="user",
36+
methods=["POST"],
37+
)
38+
def writeback(self, spreadsheet_id, model, record_id, field_name, new_value):
39+
"""
40+
Write a single field value to an Odoo record on behalf of the
41+
spreadsheet's List cell-edit handler.
42+
43+
Security checks (in order):
44+
1. spreadsheet.writeback_enabled must be True.
45+
2. Current user must have read access on the spreadsheet record.
46+
3. model must be a registered model in this environment.
47+
4. The target record must exist.
48+
5. Current user must have write access on the target record.
49+
50+
The old value is captured before the write and stored in the audit
51+
log. For relational fields str() is used which may not be
52+
directly re-writable; see the model docstring for details.
53+
"""
54+
log_vals_base = {
55+
"spreadsheet_id": spreadsheet_id,
56+
"res_model": model,
57+
"record_id": record_id,
58+
"field_name": field_name,
59+
"new_value": str(new_value),
60+
}
61+
62+
try:
63+
# 1. Load spreadsheet and check writeback_enabled
64+
spreadsheet = request.env["spreadsheet.spreadsheet"].browse(spreadsheet_id)
65+
if not spreadsheet.exists():
66+
return {"error": "Spreadsheet not found."}
67+
68+
if not spreadsheet.writeback_enabled:
69+
return {"error": "Writeback not enabled for this spreadsheet."}
70+
71+
# 2. Check spreadsheet read access
72+
try:
73+
spreadsheet.check_access("read")
74+
except AccessError:
75+
return {"error": "Access denied to spreadsheet."}
76+
77+
# 3. Validate model
78+
if model not in request.env:
79+
return {"error": f"Model {model!r} is not available."}
80+
81+
# 4. Load and check record existence
82+
record = request.env[model].browse(record_id)
83+
if not record.exists():
84+
return {"error": f"Record {model}({record_id}) not found."}
85+
86+
# 5. Check write access on the target record
87+
try:
88+
record.check_access("write")
89+
except AccessError:
90+
_logger.warning(
91+
"Writeback: user %d denied write on %s(%d)",
92+
request.env.uid,
93+
model,
94+
record_id,
95+
)
96+
return {"error": "Access denied: no write access on record."}
97+
98+
# 6. Validate field_name exists and is writable
99+
model_fields = request.env[model]._fields
100+
if field_name not in model_fields:
101+
return {
102+
"error": _(
103+
"Field %(field)s does not exist on model %(model)s.",
104+
field=field_name,
105+
model=model,
106+
)
107+
}
108+
field_obj = model_fields[field_name]
109+
if field_obj.readonly or field_obj.compute:
110+
return {
111+
"error": _(
112+
"Field %(field)s on %(model)s is computed or readonly"
113+
" and cannot be written to.",
114+
field=field_name,
115+
model=model,
116+
)
117+
}
118+
119+
# Capture old value before writing
120+
old_value = record[field_name]
121+
old_value_str = str(old_value)
122+
123+
# Perform the write
124+
record.write({field_name: new_value})
125+
126+
# Create audit log (sudo so the log can always be written
127+
# regardless of the user's access on spreadsheet.writeback.log)
128+
log = (
129+
request.env["spreadsheet.writeback.log"]
130+
.sudo()
131+
.create(
132+
dict(
133+
log_vals_base,
134+
old_value=old_value_str,
135+
status="ok",
136+
)
137+
)
138+
)
139+
140+
# Post a brief chatter note on the spreadsheet
141+
spreadsheet.sudo().message_post(
142+
body=_(
143+
"Writeback: field <b>%(field)s</b> on "
144+
"<b>%(model)s</b> #%(record_id)d changed "
145+
"from <b>%(old)s</b> to <b>%(new)s</b>.",
146+
field=field_name,
147+
model=model,
148+
record_id=record_id,
149+
old=old_value_str,
150+
new=str(new_value),
151+
),
152+
subtype_xmlid="mail.mt_note",
153+
)
154+
155+
return {
156+
"success": True,
157+
"old_value": old_value_str,
158+
"new_value": str(new_value),
159+
"log_id": log.id,
160+
}
161+
162+
except Exception as exc:
163+
_logger.exception(
164+
"Writeback error: spreadsheet=%d model=%s record=%d field=%s",
165+
spreadsheet_id,
166+
model,
167+
record_id,
168+
field_name,
169+
)
170+
# Attempt to write an error log (best effort — use sudo and
171+
# ignore any secondary failure so the route always returns JSON)
172+
try:
173+
request.env["spreadsheet.writeback.log"].sudo().create(
174+
dict(
175+
log_vals_base,
176+
status="error",
177+
error_message=str(exc)[:255],
178+
)
179+
)
180+
except Exception:
181+
_logger.exception("Failed to create writeback error log")
182+
183+
return {"error": str(exc)}

0 commit comments

Comments
 (0)