2026-05-11 03:51:24 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import uuid
|
|
|
|
|
from datetime import date, datetime
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
2026-05-24 21:44:17 +08:00
|
|
|
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, Numeric, String, Text, func
|
2026-05-11 03:51:24 +00:00
|
|
|
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)
|
2026-05-24 21:44:17 +08:00
|
|
|
hermes_scanned_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
|
|
|
hermes_risk_flag: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
|
2026-05-11 03:51:24 +00:00
|
|
|
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()
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-13 06:54:27 +00:00
|
|
|
employee = relationship("Employee", foreign_keys=[employee_id])
|
2026-05-11 03:51:24 +00:00
|
|
|
items = relationship(
|
|
|
|
|
"ExpenseClaimItem",
|
|
|
|
|
back_populates="claim",
|
|
|
|
|
cascade="all, delete-orphan",
|
|
|
|
|
order_by="asc(ExpenseClaimItem.item_date)",
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-13 06:54:27 +00:00
|
|
|
@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]
|
|
|
|
|
|
2026-05-11 03:51:24 +00:00
|
|
|
|
|
|
|
|
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))
|
2026-06-03 15:46:56 +08:00
|
|
|
item_note: Mapped[str] = mapped_column(Text(), default="")
|
2026-05-11 03:51:24 +00:00
|
|
|
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")
|
|
|
|
|
|
2026-05-21 09:28:33 +08:00
|
|
|
@property
|
|
|
|
|
def is_system_generated(self) -> bool:
|
|
|
|
|
return str(self.item_type or "").strip().lower() in {"travel_allowance"}
|
|
|
|
|
|
2026-05-11 03:51:24 +00:00
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
)
|