From 716d2d3262db6dd84044b7a42c4b0a4d18835238 Mon Sep 17 00:00:00 2001 From: Guillaume Raffy Date: Wed, 22 Oct 2025 14:51:15 +0200 Subject: [PATCH] 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] --- concho/config.py | 7 +++++-- concho/dell.py | 43 +++++++++++++++++++++++++++---------------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/concho/config.py b/concho/config.py index 3587293..b7072f6 100644 --- a/concho/config.py +++ b/concho/config.py @@ -404,6 +404,9 @@ class Config(): self.cpu = None 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 def chassis(self) -> ItemUid: return self.configurator.chassis.item @@ -513,7 +516,7 @@ class Config(): def get_price(self) -> 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 assert price > 0.0 return price @@ -617,7 +620,7 @@ class Configurator(): def get_item_price(self, item_uid: ItemUid) -> Optional[Price]: 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: return module.options[item_uid].price diff --git a/concho/dell.py b/concho/dell.py index d9ab72f..2949c98 100644 --- a/concho/dell.py +++ b/concho/dell.py @@ -428,6 +428,11 @@ class DellConfiguratorParser(IHtmlConfiguratorParser): def get_xpath_filter(self, filter_id) -> str: 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 def get_base_price(self, html_root: HtmlElement) -> Price: assert False @@ -484,6 +489,8 @@ class DellConfiguratorParser(IHtmlConfiguratorParser): if cpu_class == 'platinium': cpu_class = 'platinum' 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 # print(match['cpu_class'], match['cpu_number']) option = Option(Cpu(cpu_id), price / num_cpus) @@ -810,7 +817,7 @@ class DellConfiguratorParser(IHtmlConfiguratorParser): break if not cpu_already_exists: 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)) # 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 @@ -818,6 +825,7 @@ class DellConfiguratorParser(IHtmlConfiguratorParser): one_cpu_price = configurator.get_item_price(configurator.base_config.cpu.uid) 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 @@ -846,6 +854,9 @@ class DellConfiguratorParser2020(DellConfiguratorParser): 'base_module_to_label': ".//div[@class='option-selected ']", }[filter_id] + def additional_cpu_is_automatic(self) -> bool: + return False + def price_str_as_float(self, price_as_str): # eg '+ 2,255.00 €' match = re.match(r'^\s*(?P[-+]?)\s*(?P[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']", }[filter_id] + def additional_cpu_is_automatic(self) -> bool: + return False + def price_str_as_float(self, price_as_str): # eg '+ 2 255,00 €' # contains a Narrow No-Break Space (NNBSP) https://www.compart.com/en/unicode/U+202F nnbsp = ' ' @@ -1022,6 +1036,13 @@ class DellConfiguratorParser2025(DellConfiguratorParser): 'base_module_to_label': ".//div[@class='product-options-configuration-block option-selected']", }[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): # eg '+ 2 255,00 €' # contains a Narrow No-Break Space (NNBSP) https://www.compart.com/en/unicode/U+202F nnbsp = ' ' @@ -1181,7 +1202,10 @@ class DellConfiguratorParser2025(DellConfiguratorParser): def _parse_base_config(self, html_root: HtmlElement, configurator: Configurator) -> Config: base_config = Config(configurator) base_config.num_servers = configurator.chassis.item.max_num_servers - base_config.num_cpu_per_server = 1 + if self.additional_cpu_is_automatic(self): + base_config.num_cpu_per_server = 2 + else: + base_config.num_cpu_per_server = 1 # initialize cpu item_label = self._get_module_default_item_label('Processeur', html_root) @@ -1191,21 +1215,8 @@ class DellConfiguratorParser2025(DellConfiguratorParser): assert match, 'unhandled label : %s' % item_label if match: 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 (?PSilver|Gold|Platinium) (?P[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[0-9][0-9][0-9][0-9]).*', item_label) - match = re.match(r'^2 Processeurs AMD EPYC (?P[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 + # print(match['cpu_class'], match['cpu_number']) base_config.set_cpu(Cpu(cpu_id))