"""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