Spaces:
Running
Running
Download app/api/admin.py from amirelrefai/ERP_Sysyem: direct link, hf CLI and curl.
- Browser
- Download file 16.2 kB
-
https://huggingface.co/spaces/amirelrefai/ERP_Sysyem/resolve/main/app/api/admin.py
- Command line
-
hf download hf://spaces/amirelrefai/ERP_Sysyem/app/api/admin.py
-
curl -L -o admin.py https://huggingface.co/spaces/amirelrefai/ERP_Sysyem/resolve/main/app/api/admin.py
16.2 kB
| """ | |
| app/api/admin β Super-Admin endpoints for platform management | |
| ================================================================ | |
| Only users with role == "super_admin" can access these routes. | |
| """ | |
| from fastapi import APIRouter, Depends, HTTPException, status | |
| from sqlalchemy.orm import Session | |
| from sqlalchemy import func | |
| from app.core.database import get_db | |
| from app.core.deps import get_current_user | |
| from app.models.domain import Tenant, User, Invoice, Product, Party | |
| router = APIRouter(prefix="/admin", tags=["admin"]) | |
| # ββ Guard: require super_admin role ββββββββββββββββββββββββββββββββββββββββββ | |
| def require_super_admin(current_user: User = Depends(get_current_user)): | |
| if current_user.role != "super_admin": | |
| raise HTTPException( | |
| status_code=status.HTTP_403_FORBIDDEN, | |
| detail="Super-admin access required.", | |
| ) | |
| return current_user | |
| # ββ Platform statistics ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_platform_stats( | |
| db: Session = Depends(get_db), | |
| _admin: User = Depends(require_super_admin), | |
| ): | |
| """Return aggregate platform metrics.""" | |
| total_tenants = db.query(func.count(Tenant.id)).scalar() or 0 | |
| active_tenants = db.query(func.count(Tenant.id)).filter(Tenant.is_active.is_(True)).scalar() or 0 | |
| approved_tenants = db.query(func.count(Tenant.id)).filter(Tenant.is_approved.is_(True)).scalar() or 0 | |
| pending_tenants = db.query(func.count(Tenant.id)).filter(Tenant.is_approved.is_(False)).scalar() or 0 | |
| total_users = db.query(func.count(User.id)).scalar() or 0 | |
| total_products = db.query(func.count(Product.id)).scalar() or 0 | |
| total_invoices = db.query(func.count(Invoice.id)).scalar() or 0 | |
| total_parties = db.query(func.count(Party.id)).scalar() or 0 | |
| return { | |
| "total_tenants": total_tenants, | |
| "active_tenants": active_tenants, | |
| "approved_tenants": approved_tenants, | |
| "pending_tenants": pending_tenants, | |
| "total_users": total_users, | |
| "total_products": total_products, | |
| "total_invoices": total_invoices, | |
| "total_parties": total_parties, | |
| } | |
| async def get_cache_monitoring_stats( | |
| _admin: User = Depends(require_super_admin), | |
| ): | |
| """Return Redis & memory cache performance metrics (Hits, Misses, Hit Ratio %).""" | |
| from app.core.cache import get_cache_stats | |
| return await get_cache_stats() | |
| # ββ List all tenants βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def list_all_tenants( | |
| skip: int = 0, | |
| limit: int = 100, | |
| status_filter: str | None = None, | |
| db: Session = Depends(get_db), | |
| _admin: User = Depends(require_super_admin), | |
| ): | |
| """List all tenants on the platform with their owner info.""" | |
| query = db.query(Tenant) | |
| if status_filter == "approved": | |
| query = query.filter(Tenant.is_approved.is_(True)) | |
| elif status_filter == "pending": | |
| query = query.filter(Tenant.is_approved.is_(False)) | |
| elif status_filter == "inactive": | |
| query = query.filter(Tenant.is_active.is_(False)) | |
| tenants = query.order_by(Tenant.id.desc()).offset(skip).limit(limit).all() | |
| results = [] | |
| for t in tenants: | |
| # Get the admin user for this tenant | |
| owner = db.query(User).filter(User.tenant_id == t.id, User.role.in_(["admin", "super_admin"])).first() | |
| user_count = db.query(func.count(User.id)).filter(User.tenant_id == t.id).scalar() or 0 | |
| product_count = db.query(func.count(Product.id)).filter(Product.tenant_id == t.id).scalar() or 0 | |
| invoice_count = db.query(func.count(Invoice.id)).filter(Invoice.tenant_id == t.id).scalar() or 0 | |
| results.append({ | |
| "id": t.id, | |
| "company_name": t.company_name, | |
| "store_name": t.store_name, | |
| "phone": t.phone, | |
| "address": t.address, | |
| "is_active": t.is_active, | |
| "is_approved": t.is_approved, | |
| "created_at": t.created_at.isoformat() if t.created_at else None, | |
| "owner_username": owner.username if owner else None, | |
| "owner_full_name": owner.full_name if owner else None, | |
| "user_count": user_count, | |
| "product_count": product_count, | |
| "invoice_count": invoice_count, | |
| }) | |
| return results | |
| # ββ Approve a tenant ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def approve_tenant( | |
| tenant_id: int, | |
| db: Session = Depends(get_db), | |
| _admin: User = Depends(require_super_admin), | |
| ): | |
| tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first() | |
| if not tenant: | |
| raise HTTPException(status_code=404, detail="Tenant not found") | |
| tenant.is_approved = True | |
| tenant.is_active = True | |
| db.commit() | |
| return {"message": "Tenant approved", "tenant_id": tenant_id} | |
| # ββ Reject (deactivate) a tenant βββββββββββββββββββββββββββββββββββββββββββββ | |
| def reject_tenant( | |
| tenant_id: int, | |
| db: Session = Depends(get_db), | |
| _admin: User = Depends(require_super_admin), | |
| ): | |
| tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first() | |
| if not tenant: | |
| raise HTTPException(status_code=404, detail="Tenant not found") | |
| tenant.is_approved = False | |
| tenant.is_active = False | |
| db.commit() | |
| return {"message": "Tenant rejected", "tenant_id": tenant_id} | |
| # ββ Toggle tenant active status ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def toggle_tenant_active( | |
| tenant_id: int, | |
| db: Session = Depends(get_db), | |
| _admin: User = Depends(require_super_admin), | |
| ): | |
| tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first() | |
| if not tenant: | |
| raise HTTPException(status_code=404, detail="Tenant not found") | |
| tenant.is_active = not tenant.is_active | |
| db.commit() | |
| return {"message": f"Tenant {'activated' if tenant.is_active else 'deactivated'}", "is_active": tenant.is_active} | |
| # ββ Delete a tenant (Account Deletion) βββββββββββββββββββββββββββββββββββββββ | |
| def delete_tenant( | |
| tenant_id: int, | |
| db: Session = Depends(get_db), | |
| _admin: User = Depends(require_super_admin), | |
| ): | |
| tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first() | |
| if not tenant: | |
| raise HTTPException(status_code=404, detail="Tenant not found") | |
| db.delete(tenant) | |
| db.commit() | |
| return {"message": "Tenant deleted successfully"} | |
| # ββ List all users (System-wide) βββββββββββββββββββββββββββββββββββββββββββββ | |
| def list_all_users( | |
| skip: int = 0, | |
| limit: int = 100, | |
| db: Session = Depends(get_db), | |
| _admin: User = Depends(require_super_admin), | |
| ): | |
| users = db.query(User).order_by(User.id.desc()).offset(skip).limit(limit).all() | |
| results = [] | |
| for u in users: | |
| tenant_name = u.tenant.company_name if u.tenant else "Unknown" | |
| results.append({ | |
| "id": u.id, | |
| "username": u.username, | |
| "full_name": u.full_name, | |
| "role": u.role, | |
| "tenant_id": u.tenant_id, | |
| "company_name": tenant_name | |
| }) | |
| return results | |
| # ββ Delete a user ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def delete_user( | |
| user_id: int, | |
| db: Session = Depends(get_db), | |
| _admin: User = Depends(require_super_admin), | |
| ): | |
| user = db.query(User).filter(User.id == user_id).first() | |
| if not user: | |
| raise HTTPException(status_code=404, detail="User not found") | |
| if user.role == "super_admin": | |
| raise HTTPException(status_code=403, detail="Cannot delete super admin") | |
| db.delete(user) | |
| db.commit() | |
| return {"message": "User deleted successfully"} | |
| # ββ Get user details βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_user_details( | |
| user_id: int, | |
| db: Session = Depends(get_db), | |
| _admin: User = Depends(require_super_admin), | |
| ): | |
| user = db.query(User).filter(User.id == user_id).first() | |
| if not user: | |
| raise HTTPException(status_code=404, detail="User not found") | |
| tenant = user.tenant | |
| return { | |
| "id": user.id, | |
| "username": user.username, | |
| "full_name": user.full_name, | |
| "role": user.role, | |
| "tenant_id": user.tenant_id, | |
| "company_name": tenant.company_name if tenant else "Unknown", | |
| "tenant_is_active": tenant.is_active if tenant else False, | |
| "tenant_is_approved": tenant.is_approved if tenant else False, | |
| } | |
| # ββ Stock & Invoice diagnostics / repair βββββββββββββββββββββββββββββββββββββ | |
| from decimal import Decimal | |
| from sqlalchemy import select | |
| from app.models.domain import Invoice, InvoiceItem, InvoiceType, Payment, StockBatch | |
| def _diagnose_tenant(db: Session, tenant_id: int): | |
| """Read-only: compute discrepancies in stock and invoice totals.""" | |
| from sqlalchemy.orm import selectinload | |
| batches = db.execute( | |
| select(StockBatch).where(StockBatch.tenant_id == tenant_id) | |
| ).scalars().all() | |
| batch_issues = [] | |
| for batch in batches: | |
| sold = Decimal(str(db.execute( | |
| select(func.coalesce(func.sum(InvoiceItem.quantity), 0)) | |
| .join(Invoice, InvoiceItem.invoice_id == Invoice.id) | |
| .where(InvoiceItem.batch_id == batch.id, | |
| Invoice.invoice_type == InvoiceType.SELL, | |
| Invoice.tenant_id == tenant_id) | |
| ).scalar_one())) | |
| sell_ret = Decimal(str(db.execute( | |
| select(func.coalesce(func.sum(InvoiceItem.quantity), 0)) | |
| .join(Invoice, InvoiceItem.invoice_id == Invoice.id) | |
| .where(InvoiceItem.batch_id == batch.id, | |
| Invoice.invoice_type == InvoiceType.SELL_RETURN, | |
| Invoice.tenant_id == tenant_id) | |
| ).scalar_one())) | |
| purch_ret = Decimal(str(db.execute( | |
| select(func.coalesce(func.sum(InvoiceItem.quantity), 0)) | |
| .join(Invoice, InvoiceItem.invoice_id == Invoice.id) | |
| .where(InvoiceItem.batch_id == batch.id, | |
| Invoice.invoice_type == InvoiceType.PURCHASE_RETURN, | |
| Invoice.tenant_id == tenant_id) | |
| ).scalar_one())) | |
| initial = Decimal(str(batch.initial_quantity or 0)) | |
| # Note: SELL_RETURN creates a new StockBatch with its own initial_quantity. | |
| # Adding sell_ret here would double-count the returned stock for that new batch. | |
| correct = initial - sold - purch_ret | |
| actual = Decimal(str(batch.remaining_quantity or 0)) | |
| diff = float(correct - actual) | |
| if abs(diff) > 0.001: | |
| batch_issues.append({ | |
| "batch_id": batch.id, | |
| "product_id": batch.product_id, | |
| "initial": float(initial), | |
| "sold": float(sold), | |
| "sell_returns": float(sell_ret), | |
| "purchase_returns": float(purch_ret), | |
| "correct_remaining": float(correct), | |
| "actual_remaining": float(actual), | |
| "difference": diff, | |
| }) | |
| invoices = db.execute( | |
| select(Invoice) | |
| .options(selectinload(Invoice.items)) | |
| .where(Invoice.tenant_id == tenant_id) | |
| ).unique().scalars().all() | |
| invoice_issues = [] | |
| for inv in invoices: | |
| computed = float(sum( | |
| Decimal(str(item.quantity or 0)) * Decimal(str(item.unit_price or 0)) | |
| for item in inv.items | |
| )) | |
| stored = float(inv.total_amount or 0) | |
| if abs(computed - stored) > 0.01: | |
| invoice_issues.append({ | |
| "invoice_id": inv.id, | |
| "invoice_type": inv.invoice_type.value if inv.invoice_type else None, | |
| "party_id": inv.party_id, | |
| "stored_total": stored, | |
| "correct_total": computed, | |
| "difference": round(computed - stored, 2), | |
| }) | |
| return batch_issues, invoice_issues | |
| def diagnose_tenant_data( | |
| tenant_id: int, | |
| db: Session = Depends(get_db), | |
| _admin: User = Depends(require_super_admin), | |
| ): | |
| """Diagnose stock & invoice inconsistencies for a tenant (read-only).""" | |
| tenant = db.get(Tenant, tenant_id) | |
| if not tenant: | |
| raise HTTPException(status_code=404, detail="Tenant not found") | |
| batch_issues, invoice_issues = _diagnose_tenant(db, tenant_id) | |
| return { | |
| "tenant_id": tenant_id, | |
| "company_name": tenant.company_name, | |
| "batch_issues_count": len(batch_issues), | |
| "invoice_issues_count": len(invoice_issues), | |
| "batch_issues": batch_issues, | |
| "invoice_issues": invoice_issues, | |
| "status": "clean" if not batch_issues and not invoice_issues else "has_issues", | |
| } | |
| def fix_tenant_stock( | |
| tenant_id: int, | |
| db: Session = Depends(get_db), | |
| _admin: User = Depends(require_super_admin), | |
| ): | |
| """ | |
| Apply fixes for a tenant: | |
| - Correct StockBatch.remaining_quantity from actual invoice items. | |
| - Correct Invoice.total_amount from sum of line items. | |
| """ | |
| tenant = db.get(Tenant, tenant_id) | |
| if not tenant: | |
| raise HTTPException(status_code=404, detail="Tenant not found") | |
| batch_issues, invoice_issues = _diagnose_tenant(db, tenant_id) | |
| fixed_batches: list = [] | |
| fixed_invoices: list = [] | |
| try: | |
| for issue in batch_issues: | |
| batch = db.get(StockBatch, issue["batch_id"]) | |
| if batch: | |
| batch.remaining_quantity = issue["correct_remaining"] | |
| fixed_batches.append({ | |
| "batch_id": issue["batch_id"], | |
| "product_id": issue["product_id"], | |
| "old_value": issue["actual_remaining"], | |
| "new_value": issue["correct_remaining"], | |
| }) | |
| for issue in invoice_issues: | |
| inv = db.get(Invoice, issue["invoice_id"]) | |
| if inv: | |
| inv.total_amount = issue["correct_total"] | |
| fixed_invoices.append({ | |
| "invoice_id": issue["invoice_id"], | |
| "invoice_type": issue["invoice_type"], | |
| "old_value": issue["stored_total"], | |
| "new_value": issue["correct_total"], | |
| }) | |
| db.commit() | |
| except Exception as exc: | |
| db.rollback() | |
| raise HTTPException(status_code=500, detail=f"Fix failed: {exc}") | |
| return { | |
| "tenant_id": tenant_id, | |
| "company_name": tenant.company_name, | |
| "fixed_batches_count": len(fixed_batches), | |
| "fixed_invoices_count": len(fixed_invoices), | |
| "fixed_batches": fixed_batches, | |
| "fixed_invoices": fixed_invoices, | |
| "status": "ok", | |
| } | |