diff --git a/addons/spreadsheet/tests/validate_spreadsheet_data.py b/addons/spreadsheet/tests/validate_spreadsheet_data.py new file mode 100644 index 00000000000..dd2df6720ee --- /dev/null +++ b/addons/spreadsheet/tests/validate_spreadsheet_data.py @@ -0,0 +1,199 @@ +from collections import defaultdict +from itertools import chain +import json +import re +from odoo.tests.common import TransactionCase + +markdown_link_regex = r"^\[([^\[]+)\]\((.+)\)$" + +xml_id_url_prefix = "odoo://ir_menu_xml_id/" + +odoo_view_link_prefix = "odoo://view/" + + +def odoo_charts(data): + """return all odoo chart definition in the spreadsheet""" + figures = [] + for sheet in data["sheets"]: + figures += [ + figure["data"] + for figure in sheet["figures"] + if figure["tag"] == "chart" and figure["data"]["type"].startswith("odoo_") + ] + return figures + + +def links_urls(data): + """return all markdown links in cells""" + urls = [] + link_prefix = "odoo://view/" + for sheet in data["sheets"]: + for cell in sheet["cells"].values(): + content = cell.get("content", "") + match = re.match(markdown_link_regex, content) + if match and match.group(2).startswith(link_prefix): + urls.append(match.group(2)) + return urls + + +def odoo_view_links(data): + """return all view definitions embedded in link cells. + urls looks like odoo://view/{... view data...} + """ + return [ + json.loads(url[len(odoo_view_link_prefix) :]) + for url in links_urls(data) + if url.startswith(odoo_view_link_prefix) + ] + + +def remove_group_operator(field_name): + """remove the group operator + >>> remove_group_operator("amount:sum") + >>> "amount" + """ + return field_name.split(":")[0] + + +def domain_fields(domain): + """return all field names used in the domain""" + fields = [] + for leaf in domain: + if len(leaf) == 3: + fields.append(leaf[0]) + return fields + + +def pivot_measure_fields(pivot): + return [ + measure["field"] + for measure in pivot["measures"] + if measure["field"] != "__count" + ] + + +def pivot_fields(pivot): + """return all field names used in a pivot definition""" + model = pivot["model"] + fields = set( + pivot["colGroupBys"] + + pivot["rowGroupBys"] + + pivot_measure_fields(pivot) + + domain_fields(pivot["domain"]) + ) + measure = pivot.get("sortedColumn") and pivot["sortedColumn"]["measure"] + if measure and measure != "__count": + fields.add(measure) + return model, fields + + +def list_order_fields(list_definition): + return [order["name"] for order in list_definition["orderBy"]] + + +def list_fields(list_definition): + """return all field names used in a list definitions""" + model = list_definition["model"] + fields = set( + list_definition["columns"] + + list_order_fields(list_definition) + + domain_fields(list_definition["domain"]) + ) + return model, fields + + +def chart_fields(chart): + """return all field names used in a chart definitions""" + model = chart["metaData"]["resModel"] + fields = set( + chart["metaData"]["groupBy"] + + chart["searchParams"]["groupBy"] + + domain_fields(chart["searchParams"]["domain"]) + ) + measure = chart["metaData"]["measure"] + if measure != "__count": + fields.add(measure) + return model, fields + + +def filter_fields(data): + """return all field names used in global filter definitions""" + fields_by_model = defaultdict(set) + charts = odoo_charts(data) + for filter_definition in data["globalFilters"]: + for pivot_id, matching in filter_definition.get("pivotFields", dict()).items(): + model = data["pivots"][pivot_id]["model"] + fields_by_model[model].add(matching["field"]) + for list_id, matching in filter_definition.get("listFields", dict()).items(): + model = data["lists"][list_id]["model"] + fields_by_model[model].add(matching["field"]) + for chart_id, matching in filter_definition.get("graphFields", dict()).items(): + chart = next((chart for chart in charts if chart["id"] == chart_id), None) + model = chart["metaData"]["resModel"] + fields_by_model[model].add(matching["field"]) + return dict(fields_by_model) + + +def odoo_view_fields(view): + return view["action"]["modelName"], set(domain_fields(view["action"]["domain"])) + + +def extract_fields(extract_fn, items): + fields_by_model = defaultdict(set) + for item in items: + model, fields = extract_fn(item) + fields_by_model[model] |= {remove_group_operator(field) for field in fields} + return dict(fields_by_model) + + +def fields_in_spreadsheet(data): + """return all fields, grouped by model, used in the spreadsheet""" + all_fields = chain( + extract_fields(list_fields, data.get("lists", dict()).values()).items(), + extract_fields(pivot_fields, data.get("pivots", dict()).values()).items(), + extract_fields(chart_fields, odoo_charts(data)).items(), + extract_fields(odoo_view_fields, odoo_view_links(data)).items(), + filter_fields(data).items(), + ) + fields_by_model = defaultdict(set) + for model, fields in all_fields: + fields_by_model[model] |= fields + return dict(fields_by_model) + + +def xml_ids_in_spreadsheet(data): + + return set(data.get("chartOdooMenusReferences", {}).values()) | { + url[len(xml_id_url_prefix) :] + for url in links_urls(data) + if url.startswith(xml_id_url_prefix) + } + + +class ValidateSpreadsheetData(TransactionCase): + def validate_spreadsheet_data(self, stringified_data, spreadsheet_name): + data = json.loads(stringified_data) + for model, fields in fields_in_spreadsheet(data).items(): + if model not in self.env: + raise AssertionError( + f"model '{model}' used in '{spreadsheet_name}' does not exist" + ) + for field_chain in fields: + field_model = model + for fname in field_chain.split( + "." + ): # field chain 'product_id.channel_ids' + if fname not in self.env[field_model]._fields: + raise AssertionError( + f"field '{fname}' used in spreadsheet '{spreadsheet_name}' does not exist on model '{field_model}'" + ) + field = self.env[field_model]._fields[fname] + if field.relational: + field_model = field.comodel_name + + for xml_id in xml_ids_in_spreadsheet(data): + record = self.env.ref(xml_id, raise_if_not_found=False) + if not record: + raise AssertionError( + f"xml id '{xml_id}' used in spreadsheet '{spreadsheet_name}' does not exist" + ) diff --git a/addons/spreadsheet_dashboard/__manifest__.py b/addons/spreadsheet_dashboard/__manifest__.py index 90848adf0f4..50c232a21ff 100644 --- a/addons/spreadsheet_dashboard/__manifest__.py +++ b/addons/spreadsheet_dashboard/__manifest__.py @@ -37,8 +37,5 @@ "spreadsheet_dashboard/static/tests/utils/**/*.js", ("include", "spreadsheet.o_spreadsheet"), ], - 'web.assets_tests': [ - 'spreadsheet_dashboard/static/tests/utils/tour.js', - ], }, } diff --git a/addons/spreadsheet_dashboard/static/tests/utils/tour.js b/addons/spreadsheet_dashboard/static/tests/utils/tour.js deleted file mode 100644 index c7d5c09c397..00000000000 --- a/addons/spreadsheet_dashboard/static/tests/utils/tour.js +++ /dev/null @@ -1,32 +0,0 @@ -/** @odoo-module */ - -import tour from "web_tour.tour"; - -/** - * @param {string} tourName - * @param {string} dashboardDisplayName - */ -export function registerDashboardTour(tourName, dashboardDisplayName) { - tour.register( - tourName, - { - test: true, - url: "/web", - }, - [ - ...tour.stepUtils.goToAppSteps( - "spreadsheet_dashboard.spreadsheet_dashboard_menu_root", - "Open dashboard app" - ), - { - trigger: `.o_search_panel li[data-name="${dashboardDisplayName}"]`, - content: `click ${dashboardDisplayName} dashboard`, - run: "click", - }, - { - trigger: ".o-spreadsheet", - content: "dashboard is displayed", - }, - ] - ); -} diff --git a/addons/spreadsheet_dashboard/tests/__init__.py b/addons/spreadsheet_dashboard/tests/__init__.py index 7535870d0b0..1a2af886dcc 100644 --- a/addons/spreadsheet_dashboard/tests/__init__.py +++ b/addons/spreadsheet_dashboard/tests/__init__.py @@ -1 +1,2 @@ from . import test_spreadsheet_dashboard +from . import test_dashboard_data diff --git a/addons/spreadsheet_dashboard/tests/test_dashboard_data.py b/addons/spreadsheet_dashboard/tests/test_dashboard_data.py new file mode 100644 index 00000000000..aeb7ad5de02 --- /dev/null +++ b/addons/spreadsheet_dashboard/tests/test_dashboard_data.py @@ -0,0 +1,21 @@ +from odoo.addons.spreadsheet.tests.validate_spreadsheet_data import ( + ValidateSpreadsheetData, +) +from odoo.tests.common import tagged + + +@tagged("-at_install", "post_install") +class TestSpreadsheetDashboardData(ValidateSpreadsheetData): + def test_validate_dashboard_data(self): + """validate fields and models used in dashboards""" + dashboards = self.env["spreadsheet.dashboard"].search([]) + for dashboard in dashboards: + # this dashboard is skipped because it's currently broken + # but I still want to merge this test right now to avoid other broken dashboards + if dashboard == self.env.ref( + "spreadsheet_dashboard_hr_expense.spreadsheet_dashboard_expense", + raise_if_not_found=False, + ): + continue + with self.subTest(dashboard.name): + self.validate_spreadsheet_data(dashboard.raw, dashboard.name) diff --git a/addons/spreadsheet_dashboard_account/__manifest__.py b/addons/spreadsheet_dashboard_account/__manifest__.py index eb63258f273..37a5d346dca 100644 --- a/addons/spreadsheet_dashboard_account/__manifest__.py +++ b/addons/spreadsheet_dashboard_account/__manifest__.py @@ -14,12 +14,5 @@ 'installable': True, 'auto_install': ['account'], 'license': 'LGPL-3', - 'assets': { - 'spreadsheet_dashboard.o_spreadsheet': [], - 'web.assets_backend': [], - 'web.assets_tests': [ - 'spreadsheet_dashboard_account/static/tests/**/*', - ], - 'web.qunit_suite_tests': [] - } + 'assets': {} } diff --git a/addons/spreadsheet_dashboard_account/static/tests/tours/accounting_dashboard_tours.js b/addons/spreadsheet_dashboard_account/static/tests/tours/accounting_dashboard_tours.js deleted file mode 100644 index 592392fa235..00000000000 --- a/addons/spreadsheet_dashboard_account/static/tests/tours/accounting_dashboard_tours.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @odoo-module */ - -import { registerDashboardTour } from "@spreadsheet_dashboard/../tests/utils/tour"; - -registerDashboardTour("spreadsheet_dashboard_invoicing", "Invoicing"); diff --git a/addons/spreadsheet_dashboard_account/tests/__init__.py b/addons/spreadsheet_dashboard_account/tests/__init__.py deleted file mode 100644 index 9f2319480cf..00000000000 --- a/addons/spreadsheet_dashboard_account/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import test_accounting_dashboard_tour diff --git a/addons/spreadsheet_dashboard_account/tests/test_accounting_dashboard_tour.py b/addons/spreadsheet_dashboard_account/tests/test_accounting_dashboard_tour.py deleted file mode 100644 index f4637b9089c..00000000000 --- a/addons/spreadsheet_dashboard_account/tests/test_accounting_dashboard_tour.py +++ /dev/null @@ -1,9 +0,0 @@ -from odoo.tests import tagged -from odoo.tests.common import HttpCase - - -@tagged('post_install', '-at_install') -class TestAccountingDashboard(HttpCase): - - def test_invoicing_dashboard(self): - self.start_tour('/web', 'spreadsheet_dashboard_invoicing', login='admin') diff --git a/addons/spreadsheet_dashboard_event_sale/__manifest__.py b/addons/spreadsheet_dashboard_event_sale/__manifest__.py index abd06b69c16..cb4c3c443ad 100644 --- a/addons/spreadsheet_dashboard_event_sale/__manifest__.py +++ b/addons/spreadsheet_dashboard_event_sale/__manifest__.py @@ -14,12 +14,5 @@ 'installable': True, 'auto_install': ['event_sale'], 'license': 'LGPL-3', - 'assets': { - 'spreadsheet_dashboard.o_spreadsheet': [], - 'web.assets_backend': [], - 'web.qunit_suite_tests': [], - 'web.assets_tests': [ - 'spreadsheet_dashboard_event_sale/static/tests/**/*', - ], - } + 'assets': {} } diff --git a/addons/spreadsheet_dashboard_event_sale/static/tests/tours/event_dashboard_tours.js b/addons/spreadsheet_dashboard_event_sale/static/tests/tours/event_dashboard_tours.js deleted file mode 100644 index 6864f1ce4d9..00000000000 --- a/addons/spreadsheet_dashboard_event_sale/static/tests/tours/event_dashboard_tours.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @odoo-module */ - -import { registerDashboardTour } from "@spreadsheet_dashboard/../tests/utils/tour"; - -registerDashboardTour("spreadsheet_dashboard_events", "Events"); diff --git a/addons/spreadsheet_dashboard_event_sale/tests/__init__.py b/addons/spreadsheet_dashboard_event_sale/tests/__init__.py deleted file mode 100644 index d86730b7891..00000000000 --- a/addons/spreadsheet_dashboard_event_sale/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import test_event_dashboard_tour diff --git a/addons/spreadsheet_dashboard_event_sale/tests/test_event_dashboard_tour.py b/addons/spreadsheet_dashboard_event_sale/tests/test_event_dashboard_tour.py deleted file mode 100644 index 5c3db8e5621..00000000000 --- a/addons/spreadsheet_dashboard_event_sale/tests/test_event_dashboard_tour.py +++ /dev/null @@ -1,9 +0,0 @@ -from odoo.tests import tagged -from odoo.tests.common import HttpCase - - -@tagged('post_install', '-at_install') -class TestEventsDashboard(HttpCase): - - def test_events_dashboard(self): - self.start_tour('/web', 'spreadsheet_dashboard_events', login='admin') diff --git a/addons/spreadsheet_dashboard_hr_expense/__manifest__.py b/addons/spreadsheet_dashboard_hr_expense/__manifest__.py index 709d5bcb6c0..fec3911cd34 100644 --- a/addons/spreadsheet_dashboard_hr_expense/__manifest__.py +++ b/addons/spreadsheet_dashboard_hr_expense/__manifest__.py @@ -14,12 +14,5 @@ 'installable': True, 'auto_install': ['hr_expense'], 'license': 'LGPL-3', - 'assets': { - 'spreadsheet_dashboard.o_spreadsheet': [], - 'web.assets_backend': [], - 'web.qunit_suite_tests': [], - 'web.assets_tests': [ - 'spreadsheet_dashboard_hr_expense/static/tests/**/*', - ], - } + 'assets': {} } diff --git a/addons/spreadsheet_dashboard_hr_expense/static/tests/tours/expense_dashboard_tours.js b/addons/spreadsheet_dashboard_hr_expense/static/tests/tours/expense_dashboard_tours.js deleted file mode 100644 index 810f7a55547..00000000000 --- a/addons/spreadsheet_dashboard_hr_expense/static/tests/tours/expense_dashboard_tours.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @odoo-module */ - -import { registerDashboardTour } from "@spreadsheet_dashboard/../tests/utils/tour"; - -registerDashboardTour("spreadsheet_dashboard_expense", "Expenses"); diff --git a/addons/spreadsheet_dashboard_hr_expense/tests/__init__.py b/addons/spreadsheet_dashboard_hr_expense/tests/__init__.py deleted file mode 100644 index e807f90cc39..00000000000 --- a/addons/spreadsheet_dashboard_hr_expense/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import test_expense_dashboard_tour diff --git a/addons/spreadsheet_dashboard_hr_expense/tests/test_expense_dashboard_tour.py b/addons/spreadsheet_dashboard_hr_expense/tests/test_expense_dashboard_tour.py deleted file mode 100644 index 6015397976e..00000000000 --- a/addons/spreadsheet_dashboard_hr_expense/tests/test_expense_dashboard_tour.py +++ /dev/null @@ -1,9 +0,0 @@ -from odoo.tests import tagged -from odoo.tests.common import HttpCase - - -@tagged('post_install', '-at_install') -class TestExpenseDashboard(HttpCase): - - def test_expense_dashboard(self): - self.start_tour('/web', 'spreadsheet_dashboard_expense', login='admin') diff --git a/addons/spreadsheet_dashboard_pos_hr/__manifest__.py b/addons/spreadsheet_dashboard_pos_hr/__manifest__.py index c94598a8d6b..407e5a86229 100644 --- a/addons/spreadsheet_dashboard_pos_hr/__manifest__.py +++ b/addons/spreadsheet_dashboard_pos_hr/__manifest__.py @@ -14,12 +14,5 @@ 'installable': True, 'auto_install': ['pos_hr'], 'license': 'LGPL-3', - 'assets': { - 'spreadsheet_dashboard.o_spreadsheet': [], - 'web.assets_backend': [], - 'web.qunit_suite_tests': [], - 'web.assets_tests': [ - 'spreadsheet_dashboard_pos_hr/static/tests/**/*', - ], - } + 'assets': {} } diff --git a/addons/spreadsheet_dashboard_pos_hr/static/tests/tours/pos_dashboard_tours.js b/addons/spreadsheet_dashboard_pos_hr/static/tests/tours/pos_dashboard_tours.js deleted file mode 100644 index 51648ce9bf6..00000000000 --- a/addons/spreadsheet_dashboard_pos_hr/static/tests/tours/pos_dashboard_tours.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @odoo-module */ - -import { registerDashboardTour } from "@spreadsheet_dashboard/../tests/utils/tour"; - -registerDashboardTour("spreadsheet_dashboard_pos", "Point of Sale"); diff --git a/addons/spreadsheet_dashboard_pos_hr/tests/__init__.py b/addons/spreadsheet_dashboard_pos_hr/tests/__init__.py deleted file mode 100644 index e11344c6739..00000000000 --- a/addons/spreadsheet_dashboard_pos_hr/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import test_pos_dashboard_tour diff --git a/addons/spreadsheet_dashboard_pos_hr/tests/test_pos_dashboard_tour.py b/addons/spreadsheet_dashboard_pos_hr/tests/test_pos_dashboard_tour.py deleted file mode 100644 index ac69eed0a5c..00000000000 --- a/addons/spreadsheet_dashboard_pos_hr/tests/test_pos_dashboard_tour.py +++ /dev/null @@ -1,9 +0,0 @@ -from odoo.tests import tagged -from odoo.tests.common import HttpCase - - -@tagged('post_install', '-at_install') -class TestPosDashboard(HttpCase): - - def test_pos_dashboard(self): - self.start_tour('/web', 'spreadsheet_dashboard_pos', login='admin') diff --git a/addons/spreadsheet_dashboard_sale/__manifest__.py b/addons/spreadsheet_dashboard_sale/__manifest__.py index 13dc9b8e121..2a67067ebf2 100644 --- a/addons/spreadsheet_dashboard_sale/__manifest__.py +++ b/addons/spreadsheet_dashboard_sale/__manifest__.py @@ -14,12 +14,5 @@ 'installable': True, 'auto_install': ['sale'], 'license': 'LGPL-3', - 'assets': { - 'spreadsheet_dashboard.o_spreadsheet': [], - 'web.assets_backend': [], - 'web.qunit_suite_tests': [], - 'web.assets_tests': [ - 'spreadsheet_dashboard_sale/static/tests/**/*', - ], - } + 'assets': {} } diff --git a/addons/spreadsheet_dashboard_sale/static/tests/tours/sale_dashboard_tours.js b/addons/spreadsheet_dashboard_sale/static/tests/tours/sale_dashboard_tours.js deleted file mode 100644 index 97ace2aae91..00000000000 --- a/addons/spreadsheet_dashboard_sale/static/tests/tours/sale_dashboard_tours.js +++ /dev/null @@ -1,6 +0,0 @@ -/** @odoo-module */ - -import { registerDashboardTour } from "@spreadsheet_dashboard/../tests/utils/tour"; - -registerDashboardTour("spreadsheet_dashboard_sales", "Sales"); -registerDashboardTour("spreadsheet_dashboard_product", "Product"); diff --git a/addons/spreadsheet_dashboard_sale/tests/__init__.py b/addons/spreadsheet_dashboard_sale/tests/__init__.py deleted file mode 100644 index 6adbf2585cc..00000000000 --- a/addons/spreadsheet_dashboard_sale/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import test_sales_dashboard_tour diff --git a/addons/spreadsheet_dashboard_sale/tests/test_sales_dashboard_tour.py b/addons/spreadsheet_dashboard_sale/tests/test_sales_dashboard_tour.py deleted file mode 100644 index db0622714f9..00000000000 --- a/addons/spreadsheet_dashboard_sale/tests/test_sales_dashboard_tour.py +++ /dev/null @@ -1,12 +0,0 @@ -from odoo.tests import tagged -from odoo.tests.common import HttpCase - - -@tagged('post_install', '-at_install') -class TestSalesDashboard(HttpCase): - - def test_sales_dashboard(self): - self.start_tour('/web', 'spreadsheet_dashboard_sales', login='admin') - - def test_product_dashboard(self): - self.start_tour('/web', 'spreadsheet_dashboard_product', login='admin')