diff --git a/addons/base_import_module/models/ir_module.py b/addons/base_import_module/models/ir_module.py
index 3ab7aeb4d68f4559f6f2d2d625895307bbee565a..7bca006c1f9c07d662c1cdae79a75bbfba260da3 100644
--- a/addons/base_import_module/models/ir_module.py
+++ b/addons/base_import_module/models/ir_module.py
@@ -104,7 +104,13 @@ class IrModule(models.Model):
                     if attachment:
                         attachment.write(values)
                     else:
-                        IrAttachment.create(values)
+                        attachment = IrAttachment.create(values)
+                        self.env['ir.model.data'].create({
+                            'name': f"attachment_{url_path}".replace('.', '_'),
+                            'model': 'ir.attachment',
+                            'module': module,
+                            'res_id': attachment.id,
+                        })
 
         IrAsset = self.env['ir.asset']
         assets_vals = []
@@ -139,7 +145,7 @@ class IrModule(models.Model):
         # Create new assets and attach 'ir.model.data' records to them
         created_assets = IrAsset.create(assets_to_create)
         self.env['ir.model.data'].create([{
-            'name': f"{asset['bundle']}.{asset['path']}",
+            'name': f"{asset['bundle']}_{asset['path']}".replace(".", "_"),
             'model': 'ir.asset',
             'module': module,
             'res_id': asset.id,
diff --git a/addons/base_import_module/tests/test_cloc.py b/addons/base_import_module/tests/test_cloc.py
index 05611a0cc6f86bf1df6bcd22173524adda53b255..7b393e715730ce03ddd2ac73c4ed85a89ad86ee4 100644
--- a/addons/base_import_module/tests/test_cloc.py
+++ b/addons/base_import_module/tests/test_cloc.py
@@ -1,9 +1,23 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
+import json
+from io import BytesIO
+from zipfile import ZipFile
 
 from odoo.tools import cloc
 from odoo.addons.base.tests import test_cloc
 
+VALID_XML = """
+<templates id="template" xml:space="preserve">
+    <t t-name="stock_barcode.LineComponent">
+        <div t-if="line.picking_id and line.picking_id.origin" name="origin">
+            <i class="fa fa-fw fa-file" />
+            <span t-esc="line.picking_id.origin" />
+        </div>
+    </t>
+</templates>
+"""
+
 class TestClocFields(test_cloc.TestClocCustomization):
 
     def test_fields_from_import_module(self):
@@ -17,12 +31,76 @@ class TestClocFields(test_cloc.TestClocCustomization):
             'imported': True,
         })
         f1 = self.create_field('x_imported_field')
-        self.create_xml_id('import_field', f1.id, 'imported_module')
+        self.create_xml_id('import_field', 'ir.model.fields', f1.id, 'imported_module')
         cl = cloc.Cloc()
         cl.count_customization(self.env)
         self.assertEqual(cl.code.get('imported_module', 0), 1, 'Count fields with xml_id of imported module')
         f2 = self.create_field('x_base_field')
-        self.create_xml_id('base_field', f2.id, 'base')
+        self.create_xml_id('base_field', 'ir.model.fields', f2.id, 'base')
         cl = cloc.Cloc()
         cl.count_customization(self.env)
         self.assertEqual(cl.code.get('base', 0), 0, "Don't count fields from standard module")
+
+    def test_count_qweb_imported_module(self):
+        self.env['ir.module.module'].create({
+            'author': 'Odoo',
+            'imported': True,
+            'latest_version': '15.0.1.0.0',
+            'name': 'test_imported_module',
+            'state': 'installed',
+            'summary': 'Test imported module for cloc',
+        })
+        qweb_view = self.env['ir.ui.view'].create({
+            "name": "Qweb Test",
+            "type": "qweb",
+            "mode": "primary",
+            "arch_base": "<html>\n  <body>\n    <t t-out=\"Hello World\"/>\n  </body>\n</html>",
+        })
+        self.create_xml_id("qweb_view_test", 'ir.ui.view', qweb_view.id, 'test_imported_module')
+
+        # Add qweb view from non imported module
+        qweb_view = self.env['ir.ui.view'].create({
+            "name": "Qweb Test",
+            "type": "qweb",
+            "arch_base": "<html>\n  <body>\n    <t t-out=\"Hello World\"/>\n  </body>\n</html>",
+        })
+        self.create_xml_id("qweb_view_test", 'ir.ui.view', qweb_view.id)
+
+        # Add form view from module
+        form_view = self.env['ir.ui.view'].create({
+            "name": "Test partner",
+            "type": "form",
+            "model": "res.partner",
+            "arch_base": "<form>\n  <field name=\"name\" \n         invisible=\"1\" />\n</form>",
+        })
+        self.create_xml_id("form_view_test", 'ir.ui.view', form_view.id, 'test_imported_module')
+
+        cl = cloc.Cloc()
+        cl.count_customization(self.env)
+        self.assertEqual(cl.code.get('test_imported_module', 0), 5, "Count only qweb view from imported module")
+
+    def test_count_attachment_imported_module(self):
+        manifest_content = json.dumps({
+            'name': 'test_imported_module',
+            'description': 'Test',
+            'assets': {
+                'web.assets_backend': [
+                    'test_imported_module/static/src/js/test.js',
+                    'test_imported_module/static/src/css/test.scss',
+                ]
+            },
+            'license': 'LGPL-3',
+        })
+
+        stream = BytesIO()
+        with ZipFile(stream, 'w') as archive:
+            archive.writestr('test_imported_module/__manifest__.py', manifest_content)
+            archive.writestr('test_imported_module/static/src/js/test.js', test_cloc.JS_TEST)
+            archive.writestr('test_imported_module/static/src/js/test.scss', test_cloc.SCSS_TEST)
+            archive.writestr('test_imported_module/static/src/js/test.xml', VALID_XML)
+
+        # Import test module
+        self.env['ir.module.module'].import_zipfile(stream)
+        cl = cloc.Cloc()
+        cl.count_customization(self.env)
+        self.assertEqual(cl.code.get('test_imported_module', 0), 35)
diff --git a/addons/base_import_module/tests/test_import_module.py b/addons/base_import_module/tests/test_import_module.py
index 2b9398999d53f431bf7469f020cf6ba3a8ed0466..af0a5271d4d884e2b143d56feefbb4ffed535d9c 100644
--- a/addons/base_import_module/tests/test_import_module.py
+++ b/addons/base_import_module/tests/test_import_module.py
@@ -44,13 +44,13 @@ class TestImportModule(odoo.tests.TransactionCase):
 
         asset_data = self.env['ir.model.data'].search([('model', '=', 'ir.asset'), ('res_id', '=', asset.id)])
         self.assertEqual(asset_data.module, 'test_module')
-        self.assertEqual(asset_data.name, f'{bundle}.{path}')
+        self.assertEqual(asset_data.name, f'{bundle}_{path}'.replace(".", "_"))
 
         # Uninstall test module
         self.env['ir.module.module'].search([('name', '=', 'test_module')]).module_uninstall()
 
         attachment = self.env['ir.attachment'].search([('url', '=', path)])
-        self.assertEqual(len(attachment), 1)
+        self.assertEqual(len(attachment), 0)
 
         asset = self.env['ir.asset'].search([('name', '=', f'test_module.{bundle}.{path}')])
         self.assertEqual(len(asset), 0)
diff --git a/odoo/addons/base/tests/test_cloc.py b/odoo/addons/base/tests/test_cloc.py
index 94965dc9e054c9c12b0a810fc54b1168ab76d9fd..6d48eed8835ed3049a985f79b570d4a42e3408e2 100644
--- a/odoo/addons/base/tests/test_cloc.py
+++ b/odoo/addons/base/tests/test_cloc.py
@@ -79,11 +79,60 @@ function() {
 }
 '''
 
+CSS_TEST = '''
+/*
+  Comment
+*/
+
+p {
+  text-align: center;
+  color: red;
+  text-overflow: ' /* ';
+}
+
+
+#content, #footer, #supplement {
+   position: absolute;
+   left: 510px;
+   width: 200px;
+   text-overflow: ' */ ';
+}
+'''
+
+SCSS_TEST = '''
+/*
+  Comment
+*/
+
+// Standalone list views
+.o_content > .o_list_view > .table-responsive > .table {
+    // List views always have the table-sm class, maybe we should remove
+    // it (and consider it does not exist) and change the default table paddings
+    @include o-list-view-full-width-padding($base: $table-cell-padding-sm, $ratio: 2);
+    &:not(.o_list_table_grouped) {
+        @include media-breakpoint-up(xl) {
+            @include o-list-view-full-width-padding($base: $table-cell-padding-sm, $ratio: 2.5);
+        }
+    }
+
+    .o_optional_columns_dropdown_toggle {
+        padding: 8px 10px;
+    }
+}
+
+#content, #footer, #supplement {
+   text-overflow: '/*';
+   left: 510px;
+   width: 200px;
+   text-overflow: '*/';
+}
+'''
+
 class TestClocCustomization(TransactionCase):
-    def create_xml_id(self, name, res_id, module='studio_customization'):
+    def create_xml_id(self, name, model, res_id, module='studio_customization'):
         self.env['ir.model.data'].create({
             'name': name,
-            'model': 'ir.model.fields',
+            'model': model,
             'res_id': res_id,
             'module': module,
         })
@@ -105,12 +154,12 @@ class TestClocCustomization(TransactionCase):
             Having an xml_id but no existing module is consider as not belonging to a module
         """
         f1 = self.create_field('x_invoice_count')
-        self.create_xml_id('invoice_count', f1.id)
+        self.create_xml_id('invoice_count', 'ir.model.fields', f1.id)
         cl = cloc.Cloc()
         cl.count_customization(self.env)
         self.assertEqual(cl.code.get('odoo/studio', 0), 0, 'Studio auto generated count field should not be counted in cloc')
         f2 = self.create_field('x_studio_custom_field')
-        self.create_xml_id('studio_custom', f2.id)
+        self.create_xml_id('studio_custom', 'ir.model.fields', f2.id)
         cl = cloc.Cloc()
         cl.count_customization(self.env)
         self.assertEqual(cl.code.get('odoo/studio', 0), 1, 'Count other studio computed field')
@@ -119,7 +168,7 @@ class TestClocCustomization(TransactionCase):
         cl.count_customization(self.env)
         self.assertEqual(cl.code.get('odoo/studio', 0), 2, 'Count fields without xml_id')
         f4 = self.create_field('x_custom_field_export')
-        self.create_xml_id('studio_custom', f4.id, '__export__')
+        self.create_xml_id('studio_custom', 'ir.model.fields', f4.id, '__export__')
         cl = cloc.Cloc()
         cl.count_customization(self.env)
         self.assertEqual(cl.code.get('odoo/studio', 0), 3, 'Count fields with xml_id but without module')
@@ -143,6 +192,10 @@ class TestClocParser(TransactionCase):
             self.assertEqual(py_count, (8, 16))
         js_count = cl.parse_js(JS_TEST)
         self.assertEqual(js_count, (10, 17))
+        css_count = cl.parse_css(CSS_TEST)
+        self.assertEqual(css_count, (11, 17))
+        scss_count = cl.parse_scss(SCSS_TEST)
+        self.assertEqual(scss_count, (17, 26))
 
 
 @tagged('post_install', '-at_install')
diff --git a/odoo/tools/cloc.py b/odoo/tools/cloc.py
index 0b40a43be8f9725da1503e5b29b26d8ffde6bdd0..34836b66648cf371aa6b152b30cff08ffea5add1 100644
--- a/odoo/tools/cloc.py
+++ b/odoo/tools/cloc.py
@@ -22,6 +22,7 @@ DEFAULT_EXCLUDE = [
 
 STANDARD_MODULES = ['web', 'web_enterprise', 'theme_common', 'base']
 MAX_FILE_SIZE = 25 * 2**20 # 25 MB
+VALID_EXTENSION = ['.py', '.js', '.xml', '.css', '.scss']
 
 class Cloc(object):
     def __init__(self):
@@ -56,18 +57,41 @@ class Cloc(object):
         except Exception:
             return (-1, "Syntax Error")
 
-    def parse_js(self, s):
+    def parse_c_like(self, s, regex):
         # Based on https://stackoverflow.com/questions/241327
         s = s.strip() + "\n"
         total = s.count("\n")
+
         def replacer(match):
             s = match.group(0)
             return " " if s.startswith('/') else s
-        comments_re = re.compile(r'//.*?$|(?<!\\)/\*.*?\*/|\'(\\.|[^\\\'])*\'|"(\\.|[^\\"])*"', re.DOTALL|re.MULTILINE)
+
+        comments_re = re.compile(regex, re.DOTALL | re.MULTILINE)
         s = re.sub(comments_re, replacer, s)
         s = re.sub(r"\s*\n\s*", r"\n", s).lstrip()
         return s.count("\n"), total
 
+    def parse_js(self, s):
+        return self.parse_c_like(s, r'//.*?$|(?<!\\)/\*.*?\*/|\'(\\.|[^\\\'])*\'|"(\\.|[^\\"])*"')
+
+    def parse_scss(self, s):
+        return self.parse_c_like(s, r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"')
+
+    def parse_css(self, s):
+        return self.parse_c_like(s, r'/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"')
+
+    def parse(self, s, ext):
+        if ext == '.py':
+            return self.parse_py(s)
+        elif ext == '.js':
+            return self.parse_js(s)
+        elif ext == '.xml':
+            return self.parse_xml(s)
+        elif ext == '.css':
+            return self.parse_css(s)
+        elif ext == '.scss':
+            return self.parse_scss(s)
+
     #------------------------------------------------------
     # Enumeration
     #------------------------------------------------------
@@ -112,19 +136,18 @@ class Cloc(object):
                     continue
 
                 ext = os.path.splitext(file_path)[1].lower()
-                if ext in ['.py', '.js', '.xml']:
-                    if os.path.getsize(file_path) > MAX_FILE_SIZE:
-                        self.book(module_name, file_path, (-1, "Max file size exceeded"))
-                        continue
-
-                    with open(file_path, 'rb') as f:
-                        content = f.read().decode('latin1')
-                    if ext == '.py':
-                        self.book(module_name, file_path, self.parse_py(content))
-                    elif ext == '.js':
-                        self.book(module_name, file_path, self.parse_js(content))
-                    elif ext == '.xml':
-                        self.book(module_name, file_path, self.parse_xml(content))
+                if ext not in VALID_EXTENSION:
+                    continue
+
+                if os.path.getsize(file_path) > MAX_FILE_SIZE:
+                    self.book(module_name, file_path, (-1, "Max file size exceeded"))
+                    continue
+
+                with open(file_path, 'rb') as f:
+                    # Decode using latin1 to avoid error that may raise by decoding with utf8
+                    # The chars not correctly decoded in latin1 have no impact on how many lines will be counted
+                    content = f.read().decode('latin1')
+                self.book(module_name, file_path, self.parse(content, ext))
 
     def count_modules(self, env):
         # Exclude standard addons paths
@@ -175,6 +198,48 @@ class Cloc(object):
         for f in env['ir.model.fields'].browse(data.keys()):
             self.book(data[f.id] or "odoo/studio", "ir.model.fields/%s: %s" % (f.id, f.name), self.parse_py(f.compute))
 
+        if not imported_module:
+            return
+
+        # Count qweb view only from imported module
+        query = """
+            SELECT view.id, data.module
+              FROM ir_ui_view view
+        INNER JOIN ir_model_data data ON view.id = data.res_id AND data.model = 'ir.ui.view'
+        INNER JOIN ir_module_module mod ON mod.name = data.module AND mod.imported = True
+             WHERE view.type = 'qweb'
+        """
+        env.cr.execute(query)
+        custom_views = dict(env.cr.fetchall())
+        for view in env['ir.ui.view'].browse(custom_views.keys()):
+            modue_name = custom_views[view.id]
+            self.book(modue_name, "/%s/views/%s.xml" % (modue_name, view.name), self.parse_xml(view.arch_base))
+
+        # Count js, xml, css/scss file from imported module
+        query = r"""
+            SELECT attach.id, data.module
+              FROM ir_attachment attach
+        INNER JOIN ir_model_data data ON attach.id = data.res_id AND data.model = 'ir.attachment'
+        INNER JOIN ir_module_module mod ON mod.name = data.module AND mod.imported = True
+             WHERE attach.name ~ '.*\.(js|xml|css|scss)$'
+        """
+        env.cr.execute(query)
+        uploaded_file = dict(env.cr.fetchall())
+        for attach in env['ir.attachment'].browse(uploaded_file.keys()):
+            module_name = uploaded_file[attach.id]
+            ext = os.path.splitext(attach.url)[1].lower()
+            if ext not in VALID_EXTENSION:
+                continue
+
+            if len(attach.datas) > MAX_FILE_SIZE:
+                self.book(module_name, attach.url, (-1, "Max file size exceeded"))
+                continue
+
+            # Decode using latin1 to avoid error that may raise by decoding with utf8
+            # The chars not correctly decoded in latin1 have no impact on how many lines will be counted
+            content = attach.raw.decode('latin1')
+            self.book(module_name, attach.url, self.parse(content, ext))
+
     def count_env(self, env):
         self.count_modules(env)
         self.count_customization(env)