digitalization

This commit is contained in:
2026-01-21 19:39:37 +03:00
parent cd5fcd3452
commit 5041e6bd51
8 changed files with 122 additions and 11 deletions

View File

@@ -2,7 +2,7 @@
Проект для обработки, валидации и агрегации Excel-отчётов от маркетплейсов Проект для обработки, валидации и агрегации Excel-отчётов от маркетплейсов
(**Ozon, Wildberries, Яндекс Маркет**) с целью формирования унифицированных данных и заноса их в 1С (**Ozon, Wildberries, Яндекс Маркет**) с целью формирования унифицированных данных и заноса их в 1С
(реализация, выкупы, товары на комиссии). (реализация, выкупы, товары на комиссии) а также для обработки частного файла оцифровки данных.
Проект представляет собой Python-приложение с backend-логикой, API и обработчиками Excel-файлов. Проект представляет собой Python-приложение с backend-логикой, API и обработчиками Excel-файлов.
@@ -18,10 +18,10 @@
- реализация - реализация
- выкупы - выкупы
- товары, переданные на комиссию - товары, переданные на комиссию
- оцифровка
- ✅ Валидация входных файлов - ✅ Валидация входных файлов
- 🗂 Работа со справочниками (компании, контрагенты, номенклатура, склады) - 🗂 Работа со справочниками (компании, контрагенты, номенклатура, склады)
- 🔌 API для работы с данными - 🔌 API для работы с данными
- 📁 Примеры входных документов
--- ---

View File

@@ -1,4 +1,5 @@
DIR="/dir/dir/dir" DIR="/dir/dir/dir"
PRICES_DIR="..."
PATTERN=[A-ZА-Я]{0,1}\d{4}[A-ZА-Я]{1,2}\d{0,1} PATTERN=[A-ZА-Я]{0,1}\d{4}[A-ZА-Я]{1,2}\d{0,1}
TIMEFORMAT="%Y-%m-%dT%H:%M:%S" TIMEFORMAT="%Y-%m-%dT%H:%M:%S"

View File

@@ -0,0 +1,31 @@
import math
import server.backend.services.excel as excel
from server.backend.schemas.pydantic import settings
from functools import lru_cache
@lru_cache(maxsize=1)
def fetch_nomenclature(arti:int,price:int):
xls = excel.BaseHandler(settings.PRICES_DIR)
struct = xls.struct()
if "ЦЕНЫ" in struct.sheet_names:
df = xls.read(sheet_name="ЦЕНЫ").iloc[:,[arti,price]].copy()
else:
raise Exception(f"В файле {settings.PRICES_DIR} отсутствуют необходимые листы")
df.dropna(inplace=True)
df.columns = ['description', 'price']
#df["price"] = (df["price"]+ df["price"]*0.1).apply(math.ceil)
df['description'] = df['description'].astype(str).str.upper().str.extract(f'({settings.PATTERN})')
df = df.drop_duplicates(subset='description')
return df
def processing(df):
df2 = fetch_nomenclature(1,6)
result = df.merge(
df2[['description', 'price']], #берутся столбцы из df2
left_on='arti', #столбец для сравнения в df
right_on='description', #столбец для сравнения в df2
how='left' #left join для df
).drop(columns='description') #удаление временного стобца
print(result)
not_matched = result.loc[result['price'].isna(), 'arti'].unique()
if len(not_matched) > 0:
raise ValueError(f'Не найдены значения: {not_matched}')
return result

View File

@@ -37,6 +37,7 @@ def parse_nomenclature(xml: str):
df = pd.DataFrame(rows) df = pd.DataFrame(rows)
df = df[df['parent_key'] == 'e0eb911c-03a0-11ef-95bd-fa163e7429d8'] df = df[df['parent_key'] == 'e0eb911c-03a0-11ef-95bd-fa163e7429d8']
df['description'] = df['description'].str.extract(r'^([^\s(]+)') df['description'] = df['description'].str.extract(r'^([^\s(]+)')
df = df.drop_duplicates(subset='description')
return df return df
except ET.ParseError: except ET.ParseError:
raise raise

View File

@@ -0,0 +1,32 @@
from pydantic import ValidationError
from server.backend.schemas.pydantic import ExcelDigitalization,settings,Translit
from server.backend.api.excel_prices import processing
import re
import pandas as pd
def process_sheet(df,real_arti: int):
# выбираем нужные колонки по индексам
df = df.iloc[:, [real_arti]].copy()
df.dropna(inplace=True)
# складываем суммы
df = df.iloc[:, [0]]
df.columns = ['arti']
# нормализация
df['arti'] = df['arti'].replace(Translit.TRANSLIT, regex=True)
df['arti'] = df['arti'].astype(str).str.upper().str.extract(f'({settings.PATTERN})')
# группировка
print(df)
df = processing(df)
validated_rows, errors = [], []
for i, row in df.iterrows():
try:
validated_rows.append(ExcelDigitalization(**row.to_dict()))
except ValidationError as e:
errors.append((i, e.errors()))
if errors:
raise Exception("There are some errors with validation in digitalization", errors)
df_validated = pd.DataFrame([r.model_dump() for r in validated_rows])
return df_validated
def evaluating(df, real_arti:int, types:str):
validated_rows_1 = process_sheet(df, real_arti=real_arti) # номера столбцов от озона
validated_rows_1.to_excel(f"./excel_files/{types}.xlsx")
return validated_rows_1

View File

@@ -1,18 +1,18 @@
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
class ExcelInfo(BaseModel): class ExcelDigitalization(BaseModel):
arti:str = Field(..., min_length=5, max_length=12, description="arti of the clothes") arti:str = Field(..., min_length=5, max_length=12, description="arti of the clothes")
counts:int = Field(..., gt=0, description="the quantity of the clothes")
price:float = Field(..., gt=0, description="the price of the clothes") price:float = Field(..., gt=0, description="the price of the clothes")
class ExcelInfo(ExcelDigitalization):
counts:int = Field(..., gt=0, description="the quantity of the clothes")
ref_key:str = Field(..., description="reffering key from db") ref_key:str = Field(..., description="reffering key from db")
class ExcelRealization(BaseModel):
pass
class ExcelReturning(BaseModel): class ExcelReturning(BaseModel):
pass pass
class ExcelOut(BaseModel): class ExcelOut(BaseModel):
pass pass
class Settings(BaseSettings): class Settings(BaseSettings):
DIR:str DIR:str
PRICES_DIR:str
PATTERN: str PATTERN: str
TIMEFORMAT:str TIMEFORMAT:str
USERNAME: str USERNAME: str

View File

@@ -5,18 +5,30 @@ import server.backend.handlers.ozon_handler as ozon_handler
import server.backend.handlers.ozon_purchases_handler as ozon_purchases_handler import server.backend.handlers.ozon_purchases_handler as ozon_purchases_handler
import server.backend.handlers.wb_purchases_handler as wb_purchases_handler import server.backend.handlers.wb_purchases_handler as wb_purchases_handler
import server.backend.handlers.ozon_wb_yandex_com_handler as ozon_wb_yandex_com_handler import server.backend.handlers.ozon_wb_yandex_com_handler as ozon_wb_yandex_com_handler
import server.backend.handlers.digitalization as digitalization
from server.backend.schemas.pydantic import settings from server.backend.schemas.pydantic import settings
from server.backend.api.report import DocumentCreation from server.backend.api.report import DocumentCreation
class BaseHandler: class BaseHandler:
def __init__(self, file_path): def __init__(self, file_path):
self.file_path = file_path self.file_path = file_path
self.dfs = self.read()
def read(self, skiprows=None, skipfooter=0): def read(self, xls=None, skiprows=None, skipfooter=0, sheet_name=None):
source = xls if xls is not None else self.file_path
try: try:
return pd.read_excel(self.file_path, sheet_name=None, skiprows=skiprows,skipfooter=skipfooter) return pd.read_excel(
source,
sheet_name=sheet_name,
skiprows=skiprows,
skipfooter=skipfooter
)
except Exception as e: except Exception as e:
raise Exception(f"⚠️ Ошибка при чтении {self.file_path}: {e}") raise Exception(f"⚠️ Ошибка при чтении {self.file_path}: {e}")
def struct(self):
try:
return pd.ExcelFile(self.file_path)
except Exception as e:
raise Exception(f"⚠️ Ошибка при получении структуры {self.file_path}: {e}")
def process(self): def process(self):
raise NotImplementedError raise NotImplementedError
@@ -225,4 +237,29 @@ class YandexComHandler(BaseHandler):
СчетУчетаРасчетовСКонтрагентом_Key=settings.A76_09 СчетУчетаРасчетовСКонтрагентом_Key=settings.A76_09
) )
doc_creator.fill_document_items_to_real(doc_key, validated_data) doc_creator.fill_document_items_to_real(doc_key, validated_data)
class YandexDigital(BaseHandler):
def process(self):
xls = self.struct()
if "Себестоимость товаров" in xls.sheet_names:
df = self.read(xls, sheet_name="Себестоимость товаров", skiprows=2)
else:
raise Exception(f"В файле {self.file_path.name} отсутствуют необходимые листы")
validated_data = digitalization.evaluating(df, real_arti=0, types="yandex")
print(validated_data)
class OzonDigital(BaseHandler):
def process(self):
xls = self.struct()
if "Себестоимость" in xls.sheet_names:
df = self.read(xls, sheet_name="Себестоимость", skiprows=1)
else:
raise Exception(f"В файле {self.file_path.name} отсутствуют необходимые листы")
validated_data = digitalization.evaluating(df, real_arti=3, types="ozon")
class WBDigital(BaseHandler):
def process(self):
xls = self.struct()
if "Себестоимость" in xls.sheet_names:
df = self.read(xls, sheet_name="Себестоимость", skiprows=2)
else:
raise Exception(f"В файле {self.file_path.name} отсутствуют необходимые листы")
validated_data = digitalization.evaluating(df, real_arti=4, types="wb")
print(validated_data)

View File

@@ -11,7 +11,10 @@ handlers = { #метки какие файлы есть и должны быть
"уведомление о выкупе": excel.WBPurchasesHandler, "уведомление о выкупе": excel.WBPurchasesHandler,
"Товары, переданные на комиссию озон": excel.OZONComHandler, "Товары, переданные на комиссию озон": excel.OZONComHandler,
"Товары, переданные на комиссию вб": excel.WBComHandler, "Товары, переданные на комиссию вб": excel.WBComHandler,
"Товары, переданные на комиссию яндекс": excel.YandexComHandler "Товары, переданные на комиссию яндекс": excel.YandexComHandler,
"Яндекс":excel.YandexDigital,
"Озон":excel.OzonDigital,
"ВБ":excel.WBDigital
} }
#Проход по всем файлам в директории #Проход по всем файлам в директории
@@ -43,6 +46,12 @@ def validating():
label = "realizationreportcis" label = "realizationreportcis"
case _ if "уведомление о выкупе" in name: case _ if "уведомление о выкупе" in name:
label = "уведомление о выкупе" label = "уведомление о выкупе"
case _ if "yandex" in name:
label = "Яндекс"
case _ if "ozon" in name:
label = "Озон"
case _ if "wb" in name:
label = "ВБ"
case _: #Для неизвестных файлов case _: #Для неизвестных файлов
print("Неизвестный файл") print("Неизвестный файл")
if label: if label: