226 lines
7.7 KiB
Python
226 lines
7.7 KiB
Python
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"
|
|
)
|