diff --git a/.travis.yml b/.travis.yml
index c3378649c..874782661 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -25,7 +25,7 @@ install:
- git clone https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools
- export PATH=${HOME}/maintainer-quality-tools/travis:${PATH}
- travis_install_nightly
- - pip install python-ldap raven raven_sanitize_openerp bzr GitPython
+ - pip install python-ldap raven raven_sanitize_openerp bzr GitPython unicodecsv
- printf '[options]\n\nrunning_env = dev' > ${HOME}/.openerp_serverrc
script:
diff --git a/sql_view/__init__.py b/sql_view/__init__.py
new file mode 100644
index 000000000..f553d8ff3
--- /dev/null
+++ b/sql_view/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import models
+from . import wizards
diff --git a/sql_view/__openerp__.py b/sql_view/__openerp__.py
new file mode 100644
index 000000000..78f913f96
--- /dev/null
+++ b/sql_view/__openerp__.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+#
+#
+# Authors: Guewen Baconnier
+# Copyright 2015 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+#
+
+{'name': 'SQL Views',
+ 'version': '1.0',
+ 'author': 'Camptocamp,Odoo Community Association (OCA)',
+ 'license': 'AGPL-3',
+ 'category': 'Tools',
+ 'depends': ['base'],
+ 'description': """
+=========
+SQL Views
+=========
+
+This addon allows to create SQL views on the database. It also features
+a simple CSV export of the views to check their result.
+
+Usage
+=====
+
+To create new SQL views, you need to go to ``Settings > Technical >
+Database Structure > SQL Views``.
+
+Give a view a human name, a SQL name (which will be prefixed with
+``sql_view_`` in the database, and the definition of the view itself
+(without trailing semicolon).
+
+Known issues / Roadmap
+======================
+
+The CSV preview can be used to read any data on the database. So this
+menu **must** be accessible only by allowed admin users. By
+default, the module is configured to be accessible only by users having
+the ``Settings`` administration level.
+
+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 smashing it by providing a detailed and
+welcomed feedback
+`here
+`_.
+
+Credits
+=======
+
+Contributors
+------------
+
+* Guewen Baconnier
+
+Maintainer
+----------
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://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.
+ """,
+ 'website': 'http://www.camptocamp.com',
+ 'external_dependencies': {'python': ['unicodecsv']},
+ 'data': ['wizards/sql_view_csv_preview_views.xml',
+ 'views/sql_view_views.xml',
+ 'security/ir.model.access.csv',
+ ],
+ 'installable': True,
+ }
diff --git a/sql_view/i18n/fr.po b/sql_view/i18n/fr.po
new file mode 100644
index 000000000..d50307876
--- /dev/null
+++ b/sql_view/i18n/fr.po
@@ -0,0 +1,138 @@
+# Translation of OpenERP Server.
+# This file contains the translation of the following modules:
+# * sql_view
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: OpenERP Server 7.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-09-01 07:19+0000\n"
+"PO-Revision-Date: 2015-09-01 07:19+0000\n"
+"Last-Translator: Guewen Baconnier \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: sql_view
+#: sql_constraint:sql.view:0
+msgid "Another view has the same SQL name."
+msgstr "Une autre vue a le même nom SQL."
+
+#. module: sql_view
+#: field:sql.view.csv.preview,data:0
+msgid "CSV"
+msgstr "CSV"
+
+#. module: sql_view
+#: view:sql.view:0
+msgid "CSV Preview"
+msgstr "Aperçu en CSV"
+
+#. module: sql_view
+#: view:sql.view.csv.preview:0
+msgid "Cancel"
+msgstr "Annuler"
+
+#. module: sql_view
+#: view:sql.view.csv.preview:0
+msgid "Close"
+msgstr "Fermer"
+
+#. module: sql_view
+#: field:sql.view,complete_sql_name:0
+msgid "Complete SQL Name"
+msgstr "Nom SQL complet"
+
+#. module: sql_view
+#: view:sql.view:0
+#: field:sql.view,definition:0
+msgid "Definition"
+msgstr "Définition"
+
+#. module: sql_view
+#: code:addons/sql_view/models/sql_view.py:127
+#, python-format
+msgid "Error"
+msgstr "Erreur"
+
+#. module: sql_view
+#: field:sql.view.csv.preview,filename:0
+msgid "File Name"
+msgstr "Nom du fichier"
+
+#. module: sql_view
+#: field:sql.view.csv.preview,limit:0
+msgid "Limit"
+msgstr "Limite"
+
+#. module: sql_view
+#: help:sql.view,sql_name:0
+msgid "Name of the view. Will be prefixed by sql_view_"
+msgstr "Nom de la vue. Sera préfixé par sql_view_"
+
+#. module: sql_view
+#: help:sql.view.csv.preview,limit:0
+msgid "Number of records. 0 means infinite."
+msgstr "Nombre d'enregistrements. 0 signifie infini."
+
+#. module: sql_view
+#: view:sql.view.csv.preview:0
+msgid "Preview"
+msgstr "Aperçu"
+
+#. module: sql_view
+#: field:sql.view,sql_name:0
+msgid "SQL Name"
+msgstr "Nom SQL"
+
+#. module: sql_view
+#: view:sql.view:0
+msgid "SQL View"
+msgstr "Vue SQL"
+
+#. module: sql_view
+#: code:_description:0
+#: model:ir.actions.act_window,name:sql_view.action_sql_view_csv_preview
+#: view:sql.view.csv.preview:0
+msgid "SQL View CSV Preview"
+msgstr "Aperçu CSV de vue SQL"
+
+#. module: sql_view
+#: code:_description:0
+#: model:ir.actions.act_window,name:sql_view.action_sql_view
+#: model:ir.ui.menu,name:sql_view.menu_sql_view
+#: view:sql.view:0
+msgid "SQL Views"
+msgstr "Vues SQL"
+
+#. module: sql_view
+#: constraint:sql.view:0
+msgid "The SQL name is not a valid PostgreSQL identifier"
+msgstr "Le nom SQL n'est pas un identifiant PostgreSQL valide."
+
+#. module: sql_view
+#: code:addons/sql_view/models/sql_view.py:128
+#, python-format
+msgid "The definition of the view is not correct:\n"
+"\n"
+"%s"
+msgstr "La définition de la vue n'est pas correcte :\n"
+"\n"
+"%s"
+
+#. module: sql_view
+#: constraint:sql.view:0
+msgid "This SQL definition is not allowed"
+msgstr "Cette définition SQL n'est pas autorisée"
+
+#. module: sql_view
+#: field:sql.view,name:0
+msgid "View Name"
+msgstr "Nom de la vue"
+
+#. module: sql_view
+#: view:sql.view.csv.preview:0
+msgid "or"
+msgstr "ou"
diff --git a/sql_view/i18n/sql_view.pot b/sql_view/i18n/sql_view.pot
new file mode 100644
index 000000000..bb31db26b
--- /dev/null
+++ b/sql_view/i18n/sql_view.pot
@@ -0,0 +1,137 @@
+# Translation of OpenERP Server.
+# This file contains the translation of the following modules:
+# * sql_view
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: OpenERP Server 7.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-09-01 07:19+0000\n"
+"PO-Revision-Date: 2015-09-01 07:19+0000\n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: sql_view
+#: sql_constraint:sql.view:0
+msgid "Another view has the same SQL name."
+msgstr ""
+
+#. module: sql_view
+#: field:sql.view.csv.preview,data:0
+msgid "CSV"
+msgstr ""
+
+#. module: sql_view
+#: view:sql.view:0
+msgid "CSV Preview"
+msgstr ""
+
+#. module: sql_view
+#: view:sql.view.csv.preview:0
+msgid "Cancel"
+msgstr ""
+
+#. module: sql_view
+#: view:sql.view.csv.preview:0
+msgid "Close"
+msgstr ""
+
+#. module: sql_view
+#: field:sql.view,complete_sql_name:0
+msgid "Complete SQL Name"
+msgstr ""
+
+#. module: sql_view
+#: view:sql.view:0
+#: field:sql.view,definition:0
+msgid "Definition"
+msgstr ""
+
+#. module: sql_view
+#: code:addons/sql_view/models/sql_view.py:127
+#, python-format
+msgid "Error"
+msgstr ""
+
+#. module: sql_view
+#: field:sql.view.csv.preview,filename:0
+msgid "File Name"
+msgstr ""
+
+#. module: sql_view
+#: field:sql.view.csv.preview,limit:0
+msgid "Limit"
+msgstr ""
+
+#. module: sql_view
+#: help:sql.view,sql_name:0
+msgid "Name of the view. Will be prefixed by sql_view_"
+msgstr ""
+
+#. module: sql_view
+#: help:sql.view.csv.preview,limit:0
+msgid "Number of records. 0 means infinite."
+msgstr ""
+
+#. module: sql_view
+#: view:sql.view.csv.preview:0
+msgid "Preview"
+msgstr ""
+
+#. module: sql_view
+#: field:sql.view,sql_name:0
+msgid "SQL Name"
+msgstr ""
+
+#. module: sql_view
+#: view:sql.view:0
+msgid "SQL View"
+msgstr ""
+
+#. module: sql_view
+#: code:_description:0
+#: model:ir.actions.act_window,name:sql_view.action_sql_view_csv_preview
+#: view:sql.view.csv.preview:0
+msgid "SQL View CSV Preview"
+msgstr ""
+
+#. module: sql_view
+#: code:_description:0
+#: model:ir.actions.act_window,name:sql_view.action_sql_view
+#: model:ir.ui.menu,name:sql_view.menu_sql_view
+#: view:sql.view:0
+#, python-format
+msgid "SQL Views"
+msgstr ""
+
+#. module: sql_view
+#: constraint:sql.view:0
+msgid "The SQL name is not a valid PostgreSQL identifier"
+msgstr ""
+
+#. module: sql_view
+#: code:addons/sql_view/models/sql_view.py:128
+#, python-format
+msgid "The definition of the view is not correct:\n"
+"\n"
+"%s"
+msgstr ""
+
+#. module: sql_view
+#: constraint:sql.view:0
+msgid "This SQL definition is not allowed"
+msgstr ""
+
+#. module: sql_view
+#: field:sql.view,name:0
+msgid "View Name"
+msgstr ""
+
+#. module: sql_view
+#: view:sql.view.csv.preview:0
+msgid "or"
+msgstr ""
diff --git a/sql_view/models/__init__.py b/sql_view/models/__init__.py
new file mode 100644
index 000000000..f6da3dc49
--- /dev/null
+++ b/sql_view/models/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import sql_view
diff --git a/sql_view/models/sql_view.py b/sql_view/models/sql_view.py
new file mode 100644
index 000000000..e00469646
--- /dev/null
+++ b/sql_view/models/sql_view.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+#
+#
+# Authors: Guewen Baconnier
+# Copyright 2015 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+#
+
+import re
+import psycopg2
+from openerp.osv import orm, fields
+from openerp.tools.translate import _
+
+# views are created with a prefix to prevent conflicts
+SQL_VIEW_PREFIX = u'sql_view_'
+# 63 chars is the length of a postgres identifier
+USER_NAME_SIZE = 63 - len(SQL_VIEW_PREFIX)
+
+PG_NAME_RE = re.compile(r'^[a-z_][a-z0-9_$]*$', re.I)
+
+
+class SQLView(orm.Model):
+ _name = 'sql.view'
+ _description = 'SQL Views'
+
+ def _complete_from_sql_name(self, cr, uid, sql_name, context=None):
+ return SQL_VIEW_PREFIX + (sql_name or '')
+
+ def _compute_complete_sql_name(self, cr, uid, ids, name, args,
+ context=None):
+ res = {}
+ for sql_view in self.browse(cr, uid, ids, context=context):
+ res[sql_view.id] = self._complete_from_sql_name(cr, uid,
+ sql_view.sql_name,
+ context=context)
+ return res
+
+ _columns = {
+ 'name': fields.char(string='View Name', required=True),
+ 'sql_name': fields.char(string='SQL Name', required=True,
+ size=USER_NAME_SIZE,
+ help="Name of the view. Will be prefixed "
+ "by %s" % (SQL_VIEW_PREFIX,)),
+ 'complete_sql_name': fields.function(_compute_complete_sql_name,
+ string='Complete SQL Name',
+ readonly=True,
+ type='char'),
+ 'definition': fields.text(string='Definition', required=True),
+ }
+
+ def _check_sql_name(self, cr, uid, ids, context=None):
+ records = self.browse(cr, uid, ids, context=context)
+ return all(PG_NAME_RE.match(record.sql_name) for record in records)
+
+ def _check_definition(self, cr, uid, ids, context=None):
+ """ Forbid a SQL definition with unbalanced parenthesis.
+
+ The reason is that the view's definition will be enclosed in:
+
+ CREATE VIEW {view_name} AS ({definition})
+
+ The parenthesis around the definition prevent users to inject
+ and execute arbitrary queries after the SELECT part (by using a
+ semicolon). However, it would still be possible to craft a
+ definition like the following which would close the parenthesis
+ and start a new query:
+
+ SELECT * FROM res_users); DELETE FROM res_users WHERE id IN (1
+
+ This is no longer possible if we ensure that we don't have an
+ unbalanced closing parenthesis.
+
+ """
+ for record in self.browse(cr, uid, ids, context=context):
+ balanced = 0
+ for char in record.definition:
+ if char == '(':
+ balanced += 1
+ elif char == ')':
+ balanced -= 1
+ if balanced == -1:
+ return False
+ return True
+
+ _constraints = [
+ (_check_sql_name,
+ 'The SQL name is not a valid PostgreSQL identifier',
+ ['sql_name']),
+ (_check_definition,
+ 'This SQL definition is not allowed',
+ ['definition']),
+ ]
+
+ _sql_constraints = [
+ ('sql_name_uniq', 'unique (sql_name)',
+ 'Another view has the same SQL name.')
+ ]
+
+ def _sql_view_comment(self, cr, uid, sql_view, context=None):
+ return "%s (created by the module sql_view)" % sql_view.name
+
+ def _create_or_replace_sql_view(self, cr, uid, sql_view, context=None):
+ view_name = sql_view.complete_sql_name
+ try:
+ # The parenthesis around the definition are important:
+ # they prevent to insert a semicolon and another query after
+ # the view declaration
+ cr.execute(
+ "CREATE VIEW {view_name} AS ({definition})".format(
+ view_name=view_name,
+ definition=sql_view.definition)
+ )
+ except psycopg2.ProgrammingError as err:
+ raise orm.except_orm(
+ _('Error'),
+ _('The definition of the view is not correct:\n\n%s') % (err,)
+ )
+ comment = self._sql_view_comment(cr, uid, sql_view, context=context)
+ cr.execute(
+ "COMMENT ON VIEW {view_name} IS %s".format(view_name=view_name),
+ (comment,)
+ )
+
+ def _delete_sql_view(self, cr, uid, sql_view, context=None):
+ view_name = sql_view.complete_sql_name
+ cr.execute("DROP view IF EXISTS %s" % (view_name,))
+
+ def create(self, cr, uid, vals, context=None):
+ record_id = super(SQLView, self).create(cr, uid, vals,
+ context=context)
+
+ record = self.browse(cr, uid, record_id, context=context)
+ self._create_or_replace_sql_view(cr, uid, record, context=context)
+ return record_id
+
+ def write(self, cr, uid, ids, vals, context=None):
+ # If the name has been changed, we have to drop the view,
+ # it will be created with the new name.
+ # If the definition have been changed, we better have to delete
+ # it and create it again otherwise we can have 'cannot drop
+ # columns from view' errors.
+ for record in self.browse(cr, uid, ids, context=context):
+ self._delete_sql_view(cr, uid, record, context=context)
+
+ result = super(SQLView, self).write(cr, uid, ids, vals,
+ context=context)
+ for record in self.browse(cr, uid, ids, context=context):
+ self._create_or_replace_sql_view(cr, uid, record,
+ context=context)
+ return result
+
+ def unlink(self, cr, uid, ids, context=None):
+ for record in self.browse(cr, uid, ids, context=context):
+ self._delete_sql_view(cr, uid, record, context=context)
+ result = super(SQLView, self).unlink(cr, uid, ids, context=context)
+ return result
+
+ def onchange_sql_name(self, cr, uid, ids, sql_name, context=None):
+ complete_name = self._complete_from_sql_name(cr, uid, sql_name,
+ context=context)
+ return {'value': {'complete_sql_name': complete_name}}
diff --git a/sql_view/security/ir.model.access.csv b/sql_view/security/ir.model.access.csv
new file mode 100644
index 000000000..c7bd1ce7a
--- /dev/null
+++ b/sql_view/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_sql_view_admin,sql_view admin,model_sql_view,base.group_system,1,1,1,1
+
diff --git a/sql_view/static/description/icon.png b/sql_view/static/description/icon.png
new file mode 100644
index 000000000..3a0328b51
Binary files /dev/null and b/sql_view/static/description/icon.png differ
diff --git a/sql_view/views/sql_view_views.xml b/sql_view/views/sql_view_views.xml
new file mode 100644
index 000000000..415fd51ea
--- /dev/null
+++ b/sql_view/views/sql_view_views.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+ sql.view.form
+ sql.view
+
+
+
+
+
+
+ sql.view.tree
+ sql.view
+
+
+
+
+
+
+
+
+
+ sql.view.filter
+ sql.view
+
+
+
+
+
+
+
+
+
+ SQL Views
+ ir.actions.act_window
+ sql.view
+ form
+ tree,form
+
+
+
+
+
+
+
diff --git a/sql_view/wizards/__init__.py b/sql_view/wizards/__init__.py
new file mode 100644
index 000000000..d4e7bf227
--- /dev/null
+++ b/sql_view/wizards/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import sql_view_csv_preview
diff --git a/sql_view/wizards/sql_view_csv_preview.py b/sql_view/wizards/sql_view_csv_preview.py
new file mode 100644
index 000000000..9ae80d58f
--- /dev/null
+++ b/sql_view/wizards/sql_view_csv_preview.py
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+#
+#
+# Authors: Guewen Baconnier
+# Copyright 2015 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+#
+
+import base64
+from StringIO import StringIO
+
+import unicodecsv
+
+from openerp.osv import orm, fields
+
+
+class SQLViewCSVPreview(orm.TransientModel):
+ _name = 'sql.view.csv.preview'
+ _description = 'SQL View CSV Preview'
+
+ _columns = {
+ 'limit': fields.integer(string='Limit',
+ help='Number of records. 0 means infinite.'),
+ 'data': fields.binary('CSV', readonly=True),
+ 'filename': fields.char('File Name', readonly=True),
+ }
+
+ _defaults = {
+ 'filename': 'csv-preview.csv',
+ 'limit': 100,
+ }
+
+ def _query(self, cr, uid, form, sql_view, context=None):
+ view_name = sql_view.complete_sql_name
+ query = "SELECT * FROM {view_name} "
+ if form.limit:
+ query += "LIMIT {limit}"
+ return query.format(view_name=view_name, limit=form.limit)
+
+ def export_csv(self, cr, uid, ids, context=None):
+ if context is None:
+ return
+ sql_view_ids = context.get('active_ids', [])
+ assert len(ids) == 1, "1 wizard ID expected"
+ assert len(sql_view_ids) == 1, "1 active ID expected"
+
+ form = self.browse(cr, uid, ids[0], context=context)
+ sql_view = self.pool['sql.view'].browse(cr, uid, sql_view_ids[0],
+ context=context)
+ query = self._query(cr, uid, form, sql_view, context=context)
+ cr.execute(query)
+ headers = [desc[0] for desc in cr.description]
+ records = cr.fetchall()
+ filedata = StringIO()
+ try:
+ writer = unicodecsv.writer(filedata, encoding='utf-8')
+ writer.writerow(headers)
+ writer.writerows(records)
+ form.write({'data': base64.encodestring(filedata.getvalue())})
+ finally:
+ filedata.close()
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': self._name,
+ 'view_mode': 'form',
+ 'view_type': 'form',
+ 'res_id': ids[0],
+ 'views': [(False, 'form')],
+ 'target': 'new',
+ }
diff --git a/sql_view/wizards/sql_view_csv_preview_views.xml b/sql_view/wizards/sql_view_csv_preview_views.xml
new file mode 100644
index 000000000..14dcf6fdf
--- /dev/null
+++ b/sql_view/wizards/sql_view_csv_preview_views.xml
@@ -0,0 +1,37 @@
+
+
+
+
+ sql.view.csv.preview.form
+ sql.view.csv.preview
+
+
+
+
+
+
+ SQL View CSV Preview
+ sql.view.csv.preview
+ form
+ form
+
+ new
+
+
+
+