Files
evo-sync/web/routes/catalog.py

310 lines
9.9 KiB
Python
Raw Normal View History

import csv
import io
from fastapi import APIRouter, Request, Depends
from fastapi.responses import RedirectResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
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")
templates = Jinja2Templates(directory="web/templates")
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}"},
)