forked from hesabix/arc
305 lines
No EOL
15 KiB
Python
Executable file
305 lines
No EOL
15 KiB
Python
Executable file
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
|
|
from sqlalchemy import String, DateTime, Integer, ForeignKey, Enum as SQLEnum, Text, Numeric, Boolean, JSON
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from adapters.db.session import Base
|
|
|
|
|
|
class BusinessType(str, Enum):
|
|
"""نوع کسب و کار"""
|
|
COMPANY = "شرکت" # شرکت
|
|
SHOP = "مغازه" # مغازه
|
|
STORE = "فروشگاه" # فروشگاه
|
|
UNION = "اتحادیه" # اتحادیه
|
|
CLUB = "باشگاه" # باشگاه
|
|
INSTITUTE = "موسسه" # موسسه
|
|
INDIVIDUAL = "شخصی" # شخصی
|
|
|
|
|
|
class BusinessField(str, Enum):
|
|
"""زمینه فعالیت کسب و کار"""
|
|
MANUFACTURING = "تولیدی" # تولیدی
|
|
TRADING = "بازرگانی" # بازرگانی
|
|
SERVICE = "خدماتی" # خدماتی
|
|
OTHER = "سایر" # سایر
|
|
|
|
|
|
class Business(Base):
|
|
__tablename__ = "businesses"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
|
business_type: Mapped[BusinessType] = mapped_column(SQLEnum(BusinessType), nullable=False)
|
|
business_field: Mapped[BusinessField] = mapped_column(SQLEnum(BusinessField), nullable=False)
|
|
owner_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
|
default_currency_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("currencies.id", ondelete="RESTRICT"), nullable=True, index=True)
|
|
|
|
# فیلدهای جدید
|
|
address: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
phone: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
|
mobile: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
|
national_id: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True)
|
|
registration_number: Mapped[str | None] = mapped_column(String(50), nullable=True, index=True)
|
|
economic_id: Mapped[str | None] = mapped_column(String(50), nullable=True, index=True)
|
|
|
|
# شناسه فایلهای ذخیرهسازی شده برای لوگو و مهر (ارجاع به جدول file_storage)
|
|
logo_file_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
|
|
stamp_file_id: Mapped[str | None] = mapped_column(String(36), nullable=True)
|
|
|
|
# فیلدهای جغرافیایی
|
|
country: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
province: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
city: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
|
postal_code: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
|
|
|
# تنظیمات اعتبار مشتریان
|
|
default_credit_limit: Mapped[float | None] = mapped_column(Numeric(14, 2), nullable=True, comment="سقف اعتبار پیشفرض اشخاص")
|
|
check_credit_enabled_by_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default="0", comment="بررسی اعتبار مشتریان به صورت پیشفرض")
|
|
|
|
# تنظیمات محاسبه سود فاکتور
|
|
invoice_profit_calculation_method: Mapped[str | None] = mapped_column(String(20), nullable=True, default="automatic", server_default="automatic", comment="روش محاسبه سود فاکتور: automatic, manual, disabled")
|
|
invoice_profit_calculation_basis: Mapped[str | None] = mapped_column(
|
|
String(30),
|
|
nullable=True,
|
|
default="purchase_price",
|
|
server_default="purchase_price",
|
|
comment="مبنای محاسبه سود: purchase_price, cost_price, average_cost, fifo, lifo, weighted_average, moving_weighted_average, standard_cost, actual_cost",
|
|
)
|
|
invoice_profit_include_overhead: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default="0", comment="آیا هزینههای سربار در محاسبه سود لحاظ شود؟")
|
|
invoice_profit_overhead_type: Mapped[str | None] = mapped_column(String(30), nullable=True, default="none", server_default="none", comment="نوع هزینههای سربار: none, production_overhead, all_overhead, custom_percent")
|
|
invoice_profit_overhead_percent: Mapped[float | None] = mapped_column(Numeric(5, 2), nullable=True, default=0, server_default="0", comment="درصد هزینههای سربار (در صورت انتخاب custom_percent)")
|
|
invoice_profit_calculation_type: Mapped[str | None] = mapped_column(String(20), nullable=True, default="gross", server_default="gross", comment="نوع محاسبه سود: gross, net, both")
|
|
invoice_profit_fifo_shortage_mode: Mapped[str] = mapped_column(
|
|
String(40),
|
|
nullable=False,
|
|
default="perpetual_mixed",
|
|
server_default="perpetual_mixed",
|
|
comment=(
|
|
"سیاست بهای کسری در مبنای FIFO/LIFO: perpetual_mixed (لایه+آخرین قیمت) | "
|
|
"average_purchase_on_shortage (میانگین خرید برای بخش بدون لایه)"
|
|
),
|
|
)
|
|
# شناسایی سود/بهای تمامشده قطعی (دفتر) در برابر محاسبه تحلیلی
|
|
invoice_profit_ledger_recognition_basis: Mapped[str] = mapped_column(
|
|
String(40),
|
|
nullable=False,
|
|
default="warehouse_document_posting",
|
|
server_default="warehouse_document_posting",
|
|
comment="زمان شناسایی بهای تمامشده قطعی: warehouse_document_posting | sales_invoice_document",
|
|
)
|
|
|
|
# بهروزرسانی قیمت پایه کالا از فاکتور قطعی (ارز کالا = ارز پیشفرض کسبوکار)
|
|
invoice_sync_update_sales_price_enabled: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=False, server_default="0",
|
|
comment="بهروزرسانی قیمت فروش پایه از فاکتور فروش قطعی",
|
|
)
|
|
invoice_sync_update_purchase_price_enabled: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, default=False, server_default="0",
|
|
comment="بهروزرسانی قیمت خرید پایه از فاکتور خرید قطعی",
|
|
)
|
|
invoice_sync_sales_price_basis: Mapped[str | None] = mapped_column(
|
|
String(40), nullable=True, default="net_after_line_discount", server_default="net_after_line_discount",
|
|
comment="مبنای قیمت فروش: unit_price, net_after_line_discount, net_with_tax, cost_price",
|
|
)
|
|
invoice_sync_purchase_price_basis: Mapped[str | None] = mapped_column(
|
|
String(40), nullable=True, default="net_after_line_discount", server_default="net_after_line_discount",
|
|
comment="مبنای قیمت خرید: unit_price, net_after_line_discount, net_with_tax, cost_price",
|
|
)
|
|
# حواله انبار پس از ثبت فاکتور: none (بدون حواله)، draft (پیشنویس)، posted (قطعی فوری)
|
|
invoice_warehouse_release_mode: Mapped[str] = mapped_column(
|
|
String(20),
|
|
nullable=False,
|
|
default="draft",
|
|
server_default="draft",
|
|
comment="none | draft | posted",
|
|
)
|
|
invoice_purchase_accounting_mode: Mapped[str] = mapped_column(
|
|
String(40),
|
|
nullable=False,
|
|
default="direct_inventory",
|
|
server_default="direct_inventory",
|
|
comment="direct_inventory | grni_two_step | grni_legacy",
|
|
)
|
|
# ردیفهای انبارداری بدون انبار در فاکتور (هنگام ثبت/ویرایش با post_inventory)
|
|
invoice_missing_line_warehouse_policy: Mapped[str] = mapped_column(
|
|
String(32),
|
|
nullable=False,
|
|
default="reject",
|
|
server_default="reject",
|
|
comment="reject | use_default_warehouse",
|
|
)
|
|
invoice_default_warehouse_id: Mapped[int | None] = mapped_column(
|
|
Integer,
|
|
ForeignKey("warehouses.id", ondelete="SET NULL"),
|
|
nullable=True,
|
|
index=True,
|
|
comment="انبار پیشفرض برای ردیفهای بدون انبار وقتی سیاست use_default_warehouse است",
|
|
)
|
|
invoice_default_warehouse_fill_document_header: Mapped[bool] = mapped_column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=True,
|
|
server_default="1",
|
|
comment="با پر کردن خودکار خطوط، انبار سطح سربرگ فاکتور هم اگر خالی بود پر شود",
|
|
)
|
|
|
|
# تخفیف کلی فاکتور (در کنار تخفیف ردیف)
|
|
invoice_global_discount_percent_basis: Mapped[str] = mapped_column(
|
|
String(64),
|
|
nullable=False,
|
|
default="subtotal_after_line_discount",
|
|
server_default="subtotal_after_line_discount",
|
|
comment=(
|
|
"مبنای درصد تخفیف کلی: subtotal_after_line_discount | gross_before_line_discount | "
|
|
"total_after_lines_including_tax"
|
|
),
|
|
)
|
|
invoice_global_discount_tax_mode: Mapped[str] = mapped_column(
|
|
String(64),
|
|
nullable=False,
|
|
default="recalculate_tax_proportional",
|
|
server_default="recalculate_tax_proportional",
|
|
comment="recalculate_tax_proportional | keep_line_taxes",
|
|
)
|
|
invoice_global_discount_max_percent: Mapped[float | None] = mapped_column(
|
|
Numeric(5, 2),
|
|
nullable=True,
|
|
comment="سقف درصد تخفیف کلی نسبت به مبنا (اختیاری)",
|
|
)
|
|
invoice_global_discount_max_amount: Mapped[float | None] = mapped_column(
|
|
Numeric(18, 2),
|
|
nullable=True,
|
|
comment="سقف مبلغ تخفیف کلی (اختیاری)",
|
|
)
|
|
|
|
# کنترل کسری هنگام قطعی کردن حواله (خروج): پیشفرض سختگیرانه
|
|
allow_negative_inventory_for_bulk: Mapped[bool] = mapped_column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=False,
|
|
server_default="0",
|
|
comment="خروج با موجودی منفی برای کالاهای فلهای (inventory_mode غیر unique)",
|
|
)
|
|
allow_negative_inventory_for_unique: Mapped[bool] = mapped_column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=False,
|
|
server_default="0",
|
|
comment="خروج با موجودی منفی برای کالاهای یونیک",
|
|
)
|
|
warehouse_transfer_require_positive_stock: Mapped[bool] = mapped_column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=True,
|
|
server_default="1",
|
|
comment="انتقال بین انبار همیشه نیاز به موجودی کافی (نادیده گرفتن اجازه منفی)",
|
|
)
|
|
# کالای هزینهشده / کالای درآمدشده
|
|
goods_expense_income_workflow_mode: Mapped[str] = mapped_column(
|
|
String(20),
|
|
nullable=False,
|
|
default="simple",
|
|
server_default="simple",
|
|
comment="simple | two_step",
|
|
)
|
|
goods_expense_income_auto_post_in_simple_mode: Mapped[bool] = mapped_column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=True,
|
|
server_default="1",
|
|
comment="در حالت simple، ثبت قطعی در صورت داشتن مجوز post",
|
|
)
|
|
goods_expense_income_default_expense_account_code: Mapped[str] = mapped_column(
|
|
String(50),
|
|
nullable=False,
|
|
default="70407",
|
|
server_default="70407",
|
|
)
|
|
goods_expense_income_default_income_account_code: Mapped[str] = mapped_column(
|
|
String(50),
|
|
nullable=False,
|
|
default="60103",
|
|
server_default="60103",
|
|
)
|
|
goods_expense_income_stock_count_mode: Mapped[str] = mapped_column(
|
|
String(32),
|
|
nullable=False,
|
|
default="goods_docs",
|
|
server_default="goods_docs",
|
|
comment="goods_docs | physical_adjustment | ask",
|
|
)
|
|
goods_expense_income_allow_manual_unit_cost: Mapped[bool] = mapped_column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=False,
|
|
server_default="0",
|
|
)
|
|
goods_expense_income_require_person: Mapped[bool] = mapped_column(
|
|
Boolean,
|
|
nullable=False,
|
|
default=False,
|
|
server_default="0",
|
|
)
|
|
# سیاست تسعیر ارز روی فاکتور/اسناد (کلیدها در app.services.invoice_fx_revaluation)
|
|
fx_revaluation_policy: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
|
|
# پیشفرضهای لینک اشتراک فاکتور (مثلاً default_online_payment_enabled / default_online_payment_gateway_id)
|
|
invoice_share_settings: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
|
|
|
# کاتالوگ عمومی (شبکهٔ انتشار کالا): نمایش شماره تماس در API عمومی
|
|
public_catalog_show_contact: Mapped[bool] = mapped_column(
|
|
Boolean,
|
|
default=False,
|
|
nullable=False,
|
|
server_default="0",
|
|
comment="نمایش تلفن/موبایل در API عمومی کاتالوگ",
|
|
)
|
|
public_catalog_show_base_sales_price: Mapped[bool] = mapped_column(
|
|
Boolean,
|
|
default=True,
|
|
nullable=False,
|
|
server_default="1",
|
|
comment="نمایش قیمت فروش پایه در API عمومی کاتالوگ",
|
|
)
|
|
display_timezone: Mapped[str | None] = mapped_column(
|
|
String(80),
|
|
nullable=True,
|
|
comment="IANA timezone for datetime display; null = use system default",
|
|
)
|
|
|
|
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)
|
|
|
|
# Soft Delete fields
|
|
deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
|
|
deletion_requested_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
|
deletion_requested_by: Mapped[int | None] = mapped_column(Integer, ForeignKey("users.id"), nullable=True)
|
|
deletion_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
auto_delete_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True) # 30 days after deleted_at
|
|
|
|
# Relationships
|
|
persons: Mapped[list["Person"]] = relationship("Person", back_populates="business", cascade="all, delete-orphan")
|
|
person_groups = relationship("PersonGroup", back_populates="business", cascade="all, delete-orphan")
|
|
fiscal_years = relationship("FiscalYear", back_populates="business", cascade="all, delete-orphan")
|
|
currencies = relationship("Currency", secondary="business_currencies", back_populates="businesses")
|
|
default_currency = relationship("Currency", foreign_keys="[Business.default_currency_id]", uselist=False)
|
|
documents = relationship("Document", back_populates="business", cascade="all, delete-orphan")
|
|
accounts = relationship("Account", back_populates="business", cascade="all, delete-orphan")
|
|
projects = relationship("Project", back_populates="business", cascade="all, delete-orphan")
|
|
crm_process_definitions = relationship(
|
|
"CrmProcessDefinition",
|
|
back_populates="business",
|
|
cascade="all, delete-orphan",
|
|
)
|
|
crm_leads = relationship("Lead", back_populates="business", cascade="all, delete-orphan")
|
|
crm_deals = relationship("Deal", back_populates="business", cascade="all, delete-orphan")
|
|
crm_activities = relationship("CrmActivity", back_populates="business", cascade="all, delete-orphan")
|
|
frequent_descriptions = relationship(
|
|
"BusinessFrequentDescription",
|
|
back_populates="business",
|
|
cascade="all, delete-orphan",
|
|
) |