|
| 1 | +1. Go to *Settings \> Technical \> User Interface \> Form Banner Rules* |
| 2 | + and create a rule. |
| 3 | +2. Choose Model, select Trigger Fields (optional), set Default |
| 4 | + Severity, select Views (optional), update Target XPath (insertion |
| 5 | + point) and Position, and configure the message. |
| 6 | +3. Save. Open any matching form record—the banner will appear and |
| 7 | + auto-refresh after load/save/reload. |
| 8 | + |
| 9 | +## Usage of message fields: |
| 10 | + |
| 11 | +- **Message** (message): Text shown in the banner. Supports |
| 12 | + \${placeholders} filled from values returned by message_value_code. |
| 13 | + Ignored if message_value_code returns an html value. |
| 14 | +- **HTML** (message_is_html): If enabled, the message string is rendered |
| 15 | + as HTML; otherwise it's treated as plain text. |
| 16 | +- **Message Value Code** (message_value_code): Safe Python expression |
| 17 | + evaluated per record. Return a dict such as {"visible": True, |
| 18 | + "severity": "warning", "values": {"name": record.name}}. Use either |
| 19 | + message or html (from this code), not both. Several evaluation context |
| 20 | + variables are available. |
| 21 | + |
| 22 | +## Evaluation context variables available in Message Value Code: |
| 23 | + |
| 24 | +- \`env\`: Odoo environment for ORM access. |
| 25 | +- \`user\`: Current user (env.user). |
| 26 | +- \`ctx\`: Copy of the current context (dict(env.context)). |
| 27 | +- \`record\`: Current record (the form's record). |
| 28 | +- \`draft\`: The persisted field values of the ORM record (before |
| 29 | + applying the current form's unsaved changes) + the current unsaved |
| 30 | + changes on trigger fields. Should be used instead of record when your |
| 31 | + rule is triggered dynamically by an update to a trigger field. It |
| 32 | + doesn't include any values from complex fields (one2many/reference, |
| 33 | + etc). |
| 34 | +- \`record_id\`: Integer id of the record being edited, or False if the |
| 35 | + form is creating a new record. |
| 36 | +- \`model\`: Shortcut to the current model (env\[record.\_name\]). |
| 37 | +- \`url_for(obj): Helper that returns a backend form URL for \`obj. |
| 38 | +- \`context_today(ts=None)\`: User-timezone “today” (date) for reliable |
| 39 | + date comparisons. |
| 40 | +- time, \`datetime\`: Standard Python time/datetime modules. |
| 41 | +- \`dateutil\`: { "parser": dateutil.parser, "relativedelta": |
| 42 | + dateutil.relativedelta } |
| 43 | +- \`timezone\`: pytz.timezone for TZ handling. |
| 44 | +- float_compare, float_is_zero, \`float_round\`: Odoo float utils for |
| 45 | + precision-safe comparisons/rounding. |
| 46 | + |
| 47 | +All of the above are injected by the module to the safe_eval locals. |
| 48 | + |
| 49 | +## Trigger Fields |
| 50 | + |
| 51 | +*Trigger Fields* is an optional list of model fields that, when changed |
| 52 | +in the open form, cause the banner to **recompute live**. If left empty, |
| 53 | +the banner does **not** auto-refresh as the user edits the form. |
| 54 | + |
| 55 | +When a trigger fires, the module sends the current draft values to the |
| 56 | +server, sanitizes them, builds an evaluation record, and re-runs your |
| 57 | +message_value_code. |
| 58 | + |
| 59 | +You should use draft instead of record to access the current form values |
| 60 | +if your rule is triggered based on an update to a trigger field. |
| 61 | + |
| 62 | +## Message setting examples: |
| 63 | + |
| 64 | +**A) Missing email on contact (warning)** |
| 65 | + |
| 66 | +- Model: res.partner |
| 67 | +- Message: This contact has no email. |
| 68 | +- Message Value Code: |
| 69 | + |
| 70 | +``` python |
| 71 | +{"visible": not bool(record.email)} |
| 72 | +``` |
| 73 | + |
| 74 | +**B) Show partner comment if available** |
| 75 | + |
| 76 | +- Model: purchase.order |
| 77 | +- Message: Vendor Comments: \${comment} |
| 78 | +- Message Value Code (single expression): |
| 79 | + |
| 80 | +``` python |
| 81 | +{ |
| 82 | + "visible": bool(record.partner_id.comment), |
| 83 | + "values": {"comment": record.partner_id.comment}, |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +It is also possible to use "convenience placeholders" without an |
| 88 | +explicit values key: |
| 89 | + |
| 90 | +``` python |
| 91 | +{ |
| 92 | + "visible": bool(record.partner_id.comment), |
| 93 | + "comment": record.partner_id.comment, |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +**C) High-value sale order (dynamic severity)** |
| 98 | + |
| 99 | +- Model: sale.order |
| 100 | +- Message: High-value order: \${amount_total} |
| 101 | +- Message Value Code: |
| 102 | + |
| 103 | +``` python |
| 104 | +{ |
| 105 | + "visible": record.amount_total >= 30000, |
| 106 | + "severity": "danger" if record.amount_total >= 100000 else "warning", |
| 107 | + "values": {"amount_total": record.amount_total}, |
| 108 | +} |
| 109 | +``` |
| 110 | + |
| 111 | +**D) Quotation past validity date** |
| 112 | + |
| 113 | +- Model: sale.order |
| 114 | +- Message: This quotation is past its validity date (\${validity_date}). |
| 115 | +- Message Value Code: |
| 116 | + |
| 117 | +``` python |
| 118 | +{ |
| 119 | + "visible": bool(record.validity_date and context_today() > record.validity_date and record.state in ["draft", "sent"]), |
| 120 | + "values": {"validity_date": record.validity_date}, |
| 121 | +} |
| 122 | +``` |
| 123 | + |
| 124 | +**E) Pending activities on a task (uses \`env\`)** |
| 125 | + |
| 126 | +- Model: project.task |
| 127 | +- Message: There are \${cnt} pending activities. |
| 128 | +- Message Value Code (multi-line with result): |
| 129 | + |
| 130 | +``` python |
| 131 | +cnt = env["mail.activity"].search_count([("res_model","=",record._name),("res_id","=",record.id)]) |
| 132 | +result = {"visible": cnt > 0, "values": {"cnt": cnt}} |
| 133 | +``` |
| 134 | + |
| 135 | +**F) Product is missing internal reference (uses trigger fields)** |
| 136 | + |
| 137 | +- Model: product.template |
| 138 | +- Trigger Fields: default_code |
| 139 | +- Message: Make sure to set an internal reference! |
| 140 | +- Message Value Code: |
| 141 | + |
| 142 | +``` python |
| 143 | +{"visible": not bool(draft.default_code)} |
| 144 | +``` |
| 145 | + |
| 146 | +**G) HTML banner linking to the customer's last sales order (uses |
| 147 | +trigger fields)** |
| 148 | + |
| 149 | +- Model: sale.order |
| 150 | +- Trigger Fields: partner_id |
| 151 | +- Message: (leave blank; html provided by Message Value Code) |
| 152 | +- Message Value Code (multi-line with result): |
| 153 | + |
| 154 | +``` python |
| 155 | +domain = [("partner_id", "=", draft.partner_id.id)] |
| 156 | +if record_id: |
| 157 | + domain += [("id", "<", record_id)] |
| 158 | +last = model.search(domain, order="date_order desc, id desc", limit=1) |
| 159 | +if last: |
| 160 | + html = "<strong>Previous order:</strong> <a href='%s'>%s</a>" % (url_for(last), last.name) |
| 161 | + result = {"visible": True, "html": html} |
| 162 | +else: |
| 163 | + result = {"visible": False} |
| 164 | +``` |
0 commit comments