Let customer-service quick register write storage, certificate, and insurance separately without extra menu or data-scope checks. List freight by document data scope like the web admin, allow protocol staff outside the collection region, and pad OA datetimes to seconds so PC date pickers can show historical minute values. Co-authored-by: Cursor <cursoragent@cursor.com>
382 lines
12 KiB
Python
382 lines
12 KiB
Python
"""OA 发起审批草稿 CRUD(oa_instance_draft)"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from datetime import datetime
|
||
from typing import Any, Dict, List, Optional, Tuple
|
||
|
||
from sqlalchemy import func, or_
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.utils import generate_uuid
|
||
from app.crud import oa_approval as oa_crud
|
||
from app.models.models import OaApp, OaAppGroup, OaAppVersion, OaInstanceDraft
|
||
from app.services.oa_related_approval import flatten_form_fields_for_summary
|
||
|
||
|
||
def _loads(raw: Any, default: Any) -> Any:
|
||
if raw is None or raw == "":
|
||
return default
|
||
if isinstance(raw, (dict, list)):
|
||
return raw
|
||
try:
|
||
return json.loads(raw)
|
||
except (TypeError, ValueError, json.JSONDecodeError):
|
||
return default
|
||
|
||
|
||
def _dumps(obj: Any) -> Optional[str]:
|
||
if obj is None:
|
||
return None
|
||
return json.dumps(obj, ensure_ascii=False)
|
||
|
||
|
||
def row_to_dict(
|
||
d: OaInstanceDraft,
|
||
*,
|
||
app_name: str = "",
|
||
version_no: Optional[int] = None,
|
||
group_id: str = "",
|
||
group_name: str = "",
|
||
include_payload: bool = False,
|
||
) -> Dict[str, Any]:
|
||
out: Dict[str, Any] = {
|
||
"id": d.id,
|
||
"appId": d.app_id,
|
||
"appVersionId": d.app_version_id,
|
||
"appName": app_name or "",
|
||
"versionNo": version_no,
|
||
"groupId": group_id or "",
|
||
"groupName": group_name or "",
|
||
"title": d.title or "",
|
||
"ownerUserId": d.owner_user_id,
|
||
"initiatorDeptId": d.initiator_dept_id or "",
|
||
"status": "draft",
|
||
"createTime": d.create_time.strftime("%Y-%m-%d %H:%M:%S") if d.create_time else "",
|
||
"updateTime": d.update_time.strftime("%Y-%m-%d %H:%M:%S") if d.update_time else "",
|
||
}
|
||
if include_payload:
|
||
out["formData"] = _loads(d.form_data_json, {}) or {}
|
||
out["initiatorSelectByNode"] = _loads(d.initiator_select_json, {}) or {}
|
||
return out
|
||
|
||
|
||
class DraftAccessError(ValueError):
|
||
"""草稿不存在或无权访问"""
|
||
|
||
|
||
def get_owned_draft(
|
||
db: Session,
|
||
draft_id: str,
|
||
*,
|
||
tenant_no: str,
|
||
owner_user_id: str,
|
||
) -> OaInstanceDraft:
|
||
row = (
|
||
db.query(OaInstanceDraft)
|
||
.filter(
|
||
OaInstanceDraft.id == draft_id,
|
||
OaInstanceDraft.tenant_no == tenant_no,
|
||
OaInstanceDraft.owner_user_id == owner_user_id,
|
||
OaInstanceDraft.del_flag == 0,
|
||
)
|
||
.first()
|
||
)
|
||
if not row:
|
||
raise DraftAccessError("草稿不存在或无权访问")
|
||
return row
|
||
|
||
|
||
def _enrich(db: Session, d: OaInstanceDraft, *, include_payload: bool = False) -> Dict[str, Any]:
|
||
app = (
|
||
db.query(OaApp)
|
||
.filter(OaApp.id == d.app_id, OaApp.tenant_no == d.tenant_no, OaApp.del_flag == 0)
|
||
.first()
|
||
)
|
||
ver = (
|
||
db.query(OaAppVersion)
|
||
.filter(OaAppVersion.id == d.app_version_id, OaAppVersion.del_flag == 0)
|
||
.first()
|
||
)
|
||
group_name = ""
|
||
group_id = ""
|
||
if app and app.group_id:
|
||
group_id = app.group_id
|
||
g = (
|
||
db.query(OaAppGroup)
|
||
.filter(OaAppGroup.id == app.group_id, OaAppGroup.del_flag == 0)
|
||
.first()
|
||
)
|
||
if g:
|
||
group_name = g.group_name or ""
|
||
out = row_to_dict(
|
||
d,
|
||
app_name=(app.app_name if app else "") or "",
|
||
version_no=ver.version_no if ver else None,
|
||
group_id=group_id,
|
||
group_name=group_name,
|
||
include_payload=include_payload,
|
||
)
|
||
if include_payload and "formData" in out:
|
||
from app.services.oa_datetime_normalize import normalize_datetime_form_values
|
||
|
||
schema = _loads(getattr(ver, "form_schema_json", None), {}) if ver else {}
|
||
out["formData"] = normalize_datetime_form_values(schema, out.get("formData") or {})
|
||
return out
|
||
|
||
|
||
def count_drafts(db: Session, *, tenant_no: str, owner_user_id: str) -> int:
|
||
return (
|
||
db.query(OaInstanceDraft)
|
||
.filter(
|
||
OaInstanceDraft.tenant_no == tenant_no,
|
||
OaInstanceDraft.owner_user_id == owner_user_id,
|
||
OaInstanceDraft.del_flag == 0,
|
||
)
|
||
.count()
|
||
)
|
||
|
||
|
||
def list_drafts(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
owner_user_id: str,
|
||
keyword: Optional[str] = None,
|
||
group_id: Optional[str] = None,
|
||
update_time_from: Optional[datetime] = None,
|
||
update_time_to: Optional[datetime] = None,
|
||
page: int = 1,
|
||
page_size: int = 20,
|
||
) -> Tuple[List[Dict[str, Any]], int]:
|
||
q = (
|
||
db.query(OaInstanceDraft, OaApp, OaAppVersion, OaAppGroup)
|
||
.outerjoin(
|
||
OaApp,
|
||
(OaApp.id == OaInstanceDraft.app_id)
|
||
& (OaApp.tenant_no == OaInstanceDraft.tenant_no)
|
||
& (OaApp.del_flag == 0),
|
||
)
|
||
.outerjoin(
|
||
OaAppVersion,
|
||
(OaAppVersion.id == OaInstanceDraft.app_version_id) & (OaAppVersion.del_flag == 0),
|
||
)
|
||
.outerjoin(
|
||
OaAppGroup,
|
||
(OaAppGroup.id == OaApp.group_id) & (OaAppGroup.del_flag == 0),
|
||
)
|
||
.filter(
|
||
OaInstanceDraft.tenant_no == tenant_no,
|
||
OaInstanceDraft.owner_user_id == owner_user_id,
|
||
OaInstanceDraft.del_flag == 0,
|
||
)
|
||
)
|
||
kw = (keyword or "").strip()
|
||
if kw:
|
||
like = f"%{kw}%"
|
||
q = q.filter(or_(OaApp.app_name.like(like), OaInstanceDraft.title.like(like)))
|
||
gid = (group_id or "").strip()
|
||
if gid:
|
||
q = q.filter(OaApp.group_id == gid)
|
||
if update_time_from is not None:
|
||
q = q.filter(OaInstanceDraft.update_time >= update_time_from)
|
||
if update_time_to is not None:
|
||
q = q.filter(OaInstanceDraft.update_time <= update_time_to)
|
||
|
||
total = q.order_by(None).with_entities(func.count(OaInstanceDraft.id)).scalar() or 0
|
||
page = max(1, page)
|
||
page_size = min(max(1, page_size), 100)
|
||
rows = (
|
||
q.order_by(OaInstanceDraft.update_time.desc())
|
||
.offset((page - 1) * page_size)
|
||
.limit(page_size)
|
||
.all()
|
||
)
|
||
items: List[Dict[str, Any]] = []
|
||
for d, app, ver, group in rows:
|
||
item = row_to_dict(
|
||
d,
|
||
app_name=(app.app_name if app else "") or "",
|
||
version_no=ver.version_no if ver else None,
|
||
group_id=(app.group_id if app else "") or "",
|
||
group_name=(group.group_name if group else "") or "",
|
||
)
|
||
schema = _loads(getattr(ver, "form_schema_json", None), {}) if ver else {}
|
||
form_data = _loads(d.form_data_json, {}) or {}
|
||
preview = [
|
||
{"label": f.get("label") or "", "value": f.get("value") or ""}
|
||
for f in flatten_form_fields_for_summary(schema, form_data)[:3]
|
||
]
|
||
item["previewFields"] = preview
|
||
items.append(item)
|
||
return items, int(total)
|
||
|
||
|
||
def get_draft_detail(
|
||
db: Session,
|
||
draft_id: str,
|
||
*,
|
||
tenant_no: str,
|
||
owner_user_id: str,
|
||
) -> Dict[str, Any]:
|
||
d = get_owned_draft(db, draft_id, tenant_no=tenant_no, owner_user_id=owner_user_id)
|
||
return _enrich(db, d, include_payload=True)
|
||
|
||
|
||
def create_draft(
|
||
db: Session,
|
||
*,
|
||
tenant_no: str,
|
||
owner_user_id: str,
|
||
app_id: str,
|
||
form_data: Optional[Dict[str, Any]] = None,
|
||
initiator_select_by_node: Optional[Dict[str, Any]] = None,
|
||
initiator_dept_id: Optional[str] = None,
|
||
title: Optional[str] = None,
|
||
create_dept_id: str = "",
|
||
) -> Dict[str, Any]:
|
||
app = oa_crud.get_app(db, app_id, tenant_no)
|
||
if not app or app.status != "1":
|
||
raise ValueError("应用不存在或已停用")
|
||
ver = oa_crud._find_latest_published(db, app_id, tenant_no)
|
||
if not ver:
|
||
raise ValueError("应用尚未发布,无法暂存草稿")
|
||
now = datetime.now()
|
||
row = OaInstanceDraft(
|
||
id=generate_uuid(),
|
||
app_id=app_id,
|
||
app_version_id=ver.id,
|
||
title=(title or app.app_name or "")[:200] or None,
|
||
form_data_json=_dumps(form_data or {}),
|
||
initiator_select_json=_dumps(initiator_select_by_node or {}),
|
||
initiator_dept_id=(initiator_dept_id or "").strip() or None,
|
||
owner_user_id=owner_user_id,
|
||
tenant_no=tenant_no,
|
||
create_by=owner_user_id,
|
||
create_dept_id=create_dept_id or "",
|
||
create_time=now,
|
||
update_by=owner_user_id,
|
||
update_time=now,
|
||
del_flag=0,
|
||
)
|
||
db.add(row)
|
||
db.commit()
|
||
db.refresh(row)
|
||
return _enrich(db, row, include_payload=True)
|
||
|
||
|
||
def update_draft(
|
||
db: Session,
|
||
draft_id: str,
|
||
*,
|
||
tenant_no: str,
|
||
owner_user_id: str,
|
||
form_data: Optional[Dict[str, Any]] = None,
|
||
initiator_select_by_node: Optional[Dict[str, Any]] = None,
|
||
initiator_dept_id: Optional[str] = None,
|
||
title: Optional[str] = None,
|
||
) -> Dict[str, Any]:
|
||
d = get_owned_draft(db, draft_id, tenant_no=tenant_no, owner_user_id=owner_user_id)
|
||
ver = oa_crud._find_latest_published(db, d.app_id, tenant_no)
|
||
if ver:
|
||
d.app_version_id = ver.id
|
||
if form_data is not None:
|
||
d.form_data_json = _dumps(form_data)
|
||
if initiator_select_by_node is not None:
|
||
d.initiator_select_json = _dumps(initiator_select_by_node)
|
||
if initiator_dept_id is not None:
|
||
d.initiator_dept_id = (initiator_dept_id or "").strip() or None
|
||
if title is not None:
|
||
d.title = (title or "")[:200] or None
|
||
d.update_by = owner_user_id
|
||
d.update_time = datetime.now()
|
||
db.commit()
|
||
db.refresh(d)
|
||
return _enrich(db, d, include_payload=True)
|
||
|
||
|
||
def soft_delete_draft(
|
||
db: Session,
|
||
draft_id: str,
|
||
*,
|
||
tenant_no: str,
|
||
owner_user_id: str,
|
||
) -> None:
|
||
d = get_owned_draft(db, draft_id, tenant_no=tenant_no, owner_user_id=owner_user_id)
|
||
d.del_flag = 1
|
||
d.update_by = owner_user_id
|
||
d.update_time = datetime.now()
|
||
db.commit()
|
||
|
||
|
||
def batch_soft_delete_drafts(
|
||
db: Session,
|
||
ids: List[str],
|
||
*,
|
||
tenant_no: str,
|
||
owner_user_id: str,
|
||
) -> int:
|
||
cleaned = [str(x).strip() for x in (ids or []) if str(x).strip()]
|
||
if not cleaned:
|
||
raise ValueError("请选择要删除的草稿")
|
||
rows = (
|
||
db.query(OaInstanceDraft)
|
||
.filter(
|
||
OaInstanceDraft.id.in_(cleaned),
|
||
OaInstanceDraft.tenant_no == tenant_no,
|
||
OaInstanceDraft.owner_user_id == owner_user_id,
|
||
OaInstanceDraft.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
if len(rows) != len(set(cleaned)):
|
||
raise DraftAccessError("草稿不存在或无权访问")
|
||
now = datetime.now()
|
||
for d in rows:
|
||
d.del_flag = 1
|
||
d.update_by = owner_user_id
|
||
d.update_time = now
|
||
db.commit()
|
||
return len(rows)
|
||
|
||
|
||
def submit_draft(
|
||
db: Session,
|
||
draft_id: str,
|
||
*,
|
||
tenant_no: str,
|
||
owner_user_id: str,
|
||
form_data: Optional[Dict[str, Any]] = None,
|
||
initiator_select_by_node: Optional[Dict[str, Any]] = None,
|
||
initiator_dept_id: Optional[str] = None,
|
||
title: Optional[str] = None,
|
||
) -> Dict[str, Any]:
|
||
"""提交草稿:走 start_instance,成功后软删草稿。"""
|
||
from app.services.oa_engine import start_instance
|
||
|
||
d = get_owned_draft(db, draft_id, tenant_no=tenant_no, owner_user_id=owner_user_id)
|
||
payload_form = form_data if form_data is not None else (_loads(d.form_data_json, {}) or {})
|
||
payload_select = (
|
||
initiator_select_by_node
|
||
if initiator_select_by_node is not None
|
||
else (_loads(d.initiator_select_json, {}) or {})
|
||
)
|
||
dept = initiator_dept_id if initiator_dept_id is not None else d.initiator_dept_id
|
||
use_title = title if title is not None else d.title
|
||
|
||
data = start_instance(
|
||
db,
|
||
tenant_no=tenant_no,
|
||
app_id=d.app_id,
|
||
initiator_user_id=owner_user_id,
|
||
initiator_dept_id=dept,
|
||
form_data=payload_form if isinstance(payload_form, dict) else {},
|
||
title=use_title,
|
||
initiator_select_by_node=payload_select if isinstance(payload_select, dict) else {},
|
||
)
|
||
d.del_flag = 1
|
||
d.update_by = owner_user_id
|
||
d.update_time = datetime.now()
|
||
db.commit()
|
||
return data
|