first commit
This commit is contained in:
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# виртуальное окружение
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# кэш питона
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.pytest_cache
|
||||||
|
|
||||||
|
# IDE и редакторы
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS мусор
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
#Подсказки
|
||||||
|
hint.py
|
||||||
|
|
||||||
|
#env
|
||||||
|
*.env
|
||||||
|
#db
|
||||||
|
*.db
|
||||||
|
|
||||||
|
#Примеры документов
|
||||||
|
input/
|
||||||
|
output/
|
||||||
25
columns.json
Normal file
25
columns.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"ozon":{
|
||||||
|
"Артикул": 0,
|
||||||
|
"Наименование":1,
|
||||||
|
"Выручка": 14,
|
||||||
|
"Выкупы": 7,
|
||||||
|
"Возвраты":13,
|
||||||
|
"Не_выкупы":16,
|
||||||
|
"Налог_в_руб":113,
|
||||||
|
"Прибыль":116,
|
||||||
|
"Все_удержания_магазина": 102
|
||||||
|
},
|
||||||
|
"wb":{
|
||||||
|
|
||||||
|
},
|
||||||
|
"yandex":{
|
||||||
|
"Артикул": 0,
|
||||||
|
"Выручка": 16,
|
||||||
|
"Выкупы": 8,
|
||||||
|
"Возвраты":15,
|
||||||
|
"Налог_в_руб":64,
|
||||||
|
"Прибыль":65,
|
||||||
|
"Все_удержания_магазина": 54
|
||||||
|
}
|
||||||
|
}
|
||||||
3
env_example
Normal file
3
env_example
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
INPUTDIR="..."
|
||||||
|
OUTPUTDIR="..."
|
||||||
|
PATTERN="..."
|
||||||
1
handlers/__init__.py
Normal file
1
handlers/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import s_daemon
|
||||||
71
handlers/handler.py
Normal file
71
handlers/handler.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from schema.pydantic import settings, Translit
|
||||||
|
import os
|
||||||
|
class BaseHandler:
|
||||||
|
def __init__(self, file_path:str):
|
||||||
|
self.file_name = file_path
|
||||||
|
self.file_path = os.path.join(settings.INPUTDIR, file_path)
|
||||||
|
|
||||||
|
def struct(self):
|
||||||
|
try:
|
||||||
|
return pd.ExcelFile(self.file_path)
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"⚠️ Ошибка при получении структуры {self.file_path}: {e}")
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
try:
|
||||||
|
return pd.read_excel(self.file_path)
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(f"⚠️ Ошибка при чтении файла {self.file_path}: {e}")
|
||||||
|
|
||||||
|
class Handler(BaseHandler):
|
||||||
|
def get_articles_with_sales(self, columns:dict, sheet_name:str):
|
||||||
|
xls = self.struct()
|
||||||
|
|
||||||
|
if sheet_name not in xls.sheet_names:
|
||||||
|
raise Exception('⚠️ Лист {sheet_name} не найден')
|
||||||
|
|
||||||
|
df = pd.read_excel(xls, sheet_name=sheet_name)
|
||||||
|
df = df.iloc[:, list(columns.values())]
|
||||||
|
df.columns = list(columns.keys())
|
||||||
|
|
||||||
|
#Нормализация
|
||||||
|
df['Артикул'] = df['Артикул'].replace(Translit.TRANSLIT, regex=True)
|
||||||
|
|
||||||
|
#группировка
|
||||||
|
df['Артикул'] = df['Артикул'].astype(str).str.upper().str.extract(f'({settings.PATTERN})')
|
||||||
|
df.dropna(subset=["Артикул"], inplace=True)
|
||||||
|
agg_dict = {col: "sum" for col in df.columns if col != "Артикул" and col != "Наименование"} # по умолчанию суммируем все кроме Артикул
|
||||||
|
if "Наименование" in df.columns:
|
||||||
|
agg_dict["Наименование"] = lambda x: "\n".join(sorted(set(x)))
|
||||||
|
df= df.groupby("Артикул", as_index=False).agg(agg_dict)
|
||||||
|
|
||||||
|
#Исчисляемые колонки
|
||||||
|
df["Все удержания в %"] = df.apply(
|
||||||
|
lambda row: (row["Все_удержания_магазина"] / row["Выручка"] * 100) if row["Выручка"] != 0 else 0,
|
||||||
|
axis=1
|
||||||
|
)
|
||||||
|
if "Выкупы" in df.columns and "Не_выкупы" in df.columns:
|
||||||
|
df["Всего заказано"] = df.apply(
|
||||||
|
lambda row: row["Выкупы"]+row["Не_выкупы"],
|
||||||
|
axis=1
|
||||||
|
)
|
||||||
|
df["Процент выкупа"] = df.apply(
|
||||||
|
lambda row: ((row["Выкупы"] - row["Возвраты"]) / row["Всего заказано"] * 100)
|
||||||
|
if row["Всего заказано"] != 0 else 0,
|
||||||
|
axis=1
|
||||||
|
)
|
||||||
|
df["Маржинальность"] = df.apply(
|
||||||
|
lambda row: (row["Прибыль"] / row["Выручка"] * 100) if row["Выручка"] != 0 else 0,
|
||||||
|
axis=1
|
||||||
|
)
|
||||||
|
df=df.round(2)
|
||||||
|
df = df.sort_values(ascending=False,by="Прибыль")
|
||||||
|
def multi_style(val):
|
||||||
|
if val < 0:
|
||||||
|
return "background-color: red"
|
||||||
|
elif val > 0:
|
||||||
|
return "background-color: green; color: white"
|
||||||
|
return ""
|
||||||
|
styled = df.style.map(multi_style, subset=["Маржинальность","Прибыль"])
|
||||||
|
styled.to_excel(f"output/{self.file_name}", engine="openpyxl", index=False)
|
||||||
29
handlers/s_daemon.py
Normal file
29
handlers/s_daemon.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import os
|
||||||
|
from schema.pydantic import settings, jsonread
|
||||||
|
from handlers.handler import Handler
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
files = os.listdir(settings.INPUTDIR)
|
||||||
|
for i in ("input", "output"):
|
||||||
|
path=Path(f"./{i}")
|
||||||
|
path.mkdir(exist_ok=True)
|
||||||
|
# Print the files
|
||||||
|
for file in files:
|
||||||
|
if file.startswith("~$"): #Проверка не редактируемый ли файл
|
||||||
|
continue
|
||||||
|
# Check if item is a file, not a directory
|
||||||
|
if not os.path.isdir(os.path.join(settings.INPUTDIR, file)):
|
||||||
|
match file:
|
||||||
|
case _ if "ozon" in file:
|
||||||
|
print("Это OZON")
|
||||||
|
calculate = Handler(file)
|
||||||
|
calculate.get_articles_with_sales(jsonread.merchant("ozon"), sheet_name="По товарам")
|
||||||
|
case _ if "yandex" in file:
|
||||||
|
print("Это Yandex")
|
||||||
|
calculate = Handler(file)
|
||||||
|
calculate.get_articles_with_sales(jsonread.merchant("yandex"), sheet_name="Отчёт по товарам")
|
||||||
|
case _ if "wb" in file:
|
||||||
|
print("Это WB")
|
||||||
|
case _:
|
||||||
|
print("Не распознано")
|
||||||
|
|
||||||
4
makefile
Normal file
4
makefile
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
VENV=source ./.venv/bin/activate;
|
||||||
|
.PHONY: run
|
||||||
|
run:
|
||||||
|
$(VENV) python run.py
|
||||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pydantic==2.12.5
|
||||||
|
pydantic_settings==2.13.0
|
||||||
|
openpyxl==3.1.5
|
||||||
|
pandas==3.0.0
|
||||||
|
jinja2==3.1.6
|
||||||
6
run.py
Normal file
6
run.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import handlers
|
||||||
|
|
||||||
|
def init():
|
||||||
|
handlers.s_daemon
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init()
|
||||||
34
schema/pydantic.py
Normal file
34
schema/pydantic.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
import json
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
INPUTDIR:str
|
||||||
|
OUTPUTDIR:str
|
||||||
|
PATTERN:str
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8"
|
||||||
|
)
|
||||||
|
class JsonRead():
|
||||||
|
def __init__(self):
|
||||||
|
with open("columns.json", "r", encoding="utf-8") as f:
|
||||||
|
self.data = json.load(f)
|
||||||
|
def merchant(self, key):
|
||||||
|
return self.data.get(key)
|
||||||
|
|
||||||
|
class Translit():
|
||||||
|
TRANSLIT = {
|
||||||
|
'А': 'A',
|
||||||
|
'В': 'B',
|
||||||
|
'Е': 'E',
|
||||||
|
'К': 'K',
|
||||||
|
'М': 'M',
|
||||||
|
'Н': 'H',
|
||||||
|
'О': 'O',
|
||||||
|
'Р': 'P',
|
||||||
|
'С': 'C',
|
||||||
|
'Т': 'T',
|
||||||
|
'Х': 'X',
|
||||||
|
}
|
||||||
|
settings = Settings()
|
||||||
|
jsonread = JsonRead()
|
||||||
Reference in New Issue
Block a user