diff --git a/addons/web/static/src/core/errors/error_dialogs.xml b/addons/web/static/src/core/errors/error_dialogs.xml index a26cab4cded..848d16a9d30 100644 --- a/addons/web/static/src/core/errors/error_dialogs.xml +++ b/addons/web/static/src/core/errors/error_dialogs.xml @@ -85,7 +85,7 @@
         
         
-          
+          
         
       
     
diff --git a/addons/web/static/src/core/errors/scss_error_dialog.js b/addons/web/static/src/core/errors/scss_error_dialog.js
index 17578758a93..bdb718d23bb 100644
--- a/addons/web/static/src/core/errors/scss_error_dialog.js
+++ b/addons/web/static/src/core/errors/scss_error_dialog.js
@@ -13,11 +13,16 @@ ScssErrorDialog.title = _t("Style error");
 const scssErrorDisplayService = {
     dependencies: ["dialog"],
     start(env, { dialog }) {
-        if (window.__odooScssCompilationError) {
-            dialog.add(ScssErrorDialog, {
-                message: window.__odooScssCompilationError,
-            });
+        const assets = [...document.styleSheets].filter((sheet) => sheet.href?.includes("/web/assets"));
+        for (const { cssRules } of assets) {
+            const lastRule = cssRules?.[cssRules?.length - 1];
+            if (lastRule?.selectorText === "css_error_message") {
+                dialog.add(ScssErrorDialog, {
+                    message: lastRule.style.content.replaceAll("\\a", "\n").replaceAll("\\*", "*").replaceAll(`\\"`, `"`),
+                });
+            }
         }
+
     },
 };
 
diff --git a/addons/web/static/src/start.js b/addons/web/static/src/start.js
index 8f6b6719883..49b33c662f5 100644
--- a/addons/web/static/src/start.js
+++ b/addons/web/static/src/start.js
@@ -26,13 +26,13 @@ export async function startWebClient(Webclient) {
     odoo.isReady = false;
 
     // setup environment
+    await whenReady();
     const env = makeEnv();
     await startServices(env);
 
     Component.env = env;
 
     // start web client
-    await whenReady();
     const app = new App(Webclient, {
         name: "Odoo Web Client",
         env,
diff --git a/odoo/addons/base/models/assetsbundle.py b/odoo/addons/base/models/assetsbundle.py
index c6e483d372b..815f9e3ef36 100644
--- a/odoo/addons/base/models/assetsbundle.py
+++ b/odoo/addons/base/models/assetsbundle.py
@@ -115,33 +115,18 @@ class AssetsBundle(object):
         """
         response = []
         if self.has_css and self.stylesheets:
-            css_attachments = self.css(is_minified=not self.is_debug_assets) or []
-            for attachment in css_attachments:
-                if self.is_debug_assets:
-                    href = self.get_debug_asset_url(extra=self.extra('css'),
-                                                    name=css_attachments.name,
-                                                    extension='')
-                else:
-                    href = attachment.url
-                response.append((href, None))
-            if self.css_errors:
-                msg = '\n'.join(self.css_errors).replace('"', '\\"').replace('\n', '\\n')
-                js_error = """
-                    window.__odooScssCompilationError = "%s";
-                    console.error("SCSS compilation failure:", window.__odooScssCompilationError);
-                    window.addEventListener("DOMContentLoaded", () => {
-                        if (!odoo || !odoo.define) {
-                            alert(window.__odooScssCompilationError);
-                        }
-                    });
-                """ % msg
-                response.append((None, js_error))
-                response.append(('/web/static/lib/bootstrap/dist/css/bootstrap.css', None))
+            css_attachment = self.css(is_minified=not self.is_debug_assets)
+            if self.is_debug_assets:
+                response.append((self.get_debug_asset_url(extra=self.extra('css'), name=css_attachment.name, extension=''), None))
+            else:
+                response.append((css_attachment.url, None))
 
         if self.has_js and self.javascripts:
             js_attachment = self.js(is_minified=not self.is_debug_assets)
-            src = self.get_debug_asset_url(name=js_attachment.name, extension='') if self.is_debug_assets else js_attachment[0].url
-            response.append((src, None))
+            if self.is_debug_assets:
+                response.append((self.get_debug_asset_url(extra=self.extra('js'), name=js_attachment.name, extension=''), None))
+            else:
+                response.append((js_attachment.url, None))
 
         return self.external_assets + response
 
@@ -578,25 +563,47 @@ class AssetsBundle(object):
     def css(self, is_minified=True):
         extension = 'min.css' if is_minified else 'css'
         attachments = self.get_attachments(extension)
-        if not attachments:
-            # get css content
-            css = self.preprocess_css()
-            if self.css_errors:
-                return self.get_attachments(extension, ignore_version=True)
+        if attachments:
+            return attachments
 
-            matches = []
-            css = re.sub(self.rx_css_import, lambda matchobj: matches.append(matchobj.group(0)) and '', css)
+        css = self.preprocess_css()
+        if self.css_errors:
+            error_message = '\n'.join(self.css_errors).replace('"', '\\"').replace('\n', '\A').replace('*', '\*')
+            previous_attachment = self.get_attachments(extension, ignore_version=True)
+            previous_css = previous_attachment.raw.decode() if previous_attachment else ''
+            css_error_message_header = '\n\n/* ## CSS error message ##*/'
+            previous_css = previous_css.split(css_error_message_header)[0]
+            css = css_error_message_header.join([
+                previous_css, """
+body::before {
+  font-weight: bold;
+  content: "A css error occured, using an old style to render this page";
+  position: fixed;
+  left: 0;
+  bottom: 0;
+  z-index: 100000000000;
+  background-color: #C00;
+  color: #DDD;
+}
 
-            if is_minified:
-                # move up all @import rules to the top
-                matches.append(css)
-                css = u'\n'.join(matches)
+css_error_message {
+  content: "%s";
+}
+""" % error_message
+            ])
+            return self.save_attachment(extension, css)
 
-                self.save_attachment(extension, css)
-                attachments = self.get_attachments(extension)
-            else:
-                return self.css_with_sourcemap(u'\n'.join(matches))
-        return attachments
+        matches = []
+        css = re.sub(self.rx_css_import, lambda matchobj: matches.append(matchobj.group(0)) and '', css)
+
+        if is_minified:
+            # move up all @import rules to the top
+            matches.append(css)
+            css = u'\n'.join(matches)
+
+            return self.save_attachment(extension, css)
+        else:
+            return self.css_with_sourcemap(u'\n'.join(matches))
 
     def css_with_sourcemap(self, content_import_rules):
         """Create the ir.attachment representing the not-minified content of the bundleCSS
diff --git a/odoo/addons/test_assetsbundle/__manifest__.py b/odoo/addons/test_assetsbundle/__manifest__.py
index 470d0ad2f49..1ab5561d25b 100644
--- a/odoo/addons/test_assetsbundle/__manifest__.py
+++ b/odoo/addons/test_assetsbundle/__manifest__.py
@@ -12,6 +12,9 @@
     ],
 
     'assets': {
+        'web.assets_tests': [
+            'test_assetsbundle/static/tests/test_css_error.js',
+        ],
         'test_assetsbundle.bundle2': [
             'test_assetsbundle/static/src/css/test_cssfile1.css',
         ],
diff --git a/odoo/addons/test_assetsbundle/static/src/css/test_error.scss b/odoo/addons/test_assetsbundle/static/src/css/test_error.scss
new file mode 100644
index 00000000000..88878b2c0b8
--- /dev/null
+++ b/odoo/addons/test_assetsbundle/static/src/css/test_error.scss
@@ -0,0 +1,3 @@
+.rule1 ()){ /* error */
+    color: black;
+}
diff --git a/odoo/addons/test_assetsbundle/static/tests/test_css_error.js b/odoo/addons/test_assetsbundle/static/tests/test_css_error.js
new file mode 100644
index 00000000000..b8cc8e98ba4
--- /dev/null
+++ b/odoo/addons/test_assetsbundle/static/tests/test_css_error.js
@@ -0,0 +1,28 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+
+registry.category("web_tour.tours").add('css_error_tour', {
+    test: true,
+    url: '/web',
+    steps: () => [
+    {
+        content: "Error message",
+        trigger: ".modal-body",
+        run: () => {},
+    },
+]});
+
+
+registry.category("web_tour.tours").add('css_error_tour_frontend', {
+    test: true,
+    url: '/',
+    steps: () => [
+    {
+        content: "Error message",
+        trigger: ".modal-body",
+        run: () => {},
+    },
+]});
+
+// Note, the ideal steap would be `.modal-body:contains('Error: Invalid CSS after \".rule1\": expected selector, was \"()){ /* error */')` but it fails sometimes
diff --git a/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py b/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py
index 41874774b5a..d178a78cfe5 100644
--- a/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py
+++ b/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py
@@ -2028,3 +2028,26 @@ class AssetsNodeOrmCacheUsage(TransactionCase):
         asset_keys, qweb_keys = self.cache_keys()
         self.assertEqual(len(asset_keys), 1, "lazy_load shouldn't create another entry")
         self.assertEqual(len(qweb_keys), 1, "lazy_load shouldn't create another entry")
+
+@tagged('-at_install', 'post_install')
+class TestErrorManagement(HttpCase):
+
+    def test_assets_bundle_css_error_backend(self):
+        self.env['ir.qweb']._get_asset_bundle('web.assets_backend', assets_params={'website_id': self.env['website'].search([], limit=1).id}).css() # force pregeneration so that we have the base style
+        self.env['ir.asset'].create({
+            'name': 'Css error',
+            'bundle': 'web.assets_backend',
+            'path': 'test_assetsbundle/static/src/css/test_error.scss',
+        })
+
+        with mute_logger('odoo.addons.base.models.assetsbundle'):
+            self.start_tour('/web', 'css_error_tour', login='admin')
+
+    def test_assets_bundle_css_error_frontend(self):
+        self.env['ir.asset'].create({
+            'name': 'Css error',
+            'bundle': 'web.assets_frontend',
+            'path': 'test_assetsbundle/static/src/css/test_error.scss',
+        })
+        with mute_logger('odoo.addons.base.models.assetsbundle'):
+            self.start_tour('/', 'css_error_tour_frontend', login='admin')