OCA-git-bot
4 years ago
15 changed files with 354 additions and 12 deletions
-
22sql_export/models/sql_export.py
-
3sql_export/views/sql_export_view.xml
-
17sql_export/wizard/wizard_file.py
-
47sql_export_excel/README.rst
-
1sql_export_excel/__init__.py
-
19sql_export_excel/__manifest__.py
-
1sql_export_excel/models/__init__.py
-
107sql_export_excel/models/sql_export.py
-
4sql_export_excel/readme/CONFIGURE.rst
-
1sql_export_excel/readme/CONTRIBUTORS.rst
-
4sql_export_excel/readme/DESCRIPTION.rst
-
1sql_export_excel/tests/__init__.py
-
107sql_export_excel/tests/test_sql_query_excel.py
-
21sql_export_excel/views/sql_export_view.xml
-
11sql_request_abstract/models/sql_request_mixin.py
@ -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,19 @@ |
|||
# Copyright 2019 Akretion |
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
|||
|
|||
{ |
|||
'name': 'SQL Export Excel', |
|||
'version': '12.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,107 @@ |
|||
# Copyright 2019 Akretion |
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
|||
|
|||
from odoo import api, exceptions, fields, models, _ |
|||
from io import BytesIO |
|||
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()._get_file_extension() |
|||
|
|||
@api.multi |
|||
def excel_get_data_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 = BytesIO() |
|||
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 = BytesIO() |
|||
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 @@ |
|||
from . import test_sql_query_excel |
@ -0,0 +1,107 @@ |
|||
# 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 odoo.tests.common import TransactionCase |
|||
import base64 |
|||
from io import BytesIO |
|||
import logging |
|||
|
|||
_logger = logging.getLogger(__name__) |
|||
|
|||
try: |
|||
import openpyxl |
|||
except ImportError: |
|||
_logger.debug('Can not import openpyxl') |
|||
|
|||
|
|||
class TestExportSqlQueryExcel(TransactionCase): |
|||
|
|||
def setUp(self): |
|||
super().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 = BytesIO(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 = BytesIO() |
|||
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> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue