File size: 10,813 Bytes
b4f2238 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | """
法律文书生成系统 - 文件模板模型
本模块定义文件模板相关的数据模型.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, ClassVar
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
from apps.core.utils.path import Path
from apps.documents.storage import document_template_storage, resolve_docx_template_path
from .choices import (
DocumentCaseFileSubType,
DocumentCaseStage,
DocumentCaseType,
DocumentContractSubType,
DocumentContractType,
DocumentTemplateType,
LegalStatusMatchMode,
)
if TYPE_CHECKING:
from django.db.models.fields.related_descriptors import RelatedManager
from .evidence import EvidenceList
class DocumentTemplate(models.Model):
"""
文书模板
基于 docx 格式的法律文书模板文件.
支持两种模式:文件上传和路径引用.
支持两种模板类型:单个文书和全套文书.
Requirements: 2.1, 2.4, 2.6, 7.2
"""
id: int
name: Any = models.CharField(max_length=200, verbose_name=_("模板名称"))
template_type: Any = models.CharField(
max_length=20,
choices=DocumentTemplateType.choices,
default=DocumentTemplateType.CONTRACT,
verbose_name=_("模板类型"),
help_text=_("选择此模板用于合同还是案件"),
)
contract_sub_type: Any = models.CharField(
max_length=30,
choices=DocumentContractSubType.choices,
blank=True,
null=True,
verbose_name=_("合同子类型"),
help_text=_("仅在选择'合同文件模板'时有效,必须选择合同模板或补充协议模板"),
)
case_sub_type: Any = models.CharField(
max_length=50,
choices=DocumentCaseFileSubType.choices,
blank=True,
null=True,
verbose_name=_("案件文件子类型"),
help_text=_("仅在选择'案件文件模板'时有效,可选择诉状材料、证据材料、授权委托材料等"),
)
file = models.FileField(
storage=document_template_storage,
upload_to="", # 存储类会自动处理路径
blank=True,
null=True,
verbose_name=_("上传文件"),
)
file_path: Any = models.CharField(
max_length=500, blank=True, verbose_name=_("文件路径"), help_text=_("相对于模板基础目录的路径")
)
# 适用范围字段(与文件夹模板保持一致)
case_types: Any = models.JSONField(
default=list, verbose_name=_("案件类型"), help_text=_("JSON 数组,如 ['civil', 'criminal'],支持多选")
)
case_stages: Any = models.JSONField(
default=list, verbose_name=_("案件阶段"), help_text=_("JSON 数组,如 ['first_trial', 'second_trial'],支持多选")
)
contract_types: Any = models.JSONField(
default=list, verbose_name=_("合同类型"), help_text=_("JSON 数组,如 ['civil', 'criminal'],支持多选")
)
legal_statuses: Any = models.JSONField(
default=list,
blank=True,
verbose_name=_("我方诉讼地位"),
help_text=_("可单选或多选;为空表示匹配任意诉讼地位"),
)
legal_status_match_mode: Any = models.CharField(
max_length=16,
choices=LegalStatusMatchMode.choices,
default=LegalStatusMatchMode.ANY,
verbose_name=_("诉讼地位匹配模式"),
)
applicable_institutions: Any = models.JSONField(
default=list,
blank=True,
verbose_name=_("适用机构"),
help_text=_("JSON 数组,如 ['北京市第一中级人民法院'],支持多选"),
)
is_active: Any = models.BooleanField(default=True, verbose_name=_("是否启用"))
created_at: Any = models.DateTimeField(auto_now_add=True, verbose_name=_("创建时间"))
updated_at: Any = models.DateTimeField(auto_now=True, verbose_name=_("更新时间"))
if TYPE_CHECKING:
folder_bindings: RelatedManager[DocumentTemplateFolderBinding]
evidence_lists: RelatedManager[EvidenceList]
class Meta:
app_label = "documents"
verbose_name = _("文件模板")
verbose_name_plural = _("文件模板")
ordering: ClassVar = ["-updated_at"]
indexes: ClassVar = [
models.Index(fields=["template_type"]),
models.Index(fields=["is_active"]),
]
def __str__(self) -> str:
return str(self.name)
def clean(self) -> None:
file_present = bool(self.file)
file_path_present = bool((self.file_path or "").strip())
if file_present and file_path_present:
message = _("不能同时提供上传文件和文件路径")
raise ValidationError({"file": message, "file_path": message})
if not file_present and not file_path_present:
raise ValidationError(_("必须提供上传文件或文件路径"))
def get_file_location(self) -> str:
"""获取文件实际位置"""
if self.file and self.file.name:
return str(self.file.storage.path(self.file.name))
if self.file_path:
return str(resolve_docx_template_path(self.file_path))
return ""
def _get_types_display(self, types_list: list[Any], choices_class: type[models.TextChoices]) -> str:
"""获取类型列表的显示文本"""
if not types_list:
return "-"
if len(types_list) == 1:
return str(dict(choices_class.choices).get(types_list[0], types_list[0]))
return f"{len(types_list)}种类型"
@property
def template_type_display(self) -> str:
"""模板类型显示"""
base_type = str(dict(DocumentTemplateType.choices).get(self.template_type, self.template_type))
if self.template_type == DocumentTemplateType.CONTRACT and self.contract_sub_type:
sub_type = str(dict(DocumentContractSubType.choices).get(self.contract_sub_type, self.contract_sub_type))
return f"{base_type} - {sub_type}"
if self.template_type == DocumentTemplateType.CASE and self.case_sub_type:
sub_type = str(dict(DocumentCaseFileSubType.choices).get(self.case_sub_type, self.case_sub_type))
return f"{base_type} - {sub_type}"
return base_type
@property
def case_types_display(self) -> str:
"""案件类型显示"""
return self._get_types_display(self.case_types, DocumentCaseType)
@property
def case_stages_display(self) -> str:
"""案件阶段显示"""
return self._get_types_display(self.case_stages, DocumentCaseStage)
@property
def contract_types_display(self) -> str:
"""合同类型显示"""
return self._get_types_display(self.contract_types, DocumentContractType)
def get_legal_statuses_display(self) -> str:
"""获取诉讼地位显示文本"""
from apps.core.models.enums import LegalStatus
choices = dict(LegalStatus.choices)
statuses = self.legal_statuses or []
labels = [str(choices.get(code, code)) for code in statuses]
return "、".join([x for x in labels if x]) or "任意"
@property
def absolute_file_path(self) -> str:
"""文件的绝对路径"""
if not self.file_path:
return ""
return str(resolve_docx_template_path(self.file_path))
class DocumentTemplateFolderBinding(models.Model):
"""
文件模板与文件夹节点绑定
建立文件模板和文件夹模板中具体节点的多对多关系.
支持:
- 一个文件模板绑定到多个文件夹模板的不同节点
- 同一文件夹模板中,一个文件模板可放在不同位置
Requirements: 2.8
"""
id: int
document_template_id: int # 外键ID字段
folder_template_id: int # 外键ID字段
document_template: Any = models.ForeignKey(
"documents.DocumentTemplate",
on_delete=models.CASCADE,
related_name="folder_bindings",
verbose_name=_("文件模板"),
)
folder_template: Any = models.ForeignKey(
"documents.FolderTemplate",
on_delete=models.CASCADE,
related_name="document_bindings",
verbose_name=_("文件夹模板"),
)
folder_node_id: Any = models.CharField(
max_length=100,
verbose_name=_("文件夹节点ID"),
help_text=_("文件夹结构JSON中的节点ID"),
)
folder_node_path: Any = models.CharField(
max_length=500,
blank=True,
verbose_name=_("文件夹路径"),
help_text=_("自动计算的文件夹路径,如:一审/1-立案材料/1-起诉状和反诉答辩状"),
)
is_active: Any = models.BooleanField(default=True, verbose_name=_("是否启用"))
created_at: Any = models.DateTimeField(auto_now_add=True, verbose_name=_("创建时间"))
updated_at: Any = models.DateTimeField(auto_now=True, verbose_name=_("更新时间"))
class Meta:
app_label = "documents"
verbose_name = _("文件模板文件夹绑定")
verbose_name_plural = _("文件模板文件夹绑定")
ordering: ClassVar = ["folder_template", "document_template"]
# 同一文件模板在同一文件夹模板的同一节点只能绑定一次
unique_together: ClassVar = ["document_template", "folder_template", "folder_node_id"]
indexes: ClassVar = [
models.Index(fields=["folder_template", "folder_node_id"]),
models.Index(fields=["document_template"]),
models.Index(fields=["is_active"]),
]
def __str__(self) -> str:
return (
f"{self.document_template.name} → "
f"{self.folder_template.name}/{self.folder_node_path or self.folder_node_id}"
)
def save(self, *args: Any, **kwargs: Any) -> None:
"""保存时自动根据 folder_node_id 计算 folder_node_path"""
if self.folder_node_id and self.folder_template_id:
try:
structure = self.folder_template.structure or {}
path = self._find_node_path(structure.get("children", []), self.folder_node_id, [])
self.folder_node_path = "/".join(path) if path else ""
except Exception:
pass
super().save(*args, **kwargs)
def _find_node_path(self, children: list[Any], target_id: str, current_path: list[str]) -> list[str]:
for child in children:
path = current_path + [child.get("name", "")]
if child.get("id") == target_id:
return path
result = self._find_node_path(child.get("children", []), target_id, path)
if result:
return result
return []
|