Compare commits

...

42 Commits

Author SHA1 Message Date
27942
f3a3ac61e5 哈哈 2026-02-06 02:42:30 +08:00
27942
fe90de7e1f haa 2026-02-06 02:33:33 +08:00
27942
3ad5a16297 优化合同编号 2026-02-06 02:26:29 +08:00
27942
b3e6235c20 哈哈 2026-02-06 02:19:54 +08:00
27942
456b7ca03b haha 2026-02-06 02:08:20 +08:00
27942
13883519a4 haha 2026-02-06 01:55:53 +08:00
27942
a1a65b8725 优化利益检索 2026-02-06 01:40:03 +08:00
27942
52be5caa7f 优化利益检索 2026-02-06 01:26:00 +08:00
27942
9203b17f2b 优化合同编号 2026-02-06 00:40:49 +08:00
ddrwode
de2d87553d 优化利益冲突 2026-02-05 16:44:17 +08:00
ddrwode
64483a8ec7 优化 2026-02-05 15:46:02 +08:00
ddrwode
4306291fd6 优化案件通过标签搜索 2026-02-05 15:44:19 +08:00
ddrwode
f0a52cd07b 优化案件搜索的标签搜索 2026-02-05 15:41:44 +08:00
ddrwode
6c4cdf9daa 优化案件搜索 2026-02-05 15:37:51 +08:00
ddrwode
f69ef6c930 待办中返回列表文件数据 2026-02-05 11:50:50 +08:00
ddrwode
9228327a76 待办中返回列表文件数据 2026-02-05 11:43:14 +08:00
ddrwode
d5aeae380c 待办中返回列表文件数据 2026-02-05 11:39:29 +08:00
ddrwode
a5ab26e3c1 待办中返回列表文件数据 2026-02-05 10:52:57 +08:00
ddrwode
ba27d531a0 待办中返回列表文件数据 2026-02-05 10:46:34 +08:00
ddrwode
8434b4651f 优化投标登记搜索 2026-02-05 10:36:17 +08:00
ddrwode
107192b14c 优化案件生成 2026-02-04 14:13:17 +08:00
ddrwode
f533cfde79 优化案件生成 2026-02-04 14:05:35 +08:00
ddrwode
27275e7884 优化案件生成 2026-02-04 14:03:15 +08:00
ddrwode
92b0c3b136 删除不必要的文件 2026-02-04 13:45:24 +08:00
27942
4970ee3676 优化合同编号 2026-02-03 20:27:50 +08:00
Administrator
b42e9808a6 Merge remote-tracking branch 'origin/master' 2026-02-02 17:16:06 +08:00
Administrator
2682dc1f1e 日程搜索 2026-02-02 17:15:44 +08:00
Administrator
3493d62348 Merge remote-tracking branch 'origin/master' 2026-02-02 17:05:56 +08:00
Administrator
22b5a8ed0c 1.日程搜索 2026-02-02 17:03:47 +08:00
27942
ca8561796c 加入导出案件日志功能 2026-02-02 16:58:33 +08:00
Administrator
20a148b4fa 1.修改了公告附件与制度附件转,json,空数据不转json 2026-02-02 16:40:54 +08:00
Administrator
f5fdc8dd3f 1.修改了公告附件与制度附件转json 2026-02-02 16:23:42 +08:00
27942
9b19987387 加入导出案件日志功能 2026-02-01 23:07:28 +08:00
27942
f2aff9add8 加入导出案件日志功能 2026-02-01 23:03:45 +08:00
27942
149d18f72e 加入导出案件日志功能 2026-02-01 22:53:15 +08:00
27942
ca512b9626 加入导出案件日志功能 2026-02-01 20:22:04 +08:00
27942
a03ef1cde8 加入导出案件日志功能 2026-02-01 18:17:40 +08:00
27942
5db0af8360 优化收入确认 2026-02-01 17:36:37 +08:00
27942
609b66fe8a 优化收入确认 2026-02-01 17:07:08 +08:00
27942
dcbf5bb829 优化结案流程 2026-02-01 14:36:21 +08:00
27942
f187a4939a 优化结案流程 2026-02-01 14:33:23 +08:00
27942
54372a3c2c 优化结案流程 2026-02-01 14:22:56 +08:00
16 changed files with 1464 additions and 1030 deletions

View File

@@ -214,12 +214,10 @@ def log_operation(request, operation_type, module, action, target_type, target_i
operator_id = None
if token:
try:
user = User.objects.get(token=token, is_deleted=False)
user = User.objects.filter(token=token, is_deleted=False).first()
if user:
operator = user.username
operator_id = user.id
except User.DoesNotExist:
pass
# 获取IP地址
ip_address = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip()
@@ -826,9 +824,9 @@ def process_approval_flow(approval, business_record, current_approver, state,
logger.info(f"process_approval_flow: 已更新审批记录personincharge={next_approver}, state=审核中")
return False, None
else:
# 最后一个审核人已通过:投标/立项/案件变更转申请人待查看,其他类型抄送财务
# 最后一个审核人已通过:投标/立项/案件变更/结案申请转申请人待查看,其他类型抄送财务
applicant = getattr(approval, 'applicant', None)
if approval_type in ("投标登记", "立项登记", "案件变更") and applicant:
if approval_type in ("投标登记", "立项登记", "案件变更", "结案申请") and applicant:
logger.info(f"process_approval_flow: 最后一个审核人已审核,流转到申请人待查看: {applicant}")
approval.personincharge = applicant
approval.state = "待查看"
@@ -867,7 +865,7 @@ def process_approval_flow(approval, business_record, current_approver, state,
def create_approval_with_team_logic(team_name, approvers, title, content, approval_type, user_id,
business_record=None, today=None, applicant=None):
business_record=None, today=None, applicant=None, force_approval=False):
"""
根据团队类型创建审批记录(统一逻辑)
@@ -876,6 +874,7 @@ def create_approval_with_team_logic(team_name, approvers, title, content, approv
- 投标登记/立项登记:最后一步给申请人,生成「待查看」待办,申请人查看后完成(不再给财务部)
- 其他类型团队team需要审核人按顺序审核最后抄送财务
- 无团队:直接抄送财务
- 强制审批模式force_approval=True即使是个人团队也需要审批用于付款申请、报销、工资/奖金变更等
注意personincharge字段统一使用财务部ID优先或回退到"财务"字符串
@@ -889,6 +888,7 @@ def create_approval_with_team_logic(team_name, approvers, title, content, approv
business_record: 业务记录对象(可选)
today: 日期字符串可选格式YYYY-MM-DD
applicant: 申请人用户名(可选,投标/立项时填,最后一步生成待查看待办给申请人)
force_approval: 是否强制审批默认False。设为True时即使是个人团队也需要审批如付款申请、报销、工资/奖金变更)
Returns:
tuple: (approval对象, approvers_order_json, 是否需要审核)
@@ -941,8 +941,8 @@ def create_approval_with_team_logic(team_name, approvers, title, content, approv
# 创建审批记录,第一个审核人
first_approver = approvers_list[0]
approvers_str = ''.join(approvers_list) # 使用箭头表示顺序
# 投标登记/立项登记/案件变更:最后一步给申请人(待查看),不再给财务部
if approval_type in ("投标登记", "立项登记", "案件变更") and applicant:
# 投标登记/立项登记/案件变更/结案申请:最后一步给申请人(待查看),不再给财务部
if approval_type in ("投标登记", "立项登记", "案件变更", "结案申请") and applicant:
flow_suffix = " → 申请人(待查看)"
else:
flow_suffix = " → 财务部(按顺序审批)"
@@ -970,8 +970,49 @@ def create_approval_with_team_logic(team_name, approvers, title, content, approv
# 如果没有传入审核人,则根据团队类型判断
# 判断团队类型
if not team_name or not team or (team and team.team_type == 'personal'):
# 投标登记/立项登记/案件变更且传入了申请人:最后一步给申请人,生成待查看待办(不再给财务部)
if approval_type in ("投标登记", "立项登记", "案件变更") and applicant:
# 强制审批模式(付款申请、报销、工资/奖金变更等):即使是个人团队也需要审批
if force_approval:
import logging
logger = logging.getLogger(__name__)
logger.info(f"create_approval_with_team_logic: 强制审批模式 - 审批类型={approval_type}, 团队={team_name}")
# 尝试获取默认审核人:优先律所负责人,然后管委会成员
default_approver = get_law_firm_leader(team_name)
if not default_approver:
# 如果找不到默认审核人,返回错误
logger.warning(f"create_approval_with_team_logic: 强制审批模式下找不到默认审核人")
return None, None, True # needs_approval = True表示需要审批但缺少审核人
# 使用默认审核人创建审批
approvers_list = [default_approver]
approvers_order_json = json.dumps(approvers_list, ensure_ascii=False)
# 存储到业务记录
if business_record and hasattr(business_record, 'approvers_order'):
business_record.approvers_order = approvers_order_json
business_record.state = "审核中"
business_record.save(update_fields=['approvers_order', 'state'])
# 创建审批流程内容
content_with_flow = f"{content},审批流程:{default_approver} → 财务部(按顺序审批),当前审批人:{default_approver}"
logger.info(f"create_approval_with_team_logic: 强制审批 - 使用默认审核人 {default_approver}")
approval = Approval.objects.create(
title=title,
content=content_with_flow,
times=today,
personincharge=default_approver,
state="审核中",
type=approval_type,
user_id=str(user_id),
applicant=applicant
)
return approval, approvers_order_json, True
# 投标登记/立项登记/案件变更/结案申请且传入了申请人:最后一步给申请人,生成待查看待办(不再给财务部)
if approval_type in ("投标登记", "立项登记", "案件变更", "结案申请") and applicant:
content_to_save = content + ",待申请人查看"
approval = Approval.objects.create(
title=title,
@@ -1056,7 +1097,7 @@ def create_approval_with_team_logic(team_name, approvers, title, content, approv
# 创建审批记录,第一个审核人
first_approver = approvers_list[0]
approvers_str = ''.join(approvers_list)
if approval_type in ("投标登记", "立项登记", "案件变更") and applicant:
if approval_type in ("投标登记", "立项登记", "案件变更", "结案申请") and applicant:
flow_suffix = " → 申请人(待查看)"
else:
flow_suffix = " → 财务部(按顺序审批)"

View File

@@ -8,7 +8,7 @@ from .models import User, Approval, Department, OperationLog, Team
from business.models import permission
from finance.models import Income, Accounts, Payment, Reimbursement, BonusChange
from finance.models import Invoice
from business.models import ProjectRegistration, Case, SealApplication, PreFiling, Bid, CaseChangeRequest,Propaganda
from business.models import ProjectRegistration, Case, SealApplication, PreFiling, Bid, CaseChangeRequest, Propaganda, Schedule
import datetime
from utility.utility import flies
from django.contrib.sessions.backends.db import SessionStore
@@ -1411,52 +1411,58 @@ class roxyExhibition(APIView):
except (ValueError, TypeError, AttributeError):
itme["approvers_order"] = []
try:
from business.views import search_related_records
from business.views import conflict_search
project_id = int(info.user_id)
project = ProjectRegistration.objects.filter(id=project_id, is_deleted=False).first()
if project and project.client_info and project.party_info:
# 检索冲突记录
conflict_records = search_related_records(
project.client_info,
project.party_info,
# 修复:只要有委托人或相对方任一不为空就进行冲突检索(相对方为非必填)
if project and (project.client_info or project.party_info):
# 使用 conflict_search 函数进行冲突检索(支持更灵活的参数组合)
conflict_records = conflict_search(
client_info=project.client_info,
party_info=project.party_info,
exclude_project_id=project_id
)
# 处理冲突记录:将 client_info、party_info、BiddingUnit 字段从 JSON 字符串解析为列表
# 直接返回原始列表,不提取 name 字段
# 处理冲突记录:将 client_info、party_info、BiddingUnit、client_username、party_username 从 JSON 解析为列表
# 并归一化为前端期望的格式 [{ index, name, idNumber }, ...](兼容前端 isValidFormat 与查看冲突展示)
def _normalize_person_list(val):
"""将 JSON 字符串或列表归一化为 [{ index, name, idNumber }, ...],兼容前端;解析失败返回 None 以保留原值"""
if not val:
return []
try:
lst = json.loads(val) if isinstance(val, str) else val
if not isinstance(lst, list):
return None
out = []
for i, item in enumerate(lst):
if not isinstance(item, dict):
continue
name = item.get('name') or item.get('name_original') or ''
id_num = item.get('idNumber') or item.get('id_number') or ''
if not isinstance(name, str):
name = str(name) if name else ''
if not isinstance(id_num, str):
id_num = str(id_num) if id_num else ''
out.append({
'index': item.get('index') if isinstance(item.get('index'), (int, float)) else (i + 1),
'name': name,
'idNumber': id_num
})
return out
except (json.JSONDecodeError, TypeError, ValueError):
return None
def parse_json_fields(records):
"""将记录中的 JSON 字符串字段解析为列表,不提取 name"""
"""将记录中的 JSON 字符串字段解析为列表,并归一化为前端格式"""
processed = []
for record in records:
new_record = dict(record)
# 处理 client_info 字段:如果是 JSON 字符串,解析为列表
if 'client_info' in new_record and new_record['client_info']:
try:
if isinstance(new_record['client_info'], str):
parsed = json.loads(new_record['client_info'])
if isinstance(parsed, list):
new_record['client_info'] = parsed
except (json.JSONDecodeError, TypeError, ValueError):
pass # 解析失败,保持原值
# 处理 party_info 字段:如果是 JSON 字符串,解析为列表
if 'party_info' in new_record and new_record['party_info']:
try:
if isinstance(new_record['party_info'], str):
parsed = json.loads(new_record['party_info'])
if isinstance(parsed, list):
new_record['party_info'] = parsed
except (json.JSONDecodeError, TypeError, ValueError):
pass # 解析失败,保持原值
# 处理 BiddingUnit 字段:如果是 JSON 字符串,解析为列表
if 'BiddingUnit' in new_record and new_record['BiddingUnit']:
try:
if isinstance(new_record['BiddingUnit'], str):
parsed = json.loads(new_record['BiddingUnit'])
if isinstance(parsed, list):
new_record['BiddingUnit'] = parsed
except (json.JSONDecodeError, TypeError, ValueError):
pass # 解析失败,保持原值
for key in ('client_info', 'party_info', 'BiddingUnit', 'client_username', 'party_username'):
if key not in new_record or not new_record[key]:
continue
normalized = _normalize_person_list(new_record[key])
if normalized is not None:
new_record[key] = normalized
processed.append(new_record)
return processed
@@ -1505,35 +1511,42 @@ class roxyExhibition(APIView):
itme["content"] = content_val.rstrip() + ",申请人:" + submitter
if bid and bid.BiddingUnit:
conflict_result = conflict_search(bidding_unit=bid.BiddingUnit, exclude_bid_id=bid_id)
# 与立项登记共用同一套解析与归一化逻辑(预立案含 client_username/party_username立项/投标含 client_info/party_info/BiddingUnit
def _normalize_person_list_bid(val):
if not val:
return []
try:
lst = json.loads(val) if isinstance(val, str) else val
if not isinstance(lst, list):
return None
out = []
for i, item in enumerate(lst):
if not isinstance(item, dict):
continue
name = item.get('name') or item.get('name_original') or ''
id_num = item.get('idNumber') or item.get('id_number') or ''
if not isinstance(name, str):
name = str(name) if name else ''
if not isinstance(id_num, str):
id_num = str(id_num) if id_num else ''
out.append({
'index': item.get('index') if isinstance(item.get('index'), (int, float)) else (i + 1),
'name': name,
'idNumber': id_num
})
return out
except (json.JSONDecodeError, TypeError, ValueError):
return None
def parse_json_fields_bid(records):
"""将记录中的 JSON 字符串字段解析为列表,与立项展示逻辑一致"""
processed = []
for record in records:
new_record = dict(record)
if 'client_info' in new_record and new_record['client_info']:
try:
if isinstance(new_record['client_info'], str):
parsed = json.loads(new_record['client_info'])
if isinstance(parsed, list):
new_record['client_info'] = parsed
except (json.JSONDecodeError, TypeError, ValueError):
pass
if 'party_info' in new_record and new_record['party_info']:
try:
if isinstance(new_record['party_info'], str):
parsed = json.loads(new_record['party_info'])
if isinstance(parsed, list):
new_record['party_info'] = parsed
except (json.JSONDecodeError, TypeError, ValueError):
pass
if 'BiddingUnit' in new_record and new_record['BiddingUnit']:
try:
if isinstance(new_record['BiddingUnit'], str):
parsed = json.loads(new_record['BiddingUnit'])
if isinstance(parsed, list):
new_record['BiddingUnit'] = parsed
except (json.JSONDecodeError, TypeError, ValueError):
pass
for key in ('client_info', 'party_info', 'BiddingUnit', 'client_username', 'party_username'):
if key not in new_record or not new_record[key]:
continue
normalized = _normalize_person_list_bid(new_record[key])
if normalized is not None:
new_record[key] = normalized
processed.append(new_record)
return processed
itme["prefiling_conflicts"] = parse_json_fields_bid(conflict_result.get('prefiling_conflicts', []))
@@ -1548,6 +1561,37 @@ class roxyExhibition(APIView):
itme["project_conflicts"] = []
itme["bid_conflicts"] = []
# 案件变更、结案申请:返回上传文件 URL统一用 attachment_urls字符串单个为 URL多个用逗号拼接无文件为空字符串
if info.type == "案件变更":
try:
change_request = CaseChangeRequest.objects.filter(id=int(info.user_id), is_deleted=False).first()
if change_request and change_request.change_agreement:
try:
agreement_list = json.loads(change_request.change_agreement)
lst = agreement_list if isinstance(agreement_list, list) else []
itme["attachment_urls"] = lst[0] if len(lst) == 1 else (",".join(str(u) for u in lst) if lst else "")
except (json.JSONDecodeError, TypeError):
itme["attachment_urls"] = ""
else:
itme["attachment_urls"] = ""
except (ValueError, TypeError):
itme["attachment_urls"] = ""
elif info.type == "结案申请":
try:
schedule = Schedule.objects.filter(id=int(info.user_id), is_deleted=False).first()
if schedule and schedule.remark:
try:
remark_data = json.loads(schedule.remark)
closing_files = remark_data.get("closing_application_files", [])
lst = closing_files if isinstance(closing_files, list) else []
itme["attachment_urls"] = lst[0] if len(lst) == 1 else (",".join(str(u) for u in lst) if lst else "")
except (json.JSONDecodeError, TypeError):
itme["attachment_urls"] = ""
else:
itme["attachment_urls"] = ""
except (ValueError, TypeError):
itme["attachment_urls"] = ""
data.append(itme)
return Response({'message': '展示成功', "total": total, 'data': data, 'code': 0}, status=status.HTTP_200_OK)
@@ -1774,7 +1818,17 @@ class approvalProcessing(APIView):
}
}, status=status.HTTP_200_OK)
# 兼容旧流程:如果负责人未填写分配,但审批人指定了分配方案
# 审批人审批时必须填写收入分配
# 如果收入分配还是"待负责人指定",且审批人要通过审批,则必须填写收入分配
if state == "已通过" and income.allocate == "待负责人指定":
if not allocate:
return Response({
'status': 'error',
'message': '请填写收入分配后再通过审批',
'code': 1
}, status=status.HTTP_400_BAD_REQUEST)
# 如果审批人填写了收入分配,更新到记录中
if allocate:
income.allocate = allocate
# 更新审批内容,添加分配信息
@@ -1785,8 +1839,9 @@ class approvalProcessing(APIView):
else:
approval.content = approval.content + f",收入分配:{allocate}"
income.save(update_fields=['allocate'])
approval.save(update_fields=['content'])
# 使用统一的审核流程处理函数(兼容旧流程)
# 使用统一的审核流程处理函数
# 非财务查看时state参数是必填的
if not state:
return Response({'status': 'error', 'message': '缺少参数state审核状态已通过/未通过)', 'code': 1}, status=status.HTTP_400_BAD_REQUEST)
@@ -2280,8 +2335,8 @@ class approvalProcessing(APIView):
return Response({'message': '处理成功', 'code': 0}, status=status.HTTP_200_OK)
if type == "结案申请":
from business.models import Schedule, Case
try:
from business.models import Schedule
schedule = Schedule.objects.get(id=approval.user_id, is_deleted=False)
except Schedule.DoesNotExist:
return Response({'status': 'error', 'message': '结案申请记录不存在或已被删除', 'code': 1}, status=status.HTTP_404_NOT_FOUND)
@@ -2302,6 +2357,31 @@ class approvalProcessing(APIView):
if error:
return Response({'status': 'error', 'message': error, 'code': 1}, status=status.HTTP_400_BAD_REQUEST)
# 审批通过后,更新 Case.Closingapplication 字段
# 刷新 schedule 和 approval 以获取最新状态
schedule.refresh_from_db()
approval.refresh_from_db()
if schedule.state == "已完成" or approval.state == "已通过":
try:
import json
import logging
logger = logging.getLogger(__name__)
# 从 schedule.remark 中获取 case_id 和 closing_application_files
remark_data = json.loads(schedule.remark) if schedule.remark else {}
case_id = remark_data.get('case_id')
closing_files = remark_data.get('closing_application_files', [])
if case_id and closing_files:
case = Case.objects.filter(id=case_id, is_deleted=False).first()
if case:
case.Closingapplication = json.dumps(closing_files, ensure_ascii=False)
case.save(update_fields=['Closingapplication'])
logger.info(f"结案申请审批通过,已更新 Case(id={case_id}) 的 Closingapplication 字段")
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"结案申请审批通过后更新 Case.Closingapplication 失败: {str(e)}")
return Response({'message': '处理成功', 'code': 0}, status=status.HTTP_200_OK)
return Response({'message': '处理成功', 'code': 0}, status=status.HTTP_200_OK)
@@ -2747,8 +2827,8 @@ class ApprovalStatusCheck(APIView):
is_finance_view = True
approval.state = "已通过"
approval.save(update_fields=['state'])
# 投标/立项/案件变更:当前为「待查看」且当前用户是申请人时,调用本接口即视为申请人已查看,消除待查看状态
elif type in ("投标登记", "立项登记", "案件变更") and approval.state == "待查看" and getattr(approval, 'applicant', None) and approval.personincharge == approval.applicant and current_user.username == approval.applicant:
# 投标/立项/案件变更/结案申请:当前为「待查看」且当前用户是申请人时,调用本接口即视为申请人已查看,消除待查看状态
elif type in ("投标登记", "立项登记", "案件变更", "结案申请") and approval.state == "待查看" and getattr(approval, 'applicant', None) and approval.personincharge == approval.applicant and current_user.username == approval.applicant:
is_applicant_view = True
approval.state = "已通过"
update_fields = ['state', 'content']
@@ -2913,6 +2993,37 @@ class ApprovalStatusCheck(APIView):
is_approved = (schedule.state == "已完成")
except Schedule.DoesNotExist:
pass
elif type == "结案申请":
# 结案申请类型:使用 Schedule 作为承载对象
from business.models import Schedule, Case
try:
schedule = Schedule.objects.get(id=approval.user_id, is_deleted=False)
business_state = schedule.state
is_approved = (schedule.state == "已完成" or approval.state == "已通过")
# 申请人通过本接口查看后,消除待查看并更新业务记录为已完成,同时更新 Case.Closingapplication
if is_applicant_view:
if schedule.state != "已完成":
schedule.state = "已完成"
schedule.save(update_fields=['state'])
business_state = "已完成"
is_approved = True
# 更新 Case.Closingapplication 字段
try:
import json
remark_data = json.loads(schedule.remark) if schedule.remark else {}
case_id = remark_data.get('case_id')
closing_files = remark_data.get('closing_application_files', [])
if case_id and closing_files:
case = Case.objects.filter(id=case_id, is_deleted=False).first()
if case:
case.Closingapplication = json.dumps(closing_files, ensure_ascii=False)
case.save(update_fields=['Closingapplication'])
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"结案申请待查看确认后更新 Case.Closingapplication 失败: {str(e)}")
except Schedule.DoesNotExist:
pass
# 可以根据需要添加其他类型
except Exception as e:
import logging

View File

@@ -1,144 +0,0 @@
"""
分析所有模型类,生成字段列表报告
用于对比数据库字段和模型类字段
"""
import os
import sys
import django
# 设置 Django 环境
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'jyls_django.settings')
django.setup()
from django.apps import apps
from django.db import models
def analyze_model(model):
"""分析单个模型,返回字段信息"""
fields_info = {
'model_name': model.__name__,
'table_name': model._meta.db_table,
'fields': [],
'm2m_fields': [],
'foreign_keys': []
}
for field in model._meta.get_fields(include_parents=False):
# 多对多关系
if isinstance(field, models.ManyToManyField):
fields_info['m2m_fields'].append({
'name': field.name,
'related_model': field.related_model.__name__ if field.related_model else None,
'through_table': field.remote_field.through._meta.db_table if hasattr(field.remote_field, 'through') else None
})
continue
# 跳过反向关系
if getattr(field, 'auto_created', False):
continue
# 跳过没有 column 属性的字段(反向关系)
if not hasattr(field, 'column'):
continue
field_info = {
'name': field.name,
'column': field.column,
'type': type(field).__name__,
'null': getattr(field, 'null', False),
'blank': getattr(field, 'blank', False),
'max_length': getattr(field, 'max_length', None),
'default': str(getattr(field, 'default', 'NOT_PROVIDED')),
}
# 外键
if isinstance(field, models.ForeignKey):
field_info['related_model'] = field.related_model.__name__ if field.related_model else None
fields_info['foreign_keys'].append(field_info)
else:
fields_info['fields'].append(field_info)
return fields_info
def main():
print('=' * 80)
print('模型类字段分析报告')
print('=' * 80)
apps_to_check = [
apps.get_app_config('User'),
apps.get_app_config('finance'),
apps.get_app_config('business')
]
all_models_info = []
for app_config in apps_to_check:
print(f'\n\n应用: {app_config.name}')
print('=' * 80)
models_to_check = app_config.get_models()
for model in models_to_check:
info = analyze_model(model)
all_models_info.append(info)
print(f'\n模型: {info["model_name"]}')
print(f'表名: {info["table_name"]}')
print('-' * 80)
# 普通字段
if info['fields']:
print('\n普通字段:')
for field in info['fields']:
null_str = 'NULL' if field['null'] else 'NOT NULL'
max_len_str = f", max_length={field['max_length']}" if field['max_length'] else ''
default_str = f", default={field['default']}" if field['default'] != 'NOT_PROVIDED' else ''
print(f" - {field['column']} ({field['type']}{max_len_str}, {null_str}{default_str})")
# 外键
if info['foreign_keys']:
print('\n外键字段:')
for field in info['foreign_keys']:
null_str = 'NULL' if field['null'] else 'NOT NULL'
related = field.get('related_model', 'Unknown')
print(f" - {field['column']} (ForeignKey -> {related}, {null_str})")
# 多对多关系
if info['m2m_fields']:
print('\n多对多关系:')
for field in info['m2m_fields']:
related = field.get('related_model', 'Unknown')
through = field.get('through_table', 'auto')
print(f" - {field['name']} (ManyToMany -> {related}, through={through})")
# 生成汇总报告
print('\n\n' + '=' * 80)
print('汇总报告')
print('=' * 80)
print(f'\n总共检查了 {len(all_models_info)} 个模型:')
for info in all_models_info:
field_count = len(info['fields']) + len(info['foreign_keys'])
m2m_count = len(info['m2m_fields'])
print(f" - {info['model_name']} ({info['table_name']}): {field_count} 个字段, {m2m_count} 个多对多关系")
# 检查是否有 User 模型缺少 approvers_order 字段
print('\n\n' + '=' * 80)
print('特殊检查: User 模型字段')
print('=' * 80)
user_model = apps.get_model('User', 'User')
user_info = analyze_model(user_model)
has_approvers_order = any(f['name'] == 'approvers_order' for f in user_info['fields'])
if not has_approvers_order:
print('\n⚠️ 发现: User 模型没有 approvers_order 字段')
print(' 这可能导致入职/离职登记的审批流程无法正确获取审核人列表')
print(' 建议: 从 Approval.content 字段解析审核人列表(已在代码中实现)')
else:
print('\n✅ User 模型有 approvers_order 字段')
if __name__ == '__main__':
main()

103
business/contract_no.py Normal file
View File

@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
"""
合同编号生成规则与生成函数。
格式:校准(字)字【年份】第序号号
各项目类型与“字”的对应:
- 法律顾问 -> 校准(顾)字【YYYY】第n号
- 专项服务 -> 校准(专)字【YYYY】第n号
- 民事案件代理 -> 校准(代)字【YYYY】第n号
- 刑事案件代理 -> 校准(刑辩)字【YYYY】第n号
- 行政案件代理 -> 校准(行政)字【YYYY】第n号
- 法律咨询 -> 校准(询)字【YYYY】第n号
- 破产程序代理 -> 校准(破产)字【YYYY】第n号
每个类别按各自类型+年份独立计数,跨年归零。
"""
# 项目类型 -> 合同编号中的“字”(括号内部分)
PROJECT_TYPE_SUFFIX = {
"法律顾问": "",
"专项服务": "",
"民事案件代理": "",
"刑事案件代理": "刑辩",
"行政案件代理": "行政",
"法律咨询": "",
"破产程序代理": "破产",
}
def get_contract_no_suffix(project_type: str):
"""
获取项目类型对应的合同编号“字”。
:param project_type: 项目类型
:return: 字,如 "";未配置则返回 None
"""
if not project_type:
return None
return PROJECT_TYPE_SUFFIX.get(project_type.strip())
def format_contract_no(project_type: str, year: int, number: int) -> str:
"""
格式化为标准合同编号字符串。
:param project_type: 项目类型
:param year: 年份,如 2025
:param number: 序号(从 1 开始)
:return: 如 "校准(顾)字【2025】第1号";未配置类型则返回空字符串
"""
suffix = get_contract_no_suffix(project_type)
if suffix is None:
return ""
return f"校准({suffix})字【{year}】第{number}"
def generate_next_contract_no(project_type: str, year: int):
"""
在**当前事务内**为该类型+年份占用下一个序号并返回合同编号。
必须在 transaction.atomic() 内调用,内部使用 select_for_update 保证并发安全。
:param project_type: 项目类型
:param year: 年份
:return: 生成的合同编号字符串;若项目类型未配置则返回 None
"""
from business.models import ContractCounter
suffix = get_contract_no_suffix(project_type)
if suffix is None:
return None
counter, created = ContractCounter.objects.select_for_update().get_or_create(
project_type=project_type,
year=year,
defaults={"current_number": 0},
)
next_number = counter.current_number + 1
counter.current_number = next_number
counter.save(update_fields=["current_number", "updated_at"])
return format_contract_no(project_type, year, next_number)
def get_next_contract_no_preview(project_type: str, year: int = None):
"""
仅查询下一个合同编号的预览(不占用序号,不写库)。
用于前端展示“下一个编号将是 XXX”。
:param project_type: 项目类型
:param year: 年份,默认当前年
:return: (next_number, formatted_string);未配置类型则 (0, "")
"""
from datetime import datetime
from business.models import ContractCounter
if year is None:
year = datetime.now().year
suffix = get_contract_no_suffix(project_type)
if suffix is None:
return 0, ""
counter = ContractCounter.objects.filter(
project_type=project_type,
year=year,
).first()
current = counter.current_number if counter else 0
next_number = current + 1
return next_number, format_contract_no(project_type, year, next_number)

View File

View File

View File

@@ -0,0 +1,124 @@
"""
初始化合同编号计数器
此命令用于根据现有的 ProjectRegistration 数据初始化 ContractCounter 表,
确保每个项目类型每年的计数器值等于该类型该年已有的立项数量。
用法:
python manage.py init_contract_counters
说明:
1. 遍历所有未删除的立项登记
2. 根据项目类型和立项日期年份统计数量
3. 更新或创建对应的计数器记录
"""
from django.core.management.base import BaseCommand
from django.db import transaction
from django.db.models import Count
from business.models import ProjectRegistration, ContractCounter
class Command(BaseCommand):
help = '根据现有立项数据初始化合同编号计数器'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='只显示将要执行的操作,不实际修改数据',
)
parser.add_argument(
'--force',
action='store_true',
help='强制重新初始化所有计数器(会覆盖现有数据)',
)
def handle(self, *args, **options):
dry_run = options['dry_run']
force = options['force']
self.stdout.write(self.style.NOTICE('开始初始化合同编号计数器...'))
if dry_run:
self.stdout.write(self.style.WARNING('【预览模式】不会实际修改数据'))
# 获取所有未删除的立项登记
projects = ProjectRegistration.objects.filter(is_deleted=False)
# 统计每个类型每年的数量
type_year_counts = {}
for project in projects:
project_type = project.type
# 从立项日期中提取年份
times = project.times
if times and len(times) >= 4:
try:
year = int(times[:4])
except ValueError:
self.stdout.write(self.style.WARNING(
f' 跳过: 立项ID={project.id},日期格式无效: {times}'
))
continue
else:
self.stdout.write(self.style.WARNING(
f' 跳过: 立项ID={project.id},日期为空或格式错误: {times}'
))
continue
key = (project_type, year)
type_year_counts[key] = type_year_counts.get(key, 0) + 1
self.stdout.write(f'共发现 {len(type_year_counts)} 种类型-年份组合')
self.stdout.write('')
# 更新或创建计数器
created_count = 0
updated_count = 0
skipped_count = 0
with transaction.atomic():
for (project_type, year), count in sorted(type_year_counts.items()):
self.stdout.write(f' {project_type} - {year}年: {count}')
if dry_run:
continue
try:
counter, created = ContractCounter.objects.get_or_create(
project_type=project_type,
year=year,
defaults={'current_number': count}
)
if created:
created_count += 1
self.stdout.write(self.style.SUCCESS(
f' -> 创建计数器: 第{count}'
))
elif force or counter.current_number < count:
old_number = counter.current_number
counter.current_number = count
counter.save(update_fields=['current_number', 'updated_at'])
updated_count += 1
self.stdout.write(self.style.SUCCESS(
f' -> 更新计数器: 第{old_number}号 -> 第{count}'
))
else:
skipped_count += 1
self.stdout.write(self.style.NOTICE(
f' -> 跳过: 现有计数器值({counter.current_number}) >= 统计值({count})'
))
except Exception as e:
self.stdout.write(self.style.ERROR(
f' -> 错误: {e}'
))
self.stdout.write('')
if dry_run:
self.stdout.write(self.style.WARNING('【预览模式】以上操作未实际执行'))
else:
self.stdout.write(self.style.SUCCESS(
f'完成! 创建: {created_count}, 更新: {updated_count}, 跳过: {skipped_count}'
))

View File

@@ -0,0 +1,30 @@
# Generated by Django 4.2.25 on 2026-02-03 12:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('business', '0017_system'),
]
operations = [
migrations.CreateModel(
name='ContractCounter',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('project_type', models.CharField(max_length=100, verbose_name='项目类型')),
('year', models.IntegerField(verbose_name='年份')),
('current_number', models.IntegerField(default=0, verbose_name='当前序号')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
],
options={
'verbose_name': '合同编号计数器',
'verbose_name_plural': '合同编号计数器',
'db_table': 'contract_counter',
'unique_together': {('project_type', 'year')},
},
),
]

View File

@@ -198,4 +198,27 @@ class permission(models.Model):
class Propaganda(models.Model):
content = models.TextField() # 文喧
content = models.TextField() # 文喧
class ContractCounter(models.Model):
"""
合同编号计数器表
用于全局维护每个项目类型每年的合同编号序号
保证合同编号唯一、递增、跨年归零
"""
project_type = models.CharField(max_length=100, verbose_name='项目类型') # 如:法律顾问、专项服务等
year = models.IntegerField(verbose_name='年份') # 如2026
current_number = models.IntegerField(default=0, verbose_name='当前序号') # 当年该类型的最大序号
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'contract_counter'
verbose_name = '合同编号计数器'
verbose_name_plural = '合同编号计数器'
# 保证同一年份同一类型只有一条记录
unique_together = [['project_type', 'year']]
def __str__(self):
return f"{self.project_type} - {self.year} - 第{self.current_number}"

View File

@@ -1,7 +1,7 @@
from django.urls import path
from .models import Schedule
from .views import registration,registrationDetail,Project,Projectquerytype,ProjectDetail,EditProject,BidRegistration,BidDetail,registrationList,caseManagement,caseManagementDetail,CaseAttachmentUpload,CaseAttachmentUpdate,Uploadinvoice,InvoiceDetail,Log,LogDetail,accumulate,preFilingLinkedCases,Application,ApplicationDetail,WarehousingRegistration,WarehousingDetail,PlatformRegistration,PlatformDetail,EditPlatformDetail,DeletePlatformDetail,bulletin,BulletinDetail,EditBulletin,deleteBulletin,Lawyersdocuments,LawyersdocumentsDetail,LwaDetail,CreateSchedule,DeleteSchedule,ScheduleDetail,handleSchedule,AddRermission,DisplayRermission,DeleteRermission,EditRermission,addRole,DeleteRole,EditRole,displayRole,modifypermissions,getRolePermissions,DeleteRegistration,EditRegistration,DeleteProject,EditBid,DeleteBid,EditCase,DeleteCase,EditApplication,DeleteApplication,EditWarehousing,DeleteWarehousing,EditLawyerFlie,EditSchedule,TransferCase,CaseChangeRequestCreate,CaseChangeRequestList,CaseChangeRequestDetail,ProjectDropdownList,CaseDropdownList,ConflictSearch,CreateCaseTag,CaseTagList,CaseTagDetail,EditCaseTag,DeleteCaseTag,CaseTagDropdownList,CaseListByTag,SetCaseTags,PropagandaEit,addSystem,SystemList,eitSystem,deleteSystem
from .views import registration,registrationDetail,Project,Projectquerytype,ProjectDetail,EditProject,BidRegistration,BidDetail,registrationList,caseManagement,caseManagementDetail,CaseAttachmentUpload,CaseAttachmentUpdate,Uploadinvoice,InvoiceDetail,Log,LogDetail,accumulate,preFilingLinkedCases,Application,ApplicationDetail,WarehousingRegistration,WarehousingDetail,PlatformRegistration,PlatformDetail,EditPlatformDetail,DeletePlatformDetail,bulletin,BulletinDetail,EditBulletin,deleteBulletin,Lawyersdocuments,LawyersdocumentsDetail,LwaDetail,CreateSchedule,DeleteSchedule,ScheduleDetail,handleSchedule,AddRermission,DisplayRermission,DeleteRermission,EditRermission,addRole,DeleteRole,EditRole,displayRole,modifypermissions,getRolePermissions,DeleteRegistration,EditRegistration,DeleteProject,EditBid,DeleteBid,EditCase,DeleteCase,EditApplication,DeleteApplication,EditWarehousing,DeleteWarehousing,EditLawyerFlie,EditSchedule,TransferCase,CaseChangeRequestCreate,CaseChangeRequestList,CaseChangeRequestDetail,ProjectDropdownList,CaseDropdownList,ConflictSearch,CreateCaseTag,CaseTagList,CaseTagDetail,EditCaseTag,DeleteCaseTag,CaseTagDropdownList,CaseListByTag,SetCaseTags,PropagandaEit,addSystem,SystemList,eitSystem,deleteSystem,ExportCaseLogExcel
urlpatterns = [
path('register',registration.as_view(),name='register'),
@@ -85,5 +85,7 @@ urlpatterns = [
path('addSystem',addSystem.as_view(),name='addSystem'),
path('SystemList',SystemList.as_view(),name='SystemList'),
path('eitSystem',eitSystem.as_view(),name='eitSystem'),
path('deleteSystem',deleteSystem.as_view(),name='deleteSystem')
path('deleteSystem',deleteSystem.as_view(),name='deleteSystem'),
# 案件日志导出Excel
path('export-case-log-excel',ExportCaseLogExcel.as_view(),name='export-case-log-excel'),
]

File diff suppressed because it is too large Load Diff

View File

@@ -1059,7 +1059,7 @@ class confirm(APIView):
personincharge = request.data.get('personincharge')
approvers = normalize_approvers_param(approvers, personincharge)
allocate = request.data.get('allocate') # 收入分配(财务提交时不填写,由负责人填写)
# 收入分配由审批人(负责人)在审批时填写,新增时不接收此参数
token = request.META.get('token')
try:
@@ -1274,12 +1274,13 @@ class confirm(APIView):
initial_state = "审核中" if (team and team.team_type == 'team') else "待财务处理"
# 创建收入确认记录
# 收入分配由审批人(负责人)在审批时填写
income = Income.objects.create(
times=times,
ContractNo=ContractNo,
CustomerID=CustomerID,
amount=amount,
allocate=allocate if allocate else "待负责人指定", # 如果未提供,设为"待负责人指定"
allocate="待负责人指定", # 收入分配由审批人在审批时填写
submit=user.username,
submit_tiem=date_string,
state=initial_state
@@ -2277,9 +2278,8 @@ class PaymentRequest(APIView):
# 如果团队不存在,默认按团队类型处理(需要审批)
pass
# 根据团队类型决定初始状态
# 如果是团队类型且需要审核,状态为"审核中";否则为"已通过"(直接抄送财务)
initial_state = "审核中" if (team and team.team_type == 'team') else "已通过"
# 付款申请统一都需要审批(无论团队类型)
initial_state = "审核中"
# 创建付款申请记录
pay = Payment.objects.create(
@@ -2315,7 +2315,7 @@ class PaymentRequest(APIView):
content_parts.insert(3, f"付款日期:{times}")
content = "".join(content_parts)
# 使用统一的审核流程函数(与离职逻辑一样
# 使用统一的审核流程函数(付款申请统一需要审批force_approval=True
from User.utils import create_approval_with_team_logic
approval, approvers_order_json, needs_approval = create_approval_with_team_logic(
@@ -2326,7 +2326,8 @@ class PaymentRequest(APIView):
approval_type="付款申请",
user_id=str(pay.id),
business_record=pay,
today=date_string
today=date_string,
force_approval=True # 付款申请统一需要审批
)
# 如果返回None且需要审核说明缺少审核人
@@ -2366,7 +2367,7 @@ class PaymentRequest(APIView):
'id': pay.id,
'state': pay.state,
'approval_id': approval.id if approval else None,
'needs_approval': team is None or team.team_type != 'personal', # 是否需要审批(前端用这个字段判断是团队还是个人)
'needs_approval': True, # 付款申请统一都需要审批
'team_type': team.team_type if team else None, # 团队类型personal/team前端用这个字段判断
'team_name': team_name # 团队名称
}
@@ -2652,9 +2653,8 @@ class reimbursement(APIView):
now = datetime.now()
date_string = now.strftime("%Y-%m-%d")
# 根据团队类型决定初始状态
# 如果是团队类型且需要审核,状态为"审核中";否则为"已通过"(直接抄送财务)
initial_state = "审核中" if (team and team.team_type == 'team') else "已通过"
# 报销统一都需要审批(无论团队类型)
initial_state = "审核中"
reim = Reimbursement.objects.create(
person=person,
@@ -2666,7 +2666,7 @@ class reimbursement(APIView):
state=initial_state
)
# 使用统一的审核流程函数
# 使用统一的审核流程函数报销统一需要审批force_approval=True
from User.utils import create_approval_with_team_logic
content = f"{person}{times}提交了报销申请,报销理由:{reason},报销金额:{amount},报销日期:{times},费用说明:{FeeDescription}"
@@ -2679,7 +2679,8 @@ class reimbursement(APIView):
approval_type="报销申请",
user_id=str(reim.id),
business_record=reim,
today=date_string
today=date_string,
force_approval=True # 报销统一需要审批
)
# 如果返回None且需要审核说明缺少审核人
@@ -2717,7 +2718,7 @@ class reimbursement(APIView):
'id': reim.id,
'state': reim.state,
'approval_id': approval.id if approval else None,
'needs_approval': team is None or team.team_type != 'personal', # 是否需要审批(前端用这个字段判断是团队还是个人)
'needs_approval': True, # 报销统一都需要审批
'team_type': team.team_type if team else None, # 团队类型personal/team前端用这个字段判断
'team_name': team_name # 团队名称
}
@@ -2917,9 +2918,8 @@ class Change(APIView):
except Team.DoesNotExist:
pass
# 根据团队类型决定初始状态
# 如果是团队类型且需要审核,状态为"审核中";否则为"已通过"(直接抄送财务)
initial_state = "审核中" if (team and team.team_type == 'team') else "已通过"
# 工资/奖金变更统一都需要审批(无论团队类型)
initial_state = "审核中"
bonus = BonusChange.objects.create(
username=username,
@@ -2934,7 +2934,7 @@ class Change(APIView):
content = f"{submitter}{date_string}提交了工资/奖金变更,类型:{type},调整说明:{Instructions}"
# 使用统一的审核流程函数(与付款申请、报销申请逻辑一样
# 使用统一的审核流程函数(工资/奖金变更统一需要审批force_approval=True
approval, approvers_order_json, needs_approval = create_approval_with_team_logic(
team_name=team_name,
approvers=approvers,
@@ -2943,7 +2943,8 @@ class Change(APIView):
approval_type="工资/奖金变更",
user_id=str(bonus.id),
business_record=bonus,
today=date_string
today=date_string,
force_approval=True # 工资/奖金变更统一需要审批
)
# 如果返回None且需要审核说明缺少审核人
@@ -2982,7 +2983,7 @@ class Change(APIView):
'submitter': bonus.submitter, # 提交人(明确是谁提交的申请)
'state': bonus.state,
'approval_id': approval.id if approval else None,
'needs_approval': team is None or team.team_type != 'personal', # 是否需要审批(前端用这个字段判断是团队还是个人)
'needs_approval': True, # 工资/奖金变更统一都需要审批
'team_type': team.team_type if team else None, # 团队类型personal/team前端用这个字段判断
'team_name': team_name # 团队名称
}

View File

@@ -92,27 +92,30 @@ class JWTAuthenticationMiddleware(MiddlewareMixin):
# 允许登录接口(支持 /api2/user/login 和 /user/login
if request.path == '/api2/user/login' or request.path == '/user/login':
return None
try:
if not token:
# 标记为未授权请求(可能是正常的前端访问,也可能是恶意扫描)
request.META['_is_unauthorized'] = True
return JsonResponse(
{'status': 401,'message':"token为空"},
status=401,
content_type='application/json',
headers={'Access-Control-Allow-Origin': '*'}
)
User.objects.get(token=token, is_deleted=False)
except User.DoesNotExist:
# 标记为未授权请求
if not token:
request.META['_is_unauthorized'] = True
return JsonResponse(
{'status': 401,'message':"身份过期"},
{'status': 401, 'message': "token为空"},
status=401,
content_type='application/json',
headers={'Access-Control-Allow-Origin': '*'}
)
# 使用 filter().first() 避免同一 token 存在多条用户时 get() 抛出 MultipleObjectsReturned
users = User.objects.filter(token=token, is_deleted=False)
user = users.first()
if user is None:
request.META['_is_unauthorized'] = True
return JsonResponse(
{'status': 401, 'message': "身份过期"},
status=401,
content_type='application/json',
headers={'Access-Control-Allow-Origin': '*'}
)
if users.count() > 1:
logger.warning(
'同一 token 存在 %s 个用户token 应唯一),请检查 User 表并清理重复数据。',
users.count()
)
return None

Binary file not shown.

View File

@@ -1,124 +0,0 @@
"""
测试待办事项的已完成逻辑
验证:审核通过后(状态为"已抄送财务"),不管财务有没有查看,都应该显示"已完成"
"""
import os
import sys
import django
# 设置Django环境
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'jyls_django.settings')
django.setup()
from User.models import Approval
from business.models import Schedule
from User.views import roxyExhibition
from rest_framework.test import APIRequestFactory
from rest_framework.request import Request
import json
def test_todo_completion_logic():
"""测试待办事项的已完成逻辑"""
print("=" * 60)
print("测试待办事项的已完成逻辑")
print("=" * 60)
# 查找所有"待办"类型的审批记录
todo_approvals = Approval.objects.filter(type="待办", is_deleted=False)
if not todo_approvals.exists():
print("\n❌ 未找到任何待办类型的审批记录")
print("请先创建一些待办事项进行测试")
return
print(f"\n找到 {todo_approvals.count()} 条待办审批记录\n")
# 测试不同状态下的逻辑
test_cases = [
("已抄送财务", "已完成", "审核通过后,状态为'已抄送财务',应该显示'已完成'"),
("已通过", "已完成", "财务查看后,状态为'已通过',应该显示'已完成'"),
("审核中", "审批中", "审核中,应该显示'审批中'"),
("未通过", "审批中", "未通过,应该显示'审批中'"),
]
print("测试场景:")
print("-" * 60)
for state, expected_status, description in test_cases:
approvals = todo_approvals.filter(state=state)
count = approvals.count()
if count > 0:
print(f"\n✅ 场景:{description}")
print(f" 状态:{state}")
print(f" 期望返回:{expected_status}")
print(f" 找到 {count} 条记录")
# 检查第一条记录
approval = approvals.first()
print(f" 示例记录ID{approval.id}")
print(f" 审批类型:{approval.type}")
print(f" 当前状态:{approval.state}")
# 模拟逻辑判断
if approval.type == "待办":
if approval.state == "已抄送财务" or approval.state == "已通过":
calculated_status = "已完成"
elif approval.state == "审核中":
calculated_status = "审批中"
elif approval.state == "未通过":
calculated_status = "审批中"
else:
calculated_status = "审批中"
else:
calculated_status = "未知"
if calculated_status == expected_status:
print(f" ✅ 逻辑正确:计算状态 = {calculated_status}")
else:
print(f" ❌ 逻辑错误:计算状态 = {calculated_status},期望 = {expected_status}")
else:
print(f"\n⚠️ 场景:{description}")
print(f" 状态:{state}")
print(f" 未找到该状态的记录")
print("\n" + "=" * 60)
print("测试完成")
print("=" * 60)
# 显示所有待办记录的状态分布
print("\n待办审批记录状态分布:")
print("-" * 60)
from django.db.models import Count
status_distribution = todo_approvals.values('state').annotate(count=Count('id')).order_by('state')
for item in status_distribution:
print(f" {item['state']}: {item['count']}")
print("\n" + "=" * 60)
print("验证逻辑代码:")
print("=" * 60)
print("""
# 在 roxyExhibition 中的逻辑:
if info.type == "待办":
# 待办类型:审核通过后(已抄送财务或已通过)即为已完成,不需要等待财务查看
if info.state == "已抄送财务" or info.state == "已通过":
approval_status = "已完成"
elif info.state == "审核中":
approval_status = "审批中"
elif info.state == "未通过":
approval_status = "审批中"
""")
print("\n" + "=" * 60)
print("结论:")
print("=" * 60)
print("✅ 对于'待办'类型:")
print(" - 状态为'已抄送财务' → 返回'已完成'(不需要等待财务查看)")
print(" - 状态为'已通过' → 返回'已完成'")
print(" - 状态为'审核中' → 返回'审批中'")
print(" - 状态为'未通过' → 返回'审批中'")
print("\n✅ 逻辑已正确实现:审核通过后(已抄送财务),不管财务有没有查看,都显示'已完成'")
if __name__ == "__main__":
test_todo_completion_logic()

View File

@@ -1,127 +0,0 @@
"""
简单测试待办事项的已完成逻辑
直接验证代码逻辑,不依赖数据库
"""
def test_todo_status_logic():
"""测试待办事项状态判断逻辑"""
print("=" * 60)
print("测试待办事项的已完成逻辑")
print("=" * 60)
# 模拟不同的审批状态
test_cases = [
{
"type": "待办",
"state": "已抄送财务",
"expected": "已完成",
"description": "审核通过后,状态为'已抄送财务',应该显示'已完成'(不需要等待财务查看)"
},
{
"type": "待办",
"state": "已通过",
"expected": "已完成",
"description": "财务查看后,状态为'已通过',应该显示'已完成'"
},
{
"type": "待办",
"state": "审核中",
"expected": "审批中",
"description": "审核中,应该显示'审批中'"
},
{
"type": "待办",
"state": "未通过",
"expected": "审批中",
"description": "未通过,应该显示'审批中'"
},
{
"type": "其他类型",
"state": "已抄送财务",
"expected": "待查看",
"description": "其他类型:已抄送财务,应该显示'待查看'(需要财务查看)"
},
{
"type": "其他类型",
"state": "已通过",
"expected": "已完成",
"description": "其他类型:已通过,应该显示'已完成'"
},
]
print("\n测试场景:")
print("-" * 60)
all_passed = True
for i, case in enumerate(test_cases, 1):
info_type = case["type"]
info_state = case["state"]
expected_status = case["expected"]
description = case["description"]
# 模拟 roxyExhibition 中的逻辑
approval_status = "审批中" # 默认状态
if info_type == "待办":
# 待办类型:审核通过后(已抄送财务或已通过)即为已完成,不需要等待财务查看
if info_state == "已抄送财务" or info_state == "已通过":
approval_status = "已完成"
elif info_state == "审核中":
approval_status = "审批中"
elif info_state == "未通过":
approval_status = "审批中" # 未通过也可以继续审批流程
else:
# 其他类型:保持原有逻辑
if info_state == "已抄送财务":
# 优先判断:已抄送财务时,显示"待查看"(财务部需要查看)
approval_status = "待查看"
elif info_state == "已通过":
# 审批记录状态为"已通过"(通常是财务查看后),显示"已完成"
approval_status = "已完成"
elif info_state == "审核中":
approval_status = "审批中"
elif info_state == "未通过":
approval_status = "审批中" # 未通过也可以继续审批流程
# 验证结果
passed = approval_status == expected_status
status_icon = "[PASS]" if passed else "[FAIL]"
print(f"\n{status_icon} 测试 {i}: {description}")
print(f" 类型: {info_type}")
print(f" 状态: {info_state}")
print(f" 期望返回: {expected_status}")
print(f" 实际返回: {approval_status}")
if not passed:
all_passed = False
print(f" [WARNING] 不匹配!")
print("\n" + "=" * 60)
print("测试结果:")
print("=" * 60)
if all_passed:
print("[PASS] 所有测试通过!")
print("\n[PASS] 逻辑验证正确:")
print(" - 对于'待办'类型:")
print(" * 状态为'已抄送财务' → 返回'已完成'(不需要等待财务查看)")
print(" * 状态为'已通过' → 返回'已完成'")
print(" * 状态为'审核中' → 返回'审批中'")
print(" * 状态为'未通过' → 返回'审批中'")
print("\n - 对于其他类型:")
print(" * 状态为'已抄送财务' → 返回'待查看'(需要财务查看)")
print(" * 状态为'已通过' → 返回'已完成'")
else:
print("[FAIL] 部分测试失败,请检查逻辑")
print("\n" + "=" * 60)
print("关键逻辑验证:")
print("=" * 60)
print("[PASS] 待办类型在审核通过后(状态为'已抄送财务'")
print(" 不管财务有没有查看,都会显示'已完成'")
print(" 这符合需求:'审核通过了不管财务有没有查看都是已完成'")
print("=" * 60)
if __name__ == "__main__":
test_todo_status_logic()