no message

This commit is contained in:
2025-12-14 15:00:18 +03:00
parent 0c39af460f
commit 7043743373
10 changed files with 168 additions and 61 deletions

View File

@@ -1,7 +1,7 @@
import logging
from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask
from mail_order_bot.abcp_api.abcp_provider import AbcpProvider
from .abcp_provider import AbcpProvider
from mail_order_bot.credential_provider import CredentialProvider
from mail_order_bot.email_processor.handlers.abcp.stock_selector import StockSelector

View File

@@ -40,7 +40,7 @@ class StockSelector:
self.system_stock = None
def filter_stock(self, client_stock, system_stock) -> None:
def filter_stock(self, client_stock) -> None:
"""
Обрабатывает результат запроса остатков из-под учетной записи клиента и обновляет позицию.
@@ -49,9 +49,8 @@ class StockSelector:
system_stock: Результат запроса остатков от API из-под системной учетной записи
"""
self.client_stock = client_stock
self.system_stock = system_stock
if client_stock["success"] and system_stock["success"]:
if client_stock["success"]:
available_distributors = client_stock["data"]
# Для доставки только с локального склада сперва убираем все остальные склады
@@ -71,40 +70,13 @@ class StockSelector:
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=True)
return available_distributors
else:
return None
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")
)
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]]:
"""Фильтрует только локальные склады"""
return [item for item in distributors if str(item["distributorId"]) == self.DISTRIBUTOR_ID]

View File

@@ -0,0 +1,92 @@
from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask
from mail_order_bot.email_client.utils import EmailUtils
import logging
import re
logger = logging.getLogger(__name__)
class DeliveryPeriodFromSubject(AbstractTask):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def do(self) -> None:
"""
Извлекает срок доставки из темы письма и сохраняет в каждый элемент attachments.
Правила парсинга:
- Если есть слово "Наличие" - срок доставки = 0 дней
- Если найдены оба варианта (диапазон и точное значение) - используется точное значение
- Если есть только фраза "N-M дней/дня/день" (диапазон) - срок доставки = минимальное значение (N)
- Если есть только фраза "N дней/дня/день" - срок доставки = N дней
- Если ничего не указано - срок доставки = 0 дней
- Срок переводится в часы (умножается на 24)
"""
# Получаем тему письма
email_subj = self.context.data.get("email_subj", "")
if not email_subj:
logger.warning("Тема письма не найдена в контексте")
email_subj = ""
# Парсим срок доставки
delivery_days = self._parse_delivery_period(email_subj)
# Переводим в часы
delivery_time = delivery_days * 24
logger.info(f"Извлечен срок доставки из темы: {delivery_days} дней ({delivery_time} часов)")
# Сохраняем в каждый элемент attachments
attachments = self.context.data.get("attachments", [])
for attachment in attachments:
attachment["delivery_time"] = delivery_time
logger.debug(f"Срок доставки сохранен в {len(attachments)} вложений")
def _parse_delivery_period(self, subject: str) -> int:
"""
Парсит срок доставки из темы письма.
Args:
subject: Тема письма
Returns:
Количество дней доставки (0 по умолчанию)
"""
if not subject:
return 0
subject_lower = subject.lower()
# Проверяем наличие слова "Наличие"
if "наличие" in subject_lower:
return 0
# Ищем оба паттерна одновременно
range_pattern = r'(\d+)-(\d+)\s+(?:дней|дня|день)'
single_pattern = r'(\d+)\s+(?:дней|дня|день)'
range_match = re.search(range_pattern, subject_lower)
single_match = re.search(single_pattern, subject_lower)
# Если найдены оба варианта - используем точное значение (одиночное число)
if range_match and single_match:
days = int(single_match.group(1))
logger.debug(f"Найдены оба варианта (диапазон и точное значение), используется точное: {days} дней")
return days
# Если найден только диапазон - используем минимальное значение
if range_match:
min_days = int(range_match.group(1))
max_days = int(range_match.group(2))
logger.debug(f"Найден диапазон: {min_days}-{max_days} дней, используется минимальное: {min_days} дней")
return min(min_days, max_days)
# Если найдено только одиночное число - используем его
if single_match:
days = int(single_match.group(1))
logger.debug(f"Найдено точное значение: {days} дней")
return days
# Если ничего не найдено, возвращаем 0 (из наличия)
return 0

View File

@@ -12,8 +12,7 @@ class DeliveryPeriodLocalStore(AbstractTask):
def do(self) -> None:
attachments = self.context.data["attachments"]
for attachment in attachments:
order = attachment["order"]
order.set_delivery_period(0)
attachment["delivery_period"] = 0
logger.info(f"Доставка только с локального склада, срок 1 день.")

View File

@@ -1,24 +0,0 @@
import random
import logging
from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask
from mail_order_bot.email_processor.order.auto_part_order import OrderStatus
from mail_order_bot.email_processor.order.auto_part_position import AutoPartPosition, PositionStatus
from decimal import Decimal
import random
logger = logging.getLogger(__name__)
class LocalStoreOrder(AbstractTask):
"""Сейчас логика такая
- ищем на складе наш сапплиер код, берем самую дешевую позицию и делаем заказ из нее
Другие чуть более дорогие не рассматриваем
"""
# это код нашего склада
def do(self) -> None:
attachments = self.context.data["attachments"]
for attachment in attachments:
order = attachment["order"]

View File

@@ -14,12 +14,14 @@ class PositionStatus(Enum):
ORDERED = "ordered" # Заказано
REFUSED = "refused" # Отказано
@dataclass
class AutoPartPosition:
"""
Унифицированная модель позиции для заказа.
Все контрагенты приводятся к этой структуре.
"""
DISTRIBUTOR_ID = "1577730" # ID локального склада
sku: str # Артикул товара
manufacturer: str # Производитель
@@ -57,8 +59,35 @@ class AutoPartPosition:
self.status = PositionStatus.STOCK_FAILED
def set_order_item(self):
"""Выбирает позицию для заказа по максимальному профиту"""
"""Выбирает позицию для заказа"""
if self.status == PositionStatus.STOCK_RECIEVED:
available_distributors = self.stock
# BR-1. Отсекаем склады для заказов из наличия (только локальный склад)
if self.delivery_period == 0:
available_distributors = self._filter_only_local_storage(available_distributors)
# BR-2. Цена не должна превышать цену из заказа
available_distributors = self._filter_proper_price(available_distributors)
# BR-3. Срок доставки не должен превышать ожидаемый
available_distributors = self._filter_proper_delivery_time(available_distributors, self.delivery_period)
# BR-4. Без отрицательных остатков
available_distributors = self._filter_proper_availability(available_distributors)
# Приоритет на склады с полным стоком
# BR-5. Сначала оборачиваем локальный склад, потом удаленные
# BR-6. Выбираем цену максимально близкую к цене из заказа (максимальная)
available_distributors.sort(key=lambda item: Decimal(item["price"]), reverse=True)
# BR-7.
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"]
@@ -67,6 +96,28 @@ class AutoPartPosition:
self.order_item = self.stock[0]
def _filter_only_local_storage(self, distributors: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Фильтрует только локальные склады"""
return [item for item in distributors if str(item["distributorId"]) == self.DISTRIBUTOR_ID]
def _filter_proper_delivery_time(self, distributors: List[Dict[str, Any]], delivery_period: int) -> List[Dict[str, Any]]:
"""Фильтрует склады по сроку доставки"""
return [item for item in distributors if item["deliveryPeriod"] <= delivery_period]
def _filter_proper_price(self, distributors: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Фильтрует склады по цене (убирает дорогие)"""
return [item for item in distributors if Decimal(item["price"]) <= self.position.requested_price]
def _filter_proper_availability(self, distributors: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Фильтрует склады с положительным остатком"""
return [item for item in distributors if Decimal(item["availability"]) > 0]
def _filter_proper_availability(self, distributors: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Фильтрует склады с положительным остатком"""
return [item for item in distributors if Decimal(item["availability"]) > 0]

View File

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