unis_manager/app/routers/export.py

383 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""数据导出(Excel / xlsx)。
提供四类导出,均可带上页面当前的筛选条件:
- /export/custom 定制项目明细
- /export/custom-updates 定制项目周期记录
- /export/projects 常规项目管理
- /export/updates 常规项目周期执行记录
- /export/staff 人力资源库(含项目负载)
- /export/focus 每周重点工作
"""
from __future__ import annotations
from fastapi import APIRouter, Depends, Query
from fastapi.responses import Response
from sqlalchemy.orm import Session
from ..constants import CUSTOM_STAGE_MAP, STATUS_MAP
from ..db import get_db
from ..export import build_xlsx, xlsx_response
from ..models import (
CustomProject,
CustomUpdate,
Period,
Project,
Staff,
Task,
Update,
WeeklyFocus,
)
from .board import worst_status
from .custom import _sort_by_entry, _sort_by_update
router = APIRouter()
def _stage_name(code: str | None) -> str:
return CUSTOM_STAGE_MAP.get(code or "", (code or "", "", 0))[0]
def _resp(content: bytes, filename: str) -> Response:
body, headers = xlsx_response(content, filename)
return Response(content=body, headers=headers)
# ---------------------------------------------------------------- 定制项目
@router.get("/export/custom")
def export_custom(
region: str | None = None,
origin: str | None = None,
status: str | None = None,
risk: str | None = None,
accept_plan: str | None = None,
keyword: str | None = None,
archived: str = Query("false", pattern="^(true|false|all)$"),
db: Session = Depends(get_db),
):
q = db.query(CustomProject)
if archived == "false":
q = q.filter(CustomProject.archived.is_(False))
elif archived == "true":
q = q.filter(CustomProject.archived.is_(True))
if region:
q = q.filter(CustomProject.region == region)
if origin:
q = q.filter(CustomProject.origin == origin)
if status:
q = q.filter(CustomProject.project_status == status)
if risk:
q = q.filter(CustomProject.risk_level == risk)
if accept_plan:
q = q.filter(CustomProject.accept_plan == accept_plan)
if keyword:
q = q.filter(CustomProject.name.contains(keyword))
projects = _sort_by_update(q.all())
# 列与「定制项目明细」表格一致
headers = [
"项目归属", "项目名称", "项目ID", "下单时间", "办事处", "行业",
"市场下单人天", "订单金额(元)", "市场责任人", "项目状态",
"验收时间", "推动验收计划", "风险值", "备注", "更新时间",
]
rows = []
for c in projects:
rows.append(
[
c.origin or "",
c.name,
c.project_code or "",
c.sign_date or "",
c.office or "",
c.industry or "",
c.man_days if c.man_days is not None else "",
c.amount if c.amount is not None else "",
c.market_owner or "",
c.project_status or "",
c.accept_date or "",
c.accept_plan or "",
c.risk_level or "",
(c.remark or "").replace("\n", " "),
c.update_date or "",
]
)
dicts = [c.to_dict() for c in projects]
def total(key: str) -> float:
return round(sum(d.get(key) or 0 for d in dicts), 2)
rows.append([""] * 15)
rows.append(
["合计", f"{len(projects)} 个项目", "", "", "", "",
total("man_days"), total("amount"), "", "", "", "", "", "", ""]
)
content = build_xlsx(
"定制项目明细",
headers,
rows,
widths=[10, 46, 18, 12, 10, 8, 13, 14, 14, 11, 11, 14, 10, 40, 18],
title="定制项目明细",
money_cols=(8,), # 订单金额(元)
)
return _resp(content, "定制项目明细")
@router.get("/export/custom-updates")
def export_custom_updates(
period_id: int | None = None,
year: int | None = None,
month: int | None = None,
db: Session = Depends(get_db),
):
from .board import resolve_period_ids
period_ids = resolve_period_ids(db, period_id, year, month)
q = db.query(CustomUpdate)
if period_ids:
q = q.filter(CustomUpdate.period_id.in_(period_ids))
ups = q.order_by(CustomUpdate.period_id, CustomUpdate.custom_project_id).all()
headers = [
"周期", "项目名称", "区域", "阶段", "执行情况", "进度(%)",
"本期新增下单(元)", "本期新增计收(元)", "风险", "下一步",
]
rows = []
for u in ups:
cp = u.project
rows.append(
[
u.period.to_dict()["full_label"] if u.period else "",
cp.name if cp else "",
(cp.region or "") if cp else "",
_stage_name(u.stage),
(u.content or "").replace("\n", " ")[:500],
u.progress,
u.amount_delta,
u.revenue_delta,
(u.risk or "").replace("\n", " ")[:200],
(u.next_step or "").replace("\n", " ")[:200],
]
)
content = build_xlsx(
"定制项目记录",
headers,
rows,
widths=[18, 42, 10, 14, 70, 10, 16, 16, 30, 30],
title="定制项目周期记录",
money_cols=(7, 8),
)
return _resp(content, "定制项目周期记录")
# ---------------------------------------------------------------- 常规项目
@router.get("/export/projects")
def export_projects(
category_id: int | None = None,
keyword: str | None = None,
archived: str = Query("all", pattern="^(true|false|all)$"),
db: Session = Depends(get_db),
):
q = db.query(Project)
if archived == "false":
q = q.filter(Project.archived.is_(False))
elif archived == "true":
q = q.filter(Project.archived.is_(True))
if category_id:
q = q.filter(Project.category_id == category_id)
if keyword:
q = q.filter(Project.name.contains(keyword))
projects = q.order_by(Project.is_key.desc(), Project.name).all()
periods = {p.id: p for p in db.query(Period).all()}
headers = [
"项目名称", "分类", "负责人", "状态", "优先级", "重点",
"进度(%)", "标签", "子任务数", "执行记录数", "最近更新周期", "归档",
]
status_names = {
"not_started": "未开始", "in_progress": "进行中", "done": "已完成",
"at_risk": "风险", "blocked": "阻塞", "paused": "暂停/终止",
}
rows = []
for p in projects:
ups = sorted(p.updates, key=lambda u: u.period_id or 0, reverse=True)
last = periods.get(ups[0].period_id) if ups else None
rows.append(
[
p.name,
p.category.name if p.category else "未分类",
p.owner or "",
status_names.get(p.status, p.status),
(p.priority or "").upper(),
"是" if p.is_key else "",
round(p.progress or 0, 1),
"、".join(t.name for t in p.tags),
len(p.tasks),
len(p.updates),
last.to_dict()["full_label"] if last else "",
"是" if p.archived else "",
]
)
content = build_xlsx(
"项目管理",
headers,
rows,
widths=[46, 12, 12, 10, 9, 7, 10, 24, 11, 12, 18, 7],
title="项目管理清单",
)
return _resp(content, "项目管理清单")
@router.get("/export/updates")
def export_updates(
period_id: int | None = None,
year: int | None = None,
month: int | None = None,
project_id: int | None = None,
db: Session = Depends(get_db),
):
from .board import resolve_period_ids
period_ids = resolve_period_ids(db, period_id, year, month)
q = db.query(Update)
if period_ids:
q = q.filter(Update.period_id.in_(period_ids))
if project_id:
q = q.filter(Update.project_id == project_id)
ups = q.order_by(Update.period_id, Update.project_id, Update.id).all()
status_names = {
"not_started": "未开始", "in_progress": "进行中", "done": "已完成",
"at_risk": "风险", "blocked": "阻塞", "paused": "暂停/终止",
}
headers = [
"周期", "项目", "子任务", "执行情况", "进度(%)", "状态", "风险", "下一步", "来源",
]
rows = []
for u in ups:
rows.append(
[
u.period.to_dict()["full_label"] if u.period else "",
u.project.name if u.project else "",
u.task.name if u.task else "",
(u.content or "").replace("\n", " ")[:500],
u.progress,
status_names.get(u.status, u.status),
(u.risk or "").replace("\n", " ")[:200],
(u.next_step or "").replace("\n", " ")[:200],
{"import": "导入", "ai": "AI整理", "manual": "手工"}.get(u.source, u.source),
]
)
content = build_xlsx(
"周期执行记录",
headers,
rows,
widths=[18, 30, 22, 70, 10, 10, 30, 30, 10],
title="周期执行记录",
)
return _resp(content, "周期执行记录")
# ---------------------------------------------------------------- 人力资源库
@router.get("/export/staff")
def export_staff(
archived: str = Query("false", pattern="^(true|false|all)$"),
db: Session = Depends(get_db),
):
from .staff import _workload
q = db.query(Staff)
if archived == "false":
q = q.filter(Staff.archived.is_(False))
elif archived == "true":
q = q.filter(Staff.archived.is_(True))
members = q.order_by(Staff.sort_order, Staff.id).all()
load = _workload(db)
headers = [
"序号", "姓名", "归属小组", "人员归属", "在管项目", "重点项目",
"定制项目", "定制合同额(元)", "定制待计收(元)", "最近更新", "说明", "归档",
]
rows = []
for i, m in enumerate(members, 1):
w = load.get(m.name, {})
rows.append(
[
i,
m.name,
m.group or "",
m.affiliation or "",
w.get("project_count", 0),
w.get("key_project_count", 0),
w.get("custom_count", 0),
w.get("custom_amount", 0),
w.get("custom_pending", 0),
w.get("last_update") or "",
(m.note or "").replace("\n", " "),
"是" if m.archived else "",
]
)
content = build_xlsx(
"人力资源库",
headers,
rows,
widths=[6, 12, 14, 10, 10, 10, 10, 16, 16, 16, 52, 7],
title="软件开发部人力资源库",
money_cols=(8, 9),
)
return _resp(content, "人力资源库")
# ---------------------------------------------------------------- 每周重点工作
@router.get("/export/focus")
def export_focus(
archived: str = Query("false", pattern="^(true|false|all)$"),
status: str | None = None,
keyword: str | None = None,
db: Session = Depends(get_db),
):
q = db.query(WeeklyFocus)
if archived == "false":
q = q.filter(WeeklyFocus.archived.is_(False))
elif archived == "true":
q = q.filter(WeeklyFocus.archived.is_(True))
if status:
q = q.filter(WeeklyFocus.status == status)
if keyword:
q = q.filter(WeeklyFocus.content.contains(keyword) | WeeklyFocus.detail.contains(keyword))
periods = {p.id: p for p in db.query(Period).all()}
items = sorted(
q.all(),
key=lambda f: (-(periods[f.period_id].sort_key if f.period_id in periods else 0), -f.id),
)
headers = ["序号", "工作内容", "说明", "预计完成时间", "录入周", "执行状态", "是否延期", "结果反馈", "更新"]
rows = []
for i, f in enumerate(items, 1):
period = periods.get(f.period_id)
rows.append(
[
i,
f.content,
(f.detail or "").replace("\n", " "),
f.due_date or "",
period.to_dict()["full_label"] if period else "",
STATUS_MAP.get(f.status, (f.status,))[0],
"是" if f.delayed else "",
(f.result or "").replace("\n", " "),
f.updated_at.strftime("%Y-%m-%d %H:%M") if f.updated_at else "",
]
)
content = build_xlsx(
"每周重点工作",
headers,
rows,
widths=[6, 46, 54, 14, 14, 12, 10, 54, 17],
title="每周重点工作",
)
return _resp(content, "每周重点工作")