Skip to content

Commit ce13027

Browse files
committed
simplify a bit
1 parent e8e7f31 commit ce13027

File tree

7 files changed

+211
-193
lines changed

7 files changed

+211
-193
lines changed

web_form_banner/README.rst

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ Usage
4343
=====
4444

4545
#. 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.
46+
#. Choose Model, select Trigger Fields (optional), set Default Severity, select Views
47+
(optional), update Target XPath (insertion point) and Position, and configure the
48+
message.
4849
#. Save. Open any matching form record—the banner will appear and auto-refresh after
4950
load/save/reload.
5051

@@ -68,6 +69,13 @@ Evaluation context variables available in Message Value Code:
6869
* `user`: Current user (`env.user`).
6970
* `ctx`: Copy of the current context (`dict(env.context)`).
7071
* `record`: Current record (the form's record).
72+
* `draft`: The persisted field values of the ORM record (before applying the current
73+
form's unsaved changes) + the current unsaved changes on trigger fields.
74+
Should be used instead of `record` when your rule is triggered dynamically by an
75+
update to a trigger field. It doesn't include any values from complex fields
76+
(x2many/reference, etc).
77+
* `record_id`: Integer id of the record being edited, or `False` if the form
78+
is creating a new record.
7179
* `model`: Shortcut to the current model (`env[record._name]`).
7280
* `url_for(obj)`: Helper that returns a backend form URL for `obj`.
7381
* `context_today(ts=None)`: User-timezone “today” (date) for reliable date comparisons.
@@ -79,6 +87,19 @@ Evaluation context variables available in Message Value Code:
7987

8088
All of the above are injected by the module to the safe_eval locals.
8189

90+
Trigger Fields
91+
~~~~~~~~~~~~~~
92+
93+
*Trigger Fields* is an optional list of model fields that, when changed in the open
94+
form, cause the banner to **recompute live**. If left empty, the banner does **not**
95+
auto-refresh as the user edits the form.
96+
97+
When a trigger fires, the module sends the current draft values to the server, sanitizes
98+
them, builds an evaluation record, and re-runs your `message_value_code`.
99+
100+
You should use `draft` instead of `record` to access the current form values if your
101+
rule is triggered based on an update to a trigger field.
102+
82103
Message setting examples:
83104
~~~~~~~~~~~~~~~~~~~~~~~~~
84105

@@ -123,7 +144,7 @@ It is also possible to use "convenience placeholders" without an explicit `value
123144
.. code-block:: python
124145
125146
{
126-
"visible": record.amount_total > 30000,
147+
"visible": record.amount_total >= 30000,
127148
"severity": "danger" if record.amount_total >= 100000 else "warning",
128149
"values": {"amount_total": record.amount_total},
129150
}
@@ -141,7 +162,7 @@ It is also possible to use "convenience placeholders" without an explicit `value
141162
"values": {"validity_date": record.validity_date},
142163
}
143164
144-
**E) Pending activities on a task (uses env)**
165+
**E) Pending activities on a task (uses `env`)**
145166

146167
* Model: `project.task`
147168
* Message: `There are ${cnt} pending activities.`
@@ -152,19 +173,30 @@ It is also possible to use "convenience placeholders" without an explicit `value
152173
cnt = env["mail.activity"].search_count([("res_model","=",record._name),("res_id","=",record.id)])
153174
result = {"visible": cnt > 0, "values": {"cnt": cnt}}
154175
155-
**F) HTML banner linking to the customer's last sales order**
176+
**F) Product is missing internal reference (uses trigger fields)**
177+
178+
* Model: `product.template`
179+
* Trigger Fields: `default_code`
180+
* Message: `Make sure to set an internal reference!`
181+
* Message Value Code:
182+
183+
.. code-block:: python
184+
185+
{"visible": not bool(draft.default_code)}
186+
187+
**G) HTML banner linking to the customer's last sales order (uses trigger fields)**
156188

157189
* Model: `sale.order`
190+
* Trigger Fields: `partner_id`
158191
* Message: (leave blank; `html` provided by Message Value Code)
159192
* Message Value Code (multi-line with `result`):
160193

161194
.. code-block:: python
162195
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-
)
196+
domain = [("partner_id", "=", draft.partner_id.id)]
197+
if record_id:
198+
domain += [("id", "<", record_id)]
199+
last = model.search(domain, order="date_order desc, id desc", limit=1)
168200
if last:
169201
html = "<strong>Previous order:</strong> <a href='%s'>%s</a>" % (url_for(last), last.name)
170202
result = {"visible": True, "html": html}
@@ -174,9 +206,23 @@ It is also possible to use "convenience placeholders" without an explicit `value
174206
Known issues / Roadmap
175207
======================
176208

209+
Banner presentation inside `<group>`
210+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
211+
177212
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.
213+
presentation of the banner and the child fields will be distorted.
214+
215+
Limitations of `draft` eval context variable
216+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
217+
218+
* `draft` is always available in the eval context, but for new records (`record_id` =
219+
`False`) it only contains the trigger fields from the banner rules.
220+
* For existing records, `draft` overlays the trigger field values on top of the
221+
persisted record; all other fields come from `Model.new` defaults rather than the
222+
database.
223+
* Only simple field types are included: `char`, `text`, `html`, `selection`, `boolean`,
224+
`integer`, `float`, `monetary`, `date`, `datetime`, and `many2one` (normalized to an
225+
integer ID). **x2many/reference/other types are omitted.**
180226

181227
Bug Tracker
182228
===========
@@ -202,6 +248,7 @@ Contributors
202248
* `Quartile <https://www.quartile.co>`_:
203249

204250
* Yoshi Tashiro
251+
* Aung Ko Ko Lin
205252

206253
Maintainers
207254
~~~~~~~~~~~

web_form_banner/models/ir_model.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,7 @@ def fields_view_get(
4141
continue
4242
# Lightweight placeholder; JS will fill and toggle visibility
4343
css = "o_form_banner alert alert-%s" % (rule.severity or "danger")
44-
triggers_fields = (
45-
",".join(rule.trigger_field_ids.mapped("name"))
46-
if getattr(rule, "trigger_field_ids", False)
47-
else ""
48-
)
44+
trigger_fields = ",".join(rule.trigger_field_ids.mapped("name"))
4945
node = etree.Element(
5046
"div",
5147
{
@@ -54,7 +50,7 @@ def fields_view_get(
5450
"data-rule-id": str(rule.id),
5551
"data-model": self._name,
5652
"data-default-severity": (rule.severity or "danger"),
57-
"data-trigger-fields": triggers_fields,
53+
"data-trigger-fields": trigger_fields,
5854
"style": "display:none;",
5955
},
6056
)

web_form_banner/models/web_form_banner_rule.py

Lines changed: 19 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,6 @@
2121
_logger = logging.getLogger(__name__)
2222

2323

24-
class _EvalRecordProxy:
25-
"""Read-only-ish view of an existing record with field overrides."""
26-
__slots__ = ("_b", "_o")
27-
28-
def __init__(self, base, overrides):
29-
self._b = base
30-
self._o = overrides
31-
32-
def __getattr__(self, name):
33-
# Prefer explicit overrides; otherwise fall back to the base record
34-
if name in self._o:
35-
return self._o[name]
36-
return getattr(self._b, name)
37-
38-
def __repr__(self):
39-
return f"<_EvalRecordProxy base={self._b} overrides={list(self._o.keys())}>"
40-
41-
@property
42-
def id(self):
43-
# Keep the real DB id for RPCs / URL building / permissions
44-
return self._b.id
45-
46-
4724
_SIMPLE_FIELD_TYPES = frozenset(
4825
{
4926
"char", "text", "html", "selection", "boolean",
@@ -191,7 +168,7 @@ def _get_eval_context(self, record):
191168
return eval_ctx
192169

193170
@api.model
194-
def _sanitize_draft(self, model, form_vals):
171+
def _sanitize_values(self, model, form_vals):
195172
"""Return a sanitized dict of simple field values safe for new()/eval."""
196173
flds = self.env[model]._fields
197174
out = {}
@@ -203,26 +180,16 @@ def _sanitize_draft(self, model, form_vals):
203180

204181
@api.model
205182
def _build_eval_record(self, model, res_id, vals):
206-
"""Build the record used for evaluation.
207-
- existing record: wrap with overrides but keep real id
208-
- new record: new(vals)
209-
"""
210-
if not res_id:
211-
return self.env[model].new(vals) if vals else self.env[model]
212-
base = self.env[model].browse(int(res_id))
213-
if not vals:
214-
return base
215-
flds = self.env[model]._fields
216-
ovr = {}
217-
for n, v in vals.items():
218-
f = flds[n]
219-
if not f:
220-
continue
221-
if f.type == "many2one" and isinstance(v, int):
222-
ovr[n] = self.env[f.comodel_name].browse(v)
223-
else:
224-
ovr[n] = v
225-
return _EvalRecordProxy(base, ovr)
183+
"""Return (draft_record, record_id, persisted_record)."""
184+
Model = self.env[model]
185+
vals = vals or {}
186+
if res_id:
187+
persisted = Model.browse(int(res_id))
188+
base_vals = persisted.read(list(vals.keys()))[0] if vals else {}
189+
draft = Model.new({**base_vals, **vals})
190+
return draft, persisted, persisted.id
191+
# new record (no res_id yet): persisted is an empty recordset, not None
192+
return Model.new(vals), Model, False
226193

227194
@api.model
228195
def _run_rule_code(self, rule, eval_ctx):
@@ -259,16 +226,19 @@ def compute_message(self, rule_id, model, res_id, form_vals=None):
259226
rule = self.browse(int(rule_id)).sudo()
260227
if not rule.exists() or not rule.active:
261228
return {"visible": False}
262-
vals = self._sanitize_draft(model, form_vals)
263-
record = self._build_eval_record(model, res_id, vals)
229+
values = self._sanitize_values(model, form_vals)
230+
# record, current_id = self._build_eval_record(model, res_id, vals)
231+
draft, record, record_id = self._build_eval_record(model, res_id, values)
264232
eval_ctx = self._get_eval_context(record)
265-
# expose changes for rule code that wants direct access to raw values
266233
eval_ctx.update(
267-
{"changes": vals, "current_id": int(res_id) if res_id else False}
234+
{
235+
"draft": draft, # DB base + simple field overrides
236+
"record_id": record_id,
237+
}
268238
)
269239
out = self._run_rule_code(rule, eval_ctx) or {}
270240
severity = out.get("severity", rule.severity or "danger")
271-
visible = out.get("visible", True) # default True like before
241+
visible = out.get("visible", True)
272242
if not visible:
273243
return {"visible": False}
274244
values = out.get("values") or {

web_form_banner/readme/ROADMAP.rst

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
Banner presentation inside `<group>`
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
14
Placing a full-width inline banner inside `<group>` is currently not supported. The
2-
banner will be limited to 50% of the group's width, and its label/value ratio will be
3-
forced to 1:1.
5+
presentation of the banner and the child fields will be distorted.
6+
7+
Limitations of `draft` eval context variable
8+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
9+
10+
* `draft` is always available in the eval context, but for new records (`record_id` =
11+
`False`) it only contains the trigger fields from the banner rules.
12+
* For existing records, `draft` overlays the trigger field values on top of the
13+
persisted record; all other fields come from `Model.new` defaults rather than the
14+
database.
15+
* Only simple field types are included: `char`, `text`, `html`, `selection`, `boolean`,
16+
`integer`, `float`, `monetary`, `date`, `datetime`, and `many2one` (normalized to an
17+
integer ID). **x2many/reference/other types are omitted.**

0 commit comments

Comments
 (0)