[FIX] upgrade_analysis: misleading representation of read/write computed fields

Odoo 14 introduced the widescale usage of computed fields with readonly=False.
In that case, the compute method functions as a default that can also be used
to compute a value some time *after* the initial creation of the record.

In the OpenUpgrade analysis files, these fields would be misrepresented as
computed fields rather than fields with a default function. This change fixes
that.
pull/2416/head
Stefan Rijnhart 2022-10-11 21:27:36 +02:00
parent 8510463ffe
commit a01c6d478e
4 changed files with 72 additions and 57 deletions

View File

@ -134,8 +134,6 @@ def report_generic(new, old, attrs, reprs):
if attr == "required": if attr == "required":
if old[attr] != new["required"] and new["required"]: if old[attr] != new["required"] and new["required"]:
text = "now required" text = "now required"
if new["req_default"]:
text += ", req_default: %s" % new["req_default"]
fieldprint(old, new, "", text, reprs) fieldprint(old, new, "", text, reprs)
elif attr == "stored": elif attr == "stored":
if old[attr] != new[attr]: if old[attr] != new[attr]:
@ -284,15 +282,19 @@ def compare_sets(old_records, new_records):
], ],
) )
printkeys = [ # Info that is displayed for deleted fields
printkeys_old = [
"relation", "relation",
"required", "required",
"selection_keys", "selection_keys",
"req_default",
"_inherits", "_inherits",
"mode", "mode",
"attachment", "attachment",
] ]
# Info that is displayed for new fields
printkeys_new = printkeys_old + [
"hasdefault",
]
for column in old_records: for column in old_records:
if column["field"] == "_order": if column["field"] == "_order":
continue continue
@ -304,7 +306,7 @@ def compare_sets(old_records, new_records):
extra_message = ", ".join( extra_message = ", ".join(
[ [
k + ": " + str(column[k]) if k != str(column[k]) else k k + ": " + str(column[k]) if k != str(column[k]) else k
for k in printkeys for k in printkeys_old
if column[k] if column[k]
] ]
) )
@ -312,11 +314,6 @@ def compare_sets(old_records, new_records):
extra_message = " " + extra_message extra_message = " " + extra_message
fieldprint(column, "", "", "DEL" + extra_message, reprs) fieldprint(column, "", "", "DEL" + extra_message, reprs)
printkeys.extend(
[
"hasdefault",
]
)
for column in new_records: for column in new_records:
if column["field"] == "_order": if column["field"] == "_order":
continue continue
@ -325,13 +322,13 @@ def compare_sets(old_records, new_records):
continue continue
if column["mode"] == "create": if column["mode"] == "create":
column["mode"] = "" column["mode"] = ""
printkeys_plus = printkeys.copy() printkeys = printkeys_new.copy()
if column["isfunction"] or column["isrelated"]: if column["isfunction"] or column["isrelated"]:
printkeys_plus.extend(["isfunction", "isrelated", "stored"]) printkeys.extend(["isfunction", "isrelated", "stored"])
extra_message = ", ".join( extra_message = ", ".join(
[ [
k + ": " + str(column[k]) if k != str(column[k]) else k k + ": " + str(column[k]) if k != str(column[k]) else k
for k in printkeys_plus for k in printkeys
if column[k] if column[k]
] ]
) )

View File

@ -104,7 +104,6 @@ class UpgradeRecord(models.Model):
"required", "required",
"stored", "stored",
"selection_keys", "selection_keys",
"req_default",
"hasdefault", "hasdefault",
"table", "table",
"_inherits", "_inherits",

View File

@ -74,33 +74,56 @@ def compare_registries(cr, module, registry, local_registry):
old_field[key] = value old_field[key] = value
def isfunction(model, k): def hasdefault(field):
"""Return a representation of the field's default method.
The default method is only meaningful if the field is a regular read/write
field with a `default` method or a `compute` method.
Note that Odoo fields accept a literal value as a `default` attribute
this value is wrapped in a lambda expression in odoo/fields.py:
https://github.com/odoo/odoo/blob/7eeba9d/odoo/fields.py#L484-L487
"""
if ( if (
model._fields[k].compute not field.readonly # It's not a proper computed field
and not model._fields[k].related and not field.inverse # It's not a field that delegates their data
and not model._fields[k].company_dependent and not isrelated(field) # It's not an (unstored) related field.
):
if field.default:
return "default"
if field.compute:
return "compute"
return ""
def isfunction(field):
if (
field.compute
and (field.readonly or field.inverse)
and not field.related
and not field.company_dependent
): ):
return "function" return "function"
return "" return ""
def isproperty(model, k): def isproperty(field):
if model._fields[k].company_dependent: if field.company_dependent:
return "property" return "property"
return "" return ""
def isrelated(model, k): def isrelated(field):
if model._fields[k].related: if field.related:
return "related" return "related"
return "" return ""
def _get_relation(v): def _get_relation(field):
if v.type in ("many2many", "many2one", "one2many"): if field.type in ("many2many", "many2one", "one2many"):
return v.comodel_name return field.comodel_name
elif v.type == "many2one_reference": elif field.type == "many2one_reference":
return v.model_field return field.model_field
else: else:
return "" return ""
@ -125,41 +148,31 @@ def log_model(model, local_registry):
if model._inherits: if model._inherits:
model_registry["_inherits"] = {"_inherits": str(model._inherits)} model_registry["_inherits"] = {"_inherits": str(model._inherits)}
model_registry["_order"] = {"_order": model._order} model_registry["_order"] = {"_order": model._order}
for k, v in model._fields.items(): for fieldname, field in model._fields.items():
properties = { properties = {
"type": typemap.get(v.type, v.type), "type": typemap.get(field.type, field.type),
"isfunction": isfunction(model, k), "isfunction": isfunction(field),
"isproperty": isproperty(model, k), "isproperty": isproperty(field),
"isrelated": isrelated(model, k), "isrelated": isrelated(field),
"relation": _get_relation(v), "relation": _get_relation(field),
"table": v.relation if v.type == "many2many" else "", "table": field.relation if field.type == "many2many" else "",
"required": v.required and "required" or "", "required": field.required and "required" or "",
"stored": v.store and "stored" or "", "stored": field.store and "stored" or "",
"selection_keys": "", "selection_keys": "",
"req_default": "", "hasdefault": hasdefault(field),
"hasdefault": model._fields[k].default and "hasdefault" or "",
} }
if v.type == "selection": if field.type == "selection":
if isinstance(v.selection, (tuple, list)): if isinstance(field.selection, (tuple, list)):
properties["selection_keys"] = str(sorted(x[0] for x in v.selection)) properties["selection_keys"] = str(
sorted(x[0] for x in field.selection)
)
else: else:
properties["selection_keys"] = "function" properties["selection_keys"] = "function"
elif v.type == "binary": elif field.type == "binary":
properties["attachment"] = str(getattr(v, "attachment", False)) properties["attachment"] = str(getattr(field, "attachment", False))
default = model._fields[k].default
if v.required and default:
if (
callable(default)
or isinstance(default, str)
and getattr(model._fields[k], default, False)
and callable(getattr(model._fields[k], default))
):
properties["req_default"] = "function"
else:
properties["req_default"] = str(default)
for key, value in properties.items(): for key, value in properties.items():
if value: if value:
model_registry.setdefault(k, {})[key] = value model_registry.setdefault(fieldname, {})[key] = value
def log_xml_id(cr, module, xml_id): def log_xml_id(cr, module, xml_id):

View File

@ -42,9 +42,15 @@ class UpgradeInstallWizard(models.TransientModel):
modules = self.env["ir.module.module"].search(domain) modules = self.env["ir.module.module"].search(domain)
for start_pattern in BLACKLIST_MODULES_STARTS_WITH: for start_pattern in BLACKLIST_MODULES_STARTS_WITH:
modules = modules.filtered(lambda x: not x.name.startswith(start_pattern)) modules = modules.filtered(
lambda x, start_pattern=start_pattern: not x.name.startswith(
start_pattern
)
)
for end_pattern in BLACKLIST_MODULES_ENDS_WITH: for end_pattern in BLACKLIST_MODULES_ENDS_WITH:
modules = modules.filtered(lambda x: not x.name.endswith(end_pattern)) modules = modules.filtered(
lambda x, end_pattern=end_pattern: not x.name.endswith(end_pattern)
)
return [("id", "in", modules.ids)] return [("id", "in", modules.ids)]
@api.depends("module_ids") @api.depends("module_ids")