3 Commits

18 changed files with 0 additions and 1434 deletions

View File

@@ -1,127 +0,0 @@
========================
Web Refresh From Backend
========================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:e841ff66d3bfff0a3de22c9be5dc40f1ca539739f5487a9162fdf887fc5ac6ad
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github
:target: https://github.com/cetmix/cetmix-tower/tree/16.0/cx_web_refresh_from_backend
:alt: cetmix/cetmix-tower
|badge1| |badge2| |badge3|
Backend UI Reload Module
========================
This is a **technical module** that allows triggering a **UI reload**
from the backend. It enables triggering the reload action for selected
users and record IDs.
--------------
🔧 Helper Function: ``reload_views``
------------------------------------
A special helper function ``reload_views`` is added to the ``res.users``
model.
**Arguments**
~~~~~~~~~~~~~
+----------------+--------------------------+--------------------------+
| Argument | Type | Description |
+================+==========================+==========================+
| **model** | ``Char`` | Model name, e.g. |
| | | ``'res.partner'`` |
+----------------+--------------------------+--------------------------+
| **view_types** | ``List of Char`` | View types to reload, |
| | *(optional)* | e.g. |
| | | ``["form", "kanban"]``. |
| | | Leave blank to reload |
| | | all views. |
+----------------+--------------------------+--------------------------+
| **rec_ids** | ``List of Integer`` | The view will be |
| | *(optional)* | reloaded only if a |
| | | record with an ID from |
| | | this list is present in |
| | | the view. |
+----------------+--------------------------+--------------------------+
--------------
⚠️ Important Notes
------------------
Use this function **wisely**.
When reloading **form views**, be aware that if a user is currently
editing a record, **their unsaved updates may be lost**.
**Table of contents**
.. contents::
:local:
Usage
=====
🧩 Example Usage
----------------
Below is a code snippet showing how to use the ``reload_views`` helper
function.
.. code:: python
# Reload the kanban and form views for all salespeople when an opportunity is won
# Will reload views only if the current opportunity is being displayed
group_id = self.env.ref("sales_team.group_sale_salesman").id
users_to_reload = self.env["res.users"].search([("groups_id", "in", [group_id])])
users_to_reload.reload_views(
model="crm.lead",
view_types=["kanban", "form"],
rec_ids=[self.id],
)
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/cetmix/cetmix-tower/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cx_web_refresh_from_backend%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* Cetmix
Contributors
------------
- Cetmix
Maintainers
-----------
This module is part of the `cetmix/cetmix-tower <https://github.com/cetmix/cetmix-tower/tree/16.0/cx_web_refresh_from_backend>`_ project on GitHub.
You are welcome to contribute.

View File

@@ -1,4 +0,0 @@
# Copyright 2025 Cetmix OÜ
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from . import models

View File

@@ -1,26 +0,0 @@
# Copyright 2025 Cetmix OÜ
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
# Mail is required: its ir.websocket override subscribes the partner channel to the
# bus, so users receive web.refresh_view notifications.
{
"name": "Web Refresh From Backend",
"summary": "Refresh frontend views from backend",
"version": "16.0.1.0.0",
"category": "Web",
"license": "LGPL-3",
"author": "Cetmix",
"website": "https://tower.cetmix.com",
"images": ["static/description/banner.png"],
"depends": ["mail"],
"assets": {
"web.assets_backend": [
"cx_web_refresh_from_backend/static/src/views/list/list_controller_patch.esm.js",
"cx_web_refresh_from_backend/static/src/views/kanban/kanban_controller_patch.esm.js",
"cx_web_refresh_from_backend/static/src/views/form/form_controller_patch.esm.js",
],
},
"installable": True,
"auto_install": False,
}

View File

@@ -1,97 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * cx_web_refresh_from_backend
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: cx_web_refresh_from_backend
#. odoo-javascript
#: code:addons/cx_web_refresh_from_backend/static/src/views/form/form_controller_patch.esm.js:0
#, python-format
msgid "All unsaved changes will be lost! Continue?"
msgstr ""
#. module: cx_web_refresh_from_backend
#. odoo-javascript
#: code:addons/cx_web_refresh_from_backend/static/src/views/form/form_controller_patch.esm.js:0
#: code:addons/cx_web_refresh_from_backend/static/src/views/list/list_controller_patch.esm.js:0
#, python-format
msgid "Cancel"
msgstr ""
#. module: cx_web_refresh_from_backend
#. odoo-javascript
#: code:addons/cx_web_refresh_from_backend/static/src/views/form/form_controller_patch.esm.js:0
#, python-format
msgid "Continue"
msgstr ""
#. module: cx_web_refresh_from_backend
#. odoo-javascript
#: code:addons/cx_web_refresh_from_backend/static/src/views/form/form_controller_patch.esm.js:0
#, python-format
msgid "Could not reload form. "
msgstr ""
#. module: cx_web_refresh_from_backend
#. odoo-javascript
#: code:addons/cx_web_refresh_from_backend/static/src/views/kanban/kanban_controller_patch.esm.js:0
#, python-format
msgid "Could not reload kanban. "
msgstr ""
#. module: cx_web_refresh_from_backend
#. odoo-javascript
#: code:addons/cx_web_refresh_from_backend/static/src/views/list/list_controller_patch.esm.js:0
#, python-format
msgid "Could not reload list. "
msgstr ""
#. module: cx_web_refresh_from_backend
#. odoo-javascript
#: code:addons/cx_web_refresh_from_backend/static/src/views/list/list_controller_patch.esm.js:0
#, python-format
msgid "Could not save record. "
msgstr ""
#. module: cx_web_refresh_from_backend
#. odoo-javascript
#: code:addons/cx_web_refresh_from_backend/static/src/views/form/form_controller_patch.esm.js:0
#, python-format
msgid "Form is being refreshed from backend"
msgstr ""
#. module: cx_web_refresh_from_backend
#. odoo-javascript
#: code:addons/cx_web_refresh_from_backend/static/src/views/list/list_controller_patch.esm.js:0
#, python-format
msgid "List is being refreshed from backend"
msgstr ""
#. module: cx_web_refresh_from_backend
#. odoo-javascript
#: code:addons/cx_web_refresh_from_backend/static/src/views/list/list_controller_patch.esm.js:0
#, python-format
msgid "Save & Refresh"
msgstr ""
#. module: cx_web_refresh_from_backend
#: model:ir.model,name:cx_web_refresh_from_backend.model_res_users
msgid "User"
msgstr ""
#. module: cx_web_refresh_from_backend
#. odoo-javascript
#: code:addons/cx_web_refresh_from_backend/static/src/views/list/list_controller_patch.esm.js:0
#, python-format
msgid "You have unsaved edits. Save them before refreshing?"
msgstr ""

View File

@@ -1,4 +0,0 @@
# Copyright 2025 Cetmix OÜ
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from . import res_users

View File

@@ -1,50 +0,0 @@
# Copyright 2025 Cetmix OÜ
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import models
class ResUsers(models.Model):
_inherit = "res.users"
def reload_views(self, model, view_types=None, rec_ids=None):
"""
Trigger UI reload for selected users and record IDs.
This method allows to reload specific views from the backend.
Be aware that when reloading form views, if a user is currently
doing some updates, those updates may be lost.
:param model: str, Model name (e.g., 'res.partner')
:param view_types: list of str, optional, View types to reload
(e.g., ['form', 'kanban']). Leave blank to reload all views.
:param rec_ids: list of int, optional, View will be reloaded only if a record
with id from the list is present in the view.
Example usage:
# Reload the kanban and form views for all salespeople
# when an opportunity is won.
# Will reload views only if the current opportunity is being displayed
group_id = self.env.ref("sales_team.group_sale_salesman").id
users_to_reload = self.env["res.users"].search(
[("groups_id", "in", [group_id])]
)
users_to_reload.reload_views(
model="crm.lead",
view_types=["kanban", "form"],
rec_ids=[self.id]
)
"""
# Prepare the message payload
bus_message = {
"model": model,
"view_types": view_types or [],
"rec_ids": rec_ids or [],
}
# Send notification to each user's partner
notifications = [
[user.partner_id, "web.refresh_view", bus_message] for user in self
]
self.env["bus.bus"]._sendmany(notifications)

View File

@@ -1,3 +0,0 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@@ -1,2 +0,0 @@
* Cetmix

View File

@@ -1,27 +0,0 @@
# Backend UI Reload Module
This is a **technical module** that allows triggering a **UI reload** from the backend.
It enables triggering the reload action for selected users and record IDs.
---
## 🔧 Helper Function: `reload_views`
A special helper function `reload_views` is added to the `res.users` model.
### **Arguments**
| Argument | Type | Description |
|-----------|------|-------------|
| **model** | `Char` | Model name, e.g. `'res.partner'` |
| **view_types** | `List of Char` *(optional)* | View types to reload, e.g. `["form", "kanban"]`. Leave blank to reload all views. |
| **rec_ids** | `List of Integer` *(optional)* | The view will be reloaded only if a record with an ID from this list is present in the view. |
---
## ⚠️ Important Notes
Use this function **wisely**.
When reloading **form views**, be aware that if a user is currently editing a record,
**their unsaved updates may be lost**.

View File

@@ -1,16 +0,0 @@
## 🧩 Example Usage
Below is a code snippet showing how to use the `reload_views` helper function.
```python
# Reload the kanban and form views for all salespeople when an opportunity is won
# Will reload views only if the current opportunity is being displayed
group_id = self.env.ref("sales_team.group_sale_salesman").id
users_to_reload = self.env["res.users"].search([("groups_id", "in", [group_id])])
users_to_reload.reload_views(
model="crm.lead",
view_types=["kanban", "form"],
rec_ids=[self.id],
)
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -1,484 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Web Refresh From Backend</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="web-refresh-from-backend">
<h1 class="title">Web Refresh From Backend</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:e841ff66d3bfff0a3de22c9be5dc40f1ca539739f5487a9162fdf887fc5ac6ad
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/license-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/cetmix/cetmix-tower/tree/16.0/cx_web_refresh_from_backend"><img alt="cetmix/cetmix-tower" src="https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github" /></a></p>
<div class="section" id="backend-ui-reload-module">
<h1>Backend UI Reload Module</h1>
<p>This is a <strong>technical module</strong> that allows triggering a <strong>UI reload</strong>
from the backend. It enables triggering the reload action for selected
users and record IDs.</p>
<hr class="docutils" />
<div class="section" id="helper-function-reload-views">
<h2>🔧 Helper Function: <tt class="docutils literal">reload_views</tt></h2>
<p>A special helper function <tt class="docutils literal">reload_views</tt> is added to the <tt class="docutils literal">res.users</tt>
model.</p>
<div class="section" id="arguments">
<h3><strong>Arguments</strong></h3>
<table border="1" class="docutils">
<colgroup>
<col width="24%" />
<col width="38%" />
<col width="38%" />
</colgroup>
<thead valign="bottom">
<tr><th class="head">Argument</th>
<th class="head">Type</th>
<th class="head">Description</th>
</tr>
</thead>
<tbody valign="top">
<tr><td><strong>model</strong></td>
<td><tt class="docutils literal">Char</tt></td>
<td>Model name, e.g.
<tt class="docutils literal">'res.partner'</tt></td>
</tr>
<tr><td><strong>view_types</strong></td>
<td><tt class="docutils literal">List of Char</tt>
<em>(optional)</em></td>
<td>View types to reload,
e.g.
<tt class="docutils literal">[&quot;form&quot;, &quot;kanban&quot;]</tt>.
Leave blank to reload
all views.</td>
</tr>
<tr><td><strong>rec_ids</strong></td>
<td><tt class="docutils literal">List of Integer</tt>
<em>(optional)</em></td>
<td>The view will be
reloaded only if a
record with an ID from
this list is present in
the view.</td>
</tr>
</tbody>
</table>
</div>
</div>
<hr class="docutils" />
<div class="section" id="important-notes">
<h2>⚠️ Important Notes</h2>
<p>Use this function <strong>wisely</strong>.</p>
<p>When reloading <strong>form views</strong>, be aware that if a user is currently
editing a record, <strong>their unsaved updates may be lost</strong>.</p>
<p><strong>Table of contents</strong></p>
</div>
</div>
<div class="section" id="usage">
<h1>Usage</h1>
<div class="section" id="example-usage">
<h2>🧩 Example Usage</h2>
<p>Below is a code snippet showing how to use the <tt class="docutils literal">reload_views</tt> helper
function.</p>
<pre class="code python literal-block">
<span class="c1"># Reload the kanban and form views for all salespeople when an opportunity is won</span><span class="w">
</span><span class="c1"># Will reload views only if the current opportunity is being displayed</span><span class="w">
</span><span class="n">group_id</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">ref</span><span class="p">(</span><span class="s2">&quot;sales_team.group_sale_salesman&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">id</span><span class="w">
</span><span class="n">users_to_reload</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="p">[</span><span class="s2">&quot;res.users&quot;</span><span class="p">]</span><span class="o">.</span><span class="n">search</span><span class="p">([(</span><span class="s2">&quot;groups_id&quot;</span><span class="p">,</span> <span class="s2">&quot;in&quot;</span><span class="p">,</span> <span class="p">[</span><span class="n">group_id</span><span class="p">])])</span><span class="w">
</span><span class="n">users_to_reload</span><span class="o">.</span><span class="n">reload_views</span><span class="p">(</span><span class="w">
</span> <span class="n">model</span><span class="o">=</span><span class="s2">&quot;crm.lead&quot;</span><span class="p">,</span><span class="w">
</span> <span class="n">view_types</span><span class="o">=</span><span class="p">[</span><span class="s2">&quot;kanban&quot;</span><span class="p">,</span> <span class="s2">&quot;form&quot;</span><span class="p">],</span><span class="w">
</span> <span class="n">rec_ids</span><span class="o">=</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">id</span><span class="p">],</span><span class="w">
</span><span class="p">)</span>
</pre>
</div>
</div>
<div class="section" id="bug-tracker">
<h1>Bug Tracker</h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cx_web_refresh_from_backend%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1>Credits</h1>
<div class="section" id="authors">
<h2>Authors</h2>
<ul class="simple">
<li>Cetmix</li>
</ul>
</div>
<div class="section" id="contributors">
<h2>Contributors</h2>
<ul class="simple">
<li>Cetmix</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2>Maintainers</h2>
<p>This module is part of the <a class="reference external" href="https://github.com/cetmix/cetmix-tower/tree/16.0/cx_web_refresh_from_backend">cetmix/cetmix-tower</a> project on GitHub.</p>
<p>You are welcome to contribute.</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,181 +0,0 @@
/** @odoo-module **/
import {FormController} from "@web/views/form/form_controller";
import {patch} from "@web/core/utils/patch";
import {useService} from "@web/core/utils/hooks";
import {onWillUnmount} from "@odoo/owl";
import {ConfirmationDialog} from "@web/core/confirmation_dialog/confirmation_dialog";
// Patch the standard FormController to react on bus notifications
patch(FormController.prototype, "cx_web_refresh_from_backend.FormController", {
setup() {
// Call original setup logic
this._super(...arguments);
// Get core services used by this behavior
this.busService = useService("bus_service");
this.actionService = useService("action");
this.notificationService = useService("notification");
// Timestamp of last local save (used to avoid immediate auto-refresh)
this._lastLocalSave = null;
// Bind the handler to keep reference for cleanup
this._boundBusHandler = this._onBusNotification.bind(this);
// Subscribe to bus notifications
this.busService.addEventListener("notification", this._boundBusHandler);
// Cleanup subscription on component unmount
onWillUnmount(() => {
if (this.busService && this._boundBusHandler) {
this.busService.removeEventListener(
"notification",
this._boundBusHandler
);
}
});
},
/**
* Handle bus notification for view refresh.
* Listens for notifications with type "web.refresh_view" and delegates
* processing to _handleViewRefresh.
*
* @param {Event} event - Bus notification event
*/
async _onBusNotification({detail: notifications}) {
// Check if component is still alive
if (!this.model || !this.model.root) {
return;
}
for (const {payload, type} of notifications) {
if (type === "web.refresh_view") {
await this._handleViewRefresh(payload);
}
}
},
/**
* Handle view refresh notification.
*
* Only refreshes when:
* - model matches current form model
* - requested view types include "form" (if specified)
* - record id matches current record (if specified)
*
* @param {Object} notification - Notification payload
*/
async _handleViewRefresh(notification) {
const {model, view_types = [], rec_ids = []} = notification;
// Check if the model matches current form model
if (this.props.resModel !== model) {
return;
}
// Check if view_type matches (if specified in notification)
if (view_types.length > 0 && !view_types.includes("form")) {
return;
}
// Check if record ID matches (if rec_ids is specified)
const currentResId = this.model && this.model.root && this.model.root.resId;
if (rec_ids.length > 0 && (!currentResId || !rec_ids.includes(currentResId))) {
return;
}
// Skip refresh when form is in a dialog or when a wizard is on top of the stack.
// Refreshing in that context can leave wizard/confirmation dialogs stuck open
// (e.g. confirm="..." in wizard view).
if (this.env.inDialog) {
return;
}
const currentController = this.actionService.currentController;
const currentAction = currentController && currentController.action;
if (currentAction && currentAction.target === "new") {
return;
}
await this.refreshForm();
},
/**
* Refresh the form with actual data from server.
*
* For normal forms:
* - if record is clean: perform a soft_reload action
* - if record has unsaved changes: ask for confirmation, then reload
*
* For wizards (dialogs, target="new"):
* - reload only the current record without full action reload
*
* @returns {Promise<void>}
*/
async refreshForm() {
// Do not refresh immediately after an explicit save (debounce window)
if (this._lastLocalSave && Date.now() - this._lastLocalSave < 1000) {
return;
}
if (!this.model || !this.model.root) {
return;
}
// Check if this form is opened as a wizard (dialog)
const currentController = this.actionService.currentController;
const action = currentController && currentController.action;
const isWizard = action && action.target === "new";
const record = this.model.root;
if (!isWizard && record.isDirty) {
// Ask user whether to discard unsaved changes before refreshing
const confirmed = await new Promise((resolve) => {
this.dialogService.add(ConfirmationDialog, {
title: this.env._t("Form is being refreshed from backend"),
body: this.env._t("All unsaved changes will be lost! Continue?"),
confirm: () => resolve(true),
cancel: () => resolve(false),
confirmLabel: this.env._t("Continue"),
cancelLabel: this.env._t("Cancel"),
});
});
if (!confirmed) {
return;
}
}
try {
await record.load();
} catch (error) {
const message =
(error && error.data && error.data.message) ||
(error && error.message) ||
String(error);
this.notificationService.add(
this.env._t("Could not reload form. ") + message,
{type: "danger"}
);
return;
}
// Update the view (only if component is still mounted)
if (this.model && this.model.root) {
this.render(true);
}
},
/**
* Override of save button handler.
*
* Stores timestamp of last local save to avoid immediate auto-refresh
* triggered by our own changes.
*/
async saveButtonClicked() {
this._lastLocalSave = Date.now();
return await this._super(...arguments);
},
});

View File

@@ -1,137 +0,0 @@
/** @odoo-module **/
import {KanbanController} from "@web/views/kanban/kanban_controller";
import {patch} from "@web/core/utils/patch";
import {useService} from "@web/core/utils/hooks";
import {onWillUnmount} from "@odoo/owl";
patch(KanbanController.prototype, "cx_web_refresh_from_backend.KanbanController", {
setup() {
this._super(...arguments);
this.busService = useService("bus_service");
this.notificationService = useService("notification");
// Bind the handler to keep reference for cleanup
this._boundBusHandler = this._onBusNotification.bind(this);
// Subscribe to bus notifications
this.busService.addEventListener("notification", this._boundBusHandler);
// Cleanup on unmount
onWillUnmount(() => {
if (this.busService && this._boundBusHandler) {
this.busService.removeEventListener(
"notification",
this._boundBusHandler
);
}
});
},
/**
* Handle bus notification for view refresh
* @param {Event} event - Bus notification event
*/
async _onBusNotification({detail: notifications}) {
// Check if component is still alive
if (!this.model || !this.model.root) {
return;
}
for (const {payload, type} of notifications) {
if (type === "web.refresh_view") {
await this._handleViewRefresh(payload);
}
}
},
/**
* Handle view refresh notification
* @param {Object} notification - Notification payload
*/
async _handleViewRefresh(notification) {
const {model, view_types = [], rec_ids = []} = notification;
// Check if the model matches
if (this.props.resModel !== model) {
return;
}
// Check if view_type matches (if specified)
if (view_types.length > 0 && !view_types.includes("kanban")) {
return;
}
// Check if record ID matches (if rec_ids is specified)
if (rec_ids.length > 0) {
const loadedIds = this.getLoadedRecordIds();
const shouldReload = loadedIds.some((id) => rec_ids.includes(id));
if (!shouldReload) {
return;
}
}
await this.refreshList();
},
/**
* Refresh the kanban with actual data from server
* @returns {Promise<void>}
*/
async refreshList() {
// Safety check: component might be destroyed
if (!this.model || !this.model.root) {
return;
}
const list = this.model.root;
// Reload data from server
try {
await list.load();
} catch (error) {
const message =
(error && error.data && error.data.message) ||
(error && error.message) ||
String(error);
this.notificationService.add(
this.env._t("Could not reload kanban. ") + message,
{type: "danger"}
);
return;
}
// Update the view (only if component is still mounted)
if (this.model && this.model.root) {
this.render(true);
}
},
/**
* Get IDs of all loaded records on the current page
* @returns {Array<Number>} Array of record IDs
*/
getLoadedRecordIds() {
const list = this.model.root;
if (list.isGrouped) {
// For grouped kanban, collect IDs from all groups
const recordIds = [];
const collectIds = (groups) => {
for (const group of groups) {
if (group.list && group.list.records) {
recordIds.push(...group.list.records.map((r) => r.resId));
}
if (group.groups) {
collectIds(group.groups);
}
}
};
collectIds(list.groups);
return recordIds;
}
// For regular kanban, return IDs of all records
return list.records.map((record) => record.resId);
},
});

View File

@@ -1,177 +0,0 @@
/** @odoo-module **/
import {ListController} from "@web/views/list/list_controller";
import {patch} from "@web/core/utils/patch";
import {useService} from "@web/core/utils/hooks";
import {onWillUnmount} from "@odoo/owl";
import {ConfirmationDialog} from "@web/core/confirmation_dialog/confirmation_dialog";
patch(ListController.prototype, "cx_web_refresh_from_backend.ListController", {
setup() {
this._super(...arguments);
this.busService = useService("bus_service");
this.dialogService = useService("dialog");
this.notificationService = useService("notification");
// Bind the handler to keep reference for cleanup
this._boundBusHandler = this._onBusNotification.bind(this);
// Subscribe to bus notifications
this.busService.addEventListener("notification", this._boundBusHandler);
// Cleanup on unmount
onWillUnmount(() => {
if (this.busService && this._boundBusHandler) {
this.busService.removeEventListener(
"notification",
this._boundBusHandler
);
}
});
},
/**
* Handle bus notification for view refresh
* @param {Event} event - Bus notification event
*/
async _onBusNotification({detail: notifications}) {
// Check if component is still alive
if (!this.model || !this.model.root) {
return;
}
for (const {payload, type} of notifications) {
if (type === "web.refresh_view") {
await this._handleViewRefresh(payload);
}
}
},
/**
* Handle view refresh notification
* @param {Object} notification - Notification payload
*/
async _handleViewRefresh(notification) {
const {model, view_types = [], rec_ids = []} = notification;
// Check if the model matches
if (this.props.resModel !== model) {
return;
}
// Check if view_type matches (if specified)
if (
view_types.length > 0 &&
!view_types.includes("list") &&
!view_types.includes("tree")
) {
return;
}
// Check if record ID matches (if rec_ids is specified)
if (rec_ids.length > 0) {
const loadedIds = this.getLoadedRecordIds();
const shouldReload = loadedIds.some((id) => rec_ids.includes(id));
if (!shouldReload) {
return;
}
}
await this.refreshList();
},
/**
* Refresh the list with actual data from server.
* If there is an edited record, asks the user to save or cancel.
*
* @returns {Promise<void>}
*/
async refreshList() {
// Safety check: component might be destroyed
if (!this.model || !this.model.root) {
return;
}
const list = this.model.root;
if (list.editedRecord) {
const confirmed = await new Promise((resolve) => {
this.dialogService.add(ConfirmationDialog, {
title: this.env._t("List is being refreshed from backend"),
body: this.env._t(
"You have unsaved edits. Save them before refreshing?"
),
confirm: () => resolve(true),
cancel: () => resolve(false),
confirmLabel: this.env._t("Save & Refresh"),
cancelLabel: this.env._t("Cancel"),
});
});
if (!confirmed) {
return;
}
try {
await list.editedRecord.save();
} catch (error) {
const message =
(error && error.data && error.data.message) ||
(error && error.message) ||
String(error);
this.notificationService.add(
this.env._t("Could not save record. ") + message,
{type: "danger"}
);
return;
}
}
// Reload data from server
try {
await list.load();
} catch (error) {
const message =
(error && error.data && error.data.message) ||
(error && error.message) ||
String(error);
this.notificationService.add(
this.env._t("Could not reload list. ") + message,
{type: "danger"}
);
return;
}
// Update the view (only if component is still mounted)
if (this.model && this.model.root) {
this.render(true);
}
},
/**
* Get IDs of all loaded records on the current page
* @returns {Array<Number>} Array of record IDs
*/
getLoadedRecordIds() {
const list = this.model.root;
if (list.isGrouped) {
// For grouped list, collect IDs from all groups
const recordIds = [];
const collectIds = (groups) => {
for (const group of groups) {
if (group.list && group.list.records) {
recordIds.push(...group.list.records.map((r) => r.resId));
}
if (group.groups) {
collectIds(group.groups);
}
}
};
collectIds(list.groups);
return recordIds;
}
// For regular list, return IDs of all records
return list.records.map((record) => record.resId);
},
});

View File

@@ -1,4 +0,0 @@
# Copyright 2025 Cetmix OÜ
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from . import test_reload_views

View File

@@ -1,95 +0,0 @@
# Copyright 2025 Cetmix OÜ
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from unittest.mock import patch
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
@tagged("post_install", "-at_install")
class TestReloadViews(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user_admin = cls.env.ref("base.user_admin")
cls.user_demo = cls.env.ref("base.user_demo")
cls.partner = cls.env["res.partner"].create(
{
"name": "Test Partner",
}
)
def test_reload_views_basic(self):
"""Test basic reload_views call without parameters"""
with patch.object(type(self.env["bus.bus"]), "_sendmany") as mock_sendmany:
self.user_admin.reload_views(model="res.partner")
mock_sendmany.assert_called_once()
# Get the notifications list - it's the first positional argument
notifications = mock_sendmany.call_args[0][0]
self.assertEqual(len(notifications), 1)
partner, channel, message = notifications[0]
self.assertEqual(partner, self.user_admin.partner_id)
self.assertEqual(channel, "web.refresh_view")
self.assertEqual(message["model"], "res.partner")
self.assertEqual(message["view_types"], [])
self.assertEqual(message["rec_ids"], [])
def test_reload_views_with_params(self):
"""Test reload_views with view_types and rec_ids parameters"""
with patch.object(type(self.env["bus.bus"]), "_sendmany") as mock_sendmany:
self.user_admin.reload_views(
model="res.partner",
view_types=["form", "kanban"],
rec_ids=[self.partner.id],
)
notifications = mock_sendmany.call_args[0][0]
message = notifications[0][2]
self.assertEqual(message["view_types"], ["form", "kanban"])
self.assertEqual(message["rec_ids"], [self.partner.id])
def test_reload_views_multiple_users(self):
"""Test reload_views for multiple users at once"""
users = self.user_admin | self.user_demo
with patch.object(type(self.env["bus.bus"]), "_sendmany") as mock_sendmany:
users.reload_views(model="res.partner")
notifications = mock_sendmany.call_args[0][0]
self.assertEqual(len(notifications), 2)
# Verify both users' partners are notified
notified_partners = {n[0] for n in notifications}
expected_partners = {self.user_admin.partner_id, self.user_demo.partner_id}
self.assertEqual(notified_partners, expected_partners)
def test_reload_views_recordset(self):
"""Test reload_views on a multi-record user recordset.
Ensures that calling reload_views on a recordset still results in a
single _sendmany call, with one notification entry per user.
"""
users = self.user_admin | self.user_demo
with patch.object(type(self.env["bus.bus"]), "_sendmany") as mock_sendmany:
users.reload_views(model="res.partner")
# _sendmany should be called only once for the whole recordset
mock_sendmany.assert_called_once()
notifications = mock_sendmany.call_args[0][0]
# We expect one notification tuple per user in the recordset
self.assertEqual(len(notifications), 2)
# Verify both users' partners are notified and payload is correct
for partner, channel, message in notifications:
self.assertIn(
partner, {self.user_admin.partner_id, self.user_demo.partner_id}
)
self.assertEqual(channel, "web.refresh_view")
self.assertEqual(message["model"], "res.partner")
self.assertEqual(message["view_types"], [])
self.assertEqual(message["rec_ids"], [])