From ae9c89c765882f4fff9e311e13926e8918bf89af Mon Sep 17 00:00:00 2001 From: "MH.Dmitrii" Date: Mon, 23 Feb 2026 13:22:55 +0300 Subject: [PATCH] first commit --- .gitignore | 30 +++++++++++++++++++ README.md | 0 columns.json | 25 ++++++++++++++++ env_example | 3 ++ handlers/__init__.py | 1 + handlers/handler.py | 71 ++++++++++++++++++++++++++++++++++++++++++++ handlers/s_daemon.py | 29 ++++++++++++++++++ makefile | 4 +++ requirements.txt | 5 ++++ run.py | 6 ++++ schema/pydantic.py | 34 +++++++++++++++++++++ 11 files changed, 208 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 columns.json create mode 100644 env_example create mode 100644 handlers/__init__.py create mode 100644 handlers/handler.py create mode 100644 handlers/s_daemon.py create mode 100644 makefile create mode 100644 requirements.txt create mode 100644 run.py create mode 100644 schema/pydantic.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2560a10 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/columns.json b/columns.json new file mode 100644 index 0000000..2626fc0 --- /dev/null +++ b/columns.json @@ -0,0 +1,25 @@ +{ + "ozon":{ + "Артикул": 0, + "Наименование":1, + "Выручка": 14, + "Выкупы": 7, + "Возвраты":13, + "Не_выкупы":16, + "Налог_в_руб":113, + "Прибыль":116, + "Все_удержания_магазина": 102 + }, + "wb":{ + + }, + "yandex":{ + "Артикул": 0, + "Выручка": 16, + "Выкупы": 8, + "Возвраты":15, + "Налог_в_руб":64, + "Прибыль":65, + "Все_удержания_магазина": 54 + } +} \ No newline at end of file diff --git a/env_example b/env_example new file mode 100644 index 0000000..710dfb2 --- /dev/null +++ b/env_example @@ -0,0 +1,3 @@ +INPUTDIR="..." +OUTPUTDIR="..." +PATTERN="..." \ No newline at end of file diff --git a/handlers/__init__.py b/handlers/__init__.py new file mode 100644 index 0000000..8659295 --- /dev/null +++ b/handlers/__init__.py @@ -0,0 +1 @@ +from . import s_daemon \ No newline at end of file diff --git a/handlers/handler.py b/handlers/handler.py new file mode 100644 index 0000000..68733d8 --- /dev/null +++ b/handlers/handler.py @@ -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) \ No newline at end of file diff --git a/handlers/s_daemon.py b/handlers/s_daemon.py new file mode 100644 index 0000000..03a22c2 --- /dev/null +++ b/handlers/s_daemon.py @@ -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("Не распознано") + diff --git a/makefile b/makefile new file mode 100644 index 0000000..c24ca7f --- /dev/null +++ b/makefile @@ -0,0 +1,4 @@ +VENV=source ./.venv/bin/activate; +.PHONY: run +run: + $(VENV) python run.py \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a4b6873 --- /dev/null +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..e96e7da --- /dev/null +++ b/run.py @@ -0,0 +1,6 @@ +import handlers + +def init(): + handlers.s_daemon +if __name__ == "__main__": + init() \ No newline at end of file diff --git a/schema/pydantic.py b/schema/pydantic.py new file mode 100644 index 0000000..2cf06c4 --- /dev/null +++ b/schema/pydantic.py @@ -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()