[10.0][MIG][base_import_match] Migration and update

Includes:

- Normal migration steps.
- Usage of brand new `_inherit = "base"` in Odoo 10, which implies
  removing a lot of monkey-patching code.
- Log a warning when multiple matches are found.
This commit is contained in:
Jairo Llopis
2017-06-22 12:09:47 +02:00
committed by Augusto Weiss
parent 4f32abe7be
commit a3492e64eb
6 changed files with 96 additions and 165 deletions

View File

@@ -2,8 +2,10 @@
# Copyright 2016 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis
# Copyright 2016 Tecnativa - Vicent Cubells
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import api, fields, models
from openerp import SUPERUSER_ID # TODO remove in v10
import logging
from odoo import api, fields, models, tools
_logger = logging.getLogger(__name__)
class BaseImportMatch(models.Model):
@@ -21,7 +23,7 @@ class BaseImportMatch(models.Model):
"Model",
required=True,
ondelete="cascade",
domain=[("transient ", "=", False)],
domain=[("transient", "=", False)],
help="In this model you will apply the match.")
model_name = fields.Char(
related="model_id.model",
@@ -34,63 +36,18 @@ class BaseImportMatch(models.Model):
required=True,
help="Fields that will define an unique key.")
@api.multi
@api.onchange("model_id")
def _onchange_model_id(self):
self.field_ids.unlink()
self.field_ids = False
@api.model
def create(self, vals):
"""Wrap the model after creation."""
result = super(BaseImportMatch, self).create(vals)
self._load_autopatch(result.model_name)
return result
@api.multi
def unlink(self):
"""Unwrap the model after deletion."""
models = set(self.mapped("model_name"))
result = super(BaseImportMatch, self).unlink()
for model in models:
self._load_autopatch(model)
return result
@api.multi
def write(self, vals):
"""Wrap the model after writing."""
result = super(BaseImportMatch, self).write(vals)
if "model_id" in vals or "model_name" in vals:
for s in self:
self._load_autopatch(s.model_name)
return result
# TODO convert to @api.model_cr in v10
def _register_hook(self, cr):
"""Autopatch on init."""
models = set(
self.browse(
cr,
SUPERUSER_ID,
self.search(cr, SUPERUSER_ID, list()))
.mapped("model_name"))
for model in models:
self._load_autopatch(cr, SUPERUSER_ID, model)
@api.multi
@api.depends("model_id", "field_ids")
def _compute_name(self):
"""Automatic self-descriptive name for the setting records."""
for s in self:
s.name = u"{}: {}".format(
s.model_id.display_name,
" + ".join(
s.field_ids.mapped(
lambda r: (
(u"{} ({})" if r.conditional else u"{}").format(
r.field_id.name,
r.imported_value)))))
for one in self:
one.name = u"{}: {}".format(
one.model_id.display_name,
" + ".join(one.field_ids.mapped("display_name")),
)
@api.model
def _match_find(self, model, converted_row, imported_row):
@@ -100,12 +57,12 @@ class BaseImportMatch(models.Model):
imported data, and return a match for the first rule that returns a
single result.
:param openerp.models.Model model:
:param odoo.models.Model model:
Model object that is being imported.
:param dict converted_row:
Row converted to Odoo api format, like the 3rd value that
:meth:`openerp.models.Model._convert_records` returns.
:meth:`odoo.models.Model._convert_records` returns.
:param dict imported_row:
Row as it is being imported, in format::
@@ -116,18 +73,16 @@ class BaseImportMatch(models.Model):
...
}
:return openerp.models.Model:
:return odoo.models.Model:
Return a dataset with one single match if it was found, or an
empty dataset if none or multiple matches were found.
"""
# Get usable rules to perform matches
usable = self._usable_for_load(model._name, converted_row.keys())
usable = self._usable_rules(model._name, converted_row)
# Traverse usable combinations
for combination in usable:
combination_valid = True
domain = list()
for field in combination.field_ids:
# Check imported value if it is a conditional field
if field.conditional:
@@ -135,114 +90,26 @@ class BaseImportMatch(models.Model):
if imported_row[field.name] != field.imported_value:
combination_valid = False
break
domain.append((field.name, "=", converted_row[field.name]))
if not combination_valid:
continue
match = model.search(domain)
# When a single match is found, stop searching
if len(match) == 1:
return match
elif match:
_logger.warning(
"Found multiple matches for model %s and domain %s; "
"falling back to default behavior (create new record)",
model._name,
domain,
)
# Return an empty match if none or multiple was found
return model
@api.model
def _load_wrapper(self):
"""Create a new load patch method."""
@api.model
def wrapper(self, fields, data):
"""Try to identify rows by other pseudo-unique keys.
It searches for rows that have no XMLID specified, and gives them
one if any :attr:`~.field_ids` combination is found. With a valid
XMLID in place, Odoo will understand that it must *update* the
record instead of *creating* a new one.
"""
newdata = list()
# Data conversion to ORM format
import_fields = map(models.fix_import_export_id_paths, fields)
converted_data = self._convert_records(
self._extract_records(import_fields, data))
# Mock Odoo to believe the user is importing the ID field
if "id" not in fields:
fields.append("id")
import_fields.append(["id"])
# Needed to match with converted data field names
clean_fields = [f[0] for f in import_fields]
for dbid, xmlid, record, info in converted_data:
row = dict(zip(clean_fields, data[info["record"]]))
match = self
if xmlid:
# Skip rows with ID, they do not need all this
row["id"] = xmlid
elif dbid:
# Find the xmlid for this dbid
match = self.browse(dbid)
else:
# Store records that match a combination
match = self.env["base_import.match"]._match_find(
self, record, row)
# Give a valid XMLID to this row if a match was found
row["id"] = (match._BaseModel__export_xml_id()
if match else row.get("id", u""))
# Store the modified row, in the same order as fields
newdata.append(tuple(row[f] for f in clean_fields))
# Leave the rest to Odoo itself
del data
return wrapper.origin(self, fields, newdata)
# Flag to avoid confusions with other possible wrappers
wrapper.__base_import_match = True
return wrapper
@api.model
def _load_autopatch(self, model_name):
"""[Un]apply patch automatically."""
self._load_unpatch(model_name)
if self.search([("model_name", "=", model_name)]):
self._load_patch(model_name)
@api.model
def _load_patch(self, model_name):
"""Apply patch for :param:`model_name`'s load method.
:param str model_name:
Model technical name, such as ``res.partner``.
"""
self.env[model_name]._patch_method(
"load", self._load_wrapper())
@api.model
def _load_unpatch(self, model_name):
"""Apply patch for :param:`model_name`'s load method.
:param str model_name:
Model technical name, such as ``res.partner``.
"""
model = self.env[model_name]
# Unapply patch only if there is one
try:
if model.load.__base_import_match:
model._revert_method("load")
except AttributeError:
pass
@api.model
def _usable_for_load(self, model_name, fields):
@tools.ormcache("model_name", "fields")
def _usable_rules(self, model_name, fields):
"""Return a set of elements usable for calling ``load()``.
:param str model_name:
@@ -251,15 +118,16 @@ class BaseImportMatch(models.Model):
:param list(str|bool) fields:
List of field names being imported.
:return bool:
Indicates if we should patch its load method.
"""
result = self
available = self.search([("model_name", "=", model_name)])
# Use only criteria with all required fields to match
for record in available:
if all(f.name in fields for f in record.field_ids):
result += record
result |= record
return result
@@ -291,7 +159,15 @@ class BaseImportMatchField(models.Model):
"string, and comparison is case-sensitive so if you set 'True', "
"it will NOT match '1' nor 'true', only EXACTLY 'True'.")
@api.multi
@api.depends("conditional", "field_id", "imported_value")
def _compute_display_name(self):
for one in self:
pattern = u"{name} ({cond})" if one.conditional else u"{name}"
one.display_name = pattern.format(
name=one.field_id.name,
cond=one.imported_value,
)
@api.onchange("field_id", "match_id", "conditional", "imported_value")
def _onchange_match_id_name(self):
"""Update match name."""