[MIG] base_import : migrate to new api
This commit is contained in:
committed by
Jérome Maes
parent
cf3e8324f1
commit
46df9da439
@@ -1,19 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import cgi
|
||||
import json
|
||||
|
||||
from openerp.http import Controller, route
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
class ImportController(Controller):
|
||||
@route('/base_import/set_file', methods=['POST'])
|
||||
def set_file(self, req, file, import_id, jsonp='callback'):
|
||||
|
||||
class ImportController(http.Controller):
|
||||
|
||||
@http.route('/base_import/set_file', methods=['POST'])
|
||||
def set_file(self, file, import_id, jsonp='callback'):
|
||||
import_id = int(import_id)
|
||||
|
||||
written = req.session.model('base_import.import').write(import_id, {
|
||||
written = request.env['base_import.import'].browse(import_id).write({
|
||||
'file': file.read(),
|
||||
'file_name': file.filename,
|
||||
'file_type': file.content_type,
|
||||
}, req.context)
|
||||
})
|
||||
|
||||
return 'window.top.%s(%s)' % (
|
||||
cgi.escape(jsonp), json.dumps({'result': written}))
|
||||
return 'window.top.%s(%s)' % (cgi.escape(jsonp), json.dumps({'result': written}))
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import csv
|
||||
import datetime
|
||||
import io
|
||||
import itertools
|
||||
import logging
|
||||
import psycopg2
|
||||
import operator
|
||||
import os
|
||||
import re
|
||||
|
||||
from openerp.tools.mimetypes import guess_mimetype
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools.translate import _
|
||||
from odoo.tools.mimetypes import guess_mimetype
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
try:
|
||||
from cStringIO import StringIO
|
||||
except ImportError:
|
||||
from StringIO import StringIO
|
||||
|
||||
import psycopg2
|
||||
|
||||
from openerp.osv import orm, fields
|
||||
from openerp.tools.translate import _
|
||||
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, \
|
||||
DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
FIELDS_RECURSION_LIMIT = 2
|
||||
ERROR_PREVIEW_BYTES = 200
|
||||
@@ -50,21 +51,21 @@ EXTENSIONS = {
|
||||
for mime, (ext, handler, req) in FILE_TYPE_DICT.iteritems()
|
||||
}
|
||||
|
||||
class ir_import(orm.TransientModel):
|
||||
|
||||
class Import(models.TransientModel):
|
||||
|
||||
_name = 'base_import.import'
|
||||
|
||||
# allow imports to survive for 12h in case user is slow
|
||||
_transient_max_hours = 12.0
|
||||
|
||||
_columns = {
|
||||
'res_model': fields.char('Model'),
|
||||
'file': fields.binary(
|
||||
'File', help="File to check and/or import, raw binary (not base64)"),
|
||||
'file_name': fields.char('File Name'),
|
||||
'file_type': fields.char(string='File Type'),
|
||||
}
|
||||
res_model = fields.Char('Model')
|
||||
file = fields.Binary('File', help="File to check and/or import, raw binary (not base64)")
|
||||
file_name = fields.Char('File Name')
|
||||
file_type = fields.Char('File Type')
|
||||
|
||||
def get_fields(self, cr, uid, model, context=None,
|
||||
depth=FIELDS_RECURSION_LIMIT):
|
||||
@api.model
|
||||
def get_fields(self, model, depth=FIELDS_RECURSION_LIMIT):
|
||||
""" Recursively get fields for the provided model (through
|
||||
fields_get) and filter them according to importability
|
||||
|
||||
@@ -108,8 +109,8 @@ class ir_import(orm.TransientModel):
|
||||
:param str model: name of the model to get fields form
|
||||
:param int landing: depth of recursion into o2m fields
|
||||
"""
|
||||
model_obj = self.pool[model]
|
||||
fields = [{
|
||||
Model = self.env[model]
|
||||
importable_fields = [{
|
||||
'id': 'id',
|
||||
'name': 'id',
|
||||
'string': _("External ID"),
|
||||
@@ -117,9 +118,9 @@ class ir_import(orm.TransientModel):
|
||||
'fields': [],
|
||||
'type': 'id',
|
||||
}]
|
||||
fields_got = model_obj.fields_get(cr, uid, context=context)
|
||||
blacklist = orm.MAGIC_COLUMNS + [model_obj.CONCURRENCY_CHECK_FIELD]
|
||||
for name, field in fields_got.iteritems():
|
||||
model_fields = Model.fields_get()
|
||||
blacklist = models.MAGIC_COLUMNS + [Model.CONCURRENCY_CHECK_FIELD]
|
||||
for name, field in model_fields.iteritems():
|
||||
if name in blacklist:
|
||||
continue
|
||||
# an empty string means the field is deprecated, @deprecated must
|
||||
@@ -132,10 +133,9 @@ class ir_import(orm.TransientModel):
|
||||
continue
|
||||
# states = {state: [(attr, value), (attr2, value2)], state2:...}
|
||||
if not any(attr == 'readonly' and value is False
|
||||
for attr, value in itertools.chain.from_iterable(
|
||||
states.itervalues())):
|
||||
for attr, value in itertools.chain.from_iterable(states.itervalues())):
|
||||
continue
|
||||
f = {
|
||||
field_value = {
|
||||
'id': name,
|
||||
'name': name,
|
||||
'string': field['string'],
|
||||
@@ -146,60 +146,64 @@ class ir_import(orm.TransientModel):
|
||||
}
|
||||
|
||||
if field['type'] in ('many2many', 'many2one'):
|
||||
f['fields'] = [
|
||||
dict(f, name='id', string=_("External ID"), type='id'),
|
||||
dict(f, name='.id', string=_("Database ID"), type='id'),
|
||||
field_value['fields'] = [
|
||||
dict(field_value, name='id', string=_("External ID"), type='id'),
|
||||
dict(field_value, name='.id', string=_("Database ID"), type='id'),
|
||||
]
|
||||
elif field['type'] == 'one2many' and depth:
|
||||
f['fields'] = self.get_fields(
|
||||
cr, uid, field['relation'], context=context, depth=depth-1)
|
||||
if self.user_has_groups(cr, uid, 'base.group_no_one'):
|
||||
f['fields'].append({'id' : '.id', 'name': '.id', 'string': _("Database ID"), 'required': False, 'fields': [], 'type': 'id'})
|
||||
field_value['fields'] = self.get_fields(field['relation'], depth=depth-1)
|
||||
if self.user_has_groups('base.group_no_one'):
|
||||
field_value['fields'].append({'id': '.id', 'name': '.id', 'string': _("Database ID"), 'required': False, 'fields': [], 'type': 'id'})
|
||||
|
||||
fields.append(f)
|
||||
importable_fields.append(field_value)
|
||||
|
||||
# TODO: cache on model?
|
||||
return fields
|
||||
return importable_fields
|
||||
|
||||
def _read_file(self, file_type, record, options):
|
||||
@api.multi
|
||||
def _read_file(self, options):
|
||||
""" Dispatch to specific method to read file content, according to its mimetype or file type
|
||||
:param options : dict of reading options (quoting, separator, ...)
|
||||
"""
|
||||
self.ensure_one()
|
||||
# guess mimetype from file content
|
||||
mimetype = guess_mimetype(record.file)
|
||||
mimetype = guess_mimetype(self.file)
|
||||
(file_extension, handler, req) = FILE_TYPE_DICT.get(mimetype, (None, None, None))
|
||||
if handler:
|
||||
try:
|
||||
return getattr(self, '_read_' + file_extension)(record, options)
|
||||
return getattr(self, '_read_' + file_extension)(options)
|
||||
except Exception:
|
||||
_logger.warn("Failed to read file '%s' (transient id %d) using guessed mimetype %s",
|
||||
record.file_name or '<unknown>', record.id, mimetype)
|
||||
_logger.warn("Failed to read file '%s' (transient id %d) using guessed mimetype %s", self.file_name or '<unknown>', self.id, mimetype)
|
||||
|
||||
# try reading with user-provided mimetype
|
||||
(file_extension, handler, req) = FILE_TYPE_DICT.get(file_type, (None, None, None))
|
||||
(file_extension, handler, req) = FILE_TYPE_DICT.get(self.file_type, (None, None, None))
|
||||
if handler:
|
||||
try:
|
||||
return getattr(self, '_read_' + file_extension)(record, options)
|
||||
return getattr(self, '_read_' + file_extension)(options)
|
||||
except Exception:
|
||||
_logger.warn("Failed to read file '%s' (transient id %d) using user-provided mimetype %s",
|
||||
record.file_name or '<unknown>', record.id, file_type)
|
||||
_logger.warn("Failed to read file '%s' (transient id %d) using user-provided mimetype %s", self.file_name or '<unknown>', self.id, self.file_type)
|
||||
|
||||
# fallback on file extensions as mime types can be unreliable (e.g.
|
||||
# software setting incorrect mime types, or non-installed software
|
||||
# leading to browser not sending mime types)
|
||||
if record.file_name:
|
||||
p, ext = os.path.splitext(record.file_name)
|
||||
if self.file_name:
|
||||
p, ext = os.path.splitext(self.file_name)
|
||||
if ext in EXTENSIONS:
|
||||
try:
|
||||
return getattr(self, '_read_' + ext[1:])(record, options)
|
||||
return getattr(self, '_read_' + ext[1:])(options)
|
||||
except Exception:
|
||||
_logger.warn("Failed to read file '%s' (transient id %s) using file extension",
|
||||
record.file_name, record.id)
|
||||
_logger.warn("Failed to read file '%s' (transient id %s) using file extension", self.file_name, self.id)
|
||||
|
||||
if req:
|
||||
raise ImportError(_("Unable to load \"{extension}\" file: requires Python module \"{modname}\"").format(extension=file_extension, modname=req))
|
||||
raise ValueError(_("Unsupported file format \"{}\", import only supports CSV, ODS, XLS and XLSX").format(file_type))
|
||||
raise ValueError(_("Unsupported file format \"{}\", import only supports CSV, ODS, XLS and XLSX").format(self.file_type))
|
||||
|
||||
def _read_xls(self, record, options):
|
||||
book = xlrd.open_workbook(file_contents=record.file)
|
||||
@api.multi
|
||||
def _read_xls(self, options):
|
||||
""" Read file content, using xlrd lib """
|
||||
book = xlrd.open_workbook(file_contents=self.file)
|
||||
return self._read_xls_book(book)
|
||||
|
||||
def _read_xls_book(self, book):
|
||||
sheet = book.sheet_by_index(0)
|
||||
# emulate Sheet.get_rows for pre-0.9.4
|
||||
@@ -216,8 +220,7 @@ class ir_import(orm.TransientModel):
|
||||
elif cell.ctype is xlrd.XL_CELL_DATE:
|
||||
is_datetime = cell.value % 1 != 0.0
|
||||
# emulate xldate_as_datetime for pre-0.9.3
|
||||
dt = datetime.datetime(*xlrd.xldate.xldate_as_tuple(
|
||||
cell.value, book.datemode))
|
||||
dt = datetime.datetime(*xlrd.xldate.xldate_as_tuple(cell.value, book.datemode))
|
||||
values.append(
|
||||
dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
if is_datetime
|
||||
@@ -235,10 +238,14 @@ class ir_import(orm.TransientModel):
|
||||
values.append(cell.value)
|
||||
if any(x for x in values if x.strip()):
|
||||
yield values
|
||||
|
||||
# use the same method for xlsx and xls files
|
||||
_read_xlsx = _read_xls
|
||||
|
||||
def _read_ods(self, record, options):
|
||||
doc = odf_ods_reader.ODSReader(file=io.BytesIO(record.file))
|
||||
@api.multi
|
||||
def _read_ods(self, options):
|
||||
""" Read file content using ODSReader custom lib """
|
||||
doc = odf_ods_reader.ODSReader(file=io.BytesIO(self.file))
|
||||
|
||||
return (
|
||||
row
|
||||
@@ -246,13 +253,13 @@ class ir_import(orm.TransientModel):
|
||||
if any(x for x in row if x.strip())
|
||||
)
|
||||
|
||||
def _read_csv(self, record, options):
|
||||
@api.multi
|
||||
def _read_csv(self, options):
|
||||
""" Returns a CSV-parsed iterator of all empty lines in the file
|
||||
|
||||
:throws csv.Error: if an error is detected during CSV parsing
|
||||
:throws UnicodeDecodeError: if ``options.encoding`` is incorrect
|
||||
:throws csv.Error: if an error is detected during CSV parsing
|
||||
:throws UnicodeDecodeError: if ``options.encoding`` is incorrect
|
||||
"""
|
||||
csv_data = record.file
|
||||
csv_data = self.file
|
||||
|
||||
# TODO: guess encoding with chardet? Or https://github.com/aadsm/jschardet
|
||||
encoding = options.get('encoding', 'utf-8')
|
||||
@@ -271,7 +278,12 @@ class ir_import(orm.TransientModel):
|
||||
if any(x for x in row if x.strip())
|
||||
)
|
||||
|
||||
def _try_match_column(self, cr, uid, index, preview_values, fields, options):
|
||||
@api.model
|
||||
def _try_match_column(self, preview_values, options):
|
||||
""" Returns the potential field types, based on the preview values, using heuristics
|
||||
:param preview_values : list of value for the column to determine
|
||||
:param options : parsing options
|
||||
"""
|
||||
# If all values are empty in preview than can be any field
|
||||
if all([v == '' for v in preview_values]):
|
||||
return ['all']
|
||||
@@ -289,7 +301,7 @@ class ir_import(orm.TransientModel):
|
||||
except ValueError:
|
||||
pass
|
||||
# If all values are either True or False, type is boolean
|
||||
if all(val.lower() in ('true','false','t','f','') for val in preview_values):
|
||||
if all(val.lower() in ('true', 'false', 't', 'f', '') for val in preview_values):
|
||||
return ['boolean']
|
||||
# If all values can be cast to float, type is either float or monetary
|
||||
try:
|
||||
@@ -298,11 +310,11 @@ class ir_import(orm.TransientModel):
|
||||
if val == '':
|
||||
continue
|
||||
# value might have the currency symbol left or right from the value
|
||||
val = self._remove_currency_symbol(cr, uid, val)
|
||||
val = self._remove_currency_symbol(val)
|
||||
if val:
|
||||
if options.get('float_thousand_separator') and options.get('float_decimal_separator'):
|
||||
val = val.replace(options['float_thousand_separator'], '').replace(options['float_decimal_separator'], '.')
|
||||
# We are now sure that this is a float, but we still need to find the
|
||||
# We are now sure that this is a float, but we still need to find the
|
||||
# thousand and decimal separator
|
||||
else:
|
||||
if val.count('.') > 1:
|
||||
@@ -328,12 +340,12 @@ class ir_import(orm.TransientModel):
|
||||
pass
|
||||
# Try to see if all values are a date or datetime
|
||||
dt = datetime.datetime
|
||||
separator = [' ','/','-']
|
||||
separator = [' ', '/', '-']
|
||||
date_format = ['%mr%dr%Y', '%dr%mr%Y', '%Yr%mr%d', '%Yr%dr%m']
|
||||
date_patterns = [options['date_format']] if options.get('date_format') else []
|
||||
if not date_patterns:
|
||||
date_patterns = [pattern.replace('r', sep) for sep in separator for pattern in date_format]
|
||||
date_patterns.extend([p.replace('Y','y') for p in date_patterns])
|
||||
date_patterns.extend([p.replace('Y', 'y') for p in date_patterns])
|
||||
current_date_pattern = False
|
||||
for date_pattern in date_patterns:
|
||||
date_ok = True
|
||||
@@ -360,26 +372,26 @@ class ir_import(orm.TransientModel):
|
||||
|
||||
return ['text', 'char', 'datetime', 'selection', 'many2one', 'one2many', 'many2many', 'html']
|
||||
|
||||
def _find_type_from_preview(self, cr, uid, fields, options, preview):
|
||||
@api.model
|
||||
def _find_type_from_preview(self, options, preview):
|
||||
type_fields = []
|
||||
if preview:
|
||||
for column in range(0,len(preview[0])):
|
||||
for column in range(0, len(preview[0])):
|
||||
preview_values = [value[column].strip() for value in preview]
|
||||
type_field = self._try_match_column(cr, uid, column, preview_values, fields, options)
|
||||
type_field = self._try_match_column(preview_values, options)
|
||||
type_fields.append(type_field)
|
||||
return type_fields
|
||||
|
||||
|
||||
def _match_header(self, header, fields, options):
|
||||
""" Attempts to match a given header to a field of the
|
||||
imported model.
|
||||
imported model.
|
||||
|
||||
:param str header: header name from the CSV file
|
||||
:param fields:
|
||||
:param dict options:
|
||||
:returns: an empty list if the header couldn't be matched, or
|
||||
all the fields to traverse
|
||||
:rtype: list(Field)
|
||||
:param str header: header name from the CSV file
|
||||
:param fields:
|
||||
:param dict options:
|
||||
:returns: an empty list if the header couldn't be matched, or
|
||||
all the fields to traverse
|
||||
:rtype: list(Field)
|
||||
"""
|
||||
string_match = None
|
||||
for field in fields:
|
||||
@@ -407,7 +419,8 @@ class ir_import(orm.TransientModel):
|
||||
# readability of paths
|
||||
match = self._match_header(section.strip(), subfields, options)
|
||||
# Any match failure, exit
|
||||
if not match: return []
|
||||
if not match:
|
||||
return []
|
||||
# prep subfields for next iteration within match[0]
|
||||
field = match[0]
|
||||
subfields = field['fields']
|
||||
@@ -416,56 +429,55 @@ class ir_import(orm.TransientModel):
|
||||
|
||||
def _match_headers(self, rows, fields, options):
|
||||
""" Attempts to match the imported model's fields to the
|
||||
titles of the parsed CSV file, if the file is supposed to have
|
||||
headers.
|
||||
titles of the parsed CSV file, if the file is supposed to have
|
||||
headers.
|
||||
|
||||
Will consume the first line of the ``rows`` iterator.
|
||||
Will consume the first line of the ``rows`` iterator.
|
||||
|
||||
Returns a pair of (None, None) if headers were not requested
|
||||
or the list of headers and a dict mapping cell indices
|
||||
to key paths in the ``fields`` tree
|
||||
Returns a pair of (None, None) if headers were not requested
|
||||
or the list of headers and a dict mapping cell indices
|
||||
to key paths in the ``fields`` tree
|
||||
|
||||
:param Iterator rows:
|
||||
:param dict fields:
|
||||
:param dict options:
|
||||
:rtype: (None, None) | (list(str), dict(int: list(str)))
|
||||
:param Iterator rows:
|
||||
:param dict fields:
|
||||
:param dict options:
|
||||
:rtype: (None, None) | (list(str), dict(int: list(str)))
|
||||
"""
|
||||
if not options.get('headers'):
|
||||
return None, None
|
||||
|
||||
headers = next(rows)
|
||||
return headers, {
|
||||
return headers, {
|
||||
index: [field['name'] for field in self._match_header(header, fields, options)] or None
|
||||
for index, header in enumerate(headers)
|
||||
}
|
||||
|
||||
def parse_preview(self, cr, uid, id, options, count=10, context=None):
|
||||
@api.multi
|
||||
def parse_preview(self, options, count=10):
|
||||
""" Generates a preview of the uploaded files, and performs
|
||||
fields-matching between the import's file data and the model's
|
||||
columns.
|
||||
fields-matching between the import's file data and the model's
|
||||
columns.
|
||||
|
||||
If the headers are not requested (not options.headers),
|
||||
``matches`` and ``headers`` are both ``False``.
|
||||
If the headers are not requested (not options.headers),
|
||||
``matches`` and ``headers`` are both ``False``.
|
||||
|
||||
:param id: identifier of the import
|
||||
:param int count: number of preview lines to generate
|
||||
:param options: format-specific options.
|
||||
CSV: {encoding, quoting, separator, headers}
|
||||
:type options: {str, str, str, bool}
|
||||
:returns: {fields, matches, headers, preview} | {error, preview}
|
||||
:rtype: {dict(str: dict(...)), dict(int, list(str)), list(str), list(list(str))} | {str, str}
|
||||
:param int count: number of preview lines to generate
|
||||
:param options: format-specific options.
|
||||
CSV: {encoding, quoting, separator, headers}
|
||||
:type options: {str, str, str, bool}
|
||||
:returns: {fields, matches, headers, preview} | {error, preview}
|
||||
:rtype: {dict(str: dict(...)), dict(int, list(str)), list(str), list(list(str))} | {str, str}
|
||||
"""
|
||||
|
||||
(record,) = self.browse(cr, uid, [id], context=context)
|
||||
fields = self.get_fields(cr, uid, record.res_model, context=context)
|
||||
self.ensure_one()
|
||||
fields = self.get_fields(self.res_model)
|
||||
try:
|
||||
rows = self._read_file(record.file_type, record, options)
|
||||
rows = self._read_file(options)
|
||||
headers, matches = self._match_headers(rows, fields, options)
|
||||
# Match should have consumed the first row (iif headers), get
|
||||
# the ``count`` next rows for preview
|
||||
preview = list(itertools.islice(rows, count))
|
||||
assert preview, "CSV file seems to have no content"
|
||||
header_types = self._find_type_from_preview(cr, uid, fields, options, preview)
|
||||
header_types = self._find_type_from_preview(options, preview)
|
||||
if options.get('keep_matches', False) and len(options.get('fields', [])):
|
||||
matches = {}
|
||||
for index, match in enumerate(options.get('fields')):
|
||||
@@ -479,18 +491,18 @@ class ir_import(orm.TransientModel):
|
||||
'headers_type': header_types or False,
|
||||
'preview': preview,
|
||||
'options': options,
|
||||
'debug': self.user_has_groups(cr, uid, 'base.group_no_one'),
|
||||
'debug': self.user_has_groups('base.group_no_one'),
|
||||
}
|
||||
except Exception, e:
|
||||
except Exception, error:
|
||||
# Due to lazy generators, UnicodeDecodeError (for
|
||||
# instance) may only be raised when serializing the
|
||||
# preview to a list in the return.
|
||||
_logger.debug("Error during parsing preview", exc_info=True)
|
||||
preview = None
|
||||
if record.file_type == 'text/csv':
|
||||
preview = record.file[:ERROR_PREVIEW_BYTES].decode('iso-8859-1')
|
||||
if self.file_type == 'text/csv':
|
||||
preview = self.file[:ERROR_PREVIEW_BYTES].decode('iso-8859-1')
|
||||
return {
|
||||
'error': str(e),
|
||||
'error': str(error),
|
||||
# iso-8859-1 ensures decoding will always succeed,
|
||||
# even if it yields non-printable characters. This is
|
||||
# in case of UnicodeDecodeError (or csv.Error
|
||||
@@ -498,17 +510,17 @@ class ir_import(orm.TransientModel):
|
||||
'preview': preview,
|
||||
}
|
||||
|
||||
def _convert_import_data(self, record, fields, options, context=None):
|
||||
@api.model
|
||||
def _convert_import_data(self, fields, options):
|
||||
""" Extracts the input browse_record and fields list (with
|
||||
``False``-y placeholders for fields to *not* import) into a
|
||||
format Model.load can use: a fields list without holes
|
||||
and the precisely matching data matrix
|
||||
``False``-y placeholders for fields to *not* import) into a
|
||||
format Model.import_data can use: a fields list without holes
|
||||
and the precisely matching data matrix
|
||||
|
||||
:param browse_record record:
|
||||
:param list(str|bool): fields
|
||||
:returns: (data, fields)
|
||||
:rtype: (list(list(str)), list(str))
|
||||
:raises ValueError: in case the import data could not be converted
|
||||
:param list(str|bool): fields
|
||||
:returns: (data, fields)
|
||||
:rtype: (list(list(str)), list(str))
|
||||
:raises ValueError: in case the import data could not be converted
|
||||
"""
|
||||
# Get indices for non-empty fields
|
||||
indices = [index for index, field in enumerate(fields) if field]
|
||||
@@ -516,15 +528,16 @@ class ir_import(orm.TransientModel):
|
||||
raise ValueError(_("You must configure at least one field to import"))
|
||||
# If only one index, itemgetter will return an atom rather
|
||||
# than a 1-tuple
|
||||
if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
|
||||
else: mapper = operator.itemgetter(*indices)
|
||||
if len(indices) == 1:
|
||||
mapper = lambda row: [row[indices[0]]]
|
||||
else:
|
||||
mapper = operator.itemgetter(*indices)
|
||||
# Get only list of actually imported fields
|
||||
import_fields = filter(None, fields)
|
||||
|
||||
rows_to_import = self._read_file(record.file_type, record, options)
|
||||
rows_to_import = self._read_file(options)
|
||||
if options.get('headers'):
|
||||
rows_to_import = itertools.islice(
|
||||
rows_to_import, 1, None)
|
||||
rows_to_import = itertools.islice(rows_to_import, 1, None)
|
||||
data = [
|
||||
list(row) for row in itertools.imap(mapper, rows_to_import)
|
||||
# don't try inserting completely empty rows (e.g. from
|
||||
@@ -534,7 +547,8 @@ class ir_import(orm.TransientModel):
|
||||
|
||||
return data, import_fields
|
||||
|
||||
def _remove_currency_symbol(self, cr, uid, value):
|
||||
@api.model
|
||||
def _remove_currency_symbol(self, value):
|
||||
value = value.strip()
|
||||
negative = False
|
||||
# Careful that some countries use () for negative so replace it by - sign
|
||||
@@ -548,21 +562,22 @@ class ir_import(orm.TransientModel):
|
||||
return False
|
||||
if len(split_value) == 1:
|
||||
if float_regex.search(split_value[0]) is not None:
|
||||
return split_value[0] if not negative else '-'+split_value[0]
|
||||
return False
|
||||
return split_value[0] if not negative else '-' + split_value[0]
|
||||
return False
|
||||
else:
|
||||
# String has been split in 2, locate which index contains the float and which does not
|
||||
currency_index = 0
|
||||
if float_regex.search(split_value[0]) is not None:
|
||||
currency_index = 1
|
||||
# Check that currency exists
|
||||
currency = self.pool.get('res.currency').search(cr, uid, [('symbol', '=', split_value[currency_index].strip())])
|
||||
currency = self.env['res.currency'].search([('symbol', '=', split_value[currency_index].strip())])
|
||||
if len(currency):
|
||||
return split_value[(currency_index+1)%2] if not negative else '-'+split_value[(currency_index+1)%2]
|
||||
return split_value[currency_index + 1 % 2] if not negative else '-' + split_value[currency_index + 1 % 2]
|
||||
# Otherwise it is not a float with a currency symbol
|
||||
return False
|
||||
|
||||
def _parse_float_from_data(self, cr, uid, data, index, name, options, context=None):
|
||||
@api.model
|
||||
def _parse_float_from_data(self, data, index, name, options):
|
||||
thousand_separator = options.get('float_thousand_separator', ' ')
|
||||
decimal_separator = options.get('float_decimal_separator', '.')
|
||||
for line in data:
|
||||
@@ -570,13 +585,14 @@ class ir_import(orm.TransientModel):
|
||||
continue
|
||||
line[index] = line[index].replace(thousand_separator, '').replace(decimal_separator, '.')
|
||||
old_value = line[index]
|
||||
line[index] = self._remove_currency_symbol(cr, uid, line[index])
|
||||
if line[index] == False:
|
||||
line[index] = self._remove_currency_symbol(line[index])
|
||||
if line[index] is False:
|
||||
raise ValueError(_("Column %s contains incorrect values (value: %s)" % (name, old_value)))
|
||||
|
||||
def _parse_import_data(self, cr, uid, data, import_fields, record, options, context=None):
|
||||
@api.multi
|
||||
def _parse_import_data(self, data, import_fields, options):
|
||||
# Get fields of type date/datetime
|
||||
all_fields = self.pool[record.res_model].fields_get(cr, uid, context=context)
|
||||
all_fields = self.env[self.res_model].fields_get()
|
||||
for name, field in all_fields.iteritems():
|
||||
if field['type'] in ('date', 'datetime') and name in import_fields:
|
||||
# Parse date
|
||||
@@ -592,12 +608,13 @@ class ir_import(orm.TransientModel):
|
||||
raise ValueError(_("Column %s contains incorrect values (value: %s does not match date format" % (name, line[index])))
|
||||
elif field['type'] in ('float', 'monetary') and name in import_fields:
|
||||
# Parse float, sometimes float values from file have currency symbol or () to denote a negative value
|
||||
# We should be able to manage both case
|
||||
# We should be able to manage both case
|
||||
index = import_fields.index(name)
|
||||
self._parse_float_from_data(cr, uid, data, index, name, options, context=context)
|
||||
self._parse_float_from_data(data, index, name, options)
|
||||
return data
|
||||
|
||||
def do(self, cr, uid, id, fields, options, dryrun=False, context=None):
|
||||
@api.multi
|
||||
def do(self, fields, options, dryrun=False):
|
||||
""" Actual execution of the import
|
||||
|
||||
:param fields: import mapping: maps each column to a field,
|
||||
@@ -617,24 +634,22 @@ class ir_import(orm.TransientModel):
|
||||
``false`` if that data isn't available or provided)
|
||||
:rtype: list({type, message, record})
|
||||
"""
|
||||
cr.execute('SAVEPOINT import')
|
||||
self.ensure_one()
|
||||
self._cr.execute('SAVEPOINT import')
|
||||
|
||||
(record,) = self.browse(cr, uid, [id], context=context)
|
||||
try:
|
||||
data, import_fields = self._convert_import_data(
|
||||
record, fields, options, context=context)
|
||||
data, import_fields = self._convert_import_data(fields, options)
|
||||
# Parse date and float field
|
||||
data = self._parse_import_data(cr, uid, data, import_fields, record, options, context=context)
|
||||
except ValueError, e:
|
||||
data = self._parse_import_data(data, import_fields, options)
|
||||
except ValueError, error:
|
||||
return [{
|
||||
'type': 'error',
|
||||
'message': unicode(e),
|
||||
'message': unicode(error),
|
||||
'record': False,
|
||||
}]
|
||||
|
||||
_logger.info('importing %d rows...', len(data))
|
||||
import_result = self.pool[record.res_model].load(
|
||||
cr, uid, import_fields, data, context=context)
|
||||
import_result = self.env[self.res_model].load(import_fields, data)
|
||||
_logger.info('done')
|
||||
|
||||
# If transaction aborted, RELEASE SAVEPOINT is going to raise
|
||||
@@ -646,9 +661,9 @@ class ir_import(orm.TransientModel):
|
||||
# error in the results.
|
||||
try:
|
||||
if dryrun:
|
||||
cr.execute('ROLLBACK TO SAVEPOINT import')
|
||||
self._cr.execute('ROLLBACK TO SAVEPOINT import')
|
||||
else:
|
||||
cr.execute('RELEASE SAVEPOINT import')
|
||||
self._cr.execute('RELEASE SAVEPOINT import')
|
||||
except psycopg2.InternalError:
|
||||
pass
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ class test_basic_fields(BaseImportCase):
|
||||
{'id': 'value', 'name': 'id', 'string': 'External ID', 'required': False, 'fields': [], 'type': 'id'},
|
||||
{'id': 'value', 'name': '.id', 'string': 'Database ID', 'required': False, 'fields': [], 'type': 'id'},
|
||||
]))
|
||||
|
||||
|
||||
def test_m2o_required(self):
|
||||
""" If an m2o field is required, its three sub-fields are
|
||||
required as well (the client has to handle that: requiredness
|
||||
|
||||
Reference in New Issue
Block a user