894 lines
28 KiB
Python
894 lines
28 KiB
Python
"""CRM 客户 CRUD(客户主表 + 转移/成交/公海池操作)"""
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from datetime import datetime, timedelta, date
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
from sqlalchemy import and_, func, or_
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.delete_guard import assert_row_deletable_by_creator
|
|
from app.core.utils import generate_uuid
|
|
from app.models.crm_models import CrmCustomer
|
|
from app.models.models import SysUser
|
|
from app.crud.crm.data_scope_helpers import (
|
|
apply_crm_create_by_scope,
|
|
apply_crm_owner_scope,
|
|
ensure_crm_owner_row_in_scope,
|
|
)
|
|
from app.utils.pinyin_abbr import resolve_name_pinyin_abbr
|
|
|
|
|
|
def _ensure_customer_in_scope(db: Session, customer: CrmCustomer, user_info, tenant_no: str) -> None:
|
|
ensure_crm_owner_row_in_scope(db, customer, user_info, tenant_no, message="无权访问该客户")
|
|
|
|
|
|
def total_pages(total: int, page_size: int) -> int:
|
|
if page_size <= 0:
|
|
return 0
|
|
return math.ceil(total / page_size)
|
|
|
|
|
|
_FMT = "%Y-%m-%d %H:%M:%S"
|
|
_DATE_FMT = "%Y-%m-%d"
|
|
|
|
|
|
def _parse_optional_date(value: Any) -> Optional[date]:
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, date) and not isinstance(value, datetime):
|
|
return value
|
|
if isinstance(value, datetime):
|
|
return value.date()
|
|
text = str(value).strip()
|
|
if not text:
|
|
return None
|
|
for fmt in (_DATE_FMT, _FMT):
|
|
try:
|
|
return datetime.strptime(text, fmt).date()
|
|
except ValueError:
|
|
continue
|
|
raise ValueError(f"日期格式无效: {text}")
|
|
|
|
|
|
def _parse_optional_datetime(value: Any) -> Optional[datetime]:
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, datetime):
|
|
return value
|
|
text = str(value).strip()
|
|
if not text:
|
|
return None
|
|
for fmt in (_FMT, _DATE_FMT):
|
|
try:
|
|
return datetime.strptime(text, fmt)
|
|
except ValueError:
|
|
continue
|
|
raise ValueError(f"日期时间格式无效: {text}")
|
|
|
|
|
|
def _resolve_user_display_name(db: Session, user_id: Optional[str]) -> str:
|
|
uid = (user_id or "").strip()
|
|
if not uid:
|
|
return ""
|
|
u = db.query(SysUser).filter(SysUser.id == uid, SysUser.del_flag == 0).first()
|
|
if not u:
|
|
return ""
|
|
return (u.nick_name or u.user_no or "").strip()
|
|
|
|
|
|
def _enrich_user_names(db: Session, rows: List[Dict[str, Any]]) -> None:
|
|
field_pairs = (
|
|
("ownerUserId", "ownerUserName"),
|
|
("createBy", "createByName"),
|
|
("updateBy", "updateByName"),
|
|
)
|
|
need_ids: set[str] = set()
|
|
for d in rows:
|
|
for id_field, name_field in field_pairs:
|
|
uid = str(d.get(id_field) or "").strip()
|
|
if uid and not (d.get(name_field) or "").strip():
|
|
need_ids.add(uid)
|
|
if not need_ids:
|
|
return
|
|
users = (
|
|
db.query(SysUser.id, SysUser.nick_name, SysUser.user_no)
|
|
.filter(SysUser.id.in_(need_ids), SysUser.del_flag == 0)
|
|
.all()
|
|
)
|
|
name_map = {u.id: (u.nick_name or u.user_no or "").strip() for u in users}
|
|
for d in rows:
|
|
for id_field, name_field in field_pairs:
|
|
uid = str(d.get(id_field) or "").strip()
|
|
if uid and not (d.get(name_field) or "").strip():
|
|
d[name_field] = name_map.get(uid, "")
|
|
|
|
|
|
def _row_to_dict(row: CrmCustomer) -> Dict[str, Any]:
|
|
return {
|
|
"id": row.id,
|
|
"customerNo": row.customer_no or "",
|
|
"customerName": row.customer_name or "",
|
|
"namePinyinAbbr": getattr(row, "name_pinyin_abbr", None) or "",
|
|
"shortName": row.short_name or "",
|
|
"customerType": row.customer_type or "",
|
|
"mobile": row.mobile or "",
|
|
"telephone": row.telephone or "",
|
|
"email": row.email or "",
|
|
"website": row.website or "",
|
|
"level": row.level or "",
|
|
"source": row.source or "",
|
|
"industry": row.industry or "",
|
|
"parkArea": row.park_area or "",
|
|
"paymentTerms": row.payment_terms or "",
|
|
"scale": row.scale or "",
|
|
"province": row.province or "",
|
|
"city": row.city or "",
|
|
"district": row.district or "",
|
|
"address": row.address or "",
|
|
"status": row.status or "",
|
|
"dealStatus": row.deal_status or "",
|
|
"ownerUserId": row.owner_user_id or "",
|
|
"ownerUserName": row.owner_user_name or "",
|
|
"lastTime": row.last_time.strftime(_FMT) if row.last_time else "",
|
|
"nextTime": row.next_time.strftime(_FMT) if row.next_time else "",
|
|
"followCount": row.follow_count if row.follow_count is not None else 0,
|
|
"dealTime": row.deal_time.strftime(_FMT) if row.deal_time else "",
|
|
"dealAmount": float(row.deal_amount) if row.deal_amount else 0.0,
|
|
"contactCount": row.contact_count if row.contact_count is not None else 0,
|
|
"opportunityCount": row.opportunity_count if row.opportunity_count is not None else 0,
|
|
"contractCount": row.contract_count if row.contract_count is not None else 0,
|
|
"intoPoolTime": row.into_pool_time.strftime(_FMT) if row.into_pool_time else "",
|
|
"intoPoolBy": row.into_pool_by or "",
|
|
"intoPoolReason": row.into_pool_reason or "",
|
|
"claimTime": row.claim_time.strftime(_FMT) if row.claim_time else "",
|
|
"assignTime": row.assign_time.strftime(_FMT) if row.assign_time else "",
|
|
"assignBy": row.assign_by or "",
|
|
"protectTime": row.protect_time.strftime(_FMT) if row.protect_time else "",
|
|
"businessLicenseImg": row.business_license_img or "",
|
|
"businessLicenseImages": row.business_license_img or "",
|
|
"businessPermitImages": row.business_permit_images or "",
|
|
"otherCertImages": row.other_cert_images or "",
|
|
"clueId": row.clue_id or "",
|
|
"orderNum": row.order_num if row.order_num is not None else 0,
|
|
"remark": row.remark or "",
|
|
"createBy": row.create_by or "",
|
|
"createTime": row.create_time.strftime(_FMT) if row.create_time else "",
|
|
"updateBy": row.update_by or "",
|
|
"updateTime": row.update_time.strftime(_FMT) if row.update_time else "",
|
|
}
|
|
|
|
|
|
def list_customer_options(
|
|
db: Session,
|
|
tenant_no: str,
|
|
*,
|
|
keyword: str = None,
|
|
customer_name: str = None,
|
|
limit: int = 50,
|
|
) -> List[Dict[str, Any]]:
|
|
"""客户下拉选项:租户内只读搜索,不做 owner 数据权限(供地磅/合同等主数据选用)。"""
|
|
q = db.query(CrmCustomer).filter(
|
|
CrmCustomer.tenant_no == tenant_no,
|
|
CrmCustomer.del_flag == 0,
|
|
)
|
|
kw = (keyword or "").strip()
|
|
if kw:
|
|
like = f"%{kw}%"
|
|
q = q.filter(
|
|
or_(
|
|
CrmCustomer.customer_name.like(like),
|
|
CrmCustomer.customer_no.like(like),
|
|
CrmCustomer.short_name.like(like),
|
|
func.lower(func.coalesce(CrmCustomer.name_pinyin_abbr, "")).like(
|
|
f"%{kw.lower()}%"
|
|
),
|
|
)
|
|
)
|
|
name = (customer_name or "").strip()
|
|
if name:
|
|
q = q.filter(
|
|
or_(
|
|
CrmCustomer.customer_name.like(f"%{name}%"),
|
|
func.lower(func.coalesce(CrmCustomer.name_pinyin_abbr, "")).like(
|
|
f"%{name.lower()}%"
|
|
),
|
|
)
|
|
)
|
|
|
|
limit = min(max(1, int(limit or 50)), 200)
|
|
rows = q.order_by(CrmCustomer.create_time.desc()).limit(limit).all()
|
|
return [
|
|
{
|
|
"id": r.id,
|
|
"customerName": r.customer_name,
|
|
"customerNo": r.customer_no,
|
|
"label": r.customer_name,
|
|
"value": r.id,
|
|
}
|
|
for r in rows
|
|
]
|
|
|
|
|
|
def list_customers(
|
|
db: Session,
|
|
tenant_no: str,
|
|
user_id: str = None,
|
|
*,
|
|
page: int = 1,
|
|
page_size: int = 10,
|
|
keyword: str = None,
|
|
view_mode: str = None,
|
|
customer_name: str = None,
|
|
customer_no: str = None,
|
|
status: str = None,
|
|
deal_status: str = None,
|
|
level: str = None,
|
|
source: str = None,
|
|
customer_type: str = None,
|
|
industry: str = None,
|
|
park_area: str = None,
|
|
owner_user_id: str = None,
|
|
create_time_start: str = None,
|
|
create_time_end: str = None,
|
|
user_info=None,
|
|
skip_data_scope: bool = False,
|
|
max_page_size: Optional[int] = None,
|
|
) -> Tuple[List[Dict[str, Any]], int]:
|
|
q = db.query(CrmCustomer).filter(
|
|
CrmCustomer.tenant_no == tenant_no,
|
|
CrmCustomer.del_flag == 0,
|
|
)
|
|
|
|
kw = (keyword or "").strip()
|
|
if kw:
|
|
like = f"%{kw}%"
|
|
q = q.filter(
|
|
or_(
|
|
CrmCustomer.customer_name.like(like),
|
|
CrmCustomer.customer_no.like(like),
|
|
CrmCustomer.mobile.like(like),
|
|
CrmCustomer.short_name.like(like),
|
|
func.lower(func.coalesce(CrmCustomer.name_pinyin_abbr, "")).like(
|
|
f"%{kw.lower()}%"
|
|
),
|
|
)
|
|
)
|
|
|
|
if view_mode == "mine" and user_id:
|
|
q = q.filter(CrmCustomer.owner_user_id == user_id)
|
|
q = q.filter(CrmCustomer.status == "normal")
|
|
elif view_mode == "toFollow":
|
|
q = q.filter(CrmCustomer.status == "normal")
|
|
q = q.filter(CrmCustomer.next_time.isnot(None))
|
|
q = q.filter(CrmCustomer.next_time <= datetime.now())
|
|
elif view_mode == "key":
|
|
q = q.filter(CrmCustomer.level == "key")
|
|
elif view_mode == "pool":
|
|
q = q.filter(CrmCustomer.status == "pool")
|
|
elif view_mode == "dealed":
|
|
q = q.filter(CrmCustomer.deal_status == "dealed")
|
|
elif view_mode == "weekNew":
|
|
week_ago = datetime.now() - timedelta(days=7)
|
|
q = q.filter(CrmCustomer.create_time >= week_ago)
|
|
elif view_mode == "longTimeNoFollow":
|
|
days_30_ago = datetime.now() - timedelta(days=30)
|
|
q = q.filter(
|
|
or_(
|
|
CrmCustomer.last_time.is_(None),
|
|
CrmCustomer.last_time <= days_30_ago,
|
|
)
|
|
)
|
|
|
|
if customer_name:
|
|
cn = customer_name.strip()
|
|
if cn:
|
|
q = q.filter(
|
|
or_(
|
|
CrmCustomer.customer_name.like(f"%{cn}%"),
|
|
func.lower(func.coalesce(CrmCustomer.name_pinyin_abbr, "")).like(
|
|
f"%{cn.lower()}%"
|
|
),
|
|
)
|
|
)
|
|
if customer_no:
|
|
q = q.filter(CrmCustomer.customer_no.like(f"%{customer_no}%"))
|
|
|
|
if status:
|
|
q = q.filter(CrmCustomer.status == status)
|
|
if deal_status:
|
|
q = q.filter(CrmCustomer.deal_status == deal_status)
|
|
if level:
|
|
q = q.filter(CrmCustomer.level == level)
|
|
if source:
|
|
q = q.filter(CrmCustomer.source == source)
|
|
if customer_type:
|
|
q = q.filter(CrmCustomer.customer_type == customer_type)
|
|
if industry:
|
|
q = q.filter(CrmCustomer.industry == industry)
|
|
if park_area:
|
|
q = q.filter(CrmCustomer.park_area == park_area)
|
|
if owner_user_id:
|
|
q = q.filter(CrmCustomer.owner_user_id == owner_user_id)
|
|
if create_time_start:
|
|
q = q.filter(CrmCustomer.create_time >= create_time_start)
|
|
if create_time_end:
|
|
q = q.filter(CrmCustomer.create_time <= create_time_end)
|
|
|
|
pool_scope_exempt = skip_data_scope or view_mode == "pool" or status == "pool"
|
|
if user_info and not pool_scope_exempt:
|
|
q = apply_crm_owner_scope(q, CrmCustomer, user_info, db, tenant_no)
|
|
|
|
total = q.order_by(None).with_entities(func.count(CrmCustomer.id)).scalar() or 0
|
|
page = max(1, page)
|
|
cap = max_page_size if max_page_size is not None else 500
|
|
page_size = min(max(1, page_size), cap)
|
|
|
|
order_by = CrmCustomer.create_time.desc()
|
|
if view_mode == "toFollow":
|
|
order_by = CrmCustomer.next_time.asc()
|
|
|
|
rows = (
|
|
q.order_by(order_by)
|
|
.offset((page - 1) * page_size)
|
|
.limit(page_size)
|
|
.all()
|
|
)
|
|
result = [_row_to_dict(r) for r in rows]
|
|
_enrich_user_names(db, result)
|
|
return result, int(total)
|
|
|
|
|
|
def list_pool_customers(
|
|
db: Session,
|
|
tenant_no: str,
|
|
*,
|
|
page: int = 1,
|
|
page_size: int = 10,
|
|
keyword: str = None,
|
|
level: str = None,
|
|
industry: str = None,
|
|
park_area: str = None,
|
|
user_info=None,
|
|
max_page_size: Optional[int] = None,
|
|
) -> Tuple[List[Dict[str, Any]], int]:
|
|
return list_customers(
|
|
db,
|
|
tenant_no,
|
|
page=page,
|
|
page_size=page_size,
|
|
keyword=keyword,
|
|
status="pool",
|
|
level=level,
|
|
industry=industry,
|
|
park_area=park_area,
|
|
user_info=user_info,
|
|
skip_data_scope=True,
|
|
max_page_size=max_page_size,
|
|
)
|
|
|
|
|
|
def get_customer(db: Session, customer_id: str) -> Optional[CrmCustomer]:
|
|
return (
|
|
db.query(CrmCustomer)
|
|
.filter(
|
|
CrmCustomer.id == customer_id,
|
|
CrmCustomer.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
|
|
|
|
def get_customer_by_no(db: Session, customer_no: str, tenant_no: str) -> Optional[CrmCustomer]:
|
|
return (
|
|
db.query(CrmCustomer)
|
|
.filter(
|
|
CrmCustomer.customer_no == customer_no,
|
|
CrmCustomer.tenant_no == tenant_no,
|
|
CrmCustomer.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
|
|
|
|
def generate_customer_no(db: Session, tenant_no: str) -> str:
|
|
today_str = datetime.now().strftime("%Y%m%d")
|
|
prefix = f"C{today_str}"
|
|
|
|
max_no = (
|
|
db.query(CrmCustomer.customer_no)
|
|
.filter(
|
|
CrmCustomer.customer_no.like(f"{prefix}%"),
|
|
CrmCustomer.tenant_no == tenant_no,
|
|
)
|
|
.order_by(CrmCustomer.customer_no.desc())
|
|
.first()
|
|
)
|
|
|
|
if max_no and max_no[0]:
|
|
seq_str = max_no[0][len(prefix):]
|
|
try:
|
|
seq = int(seq_str) + 1
|
|
except ValueError:
|
|
seq = 1
|
|
else:
|
|
seq = 1
|
|
|
|
return f"{prefix}{seq:04d}"
|
|
|
|
|
|
def customer_name_exists(
|
|
db: Session, tenant_no: str, name: str, exclude_id: str = None
|
|
) -> bool:
|
|
normalized = (name or "").strip()
|
|
if not normalized:
|
|
return False
|
|
q = db.query(CrmCustomer).filter(
|
|
CrmCustomer.customer_name == normalized,
|
|
CrmCustomer.tenant_no == tenant_no,
|
|
CrmCustomer.del_flag == 0,
|
|
)
|
|
if exclude_id:
|
|
q = q.filter(CrmCustomer.id != exclude_id)
|
|
return q.first() is not None
|
|
|
|
|
|
def check_duplicate_customer(
|
|
db: Session,
|
|
tenant_no: str,
|
|
mobile: str = None,
|
|
customer_name: str = None,
|
|
exclude_id: str = None,
|
|
) -> list:
|
|
conditions = [CrmCustomer.tenant_no == tenant_no, CrmCustomer.del_flag == 0]
|
|
if exclude_id:
|
|
conditions.append(CrmCustomer.id != exclude_id)
|
|
or_clauses = []
|
|
if mobile:
|
|
or_clauses.append(CrmCustomer.mobile == mobile)
|
|
if customer_name:
|
|
normalized_name = customer_name.strip()
|
|
if normalized_name:
|
|
or_clauses.append(CrmCustomer.customer_name == normalized_name)
|
|
if not or_clauses:
|
|
return []
|
|
rows = db.query(CrmCustomer).filter(*conditions).filter(or_(*or_clauses)).all()
|
|
return [_row_to_dict(r) for r in rows]
|
|
|
|
|
|
def create_customer(
|
|
db: Session,
|
|
*,
|
|
tenant_no: str,
|
|
user_id: str = None,
|
|
user_name: str = None,
|
|
dept_id: str = "",
|
|
**kwargs,
|
|
) -> CrmCustomer:
|
|
customer_name = (kwargs.get("customer_name") or "").strip()
|
|
if not customer_name:
|
|
raise ValueError("客户名称不能为空")
|
|
if customer_name_exists(db, tenant_no, customer_name):
|
|
raise ValueError("客户名称已存在")
|
|
kwargs["customer_name"] = customer_name
|
|
|
|
customer_no = kwargs.get("customer_no")
|
|
if not customer_no:
|
|
customer_no = generate_customer_no(db, tenant_no)
|
|
|
|
owner_id = (kwargs.pop("owner_user_id", None) or user_id or "").strip() or None
|
|
owner_name = _resolve_user_display_name(db, owner_id) or (user_name or "")
|
|
|
|
# 拼音首字母:新增始终按提交值解析(空则由客户名称生成);兼容 snake / camel 键
|
|
submitted = kwargs.get("name_pinyin_abbr")
|
|
if submitted is None and "namePinyinAbbr" in kwargs:
|
|
submitted = kwargs.get("namePinyinAbbr")
|
|
name_pinyin_abbr = resolve_name_pinyin_abbr(
|
|
name=kwargs.get("customer_name"),
|
|
submitted=submitted,
|
|
submitted_provided=True,
|
|
)
|
|
|
|
obj = CrmCustomer(
|
|
id=generate_uuid(),
|
|
customer_no=customer_no,
|
|
customer_name=kwargs.get("customer_name"),
|
|
name_pinyin_abbr=name_pinyin_abbr,
|
|
short_name=kwargs.get("short_name"),
|
|
customer_type=kwargs.get("customer_type", "company"),
|
|
mobile=kwargs.get("mobile"),
|
|
telephone=kwargs.get("telephone"),
|
|
email=kwargs.get("email"),
|
|
website=kwargs.get("website"),
|
|
level=kwargs.get("level", "normal"),
|
|
source=kwargs.get("source"),
|
|
industry=kwargs.get("industry"),
|
|
park_area=kwargs.get("park_area"),
|
|
payment_terms=kwargs.get("payment_terms"),
|
|
scale=kwargs.get("scale"),
|
|
province=kwargs.get("province"),
|
|
city=kwargs.get("city"),
|
|
district=kwargs.get("district"),
|
|
address=kwargs.get("address"),
|
|
status="normal",
|
|
deal_status="not_deal",
|
|
owner_user_id=owner_id,
|
|
owner_user_name=owner_name,
|
|
follow_count=0,
|
|
deal_amount=0,
|
|
contact_count=0,
|
|
opportunity_count=0,
|
|
contract_count=0,
|
|
business_license_img=kwargs.get("business_license_img"),
|
|
business_permit_images=kwargs.get("business_permit_images"),
|
|
other_cert_images=kwargs.get("other_cert_images"),
|
|
clue_id=kwargs.get("clue_id"),
|
|
next_time=_parse_optional_datetime(kwargs.get("next_time")),
|
|
tenant_no=tenant_no,
|
|
order_num=kwargs.get("order_num"),
|
|
remark=kwargs.get("remark"),
|
|
create_by=user_id,
|
|
create_dept_id=dept_id,
|
|
del_flag=0,
|
|
)
|
|
db.add(obj)
|
|
db.commit()
|
|
db.refresh(obj)
|
|
return obj
|
|
|
|
|
|
def update_customer(
|
|
db: Session,
|
|
customer_id: str,
|
|
*,
|
|
user_id: str = None,
|
|
user_info=None,
|
|
tenant_no: str = None,
|
|
**kwargs,
|
|
) -> CrmCustomer:
|
|
obj = get_customer(db, customer_id)
|
|
if not obj:
|
|
raise ValueError("客户不存在")
|
|
if tenant_no and obj.tenant_no != tenant_no:
|
|
raise ValueError("客户不存在")
|
|
_ensure_customer_in_scope(db, obj, user_info, obj.tenant_no)
|
|
|
|
if "customer_name" in kwargs and kwargs.get("customer_name") is not None:
|
|
normalized_name = (kwargs.get("customer_name") or "").strip()
|
|
if not normalized_name:
|
|
raise ValueError("客户名称不能为空")
|
|
if normalized_name != (obj.customer_name or "").strip():
|
|
if customer_name_exists(db, obj.tenant_no, normalized_name, exclude_id=customer_id):
|
|
raise ValueError("客户名称已存在")
|
|
kwargs["customer_name"] = normalized_name
|
|
|
|
# name_pinyin_abbr 单独处理,避免双重赋值
|
|
_updatable = (
|
|
"customer_name", "short_name", "customer_type", "mobile", "telephone",
|
|
"email", "website", "level", "source", "industry", "park_area",
|
|
"payment_terms", "scale", "province", "city", "district", "address",
|
|
"business_license_img",
|
|
"business_permit_images", "other_cert_images",
|
|
"clue_id", "order_num", "remark",
|
|
"next_time",
|
|
)
|
|
_datetime_fields = {"next_time"}
|
|
for field in _updatable:
|
|
if field not in kwargs:
|
|
continue
|
|
val = kwargs[field]
|
|
if field in _datetime_fields:
|
|
setattr(obj, field, _parse_optional_datetime(val))
|
|
elif val is not None:
|
|
setattr(obj, field, val)
|
|
|
|
# 拼音首字母:提交了则用提交值(空则重生);未提交则保留已有,已有空则按名称生成
|
|
name_keys = ("name_pinyin_abbr", "namePinyinAbbr")
|
|
submitted_provided = any(k in kwargs for k in name_keys)
|
|
submitted = kwargs.get("name_pinyin_abbr")
|
|
if submitted is None:
|
|
submitted = kwargs.get("namePinyinAbbr")
|
|
if "customer_name" in kwargs and kwargs.get("customer_name") is not None:
|
|
new_name = kwargs.get("customer_name")
|
|
else:
|
|
new_name = obj.customer_name
|
|
obj.name_pinyin_abbr = resolve_name_pinyin_abbr(
|
|
name=new_name,
|
|
submitted=submitted,
|
|
existing=obj.name_pinyin_abbr,
|
|
submitted_provided=submitted_provided,
|
|
)
|
|
|
|
if "owner_user_id" in kwargs:
|
|
owner_id = (kwargs.pop("owner_user_id") or "").strip() or None
|
|
obj.owner_user_id = owner_id
|
|
obj.owner_user_name = _resolve_user_display_name(db, owner_id) if owner_id else None
|
|
|
|
obj.update_by = user_id
|
|
obj.update_time = datetime.now()
|
|
db.commit()
|
|
db.refresh(obj)
|
|
return obj
|
|
|
|
|
|
def soft_delete_customer(
|
|
db: Session, customer_id: str, user_id: str = None, user_info=None, tenant_no: str = None
|
|
) -> bool:
|
|
obj = get_customer(db, customer_id)
|
|
if not obj:
|
|
raise ValueError("客户不存在")
|
|
if tenant_no and obj.tenant_no != tenant_no:
|
|
raise ValueError("客户不存在")
|
|
_ensure_customer_in_scope(db, obj, user_info, obj.tenant_no)
|
|
assert_row_deletable_by_creator(obj, user_info, user_id=user_id)
|
|
obj.del_flag = 1
|
|
obj.update_by = user_id
|
|
obj.update_time = datetime.now()
|
|
db.commit()
|
|
return True
|
|
|
|
|
|
def transfer_customer(
|
|
db: Session,
|
|
customer_id: str,
|
|
new_owner_id: str,
|
|
new_owner_name: str,
|
|
user_id: str = None,
|
|
user_info=None,
|
|
tenant_no: str = None,
|
|
) -> CrmCustomer:
|
|
obj = get_customer(db, customer_id)
|
|
if not obj:
|
|
raise ValueError("客户不存在")
|
|
if tenant_no and obj.tenant_no != tenant_no:
|
|
raise ValueError("客户不存在")
|
|
_ensure_customer_in_scope(db, obj, user_info, obj.tenant_no)
|
|
|
|
old_owner_id = obj.owner_user_id or ""
|
|
old_owner_name = obj.owner_user_name or ""
|
|
|
|
obj.owner_user_id = new_owner_id
|
|
obj.owner_user_name = new_owner_name
|
|
obj.update_by = user_id
|
|
obj.update_time = datetime.now()
|
|
db.commit()
|
|
db.refresh(obj)
|
|
|
|
from app.crud.crm.customer_pool import create_pool_record
|
|
create_pool_record(
|
|
db,
|
|
tenant_no=obj.tenant_no,
|
|
user_id=user_id,
|
|
customer_id=obj.id,
|
|
customer_name=obj.customer_name,
|
|
operation_type="assign",
|
|
from_user_id=old_owner_id,
|
|
from_user_name=old_owner_name,
|
|
to_user_id=new_owner_id,
|
|
to_user_name=new_owner_name,
|
|
)
|
|
return obj
|
|
|
|
|
|
def deal_customer(
|
|
db: Session,
|
|
customer_id: str,
|
|
deal_amount: float = 0,
|
|
deal_time: str = None,
|
|
user_id: str = None,
|
|
user_info=None,
|
|
tenant_no: str = None,
|
|
) -> CrmCustomer:
|
|
obj = get_customer(db, customer_id)
|
|
if not obj:
|
|
raise ValueError("客户不存在")
|
|
if tenant_no and obj.tenant_no != tenant_no:
|
|
raise ValueError("客户不存在")
|
|
_ensure_customer_in_scope(db, obj, user_info, obj.tenant_no)
|
|
|
|
obj.deal_status = "dealed"
|
|
if deal_amount:
|
|
obj.deal_amount = deal_amount
|
|
parsed_deal_time = _parse_optional_datetime(deal_time)
|
|
obj.deal_time = parsed_deal_time or datetime.now()
|
|
obj.update_by = user_id
|
|
obj.update_time = datetime.now()
|
|
db.commit()
|
|
db.refresh(obj)
|
|
return obj
|
|
|
|
|
|
def into_pool(
|
|
db: Session,
|
|
customer_id: str,
|
|
reason: str = None,
|
|
user_id: str = None,
|
|
user_info=None,
|
|
tenant_no: str = None,
|
|
) -> CrmCustomer:
|
|
obj = get_customer(db, customer_id)
|
|
if not obj:
|
|
raise ValueError("客户不存在")
|
|
if tenant_no and obj.tenant_no != tenant_no:
|
|
raise ValueError("客户不存在")
|
|
_ensure_customer_in_scope(db, obj, user_info, obj.tenant_no)
|
|
|
|
old_owner_id = obj.owner_user_id or ""
|
|
old_owner_name = obj.owner_user_name or ""
|
|
|
|
obj.status = "pool"
|
|
obj.into_pool_time = datetime.now()
|
|
obj.into_pool_by = user_id
|
|
obj.into_pool_reason = reason
|
|
obj.owner_user_id = None
|
|
obj.update_by = user_id
|
|
obj.update_time = datetime.now()
|
|
db.commit()
|
|
db.refresh(obj)
|
|
|
|
from app.crud.crm.customer_pool import create_pool_record
|
|
create_pool_record(
|
|
db,
|
|
tenant_no=obj.tenant_no,
|
|
user_id=user_id,
|
|
customer_id=obj.id,
|
|
customer_name=obj.customer_name,
|
|
operation_type="into",
|
|
from_user_id=old_owner_id,
|
|
from_user_name=old_owner_name,
|
|
reason=reason,
|
|
)
|
|
return obj
|
|
|
|
|
|
def claim_customer(
|
|
db: Session,
|
|
customer_id: str,
|
|
user_id: str,
|
|
user_name: str,
|
|
) -> CrmCustomer:
|
|
obj = get_customer(db, customer_id)
|
|
if not obj:
|
|
raise ValueError("客户不存在")
|
|
if obj.status != "pool":
|
|
raise ValueError("该客户不在公海池中")
|
|
|
|
obj.status = "normal"
|
|
obj.owner_user_id = user_id
|
|
obj.owner_user_name = user_name
|
|
obj.claim_time = datetime.now()
|
|
obj.into_pool_time = None
|
|
obj.into_pool_by = None
|
|
obj.into_pool_reason = None
|
|
obj.update_by = user_id
|
|
obj.update_time = datetime.now()
|
|
db.commit()
|
|
db.refresh(obj)
|
|
|
|
from app.crud.crm.customer_pool import create_pool_record
|
|
create_pool_record(
|
|
db,
|
|
tenant_no=obj.tenant_no,
|
|
user_id=user_id,
|
|
customer_id=obj.id,
|
|
customer_name=obj.customer_name,
|
|
operation_type="claim",
|
|
to_user_id=user_id,
|
|
to_user_name=user_name,
|
|
)
|
|
return obj
|
|
|
|
|
|
def batch_claim_customers(
|
|
db: Session,
|
|
ids: List[str],
|
|
user_id: str,
|
|
user_name: str,
|
|
) -> List[CrmCustomer]:
|
|
results = []
|
|
for cid in ids:
|
|
obj = claim_customer(db, cid, user_id, user_name)
|
|
results.append(obj)
|
|
return results
|
|
|
|
|
|
def assign_customer(
|
|
db: Session,
|
|
ids: List[str],
|
|
owner_user_id: str,
|
|
owner_user_name: str,
|
|
user_id: str = None,
|
|
user_info=None,
|
|
tenant_no: str = None,
|
|
) -> List[CrmCustomer]:
|
|
results = []
|
|
for cid in ids:
|
|
obj = get_customer(db, cid)
|
|
if not obj:
|
|
continue
|
|
if tenant_no and obj.tenant_no != tenant_no:
|
|
continue
|
|
_ensure_customer_in_scope(db, obj, user_info, obj.tenant_no)
|
|
|
|
old_owner_id = obj.owner_user_id or ""
|
|
old_owner_name = obj.owner_user_name or ""
|
|
|
|
obj.status = "normal"
|
|
obj.owner_user_id = owner_user_id
|
|
obj.owner_user_name = owner_user_name
|
|
obj.assign_time = datetime.now()
|
|
obj.assign_by = user_id
|
|
obj.into_pool_time = None
|
|
obj.into_pool_by = None
|
|
obj.into_pool_reason = None
|
|
obj.update_by = user_id
|
|
obj.update_time = datetime.now()
|
|
|
|
from app.crud.crm.customer_pool import create_pool_record
|
|
create_pool_record(
|
|
db,
|
|
tenant_no=obj.tenant_no,
|
|
user_id=user_id,
|
|
customer_id=obj.id,
|
|
customer_name=obj.customer_name,
|
|
operation_type="assign",
|
|
from_user_id=old_owner_id,
|
|
from_user_name=old_owner_name,
|
|
to_user_id=owner_user_id,
|
|
to_user_name=owner_user_name,
|
|
)
|
|
results.append(obj)
|
|
|
|
db.commit()
|
|
return results
|
|
|
|
|
|
def update_follow_stats(
|
|
db: Session,
|
|
customer_id: str,
|
|
follow_time: datetime = None,
|
|
next_time: datetime = None,
|
|
) -> None:
|
|
obj = get_customer(db, customer_id)
|
|
if not obj:
|
|
return
|
|
if follow_time:
|
|
obj.last_time = follow_time
|
|
if next_time:
|
|
obj.next_time = next_time
|
|
obj.follow_count = (obj.follow_count or 0) + 1
|
|
db.commit()
|
|
|
|
|
|
def update_contact_count(db: Session, customer_id: str) -> None:
|
|
from app.models.crm_models import CrmContact
|
|
count = (
|
|
db.query(func.count(CrmContact.id))
|
|
.filter(
|
|
CrmContact.customer_id == customer_id,
|
|
CrmContact.del_flag == 0,
|
|
)
|
|
.scalar() or 0
|
|
)
|
|
obj = get_customer(db, customer_id)
|
|
if obj:
|
|
obj.contact_count = count
|
|
db.commit()
|
|
|
|
|
|
def get_park_area_list(db: Session, tenant_no: str) -> list:
|
|
return [
|
|
r[0]
|
|
for r in db.query(CrmCustomer.park_area)
|
|
.filter(
|
|
CrmCustomer.tenant_no == tenant_no,
|
|
CrmCustomer.del_flag == 0,
|
|
CrmCustomer.park_area.isnot(None),
|
|
CrmCustomer.park_area != "",
|
|
)
|
|
.distinct()
|
|
.order_by(CrmCustomer.park_area)
|
|
.all()
|
|
]
|