from __future__ import annotations import uuid from datetime import date, datetime from decimal import Decimal from typing import Any from sqlalchemy import Date, DateTime, ForeignKey, Integer, Numeric, String, Text, func from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.types import JSON from app.db.base_class import Base class ExpenseClaim(Base): __tablename__ = "expense_claims" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) claim_no: Mapped[str] = mapped_column(String(50), unique=True, index=True) employee_id: Mapped[str | None] = mapped_column( ForeignKey("employees.id"), nullable=True, index=True ) employee_name: Mapped[str] = mapped_column(String(100), index=True) department_id: Mapped[str | None] = mapped_column( ForeignKey("organization_units.id"), nullable=True, index=True ) department_name: Mapped[str] = mapped_column(String(100), index=True) project_code: Mapped[str | None] = mapped_column(String(50), nullable=True) expense_type: Mapped[str] = mapped_column(String(50), index=True) reason: Mapped[str] = mapped_column(Text()) location: Mapped[str] = mapped_column(String(100)) amount: Mapped[Decimal] = mapped_column(Numeric(12, 2)) currency: Mapped[str] = mapped_column(String(10), default="CNY") invoice_count: Mapped[int] = mapped_column(Integer, default=0) occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True) submitted_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, index=True ) status: Mapped[str] = mapped_column(String(30), index=True) approval_stage: Mapped[str | None] = mapped_column(String(50), nullable=True) risk_flags_json: Mapped[list[Any]] = mapped_column(JSON, default=list) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) employee = relationship("Employee", foreign_keys=[employee_id]) items = relationship( "ExpenseClaimItem", back_populates="claim", cascade="all, delete-orphan", order_by="asc(ExpenseClaimItem.item_date)", ) @property def employee_position(self) -> str | None: return str(self.employee.position).strip() if self.employee is not None and self.employee.position else None @property def employee_grade(self) -> str | None: return str(self.employee.grade).strip() if self.employee is not None and self.employee.grade else None @property def manager_name(self) -> str | None: if self.employee is None: return None if self.employee.manager is not None and self.employee.manager.name: return str(self.employee.manager.name).strip() or None return None @property def role_labels(self) -> list[str]: if self.employee is None or not self.employee.roles: return [] return [str(role.name).strip() for role in sorted(self.employee.roles, key=lambda item: item.name) if role.name] class ExpenseClaimItem(Base): __tablename__ = "expense_claim_items" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) claim_id: Mapped[str] = mapped_column(ForeignKey("expense_claims.id"), index=True) item_date: Mapped[date] = mapped_column(Date(), index=True) item_type: Mapped[str] = mapped_column(String(50)) item_reason: Mapped[str] = mapped_column(Text()) item_location: Mapped[str] = mapped_column(String(100)) item_amount: Mapped[Decimal] = mapped_column(Numeric(12, 2)) invoice_id: Mapped[str | None] = mapped_column(String(100), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) claim = relationship("ExpenseClaim", back_populates="items") @property def is_system_generated(self) -> bool: return str(self.item_type or "").strip().lower() in {"travel_allowance"} class AccountsReceivableRecord(Base): __tablename__ = "accounts_receivable" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) receivable_no: Mapped[str] = mapped_column(String(50), unique=True, index=True) customer_id: Mapped[str] = mapped_column(String(64), index=True) customer_name: Mapped[str] = mapped_column(String(120), index=True) contract_no: Mapped[str | None] = mapped_column(String(100), nullable=True) invoice_no: Mapped[str | None] = mapped_column(String(100), nullable=True) amount_receivable: Mapped[Decimal] = mapped_column(Numeric(12, 2)) amount_received: Mapped[Decimal] = mapped_column(Numeric(12, 2)) amount_outstanding: Mapped[Decimal] = mapped_column(Numeric(12, 2)) currency: Mapped[str] = mapped_column(String(10), default="CNY") posting_date: Mapped[date] = mapped_column(Date(), index=True) due_date: Mapped[date] = mapped_column(Date(), index=True) aging_days: Mapped[int] = mapped_column(Integer, default=0) status: Mapped[str] = mapped_column(String(30), index=True) risk_flags_json: Mapped[list[Any]] = mapped_column(JSON, default=list) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() ) class AccountsPayableRecord(Base): __tablename__ = "accounts_payable" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) payable_no: Mapped[str] = mapped_column(String(50), unique=True, index=True) vendor_id: Mapped[str] = mapped_column(String(64), index=True) vendor_name: Mapped[str] = mapped_column(String(120), index=True) invoice_no: Mapped[str | None] = mapped_column(String(100), nullable=True) amount_payable: Mapped[Decimal] = mapped_column(Numeric(12, 2)) amount_paid: Mapped[Decimal] = mapped_column(Numeric(12, 2)) amount_outstanding: Mapped[Decimal] = mapped_column(Numeric(12, 2)) currency: Mapped[str] = mapped_column(String(10), default="CNY") posting_date: Mapped[date] = mapped_column(Date(), index=True) due_date: Mapped[date] = mapped_column(Date(), index=True) aging_days: Mapped[int] = mapped_column(Integer, default=0) status: Mapped[str] = mapped_column(String(30), index=True) risk_flags_json: Mapped[list[Any]] = mapped_column(JSON, default=list) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), onupdate=func.now() )