admin / Apex
publicBid Management and Orchestration Tool with AI Capability
Apex / Synapse-Apexv2 / backend / app / routers / resources.py
5075 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | import csv import io from decimal import Decimal from fastapi import APIRouter, Depends, HTTPException, UploadFile from fastapi.responses import StreamingResponse from sqlalchemy import select from sqlalchemy.orm import Session from app.core.database import get_db from app.core.deps import audit, require_roles from app.models import ROLE_ADMIN, ROLE_FINANCE, LicenceCost, OtherCost, ResourceType, User from app.schemas import ( LicenceIn, LicenceOut, ResourceTypeIn, ResourceTypeOut, ) router = APIRouter(prefix="/api/resources", tags=["resources"]) finance = require_roles(ROLE_ADMIN, ROLE_FINANCE) # --- resource types --- @router.get("/types", response_model=list[ResourceTypeOut]) def list_types(db: Session = Depends(get_db), _: User = Depends(require_roles(ROLE_ADMIN, ROLE_FINANCE, "bid_manager"))): return db.execute( select(ResourceType).where(ResourceType.active == True).order_by(ResourceType.name) # noqa: E712 ).scalars().all() @router.post("/types", response_model=ResourceTypeOut, status_code=201) def create_type(body: ResourceTypeIn, db: Session = Depends(get_db), actor: User = Depends(finance)): rt = ResourceType(**body.model_dump()) db.add(rt) db.commit() db.refresh(rt) audit(db, actor, "resource_type_created", "resource_type", rt.id) return rt @router.patch("/types/{rt_id}", response_model=ResourceTypeOut) def update_type(rt_id: int, body: ResourceTypeIn, db: Session = Depends(get_db), actor: User = Depends(finance)): rt = db.get(ResourceType, rt_id) if not rt: raise HTTPException(404, "Not found") for k, v in body.model_dump().items(): setattr(rt, k, v) db.commit() db.refresh(rt) audit(db, actor, "resource_type_updated", "resource_type", rt.id) return rt @router.delete("/types/{rt_id}") def delete_type(rt_id: int, db: Session = Depends(get_db), _: User = Depends(finance)): rt = db.get(ResourceType, rt_id) if rt: rt.active = False db.commit() return {"ok": True} @router.get("/types/export.csv") def export_types(db: Session = Depends(get_db), _: User = Depends(finance)): rows = db.execute(select(ResourceType)).scalars().all() buf = io.StringIO() w = csv.writer(buf) w.writerow(["name", "role_family", "skills", "hourly_cost_rate", "hourly_sell_rate", "currency", "active"]) for r in rows: w.writerow([r.name, r.role_family, "|".join(r.skills), r.hourly_cost_rate, r.hourly_sell_rate, r.currency, r.active]) buf.seek(0) return StreamingResponse( iter([buf.getvalue()]), media_type="text/csv", headers={"Content-Disposition": "attachment; filename=resource_types.csv"}, ) @router.post("/types/import.csv") async def import_types(file: UploadFile, db: Session = Depends(get_db), actor: User = Depends(finance)): content = (await file.read()).decode("utf-8") reader = csv.DictReader(io.StringIO(content)) count = 0 for row in reader: db.add(ResourceType( name=row.get("name", "").strip(), role_family=row.get("role_family", "").strip(), skills=[s for s in row.get("skills", "").split("|") if s], hourly_cost_rate=Decimal(row.get("hourly_cost_rate") or "0"), hourly_sell_rate=Decimal(row.get("hourly_sell_rate") or "0"), daily_cost_rate=Decimal(row.get("hourly_cost_rate") or "0") * 8, daily_sell_rate=Decimal(row.get("hourly_sell_rate") or "0") * 8, currency=row.get("currency", "GBP").strip() or "GBP", )) count += 1 db.commit() audit(db, actor, "resource_types_imported", "resource_type", None, {"count": count}) return {"imported": count} # --- licences --- @router.get("/licences", response_model=list[LicenceOut]) def list_licences(db: Session = Depends(get_db), _: User = Depends(require_roles(ROLE_ADMIN, ROLE_FINANCE, "bid_manager"))): return db.execute( select(LicenceCost).where(LicenceCost.active == True).order_by(LicenceCost.name) # noqa: E712 ).scalars().all() @router.post("/licences", response_model=LicenceOut, status_code=201) def create_licence(body: LicenceIn, db: Session = Depends(get_db), actor: User = Depends(finance)): lic = LicenceCost(**body.model_dump()) db.add(lic) db.commit() db.refresh(lic) audit(db, actor, "licence_created", "licence", lic.id) return lic @router.patch("/licences/{lic_id}", response_model=LicenceOut) def update_licence(lic_id: int, body: LicenceIn, db: Session = Depends(get_db), _: User = Depends(finance)): lic = db.get(LicenceCost, lic_id) if not lic: raise HTTPException(404, "Not found") for k, v in body.model_dump().items(): setattr(lic, k, v) db.commit() db.refresh(lic) return lic @router.delete("/licences/{lic_id}") def delete_licence(lic_id: int, db: Session = Depends(get_db), _: User = Depends(finance)): lic = db.get(LicenceCost, lic_id) if lic: lic.active = False db.commit() return {"ok": True} |