1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
|
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2012 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import inspect
from openerp.osv import orm
def _get_openerp_module_name(module_path):
""" Extract the name of the OpenERP module from the path of the
Python module.
Taken from OpenERP server: ``openerp.osv.orm``
The (OpenERP) module name can be in the ``openerp.addons`` namespace
or not. For instance module ``sale`` can be imported as
``openerp.addons.sale`` (the good way) or ``sale`` (for backward
compatibility).
"""
module_parts = module_path.split('.')
if len(module_parts) > 2 and module_parts[:2] == ['openerp', 'addons']:
module_name = module_parts[2]
else:
module_name = module_parts[0]
return module_name
def install_in_connector():
""" Installs an OpenERP module in the ``Connector`` framework.
It has to be called once per OpenERP module to plug.
Under the cover, it creates a ``orm.AbstractModel`` whose name is
the name of the module with a ``.intalled`` suffix:
``{name_of_the_openerp_module_to_install}.installed``.
The connector then uses this model to know when the OpenERP module
is installed or not and whether it should use the ConnectorUnit
classes of this module or not and whether it should fire the
consumers of events or not.
"""
# Get the module of the caller
module = inspect.getmodule(inspect.currentframe().f_back)
openerp_module_name = _get_openerp_module_name(module.__name__)
# Build a new AbstractModel with the name of the module and the suffix
name = "%s.installed" % openerp_module_name
class_name = name.replace('.', '_')
# we need to call __new__ and __init__ in 2 phases because
# __init__ needs to have the right __module__ and _module attributes
model = orm.MetaModel.__new__(orm.MetaModel, class_name,
(orm.AbstractModel,), {'_name': name})
# Update the module of the model, it should be the caller's one
model._module = openerp_module_name
model.__module__ = module.__name__
orm.MetaModel.__init__(model, class_name,
(orm.AbstractModel,), {'_name': name})
# install the connector itself
install_in_connector()
def get_openerp_module(cls_or_func):
""" For a top level function or class, returns the
name of the OpenERP module where it lives.
So we will be able to filter them according to the modules
installation state.
"""
return _get_openerp_module_name(cls_or_func.__module__)
class MetaConnectorUnit(type):
""" Metaclass for ConnectorUnit.
Keeps a ``_module`` attribute on the classes, the same way OpenERP does
it for the Model classes. It is then used to filter them according to
the state of the module (installed or not).
"""
@property
def model_name(cls):
"""
The ``model_name`` is used to find the class and is mandatory for
:py:class:`~connector.connector.ConnectorUnit` which are registered
on a :py:class:`~connector.backend.Backend`.
"""
if cls._model_name is None:
raise NotImplementedError("no _model_name for %s" % cls)
model_name = cls._model_name
if not hasattr(model_name, '__iter__'):
model_name = [model_name]
return model_name
def __init__(cls, name, bases, attrs):
super(MetaConnectorUnit, cls).__init__(name, bases, attrs)
cls._openerp_module_ = get_openerp_module(cls)
class ConnectorUnit(object):
"""Abstract class for each piece of the connector:
Examples:
* :py:class:`connector.connector.Binder`
* :py:class:`connector.unit.mapper.Mapper`
* :py:class:`connector.unit.synchronizer.Synchronizer`
* :py:class:`connector.unit.backend_adapter.BackendAdapter`
Or basically any class intended to be registered in a
:py:class:`~connector.backend.Backend`.
"""
__metaclass__ = MetaConnectorUnit
_model_name = None # to be defined in sub-classes
def __init__(self, environment):
"""
:param environment: current environment (backend, session, ...)
:type environment: :py:class:`connector.connector.Environment`
"""
super(ConnectorUnit, self).__init__()
self.environment = environment
self.backend = self.environment.backend
self.backend_record = self.environment.backend_record
self.session = self.environment.session
self.model = self.session.pool.get(environment.model_name)
# so we can use openerp.tools.translate._, used to find the lang
# that's because _() search for a localcontext attribute
# but self.localcontext should not be used for other purposes
self.localcontext = self.session.context
@classmethod
def match(cls, session, model):
""" Returns True if the current class correspond to the
searched model.
:param session: current session
:type session: :py:class:`connector.session.ConnectorSession`
:param model: model to match
:type model: str or :py:class:`openerp.osv.orm.Model`
"""
# filter out the ConnectorUnit from modules
# not installed in the current DB
if hasattr(model, '_name'): # Model instance
model_name = model._name
else:
model_name = model # str
return model_name in cls.model_name
def get_connector_unit_for_model(self, connector_unit_class, model=None):
""" According to the current
:py:class:`~connector.connector.Environment`,
search and returns an instance of the
:py:class:`~connector.connector.ConnectorUnit` for the current
model and being a class or subclass of ``connector_unit_class``.
If a ``model`` is given, a new
:py:class:`~connector.connector.Environment`
is built for this model.
:param connector_unit_class: ``ConnectorUnit`` to search (class or subclass)
:type connector_unit_class: :py:class:`connector.connector.ConnectorUnit`
:param model: to give if the ``ConnectorUnit`` is for another
model than the current one
:type model: str
"""
if model is None:
env = self.environment
else:
env = Environment(self.backend_record,
self.session,
model)
return env.get_connector_unit(connector_unit_class)
def get_binder_for_model(self, model=None):
""" Returns an new instance of the correct ``Binder`` for
a model """
return self.get_connector_unit_for_model(Binder, model)
class Environment(object):
""" Environment used by the different units for the synchronization.
.. attribute:: backend
Current backend we are working with.
Obtained with ``backend_record.get_backend()``.
Instance of: :py:class:`connector.backend.Backend`
.. attribute:: backend_record
Browsable record of the backend. The backend is inherited
from the model ``connector.backend`` and have at least a
``type`` and a ``version``.
.. attribute:: session
Current session we are working in. It contains the OpenERP
cr, uid and context.
.. attribute:: model_name
Name of the OpenERP model to work with.
"""
def __init__(self, backend_record, session, model_name):
"""
:param backend_record: browse record of the backend
:type backend_record: :py:class:`openerp.osv.orm.browse_record`
:param session: current session (cr, uid, context)
:type session: :py:class:`connector.session.ConnectorSession`
:param model_name: name of the model
:type model_name: str
"""
self.backend_record = backend_record
self.backend = backend_record.get_backend()
self.session = session
self.model_name = model_name
self.model = self.session.pool.get(model_name)
self.pool = self.session.pool
def set_lang(self, code):
""" Change the working language in the environment.
It changes the ``lang`` key in the session's context.
"""
self.session.context['lang'] = code
def get_connector_unit(self, base_class):
""" Searches and returns an instance of the
:py:class:`~connector.connector.ConnectorUnit` for the current
model and being a class or subclass of ``base_class``.
The returned instance is built with ``self`` for its environment.
:param base_class: ``ConnectorUnit`` to search (class or subclass)
:type base_class: :py:class:`connector.connector.ConnectorUnit`
"""
return self.backend.get_class(base_class, self.session,
self.model_name)(self)
class Binder(ConnectorUnit):
""" For one record of a model, capable to find an external or
internal id, or create the binding (link) between them
The Binder should be implemented in the connectors.
"""
_model_name = None # define in sub-classes
def to_openerp(self, external_id, unwrap=False):
""" Give the OpenERP ID for an external ID
:param external_id: external ID for which we want
the OpenERP ID
:param unwrap: if True, returns the openerp_id
else return the id of the binding
:return: a record ID, depending on the value of unwrap,
or None if the external_id is not mapped
:rtype: int
"""
raise NotImplementedError
def to_backend(self, binding_id, wrap=False):
""" Give the external ID for an OpenERP binding ID
(ID in a model magento.*)
:param binding_id: OpenERP binding ID for which we want the backend id
:param wrap: if False, binding_id is the ID of the binding,
if True, binding_id is the ID of the normal record, the
method will search the corresponding binding and returns
the backend id of the binding
:return: external ID of the record
"""
raise NotImplementedError
def bind(self, external_id, binding_id):
""" Create the link between an external ID and an OpenERP ID
:param external_id: external id to bind
:param binding_id: OpenERP ID to bind
:type binding_id: int
"""
raise NotImplementedError
|