优化案件生成
This commit is contained in:
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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user