优化案件生成

This commit is contained in:
ddrwode
2026-02-04 14:03:15 +08:00
parent 92b0c3b136
commit 27275e7884
2 changed files with 184 additions and 59 deletions

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

@@ -9,6 +9,7 @@ from User.models import User, Approval
from User.utils import log_operation, normalize_approvers_param, build_missing_approvers_message
from .models import PreFiling, ProjectRegistration, Bid, Case, Invoice, Caselog, SealApplication, Warehousing, \
RegisterPlatform, Announcement, LawyerFlie, Schedule, permission, role, CaseChangeRequest, CaseTag,Propaganda,System, ContractCounter
from .contract_no import generate_next_contract_no, get_next_contract_no_preview
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from utility.utility import flies
from datetime import datetime
@@ -560,7 +561,8 @@ class Project(APIView):
def post(self, request, *args, **kwargs):
"""
立项登记 - 独立创建,不再需要预立案。
必填:项目类型、合同编号、立项日期、项目简述、收费情况、承办人信息、合同。
必填:项目类型、立项日期、项目简述、收费情况、承办人信息、合同。
合同编号(ContractNo)可选:不传或传空时由后端按规则自动生成(校准(字)字【年份】第n号
相对方(party_info)为非必填项,委托人(client_info)可为空。
:param request:
:param args:
@@ -569,6 +571,8 @@ class Project(APIView):
"""
project_type = request.data.get('type')
ContractNo = request.data.get('ContractNo')
if ContractNo is not None and not isinstance(ContractNo, str):
ContractNo = str(ContractNo)
times = request.data.get('times')
client_info = request.data.get('client_info') # 委托人身份信息(可选)
party_info = request.data.get('party_info') # 相对方身份信息(非必填)
@@ -590,9 +594,9 @@ class Project(APIView):
if 'user_id' in request.data:
return Response({'status': 'error', 'message': '此接口不需要 user_id 参数,立项登记已独立创建', 'code': 1}, status=status.HTTP_400_BAD_REQUEST)
# 验证必填字段(相对方 party_info 为非必填,不参与校验
if not all([project_type, ContractNo, times, description, charge]):
return Response({'status': 'error', 'message': '缺少必填参数(项目类型、合同编号、立项日期、项目简述、收费情况)', 'code': 1}, status=status.HTTP_400_BAD_REQUEST)
# 验证必填字段(合同编号可选,不传则后端自动生成
if not all([project_type, times, description, charge]):
return Response({'status': 'error', 'message': '缺少必填参数(项目类型、立项日期、项目简述、收费情况)', 'code': 1}, status=status.HTTP_400_BAD_REQUEST)
# 验证承办人信息(字典格式)
if not responsiblefor:
@@ -626,18 +630,31 @@ class Project(APIView):
else:
contract_str = ""
# 检查合同编号是否已存在
existing_project = ProjectRegistration.objects.filter(ContractNo=ContractNo, is_deleted=False).first()
if existing_project:
return Response({'status': 'error', 'message': '该合同编号已存在,不能重复创建', 'code': 1},
status=status.HTTP_400_BAD_REQUEST)
need_auto_contract_no = not (ContractNo and ContractNo.strip())
if not need_auto_contract_no:
# 用户传入合同编号:检查是否已存在
existing_project = ProjectRegistration.objects.filter(ContractNo=ContractNo.strip(), is_deleted=False).first()
if existing_project:
return Response({'status': 'error', 'message': '该合同编号已存在,不能重复创建', 'code': 1},
status=status.HTTP_400_BAD_REQUEST)
contract_year = int(times[:4]) if times and len(times) >= 4 else datetime.now().year
# 使用事务确保创建立项和更新计数器是原子操作
with transaction.atomic():
if need_auto_contract_no:
ContractNo = generate_next_contract_no(project_type, contract_year)
if not ContractNo:
return Response({
'status': 'error',
'message': f'项目类型「{project_type}」未配置合同编号规则,无法自动生成。请手动填写合同编号,或选择已配置的类型(如:法律顾问、专项服务、民事案件代理等)',
'code': 1
}, status=status.HTTP_400_BAD_REQUEST)
# 创建立项登记(相对方可为空,空字符串统一为 None
pro = ProjectRegistration.objects.create(
type=project_type,
ContractNo=ContractNo,
ContractNo=ContractNo.strip() if isinstance(ContractNo, str) else ContractNo,
times=times,
client_info=client_info or None,
party_info=party_info if party_info not in (None, '') else None,
@@ -647,27 +664,21 @@ class Project(APIView):
contract=contract_str,
state="审核中",
)
# 更新合同编号计数器(确保下一个编号正确
# 从合同编号中提取序号,更新计数器
try:
# 从立项日期中获取年份(合同编号的年份应该与立项日期一致)
contract_year = int(times[:4]) if times and len(times) >= 4 else datetime.now().year
# 使用 select_for_update 锁定记录,防止并发冲突
counter, created = ContractCounter.objects.select_for_update().get_or_create(
project_type=project_type,
year=contract_year,
defaults={'current_number': 1}
)
if not created:
# 如果计数器已存在,递增序号
counter.current_number += 1
counter.save(update_fields=['current_number', 'updated_at'])
except Exception as e:
# 计数器更新失败不影响立项创建,仅记录日志
import logging
logging.warning(f"更新合同编号计数器失败: {e}")
# 仅当用户手动传入合同编号时,更新计数器(与自动生成逻辑一致,保证序号连续
if not need_auto_contract_no:
try:
counter, created = ContractCounter.objects.select_for_update().get_or_create(
project_type=project_type,
year=contract_year,
defaults={'current_number': 1}
)
if not created:
counter.current_number += 1
counter.save(update_fields=['current_number', 'updated_at'])
except Exception as e:
import logging
logging.warning(f"更新合同编号计数器失败: {e}")
today = datetime.datetime.now()
@@ -753,57 +764,68 @@ class Project(APIView):
remark=f'新增立项登记:合同编号 {pro.ContractNo},承办人 {responsiblefor_dict.get("responsible_person", "")}'
)
return Response({'message': '登记成功', 'code': 0}, status=status.HTTP_200_OK)
return Response({
'message': '登记成功',
'code': 0,
'contract_no': pro.ContractNo,
}, status=status.HTTP_200_OK)
class Projectquerytype(APIView):
def post(self, request, *args, **kwargs):
"""
立项登记类型查询 - 获取下一个合同编号的序号
使用全局计数器表,保证:
1. 全局唯一计数,不受用户权限影响
2. 每个类别独立计数
3. 跨年自动归零
:param request:
:param args:
:param kwargs:
:return:
查询某一个项目类型的数量(及下一个合同编号预览)。
入参:
- type必填项目类型如 法律顾问、专项服务
- times 或 year可选年份不传则按当前年times 取前四位作为年份
返回 data
- count该项目类型在指定年份的立项数量已用序号即已有多少条
- next_number下一个序号count + 1
- next_contract_no下一个合同编号预览如 校准(顾)字【2025】第1号
每个项目类型按「类型+年份」独立计数,跨年归零。
"""
project_type = request.data.get('type')
if not project_type:
return Response({'status': 'error', 'message': '缺少参数type', 'code': 1}, status=status.HTTP_400_BAD_REQUEST)
# 获取当前年
current_year = datetime.now().year
# 从计数器表获取当前序号(不创建,只读取)
# 使用 select_for_update 确保并发安全
# 可选:传入 times 或 year 指定年份,否则用当前年
times = request.data.get('times')
if times and len(str(times)) >= 4:
try:
current_year = int(str(times)[:4])
except ValueError:
current_year = datetime.now().year
else:
current_year = datetime.now().year
try:
with transaction.atomic():
counter = ContractCounter.objects.filter(
project_type=project_type,
year=current_year
).first()
if counter:
count = counter.current_number
else:
# 如果计数器不存在说明是该类型该年第一条返回0
count = 0
except Exception as e:
# 如果出错,回退到旧的统计方式(兼容性)
count = counter.current_number if counter else 0
except Exception:
start_of_year = datetime(current_year, 1, 1)
end_of_year = datetime(current_year, 12, 31)
start_of_year_str = start_of_year.strftime("%Y-%m-%d")
end_of_year_str = end_of_year.strftime("%Y-%m-%d")
count = ProjectRegistration.objects.filter(
type=project_type,
times__gte=start_of_year_str,
type=project_type,
times__gte=start_of_year_str,
times__lte=end_of_year_str,
is_deleted=False
).count()
data = {"count": count}
next_number, next_contract_no = get_next_contract_no_preview(project_type, current_year)
data = {
"count": count,
"next_number": next_number,
"next_contract_no": next_contract_no or "",
}
return Response({'message': '查询成功', "data": data, 'code': 0}, status=status.HTTP_200_OK)