提交
This commit is contained in:
1 parent
3d98104455
commit
0d52442902
76 files changed
+21994
-12642
No files matched your search
@@ -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,
|
||||
*,
|
||||
|
||||
Reference in new issue
Block a user