10 Commits

33 changed files with 1290 additions and 596 deletions

5
.gitignore vendored
View File

@@ -3,3 +3,8 @@
__pycache__ __pycache__
.env .env
.cursorignore .cursorignore
logs/
files/
Настроено/
Не настроено/
проблемные/

View File

@@ -1,11 +1,12 @@
Metadata-Version: 2.4 Metadata-Version: 2.4
Name: MailOrderBot Name: MailOrderBot
Version: 1.0.2 Version: 1.0.4
Summary: Config manager for building applications Summary: Config manager for building applications
Author-email: Aleksei Zosimov <lesha.spb@gmail.com> Author-email: Aleksei Zosimov <lesha.spb@gmail.com>
Project-URL: Homepage, https://git.lesha.spb.ru/alex/config_manager Project-URL: Homepage, https://git.lesha.spb.ru/alex/mail_order_bot
Project-URL: Documentation, https://git.lesha.spb.ru/alex/config_manager Project-URL: Documentation, https://git.lesha.spb.ru/alex/mail_order_bot
Project-URL: Repository, https://git.lesha.spb.ru/alex/config_manager Project-URL: Repository, https://git.lesha.spb.ru/alex/mail_order_bot
Requires-Python: >=3.12 Requires-Python: >=3.12
Description-Content-Type: text/markdown Description-Content-Type: text/markdown
Requires-Dist: python-dotenv>=1.0.0 Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: config_manager@ git+https://git.lesha.spb.ru/alex/config_manager.git@master

View File

@@ -8,10 +8,11 @@ src/MailOrderBot.egg-info/top_level.txt
src/mail_order_bot/__init__.py src/mail_order_bot/__init__.py
src/mail_order_bot/main.py src/mail_order_bot/main.py
src/mail_order_bot/email_client/__init__.py src/mail_order_bot/email_client/__init__.py
src/mail_order_bot/email_client/email_client.py src/mail_order_bot/email_client/client.py
src/mail_order_bot/email_client/email_objects.py src/mail_order_bot/email_client/objects.py
src/mail_order_bot/excel_processor/__init__.py
src/mail_order_bot/excel_processor/configurable_parser.py src/mail_order_bot/excel_processor/configurable_parser.py
src/mail_order_bot/excel_processor/excel_parser.py src/mail_order_bot/excel_processor/excel_parser.py
src/mail_order_bot/excel_processor/excel_processor.py
src/mail_order_bot/excel_processor/order_position.py src/mail_order_bot/excel_processor/order_position.py
src/mail_order_bot/excel_processor/parser_factory.py src/mail_order_bot/excel_processor/parser_factory.py
src/mail_order_bot/excel_processor/processor.py

View File

@@ -1 +1,2 @@
python-dotenv>=1.0.0 python-dotenv>=1.0.0
config_manager@ git+https://git.lesha.spb.ru/alex/config_manager.git@master

View File

@@ -1,11 +1,67 @@
# === Раздел с общими конфигурационными параметрами === # Настройки обработки =================================================================
suppliers:
# Контрагент A - стандартный формат
autostels:
sheet_name: "Лист1" # Название листа Excel
header_row: 2 # Номер строки с заголовками (0 = первая)
update_interval: 2 # Маппинг: внутреннее_поле -> названиеолонки_в_Excel
work_interval: 2 mapping:
article: "№ Детали"
manufacturer: "Производитель"
name: "Наименование"
price: "Прайс"
quantity: "Количество"
total: "Сумма"
# Дополнительные настройки (опционально)
options:
decimal_separator: ","
encoding: "utf-8"
# Контрагент B - формат с английскими названиями
parterra:
sheet_name: "TDSheet"
header_row: 6 # Заголовки во второй строке
mapping:
article: "Артикул поставщика"
manufacturer: "Производитель Поставщика"
name: "Номенклатура"
price: "Цена"
quantity: "Количество (в единицах хранения)"
total: "Сумма с НДС"
options:
decimal_separator: ","
encoding: "utf-8"
#thousand_separator: ","
# Контрагент C - с запятой как разделителем
part-kom:
sheet_name: "Лист_1" # Можно указать индекс листа
header_row: 5
mapping:
article: "Артикул"
manufacturer: "Изготовитель"
name: "Наименование товара"
price: "Цена"
quantity: "Кол-во"
total: "Сумма"
options:
#skip_footer_rows: 3
decimal_separator: ","
# Раздел с общими конфигурационными параметрами ===============================
update_interval: 10
work_interval: 30
email_dir: "spareparts" email_dir: "spareparts"
# === Логирование === # Логирование =================================================================
log: log:
version: 1 version: 1
disable_existing_loggers: False disable_existing_loggers: False
@@ -32,26 +88,27 @@ log:
maxBytes: 500000 maxBytes: 500000
backupCount: 10 backupCount: 10
#telegram: telegram:
# level: CRITICAL level: CRITICAL
# formatter: telegram formatter: telegram
# class: logging_telegram_handler.TelegramHandler class: logging_telegram_handler.TelegramHandler
# chat_id: 211945135 chat_id: 211945135
# alias: "PDC" alias: "Mail order bot"
# -- Логгеры -- # Логгеры
loggers: loggers:
'': '':
handlers: [console, file] handlers: [console, file, telegram]
level: INFO level: INFO
propagate: False propagate: False
__main__: __main__:
handlers: [console, file] handlers: [console, file, telegram]
level: INFO level: INFO
propagate: False propagate: False
config_manager: config_manager:
handlers: [console, file] handlers: [console, file]
level: DEBUG level: DEBUG

View File

@@ -0,0 +1,28 @@
clients:
"todx.ru":
- handler: "OrderParser",
config:
sheet_name: "Лист1"
key_value: "Артикул"
mapping:
name: "Наименование"
manufacturer: "Производитель"
price: "Цена\nдетали"
quantity: "Кол-\nво"
total: "Сумма"
- handler: "OrderCreator"
- handler: "ExcelWriter"
config: "output.xlsx"
- handler: "EmailSender"
config:
to: "todx@yandex.ru"
- Notifier:
- channel: "email"
to : "status@zapchastiya.ru"
- channel: "telegram"
to: "123454323"

View File

@@ -0,0 +1,17 @@
pipeline:
- handler: "ConfigurableExcelParser"
result_section: "positions"
config:
sheet_name: 0
key_field: "Код детали"
mapping:
article: "Код детали"
manufacturer: "Производитель"
name: "Наименование"
price: "Цена\nдетали"
quantity: "Кол-\nво"
total: "Сумма"

View File

@@ -109,27 +109,16 @@ class EmailClient:
07.10.2025, 16:01, Имя (email@example.com): 07.10.2025, 16:01, Имя (email@example.com):
Кому: ... Кому: ...
""" """
# Ищем первую секцию пересылаемого сообщения (по структуре письма) # Ищем email внутри скобок после строки "Пересылаемое сообщение"
match = re.search( pattern = r"Пересылаемое сообщение.*?\((.*?)\)"
r"-{8,}\\s*Пересылаемое сообщение\\s*-{8,}.*?(\\d{2}\\.\\d{2}\\.\\d{4},\\s*\\d{2}:\\d{2},.*?)\\(([^\\)]+)\\):", match = re.search(pattern, body, re.DOTALL)
body, re.DOTALL)
emails = []
if match: if match:
emails.append(match.group(2)) # email из первой строки пересыла return match.group(1)
# Ищем все email в первой пересылаемой секции (например, в "Кому:") return None
forwarded_section = re.search(
r"^-{8,}.*?Пересылаемое сообщение.*?:$(.*?)(?:^[-=]{5,}|\\Z)",
body, re.MULTILINE | re.DOTALL)
if forwarded_section:
addresses = re.findall(r"\\b([\\w\\.-]+@[\\w\\.-]+)\\b", forwarded_section.group(1))
for addr in addresses:
if addr not in emails:
emails.append(addr)
return emails
def _extract_body(self, msg: email.message.Message) -> str: def _extract_body(self, msg: email.message.Message) -> str:
""" """
Извлечь текст письма. Извлечь текст письма из любого типа содержимого, кроме вложений.
Args: Args:
msg: Объект письма msg: Объект письма
@@ -141,25 +130,36 @@ class EmailClient:
if msg.is_multipart(): if msg.is_multipart():
for part in msg.walk(): for part in msg.walk():
content_type = part.get_content_type()
content_disposition = str(part.get("Content-Disposition", "")) content_disposition = str(part.get("Content-Disposition", ""))
# Пропускаем вложения
# Ищем текстовые части без вложений if "attachment" in content_disposition.lower():
if content_type == "text/plain" and "attachment" not in content_disposition: continue
try: try:
charset = part.get_content_charset() or 'utf-8' charset = part.get_content_charset() or 'utf-8'
body += part.get_payload(decode=True).decode(charset, errors='ignore') payload = part.get_payload(decode=True)
except: if payload:
body_piece = payload.decode(charset, errors='ignore')
body += body_piece
except Exception:
pass pass
else: else:
try: try:
charset = msg.get_content_charset() or 'utf-8' charset = msg.get_content_charset() or 'utf-8'
body = msg.get_payload(decode=True).decode(charset, errors='ignore') payload = msg.get_payload(decode=True)
except: if payload:
body = payload.decode(charset, errors='ignore')
except Exception:
pass pass
return body return body
def __extract_email(self, text: str) -> str:
match = re.search(r'<([^<>]+)>', text)
if match:
return match.group(1)
return None
def _extract_attachments(self, msg: email.message.Message) -> List[EmailAttachment]: def _extract_attachments(self, msg: email.message.Message) -> List[EmailAttachment]:
""" """
Извлечь вложения из письма. Извлечь вложения из письма.
@@ -239,6 +239,8 @@ class EmailClient:
from_addr = self._decode_header(msg.get("From", "")) from_addr = self._decode_header(msg.get("From", ""))
subject = self._decode_header(msg.get("Subject", "")) subject = self._decode_header(msg.get("Subject", ""))
from_email = self.__extract_email(from_addr)
# Получаем дату # Получаем дату
date_str = msg.get("Date", "") date_str = msg.get("Date", "")
try: try:
@@ -254,6 +256,7 @@ class EmailClient:
# Извлекаем тело письма # Извлекаем тело письма
body = self._extract_body(msg) body = self._extract_body(msg)
#print(body)
first_sender = self._extract_first_sender(body) first_sender = self._extract_first_sender(body)
# Извлекаем вложения # Извлекаем вложения
@@ -262,6 +265,7 @@ class EmailClient:
# Создаем объект письма # Создаем объект письма
email_obj = EmailMessage( email_obj = EmailMessage(
from_addr=from_addr, from_addr=from_addr,
from_email=from_email,
subj=subject, subj=subject,
dt=dt, dt=dt,
body=body, body=body,

View File

@@ -14,6 +14,7 @@ class EmailAttachment:
class EmailMessage: class EmailMessage:
"""Класс для представления электронного письма""" """Класс для представления электронного письма"""
from_addr: str from_addr: str
from_email: str
subj: str subj: str
dt: datetime dt: datetime
body: str body: str

View File

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

View File

@@ -1,17 +1,25 @@
from typing import Optional 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): class ConfigurableExcelParser(ExcelParser):
""" """
Универсальный парсер, настраиваемый через конфигурацию. Универсальный парсер, настраиваемый через конфигурацию.
Подходит для большинства стандартных случаев. Подходит для большинства стандартных случаев.
""" """
def parse(self, filepath: str) -> List[OrderPosition]: def parse(self, file_bytes: str) -> List[OrderPosition]:
try: try:
# Читаем Excel # Читаем Excel
df = self._read_excel(filepath) df = self._make_dataframe(file_bytes)
# Удаляем пустые строки
df = df.dropna(how='all')
# Получаем маппинг колонок из конфигурации # Получаем маппинг колонок из конфигурации
mapping = self.config['mapping'] mapping = self.config['mapping']
@@ -24,36 +32,50 @@ class ConfigurableExcelParser(ExcelParser):
if position: if position:
positions.append(position) positions.append(position)
except Exception as e: except Exception as e:
logger.warning(f"Ошибка парсинга строки {idx}: {e}") logger.error(f"Ошибка парсинга строки {idx}: {e}, {row}")
continue continue
logger.info(f"Успешно обработано {len(positions)} позиций из {len(df)} строк") logger.info(f"Успешно обработано {len(positions)} позиций из {len(df)} строк")
return positions return positions
except Exception as e: except Exception as e:
logger.error(f"Ошибка при обработке файла {filepath}: {e}") logger.error(f"Ошибка при обработке файла: {e}")
raise raise Exception from e
def _parse_row(self, row: pd.Series, mapping: Dict[str, str]) -> Optional[OrderPosition]: def _parse_row(self, row: pd.Series, mapping: Dict[str, str]) -> Optional[OrderPosition]:
"""Парсит одну строку Excel в OrderPosition""" """Парсит одну строку Excel в OrderPosition"""
# Проверяем обязательные поля # Проверяем обязательные поля
required_fields = ['article', 'manufacturer', 'name', 'price', 'quantity', 'total'] required_fields = ['article', 'price', 'quantity']
for field in required_fields: for field in required_fields:
if pd.isna(row.get(mapping[field])): if pd.isna(row.get(mapping[field])):
logger.warning(f"Позиция не создана - не заполнено поле {mapping[field]}")
return None 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( position = OrderPosition(
article=str(row[mapping['article']]).strip(), article=str(row[mapping['article']]).strip(),
manufacturer=str(row[mapping['manufacturer']]).strip(), manufacturer=str(row[mapping.get('manufacturer',"")]).strip(),
name=str(row[mapping['name']]).strip(), name=name,
price=Decimal(str(row[mapping['price']])), price=price,
quantity=int(row[mapping['quantity']]), quantity=quantity,
total=Decimal(str(row[mapping['total']])), total=total,
additional_attrs=self._extract_additional_attrs(row, mapping) additional_attrs=self._extract_additional_attrs(row, mapping)
) )
return position return position
def _extract_additional_attrs(self, row: pd.Series, mapping: Dict[str, str]) -> Dict[str, Any]: def _extract_additional_attrs(self, row: pd.Series, mapping: Dict[str, str]) -> Dict[str, Any]:
@@ -66,3 +88,26 @@ class ConfigurableExcelParser(ExcelParser):
additional[col] = row[col] additional[col] = row[col]
return additional 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

View File

@@ -0,0 +1,105 @@
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

View File

@@ -1,7 +1,12 @@
from abc import ABC, abstractmethod
from typing import List
import pandas as pd
import logging 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__) logger = logging.getLogger(__name__)
@@ -15,19 +20,9 @@ class ExcelParser(ABC):
self.config = config self.config = config
@abstractmethod @abstractmethod
def parse(self, filepath: str) -> List[OrderPosition]: def parse(self, file: BytesIO) -> List[OrderPosition]:
""" """
Парсит Excel файл и возвращает список позиций. Парсит Excel файл и возвращает список позиций.
Должен быть реализован в каждом конкретном парсере. Должен быть реализован в каждом конкретном парсере.
""" """
pass pass
def _read_excel(self, filepath: str) -> pd.DataFrame:
"""Общий метод для чтения Excel файлов"""
return pd.read_excel(
filepath,
sheet_name=self.config.get('sheet_name', 0),
header=self.config.get('header_row', 0),
#engine='openpyxl'
engine='calamine'
)

View File

@@ -1,6 +1,15 @@
import yaml import yaml
import json import json
import logging
from pathlib import Path 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: class ParserFactory:
""" """
@@ -10,48 +19,36 @@ class ParserFactory:
# Реестр кастомных парсеров # Реестр кастомных парсеров
CUSTOM_PARSERS = { CUSTOM_PARSERS = {
'supplier_a': SupplierAParser, 'autoeuro.ru': CustomExcelParserAutoeuro,
# Добавляйте сюда специализированные парсеры # Добавляйте сюда специализированные парсеры
} }
def __init__(self, config_path: str): def __init__(self, config: Dict[str, Any]):
self.config_path = Path(config_path) self.config = config
self.suppliers_config = self._load_config()
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}")
def get_parser(self, supplier_name: str) -> ExcelParser: def get_parser(self, supplier_name: str) -> ExcelParser:
""" """
Возвращает парсер для указанного контрагента. Возвращает парсер для указанного контрагента.
Использует кастомный парсер если есть, иначе конфигурируемый. Использует кастомный парсер если есть, иначе конфигурируемый.
""" """
if supplier_name not in self.suppliers_config['suppliers']: if supplier_name not in self.config['suppliers']:
raise ValueError( raise ValueError(
f"Контрагент '{supplier_name}' не найден в конфигурации. " f"Контрагент '{supplier_name}' не найден в конфигурации. "
f"Доступные: {list(self.suppliers_config['suppliers'].keys())}" f"Доступные: {list(self.config['suppliers'].keys())}"
) )
config = self.suppliers_config['suppliers'][supplier_name] config = self.config['suppliers'][supplier_name]
# Проверяем, есть ли кастомный парсер # Проверяем, есть ли кастомный парсер
if supplier_name in self.CUSTOM_PARSERS: if supplier_name in self.CUSTOM_PARSERS:
parser_class = self.CUSTOM_PARSERS[supplier_name] parser_class = self.CUSTOM_PARSERS[supplier_name]
logger.info(f"Используется кастомный парсер для {supplier_name}") logger.debug(f"Используется кастомный парсер для {supplier_name}")
else: else:
parser_class = ConfigurableExcelParser parser_class = ConfigurableExcelParser
logger.info(f"Используется конфигурируемый парсер для {supplier_name}") logger.debug(f"Используется конфигурируемый парсер для {supplier_name}")
return parser_class(config) return parser_class(config)
def list_suppliers(self) -> List[str]: def list_suppliers(self) -> List[str]:
"""Возвращает список всех доступных контрагентов""" """Возвращает список всех доступных контрагентов"""
return list(self.suppliers_config['suppliers'].keys()) return list(self.config['suppliers'].keys())

View File

@@ -1,31 +1,36 @@
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: class ExcelProcessor:
""" """
Главный класс-фасад для обработки Excel файлов. Главный класс-фасад для обработки Excel файлов.
Упрощает использование системы. Упрощает использование системы.
""" """
def __init__(self, config_path: str = 'config/suppliers.yaml'): def __init__(self, config_path: str = 'config/suppliers.yaml', ):
self.factory = ParserFactory(config_path) self.config_path = Path(config_path)
self._setup_logging() self.config = self._load_config()
self.factory = ParserFactory(self.config)
def _setup_logging(self): def process(self, file_bytes: BytesIO, file_name: str, supplier_name: str, validate: bool = False) -> List[OrderPosition]:
"""Настройка логирования"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
def process_file(
self,
filepath: str,
supplier_name: str,
validate: bool = True
) -> List[OrderPosition]:
""" """
Обрабатывает Excel файл от контрагента. Обрабатывает Excel файл от контрагента.
Args: Args:
filepath: Путь к Excel файлу file_bytes: Байты файла
file_name: Имя файла
supplier_name: Название контрагента (из конфигурации) supplier_name: Название контрагента (из конфигурации)
validate: Выполнять ли дополнительную валидацию validate: Выполнять ли дополнительную валидацию
@@ -34,25 +39,34 @@ class ExcelProcessor:
Raises: Raises:
ValueError: Если контрагент не найден ValueError: Если контрагент не найден
FileNotFoundError: Если файл не найден
""" """
logger.info(f"Начало обработки файла: {filepath} для {supplier_name}") logger.info(f"Обработка файла: {file_name} для {supplier_name}")
# Проверка существования файла
if not Path(filepath).exists():
raise FileNotFoundError(f"Файл не найден: {filepath}")
# Получаем парсер и обрабатываем
parser = self.factory.get_parser(supplier_name) parser = self.factory.get_parser(supplier_name)
positions = parser.parse(filepath) positions = parser.parse(file_bytes)
# Дополнительная валидация если нужна # Дополнительная валидация если нужна
if validate: if validate:
positions = self._validate_positions(positions) positions = self._validate_positions(positions)
logger.info(f"Обработка завершена: получено {len(positions)} позиций") logger.debug(f"Обработка завершена: получено {len(positions)} позиций")
return 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]: def _validate_positions(self, positions: List[OrderPosition]) -> List[OrderPosition]:
"""Дополнительная валидация позиций""" """Дополнительная валидация позиций"""
valid_positions = [] valid_positions = []
@@ -83,3 +97,14 @@ class ExcelProcessor:
def get_available_suppliers(self) -> List[str]: def get_available_suppliers(self) -> List[str]:
"""Возвращает список доступных контрагентов""" """Возвращает список доступных контрагентов"""
return self.factory.list_suppliers() 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}")

View File

@@ -1,452 +0,0 @@
2025-11-01 21:42:16,440 log_manager [ INFO]: Logging configuration applied
2025-11-01 21:42:16,440 log_manager [ INFO]: Logging configuration applied
2025-11-01 21:42:16,440 cfg_manager [ INFO]: Update interval set to 2.0 seconds
2025-11-01 21:42:16,440 cfg_manager [ INFO]: Update interval set to 2.0 seconds
2025-11-01 21:42:16,440 cfg_manager [ INFO]: Work interval set to 2.0 seconds
2025-11-01 21:42:16,440 cfg_manager [ INFO]: Work interval set to 2.0 seconds
2025-11-01 21:42:17,109 main [ INFO]: Check emails for new orders
2025-11-01 21:42:19,239 main [ INFO]: Check emails for new orders
2025-11-01 21:42:21,371 main [ INFO]: Check emails for new orders
2025-11-01 21:42:23,499 main [ INFO]: Check emails for new orders
2025-11-01 21:42:25,636 main [ INFO]: Check emails for new orders
2025-11-01 21:42:27,770 main [ INFO]: Check emails for new orders
2025-11-01 21:42:27,772 cfg_manager [ INFO]: ConfigManager tasks cancelled
2025-11-01 21:42:27,772 cfg_manager [ INFO]: ConfigManager tasks cancelled
2025-11-01 21:42:27,772 cfg_manager [ INFO]: ConfigManager stopped
2025-11-01 21:42:27,772 cfg_manager [ INFO]: ConfigManager stopped
2025-11-01 21:42:35,229 log_manager [ INFO]: Logging configuration applied
2025-11-01 21:42:35,229 log_manager [ INFO]: Logging configuration applied
2025-11-01 21:42:35,230 cfg_manager [ INFO]: Update interval set to 2.0 seconds
2025-11-01 21:42:35,230 cfg_manager [ INFO]: Update interval set to 2.0 seconds
2025-11-01 21:42:35,230 cfg_manager [ INFO]: Work interval set to 2.0 seconds
2025-11-01 21:42:35,230 cfg_manager [ INFO]: Work interval set to 2.0 seconds
2025-11-01 21:42:37,462 main [ INFO]: Check emails for new orders
2025-11-01 21:42:39,542 main [ INFO]: Check emails for new orders
2025-11-01 21:42:41,623 main [ INFO]: Check emails for new orders
2025-11-01 21:42:43,705 main [ INFO]: Check emails for new orders
2025-11-01 21:42:45,791 main [ INFO]: Check emails for new orders
2025-11-01 21:42:47,872 main [ INFO]: Check emails for new orders
2025-11-01 21:42:49,514 cfg_manager [ INFO]: ConfigManager tasks cancelled
2025-11-01 21:42:49,514 cfg_manager [ INFO]: ConfigManager tasks cancelled
2025-11-01 21:42:49,514 cfg_manager [ INFO]: ConfigManager stopped
2025-11-01 21:42:49,514 cfg_manager [ INFO]: ConfigManager stopped
2025-11-01 21:43:04,728 log_manager [ INFO]: Logging configuration applied
2025-11-01 21:43:04,728 log_manager [ INFO]: Logging configuration applied
2025-11-01 21:43:04,730 cfg_manager [ INFO]: Update interval set to 2.0 seconds
2025-11-01 21:43:04,730 cfg_manager [ INFO]: Update interval set to 2.0 seconds
2025-11-01 21:43:04,730 cfg_manager [ INFO]: Work interval set to 2.0 seconds
2025-11-01 21:43:04,730 cfg_manager [ INFO]: Work interval set to 2.0 seconds
2025-11-01 21:43:07,002 main [ DEBUG]: Check emails for new orders
2025-11-01 21:43:09,104 main [ DEBUG]: Check emails for new orders
2025-11-01 21:43:11,205 main [ DEBUG]: Check emails for new orders
2025-11-01 21:43:13,309 main [ DEBUG]: Check emails for new orders
2025-11-01 21:43:15,413 main [ DEBUG]: Check emails for new orders
2025-11-01 21:43:17,516 main [ DEBUG]: Check emails for new orders
2025-11-01 21:43:19,630 main [ DEBUG]: Check emails for new orders
2025-11-01 21:43:21,729 main [ DEBUG]: Check emails for new orders
2025-11-01 21:43:22,746 log_manager [ INFO]: Logging configuration applied
2025-11-01 21:43:22,746 log_manager [ INFO]: Logging configuration applied
2025-11-01 21:43:22,747 cfg_manager [ INFO]: Update interval set to 2.0 seconds
2025-11-01 21:43:22,747 cfg_manager [ INFO]: Update interval set to 2.0 seconds
2025-11-01 21:43:22,747 cfg_manager [ INFO]: Work interval set to 2.0 seconds
2025-11-01 21:43:22,747 cfg_manager [ INFO]: Work interval set to 2.0 seconds
2025-11-01 21:44:56,825 log_manager [ INFO]: Logging configuration applied
2025-11-01 21:44:56,825 log_manager [ INFO]: Logging configuration applied
2025-11-01 21:44:56,826 cfg_manager [ INFO]: Update interval set to 2.0 seconds
2025-11-01 21:44:56,826 cfg_manager [ INFO]: Update interval set to 2.0 seconds
2025-11-01 21:44:56,826 cfg_manager [ INFO]: Work interval set to 2.0 seconds
2025-11-01 21:44:56,826 cfg_manager [ INFO]: Work interval set to 2.0 seconds
2025-11-01 21:46:08,675 cfg_manager [ INFO]: ConfigManager tasks cancelled
2025-11-01 21:46:08,675 cfg_manager [ INFO]: ConfigManager tasks cancelled
2025-11-01 21:46:08,677 cfg_manager [ INFO]: ConfigManager stopped
2025-11-01 21:46:08,677 cfg_manager [ INFO]: ConfigManager stopped
2025-11-01 21:46:11,962 log_manager [ INFO]: Logging configuration applied
2025-11-01 21:46:11,962 log_manager [ INFO]: Logging configuration applied
2025-11-01 21:46:11,962 cfg_manager [ INFO]: Update interval set to 2.0 seconds
2025-11-01 21:46:11,962 cfg_manager [ INFO]: Update interval set to 2.0 seconds
2025-11-01 21:46:11,962 cfg_manager [ INFO]: Work interval set to 2.0 seconds
2025-11-01 21:46:11,962 cfg_manager [ INFO]: Work interval set to 2.0 seconds
2025-11-01 21:46:23,627 main [ INFO]: Fwd: Заказ 95 на поставку запчастей Detali.ru
2025-11-01 21:46:23,627 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,627 main [ INFO]: 2025-10-27 13:39:28
2025-11-01 21:46:23,627 main [ INFO]:
2025-11-01 21:46:23,627 main [ INFO]: []
2025-11-01 21:46:23,627 main [ INFO]: --------------------------------
2025-11-01 21:46:23,628 main [ INFO]: Fwd: Заказ поставщику номер 00000056685 (Сальткрока, 000426967) от ООО Нова
2025-11-01 21:46:23,628 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,628 main [ INFO]: 2025-10-27 13:39:36
2025-11-01 21:46:23,628 main [ INFO]:
2025-11-01 21:46:23,628 main [ INFO]: []
2025-11-01 21:46:23,628 main [ INFO]: --------------------------------
2025-11-01 21:46:23,628 main [ INFO]: Fwd: Заказ 00УТ-011202 ЦС 2
2025-11-01 21:46:23,628 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,628 main [ INFO]: 2025-10-27 13:39:42
2025-11-01 21:46:23,628 main [ INFO]:
2025-11-01 21:46:23,628 main [ INFO]: []
2025-11-01 21:46:23,628 main [ INFO]: --------------------------------
2025-11-01 21:46:23,628 main [ INFO]: Fwd: Заказ 00УТ-010792 Наличие
2025-11-01 21:46:23,628 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,628 main [ INFO]: 2025-10-27 13:39:50
2025-11-01 21:46:23,628 main [ INFO]:
2025-11-01 21:46:23,628 main [ INFO]: []
2025-11-01 21:46:23,628 main [ INFO]: --------------------------------
2025-11-01 21:46:23,628 main [ INFO]: Fwd: Заказ поставщику Партерра Сальткрока СПБ на Санкт-Петербург ЦКД
2025-11-01 21:46:23,628 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,628 main [ INFO]: 2025-10-27 13:40:05
2025-11-01 21:46:23,628 main [ INFO]:
2025-11-01 21:46:23,628 main [ INFO]: []
2025-11-01 21:46:23,628 main [ INFO]: --------------------------------
2025-11-01 21:46:23,628 main [ INFO]: Fwd: Заказ поставщику Партерра Сальткрока СПБ 1Д на Санкт-Петербург ЦКД
2025-11-01 21:46:23,628 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,628 main [ INFO]: 2025-10-27 13:40:10
2025-11-01 21:46:23,628 main [ INFO]:
2025-11-01 21:46:23,628 main [ INFO]: []
2025-11-01 21:46:23,628 main [ INFO]: --------------------------------
2025-11-01 21:46:23,628 main [ INFO]: Fwd: FAOrderSPB_56151479_2025-04-02-10-04-25.xlsx
2025-11-01 21:46:23,628 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,628 main [ INFO]: 2025-10-27 13:40:21
2025-11-01 21:46:23,628 main [ INFO]:
2025-11-01 21:46:23,628 main [ INFO]: []
2025-11-01 21:46:23,628 main [ INFO]: --------------------------------
2025-11-01 21:46:23,628 main [ INFO]: Fwd: Заказ компании АвтоСпутник (Автоспутник Санкт-Петербург) № 2087185 от 24.10.2025 склад Наличие
2025-11-01 21:46:23,628 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,628 main [ INFO]: 2025-10-27 13:40:28
2025-11-01 21:46:23,628 main [ INFO]:
2025-11-01 21:46:23,628 main [ INFO]: []
2025-11-01 21:46:23,629 main [ INFO]: --------------------------------
2025-11-01 21:46:23,629 main [ INFO]: Fwd: Заказ компании АвтоСпутник (Автоспутник Санкт-Петербург) № 2085697 от 23.10.2025 склад ЦС 5 дней
2025-11-01 21:46:23,629 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,629 main [ INFO]: 2025-10-27 13:40:32
2025-11-01 21:46:23,629 main [ INFO]:
2025-11-01 21:46:23,629 main [ INFO]: []
2025-11-01 21:46:23,629 main [ INFO]: --------------------------------
2025-11-01 21:46:23,629 main [ INFO]: Fwd: заказ Юником Санкт-Петербург Заказ СПФ00000007980
2025-11-01 21:46:23,629 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,629 main [ INFO]: 2025-10-27 13:40:40
2025-11-01 21:46:23,629 main [ INFO]:
2025-11-01 21:46:23,629 main [ INFO]: []
2025-11-01 21:46:23,629 main [ INFO]: --------------------------------
2025-11-01 21:46:23,629 main [ INFO]: Fwd: заказ Юником Санкт-Петербург Заказ СПФ00000007928
2025-11-01 21:46:23,629 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,629 main [ INFO]: 2025-10-27 13:41:46
2025-11-01 21:46:23,629 main [ INFO]:
2025-11-01 21:46:23,629 main [ INFO]: []
2025-11-01 21:46:23,629 main [ INFO]: --------------------------------
2025-11-01 21:46:23,629 main [ INFO]: Fwd: Заказ ООО Амтел Санкт-Петербург от 27.10.2025
2025-11-01 21:46:23,629 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,629 main [ INFO]: 2025-10-27 13:41:54
2025-11-01 21:46:23,629 main [ INFO]:
2025-11-01 21:46:23,629 main [ INFO]: []
2025-11-01 21:46:23,629 main [ INFO]: --------------------------------
2025-11-01 21:46:23,629 main [ INFO]: Fwd: Заказ «Сальткрока» от Sigma №686120 на сумму 151434.12 руб !
2025-11-01 21:46:23,629 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,629 main [ INFO]: 2025-10-27 13:42:03
2025-11-01 21:46:23,629 main [ INFO]:
2025-11-01 21:46:23,629 main [ INFO]: []
2025-11-01 21:46:23,629 main [ INFO]: --------------------------------
2025-11-01 21:46:23,629 main [ INFO]: Fwd: Зпт № Зпт-303384 Доставка в Санкт-Петербург (клиентский) Склад: Сальткрока (СПБ) 0-1 день
2025-11-01 21:46:23,629 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,629 main [ INFO]: 2025-10-27 13:42:13
2025-11-01 21:46:23,629 main [ INFO]:
2025-11-01 21:46:23,629 main [ INFO]: []
2025-11-01 21:46:23,629 main [ INFO]: --------------------------------
2025-11-01 21:46:23,629 main [ INFO]: Fwd: Зпт № Зпт-303044 Доставка в Санкт-Петербург (экспресс) Склад: Сальткрока (СПБ) 2-3 дня
2025-11-01 21:46:23,629 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,629 main [ INFO]: 2025-10-27 13:42:18
2025-11-01 21:46:23,629 main [ INFO]:
2025-11-01 21:46:23,629 main [ INFO]: []
2025-11-01 21:46:23,629 main [ INFO]: --------------------------------
2025-11-01 21:46:23,629 main [ INFO]: Fwd: Зпт № Зпт-303041 Доставка в Санкт-Петербург (экспресс) Склад: Сальткрока (СПБ) 5-7 дней НЕТ ВПС
2025-11-01 21:46:23,629 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,630 main [ INFO]: 2025-10-27 13:42:23
2025-11-01 21:46:23,630 main [ INFO]:
2025-11-01 21:46:23,630 main [ INFO]: []
2025-11-01 21:46:23,630 main [ INFO]: --------------------------------
2025-11-01 21:46:23,630 main [ INFO]: Fwd: Зпт № Зпт-301326 Доставка в Санкт-Петербург (клиентский) Склад: Сальткрока (СПБ) 2-3 дня
2025-11-01 21:46:23,630 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,630 main [ INFO]: 2025-10-27 13:42:32
2025-11-01 21:46:23,630 main [ INFO]:
2025-11-01 21:46:23,630 main [ INFO]: []
2025-11-01 21:46:23,630 main [ INFO]: --------------------------------
2025-11-01 21:46:23,630 main [ INFO]: Fwd: Зпт № Зпт-302349 Доставка в Санкт-Петербург (экспресс) Склад: Сальткрока (СПБ) 0-1 день
2025-11-01 21:46:23,630 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,630 main [ INFO]: 2025-10-27 13:42:56
2025-11-01 21:46:23,630 main [ INFO]:
2025-11-01 21:46:23,630 main [ INFO]: []
2025-11-01 21:46:23,630 main [ INFO]: --------------------------------
2025-11-01 21:46:23,630 main [ INFO]: Fwd: Зпт № Зпт-301326 Доставка в Санкт-Петербург (клиентский) Склад: Сальткрока (СПБ) 2-3 дня
2025-11-01 21:46:23,630 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,630 main [ INFO]: 2025-10-27 13:43:10
2025-11-01 21:46:23,630 main [ INFO]:
2025-11-01 21:46:23,630 main [ INFO]: []
2025-11-01 21:46:23,630 main [ INFO]: --------------------------------
2025-11-01 21:46:23,630 main [ INFO]: Fwd: Заказ от ООО "Шате-М Плюс" (CD)_Сальткрока_23.09.25_ 9:52:37
2025-11-01 21:46:23,630 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,630 main [ INFO]: 2025-10-27 13:43:35
2025-11-01 21:46:23,630 main [ INFO]:
2025-11-01 21:46:23,630 main [ INFO]: []
2025-11-01 21:46:23,630 main [ INFO]: --------------------------------
2025-11-01 21:46:23,630 main [ INFO]: Fwd: Заказ от ООО ИНТЕРКАР ИНН: 7448229540 !! почта для ответов zakaz.autoliga174@mail.ru !!!
2025-11-01 21:46:23,630 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,630 main [ INFO]: 2025-10-27 13:43:43
2025-11-01 21:46:23,631 main [ INFO]:
2025-11-01 21:46:23,631 main [ INFO]: []
2025-11-01 21:46:23,631 main [ INFO]: --------------------------------
2025-11-01 21:46:23,631 main [ INFO]: Fwd: Autostels - САЛЬТКРОКА-СПБ - Заказ 271 от 27.10.2025 13:10.
2025-11-01 21:46:23,631 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,631 main [ INFO]: 2025-10-27 13:43:50
2025-11-01 21:46:23,631 main [ INFO]:
2025-11-01 21:46:23,631 main [ INFO]: []
2025-11-01 21:46:23,631 main [ INFO]: --------------------------------
2025-11-01 21:46:23,631 main [ INFO]: Fwd: Autostels - САЛЬТКРОКА_2Д_МСК - Заказ 230 от 27.10.2025 12:20.
2025-11-01 21:46:23,631 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,631 main [ INFO]: 2025-10-27 13:43:55
2025-11-01 21:46:23,631 main [ INFO]:
2025-11-01 21:46:23,631 main [ INFO]: []
2025-11-01 21:46:23,631 main [ INFO]: --------------------------------
2025-11-01 21:46:23,631 main [ INFO]: Fwd: Заказ(ы) Аврора -> Запчастия #235506458#
2025-11-01 21:46:23,631 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,631 main [ INFO]: 2025-10-27 13:44:02
2025-11-01 21:46:23,631 main [ INFO]:
2025-11-01 21:46:23,631 main [ INFO]: []
2025-11-01 21:46:23,631 main [ INFO]: --------------------------------
2025-11-01 21:46:23,631 main [ INFO]: Fwd: Заказ от АО "Автоформула" №А-251022-02437 от 22.10.2025 поставщику ООО "Сальткрока"
2025-11-01 21:46:23,631 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,631 main [ INFO]: 2025-10-27 13:44:09
2025-11-01 21:46:23,631 main [ INFO]:
2025-11-01 21:46:23,631 main [ INFO]: []
2025-11-01 21:46:23,631 main [ INFO]: --------------------------------
2025-11-01 21:46:23,631 main [ INFO]: Fwd: Заказ от E-tape.ru Якорная д. 9А ИП ЮН Станислав Власович
2025-11-01 21:46:23,631 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,631 main [ INFO]: 2025-10-27 13:44:16
2025-11-01 21:46:23,631 main [ INFO]:
2025-11-01 21:46:23,631 main [ INFO]: []
2025-11-01 21:46:23,631 main [ INFO]: --------------------------------
2025-11-01 21:46:23,631 main [ INFO]: Fwd: Заказ со склада ABSTD MSK 966
2025-11-01 21:46:23,631 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,631 main [ INFO]: 2025-10-27 13:44:23
2025-11-01 21:46:23,631 main [ INFO]:
2025-11-01 21:46:23,631 main [ INFO]: []
2025-11-01 21:46:23,631 main [ INFO]: --------------------------------
2025-11-01 21:46:23,631 main [ INFO]: Fwd: Заказ № 5107682 от 21-10-2025
2025-11-01 21:46:23,631 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,632 main [ INFO]: 2025-10-27 13:44:29
2025-11-01 21:46:23,632 main [ INFO]:
2025-11-01 21:46:23,632 main [ INFO]: []
2025-11-01 21:46:23,632 main [ INFO]: --------------------------------
2025-11-01 21:46:23,632 main [ INFO]: Fwd: Заказ от ООО "Рай Авто СПб"
2025-11-01 21:46:23,632 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,632 main [ INFO]: 2025-10-27 13:44:37
2025-11-01 21:46:23,632 main [ INFO]:
2025-11-01 21:46:23,632 main [ INFO]: []
2025-11-01 21:46:23,632 main [ INFO]: --------------------------------
2025-11-01 21:46:23,632 main [ INFO]: Fwd: Заказ ООО "МЕХАНИКА" - поставщик ООО "Сальткрока" ( подтвердите обратным письмом) codeStock:1797936 Заказ поставщику ST000985485/24.10.2025
2025-11-01 21:46:23,632 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:46:23,632 main [ INFO]: 2025-10-27 13:44:45
2025-11-01 21:46:23,632 main [ INFO]:
2025-11-01 21:46:23,632 main [ INFO]: []
2025-11-01 21:46:23,632 main [ INFO]: --------------------------------
2025-11-01 21:46:46,440 cfg_manager [ INFO]: ConfigManager tasks cancelled
2025-11-01 21:46:46,440 cfg_manager [ INFO]: ConfigManager tasks cancelled
2025-11-01 21:46:46,442 cfg_manager [ INFO]: ConfigManager stopped
2025-11-01 21:46:46,442 cfg_manager [ INFO]: ConfigManager stopped
2025-11-01 21:47:30,413 log_manager [ INFO]: Logging configuration applied
2025-11-01 21:47:30,413 log_manager [ INFO]: Logging configuration applied
2025-11-01 21:47:30,413 cfg_manager [ INFO]: Update interval set to 2.0 seconds
2025-11-01 21:47:30,413 cfg_manager [ INFO]: Update interval set to 2.0 seconds
2025-11-01 21:47:30,414 cfg_manager [ INFO]: Work interval set to 2.0 seconds
2025-11-01 21:47:30,414 cfg_manager [ INFO]: Work interval set to 2.0 seconds
2025-11-01 21:47:35,112 cfg_manager [ INFO]: ConfigManager tasks cancelled
2025-11-01 21:47:35,112 cfg_manager [ INFO]: ConfigManager tasks cancelled
2025-11-01 21:47:35,113 cfg_manager [ INFO]: ConfigManager stopped
2025-11-01 21:47:35,113 cfg_manager [ INFO]: ConfigManager stopped
2025-11-01 21:47:51,834 log_manager [ INFO]: Logging configuration applied
2025-11-01 21:47:51,834 log_manager [ INFO]: Logging configuration applied
2025-11-01 21:47:51,835 cfg_manager [ INFO]: Update interval set to 2.0 seconds
2025-11-01 21:47:51,835 cfg_manager [ INFO]: Update interval set to 2.0 seconds
2025-11-01 21:47:51,835 cfg_manager [ INFO]: Work interval set to 2.0 seconds
2025-11-01 21:47:51,835 cfg_manager [ INFO]: Work interval set to 2.0 seconds
2025-11-01 21:48:06,117 main [ INFO]: Fwd: Заказ 95 на поставку запчастей Detali.ru
2025-11-01 21:48:06,118 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,118 main [ INFO]: 2025-10-27 13:39:28
2025-11-01 21:48:06,118 main [ INFO]:
2025-11-01 21:48:06,119 main [ INFO]: []
2025-11-01 21:48:06,119 main [ INFO]: --------------------------------
2025-11-01 21:48:06,119 main [ INFO]: Fwd: Заказ поставщику номер 00000056685 (Сальткрока, 000426967) от ООО Нова
2025-11-01 21:48:06,119 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,119 main [ INFO]: 2025-10-27 13:39:36
2025-11-01 21:48:06,120 main [ INFO]:
2025-11-01 21:48:06,120 main [ INFO]: []
2025-11-01 21:48:06,120 main [ INFO]: --------------------------------
2025-11-01 21:48:06,120 main [ INFO]: Fwd: Заказ 00УТ-011202 ЦС 2
2025-11-01 21:48:06,120 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,120 main [ INFO]: 2025-10-27 13:39:42
2025-11-01 21:48:06,120 main [ INFO]:
2025-11-01 21:48:06,120 main [ INFO]: []
2025-11-01 21:48:06,121 main [ INFO]: --------------------------------
2025-11-01 21:48:06,121 main [ INFO]: Fwd: Заказ 00УТ-010792 Наличие
2025-11-01 21:48:06,121 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,121 main [ INFO]: 2025-10-27 13:39:50
2025-11-01 21:48:06,121 main [ INFO]:
2025-11-01 21:48:06,121 main [ INFO]: []
2025-11-01 21:48:06,121 main [ INFO]: --------------------------------
2025-11-01 21:48:06,122 main [ INFO]: Fwd: Заказ поставщику Партерра Сальткрока СПБ на Санкт-Петербург ЦКД
2025-11-01 21:48:06,122 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,122 main [ INFO]: 2025-10-27 13:40:05
2025-11-01 21:48:06,122 main [ INFO]:
2025-11-01 21:48:06,122 main [ INFO]: []
2025-11-01 21:48:06,122 main [ INFO]: --------------------------------
2025-11-01 21:48:06,122 main [ INFO]: Fwd: Заказ поставщику Партерра Сальткрока СПБ 1Д на Санкт-Петербург ЦКД
2025-11-01 21:48:06,122 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,122 main [ INFO]: 2025-10-27 13:40:10
2025-11-01 21:48:06,122 main [ INFO]:
2025-11-01 21:48:06,123 main [ INFO]: []
2025-11-01 21:48:06,123 main [ INFO]: --------------------------------
2025-11-01 21:48:06,123 main [ INFO]: Fwd: FAOrderSPB_56151479_2025-04-02-10-04-25.xlsx
2025-11-01 21:48:06,123 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,123 main [ INFO]: 2025-10-27 13:40:21
2025-11-01 21:48:06,123 main [ INFO]:
2025-11-01 21:48:06,124 main [ INFO]: []
2025-11-01 21:48:06,124 main [ INFO]: --------------------------------
2025-11-01 21:48:06,124 main [ INFO]: Fwd: Заказ компании АвтоСпутник (Автоспутник Санкт-Петербург) № 2087185 от 24.10.2025 склад Наличие
2025-11-01 21:48:06,124 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,124 main [ INFO]: 2025-10-27 13:40:28
2025-11-01 21:48:06,124 main [ INFO]:
2025-11-01 21:48:06,124 main [ INFO]: []
2025-11-01 21:48:06,124 main [ INFO]: --------------------------------
2025-11-01 21:48:06,124 main [ INFO]: Fwd: Заказ компании АвтоСпутник (Автоспутник Санкт-Петербург) № 2085697 от 23.10.2025 склад ЦС 5 дней
2025-11-01 21:48:06,124 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,125 main [ INFO]: 2025-10-27 13:40:32
2025-11-01 21:48:06,125 main [ INFO]:
2025-11-01 21:48:06,125 main [ INFO]: []
2025-11-01 21:48:06,125 main [ INFO]: --------------------------------
2025-11-01 21:48:06,125 main [ INFO]: Fwd: заказ Юником Санкт-Петербург Заказ СПФ00000007980
2025-11-01 21:48:06,125 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,125 main [ INFO]: 2025-10-27 13:40:40
2025-11-01 21:48:06,125 main [ INFO]:
2025-11-01 21:48:06,125 main [ INFO]: []
2025-11-01 21:48:06,125 main [ INFO]: --------------------------------
2025-11-01 21:48:06,126 main [ INFO]: Fwd: заказ Юником Санкт-Петербург Заказ СПФ00000007928
2025-11-01 21:48:06,126 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,126 main [ INFO]: 2025-10-27 13:41:46
2025-11-01 21:48:06,126 main [ INFO]:
2025-11-01 21:48:06,126 main [ INFO]: []
2025-11-01 21:48:06,126 main [ INFO]: --------------------------------
2025-11-01 21:48:06,126 main [ INFO]: Fwd: Заказ ООО Амтел Санкт-Петербург от 27.10.2025
2025-11-01 21:48:06,126 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,126 main [ INFO]: 2025-10-27 13:41:54
2025-11-01 21:48:06,126 main [ INFO]:
2025-11-01 21:48:06,127 main [ INFO]: []
2025-11-01 21:48:06,127 main [ INFO]: --------------------------------
2025-11-01 21:48:06,127 main [ INFO]: Fwd: Заказ «Сальткрока» от Sigma №686120 на сумму 151434.12 руб !
2025-11-01 21:48:06,127 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,127 main [ INFO]: 2025-10-27 13:42:03
2025-11-01 21:48:06,127 main [ INFO]:
2025-11-01 21:48:06,127 main [ INFO]: []
2025-11-01 21:48:06,127 main [ INFO]: --------------------------------
2025-11-01 21:48:06,127 main [ INFO]: Fwd: Зпт № Зпт-303384 Доставка в Санкт-Петербург (клиентский) Склад: Сальткрока (СПБ) 0-1 день
2025-11-01 21:48:06,127 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,127 main [ INFO]: 2025-10-27 13:42:13
2025-11-01 21:48:06,128 main [ INFO]:
2025-11-01 21:48:06,128 main [ INFO]: []
2025-11-01 21:48:06,128 main [ INFO]: --------------------------------
2025-11-01 21:48:06,128 main [ INFO]: Fwd: Зпт № Зпт-303044 Доставка в Санкт-Петербург (экспресс) Склад: Сальткрока (СПБ) 2-3 дня
2025-11-01 21:48:06,128 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,128 main [ INFO]: 2025-10-27 13:42:18
2025-11-01 21:48:06,128 main [ INFO]:
2025-11-01 21:48:06,128 main [ INFO]: []
2025-11-01 21:48:06,128 main [ INFO]: --------------------------------
2025-11-01 21:48:06,128 main [ INFO]: Fwd: Зпт № Зпт-303041 Доставка в Санкт-Петербург (экспресс) Склад: Сальткрока (СПБ) 5-7 дней НЕТ ВПС
2025-11-01 21:48:06,128 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,128 main [ INFO]: 2025-10-27 13:42:23
2025-11-01 21:48:06,128 main [ INFO]:
2025-11-01 21:48:06,128 main [ INFO]: []
2025-11-01 21:48:06,129 main [ INFO]: --------------------------------
2025-11-01 21:48:06,129 main [ INFO]: Fwd: Зпт № Зпт-301326 Доставка в Санкт-Петербург (клиентский) Склад: Сальткрока (СПБ) 2-3 дня
2025-11-01 21:48:06,129 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,129 main [ INFO]: 2025-10-27 13:42:32
2025-11-01 21:48:06,129 main [ INFO]:
2025-11-01 21:48:06,129 main [ INFO]: []
2025-11-01 21:48:06,129 main [ INFO]: --------------------------------
2025-11-01 21:48:06,129 main [ INFO]: Fwd: Зпт № Зпт-302349 Доставка в Санкт-Петербург (экспресс) Склад: Сальткрока (СПБ) 0-1 день
2025-11-01 21:48:06,129 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,129 main [ INFO]: 2025-10-27 13:42:56
2025-11-01 21:48:06,129 main [ INFO]:
2025-11-01 21:48:06,130 main [ INFO]: []
2025-11-01 21:48:06,130 main [ INFO]: --------------------------------
2025-11-01 21:48:06,130 main [ INFO]: Fwd: Зпт № Зпт-301326 Доставка в Санкт-Петербург (клиентский) Склад: Сальткрока (СПБ) 2-3 дня
2025-11-01 21:48:06,130 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,130 main [ INFO]: 2025-10-27 13:43:10
2025-11-01 21:48:06,130 main [ INFO]:
2025-11-01 21:48:06,130 main [ INFO]: []
2025-11-01 21:48:06,130 main [ INFO]: --------------------------------
2025-11-01 21:48:06,130 main [ INFO]: Fwd: Заказ от ООО "Шате-М Плюс" (CD)_Сальткрока_23.09.25_ 9:52:37
2025-11-01 21:48:06,130 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,130 main [ INFO]: 2025-10-27 13:43:35
2025-11-01 21:48:06,130 main [ INFO]:
2025-11-01 21:48:06,130 main [ INFO]: []
2025-11-01 21:48:06,131 main [ INFO]: --------------------------------
2025-11-01 21:48:06,131 main [ INFO]: Fwd: Заказ от ООО ИНТЕРКАР ИНН: 7448229540 !! почта для ответов zakaz.autoliga174@mail.ru !!!
2025-11-01 21:48:06,131 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,131 main [ INFO]: 2025-10-27 13:43:43
2025-11-01 21:48:06,131 main [ INFO]:
2025-11-01 21:48:06,131 main [ INFO]: []
2025-11-01 21:48:06,131 main [ INFO]: --------------------------------
2025-11-01 21:48:06,131 main [ INFO]: Fwd: Autostels - САЛЬТКРОКА-СПБ - Заказ 271 от 27.10.2025 13:10.
2025-11-01 21:48:06,131 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,131 main [ INFO]: 2025-10-27 13:43:50
2025-11-01 21:48:06,131 main [ INFO]:
2025-11-01 21:48:06,131 main [ INFO]: []
2025-11-01 21:48:06,132 main [ INFO]: --------------------------------
2025-11-01 21:48:06,132 main [ INFO]: Fwd: Autostels - САЛЬТКРОКА_2Д_МСК - Заказ 230 от 27.10.2025 12:20.
2025-11-01 21:48:06,132 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,132 main [ INFO]: 2025-10-27 13:43:55
2025-11-01 21:48:06,132 main [ INFO]:
2025-11-01 21:48:06,132 main [ INFO]: []
2025-11-01 21:48:06,132 main [ INFO]: --------------------------------
2025-11-01 21:48:06,132 main [ INFO]: Fwd: Заказ(ы) Аврора -> Запчастия #235506458#
2025-11-01 21:48:06,132 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,132 main [ INFO]: 2025-10-27 13:44:02
2025-11-01 21:48:06,132 main [ INFO]:
2025-11-01 21:48:06,132 main [ INFO]: []
2025-11-01 21:48:06,132 main [ INFO]: --------------------------------
2025-11-01 21:48:06,132 main [ INFO]: Fwd: Заказ от АО "Автоформула" №А-251022-02437 от 22.10.2025 поставщику ООО "Сальткрока"
2025-11-01 21:48:06,132 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,132 main [ INFO]: 2025-10-27 13:44:09
2025-11-01 21:48:06,132 main [ INFO]:
2025-11-01 21:48:06,133 main [ INFO]: []
2025-11-01 21:48:06,133 main [ INFO]: --------------------------------
2025-11-01 21:48:06,133 main [ INFO]: Fwd: Заказ от E-tape.ru Якорная д. 9А ИП ЮН Станислав Власович
2025-11-01 21:48:06,133 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,133 main [ INFO]: 2025-10-27 13:44:16
2025-11-01 21:48:06,133 main [ INFO]:
2025-11-01 21:48:06,133 main [ INFO]: []
2025-11-01 21:48:06,133 main [ INFO]: --------------------------------
2025-11-01 21:48:06,133 main [ INFO]: Fwd: Заказ со склада ABSTD MSK 966
2025-11-01 21:48:06,133 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,133 main [ INFO]: 2025-10-27 13:44:23
2025-11-01 21:48:06,133 main [ INFO]:
2025-11-01 21:48:06,133 main [ INFO]: []
2025-11-01 21:48:06,133 main [ INFO]: --------------------------------
2025-11-01 21:48:06,133 main [ INFO]: Fwd: Заказ № 5107682 от 21-10-2025
2025-11-01 21:48:06,133 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,133 main [ INFO]: 2025-10-27 13:44:29
2025-11-01 21:48:06,134 main [ INFO]:
2025-11-01 21:48:06,134 main [ INFO]: []
2025-11-01 21:48:06,134 main [ INFO]: --------------------------------
2025-11-01 21:48:06,134 main [ INFO]: Fwd: Заказ от ООО "Рай Авто СПб"
2025-11-01 21:48:06,134 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,134 main [ INFO]: 2025-10-27 13:44:37
2025-11-01 21:48:06,134 main [ INFO]:
2025-11-01 21:48:06,134 main [ INFO]: []
2025-11-01 21:48:06,134 main [ INFO]: --------------------------------
2025-11-01 21:48:06,134 main [ INFO]: Fwd: Заказ ООО "МЕХАНИКА" - поставщик ООО "Сальткрока" ( подтвердите обратным письмом) codeStock:1797936 Заказ поставщику ST000985485/24.10.2025
2025-11-01 21:48:06,134 main [ INFO]: Отдел Продаж <sale@zapchastiya.ru>
2025-11-01 21:48:06,134 main [ INFO]: 2025-10-27 13:44:45
2025-11-01 21:48:06,134 main [ INFO]:
2025-11-01 21:48:06,134 main [ INFO]: []
2025-11-01 21:48:06,134 main [ INFO]: --------------------------------
2025-11-01 21:55:49,251 cfg_manager [ INFO]: ConfigManager tasks cancelled
2025-11-01 21:55:49,251 cfg_manager [ INFO]: ConfigManager tasks cancelled
2025-11-01 21:55:49,252 cfg_manager [ INFO]: ConfigManager stopped
2025-11-01 21:55:49,252 cfg_manager [ INFO]: ConfigManager stopped

View File

@@ -7,6 +7,7 @@ import os
from dotenv import load_dotenv from dotenv import load_dotenv
from email_client import EmailClient from email_client import EmailClient
from excel_proceccor import ExcelProcessor
logger = logging.getLogger() logger = logging.getLogger()
@@ -27,8 +28,10 @@ class MailOrderBot(ConfigManager):
def execute(self): def execute(self):
logger.debug(f"Check emails for new orders") logger.debug(f"Check emails for new orders")
emails = self.email_client.get_emails(folder="spareparts", only_unseen=True, mark_as_read=True) emails = self.email_client.get_emails(folder="spareparts", only_unseen=True, mark_as_read=True)
for email in emails: for email in emails:
logger.info(email.subj) logger.info(email.subj)
logger.info(email.from_addr) logger.info(email.from_addr)
@@ -36,6 +39,7 @@ class MailOrderBot(ConfigManager):
logger.info(email.body) logger.info(email.body)
logger.info(email.first_sender) logger.info(email.first_sender)
logger.info('--------------------------------') logger.info('--------------------------------')
logger.critical("mail checked")
logger = logging.getLogger() logger = logging.getLogger()

View File

@@ -0,0 +1,56 @@
suppliers:
# order@stparts.ru
"order@stparts.ru":
sheet_name: "TDSheet" # Название листа Excel
header_row: 0 # Номер строки с заголовками (0 = первая)
# Маппинг: внутреннее_поле -> названиеолонки_в_Excel
mapping:
article: "Номер"
manufacturer: "Бренд"
name: "Описание"
price: "Цена"
quantity: "Количество"
#total: "Сумма"
#Вопросы: что за поле "Фактическая_отгрузка"?
# Дополнительные настройки (опционально)
options:
decimal_separator: ","
encoding: "utf-8"
# Рай Авто СПб
EMPTY-FROM:
sheet_name: 0
header_row: 2 # Заголовки во второй строке
mapping:
article: "Артикул"
manufacturer: "Производитель"
name: "Название"
price: "Цена"
quantity: "Количество"
#total: "Сумма с НДС"
options:
decimal_separator: ","
encoding: "utf-8"
#thousand_separator: ","
# Примечание: гемор - нет имейла
# АвтоТО
"order@avtoto.ru":
sheet_name: "Заказы" # Можно указать индекс листа
header_row: 4
mapping:
article: "Артикул"
manufacturer: "Изготовитель"
name: "Наименование товара"
price: "Цена"
quantity: "Кол-во"
total: "Сумма"
options:
#skip_footer_rows: 3
decimal_separator: ","

View File

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

View File

@@ -0,0 +1,21 @@
import logging
import requests
from ..abstract_task import AbstractTask
logger = logging.getLogger(__name__)
class InstantOrderTest(AbstractTask):
URL = "https://api.telegram.org/bot{0}/sendMessage?chat_id={1}&text={2}"
def do(self) -> None:
positions = self.context["positions"]
message = f"Запрос на создание заказа от {self.context['client']}:\n"
message += "\n".join(f"{pos.article}: {pos.name} ({pos.quantity} x {pos.price} = {pos.total})" for pos in positions)
api_key = self.config["api_key"]
chat_id = self.config["chat_id"]
url = self.URL.format(api_key, chat_id, message)
resp = requests.get(url).json()
logger.info(resp)

View File

@@ -0,0 +1,22 @@
import logging
from abc import ABC, abstractmethod
from typing import Dict, Any
class AbstractTask(ABC):
"""
Абстрактный базовый класс для всех хэндлеров.
"""
def __init__(self, config: Dict[str, Any], context: Dict[str, Any],*args, **kwargs) -> None:
self.config = config
self.context = context
@abstractmethod
def do(self) -> None:
"""
Выполняет работу над заданием
Входные и выходные данные - в self.context
Конфиг задается при инициализации
"""
raise NotImplementedError

View File

@@ -0,0 +1,114 @@
import logging
import pandas as pd
from typing import Dict, Any, Optional, List
from decimal import Decimal
from io import BytesIO
from .order_position import OrderPosition
from ..abstract_task import AbstractTask
logger = logging.getLogger(__name__)
class BasicExcelParser(AbstractTask):
RESULT_SECTION = "positions"
"""
Универсальный парсер, настраиваемый через конфигурацию.
Подходит для большинства стандартных случаев.
"""
def do(self) -> None:
# todo сделать проверку на наличие файла и его тип
file_bytes = BytesIO(self.context.get("attachment")) # 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)
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[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

View File

@@ -0,0 +1,25 @@
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}")

View File

@@ -0,0 +1,18 @@
import logging
import pandas as pd
from typing import Dict, Any, Optional, List
from decimal import Decimal
from ..abstract_task import AbstractTask
logger = logging.getLogger(__name__)
class TestNotifier(AbstractTask):
def do(self) -> None:
positions = self.context["positions"]
print(f"\nПолучено {len(positions)} позиций от {self.context["client"]}:")
for pos in positions: # Первые 5
print(f" - {pos.article}: {pos.name} "
f"({pos.quantity} x {pos.price} = {pos.total})")

View File

@@ -0,0 +1,15 @@
from enum import Enum
class OrderStatus(Enum):
NEW = 1
IN_PROGRESS = 2
COMPLETED = 3
FAILED = 4
OPERATOR_HANDLING = 5
INVALID = 6
class Order:
def __init__(self, context: dict):
attachment = context["attachment"]
self.context = context

View File

@@ -0,0 +1,41 @@
from pathlib import Path
import os
import yaml
import logging
from typing import Dict, Any
from pathlib import Path
from ..task_handler.excel_parsers.basic_excel_parcer import BasicExcelParser
from ..task_handler.notifiers.test_notifier import TestNotifier
from ..task_handler.abcp_client.OrderCreator import InstantOrderTest
logger = logging.getLogger(__name__)
class TaskProcessor:
def __init__(self, config_path: Path):
self.config_path = config_path
self.context = dict()
def process(self, client, attachment):
config = self._load_config(client)
self.context = dict()
self.context["client"] = client
self.context["attachment"] = attachment.content
self.context["status"] = client
for stage in config["pipeline"]:
handler_name = stage["handler"]
logger.info(f"Processing handler: {handler_name}")
task = globals()[handler_name](stage.get("config", None), self.context)
task.do()
return self.context
pass
def _load_config(self, client) -> Dict[str, Any]:
"""Загружает конфигурацию из YAML или JSON"""
path = os.path.join(self.config_path, client + '.yml')
with open(path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)

View File

@@ -4,6 +4,7 @@ from mail_order_bot.email_client import EmailClient
if __name__ == "__main__": if __name__ == "__main__":
print(__name__)
# подгружаем переменные окружения # подгружаем переменные окружения
load_dotenv() load_dotenv()
@@ -15,13 +16,13 @@ if __name__ == "__main__":
imap_port=os.getenv('IMAP_PORT'), imap_port=os.getenv('IMAP_PORT'),
smtp_port=os.getenv('SMTP_PORT') smtp_port=os.getenv('SMTP_PORT')
) )
emails = email_client.get_emails(folder='spareparts', only_unseen=True, mark_as_read=True) emails = email_client.get_emails(folder='spareparts', only_unseen=True, mark_as_read=False)
for email in emails: for email in emails:
print(email.subj) print(email.subj)
print(email.from_addr) print(email.from_addr)
print(email.from_email)
print(email.dt) print(email.dt)
print(email.body)
print(email.first_sender) print(email.first_sender)
print('--------------------------------') print('--------------------------------')

View File

@@ -0,0 +1,27 @@
# Конфигурационный файл для контрагента todx.ru
pipeline:
# Обработчик вложений
- handler: "BasicExcelParser"
config:
sheet_name: 0
key_field: "Код детали"
mapping:
article: "Код детали"
manufacturer: "Производитель"
name: "Наименование"
price: "Цена\nдетали"
quantity: "Кол-\nво"
total: "Сумма"
- handler: InstantOrderTest
config:
api_key: "8056899069:AAFEfw9QRMvmEwQyH0CI4e_v_sZuOSdNWcE"
chat_id: 211945135
- handler: "TestNotifier"

View File

@@ -0,0 +1,59 @@
import os
import chardet # pip install chardet
import traceback
from mail_order_bot.task_handler import TaskProcessor
import datetime
# установим рабочую директорию
import os
os.chdir(os.path.dirname(os.path.abspath(__file__)))
from io import BytesIO
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.WARNING, format='%(module)s - %(message)s') # %(asctime)s -
BASE_PATH = './files'
from mail_order_bot.email_client import EmailMessage, EmailAttachment
processor = TaskProcessor("./configs")
for provider_name in os.listdir(BASE_PATH):
provider_folder = os.path.join(BASE_PATH, provider_name)
if os.path.isdir(provider_folder):
for file_name in os.listdir(provider_folder):
file_path = os.path.join(provider_folder, file_name)
if os.path.isfile(file_path):
with open(file_path, 'rb') as file: # бинарный режим
raw_data = file.read()
# Создаем объект EmailAttachment
att = EmailAttachment(file_name, raw_data)
email = EmailMessage(
from_addr=provider_name,
from_email='test@gmail.com',
subj='order request',
dt=datetime.datetime.now(),
body= 'body text',
attachments=[att],
first_sender='test@gmail.com'
)
#bio = BytesIO(raw_data)
print("========================================================")
print(f'Обработка: {provider_name} - {file_name}')
try:
positions_a = processor.process(provider_name, att)
except Exception as e:
print(f"Ошибка обработки: {e}", traceback.format_exc())

View File

@@ -0,0 +1,48 @@
import os
import chardet # pip install chardet
import traceback
from mail_order_bot.excel_processor import ExcelProcessor
# установим рабочую директорию
import os
os.chdir(os.path.dirname(os.path.abspath(__file__)))
from io import BytesIO
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.WARNING, format='%(module)s - %(message)s') #%(asctime)s -
BASE_PATH = './files'
processor = ExcelProcessor("./suppliers.yml")
for provider_name in os.listdir(BASE_PATH):
provider_folder = os.path.join(BASE_PATH, provider_name)
if os.path.isdir(provider_folder):
for file_name in os.listdir(provider_folder):
file_path = os.path.join(provider_folder, file_name)
if os.path.isfile(file_path):
with open(file_path, 'rb') as file: # бинарный режим
raw_data = file.read()
bio = BytesIO(raw_data)
print("========================================================")
print(f'Обработка: {provider_name} - {file_name}')
try:
positions_a = processor.process(
file_bytes=bio,
file_name=file_name,
supplier_name=provider_name
)
print(f"\nПолучено {len(positions_a)} позиций от {provider_name}:")
for pos in positions_a: # Первые 5
print(f" - {pos.article}: {pos.name} "
f"({pos.quantity} x {pos.price} = {pos.total})")
except Exception as e:
print(f"Ошибка обработки: {e}", traceback.format_exc())

View File

@@ -0,0 +1,411 @@
suppliers:
# order@stparts.ru
"order@stparts.ru":
sheet_name: "TDSheet" # Название листа Excel
key_field: "Номер"
# Маппинг: внутреннее_поле -> названиеолонки_в_Excel
mapping:
article: "Номер"
manufacturer: "Бренд"
name: "Описание"
price: "Цена"
quantity: "Количество"
#total: "Сумма"
#Вопросы: что за поле "Фактическая_отгрузка"?
EMPTY-FROM:
sheet_name: 0
key_field: "Артикул" # Заголовки во второй строке
mapping:
article: "Артикул"
manufacturer: "Производитель"
name: "Название"
price: "Цена"
quantity: "Количество"
#total: "Сумма с НДС"
options:
decimal_separator: ","
encoding: "utf-8"
#thousand_separator: ","
# Примечание: гемор - нет имейла
"order@avtoto.ru":
sheet_name: "Заказы" # Можно указать индекс листа
key_field: "Артикул"
mapping:
article: "Артикул"
manufacturer: "Изготовитель"
name: "Наименование товара"
price: "Цена"
quantity: "Кол-во"
total: "Сумма"
"автолига.рф":
sheet_name: 0 # Можно указать индекс листа
key_field: "Артикул"
mapping:
article: "Артикул"
manufacturer: "Производитель"
name: "Название детали"
price: "Цена, р."
quantity: "Количество"
total: "Сумма, р."
abstd.ru:
sheet_name: 0 # Можно указать индекс листа
key_field: "Артикул поставщика"
mapping:
article: "Артикул поставщика"
manufacturer: "Бренд поставщика"
name: "Наименование"
price: "Цена поставщика"
quantity: "Кол-во"
total: "Сумма"
adeo.pro:
sheet_name: 0 # Можно указать индекс листа
key_field: "Каталожный номер"
mapping:
article: "Каталожный номер"
manufacturer: "Производитель"
name: "Наименование"
price: "Цена"
quantity: "Кол-во"
total: "Сумма"
amtel.club:
sheet_name: 0 # Можно указать индекс листа
key_field: "Номер"
mapping:
article: "Номер"
manufacturer: "Фирма"
name: "Наименование"
price: "Цена"
quantity: "Кол-во"
total: "Сумма"
auto-sputnik.ru:
sheet_name: 0 # Можно указать индекс листа
key_field: "Артикул"
mapping:
article: "Артикул"
manufacturer: "Производитель"
name: "Наименование"
price: "Цена"
quantity: "Кол-во"
total: "Сумма"
autocode.ru:
sheet_name: 0 # Можно указать индекс листа
key_field: "Артикул"
mapping:
article: "Артикул"
manufacturer: "Бренд"
name: "Наименование"
price: "Цена"
quantity: "Кол-во"
# Надо ли как-то учитывать доп поля типа Кол-во в отказ?
autopiter.ru:
sheet_name: 0 # Можно указать индекс листа
key_field: "Номер"
mapping:
article: "Номер"
manufacturer: "Каталог"
price: "Цена"
quantity: "Кол-во"
autostels.ru:
sheet_name: 0 # Можно указать индекс листа
key_field: "№ Детали"
mapping:
article: "№ Детали"
manufacturer: "Производитель"
name: "Наименование"
price: "Прайс"
quantity: "Количество"
total: "Сумма"
avtoformula.ru:
sheet_name: 0 # Можно указать индекс листа
key_field: "№ Детали"
mapping:
article: "№ Детали"
manufacturer: "Производитель"
name: "Наименование"
price: "Прайс"
quantity: "Количество"
total: "Сумма"
autoeuro.ru:
sheet_name: 0 # Можно указать индекс листа
key_field: "Номер Производителя"
mapping:
article: "Номер Производителя"
manufacturer: "Производитель"
price: "Цена"
quantity: "Количество"
avtogut.ru:
sheet_name: 0 # Можно указать индекс листа
key_field: "Артикул"
mapping:
article: "Артикул"
manufacturer: "Производитель"
name: "Наименование"
price: "Цена, р."
quantity: "Количество"
total: "Сумма, р."
avtokrep.spb.ru:
sheet_name: 0 # Можно указать индекс листа
key_field: "Артикул"
mapping:
article: "Артикул"
manufacturer: "Бренд"
name: "Наименование"
price: "Цена"
quantity: "Количество"
avtolavka.net:
sheet_name: 0 # Можно указать индекс листа
key_field: "Номер"
mapping:
article: "Номер"
manufacturer: "Бренд"
name: "Описание"
price: "Цена закупки"
quantity: "Количество"
avtoto.ru:
sheet_name: 0 # Можно указать индекс листа
key_field: "Артикул"
mapping:
article: "Артикул"
manufacturer: "Производитель"
name: "Название"
price: "Цена (рубли)"
quantity: "Кол-во"
detal.msk.ru:
sheet_name: 0 # Можно указать индекс листа
key_field: "Артикул"
mapping:
article: "Артикул"
manufacturer: "Производитель"
name: "Наименование"
price: "Цена"
quantity: "Количество"
total: "Сумма"
detali.ru:
sheet_name: 0 # Можно указать индекс листа
key_field: "Код поставщика"
mapping:
article: "Код поставщика"
manufacturer: "Бренд"
name: "Наименование"
price: "Цена"
quantity: "Количество"
e-tape.ru:
sheet_name: 0 # Можно указать индекс листа
key_field: "Артикул"
mapping:
article: "Артикул"
manufacturer: "Производитель"
name: "Название детали"
price: "Цена, р."
quantity: "Количество"
total: "Сумма, р."
forum-auto.ru:
sheet_name: 0 # Можно указать индекс листа
key_field: "Номер по каталогу"
mapping:
article: "Номер по каталогу"
manufacturer: "Производитель"
name: "Название детали"
price: "Цена"
quantity: "Количество"
"info_avtor@mail.ru":
sheet_name: 0 # Можно указать индекс листа
key_field: "Артикул"
mapping:
article: "Артикул"
manufacturer: "Бренд"
name: "Товары (работы, услуги)"
price: "Цена"
quantity: "Количество"
total: "Сумма"
"mikado-parts.ru":
sheet_name: 0 # Можно указать индекс листа
key_field: "артикул"
mapping:
article: "артикул"
manufacturer: "бренд"
name: "наименование"
price: "цена"
quantity: "количество"
"multikrep.com":
sheet_name: 0 # Можно указать индекс листа
key_field: "Артикул"
mapping:
article: "Артикул"
manufacturer: "Бренд"
name: "Наименование"
price: "Цена"
quantity: "Количество"
"only-original.parts":
sheet_name: 0 # Можно указать индекс листа
key_field: "Номер"
mapping:
article: "Номер"
manufacturer: "Бренд"
name: "Описание"
price: "Цена закупки"
quantity: "Количество"
"part-kom.ru":
sheet_name: 0 # Можно указать индекс листа
key_field: "Код\nпоставщика"
mapping:
article: "Код\nпоставщика"
manufacturer: "Изготовитель"
name: "Наименование товара"
price: "Цена"
quantity: "Кол-во"
total: "Сумма"
"parterra.ru":
sheet_name: 0 # Можно указать индекс листа
key_field: "Артикул поставщика"
mapping:
article: "Артикул поставщика"
manufacturer: "Производитель Поставщика"
name: "Номенклатура"
price: "Цена"
quantity: "Количество (в единицах хранения)"
total: "Сумма с НДС"
"pmmobile.ru":
sheet_name: 0 # Можно указать индекс листа
key_field: "Номер"
mapping:
article: "Номер"
manufacturer: "Бренд"
name: "Описание"
price: "Цена закупки"
quantity: "Количество"
"rmsauto.ru":
sheet_name: 0 # Можно указать индекс листа
key_field: "Номер детали"
mapping:
article: "Номер детали"
manufacturer: "Производитель"
name: "Наименование детали"
price: "Цена, рублей (с НДС)"
quantity: "Количество заказанное, штук"
total: "Сумма, рублей (с НДС)"
"rnsprice.ru":
sheet_name: 0 # Можно указать индекс листа
key_field: "Артикул"
mapping:
article: "Артикул"
manufacturer: "Марка"
name: "Наименование"
price: "Цена"
quantity: "Кол-во"
total: "Сумма"
"stutzen.ru":
sheet_name: 0 # Можно указать индекс листа
key_field: "Артикул"
mapping:
article: "Артикул"
manufacturer: "Производитель"
name: "Название детали"
price: "Цена, р."
quantity: "Количество"
total: "Сумма, р."
"sz-snab.ru":
sheet_name: 0 # Можно указать индекс листа
key_field: "Номер"
mapping:
article: "Номер"
manufacturer: "Бренд"
name: "Описание"
price: "Цена закупки"
quantity: "Количество"
"todx.ru":
sheet_name: 0 # Можно указать индекс листа
key_field: "Код детали"
mapping:
article: "Код детали"
manufacturer: "Производитель"
name: "Наименование"
price: "Цена\nдетали"
quantity: "Кол-\nво"
total: "Сумма"
"uniqom.ru":
sheet_name: 0 # Можно указать индекс листа
key_field: "Артикул"
mapping:
article: "Артикул"
manufacturer: "Брэнд"
name: "Наименование товара"
price: "Цена"
quantity: "Заказ, кол-во"
total: "Сумма"
"Рай Авто СПб":
sheet_name: 0 # Можно указать индекс листа
key_field: "Артикул"
mapping:
article: "Артикул"
manufacturer: "Производитель"
name: "Название"
price: "Цена"
quantity: "Количество"