"""回款模型:回款记录与批次追溯。 - shipment_id 为**手动指定**的发货批次(对照表 #18):可空=暂未指定; 必须与 business_id 同属一个业务(服务层强校验,无数据库级约束可表达 跨表同属关系);指定后回款天数方可由 formula.payment_days 派生 - settlement_status 是 **M3 核算防重锚点**(需规 2.3.3「每笔回款仅核算一次, 台账记录核销状态」):预置于本表避免 M3 反向迁移改 M2 表;M3 台账建立后 由核算服务写入,M2 仅维护 0(未核算) - receipt_date 是 M3 参数版本匹配日(对照表 #11:按回款到账日取时点参数) - 一期禁负数金额(≥0):红字/退款冲销需求出现时先补设计再放开 """ from datetime import date from decimal import Decimal from sqlalchemy import Date, ForeignKey, Index, Numeric, String from sqlalchemy.orm import Mapped, mapped_column from app.models.base import ( BIGINT_UNSIGNED, TINYINT_TYPE, Base, TimestampMixin, TPlusSyncMixin, mysql_table_args, ) # 绩效核算状态(receipts.settlement_status) SETTLEMENT_PENDING = 0 SETTLEMENT_DONE = 1 class Receipt(Base, TimestampMixin, TPlusSyncMixin): """回款表(需求 2.2.3):按业务登记到账款项,批次号系统内唯一。""" __tablename__ = "receipts" __table_args__ = mysql_table_args( "回款表(手动指定发货批次;已核算回款禁改禁删)", Index("idx_receipts_business_id", "business_id"), Index("idx_receipts_receipt_date", "receipt_date"), Index("idx_receipts_settlement_status", "settlement_status"), Index("idx_receipts_shipment_id", "shipment_id"), ) id: Mapped[int] = mapped_column( BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID" ) receipt_batch_no: Mapped[str] = mapped_column( String(32), nullable=False, unique=True, comment="回款批次号,自动生成 HK{YYYYMMDD}-{4位当日序号}(系统内唯一;T+ 同步回款以 source_no 幂等)", ) business_id: Mapped[int] = mapped_column( BIGINT_UNSIGNED, ForeignKey("businesses.id", ondelete="RESTRICT"), nullable=False, comment="业务ID(businesses.id)", ) shipment_id: Mapped[int | None] = mapped_column( BIGINT_UNSIGNED, ForeignKey("shipments.id", ondelete="RESTRICT"), comment="发货单ID(shipments.id;手动指定的回款对应批次,须与本行业务一致;NULL=暂未指定)", ) receipt_date: Mapped[date] = mapped_column( Date, nullable=False, comment="回款到账日(M3 按此日匹配参数版本,对照表 #11)" ) amount: Mapped[Decimal] = mapped_column( Numeric(14, 2), nullable=False, comment="回款金额(≥0;一期禁负数/红字)" ) is_barter: Mapped[int] = mapped_column( TINYINT_TYPE, nullable=False, server_default="0", comment="以物抵顶标记:1是 0否(M3 核算按应发绩效×barter.discount 折算,M2 仅标记)", ) settlement_status: Mapped[int] = mapped_column( TINYINT_TYPE, nullable=False, server_default="0", comment="绩效核算状态:0未核算 1已核算(M3 核算防重锚点;已核算回款禁改禁删)", ) remark: Mapped[str | None] = mapped_column(String(255), comment="备注")