- businesses 五项汇总冗余列服务层事务内维护;毛利润不落库(派生值,M3 台账固化) - 行级 product_category 折算锚点(#14);回款 settlement_status 预置为 M3 防重锚点 - 评级等级种子四条(迁移+init-dev 双写);编号前缀常量 YW/SH/HK - 双轨 SQL 同步(V4/V5 重导,schema_sync+test_models 164 例全绿) Co-Authored-By: Claude Code <noreply@anthropic.com>
219 lines
9.8 KiB
Python
219 lines
9.8 KiB
Python
"""业务数据模型:业务单、产品明细与状态流转留痕。
|
||
|
||
- 汇总冗余列(total_* 五列)由服务层在明细/发货/回款增删改时**事务内单点
|
||
重算**,禁手工直改 —— 列表页与 M3 起付判定直接读冗余列,避免每次 JOIN
|
||
聚合;代价是所有写路径必须重算(business_service 统一收口)
|
||
- 毛利润相关值(毛利润额/毛利润率/折算毛利润额)**不落库**:它们是
|
||
sale_amount/cost_amount + 时点化阈值参数的派生值,落库会在成本调整或
|
||
参数改版后漂移出「假快照」;M2 以只读毛利润视图实时计算(复用
|
||
formula.profit_rate/converted_profit),M3 核算台账才是法定固化点
|
||
- product_category 存在明细行级:混合业务按产品逐条计算(对照表 #14)
|
||
- 运费仅记录展示,不计入毛利润口径(对照表 #2);协助营销人员一期核算
|
||
不使用(对照表 #15 剔除协助 25% 分成),字段保留仅记录事实(需规 2.2.1)
|
||
"""
|
||
|
||
from datetime import date, datetime
|
||
from decimal import Decimal
|
||
|
||
from sqlalchemy import Date, DateTime, ForeignKey, Index, Numeric, String, text
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from app.models.base import (
|
||
BIGINT_UNSIGNED,
|
||
TINYINT_TYPE,
|
||
Base,
|
||
TimestampMixin,
|
||
TPlusSyncMixin,
|
||
mysql_table_args,
|
||
)
|
||
|
||
# 业务状态(businesses.status,流转规则见 business_service:终态不可变更)
|
||
BUSINESS_STATUS_ACTIVE = 1
|
||
BUSINESS_STATUS_DONE = 2
|
||
BUSINESS_STATUS_TERMINATED = 3
|
||
|
||
|
||
class Business(Base, TimestampMixin, TPlusSyncMixin):
|
||
"""业务表(需求 2.2.1):业务编号自动生成,即报表 2.4.1 的「合同编号」。"""
|
||
|
||
__tablename__ = "businesses"
|
||
__table_args__ = mysql_table_args(
|
||
"业务表(业务编号自动生成;T+同步不覆盖业务状态)",
|
||
Index("idx_businesses_customer_id", "customer_id"),
|
||
Index("idx_businesses_owner_personnel_id", "owner_personnel_id"),
|
||
Index("idx_businesses_status", "status"),
|
||
Index("idx_businesses_business_date", "business_date"),
|
||
)
|
||
|
||
id: Mapped[int] = mapped_column(
|
||
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||
)
|
||
business_no: Mapped[str] = mapped_column(
|
||
String(32),
|
||
nullable=False,
|
||
unique=True,
|
||
comment="业务编号,自动生成 YW{YYYYMMDD}-{4位当日序号}(报表合同编号)",
|
||
)
|
||
customer_id: Mapped[int] = mapped_column(
|
||
BIGINT_UNSIGNED,
|
||
ForeignKey("customers.id", ondelete="RESTRICT"),
|
||
nullable=False,
|
||
comment="客户ID(customers.id)",
|
||
)
|
||
business_date: Mapped[date | None] = mapped_column(Date, comment="业务日期(T+ 同步口径)")
|
||
status: Mapped[int] = mapped_column(
|
||
TINYINT_TYPE,
|
||
nullable=False,
|
||
server_default="1",
|
||
comment="业务状态:1执行中 2已完成 3已终止(终态不可变更;T+同步不覆盖)",
|
||
)
|
||
owner_personnel_id: Mapped[int | None] = mapped_column(
|
||
BIGINT_UNSIGNED,
|
||
ForeignKey("personnel.id", ondelete="RESTRICT"),
|
||
comment="负责营销人员ID(核算归属锚点;NULL=待指定,M3 核算前置校验非空;变更留痕不回溯)",
|
||
)
|
||
assistant_personnel_id: Mapped[int | None] = mapped_column(
|
||
BIGINT_UNSIGNED,
|
||
ForeignKey("personnel.id", ondelete="RESTRICT"),
|
||
comment="协助营销人员ID(一期核算不使用,对照表 #15;仅记录事实)",
|
||
)
|
||
is_special: Mapped[int] = mapped_column(
|
||
TINYINT_TYPE,
|
||
nullable=False,
|
||
server_default="0",
|
||
comment="特殊业务标记:1是 0否(公司参与等)",
|
||
)
|
||
special_note: Mapped[str | None] = mapped_column(String(255), comment="特殊业务说明")
|
||
total_sale_amount: Mapped[Decimal | None] = mapped_column(
|
||
Numeric(14, 2),
|
||
server_default="0",
|
||
comment="销售额合计(明细行汇总冗余,服务层事务内维护,禁手工直改;不含运费口径)",
|
||
)
|
||
total_cost_amount: Mapped[Decimal | None] = mapped_column(
|
||
Numeric(14, 2),
|
||
server_default="0",
|
||
comment="成本合计(明细行汇总冗余,服务层事务内维护,禁手工直改)",
|
||
)
|
||
total_ship_amount: Mapped[Decimal | None] = mapped_column(
|
||
Numeric(14, 2),
|
||
server_default="0",
|
||
comment="发货金额合计(发货明细汇总冗余,服务层事务内维护,禁手工直改)",
|
||
)
|
||
total_received_amount: Mapped[Decimal | None] = mapped_column(
|
||
Numeric(14, 2),
|
||
server_default="0",
|
||
comment="回款合计(回款记录汇总冗余,服务层事务内维护;M3 起付判定直接消费)",
|
||
)
|
||
total_freight_amount: Mapped[Decimal | None] = mapped_column(
|
||
Numeric(14, 2),
|
||
server_default="0",
|
||
comment="运费合计(发货单汇总冗余,仅记录展示;不计入毛利润口径,对照表 #2)",
|
||
)
|
||
remark: Mapped[str | None] = mapped_column(String(255), comment="备注")
|
||
|
||
|
||
class BusinessItem(Base, TimestampMixin):
|
||
"""业务产品明细表(需求 2.2.1/2.2.4):毛利润的行级源数据。
|
||
|
||
product_id 可空=自由录入产品(配合「数据提取」一键入产品库);
|
||
产品名称/规格冗余固化,防产品改名影响历史业务。
|
||
sale_amount 由服务层按 数量×单价 计算固化,不信任前端传值。
|
||
cost_amount 来源待 T+ 对接确认(可空,人工可录先行)。
|
||
"""
|
||
|
||
__tablename__ = "business_items"
|
||
__table_args__ = mysql_table_args(
|
||
"业务产品明细表(毛利润行级源数据;指导价 T+同步不覆盖)",
|
||
Index("idx_business_items_business_id", "business_id"),
|
||
Index("idx_business_items_product_id", "product_id"),
|
||
Index("idx_business_items_product_category", "product_category"),
|
||
)
|
||
|
||
id: Mapped[int] = mapped_column(
|
||
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||
)
|
||
business_id: Mapped[int] = mapped_column(
|
||
BIGINT_UNSIGNED,
|
||
ForeignKey("businesses.id", ondelete="RESTRICT"),
|
||
nullable=False,
|
||
comment="业务ID(businesses.id)",
|
||
)
|
||
product_id: Mapped[int | None] = mapped_column(
|
||
BIGINT_UNSIGNED,
|
||
ForeignKey("products.id", ondelete="RESTRICT"),
|
||
comment="产品ID(products.id;NULL=自由录入,未入产品库)",
|
||
)
|
||
product_name: Mapped[str] = mapped_column(
|
||
String(128), nullable=False, comment="产品名称(引用产品时冗余固化)"
|
||
)
|
||
spec_model: Mapped[str] = mapped_column(
|
||
String(128),
|
||
nullable=False,
|
||
server_default="''",
|
||
comment="规格型号(冗余固化;空串对齐 products 组合唯一键口径)",
|
||
)
|
||
unit: Mapped[str | None] = mapped_column(String(16), comment="计量单位")
|
||
product_category: Mapped[str] = mapped_column(
|
||
String(32),
|
||
nullable=False,
|
||
comment="产品类别(行级折算锚点,混合业务按产品逐条计算,对照表 #14;值域同 PRODUCT_CATEGORIES)",
|
||
)
|
||
quantity: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, comment="数量(>0)")
|
||
sale_price: Mapped[Decimal | None] = mapped_column(
|
||
Numeric(14, 4), comment="销售单价(元;T+ 同步或人工)"
|
||
)
|
||
sale_amount: Mapped[Decimal] = mapped_column(
|
||
Numeric(14, 2),
|
||
nullable=False,
|
||
comment="销售额 = 数量×销售单价(服务层计算固化;不含运费口径,毛利润率分母)",
|
||
)
|
||
cost_amount: Mapped[Decimal | None] = mapped_column(
|
||
Numeric(14, 2),
|
||
comment="成本额(T+ 同步或人工录入,来源待 T+ 确认;毛利润额 = 销售额 − 成本额)",
|
||
)
|
||
guided_price: Mapped[Decimal | None] = mapped_column(
|
||
Numeric(14, 4), comment="指导价(人工录入;T+同步不覆盖,需规 2.2.1/2.2.4)"
|
||
)
|
||
remark: Mapped[str | None] = mapped_column(String(255), comment="备注")
|
||
|
||
|
||
class BusinessStatusLog(Base):
|
||
"""业务状态变更记录表(append-only,需求 2.2.1「支持状态变更记录」)。
|
||
|
||
不继承 TimestampMixin(留痕无更新语义);business_id 为无外键的固化 ID
|
||
(businesses 建表在本表之后的迁移批次内 —— 实为同批,但留痕统一口径:
|
||
被引用方删除后记录仍完整可读,删除保护由服务层检查承担)。
|
||
首条记录在业务创建时写入(old=new=1 执行中),详情页时间线从创建起展示。
|
||
"""
|
||
|
||
__tablename__ = "business_status_logs"
|
||
__table_args__ = mysql_table_args(
|
||
"业务状态变更记录表(append-only)",
|
||
Index("idx_business_status_logs_business_id", "business_id"),
|
||
Index("idx_business_status_logs_create_time", "create_time"),
|
||
)
|
||
|
||
id: Mapped[int] = mapped_column(
|
||
BIGINT_UNSIGNED, primary_key=True, autoincrement=True, comment="主键ID"
|
||
)
|
||
business_id: Mapped[int] = mapped_column(
|
||
BIGINT_UNSIGNED,
|
||
comment="业务ID(固化,无外键;删除保护由服务层承担)",
|
||
)
|
||
old_status: Mapped[int] = mapped_column(
|
||
TINYINT_TYPE, nullable=False, comment="变更前状态(创建首条为 1)"
|
||
)
|
||
new_status: Mapped[int] = mapped_column(TINYINT_TYPE, nullable=False, comment="变更后状态")
|
||
change_reason: Mapped[str | None] = mapped_column(String(255), comment="变更原因(终止时必填)")
|
||
operator_id: Mapped[int | None] = mapped_column(
|
||
BIGINT_UNSIGNED,
|
||
ForeignKey("users.id", ondelete="SET NULL"),
|
||
comment="操作人ID(账号删除后置空)",
|
||
)
|
||
operator_name: Mapped[str] = mapped_column(
|
||
String(64), nullable=False, comment="操作人用户名(冗余固化)"
|
||
)
|
||
create_time: Mapped[datetime] = mapped_column(
|
||
DateTime, nullable=False, server_default=text("CURRENT_TIMESTAMP"), comment="创建时间"
|
||
)
|