from datetime import date, datetime from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field from app.models import ReportStatus, Role class Page(BaseModel): page: int = 1 page_size: int = 10 total: int = 0 total_pages: int = 1 class PageResponse(BaseModel): page: int page_size: int total: int total_pages: int rows: list[Any] class TokenResponse(BaseModel): access_token: str token_type: str = "bearer" user: "PersonnelOut" class AttendancePointBase(BaseModel): name: str = Field(min_length=1, max_length=128) latitude: float | None = None longitude: float | None = None radius_meters: int = Field(default=500, ge=1) day_start: str = Field(default="08:00", pattern=r"^\d{1,2}:\d{2}$") day_end: str = Field(default="17:20", pattern=r"^\d{1,2}:\d{2}$") lunch_start: str = Field(default="11:40", pattern=r"^\d{1,2}:\d{2}$") lunch_end: str = Field(default="12:40", pattern=r"^\d{1,2}:\d{2}$") dinner_start: str = Field(default="17:20", pattern=r"^\d{1,2}:\d{2}$") dinner_end: str = Field(default="18:00", pattern=r"^\d{1,2}:\d{2}$") overtime_start: str = Field(default="18:00", pattern=r"^\d{1,2}:\d{2}$") overtime_end: str = Field(default="20:00", pattern=r"^\d{1,2}:\d{2}$") night_start: str = Field(default="20:00", pattern=r"^\d{1,2}:\d{2}$") night_end: str = Field(default="06:00", pattern=r"^\d{1,2}:\d{2}$") remark: str | None = Field(default=None, max_length=500) is_active: bool = True class AttendancePointCreate(AttendancePointBase): original_name: str | None = None class AttendancePointOut(AttendancePointBase): created_at: datetime | None = None updated_at: datetime | None = None class LoginSelectionResponse(BaseModel): needs_selection: bool matched_people: list["PersonnelOut"] = [] selection_token: str | None = None access_token: str | None = None token_type: str = "bearer" user: "PersonnelOut | None" = None needs_temporary_worker: bool = False temporary_token: str | None = None temporary_phone: str | None = None temporary_expires_at: datetime | None = None class WechatLoginRequest(BaseModel): phone_code: str | None = None phone: str | None = None phone_candidates: list[str] = [] selected_role: Role | None = None create_temporary_worker: bool = False temporary_token: str | None = None temporary_name: str | None = None temporary_attendance_point_name: str | None = None temporary_latitude: float | None = None temporary_longitude: float | None = None class RoleSelectionRequest(BaseModel): selection_token: str role: Role class SwitchRoleRequest(BaseModel): role: Role class PersonnelBase(BaseModel): phone: str = Field(min_length=5, max_length=20) name: str = Field(min_length=1, max_length=64) role: Role attendance_point_names: list[str] = [] class PersonnelCreate(PersonnelBase): original_phone: str | None = None class PersonnelOut(PersonnelBase): model_config = ConfigDict(from_attributes=True) role_name: str roles: list[Role] = Field(default_factory=list) role_names: list[str] = Field(default_factory=list) attendance_points: list[AttendancePointOut] = Field(default_factory=list) is_temporary: bool = False temporary_expires_at: datetime | None = None temporary_expired: bool = False class ProductBase(BaseModel): attendance_point_name: str = Field(min_length=1, max_length=128) project_no: str = Field(min_length=1, max_length=64) product_name: str = Field(min_length=1, max_length=255) profile_no: str | None = None material_code: str | None = None material_name: str | None = None supplier: str | None = None product_net_weight_kg: float | None = None product_gross_weight_kg: float | None = None scrap_loss_rate: float | None = None waste_price_yuan_per_kg: float | None = None device_no: str = "" process_name: str = Field(min_length=1, max_length=128) stamping_method: str | None = None operator_count: float = 1 process_unit_price_yuan: float = Field(default=0, ge=0) standard_beat: float = Field(default=0, ge=0) standard_workload: float | None = None class ProductCreate(ProductBase): original_attendance_point_name: str | None = None original_project_no: str | None = None original_product_name: str | None = None original_device_no: str | None = None original_process_name: str | None = None class ProductOut(ProductBase): standard_workload: float display_name: str class MoldNameOut(BaseModel): attendance_point_name: str = "" name: str process_name: str = "" stamping_method: str | None = None operator_count: float = 1 is_cleaning: bool = False is_misc: bool = False is_continuous_die: bool = False is_multi_person: bool = False display_name: str = "" class DeviceQrCreate(BaseModel): attendance_point_name: str = Field(min_length=1, max_length=128) product_name: str = Field(min_length=1, max_length=255) process_name: str = Field(min_length=1, max_length=128) class DeviceQrBatchCreate(BaseModel): items: list[DeviceQrCreate] = Field(min_length=1, max_length=500) class DeviceQrBatchOut(BaseModel): count: int file_name: str zip_url: str class DeviceQrBatchTaskOut(BaseModel): id: int file_name: str status: str status_name: str item_count: int completed_count: int failed_count: int = 0 progress_percent: float = 0 zip_url: str | None = None error_message: str | None = None started_at: datetime | None = None finished_at: datetime | None = None created_at: datetime updated_at: datetime class EquipmentBase(BaseModel): attendance_point_name: str = Field(min_length=1, max_length=128) device_no: str = Field(min_length=1, max_length=64) device_type: Literal["冲压设备", "清洗设备"] = "冲压设备" remark: str | None = Field(default=None, max_length=500) class EquipmentCreate(EquipmentBase): original_attendance_point_name: str | None = None original_device_no: str | None = None class EquipmentOut(EquipmentBase): created_at: datetime updated_at: datetime class WorkScheduleBase(BaseModel): day_start: str = Field(pattern=r"^\d{1,2}:\d{2}$") day_end: str = Field(pattern=r"^\d{1,2}:\d{2}$") lunch_start: str = Field(pattern=r"^\d{1,2}:\d{2}$") lunch_end: str = Field(pattern=r"^\d{1,2}:\d{2}$") dinner_start: str = Field(pattern=r"^\d{1,2}:\d{2}$") dinner_end: str = Field(pattern=r"^\d{1,2}:\d{2}$") overtime_start: str = Field(pattern=r"^\d{1,2}:\d{2}$") overtime_end: str = Field(pattern=r"^\d{1,2}:\d{2}$") night_start: str = Field(pattern=r"^\d{1,2}:\d{2}$") night_end: str = Field(pattern=r"^\d{1,2}:\d{2}$") attendance_latitude: float | None = None attendance_longitude: float | None = None attendance_radius_meters: int | None = Field(default=500, ge=1) auto_submit_hours: float = Field(default=15, ge=1, le=168) class WorkScheduleUpdate(WorkScheduleBase): pass class WorkScheduleSettingsUpdate(WorkScheduleBase): attendance_points: list[AttendancePointCreate] = Field(default_factory=list) class WorkScheduleOut(WorkScheduleBase): id: int attendance_points: list[AttendancePointOut] = Field(default_factory=list) updated_by: str | None = None updated_at: datetime class ImportResult(BaseModel): imported: int updated: int skipped: int errors: list[str] class ClockState(BaseModel): action: Literal["start", "switch", "switch_or_finish", "finish", "report", "refinish"] action_text: str attendance_point_name: str = "" device_no: str mold_name: str = "" process_name: str = "" stamping_method: str | None = None display_name: str = "" is_cleaning: bool = False is_misc: bool = False current_mold_name: str = "" current_process_name: str = "" session_id: int | None = None start_at: datetime | None = None end_at: datetime | None = None devices: list[str] = [] auto_submit_hours: float = 15 class MoldLockConflictDetail(BaseModel): code: str = "mold_occupied" message: str lock_id: int attendance_point_name: str = "" mold_name: str = "" process_name: str = "" mold_display_name: str = "" occupied_phone: str = "" occupied_name: str = "" class ClockRequest(BaseModel): attendance_point_name: str = "" device_no: str = "" mold_name: str = "" process_name: str = "" latitude: float | None = None longitude: float | None = None class ContinueWorkRequest(BaseModel): session_id: int class ClockSessionOut(BaseModel): id: int attendance_point_name: str = "" employee_phone: str start_at: datetime end_at: datetime | None status: str devices: list[str] class ProductOption(BaseModel): attendance_point_name: str = "" project_no: str product_name: str material_code: str | None = None material_name: str | None = None device_no: str process_name: str | None = None stamping_method: str | None = None operator_count: float = 1 process_unit_price_yuan: float = 0 standard_beat: float standard_workload: float is_cleaning: bool = False is_misc: bool = False is_continuous_die: bool = False is_multi_person: bool = False display_name: str class EquipmentOption(BaseModel): attendance_point_name: str = "" device_no: str device_type: str = "冲压设备" remark: str | None = None display_name: str class ReportDraftItem(BaseModel): attendance_point_name: str = "" device_no: str = "" project_no: str = "" product_name: str = "" material_code: str | None = None material_name: str | None = None raw_material_batch_no: str | None = None raw_material_batch_options: list[str] = [] process_name: str | None = None stamping_method: str | None = None operator_count: float = 1 process_unit_price_yuan: float = 0 changeover_count: float = 0 standard_beat: float = 0 standard_workload: float = 0 good_qty: float = 0 defect_qty: float = 0 scrap_qty: float = 0 is_misc: bool = False is_continuous_die: bool = False remark: str | None = None class ReportDraftBlock(BaseModel): attendance_point_name: str = "" device_no: str mold_name: str = "" process_name: str = "" stamping_method: str | None = None display_name: str = "" start_at: datetime end_at: datetime duration_minutes: float = 0 options: list[ProductOption] equipment_options: list[EquipmentOption] = [] items: list[ReportDraftItem] is_misc: bool = False is_continuous_die: bool = False class ReportDraftOut(BaseModel): session_id: int attendance_point_name: str = "" employee_phone: str employee_name: str start_at: datetime end_at: datetime duration_minutes: float device_blocks: list[ReportDraftBlock] class ReportSubmitItem(BaseModel): attendance_point_name: str = "" device_no: str = "" project_no: str product_name: str material_code: str | None = None material_name: str | None = None raw_material_batch_no: str | None = None process_name: str | None = None stamping_method: str | None = None operator_count: float = 1 process_unit_price_yuan: float = 0 changeover_count: float = Field(default=0, ge=0) standard_beat: float standard_workload: float = 0 good_qty: float = Field(default=0, ge=0) defect_qty: float = Field(default=0, ge=0) scrap_qty: float = Field(default=0, ge=0) started_at: datetime | None = None is_misc: bool = False is_continuous_die: bool = False remark: str | None = None class ReportSubmitBlock(BaseModel): attendance_point_name: str = "" device_no: str process_name: str = "" items: list[ReportSubmitItem] class ReportSubmitRequest(BaseModel): session_id: int device_blocks: list[ReportSubmitBlock] class CleaningReportSubmitItem(BaseModel): attendance_point_name: str = "" device_no: str = "" device_nos: list[str] = Field(default_factory=list) product_name: str = Field(min_length=1, max_length=255) process_name: str = Field(min_length=1, max_length=128) stamping_method: str | None = None quantity: float = Field(gt=0) class CleaningReportSubmitRequest(BaseModel): items: list[CleaningReportSubmitItem] latitude: float | None = None longitude: float | None = None class ReportMetrics(BaseModel): effective_minutes: float shift_distribution_text: str = "" shift_day_minutes: float = 0 shift_overtime_minutes: float = 0 shift_night_minutes: float = 0 shift_other_minutes: float = 0 shift_meal_break_minutes: float = 0 total_good_qty: float total_output_qty: float actual_beat: float standard_beat: float expected_workload: float pace_rate: float workload_rate: float class ReportAllocationOut(BaseModel): allocation_date: date day_minutes: float = 0 overtime_minutes: float = 0 night_minutes: float = 0 effective_minutes: float = 0 good_qty: float = 0 defect_qty: float = 0 scrap_qty: float = 0 changeover_count: float = 0 reference_wage: float = 0 class ReportItemOut(BaseModel): id: int | None = None attendance_point_name: str = "" device_no: str project_no: str product_name: str material_code: str | None = None material_name: str | None = None raw_material_batch_no: str | None = None process_name: str | None = None stamping_method: str | None = None operator_count: float = 1 process_unit_price_yuan: float = 0 changeover_count: float = 0 standard_beat: float standard_workload: float good_qty: float defect_qty: float scrap_qty: float allocated_minutes: float = 0 started_at: datetime | None = None is_misc: bool = False is_continuous_die: bool = False is_multi_person: bool = False remark: str | None = None allocations: list[ReportAllocationOut] = Field(default_factory=list) corrections: dict[str, Any] = Field(default_factory=dict) class ReportDeviceSegmentOut(BaseModel): id: int attendance_point_name: str = "" device_no: str mold_name: str = "" process_name: str = "" stamping_method: str | None = None display_name: str = "" scanned_at: datetime released_at: datetime | None = None sort_order: int = 0 corrections: dict[str, Any] = Field(default_factory=dict) class ReportOut(BaseModel): id: int attendance_point_name: str = "" session_id: int employee_phone: str employee_name: str report_date: date start_at: datetime end_at: datetime duration_minutes: float break_minutes: float status: ReportStatus status_name: str reviewer_phone: str | None = None reviewer_name: str | None = None reviewed_at: datetime | None = None reject_reason: str | None = None review_remark: str | None = None submitted_at: datetime is_system_auto_submitted: bool = False auto_submit_reason: str | None = None is_multi_person_assistant: bool = False multi_person_source_report_id: int | None = None is_voided: bool = False voided_at: datetime | None = None voided_by: str | None = None voided_by_name: str | None = None unvoid_deadline_at: datetime | None = None can_void: bool = False can_unvoid: bool = False can_edit: bool = False is_modified: bool = False device_segments: list[ReportDeviceSegmentOut] = Field(default_factory=list) items: list[ReportItemOut] metrics: ReportMetrics result_text: str allocation_summary_text: str = "" corrections: dict[str, Any] = Field(default_factory=dict) class ReportItemCorrection(BaseModel): id: int device_no: str | None = None project_no: str | None = None product_name: str | None = None process_name: str | None = None raw_material_batch_no: str | None = None changeover_count: float | None = Field(default=None, ge=0) good_qty: float | None = Field(default=None, ge=0) defect_qty: float | None = Field(default=None, ge=0) scrap_qty: float | None = Field(default=None, ge=0) remark: str | None = None class ReportDeviceCorrection(BaseModel): id: int scanned_at: datetime | None = None class ApproveReportRequest(BaseModel): model_config = ConfigDict(extra="forbid") start_at: datetime | None = None end_at: datetime | None = None device_corrections: list[ReportDeviceCorrection] = Field(default_factory=list) item_corrections: list[ReportItemCorrection] = [] review_remark: str | None = Field(default=None, max_length=500) remark: str | None = None class RejectReportRequest(BaseModel): reason: str = "需重新填报" class DeviceQrOut(BaseModel): attendance_point_name: str = "" device_no: str process_name: str = "" stamping_method: str | None = None is_cleaning: bool = False is_misc: bool = False is_continuous_die: bool = False mold_name: str = "" display_name: str = "" scene: str page: str qr_url: str | None = None class DashboardRow(BaseModel): id: str attendance_point_name: str = "" report_date: date source_report_date: date | None = None source_report_dates: list[date] = Field(default_factory=list) employee_phone: str employee_name: str project_no: str product_name: str process_name: str | None = None report_count: int effective_minutes: float shift_distribution_text: str = "" shift_day_minutes: float = 0 shift_overtime_minutes: float = 0 shift_night_minutes: float = 0 shift_other_minutes: float = 0 total_good_qty: float total_defect_qty: float total_output_qty: float actual_beat: float standard_beat: float expected_workload: float pace_rate: float workload_rate: float pace_text: str workload_text: str profile_no: str | None = None material_code: str | None = None material_name: str | None = None raw_material_batch_no: str | None = None device_no: str | None = None stamping_method: str | None = None operator_count: float | None = None process_unit_price_yuan: float = 0 changeover_count: float = 0 reference_wage: float = 0 worker_type: str = "正式工" is_multi_person: bool = False is_multi_person_assistant: bool = False is_misc: bool = False is_continuous_die: bool = False is_system_auto_submitted: bool = False is_voided: bool = False review_remark: str | None = None correction_pairs: list[dict[str, Any]] = Field(default_factory=list) class UsageStatsRow(BaseModel): id: str category: str attendance_point_name: str = "" name: str object_type: str metric_kind: str value: float = 0 report_count: int = 0 product_name: str = "" process_name: str = "" stamping_method: str = "" tags: list[str] = Field(default_factory=list) class UsageStatsDailyRow(BaseModel): report_date: date value: float = 0 report_count: int = 0 class UsageStatsReportRow(BaseModel): report_id: int report_date: date employee_phone: str = "" employee_name: str = "" display_name: str = "" value: float = 0 report_count: int = 1 class UsageStatsDetailOut(BaseModel): id: str = "" category: str attendance_point_name: str = "" name: str = "" object_type: str = "" metric_kind: str = "" value: float = 0 report_count: int = 0 product_name: str = "" process_name: str = "" stamping_method: str = "" tags: list[str] = Field(default_factory=list) daily_rows: list[UsageStatsDailyRow] = Field(default_factory=list) report_rows: list[UsageStatsReportRow] = Field(default_factory=list) class NoticeCreate(BaseModel): title: str = Field(min_length=1, max_length=120) content: str = Field(min_length=1, max_length=1000) attendance_point_names: list[str] = [] sort_order: int = 0 is_active: bool = True class NoticeUpdate(BaseModel): title: str | None = Field(default=None, min_length=1, max_length=120) content: str | None = Field(default=None, min_length=1, max_length=1000) attendance_point_names: list[str] | None = None sort_order: int | None = None is_active: bool | None = None class NoticeOut(BaseModel): id: int title: str content: str attendance_point_names: list[str] = [] sort_order: int is_active: bool created_by: str creator_name: str created_at: datetime updated_at: datetime class ReconciliationMonthCell(BaseModel): month: int reported_good_qty: float = 0 reconciled_good_qty: float = 0 return_qty: float = 0 class ReconciliationRow(BaseModel): attendance_point_name: str = "" product_name: str months: list[ReconciliationMonthCell] class ReconciliationLedgerOut(BaseModel): year: int current_year: int current_month: int rows: list[ReconciliationRow] class ReconciliationYearsOut(BaseModel): years: list[int] current_year: int class MonitorLockOut(BaseModel): id: int attendance_point_name: str = "" mold_name: str process_name: str = "" stamping_method: str | None = None mold_display_name: str occupied_phone: str occupied_name: str occupied_start_at: datetime occupied_minutes: float class MoldLockFeedbackOut(BaseModel): id: int attendance_point_name: str = "" mold_name: str process_name: str = "" stamping_method: str | None = None mold_display_name: str lock_id: int reporter_phone: str reporter_name: str occupied_phone: str occupied_name: str feedback_at: datetime read_at: datetime | None = None handled_at: datetime | None = None status: str status_name: str can_release: bool = True class MoldLockFeedbackCreate(BaseModel): lock_id: int class ReconciliationEntryUpdate(BaseModel): attendance_point_name: str = Field(min_length=1, max_length=128) year: int = Field(ge=2000, le=2100) month: int = Field(ge=1, le=12) product_name: str = Field(min_length=1, max_length=255) reconciled_good_qty: float | None = Field(default=None, ge=0) return_qty: float | None = Field(default=None, ge=0) class ErpLoginQrcodeRequest(BaseModel): ticket: str = Field(min_length=1, max_length=16) session_id: int = Field(ge=1) class ErpLoginQrcodeResponse(BaseModel): qr_url: str | None = None page: str scene: str class ErpLoginPreviewResponse(BaseModel): session_id: int system_name: str started_at: datetime expires_at: datetime request_ip_hint: str | None = None user_agent_hint: str | None = None class ErpLoginConfirmRequest(BaseModel): phone_code: str = Field(min_length=1, max_length=256) class ErpLoginActionResponse(BaseModel): status: str failure_reason: str | None = None PersonnelOut.model_rebuild() TokenResponse.model_rebuild() LoginSelectionResponse.model_rebuild()