feat(regulatory): 增加整改复核闭环
This commit is contained in:
28
review_agent/migrations/0005_alter_regulatoryissue_status.py
Normal file
28
review_agent/migrations/0005_alter_regulatoryissue_status.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 5.2.14 on 2026-06-07 01:29
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("review_agent", "0004_regulatoryreviewbatch_condition_json_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="regulatoryissue",
|
||||||
|
name="status",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("open", "待处理"),
|
||||||
|
("resolved", "已整改"),
|
||||||
|
("accepted", "已接受"),
|
||||||
|
("review_passed", "复核通过"),
|
||||||
|
("review_failed", "复核未通过"),
|
||||||
|
],
|
||||||
|
default="open",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -479,6 +479,8 @@ class RegulatoryIssue(models.Model):
|
|||||||
OPEN = "open", "待处理"
|
OPEN = "open", "待处理"
|
||||||
RESOLVED = "resolved", "已整改"
|
RESOLVED = "resolved", "已整改"
|
||||||
ACCEPTED = "accepted", "已接受"
|
ACCEPTED = "accepted", "已接受"
|
||||||
|
REVIEW_PASSED = "review_passed", "复核通过"
|
||||||
|
REVIEW_FAILED = "review_failed", "复核未通过"
|
||||||
|
|
||||||
batch = models.ForeignKey(
|
batch = models.ForeignKey(
|
||||||
RegulatoryReviewBatch,
|
RegulatoryReviewBatch,
|
||||||
|
|||||||
@@ -46,12 +46,19 @@ def build_markdown_report(batch: RegulatoryReviewBatch) -> str:
|
|||||||
"# NMPA 注册资料法规核查报告",
|
"# NMPA 注册资料法规核查报告",
|
||||||
"",
|
"",
|
||||||
f"批次号:{batch.batch_no}",
|
f"批次号:{batch.batch_no}",
|
||||||
"",
|
|
||||||
"## 风险汇总",
|
|
||||||
"",
|
|
||||||
"| 风险等级 | 数量 |",
|
|
||||||
"| --- | --- |",
|
|
||||||
]
|
]
|
||||||
|
regenerated_from = (batch.condition_json or {}).get("regenerated_from")
|
||||||
|
if regenerated_from:
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"## 复核来源",
|
||||||
|
"",
|
||||||
|
f"- 来源法规核查批次:{regenerated_from.get('batch_no')}",
|
||||||
|
f"- 来源文件汇总批次:{regenerated_from.get('file_summary_batch_no')}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
lines.extend(["", "## 风险汇总", "", "| 风险等级 | 数量 |", "| --- | --- |"])
|
||||||
summary = batch.risk_summary or {}
|
summary = batch.risk_summary or {}
|
||||||
for severity, label in SEVERITY_LABELS.items():
|
for severity, label in SEVERITY_LABELS.items():
|
||||||
lines.append(f"| {label} | {summary.get(severity, 0)} |")
|
lines.append(f"| {label} | {summary.get(severity, 0)} |")
|
||||||
@@ -60,6 +67,14 @@ def build_markdown_report(batch: RegulatoryReviewBatch) -> str:
|
|||||||
lines.append(
|
lines.append(
|
||||||
f"| {SEVERITY_LABELS.get(issue.severity, issue.severity)} | {issue.title} | {issue.status} | {issue.suggestion or '-'} |"
|
f"| {SEVERITY_LABELS.get(issue.severity, issue.severity)} | {issue.title} | {issue.status} | {issue.suggestion or '-'} |"
|
||||||
)
|
)
|
||||||
|
review_records = _review_records(batch)
|
||||||
|
if review_records:
|
||||||
|
lines.extend(["", "## 复核记录", "", "| 补充批次 | 问题数 | 通过数 | 未通过数 |", "| --- | --- | --- | --- |"])
|
||||||
|
for record in review_records:
|
||||||
|
items = record.get("items", [])
|
||||||
|
passed = sum(1 for item in items if item.get("status") == RegulatoryIssue.Status.REVIEW_PASSED)
|
||||||
|
failed = sum(1 for item in items if item.get("status") == RegulatoryIssue.Status.REVIEW_FAILED)
|
||||||
|
lines.append(f"| {record.get('file_summary_batch_no')} | {len(items)} | {passed} | {failed} |")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -67,6 +82,7 @@ def build_result_payload(batch: RegulatoryReviewBatch) -> dict[str, object]:
|
|||||||
return {
|
return {
|
||||||
"batch_no": batch.batch_no,
|
"batch_no": batch.batch_no,
|
||||||
"source_summary_batch": batch.source_summary_batch.batch_no,
|
"source_summary_batch": batch.source_summary_batch.batch_no,
|
||||||
|
"regenerated_from": (batch.condition_json or {}).get("regenerated_from"),
|
||||||
"risk_summary": batch.risk_summary,
|
"risk_summary": batch.risk_summary,
|
||||||
"issues": [
|
"issues": [
|
||||||
{
|
{
|
||||||
@@ -82,6 +98,7 @@ def build_result_payload(batch: RegulatoryReviewBatch) -> dict[str, object]:
|
|||||||
}
|
}
|
||||||
for issue in batch.issues.order_by("id")
|
for issue in batch.issues.order_by("id")
|
||||||
],
|
],
|
||||||
|
"review_records": _review_records(batch),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -165,3 +182,13 @@ def _create_excel_export(batch: RegulatoryReviewBatch, path: Path) -> ExportedSu
|
|||||||
file_name=path.name,
|
file_name=path.name,
|
||||||
storage_path=str(path),
|
storage_path=str(path),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _review_records(batch: RegulatoryReviewBatch) -> list[dict[str, object]]:
|
||||||
|
records = []
|
||||||
|
for artifact in batch.artifacts.filter(metadata__artifact="review_record").order_by("created_at", "id"):
|
||||||
|
try:
|
||||||
|
records.append(json.loads(Path(artifact.storage_path).read_text(encoding="utf-8")))
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
continue
|
||||||
|
return records
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from review_agent.models import FileSummaryBatch, RegulatoryIssue, RegulatoryReviewBatch
|
||||||
|
from review_agent.regulatory_review.services.rule_loader import load_rule_file
|
||||||
|
from review_agent.regulatory_review.storage import save_artifact
|
||||||
|
|
||||||
|
|
||||||
|
def review_missing_issues(
|
||||||
|
*,
|
||||||
|
batch: RegulatoryReviewBatch,
|
||||||
|
issue_ids: list[int],
|
||||||
|
file_summary_batch: FileSummaryBatch,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
rule_set = load_rule_file()
|
||||||
|
rules_by_code = {rule["code"]: rule for rule in rule_set.get("requirements", [])}
|
||||||
|
items = list(file_summary_batch.items.order_by("file_index"))
|
||||||
|
record = {
|
||||||
|
"type": "review_record",
|
||||||
|
"reviewed_at": timezone.localtime().isoformat(),
|
||||||
|
"source_review_batch_id": batch.pk,
|
||||||
|
"source_review_batch_no": batch.batch_no,
|
||||||
|
"file_summary_batch_id": file_summary_batch.pk,
|
||||||
|
"file_summary_batch_no": file_summary_batch.batch_no,
|
||||||
|
"items": [],
|
||||||
|
}
|
||||||
|
issues = RegulatoryIssue.objects.filter(batch=batch, pk__in=issue_ids).order_by("id")
|
||||||
|
for issue in issues:
|
||||||
|
rule = rules_by_code.get(issue.rule_code, {})
|
||||||
|
matched_files = _match_items(items, [*rule.get("file_keywords", []), issue.title])
|
||||||
|
passed = bool(matched_files)
|
||||||
|
issue.status = RegulatoryIssue.Status.REVIEW_PASSED if passed else RegulatoryIssue.Status.REVIEW_FAILED
|
||||||
|
issue.evidence = {
|
||||||
|
**(issue.evidence or {}),
|
||||||
|
"latest_review": {
|
||||||
|
"file_summary_batch_id": file_summary_batch.pk,
|
||||||
|
"file_summary_batch_no": file_summary_batch.batch_no,
|
||||||
|
"matched_files": matched_files,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
issue.save(update_fields=["status", "evidence", "updated_at"])
|
||||||
|
record["items"].append(
|
||||||
|
{
|
||||||
|
"issue_id": issue.pk,
|
||||||
|
"rule_code": issue.rule_code,
|
||||||
|
"title": issue.title,
|
||||||
|
"status": issue.status,
|
||||||
|
"matched_files": matched_files,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
artifact = save_artifact(
|
||||||
|
batch,
|
||||||
|
name=f"review_record_{timezone.now().strftime('%Y%m%d%H%M%S')}.json",
|
||||||
|
artifact_type="json",
|
||||||
|
content=json.dumps(record, ensure_ascii=False, indent=2),
|
||||||
|
metadata={"artifact": "review_record", "file_summary_batch_id": file_summary_batch.pk},
|
||||||
|
)
|
||||||
|
record["artifact_id"] = artifact.pk
|
||||||
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
def _match_items(items, keywords: list[str]) -> list[dict[str, str]]:
|
||||||
|
normalized_keywords = [str(keyword).lower() for keyword in keywords if keyword]
|
||||||
|
matched = []
|
||||||
|
for item in items:
|
||||||
|
haystack = f"{item.file_name} {item.relative_path} {item.directory_level}".lower()
|
||||||
|
if any(keyword in haystack for keyword in normalized_keywords):
|
||||||
|
matched.append(
|
||||||
|
{
|
||||||
|
"file_name": item.file_name,
|
||||||
|
"relative_path": item.relative_path,
|
||||||
|
"directory_level": item.directory_level,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return matched
|
||||||
@@ -7,9 +7,10 @@ from django.http import Http404, JsonResponse
|
|||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
from review_agent.models import RegulatoryReviewBatch, WorkflowNodeRun
|
from review_agent.models import FileSummaryBatch, RegulatoryReviewBatch, WorkflowNodeRun
|
||||||
from review_agent.regulatory_review.events import record_event
|
from review_agent.regulatory_review.events import record_event
|
||||||
from review_agent.regulatory_review.workflow import start_regulatory_review_workflow
|
from review_agent.regulatory_review.services.rectification_review import review_missing_issues
|
||||||
|
from review_agent.regulatory_review.workflow import create_regulatory_review_batch, start_regulatory_review_workflow
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
@@ -78,6 +79,87 @@ def confirm_conditions(request, batch_id: int):
|
|||||||
progress=100,
|
progress=100,
|
||||||
message="适用条件已确认",
|
message="适用条件已确认",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
@login_required
|
||||||
|
def start_full_review(request, batch_id: int):
|
||||||
|
source_batch = RegulatoryReviewBatch.objects.filter(pk=batch_id, user=request.user).first()
|
||||||
|
if not source_batch:
|
||||||
|
raise Http404("批次不存在。")
|
||||||
|
payload, error_response = _json_payload(request)
|
||||||
|
if error_response:
|
||||||
|
return error_response
|
||||||
|
summary_batch = FileSummaryBatch.objects.filter(
|
||||||
|
pk=payload.get("file_summary_batch_id"),
|
||||||
|
conversation=source_batch.conversation,
|
||||||
|
user=request.user,
|
||||||
|
status=FileSummaryBatch.Status.SUCCESS,
|
||||||
|
).first()
|
||||||
|
if not summary_batch:
|
||||||
|
return JsonResponse({"error": "file_summary_batch_id 不存在或未成功。"}, status=400)
|
||||||
|
new_batch = create_regulatory_review_batch(
|
||||||
|
conversation=source_batch.conversation,
|
||||||
|
user=request.user,
|
||||||
|
source_summary_batch=summary_batch,
|
||||||
|
)
|
||||||
|
new_batch.condition_json = {
|
||||||
|
"source_review_batch_id": source_batch.pk,
|
||||||
|
"regenerated_from": {
|
||||||
|
"batch_id": source_batch.pk,
|
||||||
|
"batch_no": source_batch.batch_no,
|
||||||
|
"file_summary_batch_id": source_batch.source_summary_batch_id,
|
||||||
|
"file_summary_batch_no": source_batch.source_summary_batch.batch_no,
|
||||||
|
},
|
||||||
|
"confirmed": True,
|
||||||
|
"confirmed_conditions": source_batch.condition_json.get("confirmed_conditions", {}),
|
||||||
|
}
|
||||||
|
new_batch.save(update_fields=["condition_json"])
|
||||||
|
record_event(
|
||||||
|
new_batch,
|
||||||
|
"full_package_review_started",
|
||||||
|
{"source_review_batch_id": source_batch.pk, "source_review_batch_no": source_batch.batch_no},
|
||||||
|
)
|
||||||
|
start_regulatory_review_workflow(
|
||||||
|
new_batch,
|
||||||
|
async_run=getattr(settings, "REGULATORY_REVIEW_ASYNC", True),
|
||||||
|
)
|
||||||
|
new_batch.refresh_from_db()
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"batch": {
|
||||||
|
"id": new_batch.pk,
|
||||||
|
"workflow_type": "regulatory_review",
|
||||||
|
"batch_no": new_batch.batch_no,
|
||||||
|
"status": new_batch.status,
|
||||||
|
"source_review_batch_id": source_batch.pk,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
@login_required
|
||||||
|
def review_issues(request, batch_id: int):
|
||||||
|
batch = RegulatoryReviewBatch.objects.filter(pk=batch_id, user=request.user).first()
|
||||||
|
if not batch:
|
||||||
|
raise Http404("批次不存在。")
|
||||||
|
payload, error_response = _json_payload(request)
|
||||||
|
if error_response:
|
||||||
|
return error_response
|
||||||
|
issue_ids = payload.get("issue_ids")
|
||||||
|
if not isinstance(issue_ids, list):
|
||||||
|
return JsonResponse({"error": "issue_ids 必须是列表。"}, status=400)
|
||||||
|
summary_batch = FileSummaryBatch.objects.filter(
|
||||||
|
pk=payload.get("file_summary_batch_id"),
|
||||||
|
conversation=batch.conversation,
|
||||||
|
user=request.user,
|
||||||
|
status=FileSummaryBatch.Status.SUCCESS,
|
||||||
|
).first()
|
||||||
|
if not summary_batch:
|
||||||
|
return JsonResponse({"error": "file_summary_batch_id 不存在或未成功。"}, status=400)
|
||||||
|
record = review_missing_issues(batch=batch, issue_ids=[int(item) for item in issue_ids], file_summary_batch=summary_batch)
|
||||||
|
return JsonResponse({"review_record": record})
|
||||||
record_event(
|
record_event(
|
||||||
batch,
|
batch,
|
||||||
"condition_confirmed",
|
"condition_confirmed",
|
||||||
@@ -126,3 +208,10 @@ def _normalize_conditions(conditions: dict) -> dict[str, str]:
|
|||||||
"intended_use",
|
"intended_use",
|
||||||
]
|
]
|
||||||
return {key: str(conditions.get(key) or "").strip() for key in allowed}
|
return {key: str(conditions.get(key) or "").strip() for key in allowed}
|
||||||
|
|
||||||
|
|
||||||
|
def _json_payload(request):
|
||||||
|
try:
|
||||||
|
return json.loads(request.body.decode("utf-8") or "{}"), None
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {}, JsonResponse({"error": "请求体不是有效 JSON。"}, status=400)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ from .file_summary.views import (
|
|||||||
from .regulatory_review.views import (
|
from .regulatory_review.views import (
|
||||||
batch_status as regulatory_review_batch_status,
|
batch_status as regulatory_review_batch_status,
|
||||||
confirm_conditions as regulatory_review_confirm_conditions,
|
confirm_conditions as regulatory_review_confirm_conditions,
|
||||||
|
review_issues as regulatory_review_review_issues,
|
||||||
|
start_full_review as regulatory_review_start_full_review,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -72,4 +74,14 @@ urlpatterns = [
|
|||||||
regulatory_review_confirm_conditions,
|
regulatory_review_confirm_conditions,
|
||||||
name="regulatory_review_confirm_conditions",
|
name="regulatory_review_confirm_conditions",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"api/review-agent/regulatory-review/<int:batch_id>/full-review/",
|
||||||
|
regulatory_review_start_full_review,
|
||||||
|
name="regulatory_review_start_full_review",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"api/review-agent/regulatory-review/<int:batch_id>/issue-review/",
|
||||||
|
regulatory_review_review_issues,
|
||||||
|
name="regulatory_review_review_issues",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
133
tests/test_regulatory_rectification.py
Normal file
133
tests/test_regulatory_rectification.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from review_agent.models import (
|
||||||
|
Conversation,
|
||||||
|
FileSummaryBatch,
|
||||||
|
FileSummaryItem,
|
||||||
|
RegulatoryArtifact,
|
||||||
|
RegulatoryIssue,
|
||||||
|
RegulatoryReviewBatch,
|
||||||
|
)
|
||||||
|
from review_agent.regulatory_review.services.export import build_markdown_report, build_result_payload
|
||||||
|
from review_agent.regulatory_review.services.rectification_review import review_missing_issues
|
||||||
|
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
|
def _make_review_batch(user):
|
||||||
|
conversation = Conversation.objects.create(user=user, title="会话")
|
||||||
|
original_summary = FileSummaryBatch.objects.create(
|
||||||
|
conversation=conversation,
|
||||||
|
user=user,
|
||||||
|
batch_no="FS-ORIGINAL",
|
||||||
|
status=FileSummaryBatch.Status.SUCCESS,
|
||||||
|
)
|
||||||
|
batch = RegulatoryReviewBatch.objects.create(
|
||||||
|
conversation=conversation,
|
||||||
|
user=user,
|
||||||
|
source_summary_batch=original_summary,
|
||||||
|
batch_no="RR-ORIGINAL",
|
||||||
|
status=RegulatoryReviewBatch.Status.SUCCESS,
|
||||||
|
)
|
||||||
|
return conversation, original_summary, batch
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_full_package_review_creates_new_traceable_batch(client, settings, tmp_path, django_user_model):
|
||||||
|
settings.MEDIA_ROOT = tmp_path
|
||||||
|
settings.REGULATORY_REVIEW_ASYNC = False
|
||||||
|
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||||
|
conversation, _original_summary, original_batch = _make_review_batch(user)
|
||||||
|
new_summary = FileSummaryBatch.objects.create(
|
||||||
|
conversation=conversation,
|
||||||
|
user=user,
|
||||||
|
batch_no="FS-NEW",
|
||||||
|
status=FileSummaryBatch.Status.SUCCESS,
|
||||||
|
)
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
reverse("regulatory_review_start_full_review", args=[original_batch.pk]),
|
||||||
|
data=json.dumps({"file_summary_batch_id": new_summary.pk}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
new_batch = RegulatoryReviewBatch.objects.exclude(pk=original_batch.pk).get()
|
||||||
|
assert new_batch.source_summary_batch == new_summary
|
||||||
|
assert new_batch.condition_json["source_review_batch_id"] == original_batch.pk
|
||||||
|
assert new_batch.condition_json["regenerated_from"]["batch_no"] == "RR-ORIGINAL"
|
||||||
|
|
||||||
|
|
||||||
|
def test_review_missing_issues_updates_status_and_writes_record(settings, tmp_path, django_user_model):
|
||||||
|
settings.MEDIA_ROOT = tmp_path
|
||||||
|
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||||
|
conversation, _original_summary, batch = _make_review_batch(user)
|
||||||
|
issue = RegulatoryIssue.objects.create(
|
||||||
|
batch=batch,
|
||||||
|
rule_code="attachment4_5_3_label",
|
||||||
|
category=RegulatoryIssue.Category.COMPLETENESS,
|
||||||
|
severity=RegulatoryIssue.Severity.HIGH,
|
||||||
|
title="缺少标签样稿",
|
||||||
|
suggestion="请补充标签样稿。",
|
||||||
|
)
|
||||||
|
supplement = FileSummaryBatch.objects.create(
|
||||||
|
conversation=conversation,
|
||||||
|
user=user,
|
||||||
|
batch_no="FS-SUPPLEMENT",
|
||||||
|
status=FileSummaryBatch.Status.SUCCESS,
|
||||||
|
)
|
||||||
|
FileSummaryItem.objects.create(
|
||||||
|
batch=supplement,
|
||||||
|
file_index=1,
|
||||||
|
directory_level="5. 产品说明书和标签样稿",
|
||||||
|
file_name="标签样稿.pdf",
|
||||||
|
file_type="pdf",
|
||||||
|
relative_path="5.3 标签样稿/标签样稿.pdf",
|
||||||
|
storage_path="x/label.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
|
record = review_missing_issues(batch=batch, issue_ids=[issue.pk], file_summary_batch=supplement)
|
||||||
|
|
||||||
|
issue.refresh_from_db()
|
||||||
|
assert issue.status == RegulatoryIssue.Status.REVIEW_PASSED
|
||||||
|
assert record["items"][0]["status"] == "review_passed"
|
||||||
|
assert RegulatoryArtifact.objects.filter(batch=batch, name__startswith="review_record").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_issue_review_endpoint_and_report_output(client, settings, tmp_path, django_user_model):
|
||||||
|
settings.MEDIA_ROOT = tmp_path
|
||||||
|
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||||
|
conversation, _original_summary, batch = _make_review_batch(user)
|
||||||
|
issue = RegulatoryIssue.objects.create(
|
||||||
|
batch=batch,
|
||||||
|
rule_code="attachment4_6_quality_system",
|
||||||
|
category=RegulatoryIssue.Category.COMPLETENESS,
|
||||||
|
severity=RegulatoryIssue.Severity.HIGH,
|
||||||
|
title="缺少质量管理体系文件",
|
||||||
|
suggestion="请补充质量管理体系文件。",
|
||||||
|
)
|
||||||
|
supplement = FileSummaryBatch.objects.create(
|
||||||
|
conversation=conversation,
|
||||||
|
user=user,
|
||||||
|
batch_no="FS-SUPPLEMENT",
|
||||||
|
status=FileSummaryBatch.Status.SUCCESS,
|
||||||
|
)
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
reverse("regulatory_review_review_issues", args=[batch.pk]),
|
||||||
|
data=json.dumps({"issue_ids": [issue.pk], "file_summary_batch_id": supplement.pk}),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
issue.refresh_from_db()
|
||||||
|
payload = build_result_payload(batch)
|
||||||
|
markdown = build_markdown_report(batch)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert issue.status == RegulatoryIssue.Status.REVIEW_FAILED
|
||||||
|
assert payload["review_records"][0]["file_summary_batch_no"] == "FS-SUPPLEMENT"
|
||||||
|
assert "复核记录" in markdown
|
||||||
Reference in New Issue
Block a user