############################################################################## # # Copyright (c) 2005 Nexedi SARL and Contributors. All Rights Reserved. # Kevin Deldycke <kevin@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.ERP5SyncML.Conduit.ERP5Conduit import ERP5Conduit from AccessControl import ClassSecurityInfo from Products.ERP5Type import Permissions from Products.ERP5Type.Utils import convertToUpperCase from Products.CMFCore.utils import getToolByName from Acquisition import aq_base, aq_inner, aq_chain, aq_acquire from xml.dom import implementation from xml.dom.ext import PrettyPrint from xml.dom import Node import random import datetime from cStringIO import StringIO from zLOG import LOG class BaobabConduit(ERP5Conduit): global property_map # Declarative security security = ClassSecurityInfo() # This data structure associate a xml property to an ERP5 object property in certain conditions property_map = \ [ { 'xml_property' : 'nom' , 'erp5_property': 'first_name' , 'conditions' : {'erp5_portal_type':'Person'} } , { 'xml_property' : 'nom' , 'erp5_property': 'title' , 'conditions' : {'erp5_portal_type':'Organisation'} } , { 'xml_property' : 'adresse' , 'erp5_property': 'default_address_street_address' , 'conditions' : [{'erp5_portal_type':'Organisation'} ,{'erp5_portal_type':'Person'}] } , { 'xml_property' : 'zone_residence' , 'erp5_property': 'default_address_region' , 'conditions' : [{'erp5_portal_type':'Organisation'} ,{'erp5_portal_type':'Person'}] } , { 'xml_property' : 'titre' , 'erp5_property': 'prefix' , 'conditions' : {'erp5_portal_type':'Person'} } , { 'xml_property' : 'telephone' , 'erp5_property': 'default_telephone_number' , 'conditions' : [{'erp5_portal_type':'Organisation'} ,{'erp5_portal_type':'Person'}] } , { 'xml_property' : 'telex' , 'erp5_property': 'default_fax_number' , 'conditions' : [{'erp5_portal_type':'Organisation'} ,{'erp5_portal_type':'Person'}] } , { 'xml_property' : 'prenom' , 'erp5_property': 'last_name' , 'conditions' : {'erp5_portal_type':'Person'} } , { 'xml_property' : 'date_naissance' , 'erp5_property': 'birthday' , 'conditions' : {'erp5_portal_type':'Person'} } , { 'xml_property' : 'code_bic' , 'erp5_property': 'bic_code' , 'conditions' : {'erp5_portal_type':'Organisation'} } , { 'xml_property' : 'intitule' , 'erp5_property': 'title' , 'conditions' : {'erp5_portal_type':'Bank Account'} } , { 'xml_property' : 'montant_maxi' , 'erp5_property': 'operation_upper_limit' , 'conditions' : {'erp5_portal_type':'Agent Privilege'} } , { 'xml_property' : 'description' , 'erp5_property': 'description' , 'conditions' : {'erp5_portal_type':'Agent Privilege'} } , { 'xml_property' : 'inventory_title' , 'erp5_property': 'title' , 'conditions' : {'erp5_portal_type':'Cash Inventory'} } ] """ Methods below are tools to use the property_map. """ security.declarePrivate('buildConditions') def buildConditions(self, object): """ Build a condition dictionnary """ dict = {} dict['erp5_portal_type'] = object.getPortalType() return dict security.declarePrivate('findPropertyMapItem') def findPropertyMapItem(self, xml_property_name, conditions): """ Find the property_map item that match conditions """ for item in property_map: if item['xml_property'] == xml_property_name: c = item['conditions'] if type(c) == type([]): if conditions in c: return item else: if conditions == c: return item return None security.declareProtected(Permissions.ModifyPortalContent, 'constructContent') def constructContent(self, object, object_id, docid, portal_type): """ This is a redefinition of the original ERP5Conduit.constructContent function to create Baobab objects """ erp5_site_path = object.absolute_url(relative=1) person_module = object.restrictedTraverse(erp5_site_path + '/person') organisation_module = object.restrictedTraverse(erp5_site_path + '/organisation') cash_inventory_module = object.restrictedTraverse(erp5_site_path + '/cash_inventory_module') currency_cash_module = object.restrictedTraverse(erp5_site_path + '/currency_cash_module') subobject = None # Function to search the parent object where the new content must be construct # Given parameter is the special encoded portal type that represent the path to the wanted destination def findObjectFromSpecialPortalType(special_portal_type): source_portal_type = special_portal_type.split('_')[0] construction_location = '/'.join(special_portal_type.split('_')[1:][::-1]) parent_object = None for search_folder in ('person', 'organisation'): path = '/' + search_folder + '/' + construction_location try: parent_object = object.restrictedTraverse(erp5_site_path + path) except: LOG('BaobabConduit:', 100, "parent object of '%s' not found in %s" % (source_portal_type, erp5_site_path + path)) if parent_object == None: LOG('BaobabConduit:', 100, "parent object of '%s' not found !" % (source_portal_type)) else: LOG('BaobabConduit:', 0,"parent object of '%s' found (%s)" % (source_portal_type, repr(parent_object))) return parent_object # handle client objects if portal_type.startswith('Client'): if portal_type[-3:] == 'PER': subobject = person_module.newContent( portal_type = 'Person' , id = object_id ) subobject.setCareerRole('client') else: subobject = organisation_module.newContent( portal_type = 'Organisation' , id = object_id ) subobject.setRole('client') # handle bank account objects elif portal_type.startswith('Compte'): owner = findObjectFromSpecialPortalType(portal_type) if owner == None: return None subobject = owner.newContent( portal_type = 'Bank Account' , id = object_id ) # set the bank account owner as agent with no-limit privileges (only for persons) if owner.getPortalType() == 'Person': new_agent = subobject.newContent( portal_type = 'Agent' , id = 'owner' ) new_agent.setAgent(owner.getRelativeUrl()) privileges = ( 'circularization' , 'cash_out' , 'withdrawal_and_payment' , 'account_document_view' , 'signature' , 'treasury' ) for privilege in privileges: new_priv = new_agent.newContent(portal_type = 'Agent Privilege') new_priv.setAgentPrivilege(privilege) # handle agent objects elif portal_type.startswith('Mandataire'): dest = findObjectFromSpecialPortalType(portal_type) if dest == None: return None subobject = dest.newContent( portal_type = 'Agent' , id = object_id ) # try to get the agent in the person module person = findObjectFromSpecialPortalType('Person_' + object_id) if person == None: person = person_module.newContent( portal_type = 'Person' , id = object_id + 'a' ) subobject.setAgent(person.getRelativeUrl()) # handle privilege objects elif portal_type.startswith('Pouvoir'): dest = findObjectFromSpecialPortalType(portal_type) if dest == None: return None subobject = dest.newContent( portal_type = 'Agent Privilege' , id = object_id ) # handle inventory objects elif portal_type == 'Cash Inventory': if cash_inventory_module == None: return None subobject = cash_inventory_module.newContent( portal_type = 'Cash Inventory' , id = object_id ) # handle inventory details objects elif portal_type == 'Cash Inventory Detail': if currency_cash_module == None: return None ### get currency informations by analizing the id id_items = object_id.split('_') if len(id_items) != 4: LOG('BaobabConduit:', 100, "Cash Inventory Detail object has a wrong id (%s) !" % (object_id)) return None cell_id = id_items[0] resource_type = id_items[1] base_price = float(id_items[2]) currency_name = id_items[3] ### try to find an existing line with the same resource as the current cell # set the portal type of the searched object if resource_type in ['BIL']: currency_portal_type = 'Banknote' elif resource_type in ['MON']: currency_portal_type = 'Coin' else: LOG('BaobabConduit:', 100, "Cash Inventory Detail resource type can't be guess (%s) !" % (resource_type)) return None # get the list of existing currency to find the currency of the line line_currency_cash = None currency_cash_list = currency_cash_module.contentValues(filter={'portal_type': currency_portal_type}) for currency_cash in currency_cash_list: if currency_cash.getBasePrice() == base_price and \ currency_cash.getPriceCurrencyId() == currency_name : line_currency_cash = currency_cash break # no currency found if line_currency_cash == None: LOG('BaobabConduit:', 100, "No currency found for the Cash Inventory Detail !") return None ### search for lines inventory_lines = object.contentValues(filter={'portal_type': 'Cash Inventory Line'}) new_line = None for line in inventory_lines: if line.getResourceValue() == line_currency_cash: new_line = line break # no previous line found, create one if new_line == None: new_line = object.newContent( portal_type = 'Cash Inventory Line' , id = random.randint(10000000, 99999999) ) new_line.setResourceValue(line_currency_cash) new_line.setPrice(line_currency_cash.getBasePrice()) object.setPriceCurrency(line_currency_cash.getPriceCurrency()) # new_line.setVariationBaseCategoryList([ 'cash_status' # , 'emission_letter' # , 'variation' # ]) subobject = new_line return subobject ### EXPERIMENTAL # security.declareProtected(Permissions.ModifyPortalContent, 'getProperty') # def getProperty(self, object, kw): # """ # This is the default getProperty method. This method # can easily be overwritten. # """ # # Try to find a translation rule in the property_map # cond = self.buildConditions(object) # map_item = self.findPropertyMapItem(kw, cond) # if map_item != None: # method_id = "get" + convertToUpperCase(map_item['erp5_property']) # LOG('BaobabConduit:',0,"try to call object method %s on %s" % (repr(method_id), repr(object))) # if v not in ('', None): # if hasattr(object, method_id): # method = getattr(object, method_id) # method(v) # else: # LOG('BaobabConduit:',100,'property map item don\'t match object properties') # return object.getProperty(kw) security.declareProtected(Permissions.ModifyPortalContent, 'editDocument') def editDocument(self, object=None, **kw): """ This function transfer datas from the dictionary to the baobab document object given in parameters """ ### EXPERIMENTAL # This message help to track in the log when an object is edited # It permit us to verify the consistency of a synchronisation process : # 1- Launch a normal synchronisation. # 2- When the syncho process is finished, launch (without reseting, it's important) an other synchronisation, the same way as above. # 3- Monitor the zope log. # 4- If the message log below appear it mean that a client piece of data is missing or wrong comparing to the master. # In other words, it mean that the first synchronisation process didn't ensure the integrity of data. #LOG('BaobabConduit:',0, "An object need to be edited") if object == None: return # Cash Inventory Detail objects needs all properties to create the matrix if object.getPortalType() == 'Cash Inventory Line': category_list = [] quantity = None cell_id = None for k,v in kw.items(): if k == 'quantity': quantity = float(v) if k == 'cell_id' : cell_id = v # Get matrix variation values base_cat_map = { 'variation' : 'variation' , 'letter_code': 'emission_letter' , 'status_code': 'cash_status' } for base_key in base_cat_map.keys(): if base_key in kw.keys() and kw[base_key] not in ('', None): if base_key == 'status_code': status_table = { 'TVA' : 'valid' , 'NEE' : 'new_emitted' , 'NEU' : 'new_not_emitted' , 'RTC' : 'retired' , 'ATR' : 'to_sort' , 'MUT' : 'mutilated' , 'EAV' : 'to_ventilate' , 'TRC' : 'to_sort' , 'ARE' : 'to_sort' } category = status_table[kw[base_key]] else: category = kw[base_key] else: category = 'not_defined' category_list.append(base_cat_map[base_key] + '/' + category) # Update the matrix with this cell self.updateCashInventoryMatrix( line = object , cell_category_list = category_list , quantity = quantity , cell_description = cell_id ) # Set properties of the destination baobab object for k,v in kw.items(): # Try to find a translation rule in the property_map cond = self.buildConditions(object) map_item = self.findPropertyMapItem(k, cond) # No translation rule found, try to find a hard-coded translation method in the conduit if map_item == None: method_id = "edit%s%s" % (kw['type'], convertToUpperCase(k)) LOG('BaobabConduit:', 0, "try to call conduit method %s on %s" % (repr(method_id), repr(object))) if v not in ('', None): if hasattr(self, method_id): method = getattr(self, method_id) method(object, v) else: LOG('BaobabConduit:', 100, "there is no method to handle <%s>%s</%s> data" % (k,repr(v),k)) # There is a translation rule, so call the right setProperty() method else: method_id = "set" + convertToUpperCase(map_item['erp5_property']) LOG('BaobabConduit:', 0, "try to call object method %s on %s" % (repr(method_id), repr(object))) if v not in ('', None): if hasattr(object, method_id): method = getattr(object, method_id) method(v) else: LOG('BaobabConduit:', 100, 'property map item don\'t match object properties') """ All functions below are defined to set a document's property to a value given in parameters. The name of those functions are chosen to help the transfert of datas from a given XML format to standard Baobab objects. """ # Client-related-properties functions def editClientCategorie(self, document, value): if document.getPortalType() == 'Organisation': id_table = { 'BIF': 'institution/world/bank' , 'PFR': 'institution/world/institution' , 'ICU': 'institution/local/common' , 'BET': 'institution/local/institution' , 'ETF': 'institution/local/bank' , 'BTR': 'treasury/national' , 'ORP': 'treasury/other' , 'ORI': 'organism/international' , 'ORR': 'organism/local' , 'COR': 'intermediaries' , 'DIV': 'depositories/various' , 'DER': 'depositories/savings' , 'DAU': 'depositories/other' } document.setActivity('banking_finance/' + id_table[value]) else: LOG('BaobabConduit:', 0, 'Person\'s category ignored') def editClientNatureEconomique(self, document, value): if document.getPortalType() == 'Organisation': # build the economical class category path c = '' path = '' for i in value[1:]: c += i if c == '13': path += '/S13' if value != 'S13': path += '/' + value break path += '/S' + c document.setEconomicalClass(path) else: LOG('BaobabConduit inconsistency:', 200, 'a non-Organisation client can\'t have an economical class') def editClientSituationMatrimoniale(self, document, value): if document.getPortalType() == 'Person': id_table = { 'VEU' : 'widowed' , 'DIV' : 'divorced' , 'MAR' : 'married' , 'CEL' : 'never_married' } document.setMaritalStatus(id_table[value]) else: LOG('BaobabConduit inconsistency:', 200, 'a non-Person client can\'t have a marital status') # BankAccount-related-properties functions def editCompteDevise(self, document, value): document.setPriceCurrency('currency/' + value) def editCompteDateOuverture(self, document, value): if document.getStopDate() in ('', None): document.setStopDate(str(datetime.datetime.max)) document.setStartDate(value) def editCompteDateFermeture(self, document, value): if document.getStartDate() in ('', None): document.setStartDate(str(datetime.datetime.min)) document.setStopDate(value) def editCompteNumero(self, document, value): document.setBankCode(value[0]) document.setBranch(value[1:3]) document.setBankAccountNumber(value) # Agent-related-properties functions def editMandataireNom(self, document, value): old_value = document.getAgentValue().getFirstName() new_value = value if old_value != new_value: LOG('BaobabConduit:', 200, 'old value of agent first name (%s) was replaced by a new one (%s)' % (old_value, new_value)) document.getAgentValue().setFirstName(new_value) def editMandatairePrenom(self, document, value): old_value = document.getAgentValue().getLastName() new_value = value if old_value != new_value: LOG('BaobabConduit:', 200, 'old value of agent last name (%s) was replaced by a new one (%s)' % (old_value, new_value)) document.getAgentValue().setLastName(new_value) def editMandataireService(self, document, value): assignment = document.getAgentValue().newContent( portal_type = 'Assignment' , id = 'service' ) assignment.setGroup(value) return def editMandataireFonction(self, document, value): document.getAgentValue().setDefaultCareerGrade(value) return def editMandataireTelephone(self, document, value): old_value = document.getAgentValue().getDefaultTelephoneNumber() new_value = value if old_value != new_value: LOG('BaobabConduit:', 200, "old value of agent's telephone (%s) was replaced by a new one (%s)" % (old_value, new_value)) document.getAgentValue().setDefaultTelephoneNumber(new_value) def editMandataireDateCreation(self, document, value): if document.getStopDate() in ('', None): document.setStopDate(str(datetime.datetime.max)) document.setStartDate(value) # AgentPrivilege-related-properties functions def editPouvoirCategorie(self, document, value): id_table = { 'COM' : 'clearing' , 'CIR' : 'circularization' , 'REM' : 'cash_out' , 'RET' : 'withdrawal_and_payment' , 'RTE' : 'account_document_view' , 'SIG' : 'signature' , 'TRE' : 'treasury' } document.setAgentPrivilege(id_table[value]) def editPouvoirDateDebut(self, document, value): if document.getStopDate() in ('', None): document.setStopDate(str(datetime.datetime.max)) document.setStartDate(value) def editPouvoirDateFin(self, document, value): if document.getStartDate() in ('', None): document.setStartDate(str(datetime.datetime.min)) document.setStopDate(value) # CashInventory-related-properties functions def editCashInventoryInventoryDate(self, document, value): if value in ('', None): date = str(datetime.datetime.max) else: # Convert french date to strandard date date_items = value.split('/') day = date_items[0] month = date_items[1] year = date_items[2] date = '/'.join([year, month, day]) document.setStopDate(date) # # CashInventoryDetail-related-properties functions # def editCashInventoryDetailVariation(self, document, value): # cell = document # line = document.aq_parent() # LOG('BaobabConduit:', 200, 'Line (%s) & Cell (%s)' % (line, cell)) # year = value # # # update line variation category list # line_category_list = line.getVariationBaseCategoryList() + [year_category] # line.setVariationBaseCategoryList(line_category_list) # # # update cell variation category list # cell_category_list = cell.getCategoryList() + [year_category] # cell.edit( membership_criterion_category_list = category_list # , category_list = category_list # ) # CashInventoryDetail-related-properties functions # def editCashInventoryDetailVaultCode(self, document, value): # line = document # CashInventoryDetail-related-properties functions def updateCashInventoryMatrix(self, line, cell_category_list, quantity, cell_description): # save line current properties value line_category_list = line.getVariationCategoryList() LOG('Kevloggg>>>>> Line before update',0, repr(line.__dict__)) # set the new_line_category_list new_line_category_list = [] for category in cell_category_list + line_category_list: if category not in new_line_category_list: new_line_category_list.append(category) # update the line line.setVariationCategoryList(new_line_category_list) # update the cell range base_id = 'movement' # cell_category_list must have the same base_category order of cell_range base category, so sort the category list ->>>>>> must be verified # base_category_list = line.getVariationBaseCategoryList() # LOG('Kevloggg>>>>>',0, repr(base_category_list)) # base_category_list.sort() # LOG('Kevloggg>>>>>',0, repr(base_category_list)) base_category_list = [ 'cash_status' , 'emission_letter' , 'variation' ] cell_category_list.sort() cell_range = [] new_base_category_list = [] for base_category in base_category_list: base_group = [] for category in new_line_category_list: if category.startswith(base_category + '/') and category not in base_group: base_group.append(category) if len(base_group) > 0: new_base_category_list.append(base_category) cell_range.append(base_group) line.setVariationBaseCategoryList(new_base_category_list) line.setCellRange( base_id = base_id , *cell_range ) # create the cell kwd = { 'base_id' : base_id , 'portal_type': 'Cash Inventory Cell' } new_cell = line.newCell(*cell_category_list, **kwd) new_cell.edit( mapped_value_property_list = ('price', 'inventory') , force_update = 1 , inventory = quantity , membership_criterion_category_list = cell_category_list , category_list = cell_category_list , description = cell_description ) LOG('Kevloggg>>>>> Line after update',0, repr(line.__dict__))