diff --git a/finance/urls.py b/finance/urls.py index 243cc1b..d6f57ec 100644 --- a/finance/urls.py +++ b/finance/urls.py @@ -1,4 +1,4 @@ -from .views import UserRegister,UnregisteredUserList,RegisteredUserList,UserDeparture,UserDepartureDetail,EditUserDeparture,DeleteUserDeparture,issueAnInvoice,issueAnInvoiceDetail,confirm,loan,PaymentRequest,reimbursement,confirmdisplay,loandisplay,PaymentDisplay,reimbursementdetail,Change,ChangeDetail,EditInvoice,DeleteInvoice,EditIncome,DeleteIncome,EditAccounts,DeleteAccounts,EditPayment,DeletePayment,EditReimbursement,DeleteReimbursement,EditBonusChange,DeleteBonusChange,GetCaseListForInvoice,SearchCaseByContractNo,AddInvoice +from .views import UserRegister,UnregisteredUserList,RegisteredUserList,UserDeparture,UserDepartureDetail,EditUserDeparture,DeleteUserDeparture,issueAnInvoice,issueAnInvoiceDetail,confirm,loan,PaymentRequest,reimbursement,confirmdisplay,loandisplay,PaymentDisplay,reimbursementdetail,Change,ChangeDetail,EditInvoice,DeleteInvoice,EditIncome,DeleteIncome,EditAccounts,DeleteAccounts,EditPayment,DeletePayment,EditReimbursement,DeleteReimbursement,EditBonusChange,DeleteBonusChange,GetCaseListForInvoice,SearchCaseByContractNo,AddInvoice,GetApproversListForIncomeConfirm from django.urls import path urlpatterns = [ @@ -18,6 +18,7 @@ urlpatterns = [ path('editInvoice', EditInvoice.as_view(), name="editInvoice/"), path('deleteInvoice', DeleteInvoice.as_view(), name="deleteInvoice/"), path('confirm', confirm.as_view(), name="confirm/"), + path('approvers-list-for-income-confirm', GetApproversListForIncomeConfirm.as_view(), name="approvers-list-for-income-confirm/"), path('confirmdisplay', confirmdisplay.as_view(), name="confirmdisplay/"), path('editIncome', EditIncome.as_view(), name="editIncome/"), path('deleteIncome', DeleteIncome.as_view(), name="deleteIncome/"), diff --git a/finance/views.py b/finance/views.py index 5734304..ea3ee49 100644 --- a/finance/views.py +++ b/finance/views.py @@ -871,27 +871,30 @@ class confirm(APIView): """ 收入确认 优化后: - 1. 通过合同号搜索选择案件(case_id或project_id) + 1. 通过合同号关联案件(ContractNo必填) 2. 从案件管理中同步:客户名称、负责人 3. 手动填写:收款日期、收款金额 - 4. 财务填写好后直接抄送给案件负责人 - 5. 负责人在待办中填写收入分配后,再抄送给财务负责人 + 4. 按照团队审核逻辑处理审核流程(与其他审核待办一致) :param request: :param args: :param kwargs: :return: """ - # 案件选择(二选一) - case_id = request.data.get('case_id') # 案件管理ID - project_id = request.data.get('project_id') # 立项登记ID + # 必填参数:合同号 + ContractNo = request.data.get('ContractNo') # 合同号(必填,用于关联案件) # 手动填写信息 times = request.data.get('times') # 收款日期 amount = request.data.get('amount') # 收款金额 - # 可选字段(如果提供了case_id或project_id,将从案件管理中同步) - ContractNo = request.data.get('ContractNo') # 合同号(可选,如果提供了case_id或project_id则自动同步) - CustomerID = request.data.get('CustomerID') # 客户名称(可选,如果提供了case_id或project_id则自动同步) + # 可选字段(如果通过合同号找到了案件,将从案件管理中同步) + CustomerID = request.data.get('CustomerID') # 客户名称(可选,如果通过合同号找到案件则自动同步) + + # 审核人列表(可选,多人团队时需要,推荐:用户ID数组如[1,2,3],兼容:用户名数组) + approvers = request.data.get('approvers') + # 兼容旧接口:如果传了 personincharge,转换为 approvers + personincharge = request.data.get('personincharge') + approvers = normalize_approvers_param(approvers, personincharge) allocate = request.data.get('allocate') # 收入分配(财务提交时不填写,由负责人填写) token = request.META.get('token') @@ -908,8 +911,8 @@ class confirm(APIView): missing_params.append('times(收款日期)') if not amount: missing_params.append('amount(收款金额)') - if not case_id and not project_id: - missing_params.append('case_id或project_id(请选择案件)') + if not ContractNo: + missing_params.append('ContractNo(合同号)') if missing_params: return Response({ @@ -917,8 +920,18 @@ class confirm(APIView): 'message': f'缺少必填参数: {", ".join(missing_params)}', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) + + # 获取提交人的团队信息(用于审核流程) + from User.models import Team + team_name = user.team + team = None + if team_name: + try: + team = Team.objects.get(name=team_name, is_deleted=False) + except Team.DoesNotExist: + pass - # 从案件管理中提取信息 + # 通过合同号从案件管理中提取信息 from business.models import Case, ProjectRegistration import json @@ -926,66 +939,53 @@ class confirm(APIView): responsible_person_username = None # 负责人用户名(用于抄送) case_info = None - if case_id: - # 从Case(案件管理)中获取信息 + # 优先从Case(案件管理)中查找 + try: + case = Case.objects.select_related('project').get(contract_no=ContractNo, is_deleted=False) + + # 获取客户名称 + CustomerID = CustomerID or case.client_name + + # 获取负责人信息 + responsiblefor = case.responsiblefor + + # 如果案件信息不完整,从关联的ProjectRegistration获取 + if case.project: + project = case.project + CustomerID = CustomerID or project.client_info + responsiblefor = responsiblefor or project.responsiblefor + + # 解析负责人信息 try: - case = Case.objects.select_related('project').get(id=case_id, is_deleted=False) - - # 获取合同号 - ContractNo = ContractNo or case.contract_no - - # 获取客户名称 - CustomerID = CustomerID or case.client_name - - # 获取负责人信息 - responsiblefor = case.responsiblefor - - # 如果案件信息不完整,从关联的ProjectRegistration获取 - if case.project: - project = case.project - ContractNo = ContractNo or project.ContractNo - CustomerID = CustomerID or project.client_info - responsiblefor = responsiblefor or project.responsiblefor - - # 解析负责人信息 - try: - if responsiblefor: - if isinstance(responsiblefor, str): - responsiblefor_dict = json.loads(responsiblefor) - else: - responsiblefor_dict = responsiblefor - responsible_person = responsiblefor_dict.get('responsible_person', '') - # 查找负责人用户名 - if responsible_person: - try: - responsible_user = User.objects.get(username=responsible_person, is_deleted=False) - responsible_person_username = responsible_user.username - except User.DoesNotExist: - pass - except (json.JSONDecodeError, TypeError, AttributeError): - responsible_person = str(responsiblefor) if responsiblefor else '' - - case_info = { - 'id': case.id, - 'type': 'case', - 'contract_no': ContractNo, - 'customer_name': CustomerID, - 'responsible_person': responsible_person - } - - except Case.DoesNotExist: - return Response({ - 'status': 'error', - 'message': '案件不存在或已被删除', - 'code': 1 - }, status=status.HTTP_404_NOT_FOUND) - - elif project_id: - # 从ProjectRegistration(立项登记)中获取信息 + if responsiblefor: + if isinstance(responsiblefor, str): + responsiblefor_dict = json.loads(responsiblefor) + else: + responsiblefor_dict = responsiblefor + responsible_person = responsiblefor_dict.get('responsible_person', '') + # 查找负责人用户名 + if responsible_person: + try: + responsible_user = User.objects.get(username=responsible_person, is_deleted=False) + responsible_person_username = responsible_user.username + except User.DoesNotExist: + pass + except (json.JSONDecodeError, TypeError, AttributeError): + responsible_person = str(responsiblefor) if responsiblefor else '' + + case_info = { + 'id': case.id, + 'type': 'case', + 'contract_no': ContractNo, + 'customer_name': CustomerID, + 'responsible_person': responsible_person + } + + except Case.DoesNotExist: + # 如果Case中找不到,尝试从ProjectRegistration(立项登记)中查找 try: - project = ProjectRegistration.objects.get(id=project_id, is_deleted=False) + project = ProjectRegistration.objects.get(ContractNo=ContractNo, is_deleted=False) - ContractNo = ContractNo or project.ContractNo CustomerID = CustomerID or project.client_info responsiblefor = project.responsiblefor @@ -1018,11 +1018,11 @@ class confirm(APIView): except ProjectRegistration.DoesNotExist: return Response({ 'status': 'error', - 'message': '立项登记不存在或已被删除', + 'message': f'未找到合同号为"{ContractNo}"的案件或立项登记,请检查合同号是否正确', 'code': 1 }, status=status.HTTP_404_NOT_FOUND) - # 验证合同号、客户名称和负责人是否获取成功 + # 验证合同号、客户名称是否获取成功 if not ContractNo: return Response({ 'status': 'error', @@ -1036,19 +1036,16 @@ class confirm(APIView): 'message': '无法从案件管理中获取客户名称,请检查案件信息', 'code': 1 }, status=status.HTTP_400_BAD_REQUEST) - - if not responsible_person_username: - return Response({ - 'status': 'error', - 'message': f'无法找到负责人"{responsible_person}"的用户信息,请检查负责人是否正确', - 'code': 1 - }, status=status.HTTP_400_BAD_REQUEST) from datetime import datetime now = datetime.now() date_string = now.strftime("%Y-%m-%d") - # 创建收入确认记录,allocate为"待负责人指定" + # 根据团队类型决定初始状态 + # 如果是团队类型且需要审核,状态为"审核中";否则为"待财务处理" + initial_state = "审核中" if (team and team.team_type == 'team') else "待财务处理" + + # 创建收入确认记录 income = Income.objects.create( times=times, ContractNo=ContractNo, @@ -1057,12 +1054,11 @@ class confirm(APIView): allocate=allocate if allocate else "待负责人指定", # 如果未提供,设为"待负责人指定" submit=user.username, submit_tiem=date_string, - state="待负责人填写分配" # 新状态:等待负责人填写收入分配 + state=initial_state ) - # 直接抄送给案件负责人(不经过团队审批流程) - from User.models import Approval - from User.utils import get_finance_personincharge_value + # 使用统一的审核流程函数(与其他审核待办一致) + from User.utils import create_approval_with_team_logic # 构建审批内容 content_parts = [ @@ -1072,18 +1068,40 @@ class confirm(APIView): f"收款金额:{amount}", f"收入分配:待负责人指定" ] + if responsible_person: + content_parts.append(f"案件负责人:{responsible_person}") content = ",".join(content_parts) - # 创建审批记录,直接抄送给负责人 - approval = Approval.objects.create( - title=user.username + "提交收入确认", - content=content, - times=date_string, - personincharge=responsible_person_username, # 直接抄送给案件负责人 - state="审核中", # 负责人需要填写收入分配 - type="收入确认", - user_id=str(income.id) - ) + # 使用统一的审核流程函数 + try: + approval, approvers_order_json, needs_approval = create_approval_with_team_logic( + team_name=team_name, + approvers=approvers, + title=user.username + "提交收入确认", + content=content, + approval_type="收入确认", + user_id=str(income.id), + business_record=income, + today=date_string + ) + except Exception as e: + # 记录异常并返回错误信息 + import logging + logger = logging.getLogger(__name__) + logger.error(f"创建收入确认审批失败: {str(e)}", exc_info=True) + return Response({ + 'status': 'error', + 'message': f'创建审批流程失败: {str(e)},请检查团队配置和审核人信息', + 'code': 1 + }, status=status.HTTP_400_BAD_REQUEST) + + # 如果返回None且需要审核,说明缺少审核人 + if approval is None and needs_approval: + return Response({ + 'status': 'error', + 'message': build_missing_approvers_message(team_name, approvers), + 'code': 1 + }, status=status.HTTP_400_BAD_REQUEST) # 记录操作日志 new_data = { @@ -1092,9 +1110,10 @@ class confirm(APIView): 'CustomerID': income.CustomerID, 'amount': income.amount, 'allocate': income.allocate, - 'case_id': case_id if case_id else None, - 'project_id': project_id if project_id else None, - 'responsible_person': responsible_person_username + 'case_info': case_info, + 'responsible_person': responsible_person, + 'team_type': team.team_type if team else 'unknown', + 'approvers': approvers if (team and team.team_type == 'team') else None } log_operation( request=request, @@ -1105,23 +1124,71 @@ class confirm(APIView): target_id=income.id, target_name=f'{income.ContractNo} - {income.CustomerID}', new_data=new_data, - remark=f'新增收入确认:合同号 {income.ContractNo},金额 {income.amount},已抄送给负责人 {responsible_person_username}' + remark=f'新增收入确认:合同号 {income.ContractNo},金额 {income.amount},团队类型 {team.team_type if team else "未知"}' ) return Response({ - 'message': '提交成功,已抄送给负责人', + 'message': '提交成功', 'code': 0, 'data': { 'id': income.id, 'ContractNo': income.ContractNo, 'CustomerID': income.CustomerID, - 'responsible_person': responsible_person_username, + 'responsible_person': responsible_person, 'state': income.state, - 'approval_id': approval.id + 'approval_id': approval.id if approval else None, + 'needs_approval': team is None or team.team_type != 'personal' } }, status=status.HTTP_200_OK) +class GetApproversListForIncomeConfirm(APIView): + """获取收入确认的审核人列表接口""" + + def post(self, request, *args, **kwargs): + """ + 获取收入确认时可选的审核人列表 + 返回所有未删除的用户列表,格式简洁,适合下拉选择 + :param request: + :param args: + :param kwargs: + :return: + """ + # 可选参数:用户名搜索 + username = request.data.get('username', '') # 用户名搜索(可选) + + # 查询条件:只查询未软删除的用户 + Q_obj = Q(is_deleted=False) + + # 排除admin用户(超级管理员) + Q_obj &= ~Q(username='admin') + Q_obj &= ~Q(account='admin') + + # 用户名搜索(模糊匹配) + if username: + Q_obj &= Q(username__icontains=username) + + # 查询用户,按用户名排序 + users = User.objects.filter(Q_obj).order_by('username') + + # 构建返回数据(格式简洁,适合下拉选择) + data = [] + for user in users: + data.append({ + 'id': user.id, + 'username': user.username, # 用户名(用于显示和选择) + 'account': user.account, # 账号(辅助信息) + 'position': user.position if user.position else '', # 岗位(辅助信息) + }) + + return Response({ + 'message': '获取审核人列表成功', + 'code': 0, + 'total': len(data), + 'data': data + }, status=status.HTTP_200_OK) + + class confirmdisplay(APIView): def post(self, request, *args, **kwargs): """ diff --git a/接口重复功能分析报告.md b/接口重复功能分析报告.md new file mode 100644 index 0000000..43bf6a3 --- /dev/null +++ b/接口重复功能分析报告.md @@ -0,0 +1,219 @@ +# 接口重复功能分析报告 + +## 一、明确的重复接口 + +### 1. 财务模块 - 未入职用户列表接口重复 + +**重复的接口:** +- `POST /finance/user-register-detail` → `UnregisteredUserList` +- `POST /finance/unregistered-user-list` → `UnregisteredUserList` + +**问题:** +- 两个URL路径指向同一个视图类 +- `user-register-detail` 在注释中标注为"向后兼容,保留旧URL" + +**建议:** +- 保留 `unregistered-user-list`(更符合RESTful命名规范) +- 删除 `user-register-detail`(旧接口,已标注为兼容) + +**位置:** `finance/urls.py` 第7行和第6行 + +--- + +## 二、功能重叠的接口 + +### 2. 获取案件列表接口(开票相关) + +**接口1:** `GetCaseListForInvoice` +- 路径:`POST /finance/case-list-for-invoice` +- 功能:获取所有案件列表(仅从ProjectRegistration获取) +- 返回:案件ID、合同号、负责人、项目类型、立项时间 + +**接口2:** `SearchCaseByContractNo` +- 路径:`POST /finance/search-case-by-contract-no` +- 功能:通过合同号搜索案件(从Case和ProjectRegistration搜索) +- 返回:更详细的案件信息,包括客户名称、相对方等 + +**分析:** +- `GetCaseListForInvoice` 功能较简单,只返回所有案件 +- `SearchCaseByContractNo` 功能更强大,支持搜索且返回信息更完整 +- 两者功能有重叠,但使用场景不同 + +**建议:** +- 可以合并:在 `GetCaseListForInvoice` 中添加搜索参数,使其支持按合同号搜索 +- 或者保留两个接口,但明确使用场景: + - `GetCaseListForInvoice`:用于下拉选择(返回所有案件) + - `SearchCaseByContractNo`:用于搜索(支持模糊搜索) + +--- + +### 3. 开票申请接口 + +**接口1:** `issueAnInvoice` +- 路径:`POST /finance/issue-invoice` +- 功能:财务开票申请 +- 特点: + - 支持通过case_id选择案件 + - 需要审批流程(state="审核中") + - 必须提供token验证 + +**接口2:** `AddInvoice` +- 路径:`POST /finance/add-invoice` +- 功能:增开票申请 +- 特点: + - 支持通过case_id或project_id选择案件 + - 直接抄送财务(state="待财务处理") + - 不需要审批流程 + +**分析:** +- 两个接口功能相似,都是创建开票申请 +- 主要区别在于审批流程不同 +- `AddInvoice` 功能更完善(支持project_id,直接抄送财务) + +**建议:** +- 可以合并为一个接口,通过参数控制是否需要审批 +- 或者保留两个接口,但明确使用场景: + - `issueAnInvoice`:普通开票(需要审批) + - `AddInvoice`:增开票(直接抄送财务) + +--- + +### 4. 案件标签设置接口 + +**接口1:** `EditCase` +- 路径:`POST /business/editCase` +- 功能:编辑案件信息(包括设置标签) +- 特点: + - 可以编辑案件的各种信息(时间、文件、金额等) + - 支持通过 `tag_ids` 参数设置标签 + - 需要提供案件ID和其他可选字段 + +**接口2:** `SetCaseTags` +- 路径:`POST /business/set-case-tags` +- 功能:专门用于设置案件标签 +- 特点: + - 只设置标签,不修改其他信息 + - 更简洁,只需提供case_id和tag_ids + - 有更详细的标签验证和错误提示 + +**分析:** +- `EditCase` 功能更全面,但设置标签只是其功能之一 +- `SetCaseTags` 更专注,专门用于标签管理 +- 两者功能有重叠,但使用场景不同 + +**建议:** +- 保留两个接口,但明确使用场景: + - `EditCase`:编辑案件信息时顺便设置标签 + - `SetCaseTags`:专门用于批量设置或只设置标签的场景 + +--- + +## 三、命名容易混淆的接口 + +### 5. 日程相关接口命名 + +**接口1:** `DeleteSchedule`(但URL是 `scheduledetail`) +- 路径:`POST /business/scheduledetail` +- 功能:删除日程 +- 问题:URL名称 `scheduledetail` 暗示是详情接口,但实际是删除功能 + +**接口2:** `ScheduleDetail` +- 路径:`POST /business/ScheduleDetail` +- 功能:日程展示(列表) + +**建议:** +- 将 `scheduledetail` 改为 `delete-schedule` 或 `schedule-delete` +- 或者将 `DeleteSchedule` 类名改为更明确的名称 + +--- + +### 6. 律师文件相关接口命名 + +**接口1:** `LawyersdocumentsDetail` +- 路径:`POST /business/lawdisplay` +- 功能:律师文件列表展示 + +**接口2:** `LwaDetail` +- 路径:`POST /business/LwaDetail` +- 功能:删除律师文件(软删除) +- 问题:类名和URL都暗示是详情接口,但实际是删除功能 + +**建议:** +- 将 `LwaDetail` 改为 `DeleteLawyerFile` 或 `LawyerFileDelete` +- 将URL `LwaDetail` 改为 `delete-lawyer-file` + +--- + +## 四、功能相似但用途不同的接口 + +### 7. 下拉列表接口 + +**接口1:** `ProjectDropdownList` +- 路径:`POST /business/project-dropdown-list` +- 功能:获取立项登记下拉列表 + +**接口2:** `CaseDropdownList` +- 路径:`POST /business/case-dropdown-list` +- 功能:获取案件下拉列表 + +**分析:** +- 两个接口功能相似,都是获取下拉列表 +- 但数据源不同(ProjectRegistration vs Case) +- 属于正常的功能分离,不是重复 + +**建议:** +- 保持现状,这是合理的接口设计 + +--- + +## 五、总结和建议 + +### 需要立即处理的重复接口: + +1. **删除旧接口:** `user-register-detail`(finance/urls.py 第7行) + - 已有新接口 `unregistered-user-list` 替代 + +### 建议优化的接口: + +2. **合并或明确分工:** + - `GetCaseListForInvoice` 和 `SearchCaseByContractNo`:建议合并或明确使用场景 + - `issueAnInvoice` 和 `AddInvoice`:建议合并或明确使用场景 + +3. **重命名接口:** + - `scheduledetail` → `delete-schedule` 或 `schedule-delete` + - `LwaDetail` → `delete-lawyer-file` + +### 可以保留的接口: + +4. **功能重叠但使用场景不同:** + - `EditCase` 和 `SetCaseTags`:保留两个接口,但明确使用场景 + +--- + +## 六、接口统计 + +- **business模块:** 约60个接口 +- **finance模块:** 约30个接口 +- **User模块:** 约20个接口 +- **总计:** 约110个接口 + +**发现的重复/问题接口:** 6组 + +--- + +## 七、建议的优化方案 + +### 方案1:删除明确的重复接口 +- 删除 `user-register-detail` 路由 + +### 方案2:统一接口命名规范 +- 使用RESTful风格:`resource-action` 格式 +- 例如:`delete-schedule`、`delete-lawyer-file` + +### 方案3:合并功能相似的接口 +- 在 `GetCaseListForInvoice` 中添加搜索参数 +- 在 `issueAnInvoice` 中添加参数控制审批流程 + +### 方案4:完善接口文档 +- 明确每个接口的使用场景 +- 标注接口的优先级(推荐使用/兼容保留)