from datetime import date, datetime from enum import StrEnum from sqlalchemy import ( BigInteger, Boolean, Date, DateTime, Enum, ForeignKey, Index, Integer, Numeric, String, Text, ) from sqlalchemy.dialects.mysql import JSON from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database import Base from app.timezone import now class Role(StrEnum): worker = "worker" admin = "admin" manager = "manager" class SessionStatus(StrEnum): active = "active" reporting = "reporting" submitted = "submitted" cancelled = "cancelled" class ReportStatus(StrEnum): pending = "pending" approved = "approved" rejected = "rejected" class AuditAction(StrEnum): approve = "approve" approve_with_correction = "approve_with_correction" reject = "reject" manager_break_update = "manager_break_update" class AttendancePoint(Base): __tablename__ = "attendance_points" name: Mapped[str] = mapped_column(String(128), primary_key=True) latitude: Mapped[float | None] = mapped_column(Numeric(10, 7)) longitude: Mapped[float | None] = mapped_column(Numeric(10, 7)) radius_meters: Mapped[int] = mapped_column(Integer, nullable=False, default=500) day_start: Mapped[str] = mapped_column(String(5), nullable=False, default="08:00") day_end: Mapped[str] = mapped_column(String(5), nullable=False, default="17:20") lunch_start: Mapped[str] = mapped_column(String(5), nullable=False, default="11:40") lunch_end: Mapped[str] = mapped_column(String(5), nullable=False, default="12:40") dinner_start: Mapped[str] = mapped_column(String(5), nullable=False, default="17:20") dinner_end: Mapped[str] = mapped_column(String(5), nullable=False, default="18:00") overtime_start: Mapped[str] = mapped_column(String(5), nullable=False, default="18:00") overtime_end: Mapped[str] = mapped_column(String(5), nullable=False, default="20:00") night_start: Mapped[str] = mapped_column(String(5), nullable=False, default="20:00") night_end: Mapped[str] = mapped_column(String(5), nullable=False, default="06:00") remark: Mapped[str | None] = mapped_column(String(500)) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) updated_at: Mapped[datetime] = mapped_column(DateTime, default=now, onupdate=now, nullable=False) class Personnel(Base): __tablename__ = "personnel" phone: Mapped[str] = mapped_column(String(20), primary_key=True) name: Mapped[str] = mapped_column(String(64), nullable=False) is_temporary: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) temporary_expires_at: Mapped[datetime | None] = mapped_column(DateTime) created_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) updated_at: Mapped[datetime] = mapped_column(DateTime, default=now, onupdate=now, nullable=False) roles: Mapped[list["PersonRole"]] = relationship( back_populates="person", cascade="all, delete-orphan", ) attendance_points: Mapped[list["PersonAttendancePoint"]] = relationship( back_populates="person", cascade="all, delete-orphan", ) class PersonRole(Base): __tablename__ = "person_roles" phone: Mapped[str] = mapped_column(ForeignKey("personnel.phone"), primary_key=True) role: Mapped[Role] = mapped_column(Enum(Role), primary_key=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) person: Mapped[Personnel] = relationship(back_populates="roles") class PersonAttendancePoint(Base): __tablename__ = "person_attendance_points" phone: Mapped[str] = mapped_column(ForeignKey("personnel.phone"), primary_key=True) attendance_point_name: Mapped[str] = mapped_column( ForeignKey("attendance_points.name"), primary_key=True, ) created_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) person: Mapped[Personnel] = relationship(back_populates="attendance_points") attendance_point: Mapped[AttendancePoint] = relationship() class Product(Base): __tablename__ = "products" attendance_point_name: Mapped[str] = mapped_column(String(128), primary_key=True, default="") project_no: Mapped[str] = mapped_column(String(64), primary_key=True) product_name: Mapped[str] = mapped_column(String(255), primary_key=True) device_no: Mapped[str] = mapped_column(String(64), primary_key=True) process_name: Mapped[str] = mapped_column(String(128), primary_key=True, default="") profile_no: Mapped[str | None] = mapped_column(String(128)) material_code: Mapped[str | None] = mapped_column(String(128)) material_name: Mapped[str | None] = mapped_column(String(255)) supplier: Mapped[str | None] = mapped_column(String(255)) product_net_weight_kg: Mapped[float | None] = mapped_column(Numeric(12, 4)) product_gross_weight_kg: Mapped[float | None] = mapped_column(Numeric(12, 4)) scrap_loss_rate: Mapped[float | None] = mapped_column(Numeric(10, 6)) waste_price_yuan_per_kg: Mapped[float | None] = mapped_column(Numeric(12, 4)) stamping_method: Mapped[str | None] = mapped_column(String(128)) operator_count: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False, default=1) process_unit_price_yuan: Mapped[float] = mapped_column(Numeric(12, 4), nullable=False, default=0) standard_beat: Mapped[float] = mapped_column(Numeric(12, 3), nullable=False) standard_workload: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False, default=0) created_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) updated_at: Mapped[datetime] = mapped_column(DateTime, default=now, onupdate=now, nullable=False) __table_args__ = ( Index("idx_products_device_no", "device_no"), Index("idx_products_material_code", "material_code"), ) class Equipment(Base): __tablename__ = "equipment" attendance_point_name: Mapped[str] = mapped_column(String(128), primary_key=True, default="") device_no: Mapped[str] = mapped_column(String(64), primary_key=True) device_type: Mapped[str] = mapped_column(String(32), nullable=False, default="冲压设备") remark: Mapped[str | None] = mapped_column(String(500)) created_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) updated_at: Mapped[datetime] = mapped_column(DateTime, default=now, onupdate=now, nullable=False) class WorkSchedule(Base): __tablename__ = "work_schedules" id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1) day_start: Mapped[str] = mapped_column(String(5), nullable=False, default="08:00") day_end: Mapped[str] = mapped_column(String(5), nullable=False, default="17:20") lunch_start: Mapped[str] = mapped_column(String(5), nullable=False, default="11:40") lunch_end: Mapped[str] = mapped_column(String(5), nullable=False, default="12:40") dinner_start: Mapped[str] = mapped_column(String(5), nullable=False, default="17:20") dinner_end: Mapped[str] = mapped_column(String(5), nullable=False, default="18:00") overtime_start: Mapped[str] = mapped_column(String(5), nullable=False, default="18:00") overtime_end: Mapped[str] = mapped_column(String(5), nullable=False, default="20:00") night_start: Mapped[str] = mapped_column(String(5), nullable=False, default="20:00") night_end: Mapped[str] = mapped_column(String(5), nullable=False, default="06:00") attendance_latitude: Mapped[float | None] = mapped_column(Numeric(10, 7)) attendance_longitude: Mapped[float | None] = mapped_column(Numeric(10, 7)) attendance_radius_meters: Mapped[int] = mapped_column(Integer, nullable=False, default=500) auto_submit_hours: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False, default=15) updated_by: Mapped[str | None] = mapped_column(String(20)) created_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) updated_at: Mapped[datetime] = mapped_column(DateTime, default=now, onupdate=now, nullable=False) class WorkSession(Base): __tablename__ = "work_sessions" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) attendance_point_name: Mapped[str] = mapped_column(String(128), nullable=False, default="") employee_phone: Mapped[str] = mapped_column(ForeignKey("personnel.phone"), nullable=False) start_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) end_at: Mapped[datetime | None] = mapped_column(DateTime) status: Mapped[SessionStatus] = mapped_column( Enum(SessionStatus), nullable=False, default=SessionStatus.active, ) created_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) updated_at: Mapped[datetime] = mapped_column(DateTime, default=now, onupdate=now, nullable=False) employee: Mapped[Personnel] = relationship() devices: Mapped[list["WorkSessionDevice"]] = relationship( back_populates="session", cascade="all, delete-orphan", order_by="WorkSessionDevice.sort_order", ) __table_args__ = ( Index("idx_sessions_employee_status", "employee_phone", "status"), ) class WorkSessionDevice(Base): __tablename__ = "work_session_devices" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) session_id: Mapped[int] = mapped_column(ForeignKey("work_sessions.id"), nullable=False) attendance_point_name: Mapped[str] = mapped_column(String(128), nullable=False, default="") device_no: Mapped[str] = mapped_column(String(255), nullable=False) process_name: Mapped[str] = mapped_column(String(128), nullable=False, default="") scanned_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) sort_order: Mapped[int] = mapped_column(nullable=False, default=0) released_at: Mapped[datetime | None] = mapped_column(DateTime) released_by: Mapped[str | None] = mapped_column(String(20)) release_reason: Mapped[str | None] = mapped_column(String(120)) session: Mapped[WorkSession] = relationship(back_populates="devices") __table_args__ = ( Index("idx_session_devices_session", "session_id"), Index("idx_session_devices_device", "device_no"), Index("idx_session_devices_release", "released_at"), ) class ProductionReport(Base): __tablename__ = "production_reports" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) attendance_point_name: Mapped[str] = mapped_column(String(128), nullable=False, default="") session_id: Mapped[int] = mapped_column(ForeignKey("work_sessions.id"), nullable=False, unique=True) employee_phone: Mapped[str] = mapped_column(ForeignKey("personnel.phone"), nullable=False) report_date: Mapped[date] = mapped_column(Date, nullable=False) start_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) end_at: Mapped[datetime] = mapped_column(DateTime, nullable=False) duration_minutes: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False, default=0) break_minutes: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False, default=0) effective_minutes: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False, default=0) total_good_qty: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False, default=0) total_output_qty: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False, default=0) actual_beat: Mapped[float] = mapped_column(Numeric(12, 3), nullable=False, default=0) standard_beat: Mapped[float] = mapped_column(Numeric(12, 3), nullable=False, default=0) expected_workload: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False, default=0) pace_rate: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False, default=0) workload_rate: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False, default=0) status: Mapped[ReportStatus] = mapped_column( Enum(ReportStatus), nullable=False, default=ReportStatus.pending, ) reviewer_phone: Mapped[str | None] = mapped_column(ForeignKey("personnel.phone")) reviewed_at: Mapped[datetime | None] = mapped_column(DateTime) reject_reason: Mapped[str | None] = mapped_column(String(500)) review_remark: Mapped[str | None] = mapped_column(String(500)) submitted_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) is_system_auto_submitted: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) auto_submit_reason: Mapped[str | None] = mapped_column(String(120)) is_multi_person_assistant: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) multi_person_source_report_id: Mapped[int | None] = mapped_column(BigInteger) is_voided: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) voided_at: Mapped[datetime | None] = mapped_column(DateTime) voided_by: Mapped[str | None] = mapped_column(ForeignKey("personnel.phone")) unvoid_deadline_at: Mapped[datetime | None] = mapped_column(DateTime) updated_at: Mapped[datetime] = mapped_column(DateTime, default=now, onupdate=now, nullable=False) employee: Mapped[Personnel] = relationship(foreign_keys=[employee_phone]) reviewer: Mapped[Personnel | None] = relationship(foreign_keys=[reviewer_phone]) voider: Mapped[Personnel | None] = relationship(foreign_keys=[voided_by]) session: Mapped[WorkSession] = relationship() items: Mapped[list["ProductionReportItem"]] = relationship( back_populates="report", cascade="all, delete-orphan", ) audit_logs: Mapped[list["ReportAuditLog"]] = relationship( order_by="ReportAuditLog.created_at", ) __table_args__ = ( Index("idx_reports_employee_date", "employee_phone", "report_date"), Index("idx_reports_status_date", "status", "report_date"), Index("idx_reports_reviewer", "reviewer_phone", "reviewed_at"), Index("idx_reports_voided", "is_voided", "voided_at"), ) class ProductionReportItem(Base): __tablename__ = "production_report_items" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) report_id: Mapped[int] = mapped_column(ForeignKey("production_reports.id"), nullable=False) attendance_point_name: Mapped[str] = mapped_column(String(128), nullable=False, default="") device_no: Mapped[str] = mapped_column(String(255), nullable=False) project_no: Mapped[str] = mapped_column(String(64), nullable=False) product_name: Mapped[str] = mapped_column(String(255), nullable=False) material_code: Mapped[str | None] = mapped_column(String(128)) material_name: Mapped[str | None] = mapped_column(String(255)) raw_material_batch_no: Mapped[str | None] = mapped_column(String(128)) process_name: Mapped[str | None] = mapped_column(String(128)) stamping_method: Mapped[str | None] = mapped_column(String(128)) operator_count: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False, default=1) process_unit_price_yuan: Mapped[float] = mapped_column(Numeric(12, 4), nullable=False, default=0) changeover_count: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False, default=0) standard_beat: Mapped[float] = mapped_column(Numeric(12, 3), nullable=False, default=0) standard_workload: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False, default=0) good_qty: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False, default=0) defect_qty: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False, default=0) scrap_qty: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False, default=0) allocated_minutes: Mapped[float] = mapped_column(Numeric(10, 2), nullable=False, default=0) started_at: Mapped[datetime | None] = mapped_column(DateTime) remark: Mapped[str | None] = mapped_column(String(500)) created_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) report: Mapped[ProductionReport] = relationship(back_populates="items") __table_args__ = ( Index("idx_report_items_report", "report_id"), Index("idx_report_items_product", "project_no", "product_name"), ) class ReportAuditLog(Base): __tablename__ = "report_audit_logs" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) report_id: Mapped[int] = mapped_column(ForeignKey("production_reports.id"), nullable=False) reviewer_phone: Mapped[str] = mapped_column(ForeignKey("personnel.phone"), nullable=False) action: Mapped[AuditAction] = mapped_column(Enum(AuditAction), nullable=False) before_json: Mapped[dict | None] = mapped_column(JSON) after_json: Mapped[dict | None] = mapped_column(JSON) remark: Mapped[str | None] = mapped_column(String(500)) created_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) __table_args__ = ( Index("idx_audit_report", "report_id"), Index("idx_audit_reviewer", "reviewer_phone", "created_at"), ) class MoldLockFeedback(Base): __tablename__ = "mold_lock_feedbacks" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) attendance_point_name: Mapped[str] = mapped_column(String(128), nullable=False, default="") mold_name: Mapped[str] = mapped_column(String(255), nullable=False) process_name: Mapped[str] = mapped_column(String(128), nullable=False, default="") session_device_id: Mapped[int] = mapped_column(ForeignKey("work_session_devices.id"), nullable=False) reporter_phone: Mapped[str] = mapped_column(ForeignKey("personnel.phone"), nullable=False) occupied_phone: Mapped[str] = mapped_column(ForeignKey("personnel.phone"), nullable=False) status: Mapped[str] = mapped_column(String(32), nullable=False, default="pending") read_at: Mapped[datetime | None] = mapped_column(DateTime) handled_at: Mapped[datetime | None] = mapped_column(DateTime) handled_by: Mapped[str | None] = mapped_column(ForeignKey("personnel.phone")) created_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) session_device: Mapped[WorkSessionDevice] = relationship() reporter: Mapped[Personnel] = relationship(foreign_keys=[reporter_phone]) occupied_person: Mapped[Personnel] = relationship(foreign_keys=[occupied_phone]) handler: Mapped[Personnel | None] = relationship(foreign_keys=[handled_by]) __table_args__ = ( Index("idx_mold_lock_feedback_point_read", "attendance_point_name", "read_at", "created_at"), Index("idx_mold_lock_feedback_status", "status", "created_at"), ) class DeviceQRCode(Base): __tablename__ = "device_qrcodes" attendance_point_name: Mapped[str] = mapped_column(String(128), primary_key=True, default="") device_no: Mapped[str] = mapped_column(String(255), primary_key=True) process_name: Mapped[str] = mapped_column(String(128), primary_key=True, default="") qr_scene: Mapped[str] = mapped_column(String(128), nullable=False) qr_url: Mapped[str | None] = mapped_column(String(500)) created_by: Mapped[str] = mapped_column(ForeignKey("personnel.phone"), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) class DeviceQrBatchTask(Base): __tablename__ = "device_qrcode_batch_tasks" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) file_name: Mapped[str] = mapped_column(String(255), nullable=False) status: Mapped[str] = mapped_column(String(32), nullable=False, default="pending") item_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) completed_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) failed_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) items_json: Mapped[list] = mapped_column(JSON, nullable=False) zip_url: Mapped[str | None] = mapped_column(String(500)) error_message: Mapped[str | None] = mapped_column(String(1000)) created_by: Mapped[str] = mapped_column(ForeignKey("personnel.phone"), nullable=False) started_at: Mapped[datetime | None] = mapped_column(DateTime) finished_at: Mapped[datetime | None] = mapped_column(DateTime) created_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) updated_at: Mapped[datetime] = mapped_column(DateTime, default=now, onupdate=now, nullable=False) creator: Mapped[Personnel] = relationship() __table_args__ = ( Index("idx_device_qrcode_batch_tasks_creator", "created_by", "created_at"), Index("idx_device_qrcode_batch_tasks_status", "status", "created_at"), ) class Notice(Base): __tablename__ = "notices" id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) title: Mapped[str] = mapped_column(String(120), nullable=False) content: Mapped[str] = mapped_column(Text, nullable=False) sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) created_by: Mapped[str] = mapped_column(ForeignKey("personnel.phone"), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) updated_at: Mapped[datetime] = mapped_column(DateTime, default=now, onupdate=now, nullable=False) creator: Mapped[Personnel] = relationship() attendance_points: Mapped[list["NoticePoint"]] = relationship( back_populates="notice", cascade="all, delete-orphan", ) __table_args__ = ( Index("idx_notices_active_sort", "is_active", "sort_order"), Index("idx_notices_updated", "updated_at"), ) class NoticePoint(Base): __tablename__ = "notice_points" notice_id: Mapped[int] = mapped_column(ForeignKey("notices.id"), primary_key=True) attendance_point_name: Mapped[str] = mapped_column( ForeignKey("attendance_points.name"), primary_key=True, ) created_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) notice: Mapped[Notice] = relationship(back_populates="attendance_points") attendance_point: Mapped[AttendancePoint] = relationship() class ReconciliationLedgerEntry(Base): __tablename__ = "reconciliation_ledger_entries" attendance_point_name: Mapped[str] = mapped_column(String(128), primary_key=True, default="") year: Mapped[int] = mapped_column(Integer, primary_key=True) month: Mapped[int] = mapped_column(Integer, primary_key=True) product_name: Mapped[str] = mapped_column(String(255), primary_key=True) reconciled_good_qty: Mapped[float] = mapped_column(Numeric(14, 2), nullable=False, default=0) return_qty: Mapped[float] = mapped_column(Numeric(14, 2), nullable=False, default=0) updated_by: Mapped[str | None] = mapped_column(String(20)) created_at: Mapped[datetime] = mapped_column(DateTime, default=now, nullable=False) updated_at: Mapped[datetime] = mapped_column(DateTime, default=now, onupdate=now, nullable=False) __table_args__ = ( Index("idx_reconciliation_product", "product_name"), Index("idx_reconciliation_updated", "updated_at"), )