3 Commits

Author SHA1 Message Date
55ba627f6b no message 2025-12-23 23:12:43 +03:00
b26485c5cc Update code structure and improve readability 2025-12-22 22:48:57 +03:00
0022141684 no message 2025-12-14 16:23:40 +03:00
35 changed files with 333 additions and 125 deletions

View File

@@ -0,0 +1,5 @@
"""
Классы для работы с API платформы ABCP
"""
from .abcp_provider import AbcpProvider

View File

@@ -31,6 +31,9 @@ class AbcpProvider:
params = {"number": sku, "brand": manufacturer, "withOutAnalogs": "1"} params = {"number": sku, "brand": manufacturer, "withOutAnalogs": "1"}
return self._execute(path, method, params) return self._execute(path, method, params)
def save_order(self, order):
pass
def _execute(self, path, method="GET", params={}, data=None): def _execute(self, path, method="GET", params={}, data=None):
params["userlogin"] = self.login params["userlogin"] = self.login
params["userpsw"] = hashlib.md5(self.password.encode("utf-8")).hexdigest() params["userpsw"] = hashlib.md5(self.password.encode("utf-8")).hexdigest()

View File

@@ -1,3 +1,13 @@
#=========================================
config:
pipeline:
-
#=========================================
pipeline: pipeline:
# Настраиваем парсинг экселя # Настраиваем парсинг экселя
- handler: BasicExcelParser - handler: BasicExcelParser

View File

@@ -1,2 +1,3 @@
from .client import EmailClient from .client import EmailClient
from .objects import EmailMessage, EmailAttachment from .objects import EmailMessage, EmailAttachment
from .utils import EmailUtils

View File

@@ -11,6 +11,10 @@ from email.header import decode_header
import imaplib import imaplib
import smtplib import smtplib
import logging
logger = logging.getLogger(__name__)
# from .objects import EmailMessage, EmailAttachment # from .objects import EmailMessage, EmailAttachment

View File

@@ -1,16 +1,11 @@
from email.header import decode_header, make_header
import re import re
from datetime import datetime from typing import List
from typing import List, Optional
from dataclasses import dataclass
import email import email
from email import encoders from email.header import make_header, decode_header
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart import logging
from email.mime.base import MIMEBase
from email.header import decode_header logger = logging.getLogger(__name__)
import imaplib
import smtplib
from .objects import EmailMessage, EmailAttachment from .objects import EmailMessage, EmailAttachment
@@ -99,8 +94,3 @@ class EmailUtils:
return None return None
# убираем пробелы по краям и берём часть после '@' # убираем пробелы по краям и берём часть после '@'
return email_message.strip().split("@", 1)[1] return email_message.strip().split("@", 1)[1]

View File

@@ -1 +0,0 @@
from .processor import EmailProcessor

View File

@@ -64,6 +64,7 @@ async def main():
#await app.stop() #await app.stop()
if __name__ == "__main__": if __name__ == "__main__":
print(os.getcwd())
if os.environ.get("APP_ENV") != "PRODUCTION": if os.environ.get("APP_ENV") != "PRODUCTION":
logger.warning("Non production environment") logger.warning("Non production environment")
load_dotenv() load_dotenv()

View File

@@ -1,2 +1,3 @@
"""Классы для работы с сущностью заказа и позиции"""
from .auto_part_order import AutoPartOrder, OrderStatus from .auto_part_order import AutoPartOrder, OrderStatus
from .auto_part_position import AutoPartPosition, PositionStatus from .auto_part_position import AutoPartPosition, PositionStatus

View File

@@ -1,8 +1,11 @@
from typing import List, Optional from typing import List, Optional
from .auto_part_position import AutoPartPosition, PositionStatus from .auto_part_position import AutoPartPosition, PositionStatus
from mail_order_bot.task_processor.handlers.abcp.stock_selector import StockSelector
from enum import Enum from enum import Enum
class OrderStatus(Enum): class OrderStatus(Enum):
NEW = "new" NEW = "new"
IN_PROGRESS = "in progress" IN_PROGRESS = "in progress"
@@ -13,7 +16,8 @@ class OrderStatus(Enum):
class AutoPartOrder: class AutoPartOrder:
def __init__(self): def __init__(self, client_id):
self.client_id = client_id
self.positions: List[AutoPartPosition] = [] self.positions: List[AutoPartPosition] = []
self.status = OrderStatus.NEW self.status = OrderStatus.NEW
self.delivery_period = 0 self.delivery_period = 0
@@ -39,14 +43,12 @@ class AutoPartOrder:
Выбирает оптимального поставщика для всех позиций заказа. Выбирает оптимального поставщика для всех позиций заказа.
Предполагается, что остатки уже получены и обработаны. Предполагается, что остатки уже получены и обработаны.
""" """
from mail_order_bot.email_processor.handlers.abcp.stock_selector import StockSelector
for position in self.positions: for position in self.positions:
selector = StockSelector(position, self.delivery_period) selector = StockSelector(position, self.delivery_period)
selector.select_optimal_supplier() selector.select_optimal_supplier()
def check_order(self, config) -> None: def check_order(self, config) -> None:
""" Проверяет заказ на возможность исполнения""" """ Проверяет заказ на возможность исполнения"""
# 1. Проверка общего количества отказов # 1. Проверка общего количества отказов
@@ -60,6 +62,5 @@ class AutoPartOrder:
f"({refusal_positions_count} из {len(self.positions)})") f"({refusal_positions_count} из {len(self.positions)})")
self.status = OrderStatus.OPERATOR_REQUIRED self.status = OrderStatus.OPERATOR_REQUIRED
def __len__(self): def __len__(self):
return len(self.positions) return len(self.positions)

View File

@@ -1,26 +1,28 @@
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
from decimal import Decimal from decimal import Decimal
from enum import Enum from enum import Enum
class PositionStatus(Enum): class PositionStatus(Enum):
NEW = "new" # Новая позиция NEW = "new" # Новая позиция
STOCK_RECIEVED = "stock_received" # Получен остаток STOCK_RECIEVED = "stock_received" # Получен остаток
STOCK_FAILED = "stock_failed" # Остаток не получен STOCK_FAILED = "stock_failed" # Остаток не получен
NO_AVAILABLE_STOCK = "no_available_stock" #Нет доступных складов NO_AVAILABLE_STOCK = "no_available_stock" # Нет доступных складов
READY = "ready" READY = "ready"
ORDERED = "ordered" # Заказано ORDERED = "ordered" # Заказано
REFUSED = "refused" # Отказано REFUSED = "refused" # Отказано
@dataclass
class AutoPartPosition: class AutoPartPosition:
""" """
Унифицированная модель позиции для заказа. Унифицированная модель позиции для заказа.
Все контрагенты приводятся к этой структуре. Все контрагенты приводятся к этой структуре.
""" """
DISTRIBUTOR_ID = "1577730" # ID локального склада DISTRIBUTOR_ID = 1577730 # ID локального склада
sku: str # Артикул товара sku: str # Артикул товара
manufacturer: str # Производитель manufacturer: str # Производитель
@@ -30,7 +32,7 @@ class AutoPartPosition:
total: Decimal = 0 # Общая сумма total: Decimal = 0 # Общая сумма
name: str = "" # Наименование name: str = "" # Наименование
order_delivery_period: int = 0
order_quantity: int = 0 # Количество для заказа order_quantity: int = 0 # Количество для заказа
order_price: Decimal = Decimal('0.0') # Цена в заказе order_price: Decimal = Decimal('0.0') # Цена в заказе
order_item: Dict[str, Any] = field(default_factory=dict) order_item: Dict[str, Any] = field(default_factory=dict)
@@ -49,83 +51,59 @@ class AutoPartPosition:
raise ValueError(f"Цена не может быть отрицательной: {self.requested_price}") raise ValueError(f"Цена не может быть отрицательной: {self.requested_price}")
def set_stock(self, stock): def set_stock(self, stock):
if stock is not None: if stock.get("success"):
self.stock = stock self.stock = stock["data"]
if len(self.stock): if len(self.stock):
self.status = PositionStatus.STOCK_RECIEVED self.status = PositionStatus.STOCK_RECIEVED
else: else:
self.status = PositionStatus.NO_AVAILABLE_STOCK self.status = PositionStatus.NO_AVAILABLE_STOCK
else: else:
self.status = PositionStatus.STOCK_FAILED self.status = PositionStatus.STOCK_FAILED
def set_order_item(self): def set_order_item(self):
"""Выбирает позицию для заказа""" """Выбирает позицию для заказа"""
if self.status == PositionStatus.STOCK_RECIEVED: if self.status == PositionStatus.STOCK_RECIEVED:
available_distributors = self.stock available_distributors = self.stock
# BR-1. Отсекаем склады для заказов из наличия (только локальный склад) # BR-1. Отсекаем склады для заказов из наличия (только локальный склад)
if self.delivery_period == 0: if self.order_delivery_period == 0:
available_distributors = self._filter_only_local_storage(available_distributors) available_distributors = self._filter_only_local_storage(available_distributors)
# BR-2. Цена не должна превышать цену из заказа # BR-2. Цена не должна превышать цену из заказа
available_distributors = self._filter_proper_price(available_distributors) available_distributors = self._filter_proper_price(available_distributors)
# BR-3. Срок доставки не должен превышать ожидаемый # BR-3. Срок доставки не должен превышать ожидаемый
available_distributors = self._filter_proper_delivery_time(available_distributors, self.delivery_period) available_distributors = self._filter_proper_delivery_time(available_distributors)
# BR-4. Без отрицательных остатков # BR-4. Без отрицательных остатков
available_distributors = self._filter_proper_availability(available_distributors) available_distributors = self._filter_proper_availability(available_distributors)
# Приоритет на склады с полным стоком # Приоритет на склады с полным стоком
# BR-5. Сначала оборачиваем локальный склад, потом удаленные # BR-5. Сначала оборачиваем локальный склад, потом удаленные
# BR-6. Выбираем цену максимально близкую к цене из заказа (максимальная) # BR-6. Выбираем цену максимально близкую к цене из заказа (максимальная)
available_distributors.sort(key=lambda item: Decimal(item["price"]), reverse=True) available_distributors.sort(key=lambda item: Decimal(item["price"]), reverse=True)
# BR-7. if len(available_distributors):
self.order_item = self.stock[0]
self.status = PositionStatus.READY
for stock_item in self.stock: else:
available_quantity = min(self.requested_quantity, stock_item["availability"]) self.status = PositionStatus.NO_AVAILABLE_STOCK
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]
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]]:
"""Фильтрует только локальные склады""" """Фильтрует только локальные склады"""
return [item for item in distributors if str(item["distributorId"]) == self.DISTRIBUTOR_ID]
return [item for item in distributors if item["distributorId"] == self.DISTRIBUTOR_ID]
def _filter_proper_delivery_time(self, distributors: List[Dict[str, Any]], delivery_period: int) -> List[Dict[str, Any]]:
def _filter_proper_delivery_time(self, distributors: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Фильтрует склады по сроку доставки""" """Фильтрует склады по сроку доставки"""
return [item for item in distributors if item["deliveryPeriod"] <= delivery_period] return [item for item in distributors if item["deliveryPeriod"] <= self.order_delivery_period]
def _filter_proper_price(self, distributors: List[Dict[str, Any]]) -> List[Dict[str, Any]]: 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] return [item for item in distributors if Decimal(item["price"]) <= self.requested_price]
def _filter_proper_availability(self, distributors: List[Dict[str, Any]]) -> List[Dict[str, Any]]: 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] 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

@@ -0,0 +1,4 @@
"""Данный пакет содержит модули и классы дял парсинга объектов"""
from .excel_parcer import ExcelFileParcer
from .order_parcer import OrderParser

View File

@@ -0,0 +1,106 @@
import logging
import pandas as pd
from io import BytesIO
logger = logging.getLogger(__name__)
class ExcelFileParcer:
def __init__(self, file_bytes, config):
self.config = config
self.bytes = file_bytes
self.sheet_name = self.config.get("sheet_name", 0)
self.df = self._parse_file(file_bytes)
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
logger.warning("Не удалось распарсить значение файла")
return df
def set_value(self, sku, manufacturer, column, value):
"""Устанавливает значение в строке позиции в заданной колонке"""
# Находим строку (ось Y)
attr_row = self._get_attr_row(sku, manufacturer)
# Находим колонку (ось X)
attr_col = self._get_attr_column(column)
self.df.iloc[attr_row, attr_col] = value
logger.debug(
f"Установлено значение {value} в колонке {column} для строки {attr_row} ( {sku} | {manufacturer} )")
def get_file_bytes(self):
"Этот метод будет возвращать эксель из датафрейма в виде байтов"
buf = BytesIO()
with pd.ExcelWriter(buf, engine="xlsxwriter") as writer:
self.df.to_excel(writer, sheet_name="Sheet1", index=False, header=False)
buf.seek(0)
return buf
def get_order_rows(self):
"Будет такой метод или какой-то другой который формирует файл с заказом"
# Получаем все данные из файла
# Находим индекс строки с заголовком
key_field = self.config.get("key_field")
header_row_idx = self.df[
self.df.apply(lambda row: row.astype(str).str.contains(key_field, case=False, na=False).any(),
axis=1)].index[0]
# Считываем таблицу с правильным заголовком
df = pd.read_excel(self.bytes, header=header_row_idx, sheet_name=self.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
def _get_header_row(self):
"""Метод возвращает строку заголовка по наличию в ней ключевого слова. Поиск заголовка нужен при определении колонок с данными."""
key_column = self.config.get("key_field")
header_row_idx = int(
self.df.apply(lambda row: row.astype(str).str.contains(key_column, na=False).any(), axis=1).idxmax())
# todo надо выкинуть ошибку если в файле не найдено ключевое поле
# todo надо выкинуть ошибку если найдено несколько строк с CONFIG_KEY_COLUMN
logger.debug(f"Индекс строки заголовка - {header_row_idx}")
return header_row_idx
def _get_attr_column(self, col_name):
"""Поиск по оси Х - метод возвращает индекс колонки по названию атрибута"""
header_row_idx = self._get_header_row()
header_row = self.df.iloc[header_row_idx]
col_id = header_row[header_row == col_name].index[0]
# todo добавить перехват ошибок и выброс понятного и сключения при отсутствии колонки
logger.debug(f"Индекс колонки {col_name} - {col_id}")
return int(col_id)
def _get_attr_row(self, sku, manufacturer):
"""Поиск по оси Y - метод возвращает индекс строки по бренду и артикулу"""
sku_col_name = self.config["mapping"]["article"]
sku_col_idx = self._get_attr_column(sku_col_name)
man_col_name = self.config["mapping"]["manufacturer"]
man_col_idx = self._get_attr_column(man_col_name)
matching_rows = self.df[
(self.df.iloc[:, sku_col_idx] == sku) & (self.df.iloc[:, man_col_idx] == manufacturer)].index
# todo сделать проверку на наличие дублей
logger.info(f"Индекс строки позиции {sku}/{manufacturer} - {matching_rows}")
return matching_rows.values[0]

View File

@@ -0,0 +1,83 @@
import logging
import pandas as pd
from typing import Dict, Any, Optional
from decimal import Decimal
from io import BytesIO
from mail_order_bot.order import AutoPartPosition
logger = logging.getLogger(__name__)
class OrderParser:
"""
Универсальный парсер, настраиваемый через конфигурацию.
Подходит для большинства стандартных случаев.
"""
def __init__(self, mapping, delivery_period):
self.mapping = mapping
self.delivery_period = delivery_period
def parse(self, order, df):
# Парсим строки
positions = []
for idx, row in df.iterrows():
position = self._parse_row(row, self.mapping)
if position:
order.add_position(position)
logger.info(f"Успешно обработано {len(order)} позиций из {len(df)} строк")
# except Exception as e:
# logger.error(f"Ошибка при обработке файла: {e}")
# else:
return 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,
order_delivery_period=self.delivery_period,
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

View File

@@ -0,0 +1 @@
from .processor import TaskProcessor

View File

@@ -1,13 +1,8 @@
from .attachment_handler.attachment_handler import AttachmentHandler from .attachment_handler.attachment_handler import AttachmentHandler
from .excel_parcers.order_parcer_basic import BasicExcelParser from .excel_parcers.order_parcer_basic import BasicExcelParser
from .destination_time.local_store import DeliveryPeriodLocalStore from .destination_time.local_store import DeliveryPeriodLocalStore
from .abcp.api_get_stock import APIGetStock
from .notifications.test_notifier import TestNotifier from .notifications.test_notifier import TestNotifier

View File

@@ -1,9 +1,8 @@
import logging import logging
from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask from mail_order_bot.task_processor.abstract_task import AbstractTask
from .abcp_provider import AbcpProvider from mail_order_bot.abcp_api.abcp_provider import AbcpProvider
from mail_order_bot.credential_provider import CredentialProvider from mail_order_bot.credential_provider import CredentialProvider
from mail_order_bot.email_processor.handlers.abcp.stock_selector import StockSelector
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -12,11 +11,7 @@ class APIGetStock(AbstractTask):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
credential_provider = CredentialProvider(context=self.context) credential_provider = CredentialProvider(context=self.context)
# Создаем провайдер для системной учетной записи
system_login, system_password = credential_provider.get_system_credentials()
self.system_provider = AbcpProvider(login=system_login, password=system_password)
# Создаем провайдер для учетной записи клиента # Создаем провайдер для учетной записи клиента
client_login, client_password = credential_provider.get_client_credentials() client_login, client_password = credential_provider.get_client_credentials()
self.client_provider = AbcpProvider(login=client_login, password=client_password) self.client_provider = AbcpProvider(login=client_login, password=client_password)
@@ -28,13 +23,8 @@ 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)
position.set_stock(client_stock)
# Используем StockSelector для фильтрации неподходящих поставщиков
selector = StockSelector(position, order.delivery_period)
available_distributors = selector.filter_stock(client_stock, system_stock)
position.set_stock(available_distributors)
position.set_order_item() position.set_order_item()
logger.info(f"Получены позиции со склада для файла {attachment.get('name', "no name")}") logger.info(f"Получены позиции со склада для файла {attachment.get('name', "no name")}")

View File

@@ -1,4 +1,4 @@
from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask from mail_order_bot.task_processor.abstract_task import AbstractTask
from mail_order_bot.email_client.utils import EmailUtils from mail_order_bot.email_client.utils import EmailUtils
import logging import logging

View File

@@ -1,5 +1,4 @@
from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask from mail_order_bot.task_processor.abstract_task import AbstractTask
from mail_order_bot.email_client.utils import EmailUtils
import logging import logging
import re import re

View File

@@ -1,5 +1,4 @@
from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask from mail_order_bot.task_processor.abstract_task import AbstractTask
from mail_order_bot.email_client.utils import EmailUtils
import logging import logging

View File

@@ -3,16 +3,16 @@ import pandas as pd
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from decimal import Decimal from decimal import Decimal
from io import BytesIO from io import BytesIO
#from mail_order_bot.email_processor.handlers.order_position import OrderPosition #from mail_order_bot.task_processor.handlers.order_position import OrderPosition
from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask from mail_order_bot.task_processor.abstract_task import AbstractTask
from ...order.auto_part_position import AutoPartPosition from mail_order_bot.task_processor.order.auto_part_position import AutoPartPosition
from ...order.auto_part_order import AutoPartOrder from mail_order_bot.task_processor.order.auto_part_order import AutoPartOrder
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class BasicExcelParser(AbstractTask): class OrderParser(AbstractTask):
""" """
Универсальный парсер, настраиваемый через конфигурацию. Универсальный парсер, настраиваемый через конфигурацию.
Подходит для большинства стандартных случаев. Подходит для большинства стандартных случаев.
@@ -27,24 +27,28 @@ class BasicExcelParser(AbstractTask):
attachments = self.context.data.get("attachments", []) attachments = self.context.data.get("attachments", [])
for attachment in attachments: for attachment in attachments:
file_bytes = BytesIO(attachment['bytes']) # self.context.get("attachment") # file_bytes = BytesIO(attachment['bytes']) # self.context.get("attachment") #
try: delivery_period = attachment.get("delivery_period", 0)
df = self._make_dataframe(file_bytes) #try:
mapping = self.config['mapping'] df = self._make_dataframe(file_bytes)
order = AutoPartOrder() mapping = self.config["mapping"]
client_id = self.config["client_id"]
order = AutoPartOrder()
attachment["order"] = order
# Парсим строки # Парсим строки
positions = [] positions = []
for idx, row in df.iterrows(): for idx, row in df.iterrows():
position = self._parse_row(row, mapping) position = self._parse_row(row, mapping)
if position: if position:
order.add_position(position) position.order_delivery_period = delivery_period
order.add_position(position)
logger.info(f"Успешно обработано {len(order)} позиций из {len(df)} строк") logger.info(f"Успешно обработано {len(order)} позиций из {len(df)} строк")
except Exception as e: #except Exception as e:
logger.error(f"Ошибка при обработке файла: {e}") # logger.error(f"Ошибка при обработке файла: {e}")
else: #else:
attachment["order"] = order attachment["order"] = order
@@ -67,7 +71,7 @@ class BasicExcelParser(AbstractTask):
else: else:
total = price * quantity total = price * quantity
if mapping.get('name', "") in mapping.keys(): if "name" in mapping:
name = str(row[mapping.get('name', "")]).strip() name = str(row[mapping.get('name', "")]).strip()
else: else:
name = "" name = ""

View File

@@ -3,7 +3,7 @@ import pandas as pd
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from decimal import Decimal from decimal import Decimal
from io import BytesIO from io import BytesIO
#from mail_order_bot.email_processor.handlers.order_position import OrderPosition #from mail_order_bot.task_processor.handlers.order_position import OrderPosition
from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask
from ...order.auto_part_position import AutoPartPosition from ...order.auto_part_position import AutoPartPosition

View File

@@ -0,0 +1,34 @@
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
logger = logging.getLogger(__name__)
class ExcelExtractor(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'])
# Получаем все данные из файла
sheet_name = self.config.get("sheet_name", 0)
try:
attachment["sheet"] = pd.read_excel(file_bytes, sheet_name=sheet_name, header=None)
except Exception as e:
attachment["sheet"] = None
logger.warning("Не удалось распарсить значение файла")

View File

@@ -8,9 +8,9 @@ from mail_order_bot.context import Context
from mail_order_bot.email_client.utils import EmailUtils from mail_order_bot.email_client.utils import EmailUtils
from enum import Enum from enum import Enum
from mail_order_bot.email_processor.handlers import * from mail_order_bot.task_processor.handlers import *
from mail_order_bot.email_processor.handlers import AttachmentHandler from mail_order_bot.task_processor.handlers import AttachmentHandler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -24,7 +24,7 @@ class RequestStatus(Enum):
INVALID = "invalid" INVALID = "invalid"
class EmailProcessor: class TaskProcessor:
def __init__(self, configs_path: str): def __init__(self, configs_path: str):
super().__init__() super().__init__()
self.context = Context() self.context = Context()
@@ -45,7 +45,7 @@ class EmailProcessor:
email_from = EmailUtils.extract_first_sender(email_body) email_from = EmailUtils.extract_first_sender(email_body)
self.context.data["email_from"] = email_from self.context.data["email_from"] = email_from
email_subj = EmailUtils.extract_header(email_body, "Subject") email_subj = EmailUtils.extract_header(email, "subj")
self.context.data["email_subj"] = email_subj self.context.data["email_subj"] = email_subj
client = EmailUtils.extract_domain(email_from) client = EmailUtils.extract_domain(email_from)