377 lines
12 KiB
Python
377 lines
12 KiB
Python
"""数据导出(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,
|
||
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 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, "每周重点工作")
|