Implement advanced variant/SKU system with new Trek XML fields
Browse files- Add rootProductStockCode, isOptionOfAProduct, isOptionedProduct to Trek XML parsing
- Expand product tuple structure (12 fields: +root_stock, +is_option, +is_optioned)
- Implement proper XML parsing for SKU lookup (vs regex patterns)
- Add 3-level SKU matching: variants → main products → root lookup
- Fix field names: price→priceTaxWithCur, producturl→productLink
- Add fallback regex method for XML parsing errors
- Successfully tested: 501 variants, 226 products with variants
Enables precise warehouse ProductCode to Trek XML stockCode matching
and better variant/main product relationship handling.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <[email protected]>
- app.py +13 -3
- smart_warehouse_with_price.py +67 -7
app.py
CHANGED
@@ -400,6 +400,16 @@ try:
|
|
400 |
stock_code_element = item.find('stockCode')
|
401 |
stock_code = stock_code_element.text if stock_code_element is not None and stock_code_element.text else ""
|
402 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
403 |
# Fiyat formatting (kampanya fiyatı veya normal fiyat)
|
404 |
try:
|
405 |
price_float = float(final_price_str)
|
@@ -435,9 +445,9 @@ try:
|
|
435 |
except:
|
436 |
price_eft = ""
|
437 |
|
438 |
-
# Ürün bilgilerini tuple olarak oluştur (resim URL'si, kategoriler ve
|
439 |
-
# Index mapping: 0=stock_amount, 1=price, 2=product_link, 3=price_eft, 4=stock_number, 5=picture_url, 6=category_tree, 7=category_label, 8=stock_code
|
440 |
-
item_info = (stock_amount, price, product_link, price_eft, str(stock_number), picture_url, category_tree, category_label, stock_code)
|
441 |
products.append((name, item_info, full_name))
|
442 |
|
443 |
# Summary disabled for production
|
|
|
400 |
stock_code_element = item.find('stockCode')
|
401 |
stock_code = stock_code_element.text if stock_code_element is not None and stock_code_element.text else ""
|
402 |
|
403 |
+
# Yeni variant/main product ilişki alanları
|
404 |
+
root_product_stock_code_element = item.find('rootProductStockCode')
|
405 |
+
root_product_stock_code = root_product_stock_code_element.text if root_product_stock_code_element is not None and root_product_stock_code_element.text else ""
|
406 |
+
|
407 |
+
is_option_of_product_element = item.find('isOptionOfAProduct')
|
408 |
+
is_option_of_product = is_option_of_product_element.text if is_option_of_product_element is not None and is_option_of_product_element.text else "0"
|
409 |
+
|
410 |
+
is_optioned_product_element = item.find('isOptionedProduct')
|
411 |
+
is_optioned_product = is_optioned_product_element.text if is_optioned_product_element is not None and is_optioned_product_element.text else "0"
|
412 |
+
|
413 |
# Fiyat formatting (kampanya fiyatı veya normal fiyat)
|
414 |
try:
|
415 |
price_float = float(final_price_str)
|
|
|
445 |
except:
|
446 |
price_eft = ""
|
447 |
|
448 |
+
# Ürün bilgilerini tuple olarak oluştur (resim URL'si, kategoriler, stockCode ve variant bilgileri eklendi)
|
449 |
+
# Index mapping: 0=stock_amount, 1=price, 2=product_link, 3=price_eft, 4=stock_number, 5=picture_url, 6=category_tree, 7=category_label, 8=stock_code, 9=root_product_stock_code, 10=is_option_of_product, 11=is_optioned_product
|
450 |
+
item_info = (stock_amount, price, product_link, price_eft, str(stock_number), picture_url, category_tree, category_label, stock_code, root_product_stock_code, is_option_of_product, is_optioned_product)
|
451 |
products.append((name, item_info, full_name))
|
452 |
|
453 |
# Summary disabled for production
|
smart_warehouse_with_price.py
CHANGED
@@ -38,11 +38,16 @@ def get_cached_trek_xml():
|
|
38 |
return None
|
39 |
|
40 |
def get_product_price_and_link_by_sku(product_code):
|
41 |
-
"""Get price and link from Trek XML using
|
42 |
-
|
43 |
-
Level
|
|
|
|
|
44 |
"""
|
45 |
try:
|
|
|
|
|
|
|
46 |
# Get cached Trek XML
|
47 |
xml_content = get_cached_trek_xml()
|
48 |
if not xml_content:
|
@@ -52,13 +57,70 @@ def get_product_price_and_link_by_sku(product_code):
|
|
52 |
if isinstance(xml_content, bytes):
|
53 |
xml_content = xml_content.decode('utf-8')
|
54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
# Level 1: Search in variants first (isOptionOfAProduct=1)
|
56 |
-
# Look for: <isOptionOfAProduct>1</isOptionOfAProduct> followed by our stockCode
|
57 |
variant_pattern = rf'<isOptionOfAProduct>1</isOptionOfAProduct>.*?<stockCode><!\[CDATA\[{re.escape(product_code)}\]\]></stockCode>.*?(?=<item>|$)'
|
58 |
variant_match = re.search(variant_pattern, xml_content, re.DOTALL)
|
59 |
|
60 |
if variant_match:
|
61 |
-
# Found in variants, extract price and link from this section
|
62 |
section = variant_match.group(0)
|
63 |
price_match = re.search(r'<price><!\[CDATA\[(.*?)\]\]></price>', section)
|
64 |
link_match = re.search(r'<producturl><!\[CDATA\[(.*?)\]\]></producturl>', section)
|
@@ -71,7 +133,6 @@ def get_product_price_and_link_by_sku(product_code):
|
|
71 |
main_match = re.search(main_pattern, xml_content, re.DOTALL)
|
72 |
|
73 |
if main_match:
|
74 |
-
# Found in main products, extract price and link
|
75 |
section = main_match.group(0)
|
76 |
price_match = re.search(r'<price><!\[CDATA\[(.*?)\]\]></price>', section)
|
77 |
link_match = re.search(r'<producturl><!\[CDATA\[(.*?)\]\]></producturl>', section)
|
@@ -79,7 +140,6 @@ def get_product_price_and_link_by_sku(product_code):
|
|
79 |
if price_match and link_match:
|
80 |
return price_match.group(1), link_match.group(1)
|
81 |
|
82 |
-
# Not found
|
83 |
return None, None
|
84 |
|
85 |
except Exception as e:
|
|
|
38 |
return None
|
39 |
|
40 |
def get_product_price_and_link_by_sku(product_code):
|
41 |
+
"""Get price and link from Trek XML using improved SKU matching with new XML fields
|
42 |
+
Uses: stockCode, rootProductStockCode, isOptionOfAProduct, isOptionedProduct
|
43 |
+
Level 1: Search variants by stockCode where isOptionOfAProduct=1
|
44 |
+
Level 2: Search main products by stockCode where isOptionOfAProduct=0
|
45 |
+
Level 3: Search by rootProductStockCode for variant-to-main mapping
|
46 |
"""
|
47 |
try:
|
48 |
+
# Import XML parsing for cleaner approach
|
49 |
+
import xml.etree.ElementTree as ET
|
50 |
+
|
51 |
# Get cached Trek XML
|
52 |
xml_content = get_cached_trek_xml()
|
53 |
if not xml_content:
|
|
|
57 |
if isinstance(xml_content, bytes):
|
58 |
xml_content = xml_content.decode('utf-8')
|
59 |
|
60 |
+
# Parse XML properly instead of regex
|
61 |
+
try:
|
62 |
+
root = ET.fromstring(xml_content)
|
63 |
+
except:
|
64 |
+
# Fallback to regex if XML parsing fails
|
65 |
+
return get_product_price_and_link_by_sku_regex(product_code)
|
66 |
+
|
67 |
+
# Level 1: Search variants first (isOptionOfAProduct=1)
|
68 |
+
for item in root.findall('.//item'):
|
69 |
+
is_option_element = item.find('isOptionOfAProduct')
|
70 |
+
stock_code_element = item.find('stockCode')
|
71 |
+
|
72 |
+
if (is_option_element is not None and is_option_element.text == '1' and
|
73 |
+
stock_code_element is not None and stock_code_element.text and stock_code_element.text.strip() == product_code):
|
74 |
+
|
75 |
+
price_element = item.find('priceTaxWithCur')
|
76 |
+
link_element = item.find('productLink')
|
77 |
+
|
78 |
+
if price_element is not None and link_element is not None:
|
79 |
+
return price_element.text, link_element.text
|
80 |
+
|
81 |
+
# Level 2: Search main products (isOptionOfAProduct=0)
|
82 |
+
for item in root.findall('.//item'):
|
83 |
+
is_option_element = item.find('isOptionOfAProduct')
|
84 |
+
stock_code_element = item.find('stockCode')
|
85 |
+
|
86 |
+
if (is_option_element is not None and is_option_element.text == '0' and
|
87 |
+
stock_code_element is not None and stock_code_element.text and stock_code_element.text.strip() == product_code):
|
88 |
+
|
89 |
+
price_element = item.find('priceTaxWithCur')
|
90 |
+
link_element = item.find('productLink')
|
91 |
+
|
92 |
+
if price_element is not None and link_element is not None:
|
93 |
+
return price_element.text, link_element.text
|
94 |
+
|
95 |
+
# Level 3: Search by rootProductStockCode (variant parent lookup)
|
96 |
+
for item in root.findall('.//item'):
|
97 |
+
root_stock_element = item.find('rootProductStockCode')
|
98 |
+
|
99 |
+
if (root_stock_element is not None and root_stock_element.text and root_stock_element.text.strip() == product_code):
|
100 |
+
price_element = item.find('priceTaxWithCur')
|
101 |
+
link_element = item.find('productLink')
|
102 |
+
|
103 |
+
if price_element is not None and link_element is not None:
|
104 |
+
return price_element.text, link_element.text
|
105 |
+
|
106 |
+
# Not found
|
107 |
+
return None, None
|
108 |
+
|
109 |
+
except Exception as e:
|
110 |
+
return None, None
|
111 |
+
|
112 |
+
def get_product_price_and_link_by_sku_regex(product_code):
|
113 |
+
"""Fallback regex method for SKU lookup if XML parsing fails"""
|
114 |
+
try:
|
115 |
+
xml_content = get_cached_trek_xml()
|
116 |
+
if isinstance(xml_content, bytes):
|
117 |
+
xml_content = xml_content.decode('utf-8')
|
118 |
+
|
119 |
# Level 1: Search in variants first (isOptionOfAProduct=1)
|
|
|
120 |
variant_pattern = rf'<isOptionOfAProduct>1</isOptionOfAProduct>.*?<stockCode><!\[CDATA\[{re.escape(product_code)}\]\]></stockCode>.*?(?=<item>|$)'
|
121 |
variant_match = re.search(variant_pattern, xml_content, re.DOTALL)
|
122 |
|
123 |
if variant_match:
|
|
|
124 |
section = variant_match.group(0)
|
125 |
price_match = re.search(r'<price><!\[CDATA\[(.*?)\]\]></price>', section)
|
126 |
link_match = re.search(r'<producturl><!\[CDATA\[(.*?)\]\]></producturl>', section)
|
|
|
133 |
main_match = re.search(main_pattern, xml_content, re.DOTALL)
|
134 |
|
135 |
if main_match:
|
|
|
136 |
section = main_match.group(0)
|
137 |
price_match = re.search(r'<price><!\[CDATA\[(.*?)\]\]></price>', section)
|
138 |
link_match = re.search(r'<producturl><!\[CDATA\[(.*?)\]\]></producturl>', section)
|
|
|
140 |
if price_match and link_match:
|
141 |
return price_match.group(1), link_match.group(1)
|
142 |
|
|
|
143 |
return None, None
|
144 |
|
145 |
except Exception as e:
|