summaryrefslogtreecommitdiff
path: root/addons
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
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')
-rw-r--r--addons/base_import/models.py126
-rw-r--r--addons/base_import/odf_ods_reader.py98
-rw-r--r--addons/base_import/static/src/js/import.js3
-rw-r--r--addons/base_import/static/src/xml/import.xml19
-rw-r--r--addons/base_import/tests/test.odsbin0 -> 4634 bytes
-rw-r--r--addons/base_import/tests/test.xlsbin0 -> 6656 bytes
-rw-r--r--addons/base_import/tests/test.xlsxbin0 -> 6473 bytes
-rw-r--r--addons/base_import/tests/test_cases.py133
-rw-r--r--addons/base_setup/res_config.py2
9 files changed, 340 insertions, 41 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)
diff --git a/addons/base_import/odf_ods_reader.py b/addons/base_import/odf_ods_reader.py
new file mode 100644
index 00000000000..0f0142f3f04
--- /dev/null
+++ b/addons/base_import/odf_ods_reader.py
@@ -0,0 +1,98 @@
+# Copyright 2011 Marco Conti
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# sourced from https://github.com/marcoconti83/read-ods-with-odfpy
+# further altered locally
+
+from odf import opendocument
+from odf.table import Table, TableRow, TableCell
+from odf.text import P
+
+
+class ODSReader(object):
+
+ # loads the file
+ def __init__(self, file=None, content=None, clonespannedcolumns=None):
+ if not content:
+ self.clonespannedcolumns = clonespannedcolumns
+ self.doc = opendocument.load(file)
+ else:
+ self.clonespannedcolumns = clonespannedcolumns
+ self.doc = content
+ self.SHEETS = {}
+ for sheet in self.doc.spreadsheet.getElementsByType(Table):
+ self.readSheet(sheet)
+
+ # reads a sheet in the sheet dictionary, storing each sheet as an
+ # array (rows) of arrays (columns)
+ def readSheet(self, sheet):
+ name = sheet.getAttribute("name")
+ rows = sheet.getElementsByType(TableRow)
+ arrRows = []
+
+ # for each row
+ for row in rows:
+ arrCells = []
+ cells = row.getElementsByType(TableCell)
+
+ # for each cell
+ for count, cell in enumerate(cells, start=1):
+ # repeated value?
+ repeat = 0
+ if count != len(cells):
+ repeat = cell.getAttribute("numbercolumnsrepeated")
+ if not repeat:
+ repeat = 1
+ spanned = int(cell.getAttribute('numbercolumnsspanned') or 0)
+ # clone spanned cells
+ if self.clonespannedcolumns is not None and spanned > 1:
+ repeat = spanned
+
+ ps = cell.getElementsByType(P)
+ textContent = u""
+
+ # for each text/text:span node
+ for p in ps:
+ for n in p.childNodes:
+ if n.nodeType == 1 and n.tagName == "text:span":
+ for c in n.childNodes:
+ if c.nodeType == 3:
+ textContent = u'{}{}'.format(textContent, n.data)
+
+ if n.nodeType == 3:
+ textContent = u'{}{}'.format(textContent, n.data)
+
+ if textContent:
+ if not textContent.startswith("#"): # ignore comments cells
+ for rr in range(int(repeat)): # repeated?
+ arrCells.append(textContent)
+ else:
+ for rr in range(int(repeat)):
+ arrCells.append("")
+
+ # if row contained something
+ if arrCells:
+ arrRows.append(arrCells)
+
+ #else:
+ # print ("Empty or commented row (", row_comment, ")")
+
+ self.SHEETS[name] = arrRows
+
+ # returns a sheet as an array (rows) of arrays (columns)
+ def getSheet(self, name):
+ return self.SHEETS[name]
+
+ def getFirstSheet(self):
+ return next(iter(self.SHEETS.itervalues()))
diff --git a/addons/base_import/static/src/js/import.js b/addons/base_import/static/src/js/import.js
index 4854df76410..5c57ff708f5 100644
--- a/addons/base_import/static/src/js/import.js
+++ b/addons/base_import/static/src/js/import.js
@@ -144,7 +144,7 @@ var DataImport = Widget.extend(ControlPanelMixin, {
this.id = null;
this.Import = new Model('base_import.import');
this.session = session;
- action.display_name = _t('Import a CSV File'); // Displayed in the breadcrumbs
+ action.display_name = _t('Import a File'); // Displayed in the breadcrumbs
},
start: function () {
var self = this;
@@ -237,6 +237,7 @@ var DataImport = Widget.extend(ControlPanelMixin, {
if (!this.$('input.oe_import_file').val()) { return; }
this.$el.removeClass('oe_import_preview oe_import_error');
+ this.$el.find('.oe_import_toggle').toggle((this.$('input.oe_import_file')[0].files[0].type == "text/csv"));
jsonp(this.$el, {
url: '/base_import/set_file'
}, this.proxy('settings_changed'));
diff --git a/addons/base_import/static/src/xml/import.xml b/addons/base_import/static/src/xml/import.xml
index c3eb2dc2b0d..ae5c78c497d 100644
--- a/addons/base_import/static/src/xml/import.xml
+++ b/addons/base_import/static/src/xml/import.xml
@@ -5,15 +5,12 @@
<input type="hidden" name="session_id"
t-att-value="widget.session.session_id"/>
<input type="hidden" name="import_id"/>
- <p>Select the <a
- href="http://en.wikipedia.org/wiki/Comma-separated_values"
- target="_blank">.CSV</a>
- file to import. If you need a sample importable file, you
+ <p>Select the file to import. If you need a sample importable file, you
can use the export tool to generate one.</p>
<div class="oe_import_box">
- <label t-attf-for="file_#{_id}" autofocus="autofocus">CSV File:</label>
+ <label t-attf-for="file_#{_id}" autofocus="autofocus">File:</label>
<input type="file" id-attf-id="file_#{_id}"
- name="file" class="oe_import_file" accept=".csv"/>
+ name="file" class="oe_import_file" accept=".csv, .xls, .xlsx, .ods"/>
<button type="button" class="oe_import_file_reload"
disabled="disabled"
title="Reload data to check changes.">
@@ -21,7 +18,7 @@
</button>
<div class="oe_import_with_file">
<a href="#" class="oe_import_toggle">
- File Format Options…</a>
+ CSV Format Options…</a>
<div class="oe_import_toggled oe_import_options">
<p t-foreach="widget.opts" t-as="option">
<!-- no @name, avoid submission when file_update called -->
@@ -363,11 +360,11 @@
</t>
<t t-name="ImportView.preview.error">
<div class="oe_import_report oe_import_report_error">
- <p>Import preview failed due to: <t t-esc="error"/>. The issue is
- usually an incorrect file encoding.</p>
- <p>Here is the start of the file we could not import:</p>
+ <p>Import preview failed due to: <t t-esc="error"/>.</p>
+ <p>For CSV files, the issue could be an incorrect encoding.</p>
+ <p t-if="preview">Here is the start of the file we could not import:</p>
</div>
- <pre><t t-esc="preview"/></pre>
+ <pre t-if="preview"><t t-esc="preview"/></pre>
</t>
<ul t-name="ImportView.error">
<li t-foreach="errors" t-as="error"
diff --git a/addons/base_import/tests/test.ods b/addons/base_import/tests/test.ods
new file mode 100644
index 00000000000..b6bfa63be1f
--- /dev/null
+++ b/addons/base_import/tests/test.ods
Binary files differ
diff --git a/addons/base_import/tests/test.xls b/addons/base_import/tests/test.xls
new file mode 100644
index 00000000000..944f14c20c5
--- /dev/null
+++ b/addons/base_import/tests/test.xls
Binary files differ
diff --git a/addons/base_import/tests/test.xlsx b/addons/base_import/tests/test.xlsx
new file mode 100644
index 00000000000..c549c7375f0
--- /dev/null
+++ b/addons/base_import/tests/test.xlsx
Binary files differ
diff --git a/addons/base_import/tests/test_cases.py b/addons/base_import/tests/test_cases.py
index 873d749d55a..bd9feafb9ab 100644
--- a/addons/base_import/tests/test_cases.py
+++ b/addons/base_import/tests/test_cases.py
@@ -1,6 +1,9 @@
# -*- encoding: utf-8 -*-
-import unittest2
-from openerp.tests.common import TransactionCase
+import unittest
+
+from openerp.tests.common import TransactionCase, can_import
+from openerp.modules.module import get_module_resource
+
from .. import models
@@ -189,6 +192,8 @@ class test_preview(TransactionCase):
id = Import.create(self.cr, self.uid, {
'res_model': 'res.users',
'file': u"로그인,언어\nbob,1\n".encode('euc_kr'),
+ 'file_type': 'text/csv',
+ 'file_name': 'kr_data.csv',
})
return Import, id
@@ -217,14 +222,15 @@ class test_preview(TransactionCase):
})
self.assertTrue('error' in result)
- def test_success(self):
+ def test_csv_success(self):
Import = self.registry('base_import.import')
id = Import.create(self.cr, self.uid, {
'res_model': 'base_import.tests.models.preview',
'file': 'name,Some Value,Counter\n'
'foo,1,2\n'
'bar,3,4\n'
- 'qux,5,6\n'
+ 'qux,5,6\n',
+ 'file_type': 'text/csv'
})
result = Import.parse_preview(self.cr, self.uid, id, {
@@ -232,7 +238,7 @@ class test_preview(TransactionCase):
'separator': ',',
'headers': True,
})
-
+ self.assertIsNone(result.get('error'))
self.assertEqual(result['matches'], {0: ['name'], 1: ['somevalue'], 2: None})
self.assertEqual(result['headers'], ['name', 'Some Value', 'Counter'])
# Order depends on iteration order of fields_get
@@ -250,6 +256,100 @@ class test_preview(TransactionCase):
# Ensure we only have the response fields we expect
self.assertItemsEqual(result.keys(), ['matches', 'headers', 'fields', 'preview'])
+ @unittest.skipUnless(can_import('xlrd'), "XLRD module not available")
+ def test_xls_success(self):
+
+ Import = self.registry('base_import.import')
+ xls_file_path = get_module_resource('base_import', 'tests', 'test.xls')
+ file_content = open(xls_file_path, 'rb').read()
+ id = Import.create(self.cr, self.uid, {
+ 'res_model': 'base_import.tests.models.preview',
+ 'file': file_content,
+ 'file_type': 'application/vnd.ms-excel'
+ })
+
+ result = Import.parse_preview(self.cr, self.uid, id, {
+ 'headers': True,
+ })
+ self.assertIsNone(result.get('error'))
+ self.assertEqual(result['matches'], {0: ['name'], 1: ['somevalue'], 2: None})
+ self.assertEqual(result['headers'], ['name', 'Some Value', 'Counter'])
+ self.assertItemsEqual(result['fields'], [
+ ID_FIELD,
+ {'id': 'name', 'name': 'name', 'string': 'Name', 'required':False, 'fields': []},
+ {'id': 'somevalue', 'name': 'somevalue', 'string': 'Some Value', 'required':True, 'fields': []},
+ {'id': 'othervalue', 'name': 'othervalue', 'string': 'Other Variable', 'required':False, 'fields': []},
+ ])
+ self.assertEqual(result['preview'], [
+ ['foo', '1', '2'],
+ ['bar', '3', '4'],
+ ['qux', '5', '6'],
+ ])
+ # Ensure we only have the response fields we expect
+ self.assertItemsEqual(result.keys(), ['matches', 'headers', 'fields', 'preview'])
+
+ @unittest.skipUnless(can_import('xlrd.xlsx'), "XLRD/XLSX not available")
+ def test_xlsx_success(self):
+ Import = self.registry('base_import.import')
+ xlsx_file_path = get_module_resource('base_import', 'tests', 'test.xlsx')
+ file_content = open(xlsx_file_path, 'rb').read()
+ id = Import.create(self.cr, self.uid, {
+ 'res_model': 'base_import.tests.models.preview',
+ 'file': file_content,
+ 'file_type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+ })
+
+ result = Import.parse_preview(self.cr, self.uid, id, {
+ 'headers': True,
+ })
+ self.assertIsNone(result.get('error'))
+ self.assertEqual(result['matches'], {0: ['name'], 1: ['somevalue'], 2: None})
+ self.assertEqual(result['headers'], ['name', 'Some Value', 'Counter'])
+ self.assertItemsEqual(result['fields'], [
+ ID_FIELD,
+ {'id': 'name', 'name': 'name', 'string': 'Name', 'required':False, 'fields': []},
+ {'id': 'somevalue', 'name': 'somevalue', 'string': 'Some Value', 'required':True, 'fields': []},
+ {'id': 'othervalue', 'name': 'othervalue', 'string': 'Other Variable', 'required':False, 'fields': []},
+ ])
+ self.assertEqual(result['preview'], [
+ ['foo', '1', '2'],
+ ['bar', '3', '4'],
+ ['qux', '5', '6'],
+ ])
+ # Ensure we only have the response fields we expect
+ self.assertItemsEqual(result.keys(), ['matches', 'headers', 'fields', 'preview'])
+
+ @unittest.skipUnless(can_import('odf'), "ODFPY not available")
+ def test_ods_success(self):
+ Import = self.registry('base_import.import')
+ ods_file_path = get_module_resource('base_import', 'tests', 'test.ods')
+ file_content = open(ods_file_path, 'rb').read()
+ id = Import.create(self.cr, self.uid, {
+ 'res_model': 'base_import.tests.models.preview',
+ 'file': file_content,
+ 'file_type': 'application/vnd.oasis.opendocument.spreadsheet'
+ })
+
+ result = Import.parse_preview(self.cr, self.uid, id, {
+ 'headers': True,
+ })
+ self.assertIsNone(result.get('error'))
+ self.assertEqual(result['matches'], {0: ['name'], 1: ['somevalue'], 2: None})
+ self.assertEqual(result['headers'], ['name', 'Some Value', 'Counter'])
+ self.assertItemsEqual(result['fields'], [
+ ID_FIELD,
+ {'id': 'name', 'name': 'name', 'string': 'Name', 'required':False, 'fields': []},
+ {'id': 'somevalue', 'name': 'somevalue', 'string': 'Some Value', 'required':True, 'fields': []},
+ {'id': 'othervalue', 'name': 'othervalue', 'string': 'Other Variable', 'required':False, 'fields': []},
+ ])
+ self.assertEqual(result['preview'], [
+ ['foo', '1', '2'],
+ ['bar', '3', '4'],
+ ['aux', '5', '6'],
+ ])
+ # Ensure we only have the response fields we expect
+ self.assertItemsEqual(result.keys(), ['matches', 'headers', 'fields', 'preview'])
+
class test_convert_import_data(TransactionCase):
""" Tests conversion of base_import.import input into data which
can be fed to Model.import_data
@@ -261,7 +361,9 @@ class test_convert_import_data(TransactionCase):
'file': 'name,Some Value,Counter\n'
'foo,1,2\n'
'bar,3,4\n'
- 'qux,5,6\n'
+ 'qux,5,6\n',
+ 'file_type': 'text/csv'
+
})
record = Import.browse(self.cr, self.uid, id)
data, fields = Import._convert_import_data(
@@ -285,7 +387,8 @@ class test_convert_import_data(TransactionCase):
'file': 'name,Some Value,Counter\n'
'foo,1,2\n'
'bar,3,4\n'
- 'qux,5,6\n'
+ 'qux,5,6\n',
+ 'file_type': 'text/csv'
})
record = Import.browse(self.cr, self.uid, id)
data, fields = Import._convert_import_data(
@@ -309,7 +412,8 @@ class test_convert_import_data(TransactionCase):
'file': 'name,Some Value,Counter\n'
'foo,1,2\n'
',3,\n'
- ',5,6\n'
+ ',5,6\n',
+ 'file_type': 'text/csv'
})
record = Import.browse(self.cr, self.uid, id)
data, fields = Import._convert_import_data(
@@ -331,7 +435,8 @@ class test_convert_import_data(TransactionCase):
'\n'
'bar,2\n'
' \n'
- '\t \n'
+ '\t \n',
+ 'file_type': 'text/csv'
})
record = Import.browse(self.cr, self.uid, id)
data, fields = Import._convert_import_data(
@@ -350,7 +455,9 @@ class test_convert_import_data(TransactionCase):
id = Import.create(self.cr, self.uid, {
'res_model': 'base_import.tests.models.preview',
'file': 'name,Some Value,Counter\n'
- 'foo,1,2\n'
+ 'foo,1,2\n',
+ 'file_type': 'text/csv'
+
})
record = Import.browse(self.cr, self.uid, id)
@@ -366,7 +473,8 @@ class test_convert_import_data(TransactionCase):
id = Import.create(self.cr, self.uid, {
'res_model': 'base_import.tests.models.preview',
'file': 'name,Some Value,Counter\n'
- 'foo,1,2\n'
+ 'foo,1,2\n',
+ 'file_type': 'text/csv'
})
record = Import.browse(self.cr, self.uid, id)
@@ -397,7 +505,8 @@ class test_failures(TransactionCase):
Import = self.env['base_import.import']
imp = Import.create({
'res_model': 'ir.attachment',
- 'file': fout.getvalue()
+ 'file': fout.getvalue(),
+ 'file_type': 'text/csv'
})
[results] = imp.do(
['name', 'db_datas'],
diff --git a/addons/base_setup/res_config.py b/addons/base_setup/res_config.py
index 0a137e51d0b..5912569b030 100644
--- a/addons/base_setup/res_config.py
+++ b/addons/base_setup/res_config.py
@@ -18,7 +18,7 @@ class base_config_settings(osv.osv_memory):
'module_portal': fields.boolean('Activate the customer portal',
help="""Give your customers access to their documents."""),
'module_auth_oauth': fields.boolean('Use external authentication providers, sign in with Google...'),
- 'module_base_import': fields.boolean("Allow users to import data from CSV files"),
+ 'module_base_import': fields.boolean("Allow users to import data from CSV/XLS/XLSX/ODS files"),
'module_google_drive': fields.boolean('Attach Google documents to any record',
help="""This installs the module google_docs."""),
'module_google_calendar': fields.boolean('Allow the users to synchronize their calendar with Google Calendar',