2026-03-06 16:08:19 +03:00
|
|
|
|
import csv
|
|
|
|
|
|
import io
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Request, Depends
|
|
|
|
|
|
from fastapi.responses import RedirectResponse, StreamingResponse
|
2026-03-10 14:11:25 +03:00
|
|
|
|
from web.templates_env import templates
|
2026-03-06 16:08:19 +03:00
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
|
|
|
|
|
|
from web.auth import get_current_user
|
|
|
|
|
|
from web.database import get_db
|
|
|
|
|
|
from web.evotor_api import refresh_catalog_cache
|
|
|
|
|
|
from web.models import User, EvotorConnection, SyncConfig, SyncFilter, CachedStore, CachedGroup, CachedProduct
|
|
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/catalog")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_or_create_sync_config(db: Session, user_id: int) -> SyncConfig:
|
|
|
|
|
|
config = db.query(SyncConfig).filter(SyncConfig.user_id == user_id).first()
|
|
|
|
|
|
if not config:
|
|
|
|
|
|
config = SyncConfig(user_id=user_id, is_enabled=False)
|
|
|
|
|
|
db.add(config)
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
db.refresh(config)
|
|
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _filter_map(config: SyncConfig) -> dict:
|
|
|
|
|
|
"""Returns {entity_id: filter_mode} for quick lookup."""
|
|
|
|
|
|
return {f.entity_id: f.filter_mode for f in config.filters}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _filter_label(mode: str | None) -> str:
|
|
|
|
|
|
if mode == "include":
|
|
|
|
|
|
return "include"
|
|
|
|
|
|
if mode == "exclude":
|
|
|
|
|
|
return "exclude"
|
|
|
|
|
|
return "none"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("")
|
|
|
|
|
|
async def catalog_stores(
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
|
user: User | None = Depends(get_current_user),
|
|
|
|
|
|
):
|
|
|
|
|
|
if not user:
|
|
|
|
|
|
return RedirectResponse("/login", 303)
|
|
|
|
|
|
|
|
|
|
|
|
evotor = db.query(EvotorConnection).filter(EvotorConnection.user_id == user.id).first()
|
|
|
|
|
|
if not evotor:
|
|
|
|
|
|
return templates.TemplateResponse("catalog_stores.html", {
|
|
|
|
|
|
"request": request, "user": user,
|
|
|
|
|
|
"evotor": None, "stores": [], "filter_map": {}, "fetched_at": None,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
stores = db.query(CachedStore).filter(CachedStore.user_id == user.id).order_by(CachedStore.name).all()
|
|
|
|
|
|
|
|
|
|
|
|
# Auto-refresh if cache is empty
|
|
|
|
|
|
if not stores:
|
|
|
|
|
|
await refresh_catalog_cache(user.id, evotor.access_token, db)
|
|
|
|
|
|
stores = db.query(CachedStore).filter(CachedStore.user_id == user.id).order_by(CachedStore.name).all()
|
|
|
|
|
|
|
|
|
|
|
|
config = _get_or_create_sync_config(db, user.id)
|
|
|
|
|
|
fmap = _filter_map(config)
|
|
|
|
|
|
fetched_at = stores[0].fetched_at if stores else None
|
|
|
|
|
|
|
|
|
|
|
|
return templates.TemplateResponse("catalog_stores.html", {
|
|
|
|
|
|
"request": request,
|
|
|
|
|
|
"user": user,
|
|
|
|
|
|
"evotor": evotor,
|
|
|
|
|
|
"stores": stores,
|
|
|
|
|
|
"filter_map": fmap,
|
|
|
|
|
|
"fetched_at": fetched_at,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/groups")
|
|
|
|
|
|
def catalog_groups(
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
store_id: str,
|
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
|
user: User | None = Depends(get_current_user),
|
|
|
|
|
|
):
|
|
|
|
|
|
if not user:
|
|
|
|
|
|
return RedirectResponse("/login", 303)
|
|
|
|
|
|
|
|
|
|
|
|
store = db.query(CachedStore).filter(
|
|
|
|
|
|
CachedStore.user_id == user.id,
|
|
|
|
|
|
CachedStore.evotor_id == store_id,
|
|
|
|
|
|
).first()
|
|
|
|
|
|
if not store:
|
|
|
|
|
|
return RedirectResponse("/catalog", 303)
|
|
|
|
|
|
|
|
|
|
|
|
groups = db.query(CachedGroup).filter(
|
|
|
|
|
|
CachedGroup.user_id == user.id,
|
|
|
|
|
|
CachedGroup.store_evotor_id == store_id,
|
|
|
|
|
|
).order_by(CachedGroup.name).all()
|
|
|
|
|
|
|
|
|
|
|
|
# Count products per group
|
|
|
|
|
|
product_counts = {}
|
|
|
|
|
|
for g in groups:
|
|
|
|
|
|
product_counts[g.evotor_id] = db.query(CachedProduct).filter(
|
|
|
|
|
|
CachedProduct.user_id == user.id,
|
|
|
|
|
|
CachedProduct.group_evotor_id == g.evotor_id,
|
|
|
|
|
|
).count()
|
|
|
|
|
|
|
|
|
|
|
|
config = _get_or_create_sync_config(db, user.id)
|
|
|
|
|
|
fmap = _filter_map(config)
|
|
|
|
|
|
|
|
|
|
|
|
return templates.TemplateResponse("catalog_groups.html", {
|
|
|
|
|
|
"request": request,
|
|
|
|
|
|
"user": user,
|
|
|
|
|
|
"store": store,
|
|
|
|
|
|
"groups": groups,
|
|
|
|
|
|
"product_counts": product_counts,
|
|
|
|
|
|
"filter_map": fmap,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/products")
|
|
|
|
|
|
def catalog_products(
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
store_id: str,
|
|
|
|
|
|
group_id: str | None = None,
|
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
|
user: User | None = Depends(get_current_user),
|
|
|
|
|
|
):
|
|
|
|
|
|
if not user:
|
|
|
|
|
|
return RedirectResponse("/login", 303)
|
|
|
|
|
|
|
|
|
|
|
|
store = db.query(CachedStore).filter(
|
|
|
|
|
|
CachedStore.user_id == user.id,
|
|
|
|
|
|
CachedStore.evotor_id == store_id,
|
|
|
|
|
|
).first()
|
|
|
|
|
|
if not store:
|
|
|
|
|
|
return RedirectResponse("/catalog", 303)
|
|
|
|
|
|
|
|
|
|
|
|
group = None
|
|
|
|
|
|
query = db.query(CachedProduct).filter(
|
|
|
|
|
|
CachedProduct.user_id == user.id,
|
|
|
|
|
|
CachedProduct.store_evotor_id == store_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
if group_id:
|
|
|
|
|
|
group = db.query(CachedGroup).filter(
|
|
|
|
|
|
CachedGroup.user_id == user.id,
|
|
|
|
|
|
CachedGroup.evotor_id == group_id,
|
|
|
|
|
|
).first()
|
|
|
|
|
|
query = query.filter(CachedProduct.group_evotor_id == group_id)
|
|
|
|
|
|
|
|
|
|
|
|
products = query.order_by(CachedProduct.name).all()
|
|
|
|
|
|
|
|
|
|
|
|
config = _get_or_create_sync_config(db, user.id)
|
|
|
|
|
|
fmap = _filter_map(config)
|
|
|
|
|
|
|
|
|
|
|
|
return templates.TemplateResponse("catalog_products.html", {
|
|
|
|
|
|
"request": request,
|
|
|
|
|
|
"user": user,
|
|
|
|
|
|
"store": store,
|
|
|
|
|
|
"group": group,
|
|
|
|
|
|
"products": products,
|
|
|
|
|
|
"filter_map": fmap,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/filter")
|
|
|
|
|
|
async def catalog_filter(
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
|
user: User | None = Depends(get_current_user),
|
|
|
|
|
|
):
|
|
|
|
|
|
if not user:
|
|
|
|
|
|
return RedirectResponse("/login", 303)
|
|
|
|
|
|
|
|
|
|
|
|
form = await request.form()
|
|
|
|
|
|
entity_type = form.get("entity_type")
|
|
|
|
|
|
entity_id = form.get("entity_id")
|
|
|
|
|
|
entity_name = form.get("entity_name")
|
|
|
|
|
|
filter_mode = form.get("filter_mode") # "include", "exclude", "none"
|
|
|
|
|
|
parent_entity_id = form.get("parent_entity_id") or None
|
|
|
|
|
|
redirect_to = form.get("redirect_to", "/catalog")
|
|
|
|
|
|
|
|
|
|
|
|
config = _get_or_create_sync_config(db, user.id)
|
|
|
|
|
|
|
|
|
|
|
|
existing = db.query(SyncFilter).filter(
|
|
|
|
|
|
SyncFilter.sync_config_id == config.id,
|
|
|
|
|
|
SyncFilter.entity_type == entity_type,
|
|
|
|
|
|
SyncFilter.entity_id == entity_id,
|
|
|
|
|
|
).first()
|
|
|
|
|
|
|
|
|
|
|
|
if filter_mode == "none":
|
|
|
|
|
|
if existing:
|
|
|
|
|
|
db.delete(existing)
|
|
|
|
|
|
elif existing:
|
|
|
|
|
|
existing.filter_mode = filter_mode
|
|
|
|
|
|
existing.entity_name = entity_name
|
|
|
|
|
|
else:
|
|
|
|
|
|
db.add(SyncFilter(
|
|
|
|
|
|
sync_config_id=config.id,
|
|
|
|
|
|
entity_type=entity_type,
|
|
|
|
|
|
entity_id=entity_id,
|
|
|
|
|
|
entity_name=entity_name,
|
|
|
|
|
|
filter_mode=filter_mode,
|
|
|
|
|
|
parent_entity_id=parent_entity_id,
|
|
|
|
|
|
))
|
|
|
|
|
|
db.commit()
|
|
|
|
|
|
|
|
|
|
|
|
return RedirectResponse(redirect_to, 303)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/refresh")
|
|
|
|
|
|
async def catalog_refresh(
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
|
user: User | None = Depends(get_current_user),
|
|
|
|
|
|
):
|
|
|
|
|
|
if not user:
|
|
|
|
|
|
return RedirectResponse("/login", 303)
|
|
|
|
|
|
|
|
|
|
|
|
evotor = db.query(EvotorConnection).filter(EvotorConnection.user_id == user.id).first()
|
|
|
|
|
|
if evotor:
|
|
|
|
|
|
await refresh_catalog_cache(user.id, evotor.access_token, db)
|
|
|
|
|
|
|
|
|
|
|
|
return RedirectResponse("/catalog", 303)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/export")
|
|
|
|
|
|
def catalog_export(
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
type: str,
|
|
|
|
|
|
store_id: str | None = None,
|
|
|
|
|
|
group_id: str | None = None,
|
|
|
|
|
|
db: Session = Depends(get_db),
|
|
|
|
|
|
user: User | None = Depends(get_current_user),
|
|
|
|
|
|
):
|
|
|
|
|
|
if not user:
|
|
|
|
|
|
return RedirectResponse("/login", 303)
|
|
|
|
|
|
|
|
|
|
|
|
config = _get_or_create_sync_config(db, user.id)
|
|
|
|
|
|
fmap = _filter_map(config)
|
|
|
|
|
|
|
|
|
|
|
|
def filter_label(eid):
|
|
|
|
|
|
m = fmap.get(eid)
|
|
|
|
|
|
if m == "include":
|
|
|
|
|
|
return "Включено"
|
|
|
|
|
|
if m == "exclude":
|
|
|
|
|
|
return "Исключено"
|
|
|
|
|
|
return "Нет правила"
|
|
|
|
|
|
|
|
|
|
|
|
output = io.StringIO()
|
|
|
|
|
|
output.write("\ufeff") # UTF-8 BOM for Excel
|
|
|
|
|
|
writer = csv.writer(output)
|
|
|
|
|
|
|
|
|
|
|
|
from datetime import date
|
|
|
|
|
|
today = date.today().strftime("%Y%m%d")
|
|
|
|
|
|
|
|
|
|
|
|
if type == "stores":
|
|
|
|
|
|
writer.writerow(["Название", "Адрес", "ID", "Фильтр"])
|
|
|
|
|
|
stores = db.query(CachedStore).filter(CachedStore.user_id == user.id).order_by(CachedStore.name).all()
|
|
|
|
|
|
for s in stores:
|
|
|
|
|
|
writer.writerow([s.name, s.address or "", s.evotor_id, filter_label(s.evotor_id)])
|
|
|
|
|
|
filename = f"stores_{today}.csv"
|
|
|
|
|
|
|
|
|
|
|
|
elif type == "groups":
|
|
|
|
|
|
writer.writerow(["Магазин", "Название", "ID", "Фильтр"])
|
|
|
|
|
|
q = db.query(CachedGroup, CachedStore).join(
|
|
|
|
|
|
CachedStore,
|
|
|
|
|
|
(CachedStore.evotor_id == CachedGroup.store_evotor_id) & (CachedStore.user_id == user.id)
|
|
|
|
|
|
).filter(CachedGroup.user_id == user.id)
|
|
|
|
|
|
if store_id:
|
|
|
|
|
|
q = q.filter(CachedGroup.store_evotor_id == store_id)
|
|
|
|
|
|
for g, s in q.order_by(CachedGroup.name).all():
|
|
|
|
|
|
writer.writerow([s.name, g.name, g.evotor_id, filter_label(g.evotor_id)])
|
|
|
|
|
|
filename = f"groups_{today}.csv"
|
|
|
|
|
|
|
|
|
|
|
|
else: # products
|
|
|
|
|
|
writer.writerow(["Магазин", "Группа", "Название", "Артикул", "Цена", "Количество", "Ед. измерения", "В продаже", "ID", "Фильтр"])
|
|
|
|
|
|
q = db.query(CachedProduct, CachedStore, CachedGroup).join(
|
|
|
|
|
|
CachedStore,
|
|
|
|
|
|
(CachedStore.evotor_id == CachedProduct.store_evotor_id) & (CachedStore.user_id == user.id)
|
|
|
|
|
|
).outerjoin(
|
|
|
|
|
|
CachedGroup,
|
|
|
|
|
|
(CachedGroup.evotor_id == CachedProduct.group_evotor_id) & (CachedGroup.user_id == user.id)
|
|
|
|
|
|
).filter(CachedProduct.user_id == user.id)
|
|
|
|
|
|
if store_id:
|
|
|
|
|
|
q = q.filter(CachedProduct.store_evotor_id == store_id)
|
|
|
|
|
|
if group_id:
|
|
|
|
|
|
q = q.filter(CachedProduct.group_evotor_id == group_id)
|
|
|
|
|
|
for p, s, g in q.order_by(CachedProduct.name).all():
|
|
|
|
|
|
writer.writerow([
|
|
|
|
|
|
s.name,
|
|
|
|
|
|
g.name if g else "",
|
|
|
|
|
|
p.name,
|
|
|
|
|
|
p.article_number or "",
|
|
|
|
|
|
p.price or "",
|
|
|
|
|
|
p.quantity or "",
|
|
|
|
|
|
p.measure_name or "",
|
|
|
|
|
|
"Да" if p.allow_to_sell else ("Нет" if p.allow_to_sell is not None else ""),
|
|
|
|
|
|
p.evotor_id,
|
|
|
|
|
|
filter_label(p.evotor_id),
|
|
|
|
|
|
])
|
|
|
|
|
|
filename = f"products_{today}.csv"
|
|
|
|
|
|
|
|
|
|
|
|
output.seek(0)
|
|
|
|
|
|
return StreamingResponse(
|
|
|
|
|
|
iter([output.getvalue()]),
|
|
|
|
|
|
media_type="text/csv; charset=utf-8",
|
|
|
|
|
|
headers={"Content-Disposition": f"attachment; filename={filename}"},
|
|
|
|
|
|
)
|