[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
This commit is contained in:
+110
-16
@@ -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)
|
||||
|
||||
@@ -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()))
|
||||
@@ -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'));
|
||||
|
||||
@@ -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"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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'],
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -7,7 +7,7 @@ import time
|
||||
import psycopg2
|
||||
import pytz
|
||||
|
||||
from openerp import models, api, _
|
||||
from openerp import models, fields, api, _
|
||||
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, ustr
|
||||
|
||||
REFERENCING_FIELDS = {None, 'id', '.id'}
|
||||
@@ -189,6 +189,8 @@ class ir_fields_converter(models.AbstractModel):
|
||||
value
|
||||
)
|
||||
|
||||
_str_to_monetary = _str_to_float
|
||||
|
||||
@api.model
|
||||
def _str_id(self, model, field, value):
|
||||
return value, []
|
||||
@@ -231,8 +233,7 @@ class ir_fields_converter(models.AbstractModel):
|
||||
@api.model
|
||||
def _str_to_datetime(self, model, field, value):
|
||||
try:
|
||||
parsed_value = datetime.datetime.strptime(
|
||||
value, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
parsed_value = fields.Datetime.from_string(value)
|
||||
except ValueError:
|
||||
raise self._format_import_error(
|
||||
ValueError,
|
||||
|
||||
@@ -6,6 +6,7 @@ helpers and classes to write tests.
|
||||
"""
|
||||
import errno
|
||||
import glob
|
||||
import importlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -392,3 +393,18 @@ class HttpCase(TransactionCase):
|
||||
phantomtest = os.path.join(os.path.dirname(__file__), 'phantomtest.js')
|
||||
cmd = ['phantomjs', phantomtest, json.dumps(options)]
|
||||
self.phantom_run(cmd, timeout)
|
||||
|
||||
def can_import(module):
|
||||
""" Checks if <module> can be imported, returns ``True`` if it can be,
|
||||
``False`` otherwise.
|
||||
|
||||
To use with ``unittest.skipUnless`` for tests conditional on *optional*
|
||||
dependencies, which may or may be present but must still be tested if
|
||||
possible.
|
||||
"""
|
||||
try:
|
||||
importlib.import_module(module)
|
||||
except ImportError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user