""" 發票記錄 Model 管理租戶的帳單和發票 """ from datetime import datetime, date from sqlalchemy import Column, Integer, String, Date, DateTime, Numeric, ForeignKey, Text from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import relationship import enum from app.db.base import Base class InvoiceStatus(str, enum.Enum): """發票狀態""" PENDING = "pending" # 待付款 PAID = "paid" # 已付款 OVERDUE = "overdue" # 逾期未付 CANCELLED = "cancelled" # 已取消 class Invoice(Base): """發票記錄表""" __tablename__ = "invoices" id = Column(Integer, primary_key=True, index=True) tenant_id = Column(Integer, ForeignKey("organizes.id", ondelete="CASCADE"), nullable=False, index=True) # 發票資訊 invoice_number = Column(String(50), unique=True, nullable=False, index=True, comment="發票號碼 (INV-2026-03-001)") issue_date = Column(Date, nullable=False, comment="開立日期") due_date = Column(Date, nullable=False, comment="到期日") # 金額 amount = Column(Numeric(10, 2), nullable=False, comment="金額 (未稅)") tax = Column(Numeric(10, 2), default=0, nullable=False, comment="稅額") total = Column(Numeric(10, 2), nullable=False, comment="總計 (含稅)") # 狀態 status = Column(String(20), default=InvoiceStatus.PENDING, nullable=False, comment="狀態") # 付款資訊 paid_at = Column(DateTime, nullable=True, comment="付款時間") payment_method = Column(String(20), nullable=True, comment="付款方式 (credit_card/wire_transfer)") # 發票明細 (JSON 格式) line_items = Column(JSONB, nullable=True, comment="發票明細") # 範例: # [ # {"description": "標準方案 (20 人)", "quantity": 1, "unit_price": 10000, "amount": 10000}, # {"description": "超額用戶 (2 人)", "quantity": 2, "unit_price": 500, "amount": 1000} # ] # PDF 檔案 pdf_path = Column(String(200), nullable=True, comment="發票 PDF 路徑") # 備註 notes = Column(Text, nullable=True, comment="備註") # 時間記錄 created_at = Column(DateTime, default=datetime.utcnow, nullable=False) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) # 關聯 tenant = relationship("Tenant") payments = relationship( "Payment", back_populates="invoice", cascade="all, delete-orphan", lazy="selectin" ) def __repr__(self): return f"" @property def is_paid(self) -> bool: """是否已付款""" return self.status == InvoiceStatus.PAID @property def is_overdue(self) -> bool: """是否逾期""" return ( self.status in [InvoiceStatus.PENDING, InvoiceStatus.OVERDUE] and date.today() > self.due_date ) @property def days_overdue(self) -> int: """逾期天數""" if not self.is_overdue: return 0 return (date.today() - self.due_date).days def mark_as_paid(self, payment_method: str = None): """標記為已付款""" self.status = InvoiceStatus.PAID self.paid_at = datetime.utcnow() if payment_method: self.payment_method = payment_method @classmethod def generate_invoice_number(cls, year: int, month: int, sequence: int) -> str: """生成發票號碼""" return f"INV-{year:04d}-{month:02d}-{sequence:03d}"