183 lines
6.8 KiB
Python
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)
|