feat(crm): 商机域接入数据权限过滤(PR3 Task 2.3)
主表列表/统计/看板/漏斗按 owner_user_id 过滤,CUD 与跟进/产品子表校验父商机 scope。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
3107d537f6
commit
ee531e9cfe
4 files changed
+265
-107
No files matched your search
+72
-11
@@ -2204,12 +2204,14 @@ async def opportunity_list(
|
||||
"""商机分页列表"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
list_body = body or OpportunityListBody()
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
rows, total = list_opportunities(
|
||||
db,
|
||||
tenant_no,
|
||||
user_id,
|
||||
user_info=user_info,
|
||||
**_opp_list_kwargs(list_body, user_id),
|
||||
)
|
||||
return success_response({
|
||||
@@ -2235,12 +2237,14 @@ async def opportunity_mine_list(
|
||||
"""我的商机列表"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
list_body = body or OpportunityListBody()
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
rows, total = list_opportunities(
|
||||
db,
|
||||
tenant_no,
|
||||
user_id,
|
||||
user_info=user_info,
|
||||
**_opp_list_kwargs(list_body, user_id, view_type="mine"),
|
||||
)
|
||||
return success_response({
|
||||
@@ -2266,6 +2270,17 @@ async def opportunity_detail(
|
||||
"""商机详情"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
from app.models.crm_models import CrmOpportunity
|
||||
|
||||
row = db.query(CrmOpportunity).filter(
|
||||
CrmOpportunity.id == body.id,
|
||||
CrmOpportunity.del_flag == 0,
|
||||
CrmOpportunity.tenant_no == tenant_no,
|
||||
).first()
|
||||
if not row:
|
||||
return error_response(404, "商机不存在")
|
||||
ensure_crm_owner_row_in_scope(db, row, user_info, tenant_no, message="无权访问该商机")
|
||||
detail = get_opportunity(db, body.id, tenant_no=tenant_no)
|
||||
if not detail:
|
||||
return error_response(404, "商机不存在")
|
||||
@@ -2285,6 +2300,7 @@ async def opportunity_add(
|
||||
"""新增商机"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
row = create_opportunity(
|
||||
db,
|
||||
@@ -2308,10 +2324,11 @@ async def opportunity_update(
|
||||
"""编辑商机"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
data = body.model_dump(by_alias=False, exclude_unset=True)
|
||||
oid = data.pop("id")
|
||||
row = update_opportunity(db, oid, data, user_id=user_id, tenant_no=tenant_no)
|
||||
row = update_opportunity(db, oid, data, user_id=user_id, tenant_no=tenant_no, user_info=user_info)
|
||||
return success_response(row, message="更新成功")
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
@@ -2328,9 +2345,10 @@ async def opportunity_delete(
|
||||
"""删除商机(软删)"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
count = soft_delete_opportunities(
|
||||
db, body.ids, user_id=user_id, tenant_no=tenant_no
|
||||
db, body.ids, user_id=user_id, tenant_no=tenant_no, user_info=user_info
|
||||
)
|
||||
return success_response({"count": count}, message="删除成功")
|
||||
except ValueError as e:
|
||||
@@ -2348,6 +2366,7 @@ async def opportunity_assign(
|
||||
"""分配商机"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
count = assign_opportunities(
|
||||
db,
|
||||
@@ -2356,6 +2375,7 @@ async def opportunity_assign(
|
||||
user_id=user_id,
|
||||
remark=body.remark,
|
||||
tenant_no=tenant_no,
|
||||
user_info=user_info,
|
||||
)
|
||||
return success_response({"count": count}, message="分配成功")
|
||||
except ValueError as e:
|
||||
@@ -2373,6 +2393,7 @@ async def opportunity_change_stage(
|
||||
"""变更商机阶段"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
row = change_stage(
|
||||
db,
|
||||
@@ -2384,6 +2405,7 @@ async def opportunity_change_stage(
|
||||
remark=body.remark,
|
||||
user_id=user_id,
|
||||
tenant_no=tenant_no,
|
||||
user_info=user_info,
|
||||
)
|
||||
return success_response(row, message="阶段已更新")
|
||||
except ValueError as e:
|
||||
@@ -2401,6 +2423,7 @@ async def opportunity_mark_won(
|
||||
"""标记赢单"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
row = mark_won(
|
||||
db,
|
||||
@@ -2410,6 +2433,7 @@ async def opportunity_mark_won(
|
||||
remark=body.remark,
|
||||
user_id=user_id,
|
||||
tenant_no=tenant_no,
|
||||
user_info=user_info,
|
||||
)
|
||||
return success_response(row, message="已标记赢单")
|
||||
except ValueError as e:
|
||||
@@ -2427,6 +2451,7 @@ async def opportunity_mark_lost(
|
||||
"""标记输单"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
row = mark_lost(
|
||||
db,
|
||||
@@ -2437,6 +2462,7 @@ async def opportunity_mark_lost(
|
||||
improvement=body.improvement,
|
||||
user_id=user_id,
|
||||
tenant_no=tenant_no,
|
||||
user_info=user_info,
|
||||
)
|
||||
return success_response(row, message="已标记输单")
|
||||
except ValueError as e:
|
||||
@@ -2454,6 +2480,7 @@ async def opportunity_batch_update_stage(
|
||||
"""批量变更阶段"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
result = batch_update_stage(
|
||||
db,
|
||||
@@ -2464,6 +2491,7 @@ async def opportunity_batch_update_stage(
|
||||
body.change_reason,
|
||||
tenant_no=tenant_no,
|
||||
user_id=user_id,
|
||||
user_info=user_info,
|
||||
)
|
||||
return success_response(result)
|
||||
except ValueError as e:
|
||||
@@ -2481,6 +2509,7 @@ async def opportunity_batch_update_expect_date(
|
||||
"""批量修改预计成交日期"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
result = batch_update_expect_date(
|
||||
db,
|
||||
@@ -2490,6 +2519,7 @@ async def opportunity_batch_update_expect_date(
|
||||
days=body.days,
|
||||
tenant_no=tenant_no,
|
||||
user_id=user_id,
|
||||
user_info=user_info,
|
||||
)
|
||||
return success_response(result)
|
||||
except ValueError as e:
|
||||
@@ -2507,7 +2537,8 @@ async def opportunity_stage_log_list(
|
||||
"""商机阶段变更日志"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
logs = get_stage_log_list(db, body.opportunity_id, tenant_no=tenant_no)
|
||||
user_info = crm_user_info(_user)
|
||||
logs = get_stage_log_list(db, body.opportunity_id, tenant_no=tenant_no, user_info=user_info)
|
||||
return success_response(logs)
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
@@ -2524,6 +2555,7 @@ async def opportunity_kanban(
|
||||
"""看板数据"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
list_body = body or OpportunityKanbanBody()
|
||||
list_kwargs = _opp_list_kwargs(list_body, "")
|
||||
data = get_kanban_data(
|
||||
@@ -2536,6 +2568,7 @@ async def opportunity_kanban(
|
||||
"create_time_start": list_kwargs["create_time_start"],
|
||||
"create_time_end": list_kwargs["create_time_end"],
|
||||
},
|
||||
user_info=user_info,
|
||||
)
|
||||
return success_response(data)
|
||||
except ValueError as e:
|
||||
@@ -2553,10 +2586,12 @@ async def opportunity_statistics_overview(
|
||||
"""商机统计概览"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
stat_body = body or OpportunityStatisticsBody()
|
||||
data = get_statistics_overview(
|
||||
db,
|
||||
tenant_no,
|
||||
user_info=user_info,
|
||||
owner_user_id=stat_body.owner_user_id,
|
||||
begin_time=stat_body.begin_time,
|
||||
end_time=stat_body.end_time,
|
||||
@@ -2576,7 +2611,8 @@ async def opportunity_statistics_stage(
|
||||
"""按阶段统计"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
return success_response(get_statistics_by_stage(db, tenant_no))
|
||||
user_info = crm_user_info(_user)
|
||||
return success_response(get_statistics_by_stage(db, tenant_no, user_info=user_info))
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
except Exception as e:
|
||||
@@ -2592,6 +2628,7 @@ async def opportunity_statistics_win_rate(
|
||||
"""赢单率趋势"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
stat_body = body or OpportunityStatisticsBody()
|
||||
data = get_win_rate_statistics(
|
||||
db,
|
||||
@@ -2599,6 +2636,7 @@ async def opportunity_statistics_win_rate(
|
||||
stat_type=stat_body.type or "month",
|
||||
begin_time=stat_body.begin_time,
|
||||
end_time=stat_body.end_time,
|
||||
user_info=user_info,
|
||||
)
|
||||
return success_response(data)
|
||||
except ValueError as e:
|
||||
@@ -2616,12 +2654,14 @@ async def opportunity_statistics_owner(
|
||||
"""按负责人统计"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
stat_body = body or OpportunityStatisticsBody()
|
||||
data = get_statistics_by_owner(
|
||||
db,
|
||||
tenant_no,
|
||||
begin_time=stat_body.begin_time,
|
||||
end_time=stat_body.end_time,
|
||||
user_info=user_info,
|
||||
)
|
||||
return success_response(data)
|
||||
except ValueError as e:
|
||||
@@ -2639,6 +2679,7 @@ async def opportunity_statistics_funnel(
|
||||
"""销售漏斗"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
stat_body = body or OpportunityStatisticsBody()
|
||||
data = get_sales_funnel(
|
||||
db,
|
||||
@@ -2646,6 +2687,7 @@ async def opportunity_statistics_funnel(
|
||||
owner_user_id=stat_body.owner_user_id,
|
||||
begin_time=stat_body.begin_time,
|
||||
end_time=stat_body.end_time,
|
||||
user_info=user_info,
|
||||
)
|
||||
return success_response(data)
|
||||
except ValueError as e:
|
||||
@@ -2663,12 +2705,14 @@ async def opportunity_follow_list(
|
||||
"""商机跟进列表"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
rows, total = list_opportunity_follows(
|
||||
db,
|
||||
opportunity_id=body.opportunity_id,
|
||||
page=body.page,
|
||||
page_size=body.pageSize,
|
||||
tenant_no=tenant_no,
|
||||
user_info=user_info,
|
||||
)
|
||||
return success_response({
|
||||
"list": rows,
|
||||
@@ -2693,12 +2737,14 @@ async def opportunity_follow_add(
|
||||
"""新增商机跟进"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
row = create_opportunity_follow(
|
||||
db,
|
||||
tenant_no,
|
||||
body.model_dump(by_alias=False, exclude_unset=True),
|
||||
user_id=user_id,
|
||||
user_info=user_info,
|
||||
)
|
||||
return success_response(row, message="创建成功")
|
||||
except ValueError as e:
|
||||
@@ -2716,11 +2762,12 @@ async def opportunity_follow_update(
|
||||
"""编辑商机跟进"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
data = body.model_dump(by_alias=False, exclude_unset=True)
|
||||
fid = data.pop("id")
|
||||
row = update_opportunity_follow(
|
||||
db, fid, data, user_id=user_id, tenant_no=tenant_no
|
||||
db, fid, data, user_id=user_id, tenant_no=tenant_no, user_info=user_info
|
||||
)
|
||||
return success_response(row, message="更新成功")
|
||||
except ValueError as e:
|
||||
@@ -2738,11 +2785,12 @@ async def opportunity_follow_delete(
|
||||
"""删除商机跟进"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
count = 0
|
||||
for fid in body.ids:
|
||||
if soft_delete_opportunity_follow(
|
||||
db, fid, user_id=user_id, tenant_no=tenant_no
|
||||
db, fid, user_id=user_id, tenant_no=tenant_no, user_info=user_info
|
||||
):
|
||||
count += 1
|
||||
return success_response({"count": count}, message="删除成功")
|
||||
@@ -2761,7 +2809,8 @@ async def opportunity_follow_detail(
|
||||
"""商机跟进详情"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
row = get_opportunity_follow(db, body.id, tenant_no=tenant_no)
|
||||
user_info = crm_user_info(_user)
|
||||
row = get_opportunity_follow(db, body.id, tenant_no=tenant_no, user_info=user_info)
|
||||
if not row:
|
||||
return error_response(404, "跟进记录不存在")
|
||||
return success_response(row)
|
||||
@@ -2780,6 +2829,7 @@ async def opportunity_product_list(
|
||||
"""商机产品列表"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
rows, total = list_opportunity_products(
|
||||
db,
|
||||
opportunity_id=body.opportunity_id,
|
||||
@@ -2787,6 +2837,7 @@ async def opportunity_product_list(
|
||||
page=body.page,
|
||||
page_size=body.pageSize,
|
||||
tenant_no=tenant_no,
|
||||
user_info=user_info,
|
||||
)
|
||||
return success_response({
|
||||
"list": rows,
|
||||
@@ -2811,10 +2862,11 @@ async def opportunity_product_list_by_opportunity(
|
||||
"""按商机查询产品列表"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
opp_id = (body.opportunity_id or body.id) if body else None
|
||||
if not opp_id:
|
||||
return error_response(400, "缺少商机ID")
|
||||
rows = list_products_by_opportunity(db, opp_id, tenant_no=tenant_no)
|
||||
rows = list_products_by_opportunity(db, opp_id, tenant_no=tenant_no, user_info=user_info)
|
||||
return success_response(rows)
|
||||
except ValueError as e:
|
||||
return error_response(400, str(e))
|
||||
@@ -2831,12 +2883,14 @@ async def opportunity_product_add(
|
||||
"""新增商机产品"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
row = create_opportunity_product(
|
||||
db,
|
||||
tenant_no,
|
||||
body.model_dump(by_alias=False, exclude_unset=True),
|
||||
user_id=user_id,
|
||||
user_info=user_info,
|
||||
)
|
||||
return success_response(row, message="创建成功")
|
||||
except ValueError as e:
|
||||
@@ -2854,6 +2908,7 @@ async def opportunity_product_batch_add(
|
||||
"""批量添加商机产品"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
product_list = [
|
||||
p.model_dump(by_alias=False, exclude_unset=True)
|
||||
@@ -2865,6 +2920,7 @@ async def opportunity_product_batch_add(
|
||||
body.opportunity_id,
|
||||
product_list,
|
||||
user_id=user_id,
|
||||
user_info=user_info,
|
||||
)
|
||||
return success_response({"count": count}, message="批量添加成功")
|
||||
except ValueError as e:
|
||||
@@ -2882,11 +2938,12 @@ async def opportunity_product_update(
|
||||
"""编辑商机产品"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
data = body.model_dump(by_alias=False, exclude_unset=True)
|
||||
pid = data.pop("id")
|
||||
row = update_opportunity_product(
|
||||
db, pid, data, user_id=user_id, tenant_no=tenant_no
|
||||
db, pid, data, user_id=user_id, tenant_no=tenant_no, user_info=user_info
|
||||
)
|
||||
return success_response(row, message="更新成功")
|
||||
except ValueError as e:
|
||||
@@ -2904,9 +2961,10 @@ async def opportunity_product_delete(
|
||||
"""删除商机产品"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
count = soft_delete_opportunity_products(
|
||||
db, body.ids, user_id=user_id, tenant_no=tenant_no
|
||||
db, body.ids, user_id=user_id, tenant_no=tenant_no, user_info=user_info
|
||||
)
|
||||
return success_response({"count": count}, message="删除成功")
|
||||
except ValueError as e:
|
||||
@@ -2924,7 +2982,8 @@ async def opportunity_product_detail(
|
||||
"""商机产品详情"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
row = get_opportunity_product(db, body.id, tenant_no=tenant_no)
|
||||
user_info = crm_user_info(_user)
|
||||
row = get_opportunity_product(db, body.id, tenant_no=tenant_no, user_info=user_info)
|
||||
if not row:
|
||||
return error_response(404, "商机产品不存在")
|
||||
return success_response(row)
|
||||
@@ -2943,12 +3002,14 @@ async def opportunity_export(
|
||||
"""导出商机列表"""
|
||||
try:
|
||||
tenant_no = get_tenant_no(db, _user)
|
||||
user_info = crm_user_info(_user)
|
||||
list_body = body or OpportunityListBody()
|
||||
user_id = _user.get("user_id", "") or _user.get("id", "")
|
||||
rows, _ = list_opportunities(
|
||||
db,
|
||||
tenant_no,
|
||||
user_id,
|
||||
user_info=user_info,
|
||||
**_opp_list_kwargs(list_body, user_id),
|
||||
page=1,
|
||||
page_size=10000,
|
||||
|
||||
@@ -17,6 +17,7 @@ from app.models.crm_models import (
|
||||
CrmOpportunityStageLog,
|
||||
)
|
||||
from app.models.models import SysUser
|
||||
from app.crud.crm.data_scope_helpers import apply_crm_owner_scope, ensure_crm_owner_row_in_scope
|
||||
|
||||
_FMT = "%Y-%m-%d %H:%M:%S"
|
||||
_DATE_FMT = "%Y-%m-%d"
|
||||
@@ -54,6 +55,27 @@ _FUNNEL_STAGE_NAMES = (
|
||||
)
|
||||
|
||||
|
||||
def _ensure_opportunity_in_scope(db: Session, row: CrmOpportunity, user_info, tenant_no: str) -> None:
|
||||
ensure_crm_owner_row_in_scope(db, row, user_info, tenant_no, message="无权访问该商机")
|
||||
|
||||
|
||||
def _scoped_opportunity_query(
|
||||
db: Session,
|
||||
tenant_no: str,
|
||||
user_info=None,
|
||||
owner_user_id: str = None,
|
||||
):
|
||||
q = db.query(CrmOpportunity).filter(
|
||||
CrmOpportunity.tenant_no == tenant_no,
|
||||
CrmOpportunity.del_flag == 0,
|
||||
)
|
||||
if owner_user_id:
|
||||
q = q.filter(CrmOpportunity.owner_user_id == owner_user_id)
|
||||
if user_info:
|
||||
q = apply_crm_owner_scope(q, CrmOpportunity, user_info, db, tenant_no)
|
||||
return q
|
||||
|
||||
|
||||
def total_pages(total: int, page_size: int) -> int:
|
||||
if page_size <= 0:
|
||||
return 0
|
||||
@@ -300,6 +322,7 @@ def list_opportunities(
|
||||
create_time_end: str = None,
|
||||
amount_min: Any = None,
|
||||
amount_max: Any = None,
|
||||
user_info=None,
|
||||
) -> Tuple[List[Dict[str, Any]], int]:
|
||||
q = db.query(CrmOpportunity)
|
||||
effective_owner = owner_user_id
|
||||
@@ -324,6 +347,9 @@ def list_opportunities(
|
||||
amount_max=amount_max,
|
||||
)
|
||||
|
||||
if user_info:
|
||||
q = apply_crm_owner_scope(q, CrmOpportunity, user_info, db, tenant_no)
|
||||
|
||||
total = q.order_by(None).with_entities(func.count(CrmOpportunity.id)).scalar() or 0
|
||||
page = max(1, page)
|
||||
page_size = min(max(1, page_size), 100)
|
||||
@@ -554,6 +580,7 @@ def update_opportunity(
|
||||
data: dict,
|
||||
user_id: str = None,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> Dict[str, Any]:
|
||||
q = db.query(CrmOpportunity).filter(
|
||||
CrmOpportunity.id == opportunity_id,
|
||||
@@ -564,6 +591,7 @@ def update_opportunity(
|
||||
obj = q.first()
|
||||
if not obj:
|
||||
raise ValueError("商机不存在")
|
||||
_ensure_opportunity_in_scope(db, obj, user_info, obj.tenant_no)
|
||||
_guard_terminal_status(obj, "修改")
|
||||
|
||||
_updatable = (
|
||||
@@ -620,6 +648,7 @@ def soft_delete_opportunities(
|
||||
ids: List[str],
|
||||
user_id: str = None,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> int:
|
||||
q = db.query(CrmOpportunity).filter(CrmOpportunity.id.in_(ids), CrmOpportunity.del_flag == 0)
|
||||
if tenant_no:
|
||||
@@ -632,6 +661,7 @@ def soft_delete_opportunities(
|
||||
|
||||
now = datetime.now()
|
||||
for row in rows:
|
||||
_ensure_opportunity_in_scope(db, row, user_info, row.tenant_no)
|
||||
_guard_terminal_status(row, "删除")
|
||||
row.del_flag = 1
|
||||
row.update_by = user_id
|
||||
@@ -647,6 +677,7 @@ def assign_opportunities(
|
||||
user_id: str = None,
|
||||
remark: str = None,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> int:
|
||||
owner_name = _resolve_user_display_name(db, owner_user_id)
|
||||
q = db.query(CrmOpportunity).filter(CrmOpportunity.id.in_(ids), CrmOpportunity.del_flag == 0)
|
||||
@@ -656,6 +687,7 @@ def assign_opportunities(
|
||||
now = datetime.now()
|
||||
count = 0
|
||||
for row in rows:
|
||||
_ensure_opportunity_in_scope(db, row, user_info, row.tenant_no)
|
||||
if row.status in ("won", "lost"):
|
||||
continue
|
||||
row.owner_user_id = owner_user_id
|
||||
@@ -680,6 +712,7 @@ def change_stage(
|
||||
remark: str = None,
|
||||
user_id: str = None,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> Dict[str, Any]:
|
||||
q = db.query(CrmOpportunity).filter(
|
||||
CrmOpportunity.id == opportunity_id,
|
||||
@@ -690,6 +723,7 @@ def change_stage(
|
||||
obj = q.first()
|
||||
if not obj:
|
||||
raise ValueError("商机不存在")
|
||||
_ensure_opportunity_in_scope(db, obj, user_info, obj.tenant_no)
|
||||
_guard_terminal_status(obj, "变更阶段")
|
||||
|
||||
old_stage = obj.stage
|
||||
@@ -749,6 +783,7 @@ def mark_won(
|
||||
remark: str = None,
|
||||
user_id: str = None,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> Dict[str, Any]:
|
||||
q = db.query(CrmOpportunity).filter(
|
||||
CrmOpportunity.id == opportunity_id,
|
||||
@@ -759,6 +794,7 @@ def mark_won(
|
||||
obj = q.first()
|
||||
if not obj:
|
||||
raise ValueError("商机不存在")
|
||||
_ensure_opportunity_in_scope(db, obj, user_info, obj.tenant_no)
|
||||
if obj.status == "won":
|
||||
raise ValueError("商机已是赢单状态")
|
||||
|
||||
@@ -802,6 +838,7 @@ def mark_lost(
|
||||
improvement: str = None,
|
||||
user_id: str = None,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> Dict[str, Any]:
|
||||
q = db.query(CrmOpportunity).filter(
|
||||
CrmOpportunity.id == opportunity_id,
|
||||
@@ -812,6 +849,7 @@ def mark_lost(
|
||||
obj = q.first()
|
||||
if not obj:
|
||||
raise ValueError("商机不存在")
|
||||
_ensure_opportunity_in_scope(db, obj, user_info, obj.tenant_no)
|
||||
if obj.status == "lost":
|
||||
raise ValueError("商机已是输单状态")
|
||||
|
||||
@@ -854,6 +892,7 @@ def batch_update_stage(
|
||||
change_reason: str,
|
||||
tenant_no: str = None,
|
||||
user_id: str = None,
|
||||
user_info=None,
|
||||
) -> Dict[str, Any]:
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
@@ -869,6 +908,7 @@ def batch_update_stage(
|
||||
change_reason=change_reason,
|
||||
user_id=user_id,
|
||||
tenant_no=tenant_no,
|
||||
user_info=user_info,
|
||||
)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
@@ -889,6 +929,7 @@ def batch_update_expect_date(
|
||||
days: int = None,
|
||||
tenant_no: str = None,
|
||||
user_id: str = None,
|
||||
user_info=None,
|
||||
) -> Dict[str, Any]:
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
@@ -905,6 +946,7 @@ def batch_update_expect_date(
|
||||
obj = q.first()
|
||||
if not obj:
|
||||
raise ValueError("商机不存在")
|
||||
_ensure_opportunity_in_scope(db, obj, user_info, obj.tenant_no)
|
||||
_guard_terminal_status(obj, "修改预计成交日期")
|
||||
|
||||
new_date: Optional[date] = None
|
||||
@@ -949,8 +991,22 @@ def batch_update_expect_date(
|
||||
|
||||
|
||||
def get_stage_log_list(
|
||||
db: Session, opportunity_id: str, tenant_no: str = None
|
||||
db: Session,
|
||||
opportunity_id: str,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
opp_q = db.query(CrmOpportunity).filter(
|
||||
CrmOpportunity.id == opportunity_id,
|
||||
CrmOpportunity.del_flag == 0,
|
||||
)
|
||||
if tenant_no:
|
||||
opp_q = opp_q.filter(CrmOpportunity.tenant_no == tenant_no)
|
||||
opp = opp_q.first()
|
||||
if not opp:
|
||||
raise ValueError("商机不存在")
|
||||
_ensure_opportunity_in_scope(db, opp, user_info, opp.tenant_no)
|
||||
|
||||
q = db.query(CrmOpportunityStageLog).filter(
|
||||
CrmOpportunityStageLog.opportunity_id == opportunity_id,
|
||||
CrmOpportunityStageLog.del_flag == 0,
|
||||
@@ -972,34 +1028,18 @@ def get_statistics_overview(
|
||||
owner_user_id: str = None,
|
||||
begin_time: str = None,
|
||||
end_time: str = None,
|
||||
user_info=None,
|
||||
) -> Dict[str, Any]:
|
||||
base = [CrmOpportunity.tenant_no == tenant_no, CrmOpportunity.del_flag == 0]
|
||||
if owner_user_id:
|
||||
base.append(CrmOpportunity.owner_user_id == owner_user_id)
|
||||
q = _scoped_opportunity_query(db, tenant_no, user_info, owner_user_id)
|
||||
if begin_time:
|
||||
base.append(CrmOpportunity.create_time >= begin_time)
|
||||
q = q.filter(CrmOpportunity.create_time >= begin_time)
|
||||
if end_time:
|
||||
base.append(CrmOpportunity.create_time <= end_time)
|
||||
q = q.filter(CrmOpportunity.create_time <= end_time)
|
||||
|
||||
total_count = db.query(func.count(CrmOpportunity.id)).filter(*base).scalar() or 0
|
||||
following_count = (
|
||||
db.query(func.count(CrmOpportunity.id))
|
||||
.filter(*base, CrmOpportunity.status == "following")
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
won_count = (
|
||||
db.query(func.count(CrmOpportunity.id))
|
||||
.filter(*base, CrmOpportunity.status == "won")
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
lost_count = (
|
||||
db.query(func.count(CrmOpportunity.id))
|
||||
.filter(*base, CrmOpportunity.status == "lost")
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
total_count = q.count()
|
||||
following_count = q.filter(CrmOpportunity.status == "following").count()
|
||||
won_count = q.filter(CrmOpportunity.status == "won").count()
|
||||
lost_count = q.filter(CrmOpportunity.status == "lost").count()
|
||||
win_rate = Decimal("0")
|
||||
if won_count + lost_count > 0:
|
||||
win_rate = (
|
||||
@@ -1009,11 +1049,8 @@ def get_statistics_overview(
|
||||
).quantize(Decimal("0.01"))
|
||||
|
||||
total_amount = (
|
||||
db.query(func.coalesce(func.sum(CrmOpportunity.amount), 0))
|
||||
.filter(
|
||||
*base,
|
||||
CrmOpportunity.status.in_(("following", "won")),
|
||||
)
|
||||
q.with_entities(func.coalesce(func.sum(CrmOpportunity.amount), 0))
|
||||
.filter(CrmOpportunity.status.in_(("following", "won")))
|
||||
.scalar()
|
||||
)
|
||||
|
||||
@@ -1028,19 +1065,18 @@ def get_statistics_overview(
|
||||
}
|
||||
|
||||
|
||||
def get_statistics_by_stage(db: Session, tenant_no: str) -> List[Dict[str, Any]]:
|
||||
def get_statistics_by_stage(
|
||||
db: Session, tenant_no: str, user_info=None
|
||||
) -> List[Dict[str, Any]]:
|
||||
rows = (
|
||||
db.query(
|
||||
_scoped_opportunity_query(db, tenant_no, user_info)
|
||||
.filter(CrmOpportunity.status == "following")
|
||||
.with_entities(
|
||||
CrmOpportunity.stage,
|
||||
func.count(CrmOpportunity.id).label("count"),
|
||||
func.coalesce(func.sum(CrmOpportunity.amount), 0).label("totalAmount"),
|
||||
func.coalesce(func.avg(CrmOpportunity.win_rate), 0).label("avgWinRate"),
|
||||
)
|
||||
.filter(
|
||||
CrmOpportunity.tenant_no == tenant_no,
|
||||
CrmOpportunity.del_flag == 0,
|
||||
CrmOpportunity.status == "following",
|
||||
)
|
||||
.group_by(CrmOpportunity.stage)
|
||||
.all()
|
||||
)
|
||||
@@ -1064,6 +1100,7 @@ def get_win_rate_statistics(
|
||||
stat_type: str = "month",
|
||||
begin_time: str = None,
|
||||
end_time: str = None,
|
||||
user_info=None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
if stat_type == "day":
|
||||
fmt = "%Y-%m-%d"
|
||||
@@ -1073,24 +1110,21 @@ def get_win_rate_statistics(
|
||||
fmt = "%Y-%m"
|
||||
|
||||
date_expr = func.date_format(CrmOpportunity.create_time, fmt).label("date")
|
||||
filters = [
|
||||
CrmOpportunity.tenant_no == tenant_no,
|
||||
CrmOpportunity.del_flag == 0,
|
||||
CrmOpportunity.status.in_(("won", "lost")),
|
||||
]
|
||||
q = _scoped_opportunity_query(db, tenant_no, user_info).filter(
|
||||
CrmOpportunity.status.in_(("won", "lost"))
|
||||
)
|
||||
if begin_time:
|
||||
filters.append(CrmOpportunity.create_time >= begin_time)
|
||||
q = q.filter(CrmOpportunity.create_time >= begin_time)
|
||||
if end_time:
|
||||
filters.append(CrmOpportunity.create_time <= end_time)
|
||||
q = q.filter(CrmOpportunity.create_time <= end_time)
|
||||
|
||||
rows = (
|
||||
db.query(
|
||||
q.with_entities(
|
||||
date_expr,
|
||||
func.count(CrmOpportunity.id).label("totalCount"),
|
||||
func.sum(case((CrmOpportunity.status == "won", 1), else_=0)).label("wonCount"),
|
||||
func.sum(case((CrmOpportunity.status == "lost", 1), else_=0)).label("lostCount"),
|
||||
)
|
||||
.filter(*filters)
|
||||
.group_by(date_expr)
|
||||
.order_by(date_expr)
|
||||
.all()
|
||||
@@ -1117,15 +1151,16 @@ def get_statistics_by_owner(
|
||||
tenant_no: str,
|
||||
begin_time: str = None,
|
||||
end_time: str = None,
|
||||
user_info=None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
filters = [CrmOpportunity.tenant_no == tenant_no, CrmOpportunity.del_flag == 0]
|
||||
q = _scoped_opportunity_query(db, tenant_no, user_info)
|
||||
if begin_time:
|
||||
filters.append(CrmOpportunity.create_time >= begin_time)
|
||||
q = q.filter(CrmOpportunity.create_time >= begin_time)
|
||||
if end_time:
|
||||
filters.append(CrmOpportunity.create_time <= end_time)
|
||||
q = q.filter(CrmOpportunity.create_time <= end_time)
|
||||
|
||||
rows = (
|
||||
db.query(
|
||||
q.with_entities(
|
||||
CrmOpportunity.owner_user_id.label("ownerUserId"),
|
||||
CrmOpportunity.owner_user_name.label("ownerUserName"),
|
||||
func.count(CrmOpportunity.id).label("totalCount"),
|
||||
@@ -1145,7 +1180,6 @@ def get_statistics_by_owner(
|
||||
0,
|
||||
).label("followingAmount"),
|
||||
)
|
||||
.filter(*filters)
|
||||
.group_by(CrmOpportunity.owner_user_id, CrmOpportunity.owner_user_name)
|
||||
.order_by(
|
||||
func.sum(case((CrmOpportunity.status == "won", 1), else_=0)).desc(),
|
||||
@@ -1182,13 +1216,12 @@ def get_kanban_data(
|
||||
db: Session,
|
||||
tenant_no: str,
|
||||
filters: dict = None,
|
||||
user_info=None,
|
||||
) -> Dict[str, Any]:
|
||||
filters = filters or {}
|
||||
result: Dict[str, Any] = {}
|
||||
for stage in _KANBAN_STAGES:
|
||||
q = db.query(CrmOpportunity).filter(
|
||||
CrmOpportunity.tenant_no == tenant_no,
|
||||
CrmOpportunity.del_flag == 0,
|
||||
q = _scoped_opportunity_query(db, tenant_no, user_info).filter(
|
||||
CrmOpportunity.stage == stage,
|
||||
CrmOpportunity.status == "following",
|
||||
)
|
||||
@@ -1228,28 +1261,22 @@ def get_sales_funnel(
|
||||
owner_user_id: str = None,
|
||||
begin_time: str = None,
|
||||
end_time: str = None,
|
||||
user_info=None,
|
||||
) -> Dict[str, Any]:
|
||||
funnel_data: List[Dict[str, Any]] = []
|
||||
for i, stage in enumerate(_FUNNEL_STAGES):
|
||||
filters = [
|
||||
CrmOpportunity.tenant_no == tenant_no,
|
||||
CrmOpportunity.del_flag == 0,
|
||||
]
|
||||
if owner_user_id:
|
||||
filters.append(CrmOpportunity.owner_user_id == owner_user_id)
|
||||
q = _scoped_opportunity_query(db, tenant_no, user_info, owner_user_id)
|
||||
if begin_time:
|
||||
filters.append(CrmOpportunity.create_time >= begin_time)
|
||||
q = q.filter(CrmOpportunity.create_time >= begin_time)
|
||||
if end_time:
|
||||
filters.append(CrmOpportunity.create_time <= end_time)
|
||||
q = q.filter(CrmOpportunity.create_time <= end_time)
|
||||
|
||||
if stage == "won":
|
||||
filters.append(CrmOpportunity.status == "won")
|
||||
q = q.filter(CrmOpportunity.status == "won")
|
||||
else:
|
||||
filters.extend(
|
||||
[CrmOpportunity.stage == stage, CrmOpportunity.status == "following"]
|
||||
)
|
||||
q = q.filter(CrmOpportunity.stage == stage, CrmOpportunity.status == "following")
|
||||
|
||||
rows = db.query(CrmOpportunity).filter(*filters).all()
|
||||
rows = q.all()
|
||||
count = len(rows)
|
||||
total_amount = sum(float(r.amount or 0) for r in rows)
|
||||
avg_win_rate = Decimal("0")
|
||||
|
||||
@@ -8,6 +8,8 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.utils import generate_uuid
|
||||
from app.crud.crm.data_scope_helpers import apply_crm_create_by_scope, ensure_crm_create_by_row_in_scope
|
||||
from app.crud.crm.opportunity import _ensure_opportunity_in_scope
|
||||
from app.models.crm_models import CrmOpportunity, CrmOpportunityFollow
|
||||
|
||||
|
||||
@@ -103,6 +105,24 @@ def _recalc_opportunity_follow_stats(db: Session, opportunity_id: str) -> None:
|
||||
opp.update_time = datetime.now()
|
||||
|
||||
|
||||
def _ensure_parent_opportunity(
|
||||
db: Session, opportunity_id: str, tenant_no: str, user_info
|
||||
) -> CrmOpportunity:
|
||||
opp = (
|
||||
db.query(CrmOpportunity)
|
||||
.filter(
|
||||
CrmOpportunity.id == opportunity_id,
|
||||
CrmOpportunity.tenant_no == tenant_no,
|
||||
CrmOpportunity.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not opp:
|
||||
raise ValueError("商机不存在")
|
||||
_ensure_opportunity_in_scope(db, opp, user_info, tenant_no)
|
||||
return opp
|
||||
|
||||
|
||||
def list_opportunity_follows(
|
||||
db: Session,
|
||||
*,
|
||||
@@ -110,12 +130,19 @@ def list_opportunity_follows(
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> Tuple[List[Dict[str, Any]], int]:
|
||||
if opportunity_id and tenant_no:
|
||||
_ensure_parent_opportunity(db, opportunity_id, tenant_no, user_info)
|
||||
|
||||
q = db.query(CrmOpportunityFollow).filter(CrmOpportunityFollow.del_flag == 0)
|
||||
if tenant_no:
|
||||
q = q.filter(CrmOpportunityFollow.tenant_no == tenant_no)
|
||||
if opportunity_id:
|
||||
q = q.filter(CrmOpportunityFollow.opportunity_id == opportunity_id)
|
||||
if user_info and tenant_no:
|
||||
q = apply_crm_create_by_scope(q, CrmOpportunityFollow, user_info, db, tenant_no)
|
||||
|
||||
total = q.count()
|
||||
rows = (
|
||||
q.order_by(CrmOpportunityFollow.create_time.desc())
|
||||
@@ -127,7 +154,10 @@ def list_opportunity_follows(
|
||||
|
||||
|
||||
def list_follows_by_opportunity(
|
||||
db: Session, opportunity_id: str, tenant_no: str = None
|
||||
db: Session,
|
||||
opportunity_id: str,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
rows, _ = list_opportunity_follows(
|
||||
db,
|
||||
@@ -135,12 +165,16 @@ def list_follows_by_opportunity(
|
||||
page=1,
|
||||
page_size=10000,
|
||||
tenant_no=tenant_no,
|
||||
user_info=user_info,
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def get_opportunity_follow(
|
||||
db: Session, follow_id: str, tenant_no: str = None
|
||||
db: Session,
|
||||
follow_id: str,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
q = db.query(CrmOpportunityFollow).filter(
|
||||
CrmOpportunityFollow.id == follow_id,
|
||||
@@ -149,7 +183,12 @@ def get_opportunity_follow(
|
||||
if tenant_no:
|
||||
q = q.filter(CrmOpportunityFollow.tenant_no == tenant_no)
|
||||
row = q.first()
|
||||
return _row_to_dict(row) if row else None
|
||||
if not row:
|
||||
return None
|
||||
ensure_crm_create_by_row_in_scope(
|
||||
db, row, user_info, row.tenant_no, message="无权访问该跟进记录"
|
||||
)
|
||||
return _row_to_dict(row)
|
||||
|
||||
|
||||
def create_opportunity_follow(
|
||||
@@ -157,18 +196,9 @@ def create_opportunity_follow(
|
||||
tenant_no: str,
|
||||
data: dict,
|
||||
user_id: str = None,
|
||||
user_info=None,
|
||||
) -> Dict[str, Any]:
|
||||
opp = (
|
||||
db.query(CrmOpportunity)
|
||||
.filter(
|
||||
CrmOpportunity.id == data.get("opportunity_id"),
|
||||
CrmOpportunity.tenant_no == tenant_no,
|
||||
CrmOpportunity.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not opp:
|
||||
raise ValueError("商机不存在")
|
||||
opp = _ensure_parent_opportunity(db, data.get("opportunity_id"), tenant_no, user_info)
|
||||
if opp.status in ("won", "lost"):
|
||||
raise ValueError("已赢单或已输单的商机不允许添加跟进")
|
||||
|
||||
@@ -213,6 +243,7 @@ def update_opportunity_follow(
|
||||
data: dict,
|
||||
user_id: str = None,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> Dict[str, Any]:
|
||||
q = db.query(CrmOpportunityFollow).filter(
|
||||
CrmOpportunityFollow.id == follow_id,
|
||||
@@ -223,6 +254,9 @@ def update_opportunity_follow(
|
||||
row = q.first()
|
||||
if not row:
|
||||
raise ValueError("跟进记录不存在")
|
||||
ensure_crm_create_by_row_in_scope(
|
||||
db, row, user_info, row.tenant_no, message="无权访问该跟进记录"
|
||||
)
|
||||
|
||||
if data.get("follow_type") is not None:
|
||||
row.follow_type = data["follow_type"]
|
||||
@@ -252,6 +286,7 @@ def soft_delete_opportunity_follow(
|
||||
follow_id: str,
|
||||
user_id: str = None,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> bool:
|
||||
q = db.query(CrmOpportunityFollow).filter(
|
||||
CrmOpportunityFollow.id == follow_id,
|
||||
@@ -262,6 +297,9 @@ def soft_delete_opportunity_follow(
|
||||
row = q.first()
|
||||
if not row:
|
||||
return False
|
||||
ensure_crm_create_by_row_in_scope(
|
||||
db, row, user_info, row.tenant_no, message="无权访问该跟进记录"
|
||||
)
|
||||
row.del_flag = 1
|
||||
row.update_by = user_id
|
||||
row.update_time = datetime.now()
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.utils import generate_uuid
|
||||
from app.crud.crm.opportunity import _ensure_opportunity_in_scope
|
||||
from app.models.crm_models import CrmOpportunity, CrmOpportunityProduct, CrmProduct
|
||||
|
||||
|
||||
@@ -96,6 +97,24 @@ def _fill_product_redundant(db: Session, entity: CrmOpportunityProduct, product_
|
||||
entity.unit = entity.unit or product.unit
|
||||
|
||||
|
||||
def _get_opportunity_for_product(
|
||||
db: Session, opportunity_id: str, tenant_no: str, user_info=None
|
||||
) -> CrmOpportunity:
|
||||
opp = (
|
||||
db.query(CrmOpportunity)
|
||||
.filter(
|
||||
CrmOpportunity.id == opportunity_id,
|
||||
CrmOpportunity.tenant_no == tenant_no,
|
||||
CrmOpportunity.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not opp:
|
||||
raise ValueError("商机不存在")
|
||||
_ensure_opportunity_in_scope(db, opp, user_info, tenant_no)
|
||||
return opp
|
||||
|
||||
|
||||
def list_opportunity_products(
|
||||
db: Session,
|
||||
*,
|
||||
@@ -104,7 +123,10 @@ def list_opportunity_products(
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> Tuple[List[Dict[str, Any]], int]:
|
||||
if opportunity_id and tenant_no:
|
||||
_get_opportunity_for_product(db, opportunity_id, tenant_no, user_info)
|
||||
q = db.query(CrmOpportunityProduct).filter(CrmOpportunityProduct.del_flag == 0)
|
||||
if tenant_no:
|
||||
q = q.filter(CrmOpportunityProduct.tenant_no == tenant_no)
|
||||
@@ -123,7 +145,10 @@ def list_opportunity_products(
|
||||
|
||||
|
||||
def list_products_by_opportunity(
|
||||
db: Session, opportunity_id: str, tenant_no: str = None
|
||||
db: Session,
|
||||
opportunity_id: str,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
rows, _ = list_opportunity_products(
|
||||
db,
|
||||
@@ -131,12 +156,16 @@ def list_products_by_opportunity(
|
||||
page=1,
|
||||
page_size=10000,
|
||||
tenant_no=tenant_no,
|
||||
user_info=user_info,
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def get_opportunity_product(
|
||||
db: Session, product_row_id: str, tenant_no: str = None
|
||||
db: Session,
|
||||
product_row_id: str,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
q = db.query(CrmOpportunityProduct).filter(
|
||||
CrmOpportunityProduct.id == product_row_id,
|
||||
@@ -145,7 +174,11 @@ def get_opportunity_product(
|
||||
if tenant_no:
|
||||
q = q.filter(CrmOpportunityProduct.tenant_no == tenant_no)
|
||||
row = q.first()
|
||||
return _row_to_dict(row) if row else None
|
||||
if not row:
|
||||
return None
|
||||
if tenant_no:
|
||||
_get_opportunity_for_product(db, row.opportunity_id, tenant_no, user_info)
|
||||
return _row_to_dict(row)
|
||||
|
||||
|
||||
def create_opportunity_product(
|
||||
@@ -153,19 +186,10 @@ def create_opportunity_product(
|
||||
tenant_no: str,
|
||||
data: dict,
|
||||
user_id: str = None,
|
||||
user_info=None,
|
||||
) -> Dict[str, Any]:
|
||||
opp_id = data.get("opportunity_id")
|
||||
opp = (
|
||||
db.query(CrmOpportunity)
|
||||
.filter(
|
||||
CrmOpportunity.id == opp_id,
|
||||
CrmOpportunity.tenant_no == tenant_no,
|
||||
CrmOpportunity.del_flag == 0,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not opp:
|
||||
raise ValueError("商机不存在")
|
||||
_get_opportunity_for_product(db, opp_id, tenant_no, user_info)
|
||||
|
||||
qty = _decimal_value(data.get("quantity"))
|
||||
price = _decimal_value(data.get("price"))
|
||||
@@ -216,14 +240,16 @@ def batch_add_opportunity_products(
|
||||
opportunity_id: str,
|
||||
product_list: List[dict],
|
||||
user_id: str = None,
|
||||
user_info=None,
|
||||
) -> int:
|
||||
if not product_list:
|
||||
raise ValueError("产品列表不能为空")
|
||||
_get_opportunity_for_product(db, opportunity_id, tenant_no, user_info)
|
||||
count = 0
|
||||
for item in product_list:
|
||||
item = dict(item)
|
||||
item["opportunity_id"] = opportunity_id
|
||||
create_opportunity_product(db, tenant_no, item, user_id)
|
||||
create_opportunity_product(db, tenant_no, item, user_id, user_info=user_info)
|
||||
count += 1
|
||||
return count
|
||||
|
||||
@@ -234,6 +260,7 @@ def update_opportunity_product(
|
||||
data: dict,
|
||||
user_id: str = None,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> Dict[str, Any]:
|
||||
q = db.query(CrmOpportunityProduct).filter(
|
||||
CrmOpportunityProduct.id == product_row_id,
|
||||
@@ -244,6 +271,8 @@ def update_opportunity_product(
|
||||
row = q.first()
|
||||
if not row:
|
||||
raise ValueError("商机产品不存在")
|
||||
if tenant_no:
|
||||
_get_opportunity_for_product(db, row.opportunity_id, tenant_no, user_info)
|
||||
|
||||
if data.get("quantity") is not None:
|
||||
row.quantity = _decimal_value(data["quantity"])
|
||||
@@ -271,6 +300,7 @@ def soft_delete_opportunity_products(
|
||||
ids: List[str],
|
||||
user_id: str = None,
|
||||
tenant_no: str = None,
|
||||
user_info=None,
|
||||
) -> int:
|
||||
q = db.query(CrmOpportunityProduct).filter(
|
||||
CrmOpportunityProduct.id.in_(ids),
|
||||
@@ -282,6 +312,8 @@ def soft_delete_opportunity_products(
|
||||
if not rows:
|
||||
return 0
|
||||
opp_id = rows[0].opportunity_id
|
||||
if tenant_no:
|
||||
_get_opportunity_for_product(db, opp_id, tenant_no, user_info)
|
||||
now = datetime.now()
|
||||
for row in rows:
|
||||
row.del_flag = 1
|
||||
|
||||
Reference in new issue
Block a user