dingdanquanliucheng/backend/app/models/business.py

449 lines
23 KiB
Python
Raw Normal View History

"""业务核心 ORM 模型。
定义订单管理系统的所有业务数据表包括客户产品供应商
订单物流提醒文件附件定价规则等
"""
2026-05-26 15:24:41 +08:00
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, Numeric, String, Text, func
2026-05-14 10:15:51 +08:00
from sqlalchemy.orm import Mapped, mapped_column
2026-05-14 13:51:06 +08:00
from backend.app.db import Base
from backend.app.models.base import AuditMixin, TimestampMixin
2026-05-14 10:15:51 +08:00
2026-05-14 13:51:06 +08:00
class Customer(TimestampMixin, AuditMixin, Base):
"""客户表customer
存储客户基本信息结算方式所属业务员信用额度等
支持软删除AuditMixin.deleted
被调用方: customer_repositorycustomer_serviceorder_service
"""
2026-05-14 13:51:06 +08:00
__tablename__ = "customer"
2026-05-14 10:15:51 +08:00
2026-05-14 13:51:06 +08:00
id: Mapped[int] = mapped_column(primary_key=True)
customer_name: Mapped[str] = mapped_column(String(64), index=True)
mobile: Mapped[str] = mapped_column(String(32), index=True)
address: Mapped[str | None] = mapped_column(String(255), nullable=True)
settlement_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
settlement_days: Mapped[int] = mapped_column(default=0)
customer_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
2026-05-26 15:24:41 +08:00
price_tier: Mapped[str | None] = mapped_column(String(32), nullable=True, default="default")
2026-05-14 13:51:06 +08:00
salesman_id: Mapped[int | None] = mapped_column(nullable=True)
credit_limit: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
2026-05-14 10:15:51 +08:00
2026-05-14 13:51:06 +08:00
class ProductCategory(TimestampMixin, AuditMixin, Base):
"""产品分类表product_category
管理产品的一级分类"铝板""钢材"
category_code 唯一标识sort_no 控制前端排序
"""
2026-05-14 13:51:06 +08:00
__tablename__ = "product_category"
id: Mapped[int] = mapped_column(primary_key=True)
category_name: Mapped[str] = mapped_column(String(64))
category_code: Mapped[str] = mapped_column(String(64), unique=True, index=True)
sort_no: Mapped[int] = mapped_column(default=0)
status: Mapped[int] = mapped_column(default=1)
remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
class Product(TimestampMixin, AuditMixin, Base):
"""产品表product
存储产品名称规格单位成本价售价等信息
category_id 关联 product_category is_default 标记默认规格
被调用方: product_repositoryproduct_servicepricing_engine
"""
2026-05-14 10:15:51 +08:00
__tablename__ = "product"
2026-05-14 13:51:06 +08:00
id: Mapped[int] = mapped_column(primary_key=True)
product_name: Mapped[str] = mapped_column(String(64), index=True)
specification: Mapped[str] = mapped_column(String(64), default="")
unit: Mapped[str] = mapped_column(String(32), default="")
category: Mapped[str | None] = mapped_column(String(64), nullable=True)
category_id: Mapped[int | None] = mapped_column(ForeignKey("product_category.id"), nullable=True)
cost_price: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
sale_price: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
2026-05-26 15:24:41 +08:00
pricing_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
pricing_unit: Mapped[str | None] = mapped_column(String(16), nullable=True, default="")
thickness: Mapped[str | None] = mapped_column(String(32), nullable=True)
weight_gsm: Mapped[int | None] = mapped_column(Integer, nullable=True)
default_width_m: Mapped[float | None] = mapped_column(Numeric(10, 4), nullable=True)
2026-05-14 13:51:06 +08:00
status: Mapped[int] = mapped_column(default=1)
2026-05-21 14:59:38 +08:00
is_default: Mapped[int] = mapped_column(default=0)
2026-05-14 13:51:06 +08:00
remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
class Supplier(TimestampMixin, AuditMixin, Base):
"""供应商表supplier
存储供应商/工厂基本信息supplier_type 区分"factory"等类型
template_type 用于生成发厂文本时选择对应的模板
"""
2026-05-14 13:51:06 +08:00
__tablename__ = "supplier"
id: Mapped[int] = mapped_column(primary_key=True)
supplier_name: Mapped[str] = mapped_column(String(64), index=True)
supplier_type: Mapped[str] = mapped_column(String(32), default="factory")
contact_name: Mapped[str | None] = mapped_column(String(64), nullable=True)
contact_mobile: Mapped[str | None] = mapped_column(String(32), nullable=True)
address: Mapped[str | None] = mapped_column(String(255), nullable=True)
template_type: Mapped[str | None] = mapped_column(String(64), nullable=True)
status: Mapped[int] = mapped_column(default=1)
remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
class SalesOrder(TimestampMixin, AuditMixin, Base):
"""销售订单主表sales_order
订单全流程的核心表包含客户信息快照金额汇总状态流转
取消审批发厂确认等字段order_status 驱动整个业务流程
状态流转: draft pending_approve approved pending_factory ...
被调用方: order_repositoryorder_servicereminder_service
"""
2026-05-14 10:15:51 +08:00
__tablename__ = "sales_order"
2026-05-14 13:51:06 +08:00
id: Mapped[int] = mapped_column(primary_key=True)
order_no: Mapped[str] = mapped_column(String(64), unique=True, index=True)
customer_id: Mapped[int | None] = mapped_column(ForeignKey("customer.id"), nullable=True)
customer_name: Mapped[str] = mapped_column(String(64))
customer_mobile: Mapped[str] = mapped_column(String(32))
customer_address: Mapped[str | None] = mapped_column(String(255), nullable=True)
salesman_id: Mapped[int | None] = mapped_column(nullable=True)
order_status: Mapped[str] = mapped_column(String(32), index=True, default="draft")
order_source: Mapped[str | None] = mapped_column(String(64), nullable=True)
delivery_type: Mapped[str | None] = mapped_column(String(64), nullable=True)
factory_id: Mapped[int | None] = mapped_column(ForeignKey("supplier.id"), nullable=True)
sale_price_total: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
cost_price_total: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
rebate_total: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
freight_total: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
tax_total: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
other_fee_total: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
profit_total: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
profit_rate: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
commission_amount: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
cancel_requested_by: Mapped[int | None] = mapped_column(nullable=True)
cancel_requested_at: Mapped[DateTime | None] = mapped_column(DateTime, nullable=True)
cancel_reason: Mapped[str | None] = mapped_column(String(255), nullable=True)
cancel_opinion: Mapped[str | None] = mapped_column(Text, nullable=True)
cancel_previous_status: Mapped[str | None] = mapped_column(String(32), nullable=True)
supplier_text_confirmed_at: Mapped[DateTime | None] = mapped_column(DateTime, nullable=True)
supplier_text_confirmed_by: Mapped[int | None] = mapped_column(nullable=True)
remark: Mapped[str | None] = mapped_column(Text, nullable=True)
class SalesOrderItem(TimestampMixin, AuditMixin, Base):
"""订单明细行表sales_order_item
每个订单包含多条明细记录产品数量单价面积附加费用等
pricing_type 区分按面积/按重量等计价方式
被调用方: order_repositoryorder_service
"""
2026-05-14 10:15:51 +08:00
__tablename__ = "sales_order_item"
2026-05-14 13:51:06 +08:00
id: Mapped[int] = mapped_column(primary_key=True)
order_id: Mapped[int] = mapped_column(ForeignKey("sales_order.id"), index=True)
product_id: Mapped[int | None] = mapped_column(nullable=True)
product_name: Mapped[str] = mapped_column(String(64))
specification: Mapped[str] = mapped_column(String(64), default="")
unit: Mapped[str] = mapped_column(String(32), default="")
quantity: Mapped[float] = mapped_column(Numeric(18, 4), default=0)
sale_price: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
cost_price: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
rebate_amount: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
freight_amount: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
tax_amount: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
other_fee_amount: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
2026-05-26 15:24:41 +08:00
pricing_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
length_m: Mapped[float | None] = mapped_column(Numeric(10, 4), nullable=True)
width_m: Mapped[float | None] = mapped_column(Numeric(10, 4), nullable=True)
area_sqm: Mapped[float | None] = mapped_column(Numeric(18, 4), nullable=True)
surcharge_detail: Mapped[str | None] = mapped_column(Text, nullable=True)
processing_detail: Mapped[str | None] = mapped_column(Text, nullable=True)
supplier_id: Mapped[int | None] = mapped_column(ForeignKey("supplier.id"), nullable=True)
supplier_model: Mapped[str | None] = mapped_column(String(64), nullable=True)
price_tier: Mapped[str | None] = mapped_column(String(32), nullable=True)
2026-05-14 13:51:06 +08:00
remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
2026-05-14 13:57:32 +08:00
class SalesOrderApproveLog(TimestampMixin, AuditMixin, Base):
"""订单审批日志表sales_order_approve_log
记录每次审批操作的结果通过/驳回审批前后状态审批意见
approve_type 区分"order"订单审批"cancel_order"取消审批
被调用方: order_service
"""
2026-05-14 13:57:32 +08:00
__tablename__ = "sales_order_approve_log"
id: Mapped[int] = mapped_column(primary_key=True)
order_id: Mapped[int] = mapped_column(ForeignKey("sales_order.id"), index=True)
approve_type: Mapped[str] = mapped_column(String(32), default="order")
approve_result: Mapped[str] = mapped_column(String(32))
before_status: Mapped[str | None] = mapped_column(String(32), nullable=True)
after_status: Mapped[str | None] = mapped_column(String(32), nullable=True)
approve_opinion: Mapped[str | None] = mapped_column(Text, nullable=True)
operator_id: Mapped[int | None] = mapped_column(nullable=True)
2026-05-19 17:30:35 +08:00
class OrderSupplierTextLog(TimestampMixin, AuditMixin, Base):
"""发厂文本日志表order_supplier_text_log
记录为供应商生成的发厂文本内容及确认状态
confirmed 标记工厂是否已确认文本内容
被调用方: order_service
"""
2026-05-19 17:30:35 +08:00
__tablename__ = "order_supplier_text_log"
id: Mapped[int] = mapped_column(primary_key=True)
order_id: Mapped[int] = mapped_column(ForeignKey("sales_order.id"), index=True)
supplier_id: Mapped[int] = mapped_column(ForeignKey("supplier.id"), index=True)
template_type: Mapped[str | None] = mapped_column(String(64), nullable=True)
text_content: Mapped[str] = mapped_column(Text)
confirmed: Mapped[int] = mapped_column(default=0)
confirmed_by: Mapped[int | None] = mapped_column(nullable=True)
confirmed_at: Mapped[DateTime | None] = mapped_column(DateTime, nullable=True)
created_by: Mapped[int | None] = mapped_column(nullable=True)
2026-05-14 13:57:32 +08:00
2026-05-14 13:51:06 +08:00
class LogisticsTask(TimestampMixin, AuditMixin, Base):
"""物流任务表logistics_task
记录每个订单的物流配送任务包含取货/送货地址司机
快递单号任务状态等status: pending accepted picked_up delivered
被调用方: logistics_repositorylogistics_servicereminder_service
"""
2026-05-14 13:51:06 +08:00
__tablename__ = "logistics_task"
id: Mapped[int] = mapped_column(primary_key=True)
task_no: Mapped[str | None] = mapped_column(String(64), nullable=True, unique=True)
2026-05-14 13:51:06 +08:00
order_id: Mapped[int] = mapped_column(ForeignKey("sales_order.id"), index=True)
driver_id: Mapped[int] = mapped_column(index=True)
factory_id: Mapped[int | None] = mapped_column(ForeignKey("supplier.id"), nullable=True)
pickup_address: Mapped[str] = mapped_column(String(255))
delivery_address: Mapped[str] = mapped_column(String(255))
pickup_content: Mapped[str] = mapped_column(String(255))
quantity: Mapped[float] = mapped_column(Numeric(18, 4), default=0)
status: Mapped[str] = mapped_column(String(32), default="pending")
2026-05-27 14:43:54 +08:00
tracking_number: Mapped[str | None] = mapped_column(String(64), nullable=True)
express_company: Mapped[str | None] = mapped_column(String(32), nullable=True)
2026-05-14 13:51:06 +08:00
created_by: Mapped[int | None] = mapped_column(nullable=True)
canceled_by: Mapped[int | None] = mapped_column(nullable=True)
cancel_reason: Mapped[str | None] = mapped_column(String(255), nullable=True)
remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
class LogisticsTrace(Base):
"""物流轨迹表logistics_trace
记录物流节点的时间描述类型数据来源平台
支持从快递100等第三方平台同步轨迹数据
"""
__tablename__ = "logistics_trace"
id: Mapped[int] = mapped_column(primary_key=True)
order_id: Mapped[int] = mapped_column(ForeignKey("sales_order.id"), index=True)
task_id: Mapped[int | None] = mapped_column(ForeignKey("logistics_task.id"), nullable=True, index=True)
node_time: Mapped[DateTime] = mapped_column(DateTime)
node_desc: Mapped[str] = mapped_column(String(500))
node_type: Mapped[str | None] = mapped_column(String(64), nullable=True)
source_platform: Mapped[str | None] = mapped_column(String(64), nullable=True)
remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
created_at: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now())
class FileAttachment(Base):
"""文件附件表file_attachment
通用文件关联表通过 biz_type + biz_id 关联任意业务对象
存储文件 URL类型大小阿里云 OSS 存储信息等
被调用方: file_repositoryfile_service
"""
__tablename__ = "file_attachment"
id: Mapped[int] = mapped_column(primary_key=True)
biz_type: Mapped[str] = mapped_column(String(64), index=True)
biz_id: Mapped[int] = mapped_column(index=True)
file_name: Mapped[str] = mapped_column(String(255))
file_url: Mapped[str] = mapped_column(String(1000))
file_type: Mapped[str | None] = mapped_column(String(64), nullable=True)
file_size: Mapped[int | None] = mapped_column(nullable=True)
created_by: Mapped[int | None] = mapped_column(nullable=True)
created_at: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now())
storage_provider: Mapped[str | None] = mapped_column(String(32), nullable=True, default="aliyun_oss")
bucket_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
object_key: Mapped[str | None] = mapped_column(String(1000), nullable=True)
content_type: Mapped[str | None] = mapped_column(String(128), nullable=True)
file_ext: Mapped[str | None] = mapped_column(String(32), nullable=True)
file_category: Mapped[str | None] = mapped_column(String(32), nullable=True)
class CustomerArrears(Base):
"""客户欠款表customer_arrears
记录客户未结清的欠款信息包括欠款金额账期起始日到期日
status: pending overdue超期后触发提醒
被调用方: reminder_repositoryreminder_service
"""
__tablename__ = "customer_arrears"
id: Mapped[int] = mapped_column(primary_key=True)
customer_id: Mapped[int] = mapped_column(ForeignKey("customer.id"), index=True)
order_id: Mapped[int] = mapped_column(ForeignKey("sales_order.id"), index=True)
arrears_amount: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
settlement_start_date: Mapped[Date | None] = mapped_column(Date, nullable=True)
due_date: Mapped[Date | None] = mapped_column(Date, nullable=True)
status: Mapped[str] = mapped_column(String(32), default="pending")
reminded_at: Mapped[DateTime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now())
class SystemReminder(Base):
"""系统提醒表system_reminder
存储系统自动生成的提醒消息如物流超时欠款逾期沉默客户等
reminder_type 区分提醒类型receiver_user_id 指定接收人
status: pending read
被调用方: reminder_repositoryreminder_service
"""
__tablename__ = "system_reminder"
id: Mapped[int] = mapped_column(primary_key=True)
reminder_type: Mapped[str] = mapped_column(String(64), index=True)
biz_type: Mapped[str] = mapped_column(String(64), index=True)
biz_id: Mapped[int] = mapped_column(index=True)
receiver_user_id: Mapped[int] = mapped_column(index=True)
reminder_title: Mapped[str] = mapped_column(String(255))
reminder_content: Mapped[str] = mapped_column(String(2000))
status: Mapped[str] = mapped_column(String(32), default="pending")
sent_at: Mapped[DateTime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now())
class AIRecognitionLog(Base):
"""AI 识别日志表ai_recognition_log
记录 AI 图片识别的原始结果置信度人工修正结果
用于 AI 识别功能的结果追踪和模型优化
被调用方: ai_repositoryai_service
"""
__tablename__ = "ai_recognition_log"
id: Mapped[int] = mapped_column(primary_key=True)
biz_type: Mapped[str] = mapped_column(String(64), index=True)
biz_id: Mapped[int] = mapped_column(index=True)
image_url: Mapped[str] = mapped_column(String(1000))
raw_result: Mapped[str | None] = mapped_column(Text, nullable=True)
confidence: Mapped[float | None] = mapped_column(Numeric(10, 4), nullable=True)
corrected_result: Mapped[str | None] = mapped_column(Text, nullable=True)
created_by: Mapped[int | None] = mapped_column(nullable=True)
created_at: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now())
2026-05-26 15:24:41 +08:00
class ProductPricingRule(TimestampMixin, AuditMixin, Base):
"""产品定价规则表product_pricing_rule
定义每个产品的计价方式按面积/按重量基准单价
计算公式附加费用等 pricing_engine 在创建订单时调用
"""
2026-05-26 15:24:41 +08:00
__tablename__ = "product_pricing_rule"
id: Mapped[int] = mapped_column(primary_key=True)
product_id: Mapped[int] = mapped_column(unique=True, index=True)
product_name: Mapped[str] = mapped_column(String(128))
pricing_type: Mapped[str] = mapped_column(String(32), default="area")
base_unit_price: Mapped[float] = mapped_column(Numeric(18, 4), default=0)
pricing_unit: Mapped[str] = mapped_column(String(16), default="")
pricing_inputs: Mapped[str | None] = mapped_column(Text, nullable=True)
formula_expr: Mapped[str | None] = mapped_column(Text, nullable=True)
formula_constants: Mapped[str | None] = mapped_column(Text, nullable=True)
surcharge_json: Mapped[str | None] = mapped_column(Text, nullable=True)
formula_note: Mapped[str | None] = mapped_column(Text, nullable=True)
tax_rate: Mapped[float] = mapped_column(Numeric(5, 2), default=0)
tax_inclusive: Mapped[int] = mapped_column(default=0)
2026-05-26 15:24:41 +08:00
status: Mapped[int] = mapped_column(default=1)
class SupplierProductCost(TimestampMixin, AuditMixin, Base):
"""供应商产品成本表supplier_product_cost
记录每个供应商对每个产品的成本价格支持按厚度克重等维度定价
is_primary 标记主供应商 pricing_engine 在计算订单成本时调用
"""
2026-05-26 15:24:41 +08:00
__tablename__ = "supplier_product_cost"
id: Mapped[int] = mapped_column(primary_key=True)
product_id: Mapped[int] = mapped_column(index=True)
supplier_id: Mapped[int] = mapped_column(index=True)
supplier_model: Mapped[str | None] = mapped_column(String(64), nullable=True)
our_model: Mapped[str | None] = mapped_column(String(64), nullable=True)
thickness: Mapped[str | None] = mapped_column(String(32), nullable=True)
weight_gsm: Mapped[int | None] = mapped_column(Integer, nullable=True)
base_fabric_weight: Mapped[int | None] = mapped_column(Integer, nullable=True)
cost_price: Mapped[float] = mapped_column(Numeric(18, 4))
cost_unit: Mapped[str] = mapped_column(String(16), default="")
is_primary: Mapped[int] = mapped_column(default=0)
remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
status: Mapped[int] = mapped_column(default=1)
class ProductPriceTier(TimestampMixin, AuditMixin, Base):
"""产品价格层级表product_price_tier
定义产品的多级价格体系如普通客户价VIP客户价
tier_code 区分层级前端根据客户的价格层级选择对应售价
"""
2026-05-26 15:24:41 +08:00
__tablename__ = "product_price_tier"
id: Mapped[int] = mapped_column(primary_key=True)
product_id: Mapped[int] = mapped_column(index=True)
tier_code: Mapped[str] = mapped_column(String(32))
tier_name: Mapped[str] = mapped_column(String(64))
price: Mapped[float] = mapped_column(Numeric(18, 4))
price_unit: Mapped[str] = mapped_column(String(16), default="")
remark: Mapped[str | None] = mapped_column(String(255), nullable=True)
status: Mapped[int] = mapped_column(default=1)
class LogisticsWaybill(TimestampMixin, AuditMixin, Base):
"""物流运单号表logistics_waybill
记录一个物流任务关联的多个运单号一票货分多件场景
通过 task_id 关联 logistics_taskorder_id 冗余关联 sales_order
被调用方: logistics_repositorylogistics_service
"""
__tablename__ = "logistics_waybill"
id: Mapped[int] = mapped_column(primary_key=True)
task_id: Mapped[int] = mapped_column(ForeignKey("logistics_task.id"), index=True)
order_id: Mapped[int] = mapped_column(ForeignKey("sales_order.id"), index=True)
tracking_number: Mapped[str] = mapped_column(String(64), index=True)
express_company: Mapped[str | None] = mapped_column(String(32), nullable=True)
express_name: Mapped[str | None] = mapped_column(String(64), nullable=True)
image_url: Mapped[str | None] = mapped_column(String(1000), nullable=True)
ocr_raw: Mapped[str | None] = mapped_column(Text, nullable=True)
status: Mapped[str] = mapped_column(String(32), default="pending")
created_by: Mapped[int | None] = mapped_column(nullable=True)
class PerformanceStatCache(Base):
"""业绩统计缓存表performance_stat_cache
存储业绩统计结果缓存避免重复查询数据库
created_at 超过 24 小时视为过期
"""
__tablename__ = "performance_stat_cache"
id: Mapped[int] = mapped_column(primary_key=True)
stat_type: Mapped[str] = mapped_column(String(16), index=True)
stat_period: Mapped[str] = mapped_column(String(32), index=True)
category_id: Mapped[int | None] = mapped_column(nullable=True)
order_count: Mapped[int] = mapped_column(Integer, default=0)
order_amount: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
commission_amount: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
total_profit: Mapped[float] = mapped_column(Numeric(18, 2), default=0)
2026-06-14 16:20:04 +08:00
category_amounts_json: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[DateTime] = mapped_column(DateTime, server_default=func.now())