From bf73b6b2d8dd6f290eed5b59a80050bbb169e3ec Mon Sep 17 00:00:00 2001 From: zlwu Date: Sun, 28 Jun 2026 21:00:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(crm):=20=E8=81=94=E7=B3=BB=E4=BA=BA?= =?UTF-8?q?=E3=80=81=E4=BA=A7=E5=93=81=E3=80=81=E6=8A=A5=E4=BB=B7=E4=B8=8E?= =?UTF-8?q?=E5=90=88=E5=90=8C=E5=9F=9F=E6=8E=A5=E5=85=A5=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=9D=83=E9=99=90=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补全 PR3 Phase 2.4–2.7,列表 apply_data_scope、详情/CUD assert_row_in_data_scope,合同/报价主表支持 owner_user_id。 Co-authored-by: Cursor --- backend/app/api/crm.py | 94 ++++++++++++--- backend/app/api/crm_contract_routes.py | 46 +++++++- backend/app/api/crm_contract_sub_routes.py | 31 ++++- backend/app/api/crm_quote_routes.py | 45 ++++++- backend/app/crud/crm/contact.py | 60 +++++++++- backend/app/crud/crm/contract.py | 129 ++++++++++++++++----- backend/app/crud/crm/contract_product.py | 5 + backend/app/crud/crm/data_scope_helpers.py | 48 ++++++++ backend/app/crud/crm/invoice_info.py | 47 +++++++- backend/app/crud/crm/product.py | 35 +++++- backend/app/crud/crm/quote.py | 88 +++++++++----- backend/app/crud/crm/quote_approval.py | 5 + backend/app/crud/crm/quote_product.py | 18 +++ backend/app/crud/crm/quote_share_log.py | 11 +- backend/app/crud/crm/supplier_info.py | 18 +++ 15 files changed, 598 insertions(+), 82 deletions(-) diff --git a/backend/app/api/crm.py b/backend/app/api/crm.py index 44436c8eb..891c00861 100644 --- a/backend/app/api/crm.py +++ b/backend/app/api/crm.py @@ -17,6 +17,8 @@ from app.core.tenant_context import get_tenant_no from app.crud.crm.data_scope_helpers import ( crm_user_info, ensure_crm_create_by_row_in_scope, + ensure_crm_contact_row_in_scope, + ensure_crm_customer_linked_row_in_scope, ensure_crm_owner_row_in_scope, ) from app.utils.crm_export_columns import ( @@ -329,6 +331,8 @@ async def category_tree( """获取产品分类树""" try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) + user_info = crm_user_info(_user) payload = body or {} category_name = payload.get("categoryName") or payload.get("category_name") status = payload.get("status") @@ -337,6 +341,7 @@ async def category_tree( tenant_no, category_name=category_name, status=status, + user_info=user_info, ) return success_response(rows) except ValueError as e: @@ -516,6 +521,7 @@ async def category_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", "") data = body.model_dump(exclude_unset=True) obj = create_category(db, tenant_no=tenant_no, user_id=user_id, **data) @@ -537,7 +543,7 @@ async def category_update( user_id = _user.get("user_id", "") or _user.get("id", "") data = body.model_dump(exclude_unset=True) category_id = data.pop("id") - obj = update_category(db, category_id, user_id=user_id, **data) + obj = update_category(db, category_id, user_id=user_id, user_info=user_info, **data) return success_response(_category_row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @@ -553,7 +559,7 @@ async def category_delete( ): """删除产品分类""" try: - soft_delete_category(db, body.id) + soft_delete_category(db, body.id, user_info=user_info) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) @@ -573,6 +579,7 @@ async def product_list( """分页查询产品列表""" try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) rows, total = list_products( db, tenant_no, @@ -581,6 +588,7 @@ async def product_list( keyword=body.keyword, category_id=body.category_id, status=body.status, + user_info=user_info, ) tp = total_pages(total, body.pageSize) return success_response( @@ -606,9 +614,12 @@ async def product_detail( ): """获取产品详情""" try: + tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) obj = get_product(db, body.id) if not obj: return error_response(404, "产品不存在") + ensure_crm_create_by_row_in_scope(db, obj, user_info, tenant_no, message="无权访问该产品") return success_response(_product_row_to_dict(obj)) except ValueError as e: return error_response(400, str(e)) @@ -625,6 +636,7 @@ async def 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", "") obj = create_product( db, @@ -650,7 +662,9 @@ async def product_update( user_id = _user.get("user_id", "") or _user.get("id", "") data = body.model_dump(exclude_unset=True) product_id = data.pop("id") - obj = update_product(db, product_id, user_id=user_id, **data) + tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) + obj = update_product(db, product_id, user_id=user_id, user_info=user_info, **data) return success_response(_product_row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @@ -666,6 +680,8 @@ async def product_delete( ): """删除产品(支持单条 body.id 和批量 body.ids)""" try: + tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) ids_to_delete: List[str] = [] if body.ids: ids_to_delete = body.ids @@ -675,7 +691,7 @@ async def product_delete( return error_response(400, "请传入 id 或 ids") for pid in ids_to_delete: - soft_delete_product(db, pid) + soft_delete_product(db, pid, user_info=user_info) return success_response(None, message="删除成功") except ValueError as e: @@ -692,10 +708,12 @@ async def product_update_status( ): """批量上架/下架""" try: + tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") status_label = "上架" if body.status == "on" else "下架" for pid in body.ids: - update_product(db, pid, user_id=user_id, status=body.status) + update_product(db, pid, user_id=user_id, user_info=user_info, status=body.status) return success_response(None, message=f"批量{status_label}成功") except ValueError as e: return error_response(400, str(e)) @@ -712,11 +730,13 @@ async def product_options( """获取产品下拉选项""" try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) options = list_product_options( db, tenant_no, keyword=body.keyword, category_id=body.category_id, + user_info=user_info, ) return success_response(options) except ValueError as e: @@ -739,6 +759,7 @@ async def product_import( from openpyxl import load_workbook tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") content = await file.read() @@ -833,6 +854,7 @@ async def product_export( """Excel 导出产品列表""" try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) rows, _ = list_products( db, tenant_no, @@ -1424,10 +1446,11 @@ async def contact_list( """联系人分页列表""" try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) page = body.page if body else 1 page_size = body.pageSize if body else 10 rows, total = list_contacts( - db, tenant_no, page=page, page_size=page_size, + db, tenant_no, page=page, page_size=page_size, user_info=user_info, keyword=body.keyword if body else None, contact_name=body.contact_name if body else None, mobile=body.mobile if body else None, @@ -1461,9 +1484,12 @@ async def contact_detail( contact_id = body.get("id") if not contact_id: return error_response(400, "缺少联系人ID") + tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) row = get_contact(db, contact_id) if not row: return error_response(404, "联系人不存在") + ensure_crm_contact_row_in_scope(db, row, user_info, tenant_no, message="无权访问该联系人") return success_response(contact_row_to_dict(row)) except ValueError as e: return error_response(400, str(e)) @@ -1480,10 +1506,11 @@ async def contact_by_customer( """按客户ID查询联系人""" try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) customer_id = body.get("customerId", "") if not customer_id: return error_response(400, "缺少客户ID") - rows = list_contacts_by_customer(db, tenant_no, customer_id) + rows = list_contacts_by_customer(db, tenant_no, customer_id, user_info=user_info) return success_response(rows) except ValueError as e: return error_response(400, str(e)) @@ -1500,9 +1527,11 @@ async def contact_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", "") + user_info = crm_user_info(_user) obj = create_contact( - db, tenant_no=tenant_no, user_id=user_id, + db, tenant_no=tenant_no, user_id=user_id, user_info=user_info, **body.model_dump(exclude_unset=True), ) return success_response(contact_row_to_dict(obj), message="创建成功") @@ -1523,7 +1552,9 @@ async def contact_update( user_id = _user.get("user_id", "") or _user.get("id", "") data = body.model_dump(exclude_unset=True) contact_id = data.pop("id") - obj = update_contact(db, contact_id, user_id=user_id, **data) + tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) + obj = update_contact(db, contact_id, user_id=user_id, tenant_no=tenant_no, user_info=user_info, **data) return success_response(contact_row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @@ -1539,8 +1570,11 @@ async def contact_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", "") for cid in body.ids: - soft_delete_contact(db, cid) + soft_delete_contact(db, cid, user_id=user_id, tenant_no=tenant_no, user_info=user_info) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) @@ -1560,7 +1594,9 @@ async def contact_set_primary( if not contact_id: return error_response(400, "缺少联系人ID") user_id = _user.get("user_id", "") or _user.get("id", "") - obj = set_primary_contact(db, contact_id, user_id=user_id) + tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) + obj = set_primary_contact(db, contact_id, user_id=user_id, tenant_no=tenant_no, user_info=user_info) return success_response(contact_row_to_dict(obj), message="设置成功") except ValueError as e: return error_response(400, str(e)) @@ -1577,8 +1613,9 @@ async def contact_export( """导出联系人列表""" try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) rows, _ = list_contacts( - db, tenant_no, page=1, page_size=10000, + db, tenant_no, page=1, page_size=10000, user_info=user_info, keyword=body.keyword if body else None, customer_id=body.customer_id if body else None, ) @@ -1606,10 +1643,11 @@ async def contact_decision( """决策人联系人列表""" try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) page = body.page if body else 1 page_size = body.pageSize if body else 10 rows, total = list_contacts( - db, tenant_no, page=page, page_size=page_size, + db, tenant_no, page=page, page_size=page_size, user_info=user_info, is_decision=True, keyword=body.keyword if body else None, ) @@ -1636,10 +1674,11 @@ async def contact_primary( """主联系人列表""" try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) page = body.page if body else 1 page_size = body.pageSize if body else 10 rows, total = list_contacts( - db, tenant_no, page=page, page_size=page_size, + db, tenant_no, page=page, page_size=page_size, user_info=user_info, is_primary=True, keyword=body.keyword if body else None, ) @@ -1666,10 +1705,11 @@ async def contact_birthday( """本月生日联系人列表""" try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) page = body.page if body else 1 page_size = body.pageSize if body else 10 rows, total = list_contacts( - db, tenant_no, page=page, page_size=page_size, + db, tenant_no, page=page, page_size=page_size, user_info=user_info, birthday_month=datetime.now().month, keyword=body.keyword if body else None, ) @@ -3044,10 +3084,12 @@ async def supplier_info_list( """分页查询供方列表 — Java: CrmSupplierInfoController.list""" try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) list_body = body or SupplierInfoListBody() rows, total = list_supplier_info( db, tenant_no, + user_info=user_info, page=list_body.page, page_size=list_body.pageSize, supplier_name=list_body.supplier_name, @@ -3077,10 +3119,12 @@ async def supplier_info_list_all( """供方全量列表(下拉)— Java: CrmSupplierInfoController.listAll""" try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) list_body = body or SupplierInfoListAllBody() rows = list_all_supplier_info( db, tenant_no, + user_info=user_info, supplier_name=list_body.supplier_name, status=list_body.status, ) @@ -3100,9 +3144,13 @@ async def supplier_info_detail( """供方详情 — Java: CrmSupplierInfoController.getInfo""" try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) obj = get_supplier_info(db, body.id, tenant_no=tenant_no) if not obj: return error_response(404, "供方信息不存在") + ensure_crm_create_by_row_in_scope( + db, obj, user_info, tenant_no, message="无权访问该供方信息" + ) return success_response(supplier_info_row_to_dict(obj)) except ValueError as e: return error_response(400, str(e)) @@ -3119,6 +3167,7 @@ async def supplier_info_add( """新增供方 — Java: CrmSupplierInfoController.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", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" obj = create_supplier_info( @@ -3144,6 +3193,7 @@ async def supplier_info_update( """修改供方 — Java: CrmSupplierInfoController.edit""" 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(exclude_unset=True) supplier_id = data.pop("id") @@ -3152,6 +3202,7 @@ async def supplier_info_update( supplier_id, tenant_no=tenant_no, user_id=user_id, + user_info=user_info, **data, ) return success_response(supplier_info_row_to_dict(obj), message="修改成功") @@ -3170,6 +3221,7 @@ async def supplier_info_delete( """删除供方 — Java: CrmSupplierInfoController.remove""" try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") ids_to_delete: List[str] = [] if body.ids: @@ -3198,10 +3250,12 @@ async def supplier_info_export( """导出供方列表 — Java: CrmSupplierInfoController.export""" try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) list_body = body or SupplierInfoListBody() rows, _ = list_supplier_info( db, tenant_no, + user_info=user_info, page=1, page_size=10000, supplier_name=list_body.supplier_name, @@ -3231,7 +3285,10 @@ async def _invoice_info_by_customer_impl( db: Session, ): tenant_no = get_tenant_no(db, _user) - rows = list_invoice_info_by_customer(db, tenant_no, body.customer_id) + user_info = crm_user_info(_user) + rows = list_invoice_info_by_customer( + db, tenant_no, body.customer_id, user_info=user_info + ) return success_response(rows) @@ -3274,11 +3331,13 @@ async def invoice_info_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", "") obj = create_invoice_info( db, tenant_no=tenant_no, user_id=user_id, + user_info=user_info, **body.model_dump(exclude_unset=True), ) return success_response(invoice_info_row_to_dict(obj), message="创建成功") @@ -3297,6 +3356,7 @@ async def invoice_info_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(exclude_unset=True) invoice_id = data.pop("id") @@ -3305,6 +3365,7 @@ async def invoice_info_update( invoice_id, tenant_no=tenant_no, user_id=user_id, + user_info=user_info, **data, ) return success_response(invoice_info_row_to_dict(obj), message="更新成功") @@ -3346,6 +3407,7 @@ async def invoice_info_set_default( """设置默认开票信息""" try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") obj = set_default_invoice_info( db, diff --git a/backend/app/api/crm_contract_routes.py b/backend/app/api/crm_contract_routes.py index 667af9682..f5aa99cc3 100644 --- a/backend/app/api/crm_contract_routes.py +++ b/backend/app/api/crm_contract_routes.py @@ -11,6 +11,7 @@ from app.core.database import get_db from app.core.menu_guard import require_admin_or_super_admin from app.core.response import error_response, success_response from app.core.tenant_context import get_tenant_no +from app.crud.crm.data_scope_helpers import crm_user_info from app.crud.crm.contract import ( batch_assign_owner, batch_submit_contract_approval, @@ -99,6 +100,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") query = ContractListQuery( pageNum=pageNum, @@ -131,6 +133,7 @@ def register_contract_routes(router): rows, total = list_contracts( db, tenant_no, + user_info=user_info, page=query.pageNum, page_size=query.pageSize, filters=filters, @@ -160,6 +163,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" detail = create_contract( @@ -183,6 +187,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" data = body.model_dump(by_alias=True) @@ -194,6 +199,7 @@ def register_contract_routes(router): data=data, user_id=user_id, dept_id=dept_id, + user_info=user_info, ) return success_response(detail, message="修改成功") except ValueError as e: @@ -209,6 +215,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) id_list = [x.strip() for x in ids.split(",") if x.strip()] soft_delete_contracts(db, id_list, tenant_no=tenant_no) return success_response(None, message="删除成功") @@ -225,6 +232,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" submit_contract_approval( @@ -235,6 +243,7 @@ def register_contract_routes(router): remark=body.remark, user_id=user_id, dept_id=dept_id, + user_info=user_info, ) return success_response(None, message="提交成功") except ValueError as e: @@ -250,6 +259,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" count = batch_submit_contract_approval( @@ -260,6 +270,7 @@ def register_contract_routes(router): remark=body.remark, user_id=user_id, dept_id=dept_id, + user_info=user_info, ) return success_response(count, message="批量提交完成") except ValueError as e: @@ -275,6 +286,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") withdraw_contract_approval( db, @@ -282,6 +294,7 @@ def register_contract_routes(router): tenant_no=tenant_no, remark=body.remark, user_id=user_id, + user_info=user_info, ) return success_response(None, message="撤回成功") except ValueError as e: @@ -297,6 +310,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") start_execute(db, body.id, tenant_no=tenant_no, remark=body.remark, user_id=user_id) return success_response(None, message="操作成功") @@ -313,6 +327,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") finish_contract(db, body.id, tenant_no=tenant_no, remark=body.remark, user_id=user_id) return success_response(None, message="操作成功") @@ -329,6 +344,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") invalid_contract( db, @@ -337,6 +353,7 @@ def register_contract_routes(router): invalid_reason=body.invalidReason, remark=body.remark, user_id=user_id, + user_info=user_info, ) return success_response(None, message="作废成功") except ValueError as e: @@ -352,12 +369,14 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) save_template( db, body.id, body.templateContent, tenant_no=tenant_no, settlement_clause_drafts=body.settlementClauseDrafts, + user_info=user_info, ) return success_response(None, message="保存成功") except ValueError as e: @@ -373,6 +392,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) content = get_template(db, contract_id, tenant_no=tenant_no) return success_response(content or "", message="获取模板成功") except ValueError as e: @@ -388,6 +408,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) upload_file(db, body.id, body.fileUrl, tenant_no=tenant_no) return success_response(None, message="上传成功") except ValueError as e: @@ -403,9 +424,11 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) if body.attachments: upload_attachments_with_category( - db, body.id, body.attachments, tenant_no=tenant_no + db, body.id, body.attachments, tenant_no=tenant_no, + user_info=user_info, ) elif body.ossIds: upload_attachments(db, body.id, body.ossIds, tenant_no=tenant_no) @@ -425,6 +448,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) list_body = body or ContractExportBody() filters = list_body.model_dump( by_alias=True, @@ -456,6 +480,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) req = ContractStatisticsOverviewQuery( ownerUserId=ownerUserId, beginTime=beginTime, @@ -467,6 +492,7 @@ def register_contract_routes(router): owner_user_id=req.ownerUserId, begin_time=req.beginTime, end_time=req.endTime, + user_info=user_info, ) return success_response(data) except ValueError as e: @@ -481,6 +507,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) return success_response(statistics_by_status(db, tenant_no)) except ValueError as e: return error_response(400, str(e)) @@ -494,6 +521,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) return success_response(statistics_by_type(db, tenant_no)) except ValueError as e: return error_response(400, str(e)) @@ -510,6 +538,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) req = ContractStatisticsTrendQuery(type=type, beginTime=beginTime, endTime=endTime) data = statistics_trend( db, @@ -517,6 +546,7 @@ def register_contract_routes(router): period_type=req.type or "month", begin_time=req.beginTime, end_time=req.endTime, + user_info=user_info, ) return success_response(data) except ValueError as e: @@ -532,6 +562,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) rows = query_expiring(db, tenant_no, days=days) return success_response(rows) except ValueError as e: @@ -548,6 +579,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" new_id = renew_contract( @@ -573,6 +605,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" contract_id = create_from_quote( @@ -598,6 +631,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" contract_id = create_from_opportunity( @@ -622,6 +656,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") sign_contract( db, @@ -631,6 +666,7 @@ def register_contract_routes(router): contract_file=body.contractFile, remark=body.remark or "", user_id=user_id, + user_info=user_info, ) return success_response(None, message="签订成功") except ValueError as e: @@ -646,6 +682,7 @@ def register_contract_routes(router): ): 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 = batch_assign_owner( db, @@ -654,6 +691,7 @@ def register_contract_routes(router): owner_user_id=body.ownerUserId, remark=body.remark or "", user_id=user_id, + user_info=user_info, ) return success_response(count, message="分配成功") except ValueError as e: @@ -669,6 +707,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) file_path = download_contract_pdf(db, contract_id, tenant_no=tenant_no) return success_response(file_path) except ValueError as e: @@ -684,6 +723,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") approve_contract_by_contract_id( db, @@ -693,6 +733,7 @@ def register_contract_routes(router): attachments=body.attachments or "", remark=body.remark or "", user_id=user_id, + user_info=user_info, ) db.commit() return success_response(None, message="审批成功") @@ -709,6 +750,7 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) rows = list_contract_approvals(db, contract_id, tenant_no=tenant_no) return success_response(rows) except ValueError as e: @@ -724,12 +766,14 @@ def register_contract_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") detail = get_contract_detail( db, contract_id, tenant_no=tenant_no, current_user_id=user_id, + user_info=user_info, ) return success_response(detail) except ValueError as e: diff --git a/backend/app/api/crm_contract_sub_routes.py b/backend/app/api/crm_contract_sub_routes.py index 78c4311da..3338eab16 100644 --- a/backend/app/api/crm_contract_sub_routes.py +++ b/backend/app/api/crm_contract_sub_routes.py @@ -11,6 +11,7 @@ from app.core.database import get_db from app.core.menu_guard import require_admin_or_super_admin from app.core.response import error_response, success_response from app.core.tenant_context import get_tenant_no +from app.crud.crm.data_scope_helpers import crm_user_info from app.crud.crm.contract import ( create_contract_change, create_contract_template, @@ -82,6 +83,7 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) query = ContractChangeListQuery( pageNum=pageNum, pageSize=pageSize, @@ -96,6 +98,7 @@ def register_contract_sub_routes(router): rows, total = list_contract_changes( db, tenant_no, + user_info=user_info, page=query.pageNum, page_size=query.pageSize, filters=query.model_dump(by_alias=True, exclude={"pageNum", "pageSize"}, exclude_none=True), @@ -123,7 +126,8 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) - detail = get_contract_change_detail(db, change_id, tenant_no=tenant_no) + user_info = crm_user_info(_user) + detail = get_contract_change_detail(db, change_id, tenant_no=tenant_no, user_info=user_info) return success_response(detail) except ValueError as e: return error_response(400, str(e)) @@ -138,6 +142,7 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" detail = create_contract_change( @@ -161,6 +166,7 @@ def register_contract_sub_routes(router): ): 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=True) change_id = data.pop("id") @@ -170,6 +176,7 @@ def register_contract_sub_routes(router): tenant_no=tenant_no, data=data, user_id=user_id, + user_info=user_info, ) return success_response(detail, message="修改成功") except ValueError as e: @@ -185,8 +192,9 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) id_list = [x.strip() for x in ids.split(",") if x.strip()] - soft_delete_contract_changes(db, id_list, tenant_no=tenant_no) + soft_delete_contract_changes(db, id_list, tenant_no=tenant_no, user_info=user_info) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) @@ -201,6 +209,7 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") submit_change_approval( db, @@ -224,6 +233,7 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") withdraw_change(db, change_id, tenant_no=tenant_no, user_id=user_id) return success_response(None, message="撤回成功") @@ -240,6 +250,7 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") execute_change(db, change_id, tenant_no=tenant_no, user_id=user_id) return success_response(None, message="执行成功") @@ -256,6 +267,7 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) list_body = body or ContractChangeExportBody() filters = list_body.model_dump( by_alias=True, @@ -293,6 +305,7 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) query = ContractTemplateListQuery( pageNum=pageNum, pageSize=pageSize, @@ -306,6 +319,7 @@ def register_contract_sub_routes(router): rows, total = list_contract_templates( db, tenant_no, + user_info=user_info, page=query.pageNum, page_size=query.pageSize, filters=query.model_dump(by_alias=True, exclude={"pageNum", "pageSize"}, exclude_none=True), @@ -333,6 +347,7 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) detail = get_contract_template_detail(db, template_id, tenant_no=tenant_no) return success_response(detail) except ValueError as e: @@ -348,6 +363,7 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" detail = create_contract_template( @@ -371,6 +387,7 @@ def register_contract_sub_routes(router): ): 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=True) template_id = data.pop("id") @@ -380,6 +397,7 @@ def register_contract_sub_routes(router): tenant_no=tenant_no, data=data, user_id=user_id, + user_info=user_info, ) return success_response(detail, message="修改成功") except ValueError as e: @@ -395,8 +413,9 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) id_list = [x.strip() for x in ids.split(",") if x.strip()] - soft_delete_contract_templates(db, id_list, tenant_no=tenant_no) + soft_delete_contract_templates(db, id_list, tenant_no=tenant_no, user_info=user_info) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) @@ -411,6 +430,7 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") toggle_contract_template_active( db, template_id, tenant_no=tenant_no, is_active=1, user_id=user_id @@ -429,6 +449,7 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") toggle_contract_template_active( db, template_id, tenant_no=tenant_no, is_active=0, user_id=user_id @@ -447,6 +468,7 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) list_body = body or ContractTemplateExportBody() filters = list_body.model_dump( by_alias=True, @@ -485,6 +507,7 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) query = ContractApprovalListQuery( pageNum=pageNum, pageSize=pageSize, @@ -526,6 +549,7 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) detail = get_contract_approval_detail(db, approval_id, tenant_no=tenant_no) return success_response(detail) except ValueError as e: @@ -595,6 +619,7 @@ def register_contract_sub_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) list_body = body or ContractApprovalExportBody() filters = list_body.model_dump( by_alias=True, diff --git a/backend/app/api/crm_quote_routes.py b/backend/app/api/crm_quote_routes.py index 28420c009..0177b331c 100644 --- a/backend/app/api/crm_quote_routes.py +++ b/backend/app/api/crm_quote_routes.py @@ -10,6 +10,7 @@ from app.core.database import get_db from app.core.menu_guard import require_admin_or_super_admin from app.core.response import error_response, success_response from app.core.tenant_context import get_tenant_no +from app.crud.crm.data_scope_helpers import crm_user_info from app.crud.crm.quote import ( accept_quote, create_new_version, @@ -88,11 +89,13 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) list_body = body or QuoteListBody() user_id = _user.get("user_id", "") or _user.get("id", "") rows, total = list_quotes( db, tenant_no, + user_info=user_info, page=list_body.page, page_size=list_body.pageSize, filters=list_body.model_dump(by_alias=True, exclude={"page", "pageSize"}), @@ -122,7 +125,8 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) - detail = get_quote_detail(db, body.id, tenant_no=tenant_no) + user_info = crm_user_info(_user) + detail = get_quote_detail(db, body.id, tenant_no=tenant_no, user_info=user_info) return success_response(detail) except ValueError as e: return error_response(400, str(e)) @@ -137,6 +141,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" detail = create_quote( @@ -160,6 +165,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" data = body.model_dump(by_alias=True) @@ -171,6 +177,7 @@ def register_quote_routes(router): data=data, user_id=user_id, dept_id=dept_id, + user_info=user_info, ) return success_response(detail, message="修改成功") except ValueError as e: @@ -186,6 +193,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) soft_delete_quotes(db, body.ids, tenant_no=tenant_no) return success_response(None, message="删除成功") except ValueError as e: @@ -201,12 +209,14 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) submit_quote( db, body.id, tenant_no=tenant_no, approver_ids=body.approverIds, remark=body.remark, + user_info=user_info, ) return success_response(None, message="提交成功") except ValueError as e: @@ -222,6 +232,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) save_template(db, body.id, body.templateContent, tenant_no=tenant_no) return success_response(None, message="保存成功") except ValueError as e: @@ -237,6 +248,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) content = get_template(db, body.id, tenant_no=tenant_no) return success_response(content or "") except ValueError as e: @@ -252,6 +264,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) pdf_url = generate_pdf(db, body.id, tenant_no=tenant_no) return success_response(pdf_url) except ValueError as e: @@ -267,12 +280,14 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) link = generate_share( db, body.id, tenant_no=tenant_no, validity_days=body.validityDays, password=body.password or "", + user_info=user_info, ) return success_response(link) except ValueError as e: @@ -288,6 +303,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) accept_quote(db, body.id, tenant_no=tenant_no, remark=body.remark) return success_response(None, message="操作成功") except ValueError as e: @@ -303,12 +319,14 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) reject_quote( db, body.id, tenant_no=tenant_no, reject_reason=body.rejectReason, remark=body.remark, + user_info=user_info, ) return success_response(None, message="操作成功") except ValueError as e: @@ -324,6 +342,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" contract_id = to_contract( @@ -337,6 +356,7 @@ def register_quote_routes(router): end_date=body.endDate, user_id=user_id, dept_id=dept_id, + user_info=user_info, ) return success_response(contract_id) except ValueError as e: @@ -352,6 +372,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" new_id = create_new_version( @@ -362,6 +383,7 @@ def register_quote_routes(router): remark=body.remark, user_id=user_id, dept_id=dept_id, + user_info=user_info, ) return success_response(new_id) except ValueError as e: @@ -377,6 +399,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) rows = query_versions(db, body.quoteNo, tenant_no=tenant_no) return success_response(rows) except ValueError as e: @@ -392,6 +415,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) invalid_quote(db, body.id, tenant_no=tenant_no, remark=body.remark) return success_response(None, message="作废成功") except ValueError as e: @@ -407,10 +431,12 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) list_body = body or QuoteListBody() rows, _ = list_quotes( db, tenant_no, + user_info=user_info, page=1, page_size=10000, filters=list_body.model_dump(by_alias=True, exclude={"page", "pageSize"}), @@ -437,6 +463,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) req = body or QuoteStatisticsOverviewBody() data = statistics_overview( db, @@ -444,6 +471,7 @@ def register_quote_routes(router): owner_user_id=req.ownerUserId, begin_time=req.beginTime, end_time=req.endTime, + user_info=user_info, ) return success_response(data) except ValueError as e: @@ -458,6 +486,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) return success_response(statistics_status(db, tenant_no)) except ValueError as e: return error_response(400, str(e)) @@ -472,6 +501,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) req = body or QuoteStatisticsTrendBody() data = statistics_trend( db, @@ -479,6 +509,7 @@ def register_quote_routes(router): period_type=req.type or "month", begin_time=req.beginTime, end_time=req.endTime, + user_info=user_info, ) return success_response(data) except ValueError as e: @@ -494,6 +525,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) rows = list_quote_products(db, body.quoteId, tenant_no=tenant_no) return success_response(rows) except ValueError as e: @@ -509,6 +541,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") dept_id = _user.get("dept_id", "") or _user.get("create_dept_id", "") or "" obj = insert_quote_product( @@ -534,6 +567,7 @@ def register_quote_routes(router): ): 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=True) line_id = data.pop("id") @@ -543,6 +577,7 @@ def register_quote_routes(router): tenant_no=tenant_no, data=data, user_id=user_id, + user_info=user_info, ) from app.crud.crm.quote_product import product_row_to_dict @@ -560,6 +595,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) soft_delete_quote_products(db, body.ids, tenant_no=tenant_no) return success_response(None, message="删除成功") except ValueError as e: @@ -575,6 +611,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) rows = list_quote_approvals(db, body.quoteId, tenant_no=tenant_no) return success_response(rows) except ValueError as e: @@ -590,6 +627,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") approve_quote( db, @@ -599,6 +637,7 @@ def register_quote_routes(router): approval_opinion=body.approvalOpinion or "", remark=body.remark or "", user_id=user_id, + user_info=user_info, ) return success_response(None, message="审批成功") except ValueError as e: @@ -614,6 +653,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) user_id = _user.get("user_id", "") or _user.get("id", "") withdraw_approval( db, @@ -621,6 +661,7 @@ def register_quote_routes(router): tenant_no=tenant_no, remark=body.remark or "", user_id=user_id, + user_info=user_info, ) return success_response(None, message="撤回成功") except ValueError as e: @@ -651,6 +692,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) rows = list_share_logs(db, body.quoteId, tenant_no=tenant_no) return success_response(rows) except ValueError as e: @@ -666,6 +708,7 @@ def register_quote_routes(router): ): try: tenant_no = get_tenant_no(db, _user) + user_info = crm_user_info(_user) stats = share_log_statistics(db, body.quoteId, tenant_no=tenant_no) return success_response(stats) except ValueError as e: diff --git a/backend/app/crud/crm/contact.py b/backend/app/crud/crm/contact.py index 5763d88f3..1d96c72cc 100644 --- a/backend/app/crud/crm/contact.py +++ b/backend/app/crud/crm/contact.py @@ -9,6 +9,11 @@ from sqlalchemy import func, or_ 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_contact_row_in_scope, + ensure_crm_owner_row_in_scope, +) from app.models.crm_models import CrmContact @@ -86,11 +91,14 @@ def list_contacts( is_decision: bool = None, birthday_month: int = None, role: str = None, + user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: q = db.query(CrmContact).filter( CrmContact.tenant_no == tenant_no, CrmContact.del_flag == 0, ) + if user_info: + q = apply_crm_create_by_scope(q, CrmContact, user_info, db, tenant_no) kw = (keyword or "").strip() if kw: @@ -146,7 +154,18 @@ def list_contacts_by_customer( db: Session, tenant_no: str, customer_id: str, + *, + user_info=None, ) -> List[Dict[str, Any]]: + if user_info: + from app.crud.crm.customer import get_customer + + customer = get_customer(db, customer_id) + if not customer or customer.tenant_no != tenant_no: + raise ValueError("客户不存在") + ensure_crm_owner_row_in_scope( + db, customer, user_info, tenant_no, message="无权访问该客户" + ) rows = ( db.query(CrmContact) .filter( @@ -177,14 +196,22 @@ def create_contact( tenant_no: str, user_id: str = None, dept_id: str = "", + user_info=None, **kwargs, ) -> CrmContact: customer_id = kwargs.get("customer_id") customer_name = kwargs.get("customer_name") - if customer_id and not customer_name: + if customer_id: from app.crud.crm.customer import get_customer + cust = get_customer(db, customer_id) - if cust: + if user_info: + if not cust or cust.tenant_no != tenant_no: + raise ValueError("客户不存在") + ensure_crm_owner_row_in_scope( + db, cust, user_info, tenant_no, message="无权访问该客户" + ) + if cust and not customer_name: customer_name = cust.customer_name obj = CrmContact( @@ -229,11 +256,16 @@ def update_contact( contact_id: str, *, user_id: str = None, + tenant_no: str = None, + user_info=None, **kwargs, ) -> CrmContact: obj = get_contact(db, contact_id) if not obj: raise ValueError("联系人不存在") + if tenant_no and obj.tenant_no != tenant_no: + raise ValueError("联系人不存在") + ensure_crm_contact_row_in_scope(db, obj, user_info, obj.tenant_no) _updatable = ( "contact_name", "gender", "mobile", "telephone", "email", @@ -260,10 +292,20 @@ def update_contact( return obj -def set_primary_contact(db: Session, contact_id: str, user_id: str = None) -> CrmContact: +def set_primary_contact( + db: Session, + contact_id: str, + user_id: str = None, + *, + tenant_no: str = None, + user_info=None, +) -> CrmContact: obj = get_contact(db, contact_id) if not obj: raise ValueError("联系人不存在") + if tenant_no and obj.tenant_no != tenant_no: + raise ValueError("联系人不存在") + ensure_crm_contact_row_in_scope(db, obj, user_info, obj.tenant_no) db.query(CrmContact).filter( CrmContact.customer_id == obj.customer_id, @@ -279,10 +321,20 @@ def set_primary_contact(db: Session, contact_id: str, user_id: str = None) -> Cr return obj -def soft_delete_contact(db: Session, contact_id: str, user_id: str = None) -> bool: +def soft_delete_contact( + db: Session, + contact_id: str, + user_id: str = None, + *, + tenant_no: str = None, + user_info=None, +) -> bool: obj = get_contact(db, contact_id) if not obj: raise ValueError("联系人不存在") + if tenant_no and obj.tenant_no != tenant_no: + raise ValueError("联系人不存在") + ensure_crm_contact_row_in_scope(db, obj, user_info, obj.tenant_no) customer_id = obj.customer_id obj.del_flag = 1 obj.update_by = user_id diff --git a/backend/app/crud/crm/contract.py b/backend/app/crud/crm/contract.py index 320d213b2..6671e09d7 100644 --- a/backend/app/crud/crm/contract.py +++ b/backend/app/crud/crm/contract.py @@ -13,6 +13,7 @@ from sqlalchemy import func, or_ from sqlalchemy.orm import Query, Session from app.core.utils import generate_uuid +from app.crud.crm.data_scope_helpers import apply_crm_owner_scope, ensure_crm_owner_row_in_scope, apply_crm_create_by_scope, ensure_crm_create_by_row_in_scope from app.crud.crm.contact import get_contact from app.crud.crm.contract_approval import ( can_user_approve, @@ -42,6 +43,25 @@ from app.models.crm_models import ( from app.models.models import SysUser _FMT = "%Y-%m-%d %H:%M:%S" + +def _ensure_contract_in_scope(db: Session, row: CrmContract, user_info, tenant_no: str) -> None: + ensure_crm_owner_row_in_scope(db, row, user_info, tenant_no, message="无权访问该合同") + + +def _get_contract_row_in_scope( + db: Session, + contract_id: str, + *, + tenant_no: str, + user_info=None, +) -> CrmContract: + row = get_contract_row(db, contract_id, tenant_no=tenant_no) + if not row: + raise ValueError("合同不存在") + _ensure_contract_in_scope(db, row, user_info, tenant_no) + return row + + _DATE_FMT = "%Y-%m-%d" @@ -374,10 +394,13 @@ def list_contracts( page_size: int = 10, filters: dict = None, current_user_id: str = None, + user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: """Java: queryPageList""" q = db.query(CrmContract) q = _build_contract_filters(db, q, filters or {}, tenant_no=tenant_no) + if user_info: + q = apply_crm_owner_scope(q, CrmContract, user_info, db, tenant_no) total = q.order_by(None).with_entities(func.count(CrmContract.id)).scalar() or 0 page = max(1, page) page_size = min(max(1, page_size), 100) @@ -403,6 +426,7 @@ def get_contract( *, tenant_no: str, current_user_id: str = None, + user_info=None, ) -> Optional[Dict[str, Any]]: """Java: queryById""" row = ( @@ -416,6 +440,7 @@ def get_contract( ) if not row: return None + _ensure_contract_in_scope(db, row, user_info, tenant_no) detail = _row_to_dict(row) detail["createByName"] = _resolve_user_display_name(db, detail.get("createBy")) @@ -506,12 +531,14 @@ def get_contract_detail( *, tenant_no: str, current_user_id: str = None, + user_info=None, ) -> Dict[str, Any]: detail = get_contract( db, contract_id, tenant_no=tenant_no, current_user_id=current_user_id, + user_info=user_info, ) if not detail: raise ValueError("合同不存在") @@ -957,9 +984,10 @@ def update_contract( data: dict, user_id: str = None, dept_id: str = "", + user_info=None, ) -> Dict[str, Any]: """Java: updateByBo""" - old = get_contract_row(db, contract_id, tenant_no=tenant_no) + old = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info) if not old: raise ValueError("合同不存在") if old.status != "draft": @@ -1026,11 +1054,12 @@ def soft_delete_contracts( *, tenant_no: str, validate: bool = True, + user_info=None, ) -> int: """Java: deleteWithValidByIds — 软删除""" count = 0 for cid in contract_ids: - entity = get_contract_row(db, cid, tenant_no=tenant_no) + entity = _get_contract_row_in_scope(db, cid, tenant_no=tenant_no, user_info=user_info) if not entity: continue if validate and entity.status != "draft": @@ -1116,9 +1145,10 @@ def submit_contract_approval( remark: str = "", user_id: str = None, dept_id: str = "", + user_info=None, ) -> bool: """Java: submitApproval""" - entity = get_contract_row(db, contract_id, tenant_no=tenant_no) + entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("合同不存在") if entity.status != "draft": @@ -1158,6 +1188,7 @@ def batch_submit_contract_approval( remark: str = "", user_id: str = None, dept_id: str = "", + user_info=None, ) -> int: """Java: batchSubmitApproval""" success = 0 @@ -1185,9 +1216,10 @@ def withdraw_contract_approval( tenant_no: str, remark: str = "", user_id: str = None, + user_info=None, ) -> bool: """Java: withdrawApproval""" - entity = get_contract_row(db, contract_id, tenant_no=tenant_no) + entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("合同不存在") if entity.status != "approving": @@ -1211,8 +1243,9 @@ def start_execute( tenant_no: str, remark: str = "", user_id: str = None, + user_info=None, ) -> bool: - entity = get_contract_row(db, contract_id, tenant_no=tenant_no) + entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("合同不存在") if entity.status != "approved": @@ -1233,8 +1266,9 @@ def finish_contract( tenant_no: str, remark: str = "", user_id: str = None, + user_info=None, ) -> bool: - entity = get_contract_row(db, contract_id, tenant_no=tenant_no) + entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("合同不存在") if entity.status != "executing": @@ -1256,10 +1290,11 @@ def invalid_contract( invalid_reason: str, remark: str = "", user_id: str = None, + user_info=None, ) -> bool: if not (invalid_reason or "").strip(): raise ValueError("作废原因不能为空") - entity = get_contract_row(db, contract_id, tenant_no=tenant_no) + entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("合同不存在") if entity.status == "finished": @@ -1282,8 +1317,9 @@ def save_template( *, tenant_no: str, settlement_clause_drafts: str = None, + user_info=None, ) -> bool: - entity = get_contract_row(db, contract_id, tenant_no=tenant_no) + entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("合同不存在") @@ -1305,7 +1341,7 @@ def save_template( def get_template(db: Session, contract_id: str, *, tenant_no: str) -> str: - entity = get_contract_row(db, contract_id, tenant_no=tenant_no) + entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("合同不存在") return entity.template_content or "" @@ -1317,8 +1353,9 @@ def upload_file( file_url: str, *, tenant_no: str, + user_info=None, ) -> bool: - entity = get_contract_row(db, contract_id, tenant_no=tenant_no) + entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("合同不存在") entity.contract_file = file_url or "" @@ -1333,10 +1370,11 @@ def upload_attachments( oss_ids: str, *, tenant_no: str, + user_info=None, ) -> bool: if not (oss_ids or "").strip(): raise ValueError("附件ossId不能为空") - entity = get_contract_row(db, contract_id, tenant_no=tenant_no) + entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("合同不存在") @@ -1357,10 +1395,11 @@ def upload_attachments_with_category( attachments: List[dict], *, tenant_no: str, + user_info=None, ) -> bool: if not attachments: raise ValueError("附件列表不能为空") - entity = get_contract_row(db, contract_id, tenant_no=tenant_no) + entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("合同不存在") @@ -1404,6 +1443,7 @@ def statistics_overview( owner_user_id: str = None, begin_time: str = None, end_time: str = None, + user_info=None, ) -> Dict[str, Any]: q = db.query(CrmContract).filter(CrmContract.tenant_no == tenant_no, CrmContract.del_flag == 0) if owner_user_id: @@ -1416,6 +1456,8 @@ def statistics_overview( q = q.filter(CrmContract.create_time >= begin_dt) elif end_dt: q = q.filter(CrmContract.create_time <= end_dt) + if user_info: + q = apply_crm_owner_scope(q, CrmContract, user_info, db, tenant_no) contracts = q.all() total_count = len(contracts) @@ -1457,12 +1499,11 @@ def statistics_overview( } -def statistics_by_status(db: Session, tenant_no: str) -> List[Dict[str, Any]]: - contracts = ( - db.query(CrmContract) - .filter(CrmContract.tenant_no == tenant_no, CrmContract.del_flag == 0) - .all() - ) +def statistics_by_status(db: Session, tenant_no: str, *, user_info=None) -> List[Dict[str, Any]]: + q = db.query(CrmContract).filter(CrmContract.tenant_no == tenant_no, CrmContract.del_flag == 0) + if user_info: + q = apply_crm_owner_scope(q, CrmContract, user_info, db, tenant_no) + contracts = q.all() total_count = len(contracts) total_amount = sum(_decimal_value(c.amount) for c in contracts) @@ -1492,12 +1533,11 @@ def statistics_by_status(db: Session, tenant_no: str) -> List[Dict[str, Any]]: return result -def statistics_by_type(db: Session, tenant_no: str) -> List[Dict[str, Any]]: - contracts = ( - db.query(CrmContract) - .filter(CrmContract.tenant_no == tenant_no, CrmContract.del_flag == 0) - .all() - ) +def statistics_by_type(db: Session, tenant_no: str, *, user_info=None) -> List[Dict[str, Any]]: + q = db.query(CrmContract).filter(CrmContract.tenant_no == tenant_no, CrmContract.del_flag == 0) + if user_info: + q = apply_crm_owner_scope(q, CrmContract, user_info, db, tenant_no) + contracts = q.all() total_count = len(contracts) type_map = ( ("sale", "销售合同"), @@ -1544,12 +1584,15 @@ def statistics_trend( period_type: str = "month", begin_time: str = None, end_time: str = None, + user_info=None, ) -> List[Dict[str, Any]]: q = db.query(CrmContract).filter(CrmContract.tenant_no == tenant_no, CrmContract.del_flag == 0) begin_dt = _parse_optional_datetime(begin_time) end_dt = _parse_optional_datetime(end_time) if begin_dt and end_dt: q = q.filter(CrmContract.create_time >= begin_dt, CrmContract.create_time <= end_dt) + if user_info: + q = apply_crm_owner_scope(q, CrmContract, user_info, db, tenant_no) contracts = q.all() if not contracts: return [] @@ -1585,6 +1628,7 @@ def query_expiring( tenant_no: str, *, days: int = 30, + user_info=None, ) -> List[Dict[str, Any]]: if not days or days <= 0: days = 30 @@ -1642,6 +1686,7 @@ def create_from_quote( data: dict, user_id: str = None, dept_id: str = "", + user_info=None, ) -> str: """Java: createFromQuote — 返回合同 id""" quote = get_quote(db, quote_id, tenant_no=tenant_no) @@ -1700,6 +1745,7 @@ def renew_contract( data: dict, user_id: str = None, dept_id: str = "", + user_info=None, ) -> str: """Java: renewContract — 基础续签""" parent = get_contract_row(db, parent_contract_id, tenant_no=tenant_no) @@ -1750,6 +1796,7 @@ def export_contracts( tenant_no: str, *, filters: dict = None, + user_info=None, ) -> List[Dict[str, Any]]: """Java: queryList for export""" q = db.query(CrmContract) @@ -1790,11 +1837,14 @@ def list_contract_changes( page: int = 1, page_size: int = 10, filters: dict = None, + user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: q = db.query(CrmContractChange).filter( CrmContractChange.tenant_no == tenant_no, CrmContractChange.del_flag == 0, ) + if user_info: + q = apply_crm_create_by_scope(q, CrmContractChange, user_info, db, tenant_no) filters = filters or {} contract_id = (filters.get("contractId") or filters.get("contract_id") or "").strip() if contract_id: @@ -1814,6 +1864,7 @@ def get_contract_change_detail( change_id: str, *, tenant_no: str, + user_info=None, ) -> Dict[str, Any]: row = ( db.query(CrmContractChange) @@ -1826,6 +1877,9 @@ def get_contract_change_detail( ) if not row: raise ValueError("合同变更记录不存在") + ensure_crm_create_by_row_in_scope( + db, row, user_info, tenant_no, message="无权访问该合同变更" + ) return _change_row_to_dict(row) @@ -1861,11 +1915,14 @@ def list_contract_templates( page: int = 1, page_size: int = 10, filters: dict = None, + user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: q = db.query(CrmContractTemplate).filter( CrmContractTemplate.tenant_no == tenant_no, CrmContractTemplate.del_flag == 0, ) + if user_info: + q = apply_crm_create_by_scope(q, CrmContractTemplate, user_info, db, tenant_no) filters = filters or {} template_no = (filters.get("templateNo") or filters.get("template_no") or "").strip() if template_no: @@ -1937,6 +1994,7 @@ def create_from_opportunity( data: dict, user_id: str = None, dept_id: str = "", + user_info=None, ) -> str: """Java: createFromOpportunity — 返回合同 id""" opp_row = ( @@ -1990,9 +2048,10 @@ def sign_contract( contract_file: str, remark: str = "", user_id: str = None, + user_info=None, ) -> bool: """Java: signContract""" - entity = get_contract_row(db, contract_id, tenant_no=tenant_no) + entity = _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("合同不存在") if entity.status != "approved": @@ -2019,6 +2078,7 @@ def batch_assign_owner( owner_user_id: str, remark: str = "", user_id: str = None, + user_info=None, ) -> int: """Java: batchAssignOwner""" if not contract_ids: @@ -2030,7 +2090,7 @@ def batch_assign_owner( success = 0 owner_name = _resolve_user_display_name(db, owner_id) for cid in contract_ids: - entity = get_contract_row(db, cid, tenant_no=tenant_no) + entity = _get_contract_row_in_scope(db, cid, tenant_no=tenant_no, user_info=user_info) if not entity: continue entity.owner_user_id = owner_id @@ -2049,6 +2109,7 @@ def download_contract_pdf( contract_id: str, *, tenant_no: str, + user_info=None, ) -> str: """Java: downloadContractPdf — 返回合同文件 URL/路径""" detail = get_contract_detail(db, contract_id, tenant_no=tenant_no) @@ -2177,10 +2238,14 @@ def update_contract_change( tenant_no: str, data: dict, user_id: str = None, + user_info=None, ) -> Dict[str, Any]: entity = _get_change_row(db, change_id, tenant_no=tenant_no) if not entity: raise ValueError("合同变更记录不存在") + ensure_crm_create_by_row_in_scope( + db, entity, user_info, tenant_no, message="无权访问该合同变更" + ) if entity.approval_status != "pending": raise ValueError("只能修改待审批状态的变更记录") @@ -2210,12 +2275,16 @@ def soft_delete_contract_changes( *, tenant_no: str, validate: bool = True, + user_info=None, ) -> int: count = 0 for cid in change_ids: entity = _get_change_row(db, cid, tenant_no=tenant_no) if not entity: continue + ensure_crm_create_by_row_in_scope( + db, entity, user_info, tenant_no, message="无权访问该合同变更" + ) if validate and entity.approval_status != "pending": raise ValueError("只能删除待审批状态的变更记录") entity.del_flag = 1 @@ -2346,6 +2415,7 @@ def update_contract_template( tenant_no: str, data: dict, user_id: str = None, + user_info=None, ) -> Dict[str, Any]: entity = ( db.query(CrmContractTemplate) @@ -2358,6 +2428,9 @@ def update_contract_template( ) if not entity: raise ValueError("合同模板不存在") + ensure_crm_create_by_row_in_scope( + db, entity, user_info, tenant_no, message="无权访问该合同模板" + ) field_map = ( (("template_no", "templateNo"), "template_no"), @@ -2398,6 +2471,7 @@ def soft_delete_contract_templates( *, tenant_no: str, validate: bool = True, + user_info=None, ) -> int: count = 0 for tid in template_ids: @@ -2412,6 +2486,9 @@ def soft_delete_contract_templates( ) if not entity: continue + ensure_crm_create_by_row_in_scope( + db, entity, user_info, tenant_no, message="无权访问该合同模板" + ) if validate and (entity.usage_count or 0) > 0: raise ValueError(f"模板【{entity.template_name}】已被使用,不能删除") entity.del_flag = 1 diff --git a/backend/app/crud/crm/contract_product.py b/backend/app/crud/crm/contract_product.py index 290cb2558..2c8ccc43e 100644 --- a/backend/app/crud/crm/contract_product.py +++ b/backend/app/crud/crm/contract_product.py @@ -76,7 +76,12 @@ def list_contract_products( *, tenant_no: str = None, include_price_tiers: bool = False, + user_info=None, ) -> List[Dict[str, Any]]: + if tenant_no and user_info: + from app.crud.crm.contract import _get_contract_row_in_scope + + _get_contract_row_in_scope(db, contract_id, tenant_no=tenant_no, user_info=user_info) q = db.query(CrmContractProduct).filter( CrmContractProduct.contract_id == contract_id, CrmContractProduct.del_flag == 0, diff --git a/backend/app/crud/crm/data_scope_helpers.py b/backend/app/crud/crm/data_scope_helpers.py index 0de7eff44..62cfb53ee 100644 --- a/backend/app/crud/crm/data_scope_helpers.py +++ b/backend/app/crud/crm/data_scope_helpers.py @@ -52,3 +52,51 @@ def ensure_crm_create_by_row_in_scope( return if not row_in_data_scope(db, row, user_info, tenant_no): raise ValueError(message) + + +def ensure_crm_contact_row_in_scope( + db: Session, row, user_info, tenant_no: str, *, message: str = "无权访问该联系人" +) -> None: + """联系人:create_by 在范围内,或关联客户在 owner 范围内。""" + if not user_info or row is None: + return + if row_in_data_scope(db, row, user_info, tenant_no): + return + customer_id = getattr(row, "customer_id", None) or "" + if customer_id: + from app.crud.crm.customer import get_customer + + customer = get_customer(db, customer_id) + if customer and customer.tenant_no == tenant_no: + if row_in_data_scope( + db, customer, user_info, tenant_no, owner_field=CRM_OWNER_FIELD + ): + return + raise ValueError(message) + + +def ensure_crm_customer_linked_row_in_scope( + db: Session, + row, + user_info, + tenant_no: str, + *, + customer_id: str = None, + message: str = "无权访问该数据", +) -> None: + """客户子表(开票信息等):先校验关联客户 owner scope,否则校验 create_by。""" + if not user_info or row is None: + return + cid = customer_id or getattr(row, "customer_id", None) or "" + if cid: + from app.crud.crm.customer import get_customer + + customer = get_customer(db, cid) + if customer and customer.tenant_no == tenant_no: + if row_in_data_scope( + db, customer, user_info, tenant_no, owner_field=CRM_OWNER_FIELD + ): + return + if row_in_data_scope(db, row, user_info, tenant_no): + return + raise ValueError(message) diff --git a/backend/app/crud/crm/invoice_info.py b/backend/app/crud/crm/invoice_info.py index 9027b9bc2..dde969422 100644 --- a/backend/app/crud/crm/invoice_info.py +++ b/backend/app/crud/crm/invoice_info.py @@ -7,6 +7,10 @@ from typing import Any, Dict, List, Optional from sqlalchemy.orm import Session from app.core.utils import generate_uuid +from app.crud.crm.data_scope_helpers import ( + ensure_crm_customer_linked_row_in_scope, + ensure_crm_owner_row_in_scope, +) from app.models.crm_models import CrmInvoiceInfo @@ -39,7 +43,18 @@ def list_invoice_info_by_customer( db: Session, tenant_no: str, customer_id: str, + *, + user_info=None, ) -> List[Dict[str, Any]]: + if user_info: + from app.crud.crm.customer import get_customer + + customer = get_customer(db, customer_id) + if not customer or customer.tenant_no != tenant_no: + raise ValueError("客户不存在") + ensure_crm_owner_row_in_scope( + db, customer, user_info, tenant_no, message="无权访问该客户" + ) rows = ( db.query(CrmInvoiceInfo) .filter( @@ -87,11 +102,21 @@ def create_invoice_info( tenant_no: str, user_id: str = None, dept_id: str = "", + user_info=None, **kwargs, ) -> CrmInvoiceInfo: customer_id = kwargs.get("customer_id") if not customer_id: raise ValueError("缺少客户ID") + if user_info: + from app.crud.crm.customer import get_customer + + customer = get_customer(db, customer_id) + if not customer or customer.tenant_no != tenant_no: + raise ValueError("客户不存在") + ensure_crm_owner_row_in_scope( + db, customer, user_info, tenant_no, message="无权访问该客户" + ) company_name = (kwargs.get("company_name") or "").strip() if not company_name: @@ -132,6 +157,7 @@ def update_invoice_info( *, tenant_no: str = None, user_id: str = None, + user_info=None, **kwargs, ) -> CrmInvoiceInfo: obj = get_invoice_info(db, invoice_id) @@ -140,6 +166,9 @@ def update_invoice_info( if tenant_no and obj.tenant_no != tenant_no: raise ValueError("开票信息不存在") + ensure_crm_customer_linked_row_in_scope( + db, obj, user_info, obj.tenant_no, message="无权访问该开票信息" + ) _updatable = ( "company_name", @@ -181,12 +210,16 @@ def set_default_invoice_info( *, tenant_no: str, user_id: str = None, + user_info=None, ) -> CrmInvoiceInfo: obj = get_invoice_info(db, invoice_id) if not obj or obj.tenant_no != tenant_no: raise ValueError("开票信息不存在") if obj.customer_id != customer_id: raise ValueError("开票信息与客户不匹配") + ensure_crm_customer_linked_row_in_scope( + db, obj, user_info, tenant_no, message="无权访问该开票信息" + ) _clear_default_for_customer(db, tenant_no, customer_id, exclude_id=invoice_id) obj.is_default = "1" @@ -197,10 +230,22 @@ def set_default_invoice_info( return obj -def soft_delete_invoice_info(db: Session, invoice_id: str, user_id: str = None) -> bool: +def soft_delete_invoice_info( + db: Session, + invoice_id: str, + user_id: str = None, + *, + tenant_no: str = None, + user_info=None, +) -> bool: obj = get_invoice_info(db, invoice_id) if not obj: raise ValueError("开票信息不存在") + if tenant_no and obj.tenant_no != tenant_no: + raise ValueError("开票信息不存在") + ensure_crm_customer_linked_row_in_scope( + db, obj, user_info, obj.tenant_no, message="无权访问该开票信息" + ) obj.del_flag = 1 obj.update_by = user_id obj.update_time = datetime.now() diff --git a/backend/app/crud/crm/product.py b/backend/app/crud/crm/product.py index b6bcc803f..34ab517b7 100644 --- a/backend/app/crud/crm/product.py +++ b/backend/app/crud/crm/product.py @@ -10,6 +10,10 @@ from sqlalchemy import and_, func, or_ 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.models.crm_models import CrmProduct, CrmProductCategory, SysOss @@ -48,12 +52,15 @@ def list_categories( *, category_name: Optional[str] = None, status: Optional[str] = None, + user_info=None, ) -> List[Dict[str, Any]]: """获取分类(扁平列表,支持名称模糊与状态筛选)""" q = db.query(CrmProductCategory).filter( CrmProductCategory.tenant_no == tenant_no, CrmProductCategory.del_flag == 0, ) + if user_info: + q = apply_crm_create_by_scope(q, CrmProductCategory, user_info, db, tenant_no) name_kw = (category_name or "").strip() if name_kw: q = q.filter(CrmProductCategory.category_name.like(f"%{name_kw}%")) @@ -99,6 +106,7 @@ def list_category_tree( *, category_name: Optional[str] = None, status: Optional[str] = None, + user_info=None, ) -> List[Dict[str, Any]]: """获取产品分类树(嵌套 children,供树形表格/树选择器使用)""" name_kw = (category_name or "").strip() @@ -106,7 +114,7 @@ def list_category_tree( has_filter = bool(name_kw) or bool(status_val) if not has_filter: - flat = list_categories(db, tenant_no) + flat = list_categories(db, tenant_no, user_info=user_info) return _build_category_tree(flat) matched = list_categories( @@ -114,6 +122,7 @@ def list_category_tree( tenant_no, category_name=category_name, status=status, + user_info=user_info, ) if not matched: return [] @@ -178,12 +187,16 @@ def update_category( category_id: str, *, user_id: str = None, + user_info=None, **kwargs, ) -> CrmProductCategory: """更新分类""" obj = get_category(db, category_id) if not obj: raise ValueError("分类不存在") + ensure_crm_create_by_row_in_scope( + db, obj, user_info, obj.tenant_no, message="无权访问该分类" + ) # 逐字段更新 for field in ("category_name", "category_code", "status", "order_num", "remark"): @@ -208,11 +221,14 @@ def update_category( return obj -def soft_delete_category(db: Session, category_id: str) -> None: +def soft_delete_category(db: Session, category_id: str, *, user_info=None) -> None: """软删除分类""" obj = get_category(db, category_id) if not obj: raise ValueError("分类不存在") + ensure_crm_create_by_row_in_scope( + db, obj, user_info, obj.tenant_no, message="无权访问该分类" + ) # 检查是否有子分类 has_children = ( @@ -303,12 +319,15 @@ def list_products( keyword: str = None, category_id: str = None, status: str = None, + user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: """分页查询产品列表""" q = db.query(CrmProduct).filter( CrmProduct.tenant_no == tenant_no, CrmProduct.del_flag == 0, ) + if user_info: + q = apply_crm_create_by_scope(q, CrmProduct, user_info, db, tenant_no) # keyword 筛选 kw = (keyword or "").strip() @@ -482,12 +501,16 @@ def update_product( product_id: str, *, user_id: str = None, + user_info=None, **kwargs, ) -> CrmProduct: """更新产品""" obj = get_product(db, product_id) if not obj: raise ValueError("产品不存在") + ensure_crm_create_by_row_in_scope( + db, obj, user_info, obj.tenant_no, message="无权访问该产品" + ) # 逐字段更新 for field in ( @@ -535,11 +558,14 @@ def update_product( return obj -def soft_delete_product(db: Session, product_id: str) -> None: +def soft_delete_product(db: Session, product_id: str, *, user_info=None) -> None: """软删除产品""" obj = get_product(db, product_id) if not obj: raise ValueError("产品不存在") + ensure_crm_create_by_row_in_scope( + db, obj, user_info, obj.tenant_no, message="无权访问该产品" + ) obj.del_flag = 1 db.commit() @@ -584,6 +610,7 @@ def list_product_options( *, keyword: str = None, category_id: str = None, + user_info=None, ) -> List[Dict[str, Any]]: """获取产品下拉选项(不分页,返回全量)""" q = db.query(CrmProduct).filter( @@ -591,6 +618,8 @@ def list_product_options( CrmProduct.del_flag == 0, CrmProduct.status == "on", ) + if user_info: + q = apply_crm_create_by_scope(q, CrmProduct, user_info, db, tenant_no) if keyword: like = f"%{keyword}%" q = q.filter(or_(CrmProduct.product_name.like(like), CrmProduct.product_no.like(like))) diff --git a/backend/app/crud/crm/quote.py b/backend/app/crud/crm/quote.py index 0ae393687..e2b348934 100644 --- a/backend/app/crud/crm/quote.py +++ b/backend/app/crud/crm/quote.py @@ -31,10 +31,28 @@ from app.crud.crm.supplier_info import get_supplier_info from app.models.crm_models import CrmContract, CrmQuote, CrmQuoteApproval from app.models.models import SysUser from app.services.crm_quote_pdf import generate_quote_pdf +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" +def _ensure_quote_in_scope(db: Session, row: CrmQuote, user_info, tenant_no: str) -> None: + ensure_crm_owner_row_in_scope(db, row, user_info, tenant_no, message="无权访问该报价单") + + +def _get_quote_in_scope( + db: Session, + quote_id: str, + *, + tenant_no: str, + user_info=None, +) -> CrmQuote: + obj = get_quote(db, quote_id, tenant_no=tenant_no) + if not obj: + raise ValueError("报价单不存在") + _ensure_quote_in_scope(db, obj, user_info, tenant_no) + return obj + def total_pages(total: int, page_size: int) -> int: if page_size <= 0: @@ -467,11 +485,13 @@ def get_quote_detail( quote_id: str, *, tenant_no: str = None, + user_info=None, ) -> Dict[str, Any]: """Java: queryById""" obj = get_quote(db, quote_id, tenant_no=tenant_no) if not obj: raise ValueError("报价单不存在") + _ensure_quote_in_scope(db, obj, user_info, tenant_no or obj.tenant_no) detail = _row_to_dict(obj) detail["productList"] = list_quote_products(db, quote_id, tenant_no=tenant_no or obj.tenant_no) @@ -513,10 +533,13 @@ def list_quotes( page_size: int = 10, filters: dict = None, current_user_id: str = None, + user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: """Java: queryPageList""" q = db.query(CrmQuote) q = _build_quote_filters(db, q, filters or {}, tenant_no=tenant_no) + if user_info: + q = apply_crm_owner_scope(q, CrmQuote, user_info, db, tenant_no) total = q.order_by(None).with_entities(func.count(CrmQuote.id)).scalar() or 0 page = max(1, page) page_size = min(max(1, page_size), 100) @@ -645,9 +668,10 @@ def update_quote( data: dict, user_id: str = None, dept_id: str = "", + user_info=None, ) -> Dict[str, Any]: """Java: updateByBo""" - old = get_quote(db, quote_id, tenant_no=tenant_no) + old = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info) if not old: raise ValueError("报价单不存在") if old.status != "draft": @@ -769,11 +793,12 @@ def soft_delete_quotes( *, tenant_no: str, validate: bool = True, + user_info=None, ) -> int: """Java: deleteWithValidByIds""" count = 0 for qid in quote_ids: - entity = get_quote(db, qid, tenant_no=tenant_no) + entity = _get_quote_in_scope(db, qid, tenant_no=tenant_no, user_info=user_info) if not entity: continue if validate: @@ -797,9 +822,10 @@ def submit_quote( remark: str = "", user_id: str = None, dept_id: str = "", + user_info=None, ) -> bool: """Java: submitQuote""" - entity = get_quote(db, quote_id, tenant_no=tenant_no) + entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("报价单不存在") if entity.status != "draft": @@ -837,9 +863,10 @@ def save_template( template_content: str, *, tenant_no: str, + user_info=None, ) -> bool: """Java: saveTemplate""" - entity = get_quote(db, quote_id, tenant_no=tenant_no) + entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("报价单不存在") entity.template_content = template_content @@ -853,9 +880,10 @@ def get_template( quote_id: str, *, tenant_no: str, + user_info=None, ) -> str: """Java: getTemplate""" - entity = get_quote(db, quote_id, tenant_no=tenant_no) + entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("报价单不存在") return entity.template_content or "" @@ -866,9 +894,10 @@ def generate_pdf( quote_id: str, *, tenant_no: str, + user_info=None, ) -> str: """Java: generatePdf""" - entity = get_quote(db, quote_id, tenant_no=tenant_no) + entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("报价单不存在") detail = get_quote_detail(db, quote_id, tenant_no=tenant_no) @@ -887,9 +916,10 @@ def generate_share( tenant_no: str, validity_days: int = None, password: str = "", + user_info=None, ) -> str: """Java: generateShare""" - entity = get_quote(db, quote_id, tenant_no=tenant_no) + entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("报价单不存在") @@ -966,9 +996,10 @@ def accept_quote( *, tenant_no: str, remark: str = "", + user_info=None, ) -> bool: """Java: acceptQuote""" - entity = get_quote(db, quote_id, tenant_no=tenant_no) + entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("报价单不存在") entity.status = "accepted" @@ -986,9 +1017,10 @@ def reject_quote( tenant_no: str, reject_reason: str = "", remark: str = "", + user_info=None, ) -> bool: """Java: rejectQuote""" - entity = get_quote(db, quote_id, tenant_no=tenant_no) + entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("报价单不存在") entity.status = "rejected" @@ -1012,6 +1044,7 @@ def to_contract( end_date: date = None, user_id: str = None, dept_id: str = "", + user_info=None, ) -> str: """Java: toContract — 返回 contractId""" sign_date = _parse_optional_date(sign_date) @@ -1075,9 +1108,10 @@ def create_new_version( remark: str = "", user_id: str = None, dept_id: str = "", + user_info=None, ) -> str: """Java: createNewVersion — 返回新报价单 id""" - old = get_quote(db, quote_id, tenant_no=tenant_no) + old = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info) if not old: raise ValueError("报价单不存在") @@ -1150,6 +1184,7 @@ def query_versions( quote_no: str, *, tenant_no: str, + user_info=None, ) -> List[Dict[str, Any]]: """Java: queryVersions""" rows = ( @@ -1172,9 +1207,10 @@ def withdraw_quote_approval( tenant_no: str, remark: str = "", user_id: str = None, + user_info=None, ) -> bool: """Java: CrmQuoteServiceImpl.withdrawApproval(校验创建人)""" - entity = get_quote(db, quote_id, tenant_no=tenant_no) + entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("报价单不存在") if entity.status != "submitted": @@ -1197,9 +1233,10 @@ def invalid_quote( *, tenant_no: str, remark: str = "", + user_info=None, ) -> bool: """Java: invalidQuote""" - entity = get_quote(db, quote_id, tenant_no=tenant_no) + entity = _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info) if not entity: raise ValueError("报价单不存在") if entity.status == "invalid": @@ -1221,6 +1258,7 @@ def statistics_overview( owner_user_id: str = None, begin_time: str = None, end_time: str = None, + user_info=None, ) -> Dict[str, Any]: """Java: statisticsOverview""" q = db.query(CrmQuote).filter(CrmQuote.tenant_no == tenant_no, CrmQuote.del_flag == 0) @@ -1232,6 +1270,8 @@ def statistics_overview( q = q.filter(CrmQuote.create_time >= begin_dt) if end_dt: q = q.filter(CrmQuote.create_time <= end_dt) + if user_info: + q = apply_crm_owner_scope(q, CrmQuote, user_info, db, tenant_no) all_quotes = q.all() total_count = len(all_quotes) @@ -1273,28 +1313,21 @@ def statistics_overview( def statistics_status( db: Session, tenant_no: str, + user_info=None, ) -> List[Dict[str, Any]]: """Java: statisticsStatus""" status_array = ("draft", "submitted", "accepted", "rejected", "expired", "contract") status_names = ("草稿", "已提交", "已接受", "已拒绝", "已过期", "已转合同") - total = ( - db.query(CrmQuote) - .filter(CrmQuote.tenant_no == tenant_no, CrmQuote.del_flag == 0) - .count() - ) + base_q = db.query(CrmQuote).filter(CrmQuote.tenant_no == tenant_no, CrmQuote.del_flag == 0) + if user_info: + base_q = apply_crm_owner_scope(base_q, CrmQuote, user_info, db, tenant_no) + total = base_q.count() result = [] for status, status_name in zip(status_array, status_names): - rows = ( - db.query(CrmQuote) - .filter( - CrmQuote.tenant_no == tenant_no, - CrmQuote.del_flag == 0, - CrmQuote.status == status, - ) - .all() - ) + q = base_q.filter(CrmQuote.status == status) + rows = q.all() count = len(rows) amount = sum(_decimal_value(r.final_amount) for r in rows) percent = (count * 100.0 / total) if total else 0.0 @@ -1326,6 +1359,7 @@ def statistics_trend( period_type: str = "month", begin_time: str = None, end_time: str = None, + user_info=None, ) -> List[Dict[str, Any]]: """Java: statisticsTrend""" q = db.query(CrmQuote).filter(CrmQuote.tenant_no == tenant_no, CrmQuote.del_flag == 0) @@ -1335,6 +1369,8 @@ def statistics_trend( q = q.filter(CrmQuote.create_time >= begin_dt) if end_dt: q = q.filter(CrmQuote.create_time <= end_dt) + if user_info: + q = apply_crm_owner_scope(q, CrmQuote, user_info, db, tenant_no) all_quotes = q.all() if not all_quotes: diff --git a/backend/app/crud/crm/quote_approval.py b/backend/app/crud/crm/quote_approval.py index af6352e86..e4e8ff9df 100644 --- a/backend/app/crud/crm/quote_approval.py +++ b/backend/app/crud/crm/quote_approval.py @@ -44,8 +44,13 @@ def list_quote_approvals( quote_id: str, *, tenant_no: str = None, + user_info=None, ) -> List[Dict[str, Any]]: """Java: queryListByQuoteId(含审批人昵称)""" + if tenant_no and user_info: + from app.crud.crm.quote import _get_quote_in_scope + + _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info) q = db.query(CrmQuoteApproval).filter( CrmQuoteApproval.quote_id == quote_id, CrmQuoteApproval.del_flag == 0, diff --git a/backend/app/crud/crm/quote_product.py b/backend/app/crud/crm/quote_product.py index aeafa486a..f12eeede6 100644 --- a/backend/app/crud/crm/quote_product.py +++ b/backend/app/crud/crm/quote_product.py @@ -13,6 +13,13 @@ from app.models.crm_models import CrmQuoteProduct _FMT = "%Y-%m-%d %H:%M:%S" +def _ensure_quote_parent_scope(db: Session, quote_id: str, tenant_no: str, user_info) -> None: + if user_info: + from app.crud.crm.quote import _get_quote_in_scope + + _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info) + + def _decimal_value(value: Any, default: Decimal = Decimal("0")) -> Decimal: if value is None: @@ -76,8 +83,11 @@ def list_quote_products( quote_id: str, *, tenant_no: str = None, + user_info=None, ) -> List[Dict[str, Any]]: """Java: queryListByQuoteId""" + if tenant_no and user_info: + _ensure_quote_parent_scope(db, quote_id, tenant_no, user_info) q = db.query(CrmQuoteProduct).filter( CrmQuoteProduct.quote_id == quote_id, CrmQuoteProduct.del_flag == 0, @@ -104,8 +114,10 @@ def insert_quote_product( data: dict, user_id: str = None, dept_id: str = "", + user_info=None, ) -> CrmQuoteProduct: """Java: insertByBo""" + _ensure_quote_parent_scope(db, quote_id, tenant_no, user_info) price = _decimal_value(_get_field(data, "price"), Decimal("0")) quantity = _decimal_value(_get_field(data, "quantity"), Decimal("0")) discount = _decimal_value(_get_field(data, "discount"), Decimal("0")) @@ -169,11 +181,14 @@ def update_quote_product( *, data: dict, user_id: str = None, + user_info=None, ) -> CrmQuoteProduct: """Java: updateByBo""" entity = get_quote_product(db, product_line_id) if not entity: raise ValueError("报价产品明细不存在") + if user_info: + _ensure_quote_parent_scope(db, entity.quote_id, entity.tenant_no, user_info) if "product_id" in data or "productId" in data: entity.product_id = (_get_field(data, "product_id", "productId") or entity.product_id or "").strip() @@ -217,6 +232,7 @@ def soft_delete_quote_products( ids: List[str], *, validate_min_one: bool = True, + user_info=None, ) -> int: """Java: deleteWithValidByIds""" count = 0 @@ -224,6 +240,8 @@ def soft_delete_quote_products( entity = get_quote_product(db, pid) if not entity: continue + if user_info: + _ensure_quote_parent_scope(db, entity.quote_id, entity.tenant_no, user_info) if validate_min_one: sibling_count = ( db.query(CrmQuoteProduct) diff --git a/backend/app/crud/crm/quote_share_log.py b/backend/app/crud/crm/quote_share_log.py index 746f39420..039245741 100644 --- a/backend/app/crud/crm/quote_share_log.py +++ b/backend/app/crud/crm/quote_share_log.py @@ -7,7 +7,6 @@ from typing import Any, Dict, List, Optional from sqlalchemy.orm import Session -from app.core.utils import generate_uuid from app.models.crm_models import CrmQuoteShareLog _FMT = "%Y-%m-%d %H:%M:%S" @@ -78,8 +77,13 @@ def list_share_logs( quote_id: str, *, tenant_no: str = None, + user_info=None, ) -> List[Dict[str, Any]]: """Java: queryLogsByQuoteId""" + if tenant_no and user_info: + from app.crud.crm.quote import _get_quote_in_scope + + _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info) q = db.query(CrmQuoteShareLog).filter( CrmQuoteShareLog.quote_id == quote_id, CrmQuoteShareLog.del_flag == 0, @@ -95,8 +99,13 @@ def share_log_statistics( quote_id: str, *, tenant_no: str = None, + user_info=None, ) -> Dict[str, Any]: """Java: statisticsVisits""" + if tenant_no and user_info: + from app.crud.crm.quote import _get_quote_in_scope + + _get_quote_in_scope(db, quote_id, tenant_no=tenant_no, user_info=user_info) q = db.query(CrmQuoteShareLog).filter( CrmQuoteShareLog.quote_id == quote_id, CrmQuoteShareLog.del_flag == 0, diff --git a/backend/app/crud/crm/supplier_info.py b/backend/app/crud/crm/supplier_info.py index 0af1cd890..51c5bc302 100644 --- a/backend/app/crud/crm/supplier_info.py +++ b/backend/app/crud/crm/supplier_info.py @@ -8,6 +8,10 @@ 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.models.crm_models import CrmSupplierInfo _FMT = "%Y-%m-%d %H:%M:%S" @@ -74,12 +78,15 @@ def list_supplier_info( page_size: int = 10, supplier_name: str = None, status: str = None, + user_info=None, ) -> Tuple[List[Dict[str, Any]], int]: """Java: queryPageList""" q = db.query(CrmSupplierInfo).filter( CrmSupplierInfo.tenant_no == tenant_no, CrmSupplierInfo.del_flag == 0, ) + if user_info: + q = apply_crm_create_by_scope(q, CrmSupplierInfo, user_info, db, tenant_no) q = _apply_list_filters(q, supplier_name=supplier_name, status=status) total = q.count() rows = ( @@ -97,12 +104,15 @@ def list_all_supplier_info( *, supplier_name: str = None, status: str = None, + user_info=None, ) -> List[Dict[str, Any]]: """Java: queryList(不分页,报价单下拉)""" q = db.query(CrmSupplierInfo).filter( CrmSupplierInfo.tenant_no == tenant_no, CrmSupplierInfo.del_flag == 0, ) + if user_info: + q = apply_crm_create_by_scope(q, CrmSupplierInfo, user_info, db, tenant_no) q = _apply_list_filters(q, supplier_name=supplier_name, status=status) rows = q.order_by(CrmSupplierInfo.create_time.desc()).all() return [_row_to_dict(r) for r in rows] @@ -168,12 +178,16 @@ def update_supplier_info( *, tenant_no: str = None, user_id: str = None, + user_info=None, **kwargs, ) -> CrmSupplierInfo: """Java: updateByBo""" obj = get_supplier_info(db, supplier_id, tenant_no=tenant_no) if not obj: raise ValueError("供方信息不存在") + ensure_crm_create_by_row_in_scope( + db, obj, user_info, obj.tenant_no, message="无权访问该供方信息" + ) if "supplier_name" in kwargs and kwargs["supplier_name"] is not None: name = (kwargs["supplier_name"] or "").strip() @@ -200,11 +214,15 @@ def soft_delete_supplier_info( *, tenant_no: str = None, user_id: str = None, + user_info=None, ) -> bool: """Java: deleteWithValidByIds(门户软删)""" obj = get_supplier_info(db, supplier_id, tenant_no=tenant_no) if not obj: raise ValueError("供方信息不存在") + ensure_crm_create_by_row_in_scope( + db, obj, user_info, obj.tenant_no, message="无权访问该供方信息" + ) obj.del_flag = 1 obj.update_by = user_id obj.update_time = datetime.now()