diff --git a/product/ERP5SyncML/Conduit/ERP5ShopOrderConduit.py b/product/ERP5SyncML/Conduit/ERP5ShopOrderConduit.py new file mode 100755 index 0000000000000000000000000000000000000000..ee16c3fb3cb651fc5385109ec8727edf10bc5be8 --- /dev/null +++ b/product/ERP5SyncML/Conduit/ERP5ShopOrderConduit.py @@ -0,0 +1,764 @@ +############################################################################## +# +# 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. + """ + + +# 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 + 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)) + object.newContent ( portal_type = 'Sale Order' + , id = new_object_id) + if portal_type == 'Order Line': + last_line_num = self.getLastOrderLineNumber(object) + new_object_id = "storever-" + str(last_line_num + 1) + "-" + object_id + object.newContent ( portal_type = 'Sale Order Line' + , id = new_object_id) + subobject = object._getOb(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) + 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 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) + # Try to find a previous product + for product_id in product_folder.objectIds(): + if product_id == erp5_product_id: + return erp5_site.restrictedTraverse(erp5_site_path + '/product/' + erp5_product_id) + # We have to create a new product + product_folder.newContent ( portal_type = 'Product' + , id = erp5_product_id) + return product_folder._getOb(erp5_product_id) + + + + 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) + """ + action = None + if hasattr(product_object, 'workflow_history'): + workflow_state = product_object.portal_workflow.getInfoFor(product_object, 'validation_state') + if product_title.lower().find('discontinued') != -1: + if workflow_state != 'invalidated': + action = 'invalidate_action' + elif workflow_state in ('draft', 'invalidated'): + action = 'validate_action' + if action != None: + product_object.portal_workflow.doActionFor( product_object + , action + , wf_id = 'validation_workflow') + + + + 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 + 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.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) + + # 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'): + + # 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' + organisation_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 + organisation_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 organisation_folder.objectIds(): + if organisation_id == owner_id: + organisation_object = erp5_site.restrictedTraverse(erp5_site_path + '/organisation/' + organisation_id) + LOG("Previous organisation found ! >>>>>>>>",0,repr(organisation_object)) + break + + # Define the previous customer structure + previous_owner_type = '' + if person_object != None: + previous_owner_type += 'p' + if organisation_object != None: + previous_owner_type += 'o' + 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: + # 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,None) + if previous_owner_type == None: + # No previous customer found, create one + if owner_type.find('o') != -1: + organisation_folder.newContent ( portal_type = 'Organisation' + , id = owner_id) + organisation_object = organisation_folder._getOb(owner_id) + LOG("new organisation created >>>>>>>>",0,repr(organisation_object)) + if owner_type.find('p') != -1: + person_folder.newContent ( portal_type = 'Person' + , id = owner_id) + person_object = person_folder._getOb(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,None) + else: + LOG("We have to convert the structure >>>>>>>>",0,None) +# # XXX Be aware of that problem: the invoice for a sale order must be the same +# 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 + organisation_folder.newContent ( portal_type = 'Organisation' + , id = owner_id) + organisation_object = organisation_folder._getOb(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 previous_owner_type == None or 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_folder.newContent ( portal_type = 'Person' + , id = owner_id) + person_object = person_folder._getOb(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,None) + pass + + LOG("Person object >>>>>>>>",0,repr(person_object)) + LOG("Organisation object >>>>>>>>",0,repr(organisation_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.setDestinationDecision("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 + if kw.has_key('address'): + 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, None) + + person_object.setDefaultAddressCity(kw['city'].title()) + 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 + person_object.setDefaultEmailText(kw['email']) + 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: +# # TODO : fix this +# person_object.setSubordination("organisation/" + owner_id) + organisation_object.setTitle(kw['organisation'].title()) + organisation_object.setCorporateName(kw['organisation'].title()) + organisation_object.setRole("client") + if kw.has_key('eu_vat'): + organisation_object.setEuVatCode(kw['eu_vat']) + # The customer is not a person or a person of an organisation, so the customer is an organisation... + else: + # Link the customer with the Sale Order + object.setDestination("organisation/" + owner_id) + object.setDestinationDecision("organisation/" + owner_id) + # All informations describe the organisation + organisation_object.setTitle(kw['organisation'].title()) + organisation_object.setCorporateName(kw['organisation'].title()) + organisation_object.setRole("client") + organisation_object.setEuVatCode(kw['eu_vat']) + organisation_object.setDefaultAddressStreetAddress(kw['address'].title()) + organisation_object.setDefaultAddressCity(kw['city'].title()) + organisation_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: + organisation_object.setDefaultAddressRegion(region_path) +# else: +# # TODO : Ask the user to select an appropriate region + organisation_object.setDefaultEmailText(kw['email']) + organisation_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 " + 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(lambda x, y: cmp(str(x), str(y))) + stor_ship_id_word_list.sort(lambda x, y: cmp(str(x), str(y))) + erp5_ship_title_word_list.sort(lambda x, y: cmp(str(x), str(y))) + 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 + ship_order_line_id = "storever-" + shipment_id + object.newContent( portal_type = 'Sale Order Line' + , id = ship_order_line_id) + ship_order_object = object._getOb(ship_order_line_id) + 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') + + # Set the worflow status + self.setProductWorkflow(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', 'keyboad_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) + , ('keyboad_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 category '", 0, cat_path + "' created") + + # 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: + 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') + # Set the workflow state of the optionnal product + self.setProductWorkflow(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 a 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 + parent_order_object.newContent ( portal_type = 'Sale Order Line' + , id = opt_prod_line_id) + opt_order_line_object = parent_order_object._getOb(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) + + return \ No newline at end of file