Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/adapters/api/v1/schema_models/person.py
2026-08-19 11:35:12 +00:00

505 lines
28 KiB
Python
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from typing import List, Literal, Optional
from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict
from enum import Enum
from datetime import datetime
class PersonOpeningBalanceInput(BaseModel):
"""مانده افتتاحیه شخص — فقط در سند تراز افتتاحیه ذخیره می‌شود."""
model_config = ConfigDict(extra="ignore")
amount: float = Field(default=0, ge=0, description="مبلغ مانده افتتاحیه (۰ همراه clear برای حذف)")
balance_type: Literal["debit", "credit"] = Field(
default="debit",
description="debit=شخص بدهکار (دریافتنی)، credit=شخص بستانکار (پرداختنی)",
)
fiscal_year_id: Optional[int] = Field(
default=None,
description="سال مالی (در صورت عدم ارسال، سال جاری)",
)
clear: bool = Field(
default=False,
description="حذف خط مانده شخص از سند تراز افتتاحیه (فقط در ویرایش)",
)
@model_validator(mode="after")
def _validate_amount_or_clear(self):
if self.clear:
return self
if self.amount <= 0:
raise ValueError("مبلغ مانده افتتاحیه باید بزرگتر از صفر باشد")
return self
# پیشوندهای مجاز برای اشخاص (همسان با UI)
ALLOWED_PERSON_NAME_PREFIXES = frozenset({
"آقای",
"خانم",
"شرکت",
"دکتر",
"مهندس",
"موسسه",
"اداره",
"سازمان",
"بنیاد",
"انجمن",
})
class PersonType(str, Enum):
"""نوع شخص"""
CUSTOMER = "مشتری"
MARKETER = "بازاریاب"
EMPLOYEE = "کارمند"
SUPPLIER = "تامین‌کننده"
PARTNER = "همکار"
SELLER = "فروشنده"
SHAREHOLDER = "سهامدار"
class PersonBankAccountCreateRequest(BaseModel):
"""درخواست ایجاد حساب بانکی شخص"""
bank_name: str = Field(..., min_length=1, max_length=255, description="نام بانک")
account_number: Optional[str] = Field(default=None, max_length=50, description="شماره حساب")
card_number: Optional[str] = Field(default=None, max_length=20, description="شماره کارت")
sheba_number: Optional[str] = Field(default=None, max_length=30, description="شماره شبا")
class PersonBankAccountUpdateRequest(BaseModel):
"""درخواست ویرایش حساب بانکی شخص"""
bank_name: Optional[str] = Field(default=None, min_length=1, max_length=255, description="نام بانک")
account_number: Optional[str] = Field(default=None, max_length=50, description="شماره حساب")
card_number: Optional[str] = Field(default=None, max_length=20, description="شماره کارت")
sheba_number: Optional[str] = Field(default=None, max_length=30, description="شماره شبا")
class PersonBankAccountResponse(BaseModel):
"""پاسخ اطلاعات حساب بانکی شخص"""
id: int = Field(..., description="شناسه حساب بانکی")
person_id: int = Field(..., description="شناسه شخص")
bank_name: str = Field(..., description="نام بانک")
account_number: Optional[str] = Field(default=None, description="شماره حساب")
card_number: Optional[str] = Field(default=None, description="شماره کارت")
sheba_number: Optional[str] = Field(default=None, description="شماره شبا")
created_at: str = Field(..., description="تاریخ ایجاد")
updated_at: str = Field(..., description="تاریخ آخرین بروزرسانی")
class Config:
from_attributes = True
class PersonSocialContactInput(BaseModel):
"""سطر ارتباط پیام‌رسان / شبکه اجتماعی (ورودی ایجاد/ویرایش)"""
model_config = ConfigDict(extra="ignore")
platform_key: str = Field(..., min_length=1, max_length=64, description="کلید پلتفرم (مثل telegram) یا other")
custom_label: Optional[str] = Field(default=None, max_length=128, description="نام نمایش برای سایر/سفارشی")
value: str = Field(..., min_length=1, max_length=2000, description="آیدی، لینک، شماره و …")
@field_validator("platform_key", mode="before")
@classmethod
def _norm_platform_key(cls, v) -> str:
if v is None:
return ""
s = str(v).strip().lower()
return s
@field_validator("custom_label", mode="before")
@classmethod
def _empty_custom_to_none(cls, v):
if v is None:
return None
t = str(v).strip()
return t if t else None
@field_validator("value", mode="before")
@classmethod
def _norm_value(cls, v) -> str:
s = ("" if v is None else str(v)).strip()
return s
@model_validator(mode="after")
def _other_needs_label(self):
if self.platform_key == "other" and not self.custom_label:
raise ValueError("برای پلتفرم «سایر» باید نام/برچسب وارد شود")
return self
class PersonSocialContactResponse(BaseModel):
"""پاسخ اطلاعات ارتباط پیام‌رسان"""
id: int = Field(..., description="شناسه رکورد")
person_id: int = Field(..., description="شناسه شخص")
platform_key: str = Field(..., description="کلید پلتفرم")
custom_label: Optional[str] = Field(default=None, description="برچسب سفارشی")
value: str = Field(..., description="مقدار")
sort_order: int = Field(..., description="ترتیب نمایش")
created_at: str = Field(..., description="تاریخ ایجاد")
updated_at: str = Field(..., description="تاریخ بروزرسانی")
class Config:
from_attributes = True
class PersonCreateRequest(BaseModel):
"""درخواست ایجاد شخص جدید"""
model_config = ConfigDict(extra="ignore")
# اطلاعات پایه
code: Optional[int] = Field(default=None, ge=1, description="کد یکتا در هر کسب و کار (در صورت عدم ارسال، خودکار تولید می‌شود)")
alias_name: str = Field(..., min_length=1, max_length=255, description="نام مستعار (الزامی)")
first_name: Optional[str] = Field(default=None, max_length=100, description="نام")
last_name: Optional[str] = Field(default=None, max_length=100, description="نام خانوادگی")
person_type: Optional[PersonType] = Field(default=None, description="نوع شخص (سازگاری قدیمی)")
person_types: Optional[List[PersonType]] = Field(default=None, description="انواع شخص (چندانتخابی)")
company_name: Optional[str] = Field(default=None, max_length=255, description="نام شرکت")
name_prefix: Optional[str] = Field(default=None, max_length=64, description="پیشوند (آقای، خانم، شرکت، …)")
legal_entity_type: str = Field(
default="natural",
description="نوع حقوقی: natural=حقیقی، legal=حقوقی",
)
payment_id: Optional[str] = Field(default=None, max_length=100, description="شناسه پرداخت")
person_group_id: Optional[int] = Field(default=None, description="گروه اشخاص (اختیاری؛ قالب پیش‌فرض)")
@field_validator("name_prefix", mode="before")
@classmethod
def _validate_name_prefix(cls, v):
if v is None:
return None
s = str(v).strip()
if not s:
return None
if s not in ALLOWED_PERSON_NAME_PREFIXES:
raise ValueError(f"پیشوند نامعتبر است. مقادیر مجاز: {', '.join(sorted(ALLOWED_PERSON_NAME_PREFIXES))}")
return s
@field_validator("legal_entity_type", mode="before")
@classmethod
def _validate_legal_entity_type(cls, v):
if v is None or str(v).strip() == "":
return "natural"
s = str(v).strip().lower()
if s in ("legal", "حقوقی", "حقوقي"):
return "legal"
if s in ("natural", "حقیقی", "حقیقي"):
return "natural"
raise ValueError("legal_entity_type باید natural یا legal باشد")
# اطلاعات اقتصادی
national_id: Optional[str] = Field(default=None, max_length=20, description="شناسه ملی")
registration_number: Optional[str] = Field(default=None, max_length=50, description="شماره ثبت")
economic_id: Optional[str] = Field(default=None, max_length=50, description="شناسه اقتصادی")
# اطلاعات تماس
country: Optional[str] = Field(default=None, max_length=100, description="کشور")
province: Optional[str] = Field(default=None, max_length=100, description="استان")
city: Optional[str] = Field(default=None, max_length=100, description="شهرستان")
address: Optional[str] = Field(default=None, description="آدرس")
postal_code: Optional[str] = Field(default=None, max_length=20, description="کد پستی")
phone: Optional[str] = Field(default=None, max_length=20, description="تلفن")
mobile: Optional[str] = Field(default=None, max_length=20, description="موبایل")
mobile_2: Optional[str] = Field(default=None, max_length=20, description="موبایل ۲")
mobile_3: Optional[str] = Field(default=None, max_length=20, description="موبایل ۳")
fax: Optional[str] = Field(default=None, max_length=20, description="فکس")
email: Optional[str] = Field(default=None, max_length=255, description="پست الکترونیکی")
website: Optional[str] = Field(default=None, max_length=255, description="وب‌سایت")
# حساب‌های بانکی
bank_accounts: Optional[List[PersonBankAccountCreateRequest]] = Field(default=[], description="حساب‌های بانکی")
# پیام‌رسان / شبکه‌های اجتماعی
social_contacts: Optional[List[PersonSocialContactInput]] = Field(default=[], description="راه‌های ارتباط (تلگرام، اینستاگرام و …)")
# سهام
share_count: Optional[int] = Field(default=None, ge=1, description="تعداد سهام (برای سهامدار، اجباری و حداقل 1)")
# پورسانت (برای بازاریاب/فروشنده)
commission_sale_percent: Optional[float] = Field(default=None, ge=0, le=100, description="درصد پورسانت از فروش")
commission_sales_return_percent: Optional[float] = Field(default=None, ge=0, le=100, description="درصد پورسانت از برگشت از فروش")
commission_sales_amount: Optional[float] = Field(default=None, ge=0, description="مبلغ فروش مبنا")
commission_sales_return_amount: Optional[float] = Field(default=None, ge=0, description="مبلغ برگشت از فروش مبنا")
commission_exclude_discounts: Optional[bool] = Field(default=False, description="عدم محاسبه تخفیف")
commission_exclude_additions_deductions: Optional[bool] = Field(default=False, description="عدم محاسبه اضافات و کسورات")
commission_post_in_invoice_document: Optional[bool] = Field(default=False, description="ثبت پورسانت در سند فاکتور")
# اعتبار
credit_limit: Optional[float] = Field(default=None, ge=0, description="سقف اعتبار شخص")
credit_check_enabled: Optional[bool] = Field(default=None, description="فعال بودن بررسی اعتبار برای شخص (در صورت عدم ارسال، از تنظیمات کسب‌وکار تبعیت می‌کند)")
# مانده افتتاحیه (فقط در سند opening_balance ذخیره می‌شود؛ روی جدول persons ذخیره نمی‌شود)
opening_balance: Optional[PersonOpeningBalanceInput] = Field(
default=None,
description="مانده ابتدای دوره در سند تراز افتتاحیه سال مالی",
)
@classmethod
def __get_validators__(cls):
yield from super().__get_validators__()
@staticmethod
def _has_shareholder(person_type: Optional[PersonType], person_types: Optional[List[PersonType]]) -> bool:
if person_type == PersonType.SHAREHOLDER:
return True
if person_types:
return PersonType.SHAREHOLDER in person_types
return False
@classmethod
def validate(cls, value): # type: ignore[override]
obj = super().validate(value)
# اعتبارسنجی شرطی سهامدار
if cls._has_shareholder(getattr(obj, 'person_type', None), getattr(obj, 'person_types', None)):
sc = getattr(obj, 'share_count', None)
if sc is None or (isinstance(sc, int) and sc <= 0):
raise ValueError("برای سهامدار، مقدار تعداد سهام الزامی و باید بزرگتر از صفر باشد")
return obj
class PersonUpdateRequest(BaseModel):
"""درخواست ویرایش شخص"""
model_config = ConfigDict(extra="ignore")
# اطلاعات پایه
code: Optional[int] = Field(default=None, ge=1, description="کد یکتا در هر کسب و کار")
alias_name: Optional[str] = Field(default=None, min_length=1, max_length=255, description="نام مستعار")
first_name: Optional[str] = Field(default=None, max_length=100, description="نام")
last_name: Optional[str] = Field(default=None, max_length=100, description="نام خانوادگی")
person_type: Optional[PersonType] = Field(default=None, description="نوع شخص (سازگاری قدیمی)")
person_types: Optional[List[PersonType]] = Field(default=None, description="انواع شخص (چندانتخابی)")
company_name: Optional[str] = Field(default=None, max_length=255, description="نام شرکت")
name_prefix: Optional[str] = Field(default=None, max_length=64, description="پیشوند")
legal_entity_type: Optional[str] = Field(default=None, description="نوع حقوقی: natural یا legal")
payment_id: Optional[str] = Field(default=None, max_length=100, description="شناسه پرداخت")
person_group_id: Optional[int] = Field(default=None, description="گروه اشخاص (خالی برای حذف انتساب)")
@field_validator("name_prefix", mode="before")
@classmethod
def _validate_name_prefix_upd(cls, v):
if v is None:
return None
s = str(v).strip()
if not s:
return None
if s not in ALLOWED_PERSON_NAME_PREFIXES:
raise ValueError(f"پیشوند نامعتبر است. مقادیر مجاز: {', '.join(sorted(ALLOWED_PERSON_NAME_PREFIXES))}")
return s
@field_validator("legal_entity_type", mode="before")
@classmethod
def _validate_legal_entity_type_upd(cls, v):
if v is None or str(v).strip() == "":
return None
s = str(v).strip().lower()
if s in ("legal", "حقوقی", "حقوقي"):
return "legal"
if s in ("natural", "حقیقی", "حقیقي"):
return "natural"
raise ValueError("legal_entity_type باید natural یا legal باشد")
# اطلاعات اقتصادی
national_id: Optional[str] = Field(default=None, max_length=20, description="شناسه ملی")
registration_number: Optional[str] = Field(default=None, max_length=50, description="شماره ثبت")
economic_id: Optional[str] = Field(default=None, max_length=50, description="شناسه اقتصادی")
# اطلاعات تماس
country: Optional[str] = Field(default=None, max_length=100, description="کشور")
province: Optional[str] = Field(default=None, max_length=100, description="استان")
city: Optional[str] = Field(default=None, max_length=100, description="شهرستان")
address: Optional[str] = Field(default=None, description="آدرس")
postal_code: Optional[str] = Field(default=None, max_length=20, description="کد پستی")
phone: Optional[str] = Field(default=None, max_length=20, description="تلفن")
mobile: Optional[str] = Field(default=None, max_length=20, description="موبایل")
mobile_2: Optional[str] = Field(default=None, max_length=20, description="موبایل ۲")
mobile_3: Optional[str] = Field(default=None, max_length=20, description="موبایل ۳")
fax: Optional[str] = Field(default=None, max_length=20, description="فکس")
email: Optional[str] = Field(default=None, max_length=255, description="پست الکترونیکی")
website: Optional[str] = Field(default=None, max_length=255, description="وب‌سایت")
# سهام
share_count: Optional[int] = Field(default=None, ge=1, description="تعداد سهام (برای سهامدار)")
# پورسانت
commission_sale_percent: Optional[float] = Field(default=None, ge=0, le=100, description="درصد پورسانت از فروش")
commission_sales_return_percent: Optional[float] = Field(default=None, ge=0, le=100, description="درصد پورسانت از برگشت از فروش")
commission_sales_amount: Optional[float] = Field(default=None, ge=0, description="مبلغ فروش مبنا")
commission_sales_return_amount: Optional[float] = Field(default=None, ge=0, description="مبلغ برگشت از فروش مبنا")
commission_exclude_discounts: Optional[bool] = Field(default=None, description="عدم محاسبه تخفیف")
commission_exclude_additions_deductions: Optional[bool] = Field(default=None, description="عدم محاسبه اضافات و کسورات")
commission_post_in_invoice_document: Optional[bool] = Field(default=None, description="ثبت پورسانت در سند فاکتور")
# اعتبار
credit_limit: Optional[float] = Field(default=None, ge=0, description="سقف اعتبار شخص")
credit_check_enabled: Optional[bool] = Field(default=None, description="فعال بودن بررسی اعتبار برای شخص (خالی یعنی تبعیت از تنظیمات کسب‌وکار)")
# حساب‌های بانکی شخص (در صورت ارسال، کل لیست جایگزین قبلی می‌شود)
bank_accounts: Optional[List[PersonBankAccountCreateRequest]] = Field(
default=None,
description="حساب‌های بانکی شخص؛ اگر ارسال شود جایگزین کامل لیست قبلی است",
)
# پیام‌رسان / شبکه‌های اجتماعی (در صورت ارسال، کل لیست جایگزین قبلی می‌شود)
social_contacts: Optional[List[PersonSocialContactInput]] = Field(default=None, description="راه‌های ارتباط؛ اگر ارسال شود جایگزین کامل است")
# مانده افتتاحیه سال جاری (فقط در سند opening_balance؛ در صورت عدم ارسال تغییری نمی‌کند)
opening_balance: Optional[PersonOpeningBalanceInput] = Field(
default=None,
description="به‌روزرسانی یا حذف مانده افتتاحیه در سند تراز افتتاحیه",
)
@classmethod
def __get_validators__(cls):
yield from super().__get_validators__()
@staticmethod
def _has_shareholder(person_type: Optional[PersonType], person_types: Optional[List[PersonType]]) -> bool:
if person_type == PersonType.SHAREHOLDER:
return True
if person_types:
return PersonType.SHAREHOLDER in person_types
return False
@classmethod
def validate(cls, value): # type: ignore[override]
obj = super().validate(value)
# اگر ورودی‌ها مشخصاً به سهامدار اشاره دارند، share_count باید معتبر باشد
if cls._has_shareholder(getattr(obj, 'person_type', None), getattr(obj, 'person_types', None)):
sc = getattr(obj, 'share_count', None)
if sc is None or (isinstance(sc, int) and sc <= 0):
raise ValueError("برای سهامدار، مقدار تعداد سهام الزامی و باید بزرگتر از صفر باشد")
return obj
class PersonResponse(BaseModel):
"""پاسخ اطلاعات شخص"""
id: int = Field(..., description="شناسه شخص")
business_id: int = Field(..., description="شناسه کسب و کار")
# اطلاعات پایه
code: Optional[int] = Field(default=None, description="کد یکتا")
alias_name: str = Field(..., description="نام مستعار")
first_name: Optional[str] = Field(default=None, description="نام")
last_name: Optional[str] = Field(default=None, description="نام خانوادگی")
person_type: str = Field(..., description="نوع شخص")
person_types: List[str] = Field(default_factory=list, description="انواع شخص")
company_name: Optional[str] = Field(default=None, description="نام شرکت")
name_prefix: Optional[str] = Field(default=None, description="پیشوند نام")
legal_entity_type: str = Field(default="natural", description="نوع حقوقی: natural یا legal")
payment_id: Optional[str] = Field(default=None, description="شناسه پرداخت")
person_group_id: Optional[int] = Field(default=None, description="شناسه گروه اشخاص")
person_group_name: Optional[str] = Field(default=None, description="نام گروه اشخاص")
# اطلاعات اقتصادی
national_id: Optional[str] = Field(default=None, description="شناسه ملی")
registration_number: Optional[str] = Field(default=None, description="شماره ثبت")
economic_id: Optional[str] = Field(default=None, description="شناسه اقتصادی")
# اطلاعات تماس
country: Optional[str] = Field(default=None, description="کشور")
province: Optional[str] = Field(default=None, description="استان")
city: Optional[str] = Field(default=None, description="شهرستان")
address: Optional[str] = Field(default=None, description="آدرس")
postal_code: Optional[str] = Field(default=None, description="کد پستی")
phone: Optional[str] = Field(default=None, description="تلفن")
mobile: Optional[str] = Field(default=None, description="موبایل")
mobile_2: Optional[str] = Field(default=None, description="موبایل ۲")
mobile_3: Optional[str] = Field(default=None, description="موبایل ۳")
fax: Optional[str] = Field(default=None, description="فکس")
email: Optional[str] = Field(default=None, description="پست الکترونیکی")
website: Optional[str] = Field(default=None, description="وب‌سایت")
# زمان‌بندی
created_at: str = Field(..., description="تاریخ ایجاد")
updated_at: str = Field(..., description="تاریخ آخرین بروزرسانی")
# حساب‌های بانکی
bank_accounts: List[PersonBankAccountResponse] = Field(default=[], description="حساب‌های بانکی")
social_contacts: List[PersonSocialContactResponse] = Field(default_factory=list, description="پیام‌رسان و شبکه‌های اجتماعی")
# سهام
share_count: Optional[int] = Field(default=None, description="تعداد سهام")
# پورسانت
commission_sale_percent: Optional[float] = Field(default=None, description="درصد پورسانت از فروش")
commission_sales_return_percent: Optional[float] = Field(default=None, description="درصد پورسانت از برگشت از فروش")
commission_sales_amount: Optional[float] = Field(default=None, description="مبلغ فروش مبنا")
commission_sales_return_amount: Optional[float] = Field(default=None, description="مبلغ برگشت از فروش مبنا")
commission_exclude_discounts: Optional[bool] = Field(default=False, description="عدم محاسبه تخفیف")
commission_exclude_additions_deductions: Optional[bool] = Field(default=False, description="عدم محاسبه اضافات و کسورات")
commission_post_in_invoice_document: Optional[bool] = Field(default=False, description="ثبت پورسانت در سند فاکتور")
# اعتبار
credit_limit: Optional[float] = Field(default=None, description="سقف اعتبار شخص")
credit_check_enabled: Optional[bool] = Field(default=None, description="فعال بودن بررسی اعتبار برای شخص")
# تراز و وضعیت مالی
balance: Optional[float] = Field(default=None, description="تراز شخص (بستانکار - بدهکار)")
status: Optional[str] = Field(default=None, description="وضعیت مالی (بستانکار/بدهکار/بالانس/بدون تراکنش)")
class Config:
from_attributes = True
class PersonListResponse(BaseModel):
"""پاسخ لیست اشخاص"""
items: List[PersonResponse] = Field(..., description="لیست اشخاص")
pagination: dict = Field(..., description="اطلاعات صفحه‌بندی")
query_info: dict = Field(..., description="اطلاعات جستجو و فیلتر")
class PersonSummaryResponse(BaseModel):
"""پاسخ خلاصه اشخاص"""
total_persons: int = Field(..., description="تعداد کل اشخاص")
by_type: dict = Field(..., description="تعداد بر اساس نوع")
active_persons: int = Field(..., description="تعداد اشخاص فعال")
inactive_persons: int = Field(..., description="تعداد اشخاص غیرفعال")
class PersonShareLinkOptions(BaseModel):
"""تنظیمات محتوای لینک اشتراک"""
include_ledger: bool = Field(
default=True,
description="آیا کارت حساب (تراکنش‌ها) برای مشتری نمایش داده شود",
)
include_invoices: bool = Field(
default=True, description="آیا فهرست فاکتورها نمایش داده شود"
)
include_invoice_lines: bool = Field(
default=True,
description="در کارت حساب، ریز اقلام خرید/فروش همراه دریافت/پرداخت نمایش داده شود",
)
documents_limit: int = Field(
default=50,
ge=10,
le=200,
description="حداکثر تعداد ردیف برای لیست‌ها",
)
class PersonShareLinkCreateRequest(BaseModel):
"""درخواست ایجاد یا به‌روزرسانی لینک اشتراک"""
expires_in_hours: Optional[int] = Field(
default=None,
ge=1,
le=720,
description="مدت اعتبار لینک (ساعت). در صورت عدم ارسال از مقدار پیش‌فرض تنظیمات استفاده می‌شود.",
)
max_view_count: Optional[int] = Field(
default=None,
ge=1,
le=1000,
description="حداکثر تعداد بازدید مجاز. خالی یعنی بدون محدودیت.",
)
replace_existing: bool = Field(
default=True,
description="در صورت وجود لینک فعال، ابتدا لغو و لینک جدید ایجاد شود",
)
options: PersonShareLinkOptions = Field(
default_factory=PersonShareLinkOptions,
description="تنظیمات محتوای قابل نمایش برای مشتری",
)
class PersonShareLinkResponse(BaseModel):
"""پاسخ اطلاعات لینک اشتراک"""
id: int
business_id: int
person_id: int
code: str
short_url: str
created_at: str
expires_at: Optional[str]
revoked_at: Optional[str]
last_view_at: Optional[str]
view_count: int
max_view_count: Optional[int]
is_active: bool
is_expired: bool
status: str
remaining_hours: Optional[float]
options: PersonShareLinkOptions