Try parse first excel file

This commit is contained in:
2025-11-08 21:46:34 +03:00
parent bd1faa5a79
commit f6d186ab56
13 changed files with 270 additions and 23 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ __pycache__
.env .env
.cursorignore .cursorignore
logs/ 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 = первая)
# Маппинг: внутреннее_поле -> названиеолонки_в_Excel
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 update_interval: 10
work_interval: 30 work_interval: 30
email_dir: "spareparts" email_dir: "spareparts"
# === Логирование === # Логирование =================================================================
log: log:
version: 1 version: 1
disable_existing_loggers: False disable_existing_loggers: False
@@ -40,7 +96,7 @@ log:
alias: "Mail order bot" alias: "Mail order bot"
# -- Логгеры -- # Логгеры
loggers: loggers:
'': '':
handlers: [console, file, telegram] handlers: [console, file, telegram]

View File

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

View File

@@ -15,10 +15,10 @@ 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._read_excel(file_bytes)
# Удаляем пустые строки # Удаляем пустые строки
df = df.dropna(how='all') df = df.dropna(how='all')

View File

@@ -2,6 +2,7 @@ import logging
import pandas as pd import pandas as pd
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, Any, List from typing import Dict, Any, List
from io import BytesIO
from .order_position import OrderPosition from .order_position import OrderPosition
@@ -26,7 +27,7 @@ class ExcelParser(ABC):
""" """
pass pass
def _read_excel(self, filepath: str) -> pd.DataFrame: def _read_excel_from_file(self, filepath: str) -> pd.DataFrame:
"""Общий метод для чтения Excel файлов""" """Общий метод для чтения Excel файлов"""
return pd.read_excel( return pd.read_excel(
filepath, filepath,
@@ -35,3 +36,14 @@ class ExcelParser(ABC):
#engine='openpyxl' #engine='openpyxl'
engine='calamine' engine='calamine'
) )
def _read_excel(self, file_content: bytes) -> pd.DataFrame:
"""Общий метод для чтения Excel файлов из байтового содержимого файла"""
bio = BytesIO(file_content)
return pd.read_excel(
bio,
sheet_name=self.config.get('sheet_name', 0),
header=self.config.get('header_row', 0),
engine='calamine'
)

View File

@@ -14,7 +14,7 @@ class ExcelProcessor:
Упрощает использование системы. Упрощает использование системы.
""" """
def __init__(self, config_path: str = 'config/suppliers.yaml'): def __init__(self, config_path: str = 'config/suppliers.yaml', ):
self.factory = ParserFactory(config_path) self.factory = ParserFactory(config_path)
self._setup_logging() self._setup_logging()
@@ -27,7 +27,9 @@ class ExcelProcessor:
def process_file( def process_file(
self, self,
filepath: str, #filepath: str,
file_bytes: str,
file_name: str,
supplier_name: str, supplier_name: str,
validate: bool = True validate: bool = True
) -> List[OrderPosition]: ) -> List[OrderPosition]:
@@ -46,15 +48,15 @@ class ExcelProcessor:
ValueError: Если контрагент не найден ValueError: Если контрагент не найден
FileNotFoundError: Если файл не найден FileNotFoundError: Если файл не найден
""" """
logger.info(f"Начало обработки файла: {filepath} для {supplier_name}") logger.info(f"Начало обработки файла: {file_name} для {supplier_name}")
# Проверка существования файла # Проверка существования файла
if not Path(filepath).exists(): #if not Path(filepath).exists():
raise FileNotFoundError(f"Файл не найден: {filepath}") # 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:

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)

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,36 @@
import os
import chardet # pip install chardet
from ..src.mail_order_bot.excel_processor import ExcelProcessor
BASE_PATH = './files'
ep = ExcelProcessor("./suppliers.yml")
print("================================================")
for provider_name in os.listdir(BASE_PATH):
print(f'Провайдер: {provider_name}')
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()
detected = chardet.detect(raw_data)
encoding = detected['encoding'] or 'utf-8'
try:
data = raw_data.decode(encoding)
except (UnicodeDecodeError, TypeError):
# Если декодировать не удалось, попробуем utf-8 игнорируя ошибки
data = raw_data.decode('utf-8', errors='ignore')
print(f'Файл: {file_name}')
#print(f'Содержимое: {data}')

View File

@@ -0,0 +1,77 @@
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: ","
# автолига.рф
"автолига.рф":
sheet_name: 0 # Можно указать индекс листа
header_row: 8
mapping:
article: "Артикул"
manufacturer: "Производитель"
name: "Название детали"
price: "Цена, р."
quantity: "Количество"
total: "Сумма, р."
options:
#skip_footer_rows: 3
decimal_separator: ","