from collections import defaultdict from itertools import chain import base64 import json import re from odoo.tests.common import TransactionCase, Form from odoo.exceptions import ValidationError 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 += [ dict(figure["data"], id=figure["id"]) 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) odoo_version = data.get("odooVersion", 1) if odoo_version < 5: for filter_definition in data.get("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"]) else: for pivot in data["pivots"].values(): model = pivot["model"] field = pivot.get("fieldMatching", {}).get("chain") if field: fields_by_model[model].add(field) for _list in data["lists"].values(): model = _list["model"] field = _list.get("fieldMatching", {}).get("chain") if field: fields_by_model[model].add(field) for chart in charts: model = chart["metaData"]["resModel"] field = chart.get("fieldMatching", {}).get("chain") if field: fields_by_model[model].add(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 menus_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 menus_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" ) # check that the menu has an action. Root menus always have an action. if not record.action and record.parent_id.id: raise AssertionError( f"menu with xml id '{xml_id}' used in spreadsheet '{spreadsheet_name}' does not have an action" )