Add sql_export_excel
parent
08c5f372d0
commit
c5e89ac2ae
|
@ -0,0 +1,47 @@
|
||||||
|
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
|
||||||
|
:alt: License: AGPL-3
|
||||||
|
|
||||||
|
SQL Export Excel
|
||||||
|
================
|
||||||
|
|
||||||
|
Add the possibility to extract data from a sql query toward an excel file.
|
||||||
|
It is also possible to provide an template excel file for a query. In this case,
|
||||||
|
the data will be inserted in the specified sheet of the provided excel file. This
|
||||||
|
is usefull when doing a lot of calculation in excel and the data is coming from Odoo.
|
||||||
|
|
||||||
|
Known issues / Roadmap
|
||||||
|
======================
|
||||||
|
|
||||||
|
* It was designed to work with xlsx files only, xls format is not supported.
|
||||||
|
|
||||||
|
|
||||||
|
Bug Tracker
|
||||||
|
===========
|
||||||
|
|
||||||
|
Bugs are tracked on `GitHub Issues
|
||||||
|
<https://github.com/OCA/server-tools/issues>`_. In case of trouble, please
|
||||||
|
check there if your issue has already been reported. If you spotted it first,
|
||||||
|
help us smash it by providing detailed and welcomed feedback.
|
||||||
|
|
||||||
|
Credits
|
||||||
|
=======
|
||||||
|
|
||||||
|
Contributors
|
||||||
|
------------
|
||||||
|
|
||||||
|
* Florian da Costa <florian.dacosta@akretion.com>
|
||||||
|
|
||||||
|
Maintainer
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. image:: http://odoo-community.org/logo.png
|
||||||
|
:alt: Odoo Community Association
|
||||||
|
:target: http://odoo-community.org
|
||||||
|
|
||||||
|
This module is maintained by the OCA.
|
||||||
|
|
||||||
|
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||||
|
mission is to support the collaborative development of Odoo features and
|
||||||
|
promote its widespread use.
|
||||||
|
|
||||||
|
To contribute to this module, please visit http://odoo-community.org.
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2019 Akretion
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': 'SQL Export Excel',
|
||||||
|
'version': '9.0.1.0.0',
|
||||||
|
'author': 'Akretion,Odoo Community Association (OCA)',
|
||||||
|
'website': 'http://github/oca/server-tools',
|
||||||
|
'license': 'AGPL-3',
|
||||||
|
'category': 'Generic Modules/Others',
|
||||||
|
'summary': 'Allow to export a sql query to an excel file.',
|
||||||
|
'depends': [
|
||||||
|
'sql_export',
|
||||||
|
],
|
||||||
|
'data': [
|
||||||
|
'views/sql_export_view.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
from . import sql_export
|
|
@ -0,0 +1,108 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2019 Akretion
|
||||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
|
from openerp import api, exceptions, fields, models, _
|
||||||
|
from cStringIO import StringIO
|
||||||
|
import logging
|
||||||
|
import base64
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import openpyxl
|
||||||
|
except ImportError:
|
||||||
|
_logger.debug('Can not import openpyxl')
|
||||||
|
|
||||||
|
|
||||||
|
class SqlExport(models.Model):
|
||||||
|
_inherit = 'sql.export'
|
||||||
|
|
||||||
|
file_format = fields.Selection(
|
||||||
|
selection_add=[('excel', 'Excel')])
|
||||||
|
header = fields.Boolean(
|
||||||
|
default=True,
|
||||||
|
help="Indicate if the header should be exported to the file.")
|
||||||
|
attachment_id = fields.Many2one(
|
||||||
|
'ir.attachment', string='Excel Template',
|
||||||
|
help="If you configure an excel file (in xlsx format) here, the "
|
||||||
|
"result of the query will be injected in it.\nIt is usefull to "
|
||||||
|
"feed data in a excel file pre-configured with calculation")
|
||||||
|
sheet_position = fields.Integer(
|
||||||
|
default=1,
|
||||||
|
help="Indicate the sheet's position of the excel template where the "
|
||||||
|
"result of the sql query should be injected.")
|
||||||
|
row_position = fields.Integer(
|
||||||
|
default=1,
|
||||||
|
help="Indicate from which row the result of the query should be "
|
||||||
|
"injected.")
|
||||||
|
col_position = fields.Integer(
|
||||||
|
string="Column Position",
|
||||||
|
default=1,
|
||||||
|
help="Indicate from which column the result of the query should be "
|
||||||
|
"injected.")
|
||||||
|
|
||||||
|
@api.constrains('sheet_position')
|
||||||
|
def check_sheet_position(self):
|
||||||
|
for export in self:
|
||||||
|
if export.sheet_position < 1:
|
||||||
|
raise exceptions.ValidationError(
|
||||||
|
_("The sheet position can't be less than 1."))
|
||||||
|
|
||||||
|
@api.constrains('row_position')
|
||||||
|
def check_row_position(self):
|
||||||
|
for export in self:
|
||||||
|
if export.row_position < 1:
|
||||||
|
raise exceptions.ValidationError(
|
||||||
|
_("The row position can't be less than 1."))
|
||||||
|
|
||||||
|
@api.constrains('col_position')
|
||||||
|
def check_column_position(self):
|
||||||
|
for export in self:
|
||||||
|
if export.col_position < 1:
|
||||||
|
raise exceptions.ValidationError(
|
||||||
|
_("The column position can't be less than 1."))
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _get_file_extension(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if self.file_format == 'excel':
|
||||||
|
return 'xlsx'
|
||||||
|
else:
|
||||||
|
return super(SqlExport, self)._get_file_extension()
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def excel_get_datas_from_query(self, variable_dict):
|
||||||
|
self.ensure_one()
|
||||||
|
res = self._execute_sql_request(
|
||||||
|
params=variable_dict, mode='fetchall', header=self.header)
|
||||||
|
# Case we insert data in an existing excel file.
|
||||||
|
if self.attachment_id:
|
||||||
|
datas = self.attachment_id.datas
|
||||||
|
infile = StringIO()
|
||||||
|
infile.write(base64.b64decode(datas))
|
||||||
|
infile.seek(0)
|
||||||
|
wb = openpyxl.load_workbook(filename=infile)
|
||||||
|
sheets = wb.worksheets
|
||||||
|
try:
|
||||||
|
ws = sheets[self.sheet_position - 1]
|
||||||
|
except IndexError:
|
||||||
|
raise exceptions.ValidationError(
|
||||||
|
_("The Excel Template file contains less than %s sheets "
|
||||||
|
"Please, adjust the Sheet Position parameter."))
|
||||||
|
row_position = self.row_position or 1
|
||||||
|
col_position = self.col_position or 1
|
||||||
|
# Case of excel file creation
|
||||||
|
else:
|
||||||
|
wb = openpyxl.Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
row_position = 1
|
||||||
|
col_position = 1
|
||||||
|
for index, row in enumerate(res, row_position):
|
||||||
|
for col, val in enumerate(row, col_position):
|
||||||
|
ws.cell(row=index, column=col).value = val
|
||||||
|
output = StringIO()
|
||||||
|
wb.save(output)
|
||||||
|
output.getvalue()
|
||||||
|
output_datas = base64.b64encode(output.getvalue())
|
||||||
|
output.close()
|
||||||
|
return output_datas
|
|
@ -0,0 +1,4 @@
|
||||||
|
If you want Odoo to update an existing excel file, you should create an attachment
|
||||||
|
with the excel file and configure this attachment on the query.
|
||||||
|
Then, you can configure the query to indicate if Odoo should export the header and where it should
|
||||||
|
insert the data. By default, it will insert it in the first sheet, at first row/column.
|
|
@ -0,0 +1 @@
|
||||||
|
* Florian da Costa <florian.dacosta@akretion.com>
|
|
@ -0,0 +1,4 @@
|
||||||
|
Add the possibility to extract data from a sql query toward an excel file.
|
||||||
|
It is also possible to provide an template excel file for a query. In this case,
|
||||||
|
the data will be inserted in the specified sheet of the provided excel file. This
|
||||||
|
is usefull when doing a lot of calculation in excel and the data is coming from Odoo.
|
|
@ -0,0 +1,2 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import test_sql_query_excel
|
|
@ -0,0 +1,108 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2019 Akretion (<http://www.akretion.com>)
|
||||||
|
# @author: Florian da Costa
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
|
from openerp.tests.common import TransactionCase
|
||||||
|
import base64
|
||||||
|
from cStringIO import StringIO
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import openpyxl
|
||||||
|
except ImportError:
|
||||||
|
_logger.debug('Can not import openpyxl')
|
||||||
|
|
||||||
|
|
||||||
|
class TestExportSqlQueryExcel(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestExportSqlQueryExcel, self).setUp()
|
||||||
|
self.wizard_obj = self.env['sql.file.wizard']
|
||||||
|
|
||||||
|
def get_workbook_from_query(self, wizard):
|
||||||
|
wizard.export_sql()
|
||||||
|
decoded_data = base64.b64decode(wizard.binary_file)
|
||||||
|
xlsx_file = StringIO(decoded_data)
|
||||||
|
return openpyxl.load_workbook(xlsx_file)
|
||||||
|
|
||||||
|
def test_excel_file_generation(self):
|
||||||
|
test_query = "SELECT 'testcol1' as firstcol, 2 as second_col"
|
||||||
|
query_vals = {
|
||||||
|
'name': 'Test Query Excel',
|
||||||
|
'query': test_query,
|
||||||
|
'file_format': 'excel'
|
||||||
|
}
|
||||||
|
query = self.env['sql.export'].create(query_vals)
|
||||||
|
query.button_validate_sql_expression()
|
||||||
|
wizard = self.wizard_obj.create({
|
||||||
|
'sql_export_id': query.id,
|
||||||
|
})
|
||||||
|
workbook = self.get_workbook_from_query(wizard)
|
||||||
|
ws = workbook.active
|
||||||
|
# Check values, header should be here by default
|
||||||
|
self.assertEqual(ws.cell(row=1, column=1).value, 'firstcol')
|
||||||
|
self.assertEqual(ws.cell(row=2, column=1).value, 'testcol1')
|
||||||
|
self.assertEqual(ws.cell(row=2, column=2).value, 2)
|
||||||
|
|
||||||
|
query.write({'header': False})
|
||||||
|
wb2 = self.get_workbook_from_query(wizard)
|
||||||
|
ws2 = wb2.active
|
||||||
|
# Check values, the header should not be present
|
||||||
|
self.assertEqual(ws2.cell(row=1, column=1).value, 'testcol1')
|
||||||
|
self.assertEqual(ws2.cell(row=1, column=2).value, 2)
|
||||||
|
|
||||||
|
def test_excel_file_insert(self):
|
||||||
|
# Create excel file with 2 sheets. Create a header in second sheet
|
||||||
|
# where data will be inserted
|
||||||
|
wb = openpyxl.Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.cell(row=1, column=1, value="My Test Value")
|
||||||
|
ws2 = wb.create_sheet("data")
|
||||||
|
ws2.cell(row=1, column=1, value='Partner Id')
|
||||||
|
ws2.cell(row=1, column=2, value='Partner Name')
|
||||||
|
output = StringIO()
|
||||||
|
wb.save(output)
|
||||||
|
data = output.getvalue()
|
||||||
|
|
||||||
|
# Create attachment with the created xlsx file which will be used as
|
||||||
|
# template in the sql query
|
||||||
|
attachmnent_vals = {
|
||||||
|
'name': 'template xlsx sql export Res Partner',
|
||||||
|
'datas': base64.b64encode(data),
|
||||||
|
}
|
||||||
|
attachment = self.env['ir.attachment'].create(attachmnent_vals)
|
||||||
|
|
||||||
|
# Create the query and configure it to insert the data in the second
|
||||||
|
# sheet of the xlsx template file and start inserting data at the
|
||||||
|
# second row, ignoring header (because the template excel file
|
||||||
|
# already contains a header)
|
||||||
|
test_query = "SELECT id, name FROM res_partner"
|
||||||
|
query_vals = {
|
||||||
|
'name': 'Test Query Excel',
|
||||||
|
'query': test_query,
|
||||||
|
'file_format': 'excel',
|
||||||
|
'attachment_id': attachment.id,
|
||||||
|
'sheet_position': 2,
|
||||||
|
'header': False,
|
||||||
|
'row_position': 2,
|
||||||
|
}
|
||||||
|
query = self.env['sql.export'].create(query_vals)
|
||||||
|
query.button_validate_sql_expression()
|
||||||
|
wizard = self.wizard_obj.create({
|
||||||
|
'sql_export_id': query.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check the generated excel file. The first sheet should still contain
|
||||||
|
# the same data and the second sheet should have kept the header and
|
||||||
|
# inserted data from the query
|
||||||
|
wb2 = self.get_workbook_from_query(wizard)
|
||||||
|
sheets = wb2.worksheets
|
||||||
|
ws1 = sheets[0]
|
||||||
|
# Check values, header should be here by default
|
||||||
|
self.assertEqual(ws1.cell(row=1, column=1).value, 'My Test Value')
|
||||||
|
ws2 = sheets[1]
|
||||||
|
self.assertEqual(ws2.cell(row=1, column=1).value, 'Partner Id')
|
||||||
|
self.assertTrue(ws2.cell(row=2, column=1).value)
|
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<openerp>
|
||||||
|
<data>
|
||||||
|
|
||||||
|
|
||||||
|
<record id="sql_export_excel_view_form" model="ir.ui.view">
|
||||||
|
<field name="model">sql.export</field>
|
||||||
|
<field name="inherit_id" ref="sql_export.sql_export_view_form" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<field name="file_format" position="after">
|
||||||
|
<field name="header" attrs="{'invisible': [('file_format', '=', 'csv')]}"/>
|
||||||
|
<field name="attachment_id" attrs="{'invisible': [('file_format', '!=', 'excel')]}"/>
|
||||||
|
<field name="sheet_position" attrs="{'invisible': [('attachment_id', '=', False)]}"/>
|
||||||
|
<field name="row_position" attrs="{'invisible': [('attachment_id', '=', False)]}"/>
|
||||||
|
<field name="col_position" attrs="{'invisible': [('attachment_id', '=', False)]}"/>
|
||||||
|
</field>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</openerp>
|
Loading…
Reference in New Issue