Browse Source

[UPDT] Jul 11: Updated 'odoo_health_report'

pull/354/merge
AjmalCybro 1 week ago
parent
commit
9cc39f1f62
  1. 2
      odoo_health_report/__manifest__.py
  2. 5
      odoo_health_report/doc/RELEASE_NOTES.md
  3. 711
      odoo_health_report/models/check_odoo_python_guidelines.py
  4. BIN
      odoo_health_report/models/check_python_violations.cpython-310-x86_64-linux-gnu.so
  5. 316
      odoo_health_report/models/check_violations.py
  6. 66
      odoo_health_report/models/module_quality_package.py
  7. 174
      odoo_health_report/reports/odoo_health_report.xml
  8. 181
      odoo_health_report/static/src/xml/module_quality.xml

2
odoo_health_report/__manifest__.py

@ -21,7 +21,7 @@
######################################################################################
{
'name': 'Odoo Health Report',
'version': '18.0.1.0.1',
'version': '18.0.2.1.1',
'category': 'Productivity',
'summary': "Odoo Module Health Monitoring Tool",
'description': 'Displays odoo apps report in the menu and as a PDF report.',

5
odoo_health_report/doc/RELEASE_NOTES.md

@ -4,3 +4,8 @@
#### Version 18.0.1.0.1
#### ADD
- Initial Commit for Odoo Health Report
#### 11.07.2025
#### Version 18.0.2.1.1
#### UPDATE
- Added new features

711
odoo_health_report/models/check_odoo_python_guidelines.py

@ -0,0 +1,711 @@
# -*- coding: utf-8 -*-
######################################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
#
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
# or modified copies of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
######################################################################################
import ast
import os
import re
results = []
module_technical_name = ['no_module']
def is_camel_case(name):
"""
Check if a given name follows CamelCase format.
Args:
name (str): The variable or class name.
Returns:
bool: True if name is CamelCase.
"""
return re.match(r'^[A-Z][a-zA-Z0-9]+$', name)
def is_snake_case(name):
"""
Check if a given name follows snake_case format.
Args:
name (str): The variable or function name.
Returns:
bool: True if name is snake_case.
"""
return re.match(r'^[a-z_][a-z0-9_]*$', name)
def check_class_name(node, file_path):
"""
Validate that the class name follows CamelCase.
Args:
node (ast.ClassDef): The class node from AST.
file_path (str): The source file path.
"""
if not is_camel_case(node.name):
file_path_shorten = f"{module_technical_name[0]}/{file_path.split(f'/{module_technical_name[0]}/')[1]}"
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Class name '{node.name}' is not CamelCase",
"suggestion": "Use CamelCase (e.g., MyClassName)"
})
def check_model_class(node, file_path):
"""
Check Odoo model class rules, including naming consistency.
Args:
node (ast.ClassDef): The class node from AST.
file_path (str): The source file path.
"""
model_type = None
model_name = None
inherit_found = False
name_found = False
for base in node.bases:
if isinstance(base, ast.Attribute) and base.attr in ["Model", "TransientModel"]:
model_type = base.attr
for item in node.body:
if isinstance(item, ast.Assign):
for target in item.targets:
if isinstance(target, ast.Name):
if target.id == "_name":
name_found = True
if isinstance(item.value, ast.Constant) and isinstance(item.value.value, str):
model_name = item.value.value
elif target.id == "_inherit":
inherit_found = True
file_path_shorten = f"{module_technical_name[0]}/{file_path.split(f'/{module_technical_name[0]}/')[1]}"
# Check if _name or _inherit is present
if model_type and not (name_found or inherit_found):
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Class '{node.name}' is missing both '_name' and '_inherit'",
"suggestion": "Add either _name (for new model) or _inherit (for extending existing model)"
})
# Consistency Check
filename = os.path.basename(file_path).replace(".py", "")
expected_class_name = "".join(part.capitalize() for part in filename.split("_"))
expected_model_name = filename.replace("_", ".")
if node.name != expected_class_name:
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Class name '{node.name}' does not match file name '{filename}.py'",
"suggestion": f"Rename class to '{expected_class_name}' to match file name"
})
if model_name and model_type == "Model":
if model_name != expected_model_name:
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"_name '{model_name}' does not match file-based model name '{expected_model_name}'",
"suggestion": f"Use _name = '{expected_model_name}'"
})
if model_name:
if "." not in model_name:
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Model name '{model_name}' should use dot notation",
"suggestion": "Use format: <module>.<model> (e.g., res.partner)"
})
if model_type == "TransientModel" and "wizard" in model_name:
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Transient model name '{model_name}' contains 'wizard'",
"suggestion": "Avoid using 'wizard'; use format: <related_model>.<action>"
})
if ".report." in model_name and len(model_name.split(".")) < 3:
parts = model_name.split(".")
if len(parts) < 3:
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Report model name '{model_name}' should follow '<model>.report.<action>'",
"suggestion": "Ensure full dot-notation like 'sale.report.invoice'"
})
if model_type == "Model":
last_part = model_name.split(".")[-1]
if last_part.endswith("s"):
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Model name '{model_name}' seems plural",
"suggestion": "Use singular form (e.g., 'res.partner', not 'res.partners')"
})
def check_missing_docstring(node, file_path, type="function"):
"""
Check whether a class or function has a docstring.
Args:
node (ast.AST): The node (ClassDef or FunctionDef).
file_path (str): File path.
type (str): Either 'function' or 'class'.
"""
file_path_shorten = f"{module_technical_name[0]}/{file_path.split(f'/{module_technical_name[0]}/')[1]}"
if not ast.get_docstring(node):
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"{type.capitalize()} '{node.name}' is missing a docstring",
"suggestion": f"Add a docstring to the {type}"
})
def check_function_name(node, file_path):
"""
Check function or method names for snake_case and method type prefixes.
Args:
node (ast.FunctionDef): The function node.
file_path (str): Path to the source file.
"""
file_path_shorten = f"{module_technical_name[0]}/{file_path.split(f'/{module_technical_name[0]}/')[1]}"
if not is_snake_case(node.name):
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Function name '{node.name}' is not snake_case",
"suggestion": "Use snake_case (e.g., my_function_name)"
})
for decorator in node.decorator_list:
if isinstance(decorator, ast.Attribute):
if decorator.attr in ["multi", "one"]:
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Decorator '@api.{decorator.attr}' is deprecated",
"suggestion": "Remove it; use '@api.model' or '@api.depends' based on logic"
})
check_method_prefix(node, file_path)
def check_method_prefix(node, file_path):
"""
Check method prefix based on decorators like @api.depends, @api.onchange, etc.
Args:
node (ast.FunctionDef): The method definition.
file_path (str): Path to the Python file.
"""
method_prefix_map = {
'depends': '_compute_',
'onchange': '_onchange_',
'constrains': '_check_',
}
file_path_shorten = f"{module_technical_name[0]}/{file_path.split(f'/{module_technical_name[0]}/')[1]}"
for decorator in node.decorator_list:
if isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Attribute):
deco_name = decorator.func.attr
expected_prefix = method_prefix_map.get(deco_name)
if expected_prefix and not node.name.startswith(expected_prefix):
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Method '{node.name}' uses @{deco_name} but doesn't start with '{expected_prefix}'",
"suggestion": f"Prefix method name with '{expected_prefix}'"
})
if 'default_' in node.name and not node.name.startswith('_default_'):
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Default method '{node.name}' should start with '_default_'",
"suggestion": "Rename method to start with '_default_'"
})
if 'selection' in node.name and not node.name.startswith('_selection_'):
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Selection method '{node.name}' should start with '_selection_'",
"suggestion": "Rename method to start with '_selection_'"
})
if node.name.startswith('do_'):
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Action method '{node.name}' should start with 'action_'",
"suggestion": "Rename method to start with 'action_'"
})
def check_field_suffix(node, file_path):
"""
Ensure field names have correct suffixes based on field type.
Args:
node (ast.Assign): The field definition node.
file_path (str): Path to the Python file.
"""
file_path_shorten = f"{module_technical_name[0]}/{file_path.split(f'/{module_technical_name[0]}/')[1]}"
if isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Attribute):
field_type = node.value.func.attr
if field_type in ['Many2one', 'One2many', 'Many2many']:
target = node.targets[0]
name = target.id if isinstance(target, ast.Name) else getattr(target, 'attr', None)
if name:
correct_suffix = '_id' if field_type == 'Many2one' else '_ids'
if not name.endswith(correct_suffix):
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"{field_type} field '{name}' should end with '{correct_suffix}'",
"suggestion": f"Rename variable to end with '{correct_suffix}'"
})
def check_variable_naming(node, file_path):
"""
Check local variable naming conventions and suffixes for record/recordsets.
Args:
node (ast.Assign): The assignment statement node.
file_path (str): Source file path.
"""
if not isinstance(node, ast.Assign) or not isinstance(node.value, ast.Call):
return
targets = node.targets
if not targets:
return
var_name = targets[0].id if isinstance(targets[0], ast.Name) else None
if not var_name:
return
call_func = node.value.func
call_attr = call_func.attr if isinstance(call_func, ast.Attribute) else ''
file_path_shorten = f"{module_technical_name[0]}/{file_path.split(f'/{module_technical_name[0]}/')[1]}"
if isinstance(call_func, ast.Subscript):
if isinstance(call_func.value, ast.Attribute) and call_func.value.attr == 'env':
if not is_camel_case(var_name):
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Model variable '{var_name}' is not CamelCase",
"suggestion": "Use CamelCase (e.g., MyModelVariable)"
})
if call_attr in ['browse', 'search', 'search_read']:
if call_attr == 'browse' and not var_name.endswith('_id'):
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Variable '{var_name}' may represent a single record, use _id suffix",
"suggestion": "Rename variable to end with '_id'"
})
elif call_attr in ['search', 'search_read'] and not var_name.endswith('_ids'):
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Variable '{var_name}' may represent multiple records, use _ids suffix",
"suggestion": "Rename variable to end with '_ids'"
})
if not is_snake_case(var_name):
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Variable name '{var_name}' is not snake_case",
"suggestion": "Use snake_case (e.g., my_variable)"
})
def check_python_file_name(file_path):
"""
Check if the Python file name follows snake_case convention.
Args:
file_path (str): Full path to the Python file.
"""
filename = os.path.basename(file_path)
if not filename.endswith(".py") or filename in {"__init__.py", "__manifest__.py"}:
return
name_without_ext = filename[:-3] # Remove .py
file_path_shorten = f"{module_technical_name[0]}/{file_path.split(f'/{module_technical_name[0]}/')[1]}"
if not re.fullmatch(r"[a-z0-9_]+", name_without_ext):
results.append({
"file": file_path_shorten,
"line": 0,
"issue": f"File name '{filename}' does not follow snake_case",
"suggestion": "Rename file using lowercase and underscores (e.g., my_model.py)"
})
if name_without_ext in {"temp", "test", "test1", "new", "misc"}:
results.append({
"file": file_path_shorten,
"line": 0,
"issue": f"File name '{filename}' is too generic or non-descriptive",
"suggestion": "Use meaningful file names related to model or logic"
})
def check_manifest_file(file_path):
"""
Check the manifest file for any standard issues.
Args:
file_path (str): Full path to the Python file.
"""
file_path_shorten = f"{module_technical_name[0]}/{file_path.split(f'/{module_technical_name[0]}/')[1]}"
with open(file_path, "r", encoding="utf-8") as f:
source = f.read()
try:
tree = ast.parse(source)
except SyntaxError:
results.append({
"file": file_path_shorten,
"line": 0,
"issue": "Invalid Python syntax in __manifest__.py",
"suggestion": "Fix the syntax error"
})
return
manifest_data = {}
seen_keys = set()
for node in ast.walk(tree):
if isinstance(node, ast.Dict):
for k_node, v_node in zip(node.keys, node.values):
if isinstance(k_node, ast.Str):
key = k_node.s
if key in seen_keys:
results.append({
"file": file_path_shorten,
"line": k_node.lineno if hasattr(k_node, "lineno") else 1,
"issue": f"Duplicate field '{key}' in manifest",
"suggestion": "Remove the duplicate entry"
})
seen_keys.add(key)
manifest_data[key] = v_node
break # Assume top-level dict is the manifest
required_fields = {"name", "version", "depends", "author", "category", "summary", "data", "installable", "license"}
for field in required_fields:
if field not in manifest_data:
results.append({
"file": file_path_shorten,
"line": 1,
"issue": f"Missing required field '{field}' in manifest",
"suggestion": f"Add '{field}' to the manifest dictionary"
})
# Version check
version = manifest_data.get("version")
if isinstance(version, ast.Constant):
version_val = version.value
if not re.fullmatch(r"\d+\.\d+\.\d+\.\d+\.\d+", version_val):
results.append({
"file": file_path_shorten,
"line": version.lineno if hasattr(version, "lineno") else 1,
"issue": f"Version '{version_val}' does not follow '16.0.1.0.0' format",
"suggestion": "Use 5-level semantic versioning like '16.0.1.0.0'"
})
# Depends check
depends = manifest_data.get("depends")
if isinstance(depends, ast.List):
for d in depends.elts:
if isinstance(d, ast.Constant) and not d.value.strip():
results.append({
"file": file_path_shorten,
"line": d.lineno if hasattr(d, "lineno") else 1,
"issue": "Empty string in 'depends'",
"suggestion": "Remove empty string or specify actual dependency"
})
# Maintainer email format check
maintainer = manifest_data.get("maintainer")
if maintainer and isinstance(maintainer, ast.Constant):
email = maintainer.value
# Use this if not worked properly: r".*<[^@]+@[^@]+\.[^@]+>"
if "@" not in email or not re.match(r"[^@]+@[^@]+\.[^@]+", email):
results.append({
"file": file_path_shorten,
"line": maintainer.lineno if hasattr(maintainer, "lineno") else 1,
"issue": f"Invalid email format for 'maintainer': '{email}'",
"suggestion": "Use a valid email address (e.g., john@example.com)"
})
# Check license
valid_licenses = {"LGPL-3", "AGPL-3", "OEEL-1", "OPL-1", "GPL-3", "MIT"}
license_node = manifest_data.get("license")
if isinstance(license_node, ast.Constant):
license_val = license_node.value
if license_val not in valid_licenses:
results.append({
"file": file_path_shorten,
"line": license_node.lineno if hasattr(license_node, "lineno") else 1,
"issue": f"License '{license_val}' is not a recognized Odoo-compatible license",
"suggestion": f"Use one of the valid licenses: {', '.join(valid_licenses)}"
})
# Data file order check: security/ files must come first
data_node = manifest_data.get("data")
if isinstance(data_node, ast.List):
found_non_security = False
for file_node in data_node.elts:
if isinstance(file_node, ast.Constant):
file_value = file_node.value
if not file_value.startswith("security/"):
found_non_security = True
elif found_non_security:
results.append({
"file": file_path_shorten,
"line": file_node.lineno if hasattr(file_node, "lineno") else 1,
"issue": f"'security' file '{file_value}' appears after non-security files",
"suggestion": "Move all 'security/' files to the top of the 'data' list"
})
def check_transient_model_location(py_file_path):
"""
Check the transient model is in the wizard folder.
Args:
file_path (str): Full path to the Python file.
"""
file_path_shorten = f"{module_technical_name[0]}/{py_file_path.split(f'/{module_technical_name[0]}/')[1]}"
with open(py_file_path, "r", encoding="utf-8") as f:
source = f.read()
try:
tree = ast.parse(source)
except SyntaxError:
return
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
for base in node.bases:
if isinstance(base, ast.Attribute) and base.attr == "TransientModel":
if "wizard" not in py_file_path.replace("\\", "/").split("/"):
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"TransientModel class '{node.name}' is not in a 'wizard/' directory",
"suggestion": "Move the file to a 'wizard/' folder to follow Odoo best practices"
})
def check_report_model_location(py_file_path):
"""
Check the report model is in the report folder.
Args:
file_path (str): Full path to the Python file.
"""
file_path_shorten = f"{module_technical_name[0]}/{py_file_path.split(f'/{module_technical_name[0]}/')[1]}"
with open(py_file_path, "r", encoding="utf-8") as f:
source = f.read()
try:
tree = ast.parse(source)
except SyntaxError:
return
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
is_abstract_model = any(
isinstance(base, ast.Attribute) and base.attr == "AbstractModel"
for base in node.bases
)
if is_abstract_model:
for body_item in node.body:
if isinstance(body_item, ast.Assign):
for target in body_item.targets:
if isinstance(target, ast.Name) and target.id == "_name":
if isinstance(body_item.value, ast.Constant):
model_name = body_item.value.value
if (model_name.startswith("report.") and
"report" not in py_file_path.replace("\\", "/").split("/")):
results.append({
"file": file_path_shorten,
"line": node.lineno,
"issue": f"Report model '{model_name}' is not in a 'report/' directory",
"suggestion": "Move the report model to a 'report/' folder"
})
def check_controller_naming(module_path, module_name):
"""
Check the controller file naming.
Args:
module_path (str): Full path to the Python file.
module_name (str): Module technical name.
"""
controller_dir = os.path.join(module_path, "controllers")
file_path_shorten = f"{module_technical_name[0]}/{module_path.split(f'/{module_technical_name[0]}/')[1]}"
if not os.path.isdir(controller_dir):
return
for filename in os.listdir(controller_dir):
filepath = os.path.join(controller_dir, filename)
if not filename.endswith(".py"):
continue
if filename == "main.py":
results.append({
"file": file_path_shorten,
"line": 1,
"issue": "'main.py' is deprecated for controller files",
"suggestion": f"Rename to '{module_name}.py' or the name of the inherited module"
})
if filename != f"{module_name}.py" and not filename.startswith("portal") and not filename.startswith("website"):
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
if "http.Controller" in content and f"{module_name}.py" not in os.listdir(controller_dir):
results.append({
"file": file_path_shorten,
"line": 1,
"issue": f"'{filename}' may not follow the controller naming convention",
"suggestion": f"Use '{module_name}.py' for your base controller, "
f"or name after the module you're inheriting"
})
def check_compute_method_exact_naming(py_file_path):
"""
Check the compute function naming.
Args:
file_path (str): Full path to the Python file.
"""
file_path_shorten = f"{module_technical_name[0]}/{py_file_path.split(f'/{module_technical_name[0]}/')[1]}"
with open(py_file_path, "r", encoding="utf-8") as f:
source = f.read()
try:
tree = ast.parse(source)
except SyntaxError:
return
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name):
field_name = target.id
for value in ast.walk(node.value):
if isinstance(value, ast.Call) and isinstance(value.func, ast.Attribute):
if value.func.attr in {
"Char", "Text", "Integer", "Float", "Boolean", "Date", "Datetime",
"Many2one", "One2many", "Many2many", "Html", "Binary", "Selection"
}:
for keyword in value.keywords:
if keyword.arg == "compute" and isinstance(keyword.value, ast.Constant):
compute_func = keyword.value.value
expected_name = f"_compute_{field_name}"
if compute_func != expected_name:
results.append({
"file": file_path_shorten,
"line": keyword.lineno,
"issue": f"Compute method should be named '{expected_name}' for "
f"field '{field_name}', found '{compute_func}'",
"suggestion": f"Rename compute method to '{expected_name}'"
})
def analyze_file(file_path):
"""
Analyze a Python file using AST and apply all naming rules.
Args:
file_path (str): Full path to the .py file.
"""
module_name = os.path.basename(file_path.rstrip("/\\")) # Get folder name
check_python_file_name(file_path)
check_transient_model_location(file_path)
check_report_model_location(file_path)
check_controller_naming(file_path, module_name)
check_compute_method_exact_naming(file_path)
try:
with open(file_path, "r", encoding="utf-8") as f:
tree = ast.parse(f.read(), filename=file_path)
except SyntaxError:
return
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
check_class_name(node, file_path)
check_model_class(node, file_path)
check_missing_docstring(node, file_path, type="class")
elif isinstance(node, ast.FunctionDef):
check_function_name(node, file_path)
check_missing_docstring(node, file_path, type="function")
elif isinstance(node, ast.Assign):
check_field_suffix(node, file_path)
check_variable_naming(node, file_path)
def scan_directory(directory):
"""
Recursively scan a directory and analyze all .py files, skipping __init__.py and __manifest__.py.
Args:
directory (str): Path to the root directory.
"""
for root, _, files in os.walk(directory):
for file in files:
if file == "__manifest__.py":
check_manifest_file(os.path.join(root, file))
if file.endswith(".py") and file not in {"__init__.py", "__manifest__.py"}:
analyze_file(os.path.join(root, file))
def check_odoo_python_standards(name, path):
"""
Perform a scan of Python files in the given directory to check for Odoo-specific coding standard violations.
This function clears previous results, sets the current module's technical name, and recursively scans the
given directory for Python files. Each file is analyzed for adherence to Odoo's Python coding standards,
and the results are accumulated.
Args:
name (str): The technical name of the Odoo module being checked.
path (str): The absolute or relative file path to the directory containing the module's Python code.
Returns:
list: A list of dictionaries containing information about detected coding standard violations.
"""
results.clear()
module_technical_name[0] = name
scan_directory(path)
return results

BIN
odoo_health_report/models/check_python_violations.cpython-310-x86_64-linux-gnu.so

Binary file not shown.

316
odoo_health_report/models/check_violations.py

@ -0,0 +1,316 @@
# -*- coding: utf-8 -*-
######################################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
#
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
# or modified copies of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
######################################################################################
import json
import re
import subprocess
import sys
from odoo.modules import get_module_path
from . import check_odoo_python_guidelines
def violations_report(self, module):
"""
Check the violations for selected modules for PDF report.
Args:
self (obj): The odoo ClassDef.
module (str): The module technical name.
Returns:
dict: Module name and its violations
"""
violations = get_violations(module)
report_dict = {
'module': self.env.ref(f'base.module_{module}').display_name,
'violations': {
"odoo_standards": violations.get('odoo_standards_check'),
"style_lint": violations.get('style_lint_check'),
"code_quality": violations.get('code_quality_check'),
"maintainability_index": violations.get('mi_check'),
"import_sort": violations.get('import_sort_check'),
"code_format": violations.get('code_format_check'),
"code_complexity": violations.get('cc_check'),
"security_scan": violations.get('security_scan')
}
}
return report_dict
def get_violations(module):
"""
Check the violations for selected module.
Args:
module (str): The module technical name.
Returns:
dict: Selected module violations
"""
return {
"style_lint_check": check_style_lint(module),
"code_quality_check": check_code_quality(module),
"mi_check": check_maintainability_index(module),
"import_sort_check": check_import_sort(module),
"code_format_check": check_code_format(module),
"cc_check": check_code_complexity(module),
"security_scan": scan_code_security(module),
"odoo_standards_check": check_odoo_python_guidelines.check_odoo_python_standards(
name=module, path=get_module_path(module))
}
def check_style_lint(module):
"""
Check the style and lint for selected module .
Args:
module (str): The module technical name.
Returns:
list of dictionaries for the violations with the file name and line number.
"""
excluded_files = ['__init__.py', '__manifest__.py', '__pycache__', '*.pyc', '.git']
ignored_violations = ['E501', 'E301', 'E302']
venv_python = sys.executable
exclude_param = f"--exclude={','.join(excluded_files)}"
ignore_param = f"--ignore={','.join(ignored_violations)}"
module_path = get_module_path(module)
result = subprocess.run(
[venv_python, '-m', 'flake8', '--select=D', exclude_param, ignore_param, module_path],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
violations = result.stdout.strip().splitlines()
violations_list = []
for violation in violations:
parts = violation.split(":", 3)
if len(parts) >= 4:
file_path = f"{module}/{parts[0].split(f'/{module}/')[1]}"
violations_list.append({
'file_name': file_path,
'line_number': parts[1],
'violation_message': parts[3]
})
return violations_list
def check_code_quality(module):
"""
Check the code quality for selected module .
Args:
module (str): The module technical name.
Returns:
list of dictionaries for the violations with the file name, code, line and column number.
"""
pylint_result = subprocess.run([
sys.executable, '-m', 'pylint',
'--load-plugins=pylint_odoo',
'-d all',
'-e odoolint',
'--ignore-patterns=__init__.py,__manifest__.py',
get_module_path(module)
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
output = pylint_result.stdout.strip().splitlines()
result = []
for rec in output:
if rec.startswith(('*', '-')) or rec == '':
continue
else:
rec = rec.split(':')
if len(rec) >= 5:
file_path = f"{module}/{rec[0].split(f'/{module}/')[1]}"
result.append({
'file': file_path,
'line': rec[1],
'column': rec[2],
'code': rec[3].strip(),
'message': rec[4].strip(),
})
return result
def check_maintainability_index(module):
"""
Check the maintainability index for selected module .
Args:
module (str): The module technical name.
Returns:
list of dictionaries for the file name and grade.
"""
result = subprocess.run([
sys.executable, '-m', 'radon',
'mi',
get_module_path(module),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
excluded_files = ("__init__.py", "__manifest__.py")
mi_list = []
for rec in result.stdout.splitlines():
if not any(exclude in rec for exclude in excluded_files):
rec = rec.split(' - ')
if len(rec) >= 2:
file_path = f"{module}/{rec[0].split(f'/{module}/')[1]}"
mi_list.append({'file': file_path, 'grade': rec[1]})
return mi_list
def check_import_sort(module):
"""
Check the import sorting for selected module .
Args:
module (str): The module technical name.
Returns:
list of dictionaries for the file name and message.
"""
result = subprocess.run([
sys.executable, '-m', 'isort',
'--check-only',
'--skip', '__init__.py',
'--skip', '__manifest__.py',
get_module_path(module),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
import_sort_list = []
for rec in result.stderr.splitlines():
rec = rec.replace("ERROR: ", "")
parts = rec.split(" ", 1)
if len(parts) >= 2:
file_path = f"{module}/{parts[0].split(f'/{module}/')[1]}"
import_sort_list.append({'file': file_path, 'message': parts[1]})
return import_sort_list
def parse_black_output_simple(line):
"""Simple parsing for all Black output types"""
actions = ['would reformat ', 'reformatted ', 'formatted ', 'would format ']
for action in actions:
if line.startswith(action):
filename = line[len(action):]
return action.strip(), filename
return None, None
def is_file_line(line):
"""Check if line is a file entry (not summary)"""
skip_patterns = ['💥', '💔', '!', 'files would be', 'file would be', 'All done']
actions = ['would reformat ', 'reformatted ', 'formatted ', 'would format ']
if any(pattern in line for pattern in skip_patterns):
return False
return any(line.startswith(action) for action in actions)
def check_code_format(module):
"""
Check the code format for selected module .
Args:
module (str): The module technical name.
Returns:
list of dictionaries for the file name and action.
"""
result = subprocess.run([
sys.executable, '-m', 'black',
'--check',
'--exclude', '__init__.py|__manifest__.py',
get_module_path(module),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
code_format_list = []
for line in result.stderr.strip().split('\n'):
line = line.strip()
if not line or not is_file_line(line):
continue
parsed = parse_black_output_simple(line)
if parsed[0] is not None:
code_format_list.append(parsed)
return code_format_list
def check_code_complexity(module):
"""
Check the code complexity for selected module .
Args:
module (str): The module technical name.
Returns:
list of dictionaries for the file name and grade.
"""
result = subprocess.run([
sys.executable, '-m', 'radon',
'cc',
'--json',
get_module_path(module),
],
capture_output=True,
text=True
)
if result.returncode == 0 and result.stdout:
clean_stdout = re.sub(r'\x1b\[[0-9;]*m', '', result.stdout)
cc_dict = json.loads(clean_stdout.strip())
return cc_dict
return {}
def scan_code_security(module):
"""
Scan the code security for selected module .
Args:
module (str): The module technical name.
Returns:
list of dictionaries of the security scan.
"""
result = subprocess.run([
sys.executable, '-m', 'bandit',
'-r', get_module_path(module),
'-x', '__init__.py,__manifest__.py',
'-f', 'json'
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True)
if result.stdout:
security_list = json.loads(result.stdout).get('results', [])
return security_list
return []

66
odoo_health_report/models/module_quality_package.py

@ -19,9 +19,14 @@
# DEALINGS IN THE SOFTWARE.
#
######################################################################################
import os
from collections import defaultdict, Counter
from pathlib import Path
from odoo import api, models
from .check_python_violations import installed_modules, count_module_lines, module_and_icons, violations_report, get_violations
from odoo import modules
from odoo.modules import get_modules, get_module_path
from . import check_violations
class ModuleQuality(models.AbstractModel):
@ -31,12 +36,49 @@ class ModuleQuality(models.AbstractModel):
def get_installed_modules(self):
"""Function to fetch installed modules"""
return installed_modules(self)
return self.env['ir.module.module'].search([
('state', '=', 'installed'),
('name', '!=', 'odoo_health_report')
])
@api.model
def count_lines_of_code_in_modules(self):
"""Count lines of Python, JavaScript, and XML code in all installed Odoo modules."""
return count_module_lines(self)
module_ids = self.get_installed_modules()
total_lines = Counter()
exclude_files = {'__init__.py', '__manifest__.py'}
result = defaultdict(list)
for module in module_ids:
module_name = module.name
author = module.author
module_path = get_module_path(module_name)
if not module_path:
continue
loc = Counter({'.py': 0, '.js': 0, '.xml': 0})
for ext in loc:
for file in Path(module_path).rglob(f'*{ext}'):
if file.name in exclude_files:
continue
try:
with file.open('r', encoding='utf-8', errors='ignore') as f:
loc[ext] += sum(1 for _ in f)
except (IOError, UnicodeDecodeError):
continue
result[author].append({
'technical_name': module_name,
'module_name': module.display_name,
'py_lines': loc['.py'],
'js_lines': loc['.js'],
'xml_lines': loc['.xml']
})
total_lines.update({
'py_lines': loc['.py'],
'js_lines': loc['.js'],
'xml_lines': loc['.xml']
})
return {'result': dict(result), 'total_lines': dict(total_lines)}
@api.model
def fields_and_apps_overview(self):
@ -79,14 +121,26 @@ class ModuleQuality(models.AbstractModel):
@api.model
def get_module_and_icons(self):
"""Retrieve all custom module name and icon as a dictionary"""
return module_and_icons(self)
module_and_icon = {}
is_updated = self.env['base.module.update'].search([], limit=1, order='id desc')
if not is_updated:
is_updated = self.env['base.module.update'].create({})
is_updated.update_module()
for module in get_modules():
module_path = get_module_path(module)
if 'addons' not in module_path.split(os.sep) and module != 'odoo_health_report':
module_name = self.env.ref(f'base.module_{module}').display_name
module_icon = modules.module.get_module_icon(module)
module_and_icon[module] = [module_name, module_icon]
return module_and_icon
@api.model
def check_violations_report(self, module):
"""Check the violations for PDF report"""
return violations_report(self, module)
return check_violations.violations_report(self, module)
@api.model
def check_violations(self, module):
"""Check the violations and standards"""
return get_violations(module)
return check_violations.get_violations(module)

174
odoo_health_report/reports/odoo_health_report.xml

@ -125,12 +125,38 @@
<h3>Violations of <t t-esc="violation_module['module']"/>
</h3>
<t t-foreach="violation_module['violations']" t-as="violations_categ">
<!-- Odoo Code Standards Check -->
<t t-if="violations_categ == 'odoo_standards'">
<h3 style="font-size: 1rem; color: #333; margin: 0; margin-bottom: 10px; font-weight: 700;">
Odoo Code Standards Check
</h3>
<t t-if="violation_module['violations']['odoo_standards']">
<table style="border-collapse: collapse; width: 100%; font-size: 0.8rem; border-radius: 10px; overflow: hidden; box-shadow: 0px 0px 20px rgb(0 0 0 / 5%);">
<thead style="background-color: #ddf66b; color: #333">
<th style="text-align: left; padding: 10px">File Name</th>
<th style="text-align: center; padding: 10px">Line</th>
<th style="text-align: left; padding: 10px">Violation Message</th>
<th style="text-align: left; padding: 10px">Suggestion</th>
</thead>
<tbody>
<tr t-foreach="violation_module['violations']['odoo_standards']" t-as="line" style="border-bottom: 1px solid #e9e9e9">
<td style="padding: 10px" t-esc="line['file']"/>
<td style="text-align: center; padding: 10px" t-esc="line['line']"/>
<td style="text-align: left; padding: 10px; color: #8B0000;" t-esc="line['issue']"/>
<td style="text-align: left; padding: 10px" t-esc="line['suggestion']"/>
</tr>
</tbody>
</table><br/>
</t>
<t t-else=""><span style="font-size: 0.6rem; color: black; margin: 0; margin-bottom: 10px; font-weight: 500;">No issues found</span><br/></t>
</t>
<!-- Style &amp; Linting Check -->
<t t-if="violations_categ == 'style_lint'">
<t t-elif="violations_categ == 'style_lint'">
<h3 style="font-size: 1rem; color: #333; margin: 0; margin-bottom: 10px; font-weight: 700;">
Style &amp; Linting Check
</h3>
<t t-if="violation_module['violations']['style_lint']">
<table style="border-collapse: collapse; width: 100%; font-size: 0.8rem; border-radius: 10px; overflow: hidden; box-shadow: 0px 0px 20px rgb(0 0 0 / 5%);">
<thead style="background-color: #ddf66b; color: #333">
<th style="text-align: left; padding: 10px">File Name</th>
@ -146,12 +172,87 @@
</tbody>
</table><br/>
</t>
<t t-else=""><span style="font-size: 0.6rem; color: black; margin: 0; margin-bottom: 10px; font-weight: 500;">No issues found</span><br/></t>
</t>
<!-- General Code Quality -->
<t t-elif="violations_categ == 'code_quality'">
<h3 style="font-size: 1rem; color: #333; margin: 0; margin-bottom: 10px; font-weight: 700;">
General Code Quality
</h3>
<t t-if="violation_module['violations']['code_quality']">
<table style="border-collapse: collapse; width: 100%; font-size: 0.8rem; border-radius: 10px; overflow: hidden; box-shadow: 0px 0px 20px rgb(0 0 0 / 5%);">
<thead style="background-color: #ddf66b; color: #333">
<th style="text-align: left; padding: 10px">File Name</th>
<th style="text-align: center; padding: 10px">Line</th>
<th style="text-align: center; padding: 10px">Column</th>
<th style="text-align: center; padding: 10px">Code</th>
<th style="text-align: left; padding: 10px">Violation Message</th>
</thead>
<tbody>
<tr t-foreach="violation_module['violations']['code_quality']" t-as="selected" style="border-bottom: 1px solid #e9e9e9">
<td style="padding: 10px" t-esc="selected['file']"/>
<td style="text-align: center; padding: 10px" t-esc="selected['line']"/>
<td style="text-align: center; padding: 10px" t-esc="selected['column']"/>
<td style="text-align: center; padding: 10px" t-esc="selected['code']"/>
<td style="text-align: left; padding: 10px; color: #8B0000;" t-esc="selected['message']"/>
</tr>
</tbody>
</table><br/>
</t>
<t t-else=""><span style="font-size: 0.6rem; color: black; margin: 0; margin-bottom: 10px; font-weight: 500;">No issues found</span><br/></t>
</t>
<!-- Maintainability Index -->
<t t-elif="violations_categ == 'maintainability_index'">
<h3 style="font-size: 1rem; color: #333; margin: 0; margin-bottom: 10px; font-weight: 700;">
Maintainability Index
</h3>
<t t-if="violation_module['violations']['maintainability_index']">
<table style="border-collapse: collapse; width: 100%; font-size: 0.8rem; border-radius: 10px; overflow: hidden; box-shadow: 0px 0px 20px rgb(0 0 0 / 5%);">
<thead style="background-color: #ddf66b; color: #333">
<th style="text-align: left; padding: 10px">File Name</th>
<th style="text-align: center; padding: 10px">Grade</th>
</thead>
<tbody>
<tr t-foreach="violation_module['violations']['maintainability_index']" t-as="selected" style="border-bottom: 1px solid #e9e9e9">
<td style="padding: 10px" t-esc="selected['file']"/>
<td style="text-align: center; padding: 10px" t-esc="selected['grade']"/>
</tr>
</tbody>
</table><br/>
</t>
<t t-else=""><span style="font-size: 0.6rem; color: black; margin: 0; margin-bottom: 10px; font-weight: 500;">No issues found</span><br/></t>
</t>
<!-- Import Sorting Check -->
<t t-elif="violations_categ == 'import_sort'">
<h3 style="font-size: 1rem; color: #333; margin: 0; margin-bottom: 10px; font-weight: 700;">
Import Sorting Check
</h3>
<t t-if="violation_module['violations']['import_sort']">
<table style="border-collapse: collapse; width: 100%; font-size: 0.8rem; border-radius: 10px; overflow: hidden; box-shadow: 0px 0px 20px rgb(0 0 0 / 5%);">
<thead style="background-color: #ddf66b; color: #333">
<th style="text-align: left; padding: 10px">File Name</th>
<th style="text-align: left; padding: 10px">Message</th>
</thead>
<tbody>
<tr t-foreach="violation_module['violations']['import_sort']" t-as="selected" style="border-bottom: 1px solid #e9e9e9">
<td style="padding: 10px" t-esc="selected['file']"/>
<td style="text-align: left; padding: 10px" t-esc="selected['message']"/>
</tr>
</tbody>
</table><br/>
</t>
<t t-else=""><span style="font-size: 0.6rem; color: black; margin: 0; margin-bottom: 10px; font-weight: 500;">No issues found</span><br/></t>
</t>
<!-- Code Formatting Check -->
<t t-elif="violations_categ == 'code_format'">
<h3 style="font-size: 1rem; color: #333; margin: 0; margin-bottom: 10px; font-weight: 700;">
Code Formatting Check
</h3>
<t t-if="violation_module['violations']['code_format']">
<table style="border-collapse: collapse; width: 100%; font-size: 0.8rem; border-radius: 10px; overflow: hidden; box-shadow: 0px 0px 20px rgb(0 0 0 / 5%);">
<thead style="background-color: #ddf66b; color: #333">
<th style="text-align: left; padding: 10px">File Name</th>
@ -165,7 +266,78 @@
</tbody>
</table><br/>
</t>
<t t-else=""><span style="font-size: 0.6rem; color: black; margin: 0; margin-bottom: 10px; font-weight: 500;">No issues found</span><br/></t>
</t>
<!-- Code Complexity -->
<t t-elif="violations_categ == 'code_complexity'">
<h3 style="font-size: 1rem; color: #333; margin: 0; margin-bottom: 10px; font-weight: 700;">
Code Complexity
</h3>
<t t-set="cc_colors" t-value="{'A': 'text-success', 'B': 'text-primary', 'C': 'text-warning', 'D': 'text-orange', 'E': 'text-danger', 'F': 'text-dark-red'}"/>
<t t-foreach="violation_module['violations']['code_complexity'].items()" t-as="cc_files">
<h3 style="font-size: 1rem; color: #333; margin: 0; margin-bottom: 10px">File: <t t-esc="cc_files[0]"/></h3>
<t t-if="cc_files[1]">
<table style="border-collapse: collapse; width: 100%; font-size: 0.8rem; border-radius: 10px; overflow: hidden; box-shadow: 0px 0px 20px rgb(0 0 0 / 5%);">
<thead style="background-color: #ddf66b; color: #333">
<th style="text-align: left; padding: 10px">Type</th>
<th style="text-align: left; padding: 10px">Name</th>
<th style="text-align: left; padding: 10px">Class Name</th>
<th style="text-align: center; padding: 10px">Line No.</th>
<th style="text-align: center; padding: 10px">End Line</th>
<th style="text-align: center; padding: 10px">Complexity</th>
<th style="text-align: center; padding: 10px">Rank</th>
</thead>
<tbody>
<tr t-if="cc_files[1]" t-foreach="cc_files[1]" t-as="line" style="border-bottom: 1px solid #e9e9e9">
<td style="padding: 10px" t-esc="line['type']"/>
<td style="text-align: left; padding: 10px" t-esc="line['name']"/>
<td style="text-align: left; padding: 10px">
<t t-if="line.get('classname')" t-esc="line['classname']"/>
</td>
<td style="text-align: center; padding: 10px" t-esc="line['lineno']"/>
<td style="text-align: center; padding: 10px" t-esc="line['endline']"/>
<td style="text-align: center; padding: 10px" t-att-class="cc_colors[line['rank']]" t-esc="line['complexity']"/>
<td style="text-align: center; padding: 10px" t-att-class="cc_colors[line['rank']]" t-esc="line['rank']"/>
</tr>
</tbody>
</table><br/>
</t>
<t t-else=""><span style="font-size: 0.6rem; color: black; margin: 0; margin-bottom: 10px; font-weight: 500;">No issues found</span><br/></t>
</t>
</t>
<!-- Security scan -->
<t t-elif="violations_categ == 'security_scan'">
<h3 style="font-size: 1rem; color: #333; margin: 0; margin-bottom: 10px; font-weight: 700;">
Security Scan
</h3>
<t t-if="violation_module['violations']['security_scan']">
<table style="border-collapse: collapse; width: 100%; font-size: 0.8rem; border-radius: 10px; overflow: hidden; box-shadow: 0px 0px 20px rgb(0 0 0 / 5%);">
<thead style="background-color: #ddf66b; color: #333">
<th style="text-align: left; padding: 10px">Test ID</th>
<th style="text-align: center; padding: 10px">Test Name</th>
<th style="text-align: center; padding: 10px">Severity</th>
<th style="text-align: center; padding: 10px">Confidence</th>
<th style="text-align: center; padding: 10px">Issue Text</th>
<th style="text-align: center; padding: 10px">File</th>
<th style="text-align: center; padding: 10px">Line</th>
</thead>
<tbody>
<tr t-foreach="violation_module['violations']['security_scan']" t-as="selected" style="border-bottom: 1px solid #e9e9e9">
<td style="padding: 10px" t-esc="selected['test_id']"/>
<td style="text-align: center; padding: 10px" t-esc="selected['test_name']"/>
<td style="text-align: center; padding: 10px" t-esc="selected['issue_severity']"/>
<td style="text-align: center; padding: 10px" t-esc="selected['issue_confidence']"/>
<td style="text-align: left; padding: 10px" t-esc="selected['issue_text']"/>
<td style="text-align: left; padding: 10px" t-esc="selected['filename']"/>
<td style="text-align: center; padding: 10px" t-esc="selected['line_number']"/>
</tr>
</tbody>
</table><br/>
</t>
<t t-else=""><span style="font-size: 0.6rem; color: black; margin: 0; margin-bottom: 10px; font-weight: 500;">No issues found</span><br/><br/></t>
</t>
</t>
</t>
</t>

181
odoo_health_report/static/src/xml/module_quality.xml

@ -180,6 +180,33 @@
</t>
<t t-else="">
<t t-if="state.module_selected">
<!-- Odoo Code Standards -->
<div class="mt-4">
<h3>Odoo Code Standards Check for: <t t-esc="module[1][0]"/></h3>
<table t-if="state.module_selected.odoo_standards_check" class="table table-bordered">
<thead>
<tr>
<th>File</th>
<th>Line</th>
<th>Issue</th>
<th>Suggestion</th>
</tr>
</thead>
<tbody>
<t t-foreach="state.module_selected.odoo_standards_check" t-as="issues" t-key="issues_index">
<tr>
<td t-esc="issues.file"/>
<td t-esc="issues.line"/>
<td class="error" t-esc="issues.issue"/>
<td><t t-if="issues.suggestion" t-esc="issues.suggestion"/></td>
</tr>
</t>
</tbody>
</table>
<t t-else="">
No issues found
</t>
</div>
<!-- Style &amp; Linting Check -->
<div class="mt-4">
@ -209,6 +236,86 @@
</t>
</div>
<!-- General Code Quality -->
<div class="mt-4">
<h3>General Code Quality for: <t t-esc="module[1][0]"/></h3>
<table t-if="state.module_selected.code_quality_check" class="table table-bordered">
<thead>
<tr>
<th>File Name</th>
<th>Line</th>
<th>Column</th>
<th>Code</th>
<th>Violation Message</th>
</tr>
</thead>
<tbody>
<t t-foreach="state.module_selected.code_quality_check" t-as="selected" t-key="selected_index">
<tr>
<td t-esc="selected['file']"/>
<td t-esc="selected['line']"/>
<td t-esc="selected['column']"/>
<td t-esc="selected['code']"/>
<td class="error">
<t t-esc="selected['message']"/>
</td>
</tr>
</t>
</tbody>
</table>
<t t-else="">
No issues found
</t>
</div>
<!-- Maintainability Index -->
<div class="mt-4">
<h3>Maintainability Index for: <t t-esc="module[1][0]"/></h3>
<table t-if="state.module_selected.mi_check" class="table table-bordered">
<thead>
<tr>
<th>File Name</th>
<th>Grade</th>
</tr>
</thead>
<tbody>
<t t-foreach="state.module_selected.mi_check" t-as="selected" t-key="selected_index">
<tr>
<td t-esc="selected['file']"/>
<td t-esc="selected['grade']"/>
</tr>
</t>
</tbody>
</table>
<t t-else="">
No issues found
</t>
</div>
<!-- Import Sorting -->
<div class="mt-4">
<h3>Import Sorting Check for: <t t-esc="module[1][0]"/></h3>
<table t-if="state.module_selected.import_sort_check" class="table table-bordered">
<thead>
<tr>
<th>File Name</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<t t-foreach="state.module_selected.import_sort_check" t-as="selected" t-key="selected_index">
<tr>
<td t-esc="selected['file']"/>
<td t-esc="selected['message']"/>
</tr>
</t>
</tbody>
</table>
<t t-else="">
No issues found
</t>
</div>
<!-- Coding Format -->
<div class="mt-4">
<h3>Code Formatting Check for: <t t-esc="module[1][0]"/></h3>
@ -228,8 +335,82 @@
</t>
</tbody>
</table>
<t t-else="">
No issues found
</t>
</div>
<!-- Code Complexity -->
<div class="mt-4">
<t t-set="cc_colors" t-value="{'A': 'text-success', 'B': 'text-primary', 'C': 'text-warning', 'D': 'text-orange', 'E': 'text-danger', 'F': 'text-dark-red'}"/>
<h3>Code Complexity for: <t t-esc="module[1][0]"/></h3>
<t t-foreach="Object.entries(state.module_selected.cc_check)" t-as="cc_files" t-key="cc_files_index">
<h5>File: <t t-esc="cc_files[0]"/></h5>
<table t-if="cc_files[1]" class="table table-bordered">
<thead>
<tr>
<th>Type</th>
<th>Name</th>
<th>Class Name</th>
<th>Line No.</th>
<th>End Line</th>
<th>Complexity</th>
<th>Rank</th>
</tr>
</thead>
<tbody>
<t t-if="cc_files[1]" t-foreach="cc_files[1]" t-as="line" t-key="line_index">
<tr>
<td t-esc="line.type"/>
<td t-esc="line.name"/>
<td><t t-if="line.classname" t-esc="line.classname"/></td>
<td t-esc="line.lineno"/>
<td t-esc="line.endline"/>
<td t-att-class="cc_colors[line.rank]" t-esc="line.complexity"/>
<td t-att-class="cc_colors[line.rank]" t-esc="line.rank"/>
</tr>
</t>
</tbody>
</table>
<t t-else="">
No issues found
</t>
</t>
</div>
<!-- Security scan -->
<div class="mt-4">
<h3>Security Scan for: <t t-esc="module[1][0]"/></h3>
<table t-if="state.module_selected.security_scan.length >= 1" class="table table-bordered">
<thead>
<tr>
<th>Test ID</th>
<th>Test Name</th>
<th>Severity</th>
<th>Confidence</th>
<th>Issue Text</th>
<th>File</th>
<th>Line</th>
</tr>
</thead>
<tbody>
<t t-foreach="state.module_selected.security_scan" t-as="selected" t-key="selected_index">
<tr>
<td t-esc="selected.test_id"/>
<td t-esc="selected.test_name"/>
<td t-esc="selected.issue_severity"/>
<td t-esc="selected.issue_confidence"/>
<td t-esc="selected.issue_text"/>
<td t-esc="selected.filename"/>
<td t-esc="selected.line_number"/>
</tr>
</t>
</tbody>
</table>
<t t-else="">
No issues found
</t>
</div>
</t>
</t>
</t>

Loading…
Cancel
Save