import csv import io from fastapi import APIRouter, Request, Depends from fastapi.responses import RedirectResponse, StreamingResponse from web.templates_env import templates 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}"}, )