no message
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
@@ -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 день.")
|
||||
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user