Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/adapters/api/v1/schema_models/crm.py
2026-07-31 13:33:00 +00:00

521 lines
19 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.

# noqa: D100
"""Schema models برای CRM (فرایندها، مراحل، سرنخ، فرصت، فعالیت)"""
from __future__ import annotations
from typing import Optional, List, Dict
from datetime import datetime, date
from decimal import Decimal
from pydantic import BaseModel, Field, model_validator
# --- فرایند و مراحل ---
class CrmProcessStageCreate(BaseModel):
"""ایجاد مرحله"""
stage_code: str = Field(..., min_length=1, max_length=50)
name: str = Field(..., min_length=1, max_length=255)
order_index: int = Field(default=0, ge=0)
color: Optional[str] = Field(None, max_length=20)
is_win: bool = False
is_lost: bool = False
allow_transition_to: Optional[List[str]] = Field(None, description="لیست stage_codeهای مجاز برای انتقال")
class CrmProcessStageUpdate(BaseModel):
"""به‌روزرسانی مرحله"""
stage_code: Optional[str] = Field(None, min_length=1, max_length=50)
name: Optional[str] = Field(None, min_length=1, max_length=255)
order_index: Optional[int] = Field(None, ge=0)
color: Optional[str] = None
is_win: Optional[bool] = None
is_lost: Optional[bool] = None
allow_transition_to: Optional[List[str]] = None
class CrmProcessStageResponse(BaseModel):
"""پاسخ مرحله"""
id: int
process_definition_id: int
stage_code: str
name: str
order_index: int
color: Optional[str] = None
is_win: bool
is_lost: bool
allow_transition_to: Optional[dict] = None
created_at: str
updated_at: str
class Config:
from_attributes = True
class CrmProcessDefinitionCreate(BaseModel):
"""ایجاد فرایند"""
process_type: str = Field(
...,
description="lead_funnel | sales_pipeline | activity_type | lead_source",
min_length=1,
max_length=50,
)
code: str = Field(..., min_length=1, max_length=50)
name: str = Field(..., min_length=1, max_length=255)
description: Optional[str] = None
is_default: bool = False
is_active: bool = True
stages: Optional[List[CrmProcessStageCreate]] = Field(default_factory=list)
class CrmProcessDefinitionUpdate(BaseModel):
"""به‌روزرسانی فرایند"""
name: Optional[str] = Field(None, min_length=1, max_length=255)
description: Optional[str] = None
is_default: Optional[bool] = None
is_active: Optional[bool] = None
class CrmProcessDefinitionResponse(BaseModel):
"""پاسخ فرایند"""
id: int
business_id: int
process_type: str
code: str
name: str
description: Optional[str] = None
is_default: bool
is_active: bool
created_at: str
updated_at: str
created_by_user_id: Optional[int] = None
stages: Optional[List[CrmProcessStageResponse]] = None
class Config:
from_attributes = True
# --- سرنخ (Lead) ---
class LeadCreate(BaseModel):
"""ایجاد سرنخ"""
process_definition_id: int = Field(..., gt=0)
stage_id: int = Field(..., gt=0)
code: Optional[str] = Field(None, max_length=50, description="کد دستی؛ در صورت خالی بودن، کد خودکار تولید می‌شود")
source_code: Optional[str] = Field(None, max_length=50)
name: str = Field(..., min_length=1, max_length=255)
company_name: Optional[str] = Field(None, max_length=255)
mobile: Optional[str] = Field(None, max_length=20)
email: Optional[str] = Field(None, max_length=255)
description: Optional[str] = None
assigned_to_user_id: Optional[int] = None
next_follow_up_at: Optional[datetime] = None
extra_info: Optional[dict] = None
custom_fields: Optional[Dict[str, object]] = Field(None, description="مقادیر فیلدهای سفارشی")
tag_ids: Optional[List[int]] = Field(None, description="شناسه برچسب‌ها")
class LeadUpdate(BaseModel):
"""به‌روزرسانی سرنخ"""
stage_id: Optional[int] = Field(None, gt=0)
code: Optional[str] = Field(None, max_length=50)
source_code: Optional[str] = None
name: Optional[str] = Field(None, min_length=1, max_length=255)
company_name: Optional[str] = None
mobile: Optional[str] = None
email: Optional[str] = None
description: Optional[str] = None
assigned_to_user_id: Optional[int] = None
next_follow_up_at: Optional[datetime] = None
extra_info: Optional[dict] = None
custom_fields: Optional[Dict[str, object]] = None
tag_ids: Optional[List[int]] = None
class LeadConvertDealOption(BaseModel):
"""اختیاری: ایجاد فرصت فروش همزمان با تبدیل سرنخ"""
process_definition_id: int = Field(..., gt=0)
stage_id: int = Field(..., gt=0)
title: str = Field(..., min_length=1, max_length=255)
amount: Decimal = Field(..., ge=0)
currency_id: Optional[int] = None
probability_percent: Optional[int] = Field(None, ge=0, le=100)
expected_close_date: Optional[date] = None
assigned_to_user_id: Optional[int] = None
description: Optional[str] = None
class LeadConvertRequest(BaseModel):
"""درخواست تبدیل سرنخ به مشتری (با امکان ایجاد فرصت فروش)"""
create_deal: Optional[LeadConvertDealOption] = Field(None, description="در صورت ارسال، همزمان یک فرصت فروش برای مشتری جدید ایجاد می‌شود")
class LeadResponse(BaseModel):
"""پاسخ سرنخ"""
id: int
business_id: int
code: Optional[str] = None
process_definition_id: int
stage_id: int
stage_name: Optional[str] = None
source_code: Optional[str] = None
name: str
company_name: Optional[str] = None
mobile: Optional[str] = None
email: Optional[str] = None
description: Optional[str] = None
assigned_to_user_id: Optional[int] = None
assigned_to_name: Optional[str] = None
next_follow_up_at: Optional[str] = None
person_id: Optional[int] = None
person_name: Optional[str] = None
converted_at: Optional[str] = None
score: Optional[int] = None
sla_due_at: Optional[str] = None
first_touched_at: Optional[str] = None
last_activity_at: Optional[str] = None
tags: Optional[List[dict]] = None
created_at: str
updated_at: str
created_by_user_id: int
created_by_name: Optional[str] = None
class Config:
from_attributes = True
# --- فرصت فروش (Deal) ---
class DealCreate(BaseModel):
"""ایجاد فرصت فروش"""
person_id: int = Field(..., gt=0)
code: Optional[str] = Field(None, max_length=50, description="کد دستی؛ در صورت خالی بودن، کد خودکار تولید می‌شود")
process_definition_id: int = Field(..., gt=0)
stage_id: int = Field(..., gt=0)
title: str = Field(..., min_length=1, max_length=255)
amount: Decimal = Field(..., ge=0)
currency_id: Optional[int] = None
probability_percent: Optional[int] = Field(None, ge=0, le=100)
expected_close_date: Optional[date] = None
next_follow_up_at: Optional[datetime] = None
assigned_to_user_id: Optional[int] = None
description: Optional[str] = None
extra_info: Optional[dict] = None
custom_fields: Optional[Dict[str, object]] = None
tag_ids: Optional[List[int]] = None
class DealUpdate(BaseModel):
"""به‌روزرسانی فرصت فروش"""
stage_id: Optional[int] = Field(None, gt=0)
code: Optional[str] = Field(None, max_length=50)
title: Optional[str] = Field(None, min_length=1, max_length=255)
amount: Optional[Decimal] = Field(None, ge=0)
currency_id: Optional[int] = None
probability_percent: Optional[int] = Field(None, ge=0, le=100)
expected_close_date: Optional[date] = None
next_follow_up_at: Optional[datetime] = None
document_id: Optional[int] = None
assigned_to_user_id: Optional[int] = None
description: Optional[str] = None
extra_info: Optional[dict] = None
closed_at: Optional[datetime] = None
won_reason_code: Optional[str] = Field(None, max_length=50)
lost_reason_code: Optional[str] = Field(None, max_length=50)
competitor_name: Optional[str] = Field(None, max_length=255)
custom_fields: Optional[Dict[str, object]] = None
tag_ids: Optional[List[int]] = None
class DealResponse(BaseModel):
"""پاسخ فرصت فروش"""
id: int
business_id: int
code: Optional[str] = None
person_id: int
person_name: Optional[str] = None
process_definition_id: int
stage_id: int
stage_name: Optional[str] = None
title: str
amount: Decimal
currency_id: Optional[int] = None
probability_percent: Optional[int] = None
expected_close_date: Optional[str] = None
next_follow_up_at: Optional[str] = None
closed_at: Optional[str] = None
document_id: Optional[int] = None
assigned_to_user_id: Optional[int] = None
assigned_to_name: Optional[str] = None
won_reason_code: Optional[str] = None
lost_reason_code: Optional[str] = None
competitor_name: Optional[str] = None
stage_entered_at: Optional[str] = None
tags: Optional[List[dict]] = None
created_at: str
updated_at: str
created_by_user_id: int
created_by_name: Optional[str] = None
class Config:
from_attributes = True
# --- فعالیت (Activity) ---
class CrmActivityCreate(BaseModel):
"""ایجاد فعالیت"""
person_id: Optional[int] = Field(None, gt=0, description="مشتری؛ برای سرنخ تبدیل‌نشده می‌توان خالی گذاشت")
lead_id: Optional[int] = Field(None, gt=0, description="سرنخ؛ حداقل یکی از person_id یا lead_id لازم است")
code: Optional[str] = Field(None, max_length=50, description="کد دستی؛ در صورت خالی بودن، کد خودکار تولید می‌شود")
activity_type: str = Field(..., description="call | email | meeting | note", max_length=50)
subject: Optional[str] = Field(None, max_length=255)
description: Optional[str] = None
activity_date: datetime
deal_id: Optional[int] = None
extra_info: Optional[dict] = None
# --- وظیفه/پیگیری ---
is_task: bool = False
due_at: Optional[datetime] = None
status: Optional[str] = Field(None, max_length=20, description="open | done | cancelled")
assigned_to_user_id: Optional[int] = None
outcome: Optional[str] = Field(None, max_length=100)
priority: Optional[str] = Field(None, max_length=20, description="low | normal | high | urgent")
custom_fields: Optional[Dict[str, object]] = None
@model_validator(mode="after")
def require_person_or_lead(self) -> "CrmActivityCreate":
if not self.person_id and not self.lead_id:
raise ValueError("یکی از person_id یا lead_id الزامی است")
return self
class CrmActivityUpdate(BaseModel):
"""به‌روزرسانی فعالیت"""
code: Optional[str] = Field(None, max_length=50)
activity_type: Optional[str] = Field(None, max_length=50)
subject: Optional[str] = None
description: Optional[str] = None
activity_date: Optional[datetime] = None
deal_id: Optional[int] = None
extra_info: Optional[dict] = None
is_task: Optional[bool] = None
due_at: Optional[datetime] = None
status: Optional[str] = Field(None, max_length=20)
assigned_to_user_id: Optional[int] = None
outcome: Optional[str] = Field(None, max_length=100)
priority: Optional[str] = Field(None, max_length=20)
custom_fields: Optional[Dict[str, object]] = None
class CrmActivityResponse(BaseModel):
"""پاسخ فعالیت"""
id: int
business_id: int
code: Optional[str] = None
person_id: Optional[int] = None
person_name: Optional[str] = None
lead_id: Optional[int] = None
lead_name: Optional[str] = None
activity_type: str
subject: Optional[str] = None
description: Optional[str] = None
activity_date: str
deal_id: Optional[int] = None
is_task: Optional[bool] = None
due_at: Optional[str] = None
status: Optional[str] = None
completed_at: Optional[str] = None
assigned_to_user_id: Optional[int] = None
assigned_to_name: Optional[str] = None
outcome: Optional[str] = None
priority: Optional[str] = None
created_by_user_id: int
created_by_name: Optional[str] = None
created_at: str
updated_at: str
class Config:
from_attributes = True
# --- یادداشت و تقویم CRM ---
class CrmNoteTypeCreate(BaseModel):
code: str = Field(..., min_length=1, max_length=50)
title_i18n: Dict[str, str] = Field(..., description="کلید زبان (fa, en) به متن نمایش")
scheduling_mode: str = Field(default="day_only", max_length=20, description="day_only | meeting")
allow_comments: bool = True
sort_order: int = Field(default=0, ge=0)
class CrmNoteTypeUpdate(BaseModel):
title_i18n: Optional[Dict[str, str]] = None
scheduling_mode: Optional[str] = Field(None, max_length=20)
allow_comments: Optional[bool] = None
is_active: Optional[bool] = None
sort_order: Optional[int] = Field(None, ge=0)
class CrmNoteCreate(BaseModel):
note_type_id: int = Field(..., gt=0)
visibility: str = Field(..., max_length=20, description="private | business_public | shared")
title: Optional[str] = Field(None, max_length=255)
body: str = Field(..., min_length=1)
occurs_on: Optional[date] = Field(None, description="برای day_only؛ برای meeting از starts_at پر می‌شود")
starts_at: Optional[datetime] = None
ends_at: Optional[datetime] = None
lead_id: Optional[int] = Field(None, gt=0)
shared_user_ids: Optional[List[int]] = Field(
default=None,
description="برای visibility=shared: شناسه کاربران عضو کسب‌وکار (غیر از سازنده)",
)
class CrmNoteUpdate(BaseModel):
note_type_id: Optional[int] = Field(None, gt=0)
visibility: Optional[str] = Field(None, max_length=20)
title: Optional[str] = Field(None, max_length=255)
body: Optional[str] = Field(None, min_length=1)
occurs_on: Optional[date] = None
starts_at: Optional[datetime] = None
ends_at: Optional[datetime] = None
lead_id: Optional[int] = None
clear_lead: bool = False
status: Optional[str] = Field(None, max_length=20)
shared_user_ids: Optional[List[int]] = None
class CrmNoteCommentCreate(BaseModel):
body: str = Field(..., min_length=1)
# --- برچسب‌ها (Tags) ---
class CrmTagCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=120)
color: Optional[str] = Field(None, max_length=20)
sort_order: int = Field(default=0, ge=0)
class CrmTagUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=120)
color: Optional[str] = Field(None, max_length=20)
is_active: Optional[bool] = None
sort_order: Optional[int] = Field(None, ge=0)
class CrmSetTagsRequest(BaseModel):
tag_ids: List[int] = Field(default_factory=list)
# --- دلایل بستن معامله ---
class CrmCloseReasonCreate(BaseModel):
reason_type: str = Field(..., description="won | lost", max_length=20)
code: str = Field(..., min_length=1, max_length=50)
name: str = Field(..., min_length=1, max_length=255)
sort_order: int = Field(default=0, ge=0)
class CrmCloseReasonUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=255)
is_active: Optional[bool] = None
sort_order: Optional[int] = Field(None, ge=0)
# --- فیلدهای سفارشی ---
class CrmCustomFieldCreate(BaseModel):
entity_type: str = Field(..., description="lead | deal | activity", max_length=20)
field_key: str = Field(..., min_length=1, max_length=80)
label: str = Field(..., min_length=1, max_length=255)
field_type: str = Field(..., description="text | number | date | select | boolean", max_length=20)
options: Optional[List[str]] = Field(None, description="گزینه‌ها برای نوع select")
is_required: bool = False
sort_order: int = Field(default=0, ge=0)
class CrmCustomFieldUpdate(BaseModel):
label: Optional[str] = Field(None, min_length=1, max_length=255)
options: Optional[List[str]] = None
is_required: Optional[bool] = None
sort_order: Optional[int] = Field(None, ge=0)
is_active: Optional[bool] = None
# --- خطوط فرصت فروش ---
class CrmDealLineInput(BaseModel):
product_id: Optional[int] = Field(None, gt=0)
description: Optional[str] = Field(None, max_length=500)
quantity: Decimal = Field(..., gt=0)
unit_price: Decimal = Field(default=Decimal(0), ge=0)
discount_percent: Decimal = Field(default=Decimal(0), ge=0, le=100)
sort_order: int = Field(default=0, ge=0)
class CrmDealLinesReplaceRequest(BaseModel):
lines: List[CrmDealLineInput] = Field(default_factory=list)
# --- توالی‌های خودکار ---
class CrmSequenceStepInput(BaseModel):
step_order: int = Field(..., ge=0)
delay_hours: int = Field(default=0, ge=0)
action_type: str = Field(
...,
description="create_task | send_sms | notify_assignee | update_lead_stage | update_deal_stage",
max_length=50,
)
action_config: Optional[dict] = None
class CrmSequenceCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=255)
description: Optional[str] = None
is_active: bool = True
steps: Optional[List[CrmSequenceStepInput]] = Field(default_factory=list)
class CrmSequenceUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=255)
description: Optional[str] = None
is_active: Optional[bool] = None
steps: Optional[List[CrmSequenceStepInput]] = None
class CrmSequenceEnrollRequest(BaseModel):
entity_type: str = Field(..., description="lead | deal", max_length=20)
entity_id: int = Field(..., gt=0)
# --- تبدیل فرصت فروش به فاکتور ---
class CrmDealConvertToInvoiceRequest(BaseModel):
is_proforma: bool = Field(default=True, description="پیش‌فاکتور یا فاکتور نهایی")
close_as_won: bool = Field(default=False, description="بستن معامله به‌عنوان برد پس از ایجاد فاکتور")
won_reason_code: Optional[str] = Field(None, max_length=50)
warehouse_id: Optional[int] = Field(None, description="انبار برای خطوط دارای کالا (اختیاری)")
# --- تنظیمات اتوماسیون CRM ---
class CrmAutomationSettingsUpdate(BaseModel):
lead_sla_hours: Optional[int] = Field(None, ge=0)
auto_assign_enabled: Optional[bool] = None
auto_assign_user_ids: Optional[List[int]] = None
follow_up_notify_enabled: Optional[bool] = None
stale_deal_days: Optional[int] = Field(None, ge=0)
score_rules: Optional[dict] = None