no message

This commit is contained in:
2025-12-12 23:25:39 +03:00
parent 1222488aec
commit 0c39af460f
7 changed files with 85 additions and 76 deletions

View File

@@ -16,7 +16,7 @@ pipeline:
- handler: DeliveryPeriodLocalStore - handler: DeliveryPeriodLocalStore
# Запрос остатков со склада # Запрос остатков со склада
- handler: GetStock - handler: APIGetStock

View File

@@ -4,8 +4,7 @@ from .destination_time.local_store import DeliveryPeriodLocalStore
from .abcp.api_get_stock import GetStock from .abcp.api_get_stock import APIGetStock
from .abcp.api_create_order import InstantOrderTest

View File

@@ -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)

View File

@@ -28,11 +28,14 @@ class APIGetStock(AbstractTask):
for position in order.positions: for position in order.positions:
# Получаем остатки из-под учетной записи клиента # Получаем остатки из-под учетной записи клиента
client_stock = self.client_provider.get_stock(position.sku, position.manufacturer) 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 = StockSelector(position, order.delivery_period)
selector.process_stock(client_stock) available_distributors = selector.filter_stock(client_stock, system_stock)
selector.select_optimal_supplier()
position.set_stock(available_distributors)
position.set_order_item()
logger.info(f"Получены позиции со склада для файла {attachment.get('name', "no name")}") logger.info(f"Получены позиции со склада для файла {attachment.get('name', "no name")}")

View File

@@ -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 decimal import Decimal
from mail_order_bot.email_processor.order.auto_part_position import AutoPartPosition, PositionStatus from mail_order_bot.email_processor.order.auto_part_position import AutoPartPosition, PositionStatus
logger = __import__('logging').getLogger(__name__) logger = __import__('logging').getLogger(__name__)
"""
1. Получить 2 вида складских остатков
2. Добавляем цену закупки
3. Применяем правила фильтрации
- можно указать какие правила применены
4. Выбираем цену по профиту
- можно указать ограничения
"""
class StockSelector: class StockSelector:
""" """
Класс для выбора оптимального поставщика для позиции заказа. Класс для выбора оптимального поставщика для позиции заказа.
@@ -23,15 +36,22 @@ class StockSelector:
""" """
self.position = position self.position = position
self.delivery_period = delivery_period 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: Args:
client_stock: Результат запроса остатков от API из-под учетной записи клиента 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"] available_distributors = client_stock["data"]
# Для доставки только с локального склада сперва убираем все остальные склады # Для доставки только с локального склада сперва убираем все остальные склады
@@ -46,40 +66,44 @@ class StockSelector:
# Убираем отрицательные остатки # Убираем отрицательные остатки
available_distributors = self._filter_proper_availability(available_distributors) 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) available_distributors.sort(key=lambda item: Decimal(item["price"]), reverse=False)
self.position.stock = available_distributors return available_distributors
if len(self.position.stock):
self.position.status = PositionStatus.STOCK_RECIEVED
else:
self.position.status = PositionStatus.NO_AVAILABLE_STOCK
else: else:
self.position.status = PositionStatus.STOCK_FAILED return None
def select_optimal_supplier(self) -> None:
"""
Выбирает оптимального поставщика из отфильтрованных складов
и обновляет позицию заказа.
""" def _set_system_price(self, available_distributors):
if self.position.status == PositionStatus.STOCK_RECIEVED: """Добавляет закупочную (системную) цену к позициям"""
# Вычисляем прибыль для каждого склада for distributor in available_distributors:
for distributor in self.position.stock: matching_system_item = self._find_matching_system_item(
distributor["profit"] = ( distributor.get("distributorId"),
int(distributor["availability"]) * self.position.requested_price - distributor.get("supplierCode")
int(distributor["availability"]) * Decimal(distributor["price"])
) )
if matching_system_item:
# Сортируем по прибыли (по убыванию) # Добавляем поле system_price со значением price из найденного элемента
self.position.stock.sort(key=lambda item: item["profit"], reverse=True) distributor["system_price"] = matching_system_item.get("price")
else:
# Выбираем лучший вариант # Если соответствие не найдено, устанавливаем None
self.position.order_quantity = self.position.stock[0]["availability"] distributor["system_price"] = None
self.position.order_price = self.position.requested_price return available_distributors
self.position.order_item = self.position.stock[0]
def _find_matching_system_item(self, distributor_id: Any, supplier_code: Any) -> Optional[Dict[str, Any]]:
self.position.status = PositionStatus.READY """Находит соответствующий элемент в 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]]: def _filter_only_local_storage(self, distributors: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Фильтрует только локальные склады""" """Фильтрует только локальные склады"""

View File

@@ -1,3 +1,4 @@
from turtle import st
from typing import List, Optional from typing import List, Optional
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Dict, Any from typing import Dict, Any
@@ -10,11 +11,9 @@ class PositionStatus(Enum):
STOCK_FAILED = "stock_failed" # Остаток не получен STOCK_FAILED = "stock_failed" # Остаток не получен
NO_AVAILABLE_STOCK = "no_available_stock" #Нет доступных складов NO_AVAILABLE_STOCK = "no_available_stock" #Нет доступных складов
READY = "ready" READY = "ready"
READY_PARTIAL = "ready_partial"
ORDERED = "ordered" # Заказано ORDERED = "ordered" # Заказано
REFUSED = "refused" # Отказано REFUSED = "refused" # Отказано
@dataclass @dataclass
class AutoPartPosition: class AutoPartPosition:
""" """
@@ -47,6 +46,26 @@ class AutoPartPosition:
if self.requested_price < 0: if self.requested_price < 0:
raise ValueError(f"Цена не может быть отрицательной: {self.requested_price}") 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]

View File

@@ -42,6 +42,7 @@ class EmailProcessor:
email_body = EmailUtils.extract_body(email) email_body = EmailUtils.extract_body(email)
email_from = EmailUtils.extract_first_sender(email_body) email_from = EmailUtils.extract_first_sender(email_body)
client = EmailUtils.extract_domain(email_from) client = EmailUtils.extract_domain(email_from)
self.context.data["client"] = client
try: try:
# Определить конфиг для пайплайна # Определить конфиг для пайплайна