diff --git a/src/mail_order_bot/abcp_api/abcp_provider.py b/src/mail_order_bot/abcp_api/abcp_provider.py index c35c228..fbce304 100644 --- a/src/mail_order_bot/abcp_api/abcp_provider.py +++ b/src/mail_order_bot/abcp_api/abcp_provider.py @@ -29,7 +29,24 @@ class AbcpProvider: path = "/search/articles" params = {"number": sku, "brand": manufacturer, "withOutAnalogs": "1"} - return self._execute(path, method, params) + + status_code, payload = self._execute(path, method, params) + + if status_code == 200: + response = {"success": True, "data": payload} + logger.debug(f"Получены данные об остатках на складе") + return response + + elif status_code == 301: + response = {"success": True, "data": []} + logger.debug(f"Не найдены позиции по запрошенным параметрам") + return response + + else: + response = {"success": False, "data": payload} + logger.debug(f"Ошибка при получении остатков со склада") + return response + def _execute(self, path, method="GET", params={}, data=None): params["userlogin"] = self.login @@ -37,18 +54,4 @@ class AbcpProvider: response = requests.request(method, self.HOST+path, data=data, headers=self.HEADERS, params=params) payload = response.json() - if response.status_code == 200: - logger.debug(f"Получены данные об остатках на складе") - result = { - "success": True, - "data": payload - } - else: - logger.warning(f"ошибка получения данных об остатках на складе: {payload}") - - result = { - "success": False, - "error": payload - } - return result - + return response.status_code, payload \ No newline at end of file diff --git a/src/mail_order_bot/config.yml b/src/mail_order_bot/config.yml index c1ea0ca..3837277 100644 --- a/src/mail_order_bot/config.yml +++ b/src/mail_order_bot/config.yml @@ -54,7 +54,7 @@ log: handlers: console: - level: DEBUG + level: WARNING formatter: standard class: logging.StreamHandler stream: ext://sys.stdout # Default is stderr @@ -79,15 +79,15 @@ log: loggers: '': handlers: [console, file, telegram] - level: DEBUG + level: WARNING propagate: False __main__: handlers: [console, file, telegram] - level: INFO + level: WARNING propagate: False config_manager: handlers: [console, file] - level: DEBUG + level: ERROR \ No newline at end of file diff --git a/src/mail_order_bot/deprecated/old_excel_parcer/__init__.py b/src/mail_order_bot/deprecated/old_excel_parcer/__init__.py deleted file mode 100644 index 329f916..0000000 --- a/src/mail_order_bot/deprecated/old_excel_parcer/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .processor import ExcelProcessor diff --git a/src/mail_order_bot/deprecated/old_excel_parcer/configurable_parser.py b/src/mail_order_bot/deprecated/old_excel_parcer/configurable_parser.py deleted file mode 100644 index 5aa64c0..0000000 --- a/src/mail_order_bot/deprecated/old_excel_parcer/configurable_parser.py +++ /dev/null @@ -1,113 +0,0 @@ -import logging -import pandas as pd -from typing import Dict, Any, Optional, List -from decimal import Decimal - -from .excel_parser import ExcelParser -from .order_position import OrderPosition - - -logger = logging.getLogger(__name__) - - -class ConfigurableExcelParser(ExcelParser): - """ - Универсальный парсер, настраиваемый через конфигурацию. - Подходит для большинства стандартных случаев. - """ - - def parse(self, file_bytes: str) -> List[OrderPosition]: - try: - # Читаем Excel - df = self._make_dataframe(file_bytes) - - # Получаем маппинг колонок из конфигурации - mapping = self.config['mapping'] - - # Парсим строки - positions = [] - for idx, row in df.iterrows(): - try: - position = self._parse_row(row, mapping) - if position: - positions.append(position) - except Exception as e: - logger.error(f"Ошибка парсинга строки {idx}: {e}, {row}") - continue - - logger.info(f"Успешно обработано {len(positions)} позиций из {len(df)} строк") - return positions - - except Exception as e: - logger.error(f"Ошибка при обработке файла: {e}") - raise Exception from e - - def _parse_row(self, row: pd.Series, mapping: Dict[str, str]) -> Optional[OrderPosition]: - """Парсит одну строку Excel в OrderPosition""" - - # Проверяем обязательные поля - required_fields = ['article', 'price', 'quantity'] - - for field in required_fields: - if pd.isna(row.get(mapping[field])): - logger.warning(f"Позиция не создана - не заполнено поле {mapping[field]}") - return None - - price = Decimal(str(row[mapping['price']]).replace(",", ".").strip()) - quantity = int(row[mapping['quantity']]) - - if "total" in mapping.keys(): - total = Decimal(str(row[mapping['total']]).replace(",", ".").strip()) - else: - total = price * quantity - - if mapping.get('name',"") in mapping.keys(): - name = str(row[mapping.get('name', "")]).strip() - else: - name = "" - - # Создаем объект позиции - position = OrderPosition( - article=str(row[mapping['article']]).strip(), - manufacturer=str(row[mapping.get('manufacturer',"")]).strip(), - name=name, - price=price, - quantity=quantity, - total=total, - additional_attrs=self._extract_additional_attrs(row, mapping) - ) - return position - - def _extract_additional_attrs(self, row: pd.Series, mapping: Dict[str, str]) -> Dict[str, Any]: - """Извлекает дополнительные атрибуты, не входящие в основную модель""" - additional = {} - mapped_columns = set(mapping.values()) - - for col in row.index: - if col not in mapped_columns and not pd.isna(row[col]): - additional[col] = row[col] - - return additional - - - def _make_dataframe(self, bio) -> pd.DataFrame: - # Получаем все данные из файла - sheet_name = self.config.get("sheet_name", 0) - df_full = pd.read_excel(bio, sheet_name=sheet_name, header=None) - - # Находим индекс строки с заголовком - key_field = self.config.get("key_field") - header_row_idx = df_full[ - df_full.apply(lambda row: row.astype(str).str.contains(key_field, case=False, na=False).any(), - axis=1)].index[0] - - # Считываем таблицу с правильным заголовком - df = pd.read_excel(bio, header=header_row_idx, sheet_name=sheet_name, engine='calamine') #openpyxl calamine - - # Находим индекс первой строки с пустым 'Артикул' - first_empty_index = df[df[key_field].isna()].index.min() - - # Обрезаем DataFrame до первой пустой строки (не включая её) - df_trimmed = df.loc[:first_empty_index - 1] - - return df_trimmed diff --git a/src/mail_order_bot/deprecated/old_excel_parcer/custom_parser_autoeuro.py b/src/mail_order_bot/deprecated/old_excel_parcer/custom_parser_autoeuro.py deleted file mode 100644 index 1ba7545..0000000 --- a/src/mail_order_bot/deprecated/old_excel_parcer/custom_parser_autoeuro.py +++ /dev/null @@ -1,105 +0,0 @@ -import logging -import pandas as pd -from typing import Dict, Any, Optional, List -from decimal import Decimal -import xlrd -from io import BytesIO - -from .excel_parser import ExcelParser -from .order_position import OrderPosition - -logger = logging.getLogger(__name__) - - -class CustomExcelParserAutoeuro(ExcelParser): - """ - Универсальный парсер, настраиваемый через конфигурацию. - Подходит для большинства стандартных случаев. - """ - - def parse(self, file_bytes: BytesIO) -> List[OrderPosition]: - try: - # Читаем Excel - df = self._make_dataframe(file_bytes) - - # Получаем маппинг колонок из конфигурации - mapping = self.config['mapping'] - - # Парсим строки - positions = [] - for idx, row in df.iterrows(): - try: - position = self._parse_row(row, mapping) - if position: - positions.append(position) - except Exception as e: - logger.error(f"Ошибка парсинга строки {idx}: {e}, {row}") - continue - - logger.info(f"Успешно обработано {len(positions)} позиций из {len(df)} строк") - return positions - - except Exception as e: - logger.error(f"Ошибка при обработке файла: {e}") - raise Exception from e - - def _parse_row(self, row: pd.Series, mapping: Dict[str, str]) -> Optional[OrderPosition]: - """Парсит одну строку Excel в OrderPosition""" - - # Проверяем обязательные поля - required_fields = ['article', 'price', 'quantity'] - - for field in required_fields: - if pd.isna(row.get(mapping[field])): - logger.warning(f"Позиция не создана - не заполнено поле {mapping[field]}") - return None - - price = Decimal(str(row[mapping['price']]).replace(",", ".").strip()) - quantity = int(row[mapping['quantity']]) - - if "total" in mapping.keys(): - total = Decimal(str(row[mapping['total']]).replace(",", ".").strip()) - else: - total = price * quantity - - # Создаем объект позиции - position = OrderPosition( - article=str(row[mapping['article']]).strip(), - manufacturer=str(row[mapping.get('manufacturer', "")]).strip(), - name="", #str(row[mapping.get('name', "name")]).strip(), - price=price, - quantity=quantity, - total=total, - additional_attrs=self._extract_additional_attrs(row, mapping) - ) - return position - - def _extract_additional_attrs(self, row: pd.Series, mapping: Dict[str, str]) -> Dict[str, Any]: - """Извлекает дополнительные атрибуты, не входящие в основную модель""" - additional = {} - mapped_columns = set(mapping.values()) - - for col in row.index: - if col not in mapped_columns and not pd.isna(row[col]): - additional[col] = row[col] - - return additional - - def _make_dataframe(self, bio) -> pd.DataFrame: - - file_bytes = bio.read() - book = xlrd.open_workbook(file_contents=file_bytes, encoding_override='cp1251') - sheet = book.sheet_by_index(self.config.get("sheet_index", 0)) - data = [sheet.row_values(row) for row in range(sheet.nrows)] - - df_full = pd.DataFrame(data) - - key_field = self.config.get("key_field") - header_row_idx = df_full[ - df_full.apply(lambda row: row.astype(str).str.contains(key_field, case=False, na=False).any(), - axis=1)].index[0] - - df = df_full[header_row_idx:] - df.columns = df.iloc[0] # первая строка становится заголовком - df = df.reset_index(drop=True).drop(0).reset_index(drop=True) # удаляем первую строку и сбрасываем индекс - return df diff --git a/src/mail_order_bot/deprecated/old_excel_parcer/excel_parser.py b/src/mail_order_bot/deprecated/old_excel_parcer/excel_parser.py deleted file mode 100644 index 1f7e674..0000000 --- a/src/mail_order_bot/deprecated/old_excel_parcer/excel_parser.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging -import pandas as pd -from abc import ABC, abstractmethod -from typing import Dict, Any, List -from io import BytesIO - - -from .order_position import OrderPosition - - -logger = logging.getLogger(__name__) - -class ExcelParser(ABC): - """ - Абстрактный базовый класс для всех парсеров Excel. - Реализует Strategy Pattern - каждый контрагент = своя стратегия. - """ - - def __init__(self, config: Dict[str, Any]): - self.config = config - - @abstractmethod - def parse(self, file: BytesIO) -> List[OrderPosition]: - """ - Парсит Excel файл и возвращает список позиций. - Должен быть реализован в каждом конкретном парсере. - """ - pass \ No newline at end of file diff --git a/src/mail_order_bot/deprecated/old_excel_parcer/order_position.py b/src/mail_order_bot/deprecated/old_excel_parcer/order_position.py deleted file mode 100644 index 9bfe69a..0000000 --- a/src/mail_order_bot/deprecated/old_excel_parcer/order_position.py +++ /dev/null @@ -1,24 +0,0 @@ -from dataclasses import dataclass, field -from typing import Dict, Any -from decimal import Decimal - -@dataclass -class OrderPosition: - """ - Унифицированная модель позиции для заказа. - Все контрагенты приводятся к этой структуре. - """ - article: str # Артикул товара - manufacturer: str # Производитель - name: str # Наименование - price: Decimal # Цена за единицу - quantity: int # Количество - total: Decimal # Общая сумма - additional_attrs: Dict[str, Any] = field(default_factory=dict) - - def __post_init__(self): - """Валидация после инициализации""" - if self.quantity < 0: - raise ValueError(f"Количество не может быть отрицательным: {self.quantity}") - if self.price < 0: - raise ValueError(f"Цена не может быть отрицательной: {self.price}") \ No newline at end of file diff --git a/src/mail_order_bot/deprecated/old_excel_parcer/parser_factory.py b/src/mail_order_bot/deprecated/old_excel_parcer/parser_factory.py deleted file mode 100644 index d5aeb87..0000000 --- a/src/mail_order_bot/deprecated/old_excel_parcer/parser_factory.py +++ /dev/null @@ -1,54 +0,0 @@ -import yaml -import json -import logging -from pathlib import Path -from typing import Dict, Any, List - -from .excel_parser import ExcelParser -from .configurable_parser import ConfigurableExcelParser -from .custom_parser_autoeuro import CustomExcelParserAutoeuro - - -logger = logging.getLogger(__name__) - -class ParserFactory: - """ - Фабрика парсеров (Factory Pattern). - Создает нужный парсер на основе названия контрагента. - """ - - # Реестр кастомных парсеров - CUSTOM_PARSERS = { - 'autoeuro.ru': CustomExcelParserAutoeuro, - # Добавляйте сюда специализированные парсеры - } - - def __init__(self, config: Dict[str, Any]): - self.config = config - - def get_parser(self, supplier_name: str) -> ExcelParser: - """ - Возвращает парсер для указанного контрагента. - Использует кастомный парсер если есть, иначе конфигурируемый. - """ - if supplier_name not in self.config['suppliers']: - raise ValueError( - f"Контрагент '{supplier_name}' не найден в конфигурации. " - f"Доступные: {list(self.config['suppliers'].keys())}" - ) - - config = self.config['suppliers'][supplier_name] - - # Проверяем, есть ли кастомный парсер - if supplier_name in self.CUSTOM_PARSERS: - parser_class = self.CUSTOM_PARSERS[supplier_name] - logger.debug(f"Используется кастомный парсер для {supplier_name}") - else: - parser_class = ConfigurableExcelParser - logger.debug(f"Используется конфигурируемый парсер для {supplier_name}") - - return parser_class(config) - - def list_suppliers(self) -> List[str]: - """Возвращает список всех доступных контрагентов""" - return list(self.config['suppliers'].keys()) diff --git a/src/mail_order_bot/deprecated/old_excel_parcer/processor.py b/src/mail_order_bot/deprecated/old_excel_parcer/processor.py deleted file mode 100644 index f1245be..0000000 --- a/src/mail_order_bot/deprecated/old_excel_parcer/processor.py +++ /dev/null @@ -1,110 +0,0 @@ -import logging -from pathlib import Path -from decimal import Decimal -from io import BytesIO -from typing import Dict, Any, List -import yaml -import json - - -from .parser_factory import ParserFactory -from .order_position import OrderPosition - - -logger = logging.getLogger(__name__) - -class ExcelProcessor: - """ - Главный класс-фасад для обработки Excel файлов. - Упрощает использование системы. - """ - - def __init__(self, config_path: str = 'config/suppliers.yaml', ): - self.config_path = Path(config_path) - self.config = self._load_config() - self.factory = ParserFactory(self.config) - - def process(self, file_bytes: BytesIO, file_name: str, supplier_name: str, validate: bool = False) -> List[OrderPosition]: - """ - Обрабатывает Excel файл от контрагента. - - Args: - file_bytes: Байты файла - file_name: Имя файла - supplier_name: Название контрагента (из конфигурации) - validate: Выполнять ли дополнительную валидацию - - Returns: - Список объектов OrderPosition - - Raises: - ValueError: Если контрагент не найден - """ - logger.info(f"Обработка файла: {file_name} для {supplier_name}") - - parser = self.factory.get_parser(supplier_name) - positions = parser.parse(file_bytes) - - # Дополнительная валидация если нужна - if validate: - positions = self._validate_positions(positions) - - logger.debug(f"Обработка завершена: получено {len(positions)} позиций") - return positions - - def process_file(self, file_path: str, supplier_name: str, validate: bool = False) -> List[OrderPosition]: - # Проверка существования файла - logger.debug(f"Чтение файла: {file_path}") - if not Path(file_path).exists(): - raise FileNotFoundError(f"Файл не найден: {file_path}") - - with open(file_path, 'rb') as file: # бинарный режим - raw_data = file.read() - bio = BytesIO(raw_data) - - positions = self.process(bio, file_path, supplier_name, validate=validate) - - return positions - - - def _validate_positions(self, positions: List[OrderPosition]) -> List[OrderPosition]: - """Дополнительная валидация позиций""" - valid_positions = [] - - for pos in positions: - try: - # Проверка на непустые обязательные поля - if not pos.article or not pos.name: - logger.warning(f"Пропущена позиция с пустыми полями: {pos}") - continue - - # Проверка корректности суммы - expected_total = pos.price * pos.quantity - if abs(pos.total - expected_total) > Decimal('0.01'): - logger.warning( - f"Несоответствие суммы для {pos.article}: " - f"ожидается {expected_total}, получено {pos.total}" - ) - - valid_positions.append(pos) - - except Exception as e: - logger.error(f"Ошибка валидации позиции: {e}") - continue - - return valid_positions - - def get_available_suppliers(self) -> List[str]: - """Возвращает список доступных контрагентов""" - return self.factory.list_suppliers() - - def _load_config(self) -> Dict[str, Any]: - """Загружает конфигурацию из YAML или JSON""" - if self.config_path.suffix in ['.yaml', '.yml']: - with open(self.config_path, 'r', encoding='utf-8') as f: - return yaml.safe_load(f) - elif self.config_path.suffix == '.json': - with open(self.config_path, 'r', encoding='utf-8') as f: - return json.load(f) - else: - raise ValueError(f"Неподдерживаемый формат конфига: {self.config_path.suffix}") diff --git a/src/mail_order_bot/email_client/client.py b/src/mail_order_bot/email_client/client.py index 47d7023..9426eff 100644 --- a/src/mail_order_bot/email_client/client.py +++ b/src/mail_order_bot/email_client/client.py @@ -14,7 +14,7 @@ import smtplib import logging -from mail_order_bot import MailOrderBotException + logger = logging.getLogger(__name__) @@ -22,8 +22,6 @@ logger = logging.getLogger(__name__) # from .objects import EmailMessage, EmailAttachment -class EmailClientException(MailOrderBotException): - pass class EmailClient: diff --git a/src/mail_order_bot/email_client/utils.py b/src/mail_order_bot/email_client/utils.py index 19e0386..111c0d1 100644 --- a/src/mail_order_bot/email_client/utils.py +++ b/src/mail_order_bot/email_client/utils.py @@ -78,7 +78,7 @@ class EmailUtils: filename = part.get_filename() if filename: # Декодируем имя файла - filename = decode_header(filename)[0] + filename = decode_header(filename)[0][0] # Получаем содержимое content = part.get_payload(decode=True) if content: diff --git a/src/mail_order_bot/parsers/excel_parcer.py b/src/mail_order_bot/parsers/excel_parcer.py index b1895be..ba7d218 100644 --- a/src/mail_order_bot/parsers/excel_parcer.py +++ b/src/mail_order_bot/parsers/excel_parcer.py @@ -18,7 +18,6 @@ class ExcelFileParcer: df = pd.read_excel(file_bytes, sheet_name=self.sheet_name, header=None) except Exception as e: df = None - logger.warning("Не удалось распарсить значение файла") return df def set_value(self, sku, manufacturer, column, value): diff --git a/src/mail_order_bot/task_processor/abstract_task.py b/src/mail_order_bot/task_processor/abstract_task.py index 357a947..b8db0f9 100644 --- a/src/mail_order_bot/task_processor/abstract_task.py +++ b/src/mail_order_bot/task_processor/abstract_task.py @@ -15,7 +15,7 @@ class AbstractTask(): self.config = self.context.data.get("config", {}) @abstractmethod - def do(self) -> None: + def do(self, attachment) -> None: """ Выполняет работу над заданием Входные и выходные данные - в self.context diff --git a/src/mail_order_bot/task_processor/handlers/__init__.py b/src/mail_order_bot/task_processor/handlers/__init__.py index 300d63c..bfb8db1 100644 --- a/src/mail_order_bot/task_processor/handlers/__init__.py +++ b/src/mail_order_bot/task_processor/handlers/__init__.py @@ -1,6 +1,4 @@ - -from .attachment_handler.attachment_handler import AttachmentHandler from .abcp.api_get_stock import APIGetStock from .delivery_time.local_store import DeliveryPeriodLocalStore from .delivery_time.from_config import DeliveryPeriodFromConfig @@ -13,6 +11,6 @@ from .stock_selectors.stock_selector import StockSelector from .excel_parcers.update_excel_file import UpdateExcelFile -from .email.send_email import EmailReplyTask +from .email.email_reply_task import EmailReplyTask diff --git a/src/mail_order_bot/task_processor/handlers/abcp/api_get_stock.py b/src/mail_order_bot/task_processor/handlers/abcp/api_get_stock.py index b048930..821638d 100644 --- a/src/mail_order_bot/task_processor/handlers/abcp/api_get_stock.py +++ b/src/mail_order_bot/task_processor/handlers/abcp/api_get_stock.py @@ -23,19 +23,16 @@ class APIGetStock(AbstractTask): client_login, client_password = credential_provider.get_client_credentials() self.client_provider = AbcpProvider(login=client_login, password=client_password) - def do(self) -> None: + def do(self, attachment) -> None: - attachments = self.context.data.get("attachments", []) - for attachment in attachments: + order = attachment.get("order", None) + for position in order.positions: + # Получаем остатки из-под учетной записи клиента + client_stock = self.client_provider.get_stock(position.sku, position.manufacturer) + position.set_order_item(client_stock) + #position.set_order_item() - order = attachment.get("order", None) - for position in order.positions: - # Получаем остатки из-под учетной записи клиента - client_stock = self.client_provider.get_stock(position.sku, position.manufacturer) - position.set_order_item(client_stock) - #position.set_order_item() - - logger.info(f"Получены позиции со склада для файла {attachment.get('name', "no name")}") + logger.info(f"Получены позиции со склада для файла {attachment.get('name', "no name")}") def get_stock(self, sku: str, manufacturer: str) -> int: diff --git a/src/mail_order_bot/task_processor/handlers/abcp/api_save_order.py b/src/mail_order_bot/task_processor/handlers/abcp/api_save_order.py index dca43fd..ef975c2 100644 --- a/src/mail_order_bot/task_processor/handlers/abcp/api_save_order.py +++ b/src/mail_order_bot/task_processor/handlers/abcp/api_save_order.py @@ -18,11 +18,10 @@ class SaveOrderToTelegram(AbstractTask): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def do(self) -> None: + def do(self, attachment) -> None: client = TelegramClient() - attachments = self.context.data.get("attachments", []) - for attachment in attachments: + try: order = attachment["order"] positions = order.positions message = "\nОбработка заказа {указать название контрагента}\n" @@ -52,9 +51,12 @@ class SaveOrderToTelegram(AbstractTask): document=file, filename="document.xlsx" ) - # logger.critical(message) + except Exception as e: + logger.error("Ошибка при отправке инфо по заказу в телеграм") + else: + logger.warning("Инфо по заказу отправлено в телеграм") - #=============================== + #=============================== diff --git a/src/mail_order_bot/task_processor/handlers/delivery_time/from_config.py b/src/mail_order_bot/task_processor/handlers/delivery_time/from_config.py index 322c8eb..9c7f1b7 100644 --- a/src/mail_order_bot/task_processor/handlers/delivery_time/from_config.py +++ b/src/mail_order_bot/task_processor/handlers/delivery_time/from_config.py @@ -13,12 +13,16 @@ class DeliveryPeriodFromConfig(AbstractTask): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def do(self) -> None: - attachments = self.context.data["attachments"] - for attachment in attachments: - delivery_period = self.config.get("delivery_period", 0) + 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.info(f"Срок доставки для файла {attachment["name"]} установлен из конфига - {delivery_period}") + logger.warning(f"Срок доставки установлен из конфига - {delivery_period} (ч.)") diff --git a/src/mail_order_bot/task_processor/handlers/email/email_parcer.py b/src/mail_order_bot/task_processor/handlers/email/email_parcer.py index c9c3f59..b58a012 100644 --- a/src/mail_order_bot/task_processor/handlers/email/email_parcer.py +++ b/src/mail_order_bot/task_processor/handlers/email/email_parcer.py @@ -6,13 +6,11 @@ import logging from mail_order_bot.task_processor.abstract_task import AbstractTask from mail_order_bot.email_client.utils import EmailUtils -from mail_order_bot import MailOrderBotException -from mail_order_bot.task_processor import LogMessage, LogMessageLevel logger = logging.getLogger(__name__) -class EmailParcerException(MailOrderBotException): +class EmailParcerException(Exception): pass @@ -52,12 +50,7 @@ class EmailParcer(AbstractTask): logger.error(e) self.context.data["error"].add( - LogMessage( - handler="EmailParcer", - level=LogMessageLevel.ERROR, - message="Возникла ошибка при парсинге письма", - error_data=str(e) - ) + "переделать ошибку на нормальную" ) #raise EmailParcerException(f"Ошибка при парсинге письма {e}") from e diff --git a/src/mail_order_bot/task_processor/handlers/email/send_email.py b/src/mail_order_bot/task_processor/handlers/email/email_reply_task.py similarity index 85% rename from src/mail_order_bot/task_processor/handlers/email/send_email.py rename to src/mail_order_bot/task_processor/handlers/email/email_reply_task.py index c493d7d..73abe35 100644 --- a/src/mail_order_bot/task_processor/handlers/email/send_email.py +++ b/src/mail_order_bot/task_processor/handlers/email/email_reply_task.py @@ -6,12 +6,12 @@ from email.mime.base import MIMEBase from email.utils import formatdate from email import encoders -from mail_order_bot import MailOrderBotException + from mail_order_bot.task_processor.abstract_task import AbstractTask logger = logging.getLogger(__name__) -class EmailReplyTaskException(MailOrderBotException): +class EmailReplyTaskException(Exception): pass @@ -19,7 +19,7 @@ class EmailReplyTask(AbstractTask): """Формирует ответ на входящее письмо с запросом на заказ°""" EMAIl = "zosimovaa@yandex.ru" #"noreply@zapchastiya.ru" - def do(self): + def do(self, attachment): try: email = self.context.data.get("email") @@ -45,13 +45,14 @@ class EmailReplyTask(AbstractTask): body = "Автоматический ответ на создание заказа" reply_message.attach(MIMEText(body, "plain", "utf-8")) - attachments = self.context.data.get("attachments") - for attachment in attachments: - self._attach_file(reply_message, attachment) + + self._attach_file(reply_message, attachment) self.context.email_client.send_email(reply_message) except Exception as e: - pass + logger.error(f"Ошибка при отправке ответа по заказу на email \n{e}") + else: + logger.warning(f"Сформирован ответ на заказ на email") def _attach_file(self, reply_message, attachment): """ @@ -69,7 +70,7 @@ class EmailReplyTask(AbstractTask): encoders.encode_base64(part) - file_name = attachment["name"][0] + file_name = attachment["name"] part.add_header( "Content-Disposition", f"attachment; filename= {file_name}" diff --git a/src/mail_order_bot/task_processor/handlers/excel_parcers/deprecated/order_parser.py b/src/mail_order_bot/task_processor/handlers/excel_parcers/deprecated/order_parser.py deleted file mode 100644 index 40ad76b..0000000 --- a/src/mail_order_bot/task_processor/handlers/excel_parcers/deprecated/order_parser.py +++ /dev/null @@ -1,122 +0,0 @@ -import logging -import pandas as pd -from typing import Dict, Any, Optional -from decimal import Decimal -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.task_processor.order.auto_part_position import AutoPartPosition -from mail_order_bot.task_processor.order.auto_part_order import AutoPartOrder - -logger = logging.getLogger(__name__) - - -class OrderParser(AbstractTask): - """ - Универсальный парсер, настраиваемый через конфигурацию. - Подходит для большинства стандартных случаев. - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def do(self) -> None: - - # todo сделать проверку на наличие файла и его тип - - attachments = self.context.data.get("attachments", []) - for attachment in attachments: - file_bytes = BytesIO(attachment['bytes']) # self.context.get("attachment") # - delivery_period = attachment.get("delivery_period", 0) - #try: - df = self._make_dataframe(file_bytes) - mapping = self.config["mapping"] - client_id = self.config["client_id"] - order = AutoPartOrder() - attachment["order"] = order - - # Парсим строки - positions = [] - for idx, row in df.iterrows(): - position = self._parse_row(row, mapping) - if position: - position.order_delivery_period = delivery_period - order.add_position(position) - - logger.info(f"Успешно обработано {len(order)} позиций из {len(df)} строк") - - #except Exception as e: - # logger.error(f"Ошибка при обработке файла: {e}") - #else: - attachment["order"] = order - - - - def _parse_row(self, row: pd.Series, mapping: Dict[str, str]) -> Optional[AutoPartPosition]: - """Парсит одну строку Excel в OrderPosition""" - - # Проверяем обязательные поля - required_fields = ['article', 'price', 'quantity'] - - for field in required_fields: - if pd.isna(row.get(mapping[field])): - logger.warning(f"Позиция не создана - не заполнено поле {mapping[field]}") - return None - - price = Decimal(str(row[mapping['price']]).replace(",", ".").strip()) - quantity = int(row[mapping['quantity']]) - - if "total" in mapping.keys(): - total = Decimal(str(row[mapping['total']]).replace(",", ".").strip()) - else: - total = price * quantity - - if "name" in mapping: - name = str(row[mapping.get('name', "")]).strip() - else: - name = "" - - # Создаем объект позиции - position = AutoPartPosition( - sku=str(row[mapping['article']]).strip(), - manufacturer=str(row[mapping.get('manufacturer', "")]).strip(), - name=name, - requested_price=price, - requested_quantity=quantity, - total=total, - additional_attrs=self._extract_additional_attrs(row, mapping) - ) - return position - - def _extract_additional_attrs(self, row: pd.Series, mapping: Dict[str, str]) -> Dict[str, Any]: - """Извлекает дополнительные атрибуты, не входящие в основную модель""" - additional = {} - mapped_columns = set(mapping.values()) - - for col in row.index: - if col not in mapped_columns and not pd.isna(row[col]): - additional[col] = row[col] - - return additional - - def _make_dataframe(self, bio) -> pd.DataFrame: - # Получаем все данные из файла - sheet_name = self.config.get("sheet_name", 0) - df_full = pd.read_excel(bio, sheet_name=sheet_name, header=None) - - # Находим индекс строки с заголовком - key_field = self.config.get("key_field") - header_row_idx = df_full[ - df_full.apply(lambda row: row.astype(str).str.contains(key_field, case=False, na=False).any(), - axis=1)].index[0] - - # Считываем таблицу с правильным заголовком - df = pd.read_excel(bio, header=header_row_idx, sheet_name=sheet_name, engine='calamine') # openpyxl calamine - - # Находим индекс первой строки с пустым 'Артикул' - first_empty_index = df[df[key_field].isna()].index.min() - - # Обрезаем DataFrame до первой пустой строки (не включая её) - df_trimmed = df.loc[:first_empty_index - 1] - - return df_trimmed diff --git a/src/mail_order_bot/task_processor/handlers/excel_parcers/deprecated/sheet_parcer.py b/src/mail_order_bot/task_processor/handlers/excel_parcers/deprecated/sheet_parcer.py deleted file mode 100644 index e24b500..0000000 --- a/src/mail_order_bot/task_processor/handlers/excel_parcers/deprecated/sheet_parcer.py +++ /dev/null @@ -1,118 +0,0 @@ -import logging -import pandas as pd -from typing import Dict, Any, Optional -from decimal import Decimal -from io import BytesIO -#from mail_order_bot.task_processor.handlers.order_position import OrderPosition -from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask - -from ...order.auto_part_position import AutoPartPosition - -logger = logging.getLogger(__name__) - - -class BasicExcelParser(AbstractTask): - RESULT_SECTION = "positions" - """ - Универсальный парсер, настраиваемый через конфигурацию. - Подходит для большинства стандартных случаев. - """ - - def do(self) -> None: - - # todo сделать проверку на наличие файла и его тип - file_bytes = BytesIO(self.context.get("attachment").content) # self.context.get("attachment") # - try: - df = self._make_dataframe(file_bytes) - # Получаем маппинг колонок из конфигурации - mapping = self.config['mapping'] - - # Парсим строки - positions = [] - for idx, row in df.iterrows(): - try: - position = self._parse_row(row, mapping) - if position: - positions.append(position) - self.order.add_position(position) - except Exception as e: - logger.error(f"Ошибка парсинга строки {idx}: {e}, {row}") - continue - - logger.info(f"Успешно обработано {len(positions)} позиций из {len(df)} строк") - - self.context[self.RESULT_SECTION] = positions - - - except Exception as e: - logger.error(f"Ошибка при обработке файла: {e}") - raise Exception from e - - def _parse_row(self, row: pd.Series, mapping: Dict[str, str]) -> Optional[AutoPartPosition]: - """Парсит одну строку Excel в OrderPosition""" - - # Проверяем обязательные поля - required_fields = ['article', 'price', 'quantity'] - - for field in required_fields: - if pd.isna(row.get(mapping[field])): - logger.warning(f"Позиция не создана - не заполнено поле {mapping[field]}") - return None - - price = Decimal(str(row[mapping['price']]).replace(",", ".").strip()) - quantity = int(row[mapping['quantity']]) - - if "total" in mapping.keys(): - total = Decimal(str(row[mapping['total']]).replace(",", ".").strip()) - else: - total = price * quantity - - if mapping.get('name', "") in mapping.keys(): - name = str(row[mapping.get('name', "")]).strip() - else: - name = "" - - # Создаем объект позиции - position = AutoPartPosition( - sku=str(row[mapping['article']]).strip(), - manufacturer=str(row[mapping.get('manufacturer', "")]).strip(), - name=name, - requested_price=price, - requested_quantity=quantity, - total=total, - additional_attrs=self._extract_additional_attrs(row, mapping) - ) - return position - - def _extract_additional_attrs(self, row: pd.Series, mapping: Dict[str, str]) -> Dict[str, Any]: - """Извлекает дополнительные атрибуты, не входящие в основную модель""" - additional = {} - mapped_columns = set(mapping.values()) - - for col in row.index: - if col not in mapped_columns and not pd.isna(row[col]): - additional[col] = row[col] - - return additional - - def _make_dataframe(self, bio) -> pd.DataFrame: - # Получаем все данные из файла - sheet_name = self.config.get("sheet_name", 0) - df_full = pd.read_excel(bio, sheet_name=sheet_name, header=None) - - # Находим индекс строки с заголовком - key_field = self.config.get("key_field") - header_row_idx = df_full[ - df_full.apply(lambda row: row.astype(str).str.contains(key_field, case=False, na=False).any(), - axis=1)].index[0] - - # Считываем таблицу с правильным заголовком - df = pd.read_excel(bio, header=header_row_idx, sheet_name=sheet_name, engine='calamine') # openpyxl calamine - - # Находим индекс первой строки с пустым 'Артикул' - first_empty_index = df[df[key_field].isna()].index.min() - - # Обрезаем DataFrame до первой пустой строки (не включая её) - df_trimmed = df.loc[:first_empty_index - 1] - - return df_trimmed diff --git a/src/mail_order_bot/task_processor/handlers/excel_parcers/excel_extractor.py b/src/mail_order_bot/task_processor/handlers/excel_parcers/excel_extractor.py index 90450ac..b36726f 100644 --- a/src/mail_order_bot/task_processor/handlers/excel_parcers/excel_extractor.py +++ b/src/mail_order_bot/task_processor/handlers/excel_parcers/excel_extractor.py @@ -19,23 +19,18 @@ class ExcelExtractor(AbstractTask): super().__init__(*args, **kwargs) self.excel_config = self.config.get("excel", {}) - def do(self) -> None: + def do(self, attachment) -> None: + try: + file_bytes = BytesIO(attachment['bytes']) + excel_file = ExcelFileParcer(file_bytes, self.excel_config) - # todo сделать проверку на наличие файла и его тип + except Exception as e: + logger.error(f"Не удалось распарсить файл: \n{e}") + attachment["excel"] = None - attachments = self.context.data.get("attachments", []) - - for attachment in attachments: - try: - file_bytes = BytesIO(attachment['bytes']) - excel_file = ExcelFileParcer(file_bytes, self.excel_config) - - except Exception as e: - logger.warning(f"Не удалось прочитать файл {attachment['name']}: {e}") - attachment["excel"] = None - - else: - attachment["excel"] = excel_file + else: + attachment["excel"] = excel_file + logger.warning(f"Произведен успешный парсинг файла") diff --git a/src/mail_order_bot/task_processor/handlers/excel_parcers/order_extractor.py b/src/mail_order_bot/task_processor/handlers/excel_parcers/order_extractor.py index 1b29692..f68f710 100644 --- a/src/mail_order_bot/task_processor/handlers/excel_parcers/order_extractor.py +++ b/src/mail_order_bot/task_processor/handlers/excel_parcers/order_extractor.py @@ -18,25 +18,27 @@ class OrderExtractor(AbstractTask): self.excel_config = self.config.get("excel", {}) - def do(self) -> None: - + def do(self, attachment) -> None: # todo сделать проверку на наличие файла и его тип - attachments = self.context.data.get("attachments", []) - for attachment in attachments: - + 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") + order_parcer = OrderParser(mapping, delivery_period, client_id) order_dataframe = excel_file.get_order_rows() - order = order_parcer.parse(order_dataframe) + + except Exception as e: + logger.error(f"Ошибка при парсинге заказа файла: \n{e}") + + else: attachment["order"] = order - + logger.warning(f"Обработан файл с заказом, извлечено позиций, {len(order.positions)}") diff --git a/src/mail_order_bot/task_processor/handlers/excel_parcers/update_excel_file.py b/src/mail_order_bot/task_processor/handlers/excel_parcers/update_excel_file.py index 80a7bb5..1efbad1 100644 --- a/src/mail_order_bot/task_processor/handlers/excel_parcers/update_excel_file.py +++ b/src/mail_order_bot/task_processor/handlers/excel_parcers/update_excel_file.py @@ -20,11 +20,10 @@ class UpdateExcelFile(AbstractTask): super().__init__(*args, **kwargs) self.excel_config = self.config.get("excel", {}) - def do(self) -> None: + def do(self, attachment) -> None: # todo сделать проверку на наличие файла и его тип - attachments = self.context.data.get("attachments", []) - for attachment in attachments: + try: excel_file = attachment.get("excel") order = attachment.get("order") config = self.context.data.get("config", {}) @@ -47,5 +46,9 @@ class UpdateExcelFile(AbstractTask): 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 успешно обновлен") diff --git a/src/mail_order_bot/task_processor/handlers/stock_selectors/stock_selector.py b/src/mail_order_bot/task_processor/handlers/stock_selectors/stock_selector.py index fb14292..a9fe4d7 100644 --- a/src/mail_order_bot/task_processor/handlers/stock_selectors/stock_selector.py +++ b/src/mail_order_bot/task_processor/handlers/stock_selectors/stock_selector.py @@ -35,10 +35,10 @@ class StockSelector(AbstractTask): client_login, client_password = credential_provider.get_system_credentials() self.client_provider = AbcpProvider(login=client_login, password=client_password) - def do(self) -> None: + def do(self, attachment) -> None: # todo сделать проверку на наличие файла и его тип - attachments = self.context.data.get("attachments", []) - for attachment in attachments: + + try: order = attachment.get("order", None) delivery_period = attachment.get("delivery_period") for position in order.positions: @@ -63,6 +63,11 @@ class StockSelector(AbstractTask): # Мне не очень нравится управление статусами в этом месте, кажется что лучше это делать внутри AutoPartPosition else: position.status = PositionStatus.STOCK_FAILED + except Exception as e: + logger.error(f"Ошибка при выборе позиции со складов: {e}") + + else: + logger.warning("Определены оптимальные позиции со складов") diff --git a/src/mail_order_bot/task_processor/processor.py b/src/mail_order_bot/task_processor/processor.py index 0bf72c4..b466ffb 100644 --- a/src/mail_order_bot/task_processor/processor.py +++ b/src/mail_order_bot/task_processor/processor.py @@ -43,7 +43,8 @@ from enum import Enum from mail_order_bot.task_processor.handlers import * from mail_order_bot.task_processor.handlers.email.email_parcer import EmailParcer -from mail_order_bot import MailOrderBotException +from mail_order_bot.task_processor.message import LogMessage, LogMessageLevel, LogMessageStorage + logger = logging.getLogger(__name__) @@ -82,12 +83,20 @@ class TaskProcessor: config = self._load_config(email_sender) self.context.data["config"] = config - # Запустить обработку пайплайна pipeline = config["pipeline"] - for handler_name in pipeline: - logger.info(f"Processing handler: {handler_name}") - task = globals()[handler_name]() - task.do() + + attachments = self.context.data.get("attachments", []) + for attachment in attachments: + file_name = attachment["name"] + logger.warning(f"Начата обработка файла: {file_name} =>") + + attachment["log_messages"] = LogMessageStorage(file_name) + + # Запустить обработку пайплайна + for handler_name in pipeline: + logger.info(f"Processing handler: {handler_name}") + task = globals()[handler_name]() + task.do(attachment) except Exception as e: logger.error(f"Произошла ошибка: {e}")