summaryrefslogtreecommitdiff
path: root/addons/base_import/models.py
diff options
context:
space:
mode:
authorXavier Morel <xmo@odoo.com>2015-08-26 16:04:14 +0200
committerXavier Morel <xmo@odoo.com>2015-08-26 16:50:09 +0200
commitf5a1693f064c4a85b7cbc71dc27d72bfcb5e66bf (patch)
tree631cb18936b1ca1c0d425fdf5ad4a43f49ac39af /addons/base_import/models.py
parentaa6ee3fddfbac63969d216ac2290766a63364006 (diff)
[ADD] base_import: support for XLS and ODS files
* added intermediate ``_read_file`` step dispatching between CSV, ODS and XLS(X) parsing - primary dispatch on mime type, secondary on file extension: + OS may not provide a relevant mime type if no software locally installed for the filetype (e.g. Windows sends excel files as application/octet-stream if excel is not installed, and ODS files as zip if OpenOffice/LibreOffice isn't installed) + applications may re-register extensions to non-standard mimetypes breaking the dispatcher So if the mimetype is found trust it, otherwise try with the filename's extension (if any) - all readers skip lines with only empty cells in their output - ODS and XLS content are "CSVified", rows are converted to arrays of unicode strings * UI altered to not assume CSV files everywhere, and avoid returning garbage preview data for non-CSV imports Various: * added a ``can_import`` utility function to tests.common, can be used to skip tests if an optional Python dependency is not installed (but one would like tests to run if the dependency is available) * fixed datetime issue in ir_fields * added converter for monetary fields (iso float) * spreadsheet don't have integers, twiddling required to ensure integral values won't be serialized as floats (breaking conversion back to Python) * improved some tests by asserting no error is generated (bonus: logs the error message if there is one, rather than just saying the result is blown) * because some systems only provide elderly versions of XLRD (e.g. current debian stable provides 0.9.2 from April 2013) - keep using xldate_as_tuple instead of 0.9.3's xldate_as_datetime, workaround is simple - check for xlrd.xlsx in case of pre-0.8 XLRD Authorship: Ronak Baxi <rba@odoo.com> Mohammed Shekha <msh@openerp.com> Task 10792 Closes #7285
Diffstat (limited to 'addons/base_import/models.py')
-rw-r--r--addons/base_import/models.py126
1 files changed, 110 insertions, 16 deletions
diff --git a/addons/base_import/models.py b/addons/base_import/models.py
index 784ae7f49f1..7463a02028e 100644
--- a/addons/base_import/models.py
+++ b/addons/base_import/models.py
@@ -1,7 +1,10 @@
import csv
+import datetime
+import io
import itertools
import logging
import operator
+import os
try:
from cStringIO import StringIO
@@ -12,10 +15,38 @@ 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
_logger = logging.getLogger(__name__)
+
+try:
+ import xlrd
+ try:
+ from xlrd import xlsx
+ except ImportError:
+ xlsx = None
+except ImportError:
+ xlrd = xlsx = None
+
+try:
+ import odf_ods_reader
+except ImportError:
+ odf_ods_reader = None
+
+FILE_TYPE_DICT = {
+ 'text/csv': ('csv', True, None),
+ 'application/vnd.ms-excel': ('xls', xlrd, 'xlrd'),
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ('xlsx', xlsx, 'xlrd >= 0.8'),
+ 'application/vnd.oasis.opendocument.spreadsheet': ('ods', odf_ods_reader, 'odfpy')
+}
+EXTENSIONS = {
+ '.' + ext: handler
+ for mime, (ext, handler, req) in FILE_TYPE_DICT.iteritems()
+}
+
class ir_import(orm.TransientModel):
_name = 'base_import.import'
# allow imports to survive for 12h in case user is slow
@@ -26,7 +57,7 @@ class ir_import(orm.TransientModel):
'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'),
+ 'file_type': fields.char(string='File Type'),
}
def get_fields(self, cr, uid, model, context=None,
@@ -126,6 +157,69 @@ class ir_import(orm.TransientModel):
# TODO: cache on model?
return fields
+ def _read_file(self, file_type, record, options):
+ (file_extension, handler, req) = FILE_TYPE_DICT.get(file_type, (None, None, None))
+ if handler:
+ return getattr(self, '_read_' + file_extension)(record, options)
+ # 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 ext and EXTENSIONS.get(ext):
+ return getattr(self, '_read_' + ext[1:])(record, options)
+
+ 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))
+
+ def _read_xls(self, record, options):
+ book = xlrd.open_workbook(file_contents=record.file)
+ sheet = book.sheet_by_index(0)
+ # emulate Sheet.get_rows for pre-0.9.4
+ for row in itertools.imap(sheet.row, range(sheet.nrows)):
+ values = []
+ for cell in row:
+ if cell.ctype is xlrd.XL_CELL_NUMBER:
+ is_float = cell.value % 1 != 0.0
+ values.append(
+ unicode(cell.value)
+ if is_float
+ else unicode(int(cell.value))
+ )
+ 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))
+ values.append(
+ dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
+ if is_datetime
+ else dt.strftime(DEFAULT_SERVER_DATE_FORMAT)
+ )
+ elif cell.ctype is xlrd.XL_CELL_BOOLEAN:
+ values.append(u'True' if cell.value else u'False')
+ elif cell.ctype is xlrd.XL_CELL_ERROR:
+ raise ValueError(
+ _("Error cell found while reading XLS/XLSX file: %s") %
+ xlrd.error_text_from_code.get(
+ cell.value, "unknown error code %s" % cell.value)
+ )
+ else:
+ values.append(cell.value)
+ if any(x for x in values if x.strip()):
+ yield values
+ _read_xlsx = _read_xls
+
+ def _read_ods(self, record, options):
+ doc = odf_ods_reader.ODSReader(file=io.BytesIO(record.file))
+
+ return (
+ row
+ for row in doc.getFirstSheet()
+ if any(x for x in row if x.strip())
+ )
+
def _read_csv(self, record, options):
""" Returns a CSV-parsed iterator of all empty lines in the file
@@ -137,15 +231,13 @@ class ir_import(orm.TransientModel):
quotechar=str(options['quoting']),
delimiter=str(options['separator']))
- def nonempty(row):
- return any(x for x in row if x.strip())
-
- csv_nonempty = itertools.ifilter(nonempty, csv_iterator)
# TODO: guess encoding with chardet? Or https://github.com/aadsm/jschardet
encoding = options.get('encoding', 'utf-8')
- return itertools.imap(
- lambda row: [item.decode(encoding) for item in row],
- csv_nonempty)
+ return (
+ [item.decode(encoding) for item in row]
+ for row in csv_iterator
+ if any(x for x in row if x.strip())
+ )
def _match_header(self, header, fields, options):
""" Attempts to match a given header to a field of the
@@ -211,10 +303,10 @@ class ir_import(orm.TransientModel):
return None, None
headers = next(rows)
- return headers, dict(
- (index, [field['name'] for field in self._match_header(header, fields, options)] or None)
+ 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):
""" Generates a preview of the uploaded files, and performs
@@ -236,7 +328,7 @@ class ir_import(orm.TransientModel):
fields = self.get_fields(cr, uid, record.res_model, context=context)
try:
- rows = self._read_csv(record, options)
+ rows = self._read_file(record.file_type, record, options)
headers, matches = self._match_headers(rows, fields, options)
# Match should have consumed the first row (iif headers), get
@@ -253,15 +345,17 @@ class ir_import(orm.TransientModel):
# 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 CSV parsing preview", exc_info=True)
+ _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')
return {
'error': str(e),
# iso-8859-1 ensures decoding will always succeed,
# even if it yields non-printable characters. This is
# in case of UnicodeDecodeError (or csv.Error
# compounded with UnicodeDecodeError)
- 'preview': record.file[:ERROR_PREVIEW_BYTES]
- .decode( 'iso-8859-1'),
+ 'preview': preview,
}
def _convert_import_data(self, record, fields, options, context=None):
@@ -287,7 +381,7 @@ class ir_import(orm.TransientModel):
# Get only list of actually imported fields
import_fields = filter(None, fields)
- rows_to_import = self._read_csv(record, options)
+ rows_to_import = self._read_file(record.file_type, record, options)
if options.get('headers'):
rows_to_import = itertools.islice(
rows_to_import, 1, None)