Compare commits
3 Commits
cx_web_ref
...
17.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e597f4737 | ||
| 2fab2467cc | |||
| defe3dd68b |
@@ -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.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# Copyright 2025 Cetmix OÜ
|
|
||||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
|
||||||
|
|
||||||
from . import models
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
@@ -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 ""
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# Copyright 2025 Cetmix OÜ
|
|
||||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
|
||||||
|
|
||||||
from . import res_users
|
|
||||||
@@ -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)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[build-system]
|
|
||||||
requires = ["whool"]
|
|
||||||
build-backend = "whool.buildapi"
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
* Cetmix
|
|
||||||
|
|
||||||
@@ -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**.
|
|
||||||
@@ -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 |
@@ -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">["form", "kanban"]</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">"sales_team.group_sale_salesman"</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">"res.users"</span><span class="p">]</span><span class="o">.</span><span class="n">search</span><span class="p">([(</span><span class="s2">"groups_id"</span><span class="p">,</span> <span class="s2">"in"</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">"crm.lead"</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">"kanban"</span><span class="p">,</span> <span class="s2">"form"</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>
|
|
||||||
@@ -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);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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
|
|
||||||
@@ -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"], [])
|
|
||||||
Reference in New Issue
Block a user