feat(validation): add editing versioning revalidation and aligned reports
This commit is contained in:
parent
f73a24df13
commit
302e542fda
28 changed files with 2691 additions and 406 deletions
|
|
@ -2,8 +2,13 @@ from __future__ import annotations
|
|||
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Response
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Response, UploadFile
|
||||
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.dependencies import current_user
|
||||
|
|
@ -14,6 +19,7 @@ from app.models.device import Device
|
|||
from app.models.equipment import Equipment
|
||||
from app.models.location import Location
|
||||
from app.models.validation import Validation
|
||||
from app.modules.orion.service import OrionReportService
|
||||
from app.schemas.common import PaginatedResponse
|
||||
from app.schemas.domain import (
|
||||
ContactCreate,
|
||||
|
|
@ -32,16 +38,22 @@ from app.schemas.domain import (
|
|||
LocationRead,
|
||||
LocationUpdate,
|
||||
ValidationCreate,
|
||||
ValidationImportPreview,
|
||||
ValidationImportRequest,
|
||||
ValidationImportSummary,
|
||||
ValidationRead,
|
||||
ValidationReview,
|
||||
ValidationUpdate,
|
||||
)
|
||||
from app.services.domain_service import CrudService, DomainServices
|
||||
from app.services.validation_workflow import ValidationWorkflowService
|
||||
|
||||
router = APIRouter(tags=["domain"], dependencies=[Depends(current_user)])
|
||||
|
||||
|
||||
@router.get("/dashboard")
|
||||
def dashboard(session: Session = Depends(get_session)) -> dict[str, int]:
|
||||
today = func.current_date()
|
||||
return {
|
||||
"customers": session.scalar(select(func.count()).select_from(Customer)) or 0,
|
||||
"locations": session.scalar(select(func.count()).select_from(Location)) or 0,
|
||||
|
|
@ -49,6 +61,11 @@ def dashboard(session: Session = Depends(get_session)) -> dict[str, int]:
|
|||
"devices": session.scalar(select(func.count()).select_from(Device)) or 0,
|
||||
"equipment": session.scalar(select(func.count()).select_from(Equipment)) or 0,
|
||||
"validations": session.scalar(select(func.count()).select_from(Validation)) or 0,
|
||||
"validation_drafts": session.scalar(select(func.count()).select_from(Validation).where(Validation.status == "ENTWURF")) or 0,
|
||||
"validation_ready": session.scalar(select(func.count()).select_from(Validation).where(Validation.status == "BEREIT_ZUR_PRUEFUNG")) or 0,
|
||||
"validation_in_review": session.scalar(select(func.count()).select_from(Validation).where(Validation.status == "IN_PRUEFUNG")) or 0,
|
||||
"validation_approved": session.scalar(select(func.count()).select_from(Validation).where(Validation.status == "FREIGEGEBEN")) or 0,
|
||||
"validation_overdue": session.scalar(select(func.count()).select_from(Validation).where(Validation.next_validation_on < today)) or 0,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -61,17 +78,33 @@ def paging(
|
|||
|
||||
|
||||
def commit_create(session: Session, service: CrudService, payload):
|
||||
item = service.create(payload.model_dump())
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
return item
|
||||
try:
|
||||
item = service.create(payload.model_dump())
|
||||
if isinstance(item, Validation):
|
||||
ValidationWorkflowService(session).apply_revalidation_date(item)
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
return item
|
||||
except IntegrityError as exc:
|
||||
session.rollback()
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(status_code=409, detail="Datensatz verletzt Datenbankbeziehungen.") from exc
|
||||
|
||||
|
||||
def commit_update(session: Session, service: CrudService, item_id: str, payload):
|
||||
item = service.update(item_id, payload.model_dump())
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
return item
|
||||
try:
|
||||
item = service.update(item_id, payload.model_dump())
|
||||
if isinstance(item, Validation):
|
||||
ValidationWorkflowService(session).apply_revalidation_date(item)
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
return item
|
||||
except IntegrityError as exc:
|
||||
session.rollback()
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(status_code=409, detail="Datensatz verletzt Datenbankbeziehungen.") from exc
|
||||
|
||||
|
||||
def commit_delete(session: Session, service: CrudService, item_id: str) -> Response:
|
||||
|
|
@ -181,8 +214,39 @@ def delete_equipment(item_id: str, session: Session = Depends(get_session)):
|
|||
|
||||
|
||||
@router.get("/validations", response_model=PaginatedResponse[ValidationRead])
|
||||
def list_validations(params: dict = Depends(paging), session: Session = Depends(get_session)):
|
||||
return DomainServices(session).validations.list(**params)
|
||||
def list_validations(
|
||||
search: str | None = Query(default=None, max_length=120),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
sort_by: str = Query(default="updated_at"),
|
||||
sort_order: str = Query(default="desc", pattern="^(asc|desc)$"),
|
||||
status: str | None = None,
|
||||
customer_id: str | None = None,
|
||||
device_id: str | None = None,
|
||||
validation_type: str | None = None,
|
||||
result: str | None = None,
|
||||
date_from: str | None = None,
|
||||
date_to: str | None = None,
|
||||
overdue_only: bool = False,
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
return ValidationWorkflowService(session).query_validations(
|
||||
search=search,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
filters={
|
||||
"status": status,
|
||||
"customer_id": customer_id,
|
||||
"device_id": device_id,
|
||||
"validation_type": validation_type,
|
||||
"result": result,
|
||||
"date_from": date_from,
|
||||
"date_to": date_to,
|
||||
"overdue_only": overdue_only,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/validations/next-report-number")
|
||||
|
|
@ -213,4 +277,101 @@ def update_validation(item_id: str, payload: ValidationUpdate, session: Session
|
|||
|
||||
@router.delete("/validations/{item_id}", status_code=204)
|
||||
def delete_validation(item_id: str, session: Session = Depends(get_session)):
|
||||
item = DomainServices(session).validations.repository.get(item_id)
|
||||
if item and item.status != "ENTWURF":
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(status_code=409, detail="Only draft validations can be deleted")
|
||||
return commit_delete(session, DomainServices(session).validations, item_id)
|
||||
|
||||
|
||||
@router.post("/validations/{item_id}/review", response_model=ValidationReview)
|
||||
def review_validation(item_id: str, session: Session = Depends(get_session)):
|
||||
item = DomainServices(session).validations.repository.get(item_id)
|
||||
if item is None:
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(status_code=404, detail="Resource not found")
|
||||
review = ValidationWorkflowService(session).mark_ready_for_review(item)
|
||||
session.commit()
|
||||
return review
|
||||
|
||||
|
||||
@router.post("/validations/{item_id}/duplicate", response_model=ValidationRead, status_code=201)
|
||||
def duplicate_validation(item_id: str, session: Session = Depends(get_session)):
|
||||
item = DomainServices(session).validations.repository.get(item_id)
|
||||
if item is None:
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(status_code=404, detail="Resource not found")
|
||||
clone = ValidationWorkflowService(session).duplicate(item)
|
||||
session.commit()
|
||||
session.refresh(clone)
|
||||
return clone
|
||||
|
||||
|
||||
@router.post("/validations/{item_id}/new-version", response_model=ValidationRead, status_code=201)
|
||||
def new_validation_version(item_id: str, session: Session = Depends(get_session)):
|
||||
item = DomainServices(session).validations.repository.get(item_id)
|
||||
if item is None:
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(status_code=404, detail="Resource not found")
|
||||
clone = ValidationWorkflowService(session).create_new_version(item)
|
||||
session.commit()
|
||||
session.refresh(clone)
|
||||
return clone
|
||||
|
||||
|
||||
@router.post("/validations/{item_id}/cancel", response_model=ValidationRead)
|
||||
def cancel_validation(item_id: str, session: Session = Depends(get_session)):
|
||||
item = DomainServices(session).validations.repository.get(item_id)
|
||||
if item is None:
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(status_code=404, detail="Resource not found")
|
||||
item.status = "STORNIERT"
|
||||
session.commit()
|
||||
session.refresh(item)
|
||||
return item
|
||||
|
||||
|
||||
@router.get("/validations/{item_id}/export.json")
|
||||
def export_validation_json(item_id: str, session: Session = Depends(get_session)):
|
||||
item = DomainServices(session).validations.repository.get(item_id)
|
||||
if item is None:
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(status_code=404, detail="Resource not found")
|
||||
data = ValidationWorkflowService(session).export_json(item)
|
||||
return JSONResponse(
|
||||
content=json.loads(json.dumps(data, default=str)),
|
||||
headers={"Content-Disposition": f'attachment; filename="{item.report_number}.json"'},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/validations/import/csv-preview", response_model=ValidationImportPreview)
|
||||
async def preview_validation_csv(file: UploadFile, session: Session = Depends(get_session)):
|
||||
return ValidationWorkflowService(session).preview_csv(await file.read())
|
||||
|
||||
|
||||
@router.post("/validations/import/json", response_model=ValidationImportSummary)
|
||||
def import_validation_json(payload: ValidationImportRequest, session: Session = Depends(get_session)):
|
||||
summary = ValidationWorkflowService(session).import_rows(payload.rows, payload.duplicate_strategy)
|
||||
session.commit()
|
||||
return summary
|
||||
|
||||
|
||||
@router.get("/validations/{item_id}/report.html", response_class=HTMLResponse)
|
||||
def validation_report_preview(item_id: str, session: Session = Depends(get_session)):
|
||||
return OrionReportService(session, Path("/app/reports")).render_html(item_id)
|
||||
|
||||
|
||||
@router.get("/validations/{item_id}/report.pdf")
|
||||
def validation_report_pdf(item_id: str, session: Session = Depends(get_session)):
|
||||
report_path = OrionReportService(session, Path("/app/reports")).render_pdf(item_id)
|
||||
return FileResponse(
|
||||
report_path,
|
||||
media_type="application/pdf",
|
||||
filename=report_path.name,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue