Merge lp:~openerp-community/server-env-tools/7.0-modules-from-openobject-extension into lp:~server-env-tools-core-editors/server-env-tools/7.0

Status: Merged
Approved by: Yannick Vaucher @ Camptocamp
Approved revision: 54
Merged at revision: 79
Proposed branch: lp:~openerp-community/server-env-tools/7.0-modules-from-openobject-extension
Merge into: lp:~server-env-tools-core-editors/server-env-tools/7.0
Diff against target: 855 lines (+778/-0)
13 files modified
base_external_dbsource/__init__.py (+24/-0)
base_external_dbsource/__openerp__.py (+66/-0)
base_external_dbsource/base_external_dbsource.py (+175/-0)
base_external_dbsource/base_external_dbsource_demo.xml (+15/-0)
base_external_dbsource/base_external_dbsource_view.xml (+54/-0)
base_external_dbsource/security/ir.model.access.csv (+2/-0)
base_external_dbsource/test/dbsource_connect.yml (+5/-0)
import_odbc/__init__.py (+24/-0)
import_odbc/__openerp__.py (+88/-0)
import_odbc/import_odbc.py (+218/-0)
import_odbc/import_odbc_demo.xml (+15/-0)
import_odbc/import_odbc_view.xml (+90/-0)
import_odbc/security/ir.model.access.csv (+2/-0)
To merge this branch: bzr merge lp:~openerp-community/server-env-tools/7.0-modules-from-openobject-extension
Reviewer Review Type Date Requested Status
Yannick Vaucher @ Camptocamp Approve
Mario Arias (community) code review and test Needs Fixing
Alexandre Fayolle - camptocamp tried to install Needs Fixing
Nicolas Bessi - Camptocamp (community) Approve
Review via email: mp+200744@code.launchpad.net

This proposal supersedes a proposal from 2013-09-02.

Description of the change

[ADD] import_odbc and base_external_dbsource from lp:openobject-extension/7.0

To post a comment you must log in.
Revision history for this message
Nicolas Bessi - Camptocamp (nbessi-c2c-deactivatedaccount) wrote : Posted in a previous version of this proposal

Hello,

Thanks for the revirew

Some comments below:

Please use explicit relative import: from . import

in __openerp__ remove the init key please it is unused

in base_external_source.py:

just a details but the order of import is not standard:
-atop should be standard lib call,
-below external used module,
-finally localy imported module

please use OpenERP/community convention to for the model:
from openerp.osv import orm, fields

class base_external_dbsource(orm.Model):

line 71: back slash is not needed
line 82 back slash an + is not needed

in conn_open
            if '%s' not in data.conn_string:
                connStr += ';PWD=%s'
I'm not sure this is a judicious choice.
What if password set in FREETDS config files

In function connexion_test you raise an exception to said that connection works.
It will be better to return an ir.act_windows.

A quick pass to a lynter to fix PEP8 will be nice.

In demo data used for yaml test

        <record model="base.external.dbsource" id="demo_postgre">
            <field name="name">PostgreSQL local</field>
            <field name="conn_string">dbname='postgres' password=%s</field>
            <field name="password">postgresql</field>
            <field name="connector">postgresql</field>
        </record>

Depends on postgres configuration. I have no better idea but a comment in test should be made to validate this first.

Thanks fro the patch. I will look if I can integrate it in connector_odbc.

Regards

Nicolas

review: Needs Fixing (code review, no tests)
Revision history for this message
Daniel Reis (dreis-pt) wrote : Posted in a previous version of this proposal

Maxime: I can work on the cleanup of this code, if you like.

Revision history for this message
Maxime Chambreuil (http://www.savoirfairelinux.com) (max3903) wrote : Posted in a previous version of this proposal

Thanks Daniel. Please go ahead.

Revision history for this message
Maxime Chambreuil (http://www.savoirfairelinux.com) (max3903) wrote : Posted in a previous version of this proposal

Daniel ?

Revision history for this message
Yannick Vaucher @ Camptocamp (yvaucher-c2c) wrote : Posted in a previous version of this proposal

Please remove executable right on those files (chmod -x):

base_external_dbsource/base_external_dbsource_demo.xml
base_external_dbsource/__init__.py
base_external_dbsource/security/ir.model.access.csv
base_external_dbsource/test/dbsource_connect.yml
import_odbc/import_odbc_demo.xml
import_odbc/__init__.py
import_odbc/security/ir.model.access.csv

base_external_dbsource.py|33 col 1| F401 'pymssql' imported but unused
base_external_dbsource.py|40 col 1| F811 redefinition of unused 'sqlalchemy' from line 32
base_external_dbsource.py|41 col 1| F401 'MySQLdb' imported but unused
base_external_dbsource.py|65 col 1| F811 redefinition of unused 'sqlalchemy' from line 40

base_external_dbsource.py|166 col 1| F821 undefined name 'osv'
base_external_dbsource.py|177 col 1| F821 undefined name 'osv'

Use orm.execpt_orm instead

And at least split the l.43 in import_odbc/import_odbc

Cheers

review: Needs Fixing (code review, no tests)
Revision history for this message
Maxime Chambreuil (http://www.savoirfairelinux.com) (max3903) wrote : Posted in a previous version of this proposal

Thanks Yannick.

I made the changes according to your review.

Revision history for this message
Yannick Vaucher @ Camptocamp (yvaucher-c2c) wrote :

Thanks for the changes

LGTM

review: Approve (code review, no tests)
Revision history for this message
Nicolas Bessi - Camptocamp (nbessi-c2c-deactivatedaccount) wrote :

LGTM

review: Approve
Revision history for this message
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) wrote :
Download full text (6.4 KiB)

I get an error when I try to install import_odbc:

2014-03-20 13:55:00,065 32374 ERROR test_ocb_oca70 openerp.tools.view_validation: Invalid XML: A `string` attribute must be on a `form` node.
2014-03-20 13:55:00,078 32374 ERROR test_ocb_oca70 openerp.tools.convert: Parse error in /home/afayolle/work/oca/server-env-tools/7.0/import_odbc/import_odbc_view.xml:7:
<record model="ir.ui.view" id="view_import_dbtable_form">
            <field name="name">import.odbc.dbtable.form</field>
            <field name="model">import.odbc.dbtable</field>
            <field name="type">form</field>
            <field name="arch" type="xml">
                <form>
                    <field name="name" search="1"/>
                    <field name="exec_order"/>
                    <field name="model_target"/>
                    <field name="dbsource_id" search="1"/>
                    <field name="noupdate"/>
                    <field name="enabled"/>
                    <field name="ignore_rel_errors"/>
                    <field name="raise_import_errors"/>
                    <field name="last_sync"/>
                    <group colspan="2">
                        <button name="import_run" string="Run Import" type="object" icon="gtk-execute"/>
                        <button name="import_schedule" string="Schedule Import" type="object" icon="gtk-paste"/>
                    </group>
                    <field name="sql_source" colspan="4"/>
                    <separator string="Last execution" colspan="4"/>
                    <field name="last_record_count"/>
                    <field name="start_run"/>
                    <field name="last_warn_count"/>
                    <field name="last_run"/>
                    <field name="last_error_count"/>
                    <field name="last_log" colspan="4"/>
                </form>
            </field>
        </record>
Traceback (most recent call last):
  File "/home/afayolle/work/oerp/openobject-server/ocb-7.0/openerp/tools/convert.py", line 847, in parse
    self._tags[rec.tag](self.cr, rec, n)
  File "/home/afayolle/work/oerp/openobject-server/ocb-7.0/openerp/tools/convert.py", line 814, in _tag_record
    id = self.pool.get('ir.model.data')._update(cr, self.uid, rec_model, self.module, res, rec_id or False, not self.isnoupdate(data_node), noupdate=self.isnoupdate(data_node), mode=self.mode, context=rec_context )
  File "/home/afayolle/work/oerp/server/ocb-7.0/openerp/addons/base/ir/ir_model.py", line 969, in _update
    res_id = model_obj.create(cr, uid, values, context=context)
  File "/home/afayolle/work/oerp/server/ocb-7.0/openerp/addons/base/ir/ir_ui_view.py", line 103, in create
    return super(view, self).create(cr, uid, values, context)
  File "/home/afayolle/work/oerp/openobject-server/ocb-7.0/openerp/osv/orm.py", line 4543, in create
    self._validate(cr, user, [id_new], context)
  File "/home/afayolle/work/oerp/openobject-server/ocb-7.0/openerp/osv/orm.py", line 1557, in _validate
    raise except_orm('ValidateError', '\n'.join(error_msgs))
except_orm: ('ValidateError', u'Error occurred while validating the field(s) arch: Invalid XML for View Architecture!')
Traceback (most recent call...

Read more...

review: Needs Fixing (tried to install)
Revision history for this message
Mario Arias (the-clone-master) wrote :

I just found two small issues:

import_odbc_view:
Line 45: the "tree" is missing the "string" attribute, that is mandatory. Just add 'string="Tree Name"' as attribute and the error will be gone.
Line 81: base.next_id_15 is not present in OpenERP Saas3. Not sure what menu was that, but this is fixed just defining what would be the right menu to place the module under...

Also it seems that base_external_dbsource branch in LP is more "up to date" than included module in this one...
I will check change history and diff, and will publish back any findings...

Regards,
Mario

review: Needs Fixing (code review and test)
Revision history for this message
Mario Arias (the-clone-master) wrote :

Being consistent with base_external_dbsource, line 81 on import_odbc_view should be set to "base.next_id_9", so that table load rules menu appears next to database source definition menu option...

54. By Maxime Chambreuil (http://www.savoirfairelinux.com)

[FIX] Menu, views and version number

Revision history for this message
Maxime Chambreuil (http://www.savoirfairelinux.com) (max3903) wrote :

I just fixed the branch according to your reviews.

Thanks.

Revision history for this message
Yannick Vaucher @ Camptocamp (yvaucher-c2c) wrote :

import_odbc install without error

and Mario's comment where resolved.

Approved

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'base_external_dbsource'
2=== added file 'base_external_dbsource/__init__.py'
3--- base_external_dbsource/__init__.py 1970-01-01 00:00:00 +0000
4+++ base_external_dbsource/__init__.py 2014-04-17 12:03:09 +0000
5@@ -0,0 +1,24 @@
6+# -*- coding: utf-8 -*-
7+##############################################################################
8+#
9+# Daniel Reis
10+# 2011
11+#
12+# This program is free software: you can redistribute it and/or modify
13+# it under the terms of the GNU Affero General Public License as
14+# published by the Free Software Foundation, either version 3 of the
15+# License, or (at your option) any later version.
16+#
17+# This program is distributed in the hope that it will be useful,
18+# but WITHOUT ANY WARRANTY; without even the implied warranty of
19+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+# GNU Affero General Public License for more details.
21+#
22+# You should have received a copy of the GNU Affero General Public License
23+# along with this program. If not, see <http://www.gnu.org/licenses/>.
24+#
25+##############################################################################
26+
27+from . import base_external_dbsource
28+
29+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
30
31=== added file 'base_external_dbsource/__openerp__.py'
32--- base_external_dbsource/__openerp__.py 1970-01-01 00:00:00 +0000
33+++ base_external_dbsource/__openerp__.py 2014-04-17 12:03:09 +0000
34@@ -0,0 +1,66 @@
35+# -*- coding: utf-8 -*-
36+##############################################################################
37+#
38+# Daniel Reis, 2011
39+# Additional contributions by Maxime Chambreuil, Savoir-faire Linux
40+#
41+# This program is free software: you can redistribute it and/or modify
42+# it under the terms of the GNU Affero General Public License as
43+# published by the Free Software Foundation, either version 3 of the
44+# License, or (at your option) any later version.
45+#
46+# This program is distributed in the hope that it will be useful,
47+# but WITHOUT ANY WARRANTY; without even the implied warranty of
48+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
49+# GNU Affero General Public License for more details.
50+#
51+# You should have received a copy of the GNU Affero General Public License
52+# along with this program. If not, see <http://www.gnu.org/licenses/>.
53+#
54+##############################################################################
55+
56+{
57+ 'name': 'External Database Sources',
58+ 'version': '1.3',
59+ 'category': 'Tools',
60+ 'description': """
61+This module allows you to define connections to foreign databases using ODBC,
62+Oracle Client or SQLAlchemy.
63+
64+Database sources can be configured in Settings > Configuration -> Data sources.
65+
66+Depending on the database, you need:
67+ * to install unixodbc and python-pyodbc packages to use ODBC connections.
68+ * to install FreeTDS driver (tdsodbc package) and configure it through ODBC to
69+ connect to Microsoft SQL Server.
70+ * to install and configure Oracle Instant Client and cx_Oracle python library
71+ to connect to Oracle.
72+
73+Contributors
74+============
75+
76+* Maxime Chambreuil <maxime.chambreuil@savoirfairelinux.com>
77+ """,
78+ 'author': 'Daniel Reis',
79+ 'website': 'http://launchpad.net/addons-tko',
80+ 'images': [
81+ 'images/screenshot01.png',
82+ ],
83+ 'depends': [
84+ 'base',
85+ ],
86+ 'data': [
87+ 'base_external_dbsource_view.xml',
88+ 'security/ir.model.access.csv',
89+ ],
90+ 'demo': [
91+ 'base_external_dbsource_demo.xml',
92+ ],
93+ 'test': [
94+ 'dbsource_connect.yml',
95+ ],
96+ 'installable': True,
97+ 'active': False,
98+}
99+
100+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
101
102=== added file 'base_external_dbsource/base_external_dbsource.py'
103--- base_external_dbsource/base_external_dbsource.py 1970-01-01 00:00:00 +0000
104+++ base_external_dbsource/base_external_dbsource.py 2014-04-17 12:03:09 +0000
105@@ -0,0 +1,175 @@
106+# -*- coding: utf-8 -*-
107+##############################################################################
108+#
109+# Daniel Reis
110+# 2011
111+#
112+# This program is free software: you can redistribute it and/or modify
113+# it under the terms of the GNU Affero General Public License as
114+# published by the Free Software Foundation, either version 3 of the
115+# License, or (at your option) any later version.
116+#
117+# This program is distributed in the hope that it will be useful,
118+# but WITHOUT ANY WARRANTY; without even the implied warranty of
119+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
120+# GNU Affero General Public License for more details.
121+#
122+# You should have received a copy of the GNU Affero General Public License
123+# along with this program. If not, see <http://www.gnu.org/licenses/>.
124+#
125+##############################################################################
126+
127+import os
128+import logging
129+from openerp.osv import orm, fields
130+from openerp.tools.translate import _
131+import openerp.tools as tools
132+_logger = logging.getLogger(__name__)
133+
134+CONNECTORS = []
135+
136+try:
137+ import sqlalchemy
138+ CONNECTORS.append(('sqlite', 'SQLite'))
139+ try:
140+ import pymssql
141+ CONNECTORS.append(('mssql', 'Microsoft SQL Server'))
142+ except:
143+ _logger.info('MS SQL Server not available. Please install "pymssql"\
144+ python package.')
145+ try:
146+ import MySQLdb
147+ CONNECTORS.append(('mysql', 'MySQL'))
148+ except:
149+ _logger.info('MySQL not available. Please install "mysqldb"\
150+ python package.')
151+except:
152+ _logger.info('SQL Alchemy not available. Please install "slqalchemy"\
153+ python package.')
154+try:
155+ import pyodbc
156+ CONNECTORS.append(('pyodbc', 'ODBC'))
157+except:
158+ _logger.info('ODBC libraries not available. Please install "unixodbc"\
159+ and "python-pyodbc" packages.')
160+
161+try:
162+ import cx_Oracle
163+ CONNECTORS.append(('cx_Oracle', 'Oracle'))
164+except:
165+ _logger.info('Oracle libraries not available. Please install "cx_Oracle"\
166+ python package.')
167+
168+import psycopg2
169+CONNECTORS.append(('postgresql', 'PostgreSQL'))
170+
171+
172+class base_external_dbsource(orm.Model):
173+ _name = "base.external.dbsource"
174+ _description = 'External Database Sources'
175+ _columns = {
176+ 'name': fields.char('Datasource name', required=True, size=64),
177+ 'conn_string': fields.text('Connection string', help="""
178+Sample connection strings:
179+- Microsoft SQL Server:
180+ mssql+pymssql://username:%s@server:port/dbname?charset=utf8
181+- MySQL: mysql://user:%s@server:port/dbname
182+- ODBC: DRIVER={FreeTDS};SERVER=server.address;Database=mydb;UID=sa
183+- ORACLE: username/%s@//server.address:port/instance
184+- PostgreSQL:
185+ dbname='template1' user='dbuser' host='localhost' port='5432' password=%s
186+- SQLite: sqlite:///test.db
187+"""),
188+ 'password': fields.char('Password', size=40),
189+ 'connector': fields.selection(CONNECTORS, 'Connector',
190+ required=True,
191+ help="If a connector is missing from the\
192+ list, check the server log to confirm\
193+ that the required components were\
194+ detected."),
195+ }
196+
197+ def conn_open(self, cr, uid, id1):
198+ #Get dbsource record
199+ data = self.browse(cr, uid, id1)
200+ #Build the full connection string
201+ connStr = data.conn_string
202+ if data.password:
203+ if '%s' not in data.conn_string:
204+ connStr += ';PWD=%s'
205+ connStr = connStr % data.password
206+ #Try to connect
207+ if data.connector == 'cx_Oracle':
208+ os.environ['NLS_LANG'] = 'AMERICAN_AMERICA.UTF8'
209+ conn = cx_Oracle.connect(connStr)
210+ elif data.connector == 'pyodbc':
211+ conn = pyodbc.connect(connStr)
212+ elif data.connector in ('sqlite', 'mysql', 'mssql'):
213+ conn = sqlalchemy.create_engine(connStr).connect()
214+ elif data.connector == 'postgresql':
215+ conn = psycopg2.connect(connStr)
216+
217+ return conn
218+
219+ def execute(self, cr, uid, ids, sqlquery, sqlparams=None, metadata=False,
220+ context=None):
221+ """Executes SQL and returns a list of rows.
222+
223+ "sqlparams" can be a dict of values, that can be referenced in
224+ the SQL statement using "%(key)s" or, in the case of Oracle,
225+ ":key".
226+ Example:
227+ sqlquery = "select * from mytable where city = %(city)s and
228+ date > %(dt)s"
229+ params = {'city': 'Lisbon',
230+ 'dt': datetime.datetime(2000, 12, 31)}
231+
232+ If metadata=True, it will instead return a dict containing the
233+ rows list and the columns list, in the format:
234+ { 'cols': [ 'col_a', 'col_b', ...]
235+ , 'rows': [ (a0, b0, ...), (a1, b1, ...), ...] }
236+ """
237+ data = self.browse(cr, uid, ids)
238+ rows, cols = list(), list()
239+ for obj in data:
240+ conn = self.conn_open(cr, uid, obj.id)
241+ if obj.connector in ["sqlite", "mysql", "mssql"]:
242+ #using sqlalchemy
243+ cur = conn.execute(sqlquery, sqlparams)
244+ if metadata:
245+ cols = cur.keys()
246+ rows = [r for r in cur]
247+ else:
248+ #using other db connectors
249+ cur = conn.cursor()
250+ cur.execute(sqlquery, sqlparams)
251+ if metadata:
252+ cols = [d[0] for d in cur.description]
253+ rows = cur.fetchall()
254+ conn.close()
255+ if metadata:
256+ return{'cols': cols, 'rows': rows}
257+ else:
258+ return rows
259+
260+ def connection_test(self, cr, uid, ids, context=None):
261+ for obj in self.browse(cr, uid, ids, context):
262+ conn = False
263+ try:
264+ conn = self.conn_open(cr, uid, obj.id)
265+ except Exception, e:
266+ raise orm.except_orm(_("Connection test failed!"),
267+ _("Here is what we got instead:\n %s")
268+ % tools.ustr(e))
269+ finally:
270+ try:
271+ if conn:
272+ conn.close()
273+ except Exception:
274+ # ignored, just a consequence of the previous exception
275+ pass
276+ #TODO: if OK a (wizard) message box should be displayed
277+ raise orm.except_orm(_("Connection test succeeded!"),
278+ _("Everything seems properly set up!"))
279+
280+#EOF
281
282=== added file 'base_external_dbsource/base_external_dbsource_demo.xml'
283--- base_external_dbsource/base_external_dbsource_demo.xml 1970-01-01 00:00:00 +0000
284+++ base_external_dbsource/base_external_dbsource_demo.xml 2014-04-17 12:03:09 +0000
285@@ -0,0 +1,15 @@
286+<?xml version="1.0"?>
287+<openerp>
288+ <data>
289+
290+ <record model="base.external.dbsource" id="demo_postgre">
291+ <field name="name">PostgreSQL local</field>
292+ <field name="conn_string">dbname='postgres' password=%s</field>
293+ <field name="password">postgresql</field>
294+ <field name="connector">postgresql</field>
295+ </record>
296+
297+ </data>
298+</openerp>
299+
300+
301
302=== added file 'base_external_dbsource/base_external_dbsource_view.xml'
303--- base_external_dbsource/base_external_dbsource_view.xml 1970-01-01 00:00:00 +0000
304+++ base_external_dbsource/base_external_dbsource_view.xml 2014-04-17 12:03:09 +0000
305@@ -0,0 +1,54 @@
306+<?xml version="1.0"?>
307+<openerp>
308+ <data>
309+
310+ <!-- DBSource -->
311+
312+ <record model="ir.ui.view" id="view_dbsource_tree">
313+ <field name="name">base.external.dbsource.tree</field>
314+ <field name="model">base.external.dbsource</field>
315+ <field name="type">tree</field>
316+ <field name="arch" type="xml">
317+ <tree string="External DB Sources">
318+ <field name="name"/>
319+ <field name="connector"/>
320+ <field name="conn_string"/>
321+ </tree>
322+ </field>
323+ </record>
324+
325+ <record model="ir.ui.view" id="view_dbsource_form">
326+ <field name="name">base.external.dbsource.form</field>
327+ <field name="model">base.external.dbsource</field>
328+ <field name="type">form</field>
329+ <field name="arch" type="xml">
330+ <form string="External DB Source">
331+ <field name="name"/>
332+ <field name="password" password="True"/>
333+ <newline/>
334+ <field name="connector" colspan="2"/>
335+ <newline/>
336+ <field name="conn_string" colspan="4"/>
337+ <newline/>
338+ <button name="connection_test" string="Test Connection" type="object" icon="gtk-network" colspan="4"/>
339+ </form>
340+ </field>
341+ </record>
342+
343+ <record model="ir.actions.act_window" id="action_dbsource">
344+ <field name="name">External Database Sources</field>
345+ <field name="res_model">base.external.dbsource</field>
346+ <field name="view_type">form</field>
347+ <field name="view_mode">tree,form</field>
348+ <field name="view_id" ref="view_dbsource_tree"/>
349+ </record>
350+
351+ <menuitem name="Database Sources"
352+ id="menu_dbsource"
353+ parent="base.next_id_9"
354+ action="action_dbsource"/>
355+
356+ </data>
357+</openerp>
358+
359+
360
361=== added directory 'base_external_dbsource/images'
362=== added file 'base_external_dbsource/images/screenshot01.png'
363Binary files base_external_dbsource/images/screenshot01.png 1970-01-01 00:00:00 +0000 and base_external_dbsource/images/screenshot01.png 2014-04-17 12:03:09 +0000 differ
364=== added directory 'base_external_dbsource/security'
365=== added file 'base_external_dbsource/security/ir.model.access.csv'
366--- base_external_dbsource/security/ir.model.access.csv 1970-01-01 00:00:00 +0000
367+++ base_external_dbsource/security/ir.model.access.csv 2014-04-17 12:03:09 +0000
368@@ -0,0 +1,2 @@
369+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
370+access_base_external_dbsource_group_system,bae_external_dbsource_group_system,model_base_external_dbsource,base.group_system,1,1,1,1
371
372=== added directory 'base_external_dbsource/test'
373=== added file 'base_external_dbsource/test/dbsource_connect.yml'
374--- base_external_dbsource/test/dbsource_connect.yml 1970-01-01 00:00:00 +0000
375+++ base_external_dbsource/test/dbsource_connect.yml 2014-04-17 12:03:09 +0000
376@@ -0,0 +1,5 @@
377+-
378+ Connect to local Postgres.
379+-
380+ !python {model: base.external.dbsource}: |
381+ self.connection_test(cr, uid, [ref("demo_postgresql")]
382
383=== added directory 'import_odbc'
384=== added file 'import_odbc/__init__.py'
385--- import_odbc/__init__.py 1970-01-01 00:00:00 +0000
386+++ import_odbc/__init__.py 2014-04-17 12:03:09 +0000
387@@ -0,0 +1,24 @@
388+# -*- coding: utf-8 -*-
389+##############################################################################
390+#
391+# Daniel Reis
392+# 2011
393+#
394+# This program is free software: you can redistribute it and/or modify
395+# it under the terms of the GNU Affero General Public License as
396+# published by the Free Software Foundation, either version 3 of the
397+# License, or (at your option) any later version.
398+#
399+# This program is distributed in the hope that it will be useful,
400+# but WITHOUT ANY WARRANTY; without even the implied warranty of
401+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
402+# GNU Affero General Public License for more details.
403+#
404+# You should have received a copy of the GNU Affero General Public License
405+# along with this program. If not, see <http://www.gnu.org/licenses/>.
406+#
407+##############################################################################
408+
409+from . import import_odbc
410+
411+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
412
413=== added file 'import_odbc/__openerp__.py'
414--- import_odbc/__openerp__.py 1970-01-01 00:00:00 +0000
415+++ import_odbc/__openerp__.py 2014-04-17 12:03:09 +0000
416@@ -0,0 +1,88 @@
417+# -*- coding: utf-8 -*-
418+##############################################################################
419+#
420+# Daniel Reis
421+# 2011
422+#
423+# This program is free software: you can redistribute it and/or modify
424+# it under the terms of the GNU Affero General Public License as
425+# published by the Free Software Foundation, either version 3 of the
426+# License, or (at your option) any later version.
427+#
428+# This program is distributed in the hope that it will be useful,
429+# but WITHOUT ANY WARRANTY; without even the implied warranty of
430+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
431+# GNU Affero General Public License for more details.
432+#
433+# You should have received a copy of the GNU Affero General Public License
434+# along with this program. If not, see <http://www.gnu.org/licenses/>.
435+#
436+##############################################################################
437+
438+{
439+ 'name': 'Import data from SQL and ODBC data sources.',
440+ 'version': '1.3',
441+ 'category': 'Tools',
442+ 'description': """
443+Import data directly from other databases.
444+
445+Installed in the Administration module, menu Configuration -> Import from SQL.
446+
447+Features:
448+ * Fetched data from the databases are used to build lines equivalent to regular import files. These are imported using the standard "import_data()" ORM method, benefiting from all its features, including xml_ids.
449+ * Each table import is defined by an SQL statement, used to build the equivalent for an import file. Each column's name should match the column names you would use in an import file. The first column must provide an unique identifier for the record, and will be used to build its xml_id.
450+ * SQL columns named "none" are ignored. This can be used for the first column of the SQL, so that it's used to build the XML Id but it's not imported to any OpenERP field.
451+ * The last sync date is the last successfull execution can be used in the SQL using "%(sync)s", or ":sync" in the case of Oracle.
452+ * When errors are found, only the record with the error fails import. The other correct records are commited. However, the "last sync date" will only be automaticaly updated when no errors are found.
453+ * The import execution can be scheduled to run automatically.
454+
455+Examples:
456+ * Importing suppliers to res.partner:
457+ SELECT distinct
458+ [SUPPLIER_CODE] as "ref"
459+ , [SUPPLIER_NAME] as "name"
460+ , 1 as "is_supplier"
461+ , [INFO] as "comment"
462+ FROM T_SUPPLIERS
463+ WHERE INACTIVE_DATE IS NULL and DATE_CHANGED >= %(sync)s
464+
465+ * Importing products to product.product:
466+ SELECT PRODUCT_CODE as "ref"
467+ , PRODUCT_NAME as "name"
468+ , 'res_partner_id_'+SUPPLIER_ID as "partner_id/id"
469+ FROM T_PRODUCTS
470+ WHERE DATE_CHANGED >= %(sync)s
471+
472+Improvements ideas waiting for a contributor:
473+ * Allow to import many2one fields (currently not supported). Done by adding a second SQL sentence to get child record list?
474+ * Allow "import sets" that can be executed at different time intervals using different scheduler jobs.
475+ * Allow to inactivate/delete OpenERP records when not present in an SQL result set.
476+
477+Contributors
478+============
479+
480+* Maxime Chambreuil <maxime.chambreuil@savoirfairelinux.com>
481+ """,
482+ 'author': 'Daniel Reis',
483+ 'website': 'http://launchpad.net/addons-tko',
484+ 'images': [
485+ 'images/snapshot1.png',
486+ 'images/snapshot2.png',
487+ ],
488+ 'depends': [
489+ 'base',
490+ 'base_external_dbsource',
491+ ],
492+ 'data': [
493+ 'import_odbc_view.xml',
494+ 'security/ir.model.access.csv',
495+ ],
496+ 'demo': [
497+ 'import_odbc_demo.xml',
498+ ],
499+ 'test': [],
500+ 'installable': True,
501+ 'active': False,
502+}
503+
504+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
505
506=== added directory 'import_odbc/images'
507=== added file 'import_odbc/images/snapshot1.png'
508Binary files import_odbc/images/snapshot1.png 1970-01-01 00:00:00 +0000 and import_odbc/images/snapshot1.png 2014-04-17 12:03:09 +0000 differ
509=== added file 'import_odbc/images/snapshot2.png'
510Binary files import_odbc/images/snapshot2.png 1970-01-01 00:00:00 +0000 and import_odbc/images/snapshot2.png 2014-04-17 12:03:09 +0000 differ
511=== added file 'import_odbc/import_odbc.py'
512--- import_odbc/import_odbc.py 1970-01-01 00:00:00 +0000
513+++ import_odbc/import_odbc.py 2014-04-17 12:03:09 +0000
514@@ -0,0 +1,218 @@
515+# -*- coding: utf-8 -*-
516+##############################################################################
517+#
518+# Daniel Reis
519+# 2011
520+#
521+# This program is free software: you can redistribute it and/or modify
522+# it under the terms of the GNU Affero General Public License as
523+# published by the Free Software Foundation, either version 3 of the
524+# License, or (at your option) any later version.
525+#
526+# This program is distributed in the hope that it will be useful,
527+# but WITHOUT ANY WARRANTY; without even the implied warranty of
528+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
529+# GNU Affero General Public License for more details.
530+#
531+# You should have received a copy of the GNU Affero General Public License
532+# along with this program. If not, see <http://www.gnu.org/licenses/>.
533+#
534+##############################################################################
535+
536+import sys
537+from datetime import datetime
538+from openerp.osv import orm, fields
539+import logging
540+_logger = logging.getLogger(__name__)
541+_loglvl = _logger.getEffectiveLevel()
542+SEP = '|'
543+
544+
545+class import_odbc_dbtable(orm.Model):
546+ _name = "import.odbc.dbtable"
547+ _description = 'Import Table Data'
548+ _order = 'exec_order'
549+ _columns = {
550+ 'name': fields.char('Datasource name', required=True, size=64),
551+ 'enabled': fields.boolean('Execution enabled'),
552+ 'dbsource_id': fields.many2one('base.external.dbsource', 'Database source', required=True),
553+ 'sql_source': fields.text('SQL', required=True, help='Column names must be valid "import_data" columns.'),
554+ 'model_target': fields.many2one('ir.model', 'Target object'),
555+ 'noupdate': fields.boolean('No updates', help="Only create new records; disable updates to existing records."),
556+ 'exec_order': fields.integer('Execution order', help="Defines the order to perform the import"),
557+ 'last_sync': fields.datetime('Last sync date',
558+ help="Datetime for the last succesfull sync."
559+ "\nLater changes on the source may not be replicated on the destination"),
560+ 'start_run': fields.datetime('Time started', readonly=True),
561+ 'last_run': fields.datetime('Time ended', readonly=True),
562+ 'last_record_count': fields.integer('Last record count', readonly=True),
563+ 'last_error_count': fields.integer('Last error count', readonly=True),
564+ 'last_warn_count': fields.integer('Last warning count', readonly=True),
565+ 'last_log': fields.text('Last run log', readonly=True),
566+ 'ignore_rel_errors': fields.boolean('Ignore relationship errors',
567+ help="On error try to reimport rows ignoring relationships."),
568+ 'raise_import_errors': fields.boolean('Raise import errors',
569+ help="Import errors not handled, intended for debugging purposes."
570+ "\nAlso forces debug messages to be written to the server log."),
571+ }
572+ _defaults = {
573+ 'enabled': True,
574+ 'exec_order': 10,
575+ }
576+
577+ def _import_data(self, cr, uid, flds, data, model_obj, table_obj, log):
578+ """Import data and returns error msg or empty string"""
579+
580+ def find_m2o(field_list):
581+ """"Find index of first column with a one2many field"""
582+ for i, x in enumerate(field_list):
583+ if len(x) > 3 and x[-3:] == ':id' or x[-3:] == '/id':
584+ return i
585+ return -1
586+
587+ def append_to_log(log, level, obj_id='', msg='', rel_id=''):
588+ if '_id_' in obj_id:
589+ obj_id = '.'.join(obj_id.split('_')[:-2]) + ': ' + obj_id.split('_')[-1]
590+ if ': .' in msg and not rel_id:
591+ rel_id = msg[msg.find(': .')+3:]
592+ if '_id_' in rel_id:
593+ rel_id = '.'.join(rel_id.split('_')[:-2]) + ': ' + rel_id.split('_')[-1]
594+ msg = msg[:msg.find(': .')]
595+ log['last_log'].append('%s|%s\t|%s\t|%s' % (level.ljust(5), obj_id, rel_id, msg))
596+ _logger.debug(data)
597+ cols = list(flds) # copy to avoid side effects
598+ errmsg = str()
599+ if table_obj.raise_import_errors:
600+ model_obj.import_data(cr, uid, cols, [data], noupdate=table_obj.noupdate)
601+ else:
602+ try:
603+ model_obj.import_data(cr, uid, cols, [data], noupdate=table_obj.noupdate)
604+ except:
605+ errmsg = str(sys.exc_info()[1])
606+ if errmsg and not table_obj.ignore_rel_errors:
607+ #Fail
608+ append_to_log(log, 'ERROR', data, errmsg)
609+ log['last_error_count'] += 1
610+ return False
611+ if errmsg and table_obj.ignore_rel_errors:
612+ #Warn and retry ignoring many2one fields...
613+ append_to_log(log, 'WARN', data, errmsg)
614+ log['last_warn_count'] += 1
615+ #Try ignoring each many2one (tip: in the SQL sentence select more problematic FKs first)
616+ i = find_m2o(cols)
617+ if i >= 0:
618+ #Try again without the [i] column
619+ del cols[i]
620+ del data[i]
621+ self._import_data(cr, uid, cols, data, model_obj, table_obj, log)
622+ else:
623+ #Fail
624+ append_to_log(log, 'ERROR', data, 'Removed all m2o keys and still fails.')
625+ log['last_error_count'] += 1
626+ return False
627+ return True
628+
629+ def import_run(self, cr, uid, ids=None, context=None):
630+ db_model = self.pool.get('base.external.dbsource')
631+ actions = self.read(cr, uid, ids, ['id', 'exec_order'])
632+ actions.sort(key=lambda x: (x['exec_order'], x['id']))
633+
634+ #Consider each dbtable:
635+ for action_ref in actions:
636+ obj = self.browse(cr, uid, action_ref['id'])
637+ if not obj.enabled:
638+ continue # skip
639+
640+ _logger.setLevel(obj.raise_import_errors and logging.DEBUG or _loglvl)
641+ _logger.debug('Importing %s...' % obj.name)
642+
643+ #now() microseconds are stripped to avoid problem with SQL smalldate
644+ #TODO: convert UTC Now to local timezone
645+ #http://stackoverflow.com/questions/4770297/python-convert-utc-datetime-string-to-local-datetime
646+ model_name = obj.model_target.model
647+ model_obj = self.pool.get(model_name)
648+ xml_prefix = model_name.replace('.', '_') + "_id_"
649+ log = {'start_run': datetime.now().replace(microsecond=0),
650+ 'last_run': None,
651+ 'last_record_count': 0,
652+ 'last_error_count': 0,
653+ 'last_warn_count': 0,
654+ 'last_log': list()}
655+ self.write(cr, uid, [obj.id], log)
656+
657+ #Prepare SQL sentence; replace "%s" with the last_sync date
658+ if obj.last_sync:
659+ sync = datetime.strptime(obj.last_sync, "%Y-%m-%d %H:%M:%S")
660+ else:
661+ sync = datetime.datetime(1900, 1, 1, 0, 0, 0)
662+ params = {'sync': sync}
663+ res = db_model.execute(cr, uid, [obj.dbsource_id.id],
664+ obj.sql_source, params, metadata=True)
665+
666+ #Exclude columns titled "None"; add (xml_)"id" column
667+ cidx = [i for i, x in enumerate(res['cols']) if x.upper() != 'NONE']
668+ cols = [x for i, x in enumerate(res['cols']) if x.upper() != 'NONE'] + ['id']
669+
670+ #Import each row:
671+ for row in res['rows']:
672+ #Build data row; import only columns present in the "cols" list
673+ data = list()
674+ for i in cidx:
675+ #TODO: Handle imported datetimes properly - convert from localtime to UTC!
676+ v = row[i]
677+ if isinstance(v, str):
678+ v = v.strip()
679+ data.append(v)
680+ data.append(xml_prefix + str(row[0]).strip())
681+
682+ #Import the row; on error, write line to the log
683+ log['last_record_count'] += 1
684+ self._import_data(cr, uid, cols, data, model_obj, obj, log)
685+ if log['last_record_count'] % 500 == 0:
686+ _logger.info('...%s rows processed...' % (log['last_record_count']))
687+
688+ #Finished importing all rows
689+ #If no errors, write new sync date
690+ if not (log['last_error_count'] or log['last_warn_count']):
691+ log['last_sync'] = log['start_run']
692+ level = logging.DEBUG
693+ if log['last_warn_count']:
694+ level = logging.WARN
695+ if log['last_error_count']:
696+ level = logging.ERROR
697+ _logger.log(level, 'Imported %s , %d rows, %d errors, %d warnings.' % (
698+ model_name, log['last_record_count'], log['last_error_count'],
699+ log['last_warn_count']))
700+ #Write run log, either if the table import is active or inactive
701+ if log['last_log']:
702+ log['last_log'].insert(0, 'LEVEL|== Line == |== Relationship ==|== Message ==')
703+ log.update({'last_log': '\n'.join(log['last_log'])})
704+ log.update({'last_run': datetime.now().replace(microsecond=0)})
705+ self.write(cr, uid, [obj.id], log)
706+
707+ #Finished
708+ _logger.debug('Import job FINISHED.')
709+ return True
710+
711+ def import_schedule(self, cr, uid, ids, context=None):
712+ cron_obj = self.pool.get('ir.cron')
713+ new_create_id = cron_obj.create(cr, uid, {
714+ 'name': 'Import ODBC tables',
715+ 'interval_type': 'hours',
716+ 'interval_number': 1,
717+ 'numbercall': -1,
718+ 'model': 'import.odbc.dbtable',
719+ 'function': 'import_run',
720+ 'doall': False,
721+ 'active': True
722+ })
723+ return {
724+ 'name': 'Import ODBC tables',
725+ 'view_type': 'form',
726+ 'view_mode': 'form,tree',
727+ 'res_model': 'ir.cron',
728+ 'res_id': new_create_id,
729+ 'type': 'ir.actions.act_window',
730+ }
731+
732+#EOF
733
734=== added file 'import_odbc/import_odbc_demo.xml'
735--- import_odbc/import_odbc_demo.xml 1970-01-01 00:00:00 +0000
736+++ import_odbc/import_odbc_demo.xml 2014-04-17 12:03:09 +0000
737@@ -0,0 +1,15 @@
738+<?xml version="1.0"?>
739+<openerp>
740+ <data>
741+
742+ <record model="import.odbc.dbtable" id="demo_postgresql_users">
743+ <field name="name">Users from PostgreSQL </field>
744+ <field name="dbsource_id" ref="base_external_dbsource.demo_postgre"/>
745+ <field name="sql_source">select usename as "login", usename as "name" from pg_catalog.pg_user</field>
746+ <field name="model_target" ref="base.model_res_users"/>
747+ </record>
748+
749+ </data>
750+</openerp>
751+
752+
753
754=== added file 'import_odbc/import_odbc_view.xml'
755--- import_odbc/import_odbc_view.xml 1970-01-01 00:00:00 +0000
756+++ import_odbc/import_odbc_view.xml 2014-04-17 12:03:09 +0000
757@@ -0,0 +1,90 @@
758+<?xml version="1.0"?>
759+<openerp>
760+ <data>
761+
762+ <!-- Table form -->
763+
764+ <record model="ir.ui.view" id="view_import_dbtable_form">
765+ <field name="name">import.odbc.dbtable.form</field>
766+ <field name="model">import.odbc.dbtable</field>
767+ <field name="type">form</field>
768+ <field name="arch" type="xml">
769+ <form string="Table">
770+ <field name="name" search="1"/>
771+ <field name="exec_order"/>
772+ <field name="model_target"/>
773+ <field name="dbsource_id" search="1"/>
774+ <field name="noupdate"/>
775+ <field name="enabled"/>
776+ <field name="ignore_rel_errors"/>
777+ <field name="raise_import_errors"/>
778+ <field name="last_sync"/>
779+ <group colspan="2">
780+ <button name="import_run" string="Run Import" type="object" icon="gtk-execute"/>
781+ <button name="import_schedule" string="Schedule Import" type="object" icon="gtk-paste"/>
782+ </group>
783+ <field name="sql_source" colspan="4"/>
784+ <separator string="Last execution" colspan="4"/>
785+ <field name="last_record_count"/>
786+ <field name="start_run"/>
787+ <field name="last_warn_count"/>
788+ <field name="last_run"/>
789+ <field name="last_error_count"/>
790+ <field name="last_log" colspan="4"/>
791+ </form>
792+ </field>
793+ </record>
794+
795+ <!-- Table Tree -->
796+
797+ <record id="view_import_dbtable_tree" model="ir.ui.view">
798+ <field name="name">import.odbc.dbtable.tree</field>
799+ <field name="model">import.odbc.dbtable</field>
800+ <field name="type">tree</field>
801+ <field name="arch" type="xml">
802+ <tree string="Tables" colors="grey: enabled==False; red:last_error_count&gt;0; blue:last_warn_count&gt;0">
803+ <field name="exec_order"/>
804+ <field name="name"/>
805+ <field name="model_target"/>
806+ <field name="dbsource_id"/>
807+ <field name="enabled"/>
808+ <field name="last_run"/>
809+ <field name="last_sync"/>
810+ <field name="last_record_count"/>
811+ <field name="last_error_count"/>
812+ <field name="last_warn_count"/>
813+ </tree>
814+ </field>
815+</record>
816+
817+ <!-- Tree Search -->
818+ <record id="view_import_dbtable_filter" model="ir.ui.view">
819+ <field name="name">import.odbc.dbtable.filter</field>
820+ <field name="model">import.odbc.dbtable</field>
821+ <field name="type">search</field>
822+ <field name="arch" type="xml">
823+ <search string="Search ODBC Imports">
824+ <field name="name"/>
825+ <field name="dbsource_id"/>
826+ <field name="model_target"/>
827+ </search>
828+ </field>
829+ </record>
830+
831+ <!--Menu-->
832+ <record model="ir.actions.act_window" id="action_import_dbtable">
833+ <field name="name">Import from SQL</field>
834+ <field name="res_model">import.odbc.dbtable</field>
835+ <field name="view_type">form</field>
836+ </record>
837+
838+ <menuitem name="Import from SQL"
839+ id="menu_import_dbtable"
840+ parent="base.next_id_9"
841+ action="action_import_dbtable"/>
842+
843+ </data>
844+</openerp>
845+
846+
847+
848
849=== added directory 'import_odbc/security'
850=== added file 'import_odbc/security/ir.model.access.csv'
851--- import_odbc/security/ir.model.access.csv 1970-01-01 00:00:00 +0000
852+++ import_odbc/security/ir.model.access.csv 2014-04-17 12:03:09 +0000
853@@ -0,0 +1,2 @@
854+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
855+access_import_odbc_dbsource_group_system,import_odbc_dbtable_group_system,model_import_odbc_dbtable,base.group_system,1,1,1,1