From 8c305dd2fd70031705041ed2c83dceacc3ab4eab Mon Sep 17 00:00:00 2001 From: Florian da Costa Date: Sun, 28 Jul 2019 12:12:02 +0200 Subject: [PATCH] Add sql_export_excel --- sql_export_excel/README.rst | 47 ++++++++ sql_export_excel/__init__.py | 1 + sql_export_excel/__openerp__.py | 20 ++++ sql_export_excel/models/__init__.py | 1 + sql_export_excel/models/sql_export.py | 108 ++++++++++++++++++ sql_export_excel/readme/CONFIGURE.rst | 4 + sql_export_excel/readme/CONTRIBUTORS.rst | 1 + sql_export_excel/readme/DESCRIPTION.rst | 4 + sql_export_excel/tests/__init__.py | 2 + .../tests/test_sql_query_excel.py | 108 ++++++++++++++++++ sql_export_excel/views/sql_export_view.xml | 21 ++++ 11 files changed, 317 insertions(+) create mode 100644 sql_export_excel/README.rst create mode 100644 sql_export_excel/__init__.py create mode 100644 sql_export_excel/__openerp__.py create mode 100644 sql_export_excel/models/__init__.py create mode 100644 sql_export_excel/models/sql_export.py create mode 100644 sql_export_excel/readme/CONFIGURE.rst create mode 100644 sql_export_excel/readme/CONTRIBUTORS.rst create mode 100644 sql_export_excel/readme/DESCRIPTION.rst create mode 100644 sql_export_excel/tests/__init__.py create mode 100644 sql_export_excel/tests/test_sql_query_excel.py create mode 100644 sql_export_excel/views/sql_export_view.xml diff --git a/sql_export_excel/README.rst b/sql_export_excel/README.rst new file mode 100644 index 000000000..5fb7bb10c --- /dev/null +++ b/sql_export_excel/README.rst @@ -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 +`_. 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 + +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. diff --git a/sql_export_excel/__init__.py b/sql_export_excel/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/sql_export_excel/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sql_export_excel/__openerp__.py b/sql_export_excel/__openerp__.py new file mode 100644 index 000000000..c130eece7 --- /dev/null +++ b/sql_export_excel/__openerp__.py @@ -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, + } diff --git a/sql_export_excel/models/__init__.py b/sql_export_excel/models/__init__.py new file mode 100644 index 000000000..014462062 --- /dev/null +++ b/sql_export_excel/models/__init__.py @@ -0,0 +1 @@ +from . import sql_export diff --git a/sql_export_excel/models/sql_export.py b/sql_export_excel/models/sql_export.py new file mode 100644 index 000000000..bb1ea5e49 --- /dev/null +++ b/sql_export_excel/models/sql_export.py @@ -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 diff --git a/sql_export_excel/readme/CONFIGURE.rst b/sql_export_excel/readme/CONFIGURE.rst new file mode 100644 index 000000000..abf9be6e1 --- /dev/null +++ b/sql_export_excel/readme/CONFIGURE.rst @@ -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. diff --git a/sql_export_excel/readme/CONTRIBUTORS.rst b/sql_export_excel/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..0bddb053a --- /dev/null +++ b/sql_export_excel/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Florian da Costa diff --git a/sql_export_excel/readme/DESCRIPTION.rst b/sql_export_excel/readme/DESCRIPTION.rst new file mode 100644 index 000000000..4808db7b5 --- /dev/null +++ b/sql_export_excel/readme/DESCRIPTION.rst @@ -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. diff --git a/sql_export_excel/tests/__init__.py b/sql_export_excel/tests/__init__.py new file mode 100644 index 000000000..22c4421ab --- /dev/null +++ b/sql_export_excel/tests/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import test_sql_query_excel diff --git a/sql_export_excel/tests/test_sql_query_excel.py b/sql_export_excel/tests/test_sql_query_excel.py new file mode 100644 index 000000000..a51423c67 --- /dev/null +++ b/sql_export_excel/tests/test_sql_query_excel.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2019 Akretion () +# @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) diff --git a/sql_export_excel/views/sql_export_view.xml b/sql_export_excel/views/sql_export_view.xml new file mode 100644 index 000000000..98fc3981c --- /dev/null +++ b/sql_export_excel/views/sql_export_view.xml @@ -0,0 +1,21 @@ + + + + + + + sql.export + + + + + + + + + + + + + +