diff --git a/src/mail_order_bot/configs/amtel.club.yml b/src/mail_order_bot/configs/amtel.club.yml index 0be828c..b4e8d69 100644 --- a/src/mail_order_bot/configs/amtel.club.yml +++ b/src/mail_order_bot/configs/amtel.club.yml @@ -16,7 +16,7 @@ pipeline: - handler: DeliveryPeriodLocalStore # Запрос остатков со склада - - handler: GetStock + - handler: APIGetStock diff --git a/src/mail_order_bot/email_processor/handlers/__init__.py b/src/mail_order_bot/email_processor/handlers/__init__.py index 72398c4..1171a5d 100644 --- a/src/mail_order_bot/email_processor/handlers/__init__.py +++ b/src/mail_order_bot/email_processor/handlers/__init__.py @@ -4,8 +4,7 @@ from .destination_time.local_store import DeliveryPeriodLocalStore -from .abcp.api_get_stock import GetStock -from .abcp.api_create_order import InstantOrderTest +from .abcp.api_get_stock import APIGetStock diff --git a/src/mail_order_bot/email_processor/handlers/abcp/api_create_order.py b/src/mail_order_bot/email_processor/handlers/abcp/api_create_order.py deleted file mode 100644 index 14192e5..0000000 --- a/src/mail_order_bot/email_processor/handlers/abcp/api_create_order.py +++ /dev/null @@ -1,37 +0,0 @@ -import logging -import requests -from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask -from mail_order_bot.email_processor.order.auto_part_order import OrderStatus - -logger = logging.getLogger(__name__) - -class InstantOrderTest(AbstractTask): - URL = "https://api.telegram.org/bot{0}/sendMessage?chat_id={1}&text={2}" - - def do(self) -> None: - api_key = self.config["api_key"] - chat_id = self.config["chat_id"] - - if self.order.status == OrderStatus.IN_PROGRESS: - positions = self.order.positions - - message = f"Запрос на создание заказа от {self.context['client']}:\n" - message += "\n".join(f"{pos.sku}: {pos.name} ({pos.order_quantity} x {pos.order_price} = {pos.total})" for pos in positions) - - elif self.order.status == OrderStatus.OPERATOR_REQUIRED: - message = f"Запрос на создание заказа от {self.context['client']} отклонен - необходима ручная обработка.\n" - message += f"Причина: {self.order.reason}" - - else: - message = f"Запрос на создание заказа от {self.context['client']} отклонен.\n" - message += f" Статус заказа: {self.order.status}" - - #url = self.URL.format(api_key, chat_id, message) - #resp = requests.get(url).json() - print(message) - #logger.info(resp) - - - - - diff --git a/src/mail_order_bot/email_processor/handlers/abcp/api_get_stock.py b/src/mail_order_bot/email_processor/handlers/abcp/api_get_stock.py index 7eb3622..1288b89 100644 --- a/src/mail_order_bot/email_processor/handlers/abcp/api_get_stock.py +++ b/src/mail_order_bot/email_processor/handlers/abcp/api_get_stock.py @@ -28,11 +28,14 @@ class APIGetStock(AbstractTask): for position in order.positions: # Получаем остатки из-под учетной записи клиента client_stock = self.client_provider.get_stock(position.sku, position.manufacturer) + system_stock = self.system_provider.get_stock(position.sku, position.manufacturer) - # Используем StockSelector для обработки остатков и выбора оптимального поставщика + # Используем StockSelector для фильтрации неподходящих поставщиков selector = StockSelector(position, order.delivery_period) - selector.process_stock(client_stock) - selector.select_optimal_supplier() + available_distributors = selector.filter_stock(client_stock, system_stock) + + position.set_stock(available_distributors) + position.set_order_item() logger.info(f"Получены позиции со склада для файла {attachment.get('name', "no name")}") diff --git a/src/mail_order_bot/email_processor/handlers/abcp/stock_selector.py b/src/mail_order_bot/email_processor/handlers/abcp/stock_selector.py index 83fa874..a5c4706 100644 --- a/src/mail_order_bot/email_processor/handlers/abcp/stock_selector.py +++ b/src/mail_order_bot/email_processor/handlers/abcp/stock_selector.py @@ -1,10 +1,23 @@ -from typing import Dict, Any, List +import sys +from turtle import position +from typing import Dict, Any, List, Optional from decimal import Decimal from mail_order_bot.email_processor.order.auto_part_position import AutoPartPosition, PositionStatus logger = __import__('logging').getLogger(__name__) +""" +1. Получить 2 вида складских остатков +2. Добавляем цену закупки +3. Применяем правила фильтрации + - можно указать какие правила применены +4. Выбираем цену по профиту + - можно указать ограничения + + + +""" class StockSelector: """ Класс для выбора оптимального поставщика для позиции заказа. @@ -23,15 +36,22 @@ class StockSelector: """ self.position = position self.delivery_period = delivery_period + self.client_stock = None + self.system_stock = None + - def process_stock(self, client_stock: Dict[str, Any]) -> None: + def filter_stock(self, client_stock, system_stock) -> None: """ Обрабатывает результат запроса остатков из-под учетной записи клиента и обновляет позицию. Args: client_stock: Результат запроса остатков от API из-под учетной записи клиента + system_stock: Результат запроса остатков от API из-под системной учетной записи """ - if client_stock["success"]: + self.client_stock = client_stock + self.system_stock = system_stock + + if client_stock["success"] and system_stock["success"]: available_distributors = client_stock["data"] # Для доставки только с локального склада сперва убираем все остальные склады @@ -46,40 +66,44 @@ class StockSelector: # Убираем отрицательные остатки available_distributors = self._filter_proper_availability(available_distributors) - + + # Добавляем данные о закупочных ценах + available_distributors = self._set_system_price(available_distributors) + # Сортируем по цене available_distributors.sort(key=lambda item: Decimal(item["price"]), reverse=False) - self.position.stock = available_distributors - if len(self.position.stock): - self.position.status = PositionStatus.STOCK_RECIEVED - else: - self.position.status = PositionStatus.NO_AVAILABLE_STOCK + return available_distributors + else: - self.position.status = PositionStatus.STOCK_FAILED + return None - def select_optimal_supplier(self) -> None: - """ - Выбирает оптимального поставщика из отфильтрованных складов - и обновляет позицию заказа. - """ - if self.position.status == PositionStatus.STOCK_RECIEVED: - # Вычисляем прибыль для каждого склада - for distributor in self.position.stock: - distributor["profit"] = ( - int(distributor["availability"]) * self.position.requested_price - - int(distributor["availability"]) * Decimal(distributor["price"]) + + + + + def _set_system_price(self, available_distributors): + """Добавляет закупочную (системную) цену к позициям""" + for distributor in available_distributors: + matching_system_item = self._find_matching_system_item( + distributor.get("distributorId"), + distributor.get("supplierCode") ) - - # Сортируем по прибыли (по убыванию) - self.position.stock.sort(key=lambda item: item["profit"], reverse=True) - - # Выбираем лучший вариант - self.position.order_quantity = self.position.stock[0]["availability"] - self.position.order_price = self.position.requested_price - self.position.order_item = self.position.stock[0] - - self.position.status = PositionStatus.READY + if matching_system_item: + # Добавляем поле system_price со значением price из найденного элемента + distributor["system_price"] = matching_system_item.get("price") + else: + # Если соответствие не найдено, устанавливаем None + distributor["system_price"] = None + return available_distributors + + def _find_matching_system_item(self, distributor_id: Any, supplier_code: Any) -> Optional[Dict[str, Any]]: + """Находит соответствующий элемент в system_stock_data по distributorId и supplierCode""" + for system_item in self.system_stock_data: + if (str(system_item.get("distributorId")) == str(distributor_id) and + str(system_item.get("supplierCode")) == str(supplier_code)): + return system_item + return None def _filter_only_local_storage(self, distributors: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """Фильтрует только локальные склады""" diff --git a/src/mail_order_bot/email_processor/order/auto_part_position.py b/src/mail_order_bot/email_processor/order/auto_part_position.py index a6748a8..43049ce 100644 --- a/src/mail_order_bot/email_processor/order/auto_part_position.py +++ b/src/mail_order_bot/email_processor/order/auto_part_position.py @@ -1,3 +1,4 @@ +from turtle import st from typing import List, Optional from dataclasses import dataclass, field from typing import Dict, Any @@ -10,11 +11,9 @@ class PositionStatus(Enum): STOCK_FAILED = "stock_failed" # Остаток не получен NO_AVAILABLE_STOCK = "no_available_stock" #Нет доступных складов READY = "ready" - READY_PARTIAL = "ready_partial" ORDERED = "ordered" # Заказано REFUSED = "refused" # Отказано - @dataclass class AutoPartPosition: """ @@ -47,6 +46,26 @@ class AutoPartPosition: if self.requested_price < 0: raise ValueError(f"Цена не может быть отрицательной: {self.requested_price}") + def set_stock(self, stock): + if stock is not None: + self.stock = stock + if len(self.stock): + self.status = PositionStatus.STOCK_RECIEVED + else: + self.status = PositionStatus.NO_AVAILABLE_STOCK + else: + self.status = PositionStatus.STOCK_FAILED + + def set_order_item(self): + """Выбирает позицию для заказа по максимальному профиту""" + if self.status == PositionStatus.STOCK_RECIEVED: + for stock_item in self.stock: + available_quantity = min(self.requested_quantity, stock_item["availability"]) + stock_item["profit"] = available_quantity * stock_item["price"] - available_quantity * stock_item["system_price"] + + self.stock.sort(key=lambda item: Decimal(item["profit"]), reverse=True) + self.order_item = self.stock[0] + diff --git a/src/mail_order_bot/email_processor/processor.py b/src/mail_order_bot/email_processor/processor.py index 3ab2b00..2e6a679 100644 --- a/src/mail_order_bot/email_processor/processor.py +++ b/src/mail_order_bot/email_processor/processor.py @@ -42,6 +42,7 @@ class EmailProcessor: email_body = EmailUtils.extract_body(email) email_from = EmailUtils.extract_first_sender(email_body) client = EmailUtils.extract_domain(email_from) + self.context.data["client"] = client try: # Определить конфиг для пайплайна