############################################################################## # # Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved. # Herve Poulain <herve@nexedi.com> # Aurelien Calonne <aurel@nexedi.com> # # WARNING: This program as such is intended to be used by professional # programmers who take the whole responsability of assessing all potential # consequences resulting from its eventual inadequacies and bugs # End users who are looking for a ready-to-use solution with commercial # garantees and support are strongly adviced to contract a Free Software # Service Company # # This program is Free Software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################## from Products.ERP5TioSafe.Conduit.TioSafeBaseConduit import TioSafeBaseConduit, \ ADDRESS_TAG_LIST from DateTime import DateTime from lxml import etree from zLOG import LOG, INFO, ERROR from base64 import b16encode, b16decode DEBUG=True class OscommerceERP5NodeConduit(TioSafeBaseConduit): """ This is the conduit use to synchonize ERP5 Persons """ def __init__(self): self.xml_object_tag = 'node' def _createSaleTradeCondition(self, object, **kw): """ Link person to a sale trade condition so that we can filter person based on the plugin they came from """ site = self.getIntegrationSite(kw['domain']) default_stc = site.getSourceTrade() # Create the STC stc = object.getPortalObject().sale_trade_condition_module.newContent(title="%s %s" %(site.getReference(), object.getTitle()), specialise=default_stc, destination_section=object.getRelativeUrl(), destination=object.getRelativeUrl(), destination_decision=object.getRelativeUrl(), destination_administration=object.getRelativeUrl(), version=001) stc.validate() def afterCreateMethod(self, object, **kw): """ This method is for actions that has to be done just after object creation and which required to have synchronization parameters This is an example which create a sale trade condion for each person thus allowing an easy listing of person related to a plugin """ self._createSaleTradeCondition(object, **kw) def afterNewObject(self, object): """ Realise actions after new object creation. """ object.validate() object.updateLocalRolesOnSecurityGroups() def _setRelation(self, document, previous_value, organisation_gid, domain, xml): """ Retrieve the organisation from its gid and do the link """ # first check if there is any conflict LOG('Organisation GID %s'%(b16encode(organisation_gid)), 300, "") LOG('Organisation GID %s'%(b16encode(organisation_gid)), 300, "") LOG('Organisation GID %s'%(b16encode(organisation_gid)), 300, "") synchronization_list = self.getSynchronizationObjectListForType(domain, 'Organisation', 'publication') if previous_value is not None and xml is not None: current_relation = document.getCareerSubordinationValue() if current_relation: for synchronization in synchronization_list: current_value = b16decode(synchronization.getGidFromObject(current_relation)) if current_value: break else: current_value = "" if current_value not in [organisation_gid, previous_value]: return [self._generateConflict(document.getPhysicalPath(), 'relation', xml, current_value, organisation_gid),] # now set the value if organisation_gid is None: document.setCareerSubordinationValue(None) else: for synchronization in synchronization_list: link_object = synchronization.getDocumentFromGid(b16encode(organisation_gid)) if link_object is not None: break if link_object is not None: document.setCareerSubordinationValue(link_object) else: raise ValueError, "Impossible to find organisation %s in %s" %(organisation_gid, synchronization_list) document.reindexObject() return [] def _createContent(self, xml=None, object=None, object_id=None, sub_object=None, reset_local_roles=0, reset_workflow=0, simulate=0, **kw): """ This is the method calling to create an object. """ if DEBUG: LOG("ERP5NodeContuide._createContent", INFO, "xml = %s" %(etree.tostring(xml, pretty_print=True),)) if True: # object_id is not None: sub_object = None if sub_object is None: # If so, it doesn't exist sub_object, reset_local_roles, reset_workflow = self.constructContent( object, object_id, self.getObjectType(xml), ) # if exist namespace retrieve only the tag index = 0 if xml.nsmap not in [None, {}]: index = -1 default_address_created = False # browse the xml phone_list = [] cellphone = None fax_list = [] relation = None category_list = [] role_list =[] relation_data_dict = {} address_tag_mapping = {"street" : "street_address", "zip" : "zip_code", "country" : "region",} for node in xml.getchildren(): # works on tags, no on comments if type(node.tag) is not str: continue tag = node.tag.split('}')[index] # specific for phone if tag == "phone": phone_list.append(node.text) elif tag == "cellphone": cellphone = node.text elif tag == "fax": fax_list.append(node.text) elif tag == "relation": relation = node.text elif tag == "category": if node.text.startswith('role'): role_list.append(node.text[len("role/"):]) else: category_list.append(node.text) elif tag == 'address': # Build dict of address properties address_data_dict = {} for element in node.getchildren(): if type(element.tag) is not str: continue element_tag = element.tag.split('}')[index] address_data_dict[address_tag_mapping.get(element_tag, element_tag)] = element.text # Create the address once we are sure it is well defined if len(address_data_dict): # Define address id if not default_address_created: address_id = "default_address" default_address_created = True else: address_id = None # Create the address object address = sub_object.newContent(portal_type='Address', **address_data_dict) # Rename to default if necessary if address_id is not None: sub_object.activate(activity="SQLQueue", after_method_id="immediateReindexObject", priority=5 ).manage_renameObject(address.getId(), address_id) # Set telephone default_phone_set = False if cellphone is not None: sub_object.edit(mobile_telephone_text=cellphone) for phone in phone_list: if not default_phone_set: sub_object.edit(default_telephone_text=phone) default_phone_set = True else: # Create new subobject sub_object.newContent(portal_type="Telephone", telephone_number=phone) # Set fax default_fax_set = False for fax in fax_list: if not default_fax_set: sub_object.edit(default_fax_text=fax) default_fax_set = True continue # Create new subobject sub_object.newContent(portal_type="Fax", telephone_number=fax) # Link to organisation if relation is not None: self._setRelation(sub_object, None, relation, kw.get('domain'), None) # Set category if len(category_list): sub_object.setCategoryList(category_list) if len(role_list): if sub_object.getPortalType() == "Person": sub_object.edit(career_role_list=role_list) elif sub_object.getPortalType() == "Organisation": sub_object.edit(role_list=role_list) # build the content of the node self.newObject( object=sub_object, xml=xml, simulate=simulate, reset_local_roles=reset_local_roles, reset_workflow=reset_workflow, ) self.afterCreateMethod(sub_object, **kw) return sub_object def _deleteContent(self, object=None, object_id=None): """ We do not delete nodes """ if object[object_id].getValidationState() == "validated": object[object_id].invalidate() def editDocument(self, object=None, **kw): """ This editDocument method allows to set attributes of the object. """ if DEBUG: LOG("ERP5NodeConduit.editDocument", INFO, "object = %s with %s" %(object, kw)) if kw.get('address_mapping') is None: mapping = { 'title': 'title', 'firstname': 'first_name', 'lastname': 'last_name', 'email': 'default_email_text', 'birthday': 'start_date', 'description': 'description', 'phone' : 'default_telephone_text', 'cellphone' : 'mobile_telephone_text', 'fax' : 'default_fax_text', } else: mapping = { 'street': 'street_address', 'zip': 'zip_code', 'city': 'city', 'country': 'region', } # translate kw with the good PropertySheet property = {} for k, v in kw.items(): k = mapping.get(k, k) property[k] = v object._edit(**property) def checkAddressConflict(self, document, tag, xml, previous_value, new_value): """ """ xpath_expression = xml.get('select') try: # work on the case: "/node/address[x]" address_index = int(xpath_expression.split('address[')[-1].split(']')[0]) except ValueError: # Work on the case: "/node/address" address_index = 1 if address_index == 1: address = document.getDefaultAddressValue() else: # the XUPDATE begin by one, so one is default_address and the # first python index list is zero, so x-2 address_index -= 2 # address list of the person without default_address address_list = document.searchFolder( portal_type='Address', sort_on=(['id', 'ASC'],), where_expression='id != "default_address"', ) try: address = address_list[address_index].getObject() except IndexError: return [self._generateConflict(document.getPhysicalPath(), tag, xml, None, new_value),] # getter used to retrieve the current values and to check conflicts getter_value_dict = { 'street': address.getStreetAddress(), 'zip': address.getZipCode(), 'city': address.getCity(), 'country': address.getRegion(), } # create and fill a conflict when the integration site value, the erp5 # value and the previous value are differents current_value = getter_value_dict[tag].decode('utf-8') if current_value not in [new_value, previous_value]: return [self._generateConflict(document.getPhysicalPath(), tag, xml, current_value, new_value),] else: keyword = {'address_mapping': True, tag: new_value} self.editDocument(object=address, **keyword) return [] def _updateXupdateUpdate(self, document=None, xml=None, previous_xml=None, **kw): """ This method is called in updateNode and allows to work on the update of elements. """ if DEBUG: LOG("ERP5NodeConduit._updateXupdateUpdate", INFO, "doc = %s, xml = %s" %(document.getPath(), etree.tostring(xml, pretty_print=1),)) xpath_expression = xml.get('select') tag = xpath_expression.split('/')[-1] new_value = xml.text keyword = {} # retrieve the previous xml etree through xpath selected_previous_xml = previous_xml.xpath(xpath_expression) try: previous_value = selected_previous_xml[0].text except IndexError: previous_value = None # check if it'a work on person or on address if tag in ADDRESS_TAG_LIST: return self.checkAddressConflict(document, tag, xml, previous_value, new_value) else: return self.checkConflict(tag, document, previous_value, new_value, kw.get('domain'), xml) return [] def _updateXupdateDel(self, document=None, xml=None, previous_xml=None, **kw): """ This method is called in updateNode and allows to remove elements. """ if DEBUG: LOG("ERP5NodeConduit._updateXupdateDel", INFO, "doc = %s, xml = %s" %(document.getPath(), etree.tostring(xml, pretty_print=1),)) conflict_list = [] tag = xml.get('select').split('/')[-1] # this variable is used to retrieve the id of address and to not remove the # orginal tag (address, street, zip, city or country) keyword = {} # retrieve the previous xml etree through xpath xpath_expression = xml.get('select') selected_previous_xml = previous_xml.xpath(xpath_expression) try: previous_value = selected_previous_xml[0].text except IndexError: previous_value = None # specific work for address and address elements address_tag = tag.split('[')[0] if address_tag == "address": try: # work on the case: "/node/address[x]" address_index = int(tag.split('[')[-1].split(']')[0]) except ValueError: # Work on the case: "/node/address" address_index = 1 if address_index == 1: address_id = "default_address" else: # the XUPDATE begin by one, so one is default_address and the # first python index list is zero, so x-2 address_index -= 2 # address list of the person without default_address address_list = document.searchFolder( portal_type='Address', sort_on=(['id', 'ASC'], ), where_expression='id != "default_address"', ) address_id = address_list[address_index].getId() try: document.manage_delObjects(address_id) except IndexError: conflict_list.append(self._generateConflict(document.getPhysicalPath(), tag, xml, None, None)) return conflict_list elif address_tag in ADDRESS_TAG_LIST: return self.checkAddressConflict(document, address_tag, xml, previous_value, None) else: return self.checkConflict(tag, document, previous_value, None, kw.get('domain'), xml) return conflict_list def _updateXupdateInsertOrAdd(self, document=None, xml=None, previous_xml=None, **kw): """ This method is called in updateNode and allows to add elements. """ if DEBUG: LOG("ERP5NodeConduit._updateXupdateInsertOrAdd", INFO, "doc = %s, xml = %s" %(document.getPath(), etree.tostring(xml, pretty_print=1),)) conflict_list = [] keyword = {} default_address_created = False previous_value = "" for subnode in xml.getchildren(): tag = subnode.attrib['name'] new_value = subnode.text if tag == 'address': address = document.newContent(portal_type='Address', ) keyword['address_mapping'] = True for subsubnode in subnode.getchildren(): keyword[subsubnode.tag] = subsubnode.text self.editDocument(object=address, **keyword) if getattr(document, "default_address", None) is None and not default_address_created: # This will become the default address default_address_created = True document.activate(activity="SQLQueue", after_method_id="immediateReindexObject", priority=5 ).manage_renameObject(address.getId(), "default_address") elif tag in ADDRESS_TAG_LIST: return self.checkAddressConflict(document, tag, xml, previous_value, new_value) else: return self.checkConflict(tag, document, previous_value, new_value, kw.get('domain'), xml) return conflict_list def checkConflict(self, tag, document, previous_value, new_value, domain, xml): """ Check conflict for each tag """ if tag == "relation": return self._setRelation(document, previous_value, new_value, domain, xml) else: if tag == "phone": current_value = document.get('default_telephone', None) and \ document.default_telephone.getTelephoneNumber("") elif tag == "cellphone": current_value = document.get('mobile_telephone', None) and \ document.mobile_telephone.getTelephoneNumber("") elif tag == "fax": current_value = document.get('default_fax', None) and \ document.default_fax.getTelephoneNumber("") elif tag == "birthday": current_value = str(document.getStartDate("")) elif tag == "email": current_value = str(document.getDefaultEmailText("")) else: current_value = getattr(document, tag) if current_value not in [new_value, previous_value, None]: LOG("ERP5NodeConduit.checkConflict", ERROR, "Generating a conflict for tag %s, current is %s, previous is %s, new is %s" %(tag, current_value, new_value, previous_value)) return [self._generateConflict(document.getPhysicalPath(), tag, xml, current_value, new_value),] else: if new_value is None: # We are deleting some properties if tag == "fax": document.manage_delObjects("default_fax") elif tag == "phone": document.manage_delObjects("default_telephone") elif tag == "cellphone": document.manage_delObjects("mobile_telephone") elif tag == "email": document.manage_delObjects("default_email") else: kw = {tag : new_value} self.editDocument(object=document, **kw) else: if tag == 'birthday': new_value = DateTime(new_value) kw = {tag : new_value} self.editDocument(object=document, **kw) return []