376 lines
11 KiB
Python
376 lines
11 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 ""
|
||
return 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,
|
||
)
|
||
|
||
|
||
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
|