fixed bugs in DellConfiguratorParser2025 that caused the computed price to not match (at all) the price displayed by the dell web page

- the price of the cpu upgrade was misinterpreted as the price for 1 cpu, while it's actually the price for 2 cpus
- now the estimated price exactly matches what we would get on the web site

work related to [https://bugzilla.ipr.univ-rennes.fr/show_bug.cgi?id=4171]
This commit is contained in:
Guillaume Raffy 2025-10-22 14:51:15 +02:00
parent 3cd99587ca
commit 716d2d3262
2 changed files with 32 additions and 18 deletions

View File

@ -404,6 +404,9 @@ class Config():
self.cpu = None self.cpu = None
self.cpu_slots_mem = [] self.cpu_slots_mem = []
def __repr__(self) -> str:
return f'Config(cpu={self.cpu.uid if self.cpu else None}, num_servers={self.num_servers}, num_cpu_per_server={self.num_cpu_per_server}, ram_size={self.ram_size} GiB)'
@property @property
def chassis(self) -> ItemUid: def chassis(self) -> ItemUid:
return self.configurator.chassis.item return self.configurator.chassis.item
@ -513,7 +516,7 @@ class Config():
def get_price(self) -> Price: def get_price(self) -> Price:
price = self.configurator.chassis.price price = self.configurator.chassis.price
print(self.cpu.uid, self.configurator.chassis.price, self.configurator.get_item_price(self.cpu.uid), self.ram_price) logging.debug(f'cpu: {self.cpu.uid}, chassis price: {self.configurator.chassis.price}, 1 cpu price: {self.configurator.get_item_price(self.cpu.uid)}, ram price: {self.ram_price}')
price += self.num_servers * self.num_cpu_per_server * self.configurator.get_item_price(self.cpu.uid) + self.ram_price price += self.num_servers * self.num_cpu_per_server * self.configurator.get_item_price(self.cpu.uid) + self.ram_price
assert price > 0.0 assert price > 0.0
return price return price
@ -617,7 +620,7 @@ class Configurator():
def get_item_price(self, item_uid: ItemUid) -> Optional[Price]: def get_item_price(self, item_uid: ItemUid) -> Optional[Price]:
for module in self.modules.values(): for module in self.modules.values():
logging.debug(f'items in module {module.name}: {list(module.options.keys())}') # logging.debug(f'items in module {module.name}: {list(module.options.keys())}')
if item_uid in module.options: if item_uid in module.options:
return module.options[item_uid].price return module.options[item_uid].price

View File

@ -428,6 +428,11 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
def get_xpath_filter(self, filter_id) -> str: def get_xpath_filter(self, filter_id) -> str:
assert False assert False
@abstractmethod
def additional_cpu_is_automatic(self) -> bool:
'''on some dell configurators (web pages), the user doesn't have the possibility to decide if he wants 1 or 2 CPUs, it is automatic. For example, on r670 configurator on 10/2025, there is no way to get a 1 cpu only configuration (the 'Processeur supplémentaires' module indicates 'Inclus dans le prix') '''
assert False
@abstractmethod @abstractmethod
def get_base_price(self, html_root: HtmlElement) -> Price: def get_base_price(self, html_root: HtmlElement) -> Price:
assert False assert False
@ -484,6 +489,8 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
if cpu_class == 'platinium': if cpu_class == 'platinium':
cpu_class = 'platinum' cpu_class = 'platinum'
cpu_id = "intel-xeon-%s-%s" % (cpu_class, match['cpu_number'].lower()) cpu_id = "intel-xeon-%s-%s" % (cpu_class, match['cpu_number'].lower())
if self.additional_cpu_is_automatic():
num_cpus = 2
assert match, 'unhandled label : %s' % label assert match, 'unhandled label : %s' % label
# print(match['cpu_class'], match['cpu_number']) # print(match['cpu_class'], match['cpu_number'])
option = Option(Cpu(cpu_id), price / num_cpus) option = Option(Cpu(cpu_id), price / num_cpus)
@ -810,7 +817,7 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
break break
if not cpu_already_exists: if not cpu_already_exists:
cpu_price = proc_change_option.price + configurator.modules['processor'].options[base_cpu.uid].price cpu_price = proc_change_option.price + configurator.modules['processor'].options[base_cpu.uid].price
print('estimated price of cpu %s : %f' % (proc_change_option.item.uid, cpu_price)) logging.info('estimated price of cpu %s : %f' % (proc_change_option.item.uid, cpu_price))
configurator.modules['processor'].add_option(Option(Cpu(proc_change_option.item.uid), cpu_price)) configurator.modules['processor'].add_option(Option(Cpu(proc_change_option.item.uid), cpu_price))
# delete the 'processor-change' module as its items ids are the same as the ones in the 'processor' modules but their prices are 'wrong' (upgrade prices rather than item price). # delete the 'processor-change' module as its items ids are the same as the ones in the 'processor' modules but their prices are 'wrong' (upgrade prices rather than item price).
# in a configuration, no item should be found more than once # in a configuration, no item should be found more than once
@ -818,6 +825,7 @@ class DellConfiguratorParser(IHtmlConfiguratorParser):
one_cpu_price = configurator.get_item_price(configurator.base_config.cpu.uid) one_cpu_price = configurator.get_item_price(configurator.base_config.cpu.uid)
ram_price = configurator.base_config.ram_price ram_price = configurator.base_config.ram_price
logging.debug('configurator.base_config.num_cpus = %d' % configurator.base_config.num_cpus)
configurator.chassis.price = base_price - configurator.base_config.num_cpus * one_cpu_price - ram_price configurator.chassis.price = base_price - configurator.base_config.num_cpus * one_cpu_price - ram_price
@ -846,6 +854,9 @@ class DellConfiguratorParser2020(DellConfiguratorParser):
'base_module_to_label': ".//div[@class='option-selected ']", 'base_module_to_label': ".//div[@class='option-selected ']",
}[filter_id] }[filter_id]
def additional_cpu_is_automatic(self) -> bool:
return False
def price_str_as_float(self, price_as_str): def price_str_as_float(self, price_as_str):
# eg '+ 2,255.00 €' # eg '+ 2,255.00 €'
match = re.match(r'^\s*(?P<sign>[-+]?)\s*(?P<numbers>[0-9.]*)\s*€\s*$', price_as_str.replace(',', '')) match = re.match(r'^\s*(?P<sign>[-+]?)\s*(?P<numbers>[0-9.]*)\s*€\s*$', price_as_str.replace(',', ''))
@ -918,6 +929,9 @@ class DellConfiguratorParser2021(DellConfiguratorParser):
'base_module_to_label': ".//div[@class='product-options-configuration-block option-selected']", 'base_module_to_label': ".//div[@class='product-options-configuration-block option-selected']",
}[filter_id] }[filter_id]
def additional_cpu_is_automatic(self) -> bool:
return False
def price_str_as_float(self, price_as_str): def price_str_as_float(self, price_as_str):
# eg '+ 2255,00 €' # contains a Narrow No-Break Space (NNBSP) https://www.compart.com/en/unicode/U+202F # eg '+ 2255,00 €' # contains a Narrow No-Break Space (NNBSP) https://www.compart.com/en/unicode/U+202F
nnbsp = '' nnbsp = ''
@ -1022,6 +1036,13 @@ class DellConfiguratorParser2025(DellConfiguratorParser):
'base_module_to_label': ".//div[@class='product-options-configuration-block option-selected']", 'base_module_to_label': ".//div[@class='product-options-configuration-block option-selected']",
}[filter_id] }[filter_id]
def additional_cpu_is_automatic(self) -> bool:
# TODO: parse actual number of cpus per server from html
# in Processeur supplémentaires module, there's:
# Processeur Intel® Xeon® 6 Performance 6505P 2,2 GHz, 12C/24T, 24 GT/s, 48 Mo de cache, Turbo (150 W), mémoire DDR5-6400
# Inclus dans le prix
return True
def price_str_as_float(self, price_as_str): def price_str_as_float(self, price_as_str):
# eg '+ 2255,00 €' # contains a Narrow No-Break Space (NNBSP) https://www.compart.com/en/unicode/U+202F # eg '+ 2255,00 €' # contains a Narrow No-Break Space (NNBSP) https://www.compart.com/en/unicode/U+202F
nnbsp = '' nnbsp = ''
@ -1181,6 +1202,9 @@ class DellConfiguratorParser2025(DellConfiguratorParser):
def _parse_base_config(self, html_root: HtmlElement, configurator: Configurator) -> Config: def _parse_base_config(self, html_root: HtmlElement, configurator: Configurator) -> Config:
base_config = Config(configurator) base_config = Config(configurator)
base_config.num_servers = configurator.chassis.item.max_num_servers base_config.num_servers = configurator.chassis.item.max_num_servers
if self.additional_cpu_is_automatic(self):
base_config.num_cpu_per_server = 2
else:
base_config.num_cpu_per_server = 1 base_config.num_cpu_per_server = 1
# initialize cpu # initialize cpu
@ -1191,21 +1215,8 @@ class DellConfiguratorParser2025(DellConfiguratorParser):
assert match, 'unhandled label : %s' % item_label assert match, 'unhandled label : %s' % item_label
if match: if match:
cpu_id = "intel-xeon-%s-%s" % (match['cpu_class'].lower(), match['cpu_number'].lower()) cpu_id = "intel-xeon-%s-%s" % (match['cpu_class'].lower(), match['cpu_number'].lower())
if match is None:
match = re.match(r'^2 processeurs Intel Xeon (?P<cpu_class>Silver|Gold|Platinium) (?P<cpu_number>[0-9][0-9][0-9][0-9][RLYU]?).*', item_label)
if match:
base_config.num_cpu_per_server = 2
cpu_id = "intel-xeon-%s-%s" % (match['cpu_class'].lower(), match['cpu_number'].lower())
if match is None:
print('item_label=%s' % item_label)
# match = re.match(r'^2 Processeurs AMD EPYC (?P<cpu_number>[0-9][0-9][0-9][0-9]).*', item_label)
match = re.match(r'^2 Processeurs AMD EPYC (?P<cpu_number>[0-9][0-9][0-9][0-9]).*', clean_string(item_label))
if match:
base_config.num_cpu_per_server = 2
cpu_id = "amd-epyc-%s" % (match['cpu_number'].lower())
assert match, 'unhandled label : %s' % item_label assert match, 'unhandled label : %s' % item_label
# print(match['cpu_class'], match['cpu_number']) # print(match['cpu_class'], match['cpu_number'])
base_config.set_cpu(Cpu(cpu_id)) base_config.set_cpu(Cpu(cpu_id))