diff --git a/addons/base_import_module/models/ir_module.py b/addons/base_import_module/models/ir_module.py
index 3ab7aeb4d68..7bca006c1f9 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 05611a0cc6f..7b393e71573 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 = """
+
+
+
+
+
+
+
+
+"""
+
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": "\n
\n \n \n",
+ })
+ 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": "\n \n \n \n",
+ })
+ 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": "",
+ })
+ 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 2b9398999d5..af0a5271d4d 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 94965dc9e05..6d48eed8835 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 0b40a43be8f..34836b66648 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'//.*?$|(? MAX_FILE_SIZE:
- self.book(module_name, file_path, (-1, "Max file size exceeded"))
- continue
+ if ext not in VALID_EXTENSION:
+ 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 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)