JhHardwareWRS_BackPoint/app/routers/work_schedule.py
2026-06-24 15:19:14 +08:00

183 lines
6.8 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.database import get_db
from app.deps import require_roles
from app.models import (
AttendancePoint,
DeviceQRCode,
Equipment,
NoticePoint,
PersonAttendancePoint,
Personnel,
Product,
ProductionReport,
ProductionReportItem,
ReconciliationLedgerEntry,
Role,
WorkSchedule,
WorkSession,
WorkSessionDevice,
)
from app.schemas import AttendancePointCreate, WorkScheduleOut, WorkScheduleSettingsUpdate, WorkScheduleUpdate
from app.services.attendance_points import clean_point_name, ensure_default_attendance_point
from app.services.misc_work import ensure_misc_work_product
from app.services.serializers import attendance_point_out
from app.services.work_schedule import get_work_schedule, normalize_schedule_payload, update_work_schedule
router = APIRouter(prefix="/api/work-schedule", tags=["work-schedule"])
def _schedule_out(schedule: WorkSchedule, db: Session | None = None) -> WorkScheduleOut:
points = []
if db is not None:
ensure_default_attendance_point(db)
db.commit()
points = [
attendance_point_out(point)
for point in db.scalars(
select(AttendancePoint)
.where(AttendancePoint.is_active.is_(True))
.order_by(AttendancePoint.name)
).all()
]
return WorkScheduleOut(
id=schedule.id,
day_start=schedule.day_start,
day_end=schedule.day_end,
lunch_start=schedule.lunch_start,
lunch_end=schedule.lunch_end,
dinner_start=schedule.dinner_start,
dinner_end=schedule.dinner_end,
overtime_start=schedule.overtime_start,
overtime_end=schedule.overtime_end,
night_start=schedule.night_start,
night_end=schedule.night_end,
attendance_latitude=schedule.attendance_latitude,
attendance_longitude=schedule.attendance_longitude,
attendance_radius_meters=schedule.attendance_radius_meters,
auto_submit_hours=float(schedule.auto_submit_hours or 15),
attendance_points=points,
updated_by=schedule.updated_by,
updated_at=schedule.updated_at,
)
def _validate_attendance_point_payload(payload: AttendancePointCreate) -> str:
name = clean_point_name(payload.name)
if not name:
raise HTTPException(status_code=400, detail="请输入考勤点名称")
if (payload.latitude is None) != (payload.longitude is None):
raise HTTPException(status_code=400, detail="经纬度必须同时填写")
if payload.latitude is not None and not (-90 <= payload.latitude <= 90):
raise HTTPException(status_code=400, detail="纬度范围为-90到90")
if payload.longitude is not None and not (-180 <= payload.longitude <= 180):
raise HTTPException(status_code=400, detail="经度范围为-180到180")
if int(payload.radius_meters or 0) <= 0:
raise HTTPException(status_code=400, detail="考勤范围半径必须大于0米")
try:
normalize_schedule_payload(payload)
except ValueError as exc:
raise HTTPException(status_code=400, detail=f"{name} {exc}") from exc
return name
def _rename_business_point(db: Session, old_name: str, new_name: str) -> None:
if old_name == new_name:
return
for model in (
PersonAttendancePoint,
Product,
Equipment,
WorkSession,
WorkSessionDevice,
ProductionReport,
ProductionReportItem,
DeviceQRCode,
NoticePoint,
ReconciliationLedgerEntry,
):
db.query(model).filter(model.attendance_point_name == old_name).update(
{model.attendance_point_name: new_name},
synchronize_session=False,
)
def _save_attendance_point_without_commit(db: Session, payload: AttendancePointCreate) -> AttendancePoint:
name = _validate_attendance_point_payload(payload)
original_name = clean_point_name(payload.original_name) or name
point = db.get(AttendancePoint, original_name)
if point is not None and original_name != name:
if db.get(AttendancePoint, name) is not None:
raise HTTPException(status_code=409, detail="该考勤点名称已存在")
next_point = AttendancePoint(name=name)
db.add(next_point)
db.flush()
_rename_business_point(db, original_name, name)
db.delete(point)
point = next_point
elif point is None:
point = db.get(AttendancePoint, name)
if point is None:
point = AttendancePoint(name=name)
db.add(point)
db.flush()
point.latitude = payload.latitude
point.longitude = payload.longitude
point.radius_meters = int(payload.radius_meters or 500)
for field, value in normalize_schedule_payload(payload).items():
setattr(point, field, value)
point.remark = payload.remark
point.is_active = bool(payload.is_active)
ensure_misc_work_product(db, name)
db.flush()
return point
@router.get("", response_model=WorkScheduleOut)
def read_work_schedule(
db: Session = Depends(get_db),
) -> WorkScheduleOut:
return _schedule_out(get_work_schedule(db), db)
@router.put("", response_model=WorkScheduleOut)
def save_work_schedule(
payload: WorkScheduleUpdate,
user: Personnel = Depends(require_roles(Role.manager)),
db: Session = Depends(get_db),
) -> WorkScheduleOut:
try:
schedule = update_work_schedule(db, payload, user.phone)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
return _schedule_out(schedule, db)
@router.put("/settings", response_model=WorkScheduleOut)
def save_work_schedule_settings(
payload: WorkScheduleSettingsUpdate,
user: Personnel = Depends(require_roles(Role.manager)),
db: Session = Depends(get_db),
) -> WorkScheduleOut:
names = [_validate_attendance_point_payload(point) for point in payload.attendance_points]
duplicates = sorted({name for name in names if names.count(name) > 1})
if duplicates:
raise HTTPException(status_code=400, detail=f"考勤点名称重复:{''.join(duplicates)}")
if not names:
raise HTTPException(status_code=400, detail="请至少维护一个考勤点")
try:
schedule = update_work_schedule(db, payload, user.phone, commit=False)
for point in payload.attendance_points:
_save_attendance_point_without_commit(db, point)
db.commit()
db.refresh(schedule)
except ValueError as exc:
db.rollback()
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
except HTTPException:
db.rollback()
raise
return _schedule_out(schedule, db)