Skip to content

Commit 5bb4e70

Browse files
committed
[16.0][ADD] base_notification
1 parent 35822c5 commit 5bb4e70

File tree

19 files changed

+972
-0
lines changed

19 files changed

+972
-0
lines changed

base_notification/README.rst

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
========================
2+
Base Notification Method
3+
========================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:b2d9db82d59881a6a679980ceb906d7da2a71114903585e2119474a7d02c77d3
11+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12+
13+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
14+
:target: https://odoo-community.org/page/development-status
15+
:alt: Beta
16+
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
17+
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
18+
:alt: License: LGPL-3
19+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github
20+
:target: https://github.com/OCA/server-tools/tree/16.0/base_notification
21+
:alt: OCA/server-tools
22+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
23+
:target: https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-base_notification
24+
:alt: Translate me on Weblate
25+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
26+
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=16.0
27+
:alt: Try me on Runboat
28+
29+
|badge1| |badge2| |badge3| |badge4| |badge5|
30+
31+
32+
**Table of contents**
33+
34+
.. contents::
35+
:local:
36+
37+
Bug Tracker
38+
===========
39+
40+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-tools/issues>`_.
41+
In case of trouble, please check there if your issue has already been reported.
42+
If you spotted it first, help us to smash it by providing a detailed and welcomed
43+
`feedback <https://github.com/OCA/server-tools/issues/new?body=module:%20base_notification%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
44+
45+
Do not contact contributors directly about support or help with technical issues.
46+
47+
Credits
48+
=======
49+
50+
Authors
51+
~~~~~~~
52+
53+
* Kencove
54+
55+
Contributors
56+
~~~~~~~~~~~~
57+
58+
* Mohamed Alkobrosli <https://kencove.com>
59+
60+
61+
Other credits
62+
~~~~~~~~~~~~~
63+
64+
The development of this module has been financially supported by:
65+
66+
* Kencove
67+
68+
69+
Maintainers
70+
~~~~~~~~~~~
71+
72+
This module is maintained by the OCA.
73+
74+
.. image:: https://odoo-community.org/logo.png
75+
:alt: Odoo Community Association
76+
:target: https://odoo-community.org
77+
78+
OCA, or the Odoo Community Association, is a nonprofit organization whose
79+
mission is to support the collaborative development of Odoo features and
80+
promote its widespread use.
81+
82+
This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/16.0/base_notification>`_ project on GitHub.
83+
84+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

base_notification/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import models
2+
3+
# from .hooks import post_init_hook

base_notification/__manifest__.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# __manifest__.py
2+
{
3+
"name": "Base Notification Method",
4+
"summary": "Generic notifications on create/write/delete or method call",
5+
"version": "16.0.1.0.0",
6+
"author": "Kencove, Odoo Community Association (OCA)",
7+
"website": "https://github.com/OCA/server-tools",
8+
"category": "Tools",
9+
"license": "LGPL-3",
10+
"depends": [
11+
"base",
12+
"mail",
13+
"queue_job",
14+
],
15+
"data": [
16+
"security/ir.model.access.csv",
17+
"views/base_notification_rule_views.xml",
18+
],
19+
"demo": [
20+
"demo/base_notification_demo.xml",
21+
],
22+
"assets": {
23+
"web.assets_backend": [
24+
"base_notification/static/src/js/base_notification_service.esm.js",
25+
],
26+
},
27+
"installable": True,
28+
"application": True,
29+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<odoo>
2+
<record id="base_notification_rule_demo" model="base.notification.rule">
3+
<field name="name">Demo: Notify on Partner Update</field>
4+
<field name="model_id" ref="base.model_res_partner" />
5+
<field name="trigger">on_write</field>
6+
<field name="active">True</field>
7+
<field name="notify_mode">immediate</field>
8+
<field name="message_type">email</field>
9+
<field name="partner_ids" eval="[(4, ref('base.user_admin'))]" />
10+
<field name="domain">[]</field>
11+
<field name="python_code">
12+
# Available variables:
13+
# - env: Odoo Environment on which the action is triggered
14+
# - model: Odoo Model of the record on which the action is triggered; is a void recordset
15+
# - rule: base.notification.rule record; may be void
16+
# - record: record on which the action is triggered; may be void
17+
# - records: recordset of all records on which the action is triggered in multi-mode; may be void
18+
# - time, datetime, dateutil, timezone: useful Python libraries
19+
# - float_compare: Odoo function to compare floats based on specific precisions
20+
# - log: log(message, level='info'): logging function to record debug information in ir.logging table
21+
# - UserError: Warning Exception to use with raise
22+
# - Command: x2Many commands namespace
23+
24+
# To return partners, assign: partners = env['res.partner'].browse(3)
25+
# To return a message, assign: message = "foo"
26+
27+
28+
partners = record.write_uid.partner_id
29+
record_ids_str = ", ".join(map(str, records.ids))
30+
message = _(
31+
"Event: %s for model: %s (IDs: %s) was triggered"
32+
) % (
33+
self.trigger,
34+
records._description,
35+
record_ids_str,
36+
)
37+
log(message, level='info')
38+
39+
</field>
40+
</record>
41+
</odoo>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import base_notification_rule
2+
from . import ir_model
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import base64
2+
import logging
3+
4+
from pytz import timezone
5+
6+
import odoo
7+
from odoo import Command, _, api, fields, models, tools
8+
from odoo.exceptions import ValidationError
9+
from odoo.tools.float_utils import float_compare
10+
from odoo.tools.safe_eval import safe_eval, test_python_expr
11+
12+
from odoo.addons.queue_job.job import identity_exact
13+
14+
_logger = logging.getLogger(__name__)
15+
16+
17+
class BaseNotificationRule(models.Model):
18+
_name = "base.notification.rule"
19+
_description = "Generic Notification Rule"
20+
_rec_name = "name"
21+
22+
DEFAULT_PYTHON_CODE = """# Available variables:
23+
# - env: Odoo Environment
24+
# - model: Odoo Model of the record; is a void recordset
25+
# - rule: base.notification.rule record; may be void
26+
# - record: record; may be void
27+
# - records: recordset of all records in multi-mode; may be void
28+
# - time, datetime, dateutil, timezone: useful Python libraries
29+
# - float_compare: Odoo function to compare floats based on specific precisions
30+
# - log: log(message, level='info')
31+
# - UserError: Warning Exception to use with raise
32+
# - Command: x2Many commands namespace
33+
34+
# To return partners, assign: partners = env['res.partner'].browse(3)
35+
# To return a message, assign: message = "foo"\n\n\n\n"""
36+
37+
name = fields.Char(required=True)
38+
model_id = fields.Many2one(
39+
"ir.model",
40+
string="Model",
41+
required=True,
42+
ondelete="cascade",
43+
)
44+
model = fields.Char(
45+
related="model_id.model",
46+
)
47+
trigger = fields.Selection(
48+
[
49+
("on_create", "On Create"),
50+
("on_write", "On Write"),
51+
("on_unlink", "On Delete"),
52+
("on_method_call", "On Method Call"),
53+
],
54+
required=True,
55+
default="on_write",
56+
)
57+
method_name = fields.Char()
58+
domain = fields.Char("Domain Filter", default="[]")
59+
active = fields.Boolean(default=True)
60+
notify_mode = fields.Selection(
61+
[
62+
("immediate", "Immediate"),
63+
("queued", "Queued"),
64+
],
65+
required=True,
66+
default="immediate",
67+
)
68+
message_type = fields.Selection(
69+
[
70+
("chatter_log", "Log message in chatter and send email"),
71+
("email", "Send email"),
72+
],
73+
required=True,
74+
default="email",
75+
)
76+
partner_ids = fields.Many2many("res.partner", string="Recipients")
77+
python_code = fields.Text(
78+
groups="base.group_system",
79+
default=DEFAULT_PYTHON_CODE,
80+
help="Custom code to generate a message and recipients.\n"
81+
"Available variables: env, model, records, message, partners",
82+
)
83+
84+
@api.constrains("python_code")
85+
def _check_python_code(self):
86+
for action in self.sudo().filtered("python_code"):
87+
msg = test_python_expr(expr=action.python_code.strip(), mode="exec")
88+
if msg:
89+
raise ValidationError(msg)
90+
91+
def _get_notification_message(self, records):
92+
self.ensure_one()
93+
record_ids_str = ", ".join(map(str, records.ids))
94+
message = _(
95+
"Event: %(trigger)s for model: %(model)s (IDs: %(ids)s) was triggered"
96+
) % {
97+
"trigger": self.trigger,
98+
"model": records._description,
99+
"ids": record_ids_str,
100+
}
101+
return message
102+
103+
def _get_dynamic_message_and_partners(self, records):
104+
"""Safely execute user-defined Python code to compute message and recipients."""
105+
self.ensure_one()
106+
message = self._get_notification_message(records)
107+
partners = self.partner_ids
108+
109+
def log(message, level="info"):
110+
self.env["ir.logging"].sudo().create(
111+
{
112+
"type": "server",
113+
"dbname": self._cr.dbname,
114+
"name": __name__,
115+
"level": level,
116+
"message": message,
117+
"path": "action",
118+
"line": self.id,
119+
"func": self.name,
120+
}
121+
)
122+
123+
localdict = {
124+
"uid": self._uid,
125+
"user": self.env.user,
126+
"time": tools.safe_eval.time,
127+
"datetime": tools.safe_eval.datetime,
128+
"dateutil": tools.safe_eval.dateutil,
129+
"timezone": timezone,
130+
"float_compare": float_compare,
131+
"b64encode": base64.b64encode,
132+
"b64decode": base64.b64decode,
133+
"Command": Command,
134+
# orm
135+
"env": self.env,
136+
"model": self.env[self.model],
137+
# Exceptions
138+
"Warning": odoo.exceptions.Warning,
139+
"UserError": odoo.exceptions.UserError,
140+
# record
141+
"rule": self[:1],
142+
"record": records[:1],
143+
"records": records,
144+
"message": message,
145+
"partners": partners,
146+
# helpers
147+
"log": log,
148+
}
149+
if self.python_code:
150+
try:
151+
safe_eval(self.python_code, localdict, mode="exec", nocopy=True)
152+
message = localdict.get("message", message)
153+
partners = localdict.get("partners", partners)
154+
except Exception as e:
155+
_logger.warning(
156+
f"Error evaluating Python code in rule '{self.name}': {e}"
157+
)
158+
return message, partners
159+
160+
@api.model
161+
def notify_changes(self, partner_ids, message):
162+
"""Send notification to partner_ids."""
163+
channel = "base_notification_updates"
164+
for partner_id in partner_ids:
165+
self.env["bus.bus"]._sendone(
166+
partner_id,
167+
channel,
168+
{
169+
"message": message,
170+
},
171+
)
172+
173+
def _execute_notification(self, records):
174+
"""Send the configured notification."""
175+
if not self.active or not records:
176+
return
177+
if self.domain:
178+
try:
179+
domain = safe_eval(self.domain)
180+
records = records.filtered_domain(domain)
181+
except Exception as e:
182+
_logger.warning(
183+
f"Invalid domain in rule {self.name}: {self.domain} ({e})"
184+
)
185+
message, partner_ids = self._get_dynamic_message_and_partners(records)
186+
self.notify_changes(partner_ids, message)
187+
if self.message_type == "chatter_log":
188+
for rec in records:
189+
rec.with_context(mail_post_autofollow=False).message_post(
190+
body=message,
191+
partner_ids=partner_ids.ids,
192+
)
193+
else:
194+
Mail = self.env["mail.mail"].sudo()
195+
for partner_id in partner_ids:
196+
mail_values = {
197+
"subject": _("%(name)s - %(model)s | Notification")
198+
% {
199+
"name": self.name,
200+
"model": self.model_id.name,
201+
},
202+
"body_html": message,
203+
"recipient_ids": partner_id,
204+
"auto_delete": True,
205+
"is_notification": True,
206+
}
207+
Mail.create(mail_values).send()
208+
209+
def _apply_trigger(self, event_type, records):
210+
"""Called by create/write/unlink hooks."""
211+
rules = self.sudo().search(
212+
[
213+
("model_id.model", "=", records._name),
214+
("trigger", "=", event_type),
215+
("active", "=", True),
216+
]
217+
)
218+
for rule in rules:
219+
if rule.notify_mode == "queued":
220+
# Use queue job to send later
221+
record_ids_str = ", ".join(map(str, records.ids))
222+
description = _(
223+
"Sending a queued notification for model: %(model)s (IDs: %(ids)s)"
224+
) % {
225+
"model": records._name,
226+
"ids": record_ids_str,
227+
}
228+
rule.with_delay(
229+
description=description, identity_key=identity_exact
230+
)._execute_notification(records)
231+
else:
232+
# Send immediately
233+
rule._execute_notification(records)

0 commit comments

Comments
 (0)