Skip to content

Commit b5c7482

Browse files
yostashiroAungKoKoLin1997
authored andcommitted
[ADD] web_form_banner
1 parent 3dcfdaa commit b5c7482

21 files changed

+1592
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../web_form_banner

setup/web_form_banner/setup.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import setuptools
2+
3+
setuptools.setup(
4+
setup_requires=['setuptools-odoo'],
5+
odoo_addon=True,
6+
)

web_form_banner/README.rst

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
===============
2+
Web Form Banner
3+
===============
4+
5+
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
6+
!! This file is generated by oca-gen-addon-readme !!
7+
!! changes will be overwritten. !!
8+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
9+
10+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
11+
:target: https://odoo-community.org/page/development-status
12+
:alt: Beta
13+
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
14+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
15+
:alt: License: AGPL-3
16+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github
17+
:target: https://github.com/OCA/web/tree/12.0/web_form_banner
18+
:alt: OCA/web
19+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
20+
:target: https://translation.odoo-community.org/projects/web-12-0/web-12-0-web_form_banner
21+
:alt: Translate me on Weblate
22+
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
23+
:target: https://runbot.odoo-community.org/runbot/162/12.0
24+
:alt: Try me on Runbot
25+
26+
|badge1| |badge2| |badge3| |badge4| |badge5|
27+
28+
The module adds configurable banners for backend **form** views. Define rules per model
29+
(and optionally per view) to show context-aware alerts with a chosen severity (info/warning/danger).
30+
31+
Messages can be plain text with ${placeholders} or fully custom HTML; visibility,
32+
severity, and values are computed server-side via a safe Python expression.
33+
34+
Banners are injected just before or after a target node (default: //sheet) and refresh
35+
on form load/save/reload.
36+
37+
**Table of contents**
38+
39+
.. contents::
40+
:local:
41+
42+
Usage
43+
=====
44+
45+
#. Go to *Settings > Tachnical > User Interface > Form Banner Rules* and create a rule.
46+
#. Choose Model, (optionally) restrict Form Views, set Default Severity, Target XPath
47+
(insertion point), Position, and configure the message.
48+
#. Save. Open any matching form record—the banner will appear and auto-refresh after
49+
load/save/reload.
50+
51+
Usage of message fields:
52+
~~~~~~~~~~~~~~~~~~~~~~~~
53+
54+
* **Message** (message): Text shown in the banner. Supports `${placeholders}` filled
55+
from values returned by message_value_code. Ignored if message_value_code returns an
56+
`html` value.
57+
* **HTML** (message_is_html): If enabled, the message string is rendered as HTML;
58+
otherwise it's treated as plain text.
59+
* **Message Value Code** (message_value_code): Safe Python expression evaluated per
60+
record. Return a dict such as `{"visible": True, "severity": "warning", "values": {"name": record.name}}`.
61+
Use either message or `html` (from this code), not both. Several evaluation context
62+
variables are available.
63+
64+
Evaluation context variables available in Message Value Code:
65+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
66+
67+
* `env`: Odoo environment for ORM access.
68+
* `user`: Current user (`env.user`).
69+
* `ctx`: Copy of the current context (`dict(env.context)`).
70+
* `record`: Current record (the form's record).
71+
* `model`: Shortcut to the current model (`env[record._name]`).
72+
* `url_for(obj)`: Helper that returns a backend form URL for `obj`.
73+
* `context_today(ts=None)`: User-timezone “today” (date) for reliable date comparisons.
74+
* `time`, `datetime`: Standard Python time/datetime modules.
75+
* `dateutil`: `{ "parser": dateutil.parser, "relativedelta": dateutil.relativedelta }`
76+
* `timezone`: `pytz.timezone` for TZ handling.
77+
* `float_compare`, `float_is_zero`, `float_round`: Odoo float utils for precision-safe
78+
comparisons/rounding.
79+
80+
All of the above are injected by the module to the safe_eval locals.
81+
82+
Message setting examples:
83+
~~~~~~~~~~~~~~~~~~~~~~~~~
84+
85+
**A) Missing email on contact (warning)**
86+
87+
* Model: `res.partner`
88+
* Message: `This contact has no email.`
89+
* Message Value Code:
90+
91+
.. code-block:: python
92+
93+
{"visible": not bool(record.email)}
94+
95+
**B) Show partner comment if available**
96+
97+
* Model: `purchase.order`
98+
* Message: `Vendor Comments: ${comment}`
99+
* Message Value Code (single expression):
100+
101+
.. code-block:: python
102+
103+
{
104+
"visible": bool(record.partner_id.comment),
105+
values: {"comment": record.partner_id.comment},
106+
}
107+
108+
It is also possible to use "convenience placeholders" without an explicit `values` key:
109+
110+
.. code-block:: python
111+
112+
{
113+
"visible": bool(record.partner_id.comment),
114+
"comment": record.partner_id.comment,
115+
}
116+
117+
**C) High-value sale order (dynamic severity)**
118+
119+
* Model: `sale.order`
120+
* Message: `High-value order: ${amount_total}`
121+
* Message Value Code:
122+
123+
.. code-block:: python
124+
125+
{
126+
"visible": record.amount_total > 30000,
127+
"severity": "danger" if record.amount_total >= 100000 else "warning",
128+
"values": {"amount_total": record.amount_total},
129+
}
130+
131+
**D) Quotation past validity date**
132+
133+
* Model: `sale.order`
134+
* Message: `This quotation is past its validity date (${validity_date}).`
135+
* Message Value Code:
136+
137+
.. code-block:: python
138+
139+
{
140+
"visible": bool(record.validity_date and context_today() > record.validity_date and record.state in ["draft", "sent"]),
141+
"values": {"validity_date": record.validity_date},
142+
}
143+
144+
**E) Pending activities on a task (uses env)**
145+
146+
* Model: `project.task`
147+
* Message: `There are ${cnt} pending activities.`
148+
* Message Value Code (multi-line with `result`):
149+
150+
.. code-block:: python
151+
152+
cnt = env["mail.activity"].search_count([("res_model","=",record._name),("res_id","=",record.id)])
153+
result = {"visible": cnt > 0, "values": {"cnt": cnt}}
154+
155+
**F) HTML banner linking to the customer's last sales order**
156+
157+
* Model: `sale.order`
158+
* Message: (leave blank; `html` provided by Message Value Code)
159+
* Message Value Code (multi-line with `result`):
160+
161+
.. code-block:: python
162+
163+
last = model.search(
164+
[("partner_id", "=", record.partner_id.id), ("id", "<", record.id)],
165+
order="date_order desc, id desc",
166+
limit=1,
167+
)
168+
if last:
169+
html = "<strong>Previous order:</strong> <a href='%s'>%s</a>" % (url_for(last), last.name)
170+
result = {"visible": True, "html": html}
171+
else:
172+
result = {"visible": False}
173+
174+
Known issues / Roadmap
175+
======================
176+
177+
Placing a full-width inline banner inside `<group>` is currently not supported. The
178+
banner will be limited to 50% of the group's width, and its label/value ratio will be
179+
forced to 1:1.
180+
181+
Bug Tracker
182+
===========
183+
184+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_.
185+
In case of trouble, please check there if your issue has already been reported.
186+
If you spotted it first, help us smashing it by providing a detailed and welcomed
187+
`feedback <https://github.com/OCA/web/issues/new?body=module:%20web_form_banner%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
188+
189+
Do not contact contributors directly about support or help with technical issues.
190+
191+
Credits
192+
=======
193+
194+
Authors
195+
~~~~~~~
196+
197+
* Quartile
198+
199+
Contributors
200+
~~~~~~~~~~~~
201+
202+
* `Quartile <https://www.quartile.co>`_:
203+
204+
* Yoshi Tashiro
205+
206+
Maintainers
207+
~~~~~~~~~~~
208+
209+
This module is maintained by the OCA.
210+
211+
.. image:: https://odoo-community.org/logo.png
212+
:alt: Odoo Community Association
213+
:target: https://odoo-community.org
214+
215+
OCA, or the Odoo Community Association, is a nonprofit organization whose
216+
mission is to support the collaborative development of Odoo features and
217+
promote its widespread use.
218+
219+
This module is part of the `OCA/web <https://github.com/OCA/web/tree/12.0/web_form_banner>`_ project on GitHub.
220+
221+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

web_form_banner/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

web_form_banner/__manifest__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright 2025 Quartile (https://www.quartile.co)
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
{
4+
"name": "Web Form Banner",
5+
"version": "12.0.1.0.0",
6+
"category": "Web",
7+
"author": "Quartile, Odoo Community Association (OCA)",
8+
"website": "https://github.com/OCA/web",
9+
"license": "AGPL-3",
10+
"depends": ["web"],
11+
"data": [
12+
"security/ir.model.access.csv",
13+
"views/assets.xml",
14+
"views/web_form_banner_rule_views.xml",
15+
],
16+
"demo": ["demo/web_form_banner_rule_demo.xml"],
17+
'installable': True,
18+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo noupdate="1">
3+
<record id="demo_rule_partner_name_length" model="web.form.banner.rule">
4+
<field name="name">Partner name length notice</field>
5+
<field name="model_id" ref="base.model_res_partner"/>
6+
<field name="severity">warning</field>
7+
<field name="target_xpath">//sheet</field>
8+
<field name="position">before</field>
9+
<field name="message_value_code"><![CDATA[
10+
name = (record.name or "").strip()
11+
n = len(name)
12+
if n > 20:
13+
result = {
14+
"visible": True,
15+
"severity": "danger",
16+
"html": "<strong>This partner's name is very long!</strong> (length: %d)" % n,
17+
}
18+
elif n > 10:
19+
result = {
20+
"visible": True,
21+
"severity": "warning",
22+
"html": "This partner's name is a bit long. (length: %d)" % n,
23+
}
24+
else:
25+
result = {"visible": False}
26+
]]></field>
27+
</record>
28+
</odoo>

web_form_banner/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import ir_model
2+
from . import web_form_banner_rule

web_form_banner/models/ir_model.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright 2025 Quartile (https://www.quartile.co)
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
from lxml import etree
5+
6+
from odoo import api, models
7+
8+
9+
class Base(models.AbstractModel):
10+
_inherit = "base"
11+
12+
@api.model
13+
def fields_view_get(
14+
self, view_id=None, view_type="form", toolbar=False, submenu=False
15+
):
16+
res = super().fields_view_get(
17+
view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu
18+
)
19+
if view_type != "form" or not res.get("arch"):
20+
return res
21+
current_view_id = view_id or res.get("view_id")
22+
if not current_view_id:
23+
return res
24+
rules = self.env["web.form.banner.rule"].sudo().search(
25+
[
26+
("model_name", "=", self._name),
27+
"|",
28+
("view_ids", "in", current_view_id),
29+
("view_ids", "=", False),
30+
]
31+
)
32+
if not rules:
33+
return res
34+
try:
35+
root = etree.fromstring(res["arch"])
36+
except Exception:
37+
return res
38+
for rule in rules:
39+
target = root.xpath(rule.target_xpath or "//sheet")
40+
if not target:
41+
continue
42+
# Lightweight placeholder; JS will fill and toggle visibility
43+
css = "o_form_banner alert alert-%s" % (rule.severity or "danger")
44+
node = etree.Element(
45+
"div",
46+
{
47+
"class": css,
48+
"role": "alert",
49+
"data-rule-id": str(rule.id),
50+
"data-model": self._name,
51+
"data-default-severity": (rule.severity or "danger"),
52+
"style": "display:none;",
53+
},
54+
)
55+
parent = target[0].getparent()
56+
if parent is None:
57+
continue
58+
if rule.position == "before":
59+
parent.insert(parent.index(target[0]), node)
60+
else:
61+
target[0].addnext(node)
62+
res["arch"] = etree.tostring(root, encoding="unicode")
63+
return res

0 commit comments

Comments
 (0)