Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/adapters/db/models/distribution.py

608 lines
36 KiB
Python
Executable file

"""مدل‌های افزونه پخش مویرگی."""
from __future__ import annotations
from datetime import date, datetime
from typing import Any
from sqlalchemy import (
Boolean,
Date,
DateTime,
ForeignKey,
Integer,
JSON,
Numeric,
String,
Text,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from adapters.db.session import Base
class DistributionBusinessSettings(Base):
"""تنظیمات افزونه پخش مویرگی در سطح کسب‌وکار."""
__tablename__ = "distribution_business_settings"
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), primary_key=True)
shared_routing_catalog: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=False,
comment="اگر False باشد ویزیتور عادی فقط مسیرهای تخصیص‌داده‌شده به خود را می‌بیند.",
)
require_visit_in_daily_plan: Mapped[bool] = mapped_column(
Boolean, nullable=False, default=False,
comment="شروع ویزیت فقط برای اشخاص موجود در برنامهٔ روز.",
)
geofence_radius_meters: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
require_geofence: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
visit_checklist_template: Mapped[list | dict | None] = mapped_column(JSON, nullable=True)
enable_van_sales: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
default_source_warehouse_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("warehouses.id", ondelete="SET NULL"), nullable=True,
)
enable_presell: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
enable_promotions: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
visitor_max_discount_percent: Mapped[float] = mapped_column(Numeric(5, 2), nullable=False, default=0)
enable_suggested_order: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
map_tile_source: Mapped[str] = mapped_column(
String(32),
nullable=False,
default="osm",
comment="منبع تایل نقشه: osm (جهانی و رایگان) یا memaps (نیاز به کلید API).",
)
memaps_api_key: Mapped[str | None] = mapped_column(
String(255),
nullable=True,
comment="کلید API می‌مپس؛ با ?key= یا هدر X-Memaps-Key ارسال می‌شود.",
)
share_live_location: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
default=True,
comment="اگر True باشد موقعیت ویزیتور برای مدیر روی نقشه تیم دیده می‌شود.",
)
carry_over_missed_visits: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
carry_over_days: Mapped[int] = mapped_column(Integer, nullable=False, default=7)
require_pod_signature: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
require_pod_photo: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
nav_provider: Mapped[str] = mapped_column(String(16), nullable=False, default="neshan")
setup_completed: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
auto_apply_promotions: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
enable_perfect_store: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
near_expiry_days: Mapped[int] = mapped_column(Integer, nullable=False, default=14)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class DistributionTerritory(Base):
__tablename__ = "distribution_territories"
__table_args__ = (UniqueConstraint("business_id", "code", name="uq_distribution_territories_business_code"),)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
code: Mapped[str] = mapped_column(String(50), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
routes: Mapped[list["DistributionRoute"]] = relationship(
"DistributionRoute",
back_populates="territory",
)
class DistributionRoute(Base):
__tablename__ = "distribution_routes"
__table_args__ = (UniqueConstraint("business_id", "code", name="uq_distribution_routes_business_code"),)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
territory_id: Mapped[int | None] = mapped_column(
Integer,
ForeignKey("distribution_territories.id", ondelete="SET NULL"),
index=True,
nullable=True,
)
code: Mapped[str] = mapped_column(String(50), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
plan_sort_overrides: Mapped[Any | None] = mapped_column(
JSON,
nullable=True,
comment="ترتیب بهینه‌شدهٔ روزمحور: {YYYY-MM-DD: {stop_id: sort_order}}",
)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
territory: Mapped[DistributionTerritory | None] = relationship("DistributionTerritory", back_populates="routes")
stops: Mapped[list["DistributionRouteStop"]] = relationship(
"DistributionRouteStop",
back_populates="route",
cascade="all, delete-orphan",
order_by="DistributionRouteStop.sort_order",
)
assignments: Mapped[list["DistributionRouteAssignment"]] = relationship(
"DistributionRouteAssignment",
back_populates="route",
cascade="all, delete-orphan",
)
class DistributionRouteStop(Base):
__tablename__ = "distribution_route_stops"
__table_args__ = (
UniqueConstraint("route_id", "person_id", "weekday", name="uq_distribution_route_stop_route_person_weekday"),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
route_id: Mapped[int] = mapped_column(Integer, ForeignKey("distribution_routes.id", ondelete="CASCADE"), index=True)
person_id: Mapped[int] = mapped_column(Integer, ForeignKey("persons.id", ondelete="CASCADE"), index=True)
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
weekday: Mapped[int | None] = mapped_column(Integer, nullable=True)
frequency: Mapped[str] = mapped_column(String(16), nullable=False, default="weekly")
cycle_offset: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
customer_class: Mapped[str | None] = mapped_column(String(8), nullable=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
route: Mapped["DistributionRoute"] = relationship("DistributionRoute", back_populates="stops")
class DistributionRouteAssignment(Base):
__tablename__ = "distribution_route_assignments"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
route_id: Mapped[int] = mapped_column(Integer, ForeignKey("distribution_routes.id", ondelete="CASCADE"), index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), index=True)
valid_from: Mapped[date] = mapped_column(Date, nullable=False)
valid_to: Mapped[date | None] = mapped_column(Date, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
route: Mapped["DistributionRoute"] = relationship("DistributionRoute", back_populates="assignments")
class DistributionFieldVisit(Base):
__tablename__ = "distribution_field_visits"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
person_id: Mapped[int] = mapped_column(Integer, ForeignKey("persons.id", ondelete="CASCADE"), index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), index=True)
route_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("distribution_routes.id", ondelete="SET NULL"), nullable=True)
route_stop_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("distribution_route_stops.id", ondelete="SET NULL"), nullable=True)
status: Mapped[str] = mapped_column(String(32), nullable=False)
started_at: Mapped[datetime] = mapped_column(DateTime, nullable=False)
ended_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
outcome: Mapped[str | None] = mapped_column(String(32), nullable=True)
no_order_reason: Mapped[str | None] = mapped_column(String(255), nullable=True)
document_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("documents.id", ondelete="SET NULL"), nullable=True)
deal_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("crm_deals.id", ondelete="SET NULL"), nullable=True)
crm_activity_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("crm_activities.id", ondelete="SET NULL"), nullable=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
start_latitude: Mapped[float | None] = mapped_column(Numeric(11, 8), nullable=True)
start_longitude: Mapped[float | None] = mapped_column(Numeric(11, 8), nullable=True)
end_latitude: Mapped[float | None] = mapped_column(Numeric(11, 8), nullable=True)
end_longitude: Mapped[float | None] = mapped_column(Numeric(11, 8), nullable=True)
checklist_answers: Mapped[dict | list | None] = mapped_column(JSON, nullable=True)
shelf_photo_file_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
supervisor_user_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True,
)
no_order_reason_code: Mapped[str | None] = mapped_column(String(32), nullable=True)
is_carried_over: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
pod_signature_file_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
extra_info: Mapped[dict | None] = mapped_column(JSON, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class DistributionReturnRequest(Base):
__tablename__ = "distribution_return_requests"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
person_id: Mapped[int] = mapped_column(Integer, ForeignKey("persons.id", ondelete="CASCADE"), index=True)
visit_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("distribution_field_visits.id", ondelete="SET NULL"), nullable=True)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="pending")
lines: Mapped[Any] = mapped_column(JSON, nullable=False)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
resolved_document_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("documents.id", ondelete="SET NULL"), nullable=True)
created_by_user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="RESTRICT"))
resolved_by_user_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
resolved_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class DistributionVan(Base):
"""ون توزیع — هر رکورد به یک انبار (موجودی ون) متصل است."""
__tablename__ = "distribution_vans"
__table_args__ = (UniqueConstraint("business_id", "code", name="uq_distribution_vans_business_code"),)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
warehouse_id: Mapped[int] = mapped_column(Integer, ForeignKey("warehouses.id", ondelete="RESTRICT"), index=True)
user_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True)
code: Mapped[str] = mapped_column(String(50), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
plate_number: Mapped[str | None] = mapped_column(String(32), nullable=True)
max_weight_kg: Mapped[float | None] = mapped_column(Numeric(12, 2), nullable=True)
max_volume_m3: Mapped[float | None] = mapped_column(Numeric(12, 3), nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class DistributionOfflineSyncBatch(Base):
__tablename__ = "distribution_offline_sync_batches"
__table_args__ = (UniqueConstraint("business_id", "client_batch_id", name="uq_distribution_offline_batch"),)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), index=True)
client_batch_id: Mapped[str] = mapped_column(String(64), nullable=False)
actions: Mapped[Any] = mapped_column(JSON, nullable=False)
results: Mapped[Any] = mapped_column(JSON, nullable=True)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="completed")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
class DistributionSalesTarget(Base):
"""هدف فروش ویزیتور — روزانه یا ماهانه."""
__tablename__ = "distribution_sales_targets"
__table_args__ = (
UniqueConstraint(
"business_id",
"user_id",
"period_type",
"period_start",
"metric",
"product_id",
name="uq_distribution_sales_target_metric",
),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), index=True)
period_type: Mapped[str] = mapped_column(String(16), nullable=False) # day | month
period_start: Mapped[date] = mapped_column(Date, nullable=False)
target_amount: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False)
metric: Mapped[str] = mapped_column(String(32), nullable=False, default="amount") # amount|visits|sku_qty|coverage_pct
# 0 = بدون SKU (برای یکتایی پایدار؛ بدون FK تا sentinel مجاز باشد)
product_id: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
target_value: Mapped[float | None] = mapped_column(Numeric(18, 2), nullable=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
created_by_user_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class DistributionDailySettlement(Base):
"""تسویه روزانه ویزیتور میدانی."""
__tablename__ = "distribution_daily_settlements"
__table_args__ = (
UniqueConstraint(
"business_id",
"user_id",
"settlement_date",
name="uq_distribution_daily_settlement",
),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), index=True)
settlement_date: Mapped[date] = mapped_column(Date, nullable=False, index=True)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="draft")
expected_sales: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False, default=0)
cash_collected: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False, default=0)
cheque_collected: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False, default=0)
card_collected: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False, default=0)
other_collected: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False, default=0)
expenses: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False, default=0)
variance: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False, default=0)
visit_snapshot: Mapped[Any] = mapped_column(JSON, nullable=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
receipt_document_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("documents.id", ondelete="SET NULL"), nullable=True)
cash_register_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
cheque_refs: Mapped[Any | None] = mapped_column(JSON, nullable=True)
created_by_user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="RESTRICT"))
confirmed_by_user_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
confirmed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class DistributionVisitHeartbeat(Base):
"""نقاط GPS زنده ویزیت برای trail روز."""
__tablename__ = "distribution_visit_heartbeats"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
visit_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("distribution_field_visits.id", ondelete="CASCADE"), nullable=True, index=True
)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"))
latitude: Mapped[float] = mapped_column(Numeric(11, 8), nullable=False)
longitude: Mapped[float] = mapped_column(Numeric(11, 8), nullable=False)
recorded_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow)
class DistributionUserLiveLocation(Base):
"""آخرین موقعیت شناخته‌شدهٔ ویزیتور، مستقل از ویزیت باز."""
__tablename__ = "distribution_user_live_locations"
__table_args__ = (UniqueConstraint("business_id", "user_id", name="uq_dist_user_live_location"),)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), index=True)
visit_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("distribution_field_visits.id", ondelete="SET NULL"), nullable=True
)
latitude: Mapped[float] = mapped_column(Numeric(11, 8), nullable=False)
longitude: Mapped[float] = mapped_column(Numeric(11, 8), nullable=False)
recorded_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class DistributionVisitOrder(Base):
"""سفارش پیش‌فروش ویزیتور (جدا از فروش ون)."""
__tablename__ = "distribution_visit_orders"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
visit_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("distribution_field_visits.id", ondelete="SET NULL"), nullable=True)
person_id: Mapped[int] = mapped_column(Integer, ForeignKey("persons.id", ondelete="CASCADE"), index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), index=True)
route_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("distribution_routes.id", ondelete="SET NULL"), nullable=True)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="draft")
lines: Mapped[Any] = mapped_column(JSON, nullable=False)
document_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("documents.id", ondelete="SET NULL"), nullable=True)
delivery_trip_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("distribution_delivery_trips.id", ondelete="SET NULL"), nullable=True,
)
requested_delivery_date: Mapped[date | None] = mapped_column(Date, nullable=True)
promotion_ids: Mapped[Any | None] = mapped_column(JSON, nullable=True)
discount_total: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False, default=0)
net_total: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False, default=0)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
extra_info: Mapped[dict | None] = mapped_column(JSON, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class DistributionTradePromotion(Base):
__tablename__ = "distribution_trade_promotions"
__table_args__ = (UniqueConstraint("business_id", "code", name="uq_distribution_promo_code"),)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
code: Mapped[str] = mapped_column(String(50), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
mechanic: Mapped[str] = mapped_column(String(32), nullable=False) # percent_off|amount_off|bxgy|foc
config: Mapped[Any] = mapped_column(JSON, nullable=False)
valid_from: Mapped[date] = mapped_column(Date, nullable=False)
valid_to: Mapped[date | None] = mapped_column(Date, nullable=True)
territory_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("distribution_territories.id", ondelete="SET NULL"), nullable=True)
route_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("distribution_routes.id", ondelete="SET NULL"), nullable=True)
product_ids: Mapped[Any | None] = mapped_column(JSON, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class DistributionDeliveryTrip(Base):
__tablename__ = "distribution_delivery_trips"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
driver_user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), index=True)
van_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("distribution_vans.id", ondelete="SET NULL"), nullable=True)
trip_date: Mapped[date] = mapped_column(Date, nullable=False, index=True)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="draft")
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
started_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_by_user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="RESTRICT"))
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
stops: Mapped[list["DistributionDeliveryStop"]] = relationship(
"DistributionDeliveryStop",
back_populates="trip",
cascade="all, delete-orphan",
order_by="DistributionDeliveryStop.sort_order",
)
class DistributionDeliveryStop(Base):
__tablename__ = "distribution_delivery_stops"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
trip_id: Mapped[int] = mapped_column(Integer, ForeignKey("distribution_delivery_trips.id", ondelete="CASCADE"), index=True)
order_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("distribution_visit_orders.id", ondelete="SET NULL"), nullable=True)
person_id: Mapped[int] = mapped_column(Integer, ForeignKey("persons.id", ondelete="CASCADE"))
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="pending")
pod_confirmed: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
pod_signer_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
pod_note: Mapped[str | None] = mapped_column(Text, nullable=True)
pod_photo_file_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
pod_latitude: Mapped[float | None] = mapped_column(Numeric(11, 8), nullable=True)
pod_longitude: Mapped[float | None] = mapped_column(Numeric(11, 8), nullable=True)
delivered_qty_snapshot: Mapped[Any | None] = mapped_column(JSON, nullable=True)
failure_reason: Mapped[str | None] = mapped_column(String(255), nullable=True)
completed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
trip: Mapped["DistributionDeliveryTrip"] = relationship("DistributionDeliveryTrip", back_populates="stops")
class DistributionLoadPlan(Base):
__tablename__ = "distribution_load_plans"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
plan_date: Mapped[date] = mapped_column(Date, nullable=False)
warehouse_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("warehouses.id", ondelete="SET NULL"), nullable=True)
van_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("distribution_vans.id", ondelete="SET NULL"), nullable=True)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="draft")
lines: Mapped[Any] = mapped_column(JSON, nullable=False)
order_ids: Mapped[Any | None] = mapped_column(JSON, nullable=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
created_by_user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="RESTRICT"))
confirmed_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class DistributionCommissionRule(Base):
__tablename__ = "distribution_commission_rules"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
name: Mapped[str] = mapped_column(String(255), nullable=False)
rule_type: Mapped[str] = mapped_column(String(32), nullable=False, default="percent_of_sales")
config: Mapped[Any] = mapped_column(JSON, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
valid_from: Mapped[date | None] = mapped_column(Date, nullable=True)
valid_to: Mapped[date | None] = mapped_column(Date, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class DistributionCommissionRun(Base):
__tablename__ = "distribution_commission_runs"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), index=True)
period_start: Mapped[date] = mapped_column(Date, nullable=False)
period_end: Mapped[date] = mapped_column(Date, nullable=False)
rule_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("distribution_commission_rules.id", ondelete="SET NULL"), nullable=True)
sales_amount: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False, default=0)
commission_amount: Mapped[float] = mapped_column(Numeric(18, 2), nullable=False, default=0)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="draft")
breakdown: Mapped[Any | None] = mapped_column(JSON, nullable=True)
created_by_user_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class DistributionShelfAudit(Base):
__tablename__ = "distribution_shelf_audits"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
visit_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("distribution_field_visits.id", ondelete="SET NULL"), nullable=True)
person_id: Mapped[int] = mapped_column(Integer, ForeignKey("persons.id", ondelete="CASCADE"), index=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"))
answers: Mapped[Any] = mapped_column(JSON, nullable=False)
score: Mapped[float | None] = mapped_column(Numeric(5, 2), nullable=True)
photo_file_ids: Mapped[Any | None] = mapped_column(JSON, nullable=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
class DistributionCustomerAsset(Base):
__tablename__ = "distribution_customer_assets"
__table_args__ = (UniqueConstraint("business_id", "asset_code", name="uq_dist_customer_asset_code"),)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
person_id: Mapped[int] = mapped_column(Integer, ForeignKey("persons.id", ondelete="CASCADE"), index=True)
asset_code: Mapped[str] = mapped_column(String(64), nullable=False)
asset_type: Mapped[str] = mapped_column(String(64), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
status: Mapped[str] = mapped_column(String(32), nullable=False, default="active")
placed_at: Mapped[date | None] = mapped_column(Date, nullable=True)
last_checked_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
extra_info: Mapped[dict | None] = mapped_column(JSON, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class DistributionAssortment(Base):
__tablename__ = "distribution_assortments"
__table_args__ = (UniqueConstraint("business_id", "code", name="uq_dist_assortment_code"),)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
code: Mapped[str] = mapped_column(String(50), nullable=False)
name: Mapped[str] = mapped_column(String(255), nullable=False)
outlet_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
product_ids: Mapped[Any | None] = mapped_column(JSON, nullable=True)
must_sell_product_ids: Mapped[Any | None] = mapped_column(JSON, nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class DistributionCustomerProfile(Base):
"""پروفایل پخش مشتری: کلاس، فرکانس، لیست قیمت، نوع فروشگاه."""
__tablename__ = "distribution_customer_profiles"
__table_args__ = (UniqueConstraint("business_id", "person_id", name="uq_dist_customer_profile_person"),)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
person_id: Mapped[int] = mapped_column(Integer, ForeignKey("persons.id", ondelete="CASCADE"), index=True)
customer_class: Mapped[str | None] = mapped_column(String(8), nullable=True)
visit_frequency: Mapped[str] = mapped_column(String(16), nullable=False, default="weekly")
cycle_offset: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
price_list_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("price_lists.id", ondelete="SET NULL"), nullable=True,
)
outlet_type: Mapped[str | None] = mapped_column(String(32), nullable=True)
assortment_id: Mapped[int | None] = mapped_column(
Integer, ForeignKey("distribution_assortments.id", ondelete="SET NULL"), nullable=True,
)
storefront_photo_file_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
onboarded_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
extra_info: Mapped[dict | None] = mapped_column(JSON, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
class DistributionVanLot(Base):
"""لات/انقضای موجودی ون برای FEFO."""
__tablename__ = "distribution_van_lots"
__table_args__ = (
UniqueConstraint("van_id", "product_id", "lot_code", "expiry_date", name="uq_dist_van_lot_key"),
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
business_id: Mapped[int] = mapped_column(Integer, ForeignKey("businesses.id", ondelete="CASCADE"), index=True)
van_id: Mapped[int] = mapped_column(Integer, ForeignKey("distribution_vans.id", ondelete="CASCADE"), index=True)
product_id: Mapped[int] = mapped_column(Integer, ForeignKey("products.id", ondelete="CASCADE"), index=True)
lot_code: Mapped[str] = mapped_column(String(64), nullable=False, default="-")
expiry_date: Mapped[date | None] = mapped_column(Date, nullable=True)
quantity: Mapped[float] = mapped_column(Numeric(18, 3), nullable=False, default=0)
unit_weight_kg: Mapped[float | None] = mapped_column(Numeric(12, 3), nullable=True)
unit_volume_m3: Mapped[float | None] = mapped_column(Numeric(12, 4), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)