Добавлен статус заказа и общие действия в декораторах

This commit is contained in:
2026-01-11 01:03:22 +03:00
parent 15500d74bd
commit 049f018232
17 changed files with 188 additions and 178 deletions

View File

@@ -45,7 +45,7 @@ class MailOrderBot(ConfigManager):
def execute(self):
# Получить список айдишников письма
#Получить список айдишников письма
folder = self.config.get("folder")
try:

View File

@@ -14,10 +14,7 @@ class ExcelFileParcer:
def _parse_file(self, file_bytes):
"""Парсит вложение в формате эл таблиц"""
try:
df = pd.read_excel(file_bytes, sheet_name=self.sheet_name, header=None)
except Exception as e:
df = None
df = pd.read_excel(file_bytes, sheet_name=self.sheet_name, header=None)
return df
def set_value(self, sku, manufacturer, column, value):

View File

@@ -1,2 +1,4 @@
from .processor import TaskProcessor
from .message import LogMessage, LogMessageLevel, LogMessageStorage
from .abstract_task import AbstractTask, pass_if_error, handle_errors

View File

@@ -1,11 +1,60 @@
from abc import ABC, abstractmethod
from typing import Dict, Any
import logging
import functools
from mail_order_bot.context import Context
logger = logging.getLogger(__name__)
def handle_errors(func):
"""
Декоратор для обработки ошибок в методе do класса AbstractTask.
Оборачивает выполнение метода в try-except, при ошибке устанавливает статус "error",
логирует ошибку и пробрасывает исключение дальше.
Применяется везде к методу do.
"""
@functools.wraps(func)
def wrapper(self, attachment) -> None:
file_name = attachment.get("name", "неизвестный файл")
try:
# Выполняем метод do
return func(self, attachment)
except Exception as e:
# При ошибке устанавливаем статус и логируем
if attachment:
attachment["status"] = "error"
logger.error(f"Ошибка при обработке файла {file_name} на стадии {self.STEP} \n{e}", exc_info=True)
# Пробрасываем исключение дальше
# raise
return wrapper
def pass_if_error(func):
"""
Декоратор для проверки статуса attachment перед выполнением метода do.
Если статус attachment["status"] != "ok", метод не выполняется.
Применяется опционально в конкретных классах, где нужна проверка статуса.
"""
@functools.wraps(func)
def wrapper(self, attachment) -> None:
# Проверяем статус перед выполнением
if attachment and attachment.get("status") != "ok":
file_name = attachment.get("name", "неизвестный файл")
logger.warning(f"Пропускаем шаг для файла {file_name}, статус {attachment.get('status')}")
return
# Выполняем метод do
return func(self, attachment)
return wrapper
class AbstractTask():
RESULT_SECTION = "section"
STEP = "Название шага обработки"
"""
Абстрактный базовый класс для всех хэндлеров.
"""

View File

@@ -1,5 +1,5 @@
from .abcp.api_get_stock import APIGetStock
from .abcp._api_get_stock import APIGetStock
from .delivery_time.local_store import DeliveryPeriodLocalStore
from .delivery_time.from_config import DeliveryPeriodFromConfig
from .notifications.test_notifier import TestNotifier

View File

@@ -24,7 +24,7 @@ class APIGetStock(AbstractTask):
self.client_provider = AbcpProvider(login=client_login, password=client_password)
def do(self, attachment) -> None:
#
order = attachment.get("order", None)
for position in order.positions:
# Получаем остатки из-под учетной записи клиента

View File

@@ -6,7 +6,7 @@
"""
import logging
from mail_order_bot.task_processor.abstract_task import AbstractTask
from ...abstract_task import AbstractTask, pass_if_error, handle_errors
from mail_order_bot.abcp_api.abcp_provider import AbcpProvider
from mail_order_bot.credential_provider import CredentialProvider
@@ -14,47 +14,47 @@ from mail_order_bot.telegram.client import TelegramClient
logger = logging.getLogger(__name__)
class SaveOrderToTelegram(AbstractTask):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@pass_if_error
@handle_errors
def do(self, attachment) -> None:
client = TelegramClient()
try:
order = attachment["order"]
positions = order.positions
message = "\nОбработка заказа {указать название контрагента}\n"
message += f"\nПолучено {len(positions)} позиций от {order.client_id}\n"
message += "===============================\n"
for position in positions:
message += f"{position.sku} - {position.manufacturer} - {position.name} \n"
message += f"{position.asking_quantity} x {position.asking_price} = {position.total} \n"
order = attachment["order"]
positions = order.positions
message = "\nОбработка заказа {указать название контрагента}\n"
message += f"\nПолучено {len(positions)} позиций от {order.client_id}\n"
message += "===============================\n"
for position in positions:
message += f"{position.sku} - {position.manufacturer} - {position.name} \n"
message += f"{position.asking_quantity} x {position.asking_price} = {position.total} \n"
rejected = position.asking_quantity - position.order_quantity
if position.order_quantity == 0:
message += f"Отказ\n"
elif rejected:
message += (f"Отказ: {rejected}, запрошено, {position.asking_quantity}, "
f"отгружено {position.order_quantity}, профит {position.profit}\n")
else:
message += f"Позиция отгружена полностью, профит {position.profit}\n"
message += "-------------------------------\n"
rejected = position.asking_quantity - position.order_quantity
if position.order_quantity == 0:
message += f"Отказ\n"
elif rejected:
message += (f"Отказ: {rejected}, запрошено, {position.asking_quantity}, "
f"отгружено {position.order_quantity}, профит {position.profit}\n")
else:
message += f"Позиция отгружена полностью, профит {position.profit}\n"
message += "-------------------------------\n"
result = client.send_message(message)
result = client.send_message(message)
# Отправка экселя в телеграм
excel = attachment["excel"]
file = excel.get_file_bytes()
# Отправка экселя в телеграм
excel = attachment["excel"]
file = excel.get_file_bytes()
client.send_document(
document=file,
filename="document.xlsx"
)
except Exception as e:
logger.error("Ошибка при отправке инфо по заказу в телеграм")
else:
logger.warning("Инфо по заказу отправлено в телеграм")
client.send_document(
document=file,
filename="document.xlsx"
)
logger.warning("Инфо по заказу отправлено в телеграм")
#===============================

View File

@@ -5,7 +5,7 @@
import logging
from mail_order_bot.task_processor.abstract_task import AbstractTask
from ...abstract_task import AbstractTask, pass_if_error, handle_errors
logger = logging.getLogger(__name__)
@@ -13,17 +13,9 @@ class DeliveryPeriodFromConfig(AbstractTask):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@pass_if_error
@handle_errors
def do(self, attachment) -> None:
try:
delivery_period = self.config.get("delivery_period")
except Exception as e:
logger.error(f"Ошибка при получении срока доставки из конфига: {e}")
else:
attachment["delivery_period"] = delivery_period
logger.warning(f"Срок доставки установлен из конфига - {delivery_period} (ч.)")
delivery_period = self.config.get("delivery_period")
attachment["delivery_period"] = delivery_period
logger.warning(f"Срок доставки установлен из конфига - {delivery_period} (ч.)")

View File

@@ -2,7 +2,7 @@
Парсер срока доставки из темы письма
"""
from mail_order_bot.task_processor.abstract_task import AbstractTask
from ...abstract_task import AbstractTask, pass_if_error, handle_errors
import logging
import re

View File

@@ -5,7 +5,7 @@
import logging
from mail_order_bot.task_processor.abstract_task import AbstractTask
from ...abstract_task import AbstractTask, pass_if_error, handle_errors
logger = logging.getLogger(__name__)

View File

@@ -4,7 +4,7 @@
"""
import logging
from mail_order_bot.task_processor.abstract_task import AbstractTask
from ...abstract_task import AbstractTask, pass_if_error, handle_errors
from mail_order_bot.email_client.utils import EmailUtils

View File

@@ -7,7 +7,7 @@ from email.utils import formatdate
from email import encoders
from mail_order_bot.task_processor.abstract_task import AbstractTask
from ...abstract_task import AbstractTask, pass_if_error, handle_errors
logger = logging.getLogger(__name__)
@@ -19,40 +19,40 @@ class EmailReplyTask(AbstractTask):
"""Формирует ответ на входящее письмо с запросом на заказ°"""
EMAIl = "zosimovaa@yandex.ru" #"noreply@zapchastiya.ru"
@pass_if_error
@handle_errors
def do(self, attachment):
try:
email = self.context.data.get("email")
email = self.context.data.get("email")
if not email:
raise ValueError("В контексте нет входящего сообщения")
if not email:
raise ValueError("В контексте нет входящего сообщения")
email_from = self.context.data.get("email_from")
if not email_from:
raise ValueError("В контексте не определен адрес отправителя")
email_from = self.context.data.get("email_from")
if not email_from:
raise ValueError("В контексте не определен адрес отправителя")
reply_message = MIMEMultipart()
reply_message = MIMEMultipart()
email_subj = self.context.data.get("email_subj")
email_subj = self.context.data.get("email_subj")
reply_message["From"] = self.EMAIl
reply_message["To"] = email_from
#reply_message["Cc"] = self.config.get("reply_to", "")
reply_message["Subject"] = f"Re: {email_subj}"
reply_message["Date"] = formatdate(localtime=True)
reply_message["From"] = self.EMAIl
reply_message["To"] = email_from
#reply_message["Cc"] = self.config.get("reply_to", "")
reply_message["Subject"] = f"Re: {email_subj}"
reply_message["Date"] = formatdate(localtime=True)
body = "Автоматический ответ на создание заказа"
reply_message.attach(MIMEText(body, "plain", "utf-8"))
body = "Автоматический ответ на создание заказа"
reply_message.attach(MIMEText(body, "plain", "utf-8"))
self._attach_file(reply_message, attachment)
self._attach_file(reply_message, attachment)
self.context.email_client.send_email(reply_message)
logger.warning(f"Сформирован ответ на заказ на email")
self.context.email_client.send_email(reply_message)
except Exception as e:
logger.error(f"Ошибка при отправке ответа по заказу на email \n{e}")
else:
logger.warning(f"Сформирован ответ на заказ на email")
def _attach_file(self, reply_message, attachment):
"""

View File

@@ -1,12 +1,7 @@
import logging
import pandas as pd
from io import BytesIO
from mail_order_bot.email_client import EmailUtils
#from mail_order_bot.task_processor.handlers.order_position import OrderPosition
from mail_order_bot.task_processor.abstract_task import AbstractTask
from mail_order_bot.parsers.excel_parcer import ExcelFileParcer
from ...abstract_task import AbstractTask, pass_if_error, handle_errors
from ....parsers.excel_parcer import ExcelFileParcer
logger = logging.getLogger(__name__)
@@ -19,20 +14,10 @@ class ExcelExtractor(AbstractTask):
super().__init__(*args, **kwargs)
self.excel_config = self.config.get("excel", {})
@pass_if_error
@handle_errors
def do(self, attachment) -> None:
try:
file_bytes = BytesIO(attachment['bytes'])
excel_file = ExcelFileParcer(file_bytes, self.excel_config)
except Exception as e:
logger.error(f"Не удалось распарсить файл: \n{e}")
attachment["excel"] = None
else:
attachment["excel"] = excel_file
logger.warning(f"Произведен успешный парсинг файла")
file_bytes = BytesIO(attachment['bytes'])
excel_file = ExcelFileParcer(file_bytes, self.excel_config)
attachment["excel"] = excel_file
logger.warning(f"Произведен успешный парсинг файла {attachment.get('name', 'неизвестный файл')}")

View File

@@ -2,7 +2,7 @@ import logging
import pandas as pd
from io import BytesIO
from mail_order_bot.parsers.order_parcer import OrderParser
from mail_order_bot.task_processor.abstract_task import AbstractTask
from ...abstract_task import AbstractTask, pass_if_error, handle_errors
from mail_order_bot.parsers.excel_parcer import ExcelFileParcer
@@ -10,6 +10,7 @@ logger = logging.getLogger(__name__)
class OrderExtractor(AbstractTask):
STEP = "Извлечение заказа"
"""
Хендлер для каждого вложения считывает эксель файл и сохраняет его контекст
"""
@@ -17,28 +18,24 @@ class OrderExtractor(AbstractTask):
super().__init__(*args, **kwargs)
self.excel_config = self.config.get("excel", {})
@pass_if_error
@handle_errors
def do(self, attachment) -> None:
# todo сделать проверку на наличие файла и его тип
delivery_period = attachment.get("delivery_period", 0)
mapping = self.excel_config.get("mapping")
try:
delivery_period = attachment.get("delivery_period", 0)
mapping = self.excel_config.get("mapping")
excel_file = attachment.get("excel")
client_id = self.config.get("client_id")
excel_file = attachment.get("excel")
client_id = self.config.get("client_id")
order_parcer = OrderParser(mapping, delivery_period, client_id)
order_parcer = OrderParser(mapping, delivery_period, client_id)
order_dataframe = excel_file.get_order_rows()
order = order_parcer.parse(order_dataframe)
order_dataframe = excel_file.get_order_rows()
order = order_parcer.parse(order_dataframe)
attachment["order"] = order
except Exception as e:
logger.error(f"Ошибка при парсинге заказа файла: \n{e}")
else:
attachment["order"] = order
logger.warning(f"Обработан файл с заказом, извлечено позиций, {len(order.positions)}")
logger.warning(f"Файл заказа обработан успешно, извлечено {len(order.positions)} позиций")

View File

@@ -1,12 +1,5 @@
import logging
import pandas as pd
from io import BytesIO
# from mail_order_bot.task_processor.handlers.order_position import OrderPosition
from mail_order_bot.task_processor.abstract_task import AbstractTask
from mail_order_bot.order.auto_part_position import PositionStatus
from mail_order_bot.parsers.excel_parcer import ExcelFileParcer
from ...abstract_task import AbstractTask, pass_if_error, handle_errors
logger = logging.getLogger(__name__)
@@ -20,35 +13,31 @@ class UpdateExcelFile(AbstractTask):
super().__init__(*args, **kwargs)
self.excel_config = self.config.get("excel", {})
@pass_if_error
@handle_errors
def do(self, attachment) -> None:
# todo сделать проверку на наличие файла и его тип
excel_file = attachment.get("excel")
order = attachment.get("order")
config = self.context.data.get("config", {})
excel_config = config.get("excel", {})
updatable_fields = excel_config.get("updatable_fields", {})
try:
excel_file = attachment.get("excel")
order = attachment.get("order")
config = self.context.data.get("config", {})
excel_config = config.get("excel", {})
updatable_fields = excel_config.get("updatable_fields", {})
for position in order.positions:
for position in order.positions:
sku = position.sku
manufacturer = position.manufacturer
sku = position.sku
manufacturer = position.manufacturer
for key, value in updatable_fields.items():
for key, value in updatable_fields.items():
if key == "ordered_quantity":
column = value
value = position.order_quantity
excel_file.set_value(sku, manufacturer, column, value)
if key == "ordered_quantity":
column = value
value = position.order_quantity
excel_file.set_value(sku, manufacturer, column, value)
if key == "ordered_price":
column = value
value = position.order_price
excel_file.set_value(sku, manufacturer, column, value)
except Exception as e:
logger.error(f"Ошибка при правке excel файла: \n{e}")
else:
logger.warning(f"Файл excel успешно обновлен")
if key == "ordered_price":
column = value
value = position.order_price
excel_file.set_value(sku, manufacturer, column, value)
logger.warning(f"Файла {attachment.get('name', 'неизвестный файл')} отредактирован")

View File

@@ -10,7 +10,7 @@ from mail_order_bot.order.auto_part_position import AutoPartPosition, PositionSt
from mail_order_bot.parsers.excel_parcer import ExcelFileParcer
from decimal import Decimal
from mail_order_bot.task_processor.abstract_task import AbstractTask
from ...abstract_task import AbstractTask, pass_if_error, handle_errors
from mail_order_bot.abcp_api.abcp_provider import AbcpProvider
from mail_order_bot.credential_provider import CredentialProvider
from mail_order_bot.order.auto_part_order import OrderStatus
@@ -35,39 +35,37 @@ class StockSelector(AbstractTask):
client_login, client_password = credential_provider.get_system_credentials()
self.client_provider = AbcpProvider(login=client_login, password=client_password)
@pass_if_error
@handle_errors
def do(self, attachment) -> None:
# todo сделать проверку на наличие файла и его тип
order = attachment.get("order", None)
delivery_period = attachment.get("delivery_period")
for position in order.positions:
try:
order = attachment.get("order", None)
delivery_period = attachment.get("delivery_period")
for position in order.positions:
#1. Получаем остатки со складов
stock_data = self.client_provider.get_stock(position.sku, position.manufacturer)
#1. Получаем остатки со складов
stock_data = self.client_provider.get_stock(position.sku, position.manufacturer)
#2. Из данных остатков выбираем оптимальное значение по стратегии
if stock_data["success"]:
stock_list = stock_data.get("data", [])
asking_price = position.asking_price
asking_quantity = position.asking_quantity
#2. Из данных остатков выбираем оптимальное значение по стратегии
if stock_data["success"]:
stock_list = stock_data.get("data", [])
asking_price = position.asking_price
asking_quantity = position.asking_quantity
optimal_stock_positions = self.get_optimal_stock(stock_list, asking_price, asking_quantity, delivery_period)
optimal_stock_positions = self.get_optimal_stock(stock_list, asking_price, asking_quantity, delivery_period)
# 3. Устанавливаем выбранное значение в позицию
if len(optimal_stock_positions):
position.set_order_item(optimal_stock_positions[0])
else:
position.status = PositionStatus.NO_AVAILABLE_STOCK
# Мне не очень нравится управление статусами в этом месте, кажется что лучше это делать внутри AutoPartPosition
# 3. Устанавливаем выбранное значение в позицию
if len(optimal_stock_positions):
position.set_order_item(optimal_stock_positions[0])
else:
position.status = PositionStatus.STOCK_FAILED
except Exception as e:
logger.error(f"Ошибка при выборе позиции со складов: {e}")
position.status = PositionStatus.NO_AVAILABLE_STOCK
# Мне не очень нравится управление статусами в этом месте, кажется что лучше это делать внутри AutoPartPosition
else:
position.status = PositionStatus.STOCK_FAILED
logger.warning("Определены оптимальные позиции со складов")
else:
logger.warning("Определены оптимальные позиции со складов")

View File

@@ -90,7 +90,8 @@ class TaskProcessor:
file_name = attachment["name"]
logger.warning(f"Начата обработка файла: {file_name} =>")
attachment["log_messages"] = LogMessageStorage(file_name)
#attachment["log_messages"] = LogMessageStorage(file_name)
attachment["status"] = "ok"
# Запустить обработку пайплайна
for handler_name in pipeline: