@ -2,8 +2,10 @@
# Copyright 2016 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis
# Copyright 2016 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis
# Copyright 2016 Tecnativa - Vicent Cubells
# Copyright 2016 Tecnativa - Vicent Cubells
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
# 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 ) :
class BaseImportMatch ( models . Model ) :
@ -34,63 +36,18 @@ class BaseImportMatch(models.Model):
required = True ,
required = True ,
help = " Fields that will define an unique key. " )
help = " Fields that will define an unique key. " )
@api.multi
@api.onchange ( " model_id " )
@api.onchange ( " model_id " )
def _onchange_model_id ( self ) :
def _onchange_model_id ( self ) :
self . field_ids . unlink ( )
@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 )
self . field_ids = False
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 " )
@api.depends ( " model_id " , " field_ids " )
def _compute_name ( self ) :
def _compute_name ( self ) :
""" Automatic self-descriptive name for the setting records. """
""" 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
@api.model
def _match_find ( self , model , converted_row , imported_row ) :
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
imported data , and return a match for the first rule that returns a
single result .
single result .
: param openerp . models . Model model :
: param odoo . models . Model model :
Model object that is being imported .
Model object that is being imported .
: param dict converted_row :
: param dict converted_row :
Row converted to Odoo api format , like the 3 rd value that
Row converted to Odoo api format , like the 3 rd value that
: meth : `openerp .models.Model._convert_records` returns .
: meth : `odoo .models.Model._convert_records` returns .
: param dict imported_row :
: param dict imported_row :
Row as it is being imported , in format : :
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
Return a dataset with one single match if it was found , or an
empty dataset if none or multiple matches were found .
empty dataset if none or multiple matches were found .
"""
"""
# Get usable rules to perform matches
# 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
# Traverse usable combinations
for combination in usable :
for combination in usable :
combination_valid = True
combination_valid = True
domain = list ( )
domain = list ( )
for field in combination . field_ids :
for field in combination . field_ids :
# Check imported value if it is a conditional field
# Check imported value if it is a conditional field
if field . conditional :
if field . conditional :
@ -135,114 +90,26 @@ class BaseImportMatch(models.Model):
if imported_row [ field . name ] != field . imported_value :
if imported_row [ field . name ] != field . imported_value :
combination_valid = False
combination_valid = False
break
break
domain . append ( ( field . name , " = " , converted_row [ field . name ] ) )
domain . append ( ( field . name , " = " , converted_row [ field . name ] ) )
if not combination_valid :
if not combination_valid :
continue
continue
match = model . search ( domain )
match = model . search ( domain )
# When a single match is found, stop searching
# When a single match is found, stop searching
if len ( match ) == 1 :
if len ( match ) == 1 :
return match
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 an empty match if none or multiple was found
return model
return model
@api.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()``.
""" Return a set of elements usable for calling ``load()``.
: param str model_name :
: param str model_name :
@ -251,15 +118,16 @@ class BaseImportMatch(models.Model):
: param list ( str | bool ) fields :
: param list ( str | bool ) fields :
List of field names being imported .
List of field names being imported .
: return bool :
Indicates if we should patch its load method .
"""
"""
result = self
result = self
available = self . search ( [ ( " model_name " , " = " , model_name ) ] )
available = self . search ( [ ( " model_name " , " = " , model_name ) ] )
# Use only criteria with all required fields to match
# Use only criteria with all required fields to match
for record in available :
for record in available :
if all ( f . name in fields for f in record . field_ids ) :
if all ( f . name in fields for f in record . field_ids ) :
result + = record
result | = record
return result
return result
@ -291,7 +159,15 @@ class BaseImportMatchField(models.Model):
" string, and comparison is case-sensitive so if you set ' True ' , "
" string, and comparison is case-sensitive so if you set ' True ' , "
" it will NOT match ' 1 ' nor ' true ' , only EXACTLY ' 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 " )
@api.onchange ( " field_id " , " match_id " , " conditional " , " imported_value " )
def _onchange_match_id_name ( self ) :
def _onchange_match_id_name ( self ) :
""" Update match name. """
""" Update match name. """