Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions 021-supplier-management/procurement_platform/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .main import app

__version__ = "1.0.0"
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from fastapi import APIRouter

from .endpoints import auth, users, categories, stages, suppliers, purchase_requests, rfqs, quotes, orders, dashboard

api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(categories.router, prefix="/categories", tags=["categories"])
api_router.include_router(stages.router, prefix="/stages", tags=["stages"])
api_router.include_router(suppliers.router, prefix="/suppliers", tags=["suppliers"])
api_router.include_router(purchase_requests.router, prefix="/purchase-requests", tags=["purchase_requests"])
api_router.include_router(rfqs.router, prefix="/rfqs", tags=["rfqs"])
api_router.include_router(quotes.router, prefix="/quotes", tags=["quotes"])
api_router.include_router(orders.router, prefix="/orders", tags=["orders"])
api_router.include_router(dashboard.router, prefix="/dashboard", tags=["dashboard"])
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .auth import router as auth_router
from .users import router as users_router
from .categories import router as categories_router
from .stages import router as stages_router
from .suppliers import router as suppliers_router
from .purchase_requests import router as purchase_requests_router
from .rfqs import router as rfqs_router
from .quotes import router as quotes_router
from .orders import router as orders_router
from .dashboard import router as dashboard_router
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from typing import Annotated
from datetime import timedelta

from app.database import get_db
from app.database.models import User, UserRole
from app.core.config import settings
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.api.v1.schemas.auth import Token, TokenData, LoginRequest

router = APIRouter()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password):
return pwd_context.hash(password)

def get_user(db: Session, username: str):
return db.query(User).filter(User.username == username).first()

def authenticate_user(db: Session, username: str, password: str):
user = get_user(db, username)
if not user:
return False
if not user.is_active:
return False
if not verify_password(password, user.password_hash):
return False
return user

def create_access_token(data: dict, expires_delta: timedelta):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
return encoded_jwt

from datetime import datetime

async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)], db: Session = Depends(get_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception

user = get_user(db, username=token_data.username)
if user is None:
raise credentials_exception
return user

async def get_current_active_user(current_user: User = Depends(get_current_user)):
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user

from typing import Optional
from app.api.v1.schemas.users import UserSchema

async def get_current_admin_user(current_user: User = Depends(get_current_active_user)):
if current_user.role != UserRole.ADMIN:
raise HTTPException(status_code=403, detail="Admin access required")
return current_user

Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List

from app.database import get_db
from app.database.models import Category, PurchaseRequest, Supplier, SupplierCategory, User
from app.api.v1.endpoints.auth import get_current_admin_user
from app.api.v1.schemas.categories import CategoryCreate, CategoryUpdate, CategorySchema

router = APIRouter()

@router.get("/", response_model=List[CategorySchema])
async def list_categories(current_user: User = Depends(get_current_admin_user), db: Session = Depends(get_db)):
categories = db.query(Category).all()
return categories

@router.post("/", response_model=CategorySchema)
async def create_category(category_data: CategoryCreate, current_user: User = Depends(get_current_admin_user), db: Session = Depends(get_db)):
existing_category = db.query(Category).filter(Category.name == category_data.name).first()
if existing_category:
raise HTTPException(status_code=400, detail="Category already exists")

category = Category(
name=category_data.name,
description=category_data.description
)
db.add(category)
db.commit()
db.refresh(category)
return category

@router.get("/{category_id}", response_model=CategorySchema)
async def get_category(category_id: int, current_user: User = Depends(get_current_admin_user), db: Session = Depends(get_db)):
category = db.query(Category).filter(Category.id == category_id).first()
if not category:
raise HTTPException(status_code=404, detail="Category not found")
return category

@router.put("/{category_id}", response_model=CategorySchema)
async def update_category(category_id: int, category_data: CategoryUpdate, current_user: User = Depends(get_current_admin_user), db: Session = Depends(get_db)):
category = db.query(Category).filter(Category.id == category_id).first()
if not category:
raise HTTPException(status_code=404, detail="Category not found")

if category_data.name:
existing_category = db.query(Category).filter(Category.name == category_data.name, Category.id != category_id).first()
if existing_category:
raise HTTPException(status_code=400, detail="Category name already exists")

update_data = category_data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(category, key, value)

db.commit()
db.refresh(category)
return category

@router.delete("/{category_id}", status_code=204)
async def delete_category(category_id: int, current_user: User = Depends(get_current_admin_user), db: Session = Depends(get_db)):
category = db.query(Category).filter(Category.id == category_id).first()
if not category:
raise HTTPException(status_code=404, detail="Category not found")

pr_count = db.query(PurchaseRequest).filter(PurchaseRequest.category_id == category_id).count()
supplier_count = db.query(SupplierCategory).filter(SupplierCategory.category_id == category_id).count()

if pr_count > 0 or supplier_count > 0:
raise HTTPException(status_code=400, detail="Cannot delete category used by purchase requests or suppliers")

db.delete(category)
db.commit()
return None
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from typing import List
from datetime import datetime, timedelta

from app.database import get_db
from app.database.models import RFQ, RFQStatus, RFQSupplier, Order, OrderStatus, PurchaseRequest, KanbanStage, User
from app.api.v1.endpoints.auth import get_current_active_user
from app.api.v1.schemas.dashboard import DashboardItem, DashboardItemType

router = APIRouter()

@router.get("/", response_model=List[DashboardItem])
async def get_dashboard(
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db)
):
dashboard_items = []

rfqs_ready_for_review = db.query(RFQ).filter(RFQ.status == RFQStatus.READY_FOR_REVIEW).all()
for rfq in rfqs_ready_for_review:
dashboard_items.append(DashboardItem(
item_type=DashboardItemType.RFQ_READY_FOR_REVIEW,
record_id=rfq.id,
title=f"RFQ: {rfq.title} ready for review",
record_data={"rfq_id": rfq.id, "title": rfq.title, "deadline": str(rfq.deadline)}
))

overdue_orders = db.query(Order).filter(
Order.expected_delivery < datetime.now(),
Order.status != OrderStatus.DELIVERED
).all()

for order in overdue_orders:
dashboard_items.append(DashboardItem(
item_type=DashboardItemType.OVERDUE_ORDER,
record_id=order.id,
title=f"Order {order.order_number} is overdue",
record_data={"order_id": order.id, "order_number": order.order_number, "supplier_id": order.supplier_id}
))

new_stage = db.query(KanbanStage).filter(KanbanStage.name == "New").first()
if new_stage:
stale_threshold = datetime.now() - timedelta(days=7)
stale_requests = db.query(PurchaseRequest).filter(
PurchaseRequest.stage_id == new_stage.id,
PurchaseRequest.created_at < stale_threshold
).all()

for pr in stale_requests:
age = (datetime.now() - pr.created_at).days
dashboard_items.append(DashboardItem(
item_type=DashboardItemType.STALE_REQUEST,
record_id=pr.id,
title=f"Purchase request {pr.title} stale in New stage",
record_data={"request_id": pr.id, "title": pr.title, "age": age}
))

return dashboard_items
Loading