Browse Source

Merge PR #1659 into 12.0

Signed-off-by sebastienbeau
12.0-mig-module_prototyper_last
OCA-git-bot 4 years ago
parent
commit
f43f6d5af5
  1. 22
      sql_export/models/sql_export.py
  2. 3
      sql_export/views/sql_export_view.xml
  3. 17
      sql_export/wizard/wizard_file.py
  4. 47
      sql_export_excel/README.rst
  5. 1
      sql_export_excel/__init__.py
  6. 19
      sql_export_excel/__manifest__.py
  7. 1
      sql_export_excel/models/__init__.py
  8. 107
      sql_export_excel/models/sql_export.py
  9. 4
      sql_export_excel/readme/CONFIGURE.rst
  10. 1
      sql_export_excel/readme/CONTRIBUTORS.rst
  11. 4
      sql_export_excel/readme/DESCRIPTION.rst
  12. 1
      sql_export_excel/tests/__init__.py
  13. 107
      sql_export_excel/tests/test_sql_query_excel.py
  14. 21
      sql_export_excel/views/sql_export_view.xml
  15. 11
      sql_request_abstract/models/sql_request_mixin.py

22
sql_export/models/sql_export.py

@ -16,9 +16,14 @@ class SqlExport(models.Model):
_check_execution_enabled = False _check_execution_enabled = False
copy_options = fields.Char( copy_options = fields.Char(
string='Copy Options', required=True,
string='Copy Options', required=False,
default="CSV HEADER DELIMITER ';'") default="CSV HEADER DELIMITER ';'")
file_format = fields.Selection(
[('csv', 'CSV')],
default='csv',
required=True)
field_ids = fields.Many2many( field_ids = fields.Many2many(
'ir.model.fields', 'ir.model.fields',
'fields_sqlquery_rel', 'fields_sqlquery_rel',
@ -48,3 +53,18 @@ class SqlExport(models.Model):
'context': self.env.context, 'context': self.env.context,
'nodestroy': True, 'nodestroy': True,
} }
def _get_file_extension(self):
self.ensure_one()
if self.file_format == 'csv':
return 'csv'
def csv_get_data_from_query(self, variable_dict):
self.ensure_one()
# Execute Request
res = self._execute_sql_request(
params=variable_dict, mode='stdout',
copy_options=self.copy_options)
if self.encoding:
res = res.decode(self.encoding)
return res

3
sql_export/views/sql_export_view.xml

@ -21,7 +21,8 @@
</h1> </h1>
</group> </group>
<group name="option" groups="sql_request_abstract.group_sql_request_user"> <group name="option" groups="sql_request_abstract.group_sql_request_user">
<field name="copy_options"/>
<field name="file_format"/>
<field name="copy_options" attrs="{'invisible': [('file_format', '!=', 'csv')], 'required': [('file_format', '=', 'csv')]}"/>
<field name="encoding"/> <field name="encoding"/>
</group> </group>
<group name="request" string="SQL Request" groups="sql_request_abstract.group_sql_request_user"> <group name="request" string="SQL Request" groups="sql_request_abstract.group_sql_request_user">

17
sql_export/wizard/wizard_file.py

@ -72,16 +72,15 @@ class SqlFileWizard(models.TransientModel):
'force_user', self._uid) 'force_user', self._uid)
variable_dict['user_id'] = user_id variable_dict['user_id'] = user_id
# Execute Request
res = sql_export._execute_sql_request(
params=variable_dict, mode='stdout',
copy_options=sql_export.copy_options)
if self.sql_export_id.encoding:
res = res.decode(self.sql_export_id.encoding)
# Call different method depending on file_type since the logic will be
# different
method_name = '%s_get_data_from_query' % sql_export.file_format
data = getattr(sql_export, method_name)(variable_dict)
extension = sql_export._get_file_extension()
self.write({ self.write({
'binary_file': res,
'file_name': '%(name)s_%(date)s.csv' % {
'name': sql_export.name, 'date': date}
'binary_file': data,
'file_name': '%(name)s_%(date)s.%(extension)s' % {
'name': sql_export.name, 'date': date, 'extension': extension}
}) })
return { return {
'view_mode': 'form', 'view_mode': 'form',

47
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
<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.

1
sql_export_excel/__init__.py

@ -0,0 +1 @@
from . import models

19
sql_export_excel/__manifest__.py

@ -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,
}

1
sql_export_excel/models/__init__.py

@ -0,0 +1 @@
from . import sql_export

107
sql_export_excel/models/sql_export.py

@ -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

4
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.

1
sql_export_excel/readme/CONTRIBUTORS.rst

@ -0,0 +1 @@
* Florian da Costa <florian.dacosta@akretion.com>

4
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.

1
sql_export_excel/tests/__init__.py

@ -0,0 +1 @@
from . import test_sql_query_excel

107
sql_export_excel/tests/test_sql_query_excel.py

@ -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)

21
sql_export_excel/views/sql_export_view.xml

@ -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>

11
sql_request_abstract/models/sql_request_mixin.py

@ -104,7 +104,8 @@ class SQLRequestMixin(models.AbstractModel):
@api.multi @api.multi
def _execute_sql_request( def _execute_sql_request(
self, params=None, mode='fetchall', rollback=True, self, params=None, mode='fetchall', rollback=True,
view_name=False, copy_options="CSV HEADER DELIMITER ';'"):
view_name=False, copy_options="CSV HEADER DELIMITER ';'",
header=False):
"""Execute a SQL request on the current database. """Execute a SQL request on the current database.
??? This function checks before if the user has the ??? This function checks before if the user has the
@ -130,6 +131,9 @@ class SQLRequestMixin(models.AbstractModel):
:param copy_options: (str) mentions extra options for :param copy_options: (str) mentions extra options for
"COPY request STDOUT WITH xxx" request. "COPY request STDOUT WITH xxx" request.
(Ignored if @mode != 'stdout') (Ignored if @mode != 'stdout')
:param header: (boolean) if true, the header of the query will be
returned as first element of the list if the mode is fetchall.
(Ignored if @mode != fetchall)
..note:: The following exceptions could be raised: ..note:: The following exceptions could be raised:
psycopg2.ProgrammingError: Error in the SQL Request. psycopg2.ProgrammingError: Error in the SQL Request.
@ -179,6 +183,11 @@ class SQLRequestMixin(models.AbstractModel):
self.env.cr.execute(query) self.env.cr.execute(query)
if mode == 'fetchall': if mode == 'fetchall':
res = self.env.cr.fetchall() res = self.env.cr.fetchall()
if header:
colnames = [
desc[0] for desc in self.env.cr.description
]
res.insert(0, colnames)
elif mode == 'fetchone': elif mode == 'fetchone':
res = self.env.cr.fetchone() res = self.env.cr.fetchone()
finally: finally:

Loading…
Cancel
Save