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 []