Compare commits
42 Commits
b016e5ed55
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3a3ac61e5 | ||
|
|
fe90de7e1f | ||
|
|
3ad5a16297 | ||
|
|
b3e6235c20 | ||
|
|
456b7ca03b | ||
|
|
13883519a4 | ||
|
|
a1a65b8725 | ||
|
|
52be5caa7f | ||
|
|
9203b17f2b | ||
|
|
de2d87553d | ||
|
|
64483a8ec7 | ||
|
|
4306291fd6 | ||
|
|
f0a52cd07b | ||
|
|
6c4cdf9daa | ||
|
|
f69ef6c930 | ||
|
|
9228327a76 | ||
|
|
d5aeae380c | ||
|
|
a5ab26e3c1 | ||
|
|
ba27d531a0 | ||
|
|
8434b4651f | ||
|
|
107192b14c | ||
|
|
f533cfde79 | ||
|
|
27275e7884 | ||
|
|
92b0c3b136 | ||
|
|
4970ee3676 | ||
|
|
b42e9808a6 | ||
|
|
2682dc1f1e | ||
|
|
3493d62348 | ||
|
|
22b5a8ed0c | ||
|
|
ca8561796c | ||
|
|
20a148b4fa | ||
|
|
f5fdc8dd3f | ||
|
|
9b19987387 | ||
|
|
f2aff9add8 | ||
|
|
149d18f72e | ||
|
|
ca512b9626 | ||
|
|
a03ef1cde8 | ||
|
|
5db0af8360 | ||
|
|
609b66fe8a | ||
|
|
dcbf5bb829 | ||
|
|
f187a4939a | ||
|
|
54372a3c2c |
@@ -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 = " → 财务部(按顺序审批)"
|
||||
|
||||
245
User/views.py
245
User/views.py
@@ -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
|
||||
|
||||
@@ -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
103
business/contract_no.py
Normal 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)
|
||||
0
business/management/__init__.py
Normal file
0
business/management/__init__.py
Normal file
0
business/management/commands/__init__.py
Normal file
0
business/management/commands/__init__.py
Normal file
124
business/management/commands/init_contract_counters.py
Normal file
124
business/management/commands/init_contract_counters.py
Normal 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}'
|
||||
))
|
||||
30
business/migrations/0018_contractcounter.py
Normal file
30
business/migrations/0018_contractcounter.py
Normal 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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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}号"
|
||||
@@ -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'),
|
||||
]
|
||||
1427
business/views.py
1427
business/views.py
File diff suppressed because it is too large
Load Diff
@@ -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 # 团队名称
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user