1
# -*- coding: utf-8 -*-
2
#########################################################################
3
#This module intergrates Open ERP with the magento core #
4
#Core settings are stored here #
5
#########################################################################
7
# Copyright (C) 2009 Sharoon Thomas, Raphaël Valyi #
8
# Copyright (C) 2011-2012 Akretion Sébastien BEAU sebastien.beau@akretion.com#
9
# Copyright (C) 2011-2012 Camptocamp Guewen Baconnier #
11
#This program is free software: you can redistribute it and/or modify #
12
#it under the terms of the GNU General Public License as published by #
13
#the Free Software Foundation, either version 3 of the License, or #
14
#(at your option) any later version. #
16
#This program is distributed in the hope that it will be useful, #
17
#but WITHOUT ANY WARRANTY; without even the implied warranty of #
18
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
19
#GNU General Public License for more details. #
21
#You should have received a copy of the GNU General Public License #
22
#along with this program. If not, see <http://www.gnu.org/licenses/>. #
23
#########################################################################
27
from openerp.osv import fields
28
from openerp.osv.osv import except_osv
29
from openerp import pooler
30
from openerp import tools
31
from openerp.tools.translate import _
33
from .magerp_osv import MagerpModel, Connection
34
from openerp.addons.connector.decorator import only_for_referential
35
from openerp.addons.connector.external_osv import ExternalSession
37
import openerp.addons.connector as connector
40
_logger = logging.getLogger(__name__)
48
# Don't go below this point unless you're not afraid of spiderwebs ################
50
# TODO: move all the stuff related to Magento in magento.backend
51
class external_referential(MagerpModel):
52
#This class stores instances of magento to which the ERP will
53
#connect, so you can connect OpenERP to multiple Magento
54
#installations (eg different Magento databases)
55
_inherit = "external.referential"
57
SYNC_PRODUCT_FILTERS = {'status': {'=': 1}}
58
SYNC_PARTNER_FILTERS = {}
59
SYNC_PARTNER_STEP = 500
61
def _is_magento_referential(self, cr, uid, ids, field_name, arg, context=None):
62
"""If at least one shop is magento, we consider that the external
63
referential is a magento referential
66
for referential in self.browse(cr, uid, ids, context):
67
if referential.type_id.name == 'magento':
68
res[referential.id] = True
70
res[referential.id] = False
74
'attribute_sets':fields.one2many('magerp.product_attribute_set', 'referential_id', 'Attribute Sets'),
75
'default_pro_cat':fields.many2one('product.category','Default Product Category', help="Products imported from magento may have many categories.\nOpenERP requires a specific category for a product to facilitate invoicing etc."),
76
'default_lang_id':fields.many2one('res.lang', 'Default Language', help="Choose the language which will be used for the Default Value in Magento"),
77
'active': fields.boolean('Active'),
78
'magento_referential': fields.function(_is_magento_referential, type="boolean", method=True, string="Magento Referential"),
79
'last_imported_product_id': fields.integer('Last Imported Product Id', help="Product are imported one by one. This is the magento id of the last product imported. If you clear it all product will be imported"),
80
'last_imported_partner_id': fields.integer('Last Imported Partner Id', help="Partners are imported one by one. This is the magento id of the last partner imported. If you clear it all partners will be imported"),
81
'import_all_attributs': fields.boolean('Import all attributs', help="If the option is uncheck only the attributs that doesn't exist in OpenERP will be imported"),
82
'import_image_with_product': fields.boolean('With image', help="If the option is check the product's image and the product will be imported at the same time and so the step '7-import images' is not needed"),
83
'import_links_with_product': fields.boolean('With links', help="If the option is check the product's links (Up-Sell, Cross-Sell, Related) and the product will be imported at the same time and so the step '8-import links' is not needed"),
87
'active': lambda *a: 1,
90
@only_for_referential('magento')
91
def external_connection(self, cr, uid, id, debug=False, logger=False, context=None):
92
if isinstance(id, list):
94
referential = self.browse(cr, uid, id, context=context)
95
attr_conn = Connection(referential.location, referential.apiusername, referential.apipass, debug, logger)
96
return attr_conn.connect() and attr_conn or False
98
@only_for_referential('magento')
99
def import_referentials(self, cr, uid, ids, context=None):
100
self.import_resources(cr, uid, ids, 'external.shop.group', method='search_read_no_loop', context=context)
101
self.import_resources(cr, uid, ids, 'sale.shop', method='search_read_no_loop', context=context)
102
self.import_resources(cr, uid, ids, 'magerp.storeviews', method='search_read_no_loop', context=context)
105
@only_for_referential('magento')
106
def import_product_categories(self, cr, uid, ids, context=None):
107
self.import_resources(cr, uid, ids, 'product.category', method='search_then_read_no_loop', context=context)
111
#This function will be refactored latter, need to improve Magento API before
112
def sync_attribs(self, cr, uid, ids, context=None):
113
attr_obj = self.pool.get('magerp.product_attributes')
114
attr_set_obj = self.pool.get('magerp.product_attribute_set')
115
for referential in self.browse(cr, uid, ids, context=context):
116
external_session = ExternalSession(referential, referential)
117
attr_conn = external_session.connection
118
attrib_set_ids = attr_set_obj.search(cr, uid, [('referential_id', '=', referential.id)])
119
attrib_sets = attr_set_obj.read(cr, uid, attrib_set_ids, ['magento_id'])
120
#Get all attribute set ids to get all attributes in one go
121
all_attr_set_ids = attr_set_obj.get_all_extid_from_referential(cr, uid, referential.id, context=context)
122
#Call magento for all attributes
123
if referential.import_all_attributs:
124
attributes_imported=[]
126
attributes_imported = attr_obj.get_all_extid_from_referential(cr, uid, referential.id, context=context)
127
import_cr = pooler.get_db(cr.dbname).cursor()
129
mapping = {'magerp.product_attributes' : attr_obj._get_mapping(cr, uid, referential.id, context=context)}
131
for attr_set_id in all_attr_set_ids:
132
mage_inp = attr_conn.call('ol_catalog_product_attribute.list', [attr_set_id]) #Get the tree
133
attribut_to_import = []
134
for attribut in mage_inp:
135
ext_id = attribut['attribute_id']
136
if not ext_id in attributes_imported:
137
attributes_imported.append(ext_id)
138
attr_obj._record_one_external_resource(import_cr, uid, external_session, attribut,
139
defaults={'referential_id':referential.id},
144
_logger.info("All attributs for the attributs set id %s was succesfully imported", attr_set_id)
145
#Relate attribute sets & attributes
147
#Pass in {attribute_set_id:{attributes},attribute_set_id2:{attributes}}
148
#print "Attribute sets are:", attrib_sets
149
#TODO find a solution in order to import the relation in a incremental way (maybe splitting this function in two)
150
for each in attrib_sets:
151
mage_inp[each['magento_id']] = attr_conn.call('ol_catalog_product_attribute.relations', [each['magento_id']])
153
attr_set_obj.relate(import_cr, uid, mage_inp, referential.id, DEBUG)
159
def sync_attrib_sets(self, cr, uid, ids, context=None):
160
return self.import_resources(cr, uid, ids, 'magerp.product_attribute_set', method='search_read_no_loop', context=context)
162
def sync_attrib_groups(self, cr, uid, ids, context=None):
163
return self.import_resources(cr, uid, ids, 'magerp.product_attribute_groups', method='search_read_no_loop', context=context)
165
@only_for_referential('magento')
166
def import_customer_groups(self, cr, uid, ids, context=None):
167
return self.import_resources(cr, uid, ids, 'res.partner.category', method='search_read_no_loop', context=context)
169
def sync_customer_addresses(self, cr, uid, ids, context=None):
170
for referential_id in ids:
171
attr_conn = self.external_connection(cr, uid, referential_id, DEBUG, context=context)
173
#self.pool.get('res.partner').mage_import(cr, uid, filter, attr_conn, referential_id, DEBUG)
174
#TODO fix by retrieving customer list first
175
self.pool.get('res.partner.address').mage_import_base(cr, uid, attr_conn, referential_id, {}, {'ids_or_filter':filter})
178
def _sync_product_storeview(self, cr, uid, external_session, referential_id, ext_product_id, storeview, mapping=None, context=None):
179
if context is None: context = {}
180
#we really need to clean all magento call and give the posibility to force everythere to use the id as identifier
181
product_info = external_session.connection.call('catalog_product.info', [ext_product_id, storeview.code, False, 'id'])
183
ctx.update({'magento_sku': product_info['sku']})
184
defaults={'magento_exportable': True}
185
return self.pool.get('product.product')._record_one_external_resource(cr, uid, external_session, product_info,
190
# This function will be refactored later, maybe it will be better
191
# to improve the magento API before
192
def sync_products(self, cr, uid, ids, context=None):
195
prod_obj = self.pool.get('product.product')
196
# context.update({'dont_raise_error': True})
197
for referential in self.browse(cr, uid, ids, context):
198
external_session = ExternalSession(referential, referential)
199
attr_conn = external_session.connection
200
mapping = {'product.product' : prod_obj._get_mapping(cr, uid,
205
if referential.last_imported_product_id:
206
filters = {'product_id': {'gt': referential.last_imported_product_id}}
207
filters.update(self.SYNC_PRODUCT_FILTERS)
209
#TODO call method should be not harcoded. Need refactoring
210
ext_product_ids = attr_conn.call('ol_catalog_product.search', filter)
211
storeview_obj = self.pool.get('magerp.storeviews')
213
#get all instance storeviews
215
for website in referential.shop_group_ids:
216
for shop in website.shop_ids:
217
for storeview in shop.storeview_ids:
218
storeview_ids += [storeview.id]
221
for storeview in storeview_obj.browse(cr, uid, storeview_ids, context):
222
#get lang of the storeview
223
lang_id = storeview.lang_id
227
except_osv(_('Warning!'), _('The storeviews have no language defined')) #TODO needed?
228
lang = referential.default_lang_id.code
229
if not lang_2_storeview.get(lang, False):
230
lang_2_storeview[lang] = storeview
232
if referential.import_links_with_product:
233
link_types = self.get_magento_product_link_types(cr, uid, referential.id, attr_conn, context=context)
235
import_cr = pooler.get_db(cr.dbname).cursor()
237
for ext_product_id in ext_product_ids:
238
for lang, storeview in lang_2_storeview.iteritems():
240
ctx.update({'lang': lang})
241
res = self._sync_product_storeview(import_cr, uid, external_session, referential.id, ext_product_id, storeview, mapping=mapping, context=ctx)
242
product_id = (res.get('create_id') or res.get('write_id'))
243
if referential.import_image_with_product:
244
prod_obj.import_product_image(import_cr, uid, product_id, referential.id, attr_conn, ext_id=ext_product_id, context=context)
245
if referential.import_links_with_product:
246
prod_obj.mag_import_product_links_types(import_cr, uid, product_id, link_types, external_session, context=context)
247
self.write(import_cr, uid, referential.id, {'last_imported_product_id': int(ext_product_id)}, context=context)
253
def get_magento_product_link_types(self, cr, uid, ids, conn=None, context=None):
255
conn = self.external_connection(cr, uid, ids, DEBUG, context=context)
256
return conn.call('catalog_product_link.types')
258
#TODO refactore me base on connector
259
def sync_images(self, cr, uid, ids, context=None):
260
product_obj = self.pool.get('product.product')
261
image_obj = self.pool.get('product.images')
262
import_cr = pooler.get_db(cr.dbname).cursor()
264
for referential in self.browse(cr, uid, ids, context=context):
265
external_session = ExternalSession(referential, referential)
266
conn = external_session.connection
267
product_ids = product_obj.get_all_oeid_from_referential(cr, uid, referential.id, context=context)
268
for product_id in product_ids:
269
product_obj.import_product_image(import_cr, uid, product_id, referential.id, conn, context=context)
275
#TODO refactore me base on connector
276
def sync_product_links(self, cr, uid, ids, context=None):
277
if context is None: context = {}
278
for referential in self.browse(cr, uid, ids, context):
279
external_session = ExternalSession(referential, referential)
280
conn = external_session.connection
281
exportable_product_ids= []
282
for shop_group in referential.shop_group_ids:
283
for shop in shop_group.shop_ids:
284
exportable_product_ids.extend([product.id for product in shop.exportable_product_ids])
285
exportable_product_ids = list(set(exportable_product_ids))
286
self.pool.get('product.product').mag_import_product_links(cr, uid, exportable_product_ids, external_session, context=context)
289
def export_products(self, cr, uid, ids, context=None):
290
if context is None: context = {}
291
shop_ids = self.pool.get('sale.shop').search(cr, uid, [])
292
for referential_id in ids:
293
for shop in self.pool.get('sale.shop').browse(cr, uid, shop_ids, context):
294
context['conn_obj'] = self.external_connection(cr, uid, referential_id, context=context)
296
tools.debug((cr, uid, shop, context,))
297
shop.export_products(cr, uid, shop, context)
301
def sync_partner(self, cr, uid, ids, context=None):
302
def next_partners(connection, start, step):
303
filters = {'customer_id': {'in': range(start, start + step)}}
304
filters.update(self.SYNC_PARTNER_FILTERS)
306
return attr_conn.call('ol_customer.search', filter)
308
for referential in self.browse(cr, uid, ids, context):
309
attr_conn = referential.external_connection(DEBUG)
311
if referential.last_imported_partner_id:
312
last_imported_id = referential.last_imported_partner_id
314
ext_customer_ids = next_partners(attr_conn, last_imported_id + 1, self.SYNC_PARTNER_STEP)
315
import_cr = pooler.get_db(cr.dbname).cursor()
317
while ext_customer_ids:
318
for ext_customer_id in ext_customer_ids:
319
customer_info = attr_conn.call('customer.info', [ext_customer_id])
320
customer_address_info = attr_conn.call('customer_address.list', [ext_customer_id])
323
if customer_address_info:
324
address_info = customer_address_info[0]
325
address_info['customer_id'] = ext_customer_id
326
address_info['email'] = customer_info['email']
327
external_session = ExternalSession(referential, referential)
328
self.pool.get('res.partner')._record_one_external_resource(import_cr, uid, external_session, customer_info, context=context)
330
self.pool.get('res.partner.address')._record_one_external_resource(import_cr, uid, external_session, address_info, context=context)
331
last_imported_id = int(ext_customer_id)
332
self.write(import_cr, uid, referential.id, {'last_imported_partner_id': last_imported_id}, context=context)
334
ext_customer_ids = next_partners(attr_conn, last_imported_id + 1, self.SYNC_PARTNER_STEP)
339
def sync_newsletter(self, cr, uid, ids, context=None):
340
#update first all customer
341
self.sync_partner(cr, uid, ids, context)
343
partner_obj = self.pool.get('res.partner')
345
for referential_id in ids:
346
attr_conn = self.external_connection(cr, uid, referential_id, DEBUG, context=context)
348
list_subscribers = attr_conn.call('ol_customer_subscriber.list')
350
for each in list_subscribers:
351
each_subscribers_info = attr_conn.call('ol_customer_subscriber.info', [each])
353
# search this customer. If exist, update your newsletter subscription
354
partner_ids = partner_obj.search(cr, uid, [('emailid', '=', each_subscribers_info[0]['subscriber_email'])])
356
#unsubscriber magento value: 3
357
if int(each_subscribers_info[0]['subscriber_status']) == 1:
358
subscriber_status = 1
360
subscriber_status = 0
361
partner_obj.write(cr, uid, partner_ids[0], {'mag_newsletter': subscriber_status})
364
def sync_newsletter_unsubscriber(self, cr, uid, ids, context=None):
365
partner_obj = self.pool.get('res.partner')
367
for referential_id in ids:
368
attr_conn = self.external_connection(cr, uid, referential_id, DEBUG, context=context)
369
partner_ids = partner_obj.search(cr, uid, [('mag_newsletter', '!=', 1), ('emailid', '!=', '')])
371
for partner in partner_obj.browse(cr, uid, partner_ids):
373
attr_conn.call('ol_customer_subscriber.delete', [partner.emailid])
377
# Schedules functions ============ #
378
def run_import_newsletter_scheduler(self, cr, uid, context=None):
382
referential_ids = self.search(cr, uid, [('active', '=', 1)])
385
self.sync_newsletter(cr, uid, referential_ids, context)
387
print "run_import_newsletter_scheduler: %s" % referential_ids
389
def run_import_newsletter_unsubscriber_scheduler(self, cr, uid, context=None):
393
referential_ids = self.search(cr, uid, [('active', '=', 1)])
396
self.sync_newsletter_unsubscriber(cr, uid, referential_ids, context)
398
print "run_import_newsletter_unsubscriber_scheduler: %s" % referential_ids
402
class external_shop_group(MagerpModel):
403
_inherit = "external.shop.group"
404
#Return format of API:{'code': 'base', 'name': 'Main', 'website_id': '1', 'is_default': '1', 'sort_order': '0', 'default_group_id': '1'}
405
# default_group_id is the default shop of the external_shop_group (external_shop_group = website)
407
def _get_default_shop_id(self, cr, uid, ids, prop, unknow_none, context=None):
409
for shop_group in self.browse(cr, uid, ids, context):
410
if shop_group.default_shop_integer_id:
411
rid = self.pool.get('sale.shop').extid_to_existing_oeid(cr, uid, shop_group.default_shop_integer_id, shop_group.referential_id.id)
412
res[shop_group.id] = rid
414
res[shop_group.id] = False
418
'code':fields.char('Code', size=100),
419
'is_default':fields.boolean('Is Default?'),
420
'sort_order':fields.integer('Sort Order'),
421
'default_shop_integer_id':fields.integer('Default Store'), #This field can't be a many2one because shop_group field will be mapped before creating Shop (Shop = Store, shop_group = website)
422
'default_shop_id':fields.function(_get_default_shop_id, type="many2one", relation="sale.shop", method=True, string="Default Store"),
423
'referential_type' : fields.related('referential_id', 'type_id', type='many2one', relation='external.referential.type', string='External Referential Type'),
428
class magerp_storeviews(MagerpModel):
429
_name = "magerp.storeviews"
430
_description = "The magento store views information"
433
'name':fields.char('Store View Name', size=100),
434
'code':fields.char('Code', size=100),
435
'website_id':fields.many2one('external.shop.group', 'Website', select=True, ondelete='cascade'),
436
'is_active':fields.boolean('Default ?'),
437
'sort_order':fields.integer('Sort Order'),
438
'shop_id':fields.many2one('sale.shop', 'Shop', select=True, ondelete='cascade'),
439
'lang_id':fields.many2one('res.lang', 'Language'),
443
# transfered from sale.py ###############################
449
class sale_shop(Model):
450
_inherit = "sale.shop"
452
@only_for_referential('magento')
453
def init_context_before_exporting_resource(self, cr, uid, external_session, object_id, resource_name, context=None):
454
context = super(sale_shop, self).init_context_before_exporting_resource(cr, uid, external_session, object_id, resource_name, context=context)
455
shop = self.browse(cr, uid, object_id, context=context)
456
context['main_lang'] = shop.referential_id.default_lang_id.code
457
context['lang_to_export'] = [shop.referential_id.default_lang_id.code]
458
context['storeview_to_lang'] = {}
459
for storeview in shop.storeview_ids:
460
if storeview.lang_id and storeview.lang_id.code != shop.referential_id.default_lang_id.code:
461
context['storeview_to_lang'][storeview.code] = storeview.lang_id.code
462
if not storeview.lang_id.code in context['lang_to_export']:
463
context['lang_to_export'].append(storeview.lang_id.code)
466
def _get_exportable_product_ids(self, cr, uid, ids, name, args, context=None):
467
res = super(sale_shop, self)._get_exportable_product_ids(cr, uid, ids, name, args, context=None)
469
website_id = self.read(cr, uid, shop_id, ['shop_group_id'])
470
if website_id.get('shop_group_id', False):
471
res[shop_id] = self.pool.get('product.product').search(cr, uid,
472
[('magento_exportable', '=', True),
473
('id', 'in', res[shop_id]),
474
"|", ('websites_ids', 'in', [website_id['shop_group_id'][0]]),
475
('websites_ids', '=', False)])
480
def _get_default_storeview_id(self, cr, uid, ids, prop, unknow_none, context=None):
482
for shop in self.browse(cr, uid, ids, context):
483
if shop.default_storeview_integer_id:
484
rid = self.pool.get('magerp.storeviews').extid_to_oeid(cr, uid, shop.default_storeview_integer_id, shop.referential_id.id)
490
def export_images(self, cr, uid, ids, context=None):
491
if context is None: context = {}
492
start_date = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
493
image_obj = self.pool.get('product.images')
494
for shop in self.browse(cr, uid, ids):
495
external_session = ExternalSession(shop.referential_id, shop)
496
exportable_product_ids = self.read(cr, uid, shop.id, ['exportable_product_ids'], context=context)['exportable_product_ids']
497
res = self.pool.get('product.product').get_exportable_images(cr, uid, external_session, exportable_product_ids, context=context)
499
_logger.info("Creating %s images", len(res['to_create']))
500
_logger.info("Updating %s images", len(res['to_update']))
501
image_obj.update_remote_images(cr, uid, external_session, res['to_update']+res['to_create'], context)
502
self.pool.get('product.images')._set_last_exported_date(cr, uid, external_session, start_date, context=context)
505
#TODO refactor the ay to export images
506
#No time for doing it now so I just overwrite the generic function
507
#In order to use the actual implementation
509
def export_resources(self, cr, uid, ids, resource_name, context=None):
510
if resource_name == 'product.images':
511
return self.export_images(cr, uid, ids, context=context)
513
return super(sale_shop, self).export_resources(cr, uid, ids, resource_name, context=context)
515
def _get_rootcategory(self, cr, uid, ids, name, value, context=None):
517
for shop in self.browse(cr, uid, ids, context):
518
if shop.root_category_id and shop.shop_group_id.referential_id:
519
rid = self.pool.get('product.category').extid_to_existing_oeid(
520
cr, uid, shop.shop_group_id.referential_id.id, shop.root_category_id)
526
def _set_rootcategory(self, cr, uid, id, name, value, fnct_inv_arg, context=None):
527
ir_model_data_obj = self.pool.get('ir.model.data')
528
shop = self.browse(cr, uid, id, context=context)
529
if shop.root_category_id:
530
model_data_id = self.pool.get('product.category').\
531
extid_to_existing_oeid(cr, uid, shop.root_category_id, shop.referential_id.id, context=context)
533
ir_model_data_obj.write(cr, uid, model_data_id, {'res_id' : value}, context=context)
535
raise except_osv(_('Warning!'),
536
_('No external id found, are you sure that the referential are syncronized? '
537
'Please contact your administrator. '
538
'(more information in magentoerpconnect/sale.py)'))
541
def _get_exportable_root_category_ids(self, cr, uid, ids, prop, unknow_none, context=None):
543
res1 = self._get_rootcategory(cr, uid, ids, prop, unknow_none, context)
544
for shop in self.browse(cr, uid, ids, context):
545
res[shop.id] = res1[shop.id] and [res1[shop.id]] or []
548
# xxx move to MagentoConnector._get_import_defaults_sale_shop
549
@only_for_referential('magento')
550
def _get_default_import_values(self, cr, uid, external_session, **kwargs):
551
defaults = super(sale_shop, self)._get_default_import_values(cr, uid, external_session, **kwargs)
552
if not defaults: defaults={}
553
defaults.update({'magento_shop' : True})
556
@only_for_referential('magento')
558
def _export_inventory(self, *args, **kwargs):
559
return super(sale_shop, self)._export_inventory(*args, **kwargs)
562
'default_storeview_integer_id':fields.integer('Magento default Storeview ID'), #This field can't be a many2one because store field will be mapped before creating storeviews
563
'default_storeview_id':fields.function(_get_default_storeview_id, type="many2one", relation="magerp.storeviews", method=True, string="Default Storeview"),
564
'root_category_id':fields.integer('Root product Category'), #This field can't be a many2one because store field will be mapped before creating category
565
'magento_root_category':fields.function(_get_rootcategory, fnct_inv = _set_rootcategory, type="many2one", relation="product.category", string="Root Category"),
566
'exportable_root_category_ids': fields.function(_get_exportable_root_category_ids, type="many2many", relation="product.category", method=True, string="Root Category"), #fields.function(_get_exportable_root_category_ids, type="many2one", relation="product.category", method=True, 'Exportable Root Categories'),
567
'storeview_ids': fields.one2many('magerp.storeviews', 'shop_id', 'Store Views'),
568
'exportable_product_ids': fields.function(_get_exportable_product_ids, method=True, type='one2many', relation="product.product", string='Exportable Products'),
569
#TODO fix me, it's look like related field on a function fielf doesn't work.
570
#'magento_shop': fields.related('referential_id', 'magento_referential',type="boolean", string='Magento Shop', readonly=True),
571
'magento_shop': fields.boolean('Magento Shop', readonly=True),
572
'allow_magento_order_status_push': fields.boolean('Allow Magento Order Status push', help='Allow to send back order status to Magento if order status changed in OpenERP first?'),
573
'allow_magento_notification': fields.boolean('Allow Magento Notification', help='Allow Magento to notify customer with an e-mail when OpenERP change an order status, create an invoice or a delivery order on Magento.'),
577
'allow_magento_order_status_push': lambda * a: False,
578
'allow_magento_notification': lambda * a: False,
581
def _get_magento_status(self, cr, uid, order, context=None):
582
return ORDER_STATUS_MAPPING.get(order.state)
584
def update_shop_orders(self, cr, uid, external_session, order, ext_id, context=None):
585
if context is None: context = {}
587
if order.shop_id.allow_magento_order_status_push:
588
sale_obj = self.pool.get('sale.order')
590
status = self._get_magento_status(cr, uid, order, context=context)
592
result = external_session.connection.call(
593
'sales_order.addComment',
595
order.shop_id.allow_magento_notification])
597
# If status has changed into OERP and the order need_to_update,
598
# then we consider the update is done
599
# remove the 'need_to_update': True
600
if order.need_to_update:
602
cr, uid, order.id, {'need_to_update': False})
605
def _sale_shop(self, cr, uid, callback, context=None):
608
proxy = self.pool.get('sale.shop')
609
domain = [ ('magento_shop', '=', True), ('auto_import', '=', True) ]
611
ids = proxy.search(cr, uid, domain, context=context)
613
callback(cr, uid, ids, context=context)
615
# tools.debug(callback)
619
# Schedules functions ============ #
620
def run_import_orders_scheduler(self, cr, uid, context=None):
621
self._sale_shop(cr, uid, self.import_orders, context=context)
623
def run_update_orders_scheduler(self, cr, uid, context=None):
624
self._sale_shop(cr, uid, self.update_orders, context=context)
626
def run_export_catalog_scheduler(self, cr, uid, context=None):
627
self._sale_shop(cr, uid, self.export_catalog, context=context)
629
def run_export_stock_levels_scheduler(self, cr, uid, context=None):
630
self._sale_shop(cr, uid, self.export_inventory, context=context)
632
def run_update_images_scheduler(self, cr, uid, context=None):
633
self._sale_shop(cr, uid, self.export_images, context=context)
635
def run_export_shipping_scheduler(self, cr, uid, context=None):
636
self._sale_shop(cr, uid, self.export_shipping, context=context)
638
def run_import_check_need_to_update(self, cr, uid, context=None):
639
self._sale_shop(cr, uid, self.check_need_to_update, context=context)
641
class sale_order(Model):
642
_inherit = "sale.order"
644
'magento_incrementid': fields.char('Magento Increment ID', size=32),
645
'magento_storeview_id': fields.many2one('magerp.storeviews', 'Magento Store View'),
646
'is_magento': fields.related(
647
'shop_id', 'referential_id', 'magento_referential',
649
string='Is a Magento Sale Order')
652
def _auto_init(self, cr, context=None):
653
tools.drop_view_if_exists(cr, 'sale_report')
654
cr.execute("ALTER TABLE sale_order_line ALTER COLUMN discount TYPE numeric(16,6);")
655
cr.execute("ALTER TABLE account_invoice_line ALTER COLUMN discount TYPE numeric(16,6);")
656
self.pool.get('sale.report').init(cr)
657
super(sale_order, self)._auto_init(cr, context)
660
def _get_payment_information(self, cr, uid, external_session, order_id, resource, context=None):
662
Parse the external resource and return a dict of data converted
664
vals = super(sale_order, self)._get_payment_information(cr, uid, external_session, order_id, resource, context=context)
665
payment_info = resource.get('payment')
666
if payment_info and payment_info.get('amount_paid'):
668
vals['amount'] = float(payment_info['amount_paid'])
671
def _chain_cancel_orders(self, cr, uid, external_id, external_referential_id, defaults=None, context=None):
672
""" Get all the chain of edited orders (an edited order is canceled on Magento)
673
and cancel them on OpenERP. If an order cannot be canceled (confirmed for example)
674
A request is created to inform the user.
678
conn = context.get('conn_obj', False)
680
# get all parents orders (to cancel) of the sale orders
681
parent = conn.call('sales_order.get_parent', [external_id])
683
parent_list.append(parent)
684
parent = conn.call('sales_order.get_parent', [parent])
686
wf_service = netsvc.LocalService("workflow")
687
for parent_incr_id in parent_list:
688
canceled_order_id = self.extid_to_existing_oeid(cr, uid, parent_incr_id, external_referential_id)
689
if canceled_order_id:
691
wf_service.trg_validate(uid, 'sale.order', canceled_order_id, 'cancel', cr)
692
self.log(cr, uid, canceled_order_id, "order %s canceled when updated from external system" % (canceled_order_id,))
693
_logger.info("Order %s canceled when updated from external system because it has been replaced by a new one", canceled_order_id)
694
except except_osv, e:
695
#TODO: generic reporting of errors in magentoerpconnect
696
# except if the sale order has been confirmed for example, we cannot cancel the order
697
to_cancel_order_name = self.read(cr, uid, canceled_order_id, ['name'])['name']
698
request = self.pool.get('res.request')
699
summary = _(("The sale order %s has been replaced by the sale order %s on Magento.\n"
700
"The sale order %s has to be canceled on OpenERP but it is currently impossible.\n\n"
703
"%s")) % (parent_incr_id,
705
to_cancel_order_name,
708
request.create(cr, uid,
709
{'name': _("Could not cancel sale order %s during Magento's sale orders import") % (to_cancel_order_name,),
718
#TODO reimplement chain cancel orders
719
# # if a created order has a relation_parent_real_id, the new one replaces the original, so we have to cancel the old one
720
# if data[0].get('relation_parent_real_id', False): # data[0] because orders are imported one by one so data always has 1 element
721
# self._chain_cancel_orders(order_cr, uid, ext_order_id, external_referential_id, defaults=defaults, context=context)
723
# XXX a deplacer dans MagentoConnector
724
def _get_filter(self, cr, uid, external_session, step, previous_filter=None, context=None):
725
magento_storeview_ids=[]
726
shop = external_session.sync_from_object
727
for storeview in shop.storeview_ids:
728
magento_storeview_id = self.pool.get('magerp.storeviews').get_extid(cr, uid, storeview.id, shop.referential_id.id, context={})
729
if magento_storeview_id:
730
magento_storeview_ids.append(magento_storeview_id)
733
'state': {'neq': 'canceled'},
734
'store_id': {'in': magento_storeview_ids},
737
if shop.import_orders_from_date:
738
mag_filter.update({'created_at' : {'gt': shop.import_orders_from_date}})
742
'filters': mag_filter,
745
def create_onfly_partner(self, cr, uid, external_session, resource, mapping, defaults, context=None):
747
As magento allow guest order we have to create an on fly partner without any external id
749
if not defaults: defaults={}
750
local_defaults = defaults.copy()
752
resource['firstname'] = resource['customer_firstname']
753
resource['lastname'] = resource['customer_lastname']
754
resource['email'] = resource['customer_email']
756
shop = external_session.sync_from_object
757
partner_defaults = {'website_id': shop.shop_group_id.id}
758
res = self.pool.get('res.partner')._record_one_external_resource(cr, uid, external_session, resource,\
759
mapping=mapping, defaults=partner_defaults, context=context)
760
partner_id = res.get('create_id') or res.get('write_id')
762
local_defaults['partner_id'] = partner_id
763
for address_key in ['partner_invoice_id', 'partner_shipping_id']:
764
if not defaults.get(address_key): local_defaults[address_key] = {}
765
local_defaults[address_key]['partner_id'] = partner_id
766
return local_defaults
768
@only_for_referential('magento')
769
def _transform_one_resource(self, cr, uid, external_session, convertion_type, resource, mapping, mapping_id, \
770
mapping_line_filter_ids=None, parent_data=None, previous_result=None, defaults=None, context=None):
771
resource = self.clean_magento_resource(cr, uid, resource, context=context)
772
resource = self.clean_magento_items(cr, uid, resource, context=context)
773
for line in mapping[mapping_id]['mapping_lines']:
774
if line['name'] == 'customer_id' and not resource.get('customer_id'):
775
#If there is not partner it's a guest order
776
#So we remove the useless information
777
#And create a partner on fly and set the data in the default value
778
#We only do this if the customer_id is in the mapping line
779
#Indeed when we check if a sale order exist only the name is asked for convertion
780
resource.pop('customer_id', None)
781
resource['billing_address'].pop('customer_id', None)
782
resource['shipping_address'].pop('customer_id', None)
783
defaults = self.create_onfly_partner(cr, uid, external_session, resource, mapping, defaults, context=context)
785
return super(sale_order, self)._transform_one_resource(cr, uid, external_session, convertion_type, resource,\
786
mapping, mapping_id, mapping_line_filter_ids=mapping_line_filter_ids, parent_data=parent_data,\
787
previous_result=previous_result, defaults=defaults, context=context)
789
# XXX move to MagentoConnector _ext_search_sale_order
790
@only_for_referential('magento')
791
def _get_external_resource_ids(self, cr, uid, external_session, resource_filter=None, mapping=None, mapping_id=None, context=None):
792
res = super(sale_order, self)._get_external_resource_ids(cr, uid, external_session, resource_filter=resource_filter, mapping=mapping, mapping_id=mapping_id, context=context)
793
order_ids_to_import=[]
794
for external_id in res:
795
existing_id = self.get_oeid(cr, uid, external_id, external_session.referential_id.id, context=context)
797
external_session.logger.info(_("the order %s already exist in OpenERP") % (external_id,))
798
self.ext_set_resource_as_imported(cr, uid, external_session, external_id, mapping=mapping, mapping_id=mapping_id, context=context)
800
order_ids_to_import.append(external_id)
801
return order_ids_to_import
803
# xxx a deplacer dans MagentoConnector _record_one_sale_order
804
@only_for_referential('magento')
805
def _record_one_external_resource(self, cr, uid, external_session, resource, defaults=None, mapping=None, mapping_id=None, context=None):
806
res = super(sale_order, self)._record_one_external_resource(cr, uid, external_session, resource, defaults=defaults, mapping=mapping, mapping_id=mapping_id, context=context)
807
external_id = resource['increment_id'] # TODO it will be better to not hardcode this parameter
808
self.ext_set_resource_as_imported(cr, uid, external_session, external_id, mapping=mapping, mapping_id=mapping_id, context=context)
811
@only_for_referential('magento')
812
def _check_need_to_update_single(self, cr, uid, external_session, order, context=None):
814
For one order, check on Magento if it has been paid since last
815
check. If so, it will launch the defined flow based on the
816
payment type (validate order, invoice, ...)
818
:param browse_record order: browseable sale.order
819
:param Connection conn: connection with Magento
823
#TODO improve me and replace me by a generic function in connector_ecommerce
824
#Only the call to magento should be here
826
model_data_obj = self.pool.get('ir.model.data')
827
# check if the status has changed in Magento
828
# We don't use oeid_to_extid function cause it only handles integer ids
829
# Magento can have something like '100000077-2'
830
model_data_ids = model_data_obj.search(
832
[('model', '=', self._name),
833
('res_id', '=', order.id),
834
('referential_id', '=', order.shop_id.referential_id.id)],
838
prefixed_id = model_data_obj.read(
839
cr, uid, model_data_ids[0], ['name'], context=context)['name']
840
ext_id = self.id_from_prefixed_id(prefixed_id)
844
resource = external_session.connection.call('sales_order.info', [ext_id])
846
if resource['status'] == 'canceled':
847
wf_service = netsvc.LocalService("workflow")
848
wf_service.trg_validate(uid, 'sale.order', order.id, 'cancel', cr)
850
self.log(cr, uid, order.id, "order %s canceled when updated from external system" % (order.id,))
851
# If the order isn't canceled and was waiting for a payment,
852
# so we follow the standard flow according to ext_payment_method:
854
updated = self.paid_and_update(cr, uid, external_session, order.id, resource, context=context)
858
"order %s paid when updated from external system" %
860
# Untick the need_to_update if updated (if so was canceled in magento
861
# or if it has been paid through magento)
863
self.write(cr, uid, order.id, {'need_to_update': False})
864
cr.commit() #Ugly we should not commit in the current cursor
867
########################################################################################################################
869
# CODE THAT CLEAN MAGENTO DATA BEFORE IMPORTING IT THE BEST WILL BE TO REFACTOR MAGENTO API
871
########################################################################################################################
874
def _merge_sub_items(self, cr, uid, product_type, top_item, child_items, context=None):
876
Manage the sub items of the magento sale order lines. A top item contains one
877
or many child_items. For some product types, we want to merge them in the main
878
item, or keep them as order line.
880
This method has to stay because it allow to customize the behavior of the sale
881
order according to the product type.
883
A list may be returned to add many items (ie to keep all child_items as items.
885
:param top_item: main item (bundle, configurable)
886
:param child_items: list of childs of the top item
887
:return: item or list of items
889
if product_type == 'configurable':
890
item = top_item.copy()
891
# For configurable product all information regarding the price is in the configurable item
892
# In the child a lot of information is empty, but contains the right sku and product_id
893
# So the real product_id and the sku and the name have to be extracted from the child
894
for field in ['sku', 'product_id', 'name']:
895
item[field] = child_items[0][field]
899
def clean_magento_items(self, cr, uid, resource, context=None):
901
Method that clean the sale order line given by magento before importing it
903
This method has to stay here because it allow to customize the behavior of the sale
907
child_items = {} # key is the parent item id
910
# Group the childs with their parent
911
for item in resource['items']:
912
if item.get('parent_item_id'):
913
child_items.setdefault(item['parent_item_id'], []).append(item)
915
top_items.append(item)
918
for top_item in top_items:
919
if top_item['item_id'] in child_items:
920
item_modified = self._merge_sub_items(cr, uid,
921
top_item['product_type'],
923
child_items[top_item['item_id']],
925
if not isinstance(item_modified, list):
926
item_modified = [item_modified]
927
all_items.extend(item_modified)
929
all_items.append(top_item)
931
resource['items'] = all_items
934
def clean_magento_resource(self, cr, uid, resource, context=None):
936
Magento copy each address in a address sale table.
937
Keeping the extid of this table 'address_id' is useless because we don't need it later
938
And it's dangerous because we will have various external id for the same resource and the same referential
939
Getting the ext_id of the table customer address is also not posible because Magento LIE
940
Indeed if a customer create a new address on fly magento will give us the default id instead of False
941
So it better to NOT trust magento and not based the address on external_id
942
To avoid any erreur we remove the key
944
for remove_key in ['customer_address_id', 'address_id']:
945
for key in ['billing_address', 'shipping_address']:
946
if remove_key in resource[key]: del resource[key][remove_key]
948
# For really strange and unknow reason magento want to play with me and make me some joke.
949
# Depending of the customer installation some time the field customer_id is equal to NONE
950
# in the sale order and sometime it's equal to NONE in the address but at least the
951
# the information is correct in one of this field
952
# So I make this ugly code to try to fix it.
953
if not resource.get('customer_id'):
954
if resource['billing_address'].get('customer_id'):
955
resource['customer_id'] = resource['billing_address']['customer_id']
957
if not resource['billing_address'].get('customer_id'):
958
resource['billing_address']['customer_id'] = resource['customer_id']
959
if not resource['shipping_address'].get('customer_id'):
960
resource['shipping_address']['customer_id'] = resource['customer_id']
964
class sale_order_line(Model):
965
_inherit = 'sale.order.line'
967
# Rised the precision of the sale.order.line discount field
968
# from 2 to 3 digits in order to be able to have the same amount as Magento.
969
# Example: Magento has a sale line of 299€ and 150€ of discount, so a line at 149€.
970
# We translate it to a percent in the openerp sale order
971
# With a 2 digits precision, we can have 50.17 % => 148.99 or 50.16% => 149.02.
972
# Rise the digits to 3 allows to have 50.167% => 149€
973
'discount': fields.float('Discount (%)', digits=(16, 3), readonly=True, states={'draft': [('readonly', False)]}),
977
# transfered from invoice.py ###############################
980
class account_invoice(orm.Model):
981
_inherit = 'account.invoice'
983
#TODO instead of calling again the sale order information
984
# it will be better to store the ext_id of each sale order line
985
#Moreover some code should be share between the partial export of picking and invoice
986
def add_invoice_line(self, cr, uid, lines, line, context=None):
987
""" A line to add in the invoice is a dict with : product_id and product_qty keys."""
988
line_info = {'product_id': line.product_id.id,
989
'product_qty': line.quantity,
991
lines.append(line_info)
994
def get_invoice_items(self, cr, uid, external_session, invoice_id, order_increment_id, context=None):
995
invoice = self.browse(cr, uid, invoice_id, context=context)
996
balance = invoice.sale_ids[0].amount_total - invoice.amount_total
997
precision = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account')
999
if round(balance, precision):
1000
order_items = external_session.connection.call('sales_order.info', [order_increment_id])['items']
1002
for item in order_items:
1003
product_2_item.update({self.pool.get('product.product').get_oeid(cr, uid, item['product_id'],
1004
external_session.referential_id.id, context={}): item['item_id']})
1007
# get product and quantities to invoice from the invoice
1008
for line in invoice.invoice_line:
1009
lines = self.add_invoice_line(cr, uid, lines, line, context)
1012
#Only export product that exist in the original sale order
1013
if product_2_item.get(line['product_id']):
1014
if item_qty.get(product_2_item[line['product_id']], False):
1015
item_qty[product_2_item[line['product_id']]] += line['product_qty']
1017
item_qty.update({product_2_item[line['product_id']]: line['product_qty']})
1020
def map_magento_order(self, cr, uid, external_session, invoice_id, order_increment_id, context=None):
1021
#TODO Error should be catch by the external report system (need some improvement before)
1022
#For now error are just logged into the OpenERP log file
1024
external_session.logger.warning('Try to map the invoice with an existing order')
1025
invoice_ids = external_session.connection.call('sales_order.get_invoice_ids', [order_increment_id])
1026
#TODO support mapping for partiel invoice if needed
1027
if len(invoice_ids) == 1:
1028
external_session.logger.info(
1029
'Success to map the invoice %s with an existing order for the order %s.'
1030
%(invoice_ids[0], order_increment_id))
1031
return invoice_ids[0]
1033
external_session.logger.error(
1034
'Failed to map the invoice %s with an existing order for the order %s. Too many invoice found'
1035
%(invoice_ids[0], order_increment_id))
1037
except Exception, e:
1038
external_session.logger.error(
1039
'Failed to map the invoice with an existing order for the order %s. Error : %s'
1040
%(order_increment_id, e))
1043
def create_magento_invoice(self, cr, uid, external_session, invoice_id, order_increment_id, context=None):
1044
item_qty = self.get_invoice_items(cr, uid, external_session, invoice_id, order_increment_id, context=context)
1046
return external_session.connection.call('sales_order_invoice.create', [order_increment_id,
1047
item_qty, _('Invoice Created'), False, False])
1048
except Exception, e:
1049
external_session.logger.warning(
1050
'Can not create the invoice for the order %s in the external system. Error : %s'
1051
%(order_increment_id, e))
1052
invoice_id = self.map_magento_order(cr, uid, external_session, invoice_id, order_increment_id, context=context)
1056
raise except_osv(_('Magento Error'), _('Failed to synchronize Magento invoice with OpenERP invoice'))
1058
def ext_create(self, cr, uid, external_session, resources, mapping=None, mapping_id=None, context=None):
1060
for resource_id, resource in resources.items():
1061
res = self.ext_create_one_invoice(cr, uid, external_session, resource_id, resource, context=context)
1063
ext_create_ids[resource_id] = res
1064
return ext_create_ids
1066
def ext_create_one_invoice(self, cr, uid, external_session, resource_id, resource, context=None):
1067
resource = resource[resource.keys()[0]]
1068
if resource['type'] == 'out_invoice':
1069
return self.create_magento_invoice(cr, uid, external_session,
1070
resource_id, resource['order_increment_id'], context=context)
1073
def _export_one_invoice(self, cr, uid, invoice, context=None):
1074
if invoice.sale_ids:
1075
sale = invoice.sale_ids[0]
1076
referential = sale.shop_id.referential_id
1077
if referential and referential.type_name == 'Magento':
1078
ext_id = invoice.get_extid(referential.id)
1082
external_session = ExternalSession(referential, sale.shop_id)
1083
return self._export_one_resource(cr, uid, external_session, invoice.id,
1086
def export_invoice(self, cr, uid, ids, context=None):
1087
for invoice in self .browse(cr, uid, ids, context=context):
1088
self._export_one_invoice(cr, uid, invoice, context=context)
1092
# transfered from product.py ###############################
1095
#Enabling this to True will put all custom attributes into One page in
1097
GROUP_CUSTOM_ATTRS_TOGETHER = False
1100
#TODO find a good method to replace all of the special caracter allowed by magento as name for product fields
1101
special_character_to_replace = [
1103
(u'\xb5', u'micro'),
1110
def convert_to_ascii(my_unicode):
1111
'''Convert to ascii, with clever management of accents (é -> e, è -> e)'''
1112
if isinstance(my_unicode, unicode):
1113
my_unicode_with_ascii_chars_only = ''.join((char for char in unicodedata.normalize('NFD', my_unicode) if unicodedata.category(char) != 'Mn'))
1114
for special_caracter in special_character_to_replace:
1115
my_unicode_with_ascii_chars_only = my_unicode_with_ascii_chars_only.replace(special_caracter[0], special_caracter[1])
1116
return str(my_unicode_with_ascii_chars_only)
1117
# If the argument is already of string type, we return it with the same value
1118
elif isinstance(my_unicode, str):
1123
class magerp_product_category_attribute_options(MagerpModel):
1124
_name = "magerp.product_category_attribute_options"
1125
_description = "Option products category Attributes"
1128
def _get_default_option(self, cr, uid, field_name, value, context=None):
1129
res = self.search(cr, uid, [['attribute_name', '=', field_name], ['value', '=', value]], context=context)
1130
return res and res[0] or False
1133
def get_create_option_id(self, cr, uid, value, attribute_name, context=None):
1134
id = self.search(cr, uid, [['attribute_name', '=', attribute_name], ['value', '=', value]], context=context)
1138
return self.create(cr, uid, {
1140
'attribute_name': attribute_name,
1141
'label': value.replace('_', ' '),
1144
#TODO to finish : this is just the start of the implementation of attributs for category
1146
#'attribute_id':fields.many2one('magerp.product_attributes', 'Attribute'),
1147
'attribute_name':fields.char(string='Attribute Code',size=64),
1148
'value':fields.char('Value', size=200),
1149
#'ipcast':fields.char('Type cast', size=50),
1150
'label':fields.char('Label', size=100),
1154
class product_category(MagerpModel):
1155
_inherit = "product.category"
1157
def _merge_with_default_values(self, cr, uid, external_session, ressource, vals, sub_mapping_list, defaults=None, context=None):
1158
vals = super(product_category, self)._merge_with_default_values(cr, uid, external_session, ressource, vals, sub_mapping_list, defaults=defaults, context=context)
1159
#some time magento category doesn't have a name
1160
if not vals.get('name'):
1161
vals['name'] = 'Undefined'
1164
def _get_default_export_values(self, *args, **kwargs):
1165
defaults = super(product_category, self)._get_default_export_values(*args, **kwargs)
1166
if defaults == None: defaults={}
1167
defaults.update({'magento_exportable': True})
1170
def multi_lang_read(self, cr, uid, external_session, ids, fields_to_read, langs, resources=None, use_multi_lang = True, context=None):
1171
return super(product_category, self).multi_lang_read(cr, uid, external_session, ids, fields_to_read, langs,
1172
resources=resources,
1173
use_multi_lang = False,
1176
def ext_create(self, cr, uid, external_session, resources, mapping=None, mapping_id=None, context=None):
1178
storeview_to_lang = context['storeview_to_lang']
1179
main_lang = context['main_lang']
1180
for resource_id, resource in resources.items():
1181
#Move this part of code in a python lib
1182
parent_id = resource[main_lang]['parent_id']
1183
del resource[main_lang]['parent_id']
1184
ext_id = external_session.connection.call('catalog_category.create', [parent_id, resource[main_lang]])
1185
for storeview, lang in storeview_to_lang.items():
1186
external_session.connection.call('catalog_category.update', [ext_id, resource[lang], storeview])
1187
ext_create_ids[resource_id] = ext_id
1188
return ext_create_ids
1191
def ext_update(self, cr, uid, external_session, resources, mapping=None, mapping_id=None, context=None):
1193
storeview_to_lang = context['storeview_to_lang']
1194
main_lang = context['main_lang']
1195
for resource_id, resource in resources.items():
1196
#Move this part of code in a python lib
1197
ext_id = resource[main_lang]['ext_id']
1198
del resource[main_lang]['ext_id']
1199
parent_id = resource[main_lang]['parent_id']
1200
del resource[main_lang]['parent_id']
1201
external_session.connection.call('catalog_category.update', [ext_id, resource[main_lang], False])
1202
external_session.connection.call('oerp_catalog_category.move', [ext_id, parent_id])
1203
for storeview, lang in storeview_to_lang.items():
1204
del resource[lang]['ext_id']
1205
external_session.connection.call('catalog_category.update', [ext_id, resource[lang], storeview])
1206
ext_update_ids[resource_id] = ext_id
1207
return ext_update_ids
1210
'magerp_fields' : fields.serialized('Magento Product Categories Extra Fields'),
1211
'create_date': fields.datetime('Created date', readonly=True),
1212
'write_date': fields.datetime('Updated date', readonly=True),
1213
'magento_exportable':fields.boolean('Export to Magento'),
1214
'updated':fields.boolean('To synchronize', help="Set if the category underwent a change & has to be synched."),
1215
#*************** Magento Fields ********************
1216
#==== General Information ====
1217
'level': fields.integer('Level', readonly=True),
1218
'magento_parent_id': fields.integer('Magento Parent', readonly=True), #Key Changed from parent_id
1219
'is_active': fields.boolean('Active?', help="Indicates whether active in magento"),
1220
'description': fields.text('Description'),
1221
'image': fields.binary('Image'),
1222
'image_name':fields.char('File Name', size=100),
1223
'meta_title': fields.char('Title (Meta)', size=75),
1224
'meta_keywords': fields.text('Meta Keywords'),
1225
'meta_description': fields.text('Meta Description'),
1226
'url_key': fields.char('URL-key', size=100), #Readonly
1227
#==== Display Settings ====
1228
'display_mode': fields.selection([
1229
('PRODUCTS', 'Products Only'),
1230
('PAGE', 'Static Block Only'),
1231
('PRODUCTS_AND_PAGE', 'Static Block & Products')], 'Display Mode', required=True),
1232
'is_anchor': fields.boolean('Anchor?'),
1233
'use_default_available_sort_by': fields.boolean('Default Config For Available Sort By', help="Use default config for available sort by"),
1234
# 'available_sort_by': fields.sparse(type='many2many', relation='magerp.product_category_attribute_options', string='Available Product Listing (Sort By)', serialization_field='magerp_fields', domain="[('attribute_name', '=', 'sort_by'), ('value', '!=','None')]"),
1235
# 'default_sort_by': fields.many2one('magerp.product_category_attribute_options', 'Default Product Listing Sort (Sort By)', domain="[('attribute_name', '=', 'sort_by')]", require=True),
1236
'magerp_stamp':fields.datetime('Magento stamp'),
1237
'include_in_menu': fields.boolean('Include in Navigation Menu'),
1238
# 'page_layout': fields.many2one('magerp.product_category_attribute_options', 'Page Layout', domain="[('attribute_name', '=', 'page_layout')]"),
1242
'display_mode':lambda * a:'PRODUCTS',
1243
'use_default_available_sort_by': lambda * a:True,
1244
# 'default_sort_by': lambda self,cr,uid,c: self.pool.get('magerp.product_category_attribute_options')._get_default_option(cr, uid, 'sort_by', 'None', context=c),
1245
'level':lambda * a:1,
1246
'include_in_menu': lambda * a:True,
1247
# 'page_layout': lambda self,cr,uid,c: self.pool.get('magerp.product_category_attribute_options')._get_default_option(cr, uid, 'page_layout', 'None', context=c),
1250
def write(self, cr, uid, ids, vals, context=None):
1251
if not 'magerp_stamp' in vals.keys():
1252
vals['magerp_stamp'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
1253
return super(product_category, self).write(cr, uid, ids, vals, context)
1255
# XXX reimplement in Connector as _ext_search_product_category
1256
def _get_external_resource_ids(self, cr, uid, external_session, resource_filter=None, mapping=None, context=None):
1257
def get_child_ids(tree):
1259
result.append(tree['category_id'])
1260
for categ in tree['children']:
1261
result += get_child_ids(categ)
1264
confirmation = external_session.connection.call('catalog_category.currentStore', [0]) #Set browse to root store
1266
categ_tree = external_session.connection.call('catalog_category.tree') #Get the tree
1267
ids = get_child_ids(categ_tree)
1271
class magerp_product_attributes(MagerpModel):
1272
_name = "magerp.product_attributes"
1273
_description = "Attributes of products"
1274
_rec_name = "attribute_code"
1276
def _get_group(self, cr, uid, ids, prop, unknow_none, context=None):
1278
for attribute in self.browse(cr, uid, ids, context):
1279
res[attribute.id] = self.pool.get('magerp.product_attribute_groups').extid_to_existing_oeid(cr, uid, attribute.group_id, attribute.referential_id.id)
1283
'attribute_code':fields.char('Code', size=200),
1284
'magento_id':fields.integer('ID'),
1285
'set_id':fields.integer('Attribute Set'),
1286
'options':fields.one2many('magerp.product_attribute_options', 'attribute_id', 'Attribute Options'),
1287
#'set':fields.function(_get_set, type="many2one", relation="magerp.product_attribute_set", method=True, string="Attribute Set"), This field is invalid as attribs have m2m relation
1288
'frontend_input':fields.selection([
1290
('textarea', 'Text Area'),
1291
('select', 'Selection'),
1292
('multiselect', 'Multi-Selection'),
1293
('boolean', 'Yes/No'),
1296
('media_image', 'Media Image'),
1297
('gallery', 'Gallery'),
1298
('weee', 'Fixed Product Tax'),
1299
('file', 'File'), #this option is not a magento native field it will be better to found a generic solutionto manage this kind of custom option
1300
('weight', 'Weight'),
1303
'frontend_class':fields.char('Frontend Class', size=100),
1304
'backend_model':fields.char('Backend Model', size=200),
1305
'backend_type':fields.selection([
1306
('static', 'Static'),
1307
('varchar', 'Varchar'),
1309
('decimal', 'Decimal'),
1311
('datetime', 'Datetime')], 'Backend Type'),
1312
'frontend_label':fields.char('Label', size=100),
1313
'is_visible_in_advanced_search':fields.boolean('Visible in advanced search?', required=False),
1314
'is_global':fields.boolean('Global ?', required=False),
1315
'is_filterable':fields.boolean('Filterable?', required=False),
1316
'is_comparable':fields.boolean('Comparable?', required=False),
1317
'is_visible':fields.boolean('Visible?', required=False),
1318
'is_searchable':fields.boolean('Searchable ?', required=False),
1319
'is_user_defined':fields.boolean('User Defined?', required=False),
1320
'is_configurable':fields.boolean('Configurable?', required=False),
1321
'is_visible_on_front':fields.boolean('Visible (Front)?', required=False),
1322
'is_used_for_price_rules':fields.boolean('Used for pricing rules?', required=False),
1323
'is_unique':fields.boolean('Unique?', required=False),
1324
'is_required':fields.boolean('Required?', required=False),
1325
'position':fields.integer('Position', required=False),
1326
'group_id': fields.integer('Group') ,
1327
'group':fields.function(_get_group, type="many2one", relation="magerp.product_attribute_groups", method=True, string="Attribute Group"),
1328
'apply_to': fields.char('Apply to', size=200),
1329
'default_value': fields.char('Default Value', size=10),
1330
'note':fields.char('Note', size=200),
1331
'entity_type_id':fields.integer('Entity Type'),
1332
'referential_id':fields.many2one('external.referential', 'Magento Instance', readonly=True),
1333
#These parameters are for automatic management
1334
'field_name':fields.char('Open ERP Field name', size=100),
1335
'attribute_set_info':fields.text('Attribute Set Information'),
1336
'based_on':fields.selection([('product_product', 'Product Product'), ('product_template', 'Product Template')], 'Based On'),
1339
_defaults = {'based_on': lambda*a: 'product_template',
1341
#mapping magentofield:(openerpfield,typecast,)
1342
#have an entry for each mapped field
1343
_no_create_list = ['product_id',
1346
'short_description',
1355
_translatable_default_codes = ['description',
1360
'short_description',
1363
_not_store_in_json = ['minimal_price',
1370
'short_description',
1373
_type_conversion = {'':'char',
1376
'select':'many2one',
1379
'media_image':'binary',
1381
'multiselect':'many2many',
1382
'boolean':'boolean',
1385
'file':'char', #this option is not a magento native field it will be better to found a generic solutionto manage this kind of custom option
1387
_type_casts = {'':'unicode',
1389
'textarea':'unicode',
1393
'media_image':'False',
1395
'multiselect':'list',
1399
'file':'unicode', #this option is not a magento native field it will be better to found a generic solutionto manage this kind of custom option
1401
_variant_fields = ['color',
1405
'special_price_from_date',
1406
'special_price_to_date',
1410
#For some field you can specify the syncronisation way
1411
#in : Magento => OpenERP
1412
#out : Magento <= OpenERP
1413
#in_out (default_value) : Magento <=> OpenERP
1414
#TODO check if this field have to be in only one way and if yes add this feature
1415
_sync_way = {'has_options' : 'in',
1417
'special_price' : 'in',
1420
def _is_attribute_translatable(self, vals):
1421
"""Tells if field associated to attribute should be translatable or not.
1422
For now we are using a default list, later we could say that any attribute
1423
which scope in Magento is 'store' should be translated."""
1424
if vals['attribute_code'] in self._translatable_default_codes:
1429
def write(self, cr, uid, ids, vals, context=None):
1430
"""Will recreate the mapping attributes, beware if you customized some!"""
1434
if type(ids) == int:
1436
result = super(magerp_product_attributes, self).write(cr, uid, ids, vals, context)
1437
model_ids = self.pool.get('ir.model').search(cr, uid, [('model', 'in', ['product.product', 'product.template'])])
1438
product_model_id = self.pool.get('ir.model').search(cr, uid, [('model', 'in', ['product.product'])])[0]
1439
referential_id = context.get('referential_id', False)
1442
all_vals = self.read(cr, uid, id, [], context)
1445
if 'frontend_input' in all_vals.keys() and all_vals['frontend_input'] in ['select', 'multiselect']:
1446
core_imp_conn = self.pool.get('external.referential').external_connection(cr, uid, [referential_id])
1447
options_data = core_imp_conn.call('ol_catalog_product_attribute.options', [all_vals['magento_id']])
1449
self.pool.get('magerp.product_attribute_options').data_to_save(cr, uid, options_data, update=True, context={'attribute_id': id, 'referential_id': referential_id})
1452
field_name = all_vals['field_name']
1453
#TODO refactor me it will be better to add a one2many between the magerp_product_attributes and the ir.model.fields
1454
field_ids = self.pool.get('ir.model.fields').search(cr, uid, [('name', '=', field_name), ('model_id', 'in', model_ids)])
1456
self._create_mapping(cr, uid, self._type_conversion[all_vals.get('frontend_input', False)], field_ids[0], field_name, referential_id, product_model_id, all_vals, id)
1459
def create(self, cr, uid, vals, context=None):
1460
"""Will create product.template new fields accordingly to Magento product custom attributes and also create mappings for them"""
1463
if not vals['attribute_code'] in self._no_create_list:
1464
field_name = "x_magerp_" + vals['attribute_code']
1465
field_name = convert_to_ascii(field_name)
1466
vals['field_name']= field_name
1467
if 'attribute_set_info' in vals.keys():
1468
attr_set_info = eval(vals.get('attribute_set_info',{}))
1469
for each_key in attr_set_info.keys():
1470
vals['group_id'] = attr_set_info[each_key].get('group_id', False)
1472
crid = super(magerp_product_attributes, self).create(cr, uid, vals, context)
1473
if not vals['attribute_code'] in self._no_create_list:
1474
#If the field has to be created
1477
if 'frontend_input' in vals.keys() and vals['frontend_input'] in ['select', 'multiselect']:
1478
core_imp_conn = self.pool.get('external.referential').external_connection(cr, uid, vals['referential_id'])
1479
options_data = core_imp_conn.call('ol_catalog_product_attribute.options', [vals['magento_id']])
1481
self.pool.get('magerp.product_attribute_options').data_to_save(cr, uid, options_data, update=False, context={'attribute_id': crid, 'referential_id': vals['referential_id']})
1484
if vals['attribute_code'] and vals.get('frontend_input', False):
1485
#Code for dynamically generating field name and attaching to this
1486
if vals['attribute_code'] in self._variant_fields:
1487
model_name='product.product'
1489
model_name='product.template'
1491
model_id = self.pool.get('ir.model').search(cr, uid, [('model', '=', model_name)])
1493
if model_id and len(model_id) == 1:
1494
model_id = model_id[0]
1495
#Check if field already exists
1496
referential_id = context.get('referential_id',False)
1497
field_ids = self.pool.get('ir.model.fields').search(cr, uid, [('name', '=', field_name), ('model_id', '=', model_id)])
1500
'model_id':model_id,
1502
'field_description':vals.get('frontend_label', False) or vals['attribute_code'],
1503
'ttype':self._type_conversion[vals.get('frontend_input', False)],
1504
'translate': self._is_attribute_translatable(vals),
1506
if not vals['attribute_code'] in self._not_store_in_json:
1507
if model_name == 'product.template':
1508
field_vals['serialization_field_id'] = self.pool.get('ir.model.fields').search(cr, uid, [('name', '=', 'magerp_tmpl'), ('model', '=', 'product.template')], context=context)[0]
1510
field_vals['serialization_field_id'] = self.pool.get('ir.model.fields').search(cr, uid, [('name', '=', 'magerp_variant'), ('model', '=', 'product.product')], context=context)[0]
1512
#The field is not there create it
1514
if field_vals['ttype'] == 'char':
1515
field_vals['size'] = 100
1516
if field_vals['ttype'] == 'many2one':
1517
field_vals['relation'] = 'magerp.product_attribute_options'
1518
field_vals['domain'] = "[('attribute_id','='," + str(crid) + ")]"
1519
if field_vals['ttype'] == 'many2many':
1520
field_vals['relation'] = 'magerp.product_attribute_options'
1521
field_vals['domain'] = "[('attribute_id','='," + str(crid) + ")]"
1522
field_vals['state'] = 'manual'
1523
#All field values are computed, now save
1524
field_id = self.pool.get('ir.model.fields').create(cr, uid, field_vals)
1525
# mapping have to be based on product.product
1526
model_id = self.pool.get('ir.model').search(cr, uid, [('model', '=', 'product.product')])[0]
1527
self._create_mapping(cr, uid, field_vals['ttype'], field_id, field_name, referential_id, model_id, vals, crid)
1530
def _default_mapping(self, cr, uid, ttype, field_name, vals, attribute_id, model_id, mapping_line, referential_id):
1531
#TODO refactor me and use the direct mapping
1532
#If the field have restriction on domain
1533
#Maybe we can give the posibility to map directly m2m and m2o field_description
1534
#by filtrering directly with the domain and the string value
1535
if ttype in ['char', 'text', 'date', 'float', 'weee', 'boolean']:
1536
mapping_line['evaluation_type'] = 'direct'
1537
if ttype == 'float':
1538
mapping_line['external_type'] = 'float'
1539
elif ttype == 'boolean':
1540
mapping_line['external_type'] = 'int'
1542
mapping_line['external_type'] = 'unicode'
1544
elif ttype in ['many2one']:
1545
mapping_line['evaluation_type'] = 'function'
1546
mapping_line['in_function'] = \
1547
("if '%(attribute_code)s' in resource:\n"
1548
" option_id = self.pool.get('magerp.product_attribute_options').search(cr, uid, [('attribute_id','=',%(attribute_id)s),('value','=',ifield)])\n"
1550
" result = [('%(field_name)s', option_id[0])]") % ({'attribute_code': vals['attribute_code'], 'attribute_id': attribute_id, 'field_name': field_name})
1551
# we browse on resource['%(field_name)s'][0] because resource[field_name] is in the form (id, name)
1552
mapping_line['out_function'] = \
1553
("if '%(field_name)s' in resource:\n"
1554
" result = [('%(attribute_code)s', False)]\n"
1555
" if resource.get('%(field_name)s'):\n"
1556
" option = self.pool.get('magerp.product_attribute_options').browse(cr, uid, resource['%(field_name)s'][0])\n"
1558
" result = [('%(attribute_code)s', option.value)]") % ({'field_name': field_name, 'attribute_code': vals['attribute_code']})
1559
elif ttype in ['many2many']:
1560
mapping_line['evaluation_type'] = 'function'
1561
mapping_line['in_function'] = ("option_ids = []\n"
1562
"opt_obj = self.pool.get('magerp.product_attribute_options')\n"
1563
"for ext_option_id in ifield:\n"
1564
" option_ids.extend(opt_obj.search(cr, uid, [('attribute_id','=',%(attribute_id)s), ('value','=',ext_option_id)]))\n"
1565
"result = [('%(field_name)s', [(6, 0, option_ids)])]") % ({'attribute_id': attribute_id, 'field_name': field_name})
1566
mapping_line['out_function'] = ("result=[('%(attribute_code)s', [])]\n"
1567
"if resource.get('%(field_name)s'):\n"
1568
" options = self.pool.get('magerp.product_attribute_options').browse(cr, uid, resource['%(field_name)s'])\n"
1569
" result = [('%(attribute_code)s', [option.value for option in options])]") % \
1570
({'field_name': field_name, 'attribute_code': vals['attribute_code']})
1571
elif ttype in ['binary']:
1572
warning_text = "Binary mapping is actually not supported (attribute: %s)" % (vals['attribute_code'],)
1573
_logger.warn(warning_text)
1574
warning_msg = ("import logging\n"
1575
"logger = logging.getLogger('in/out_function')\n"
1576
"logger.warn('%s')") % (warning_text,)
1577
mapping_line['in_function'] = mapping_line['out_function'] = warning_msg
1580
def _create_mapping(self, cr, uid, ttype, field_id, field_name, referential_id, model_id, vals, attribute_id):
1581
"""Search & create mapping entries"""
1582
if vals['attribute_code'] in self._no_create_list:
1584
mapping_id = self.pool.get('external.mapping').search(cr, uid, [('referential_id', '=', referential_id), ('model_id', '=', model_id)])
1586
existing_line = self.pool.get('external.mapping.line').search(cr, uid, [('external_field', '=', vals['attribute_code']), ('mapping_id', '=', mapping_id[0])])
1587
if not existing_line:
1588
mapping_line = {'external_field': vals['attribute_code'],
1590
'mapping_id': mapping_id[0],
1591
'type': self._sync_way.get(vals['attribute_code'], 'in_out'),
1592
'external_type': self._type_casts[vals.get('frontend_input', False)],
1593
'field_id': field_id, }
1594
mapping_line = self._default_mapping(cr, uid, ttype, field_name, vals, attribute_id, model_id, mapping_line, referential_id)
1595
self.pool.get('external.mapping.line').create(cr, uid, mapping_line)
1599
"""Dont remove the code, we might need it --sharoon
1600
class magerp_product_attributes_set_info(Model):
1601
_name="magerp.product_attributes.set_info"
1602
_description = "Attribute Set information for each attribute"
1604
'referential_id':fields.many2one('external.referential', 'Magento Instance', readonly=True),
1606
'sort':fields.integer('sort')
1607
'group_sort':fields.integer('group_sort')
1610
magerp_product_attributes_set_info()"""
1612
class magerp_product_attribute_options(MagerpModel):
1613
_name = "magerp.product_attribute_options"
1614
_description = "Options of selected attributes"
1618
'attribute_id':fields.many2one('magerp.product_attributes', 'Attribute'),
1619
'attribute_name':fields.related('attribute_id', 'attribute_code', type='char', string='Attribute Code',),
1620
'value':fields.char('Value', size=200),
1621
'ipcast':fields.char('Type cast', size=50),
1622
'label':fields.char('Label', size=100),
1623
'referential_id':fields.many2one('external.referential', 'Magento Instance', readonly=True),
1627
def data_to_save(self, cr, uid, vals_list, update=False, context=None):
1628
"""This method will take data from vals and use context to create record"""
1633
to_remove_ids = self.search(cr, uid, [('attribute_id', '=', context['attribute_id'])])
1635
for vals in vals_list:
1636
if vals.get('value', False) and vals.get('label', False):
1637
value = unicode(vals['value'])
1638
#Fixme: What to do when Magento offers emty options which open erp doesnt?
1639
#Such cases dictionary is: {'value':'','label':''}
1641
existing_ids = self.search(
1643
[('attribute_id', '=', context['attribute_id']),
1644
('value', '=', value)],
1646
if len(existing_ids) == 1:
1647
to_remove_ids.remove(existing_ids[0])
1648
self.write(cr, uid, existing_ids[0], {'label': vals.get('label', False)})
1651
self.create(cr, uid, {
1652
'attribute_id': context['attribute_id'],
1654
'label': vals['label'],
1655
'referential_id': context['referential_id'],
1659
self.unlink(cr, uid, to_remove_ids) #if a product points to a removed option, it will get no option instead
1661
def get_option_id(self, cr, uid, attr_name, value, instance):
1662
attr_id = self.search(cr, uid, [('attribute_name', '=', attr_name), ('value', '=', value), ('referential_id', '=', instance)])
1669
class magerp_product_attribute_set(MagerpModel):
1670
_name = "magerp.product_attribute_set"
1671
_description = "Attribute sets in products"
1672
_rec_name = 'attribute_set_name'
1675
'sort_order':fields.integer('Sort Order'),
1676
'attribute_set_name':fields.char('Set Name', size=100),
1677
'attributes':fields.many2many('magerp.product_attributes', 'magerp_attrset_attr_rel', 'set_id', 'attr_id', 'Attributes'),
1678
'referential_id':fields.many2one('external.referential', 'Magento Instance', readonly=True),
1679
'magento_id':fields.integer('Magento ID'),
1683
def update_attribute(self, cr, uid, ids, context=None):
1684
ref_obj = self.pool.get('external.referential')
1685
mag_ref_ids = ref_obj.search(cr, uid, [('version_id','ilike', 'magento')], context=context)
1686
for referential in ref_obj.browse(cr, uid, mag_ref_ids, context=context):
1687
external_session = ExternalSession(referential, referential)
1688
for attr_set_id in ids:
1689
attr_set_ext_id = self.get_extid(cr, uid, attr_set_id, referential.id, context=context)
1691
self._import_attribute(cr, uid, external_session, attr_set_ext_id, context=context)
1692
self._import_attribute_relation(cr, uid, external_session, [attr_set_ext_id], context=context)
1696
def _import_attribute(self, cr, uid, external_session, attr_set_ext_id, attributes_imported=None, context=None):
1697
attr_obj = self.pool.get('magerp.product_attributes')
1698
mage_inp = external_session.connection.call('ol_catalog_product_attribute.list', [attr_set_ext_id]) #Get the tree
1699
mapping = {'magerp.product_attributes' : attr_obj._get_mapping(cr, uid, external_session.referential_id.id, context=context)}
1700
attribut_to_import = []
1701
if not attributes_imported: attributes_imported=[]
1702
for attribut in mage_inp:
1703
ext_id = attribut['attribute_id']
1704
if not ext_id in attributes_imported:
1705
attributes_imported.append(ext_id)
1706
attr_obj._record_one_external_resource(cr, uid, external_session, attribut,
1707
defaults={'referential_id':external_session.referential_id.id},
1711
external_session.logger.info("All attributs for the attributs set id %s was succesfully imported", attr_set_ext_id)
1715
def _import_attribute_relation(self, cr, uid, external_session, attr_set_ext_ids, context=None):
1716
#Relate attribute sets & attributes
1718
#Pass in {attribute_set_id:{attributes},attribute_set_id2:{attributes}}
1719
#print "Attribute sets are:", attrib_sets
1720
#TODO find a solution in order to import the relation in a incremental way (maybe splitting this function in two)
1721
for attr_id in attr_set_ext_ids:
1722
mage_inp[attr_id] = external_session.connection.call('ol_catalog_product_attribute.relations', [attr_id])
1724
self.relate(cr, uid, mage_inp, external_session.referential_id.id, context)
1727
def relate(self, cr, uid, mage_inp, instance, *args):
1728
#TODO: Build the relations code
1729
#Note: It is better to insert multiple record by cr.execute because:
1730
#1. Everything ends in a sinlge query (Fast)
1731
#2. If the values are updated using the return value for m2m field it may execute much slower
1732
#3. Multirow insert is 4x faster than reduntant insert ref:http://kaiv.wordpress.com/2007/07/19/faster-insert-for-multiple-rows/
1734
#Get all attributes in onew place to convert from mage_id to oe_id
1735
attr_ids = self.pool.get('magerp.product_attributes').search(cr, uid, [])
1736
attr_list_oe = self.pool.get('magerp.product_attributes').read(cr, uid, attr_ids, ['magento_id'])
1738
for each_set in attr_list_oe:
1739
attr_list[each_set['magento_id']] = each_set['id']
1740
attr_set_ids = self.search(cr, uid, [])
1741
attr_set_list_oe = self.read(cr, uid, attr_set_ids, ['magento_id'])
1743
#print attr_set_list_oe
1744
for each_set in attr_set_list_oe:
1745
attr_set_list[each_set['magento_id']] = each_set['id']
1748
for each_key in mage_inp.keys():
1749
self.write(cr, uid, attr_set_list[each_key], {'attributes': [[6, 0, []]]})
1750
for each_attr in mage_inp[each_key]:
1751
if each_attr['attribute_id']:
1753
key_attrs.append((attr_set_list[each_key], attr_list[int(each_attr['attribute_id'])]))
1754
except Exception, e:
1756
#rel_dict {set_id:[attr_id_1,attr_id_2,],set_id2:[attr_id_1,attr_id_3]}
1757
if len(key_attrs) > 0:
1758
#rel_dict {set_id:[attr_id_1,attr_id_2,],set_id2:[attr_id_1,attr_id_3]}
1759
query = "INSERT INTO magerp_attrset_attr_rel (set_id,attr_id) VALUES "
1760
for each_pair in key_attrs:
1761
query += str(each_pair)
1763
query = query[0:len(query) - 1] + ";"
1768
class magerp_product_attribute_groups(MagerpModel):
1769
_name = "magerp.product_attribute_groups"
1770
_description = "Attribute groups in Magento"
1771
_rec_name = 'attribute_group_name'
1772
_order = "sort_order"
1773
def _get_set(self, cr, uid, ids, prop, unknow_none, context=None):
1775
for attribute_group in self.browse(cr, uid, ids, context):
1776
res[attribute_group.id] = self.pool.get('magerp.product_attribute_set').extid_to_oeid(cr, uid, attribute_group.attribute_set_id, attribute_group.referential_id.id)
1779
# XXX a deplacer dans MagentoConnector
1780
def _get_filter(self, cr, uid, external_session, step, previous_filter=None, context=None):
1781
attrset_ids = self.pool.get('magerp.product_attribute_set').get_all_extid_from_referential(cr, uid, external_session.referential_id.id, context=context)
1782
return {'attribute_set_id':{'in':attrset_ids}}
1785
'attribute_set_id':fields.integer('Attribute Set ID'),
1786
'attribute_set':fields.function(_get_set, type="many2one", relation="magerp.product_attribute_set", method=True, string="Attribute Set"),
1787
'attribute_group_name':fields.char('Group Name', size=100),
1788
'sort_order':fields.integer('Sort Order'),
1789
'default_id':fields.integer('Default'),
1790
'referential_id':fields.many2one('external.referential', 'Magento Instance', readonly=True),
1793
class product_tierprice(Model):
1794
_name = "product.tierprice"
1795
_description = "Implements magento tier pricing"
1798
'web_scope':fields.selection([
1799
('all', 'All Websites'),
1800
('specific', 'Specific Website'),
1802
'website_id':fields.many2one('external.shop.group', 'Website'),
1803
'group_scope':fields.selection([
1804
('1', 'All groups'),
1805
('0', 'Specific group')
1807
'cust_group':fields.many2one('res.partner.category', 'Customer Group'),
1808
'website_price':fields.float('Website Price', digits=(10, 2),),
1809
'price':fields.float('Price', digits=(10, 2),),
1810
'price_qty':fields.float('Quantity Slab', digits=(10, 4), help="Slab & above eg.For 10 and above enter 10"),
1811
'product':fields.many2one('product.product', 'Product'),
1812
'referential_id':fields.many2one('external.referential', 'Magento Instance', readonly=True),
1815
'cust_group':(False, int, """result=self.pool.get('res.partner.category').mage_to_oe(cr,uid,cust_group,instance)\nif result:\n\tresult=[('cust_group',result[0])]\nelse:\n\tresult=[('cust_group',False)]"""),
1816
'all_groups':(False, str, """if all_groups=='1':\n\tresult=[('group_scope','1')]\nelse:\n\tresult=[('group_scope','1')]"""),
1817
'website_price':('website_price', float),
1818
'price':('price', float),
1819
'website_id':(False, int, """result=self.pool.get('external.shop.group').mage_to_oe(cr,uid,website_id,instance)\nif result:\n\tresult=[('website_id',result[0])]\nelse:\n\tresult=[('website_id',False)]"""),
1820
'price_qty':('price_qty', float),
1823
class product_product_type(Model):
1824
_name = 'magerp.product_product_type'
1826
'name': fields.char('Name', size=100, required=True, translate=True),
1827
'product_type': fields.char('Type', size=100, required=True, help="Use the same name of Magento product type, for example 'simple'."),
1828
'default_type': fields.selection([('product','Stockable Product'),('consu', 'Consumable'),('service','Service')], 'Default Product Type', required=True, help="Default product's type (Procurement) when a product is imported from Magento."),
1832
class product_mag_osv(MagerpModel):
1833
_register = False # Set to false if the model shouldn't be automatically discovered.
1835
#remember one thing in life: Magento lies: it tells attributes are required while they are awkward to fill
1836
#and will have a nice default vaule anyway, that's why we avoid making them mandatory in the product view
1837
_magento_fake_mandatory_attrs = ['created_at', 'updated_at', 'has_options', 'required_options', 'model']
1839
def open_magento_fields(self, cr, uid, ids, context=None):
1840
ir_model_data_obj = self.pool.get('ir.model.data')
1841
ir_model_data_id = ir_model_data_obj.search(cr, uid, [['model', '=', 'ir.ui.view'], ['name', '=', self._name.replace('.','_') + '_wizard_form_view_magerpdynamic']], context=context)
1842
if ir_model_data_id:
1843
res_id = ir_model_data_obj.read(cr, uid, ir_model_data_id, fields=['res_id'])[0]['res_id']
1844
set_id = self.read(cr, uid, ids, fields=['set'], context=context)[0]['set']
1847
raise except_osv(_('User Error'), _('Please chose an attribute set before'))
1850
'name': 'Magento Fields',
1851
'view_type': 'form',
1852
'view_mode': 'form',
1853
'view_id': [res_id],
1854
'res_model': self._name,
1855
'context': "{'set': %s, 'open_from_button_object_id': %s}"%(set_id[0], ids),
1856
'type': 'ir.actions.act_window',
1859
'res_id': ids and ids[0] or False,
1862
def save_and_close_magento_fields(self, cr, uid, ids, context=None):
1863
'''this empty function will save the magento field'''
1864
return {'type': 'ir.actions.act_window_close'}
1866
def redefine_prod_view(self, cr, uid, field_names, context=None):
1868
Rebuild the product view with attribute groups and attributes
1870
if context is None: context = {}
1871
attr_set_obj = self.pool.get('magerp.product_attribute_set')
1872
attr_group_obj = self.pool.get('magerp.product_attribute_groups')
1873
attr_obj = self.pool.get('magerp.product_attributes')
1874
translation_obj = self.pool.get('ir.translation')
1876
attribute_set_id = context['set']
1877
attr_set = attr_set_obj.browse(cr, uid, attribute_set_id)
1878
attr_group_fields_rel = {}
1880
multiwebsites = context.get('multiwebsite', False)
1882
fields_get = self.fields_get(cr, uid, field_names, context)
1884
cr.execute("select attr_id, group_id, attribute_code, frontend_input, "
1885
"frontend_label, is_required, apply_to, field_name "
1886
"from magerp_attrset_attr_rel "
1887
"left join magerp_product_attributes "
1888
"on magerp_product_attributes.id = attr_id "
1889
"where magerp_attrset_attr_rel.set_id=%s" %
1892
results = cr.dictfetchall()
1893
attribute = results.pop()
1895
mag_group_id = attribute['group_id']
1896
oerp_group_id = attr_group_obj.extid_to_existing_oeid(
1897
cr, uid, attr_set.referential_id.id, mag_group_id)
1898
# FIXME: workaround in multi-Magento instances (databases)
1899
# where attribute group might not be found due to the way we
1900
# share attributes currently
1901
if not oerp_group_id:
1902
ref_ids = self.pool.get(
1903
'external.referential').search(cr, uid, [])
1904
for ref_id in ref_ids:
1905
if ref_id != attr_set.referential_id.id:
1906
oerp_group_id = attr_group_obj.extid_to_existing_oeid(
1907
cr, uid, ref_id, mag_group_id)
1911
group_name = attr_group_obj.read(
1912
cr, uid, oerp_group_id,
1913
['attribute_group_name'],
1914
context=context)['attribute_group_name']
1916
# Create a page for each attribute group
1917
attr_group_fields_rel.setdefault(group_name, [])
1919
if attribute['group_id'] != mag_group_id:
1922
if attribute['field_name'] in field_names:
1923
if not attribute['attribute_code'] in attr_obj._no_create_list:
1924
if (group_name in ['Meta Information',
1926
'Custom Layout Update',
1929
GROUP_CUSTOM_ATTRS_TOGETHER==False:
1930
attr_group_fields_rel[group_name].append(attribute)
1932
attr_group_fields_rel.setdefault(
1933
'Custom Attributes', []).append(attribute)
1935
attribute = results.pop()
1939
notebook = etree.Element('notebook', colspan="4")
1941
attribute_groups = attr_group_fields_rel.keys()
1942
attribute_groups.sort()
1943
for group in attribute_groups:
1944
lang = context.get('lang', '')
1945
trans = translation_obj._get_source(
1946
cr, uid, 'product.product', 'view', lang, group)
1947
trans = trans or group
1948
if attr_group_fields_rel.get(group):
1949
page = etree.SubElement(notebook, 'page', string=trans)
1950
for attribute in attr_group_fields_rel.get(group, []):
1951
if attribute['frontend_input'] == 'textarea':
1952
etree.SubElement(page, 'newline')
1957
string=fields_get[attribute['field_name']]['string'])
1959
f = etree.SubElement(
1960
page, 'field', name=attribute['field_name'])
1962
# apply_to is a string like
1963
# "simple,configurable,virtual,bundle,downloadable"
1964
req_apply_to = not attribute['apply_to'] or \
1965
'simple' in attribute['apply_to'] or \
1966
'configurable' in attribute['apply_to']
1967
if attribute['is_required'] and \
1969
attribute['attribute_code'] not in self._magento_fake_mandatory_attrs:
1970
f.set('attrs', "{'required': [('magento_exportable', '=', True)]}")
1972
if attribute['frontend_input'] == 'textarea':
1973
f.set('nolabel', "1")
1974
f.set('colspan', "4")
1976
setup_modifiers(f, fields_get[attribute['field_name']],
1980
website_page = etree.SubElement(
1981
notebook, 'page', string=_('Websites'))
1982
wf = etree.SubElement(
1983
website_page, 'field', name='websites_ids', nolabel="1")
1984
setup_modifiers(wf, fields_get['websites_ids'], context=context)
1988
def _filter_fields_to_return(self, cr, uid, field_names, context=None):
1989
'''This function is a hook in order to filter the fields that appears on the view'''
1992
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
1995
result = super(product_mag_osv, self).fields_view_get(
1996
cr, uid, view_id,view_type,context,toolbar=toolbar)
1997
if view_type == 'form':
1998
eview = etree.fromstring(result['arch'])
1999
btn = eview.xpath("//button[@name='open_magento_fields']")
2002
page_placeholder = eview.xpath(
2003
"//page[@string='attributes_placeholder']")
2005
attrs_mag_notebook = "{'invisible': [('set', '=', False)]}"
2007
if context.get('set'):
2008
fields_obj = self.pool.get('ir.model.fields')
2009
models = ['product.template']
2010
if self._name == 'product.product':
2011
models.append('product.product')
2013
model_ids = self.pool.get('ir.model').search(
2014
cr, uid, [('model', 'in', models)], context=context)
2015
field_ids = fields_obj.search(
2017
[('model_id', 'in', model_ids)],
2019
#TODO it will be better to avoid adding fields here
2020
#Moreover we should also add the field mag_manage_stock
2021
field_names = ['product_type']
2022
fields = fields_obj.browse(cr, uid, field_ids, context=context)
2023
for field in fields:
2024
if field.name.startswith('x_'):
2025
field_names.append(field.name)
2026
website_ids = self.pool.get('external.shop.group').search(
2028
[('referential_type', '=ilike', 'mag%')],
2030
if len(website_ids) > 1:
2031
context['multiwebsite'] = True
2032
field_names.append('websites_ids')
2034
field_names = self._filter_fields_to_return(
2035
cr, uid, field_names, context)
2036
result['fields'].update(
2037
self.fields_get(cr, uid, field_names, context))
2039
attributes_notebook = self.redefine_prod_view(
2040
cr, uid, field_names, context)
2042
# if the placeholder is a "page", that means we are
2043
# in the product main form. If it is a "separator", it
2044
# means we are in the attributes popup
2045
if page_placeholder:
2046
placeholder = page_placeholder[0]
2047
magento_page = etree.Element(
2049
string=_('Magento Information'),
2050
attrs=attrs_mag_notebook)
2051
setup_modifiers(magento_page, context=context)
2052
f = etree.SubElement(
2055
name='product_type',
2056
attrs="{'required': [('magento_exportable', '=', True)]}")
2057
setup_modifiers(f, field=result['fields']['product_type'], context=context)
2058
magento_page.append(attributes_notebook)
2059
btn.getparent().remove(btn)
2061
placeholder = eview.xpath(
2062
"//separator[@string='attributes_placeholder']")[0]
2063
magento_page = attributes_notebook
2065
placeholder.getparent().replace(placeholder, magento_page)
2067
new_btn = etree.Element(
2069
name='open_magento_fields',
2070
string=_('Open Magento Fields'),
2071
icon='gtk-go-forward',
2074
attrs=attrs_mag_notebook)
2075
setup_modifiers(new_btn, context=context)
2076
btn.getparent().replace(btn, new_btn)
2077
if page_placeholder:
2078
placeholder = page_placeholder[0]
2079
placeholder.getparent().remove(placeholder)
2081
result['arch'] = etree.tostring(eview, pretty_print=True)
2082
#TODO understand (and fix) why the orm fill the field size for the text field :S
2083
for field in result['fields']:
2084
if result['fields'][field]['type'] == 'text':
2085
if 'size' in result['fields'][field]: del result['fields'][field]['size']
2088
class product_template(product_mag_osv):
2089
_inherit = "product.template"
2091
'magerp_tmpl' : fields.serialized('Magento Template Fields'),
2092
'set':fields.many2one('magerp.product_attribute_set', 'Attribute Set'),
2093
'websites_ids': fields.many2many('external.shop.group', 'magerp_product_shop_group_rel', 'product_id', 'shop_group_id', 'Websites', help='By defaut product will be exported on every website, if you want to export it only on some website select them here'),
2094
'mag_manage_stock': fields.selection([
2095
('use_default','Use Default Config'),
2096
('no', 'Do Not Manage Stock'),
2097
('yes','Manage Stock')],
2098
'Manage Stock Level'),
2099
'mag_manage_stock_shortage': fields.selection([
2100
('use_default','Use Default Config'),
2102
('yes','Sell qty < 0'),
2103
('yes-and-notification','Sell qty < 0 and Use Customer Notification')],
2104
'Manage Inventory Shortage'),
2108
'mag_manage_stock': 'use_default',
2109
'mag_manage_stock_shortage': 'use_default',
2113
class product_product(orm.Model):
2114
_inherit = 'product.product'
2116
def write(self, cr, uid, ids, vals, context=None):
2117
if vals.get('referential_id', False):
2118
instance = vals['referential_id']
2119
#Filter the keys to be changes
2121
if type(ids) == list and len(ids) == 1:
2123
elif type(ids) == int or type(ids) == long:
2128
if 'x_magerp_tier_price' in vals.keys():
2129
tier_price = vals.pop('x_magerp_tier_price')
2130
tp_obj = self.pool.get('product.tierprice')
2131
#Delete existing tier prices
2132
tier_price_ids = tp_obj.search(cr, uid, [('product', '=', ids)])
2134
tp_obj.unlink(cr, uid, tier_price_ids)
2135
#Save the tier price
2137
self.create_tier_price(cr, uid, tier_price, instance, ids)
2138
stat = super(product_product, self).write(cr, uid, ids, vals, context)
2139
#Perform other operation
2142
def create_tier_price(self, cr, uid, tier_price, instance, product_id):
2143
tp_obj = self.pool.get('product.tierprice')
2144
for each in eval(tier_price):
2146
cust_group = self.pool.get('res.partner.category').mage_to_oe(cr, uid, int(each['cust_group']), instance)
2148
tier_vals['cust_group'] = cust_group[0]
2150
tier_vals['cust_group'] = False
2151
tier_vals['website_price'] = float(each['website_price'])
2152
tier_vals['price'] = float(each['price'])
2153
tier_vals['price_qty'] = float(each['price_qty'])
2154
tier_vals['product'] = product_id
2155
tier_vals['referential_id'] = instance
2156
tier_vals['group_scope'] = each['all_groups']
2157
if each['website_id'] == '0':
2158
tier_vals['web_scope'] = 'all'
2160
tier_vals['web_scope'] = 'specific'
2161
tier_vals['website_id'] = self.pool.get('external.shop.group').mage_to_oe(cr, uid, int(each['website_id']), instance)
2162
tp_obj.create(cr, uid, tier_vals)
2164
def create(self, cr, uid, vals, context=None):
2166
if vals.get('referential_id', False):
2167
instance = vals['referential_id']
2168
#Filter keys to be changed
2169
if 'x_magerp_tier_price' in vals.keys():
2170
tier_price = vals.pop('x_magerp_tier_price')
2172
crid = super(product_product, self).create(cr, uid, vals, context)
2173
#Save the tier price
2175
self.create_tier_price(cr, uid, tier_price, instance, crid)
2176
#Perform other operations
2179
def copy(self, cr, uid, id, default=None, context=None):
2183
default['magento_exportable'] = False
2185
return super(product_product, self).copy(cr, uid, id, default=default, context=context)
2187
def unlink(self, cr, uid, ids, context=None):
2188
#if product is mapped to magento, not delete it
2190
sale_obj = self.pool.get('sale.shop')
2192
('magento_shop', '=', True),
2194
shops_ids = sale_obj.search(cr, uid, search_params)
2196
for shop in sale_obj.browse(cr, uid, shops_ids, context):
2197
if shop.referential_id and shop.referential_id.type_id.name == 'Magento':
2198
for product_id in ids:
2199
mgn_product = self.get_extid(cr, uid, product_id, shop.referential_id.id)
2205
raise except_osv(_('Warning!'),
2206
_('They are some products related to Magento. '
2207
'They can not be deleted!\n'
2208
'You can change their Magento status to "Disabled" '
2209
'and uncheck the active box to hide them from OpenERP.'))
2211
raise except_osv(_('Warning!'),
2212
_('This product is related to Magento. '
2213
'It can not be deleted!\n'
2214
'You can change it Magento status to "Disabled" '
2215
'and uncheck the active box to hide it from OpenERP.'))
2217
return super(product_product, self).unlink(cr, uid, ids, context)
2219
def _prepare_inventory_magento_vals(self, cr, uid, product, stock, shop,
2222
Prepare the values to send to Magento (message product_stock.update).
2223
Can be inherited to customize the values to send.
2225
:param browse_record product: browseable product
2226
:param browse_record stock: browseable stock location
2227
:param browse_record shop: browseable shop
2228
:return: a dict of values which will be sent to Magento with a call to:
2229
product_stock.update
2235
"yes-and-notification": 2,
2238
stock_field = (shop.product_stock_field_id and
2239
shop.product_stock_field_id.name or
2240
'virtual_available')
2241
stock_quantity = product[stock_field]
2243
return {'qty': stock_quantity,
2244
'manage_stock': int(product.mag_manage_stock == 'yes'),
2245
'use_config_manage_stock': int(product.mag_manage_stock == 'use_default'),
2246
'backorders': map_shortage[product.mag_manage_stock_shortage],
2247
'use_config_backorders':int(product.mag_manage_stock_shortage == 'use_default'),
2248
# put the stock availability to "out of stock"
2249
'is_in_stock': int(stock_quantity > 0)}
2251
def export_inventory(self, cr, uid, external_session, ids, context=None):
2253
Export to Magento the stock quantity for the products in ids which
2254
are already exported on Magento and are not service products.
2256
:param int shop_id: id of the shop where the stock inventory has
2258
:param Connection connection: connection object
2261
#TODO get also the list of product which the option mag_manage_stock have changed
2262
#This can be base on the group_fields that can try tle last write date of a group of fields
2263
if context is None: context = {}
2265
# use the stock location defined on the sale shop
2266
# to compute the stock value
2267
stock = external_session.sync_from_object.warehouse_id.lot_stock_id
2268
location_ctx = context.copy()
2269
location_ctx['location'] = stock.id
2270
for product_id in ids:
2271
self._export_inventory(cr, uid, external_session, product_id, context=location_ctx)
2275
@catch_error_in_report
2276
def _export_inventory(self, cr, uid, external_session, product_id, context=None):
2277
product = self.browse(cr, uid, product_id, context=context)
2278
stock = external_session.sync_from_object.warehouse_id.lot_stock_id
2279
mag_product_id = self.get_extid(
2280
cr, uid, product.id, external_session.referential_id.id, context=context)
2281
if not mag_product_id:
2282
return False # skip products which are not exported
2283
inventory_vals = self._prepare_inventory_magento_vals(
2284
cr, uid, product, stock, external_session.sync_from_object, context=context)
2286
external_session.connection.call('oerp_cataloginventory_stock_item.update',
2287
[mag_product_id, inventory_vals])
2289
external_session.logger.info(
2290
"Successfully updated stock level at %s for "
2291
"product with code %s " %
2292
(inventory_vals['qty'], product.default_code))
2295
#TODO change the magento api to be able to change the link direct from the function
2296
# ol_catalog_product.update
2297
def ext_update_link_data(self, cr, uid, external_session, resources, mapping=None, mapping_id=None, context=None):
2298
for resource_id, resource in resources.items():
2299
for type_selection in self.pool.get('product.link').get_link_type_selection(cr, uid, context):
2300
link_type = type_selection[0]
2302
linked_product_ids = []
2303
for link in resource[context['main_lang']].get('product_link', []):
2304
if link['type'] == link_type:
2305
if link['is_active']:
2306
linked_product_ids.append(link['link_product_id'])
2307
position[link['link_product_id']] = link['position']
2308
self.ext_product_assign(cr, uid, external_session, link_type, resource[context['main_lang']]['ext_id'],
2309
linked_product_ids, position=position, context=context)
2312
def ext_product_assign(self, cr, uid, external_session, link_type, ext_parent_id, ext_child_ids,
2313
quantities=None, position=None, context=None):
2314
context = context or {}
2315
position = position or {}
2316
quantities = quantities or {}
2319
#Patch for magento api prototype
2320
#for now the method for goodies is freeproduct
2321
#It will be renammed soon and so this patch will be remove too
2322
if link_type == 'goodies': link_type= 'freeproduct'
2325
magento_args = [link_type, ext_parent_id]
2326
# magento existing children ids
2327
child_list = external_session.connection.call('product_link.list', magento_args)
2328
old_child_ext_ids = [x['product_id'] for x in child_list]
2330
ext_id_to_remove = []
2331
ext_id_to_assign = []
2332
ext_id_to_update = []
2334
# compute the diff between openerp and magento
2335
for c_ext_id in old_child_ext_ids:
2336
if c_ext_id not in ext_child_ids:
2337
ext_id_to_remove.append(c_ext_id)
2338
for c_ext_id in ext_child_ids:
2339
if c_ext_id in old_child_ext_ids:
2340
ext_id_to_update.append(c_ext_id)
2342
ext_id_to_assign.append(c_ext_id)
2344
# calls to magento to delete, create or update the links
2345
for c_ext_id in ext_id_to_remove:
2346
# remove the product links that are no more setup on openerp
2347
external_session.connection.call('product_link.remove', magento_args + [c_ext_id])
2348
external_session.logger.info(("Successfully removed assignment of type %s for"
2349
"product %s to product %s") % (link_type, ext_parent_id, c_ext_id))
2350
for c_ext_id in ext_id_to_assign:
2351
# assign new product links
2352
external_session.connection.call('product_link.assign',
2355
{'position': position.get(c_ext_id, 0),
2356
'qty': quantities.get(c_ext_id, 1)}])
2357
external_session.logger.info(("Successfully assigned product %s to product %s"
2358
"with type %s") %(link_type, ext_parent_id, c_ext_id))
2359
for child_ext_id in ext_id_to_update:
2360
# update products links already assigned
2361
external_session.connection.call('product_link.update',
2364
{'position': position.get(c_ext_id, 0),
2365
'qty': quantities.get(c_ext_id, 1)}])
2366
external_session.logger.info(("Successfully updated assignment of type %s of"
2367
"product %s to product %s") %(link_type, ext_parent_id, c_ext_id))
2370
#TODO move this code (get exportable image) and also some code in product_image.py and sale.py in base_sale_multichannel or in a new module in order to be more generic
2371
def get_exportable_images(self, cr, uid, external_session, ids, context=None):
2372
shop = external_session.sync_from_object
2373
image_obj = self.pool.get('product.images')
2374
images_exportable_ids = image_obj.search(cr, uid, [('product_id', 'in', ids)], context=context)
2375
images_to_update_ids = image_obj.get_all_oeid_from_referential(cr, uid, external_session.referential_id.id, context=None)
2376
images_to_create = [x for x in images_exportable_ids if not x in images_to_update_ids]
2377
if shop.last_images_export_date:
2378
images_to_update_ids = image_obj.search(cr, uid, [('id', 'in', images_to_update_ids), '|', ('create_date', '>', shop.last_images_export_date), ('write_date', '>', shop.last_images_export_date)], context=context)
2379
return {'to_create' : images_to_create, 'to_update' : images_to_update_ids}
2381
def _mag_import_product_links_type(self, cr, uid, product, link_type, external_session, context=None):
2382
if context is None: context = {}
2383
conn = external_session.connection
2384
product_link_obj = self.pool.get('product.link')
2385
selection_link_types = product_link_obj.get_link_type_selection(cr, uid, context)
2386
mag_product_id = self.get_extid(
2387
cr, uid, product.id, external_session.referential_id.id, context=context)
2388
# This method could be completed to import grouped products too, you know, for Magento a product link is as
2389
# well a cross-sell, up-sell, related than the assignment between grouped products
2390
if link_type in [ltype[0] for ltype in selection_link_types]:
2393
product_links = conn.call('product_link.list', [link_type, mag_product_id])
2394
except Exception, e:
2395
self.log(cr, uid, product.id, "Error when retrieving the list of links in Magento for product with reference %s and product id %s !" % (product.default_code, product.id,))
2396
conn.logger.debug("Error when retrieving the list of links in Magento for product with reference %s and product id %s !" % (product.magento_sku, product.id,))
2398
for product_link in product_links:
2399
linked_product_id = self.get_or_create_oeid(
2402
product_link['product_id'],
2405
'product_id': product.id,
2407
'linked_product_id': linked_product_id,
2408
'sequence': product_link['position'],
2411
existing_link = product_link_obj.search(cr, uid,
2412
[('product_id', '=', link_data['product_id']),
2413
('type', '=', link_data['type']),
2414
('linked_product_id', '=', link_data['linked_product_id'])
2417
product_link_obj.write(cr, uid, existing_link, link_data, context=context)
2419
product_link_obj.create(cr, uid, link_data, context=context)
2420
conn.logger.info("Successfully imported product link of type %s on product %s to product %s" %(link_type, product.id, linked_product_id))
2423
def mag_import_product_links_types(self, cr, uid, ids, link_types, external_session, context=None):
2424
if isinstance(ids, (int, long)): ids = [ids]
2425
for product in self.browse(cr, uid, ids, context=context):
2426
for link_type in link_types:
2427
self._mag_import_product_links_type(cr, uid, product, link_type, external_session, context=context)
2430
def mag_import_product_links(self, cr, uid, ids, external_session, context=None):
2431
link_types = self.pool.get('external.referential').get_magento_product_link_types(cr, uid, external_session.referential_id.id, external_session.connection, context=context)
2432
local_cr = pooler.get_db(cr.dbname).cursor()
2434
for product_id in ids:
2435
self.mag_import_product_links_types(local_cr, uid, [product_id], link_types, external_session, context=context)
2442
# transfered from product.py ###############################
2445
class product_images(MagerpModel):
2446
_inherit = "product.images"
2448
'base_image':fields.boolean('Base Image'),
2449
'small_image':fields.boolean('Small Image'),
2450
'thumbnail':fields.boolean('Thumbnail'),
2451
'exclude':fields.boolean('Exclude'),
2452
'position':fields.integer('Position'),
2453
'sync_status':fields.boolean('Sync Status', readonly=True),
2454
'create_date': fields.datetime('Created date', readonly=True),
2455
'write_date': fields.datetime('Updated date', readonly=True),
2458
'sync_status':lambda * a: False,
2459
'base_image':lambda * a:True,
2460
'small_image':lambda * a:True,
2461
'thumbnail':lambda * a:True,
2462
'exclude':lambda * a:False
2465
def get_changed_ids(self, cr, uid, start_date=False):
2466
proxy = self.pool.get('product.images')
2467
domain = start_date and ['|', ('create_date', '>', start_date), ('write_date', '>', start_date)] or []
2468
return proxy.search(cr, uid, domain)
2470
def del_image_name(self, cr, uid, id, context=None):
2471
if context is None: context = {}
2472
image_ext_name_obj = self.pool.get('product.images.external.name')
2473
name_id = image_ext_name_obj.search(cr, uid, [('image_id', '=', id), ('external_referential_id', '=', context['referential_id'])], context=context)
2475
return image_ext_name_obj.unlink(cr, uid, name_id, context=context)
2480
@only_for_referential(ref_categ ='Multichannel Sale')
2481
def _get_last_exported_date(self, cr, uid, external_session, context=None):
2482
shop = external_session.sync_from_object
2483
return shop.last_images_export_date
2485
@only_for_referential(ref_categ ='Multichannel Sale')
2487
def _set_last_exported_date(self, cr, uid, external_session, date, context=None):
2488
shop = external_session.sync_from_object
2489
return self.pool.get('sale.shop').write(cr, uid, shop.id, {'last_images_export_date': date}, context=context)
2493
def update_remote_images(self, cr, uid, external_session, ids, context=None):
2497
ir_model_data_obj = self.pool.get('ir.model.data')
2499
def detect_types(image):
2501
if image.small_image:
2502
types.append('small_image')
2503
if image.base_image:
2504
types.append('image')
2506
types.append('thumbnail')
2509
#TODO update the image file
2510
def update_image(product_extid, image_name, image):
2511
result = external_session.connection.call('catalog_product_attribute_media.update',
2514
{'label':image.name,
2515
'exclude':image.exclude,
2516
'types':detect_types(image),
2521
list_image = self.read(cr, uid, ids, ['write_date', 'create_date'], context=context)
2525
for image in list_image:
2526
if date_2_image.get(image['write_date'] or image['create_date'], False):
2531
if not date_2_image.get((image['write_date'] or image['create_date']) + '-' + str(count), False):
2532
date_2_image[(image['write_date'] or image['create_date']) + '-' + str(count)] = image['id']
2535
date_2_image[image['write_date'] or image['create_date']] = image['id']
2536
image_2_date[image['id']] = image['write_date'] or image['create_date']
2537
list_date = date_2_image.keys()
2540
ids = [date_2_image[date] for date in list_date]
2543
product_images = self.browse_w_order(cr, uid, ids[:1000], context=context)
2544
for each in product_images:
2545
product_extid = each.product_id.get_extid(external_session.referential_id.id)
2546
if not product_extid:
2547
external_session.logger.info("The product %s do not exist on magento" %(each.product_id.default_code))
2549
need_to_be_created = True
2550
ext_file_name = each.get_extid(external_session.referential_id.id)
2551
if ext_file_name: #If update
2553
external_session.logger.info("Updating %s's image: %s" %(each.product_id.default_code, each.name))
2554
result = update_image(product_extid, ext_file_name, each)
2555
external_session.logger.info("%s's image updated with sucess: %s" %(each.product_id.default_code, each.name))
2556
need_to_be_created = False
2557
except Exception, e:
2558
external_session.logger.error(_("Error in connecting:%s") % (e))
2559
if not "Fault 103" in str(e):
2560
external_session.logger.error(_("Unknow error stop export"))
2563
#If the image was deleded in magento, the external name is automatically deleded before trying to re-create the image in magento
2564
model_data_ids = ir_model_data_obj.search(cr, uid, [('model', '=', self._name), ('res_id', '=', each.id), ('referential_id', '=', external_session.referential_id.id)])
2565
if model_data_ids and len(model_data_ids) > 0:
2566
ir_model_data_obj.unlink(cr, uid, model_data_ids, context=context)
2567
external_session.logger.error(_("The image don't exist in magento, try to create it"))
2568
if need_to_be_created:
2569
if each.product_id.default_code:
2572
external_session.logger.info("Sending %s's image: %s" %(each.product_id.default_code, each.name))
2576
'content': each.file,
2577
'mime': each.link and each.url and mimetypes.guess_type(each.url)[0] \
2578
or each.extention and mimetypes.guess_type(each.name + each.extention)[0] \
2582
result = external_session.connection.call('catalog_product_attribute_media.create', [product_extid, data, False, 'id'])
2584
self.create_external_id_vals(cr, uid, each.id, result, external_session.referential_id.id, context=context)
2585
result = update_image(product_extid, result, each)
2586
external_session.logger.info("%s's image send with sucess: %s" %(each.product_id.default_code, each.name))
2589
if context.get('last_images_export_date') and image_2_date[each.id] > context['last_images_export_date']: #indeed if a product was created a long time ago and checked as exportable recently, the write date of the image can be far away in the past
2590
self._set_last_exported_date(cr, uid, external_session, image_2_date[each.id], context=context)
2593
external_session.logger.info("still %s image to export" %len(ids))