############################################################################## # -*- coding: utf-8 -*- # # Copyright (c) 2002 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.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 from cStringIO import StringIO from zLOG import LOG class ERP5ShopOrderConduit(ERP5Conduit): """ This conduit is used in the synchronisation process of Storever and ERP5 to convert a Storever Shop Order to a ERP5 Sale Order. Don't forget to add this base categories in portal_category : 'hd_size', 'memory_size', 'optical_drive', 'keyboard_layout', 'cpu_type' """ # TODO: tester ce script sur le serveur de backup (qui semble ĂȘtre different) # Declarative security security = ClassSecurityInfo() # Initialize the random function random.seed() 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 allow the creation of a ERP5 Sale Order instead of a Storever Shop Order. """ portal_types = getToolByName(object, 'portal_types') subobject = None new_object_id = object_id if portal_type == 'Shop Order': # The random part of the id can be removed. It's only used for the developpement #new_object_id = 'storever-' + object_id + '-' + str(random.randint(1000, 9999)) #new_object_id = 'storever-' + object_id subobject = object.newContent( portal_type = 'Sale Order' , id = new_object_id) # And we must set the destination and destination_section to Nexedi nexedi = object.getPortalObject().organisation.nexedi subobject.setSourceValue(nexedi) subobject.setSourceSectionValue(nexedi) if portal_type == 'Order Line': last_line_num = self.getLastOrderLineNumber(object) new_object_id = "storever-" + str(last_line_num + 1) + "-" + object_id subobject = object.newContent( portal_type = 'Sale Order Line' , id = new_object_id) return subobject # # Not needed yet # security.declareProtected(Permissions.ModifyPortalContent, 'addWorkflowNode') # def addWorkflowNode(self, object, xml, simulate): # """ # This is a redefinition of the original ERP5Conduit.addWorkflowNode function to # allow the translation of a Storever Shop Order workflow to a ERP5 Sale Order one. # """ # conflict_list = [] # status = self.getStatusFromXml(xml) # # if status['action'] == 'ship': # # status['time'] # return conflict_list security.declarePrivate('dom2str') def dom2str(self, xml_root=None): """ This function transform a DOM tree to string. This function is only usefull for debugging. """ xml_str = StringIO() PrettyPrint(xml_root, xml_str) xml_string = xml_str.getvalue() LOG('XML output: ', 0, xml_string) return xml_string security.declarePrivate('str2id') def str2id(self, string=None): """ This function transform a string to a safe id. It is also used here to create a safe category id from a string. """ out = '' if string == None: return None string = string.lower() # We ignore the discontinued information to allow the use of the same category # even if the option is discontinued on the storever side string = string.replace('discontinued', '') string = string.strip() # # TODO: manage accent for char in string: if char == '_' or char.isalnum(): pass elif char.isspace() or char in ('+', '-'): char = '_' else: char = None if char != None: out += char # LOG('Category name output (using str2id) >>>>>>> ', 0, out) out = out.strip('_') return out security.declarePrivate('countrySearch') def countrySearch(self, site_root, category_path=None, country=None): """ This recursive function try to find the region category from the name of a country """ if country.lower()=='suisse': country='switzerland' if country.lower()=='united kingdom': country='uk' if country.lower()=='united states of america': country='usa' if category_path == None: portal_categories = getToolByName(site_root, 'portal_categories') categories_path = portal_categories.absolute_url(relative=1) category_path = categories_path + '/region' region_folder = site_root.restrictedTraverse(category_path) for region_id in region_folder.objectIds(): region_path = category_path + '/' + region_id splitted_path = region_path.split("/") cat_region = "/".join(splitted_path[3:]) if region_id.lower() == country.lower(): return cat_region find_path = self.countrySearch(site_root, region_path, country) if find_path != None: return find_path return None security.declarePrivate('createOrFindProduct') def createOrFindProduct(self, erp5_site, erp5_product_id): """ This function try to find a previous product with the same id, and create it if the search is unsuccessful """ erp5_site_path = erp5_site.absolute_url(relative=1) product_path = erp5_site_path + '/product' product_folder = erp5_site.restrictedTraverse(product_path) product = None # Try to find a previous product for product_id in product_folder.objectIds(): if product_id == erp5_product_id: product = erp5_site.restrictedTraverse(erp5_site_path + '/product/' + erp5_product_id) # We have to create a new product if product is None: product = product_folder.newContent( portal_type = 'Product' , id = erp5_product_id) if len(product.getProductLineValueList())==0: #storever_product_line = erp5_site.portal_categories.product_line.storever product.setProductLine('storever') return product security.declarePrivate('setProductWorkflow') def setProductWorkflow(self, product_object, product_title): """ This function set the validation workflow to indicate if a product is discontinued (workflow state = invalidate) or not (workflow state = validate) """ #!!!!!!!!!!!!!!!!!!!!!!!!!! # return #!!!!!!!!!!!!!!!!!!!!!!!!!! action = None if hasattr(product_object, 'workflow_history'): LOG('Info needed from portal_workflow >>>>>>>>> ', 0, '') workflow_state = product_object.portal_workflow.getInfoFor(product_object, 'validation_state') LOG('workflow_state is >>>>>>>>> ', 0, repr(workflow_state)) if product_title.lower().find('discontinued') != -1: #if workflow_state != 'invalidated': # action = 'invalidate_action' if workflow_state == 'draft': LOG('workflow_state we will validate ', 0, repr(workflow_state)) #product_object.portal_workflow.doActionFor( product_object # , 'validate_action' # , wf_id = 'validation_workflow') product_object.validate() new_workflow_state = product_object.portal_workflow.getInfoFor(product_object, 'validation_state') LOG('workflow_state we will new_workflow_state ', 0, repr(new_workflow_state)) product_object.invalidate() elif workflow_state in ('draft', 'invalidated'): #action = 'validate_action' product_object.validate() LOG('action is >>>>>>>>> ', 0, repr(action)) LOG('product_object.portal_type is >>>>>>>>> ', 0, product_object.getPortalType()) LOG('productobject.getPhysicalPath is >>>>>>>>> ', 0, product_object.getPhysicalPath()) LOG('productobject.title is >>>>>>>>> ', 0, product_object.getTitle()) LOG('productobject. product_title is >>>>>>>>> ', 0, product_title) #if action != None: # if action == # product_object.invalidate() #product_object.portal_workflow.doActionFor( product_object # , action # , wf_id = 'validation_workflow') LOG('end of workflow action >>>>>>>>> ', 0, repr(action)) security.declarePrivate('niceTitle') def niceTitle(self, title): """ This function create a nice title without the discontinued information """ splitted_title = title.strip().split(" ") nice_title = '' for string in splitted_title: if string.lower().find('discontinued') == -1: nice_title += string + ' ' return nice_title.strip() security.declarePrivate('getLastOrderLineNumber') def getLastOrderLineNumber(self, order_object): """ This function give the number of the last Storever Shop Order Line processed """ # Scan existing order line id to get the last order line number maximum_order_num = 0 LOG('order_object.objectIds',0,order_object.objectIds()) LOG('order_object.objectIds',0,[x for x in order_object.objectIds()]) for order_line_id in order_object.objectIds(): splitted_line_id = order_line_id.split("-") current_line_num = int(splitted_line_id[1]) if current_line_num > maximum_order_num: maximum_order_num = current_line_num LOG('getLastOrderLineNumber return >>>>>>>> ', 0, repr(maximum_order_num)) return int(maximum_order_num) # # Not needed yet because we prefer using my own owner_account_id property # security.declareProtected(Permissions.ModifyPortalContent, 'addLocalRoleNode') # def addLocalRoleNode(self, object, xml): # """ # """ # conflict_list = [] # LOG('object >>>>>>>> ', 0, object) # LOG('xml >>>>>>>> ', 0, self.dom2str(xml)) # return conflict_list security.declarePrivate('updateObjProperty') def updateObjProperty(self, object, property, kw, key): """ This function update the property of an object with a given value stored in a dictionnary. This function help the Conduit to make decision about the synchronisation of values. Example of call : self.updateObjProperty(person_object, 'DefaultAddressStreetAddress', kw, 'address') Solution (d'apres seb) : * machin = getattr (object, methos) * machin() """ if kw.has_key(key): new_value = kw[key] if new_value != None: if type(new_value) is type('s'): new_value = new_value.title() current_value = eval('object.get' + property + '()') LOG("I have to run this >>>>>>>> ", 0, 'object.get' + property + '()') LOG("current_value >>>>>>>> ", 0, repr(current_value)) # The current property value is not consistent if current_value == None or len(current_value) == 0: # Erase the current value with the new one LOG("I have to run this to set the property >>>>>>>> " + 'object.set' + str(property) + '(' + str(new_value) + ')' + str(current_value), 0, '') # A previous consistent value exist elif current_value.strip().lower() != new_value.strip().lower(): # TODO : We need to choose if we replace it or not, or mix the current with the new one LOG('We have to make the fusion of previous address with the current one >>>>>>>', 0, '') return False return True return False security.declareProtected(Permissions.ModifyPortalContent, 'editDocument') def editDocument(self, object=None, **kw): """ This function use the properties of the object to convert a Storever ShopOrder to an ERP5 SaleOrder. """ if object == None: return LOG('KW >>>>>>>> ', 0, kw) # This list contain a list of object to check to know if their workflow need to be mofified # We store these objects into a list and we will apply modification at the end to avoid mysql lock problem workflow_joblist = [] # Get the ERP5 root object portal_types = getToolByName(object, 'portal_types') erp5_site = portal_types.getPortalObject() erp5_site_path = erp5_site.absolute_url(relative=1) # The object is a ShopOrder if kw.has_key('country'): object.setStartDate(kw['target_start_date']) object.setStopDate(kw['target_stop_date']) # Find the organisation and the person folder person_path = erp5_site_path + '/person' person_folder = erp5_site.restrictedTraverse(person_path) organisation_path = erp5_site_path + '/organisation' org_folder = erp5_site.restrictedTraverse(organisation_path) # Find the service folder service_path = erp5_site_path + '/service' service_folder = erp5_site.restrictedTraverse(service_path) # # TODO : if storever-id exist dans ERP5 --> prendre en charge l'update de la facture # Get the id of the owner account in storever owner_account_id = kw['owner_account_id'] # Set the id of the owner in ERP5 (the owner could be an Organisation or a Person) owner_id = "storever-" + owner_account_id # Try to find the identity created for a previous ShopOrder of the same Storever member account person_object = None org_object = None for person_id in person_folder.objectIds(): if person_id == owner_id: person_object = erp5_site.restrictedTraverse(erp5_site_path + '/person/' + person_id) LOG("Previous person found ! >>>>>>>>",0,repr(person_object)) break for organisation_id in org_folder.objectIds(): if organisation_id == owner_id: org_object = erp5_site.restrictedTraverse(erp5_site_path + '/organisation/' + organisation_id) LOG("Previous organisation found ! >>>>>>>>",0,repr(org_object)) break # Define the previous customer structure previous_owner_type = '' if person_object != None: previous_owner_type = 'p' if org_object != None: previous_owner_type = 'o' # Organisation is more important than the person # This is a particular case where the user put # the name of an organisation in his own name if not kw.has_key('organisation'): kw['organisation'] = org_object.getId() #if len(previous_owner_type) == 0: # previous_owner_type = None LOG("Previous customer structure >>>>>>>>",0,repr(previous_owner_type)) # Try to know the type of the current storever customer owner_type = '' if kw.has_key('name') and kw['name'] not in (None, ''): owner_type += 'p' if kw.has_key('organisation') and kw['organisation'] not in (None, '', 'none'): owner_type += 'o' if kw.has_key('eu_vat') and kw['eu_vat'] not in (None, '') and owner_type.find('o') == -1: owner_type += 'o' if len(owner_type) == 0: owner_type = None LOG("Current customer structure >>>>>>>>",0,repr(owner_type)) # # TODO : in this part of the script, add the possibility to find an existing # # ERP5 person/organisation according to the name of that person/organisation # Compare the current representation of the member account with the previous one #if previous_owner_type != owner_type: # XXX Seb: I guess it's quite strange to compare "po" to "poo" # There is probably an error here, so I changed it but # I'm sure I'm not really doing what it was intended for if previous_owner_type is None and owner_type is not None: # There is difference between the two (previous and current) representation of the customer # We have to manage the differences to create a unique customer representation LOG("There is difference between previous and current >>>>>>>>",0,'') if previous_owner_type == None: # No previous customer found, create one if owner_type.find('o') != -1: org_object = org_folder.newContent( portal_type = 'Organisation' , id = owner_id) LOG("new organisation created >>>>>>>>",0,repr(org_object)) if owner_type.find('p') != -1: person_object = person_folder.newContent( portal_type = 'Person' , id = owner_id) LOG("new person created >>>>>>>>",0,repr(person_object)) else: if owner_type == None: # Use the previous Structure owner_type = previous_owner_type LOG("Use the previous Structure >>>>>>>>",0,'') else: LOG("We have to convert the structure >>>>>>>>",0,'') # # XXX Be aware of that problem: the invoice for a sale order must look the same (I mean when we generate the pdf version) # Case to process : # previous current # o --> p # o --> op # p --> o # op --> o # op --> p # p --> op - in progress # The previous customer was detected as a person only if previous_owner_type.find('p') != -1 and previous_owner_type.find('o') == -1: # Case to process : # previous current # p --> o # p --> op - in progress # The customer has now an organisation, we have to create this organisation and link the person to if owner_type.find('p') != -1 and owner_type.find('o') != -1: # Create a new organisation # # TODO : factorise this code with the same above org_object = org_folder.newContent( portal_type = 'Organisation' , id = owner_id) else: # # TODO : Transform a person to an organisation ? Is it a good idea ? pass # The previous customer was detected as an organisation only elif previous_owner_type.find('p') == -1 and previous_owner_type.find('o') != -1: # Case to process : # previous current # o --> p # o --> op pass # The previous customer was detected as a person in an organisation else: # Case to process : # previous current # op --> o # op --> p pass else: if owner_type == None: # There is not enough informations to know if the customer is an organisation or # a person and there is no previous record # By default, we consider the customer as a person, so we have to force to create one owner_type = 'p' person_object = person_folder.newContent( portal_type = 'Person' , id = owner_id) LOG("Create a person by default >>>>>>>>",0,repr(person_object)) else: # The structure is the same # We only need to be aware of data fusion between the previous and the current representation # So we don't need to do something because the information fusion process take place below LOG("The structure is the same. don't do anything >>>>>>>>",0,'') pass LOG("Person object >>>>>>>>",0,repr(person_object)) LOG("Organisation object >>>>>>>>",0,repr(org_object)) # Copy informations related to the customer in the ERP5 representation of the customer # Be carefull because all informations from the storever ShopOrder are optionnals if owner_type.find('p') != -1: # Link the customer with the Sale Order object.setDestination("person/" + owner_id) object.setDestinationSection("person/" + owner_id) # # TODO : do the same things for each single information # # TODO : before doing something working well in every case, copy the previou value in the comment field to traceback the modification and let me evaluate the solidity of my algorithm # # TODO : perhaps it's possible to factorize the code using a generic function # Synchronise the street address # Solution (d'apres seb) # machin = getattr (object, methos) # method(machin) if person_object is None: person_object = person_folder.newContent( portal_type = 'Person' , id = owner_id) LOG("new person created >>>>>>>>",0,repr(org_object)) #machin = self.updateObjProperty(person_object, 'DefaultAddressStreetAddress', kw, 'address') #LOG("My new updateObjProperty() return >>>>>>>>",0,repr(machin)) # if kw.has_key('address') and kw['address'] != None: # previous_address = person_object.getDefaultAddressStreetAddress() # if len(previous_address) == 0: # person_object.setDefaultAddressStreetAddress(kw['address'].title()) # elif previous_address.strip().lower() != kw['address'].strip().lower(): # LOG('We have to make the fusion of previous address with the current one >>>>>>>', 0, '') if kw.has_key('city') and kw['city']!=None: person_object.setDefaultAddressCity(kw['city'].title()) if kw.has_key('address') and kw['address'] != None: person_object.setDefaultAddressStreetAddress(kw['address'].title()) if kw.has_key('zipcode') and kw['zipcode']!=None: person_object.setDefaultAddressZipCode(kw['zipcode']) # # TODO : set the person products interest (storever, etc) # Search the country in the region category if kw['country'] != None: region_path = self.countrySearch(erp5_site, None, kw['country']) if region_path != None: person_object.setDefaultAddressRegion(region_path) # else: # # TODO : Ask the user to select an appropriate region if kw.has_key('email') and kw['email'] != None: person_object.setDefaultEmailText(kw['email']) if kw.has_key('phone') and kw['phone'] != None: person_object.setDefaultTelephoneText(kw['phone']) # # TODO : Don't work # person_object.setDefaultCareerRole("client") # Split the name to give at least a required LastName # Then the title will be automaticaly created by the Person object from this data if kw.has_key('name') and kw['name'] != None: splitted_name = kw['name'].strip().split(" ") person_object.setLastName((splitted_name[-1]).title()) if len(splitted_name) > 1: person_object.setFirstName((" ".join(splitted_name[:-1])).title()) else: # We have to find a title to have something to show in the RelationField of the SaleOrderForm person_object.setTitle(owner_account_id.title()) # The Person is subordinated to an Organisation ? if owner_type.find('o') != -1 and previous_owner_type =='o': # # TODO : fix this # person_object.setSubordination("organisation/" + owner_id) if kw.has_key('organisation') and kw['organisation'] != None: org_object.setTitle(kw['organisation'].title()) org_object.setCorporateName(kw['organisation'].title()) if kw.has_key('eu_vat') and kw['eu_vat'] != None: org_object.setVatCode(kw['eu_vat']) # Test for debug if (not (kw.has_key('organisation')) or (kw.has_key('organisation') and kw['organisation'] != None)) and (not (kw.has_key('eu_vat')) or (kw.has_key('eu_vat') and kw['eu_vat'] != None)): LOG("AARRGG ! Big conflict detected : this organisation has no title or eu_vat. These properties are primary key to deduced that the storever member account was an organisation >>>>>>>>>>", 0, '') org_object.setRole("client") # The customer is not a person or a person of an organisation, so the customer is an organisation... # XXX Seb: So like it was defined, if we have a person from an organisation, then # the organisation is not modified, so the vat is not defined!! # This is good to replace the person with an organisation, because vat is only # defined on organisation. An update would be to define when we have both organisation # and person the destination_administration XXX if owner_type.find('o') != -1: if org_object is None: org_object = org_folder.newContent( portal_type = 'Organisation' , id = owner_id) LOG("new organisation created >>>>>>>>",0,repr(org_object)) # Link the customer with the Sale Order object.setDestination("organisation/" + owner_id) object.setDestinationSection("organisation/" + owner_id) # All informations describe the organisation if kw.has_key('organisation') and kw['organisation'] != None: org_object.setTitle(kw['organisation'].title()) org_object.setCorporateName(kw['organisation'].title()) org_object.setRole("client") if kw.has_key('eu_vat') and kw['eu_vat'] != None: org_object.setVatCode(kw['eu_vat']) if kw.has_key('address') and kw['address'] != None: org_object.setDefaultAddressStreetAddress(kw['address'].title()) if kw.has_key('city') and kw['city'] != None: org_object.setDefaultAddressCity(kw['city'].title()) if kw.has_key('zipcode') and kw['zipcode'] != None: org_object.setDefaultAddressZipCode(kw['zipcode']) # Search the country in the region category if kw['country'] != None: region_path = self.countrySearch(erp5_site, None, kw['country']) if region_path != None: org_object.setDefaultAddressRegion(region_path) # else: # # TODO : Ask the user to select an appropriate region if kw.has_key('email') and kw['email'] != None: org_object.setDefaultEmailText(kw['email']) if kw.has_key('phone') and kw['phone'] != None: org_object.setDefaultTelephoneText(kw['phone']) # Save the billing address in the description, because there is no dedicated place for it if kw.has_key('billing_address') and len(kw['billing_address']) > 0: object.setDescription("Send the bill to : " + kw['billing_address']) # Set the Title because its required object.setTitle("Storever Order " + str(kw['order_id'])) # # ONLY for information (will be used in the future) object.setDescription(str(object.getDescription()) + "\n\nTotal Price (with transport fees) :" + str(kw['total_price'])) # Add a new orderLine for the shipment stor_ship_title = kw['send_fee_title'].strip() erp5_ship_title = stor_ship_title + ' Shipment' my_shipment_id = 'storever-' + self.str2id(stor_ship_title) # Try to find an existing shipment service using several methods shipment_id = None for service_id in service_folder.objectIds(): service_object = erp5_site.restrictedTraverse(erp5_site_path + '/service/' + service_id) # First method: compare the id with my standard layout if service_id.strip() == my_shipment_id: shipment_id = my_shipment_id LOG("Service found with method 1 ! >>>>>>>>", 0, repr(shipment_id)) break # Second method: use a standard title layout if service_object.getTitle().lower().strip() == erp5_ship_title.lower().strip(): shipment_id = service_id LOG("Service found with method 2 ! >>>>>>>>", 0, repr(shipment_id)) break # Third method: compare words erp5_ship_id_word_list = self.str2id(service_id).split("_") stor_ship_id_word_list = self.str2id(stor_ship_title).split("_") erp5_ship_title_word_list = self.str2id(erp5_ship_title).split("_") erp5_ship_id_word_list.sort(key=lambda x: str(x)) stor_ship_id_word_list.sort(key=lambda x: str(x)) erp5_ship_title_word_list.sort(key=lambda x: str(x)) if stor_ship_id_word_list in (erp5_ship_id_word_list, erp5_ship_title_word_list): shipment_id = service_id LOG("Service found with method 3 ! >>>>>>>>", 0, repr(shipment_id)) break # No previous shipment service found, so create a new one if shipment_id == None: # TODO : implement the code here to follow the comment in the LOG below LOG("We have to create the shipping service with this id >>>>>>>>", 0, repr(my_shipment_id)) # Create a new shipment service shipment_id = my_shipment_id # Get the object of the shipment service # shipment_path = erp5_site_path + '/service/' + shipment_id # shipment_object = erp5_site.restrictedTraverse(shipment_path) # Create a new order line in this order to represent the shipment service last_line_num = self.getLastOrderLineNumber(object) ship_order_line_id = "storever-" + str(last_line_num +1 ) # XXX This may fail. # It is possible to already have # a line with this id LOG('ERP5ShopOrderConduit, object',0,object.getPath()) LOG('ERP5ShopOrderConduit, objectIds',0,[x for x in object.objectIds()]) LOG('ERP5ShopOrderConduit, will create ship_order_line_id',0,ship_order_line_id) ship_order_object = object.newContent( portal_type = 'Sale Order Line' , id = ship_order_line_id) # Don't give id, it will be set # automatically. ship_order_object.setQuantity(1.0) ship_order_object.setPrice(kw['send_fee']) ship_order_object.setQuantityUnit('unit') ship_order_object.setResource("service/" + shipment_id) # The object is an OrderLine else: # Find the product folder product_path = erp5_site_path + '/product' product_folder = erp5_site.restrictedTraverse(product_path) # Find the parent order object parent_order_object = object.aq_parent # Get the id of the product in storever storever_product_id = kw['product_id'] # Set the id of the product in ERP5 erp5_product_id = "storever-" + storever_product_id # Try to find a previous product or create a new one product_object = self.createOrFindProduct(erp5_site, erp5_product_id) # Create a nice title (without discontinued) from the product title product_title = self.niceTitle(kw['product_title']) # Synchronise every data product_object.setDescription(kw['product_description']) product_object.setTitle(product_title) # # TODO : I don't know where to put this value, # # because there is no "delivery days"-like property for a product # product_object.setDeliveryDays(kw['product_delivery_days']) if kw['product_expiration_date'] != None: product_object.setSourceBasePriceValidity(kw['product_expiration_date']) product_object.setBasePrice(kw['product_price']) product_object.setQuantityUnit('unit') # Save the worflow status for later modification workflow_joblist.append((product_object, kw['product_title'])) # In storever, every option are set as string in the title of the OrderLine # This part of code create a list of all options choosen by the customer for this product splitted_title = kw['title'].strip().split(":") option_list = (":".join(splitted_title[1:])).split("/") LOG('Customer option list >>>>>> ', 0, repr(option_list)) # Now, we will find the price of each option option_classes = [ kw['product_disk_price'] , kw['product_memory_price'] , kw['product_option_price'] , kw['product_processor_price'] ] priced_list = {} for option_item in option_list: option = option_item.strip() for option_class in option_classes: for option_key in option_class.keys(): if option == option_key.strip(): priced_list[option] = option_class[option_key] # # TODO : there is no default options in the final priced_list. Is the option 'default' important ? LOG('Customer option priced list >>>>>>>>> ', 0, repr(priced_list)) # In ERP5, we have decided to represent some options as variation of a product # and some options as new order line of product # Now we will update or create the variation categories related to the initial product # Don't forget to add this base categories in portal_category : # 'hd_size', 'memory_size', 'optical_drive', 'keyboard_layout', 'cpu_type' portal_cat = product_object.portal_categories # Get all keyboard related options and all optical drive related options keyboard_options = {} optical_options = {} options_prices = kw['product_option_price'] for option_key in options_prices.keys(): if option_key.lower().find("keyboard") != -1: keyboard_options[option_key] = options_prices[option_key] elif option_key.lower().find("cd") != -1 or option_key.lower().find("dvd") != -1: optical_options[option_key] = options_prices[option_key] LOG('Product keyboard layout priced list >>>>>>>>> ', 0, repr(keyboard_options)) LOG('Product optical drive priced list >>>>>>>>> ', 0, repr(optical_options)) # Create a data structure containing all allowed variations variant_category_list = [ ('hd_size', kw['product_disk_price']) , ('memory_size', kw['product_memory_price']) , ('cpu_type', kw['product_processor_price']) , ('optical_drive', optical_options) , ('keyboard_layout', keyboard_options)] # Create or update every category representing all variantions base_cat_list = [] cat_list = [] for (cat_base, cat_data) in variant_category_list: if len(cat_data) > 0 and portal_cat.resolveCategory(cat_base) != None: base_cat_list.append(cat_base) for disk_variant_key in cat_data.keys(): cat_id = self.str2id(disk_variant_key) cat_path = cat_base + '/' + cat_id cat_list.append(cat_path) if portal_cat.resolveCategory(cat_path) == None: cat_base_object = portal_cat._getOb(cat_base) cat_base_object.newContent ( portal_type = 'Category' , id = cat_id) LOG("New created category >>>>>>>>>>> ", 0, cat_path) # Set the base variation of the product product_object.setVariationBaseCategoryList(base_cat_list) # Set the variation range of the product product_object.setVariationCategoryList(cat_list) LOG("cat_list >>>>>>>>>>", 0, repr(cat_list)) # Now we seperate options and variations of the initial product ordered by the customer customer_product_option_list = {} customer_product_variation_list = {} customer_product_base_variation_list = [] for option in priced_list: option_is_variant = None for (cat_base, cat_data) in variant_category_list: LOG('editDocument, cat_base',0,cat_base) base_cat_object = portal_cat.resolveCategory(cat_base) cat_list = base_cat_object.getCategoryChildIdItemList() for (category, category_bis) in cat_list: if self.str2id(option) == category: customer_product_variation_list[category] = cat_base + '/' + category if cat_base not in customer_product_base_variation_list: customer_product_base_variation_list.append(cat_base) option_is_variant = 1 break if option_is_variant == 1: break if option_is_variant == None: customer_product_option_list[option] = priced_list[option] if len(customer_product_option_list) + len(customer_product_variation_list) != len(priced_list): LOG('>>>>>>> Wrong repartition of the customer priced list', 200) LOG('>>>>>> Customer product option priced list: ', 0, repr(customer_product_option_list)) LOG('>>>>>> Customer product variation priced list: ', 0, repr(customer_product_variation_list)) LOG('>>>>>> Customer product base variation list: ', 0, repr(customer_product_base_variation_list)) # This variable repesent the sum of every option prices options_price_sum = 0.0 # We have to create a new product for every option not included in the variation system for opt_prod_key in customer_product_option_list.keys(): opt_prod_price = customer_product_option_list[opt_prod_key] # Set the id of the optionnal product opt_prod_key = self.str2id(opt_prod_key) opt_prod_id = "storever-" + opt_prod_key # Create the optionnal product or get it if it already exist opt_prod_object = self.createOrFindProduct(erp5_site, opt_prod_id) # Remove the "discontinued" string in the title opt_prod_title = self.niceTitle(opt_prod_key) # Set some properties of the optionnal product opt_prod_object.setTitle(opt_prod_title.title()) opt_prod_object.setBasePrice(opt_prod_price) opt_prod_object.setQuantityUnit('unit') # Save the workflow state changing for later modification workflow_joblist.append((opt_prod_object, opt_prod_key)) # Get the last number of order lines # This process is needed to distinguish the same option created for two different product # and avoid problem when a new Order line is created for an option product already used # inside the same Sale Order last_line_num = self.getLastOrderLineNumber(parent_order_object) opt_prod_line_id = "storever-" + str(last_line_num) + "-" + opt_prod_key # Create an order line for the product opt_order_line_object = parent_order_object.newContent( portal_type = 'Sale Order Line' , id = opt_prod_line_id) # Set several properties of the new orderLine opt_order_line_object.setQuantityUnit('unit') opt_order_line_object.setPrice(opt_prod_price) # There is the same quantity of the base product opt_order_line_object.setQuantity(kw['quantity']) # Link the Order Line with the product opt_order_line_object.setResource("product/" + opt_prod_id) # Calcul the sum of option prices options_price_sum += float(opt_prod_price) # # TODO: don't forget to manage the VAT values # TODO: # Try to find a previous OrderLine to update # line_object = None # for product_id in product_folder.objectIds(): # if product_id == erp5_product_id: # product_object = erp5_site.restrictedTraverse(erp5_site_path + '/product/' + erp5_product_id) # break # Migrate the line informations object.setQuantity(kw['quantity']) object.setDescription(kw['title']) object.setQuantityUnit('unit') # Substract to the product price the sum of options prices initial_prod_price = float(kw['price']) - options_price_sum object.setPrice(initial_prod_price) # Link the Order Line with the product object.setResource("product/" + erp5_product_id) # Set variations of the order line product choosen by the customer category_list = [] for variation_key in customer_product_variation_list.keys(): category_list.append(customer_product_variation_list[variation_key]) #object.setVariationBaseCategoryList(customer_product_base_variation_list) # # TODO : fix this #object.setVariationCategoryList(category_list) previous_category_list = object.getCategoryList() LOG('ERP5ShopOrderConduit, previous_category_list',0,previous_category_list) category_list = list(previous_category_list) + list(category_list) object.setCategoryList(category_list) # Do all workflow change at the end LOG("enter workflow loop >>>>>>>>",0,repr(workflow_joblist)) for (object, object_title) in workflow_joblist: LOG("Workflow to change :: >>>>>>>>",0,repr((object, object_title))) self.setProductWorkflow(object, object_title) return