This commit is contained in:
zlwu committed 2026-08-28 17:38:37 +08:00
1 parent 3d98104455
commit 0d52442902
76 files changed
+21994 -12642

No files matched your search

+494 -4
View File
@@ -1004,10 +1004,12 @@ def _enrich_list_rows(
ver = ver_by_id.get(inst.app_version_id or "")
schema = _loads(getattr(ver, "form_schema_json", None), {}) if ver else {}
form_data = _loads(inst.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)[:limit]
]
preview = []
for f in flatten_form_fields_for_summary(schema, form_data)[:limit]:
item = dict(f)
item["label"] = item.get("label") or ""
item["value"] = item.get("value") or ""
preview.append(item)
merged = dict(row)
merged.update(summary)
merged["previewFields"] = preview
@@ -2317,6 +2319,494 @@ def list_instance_history(
}
_HISTORY_EXPORT_MAX_ROWS = 5000
_EXPORT_RESULT_TEXT = {
"completed": "审批通过",
"approved": "审批通过",
"rejected": "审批拒绝",
"returned": "已打回",
"cancelled": "已撤销",
"running": "",
"pending": "",
}
_EXPORT_STATUS_TEXT = {
"completed": "已结束",
"approved": "已结束",
"rejected": "已结束",
"cancelled": "已结束",
"returned": "已打回",
"running": "进行中",
"pending": "待处理",
}
_EXPORT_ACTION_TEXT = {
"submitted": "提交申请",
"approved": "同意",
"rejected": "拒绝",
"returned": "打回",
"completed": "同意",
"pending": "待处理",
"cancelled": "取消",
"transferred": "转交",
"done": "抄送",
}
_FIXED_HISTORY_EXPORT_COLUMNS: List[Tuple[str, str]] = [
("instanceNo", "审批编号"),
("createTime", "创建时间"),
("initiatorName", "创建人"),
("currentAssignee", "当前负责人"),
("approvalResult", "审批结果"),
("approvalStatus", "审批状态"),
("updateTime", "更新时间"),
("finishTime", "完成时间"),
("initiatorDept", "创建人部门"),
("title", "审批单标题"),
("historyApprovers", "历史审批人"),
("duration", "耗时(时:分:秒)"),
("approvalRecord", "审批记录"),
]
def _format_export_duration(start: Optional[datetime], end: Optional[datetime]) -> str:
if not start or not end:
return ""
delta = end - start
if delta.total_seconds() < 0:
return ""
total = int(delta.total_seconds())
h, rem = divmod(total, 3600)
m, s = divmod(rem, 60)
return f"{h}:{m}:{s}"
def _export_upload_cell_value(field: Dict[str, Any]) -> str:
files = field.get("files") or []
if not isinstance(files, list):
return str(field.get("value") or "")
urls: List[str] = []
for f in files:
if not isinstance(f, dict):
continue
u = str(f.get("url") or "").strip()
if u:
urls.append(u)
if urls:
return ",".join(urls)
return str(field.get("value") or "")
def _export_progress_action(status: str, *, task_type: str = "") -> str:
st = (status or "").strip()
if (task_type or "").strip() == "cc" or st == "done":
return "抄送"
return _EXPORT_ACTION_TEXT.get(st, st or "")
def _format_export_progress_record(progress: List[Dict[str, Any]]) -> str:
lines: List[str] = []
for item in progress or []:
kind = (item.get("kind") or "").strip()
title = (item.get("title") or "").strip()
ts = (item.get("timestamp") or "").strip()
task_type = (item.get("taskType") or "").strip()
if kind == "submit":
name = (item.get("assigneeUserName") or "").strip()
if not name:
continue
lines.append(f"{name}|提交申请|{ts}|提交申请")
continue
people = item.get("people") or []
if isinstance(people, list) and people:
for p in people:
if not isinstance(p, dict):
continue
name = (p.get("nickName") or p.get("assigneeUserName") or "").strip()
if not name:
continue
pst = (p.get("status") or "").strip()
if pst in ("pending", "running", "cancelled") and task_type != "cc":
continue
node = title or (item.get("nodeKey") or "").strip() or "-"
pts = (p.get("timestamp") or ts or "").strip()
action = _export_progress_action(pst, task_type=task_type)
comment = (p.get("commentText") or "").strip()
seg = f"{name}|{node}|{pts}|{action}"
if comment:
seg += f"|{comment}"
lines.append(seg)
continue
name = (item.get("assigneeUserName") or "").strip()
if not name:
continue
st = (item.get("status") or "").strip()
if st in ("pending", "running", "cancelled") and task_type != "cc":
continue
node = title or (item.get("nodeKey") or "").strip() or "-"
action = _export_progress_action(st, task_type=task_type)
comment = (item.get("commentText") or "").strip()
seg = f"{name}|{node}|{ts}|{action}"
if comment:
seg += f"|{comment}"
lines.append(seg)
return ";\r\n".join(lines)
def _collect_history_approver_names(progress: List[Dict[str, Any]]) -> str:
names: List[str] = []
seen = set()
for item in progress or []:
if (item.get("kind") or "").strip() == "submit":
continue
people = item.get("people") or []
if isinstance(people, list) and people:
for p in people:
if not isinstance(p, dict):
continue
st = (p.get("status") or "").strip()
if st not in ("approved", "completed", "rejected", "returned", "transferred"):
continue
name = (p.get("nickName") or p.get("assigneeUserName") or "").strip()
if name and name not in seen:
seen.add(name)
names.append(name)
continue
st = (item.get("status") or "").strip()
if st not in ("approved", "completed", "rejected", "returned", "transferred"):
continue
name = (item.get("assigneeUserName") or "").strip()
if name and name not in seen:
seen.add(name)
names.append(name)
return ",".join(names)
def _build_history_export_business_columns(schema: Any) -> List[Dict[str, Any]]:
from app.services.oa_related_approval import LAYOUT_SKIP, _schema_rules
cols: List[Dict[str, Any]] = []
for rule in _schema_rules(schema):
typ = str(rule.get("type") or "")
if not typ or typ in LAYOUT_SKIP:
continue
field = str(rule.get("field") or "").strip()
if not field:
continue
label = str(rule.get("title") or rule.get("label") or field)
if typ == "relatedApproval":
cols.append(
{
"key": field,
"label": label,
"kind": "related",
"subLabel": "关联标题",
}
)
else:
cols.append({"key": field, "label": label, "kind": "business"})
return cols
def _merge_history_export_business_columns(col_lists: List[List[Dict[str, Any]]]) -> List[Dict[str, Any]]:
merged: List[Dict[str, Any]] = []
seen = set()
for cols in col_lists:
for c in cols:
key = str(c.get("key") or "")
if not key or key in seen:
continue
seen.add(key)
merged.append(dict(c))
return merged
def _history_export_query(
db: Session,
*,
tenant_no: str,
user_id: str,
can_view_all: bool,
app_id: str,
instance_id: Optional[str] = None,
instance_ids: Optional[List[str]] = None,
initiator_name: Optional[str] = None,
apply_time_from: Optional[str] = None,
apply_time_to: Optional[str] = None,
finish_time_from: Optional[str] = None,
finish_time_to: Optional[str] = None,
instance_status: Optional[str] = None,
):
q = db.query(OaInstance).filter(
OaInstance.tenant_no == tenant_no,
OaInstance.del_flag == 0,
OaInstance.app_id == app_id,
)
if not can_view_all:
q = q.filter(OaInstance.initiator_user_id == user_id)
initiator_name = None
clean_ids = [str(x).strip() for x in (instance_ids or []) if str(x).strip()]
if clean_ids:
q = q.filter(OaInstance.id.in_(clean_ids))
else:
iid = (instance_id or "").strip()
if iid:
q = q.filter(OaInstance.id.like(f"%{iid}%"))
q, _, _ = _apply_instance_list_filters(
q,
initiator_name=initiator_name,
apply_time_from=apply_time_from,
apply_time_to=apply_time_to,
instance_status=instance_status,
)
ft0 = _parse_day_start(finish_time_from)
ft1 = _parse_day_end(finish_time_to)
if ft0 is not None or ft1 is not None:
q = q.filter(OaInstance.status.in_(tuple(_FINISH_STATUSES)))
if ft0 is not None:
q = q.filter(OaInstance.update_time >= ft0)
if ft1 is not None:
q = q.filter(OaInstance.update_time <= ft1)
return q
def export_instance_history(
db: Session,
*,
tenant_no: str,
user_id: str,
can_view_all: bool,
app_id: str,
instance_id: Optional[str] = None,
instance_ids: Optional[List[str]] = None,
initiator_name: Optional[str] = None,
apply_time_from: Optional[str] = None,
apply_time_to: Optional[str] = None,
finish_time_from: Optional[str] = None,
finish_time_to: Optional[str] = None,
instance_status: Optional[str] = None,
) -> Dict[str, Any]:
from app.services.oa_related_approval import flatten_form_fields_for_summary
app_id = (app_id or "").strip()
if not app_id:
raise OaEngineError("appId 不能为空")
app = oa_crud.get_app(db, app_id, tenant_no)
app_name = (app.app_name or "") if app else ""
q = _history_export_query(
db,
tenant_no=tenant_no,
user_id=user_id,
can_view_all=can_view_all,
app_id=app_id,
instance_id=instance_id,
instance_ids=instance_ids,
initiator_name=initiator_name,
apply_time_from=apply_time_from,
apply_time_to=apply_time_to,
finish_time_from=finish_time_from,
finish_time_to=finish_time_to,
instance_status=instance_status,
)
total = q.count()
if total > _HISTORY_EXPORT_MAX_ROWS:
raise OaEngineError(f"导出条数超过上限 {_HISTORY_EXPORT_MAX_ROWS},请缩小筛选范围")
insts = q.order_by(OaInstance.create_time.desc()).limit(_HISTORY_EXPORT_MAX_ROWS).all()
if not insts:
pub = oa_crud._find_latest_published(db, app_id, tenant_no)
schema = _loads(getattr(pub, "form_schema_json", None), {}) if pub else {}
business_columns = _build_history_export_business_columns(schema)
return {
"appName": app_name,
"businessColumns": business_columns,
"fixedColumns": [{"key": k, "label": lb} for k, lb in _FIXED_HISTORY_EXPORT_COLUMNS],
"rows": [],
"total": 0,
}
ver_by_id: Dict[str, OaAppVersion] = {}
ver_ids = list({i.app_version_id for i in insts if i.app_version_id})
if ver_ids:
for v in (
db.query(OaAppVersion)
.filter(OaAppVersion.id.in_(ver_ids), OaAppVersion.del_flag == 0)
.all()
):
ver_by_id[v.id] = v
pub = oa_crud._find_latest_published(db, app_id, tenant_no)
pub_schema = _loads(getattr(pub, "form_schema_json", None), {}) if pub else {}
col_lists = [_build_history_export_business_columns(pub_schema)]
for inst in insts:
ver = ver_by_id.get(inst.app_version_id or "")
schema = _loads(getattr(ver, "form_schema_json", None), {}) if ver else {}
col_lists.append(_build_history_export_business_columns(schema))
business_columns = _merge_history_export_business_columns(col_lists)
inst_ids = [i.id for i in insts]
all_tasks = (
db.query(OaTask)
.filter(OaTask.instance_id.in_(inst_ids), OaTask.del_flag == 0)
.all()
)
tasks_by_inst: Dict[str, List[OaTask]] = {}
user_ids: List[str] = []
for t in all_tasks:
tasks_by_inst.setdefault(t.instance_id, []).append(t)
if t.assignee_user_id:
user_ids.append(t.assignee_user_id)
all_events = (
db.query(OaInstanceEvent)
.filter(OaInstanceEvent.instance_id.in_(inst_ids), OaInstanceEvent.del_flag == 0)
.order_by(OaInstanceEvent.create_time.asc())
.all()
)
events_by_inst: Dict[str, List[OaInstanceEvent]] = {}
for e in all_events:
events_by_inst.setdefault(e.instance_id, []).append(e)
if e.actor_user_id:
user_ids.append(e.actor_user_id)
for inst in insts:
if inst.initiator_user_id:
user_ids.append(inst.initiator_user_id)
name_map = _user_name_map(db, user_ids)
related_ids: List[str] = []
flat_by_inst: Dict[str, List[Dict[str, Any]]] = {}
for inst in insts:
ver = ver_by_id.get(inst.app_version_id or "")
schema = _loads(getattr(ver, "form_schema_json", None), {}) if ver else {}
form_data = _loads(inst.form_data_json, {}) or {}
flat = flatten_form_fields_for_summary(schema, form_data)
flat_by_inst[inst.id] = flat
for f in flat:
for rid in f.get("relatedIds") or []:
rid_s = str(rid or "").strip()
if rid_s:
related_ids.append(rid_s)
related_title_map: Dict[str, str] = {}
if related_ids:
rel_insts = (
db.query(OaInstance)
.filter(
OaInstance.tenant_no == tenant_no,
OaInstance.del_flag == 0,
OaInstance.id.in_(list(set(related_ids))),
)
.all()
)
rel_app_ids = list({r.app_id for r in rel_insts if r.app_id})
rel_app_map = {}
if rel_app_ids:
for a in db.query(OaApp).filter(OaApp.id.in_(rel_app_ids)).all():
rel_app_map[a.id] = a.app_name or ""
for r in rel_insts:
initiator = name_map.get(r.initiator_user_id or "", "")
related_title_map[r.id] = (
f"{initiator}提交的{rel_app_map.get(r.app_id, '')}" if initiator else (r.title or r.id)
)
pending_by_inst: Dict[str, List[OaTask]] = {}
for t in all_tasks:
if (t.task_type or "") == "approval" and (t.status or "") == "pending":
pending_by_inst.setdefault(t.instance_id, []).append(t)
rows_out: List[Dict[str, Any]] = []
for idx, inst in enumerate(insts, start=1):
ver = ver_by_id.get(inst.app_version_id or "")
flow_root = None
if ver:
try:
flow_root = _flow_root(_loads(ver.flow_tree_json, {}))
except OaEngineError:
flow_root = None
tasks = _sort_tasks_for_detail(tasks_by_inst.get(inst.id, []), flow_root=flow_root)
events = events_by_inst.get(inst.id, [])
adv = _advanced(ver) if ver else dict(oa_crud.DEFAULT_ADVANCED_SETTINGS)
progress = build_progress_items(
inst=inst,
flow_root=flow_root,
tasks=tasks,
name_map=name_map,
events=events,
db=db,
advanced=adv,
)
summary = _instance_progress_summary(
inst,
app_name=app_name,
initiator_user_name=name_map.get(inst.initiator_user_id or "", ""),
pending_approval_tasks=pending_by_inst.get(inst.id, []),
name_map=name_map,
)
st = (inst.status or "").strip()
finish_time = (
inst.update_time.strftime("%Y-%m-%d %H:%M:%S")
if st in _FINISH_STATUSES and inst.update_time
else ""
)
duration_end = inst.update_time if st in _FINISH_STATUSES else None
business: Dict[str, Any] = {}
flat_map = {str(f.get("fieldKey") or ""): f for f in flat_by_inst.get(inst.id, [])}
for col in business_columns:
key = str(col.get("key") or "")
field = flat_map.get(key) or {}
if col.get("kind") == "related":
ids = field.get("relatedIds") or []
business[key] = f"已关联 {len(ids)} 条" if ids else ""
titles = [related_title_map.get(str(i), str(i)) for i in ids]
business[f"{key}__titles"] = "、".join(t for t in titles if t)
elif field.get("fieldType") in ("upload", "fcUpload", "image", "oaAttachment"):
business[key] = _export_upload_cell_value(field)
else:
business[key] = str(field.get("value") or "")
rows_out.append(
{
"index": idx,
"id": inst.id,
"business": business,
"fixed": {
"instanceNo": inst.id,
"createTime": summary.get("applyTime") or "",
"initiatorName": summary.get("initiatorUserName") or "",
"currentAssignee": summary.get("currentApproverNames") or "-",
"approvalResult": _EXPORT_RESULT_TEXT.get(st, st),
"approvalStatus": _EXPORT_STATUS_TEXT.get(st, st),
"updateTime": inst.update_time.strftime("%Y-%m-%d %H:%M:%S")
if inst.update_time
else "",
"finishTime": finish_time,
"initiatorDept": _dept_name(
db, inst.initiator_dept_id, tenant_no=tenant_no
),
"title": inst.title or "",
"historyApprovers": _collect_history_approver_names(progress),
"duration": _format_export_duration(inst.create_time, duration_end),
"approvalRecord": _format_export_progress_record(progress),
},
}
)
return {
"appName": app_name,
"businessColumns": business_columns,
"fixedColumns": [{"key": k, "label": lb} for k, lb in _FIXED_HISTORY_EXPORT_COLUMNS],
"rows": rows_out,
"total": len(rows_out),
}
def list_tasks(
db: Session,
*,