v0.2.0: CRM/ERP 系统升级 - 清理 .gitignore 并移除误提交的 venv/env/db 文件

- 更新 .gitignore:全面覆盖环境变量、数据库、日志、缓存、上传文件
- 移除误跟踪的 server/venv/、crm_data.db、.env 文件
- 新增 server/.env.example 模板
- 新增合同管理、利润核算、AI教练等功能模块
- 新增 Playwright e2e 测试套件
- 前后端多项功能升级和 bug 修复
This commit is contained in:
hankin
2026-05-11 07:24:19 +00:00
parent 0f4c6b7924
commit 815cbf9d8c
2526 changed files with 11875 additions and 804148 deletions
+26 -1
View File
@@ -7,7 +7,7 @@ import uuid
from datetime import date, datetime
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, SmallInteger, String, Text, func
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.dialects.postgresql import UUID, JSONB, ARRAY
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base
@@ -30,11 +30,19 @@ class SalesLog(Base):
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
salesperson_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=False)
involved_company_ids: Mapped[list] = mapped_column(
ARRAY(UUID(as_uuid=True)), nullable=False, default=list,
comment="该篇日志涉及的公司ID列表"
)
customer_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("crm_customers.id"), nullable=True)
content: Mapped[str] = mapped_column(Text, nullable=False)
log_date: Mapped[date] = mapped_column(Date, default=date.today)
contact_ids: Mapped[list | None] = mapped_column(JSONB, default=list, nullable=True)
ai_processed: Mapped[bool] = mapped_column(Boolean, default=False)
ai_coaching_feedback: Mapped[dict | None] = mapped_column(
JSONB, default=dict, nullable=True,
comment="AI 教练引擎回写的指导反馈"
)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
@@ -53,3 +61,20 @@ class AiReportDraft(Base):
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
class KbObsidianVector(Base):
"""知识库向量表 —— pgvector 存储 Obsidian 文档分块向量"""
__tablename__ = "kb_obsidian_vectors"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
company_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_companies.id"), nullable=False, index=True
)
source_path: Mapped[str] = mapped_column(String(500), nullable=False, comment="源文件路径")
chunk_index: Mapped[int] = mapped_column(SmallInteger, default=0)
content: Mapped[str] = mapped_column(Text, nullable=False)
metadata_: Mapped[dict | None] = mapped_column("metadata", JSONB, default=dict)
# 向量字段使用 raw SQL 创建(vector(1536))因 SQLAlchemy 无原生 pgvector 类型
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
+171
View File
@@ -0,0 +1,171 @@
"""
合同域 ORM 模型
映射: erp_contracts / erp_contract_items / erp_contract_attachments
"""
from __future__ import annotations
import uuid
from datetime import date, datetime
from sqlalchemy import (
Boolean,
Date,
DateTime,
ForeignKey,
Numeric,
String,
Text,
func,
)
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
# ── 付款条件枚举 ─────────────────────────────────────────
PAYMENT_TERMS = [
"预付全款订货",
"预付30%订货,到货前付清",
"预付50%订货,到货前付清",
"货到付全款",
"开具发票后30天内付款",
"开具发票45天付款",
"开具发票60天付款",
"开具发票90天付款",
]
# ── 运费条款枚举 ─────────────────────────────────────────
SHIPPING_TERMS = [
"买方自提",
"卖方免费送达天津指定地点",
"卖方免费送达指定地点",
"物流发货,运费买方承担",
]
class ErpContract(Base):
"""合同主表 —— B2B 交易防线核心"""
__tablename__ = "erp_contracts"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
contract_no: Mapped[str] = mapped_column(String(30), unique=True, nullable=False)
buyer_customer_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("crm_customers.id"), nullable=False,
comment="买方(CRM 客户)"
)
seller_company_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_companies.id"), nullable=False,
comment="卖方(当前操作公司)"
)
company_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_companies.id"), nullable=False, index=True,
comment="多租户隔离"
)
total_amount_excl_tax: Mapped[float] = mapped_column(Numeric(14, 2), default=0)
total_amount_incl_tax: Mapped[float] = mapped_column(Numeric(14, 2), default=0)
total_amount_cn: Mapped[str | None] = mapped_column(
String(100), nullable=True, comment="大写合计金额"
)
payment_terms: Mapped[str] = mapped_column(
String(50), nullable=False, default="货到付全款"
)
shipping_terms: Mapped[str] = mapped_column(
String(50), nullable=False, default="买方自提"
)
status: Mapped[str] = mapped_column(
String(20), nullable=False, default="draft",
comment="draft→active→completed→cancelled"
)
is_signed: Mapped[bool] = mapped_column(Boolean, default=False)
signed_file_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
linked_order_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_orders.id"), nullable=True,
comment="一键推单后回填"
)
salesperson_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=True
)
sign_date: Mapped[date | None] = mapped_column(Date, nullable=True)
remark: Mapped[str | None] = mapped_column(Text, nullable=True)
delivery_terms: Mapped[str | None] = mapped_column(
String(200), nullable=True, comment="货期(手动输入)"
)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
# 关系
buyer_customer: Mapped["CrmCustomer"] = relationship( # noqa: F821
"CrmCustomer", lazy="selectin"
)
seller_company: Mapped["SysCompany"] = relationship( # noqa: F821
"SysCompany", foreign_keys=[seller_company_id], lazy="selectin"
)
salesperson: Mapped["SysUser | None"] = relationship("SysUser", foreign_keys=[salesperson_id], lazy="selectin") # noqa: F821
linked_order: Mapped["ErpOrder | None"] = relationship("ErpOrder", foreign_keys=[linked_order_id], lazy="selectin") # noqa: F821
items: Mapped[list["ErpContractItem"]] = relationship(
"ErpContractItem", back_populates="contract", lazy="selectin"
)
attachments: Mapped[list["ErpContractAttachment"]] = relationship(
"ErpContractAttachment", back_populates="contract", lazy="selectin"
)
class ErpContractItem(Base):
"""合同明细行"""
__tablename__ = "erp_contract_items"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
contract_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_contracts.id"), nullable=False
)
sku_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_product_skus.id"), nullable=False
)
qty: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False)
unit_price: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False)
sub_total: Mapped[float] = mapped_column(Numeric(14, 2), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
# 关系
contract: Mapped[ErpContract] = relationship("ErpContract", back_populates="items")
sku: Mapped["ProductSku"] = relationship("ProductSku", lazy="selectin") # noqa: F821
class ErpContractAttachment(Base):
"""合同附件(双签盖章版等)"""
__tablename__ = "erp_contract_attachments"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
contract_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_contracts.id"), nullable=False
)
file_name: Mapped[str] = mapped_column(String(200), nullable=False)
file_url: Mapped[str] = mapped_column(String(500), nullable=False)
file_type: Mapped[str] = mapped_column(
String(30), nullable=False, default="signed_copy",
comment="signed_copy / supplement / other"
)
uploader_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=True
)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
# 关系
contract: Mapped[ErpContract] = relationship("ErpContract", back_populates="attachments")
uploader: Mapped["SysUser | None"] = relationship("SysUser", lazy="selectin") # noqa: F821
+41
View File
@@ -0,0 +1,41 @@
"""
成本域 ORM 模型
映射: erp_order_item_costs
"""
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Numeric, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
class ErpOrderItemCost(Base):
"""订单明细成本快照表 —— 发货/确认瞬间锚定 MWA 成本"""
__tablename__ = "erp_order_item_costs"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
order_item_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_order_items.id"), nullable=False, unique=True,
comment="关联订单明细"
)
purchase_unit_price: Mapped[float] = mapped_column(
Numeric(12, 4), nullable=False, comment="MWA 成本快照"
)
profit_amount: Mapped[float] = mapped_column(
Numeric(14, 2), default=0, comment="利润额 = (售价-成本)*数量"
)
profit_rate: Mapped[float] = mapped_column(
Numeric(5, 4), default=0, comment="利润率"
)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
# 关系
order_item: Mapped["ErpOrderItem"] = relationship("ErpOrderItem", lazy="selectin") # noqa: F821
+12
View File
@@ -29,6 +29,18 @@ class CrmCustomer(Base):
address: Mapped[str | None] = mapped_column(Text, nullable=True)
ai_score: Mapped[float] = mapped_column(Numeric(5, 2), default=0)
ai_persona: Mapped[dict | None] = mapped_column(JSONB, default=dict, nullable=True)
billing_info: Mapped[dict | None] = mapped_column(
JSONB, default=dict, nullable=True,
comment="客户开票信息: company_name/tax_id/address/phone/bank_name/bank_account"
)
health_score: Mapped[float] = mapped_column(
Numeric(5, 2), default=0,
comment="客户健康度评分 (AI 教练引擎计算)"
)
meddic_status: Mapped[dict | None] = mapped_column(
JSONB, default=dict, nullable=True,
comment="MEDDIC 六维评估状态"
)
owner_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=True
)
+42 -2
View File
@@ -12,11 +12,13 @@ from sqlalchemy import (
Boolean,
DateTime,
ForeignKey,
Index,
Integer,
Numeric,
SmallInteger,
String,
Text,
UniqueConstraint,
func,
)
from sqlalchemy.dialects.postgresql import UUID
@@ -56,8 +58,6 @@ class ProductSku(Base):
name: Mapped[str] = mapped_column(String(200), nullable=False)
spec: Mapped[str | None] = mapped_column(String(100), nullable=True)
standard_price: Mapped[float] = mapped_column(Numeric(12, 2), default=0)
stock_qty: Mapped[float] = mapped_column(Numeric(12, 2), default=0)
warning_threshold: Mapped[float] = mapped_column(Numeric(12, 2), default=0)
unit: Mapped[str] = mapped_column(String(20), default="")
status: Mapped[int] = mapped_column(SmallInteger, default=1)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
@@ -80,9 +80,18 @@ class InventoryFlow(Base):
sku_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_product_skus.id"), nullable=False
)
company_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_companies.id"), nullable=False, index=True
)
change_qty: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False)
reason: Mapped[str] = mapped_column(String(50), nullable=False)
remark: Mapped[str | None] = mapped_column(Text, nullable=True)
purchase_unit_price: Mapped[float] = mapped_column(
Numeric(12, 2), default=0, comment="入库采购单价"
)
is_special_zero_cost: Mapped[bool] = mapped_column(
Boolean, default=False, comment="特殊零元入库标识,不参与 MWA 计算"
)
operator_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=True
)
@@ -94,3 +103,34 @@ class InventoryFlow(Base):
sku: Mapped[ProductSku | None] = relationship("ProductSku", lazy="selectin")
operator: Mapped["SysUser | None"] = relationship("SysUser", lazy="selectin") # noqa: F821
class ErpSkuInventory(Base):
"""SKU 分公司库存表 —— 同一 SKU 在不同公司有独立库存"""
__tablename__ = "erp_sku_inventory"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
sku_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_product_skus.id"), nullable=False
)
company_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_companies.id"), nullable=False, index=True
)
stock_qty: Mapped[float] = mapped_column(Numeric(12, 2), default=0)
warning_threshold: Mapped[float] = mapped_column(Numeric(12, 2), default=0)
mwa_unit_cost: Mapped[float] = mapped_column(
Numeric(12, 4), default=0,
comment="移动加权均价 (Moving Weighted Average)"
)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
__table_args__ = (
UniqueConstraint("sku_id", "company_id", name="uq_sku_company"),
)
sku: Mapped[ProductSku | None] = relationship("ProductSku", lazy="selectin")
+60
View File
@@ -33,6 +33,9 @@ class FinInvoicePool(Base):
uploader_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=True
)
company_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_companies.id"), nullable=False, index=True
)
file_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
merchant_name: Mapped[str | None] = mapped_column(String(200), nullable=True)
amount: Mapped[float] = mapped_column(Numeric(14, 2), default=0)
@@ -59,6 +62,9 @@ class FinExpenseRecord(Base):
applicant_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=False
)
company_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_companies.id"), nullable=False, index=True
)
total_amount: Mapped[float] = mapped_column(Numeric(14, 2), default=0)
status: Mapped[str] = mapped_column(String(20), nullable=False, default="draft")
remark: Mapped[str | None] = mapped_column(Text, nullable=True)
@@ -134,9 +140,23 @@ class FinSalesInvoice(Base):
payment_date: Mapped[date | None] = mapped_column(Date, nullable=True)
payment_amount: Mapped[float] = mapped_column(Numeric(14, 2), default=0)
remark: Mapped[str | None] = mapped_column(Text, nullable=True)
order_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_orders.id"), nullable=True,
comment="关联订单"
)
shipping_record_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_shipping_records.id"), nullable=True,
comment="关联发货单"
)
payment_due_date: Mapped[date | None] = mapped_column(
Date, nullable=True, comment="回款截止日(根据合同付款条件自动推算)"
)
created_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=True
)
company_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_companies.id"), nullable=False, index=True
)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
@@ -150,3 +170,43 @@ class FinSalesInvoice(Base):
creator: Mapped["SysUser | None"] = relationship( # noqa: F821
"SysUser", lazy="selectin"
)
class FinOcrTask(Base):
"""OCR 处理任务队列 — 持久化排队"""
__tablename__ = "fin_ocr_tasks"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
file_url: Mapped[str] = mapped_column(String(500), nullable=False)
file_ext: Mapped[str] = mapped_column(String(10), nullable=False, comment=".pdf/.png/.jpg")
original_name: Mapped[str] = mapped_column(String(200), nullable=False, default="")
status: Mapped[str] = mapped_column(
String(20), nullable=False, default="pending",
comment="pending/processing/success/failed/manual",
)
priority: Mapped[int] = mapped_column(default=100, comment="值越小越优先")
ocr_result: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
retry_count: Mapped[int] = mapped_column(default=0)
max_retries: Mapped[int] = mapped_column(default=3)
invoice_pool_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("fin_invoice_pool.id"), nullable=True,
comment="成功入池后关联的发票 ID",
)
uploader_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=True,
)
company_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_companies.id"), nullable=False, index=True,
)
inv_type: Mapped[str] = mapped_column(String(30), nullable=False, default="expense")
scheduled_after: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
is_deleted: Mapped[bool] = mapped_column(Boolean, default=False)
uploader: Mapped["SysUser | None"] = relationship("SysUser", lazy="selectin") # noqa: F821
+7
View File
@@ -37,6 +37,13 @@ class ErpOrder(Base):
salesperson_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=True
)
company_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_companies.id"), nullable=False, index=True
)
contract_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("erp_contracts.id"), nullable=True,
comment="来源合同(一键推单后回填)"
)
total_amount: Mapped[float] = mapped_column(Numeric(14, 2), default=0)
shipping_state: Mapped[str] = mapped_column(
String(20), nullable=False, default="pending"
+3
View File
@@ -42,6 +42,9 @@ class ErpShippingRecord(Base):
operator_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=True
)
company_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_companies.id"), nullable=False, index=True
)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
+42 -1
View File
@@ -8,7 +8,7 @@ from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, SmallInteger, String, Text, func
from sqlalchemy import Boolean, DateTime, ForeignKey, SmallInteger, String, Text, UniqueConstraint, func
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -97,3 +97,44 @@ class SysUser(Base):
"SysDepartment", lazy="selectin"
)
role: Mapped[SysRole | None] = relationship("SysRole", lazy="selectin")
class SysCompany(Base):
"""公司主体表 —— 多租户逻辑隔离核心"""
__tablename__ = "sys_companies"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
name: Mapped[str] = mapped_column(String(200), nullable=False)
code: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
full_info: Mapped[dict | None] = mapped_column(
JSONB, default=dict, nullable=True,
comment="公司完整信息: full_name/address/phone/bank_name/bank_account/tax_id"
)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime, server_default=func.now(), onupdate=func.now()
)
class SysUserCompany(Base):
"""用户-公司多对多关联 —— IDOR 防护核心"""
__tablename__ = "sys_user_companies"
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
user_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_users.id"), nullable=False
)
company_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("sys_companies.id"), nullable=False
)
is_default: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
__table_args__ = (
UniqueConstraint("user_id", "company_id", name="uq_user_company"),
)