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
.cursorignore
logs/
files/

View File

@@ -1,11 +1,12 @@
Metadata-Version: 2.4
Name: MailOrderBot
Version: 1.0.2
Version: 1.0.4
Summary: Config manager for building applications
Author-email: Aleksei Zosimov <lesha.spb@gmail.com>
Project-URL: Homepage, https://git.lesha.spb.ru/alex/config_manager
Project-URL: Documentation, https://git.lesha.spb.ru/alex/config_manager
Project-URL: Repository, 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/mail_order_bot
Project-URL: Repository, https://git.lesha.spb.ru/alex/mail_order_bot
Requires-Python: >=3.12
Description-Content-Type: text/markdown
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/main.py
src/mail_order_bot/email_client/__init__.py
src/mail_order_bot/email_client/email_client.py
src/mail_order_bot/email_client/email_objects.py
src/mail_order_bot/email_client/client.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/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/parser_factory.py
src/mail_order_bot/excel_processor/processor.py

View File

@@ -1 +1,2 @@
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
work_interval: 30
email_dir: "spareparts"
# === Логирование ===
# Логирование =================================================================
log:
version: 1
disable_existing_loggers: False
@@ -40,7 +96,7 @@ log:
alias: "Mail order bot"
# -- Логгеры --
# Логгеры
loggers:
'':
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:
# Читаем Excel
df = self._read_excel(filepath)
df = self._read_excel(file_bytes)
# Удаляем пустые строки
df = df.dropna(how='all')

View File

@@ -2,6 +2,7 @@ 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
@@ -26,7 +27,7 @@ class ExcelParser(ABC):
"""
pass
def _read_excel(self, filepath: str) -> pd.DataFrame:
def _read_excel_from_file(self, filepath: str) -> pd.DataFrame:
"""Общий метод для чтения Excel файлов"""
return pd.read_excel(
filepath,
@@ -35,3 +36,14 @@ class ExcelParser(ABC):
#engine='openpyxl'
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._setup_logging()
@@ -27,7 +27,9 @@ class ExcelProcessor:
def process_file(
self,
filepath: str,
#filepath: str,
file_bytes: str,
file_name: str,
supplier_name: str,
validate: bool = True
) -> List[OrderPosition]:
@@ -46,15 +48,15 @@ class ExcelProcessor:
ValueError: Если контрагент не найден
FileNotFoundError: Если файл не найден
"""
logger.info(f"Начало обработки файла: {filepath} для {supplier_name}")
logger.info(f"Начало обработки файла: {file_name} для {supplier_name}")
# Проверка существования файла
if not Path(filepath).exists():
raise FileNotFoundError(f"Файл не найден: {filepath}")
#if not Path(filepath).exists():
# raise FileNotFoundError(f"Файл не найден: {filepath}")
# Получаем парсер и обрабатываем
parser = self.factory.get_parser(supplier_name)
positions = parser.parse(filepath)
positions = parser.parse(file_bytes)
# Дополнительная валидация если нужна
if validate:

View File

@@ -7,6 +7,7 @@ import os
from dotenv import load_dotenv
from email_client import EmailClient
from excel_proceccor import ExcelProcessor
logger = logging.getLogger()
@@ -27,8 +28,10 @@ class MailOrderBot(ConfigManager):
def execute(self):
logger.debug(f"Check emails for new orders")
emails = self.email_client.get_emails(folder="spareparts", only_unseen=True, mark_as_read=True)
for email in emails:
logger.info(email.subj)
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: ","