16 Commits

Author SHA1 Message Date
1239db4e23 добавил в игнор файлы с заказами 2025-11-11 23:09:53 +03:00
ee439c6bf6 Рефаткоринг, добавляю пайплайн 2025-11-11 23:08:40 +03:00
6abceda30e Рефакторинг, добавляю пайплайн 2025-11-11 23:07:52 +03:00
0db1509f0f Настройка парсера 2025-11-10 21:45:14 +03:00
f6d186ab56 Try parse first excel file 2025-11-08 21:46:34 +03:00
bd1faa5a79 Completed transfer of classes, corrected imports. 2025-11-08 17:46:27 +03:00
50ac5c97ab no message 2025-11-07 21:07:04 +03:00
0de91df486 Игнорим логи 2025-11-07 21:06:52 +03:00
cfefb49ef8 Починил парсинг адресов эл почты 2025-11-07 21:06:00 +03:00
e18407f33e Добавил логирование в телегу 2025-11-07 21:05:21 +03:00
9ff34bc049 Добавил в dockerfile рабочую директорию 2025-11-02 16:42:10 +03:00
b91d621efd Поправил конифг pyproject.toml - изменил установку пакета config_manager 2025-11-02 13:03:38 +03:00
22bdcafc59 Тестовое приложене готово 2025-11-01 21:56:30 +03:00
093b29e16c Актализировал пакеты и структуру 2025-11-01 08:45:55 +03:00
81bcb0c3b8 no message 2025-10-29 23:02:21 +03:00
d93964b793 Docker configs was added 2025-10-29 23:01:26 +03:00
43 changed files with 1539 additions and 363 deletions

7
.gitignore vendored
View File

@@ -1,5 +1,10 @@
venv
.venv
.vscode
__pycache__
.env
.cursorignore
logs/
files/
Настроено/
Не настроено/
проблемные/

66
docker-compose.yml Normal file
View File

@@ -0,0 +1,66 @@
version: '3.8'
services:
mail_order_bot:
build:
context: .
dockerfile: Dockerfile
args:
GIT_REPO_URL: https://git.lesha.spb.ru/alex/mail_order_bot
GIT_BRANCH: master
container_name: mail_order_bot
restart: unless-stopped
# Монтирование .env файла для секретов
env_file:
- .env
# Дополнительные переменные окружения
environment:
- PYTHONUNBUFFERED=1
# Монтирование volumes (если нужно)
volumes:
- .//app/data # для хранения данных
- ./logs:/app/logs # для логов
# Если приложение использует сеть
# ports:
# - "8000:8000"
# Если нужны другие сервисы (БД, Redis и т.д.)
# depends_on:
# - postgres
# - redis
# Настройки логирования
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Раскомментируйте, если нужны дополнительные сервисы
#
# postgres:
# image: postgres:15-alpine
# container_name: mail_order_bot_db
# restart: unless-stopped
# environment:
# POSTGRES_DB: mail_order_bot
# POSTGRES_USER: ${POSTGRES_USER}
# POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
# volumes:
# - postgres_/var/lib/postgresql/data
# ports:
# - "5432:5432"
#
# redis:
# image: redis:7-alpine
# container_name: mail_order_bot_redis
# restart: unless-stopped
# ports:
# - "6379:6379"
# volumes:
# postgres_

32
dockerfile Normal file
View File

@@ -0,0 +1,32 @@
# Используем официальный образ Python
FROM python:3.12-slim
# Устанавливаем git для клонирования репозитория
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
# Создаем рабочую директорию
WORKDIR /app
# Клонируем репозиторий
ARG GIT_REPO_URL=https://git.lesha.spb.ru/alex/mail_order_bot
ARG GIT_BRANCH=master
RUN git clone --branch ${GIT_BRANCH} ${GIT_REPO_URL} .
# Устанавливаем зависимости из requirements.txt (если есть)
RUN if [ -f requirements.txt ]; then pip install --no-cache-dir -r requirements.txt; fi
# Устанавливаем пакет и его зависимости из pyproject.toml
RUN pip install --no-cache-dir -e .
# Альтернативный вариант для production (без editable mode):
# RUN pip install --no-cache-dir .
# Устанавливаем переменные окружения для Python
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
# Команда запуска приложения (замените на вашу)
# CMD ["python", "-m", "mail_order_bot"]
WORKDIR /app/src/mail_order_bot
CMD ["python", "/app/src/mail_order_bot/main.py"]

View File

@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "MailOrderBot"
description = "Config manager for building applications"
version = "1.0.2"
version = "1.0.4"
authors = [
{ name = "Aleksei Zosimov", email = "lesha.spb@gmail.com" }
]
@@ -13,14 +13,20 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"python-dotenv>=1.0.0"
"python-dotenv>=1.0.0",
"config_manager @ git+https://git.lesha.spb.ru/alex/config_manager.git@master"
]
dynamic = ["version"]
[tool.setuptools.packages.find]
where = ["src"]
[project.urls]
Homepage = "https://git.lesha.spb.ru/alex/config_manager"
Documentation = "https://git.lesha.spb.ru/alex/config_manager"
Repository = "https://git.lesha.spb.ru/alex/config_manager"
Homepage = "https://git.lesha.spb.ru/alex/mail_order_bot"
Documentation = "https://git.lesha.spb.ru/alex/mail_order_bot"
Repository = "https://git.lesha.spb.ru/alex/mail_order_bot"
[tool.pytest.ini_options]
addopts = [
"--import-mode=importlib",
]

View File

@@ -1,14 +0,0 @@
[build-system]
requires = ["setuptools>=75.3.0"]
build-backend = "setuptools.build_meta"
[project]
name = "MailOrderBot"
requires-python = ">=3.12"
dependencies = [
"python-dotenv>=1.0.0"
]
dynamic = ["version"]
[tool.setuptools.packages.find]
where = ["src"]

View File

@@ -0,0 +1,12 @@
Metadata-Version: 2.4
Name: MailOrderBot
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/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

@@ -0,0 +1,18 @@
README.md
pyproject.toml
src/MailOrderBot.egg-info/PKG-INFO
src/MailOrderBot.egg-info/SOURCES.txt
src/MailOrderBot.egg-info/dependency_links.txt
src/MailOrderBot.egg-info/requires.txt
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/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/order_position.py
src/mail_order_bot/excel_processor/parser_factory.py
src/mail_order_bot/excel_processor/processor.py

View File

@@ -0,0 +1 @@

View File

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

View File

@@ -0,0 +1 @@
mail_order_bot

View File

@@ -1,56 +0,0 @@
# === Раздел с общими конфигурационными параметрами ===
runtime: 5
update_interval: 1
work_interval: 3
# === Логирование ===
log:
version: 1
disable_existing_loggers: False
formatters:
standard:
format: '%(asctime)s %(name)30s [%(levelname)8s]: %(message)s'
telegram:
format: '%(message)s'
handlers:
console:
level: DEBUG
formatter: standard
class: logging.StreamHandler
stream: ext://sys.stdout # Default is stderr
file:
level: DEBUG
formatter: standard
class: logging.handlers.RotatingFileHandler
filename: logs/log.log
mode: a
maxBytes: 500000
backupCount: 15
#telegram:
# level: CRITICAL
# formatter: telegram
# class: logging_telegram_handler.TelegramHandler
# chat_id: 211945135
# alias: "PDC"
# -- Логгеры --
loggers:
'':
handlers: [console, file]
level: DEBUG
propagate: False
__main__:
handlers: [console, file]
level: WARNING
propagate: False
config_manager:
handlers: [console, file]
level: DEBUG

View File

@@ -1,100 +0,0 @@
# === Раздел с общими конфигурационными параметрами ===
runtime:
symbols: ["BTC_USDT", "ETH_USDT", "USDD_USDT", "TRX_USDT", "BTT_USDT", "NFT_USDT", "XRP_USDT",
"ETH_BTC", "XRP_BTC", "TRX_BTC", "LTC_BTC", "EOS_BTC", "XMR_BTC", "DOGE_BTC",
"NFT_TRX", "ETH_TRX", "JST_TRX", "XRP_TRX",
"ETHBULL_USDT", "BULL_USDT", "BEAR_USDT", "ADABULL_USDT"]
updateTimeout: 45
errorTimeout: 10
orderbook:
levels: [ 0.0, 0.2, 0.4, 0.6, 0.8,
1.0, 1.2, 1.4, 1.6, 1.8,
2.0, 2.2, 2.4, 2.6, 2.8,
3.0, 3.3, 3.6, 3.9,
4.2, 4.5, 4.8,
5.1, 5.4, 5.7, 100 ]
trades:
depth: 300
# === Database params ===
db:
#host: 185.117.118.107
host: 92.53.127.143
port: 59000
database: rt5_dev
# === Логирование ===
log:
version: 1
disable_existing_loggers: False
formatters:
standard:
format: '%(asctime)s %(name)30s [%(levelname)8s]: %(message)s'
telegram:
format: '%(message)s'
handlers:
console:
level: DEBUG
formatter: standard
class: logging.StreamHandler
stream: ext://sys.stdout # Default is stderr
file:
level: DEBUG
formatter: standard
class: logging.handlers.RotatingFileHandler
filename: logs/log.log
mode: a
maxBytes: 500000
backupCount: 15
telegram:
level: CRITICAL
formatter: telegram
class: logging_telegram_handler.TelegramHandler
chat_id: 211945135
alias: "PDC"
# -- Логгеры --
loggers:
'':
handlers: [console, file]
level: ERROR
propagate: False
__main__:
handlers: [console, file, telegram]
level: WARNING
propagate: False
basic_application:
handlers: [console, file, telegram]
level: INFO
config_manager:
level: INFO
log_manager:
level: INFO
poloniex.public:
level: ERROR
controllers.abstract:
level: ERROR
controllers.trades:
level: ERROR
controllers.orderbook:
level: ERROR
clickhouse_connector.clickhouse_connector:
level: ERROR

View File

@@ -0,0 +1,114 @@
# Настройки обработки =================================================================
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
formatters:
standard:
format: '%(asctime)s %(module)15s [%(levelname)8s]: %(message)s'
telegram:
format: '%(message)s'
handlers:
console:
level: DEBUG
formatter: standard
class: logging.StreamHandler
stream: ext://sys.stdout # Default is stderr
file:
level: DEBUG
formatter: standard
class: logging.handlers.RotatingFileHandler
filename: logs/log.log
mode: a
maxBytes: 500000
backupCount: 10
telegram:
level: CRITICAL
formatter: telegram
class: logging_telegram_handler.TelegramHandler
chat_id: 211945135
alias: "Mail order bot"
# Логгеры
loggers:
'':
handlers: [console, file, telegram]
level: INFO
propagate: False
__main__:
handlers: [console, file, telegram]
level: INFO
propagate: False
config_manager:
handlers: [console, file]
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

@@ -1,19 +1,2 @@
from .email_client import EmailClient
from .email_objects import EmailMessage, EmailAttachment
__all__ = ['EmailClient', 'EmailMessage', 'EmailAttachment']
def test_email_client():
email_client = EmailClient(
imap_host='imap.yandex.ru',
smtp_host='smtp.yandex.ru',
email='zosimovaa@yandex.ru',
password='test'
)
assert email_client is not None
email_client.close()
pytest.main()
if __name__ == "__main__":
test_email_client()
from .client import EmailClient
from .objects import EmailMessage, EmailAttachment

View File

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

View File

@@ -14,6 +14,7 @@ class EmailAttachment:
class EmailMessage:
"""Класс для представления электронного письма"""
from_addr: str
from_email: str
subj: str
dt: datetime
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):
"""
Универсальный парсер, настраиваемый через конфигурацию.
Подходит для большинства стандартных случаев.
"""
def parse(self, filepath: str) -> List[OrderPosition]:
def parse(self, file_bytes: str) -> List[OrderPosition]:
try:
# Читаем Excel
df = self._read_excel(filepath)
# Удаляем пустые строки
df = df.dropna(how='all')
df = self._make_dataframe(file_bytes)
# Получаем маппинг колонок из конфигурации
mapping = self.config['mapping']
@@ -24,36 +32,50 @@ class ConfigurableExcelParser(ExcelParser):
if position:
positions.append(position)
except Exception as e:
logger.warning(f"Ошибка парсинга строки {idx}: {e}")
logger.error(f"Ошибка парсинга строки {idx}: {e}, {row}")
continue
logger.info(f"Успешно обработано {len(positions)} позиций из {len(df)} строк")
return positions
except Exception as e:
logger.error(f"Ошибка при обработке файла {filepath}: {e}")
raise
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', 'manufacturer', 'name', 'price', 'quantity', 'total']
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['manufacturer']]).strip(),
name=str(row[mapping['name']]).strip(),
price=Decimal(str(row[mapping['price']])),
quantity=int(row[mapping['quantity']]),
total=Decimal(str(row[mapping['total']])),
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]:
@@ -66,3 +88,26 @@ class ConfigurableExcelParser(ExcelParser):
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,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 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__)
@@ -15,19 +20,9 @@ class ExcelParser(ABC):
self.config = config
@abstractmethod
def parse(self, filepath: str) -> List[OrderPosition]:
def parse(self, file: BytesIO) -> List[OrderPosition]:
"""
Парсит Excel файл и возвращает список позиций.
Должен быть реализован в каждом конкретном парсере.
"""
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 json
import logging
from pathlib import Path
from typing import Dict, Any, List
from .excel_parser import ExcelParser
from .configurable_parser import ConfigurableExcelParser
from .custom_parser_autoeuro import CustomExcelParserAutoeuro
logger = logging.getLogger(__name__)
class ParserFactory:
"""
@@ -10,48 +19,36 @@ class ParserFactory:
# Реестр кастомных парсеров
CUSTOM_PARSERS = {
'supplier_a': SupplierAParser,
'autoeuro.ru': CustomExcelParserAutoeuro,
# Добавляйте сюда специализированные парсеры
}
def __init__(self, config_path: str):
self.config_path = Path(config_path)
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 __init__(self, config: Dict[str, Any]):
self.config = config
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(
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:
parser_class = self.CUSTOM_PARSERS[supplier_name]
logger.info(f"Используется кастомный парсер для {supplier_name}")
logger.debug(f"Используется кастомный парсер для {supplier_name}")
else:
parser_class = ConfigurableExcelParser
logger.info(f"Используется конфигурируемый парсер для {supplier_name}")
logger.debug(f"Используется конфигурируемый парсер для {supplier_name}")
return parser_class(config)
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:
"""
Главный класс-фасад для обработки Excel файлов.
Упрощает использование системы.
"""
def __init__(self, config_path: str = 'config/suppliers.yaml'):
self.factory = ParserFactory(config_path)
self._setup_logging()
def __init__(self, config_path: str = 'config/suppliers.yaml', ):
self.config_path = Path(config_path)
self.config = self._load_config()
self.factory = ParserFactory(self.config)
def _setup_logging(self):
"""Настройка логирования"""
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]:
def process(self, file_bytes: BytesIO, file_name: str, supplier_name: str, validate: bool = False) -> List[OrderPosition]:
"""
Обрабатывает Excel файл от контрагента.
Args:
filepath: Путь к Excel файлу
file_bytes: Байты файла
file_name: Имя файла
supplier_name: Название контрагента (из конфигурации)
validate: Выполнять ли дополнительную валидацию
@@ -34,25 +39,34 @@ class ExcelProcessor:
Raises:
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)
positions = parser.parse(filepath)
positions = parser.parse(file_bytes)
# Дополнительная валидация если нужна
if validate:
positions = self._validate_positions(positions)
logger.info(f"Обработка завершена: получено {len(positions)} позиций")
logger.debug(f"Обработка завершена: получено {len(positions)} позиций")
return positions
def process_file(self, file_path: str, supplier_name: str, validate: bool = False) -> List[OrderPosition]:
# Проверка существования файла
logger.debug(f"Чтение файла: {file_path}")
if not Path(file_path).exists():
raise FileNotFoundError(f"Файл не найден: {file_path}")
with open(file_path, 'rb') as file: # бинарный режим
raw_data = file.read()
bio = BytesIO(raw_data)
positions = self.process(bio, file_path, supplier_name, validate=validate)
return positions
def _validate_positions(self, positions: List[OrderPosition]) -> List[OrderPosition]:
"""Дополнительная валидация позиций"""
valid_positions = []
@@ -83,3 +97,14 @@ class ExcelProcessor:
def get_available_suppliers(self) -> List[str]:
"""Возвращает список доступных контрагентов"""
return self.factory.list_suppliers()
def _load_config(self) -> Dict[str, Any]:
"""Загружает конфигурацию из YAML или JSON"""
if self.config_path.suffix in ['.yaml', '.yml']:
with open(self.config_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
elif self.config_path.suffix == '.json':
with open(self.config_path, 'r', encoding='utf-8') as f:
return json.load(f)
else:
raise ValueError(f"Неподдерживаемый формат конфига: {self.config_path.suffix}")

View File

@@ -0,0 +1,60 @@
from config_manager import ConfigManager
from dotenv import load_dotenv
import asyncio
import logging
import os
from dotenv import load_dotenv
from email_client import EmailClient
from excel_proceccor import ExcelProcessor
logger = logging.getLogger()
class MailOrderBot(ConfigManager):
def __init__(self, *agrs, **kwargs):
super().__init__(*agrs, **kwargs)
self.email_client = EmailClient(
imap_host=os.getenv('IMAP_HOST'),
smtp_host=os.getenv('SMTP_HOST'),
email=os.getenv('EMAIL_USER'),
password=os.getenv('EMAIL_PASSWORD'),
imap_port=os.getenv('IMAP_PORT'),
smtp_port=os.getenv('SMTP_PORT')
)
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)
logger.info(email.dt)
logger.info(email.body)
logger.info(email.first_sender)
logger.info('--------------------------------')
logger.critical("mail checked")
logger = logging.getLogger()
async def main():
app = MailOrderBot("config.yml")
await app.start()
#await asyncio.sleep(200)
#await app.stop()
if __name__ == "__main__":
if os.environ.get("APP_ENV") != "PRODUCTION":
logger.warning("Non production environment")
load_dotenv()
asyncio.run(main())

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

@@ -1,31 +0,0 @@
from config_manager.config_manager import ConfigManager
import asyncio
import logging
import os
os.chdir(os.path.dirname(__file__))
logger = logging.getLogger()
class MailOrderBot(ConfigManager):
def __init__(self, *agrs, **kwargs):
super().__init__(*agrs, **kwargs)
def execute(self):
print("run")
async def main():
app = MailOrderBot("config.yaml") # Можно config.json или config.yaml
task = asyncio.create_task(app.start())
await asyncio.sleep(5)
app.stop()
await task
logger.info("Work finished.")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -1,13 +1,13 @@
import os
from dotenv import load_dotenv
import sys
sys.path.append('./src')
load_dotenv()
from mail_order_bot.email_client import EmailClient
if __name__ == "__main__":
print(__name__)
# подгружаем переменные окружения
load_dotenv()
email_client = EmailClient(
imap_host=os.getenv('IMAP_HOST'),
smtp_host=os.getenv('SMTP_HOST'),
@@ -16,12 +16,13 @@ if __name__ == "__main__":
imap_port=os.getenv('IMAP_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:
print(email.subj)
print(email.from_addr)
print(email.from_email)
print(email.dt)
print(email.body)
print(email.first_sender)
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: "Количество"